@camstack/addon-provider-dreo 0.1.5 → 0.1.7
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 +491 -623
- package/dist/addon.mjs +491 -623
- package/dist/index.js +1 -1
- package/dist/index.mjs +2 -2
- package/package.json +2 -5
package/dist/addon.js
CHANGED
|
@@ -4664,7 +4664,7 @@ function _instanceof(cls, params = {}) {
|
|
|
4664
4664
|
return inst;
|
|
4665
4665
|
}
|
|
4666
4666
|
//#endregion
|
|
4667
|
-
//#region ../types/dist/sleep-
|
|
4667
|
+
//#region ../types/dist/sleep-C2M2zF7x.mjs
|
|
4668
4668
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4669
4669
|
EventCategory["SystemBoot"] = "system.boot";
|
|
4670
4670
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -6232,6 +6232,12 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
|
6232
6232
|
DeviceType["Switch"] = "switch";
|
|
6233
6233
|
DeviceType["Sensor"] = "sensor";
|
|
6234
6234
|
DeviceType["Thermostat"] = "thermostat";
|
|
6235
|
+
/** Air-conditioner / heat-pump climate device (HVAC) — shares the
|
|
6236
|
+
* `climate-control` cap surface with `Thermostat` but renders a
|
|
6237
|
+
* dedicated AC-appropriate control UI (mode chips, fan speed,
|
|
6238
|
+
* independent vertical/horizontal swing). Sources: native Gree, and
|
|
6239
|
+
* reusable by other AC integrations. */
|
|
6240
|
+
DeviceType["Climate"] = "climate";
|
|
6235
6241
|
DeviceType["Button"] = "button";
|
|
6236
6242
|
/** Generic stateless event emitter — carries a device's EXACT declared
|
|
6237
6243
|
* event vocabulary verbatim (no normalization). Installed with the
|
|
@@ -9027,7 +9033,7 @@ var climateControlCapability = {
|
|
|
9027
9033
|
scope: "device",
|
|
9028
9034
|
deviceNative: true,
|
|
9029
9035
|
mode: "singleton",
|
|
9030
|
-
deviceTypes: [DeviceType.Thermostat],
|
|
9036
|
+
deviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
|
|
9031
9037
|
methods: {
|
|
9032
9038
|
setMode: method(object({
|
|
9033
9039
|
deviceId: number().int().nonnegative(),
|
|
@@ -13626,10 +13632,30 @@ var deviceProviderCapability = {
|
|
|
13626
13632
|
type: string()
|
|
13627
13633
|
}))),
|
|
13628
13634
|
supportsDiscovery: method(object({}), boolean()),
|
|
13629
|
-
|
|
13635
|
+
/**
|
|
13636
|
+
* Run a network scan. `params` carries optional provider-specific scan
|
|
13637
|
+
* inputs (e.g. a broadcast address / subnet for cross-subnet discovery),
|
|
13638
|
+
* shaped by `getDiscoveryParamsSchema`. Omitted for the generic scan
|
|
13639
|
+
* (provider uses its local-network default).
|
|
13640
|
+
*/
|
|
13641
|
+
discoverDevices: method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
|
|
13630
13642
|
kind: "mutation",
|
|
13631
13643
|
auth: "admin"
|
|
13632
13644
|
}),
|
|
13645
|
+
/**
|
|
13646
|
+
* Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
|
|
13647
|
+
* provider accepts (e.g. Gree's broadcast address for a different subnet).
|
|
13648
|
+
* `null` when the provider takes no extra scan params — the generic
|
|
13649
|
+
* aggregated scan never renders this; the per-integration scan does.
|
|
13650
|
+
*/
|
|
13651
|
+
getDiscoveryParamsSchema: method(object({}), CreationSchemaOutputSchema),
|
|
13652
|
+
/**
|
|
13653
|
+
* The DeviceType this provider creates via manual add (Camera for
|
|
13654
|
+
* Reolink/ONVIF, Container for Gree, Hub for Ecowitt). `null` when the
|
|
13655
|
+
* provider does not support manual creation. Lets the Add-Device dialog
|
|
13656
|
+
* pick the right type instead of assuming Camera.
|
|
13657
|
+
*/
|
|
13658
|
+
getManualCreationType: method(object({}), object({ deviceType: _enum(DeviceType).nullable() })),
|
|
13633
13659
|
adoptDiscoveredDevice: method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
|
|
13634
13660
|
kind: "mutation",
|
|
13635
13661
|
auth: "admin"
|
|
@@ -13753,9 +13779,23 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
13753
13779
|
async supportsDiscovery() {
|
|
13754
13780
|
return false;
|
|
13755
13781
|
}
|
|
13756
|
-
async discoverDevices() {
|
|
13782
|
+
async discoverDevices(_input) {
|
|
13757
13783
|
return [];
|
|
13758
13784
|
}
|
|
13785
|
+
/** Extra per-scan input form (e.g. a broadcast address for another subnet).
|
|
13786
|
+
* Null = no extra params. Override in providers that support scoped scans. */
|
|
13787
|
+
async getDiscoveryParamsSchema() {
|
|
13788
|
+
return null;
|
|
13789
|
+
}
|
|
13790
|
+
/**
|
|
13791
|
+
* The DeviceType this provider creates via manual add — derived from the
|
|
13792
|
+
* `deviceClasses` map (first registered type). `null` when manual creation is
|
|
13793
|
+
* unsupported. Lets the Add-Device dialog pick the right type per provider.
|
|
13794
|
+
*/
|
|
13795
|
+
async getManualCreationType() {
|
|
13796
|
+
if (!await this.supportsManualCreation()) return { deviceType: null };
|
|
13797
|
+
return { deviceType: Object.values(DeviceType).find((t) => this.deviceClasses[t] !== void 0) ?? null };
|
|
13798
|
+
}
|
|
13759
13799
|
async adoptDiscoveredDevice(_input) {
|
|
13760
13800
|
throw new Error(`${this.providerName} provider does not support discovery-based adoption`);
|
|
13761
13801
|
}
|
|
@@ -14802,83 +14842,34 @@ var GetStateInputSchema = object({
|
|
|
14802
14842
|
* HA: entity_id (returns the cached entity state). */
|
|
14803
14843
|
key: string()
|
|
14804
14844
|
});
|
|
14805
|
-
|
|
14806
|
-
|
|
14807
|
-
|
|
14808
|
-
|
|
14809
|
-
|
|
14810
|
-
|
|
14811
|
-
|
|
14812
|
-
|
|
14813
|
-
|
|
14814
|
-
|
|
14815
|
-
|
|
14816
|
-
|
|
14817
|
-
|
|
14818
|
-
|
|
14819
|
-
|
|
14820
|
-
|
|
14821
|
-
|
|
14822
|
-
|
|
14823
|
-
|
|
14824
|
-
|
|
14825
|
-
|
|
14826
|
-
|
|
14827
|
-
|
|
14828
|
-
|
|
14829
|
-
|
|
14830
|
-
|
|
14831
|
-
|
|
14832
|
-
|
|
14833
|
-
}),
|
|
14834
|
-
/** Read the persisted settings record for a broker (kind-specific
|
|
14835
|
-
* shape). Admin-only — settings may contain secrets. Returns `null`
|
|
14836
|
-
* when the broker id is unknown to the provider (the collection
|
|
14837
|
-
* fallback may route a foreign id to the first provider). */
|
|
14838
|
-
getSettings: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
|
|
14839
|
-
/** Overwrite the persisted settings record. The kind-specific
|
|
14840
|
-
* provider validates the shape and applies the change (reconnects
|
|
14841
|
-
* if credentials changed). */
|
|
14842
|
-
setSettings: method(object({
|
|
14843
|
-
id: string(),
|
|
14844
|
-
settings: SettingsRecordSchema$1
|
|
14845
|
-
}), _void(), {
|
|
14846
|
-
kind: "mutation",
|
|
14847
|
-
auth: "admin"
|
|
14848
|
-
}),
|
|
14849
|
-
/** Returns the kind-specific connection config the consumer needs
|
|
14850
|
-
* to open its own client (MQTT pattern: `{url, username, password,
|
|
14851
|
-
* clientIdPrefix}`). HA providers MAY return the auth envelope
|
|
14852
|
-
* but typical HA consumers use `publish` / `subscribe` instead.
|
|
14853
|
-
* Returns `null` when the broker id is unknown to the provider. */
|
|
14854
|
-
getBrokerConfig: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
|
|
14855
|
-
getSettingsSchema: method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }),
|
|
14856
|
-
testSettings: method(TestSettingsInputSchema, TestSettingsResultSchema, {
|
|
14857
|
-
kind: "mutation",
|
|
14858
|
-
auth: "admin"
|
|
14859
|
-
}),
|
|
14860
|
-
publish: method(PublishInputSchema, unknown(), {
|
|
14861
|
-
kind: "mutation",
|
|
14862
|
-
auth: "admin"
|
|
14863
|
-
}),
|
|
14864
|
-
subscribe: method(SubscribeInputSchema, SubscribeResultSchema, {
|
|
14865
|
-
kind: "mutation",
|
|
14866
|
-
auth: "admin"
|
|
14867
|
-
}),
|
|
14868
|
-
unsubscribe: method(UnsubscribeInputSchema, _void(), {
|
|
14869
|
-
kind: "mutation",
|
|
14870
|
-
auth: "admin"
|
|
14871
|
-
}),
|
|
14872
|
-
/** Read the broker's cached state for a key. Returns `null` when
|
|
14873
|
-
* unknown to the broker (never published / unknown entity). */
|
|
14874
|
-
getState: method(GetStateInputSchema, unknown().nullable()),
|
|
14875
|
-
/** Status method — explicit registration with a `z.void()` input so
|
|
14876
|
-
* the codegen-generated tRPC router types its input as
|
|
14877
|
-
* `{addonId?: string, nodeId?: string}` (system-scoped collection
|
|
14878
|
-
* shape) instead of the device-scoped `{deviceId}` fallback. */
|
|
14879
|
-
getStatus: method(_void(), RegistryStatusSchema)
|
|
14880
|
-
}
|
|
14881
|
-
};
|
|
14845
|
+
method(ListInputSchema, array(BrokerInfoSchema$1)), method(GetInputSchema, BrokerInfoSchema$1.nullable()), method(_void(), array(BrokerProviderInfoSchema), { auth: "admin" }), method(AddInputSchema, AddResultSchema, {
|
|
14846
|
+
kind: "mutation",
|
|
14847
|
+
auth: "admin"
|
|
14848
|
+
}), method(RemoveInputSchema, _void(), {
|
|
14849
|
+
kind: "mutation",
|
|
14850
|
+
auth: "admin"
|
|
14851
|
+
}), method(GetInputSchema, TestConnectionResultSchema, {
|
|
14852
|
+
kind: "mutation",
|
|
14853
|
+
auth: "admin"
|
|
14854
|
+
}), method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }), method(object({
|
|
14855
|
+
id: string(),
|
|
14856
|
+
settings: SettingsRecordSchema$1
|
|
14857
|
+
}), _void(), {
|
|
14858
|
+
kind: "mutation",
|
|
14859
|
+
auth: "admin"
|
|
14860
|
+
}), method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }), method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }), method(TestSettingsInputSchema, TestSettingsResultSchema, {
|
|
14861
|
+
kind: "mutation",
|
|
14862
|
+
auth: "admin"
|
|
14863
|
+
}), method(PublishInputSchema, unknown(), {
|
|
14864
|
+
kind: "mutation",
|
|
14865
|
+
auth: "admin"
|
|
14866
|
+
}), method(SubscribeInputSchema, SubscribeResultSchema, {
|
|
14867
|
+
kind: "mutation",
|
|
14868
|
+
auth: "admin"
|
|
14869
|
+
}), method(UnsubscribeInputSchema, _void(), {
|
|
14870
|
+
kind: "mutation",
|
|
14871
|
+
auth: "admin"
|
|
14872
|
+
}), method(GetStateInputSchema, unknown().nullable()), method(_void(), RegistryStatusSchema);
|
|
14882
14873
|
DeviceType.Camera;
|
|
14883
14874
|
/**
|
|
14884
14875
|
* `custom-model-registry` — collection cap exposing operator-registered
|
|
@@ -15666,7 +15657,10 @@ method(object({
|
|
|
15666
15657
|
}), FieldProbeResultSchema, {
|
|
15667
15658
|
kind: "mutation",
|
|
15668
15659
|
auth: "admin"
|
|
15669
|
-
}), method(
|
|
15660
|
+
}), method(object({
|
|
15661
|
+
addonId: string(),
|
|
15662
|
+
integrationId: string()
|
|
15663
|
+
}), object({ filters: array(AdoptionFilterSchema) }), { auth: "admin" }), method(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
|
|
15670
15664
|
addonId: string(),
|
|
15671
15665
|
integrationId: string()
|
|
15672
15666
|
}), AdoptionStatusSchema, {
|
|
@@ -15681,7 +15675,24 @@ method(object({
|
|
|
15681
15675
|
}), method(ResyncInputSchema, ResyncResultSchema, {
|
|
15682
15676
|
kind: "mutation",
|
|
15683
15677
|
auth: "admin"
|
|
15678
|
+
}), method(object({}), object({ providers: array(object({
|
|
15679
|
+
addonId: string(),
|
|
15680
|
+
label: string()
|
|
15681
|
+
})).readonly() }), { auth: "admin" }), method(object({}), object({ groups: array(object({
|
|
15682
|
+
addonId: string(),
|
|
15683
|
+
label: string(),
|
|
15684
|
+
candidates: array(DiscoveryCandidateSchema).readonly(),
|
|
15685
|
+
error: string().nullable()
|
|
15686
|
+
})).readonly() }), {
|
|
15687
|
+
kind: "mutation",
|
|
15688
|
+
auth: "admin"
|
|
15684
15689
|
}), method(object({
|
|
15690
|
+
addonId: string(),
|
|
15691
|
+
params: record(string(), unknown()).optional()
|
|
15692
|
+
}), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
|
|
15693
|
+
kind: "mutation",
|
|
15694
|
+
auth: "admin"
|
|
15695
|
+
}), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
|
|
15685
15696
|
deviceId: number(),
|
|
15686
15697
|
key: string(),
|
|
15687
15698
|
value: unknown()
|
|
@@ -18333,6 +18344,17 @@ var AvailableIntegrationTypeSchema = object({
|
|
|
18333
18344
|
iconUrl: string().nullable(),
|
|
18334
18345
|
color: string(),
|
|
18335
18346
|
instanceMode: string(),
|
|
18347
|
+
/**
|
|
18348
|
+
* Integration wizard `mode` (LOCKED MODEL): `standalone` (create
|
|
18349
|
+
* immediately then add devices, no config step/button), `account` (config
|
|
18350
|
+
* step), or `broker` (broker step). Derived server-side by
|
|
18351
|
+
* `getAvailableTypes` when the addon manifest omits an explicit `mode`.
|
|
18352
|
+
*/
|
|
18353
|
+
mode: _enum([
|
|
18354
|
+
"standalone",
|
|
18355
|
+
"account",
|
|
18356
|
+
"broker"
|
|
18357
|
+
]),
|
|
18336
18358
|
discoveryMode: string(),
|
|
18337
18359
|
/**
|
|
18338
18360
|
* Which integration-marker cap the addon declared, so the wizard can
|
|
@@ -20793,6 +20815,12 @@ Object.freeze({
|
|
|
20793
20815
|
addonId: null,
|
|
20794
20816
|
access: "create"
|
|
20795
20817
|
},
|
|
20818
|
+
"deviceManager.adoptionListCandidateFilters": {
|
|
20819
|
+
capName: "device-manager",
|
|
20820
|
+
capScope: "system",
|
|
20821
|
+
addonId: null,
|
|
20822
|
+
access: "view"
|
|
20823
|
+
},
|
|
20796
20824
|
"deviceManager.adoptionListCandidates": {
|
|
20797
20825
|
capName: "device-manager",
|
|
20798
20826
|
capScope: "system",
|
|
@@ -20841,12 +20869,30 @@ Object.freeze({
|
|
|
20841
20869
|
addonId: null,
|
|
20842
20870
|
access: "create"
|
|
20843
20871
|
},
|
|
20872
|
+
"deviceManager.discoverAllProviders": {
|
|
20873
|
+
capName: "device-manager",
|
|
20874
|
+
capScope: "system",
|
|
20875
|
+
addonId: null,
|
|
20876
|
+
access: "create"
|
|
20877
|
+
},
|
|
20844
20878
|
"deviceManager.discoverDevices": {
|
|
20845
20879
|
capName: "device-manager",
|
|
20846
20880
|
capScope: "system",
|
|
20847
20881
|
addonId: null,
|
|
20848
20882
|
access: "create"
|
|
20849
20883
|
},
|
|
20884
|
+
"deviceManager.discoverProvider": {
|
|
20885
|
+
capName: "device-manager",
|
|
20886
|
+
capScope: "system",
|
|
20887
|
+
addonId: null,
|
|
20888
|
+
access: "create"
|
|
20889
|
+
},
|
|
20890
|
+
"deviceManager.discoveryProviders": {
|
|
20891
|
+
capName: "device-manager",
|
|
20892
|
+
capScope: "system",
|
|
20893
|
+
addonId: null,
|
|
20894
|
+
access: "view"
|
|
20895
|
+
},
|
|
20850
20896
|
"deviceManager.enable": {
|
|
20851
20897
|
capName: "device-manager",
|
|
20852
20898
|
capScope: "system",
|
|
@@ -20997,6 +21043,18 @@ Object.freeze({
|
|
|
20997
21043
|
addonId: null,
|
|
20998
21044
|
access: "create"
|
|
20999
21045
|
},
|
|
21046
|
+
"deviceManager.providerCreationType": {
|
|
21047
|
+
capName: "device-manager",
|
|
21048
|
+
capScope: "system",
|
|
21049
|
+
addonId: null,
|
|
21050
|
+
access: "view"
|
|
21051
|
+
},
|
|
21052
|
+
"deviceManager.providerDiscoveryParamsSchema": {
|
|
21053
|
+
capName: "device-manager",
|
|
21054
|
+
capScope: "system",
|
|
21055
|
+
addonId: null,
|
|
21056
|
+
access: "view"
|
|
21057
|
+
},
|
|
21000
21058
|
"deviceManager.registerDevice": {
|
|
21001
21059
|
capName: "device-manager",
|
|
21002
21060
|
capScope: "system",
|
|
@@ -21213,6 +21271,18 @@ Object.freeze({
|
|
|
21213
21271
|
addonId: null,
|
|
21214
21272
|
access: "view"
|
|
21215
21273
|
},
|
|
21274
|
+
"deviceProvider.getDiscoveryParamsSchema": {
|
|
21275
|
+
capName: "device-provider",
|
|
21276
|
+
capScope: "system",
|
|
21277
|
+
addonId: null,
|
|
21278
|
+
access: "view"
|
|
21279
|
+
},
|
|
21280
|
+
"deviceProvider.getManualCreationType": {
|
|
21281
|
+
capName: "device-provider",
|
|
21282
|
+
capScope: "system",
|
|
21283
|
+
addonId: null,
|
|
21284
|
+
access: "view"
|
|
21285
|
+
},
|
|
21216
21286
|
"deviceProvider.getStatus": {
|
|
21217
21287
|
capName: "device-provider",
|
|
21218
21288
|
capScope: "system",
|
|
@@ -24051,6 +24121,89 @@ object({
|
|
|
24051
24121
|
schemaVersion: literal(1)
|
|
24052
24122
|
});
|
|
24053
24123
|
//#endregion
|
|
24124
|
+
//#region src/config.ts
|
|
24125
|
+
/**
|
|
24126
|
+
* Dreo cloud regions the wrapped `@apocaliss92/nodedreo` client accepts. The
|
|
24127
|
+
* library does not export a region enum (region is a free `opts.region` string),
|
|
24128
|
+
* so we mirror the common Dreo regions locally and validate the operator-supplied
|
|
24129
|
+
* value at the system boundary. `us` is the default (the most common Dreo cloud).
|
|
24130
|
+
*/
|
|
24131
|
+
var DREO_REGIONS = ["us", "eu"];
|
|
24132
|
+
var DreoRegionSchema = _enum(DREO_REGIONS);
|
|
24133
|
+
/**
|
|
24134
|
+
* Operator-supplied Dreo account settings for ONE integration (= one cloud
|
|
24135
|
+
* account). The Dreo integration is cloud-only (REST auth + a persistent
|
|
24136
|
+
* WebSocket push), so the connection is just account credentials plus a region.
|
|
24137
|
+
*/
|
|
24138
|
+
var dreoConfigSchema = object({
|
|
24139
|
+
email: string().min(1).describe("Dreo account email"),
|
|
24140
|
+
password: string().min(1).describe("Dreo account password"),
|
|
24141
|
+
region: DreoRegionSchema.default("us").describe("Dreo cloud region")
|
|
24142
|
+
});
|
|
24143
|
+
/**
|
|
24144
|
+
* Split the validated addon config into the two arguments the `Nodedreo`
|
|
24145
|
+
* constructor expects (`creds`, `opts`). Pure: same config in → same args out.
|
|
24146
|
+
*/
|
|
24147
|
+
function toDreoConstructorArgs(config) {
|
|
24148
|
+
return {
|
|
24149
|
+
creds: {
|
|
24150
|
+
email: config.email,
|
|
24151
|
+
password: config.password
|
|
24152
|
+
},
|
|
24153
|
+
opts: { region: config.region }
|
|
24154
|
+
};
|
|
24155
|
+
}
|
|
24156
|
+
/**
|
|
24157
|
+
* Parse an integration's settings into a connection, or null when the mandatory
|
|
24158
|
+
* credentials (email + password) are missing. Used by the boot/lifecycle
|
|
24159
|
+
* reconcile to skip an integration whose account form was not completed.
|
|
24160
|
+
*/
|
|
24161
|
+
function connectionFromSettings(settings) {
|
|
24162
|
+
const parsed = dreoConfigSchema.safeParse(settings);
|
|
24163
|
+
return parsed.success ? parsed.data : null;
|
|
24164
|
+
}
|
|
24165
|
+
/**
|
|
24166
|
+
* Hand-written connection form for the account/integration creation UI — the
|
|
24167
|
+
* wizard's `account` config step renders this via `getGlobalSettings`. A flat
|
|
24168
|
+
* set of sections the admin UI renders into the "Add Dreo account" modal.
|
|
24169
|
+
*/
|
|
24170
|
+
function buildConnectionFormSchema() {
|
|
24171
|
+
return { sections: [{
|
|
24172
|
+
id: "credentials",
|
|
24173
|
+
title: "Dreo account",
|
|
24174
|
+
description: "Sign in with your Dreo app account credentials.",
|
|
24175
|
+
columns: 1,
|
|
24176
|
+
fields: [{
|
|
24177
|
+
type: "text",
|
|
24178
|
+
key: "email",
|
|
24179
|
+
label: "Email",
|
|
24180
|
+
required: true,
|
|
24181
|
+
placeholder: "you@example.com"
|
|
24182
|
+
}, {
|
|
24183
|
+
type: "password",
|
|
24184
|
+
key: "password",
|
|
24185
|
+
label: "Password",
|
|
24186
|
+
required: true,
|
|
24187
|
+
showToggle: true
|
|
24188
|
+
}]
|
|
24189
|
+
}, {
|
|
24190
|
+
id: "region",
|
|
24191
|
+
title: "Region",
|
|
24192
|
+
description: "Select the Dreo cloud region your account is registered in.",
|
|
24193
|
+
columns: 1,
|
|
24194
|
+
fields: [{
|
|
24195
|
+
type: "select",
|
|
24196
|
+
key: "region",
|
|
24197
|
+
label: "Region",
|
|
24198
|
+
default: "us",
|
|
24199
|
+
options: DREO_REGIONS.map((r) => ({
|
|
24200
|
+
value: r,
|
|
24201
|
+
label: r.toUpperCase()
|
|
24202
|
+
}))
|
|
24203
|
+
}]
|
|
24204
|
+
}] };
|
|
24205
|
+
}
|
|
24206
|
+
//#endregion
|
|
24054
24207
|
//#region ../../node_modules/ws/lib/constants.js
|
|
24055
24208
|
var require_constants = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
24056
24209
|
var BINARY_TYPES = [
|
|
@@ -28896,99 +29049,6 @@ var Nodedreo = class {
|
|
|
28896
29049
|
}
|
|
28897
29050
|
};
|
|
28898
29051
|
//#endregion
|
|
28899
|
-
//#region src/config.ts
|
|
28900
|
-
/**
|
|
28901
|
-
* Dreo cloud regions the wrapped `@apocaliss92/nodedreo` client accepts. The
|
|
28902
|
-
* library does not export a region enum (region is a free `opts.region` string),
|
|
28903
|
-
* so we mirror the common Dreo regions locally and validate the operator-supplied
|
|
28904
|
-
* value at the system boundary. `us` is the default (the most common Dreo cloud).
|
|
28905
|
-
*/
|
|
28906
|
-
var DREO_REGIONS = ["us", "eu"];
|
|
28907
|
-
var DreoRegionSchema = _enum(DREO_REGIONS);
|
|
28908
|
-
/**
|
|
28909
|
-
* Operator-supplied Dreo account settings for ONE broker (= one cloud account).
|
|
28910
|
-
* The Dreo integration is cloud-only (REST auth + a persistent WebSocket push),
|
|
28911
|
-
* so the connection is just account credentials plus a region.
|
|
28912
|
-
*/
|
|
28913
|
-
var dreoConfigSchema = object({
|
|
28914
|
-
email: string().min(1).describe("Dreo account email"),
|
|
28915
|
-
password: string().min(1).describe("Dreo account password"),
|
|
28916
|
-
region: DreoRegionSchema.default("us").describe("Dreo cloud region")
|
|
28917
|
-
});
|
|
28918
|
-
/**
|
|
28919
|
-
* Split the validated addon config into the two arguments the `Nodedreo`
|
|
28920
|
-
* constructor expects (`creds`, `opts`). Pure: same config in → same args out.
|
|
28921
|
-
*/
|
|
28922
|
-
function toDreoConstructorArgs(config) {
|
|
28923
|
-
return {
|
|
28924
|
-
creds: {
|
|
28925
|
-
email: config.email,
|
|
28926
|
-
password: config.password
|
|
28927
|
-
},
|
|
28928
|
-
opts: { region: config.region }
|
|
28929
|
-
};
|
|
28930
|
-
}
|
|
28931
|
-
/** Top-level addon config — an ordered list of broker entries (default empty). */
|
|
28932
|
-
var dreoAddonConfigSchema = object({ brokers: array(object({
|
|
28933
|
-
/** Stable opaque identifier — e.g. 'dreo_001', 'dreo_002'. */
|
|
28934
|
-
id: string().min(1),
|
|
28935
|
-
/** Human-readable label shown in the admin UI. */
|
|
28936
|
-
name: string().min(1),
|
|
28937
|
-
/** Validated account connection settings. */
|
|
28938
|
-
connection: dreoConfigSchema,
|
|
28939
|
-
/** FK to the spawning integration — auto-cleanup on integration delete. */
|
|
28940
|
-
integrationId: string().optional()
|
|
28941
|
-
})).default([]) });
|
|
28942
|
-
/**
|
|
28943
|
-
* Coerce a loose settings blob (from the broker `add`/`setSettings` cap) through
|
|
28944
|
-
* the connection schema, applying all defaults. Throws a `ZodError` on invalid
|
|
28945
|
-
* input so the caller surfaces a clear error at the system boundary.
|
|
28946
|
-
*/
|
|
28947
|
-
function settingsToDreoConfig(settings) {
|
|
28948
|
-
return dreoConfigSchema.parse(settings ?? {});
|
|
28949
|
-
}
|
|
28950
|
-
/**
|
|
28951
|
-
* Hand-written connection form for the broker/integration creation UI. Mirrors
|
|
28952
|
-
* the Dreame / Homematic broker-settings form shape — a flat set of sections the
|
|
28953
|
-
* admin UI renders into the "Add Dreo account" modal.
|
|
28954
|
-
*/
|
|
28955
|
-
function buildConnectionFormSchema() {
|
|
28956
|
-
return { sections: [{
|
|
28957
|
-
id: "credentials",
|
|
28958
|
-
title: "Dreo account",
|
|
28959
|
-
description: "Sign in with your Dreo app account credentials.",
|
|
28960
|
-
columns: 1,
|
|
28961
|
-
fields: [{
|
|
28962
|
-
type: "text",
|
|
28963
|
-
key: "email",
|
|
28964
|
-
label: "Email",
|
|
28965
|
-
required: true,
|
|
28966
|
-
placeholder: "you@example.com"
|
|
28967
|
-
}, {
|
|
28968
|
-
type: "password",
|
|
28969
|
-
key: "password",
|
|
28970
|
-
label: "Password",
|
|
28971
|
-
required: true,
|
|
28972
|
-
showToggle: true
|
|
28973
|
-
}]
|
|
28974
|
-
}, {
|
|
28975
|
-
id: "region",
|
|
28976
|
-
title: "Region",
|
|
28977
|
-
description: "Select the Dreo cloud region your account is registered in.",
|
|
28978
|
-
columns: 1,
|
|
28979
|
-
fields: [{
|
|
28980
|
-
type: "select",
|
|
28981
|
-
key: "region",
|
|
28982
|
-
label: "Region",
|
|
28983
|
-
default: "us",
|
|
28984
|
-
options: DREO_REGIONS.map((r) => ({
|
|
28985
|
-
value: r,
|
|
28986
|
-
label: r.toUpperCase()
|
|
28987
|
-
}))
|
|
28988
|
-
}]
|
|
28989
|
-
}] };
|
|
28990
|
-
}
|
|
28991
|
-
//#endregion
|
|
28992
29052
|
//#region src/dreo-integration-manager.ts
|
|
28993
29053
|
function defaultFacade(config) {
|
|
28994
29054
|
const { creds, opts } = toDreoConstructorArgs(config);
|
|
@@ -29146,15 +29206,20 @@ function sameConnection(a, b) {
|
|
|
29146
29206
|
//#endregion
|
|
29147
29207
|
//#region src/dreo-gateway.ts
|
|
29148
29208
|
/**
|
|
29149
|
-
* Per-
|
|
29209
|
+
* Per-integration registry that device classes use to reach a live Dreo device
|
|
29150
29210
|
* handle (and the discovery info) for a given Dreo account.
|
|
29151
29211
|
*
|
|
29212
|
+
* `mode: account`: keyed by `integrationId`. The string key is generically named
|
|
29213
|
+
* `brokerId` in the method signatures below because the device classes pass their
|
|
29214
|
+
* persisted `brokerId` config field — which now holds the integrationId
|
|
29215
|
+
* (unchanged device-side code, see the addon migration note).
|
|
29216
|
+
*
|
|
29152
29217
|
* The kernel constructs device classes with only a `DeviceContext` — it cannot
|
|
29153
29218
|
* thread the handle in as a constructor arg. Like the Dreame addon's
|
|
29154
29219
|
* `dreameFacades`, we keep it simple and in-process: the integration manager
|
|
29155
29220
|
* owns the connection surface per registered account and publishes it here;
|
|
29156
29221
|
* device classes resolve their live `BaseDevice` handle by `(brokerId,
|
|
29157
|
-
* deviceId)
|
|
29222
|
+
* deviceId)` where `brokerId` = integrationId.
|
|
29158
29223
|
*/
|
|
29159
29224
|
var DreoConnectionResolver = class {
|
|
29160
29225
|
#surfaces = /* @__PURE__ */ new Map();
|
|
@@ -29191,31 +29256,51 @@ var DreoConnectionResolver = class {
|
|
|
29191
29256
|
this.#surfaces.clear();
|
|
29192
29257
|
}
|
|
29193
29258
|
};
|
|
29194
|
-
/** The single in-process per-
|
|
29195
|
-
* registry/manager and device instances. */
|
|
29259
|
+
/** The single in-process per-integration connection resolver shared between the
|
|
29260
|
+
* client registry/manager and device instances (keyed by integrationId). */
|
|
29196
29261
|
var dreoConnections = new DreoConnectionResolver();
|
|
29197
29262
|
//#endregion
|
|
29198
|
-
//#region src/dreo-
|
|
29263
|
+
//#region src/dreo-client-registry.ts
|
|
29264
|
+
/**
|
|
29265
|
+
* Per-integration Dreo connection registry (account mode).
|
|
29266
|
+
*
|
|
29267
|
+
* Under the LOCKED integration/adoption model (design §7.1), Dreo is a
|
|
29268
|
+
* `mode: account` addon: each Dreo integration carries its own Dreo cloud
|
|
29269
|
+
* account credentials in its `integration.settings`, and this registry holds one
|
|
29270
|
+
* {@link DreoIntegrationManager} per `integrationId`
|
|
29271
|
+
* (`Map<integrationId, DreoIntegrationManager>`). Multi-account = multiple
|
|
29272
|
+
* integrations; there is NO shared broker.
|
|
29273
|
+
*
|
|
29274
|
+
* The live Dreo connection surface each manager owns (discovery info + per-device
|
|
29275
|
+
* handles) is published on the in-process {@link dreoConnections} resolver KEYED
|
|
29276
|
+
* BY `integrationId` — the manager's id IS the integrationId. Device classes
|
|
29277
|
+
* resolve their live handle by the `brokerId` config field they persist, which
|
|
29278
|
+
* now holds the integrationId (unchanged device-side code, see the addon
|
|
29279
|
+
* migration note). The WebSocket push lifecycle stays entirely inside the manager
|
|
29280
|
+
* + library (no leak): the registry only starts/stops managers.
|
|
29281
|
+
*/
|
|
29199
29282
|
/**
|
|
29200
|
-
*
|
|
29201
|
-
*
|
|
29202
|
-
*
|
|
29203
|
-
*
|
|
29283
|
+
* Owns the `Map<integrationId, DreoIntegrationManager>`. Callers reconcile the
|
|
29284
|
+
* map against the live integration list on boot + on every integration lifecycle
|
|
29285
|
+
* event; device classes + the adoption provider resolve their surface by
|
|
29286
|
+
* `integrationId` through {@link dreoConnections}.
|
|
29287
|
+
*
|
|
29288
|
+
* Structural twin of `DreameClientRegistry` / `WyzeClientRegistry` — `upsert` is
|
|
29289
|
+
* session-preserving when credentials are unchanged, `retain` drops vanished
|
|
29290
|
+
* integrations.
|
|
29204
29291
|
*/
|
|
29205
|
-
var
|
|
29292
|
+
var DreoClientRegistry = class {
|
|
29206
29293
|
#logger;
|
|
29207
|
-
#
|
|
29208
|
-
#
|
|
29294
|
+
#onConnected;
|
|
29295
|
+
#onDisconnected;
|
|
29209
29296
|
#makeManager;
|
|
29210
|
-
#
|
|
29211
|
-
#integrationToBroker = /* @__PURE__ */ new Map();
|
|
29212
|
-
#nextId = 1;
|
|
29297
|
+
#entries = /* @__PURE__ */ new Map();
|
|
29213
29298
|
constructor(logger, deps = {}) {
|
|
29214
29299
|
this.#logger = logger;
|
|
29215
|
-
this.#
|
|
29216
|
-
this.#
|
|
29300
|
+
this.#onConnected = deps.onConnected ?? (() => void 0);
|
|
29301
|
+
this.#onDisconnected = deps.onDisconnected ?? (() => void 0);
|
|
29217
29302
|
this.#makeManager = deps.makeManager ?? ((opts) => new DreoIntegrationManager({
|
|
29218
|
-
id: opts.
|
|
29303
|
+
id: opts.integrationId,
|
|
29219
29304
|
name: opts.name,
|
|
29220
29305
|
connection: opts.connection,
|
|
29221
29306
|
logger: opts.logger,
|
|
@@ -29224,259 +29309,119 @@ var DreoBrokerRegistry = class {
|
|
|
29224
29309
|
surfaceSink: dreoConnections
|
|
29225
29310
|
}));
|
|
29226
29311
|
}
|
|
29227
|
-
|
|
29228
|
-
|
|
29229
|
-
for (const entry of entries) this.#seedCounter(entry.id);
|
|
29230
|
-
for (const entry of entries) try {
|
|
29231
|
-
await this.#startManager(entry);
|
|
29232
|
-
} catch (err) {
|
|
29233
|
-
this.#logger.warn("DreoBrokerRegistry: failed to restore manager", {
|
|
29234
|
-
tags: { brokerId: entry.id },
|
|
29235
|
-
meta: { error: errMsg(err) }
|
|
29236
|
-
});
|
|
29237
|
-
}
|
|
29312
|
+
setOnConnected(cb) {
|
|
29313
|
+
this.#onConnected = cb;
|
|
29238
29314
|
}
|
|
29239
|
-
|
|
29240
|
-
|
|
29241
|
-
|
|
29242
|
-
|
|
29315
|
+
setOnDisconnected(cb) {
|
|
29316
|
+
this.#onDisconnected = cb;
|
|
29317
|
+
}
|
|
29318
|
+
/**
|
|
29319
|
+
* Ensure a manager exists for `integrationId` with the given credentials.
|
|
29320
|
+
* Idempotent: an existing entry with identical credentials is preserved (keeps
|
|
29321
|
+
* its live WebSocket session); a credentials change re-applies the connection
|
|
29322
|
+
* atomically (stop + start). Best-effort start — a failed login leaves the
|
|
29323
|
+
* entry registered so a later reconcile / lifecycle event retries.
|
|
29324
|
+
*/
|
|
29325
|
+
async upsert(integrationId, name, connection) {
|
|
29326
|
+
const existing = this.#entries.get(integrationId);
|
|
29327
|
+
if (existing) {
|
|
29328
|
+
if (sameConnection(existing.connection, connection)) return;
|
|
29243
29329
|
try {
|
|
29244
|
-
await
|
|
29330
|
+
await existing.manager.applyConnection(connection);
|
|
29331
|
+
this.#entries.set(integrationId, {
|
|
29332
|
+
manager: existing.manager,
|
|
29333
|
+
connection
|
|
29334
|
+
});
|
|
29245
29335
|
} catch (err) {
|
|
29246
|
-
this.#logger.warn("
|
|
29247
|
-
tags: {
|
|
29336
|
+
this.#logger.warn("DreoClientRegistry: applyConnection failed", {
|
|
29337
|
+
tags: { integrationId },
|
|
29248
29338
|
meta: { error: errMsg(err) }
|
|
29249
29339
|
});
|
|
29250
29340
|
}
|
|
29251
|
-
|
|
29252
|
-
|
|
29253
|
-
this.#
|
|
29254
|
-
|
|
29255
|
-
}
|
|
29256
|
-
setOnBrokerConnected(cb) {
|
|
29257
|
-
this.#onBrokerConnected = cb;
|
|
29258
|
-
}
|
|
29259
|
-
setOnBrokerDisconnected(cb) {
|
|
29260
|
-
this.#onBrokerDisconnected = cb;
|
|
29261
|
-
}
|
|
29262
|
-
async createEntry(name, connection, opts = {}) {
|
|
29263
|
-
const id = this.#allocateId();
|
|
29264
|
-
const entry = {
|
|
29265
|
-
id,
|
|
29341
|
+
return;
|
|
29342
|
+
}
|
|
29343
|
+
const manager = this.#makeManager({
|
|
29344
|
+
integrationId,
|
|
29266
29345
|
name,
|
|
29267
29346
|
connection,
|
|
29268
|
-
|
|
29269
|
-
|
|
29270
|
-
|
|
29271
|
-
|
|
29272
|
-
|
|
29273
|
-
|
|
29274
|
-
|
|
29275
|
-
|
|
29276
|
-
if (!mgr) throw new Error(`DreoBrokerRegistry: unknown broker id "${id}"`);
|
|
29277
|
-
for (const [integrationId, brokerId] of this.#integrationToBroker.entries()) if (brokerId === id) this.#integrationToBroker.delete(integrationId);
|
|
29278
|
-
this.#managers.delete(id);
|
|
29347
|
+
logger: this.#logger,
|
|
29348
|
+
onConnected: (id) => this.#onConnected(id),
|
|
29349
|
+
onDisconnected: (id) => this.#onDisconnected(id)
|
|
29350
|
+
});
|
|
29351
|
+
this.#entries.set(integrationId, {
|
|
29352
|
+
manager,
|
|
29353
|
+
connection
|
|
29354
|
+
});
|
|
29279
29355
|
try {
|
|
29280
|
-
await
|
|
29356
|
+
await manager.start();
|
|
29281
29357
|
} catch (err) {
|
|
29282
|
-
this.#logger.warn("
|
|
29283
|
-
tags: {
|
|
29358
|
+
this.#logger.warn("DreoClientRegistry: manager start failed", {
|
|
29359
|
+
tags: { integrationId },
|
|
29284
29360
|
meta: { error: errMsg(err) }
|
|
29285
29361
|
});
|
|
29286
29362
|
}
|
|
29287
29363
|
}
|
|
29288
|
-
|
|
29289
|
-
|
|
29290
|
-
|
|
29291
|
-
|
|
29292
|
-
|
|
29293
|
-
|
|
29294
|
-
|
|
29295
|
-
|
|
29296
|
-
|
|
29364
|
+
/** Stop + drop the manager for an integration that no longer exists. */
|
|
29365
|
+
async remove(integrationId) {
|
|
29366
|
+
const entry = this.#entries.get(integrationId);
|
|
29367
|
+
if (!entry) return;
|
|
29368
|
+
this.#entries.delete(integrationId);
|
|
29369
|
+
try {
|
|
29370
|
+
await entry.manager.stop();
|
|
29371
|
+
} catch (err) {
|
|
29372
|
+
this.#logger.warn("DreoClientRegistry: remove stop failed", {
|
|
29373
|
+
tags: { integrationId },
|
|
29374
|
+
meta: { error: errMsg(err) }
|
|
29375
|
+
});
|
|
29376
|
+
}
|
|
29297
29377
|
}
|
|
29298
|
-
|
|
29299
|
-
|
|
29378
|
+
/** Stop + drop every manager whose integrationId is not in `keep`. */
|
|
29379
|
+
async retain(keep) {
|
|
29380
|
+
const stale = [...this.#entries.keys()].filter((id) => !keep.has(id));
|
|
29381
|
+
for (const id of stale) await this.remove(id);
|
|
29300
29382
|
}
|
|
29301
|
-
|
|
29302
|
-
|
|
29303
|
-
|
|
29304
|
-
|
|
29383
|
+
/** Stop every manager and clear all state (full shutdown). */
|
|
29384
|
+
async shutdown() {
|
|
29385
|
+
const ids = [...this.#entries.keys()];
|
|
29386
|
+
await Promise.all(ids.map(async (id) => {
|
|
29387
|
+
try {
|
|
29388
|
+
await this.#entries.get(id)?.manager.stop();
|
|
29389
|
+
} catch (err) {
|
|
29390
|
+
this.#logger.warn("DreoClientRegistry: shutdown stop failed", {
|
|
29391
|
+
tags: { integrationId: id },
|
|
29392
|
+
meta: { error: errMsg(err) }
|
|
29393
|
+
});
|
|
29394
|
+
}
|
|
29395
|
+
}));
|
|
29396
|
+
this.#entries.clear();
|
|
29397
|
+
dreoConnections.clear();
|
|
29398
|
+
}
|
|
29399
|
+
/** True when a manager is registered for the integration. */
|
|
29400
|
+
has(integrationId) {
|
|
29401
|
+
return this.#entries.has(integrationId);
|
|
29305
29402
|
}
|
|
29403
|
+
/** The registered integration ids (one per account). */
|
|
29306
29404
|
list() {
|
|
29307
|
-
return
|
|
29405
|
+
return [...this.#entries.keys()];
|
|
29308
29406
|
}
|
|
29309
|
-
|
|
29310
|
-
|
|
29407
|
+
/** The current connection an integration's manager is configured with. */
|
|
29408
|
+
getConnection(integrationId) {
|
|
29409
|
+
return this.#entries.get(integrationId)?.connection ?? null;
|
|
29311
29410
|
}
|
|
29312
|
-
|
|
29313
|
-
|
|
29411
|
+
/** Immutable status snapshot for an integration's manager. */
|
|
29412
|
+
getInfo(integrationId) {
|
|
29413
|
+
return this.#entries.get(integrationId)?.manager.getInfo() ?? null;
|
|
29314
29414
|
}
|
|
29315
29415
|
size() {
|
|
29316
|
-
return this.#
|
|
29416
|
+
return this.#entries.size;
|
|
29317
29417
|
}
|
|
29318
29418
|
connectedCount() {
|
|
29319
29419
|
let count = 0;
|
|
29320
|
-
for (const
|
|
29420
|
+
for (const entry of this.#entries.values()) if (entry.manager.getInfo().status === "connected") count++;
|
|
29321
29421
|
return count;
|
|
29322
29422
|
}
|
|
29323
|
-
async #startManager(entry) {
|
|
29324
|
-
if (this.#managers.has(entry.id)) {
|
|
29325
|
-
if (entry.integrationId !== void 0) this.#integrationToBroker.set(entry.integrationId, entry.id);
|
|
29326
|
-
return;
|
|
29327
|
-
}
|
|
29328
|
-
const mgr = this.#makeManager({
|
|
29329
|
-
id: entry.id,
|
|
29330
|
-
name: entry.name,
|
|
29331
|
-
connection: entry.connection,
|
|
29332
|
-
logger: this.#logger,
|
|
29333
|
-
onConnected: (id) => this.#onBrokerConnected(id),
|
|
29334
|
-
onDisconnected: (id) => this.#onBrokerDisconnected(id)
|
|
29335
|
-
});
|
|
29336
|
-
this.#managers.set(entry.id, mgr);
|
|
29337
|
-
if (entry.integrationId !== void 0) this.#integrationToBroker.set(entry.integrationId, entry.id);
|
|
29338
|
-
await mgr.start();
|
|
29339
|
-
}
|
|
29340
|
-
#allocateId() {
|
|
29341
|
-
const id = `dreo_${String(this.#nextId).padStart(3, "0")}`;
|
|
29342
|
-
this.#nextId++;
|
|
29343
|
-
return id;
|
|
29344
|
-
}
|
|
29345
|
-
#seedCounter(id) {
|
|
29346
|
-
const match = /^dreo_(\d+)$/.exec(id);
|
|
29347
|
-
if (match === null || match[1] === void 0) return;
|
|
29348
|
-
const n = parseInt(match[1], 10);
|
|
29349
|
-
if (!isNaN(n)) this.#nextId = Math.max(this.#nextId, n + 1);
|
|
29350
|
-
}
|
|
29351
29423
|
};
|
|
29352
29424
|
//#endregion
|
|
29353
|
-
//#region src/dreo-broker-provider.ts
|
|
29354
|
-
/** Kind tag registered by this provider — matches the `listProviders` entry. */
|
|
29355
|
-
var DREO_KIND = "dreo";
|
|
29356
|
-
/**
|
|
29357
|
-
* Construct the `broker` cap provider for the Dreo addon. Pure builder: all
|
|
29358
|
-
* side-effecting deps are injected so the returned object is unit-testable.
|
|
29359
|
-
* Mirrors `buildDreameBrokerProvider`.
|
|
29360
|
-
*/
|
|
29361
|
-
function buildDreoBrokerProvider(deps) {
|
|
29362
|
-
const { ownerAddonId, registry, getBrokers, persistBrokers, cascadeRemoveDevices, logger } = deps;
|
|
29363
|
-
const owns = (id) => getBrokers().some((b) => b.id === id);
|
|
29364
|
-
return {
|
|
29365
|
-
list: async ({ kind }) => {
|
|
29366
|
-
if (kind && kind !== DREO_KIND) return [];
|
|
29367
|
-
return registry.list().map((info) => ({
|
|
29368
|
-
...info,
|
|
29369
|
-
addonId: ownerAddonId
|
|
29370
|
-
}));
|
|
29371
|
-
},
|
|
29372
|
-
get: async ({ id }) => {
|
|
29373
|
-
const info = registry.get(id);
|
|
29374
|
-
return info ? {
|
|
29375
|
-
...info,
|
|
29376
|
-
addonId: ownerAddonId
|
|
29377
|
-
} : null;
|
|
29378
|
-
},
|
|
29379
|
-
listProviders: async () => [{
|
|
29380
|
-
addonId: ownerAddonId,
|
|
29381
|
-
kinds: [{
|
|
29382
|
-
kind: DREO_KIND,
|
|
29383
|
-
label: "Dreo"
|
|
29384
|
-
}]
|
|
29385
|
-
}],
|
|
29386
|
-
add: async ({ kind, name, settings }) => {
|
|
29387
|
-
if (kind !== DREO_KIND) throw new Error(`provider-dreo: only kind '${DREO_KIND}' is handled here (got '${kind}')`);
|
|
29388
|
-
const entry = await registry.createEntry(name, settingsToDreoConfig(settings));
|
|
29389
|
-
await persistBrokers([...getBrokers(), entry]);
|
|
29390
|
-
return { id: entry.id };
|
|
29391
|
-
},
|
|
29392
|
-
remove: async ({ id }) => {
|
|
29393
|
-
if (!owns(id)) return;
|
|
29394
|
-
await registry.removeEntry(id);
|
|
29395
|
-
await cascadeRemoveDevices(id).catch((err) => {
|
|
29396
|
-
logger.warn("dreo: broker cascade-remove threw", {
|
|
29397
|
-
tags: { brokerId: id },
|
|
29398
|
-
meta: { error: errMsg(err) }
|
|
29399
|
-
});
|
|
29400
|
-
});
|
|
29401
|
-
await persistBrokers(getBrokers().filter((b) => b.id !== id));
|
|
29402
|
-
},
|
|
29403
|
-
testConnection: async ({ id }) => {
|
|
29404
|
-
if (!owns(id)) return {
|
|
29405
|
-
ok: false,
|
|
29406
|
-
error: "unknown broker"
|
|
29407
|
-
};
|
|
29408
|
-
const info = registry.get(id);
|
|
29409
|
-
if (!info) return {
|
|
29410
|
-
ok: false,
|
|
29411
|
-
error: "unknown broker"
|
|
29412
|
-
};
|
|
29413
|
-
if (info.status === "connected") return {
|
|
29414
|
-
ok: true,
|
|
29415
|
-
latencyMs: info.lastCheckedAt ? Math.max(0, Date.now() - info.lastCheckedAt) : 0
|
|
29416
|
-
};
|
|
29417
|
-
return {
|
|
29418
|
-
ok: false,
|
|
29419
|
-
error: info.error ?? `status: ${info.status}`
|
|
29420
|
-
};
|
|
29421
|
-
},
|
|
29422
|
-
getSettings: async ({ id }) => {
|
|
29423
|
-
const entry = getBrokers().find((b) => b.id === id);
|
|
29424
|
-
if (!entry) return null;
|
|
29425
|
-
return {
|
|
29426
|
-
...entry.connection,
|
|
29427
|
-
password: ""
|
|
29428
|
-
};
|
|
29429
|
-
},
|
|
29430
|
-
setSettings: async ({ id, settings }) => {
|
|
29431
|
-
if (!owns(id)) return;
|
|
29432
|
-
const existing = getBrokers().find((b) => b.id === id);
|
|
29433
|
-
if (!existing) return;
|
|
29434
|
-
const parsed = settingsToDreoConfig(settings);
|
|
29435
|
-
const merged = {
|
|
29436
|
-
...parsed,
|
|
29437
|
-
password: parsed.password.length > 0 ? parsed.password : existing.connection.password
|
|
29438
|
-
};
|
|
29439
|
-
const updated = await registry.updateEntry(id, merged);
|
|
29440
|
-
await persistBrokers(getBrokers().map((b) => b.id === id ? updated : b));
|
|
29441
|
-
},
|
|
29442
|
-
getBrokerConfig: async ({ id }) => {
|
|
29443
|
-
const entry = getBrokers().find((b) => b.id === id);
|
|
29444
|
-
if (!entry) return null;
|
|
29445
|
-
return {
|
|
29446
|
-
...entry.connection,
|
|
29447
|
-
password: ""
|
|
29448
|
-
};
|
|
29449
|
-
},
|
|
29450
|
-
getSettingsSchema: async ({ kind }) => {
|
|
29451
|
-
if (kind !== DREO_KIND) return null;
|
|
29452
|
-
return buildConnectionFormSchema();
|
|
29453
|
-
},
|
|
29454
|
-
testSettings: async ({ kind, settings }) => {
|
|
29455
|
-
if (kind !== DREO_KIND) return {
|
|
29456
|
-
ok: false,
|
|
29457
|
-
error: `unsupported kind: ${kind}`
|
|
29458
|
-
};
|
|
29459
|
-
try {
|
|
29460
|
-
settingsToDreoConfig(settings);
|
|
29461
|
-
return { ok: true };
|
|
29462
|
-
} catch (err) {
|
|
29463
|
-
return {
|
|
29464
|
-
ok: false,
|
|
29465
|
-
error: errMsg(err)
|
|
29466
|
-
};
|
|
29467
|
-
}
|
|
29468
|
-
},
|
|
29469
|
-
publish: async () => null,
|
|
29470
|
-
subscribe: async () => ({ subscriptionId: "" }),
|
|
29471
|
-
unsubscribe: async () => void 0,
|
|
29472
|
-
getState: async () => null,
|
|
29473
|
-
getStatus: async () => ({
|
|
29474
|
-
brokerCount: registry.size(),
|
|
29475
|
-
connectedCount: registry.connectedCount()
|
|
29476
|
-
})
|
|
29477
|
-
};
|
|
29478
|
-
}
|
|
29479
|
-
//#endregion
|
|
29480
29425
|
//#region src/dreo-domain-mapping.ts
|
|
29481
29426
|
/** Library device-type strings we treat as a CamStack `fan` kind. */
|
|
29482
29427
|
var FAN_LIB_TYPES = new Set([
|
|
@@ -29598,11 +29543,11 @@ var DEVICES_FILTER = {
|
|
|
29598
29543
|
label: "Devices",
|
|
29599
29544
|
isDefault: true
|
|
29600
29545
|
};
|
|
29601
|
-
/** Build a `dreoDeviceId → CamStack deviceId` map for a single
|
|
29602
|
-
async function
|
|
29546
|
+
/** Build a `dreoDeviceId → CamStack deviceId` map for a single integration. */
|
|
29547
|
+
async function adoptedMapForIntegration(integrationId, listAdoptedDreo) {
|
|
29603
29548
|
const all = await listAdoptedDreo();
|
|
29604
29549
|
const map = /* @__PURE__ */ new Map();
|
|
29605
|
-
for (const device of all) if (device.config["system"] === "dreo" && device.config["brokerId"] ===
|
|
29550
|
+
for (const device of all) if (device.config["system"] === "dreo" && device.config["brokerId"] === integrationId) {
|
|
29606
29551
|
const dreoId = device.config["dreoDeviceId"];
|
|
29607
29552
|
if (typeof dreoId === "string") map.set(dreoId, device.id);
|
|
29608
29553
|
}
|
|
@@ -29610,15 +29555,15 @@ async function adoptedMapForBroker(brokerId, listAdoptedDreo) {
|
|
|
29610
29555
|
}
|
|
29611
29556
|
/**
|
|
29612
29557
|
* Construct the `device-adoption` cap provider for the Dreo addon. Pure builder:
|
|
29613
|
-
* all side-effecting deps are injected.
|
|
29614
|
-
*
|
|
29558
|
+
* all side-effecting deps are injected. Single `devices` granularity (one
|
|
29559
|
+
* Container per cloud device); resolves each account by `integrationId`.
|
|
29615
29560
|
*/
|
|
29616
29561
|
function buildDreoAdoptionProvider(deps) {
|
|
29617
|
-
const { registry,
|
|
29618
|
-
async function
|
|
29562
|
+
const { registry, hasIntegration, listIntegrations, listAdoptedDreo, adoptDevice, removeDevice, findDeviceConfig, logger } = deps;
|
|
29563
|
+
async function allCandidatesForIntegration(integrationId) {
|
|
29619
29564
|
return buildDreoCandidates({
|
|
29620
|
-
devices: registry.infos(
|
|
29621
|
-
adopted: await
|
|
29565
|
+
devices: registry.infos(integrationId),
|
|
29566
|
+
adopted: await adoptedMapForIntegration(integrationId, listAdoptedDreo)
|
|
29622
29567
|
});
|
|
29623
29568
|
}
|
|
29624
29569
|
function applyCandidateTextFilter(cands, filterText) {
|
|
@@ -29636,7 +29581,7 @@ function buildDreoAdoptionProvider(deps) {
|
|
|
29636
29581
|
return {
|
|
29637
29582
|
listCandidateFilters: async () => ({ filters: [DEVICES_FILTER] }),
|
|
29638
29583
|
listCandidates: async ({ integrationId, page, pageSize, filterText }) => {
|
|
29639
|
-
const filtered = applyCandidateTextFilter(await
|
|
29584
|
+
const filtered = applyCandidateTextFilter(await allCandidatesForIntegration(integrationId), filterText);
|
|
29640
29585
|
const start = (page - 1) * pageSize;
|
|
29641
29586
|
return {
|
|
29642
29587
|
candidates: filtered.slice(start, start + pageSize),
|
|
@@ -29646,13 +29591,12 @@ function buildDreoAdoptionProvider(deps) {
|
|
|
29646
29591
|
};
|
|
29647
29592
|
},
|
|
29648
29593
|
getCandidate: async ({ integrationId, childNativeId }) => {
|
|
29649
|
-
return (await
|
|
29594
|
+
return (await allCandidatesForIntegration(integrationId)).find((c) => c.childNativeId === childNativeId) ?? null;
|
|
29650
29595
|
},
|
|
29651
29596
|
getStatus: async () => {
|
|
29652
29597
|
try {
|
|
29653
|
-
const brokers = registry.list();
|
|
29654
29598
|
let candidateCount = 0;
|
|
29655
|
-
for (const
|
|
29599
|
+
for (const integrationId of listIntegrations()) candidateCount += (await allCandidatesForIntegration(integrationId)).length;
|
|
29656
29600
|
const adoptedCount = (await listAdoptedDreo()).filter((d) => d.config["system"] === "dreo").length;
|
|
29657
29601
|
return {
|
|
29658
29602
|
lastDiscoveryAt: Date.now(),
|
|
@@ -29671,9 +29615,8 @@ function buildDreoAdoptionProvider(deps) {
|
|
|
29671
29615
|
}
|
|
29672
29616
|
},
|
|
29673
29617
|
refresh: async ({ integrationId }) => {
|
|
29674
|
-
const
|
|
29675
|
-
const
|
|
29676
|
-
const adoptedCount = (await listAdoptedDreo()).filter((d) => d.config["system"] === "dreo" && d.config["brokerId"] === brokerId).length;
|
|
29618
|
+
const candidateCount = (await allCandidatesForIntegration(integrationId)).length;
|
|
29619
|
+
const adoptedCount = (await adoptedMapForIntegration(integrationId, listAdoptedDreo)).size;
|
|
29677
29620
|
return {
|
|
29678
29621
|
lastDiscoveryAt: Date.now(),
|
|
29679
29622
|
candidateCount,
|
|
@@ -29682,17 +29625,16 @@ function buildDreoAdoptionProvider(deps) {
|
|
|
29682
29625
|
};
|
|
29683
29626
|
},
|
|
29684
29627
|
adopt: async ({ integrationId, childNativeIds, perCandidate }) => {
|
|
29685
|
-
|
|
29686
|
-
|
|
29687
|
-
const devices = registry.infos(brokerId);
|
|
29628
|
+
if (!registry.has(integrationId)) throw new Error(`dreo adopt: integration ${integrationId} not connected`);
|
|
29629
|
+
const devices = registry.infos(integrationId);
|
|
29688
29630
|
const adopted = [];
|
|
29689
29631
|
let failures = 0;
|
|
29690
29632
|
for (const dreoDeviceId of childNativeIds) try {
|
|
29691
29633
|
const dev = devices.find((d) => d.deviceId === dreoDeviceId);
|
|
29692
29634
|
if (dev === void 0) {
|
|
29693
|
-
logger.warn("dreo adopt: device not found on
|
|
29635
|
+
logger.warn("dreo adopt: device not found on integration — skipping", { meta: {
|
|
29694
29636
|
dreoDeviceId,
|
|
29695
|
-
|
|
29637
|
+
integrationId
|
|
29696
29638
|
} });
|
|
29697
29639
|
failures++;
|
|
29698
29640
|
continue;
|
|
@@ -29704,7 +29646,7 @@ function buildDreoAdoptionProvider(deps) {
|
|
|
29704
29646
|
if (candidate === void 0) {
|
|
29705
29647
|
logger.warn("dreo adopt: device model unsupported — skipping", { meta: {
|
|
29706
29648
|
dreoDeviceId,
|
|
29707
|
-
|
|
29649
|
+
integrationId,
|
|
29708
29650
|
model: dev.model
|
|
29709
29651
|
} });
|
|
29710
29652
|
failures++;
|
|
@@ -29713,7 +29655,6 @@ function buildDreoAdoptionProvider(deps) {
|
|
|
29713
29655
|
const name = perCandidate?.[dreoDeviceId]?.name ?? dev.name ?? dreoDeviceId;
|
|
29714
29656
|
const { deviceId, accessoryDeviceIds } = await adoptDevice({
|
|
29715
29657
|
dreoDeviceId,
|
|
29716
|
-
brokerId,
|
|
29717
29658
|
integrationId,
|
|
29718
29659
|
type: candidate.type,
|
|
29719
29660
|
name,
|
|
@@ -29727,7 +29668,7 @@ function buildDreoAdoptionProvider(deps) {
|
|
|
29727
29668
|
} catch (err) {
|
|
29728
29669
|
logger.warn("dreo adopt: failed to adopt device", { meta: {
|
|
29729
29670
|
dreoDeviceId,
|
|
29730
|
-
|
|
29671
|
+
integrationId,
|
|
29731
29672
|
error: errMsg(err)
|
|
29732
29673
|
} });
|
|
29733
29674
|
failures++;
|
|
@@ -29742,9 +29683,10 @@ function buildDreoAdoptionProvider(deps) {
|
|
|
29742
29683
|
const cfg = await findDeviceConfig(camDeviceId);
|
|
29743
29684
|
if (cfg === null) throw new Error(`dreo resync: device ${camDeviceId} not found`);
|
|
29744
29685
|
if (cfg["system"] !== "dreo") throw new Error(`dreo resync: device ${camDeviceId} is not a Dreo device`);
|
|
29745
|
-
const
|
|
29686
|
+
const integrationId = String(cfg["brokerId"]);
|
|
29746
29687
|
const dreoDeviceId = String(cfg["dreoDeviceId"]);
|
|
29747
|
-
if (!
|
|
29688
|
+
if (!hasIntegration(integrationId)) throw new Error(`dreo resync: integration ${integrationId} not connected`);
|
|
29689
|
+
if (!registry.infos(integrationId).some((d) => d.deviceId === dreoDeviceId)) throw new Error(`dreo resync: device ${dreoDeviceId} no longer present on integration ${integrationId}`);
|
|
29748
29690
|
return {
|
|
29749
29691
|
changed: false,
|
|
29750
29692
|
rebuiltChildren: 0
|
|
@@ -29753,29 +29695,31 @@ function buildDreoAdoptionProvider(deps) {
|
|
|
29753
29695
|
};
|
|
29754
29696
|
}
|
|
29755
29697
|
//#endregion
|
|
29756
|
-
//#region src/dreo-
|
|
29698
|
+
//#region src/dreo-integration-device-cascade.ts
|
|
29757
29699
|
/**
|
|
29758
29700
|
* Remove every adopted Dreo PARENT device (children cascade via the kernel)
|
|
29759
|
-
* whose persisted config carries `{ system: 'dreo', brokerId }`.
|
|
29760
|
-
*
|
|
29761
|
-
*
|
|
29762
|
-
*
|
|
29701
|
+
* whose persisted config carries `{ system: 'dreo', brokerId: integrationId }`.
|
|
29702
|
+
* Used when an integration is deleted/disabled. Best-effort per device; returns
|
|
29703
|
+
* the count removed.
|
|
29704
|
+
*
|
|
29705
|
+
* NOTE: the persisted config field is still named `brokerId` (unchanged
|
|
29706
|
+
* device-side schema) but under the account model it holds the `integrationId`.
|
|
29763
29707
|
*/
|
|
29764
|
-
async function
|
|
29765
|
-
const { reg, devices, addonId,
|
|
29708
|
+
async function cascadeRemoveDevicesForIntegration(input) {
|
|
29709
|
+
const { reg, devices, addonId, integrationId, logger } = input;
|
|
29766
29710
|
let removed = 0;
|
|
29767
29711
|
for (const d of reg.getAllForAddon(addonId)) {
|
|
29768
29712
|
if (d.parentDeviceId !== null) continue;
|
|
29769
29713
|
const cfg = await devices.loadConfig(d.id).catch(() => ({}));
|
|
29770
|
-
if (cfg["system"] !== "dreo" || cfg["brokerId"] !==
|
|
29714
|
+
if (cfg["system"] !== "dreo" || cfg["brokerId"] !== integrationId) continue;
|
|
29771
29715
|
try {
|
|
29772
29716
|
await devices.remove(d.id);
|
|
29773
29717
|
removed += 1;
|
|
29774
29718
|
} catch (err) {
|
|
29775
|
-
logger.warn("dreo:
|
|
29719
|
+
logger.warn("dreo: integration cascade-remove failed", {
|
|
29776
29720
|
tags: { deviceId: d.id },
|
|
29777
29721
|
meta: {
|
|
29778
|
-
|
|
29722
|
+
integrationId,
|
|
29779
29723
|
error: errMsg(err)
|
|
29780
29724
|
}
|
|
29781
29725
|
});
|
|
@@ -29784,20 +29728,21 @@ async function cascadeRemoveDevicesForBroker(input) {
|
|
|
29784
29728
|
return removed;
|
|
29785
29729
|
}
|
|
29786
29730
|
//#endregion
|
|
29787
|
-
//#region src/dreo-
|
|
29731
|
+
//#region src/dreo-integration-offline.ts
|
|
29788
29732
|
/**
|
|
29789
|
-
* Set every Dreo device belonging to `
|
|
29790
|
-
* `devices` is the addon-scoped device list; every Dreo top-level
|
|
29791
|
-
* carries `{ system: 'dreo', brokerId }` in its config
|
|
29733
|
+
* Set every Dreo device belonging to `integrationId` to the requested online
|
|
29734
|
+
* state. `devices` is the addon-scoped device list; every Dreo top-level
|
|
29735
|
+
* Container carries `{ system: 'dreo', brokerId: integrationId }` in its config
|
|
29736
|
+
* blob (the `brokerId` field name is unchanged; it now holds the integrationId).
|
|
29792
29737
|
*
|
|
29793
29738
|
* Churn-free: skips any device already in the target state. Returns the count of
|
|
29794
|
-
* devices actually transitioned.
|
|
29739
|
+
* devices actually transitioned.
|
|
29795
29740
|
*/
|
|
29796
|
-
function
|
|
29741
|
+
function setIntegrationDevicesOnline(devices, integrationId, online) {
|
|
29797
29742
|
let count = 0;
|
|
29798
29743
|
for (const dev of devices) {
|
|
29799
29744
|
if (dev.config.get("system") !== "dreo") continue;
|
|
29800
|
-
if (dev.config.get("brokerId") !==
|
|
29745
|
+
if (dev.config.get("brokerId") !== integrationId) continue;
|
|
29801
29746
|
if (dev.online === online) continue;
|
|
29802
29747
|
dev.markOnline(online);
|
|
29803
29748
|
count += 1;
|
|
@@ -30343,199 +30288,137 @@ var DreoContainerDevice = class extends BaseDevice$1 {
|
|
|
30343
30288
|
};
|
|
30344
30289
|
//#endregion
|
|
30345
30290
|
//#region src/addon.ts
|
|
30346
|
-
/** Default multi-broker config — a fresh install starts with no accounts. */
|
|
30347
|
-
var DEFAULTS = { brokers: [] };
|
|
30348
30291
|
/**
|
|
30349
|
-
* Dreo device-provider addon (multi-account).
|
|
30292
|
+
* Dreo device-provider addon — `mode: account` (multi-account, broker-less).
|
|
30350
30293
|
*
|
|
30351
|
-
* Wraps the `@apocaliss92/nodedreo` Dreo cloud client (REST + WebSocket).
|
|
30352
|
-
*
|
|
30353
|
-
*
|
|
30354
|
-
*
|
|
30355
|
-
*
|
|
30356
|
-
*
|
|
30357
|
-
*
|
|
30294
|
+
* Wraps the `@apocaliss92/nodedreo` Dreo cloud client (REST + WebSocket). Each
|
|
30295
|
+
* Dreo integration carries its own account credentials in its
|
|
30296
|
+
* `integration.settings`; the addon holds one {@link DreoIntegrationManager} per
|
|
30297
|
+
* `integrationId` in {@link DreoClientRegistry} (`Map<integrationId, manager>`).
|
|
30298
|
+
* There is NO shared broker. The live connection surface each manager owns
|
|
30299
|
+
* (discovery info + per-device handles) is published on the in-process
|
|
30300
|
+
* {@link dreoConnections} resolver KEYED BY `integrationId`.
|
|
30301
|
+
*
|
|
30302
|
+
* A `device-adoption` cap provider enumerates each account's cloud devices and
|
|
30303
|
+
* adopts them; each adopted cloud device becomes a {@link DeviceType.Container}
|
|
30304
|
+
* parent that fans out a single typed accessory child (fan / purifier / heater /
|
|
30305
|
+
* humidifier) and its entity children. The device classes resolve their live
|
|
30306
|
+
* handle by the `brokerId` config field — UNCHANGED device-side code; that field
|
|
30307
|
+
* now holds the integrationId (see the migration note in the addon docs).
|
|
30358
30308
|
*
|
|
30359
30309
|
* Placement: hub-only — the Dreo cloud is reached over outbound HTTPS + a
|
|
30360
30310
|
* persistent WebSocket from a single account session; no LAN proximity matters,
|
|
30361
|
-
* so (like Dreame) the
|
|
30311
|
+
* so (like Dreame) the connection lives on the hub.
|
|
30362
30312
|
*/
|
|
30363
30313
|
var DreoProviderAddon = class extends BaseDeviceProvider {
|
|
30364
30314
|
addonId = "provider-dreo";
|
|
30365
30315
|
providerName = "Dreo";
|
|
30366
30316
|
deviceClasses = { [DeviceType.Container]: DreoContainerDevice };
|
|
30367
|
-
|
|
30317
|
+
clients = null;
|
|
30368
30318
|
constructor() {
|
|
30369
|
-
super({
|
|
30319
|
+
super({});
|
|
30370
30320
|
}
|
|
30371
30321
|
async onInitialize() {
|
|
30372
30322
|
const regs = await super.onInitialize();
|
|
30373
|
-
this.
|
|
30374
|
-
this.
|
|
30375
|
-
this.ctx.logger.info("Dreo:
|
|
30376
|
-
this.
|
|
30323
|
+
this.clients = new DreoClientRegistry(this.ctx.logger);
|
|
30324
|
+
this.clients.setOnConnected((integrationId) => {
|
|
30325
|
+
this.ctx.logger.info("Dreo: integration connected", { meta: { integrationId } });
|
|
30326
|
+
this.setIntegrationDevicesOnline(integrationId, true);
|
|
30377
30327
|
});
|
|
30378
|
-
this.
|
|
30379
|
-
this.
|
|
30328
|
+
this.clients.setOnDisconnected((integrationId) => {
|
|
30329
|
+
this.setIntegrationDevicesOnline(integrationId, false);
|
|
30380
30330
|
});
|
|
30381
|
-
await this.
|
|
30382
|
-
this.ctx.logger.info("Dreo: provider initialised", { meta: { brokerCount: this.config.brokers.length } });
|
|
30383
|
-
await this.reconcileIntegrationsToBrokers();
|
|
30331
|
+
await this.reconcileIntegrations();
|
|
30384
30332
|
this.subscribeIntegrationLifecycle();
|
|
30385
|
-
|
|
30386
|
-
|
|
30387
|
-
|
|
30388
|
-
|
|
30389
|
-
|
|
30390
|
-
},
|
|
30391
|
-
{
|
|
30392
|
-
capability: deviceAdoptionCapability,
|
|
30393
|
-
provider: this.buildAdoptionProvider()
|
|
30394
|
-
}
|
|
30395
|
-
];
|
|
30396
|
-
}
|
|
30397
|
-
/** Reconcile the live registry against the persisted `brokers` array after a
|
|
30398
|
-
* settings write (UI save or `broker.*` cap). Mirrors the Dreame addon. */
|
|
30399
|
-
async onConfigChanged() {
|
|
30400
|
-
const reg = this.registry;
|
|
30401
|
-
if (!reg) return;
|
|
30402
|
-
const persisted = this.config.brokers;
|
|
30403
|
-
const liveIds = new Set(reg.list().map((b) => b.id));
|
|
30404
|
-
const persistedIds = new Set(persisted.map((e) => e.id));
|
|
30405
|
-
for (const liveId of liveIds) if (!persistedIds.has(liveId)) try {
|
|
30406
|
-
await reg.removeEntry(liveId);
|
|
30407
|
-
} catch (err) {
|
|
30408
|
-
this.ctx.logger.warn("Dreo onConfigChanged: removeEntry failed", { meta: {
|
|
30409
|
-
brokerId: liveId,
|
|
30410
|
-
error: errMsg(err)
|
|
30411
|
-
} });
|
|
30412
|
-
}
|
|
30413
|
-
for (const entry of persisted) {
|
|
30414
|
-
if (entry.id && liveIds.has(entry.id)) {
|
|
30415
|
-
if (sameConnection(entry.connection, reg.getConnection(entry.id))) continue;
|
|
30416
|
-
try {
|
|
30417
|
-
await reg.updateEntry(entry.id, entry.connection);
|
|
30418
|
-
} catch (err) {
|
|
30419
|
-
this.ctx.logger.warn("Dreo onConfigChanged: updateEntry failed", { meta: {
|
|
30420
|
-
brokerId: entry.id,
|
|
30421
|
-
error: errMsg(err)
|
|
30422
|
-
} });
|
|
30423
|
-
}
|
|
30424
|
-
continue;
|
|
30425
|
-
}
|
|
30426
|
-
try {
|
|
30427
|
-
const created = await reg.createEntry(entry.name, entry.connection);
|
|
30428
|
-
if (created.id !== entry.id) {
|
|
30429
|
-
const next = persisted.map((b) => b === entry ? {
|
|
30430
|
-
...b,
|
|
30431
|
-
id: created.id
|
|
30432
|
-
} : b);
|
|
30433
|
-
await this.updateGlobalSettings({ brokers: next });
|
|
30434
|
-
}
|
|
30435
|
-
} catch (err) {
|
|
30436
|
-
this.ctx.logger.warn("Dreo onConfigChanged: failed to start new broker", { meta: {
|
|
30437
|
-
brokerName: entry.name,
|
|
30438
|
-
error: errMsg(err)
|
|
30439
|
-
} });
|
|
30440
|
-
}
|
|
30441
|
-
}
|
|
30333
|
+
this.ctx.logger.info("Dreo: provider initialised", { meta: { integrationCount: this.requireClients().size() } });
|
|
30334
|
+
return [...regs, {
|
|
30335
|
+
capability: deviceAdoptionCapability,
|
|
30336
|
+
provider: this.buildAdoptionProvider()
|
|
30337
|
+
}];
|
|
30442
30338
|
}
|
|
30443
30339
|
async onShutdown() {
|
|
30444
30340
|
try {
|
|
30445
|
-
await this.
|
|
30341
|
+
await this.clients?.shutdown();
|
|
30446
30342
|
} catch (err) {
|
|
30447
30343
|
this.ctx.logger.warn("Dreo: provider shutdown error", { meta: { error: errMsg(err) } });
|
|
30448
30344
|
}
|
|
30449
|
-
this.
|
|
30345
|
+
this.clients = null;
|
|
30450
30346
|
await super.onShutdown();
|
|
30451
30347
|
}
|
|
30452
|
-
|
|
30453
|
-
|
|
30348
|
+
requireClients() {
|
|
30349
|
+
if (!this.clients) throw new Error("Dreo provider not initialised");
|
|
30350
|
+
return this.clients;
|
|
30351
|
+
}
|
|
30352
|
+
/**
|
|
30353
|
+
* Stable id, integration-scoped so two accounts exposing the same cloud device
|
|
30354
|
+
* id don't collide. `brokerId` in the config is the integrationId (unchanged
|
|
30355
|
+
* field name).
|
|
30356
|
+
*/
|
|
30454
30357
|
generateStableId(_type, config) {
|
|
30455
30358
|
return `dreo:${String(config?.["brokerId"] ?? "unknown")}:${String(config?.["dreoDeviceId"] ?? Date.now())}`;
|
|
30456
30359
|
}
|
|
30457
|
-
buildBrokerProvider() {
|
|
30458
|
-
return buildDreoBrokerProvider({
|
|
30459
|
-
ownerAddonId: this.ctx.id,
|
|
30460
|
-
registry: this.requireRegistry(),
|
|
30461
|
-
getBrokers: () => this.config.brokers,
|
|
30462
|
-
persistBrokers: (brokers) => this.updateGlobalSettings({ brokers: [...brokers] }),
|
|
30463
|
-
cascadeRemoveDevices: (brokerId) => this.cascadeRemoveDevicesForBroker(brokerId),
|
|
30464
|
-
logger: this.ctx.logger
|
|
30465
|
-
});
|
|
30466
|
-
}
|
|
30467
|
-
async cascadeRemoveDevicesForBroker(brokerId) {
|
|
30468
|
-
const reg = this.ctx.kernel.deviceRegistry;
|
|
30469
|
-
const devices = this.ctx.kernel.devices;
|
|
30470
|
-
if (!reg || !devices) return;
|
|
30471
|
-
await cascadeRemoveDevicesForBroker({
|
|
30472
|
-
reg,
|
|
30473
|
-
devices,
|
|
30474
|
-
addonId: this.addonId,
|
|
30475
|
-
brokerId,
|
|
30476
|
-
logger: this.ctx.logger
|
|
30477
|
-
});
|
|
30478
|
-
}
|
|
30479
30360
|
/**
|
|
30480
|
-
*
|
|
30481
|
-
*
|
|
30482
|
-
*
|
|
30483
|
-
* was deleted
|
|
30484
|
-
*
|
|
30361
|
+
* Rebuild the `Map<integrationId, manager>` from the live integration list:
|
|
30362
|
+
* for each surviving Dreo integration read its settings and upsert a manager
|
|
30363
|
+
* (session-preserving if credentials are unchanged); drop managers whose
|
|
30364
|
+
* integration was deleted/disabled — cascade-removing their adopted devices.
|
|
30365
|
+
* Idempotent; runs on boot + on every integration lifecycle event. Guarded so
|
|
30366
|
+
* a failure never fails init.
|
|
30485
30367
|
*/
|
|
30486
|
-
async
|
|
30368
|
+
async reconcileIntegrations() {
|
|
30369
|
+
const reg = this.requireClients();
|
|
30487
30370
|
try {
|
|
30488
|
-
const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id);
|
|
30489
|
-
const surviving = new Set(
|
|
30490
|
-
for (const integration of mine) {
|
|
30491
|
-
const
|
|
30492
|
-
|
|
30493
|
-
|
|
30494
|
-
if (!this.config.brokers.some((b) => b.id === brokerId)) {
|
|
30495
|
-
this.ctx.logger.warn("Dreo integration→broker: linked broker not found", { meta: {
|
|
30496
|
-
integrationId: integration.id,
|
|
30497
|
-
brokerId
|
|
30498
|
-
} });
|
|
30371
|
+
const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id && i.enabled);
|
|
30372
|
+
const surviving = /* @__PURE__ */ new Set();
|
|
30373
|
+
for (const integration of mine) try {
|
|
30374
|
+
const connection = connectionFromSettings(await this.ctx.api.integrations.getSettings.query({ id: integration.id }));
|
|
30375
|
+
if (!connection) {
|
|
30376
|
+
this.ctx.logger.warn("Dreo integration has no complete credentials — skipping", { meta: { integrationId: integration.id } });
|
|
30499
30377
|
continue;
|
|
30500
30378
|
}
|
|
30501
|
-
|
|
30502
|
-
|
|
30503
|
-
const toRemove = this.config.brokers.filter((b) => b.integrationId !== void 0 && !surviving.has(b.integrationId)).map((b) => b.id);
|
|
30504
|
-
if (toRemove.length === 0) return;
|
|
30505
|
-
for (const id of toRemove) try {
|
|
30506
|
-
await this.requireRegistry().removeEntry(id);
|
|
30507
|
-
await this.cascadeRemoveDevicesForBroker(id);
|
|
30379
|
+
await reg.upsert(integration.id, integration.name, connection);
|
|
30380
|
+
surviving.add(integration.id);
|
|
30508
30381
|
} catch (err) {
|
|
30509
|
-
this.ctx.logger.warn("Dreo
|
|
30510
|
-
|
|
30382
|
+
this.ctx.logger.warn("Dreo reconcile: failed to read integration settings", { meta: {
|
|
30383
|
+
integrationId: integration.id,
|
|
30511
30384
|
error: errMsg(err)
|
|
30512
30385
|
} });
|
|
30513
30386
|
}
|
|
30514
|
-
const
|
|
30515
|
-
await
|
|
30387
|
+
const vanished = reg.list().filter((id) => !surviving.has(id));
|
|
30388
|
+
await reg.retain(surviving);
|
|
30389
|
+
for (const integrationId of vanished) await this.cascadeRemoveDevicesForIntegration(integrationId);
|
|
30516
30390
|
} catch (err) {
|
|
30517
|
-
this.ctx.logger.warn("Dreo integration
|
|
30391
|
+
this.ctx.logger.warn("Dreo integration reconcile failed", { meta: { error: errMsg(err) } });
|
|
30518
30392
|
}
|
|
30519
30393
|
}
|
|
30520
30394
|
subscribeIntegrationLifecycle() {
|
|
30521
30395
|
const handler = (event) => {
|
|
30522
30396
|
const addonId = event.data["addonId"];
|
|
30523
30397
|
if (typeof addonId === "string" && addonId !== this.ctx.id) return;
|
|
30524
|
-
this.
|
|
30398
|
+
this.reconcileIntegrations();
|
|
30525
30399
|
};
|
|
30526
30400
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationEnabled }, handler);
|
|
30527
30401
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, handler);
|
|
30528
30402
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, handler);
|
|
30529
30403
|
}
|
|
30404
|
+
async cascadeRemoveDevicesForIntegration(integrationId) {
|
|
30405
|
+
const reg = this.ctx.kernel.deviceRegistry;
|
|
30406
|
+
const devices = this.ctx.kernel.devices;
|
|
30407
|
+
if (!reg || !devices) return;
|
|
30408
|
+
await cascadeRemoveDevicesForIntegration({
|
|
30409
|
+
reg,
|
|
30410
|
+
devices,
|
|
30411
|
+
addonId: this.addonId,
|
|
30412
|
+
integrationId,
|
|
30413
|
+
logger: this.ctx.logger
|
|
30414
|
+
});
|
|
30415
|
+
}
|
|
30530
30416
|
buildAdoptionProvider() {
|
|
30531
30417
|
return buildDreoAdoptionProvider({
|
|
30532
30418
|
registry: dreoConnections,
|
|
30533
30419
|
logger: this.ctx.logger,
|
|
30534
|
-
|
|
30535
|
-
|
|
30536
|
-
if (typeof brokerId !== "string") throw new Error(`integration ${id} has no linked brokerId`);
|
|
30537
|
-
return brokerId;
|
|
30538
|
-
},
|
|
30420
|
+
hasIntegration: (integrationId) => this.requireClients().has(integrationId),
|
|
30421
|
+
listIntegrations: () => this.requireClients().list(),
|
|
30539
30422
|
listAdoptedDreo: async () => {
|
|
30540
30423
|
const reg = this.ctx.kernel.deviceRegistry;
|
|
30541
30424
|
const devices = this.ctx.kernel.devices;
|
|
@@ -30551,12 +30434,12 @@ var DreoProviderAddon = class extends BaseDeviceProvider {
|
|
|
30551
30434
|
}
|
|
30552
30435
|
return out;
|
|
30553
30436
|
},
|
|
30554
|
-
adoptDevice: async ({ dreoDeviceId,
|
|
30437
|
+
adoptDevice: async ({ dreoDeviceId, integrationId, name, model }) => {
|
|
30555
30438
|
const devices = this.ctx.kernel.devices;
|
|
30556
30439
|
if (!devices) throw new Error("dreo adopt: kernel.devices unavailable");
|
|
30557
30440
|
const config = {
|
|
30558
30441
|
dreoDeviceId,
|
|
30559
|
-
brokerId,
|
|
30442
|
+
brokerId: integrationId,
|
|
30560
30443
|
model,
|
|
30561
30444
|
system: "dreo",
|
|
30562
30445
|
integrationId,
|
|
@@ -30585,18 +30468,7 @@ var DreoProviderAddon = class extends BaseDeviceProvider {
|
|
|
30585
30468
|
});
|
|
30586
30469
|
}
|
|
30587
30470
|
globalSettingsSchema() {
|
|
30588
|
-
return
|
|
30589
|
-
id: "dreo-broker",
|
|
30590
|
-
title: "Dreo account",
|
|
30591
|
-
description: "Dreo accounts are managed in the External systems → Brokers tab.",
|
|
30592
|
-
columns: 1,
|
|
30593
|
-
fields: [{
|
|
30594
|
-
type: "info",
|
|
30595
|
-
key: "brokerHelp",
|
|
30596
|
-
label: "Accounts are managed separately",
|
|
30597
|
-
content: "This integration links to a Dreo 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."
|
|
30598
|
-
}]
|
|
30599
|
-
}] });
|
|
30471
|
+
return buildConnectionFormSchema();
|
|
30600
30472
|
}
|
|
30601
30473
|
async supportsManualCreation() {
|
|
30602
30474
|
return false;
|
|
@@ -30607,19 +30479,15 @@ var DreoProviderAddon = class extends BaseDeviceProvider {
|
|
|
30607
30479
|
async onCreateDevice(_type, _config) {
|
|
30608
30480
|
throw new Error("Dreo devices are adopted from the cloud, not created manually");
|
|
30609
30481
|
}
|
|
30610
|
-
|
|
30482
|
+
setIntegrationDevicesOnline(integrationId, online) {
|
|
30611
30483
|
const reg = this.ctx.kernel.deviceRegistry;
|
|
30612
30484
|
if (!reg) return;
|
|
30613
|
-
const n =
|
|
30614
|
-
if (n > 0) this.ctx.logger.info("Dreo:
|
|
30615
|
-
|
|
30485
|
+
const n = setIntegrationDevicesOnline(reg.getAllForAddon(this.addonId), integrationId, online);
|
|
30486
|
+
if (n > 0) this.ctx.logger.info("Dreo: integration devices " + (online ? "online" : "offline"), { meta: {
|
|
30487
|
+
integrationId,
|
|
30616
30488
|
count: n
|
|
30617
30489
|
} });
|
|
30618
30490
|
}
|
|
30619
|
-
requireRegistry() {
|
|
30620
|
-
if (!this.registry) throw new Error("Dreo provider not initialised");
|
|
30621
|
-
return this.registry;
|
|
30622
|
-
}
|
|
30623
30491
|
};
|
|
30624
30492
|
//#endregion
|
|
30625
30493
|
exports.DREO_MAX_FAN_LEVEL = DREO_MAX_FAN_LEVEL;
|
|
@@ -30630,8 +30498,8 @@ exports.buildDreoCandidates = buildDreoCandidates;
|
|
|
30630
30498
|
exports.capsForKind = capsForKind;
|
|
30631
30499
|
exports.clampPercent = clampPercent;
|
|
30632
30500
|
exports.classifyDreoModel = classifyDreoModel;
|
|
30501
|
+
exports.connectionFromSettings = connectionFromSettings;
|
|
30633
30502
|
exports.deviceTypeForKind = deviceTypeForKind;
|
|
30634
|
-
exports.dreoAddonConfigSchema = dreoAddonConfigSchema;
|
|
30635
30503
|
exports.dreoConfigSchema = dreoConfigSchema;
|
|
30636
30504
|
exports.levelToPercentage = levelToPercentage;
|
|
30637
30505
|
exports.percentageStep = percentageStep;
|