@camstack/addon-provider-gree 0.1.8 → 0.1.10
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 +964 -1175
- package/dist/addon.mjs +959 -1174
- package/dist/index.js +38 -2
- package/dist/index.mjs +34 -2
- package/package.json +2 -8
package/dist/addon.js
CHANGED
|
@@ -4645,7 +4645,7 @@ function preprocess(fn, schema) {
|
|
|
4645
4645
|
});
|
|
4646
4646
|
}
|
|
4647
4647
|
//#endregion
|
|
4648
|
-
//#region ../types/dist/sleep-
|
|
4648
|
+
//#region ../types/dist/sleep-C2M2zF7x.mjs
|
|
4649
4649
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4650
4650
|
EventCategory["SystemBoot"] = "system.boot";
|
|
4651
4651
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -6213,6 +6213,12 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
|
6213
6213
|
DeviceType["Switch"] = "switch";
|
|
6214
6214
|
DeviceType["Sensor"] = "sensor";
|
|
6215
6215
|
DeviceType["Thermostat"] = "thermostat";
|
|
6216
|
+
/** Air-conditioner / heat-pump climate device (HVAC) — shares the
|
|
6217
|
+
* `climate-control` cap surface with `Thermostat` but renders a
|
|
6218
|
+
* dedicated AC-appropriate control UI (mode chips, fan speed,
|
|
6219
|
+
* independent vertical/horizontal swing). Sources: native Gree, and
|
|
6220
|
+
* reusable by other AC integrations. */
|
|
6221
|
+
DeviceType["Climate"] = "climate";
|
|
6216
6222
|
DeviceType["Button"] = "button";
|
|
6217
6223
|
/** Generic stateless event emitter — carries a device's EXACT declared
|
|
6218
6224
|
* event vocabulary verbatim (no normalization). Installed with the
|
|
@@ -9008,7 +9014,7 @@ var climateControlCapability = {
|
|
|
9008
9014
|
scope: "device",
|
|
9009
9015
|
deviceNative: true,
|
|
9010
9016
|
mode: "singleton",
|
|
9011
|
-
deviceTypes: [DeviceType.Thermostat],
|
|
9017
|
+
deviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
|
|
9012
9018
|
methods: {
|
|
9013
9019
|
setMode: method(object({
|
|
9014
9020
|
deviceId: number().int().nonnegative(),
|
|
@@ -13607,10 +13613,30 @@ var deviceProviderCapability = {
|
|
|
13607
13613
|
type: string()
|
|
13608
13614
|
}))),
|
|
13609
13615
|
supportsDiscovery: method(object({}), boolean()),
|
|
13610
|
-
|
|
13616
|
+
/**
|
|
13617
|
+
* Run a network scan. `params` carries optional provider-specific scan
|
|
13618
|
+
* inputs (e.g. a broadcast address / subnet for cross-subnet discovery),
|
|
13619
|
+
* shaped by `getDiscoveryParamsSchema`. Omitted for the generic scan
|
|
13620
|
+
* (provider uses its local-network default).
|
|
13621
|
+
*/
|
|
13622
|
+
discoverDevices: method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
|
|
13611
13623
|
kind: "mutation",
|
|
13612
13624
|
auth: "admin"
|
|
13613
13625
|
}),
|
|
13626
|
+
/**
|
|
13627
|
+
* Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
|
|
13628
|
+
* provider accepts (e.g. Gree's broadcast address for a different subnet).
|
|
13629
|
+
* `null` when the provider takes no extra scan params — the generic
|
|
13630
|
+
* aggregated scan never renders this; the per-integration scan does.
|
|
13631
|
+
*/
|
|
13632
|
+
getDiscoveryParamsSchema: method(object({}), CreationSchemaOutputSchema),
|
|
13633
|
+
/**
|
|
13634
|
+
* The DeviceType this provider creates via manual add (Camera for
|
|
13635
|
+
* Reolink/ONVIF, Container for Gree, Hub for Ecowitt). `null` when the
|
|
13636
|
+
* provider does not support manual creation. Lets the Add-Device dialog
|
|
13637
|
+
* pick the right type instead of assuming Camera.
|
|
13638
|
+
*/
|
|
13639
|
+
getManualCreationType: method(object({}), object({ deviceType: _enum(DeviceType).nullable() })),
|
|
13614
13640
|
adoptDiscoveredDevice: method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
|
|
13615
13641
|
kind: "mutation",
|
|
13616
13642
|
auth: "admin"
|
|
@@ -13734,9 +13760,23 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
13734
13760
|
async supportsDiscovery() {
|
|
13735
13761
|
return false;
|
|
13736
13762
|
}
|
|
13737
|
-
async discoverDevices() {
|
|
13763
|
+
async discoverDevices(_input) {
|
|
13738
13764
|
return [];
|
|
13739
13765
|
}
|
|
13766
|
+
/** Extra per-scan input form (e.g. a broadcast address for another subnet).
|
|
13767
|
+
* Null = no extra params. Override in providers that support scoped scans. */
|
|
13768
|
+
async getDiscoveryParamsSchema() {
|
|
13769
|
+
return null;
|
|
13770
|
+
}
|
|
13771
|
+
/**
|
|
13772
|
+
* The DeviceType this provider creates via manual add — derived from the
|
|
13773
|
+
* `deviceClasses` map (first registered type). `null` when manual creation is
|
|
13774
|
+
* unsupported. Lets the Add-Device dialog pick the right type per provider.
|
|
13775
|
+
*/
|
|
13776
|
+
async getManualCreationType() {
|
|
13777
|
+
if (!await this.supportsManualCreation()) return { deviceType: null };
|
|
13778
|
+
return { deviceType: Object.values(DeviceType).find((t) => this.deviceClasses[t] !== void 0) ?? null };
|
|
13779
|
+
}
|
|
13740
13780
|
async adoptDiscoveredDevice(_input) {
|
|
13741
13781
|
throw new Error(`${this.providerName} provider does not support discovery-based adoption`);
|
|
13742
13782
|
}
|
|
@@ -14783,83 +14823,34 @@ var GetStateInputSchema = object({
|
|
|
14783
14823
|
* HA: entity_id (returns the cached entity state). */
|
|
14784
14824
|
key: string()
|
|
14785
14825
|
});
|
|
14786
|
-
|
|
14787
|
-
|
|
14788
|
-
|
|
14789
|
-
|
|
14790
|
-
|
|
14791
|
-
|
|
14792
|
-
|
|
14793
|
-
|
|
14794
|
-
|
|
14795
|
-
|
|
14796
|
-
|
|
14797
|
-
|
|
14798
|
-
|
|
14799
|
-
|
|
14800
|
-
|
|
14801
|
-
|
|
14802
|
-
|
|
14803
|
-
|
|
14804
|
-
|
|
14805
|
-
|
|
14806
|
-
|
|
14807
|
-
|
|
14808
|
-
|
|
14809
|
-
|
|
14810
|
-
|
|
14811
|
-
|
|
14812
|
-
|
|
14813
|
-
|
|
14814
|
-
}),
|
|
14815
|
-
/** Read the persisted settings record for a broker (kind-specific
|
|
14816
|
-
* shape). Admin-only — settings may contain secrets. Returns `null`
|
|
14817
|
-
* when the broker id is unknown to the provider (the collection
|
|
14818
|
-
* fallback may route a foreign id to the first provider). */
|
|
14819
|
-
getSettings: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
|
|
14820
|
-
/** Overwrite the persisted settings record. The kind-specific
|
|
14821
|
-
* provider validates the shape and applies the change (reconnects
|
|
14822
|
-
* if credentials changed). */
|
|
14823
|
-
setSettings: method(object({
|
|
14824
|
-
id: string(),
|
|
14825
|
-
settings: SettingsRecordSchema$1
|
|
14826
|
-
}), _void(), {
|
|
14827
|
-
kind: "mutation",
|
|
14828
|
-
auth: "admin"
|
|
14829
|
-
}),
|
|
14830
|
-
/** Returns the kind-specific connection config the consumer needs
|
|
14831
|
-
* to open its own client (MQTT pattern: `{url, username, password,
|
|
14832
|
-
* clientIdPrefix}`). HA providers MAY return the auth envelope
|
|
14833
|
-
* but typical HA consumers use `publish` / `subscribe` instead.
|
|
14834
|
-
* Returns `null` when the broker id is unknown to the provider. */
|
|
14835
|
-
getBrokerConfig: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
|
|
14836
|
-
getSettingsSchema: method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }),
|
|
14837
|
-
testSettings: method(TestSettingsInputSchema, TestSettingsResultSchema, {
|
|
14838
|
-
kind: "mutation",
|
|
14839
|
-
auth: "admin"
|
|
14840
|
-
}),
|
|
14841
|
-
publish: method(PublishInputSchema, unknown(), {
|
|
14842
|
-
kind: "mutation",
|
|
14843
|
-
auth: "admin"
|
|
14844
|
-
}),
|
|
14845
|
-
subscribe: method(SubscribeInputSchema, SubscribeResultSchema, {
|
|
14846
|
-
kind: "mutation",
|
|
14847
|
-
auth: "admin"
|
|
14848
|
-
}),
|
|
14849
|
-
unsubscribe: method(UnsubscribeInputSchema, _void(), {
|
|
14850
|
-
kind: "mutation",
|
|
14851
|
-
auth: "admin"
|
|
14852
|
-
}),
|
|
14853
|
-
/** Read the broker's cached state for a key. Returns `null` when
|
|
14854
|
-
* unknown to the broker (never published / unknown entity). */
|
|
14855
|
-
getState: method(GetStateInputSchema, unknown().nullable()),
|
|
14856
|
-
/** Status method — explicit registration with a `z.void()` input so
|
|
14857
|
-
* the codegen-generated tRPC router types its input as
|
|
14858
|
-
* `{addonId?: string, nodeId?: string}` (system-scoped collection
|
|
14859
|
-
* shape) instead of the device-scoped `{deviceId}` fallback. */
|
|
14860
|
-
getStatus: method(_void(), RegistryStatusSchema)
|
|
14861
|
-
}
|
|
14862
|
-
};
|
|
14826
|
+
method(ListInputSchema, array(BrokerInfoSchema$1)), method(GetInputSchema, BrokerInfoSchema$1.nullable()), method(_void(), array(BrokerProviderInfoSchema), { auth: "admin" }), method(AddInputSchema, AddResultSchema, {
|
|
14827
|
+
kind: "mutation",
|
|
14828
|
+
auth: "admin"
|
|
14829
|
+
}), method(RemoveInputSchema, _void(), {
|
|
14830
|
+
kind: "mutation",
|
|
14831
|
+
auth: "admin"
|
|
14832
|
+
}), method(GetInputSchema, TestConnectionResultSchema, {
|
|
14833
|
+
kind: "mutation",
|
|
14834
|
+
auth: "admin"
|
|
14835
|
+
}), method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }), method(object({
|
|
14836
|
+
id: string(),
|
|
14837
|
+
settings: SettingsRecordSchema$1
|
|
14838
|
+
}), _void(), {
|
|
14839
|
+
kind: "mutation",
|
|
14840
|
+
auth: "admin"
|
|
14841
|
+
}), method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }), method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }), method(TestSettingsInputSchema, TestSettingsResultSchema, {
|
|
14842
|
+
kind: "mutation",
|
|
14843
|
+
auth: "admin"
|
|
14844
|
+
}), method(PublishInputSchema, unknown(), {
|
|
14845
|
+
kind: "mutation",
|
|
14846
|
+
auth: "admin"
|
|
14847
|
+
}), method(SubscribeInputSchema, SubscribeResultSchema, {
|
|
14848
|
+
kind: "mutation",
|
|
14849
|
+
auth: "admin"
|
|
14850
|
+
}), method(UnsubscribeInputSchema, _void(), {
|
|
14851
|
+
kind: "mutation",
|
|
14852
|
+
auth: "admin"
|
|
14853
|
+
}), method(GetStateInputSchema, unknown().nullable()), method(_void(), RegistryStatusSchema);
|
|
14863
14854
|
DeviceType.Camera;
|
|
14864
14855
|
/**
|
|
14865
14856
|
* `custom-model-registry` — collection cap exposing operator-registered
|
|
@@ -15095,36 +15086,19 @@ var ResyncResultSchema = object({
|
|
|
15095
15086
|
* provider re-derived the device. 0/absent for a normal incremental re-sync. */
|
|
15096
15087
|
removedChildren: number().int().nonnegative().optional()
|
|
15097
15088
|
});
|
|
15098
|
-
|
|
15099
|
-
|
|
15100
|
-
|
|
15101
|
-
|
|
15102
|
-
|
|
15103
|
-
|
|
15104
|
-
|
|
15105
|
-
|
|
15106
|
-
|
|
15107
|
-
|
|
15108
|
-
|
|
15109
|
-
|
|
15110
|
-
|
|
15111
|
-
kind: "mutation",
|
|
15112
|
-
auth: "admin"
|
|
15113
|
-
}),
|
|
15114
|
-
adopt: method(AdoptInputSchema, AdoptResultSchema, {
|
|
15115
|
-
kind: "mutation",
|
|
15116
|
-
auth: "admin"
|
|
15117
|
-
}),
|
|
15118
|
-
release: method(ReleaseInputSchema, _void(), {
|
|
15119
|
-
kind: "mutation",
|
|
15120
|
-
auth: "admin"
|
|
15121
|
-
}),
|
|
15122
|
-
resync: method(ResyncInputSchema, ResyncResultSchema, {
|
|
15123
|
-
kind: "mutation",
|
|
15124
|
-
auth: "admin"
|
|
15125
|
-
})
|
|
15126
|
-
}
|
|
15127
|
-
};
|
|
15089
|
+
method(object({ integrationId: string() }), object({ filters: array(AdoptionFilterSchema) }), { auth: "admin" }), method(ListCandidatesInputSchema, ListCandidatesOutputSchema, { auth: "admin" }), method(GetCandidateInputSchema, DiscoveredChildDeviceSchema.nullable(), { auth: "admin" }), method(object({ integrationId: string() }), AdoptionStatusSchema, {
|
|
15090
|
+
kind: "mutation",
|
|
15091
|
+
auth: "admin"
|
|
15092
|
+
}), method(AdoptInputSchema, AdoptResultSchema, {
|
|
15093
|
+
kind: "mutation",
|
|
15094
|
+
auth: "admin"
|
|
15095
|
+
}), method(ReleaseInputSchema, _void(), {
|
|
15096
|
+
kind: "mutation",
|
|
15097
|
+
auth: "admin"
|
|
15098
|
+
}), method(ResyncInputSchema, ResyncResultSchema, {
|
|
15099
|
+
kind: "mutation",
|
|
15100
|
+
auth: "admin"
|
|
15101
|
+
});
|
|
15128
15102
|
/**
|
|
15129
15103
|
* `device-export` — collection cap for addons that export camstack
|
|
15130
15104
|
* devices to external ecosystems (HomeAssistant via MQTT discovery,
|
|
@@ -15647,7 +15621,10 @@ method(object({
|
|
|
15647
15621
|
}), FieldProbeResultSchema, {
|
|
15648
15622
|
kind: "mutation",
|
|
15649
15623
|
auth: "admin"
|
|
15650
|
-
}), method(
|
|
15624
|
+
}), method(object({
|
|
15625
|
+
addonId: string(),
|
|
15626
|
+
integrationId: string()
|
|
15627
|
+
}), object({ filters: array(AdoptionFilterSchema) }), { auth: "admin" }), method(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
|
|
15651
15628
|
addonId: string(),
|
|
15652
15629
|
integrationId: string()
|
|
15653
15630
|
}), AdoptionStatusSchema, {
|
|
@@ -15662,7 +15639,24 @@ method(object({
|
|
|
15662
15639
|
}), method(ResyncInputSchema, ResyncResultSchema, {
|
|
15663
15640
|
kind: "mutation",
|
|
15664
15641
|
auth: "admin"
|
|
15642
|
+
}), method(object({}), object({ providers: array(object({
|
|
15643
|
+
addonId: string(),
|
|
15644
|
+
label: string()
|
|
15645
|
+
})).readonly() }), { auth: "admin" }), method(object({}), object({ groups: array(object({
|
|
15646
|
+
addonId: string(),
|
|
15647
|
+
label: string(),
|
|
15648
|
+
candidates: array(DiscoveryCandidateSchema).readonly(),
|
|
15649
|
+
error: string().nullable()
|
|
15650
|
+
})).readonly() }), {
|
|
15651
|
+
kind: "mutation",
|
|
15652
|
+
auth: "admin"
|
|
15665
15653
|
}), method(object({
|
|
15654
|
+
addonId: string(),
|
|
15655
|
+
params: record(string(), unknown()).optional()
|
|
15656
|
+
}), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
|
|
15657
|
+
kind: "mutation",
|
|
15658
|
+
auth: "admin"
|
|
15659
|
+
}), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
|
|
15666
15660
|
deviceId: number(),
|
|
15667
15661
|
key: string(),
|
|
15668
15662
|
value: unknown()
|
|
@@ -18314,6 +18308,17 @@ var AvailableIntegrationTypeSchema = object({
|
|
|
18314
18308
|
iconUrl: string().nullable(),
|
|
18315
18309
|
color: string(),
|
|
18316
18310
|
instanceMode: string(),
|
|
18311
|
+
/**
|
|
18312
|
+
* Integration wizard `mode` (LOCKED MODEL): `standalone` (create
|
|
18313
|
+
* immediately then add devices, no config step/button), `account` (config
|
|
18314
|
+
* step), or `broker` (broker step). Derived server-side by
|
|
18315
|
+
* `getAvailableTypes` when the addon manifest omits an explicit `mode`.
|
|
18316
|
+
*/
|
|
18317
|
+
mode: _enum([
|
|
18318
|
+
"standalone",
|
|
18319
|
+
"account",
|
|
18320
|
+
"broker"
|
|
18321
|
+
]),
|
|
18317
18322
|
discoveryMode: string(),
|
|
18318
18323
|
/**
|
|
18319
18324
|
* Which integration-marker cap the addon declared, so the wizard can
|
|
@@ -20774,6 +20779,12 @@ Object.freeze({
|
|
|
20774
20779
|
addonId: null,
|
|
20775
20780
|
access: "create"
|
|
20776
20781
|
},
|
|
20782
|
+
"deviceManager.adoptionListCandidateFilters": {
|
|
20783
|
+
capName: "device-manager",
|
|
20784
|
+
capScope: "system",
|
|
20785
|
+
addonId: null,
|
|
20786
|
+
access: "view"
|
|
20787
|
+
},
|
|
20777
20788
|
"deviceManager.adoptionListCandidates": {
|
|
20778
20789
|
capName: "device-manager",
|
|
20779
20790
|
capScope: "system",
|
|
@@ -20822,12 +20833,30 @@ Object.freeze({
|
|
|
20822
20833
|
addonId: null,
|
|
20823
20834
|
access: "create"
|
|
20824
20835
|
},
|
|
20836
|
+
"deviceManager.discoverAllProviders": {
|
|
20837
|
+
capName: "device-manager",
|
|
20838
|
+
capScope: "system",
|
|
20839
|
+
addonId: null,
|
|
20840
|
+
access: "create"
|
|
20841
|
+
},
|
|
20825
20842
|
"deviceManager.discoverDevices": {
|
|
20826
20843
|
capName: "device-manager",
|
|
20827
20844
|
capScope: "system",
|
|
20828
20845
|
addonId: null,
|
|
20829
20846
|
access: "create"
|
|
20830
20847
|
},
|
|
20848
|
+
"deviceManager.discoverProvider": {
|
|
20849
|
+
capName: "device-manager",
|
|
20850
|
+
capScope: "system",
|
|
20851
|
+
addonId: null,
|
|
20852
|
+
access: "create"
|
|
20853
|
+
},
|
|
20854
|
+
"deviceManager.discoveryProviders": {
|
|
20855
|
+
capName: "device-manager",
|
|
20856
|
+
capScope: "system",
|
|
20857
|
+
addonId: null,
|
|
20858
|
+
access: "view"
|
|
20859
|
+
},
|
|
20831
20860
|
"deviceManager.enable": {
|
|
20832
20861
|
capName: "device-manager",
|
|
20833
20862
|
capScope: "system",
|
|
@@ -20978,6 +21007,18 @@ Object.freeze({
|
|
|
20978
21007
|
addonId: null,
|
|
20979
21008
|
access: "create"
|
|
20980
21009
|
},
|
|
21010
|
+
"deviceManager.providerCreationType": {
|
|
21011
|
+
capName: "device-manager",
|
|
21012
|
+
capScope: "system",
|
|
21013
|
+
addonId: null,
|
|
21014
|
+
access: "view"
|
|
21015
|
+
},
|
|
21016
|
+
"deviceManager.providerDiscoveryParamsSchema": {
|
|
21017
|
+
capName: "device-manager",
|
|
21018
|
+
capScope: "system",
|
|
21019
|
+
addonId: null,
|
|
21020
|
+
access: "view"
|
|
21021
|
+
},
|
|
20981
21022
|
"deviceManager.registerDevice": {
|
|
20982
21023
|
capName: "device-manager",
|
|
20983
21024
|
capScope: "system",
|
|
@@ -21194,6 +21235,18 @@ Object.freeze({
|
|
|
21194
21235
|
addonId: null,
|
|
21195
21236
|
access: "view"
|
|
21196
21237
|
},
|
|
21238
|
+
"deviceProvider.getDiscoveryParamsSchema": {
|
|
21239
|
+
capName: "device-provider",
|
|
21240
|
+
capScope: "system",
|
|
21241
|
+
addonId: null,
|
|
21242
|
+
access: "view"
|
|
21243
|
+
},
|
|
21244
|
+
"deviceProvider.getManualCreationType": {
|
|
21245
|
+
capName: "device-provider",
|
|
21246
|
+
capScope: "system",
|
|
21247
|
+
addonId: null,
|
|
21248
|
+
access: "view"
|
|
21249
|
+
},
|
|
21197
21250
|
"deviceProvider.getStatus": {
|
|
21198
21251
|
capName: "device-provider",
|
|
21199
21252
|
capScope: "system",
|
|
@@ -24032,77 +24085,257 @@ object({
|
|
|
24032
24085
|
schemaVersion: literal(1)
|
|
24033
24086
|
});
|
|
24034
24087
|
//#endregion
|
|
24035
|
-
//#region
|
|
24036
|
-
|
|
24037
|
-
|
|
24038
|
-
|
|
24039
|
-
|
|
24040
|
-
|
|
24041
|
-
|
|
24042
|
-
var
|
|
24043
|
-
|
|
24044
|
-
|
|
24045
|
-
|
|
24046
|
-
|
|
24047
|
-
|
|
24048
|
-
|
|
24049
|
-
|
|
24050
|
-
|
|
24051
|
-
|
|
24052
|
-
|
|
24053
|
-
|
|
24054
|
-
|
|
24055
|
-
|
|
24056
|
-
|
|
24057
|
-
|
|
24058
|
-
|
|
24059
|
-
|
|
24060
|
-
|
|
24061
|
-
|
|
24062
|
-
|
|
24063
|
-
|
|
24064
|
-
|
|
24065
|
-
|
|
24066
|
-
|
|
24067
|
-
|
|
24068
|
-
|
|
24069
|
-
|
|
24070
|
-
|
|
24071
|
-
|
|
24072
|
-
|
|
24073
|
-
|
|
24074
|
-
|
|
24075
|
-
|
|
24076
|
-
|
|
24077
|
-
|
|
24078
|
-
|
|
24079
|
-
|
|
24080
|
-
|
|
24081
|
-
|
|
24082
|
-
|
|
24083
|
-
function buildEnvelope(opts) {
|
|
24084
|
-
const env = {
|
|
24085
|
-
t: opts.t,
|
|
24086
|
-
i: opts.bindOrScan ? 1 : 0,
|
|
24087
|
-
uid: 0,
|
|
24088
|
-
cid: "app",
|
|
24089
|
-
pack: opts.pack
|
|
24088
|
+
//#region src/config.ts
|
|
24089
|
+
/**
|
|
24090
|
+
* The AES negotiation modes the wrapped `@apocaliss92/nodegree` client accepts.
|
|
24091
|
+
* Mirrors the library's `encryption` union — kept local so the addon validates
|
|
24092
|
+
* the operator-supplied value at the system boundary without importing a runtime
|
|
24093
|
+
* value the library does not export. `auto` tries V2 (GCM) then V1 (ECB).
|
|
24094
|
+
*/
|
|
24095
|
+
var GREE_ENCRYPTION_MODES = [
|
|
24096
|
+
"auto",
|
|
24097
|
+
"v1",
|
|
24098
|
+
"v2"
|
|
24099
|
+
];
|
|
24100
|
+
var GreeEncryptionSchema = _enum(GREE_ENCRYPTION_MODES);
|
|
24101
|
+
/**
|
|
24102
|
+
* Operator-supplied CONNECTION for ONE Gree air conditioner (standalone mode —
|
|
24103
|
+
* the Reolink / Ecowitt pattern). Gree is LOCAL-ONLY (a directed-broadcast bind
|
|
24104
|
+
* handshake + per-device AES control over UDP), so the connection carries no
|
|
24105
|
+
* credentials — only the AC's LAN address and the UDP tuning knobs. The
|
|
24106
|
+
* `broadcastAddr` is the directed target the bind scan is aimed at: point it at
|
|
24107
|
+
* the AC's own IP (unicast-directed) or the subnet broadcast (e.g.
|
|
24108
|
+
* `192.168.1.255`). Empty = the library's default global broadcast.
|
|
24109
|
+
*/
|
|
24110
|
+
var greeConfigSchema = object({
|
|
24111
|
+
/** The AC's LAN IP address — the directed-bind target. Required for a manual
|
|
24112
|
+
* standalone add (the operator types it). */
|
|
24113
|
+
host: string().default("").describe("Air conditioner LAN IP address"),
|
|
24114
|
+
/** Directed broadcast address for the bind scan (e.g. `192.168.1.255`). Empty =
|
|
24115
|
+
* the library's default global broadcast; when a `host` is set the scan is
|
|
24116
|
+
* aimed at it directly. */
|
|
24117
|
+
broadcastAddr: string().default("").describe("Directed broadcast address for the bind scan"),
|
|
24118
|
+
/** UDP request timeout in ms. */
|
|
24119
|
+
timeoutMs: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(500).max(3e4).default(3e3)).describe("UDP request timeout (ms)"),
|
|
24120
|
+
/** Retry count per UDP request. */
|
|
24121
|
+
retries: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(0).max(10).default(3)).describe("Retries per UDP request"),
|
|
24122
|
+
/** AES negotiation mode. */
|
|
24123
|
+
encryption: GreeEncryptionSchema.default("auto").describe("AES cipher negotiation")
|
|
24124
|
+
});
|
|
24125
|
+
/**
|
|
24126
|
+
* Build the `NodegreeOptions` the wrapped client constructor expects from the
|
|
24127
|
+
* validated connection. Pure: same config in → same options out. `host` and
|
|
24128
|
+
* `broadcastAddr` are NOT constructor options — they are passed to `discover()`
|
|
24129
|
+
* per scan — so they are intentionally omitted here.
|
|
24130
|
+
*/
|
|
24131
|
+
function toNodegreeOptions(config) {
|
|
24132
|
+
return {
|
|
24133
|
+
timeoutMs: config.timeoutMs,
|
|
24134
|
+
retries: config.retries,
|
|
24135
|
+
encryption: config.encryption
|
|
24090
24136
|
};
|
|
24091
|
-
if (opts.mac) env.tcid = opts.mac;
|
|
24092
|
-
if (opts.tag) env.tag = opts.tag;
|
|
24093
|
-
return env;
|
|
24094
24137
|
}
|
|
24095
|
-
|
|
24096
|
-
|
|
24097
|
-
|
|
24098
|
-
|
|
24099
|
-
|
|
24100
|
-
|
|
24101
|
-
|
|
24102
|
-
|
|
24103
|
-
});
|
|
24138
|
+
/**
|
|
24139
|
+
* Coerce a loose settings blob (the manual-creation form values, or a persisted
|
|
24140
|
+
* `connection` blob) through the connection schema, applying all defaults.
|
|
24141
|
+
* Throws a `ZodError` on invalid input so the caller surfaces a clear error at
|
|
24142
|
+
* the system boundary.
|
|
24143
|
+
*/
|
|
24144
|
+
function settingsToGreeConfig(settings) {
|
|
24145
|
+
return greeConfigSchema.parse(settings ?? {});
|
|
24104
24146
|
}
|
|
24105
|
-
|
|
24147
|
+
/**
|
|
24148
|
+
* Persisted config for a Gree AC {@link import('@camstack/types').DeviceType.Container}
|
|
24149
|
+
* device. The operator-supplied CONNECTION lives directly on the device (Reolink
|
|
24150
|
+
* pattern) — there is no broker registry. The device owns its own live
|
|
24151
|
+
* `@apocaliss92/nodegree` client keyed on its own `connectionKey`; its AC +
|
|
24152
|
+
* toggle accessory children resolve the bound handle via that key.
|
|
24153
|
+
*
|
|
24154
|
+
* `greeMac` is the durable device identity (resolved by the create-time bind);
|
|
24155
|
+
* `greeIp` is the last-known LAN address; `connectionKey` is the per-device
|
|
24156
|
+
* connection-resolver key (the device's own stableId — decoupled from any
|
|
24157
|
+
* broker); `connection` is the operator's UDP settings; `system`/`name` are
|
|
24158
|
+
* provenance.
|
|
24159
|
+
*/
|
|
24160
|
+
var greeAcDeviceSchema = object({
|
|
24161
|
+
/** Durable AC identity (MAC, lowercased) resolved by the create-time bind. */
|
|
24162
|
+
greeMac: string().min(1).describe("Gree AC MAC address"),
|
|
24163
|
+
/** Last-known LAN IP (re-bound on each activate). */
|
|
24164
|
+
greeIp: string().optional().describe("Last known LAN IP"),
|
|
24165
|
+
/** Per-device connection-resolver key (the device's own stableId). */
|
|
24166
|
+
connectionKey: string().min(1).describe("Per-device connection resolver key"),
|
|
24167
|
+
/** The operator-supplied UDP connection settings the device's client dials. */
|
|
24168
|
+
connection: greeConfigSchema,
|
|
24169
|
+
system: literal("gree").optional(),
|
|
24170
|
+
integrationId: string().optional(),
|
|
24171
|
+
name: string().optional()
|
|
24172
|
+
});
|
|
24173
|
+
/**
|
|
24174
|
+
* Hand-written connection form for the AC device-creation UI (standalone mode —
|
|
24175
|
+
* Reolink pattern). The connection lives on the AC DEVICE config, not in a broker
|
|
24176
|
+
* registry. A `name` field is included so the operator names the AC at creation
|
|
24177
|
+
* time (mirrors the Reolink / Ecowitt creation form).
|
|
24178
|
+
*/
|
|
24179
|
+
function buildConnectionFormSchema() {
|
|
24180
|
+
return { sections: [{
|
|
24181
|
+
id: "identity",
|
|
24182
|
+
title: "Air conditioner",
|
|
24183
|
+
description: "Gree air conditioners are controlled directly over your LAN (no cloud). Enter the AC's IP address; CamStack binds to it over UDP.",
|
|
24184
|
+
columns: 1,
|
|
24185
|
+
fields: [{
|
|
24186
|
+
type: "text",
|
|
24187
|
+
key: "name",
|
|
24188
|
+
label: "Name",
|
|
24189
|
+
required: true,
|
|
24190
|
+
placeholder: "Living room AC"
|
|
24191
|
+
}, {
|
|
24192
|
+
type: "text",
|
|
24193
|
+
key: "host",
|
|
24194
|
+
label: "IP address",
|
|
24195
|
+
required: true,
|
|
24196
|
+
placeholder: "192.168.1.50"
|
|
24197
|
+
}]
|
|
24198
|
+
}, {
|
|
24199
|
+
id: "advanced",
|
|
24200
|
+
title: "Advanced (UDP)",
|
|
24201
|
+
columns: 2,
|
|
24202
|
+
fields: [
|
|
24203
|
+
{
|
|
24204
|
+
type: "text",
|
|
24205
|
+
key: "broadcastAddr",
|
|
24206
|
+
label: "Broadcast address (optional)",
|
|
24207
|
+
required: false,
|
|
24208
|
+
placeholder: "192.168.1.255"
|
|
24209
|
+
},
|
|
24210
|
+
{
|
|
24211
|
+
type: "number",
|
|
24212
|
+
key: "timeoutMs",
|
|
24213
|
+
label: "UDP timeout (ms)",
|
|
24214
|
+
min: 500,
|
|
24215
|
+
max: 3e4,
|
|
24216
|
+
default: 3e3
|
|
24217
|
+
},
|
|
24218
|
+
{
|
|
24219
|
+
type: "number",
|
|
24220
|
+
key: "retries",
|
|
24221
|
+
label: "Retries",
|
|
24222
|
+
min: 0,
|
|
24223
|
+
max: 10,
|
|
24224
|
+
default: 3
|
|
24225
|
+
},
|
|
24226
|
+
{
|
|
24227
|
+
type: "select",
|
|
24228
|
+
key: "encryption",
|
|
24229
|
+
label: "Encryption",
|
|
24230
|
+
default: "auto",
|
|
24231
|
+
options: GREE_ENCRYPTION_MODES.map((m) => ({
|
|
24232
|
+
value: m,
|
|
24233
|
+
label: m.toUpperCase()
|
|
24234
|
+
}))
|
|
24235
|
+
}
|
|
24236
|
+
]
|
|
24237
|
+
}] };
|
|
24238
|
+
}
|
|
24239
|
+
/**
|
|
24240
|
+
* Extra params form for a PER-INTEGRATION network scan. Gree's UDP broadcast
|
|
24241
|
+
* only reaches the addon node's local subnet; to find ACs on a DIFFERENT subnet
|
|
24242
|
+
* (e.g. 192.168.20.x) the operator supplies that subnet's directed-broadcast
|
|
24243
|
+
* address (192.168.20.255). Empty = default local-subnet broadcast.
|
|
24244
|
+
*/
|
|
24245
|
+
function buildDiscoveryParamsFormSchema() {
|
|
24246
|
+
return { sections: [{
|
|
24247
|
+
id: "scan",
|
|
24248
|
+
title: "Scan options",
|
|
24249
|
+
description: "Leave empty to scan the local subnet. To find air conditioners on a different subnet, enter that subnet’s broadcast address (e.g. 192.168.20.255).",
|
|
24250
|
+
columns: 1,
|
|
24251
|
+
fields: [{
|
|
24252
|
+
type: "text",
|
|
24253
|
+
key: "broadcastAddress",
|
|
24254
|
+
label: "Broadcast address",
|
|
24255
|
+
required: false,
|
|
24256
|
+
placeholder: "192.168.20.255"
|
|
24257
|
+
}, {
|
|
24258
|
+
type: "number",
|
|
24259
|
+
key: "timeoutMs",
|
|
24260
|
+
label: "UDP timeout (ms)",
|
|
24261
|
+
min: 500,
|
|
24262
|
+
max: 3e4,
|
|
24263
|
+
default: 3e3
|
|
24264
|
+
}]
|
|
24265
|
+
}] };
|
|
24266
|
+
}
|
|
24267
|
+
//#endregion
|
|
24268
|
+
//#region ../../node_modules/@apocaliss92/nodegree/dist/index.js
|
|
24269
|
+
var GreeError = class extends Error {
|
|
24270
|
+
constructor(message) {
|
|
24271
|
+
super(message);
|
|
24272
|
+
this.name = "GreeError";
|
|
24273
|
+
}
|
|
24274
|
+
};
|
|
24275
|
+
var GreeAuthError = class extends GreeError {
|
|
24276
|
+
constructor(message) {
|
|
24277
|
+
super(message);
|
|
24278
|
+
this.name = "GreeAuthError";
|
|
24279
|
+
}
|
|
24280
|
+
};
|
|
24281
|
+
var GreeTimeoutError = class extends GreeError {
|
|
24282
|
+
constructor(message) {
|
|
24283
|
+
super(message);
|
|
24284
|
+
this.name = "GreeTimeoutError";
|
|
24285
|
+
}
|
|
24286
|
+
};
|
|
24287
|
+
var GreeTransportError = class extends GreeError {
|
|
24288
|
+
constructor(message) {
|
|
24289
|
+
super(message);
|
|
24290
|
+
this.name = "GreeTransportError";
|
|
24291
|
+
}
|
|
24292
|
+
};
|
|
24293
|
+
function createDgramSocket() {
|
|
24294
|
+
return new Promise((resolve, reject) => {
|
|
24295
|
+
const socket = (0, dgram.createSocket)({
|
|
24296
|
+
type: "udp4",
|
|
24297
|
+
reuseAddr: true
|
|
24298
|
+
});
|
|
24299
|
+
socket.once("error", reject);
|
|
24300
|
+
socket.bind(() => {
|
|
24301
|
+
socket.removeListener("error", reject);
|
|
24302
|
+
resolve({
|
|
24303
|
+
send: (data, port, address) => new Promise((res, rej) => socket.send(data, port, address, (err) => err ? rej(err) : res())),
|
|
24304
|
+
onMessage: (handler) => socket.on("message", (data, rinfo) => handler({
|
|
24305
|
+
data,
|
|
24306
|
+
address: rinfo.address,
|
|
24307
|
+
port: rinfo.port
|
|
24308
|
+
})),
|
|
24309
|
+
setBroadcast: (enabled) => socket.setBroadcast(enabled),
|
|
24310
|
+
address: () => socket.address().port,
|
|
24311
|
+
close: () => new Promise((res) => socket.close(() => res()))
|
|
24312
|
+
});
|
|
24313
|
+
});
|
|
24314
|
+
});
|
|
24315
|
+
}
|
|
24316
|
+
function buildEnvelope(opts) {
|
|
24317
|
+
const env = {
|
|
24318
|
+
t: opts.t,
|
|
24319
|
+
i: opts.bindOrScan ? 1 : 0,
|
|
24320
|
+
uid: 0,
|
|
24321
|
+
cid: "app",
|
|
24322
|
+
pack: opts.pack
|
|
24323
|
+
};
|
|
24324
|
+
if (opts.mac) env.tcid = opts.mac;
|
|
24325
|
+
if (opts.tag) env.tag = opts.tag;
|
|
24326
|
+
return env;
|
|
24327
|
+
}
|
|
24328
|
+
function packRequest(cipher, opts) {
|
|
24329
|
+
const { pack, tag } = cipher.encrypt(opts.payload);
|
|
24330
|
+
return buildEnvelope({
|
|
24331
|
+
t: opts.t,
|
|
24332
|
+
mac: opts.mac,
|
|
24333
|
+
bindOrScan: opts.bindOrScan,
|
|
24334
|
+
pack,
|
|
24335
|
+
tag
|
|
24336
|
+
});
|
|
24337
|
+
}
|
|
24338
|
+
function unpackResponse(cipher, raw) {
|
|
24106
24339
|
if (typeof raw !== "object" || raw === null) throw new GreeTransportError("malformed response");
|
|
24107
24340
|
const obj = raw;
|
|
24108
24341
|
if (typeof obj.pack !== "string") throw new GreeTransportError("response missing pack");
|
|
@@ -24713,191 +24946,108 @@ var Nodegree = class {
|
|
|
24713
24946
|
}
|
|
24714
24947
|
};
|
|
24715
24948
|
//#endregion
|
|
24716
|
-
//#region src/config.ts
|
|
24717
|
-
/**
|
|
24718
|
-
* The AES negotiation modes the wrapped `@apocaliss92/nodegree` client accepts.
|
|
24719
|
-
* Mirrors the library's `encryption` union — kept local so the addon validates
|
|
24720
|
-
* the operator-supplied value at the system boundary without importing a runtime
|
|
24721
|
-
* value the library does not export. `auto` tries V2 (GCM) then V1 (ECB).
|
|
24722
|
-
*/
|
|
24723
|
-
var GREE_ENCRYPTION_MODES = [
|
|
24724
|
-
"auto",
|
|
24725
|
-
"v1",
|
|
24726
|
-
"v2"
|
|
24727
|
-
];
|
|
24728
|
-
var GreeEncryptionSchema = _enum(GREE_ENCRYPTION_MODES);
|
|
24729
|
-
/**
|
|
24730
|
-
* Operator-supplied settings for ONE Gree "broker" (= one LAN discovery scope on
|
|
24731
|
-
* one node). Gree is LOCAL-ONLY (UDP broadcast discovery + per-device AES
|
|
24732
|
-
* control), so a broker carries no credentials — only the broadcast target and
|
|
24733
|
-
* the UDP tuning knobs. Each node running the addon owns its own socket; the
|
|
24734
|
-
* `broadcastAddr` lets the operator point discovery at the right subnet
|
|
24735
|
-
* (directed broadcast) when the node has multiple interfaces.
|
|
24736
|
-
*/
|
|
24737
|
-
var greeConfigSchema = object({
|
|
24738
|
-
/** Directed broadcast address for discovery (e.g. `192.168.1.255`). Empty =
|
|
24739
|
-
* the library's default global broadcast. */
|
|
24740
|
-
broadcastAddr: string().default("").describe("Directed broadcast address for discovery"),
|
|
24741
|
-
/** UDP request timeout in ms. */
|
|
24742
|
-
timeoutMs: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(500).max(3e4).default(3e3)).describe("UDP request timeout (ms)"),
|
|
24743
|
-
/** Retry count per UDP request. */
|
|
24744
|
-
retries: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(0).max(10).default(3)).describe("Retries per UDP request"),
|
|
24745
|
-
/** AES negotiation mode. */
|
|
24746
|
-
encryption: GreeEncryptionSchema.default("auto").describe("AES cipher negotiation")
|
|
24747
|
-
});
|
|
24748
|
-
/**
|
|
24749
|
-
* Build the `NodegreeOptions` the wrapped client constructor expects from the
|
|
24750
|
-
* validated addon config. Pure: same config in → same options out. The
|
|
24751
|
-
* `broadcastAddr` is NOT a constructor option — it is passed to `discover()` per
|
|
24752
|
-
* scan — so it is intentionally omitted here.
|
|
24753
|
-
*/
|
|
24754
|
-
function toNodegreeOptions(config) {
|
|
24755
|
-
return {
|
|
24756
|
-
timeoutMs: config.timeoutMs,
|
|
24757
|
-
retries: config.retries,
|
|
24758
|
-
encryption: config.encryption
|
|
24759
|
-
};
|
|
24760
|
-
}
|
|
24761
|
-
/** Top-level addon config — an ordered list of broker entries (default empty). */
|
|
24762
|
-
var greeAddonConfigSchema = object({ brokers: array(object({
|
|
24763
|
-
/** Stable opaque identifier — e.g. 'gree_001', 'gree_002'. */
|
|
24764
|
-
id: string().min(1),
|
|
24765
|
-
/** Human-readable label shown in the admin UI. */
|
|
24766
|
-
name: string().min(1),
|
|
24767
|
-
/** Validated discovery-scope settings. */
|
|
24768
|
-
connection: greeConfigSchema,
|
|
24769
|
-
/** FK to the spawning integration — auto-cleanup on integration delete. */
|
|
24770
|
-
integrationId: string().optional()
|
|
24771
|
-
})).default([]) });
|
|
24772
|
-
/**
|
|
24773
|
-
* Coerce a loose settings blob (from the broker `add`/`setSettings` cap) through
|
|
24774
|
-
* the connection schema, applying all defaults. Throws a `ZodError` on invalid
|
|
24775
|
-
* input so the caller surfaces a clear error at the system boundary.
|
|
24776
|
-
*/
|
|
24777
|
-
function settingsToGreeConfig(settings) {
|
|
24778
|
-
return greeConfigSchema.parse(settings ?? {});
|
|
24779
|
-
}
|
|
24780
|
-
/**
|
|
24781
|
-
* Hand-written settings form for the broker/integration creation UI. Mirrors the
|
|
24782
|
-
* Dreame / Homematic broker-settings form shape — a flat set of sections the
|
|
24783
|
-
* admin UI renders into the "Add Gree scope" modal.
|
|
24784
|
-
*/
|
|
24785
|
-
function buildConnectionFormSchema() {
|
|
24786
|
-
return { sections: [{
|
|
24787
|
-
id: "discovery",
|
|
24788
|
-
title: "Gree discovery scope",
|
|
24789
|
-
description: "Gree air conditioners are controlled directly over your LAN (no cloud). Optionally point discovery at a specific subnet broadcast address.",
|
|
24790
|
-
columns: 1,
|
|
24791
|
-
fields: [{
|
|
24792
|
-
type: "text",
|
|
24793
|
-
key: "broadcastAddr",
|
|
24794
|
-
label: "Broadcast address (optional)",
|
|
24795
|
-
required: false,
|
|
24796
|
-
placeholder: "192.168.1.255"
|
|
24797
|
-
}]
|
|
24798
|
-
}, {
|
|
24799
|
-
id: "advanced",
|
|
24800
|
-
title: "Advanced (UDP)",
|
|
24801
|
-
columns: 2,
|
|
24802
|
-
fields: [
|
|
24803
|
-
{
|
|
24804
|
-
type: "number",
|
|
24805
|
-
key: "timeoutMs",
|
|
24806
|
-
label: "UDP timeout (ms)",
|
|
24807
|
-
min: 500,
|
|
24808
|
-
max: 3e4,
|
|
24809
|
-
default: 3e3
|
|
24810
|
-
},
|
|
24811
|
-
{
|
|
24812
|
-
type: "number",
|
|
24813
|
-
key: "retries",
|
|
24814
|
-
label: "Retries",
|
|
24815
|
-
min: 0,
|
|
24816
|
-
max: 10,
|
|
24817
|
-
default: 3
|
|
24818
|
-
},
|
|
24819
|
-
{
|
|
24820
|
-
type: "select",
|
|
24821
|
-
key: "encryption",
|
|
24822
|
-
label: "Encryption",
|
|
24823
|
-
default: "auto",
|
|
24824
|
-
options: GREE_ENCRYPTION_MODES.map((m) => ({
|
|
24825
|
-
value: m,
|
|
24826
|
-
label: m.toUpperCase()
|
|
24827
|
-
}))
|
|
24828
|
-
}
|
|
24829
|
-
]
|
|
24830
|
-
}] };
|
|
24831
|
-
}
|
|
24832
|
-
//#endregion
|
|
24833
24949
|
//#region src/gree-gateway.ts
|
|
24834
24950
|
/** Lowercase a MAC for stable map keying (Gree echoes mixed-case MACs). */
|
|
24835
|
-
function macKey
|
|
24951
|
+
function macKey(mac) {
|
|
24836
24952
|
return mac.toLowerCase();
|
|
24837
24953
|
}
|
|
24838
24954
|
/**
|
|
24839
|
-
* Per-
|
|
24840
|
-
* (and the
|
|
24955
|
+
* Per-connection registry that device classes use to reach a bound Gree AC handle
|
|
24956
|
+
* (and the bind-scan rows) for a given AC device (standalone mode — the
|
|
24957
|
+
* connection lives on the AC device, not a broker).
|
|
24841
24958
|
*
|
|
24842
24959
|
* The kernel constructs device classes with only a `DeviceContext` — it cannot
|
|
24843
|
-
* thread the handle in as a constructor arg. Like the Dreame addon's
|
|
24844
|
-
*
|
|
24845
|
-
* owns the connection surface per
|
|
24846
|
-
*
|
|
24960
|
+
* thread the handle in as a constructor arg. Like the Ecowitt / Dreame addon's
|
|
24961
|
+
* facade resolver, we keep it simple and in-process: the AC device's integration
|
|
24962
|
+
* manager owns the connection surface per `connectionKey` (the device's own
|
|
24963
|
+
* stableId) and publishes it here; the AC's accessory children resolve their live
|
|
24964
|
+
* `AcDevice` handle by `(connectionKey, mac)`.
|
|
24847
24965
|
*/
|
|
24848
24966
|
var GreeConnectionResolver = class {
|
|
24849
24967
|
#surfaces = /* @__PURE__ */ new Map();
|
|
24850
|
-
/**
|
|
24851
|
-
|
|
24968
|
+
/**
|
|
24969
|
+
* Per-connection "surface (re)published" subscribers. An AC accessory child
|
|
24970
|
+
* activates from `restoreDevices` BEFORE its parent's manager finishes the
|
|
24971
|
+
* async bind, so `getDevice` is null at `onActivate` time and its
|
|
24972
|
+
* `stateChanged` listener never attaches → the climate slice stays frozen at
|
|
24973
|
+
* cold-start (`lastFetchedAt: 0`, no temperature). Children subscribe here and
|
|
24974
|
+
* (re)attach + recompute once the handle is published. A plain `Set` has no
|
|
24975
|
+
* listener cap. Mirrors the Ecowitt facade-resolver fan-out.
|
|
24976
|
+
*/
|
|
24977
|
+
#subs = /* @__PURE__ */ new Map();
|
|
24978
|
+
/** Publish or remove the connection surface for a connection key. `null` removes. */
|
|
24979
|
+
set(connectionKey, surface) {
|
|
24852
24980
|
if (surface === null) {
|
|
24853
|
-
this.#surfaces.delete(
|
|
24981
|
+
this.#surfaces.delete(connectionKey);
|
|
24854
24982
|
return;
|
|
24855
24983
|
}
|
|
24856
|
-
this.#surfaces.set(
|
|
24984
|
+
this.#surfaces.set(connectionKey, surface);
|
|
24985
|
+
const subs = this.#subs.get(connectionKey);
|
|
24986
|
+
if (subs) for (const cb of subs) try {
|
|
24987
|
+
cb();
|
|
24988
|
+
} catch {}
|
|
24989
|
+
}
|
|
24990
|
+
/** Subscribe to surface (re)publish for a connection key. Returns unsubscribe. */
|
|
24991
|
+
onSurface(connectionKey, cb) {
|
|
24992
|
+
let set = this.#subs.get(connectionKey);
|
|
24993
|
+
if (!set) {
|
|
24994
|
+
set = /* @__PURE__ */ new Set();
|
|
24995
|
+
this.#subs.set(connectionKey, set);
|
|
24996
|
+
}
|
|
24997
|
+
set.add(cb);
|
|
24998
|
+
return () => {
|
|
24999
|
+
const current = this.#subs.get(connectionKey);
|
|
25000
|
+
current?.delete(cb);
|
|
25001
|
+
if (current && current.size === 0) this.#subs.delete(connectionKey);
|
|
25002
|
+
};
|
|
24857
25003
|
}
|
|
24858
|
-
/** The
|
|
24859
|
-
discovered(
|
|
24860
|
-
return this.#surfaces.get(
|
|
25004
|
+
/** The bind-scan rows for a connection key, or empty when unknown. */
|
|
25005
|
+
discovered(connectionKey) {
|
|
25006
|
+
return this.#surfaces.get(connectionKey)?.discovered ?? [];
|
|
24861
25007
|
}
|
|
24862
|
-
/** True when the
|
|
24863
|
-
has(
|
|
24864
|
-
return this.#surfaces.has(
|
|
25008
|
+
/** True when the connection has a published surface (i.e. it is bound). */
|
|
25009
|
+
has(connectionKey) {
|
|
25010
|
+
return this.#surfaces.has(connectionKey);
|
|
24865
25011
|
}
|
|
24866
|
-
/** The active
|
|
25012
|
+
/** The active connection keys (one entry per published surface). */
|
|
24867
25013
|
list() {
|
|
24868
25014
|
return Array.from(this.#surfaces.keys()).map((id) => ({ id }));
|
|
24869
25015
|
}
|
|
24870
25016
|
/**
|
|
24871
|
-
* Resolve the bound {@link AcDevice} handle for a `(
|
|
24872
|
-
* null when the
|
|
25017
|
+
* Resolve the bound {@link AcDevice} handle for a `(connectionKey, mac)` pair,
|
|
25018
|
+
* or null when the connection is unknown or the AC has not been bound.
|
|
24873
25019
|
*/
|
|
24874
|
-
getDevice(
|
|
24875
|
-
return this.#surfaces.get(
|
|
25020
|
+
getDevice(connectionKey, mac) {
|
|
25021
|
+
return this.#surfaces.get(connectionKey)?.handles.get(macKey(mac)) ?? null;
|
|
24876
25022
|
}
|
|
24877
25023
|
/** Remove all registered surfaces (called on full shutdown). */
|
|
24878
25024
|
clear() {
|
|
24879
25025
|
this.#surfaces.clear();
|
|
25026
|
+
this.#subs.clear();
|
|
24880
25027
|
}
|
|
24881
25028
|
};
|
|
24882
|
-
/** The single in-process per-
|
|
24883
|
-
* manager and
|
|
25029
|
+
/** The single in-process per-connection resolver shared between the AC device's
|
|
25030
|
+
* manager and its accessory children. */
|
|
24884
25031
|
var greeConnections = new GreeConnectionResolver();
|
|
24885
25032
|
//#endregion
|
|
24886
25033
|
//#region src/gree-integration-manager.ts
|
|
24887
25034
|
function defaultFacade(config) {
|
|
24888
25035
|
return new Nodegree(toNodegreeOptions(config));
|
|
24889
25036
|
}
|
|
24890
|
-
var DEFAULT_POLL_INTERVAL_MS =
|
|
25037
|
+
var DEFAULT_POLL_INTERVAL_MS = 1e4;
|
|
24891
25038
|
/**
|
|
24892
|
-
* Wraps exactly one `@apocaliss92/nodegree` facade
|
|
24893
|
-
*
|
|
24894
|
-
*
|
|
24895
|
-
*
|
|
24896
|
-
*
|
|
24897
|
-
*
|
|
25039
|
+
* Wraps exactly one `@apocaliss92/nodegree` facade bound to a SINGLE Gree AC
|
|
25040
|
+
* (standalone mode — the connection lives on the AC device). {@link start} runs a
|
|
25041
|
+
* DIRECTED bind scan aimed at the configured `host` (falling back to the
|
|
25042
|
+
* `broadcastAddr` / global broadcast), matches the responder for that host,
|
|
25043
|
+
* binds (creates) its {@link AcDevice} handle, publishes the connection surface
|
|
25044
|
+
* on the shared resolver keyed by the device's `connectionKey`, and starts
|
|
25045
|
+
* polling. {@link stop} closes the facade; {@link applyConnection} does both
|
|
25046
|
+
* atomically when the operator changes the connection.
|
|
24898
25047
|
*
|
|
24899
|
-
*
|
|
24900
|
-
*
|
|
25048
|
+
* {@link bindOnce} is the create-time helper: a static one-shot directed scan
|
|
25049
|
+
* that returns the AC's durable identity (MAC/ip/name) WITHOUT holding a handle,
|
|
25050
|
+
* so `onCreateDevice` can persist the identity before the device exists.
|
|
24901
25051
|
*/
|
|
24902
25052
|
var GreeIntegrationManager = class {
|
|
24903
25053
|
#id;
|
|
@@ -24909,6 +25059,8 @@ var GreeIntegrationManager = class {
|
|
|
24909
25059
|
#surfaceSink;
|
|
24910
25060
|
#pollIntervalMs;
|
|
24911
25061
|
#makeFacade;
|
|
25062
|
+
/** When set, only the responder for this MAC is bound (standalone: one AC). */
|
|
25063
|
+
#expectMac;
|
|
24912
25064
|
#facade = null;
|
|
24913
25065
|
#handles = /* @__PURE__ */ new Map();
|
|
24914
25066
|
#status = "disconnected";
|
|
@@ -24925,6 +25077,7 @@ var GreeIntegrationManager = class {
|
|
|
24925
25077
|
this.#surfaceSink = options.surfaceSink;
|
|
24926
25078
|
this.#pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
24927
25079
|
this.#makeFacade = options.makeFacade ?? defaultFacade;
|
|
25080
|
+
this.#expectMac = options.expectMac !== void 0 ? macKey(options.expectMac) : null;
|
|
24928
25081
|
}
|
|
24929
25082
|
/** The connection settings the manager is currently configured with. */
|
|
24930
25083
|
getConnection() {
|
|
@@ -24938,6 +25091,7 @@ var GreeIntegrationManager = class {
|
|
|
24938
25091
|
kind: "gree",
|
|
24939
25092
|
status: this.#status,
|
|
24940
25093
|
info: {
|
|
25094
|
+
host: this.#connection.host,
|
|
24941
25095
|
broadcastAddr: this.#connection.broadcastAddr,
|
|
24942
25096
|
deviceCount: this.#deviceCount
|
|
24943
25097
|
},
|
|
@@ -24946,14 +25100,14 @@ var GreeIntegrationManager = class {
|
|
|
24946
25100
|
};
|
|
24947
25101
|
}
|
|
24948
25102
|
/**
|
|
24949
|
-
* Build the facade, run a
|
|
24950
|
-
*
|
|
24951
|
-
* on
|
|
24952
|
-
*
|
|
25103
|
+
* Build the facade, run a DIRECTED bind scan (aimed at `host`), bind the
|
|
25104
|
+
* responder for the expected AC, and publish the connection surface. Sets
|
|
25105
|
+
* `connected` on a successful bind. Surfaces a clear bind error on
|
|
25106
|
+
* {@link GreeAuthError}.
|
|
24953
25107
|
*/
|
|
24954
25108
|
async start() {
|
|
24955
25109
|
if (this.#facade !== null) {
|
|
24956
|
-
this.#logger.warn("GreeIntegrationManager: start() called while running — stop first", { tags: {
|
|
25110
|
+
this.#logger.warn("GreeIntegrationManager: start() called while running — stop first", { tags: { connectionKey: this.#id } });
|
|
24957
25111
|
return;
|
|
24958
25112
|
}
|
|
24959
25113
|
this.#status = "connecting";
|
|
@@ -24962,18 +25116,19 @@ var GreeIntegrationManager = class {
|
|
|
24962
25116
|
this.#facade = facade;
|
|
24963
25117
|
try {
|
|
24964
25118
|
const discovered = await facade.discover(this.#discoverOpts());
|
|
25119
|
+
const matched = this.#selectResponders(discovered);
|
|
24965
25120
|
const handles = /* @__PURE__ */ new Map();
|
|
24966
|
-
for (const dev of
|
|
25121
|
+
for (const dev of matched) try {
|
|
24967
25122
|
const ac = await facade.createAc({
|
|
24968
25123
|
ip: dev.ip,
|
|
24969
25124
|
port: dev.port,
|
|
24970
25125
|
mac: dev.mac
|
|
24971
25126
|
});
|
|
24972
25127
|
if (this.#pollIntervalMs > 0) ac.startPolling(this.#pollIntervalMs);
|
|
24973
|
-
handles.set(macKey
|
|
25128
|
+
handles.set(macKey(dev.mac), ac);
|
|
24974
25129
|
} catch (err) {
|
|
24975
25130
|
this.#logger.warn("GreeIntegrationManager: bind (createAc) failed", {
|
|
24976
|
-
tags: {
|
|
25131
|
+
tags: { connectionKey: this.#id },
|
|
24977
25132
|
meta: {
|
|
24978
25133
|
mac: dev.mac,
|
|
24979
25134
|
ip: dev.ip,
|
|
@@ -24983,22 +25138,24 @@ var GreeIntegrationManager = class {
|
|
|
24983
25138
|
}
|
|
24984
25139
|
this.#handles = handles;
|
|
24985
25140
|
const surface = {
|
|
24986
|
-
discovered,
|
|
25141
|
+
discovered: matched,
|
|
24987
25142
|
handles
|
|
24988
25143
|
};
|
|
24989
25144
|
this.#surfaceSink.set(this.#id, surface);
|
|
24990
25145
|
this.#deviceCount = handles.size;
|
|
24991
|
-
this.#status = "connected";
|
|
24992
|
-
this.#error = null;
|
|
25146
|
+
this.#status = handles.size > 0 ? "connected" : "error";
|
|
25147
|
+
this.#error = handles.size > 0 ? null : "AC not reachable — bind returned no handle";
|
|
24993
25148
|
this.#lastCheckedAt = Date.now();
|
|
24994
|
-
|
|
24995
|
-
|
|
24996
|
-
|
|
24997
|
-
|
|
24998
|
-
|
|
24999
|
-
|
|
25000
|
-
|
|
25001
|
-
|
|
25149
|
+
if (handles.size > 0) {
|
|
25150
|
+
this.#onConnected(this.#id);
|
|
25151
|
+
this.#logger.info("GreeIntegrationManager: connected", {
|
|
25152
|
+
tags: { connectionKey: this.#id },
|
|
25153
|
+
meta: {
|
|
25154
|
+
discovered: discovered.length,
|
|
25155
|
+
bound: handles.size
|
|
25156
|
+
}
|
|
25157
|
+
});
|
|
25158
|
+
} else this.#onDisconnected(this.#id);
|
|
25002
25159
|
} catch (err) {
|
|
25003
25160
|
this.#status = "error";
|
|
25004
25161
|
this.#error = err instanceof GreeAuthError ? "device bind failed — check the AC is reachable" : errMsg(err);
|
|
@@ -25006,7 +25163,7 @@ var GreeIntegrationManager = class {
|
|
|
25006
25163
|
this.#surfaceSink.set(this.#id, null);
|
|
25007
25164
|
this.#onDisconnected(this.#id);
|
|
25008
25165
|
this.#logger.warn("GreeIntegrationManager: scan failed", {
|
|
25009
|
-
tags: {
|
|
25166
|
+
tags: { connectionKey: this.#id },
|
|
25010
25167
|
meta: { error: this.#error }
|
|
25011
25168
|
});
|
|
25012
25169
|
throw err;
|
|
@@ -25026,7 +25183,7 @@ var GreeIntegrationManager = class {
|
|
|
25026
25183
|
await facade.close();
|
|
25027
25184
|
} catch (err) {
|
|
25028
25185
|
this.#logger.warn("GreeIntegrationManager: facade close failed", {
|
|
25029
|
-
tags: {
|
|
25186
|
+
tags: { connectionKey: this.#id },
|
|
25030
25187
|
meta: { error: errMsg(err) }
|
|
25031
25188
|
});
|
|
25032
25189
|
}
|
|
@@ -25038,534 +25195,108 @@ var GreeIntegrationManager = class {
|
|
|
25038
25195
|
this.#connection = conn;
|
|
25039
25196
|
await this.start();
|
|
25040
25197
|
}
|
|
25198
|
+
/** Keep only the responder(s) this manager should bind: the one matching the
|
|
25199
|
+
* expected MAC when set, else the one matching the configured host, else all. */
|
|
25200
|
+
#selectResponders(discovered) {
|
|
25201
|
+
if (this.#expectMac !== null) return discovered.filter((d) => macKey(d.mac) === this.#expectMac);
|
|
25202
|
+
const host = this.#connection.host.trim();
|
|
25203
|
+
if (host.length > 0) {
|
|
25204
|
+
const byHost = discovered.filter((d) => d.ip === host);
|
|
25205
|
+
if (byHost.length > 0) return byHost;
|
|
25206
|
+
}
|
|
25207
|
+
return [...discovered];
|
|
25208
|
+
}
|
|
25041
25209
|
#discoverOpts() {
|
|
25042
25210
|
const out = { timeoutMs: this.#connection.timeoutMs };
|
|
25043
|
-
|
|
25211
|
+
const broadcast = resolveBroadcastTarget(this.#connection);
|
|
25212
|
+
if (broadcast.length > 0) out.broadcastAddr = broadcast;
|
|
25044
25213
|
return out;
|
|
25045
25214
|
}
|
|
25046
25215
|
};
|
|
25047
|
-
/**
|
|
25048
|
-
|
|
25049
|
-
|
|
25050
|
-
|
|
25216
|
+
/**
|
|
25217
|
+
* Resolve the directed-scan target for a connection: the explicit
|
|
25218
|
+
* `broadcastAddr` when set, else the AC `host` (unicast-directed scan), else the
|
|
25219
|
+
* empty string (library default global broadcast).
|
|
25220
|
+
*/
|
|
25221
|
+
function resolveBroadcastTarget(connection) {
|
|
25222
|
+
const broadcast = connection.broadcastAddr.trim();
|
|
25223
|
+
if (broadcast.length > 0) return broadcast;
|
|
25224
|
+
return connection.host.trim();
|
|
25051
25225
|
}
|
|
25052
|
-
//#endregion
|
|
25053
|
-
//#region src/gree-broker-registry.ts
|
|
25054
25226
|
/**
|
|
25055
|
-
*
|
|
25056
|
-
*
|
|
25057
|
-
*
|
|
25058
|
-
*
|
|
25227
|
+
* Create-time one-shot directed bind: build a throwaway facade, run a directed
|
|
25228
|
+
* scan (aimed at the connection's `host`/`broadcastAddr`), bind the matching
|
|
25229
|
+
* responder to prove the AC is reachable and learn its durable identity, then
|
|
25230
|
+
* close the facade. Returns the AC's identity (MAC/ip/name/model). Throws when no
|
|
25231
|
+
* AC responds or the bind fails — surfaced to the operator in the Add modal.
|
|
25232
|
+
*
|
|
25233
|
+
* The returned identity is what {@link import('./config.js').greeAcDeviceSchema}
|
|
25234
|
+
* persists on the new device; the live per-device manager re-binds on activate.
|
|
25235
|
+
*/
|
|
25236
|
+
/**
|
|
25237
|
+
* One-shot LAN broadcast scan for Gree ACs — the network-discovery pass. Builds a
|
|
25238
|
+
* throwaway facade from default (or provided) UDP settings, runs the broadcast,
|
|
25239
|
+
* and returns every responder. No bind, no persistence. The provider's
|
|
25240
|
+
* `discoverDevices()` maps the responders to adoption candidates.
|
|
25059
25241
|
*/
|
|
25060
|
-
|
|
25061
|
-
|
|
25062
|
-
|
|
25063
|
-
|
|
25064
|
-
|
|
25065
|
-
|
|
25066
|
-
|
|
25067
|
-
|
|
25068
|
-
|
|
25069
|
-
|
|
25070
|
-
|
|
25071
|
-
|
|
25072
|
-
|
|
25073
|
-
id: opts.id,
|
|
25074
|
-
name: opts.name,
|
|
25075
|
-
connection: opts.connection,
|
|
25076
|
-
logger: opts.logger,
|
|
25077
|
-
onConnected: opts.onConnected,
|
|
25078
|
-
onDisconnected: opts.onDisconnected,
|
|
25079
|
-
surfaceSink: greeConnections
|
|
25080
|
-
}));
|
|
25242
|
+
async function discoverGreeDevices(input) {
|
|
25243
|
+
const connection = settingsToGreeConfig({
|
|
25244
|
+
...input.broadcastAddress ? { broadcastAddr: input.broadcastAddress } : {},
|
|
25245
|
+
...input.timeoutMs ? { timeoutMs: input.timeoutMs } : {}
|
|
25246
|
+
});
|
|
25247
|
+
const facade = (input.makeFacade ?? defaultFacade)(connection);
|
|
25248
|
+
try {
|
|
25249
|
+
const broadcast = resolveBroadcastTarget(connection);
|
|
25250
|
+
const opts = { timeoutMs: connection.timeoutMs };
|
|
25251
|
+
if (broadcast.length > 0) opts.broadcastAddr = broadcast;
|
|
25252
|
+
return await facade.discover(opts);
|
|
25253
|
+
} finally {
|
|
25254
|
+
await facade.close().catch(() => void 0);
|
|
25081
25255
|
}
|
|
25082
|
-
|
|
25083
|
-
|
|
25084
|
-
|
|
25085
|
-
|
|
25086
|
-
|
|
25087
|
-
|
|
25088
|
-
|
|
25089
|
-
|
|
25090
|
-
|
|
25091
|
-
|
|
25092
|
-
|
|
25093
|
-
|
|
25094
|
-
|
|
25095
|
-
|
|
25096
|
-
|
|
25097
|
-
|
|
25098
|
-
|
|
25099
|
-
await this.#managers.get(id)?.stop();
|
|
25100
|
-
} catch (err) {
|
|
25101
|
-
this.#logger.warn("GreeBrokerRegistry: shutdown stop failed", {
|
|
25102
|
-
tags: { brokerId: id },
|
|
25103
|
-
meta: { error: errMsg(err) }
|
|
25104
|
-
});
|
|
25105
|
-
}
|
|
25106
|
-
}));
|
|
25107
|
-
this.#managers.clear();
|
|
25108
|
-
this.#integrationToBroker.clear();
|
|
25109
|
-
greeConnections.clear();
|
|
25110
|
-
}
|
|
25111
|
-
setOnBrokerConnected(cb) {
|
|
25112
|
-
this.#onBrokerConnected = cb;
|
|
25113
|
-
}
|
|
25114
|
-
setOnBrokerDisconnected(cb) {
|
|
25115
|
-
this.#onBrokerDisconnected = cb;
|
|
25116
|
-
}
|
|
25117
|
-
async createEntry(name, connection, opts = {}) {
|
|
25118
|
-
const id = this.#allocateId();
|
|
25119
|
-
const entry = {
|
|
25120
|
-
id,
|
|
25121
|
-
name,
|
|
25122
|
-
connection,
|
|
25123
|
-
...opts.integrationId !== void 0 ? { integrationId: opts.integrationId } : {}
|
|
25124
|
-
};
|
|
25125
|
-
await this.#startManager(entry);
|
|
25126
|
-
if (opts.integrationId !== void 0) this.#integrationToBroker.set(opts.integrationId, id);
|
|
25127
|
-
return entry;
|
|
25128
|
-
}
|
|
25129
|
-
async removeEntry(id) {
|
|
25130
|
-
const mgr = this.#managers.get(id);
|
|
25131
|
-
if (!mgr) throw new Error(`GreeBrokerRegistry: unknown broker id "${id}"`);
|
|
25132
|
-
for (const [integrationId, brokerId] of this.#integrationToBroker.entries()) if (brokerId === id) this.#integrationToBroker.delete(integrationId);
|
|
25133
|
-
this.#managers.delete(id);
|
|
25256
|
+
}
|
|
25257
|
+
async function bindOnce(input) {
|
|
25258
|
+
const { connection, logger } = input;
|
|
25259
|
+
const facade = (input.makeFacade ?? defaultFacade)(connection);
|
|
25260
|
+
try {
|
|
25261
|
+
const broadcast = resolveBroadcastTarget(connection);
|
|
25262
|
+
const opts = { timeoutMs: connection.timeoutMs };
|
|
25263
|
+
if (broadcast.length > 0) opts.broadcastAddr = broadcast;
|
|
25264
|
+
const discovered = await facade.discover(opts);
|
|
25265
|
+
const host = connection.host.trim();
|
|
25266
|
+
const target = host.length > 0 ? discovered.find((d) => d.ip === host) ?? discovered[0] : discovered[0];
|
|
25267
|
+
if (target === void 0) throw new Error(host.length > 0 ? `no Gree AC responded at ${host}` : "no Gree AC responded to the discovery scan");
|
|
25268
|
+
const ac = await facade.createAc({
|
|
25269
|
+
ip: target.ip,
|
|
25270
|
+
port: target.port,
|
|
25271
|
+
mac: target.mac
|
|
25272
|
+
});
|
|
25134
25273
|
try {
|
|
25135
|
-
|
|
25136
|
-
} catch
|
|
25137
|
-
this.#logger.warn("GreeBrokerRegistry: removeEntry stop failed", {
|
|
25138
|
-
tags: { brokerId: id },
|
|
25139
|
-
meta: { error: errMsg(err) }
|
|
25140
|
-
});
|
|
25141
|
-
}
|
|
25142
|
-
}
|
|
25143
|
-
async updateEntry(id, connection) {
|
|
25144
|
-
const mgr = this.#managers.get(id);
|
|
25145
|
-
if (!mgr) throw new Error(`GreeBrokerRegistry: unknown broker id "${id}"`);
|
|
25146
|
-
await mgr.applyConnection(connection);
|
|
25274
|
+
ac.stopPolling();
|
|
25275
|
+
} catch {}
|
|
25147
25276
|
return {
|
|
25148
|
-
|
|
25149
|
-
|
|
25150
|
-
|
|
25277
|
+
mac: target.mac,
|
|
25278
|
+
ip: target.ip,
|
|
25279
|
+
port: target.port,
|
|
25280
|
+
name: target.name.length > 0 ? target.name : target.mac,
|
|
25281
|
+
...target.model !== void 0 ? { model: target.model } : {}
|
|
25151
25282
|
};
|
|
25152
|
-
}
|
|
25153
|
-
|
|
25154
|
-
|
|
25155
|
-
|
|
25156
|
-
|
|
25157
|
-
|
|
25158
|
-
|
|
25159
|
-
|
|
25160
|
-
}
|
|
25161
|
-
list() {
|
|
25162
|
-
return Array.from(this.#managers.values()).map((m) => m.getInfo());
|
|
25163
|
-
}
|
|
25164
|
-
get(id) {
|
|
25165
|
-
return this.#managers.get(id)?.getInfo() ?? null;
|
|
25166
|
-
}
|
|
25167
|
-
getConnection(id) {
|
|
25168
|
-
return this.#managers.get(id)?.getConnection() ?? null;
|
|
25169
|
-
}
|
|
25170
|
-
size() {
|
|
25171
|
-
return this.#managers.size;
|
|
25172
|
-
}
|
|
25173
|
-
connectedCount() {
|
|
25174
|
-
let count = 0;
|
|
25175
|
-
for (const mgr of this.#managers.values()) if (mgr.getInfo().status === "connected") count++;
|
|
25176
|
-
return count;
|
|
25177
|
-
}
|
|
25178
|
-
async #startManager(entry) {
|
|
25179
|
-
if (this.#managers.has(entry.id)) {
|
|
25180
|
-
if (entry.integrationId !== void 0) this.#integrationToBroker.set(entry.integrationId, entry.id);
|
|
25181
|
-
return;
|
|
25182
|
-
}
|
|
25183
|
-
const mgr = this.#makeManager({
|
|
25184
|
-
id: entry.id,
|
|
25185
|
-
name: entry.name,
|
|
25186
|
-
connection: entry.connection,
|
|
25187
|
-
logger: this.#logger,
|
|
25188
|
-
onConnected: (id) => this.#onBrokerConnected(id),
|
|
25189
|
-
onDisconnected: (id) => this.#onBrokerDisconnected(id)
|
|
25190
|
-
});
|
|
25191
|
-
this.#managers.set(entry.id, mgr);
|
|
25192
|
-
if (entry.integrationId !== void 0) this.#integrationToBroker.set(entry.integrationId, entry.id);
|
|
25193
|
-
await mgr.start();
|
|
25194
|
-
}
|
|
25195
|
-
#allocateId() {
|
|
25196
|
-
const id = `gree_${String(this.#nextId).padStart(3, "0")}`;
|
|
25197
|
-
this.#nextId++;
|
|
25198
|
-
return id;
|
|
25199
|
-
}
|
|
25200
|
-
#seedCounter(id) {
|
|
25201
|
-
const match = /^gree_(\d+)$/.exec(id);
|
|
25202
|
-
if (match === null || match[1] === void 0) return;
|
|
25203
|
-
const n = parseInt(match[1], 10);
|
|
25204
|
-
if (!isNaN(n)) this.#nextId = Math.max(this.#nextId, n + 1);
|
|
25205
|
-
}
|
|
25206
|
-
};
|
|
25207
|
-
//#endregion
|
|
25208
|
-
//#region src/gree-broker-provider.ts
|
|
25209
|
-
/** Kind tag registered by this provider — matches the `listProviders` entry. */
|
|
25210
|
-
var GREE_KIND = "gree";
|
|
25211
|
-
/**
|
|
25212
|
-
* Construct the `broker` cap provider for the Gree addon. Pure builder: all
|
|
25213
|
-
* side-effecting deps are injected so the returned object is unit-testable.
|
|
25214
|
-
* Mirrors `buildDreoBrokerProvider`.
|
|
25215
|
-
*
|
|
25216
|
-
* Gree carries no secret — a "broker" is just a LAN discovery scope — so the
|
|
25217
|
-
* settings getters expose the connection verbatim (no password redaction).
|
|
25218
|
-
*/
|
|
25219
|
-
function buildGreeBrokerProvider(deps) {
|
|
25220
|
-
const { ownerAddonId, registry, getBrokers, persistBrokers, cascadeRemoveDevices, logger } = deps;
|
|
25221
|
-
const owns = (id) => getBrokers().some((b) => b.id === id);
|
|
25222
|
-
return {
|
|
25223
|
-
list: async ({ kind }) => {
|
|
25224
|
-
if (kind && kind !== GREE_KIND) return [];
|
|
25225
|
-
return registry.list().map((info) => ({
|
|
25226
|
-
...info,
|
|
25227
|
-
addonId: ownerAddonId
|
|
25228
|
-
}));
|
|
25229
|
-
},
|
|
25230
|
-
get: async ({ id }) => {
|
|
25231
|
-
const info = registry.get(id);
|
|
25232
|
-
return info ? {
|
|
25233
|
-
...info,
|
|
25234
|
-
addonId: ownerAddonId
|
|
25235
|
-
} : null;
|
|
25236
|
-
},
|
|
25237
|
-
listProviders: async () => [{
|
|
25238
|
-
addonId: ownerAddonId,
|
|
25239
|
-
kinds: [{
|
|
25240
|
-
kind: GREE_KIND,
|
|
25241
|
-
label: "Gree"
|
|
25242
|
-
}]
|
|
25243
|
-
}],
|
|
25244
|
-
add: async ({ kind, name, settings }) => {
|
|
25245
|
-
if (kind !== GREE_KIND) throw new Error(`provider-gree: only kind '${GREE_KIND}' is handled here (got '${kind}')`);
|
|
25246
|
-
const entry = await registry.createEntry(name, settingsToGreeConfig(settings));
|
|
25247
|
-
await persistBrokers([...getBrokers(), entry]);
|
|
25248
|
-
return { id: entry.id };
|
|
25249
|
-
},
|
|
25250
|
-
remove: async ({ id }) => {
|
|
25251
|
-
if (!owns(id)) return;
|
|
25252
|
-
await registry.removeEntry(id);
|
|
25253
|
-
await cascadeRemoveDevices(id).catch((err) => {
|
|
25254
|
-
logger.warn("gree: broker cascade-remove threw", {
|
|
25255
|
-
tags: { brokerId: id },
|
|
25256
|
-
meta: { error: errMsg(err) }
|
|
25257
|
-
});
|
|
25258
|
-
});
|
|
25259
|
-
await persistBrokers(getBrokers().filter((b) => b.id !== id));
|
|
25260
|
-
},
|
|
25261
|
-
testConnection: async ({ id }) => {
|
|
25262
|
-
if (!owns(id)) return {
|
|
25263
|
-
ok: false,
|
|
25264
|
-
error: "unknown broker"
|
|
25265
|
-
};
|
|
25266
|
-
const info = registry.get(id);
|
|
25267
|
-
if (!info) return {
|
|
25268
|
-
ok: false,
|
|
25269
|
-
error: "unknown broker"
|
|
25270
|
-
};
|
|
25271
|
-
if (info.status === "connected") return {
|
|
25272
|
-
ok: true,
|
|
25273
|
-
latencyMs: info.lastCheckedAt ? Math.max(0, Date.now() - info.lastCheckedAt) : 0
|
|
25274
|
-
};
|
|
25275
|
-
return {
|
|
25276
|
-
ok: false,
|
|
25277
|
-
error: info.error ?? `status: ${info.status}`
|
|
25278
|
-
};
|
|
25279
|
-
},
|
|
25280
|
-
getSettings: async ({ id }) => {
|
|
25281
|
-
const entry = getBrokers().find((b) => b.id === id);
|
|
25282
|
-
if (!entry) return null;
|
|
25283
|
-
return { ...entry.connection };
|
|
25284
|
-
},
|
|
25285
|
-
setSettings: async ({ id, settings }) => {
|
|
25286
|
-
if (!owns(id)) return;
|
|
25287
|
-
if (!getBrokers().find((b) => b.id === id)) return;
|
|
25288
|
-
const parsed = settingsToGreeConfig(settings);
|
|
25289
|
-
const updated = await registry.updateEntry(id, parsed);
|
|
25290
|
-
await persistBrokers(getBrokers().map((b) => b.id === id ? updated : b));
|
|
25291
|
-
},
|
|
25292
|
-
getBrokerConfig: async ({ id }) => {
|
|
25293
|
-
const entry = getBrokers().find((b) => b.id === id);
|
|
25294
|
-
if (!entry) return null;
|
|
25295
|
-
return { ...entry.connection };
|
|
25296
|
-
},
|
|
25297
|
-
getSettingsSchema: async ({ kind }) => {
|
|
25298
|
-
if (kind !== GREE_KIND) return null;
|
|
25299
|
-
return buildConnectionFormSchema();
|
|
25300
|
-
},
|
|
25301
|
-
testSettings: async ({ kind, settings }) => {
|
|
25302
|
-
if (kind !== GREE_KIND) return {
|
|
25303
|
-
ok: false,
|
|
25304
|
-
error: `unsupported kind: ${kind}`
|
|
25305
|
-
};
|
|
25306
|
-
try {
|
|
25307
|
-
settingsToGreeConfig(settings);
|
|
25308
|
-
return { ok: true };
|
|
25309
|
-
} catch (err) {
|
|
25310
|
-
return {
|
|
25311
|
-
ok: false,
|
|
25312
|
-
error: errMsg(err)
|
|
25313
|
-
};
|
|
25314
|
-
}
|
|
25315
|
-
},
|
|
25316
|
-
publish: async () => null,
|
|
25317
|
-
subscribe: async () => ({ subscriptionId: "" }),
|
|
25318
|
-
unsubscribe: async () => void 0,
|
|
25319
|
-
getState: async () => null,
|
|
25320
|
-
getStatus: async () => ({
|
|
25321
|
-
brokerCount: registry.size(),
|
|
25322
|
-
connectedCount: registry.connectedCount()
|
|
25323
|
-
})
|
|
25324
|
-
};
|
|
25325
|
-
}
|
|
25326
|
-
//#endregion
|
|
25327
|
-
//#region src/gree-discovery.ts
|
|
25328
|
-
/** Lowercase a MAC for stable keying (Gree echoes mixed-case MACs). */
|
|
25329
|
-
function macKey$1(mac) {
|
|
25330
|
-
return mac.toLowerCase();
|
|
25331
|
-
}
|
|
25332
|
-
/**
|
|
25333
|
-
* Map Gree discovery rows to adoption candidates — one Thermostat-type candidate
|
|
25334
|
-
* per AC. Every discovered AC is a valid candidate (there is no "unsupported"
|
|
25335
|
-
* sub-kind — they are all air conditioners). Pure: no network I/O, no side
|
|
25336
|
-
* effects. Mirrors `buildDreoCandidates`.
|
|
25337
|
-
*/
|
|
25338
|
-
function buildGreeCandidates(input) {
|
|
25339
|
-
const out = [];
|
|
25340
|
-
for (const device of input.devices) {
|
|
25341
|
-
const adoptedId = input.adopted.get(macKey$1(device.mac)) ?? null;
|
|
25342
|
-
out.push({
|
|
25343
|
-
childNativeId: macKey$1(device.mac),
|
|
25344
|
-
name: device.name.length > 0 ? device.name : device.mac,
|
|
25345
|
-
type: DeviceType.Thermostat,
|
|
25346
|
-
status: "online",
|
|
25347
|
-
metadata: {
|
|
25348
|
-
serialNumber: device.mac,
|
|
25349
|
-
...device.model !== void 0 ? { model: device.model } : {}
|
|
25350
|
-
},
|
|
25351
|
-
alreadyAdopted: adoptedId !== null,
|
|
25352
|
-
adoptedDeviceId: adoptedId,
|
|
25353
|
-
capabilities: ["climate-control", "fan-control"]
|
|
25354
|
-
});
|
|
25355
|
-
}
|
|
25356
|
-
return out;
|
|
25357
|
-
}
|
|
25358
|
-
//#endregion
|
|
25359
|
-
//#region src/gree-adoption-provider.ts
|
|
25360
|
-
/** The single granularity this provider advertises: whole-device adoption. */
|
|
25361
|
-
var DEVICES_FILTER = {
|
|
25362
|
-
id: "devices",
|
|
25363
|
-
label: "Devices",
|
|
25364
|
-
isDefault: true
|
|
25365
|
-
};
|
|
25366
|
-
function macKey(mac) {
|
|
25367
|
-
return mac.toLowerCase();
|
|
25368
|
-
}
|
|
25369
|
-
/** Build a `mac → CamStack deviceId` map for a single broker. */
|
|
25370
|
-
async function adoptedMapForBroker(brokerId, listAdoptedGree) {
|
|
25371
|
-
const all = await listAdoptedGree();
|
|
25372
|
-
const map = /* @__PURE__ */ new Map();
|
|
25373
|
-
for (const device of all) if (device.config["system"] === "gree" && device.config["brokerId"] === brokerId) {
|
|
25374
|
-
const mac = device.config["greeMac"];
|
|
25375
|
-
if (typeof mac === "string") map.set(macKey(mac), device.id);
|
|
25376
|
-
}
|
|
25377
|
-
return map;
|
|
25378
|
-
}
|
|
25379
|
-
/**
|
|
25380
|
-
* Construct the `device-adoption` cap provider for the Gree addon. Pure builder:
|
|
25381
|
-
* all side-effecting deps are injected. Mirrors `buildDreoAdoptionProvider` with
|
|
25382
|
-
* a single `devices` granularity (one Container per discovered AC).
|
|
25383
|
-
*/
|
|
25384
|
-
function buildGreeAdoptionProvider(deps) {
|
|
25385
|
-
const { registry, getBrokerIdForIntegration, listAdoptedGree, adoptDevice, removeDevice, findDeviceConfig, logger } = deps;
|
|
25386
|
-
async function allCandidatesForBroker(brokerId) {
|
|
25387
|
-
return buildGreeCandidates({
|
|
25388
|
-
devices: registry.discovered(brokerId),
|
|
25389
|
-
adopted: await adoptedMapForBroker(brokerId, listAdoptedGree)
|
|
25390
|
-
});
|
|
25391
|
-
}
|
|
25392
|
-
function applyCandidateTextFilter(cands, filterText) {
|
|
25393
|
-
let filtered = [...cands];
|
|
25394
|
-
if (filterText === void 0) return filtered;
|
|
25395
|
-
const { search, adoptedOnly, unadoptedOnly } = filterText;
|
|
25396
|
-
if (search !== void 0 && search.length > 0) {
|
|
25397
|
-
const lower = search.toLowerCase();
|
|
25398
|
-
filtered = filtered.filter((c) => c.name.toLowerCase().includes(lower) || (c.metadata.model?.toLowerCase().includes(lower) ?? false) || c.childNativeId.toLowerCase().includes(lower));
|
|
25399
|
-
}
|
|
25400
|
-
if (adoptedOnly === true) filtered = filtered.filter((c) => c.alreadyAdopted);
|
|
25401
|
-
if (unadoptedOnly === true) filtered = filtered.filter((c) => !c.alreadyAdopted);
|
|
25402
|
-
return filtered;
|
|
25403
|
-
}
|
|
25404
|
-
return {
|
|
25405
|
-
listCandidateFilters: async () => ({ filters: [DEVICES_FILTER] }),
|
|
25406
|
-
listCandidates: async ({ integrationId, page, pageSize, filterText }) => {
|
|
25407
|
-
const filtered = applyCandidateTextFilter(await allCandidatesForBroker(await getBrokerIdForIntegration(integrationId)), filterText);
|
|
25408
|
-
const start = (page - 1) * pageSize;
|
|
25409
|
-
return {
|
|
25410
|
-
candidates: filtered.slice(start, start + pageSize),
|
|
25411
|
-
totalCount: filtered.length,
|
|
25412
|
-
page,
|
|
25413
|
-
pageSize
|
|
25414
|
-
};
|
|
25415
|
-
},
|
|
25416
|
-
getCandidate: async ({ integrationId, childNativeId }) => {
|
|
25417
|
-
return (await allCandidatesForBroker(await getBrokerIdForIntegration(integrationId))).find((c) => c.childNativeId === childNativeId) ?? null;
|
|
25418
|
-
},
|
|
25419
|
-
getStatus: async () => {
|
|
25420
|
-
try {
|
|
25421
|
-
const brokers = registry.list();
|
|
25422
|
-
let candidateCount = 0;
|
|
25423
|
-
for (const broker of brokers) candidateCount += (await allCandidatesForBroker(broker.id)).length;
|
|
25424
|
-
const adoptedCount = (await listAdoptedGree()).filter((d) => d.config["system"] === "gree").length;
|
|
25425
|
-
return {
|
|
25426
|
-
lastDiscoveryAt: Date.now(),
|
|
25427
|
-
candidateCount,
|
|
25428
|
-
adoptedCount,
|
|
25429
|
-
lastError: null
|
|
25430
|
-
};
|
|
25431
|
-
} catch (err) {
|
|
25432
|
-
logger.warn("gree adoption: getStatus failed", { meta: { error: errMsg(err) } });
|
|
25433
|
-
return {
|
|
25434
|
-
lastDiscoveryAt: null,
|
|
25435
|
-
candidateCount: 0,
|
|
25436
|
-
adoptedCount: 0,
|
|
25437
|
-
lastError: errMsg(err)
|
|
25438
|
-
};
|
|
25439
|
-
}
|
|
25440
|
-
},
|
|
25441
|
-
refresh: async ({ integrationId }) => {
|
|
25442
|
-
const brokerId = await getBrokerIdForIntegration(integrationId);
|
|
25443
|
-
const candidateCount = (await allCandidatesForBroker(brokerId)).length;
|
|
25444
|
-
const adoptedCount = (await listAdoptedGree()).filter((d) => d.config["system"] === "gree" && d.config["brokerId"] === brokerId).length;
|
|
25445
|
-
return {
|
|
25446
|
-
lastDiscoveryAt: Date.now(),
|
|
25447
|
-
candidateCount,
|
|
25448
|
-
adoptedCount,
|
|
25449
|
-
lastError: null
|
|
25450
|
-
};
|
|
25451
|
-
},
|
|
25452
|
-
adopt: async ({ integrationId, childNativeIds, perCandidate }) => {
|
|
25453
|
-
const brokerId = await getBrokerIdForIntegration(integrationId);
|
|
25454
|
-
if (!registry.has(brokerId)) throw new Error(`gree adopt: broker ${brokerId} not active`);
|
|
25455
|
-
const devices = registry.discovered(brokerId);
|
|
25456
|
-
const adopted = [];
|
|
25457
|
-
let failures = 0;
|
|
25458
|
-
for (const childNativeId of childNativeIds) try {
|
|
25459
|
-
const dev = devices.find((d) => macKey(d.mac) === macKey(childNativeId));
|
|
25460
|
-
if (dev === void 0) {
|
|
25461
|
-
logger.warn("gree adopt: device not found on broker — skipping", { meta: {
|
|
25462
|
-
childNativeId,
|
|
25463
|
-
brokerId
|
|
25464
|
-
} });
|
|
25465
|
-
failures++;
|
|
25466
|
-
continue;
|
|
25467
|
-
}
|
|
25468
|
-
const name = perCandidate?.[childNativeId]?.name ?? dev.name ?? dev.mac;
|
|
25469
|
-
const candidate = buildGreeCandidates({
|
|
25470
|
-
devices: [dev],
|
|
25471
|
-
adopted: /* @__PURE__ */ new Map()
|
|
25472
|
-
})[0];
|
|
25473
|
-
if (candidate === void 0) {
|
|
25474
|
-
failures++;
|
|
25475
|
-
continue;
|
|
25476
|
-
}
|
|
25477
|
-
const { deviceId, accessoryDeviceIds } = await adoptDevice({
|
|
25478
|
-
greeMac: dev.mac,
|
|
25479
|
-
greeIp: dev.ip,
|
|
25480
|
-
brokerId,
|
|
25481
|
-
integrationId,
|
|
25482
|
-
type: candidate.type,
|
|
25483
|
-
name
|
|
25484
|
-
});
|
|
25485
|
-
adopted.push({
|
|
25486
|
-
childNativeId: macKey(childNativeId),
|
|
25487
|
-
parentDeviceId: deviceId,
|
|
25488
|
-
accessoryDeviceIds
|
|
25489
|
-
});
|
|
25490
|
-
} catch (err) {
|
|
25491
|
-
logger.warn("gree adopt: failed to adopt device", { meta: {
|
|
25492
|
-
childNativeId,
|
|
25493
|
-
brokerId,
|
|
25494
|
-
error: errMsg(err)
|
|
25495
|
-
} });
|
|
25496
|
-
failures++;
|
|
25497
|
-
}
|
|
25498
|
-
if (adopted.length === 0 && failures > 0) throw new Error(`gree adopt: all ${failures} adopt(s) failed`);
|
|
25499
|
-
return { adopted };
|
|
25500
|
-
},
|
|
25501
|
-
release: async ({ camDeviceId }) => {
|
|
25502
|
-
await removeDevice(camDeviceId);
|
|
25503
|
-
},
|
|
25504
|
-
resync: async ({ camDeviceId }) => {
|
|
25505
|
-
const cfg = await findDeviceConfig(camDeviceId);
|
|
25506
|
-
if (cfg === null) throw new Error(`gree resync: device ${camDeviceId} not found`);
|
|
25507
|
-
if (cfg["system"] !== "gree") throw new Error(`gree resync: device ${camDeviceId} is not a Gree device`);
|
|
25508
|
-
const brokerId = String(cfg["brokerId"]);
|
|
25509
|
-
const greeMac = String(cfg["greeMac"]);
|
|
25510
|
-
if (!registry.discovered(brokerId).some((d) => macKey(d.mac) === macKey(greeMac))) throw new Error(`gree resync: device ${greeMac} no longer present on broker ${brokerId}`);
|
|
25511
|
-
return {
|
|
25512
|
-
changed: false,
|
|
25513
|
-
rebuiltChildren: 0
|
|
25514
|
-
};
|
|
25515
|
-
}
|
|
25516
|
-
};
|
|
25517
|
-
}
|
|
25518
|
-
//#endregion
|
|
25519
|
-
//#region src/gree-broker-device-cascade.ts
|
|
25520
|
-
/**
|
|
25521
|
-
* Remove every adopted Gree PARENT device (children cascade via the kernel)
|
|
25522
|
-
* whose persisted config carries `{ system: 'gree', brokerId }`. Used when a
|
|
25523
|
-
* broker is removed (broker.remove) and when its spawning integration is
|
|
25524
|
-
* deleted. Best-effort per device; returns the count removed. Mirrors the
|
|
25525
|
-
* Dreo cascade helper.
|
|
25526
|
-
*/
|
|
25527
|
-
async function cascadeRemoveDevicesForBroker(input) {
|
|
25528
|
-
const { reg, devices, addonId, brokerId, logger } = input;
|
|
25529
|
-
let removed = 0;
|
|
25530
|
-
for (const d of reg.getAllForAddon(addonId)) {
|
|
25531
|
-
if (d.parentDeviceId !== null) continue;
|
|
25532
|
-
const cfg = await devices.loadConfig(d.id).catch(() => ({}));
|
|
25533
|
-
if (cfg["system"] !== "gree" || cfg["brokerId"] !== brokerId) continue;
|
|
25283
|
+
} catch (err) {
|
|
25284
|
+
const message = err instanceof GreeAuthError ? "device bind failed — check the AC is reachable and the encryption mode" : errMsg(err);
|
|
25285
|
+
logger.warn("gree bindOnce failed", { meta: {
|
|
25286
|
+
host: connection.host,
|
|
25287
|
+
error: message
|
|
25288
|
+
} });
|
|
25289
|
+
throw new Error(message);
|
|
25290
|
+
} finally {
|
|
25534
25291
|
try {
|
|
25535
|
-
await
|
|
25536
|
-
|
|
25537
|
-
} catch (err) {
|
|
25538
|
-
logger.warn("gree: broker cascade-remove failed", {
|
|
25539
|
-
tags: { deviceId: d.id },
|
|
25540
|
-
meta: {
|
|
25541
|
-
brokerId,
|
|
25542
|
-
error: errMsg(err)
|
|
25543
|
-
}
|
|
25544
|
-
});
|
|
25545
|
-
}
|
|
25292
|
+
await facade.close();
|
|
25293
|
+
} catch {}
|
|
25546
25294
|
}
|
|
25547
|
-
return removed;
|
|
25548
25295
|
}
|
|
25549
|
-
|
|
25550
|
-
|
|
25551
|
-
|
|
25552
|
-
|
|
25553
|
-
* `devices` is the addon-scoped device list; every Gree top-level Container
|
|
25554
|
-
* carries `{ system: 'gree', brokerId }` in its config blob.
|
|
25555
|
-
*
|
|
25556
|
-
* Churn-free: skips any device already in the target state. Returns the count of
|
|
25557
|
-
* devices actually transitioned. Mirrors the Dreo offline helper.
|
|
25558
|
-
*/
|
|
25559
|
-
function setBrokerDevicesOnline(devices, brokerId, online) {
|
|
25560
|
-
let count = 0;
|
|
25561
|
-
for (const dev of devices) {
|
|
25562
|
-
if (dev.config.get("system") !== "gree") continue;
|
|
25563
|
-
if (dev.config.get("brokerId") !== brokerId) continue;
|
|
25564
|
-
if (dev.online === online) continue;
|
|
25565
|
-
dev.markOnline(online);
|
|
25566
|
-
count += 1;
|
|
25567
|
-
}
|
|
25568
|
-
return count;
|
|
25296
|
+
/** True when two configs are the "same connection" (no facade rebuild needed). */
|
|
25297
|
+
function sameConnection(a, b) {
|
|
25298
|
+
if (a === null || b === null) return false;
|
|
25299
|
+
return a.host === b.host && a.broadcastAddr === b.broadcastAddr && a.timeoutMs === b.timeoutMs && a.retries === b.retries && a.encryption === b.encryption;
|
|
25569
25300
|
}
|
|
25570
25301
|
//#endregion
|
|
25571
25302
|
//#region src/gree-domain-mapping.ts
|
|
@@ -25813,14 +25544,27 @@ var FAN_COLD_START = {
|
|
|
25813
25544
|
};
|
|
25814
25545
|
/**
|
|
25815
25546
|
* Persisted config every Gree AC accessory child carries. `greeMac` resolves the
|
|
25816
|
-
* bound handle from the connection resolver; `
|
|
25817
|
-
*
|
|
25547
|
+
* bound handle from the connection resolver; `connectionKey` selects the parent
|
|
25548
|
+
* AC device's live connection (its own stableId); `system`/`integrationId` are
|
|
25549
|
+
* provenance.
|
|
25818
25550
|
*/
|
|
25819
25551
|
var greeAcSchema = object({
|
|
25820
25552
|
greeMac: string().min(1).describe("Gree AC MAC address"),
|
|
25821
|
-
|
|
25553
|
+
connectionKey: string().min(1).describe("Per-device connection resolver key"),
|
|
25822
25554
|
system: literal("gree").optional(),
|
|
25823
|
-
integrationId: string().optional()
|
|
25555
|
+
integrationId: string().optional(),
|
|
25556
|
+
/** Operator filter — HVAC modes exposed in the control UI (subset of the
|
|
25557
|
+
* model's supported modes). Empty / omitted ⇒ expose all. `off` is always
|
|
25558
|
+
* exposed (power). */
|
|
25559
|
+
enabledModes: array(string()).optional(),
|
|
25560
|
+
/** Operator filter — fan speeds exposed in the control UI (subset of
|
|
25561
|
+
* `GREE_FAN_MODES`). Empty / omitted ⇒ expose all. */
|
|
25562
|
+
enabledFanModes: array(string()).optional(),
|
|
25563
|
+
/** Operator filter — expose the vertical-swing toggle. Default true. */
|
|
25564
|
+
exposeVerticalSwing: boolean().optional(),
|
|
25565
|
+
/** Operator filter — expose the horizontal-swing toggle (model-permitting).
|
|
25566
|
+
* Default true. */
|
|
25567
|
+
exposeHorizontalSwing: boolean().optional()
|
|
25824
25568
|
});
|
|
25825
25569
|
/**
|
|
25826
25570
|
* One Gree air conditioner as a CamStack `Thermostat`-type accessory child.
|
|
@@ -25860,22 +25604,21 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
25860
25604
|
*/
|
|
25861
25605
|
get features() {
|
|
25862
25606
|
const caps = this.resolveAc()?.capabilities ?? getAcCapabilities();
|
|
25863
|
-
const
|
|
25864
|
-
|
|
25865
|
-
|
|
25866
|
-
|
|
25867
|
-
];
|
|
25868
|
-
if (caps.horizontalSwing) flags.push(DeviceFeature.ClimateSwingHorizontal);
|
|
25607
|
+
const cfg = this.config.values;
|
|
25608
|
+
const flags = [DeviceFeature.ClimateFanMode, DeviceFeature.ClimatePreset];
|
|
25609
|
+
if (cfg.exposeVerticalSwing ?? true) flags.push(DeviceFeature.ClimateSwingVertical);
|
|
25610
|
+
if (caps.horizontalSwing && (cfg.exposeHorizontalSwing ?? true)) flags.push(DeviceFeature.ClimateSwingHorizontal);
|
|
25869
25611
|
return flags;
|
|
25870
25612
|
}
|
|
25871
25613
|
greeMac;
|
|
25872
|
-
|
|
25614
|
+
connectionKey;
|
|
25873
25615
|
stateChangedUnsub = null;
|
|
25616
|
+
surfaceUnsub = null;
|
|
25874
25617
|
constructor(ctx) {
|
|
25875
25618
|
const persisted = greeAcSchema.parse(ctx.persistedConfig ?? {});
|
|
25876
25619
|
super(ctx, greeAcSchema, { type: ctx.deviceMeta.type });
|
|
25877
25620
|
this.greeMac = persisted.greeMac;
|
|
25878
|
-
this.
|
|
25621
|
+
this.connectionKey = persisted.connectionKey;
|
|
25879
25622
|
this.online = true;
|
|
25880
25623
|
this.updateSourceInfo({
|
|
25881
25624
|
id: this.greeMac,
|
|
@@ -25883,7 +25626,7 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
25883
25626
|
});
|
|
25884
25627
|
}
|
|
25885
25628
|
resolveAc() {
|
|
25886
|
-
return greeConnections.getDevice(this.
|
|
25629
|
+
return greeConnections.getDevice(this.connectionKey, this.greeMac);
|
|
25887
25630
|
}
|
|
25888
25631
|
requireAc() {
|
|
25889
25632
|
const ac = this.resolveAc();
|
|
@@ -25895,8 +25638,21 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
25895
25638
|
this.registerCaps();
|
|
25896
25639
|
this.attachStateListener();
|
|
25897
25640
|
this.recomputeSlices();
|
|
25641
|
+
this.surfaceUnsub = greeConnections.onSurface(this.connectionKey, () => {
|
|
25642
|
+
this.attachStateListener();
|
|
25643
|
+
this.recomputeSlices();
|
|
25644
|
+
});
|
|
25898
25645
|
}
|
|
25899
25646
|
async removeDevice() {
|
|
25647
|
+
this.detachStateListener();
|
|
25648
|
+
if (this.surfaceUnsub) {
|
|
25649
|
+
try {
|
|
25650
|
+
this.surfaceUnsub();
|
|
25651
|
+
} catch {}
|
|
25652
|
+
this.surfaceUnsub = null;
|
|
25653
|
+
}
|
|
25654
|
+
}
|
|
25655
|
+
detachStateListener() {
|
|
25900
25656
|
if (this.stateChangedUnsub) {
|
|
25901
25657
|
try {
|
|
25902
25658
|
this.stateChangedUnsub();
|
|
@@ -25904,12 +25660,16 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
25904
25660
|
this.stateChangedUnsub = null;
|
|
25905
25661
|
}
|
|
25906
25662
|
}
|
|
25663
|
+
/** Attach the live `stateChanged` listener to the bound AC handle. Idempotent:
|
|
25664
|
+
* drops any prior listener first, so it is safe to call on every surface
|
|
25665
|
+
* (re)publish. No-op until the handle is actually bound. */
|
|
25907
25666
|
attachStateListener() {
|
|
25908
25667
|
const ac = this.resolveAc();
|
|
25909
25668
|
if (!ac) {
|
|
25910
25669
|
this.ctx.logger.debug("GreeAcDevice: handle not present; no live listener yet", { meta: { greeMac: this.greeMac } });
|
|
25911
25670
|
return;
|
|
25912
25671
|
}
|
|
25672
|
+
this.detachStateListener();
|
|
25913
25673
|
const onState = () => {
|
|
25914
25674
|
try {
|
|
25915
25675
|
this.recomputeSlices();
|
|
@@ -25927,7 +25687,7 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
25927
25687
|
}
|
|
25928
25688
|
registerCaps() {
|
|
25929
25689
|
this.ctx.registerNativeCap(climateControlCapability, {
|
|
25930
|
-
getStatus: async () => this.runtimeState.getCapState(CLIMATE_CAP) ?? CLIMATE_COLD_START,
|
|
25690
|
+
getStatus: async () => this.runtimeState.getCapState(CLIMATE_CAP) ?? this.applyClimateFilters(CLIMATE_COLD_START),
|
|
25931
25691
|
setMode: async ({ mode }) => {
|
|
25932
25692
|
const ac = this.requireAc();
|
|
25933
25693
|
const libMode = capModeToLibMode(mode);
|
|
@@ -25974,7 +25734,7 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
25974
25734
|
await ac.setSwingHorizontal(boolToHorizontalSwing(on));
|
|
25975
25735
|
}
|
|
25976
25736
|
});
|
|
25977
|
-
this.runtimeState.setCapState(CLIMATE_CAP, CLIMATE_COLD_START);
|
|
25737
|
+
this.runtimeState.setCapState(CLIMATE_CAP, this.applyClimateFilters(CLIMATE_COLD_START));
|
|
25978
25738
|
this.ctx.registerNativeCap(fanControlCapability, {
|
|
25979
25739
|
getStatus: async () => this.runtimeState.getCapState(FAN_CAP) ?? FAN_COLD_START,
|
|
25980
25740
|
setPercentage: async ({ percentage }) => {
|
|
@@ -26021,7 +25781,7 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
26021
25781
|
swingHorizontal: ac.capabilities.horizontalSwing ? horizontalSwingToBool(ac.swingHorizontal) : null,
|
|
26022
25782
|
lastFetchedAt: now
|
|
26023
25783
|
};
|
|
26024
|
-
this.runtimeState.setCapState(CLIMATE_CAP, climateSlice);
|
|
25784
|
+
this.runtimeState.setCapState(CLIMATE_CAP, this.applyClimateFilters(climateSlice));
|
|
26025
25785
|
const fanSlice = {
|
|
26026
25786
|
percentage: fanSpeedToPercentage(ac.fanSpeed),
|
|
26027
25787
|
percentageStep: GREE_FAN_PERCENTAGE_STEP,
|
|
@@ -26033,7 +25793,97 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
26033
25793
|
};
|
|
26034
25794
|
this.runtimeState.setCapState(FAN_CAP, fanSlice);
|
|
26035
25795
|
}
|
|
25796
|
+
/**
|
|
25797
|
+
* Apply the operator's per-AC exposure filters to a climate slice: narrow
|
|
25798
|
+
* `availableModes` / `availableFanModes` to the enabled subsets (empty ⇒ all;
|
|
25799
|
+
* `off` is always kept) and null out a swing axis the operator has hidden.
|
|
25800
|
+
* Pure — returns a new slice, never mutates the input.
|
|
25801
|
+
*/
|
|
25802
|
+
applyClimateFilters(slice) {
|
|
25803
|
+
const cfg = this.config.values;
|
|
25804
|
+
const enabledModes = cfg.enabledModes;
|
|
25805
|
+
const enabledFanModes = cfg.enabledFanModes;
|
|
25806
|
+
const exposeV = cfg.exposeVerticalSwing ?? true;
|
|
25807
|
+
const exposeH = cfg.exposeHorizontalSwing ?? true;
|
|
25808
|
+
const availableModes = enabledModes && enabledModes.length > 0 ? slice.availableModes.filter((m) => m === "off" || enabledModes.includes(m)) : slice.availableModes;
|
|
25809
|
+
const availableFanModes = enabledFanModes && enabledFanModes.length > 0 ? slice.availableFanModes.filter((f) => enabledFanModes.includes(f)) : slice.availableFanModes;
|
|
25810
|
+
return {
|
|
25811
|
+
...slice,
|
|
25812
|
+
availableModes,
|
|
25813
|
+
availableFanModes,
|
|
25814
|
+
swingVertical: exposeV ? slice.swingVertical : null,
|
|
25815
|
+
swingHorizontal: exposeH ? slice.swingHorizontal : null
|
|
25816
|
+
};
|
|
25817
|
+
}
|
|
25818
|
+
/**
|
|
25819
|
+
* Per-AC exposure form (device-details settings). Lets the operator pick
|
|
25820
|
+
* which modes / fan speeds / swing axes appear in the AC control UI. An empty
|
|
25821
|
+
* multiselect means "expose all". The horizontal-swing toggle is only offered
|
|
25822
|
+
* when the model advertises horizontal swing.
|
|
25823
|
+
*/
|
|
25824
|
+
getSettingsUISchema() {
|
|
25825
|
+
const supportsHSwing = (this.resolveAc()?.capabilities ?? getAcCapabilities()).horizontalSwing;
|
|
25826
|
+
const cfg = this.config.values;
|
|
25827
|
+
const fields = [
|
|
25828
|
+
{
|
|
25829
|
+
type: "multiselect",
|
|
25830
|
+
key: "enabledModes",
|
|
25831
|
+
label: "Modes",
|
|
25832
|
+
description: "Modes shown in the control UI. Empty = all. “Off” is always available.",
|
|
25833
|
+
options: SUPPORTED_CAP_MODES.map((m) => ({
|
|
25834
|
+
value: m,
|
|
25835
|
+
label: titleCase(m)
|
|
25836
|
+
}))
|
|
25837
|
+
},
|
|
25838
|
+
{
|
|
25839
|
+
type: "multiselect",
|
|
25840
|
+
key: "enabledFanModes",
|
|
25841
|
+
label: "Fan speeds",
|
|
25842
|
+
description: "Fan speeds shown in the control UI. Empty = all.",
|
|
25843
|
+
options: GREE_FAN_MODES.map((f) => ({
|
|
25844
|
+
value: f,
|
|
25845
|
+
label: titleCase(f)
|
|
25846
|
+
}))
|
|
25847
|
+
},
|
|
25848
|
+
{
|
|
25849
|
+
type: "boolean",
|
|
25850
|
+
style: "switch",
|
|
25851
|
+
key: "exposeVerticalSwing",
|
|
25852
|
+
label: "Vertical swing",
|
|
25853
|
+
default: true
|
|
25854
|
+
}
|
|
25855
|
+
];
|
|
25856
|
+
if (supportsHSwing) fields.push({
|
|
25857
|
+
type: "boolean",
|
|
25858
|
+
style: "switch",
|
|
25859
|
+
key: "exposeHorizontalSwing",
|
|
25860
|
+
label: "Horizontal swing",
|
|
25861
|
+
default: true
|
|
25862
|
+
});
|
|
25863
|
+
return hydrateSchema({ sections: [{
|
|
25864
|
+
id: "exposure",
|
|
25865
|
+
title: "Exposed controls",
|
|
25866
|
+
description: "Choose which modes, fan speeds and swing axes appear in the air-conditioner control UI.",
|
|
25867
|
+
fields
|
|
25868
|
+
}] }, {
|
|
25869
|
+
enabledModes: cfg.enabledModes ?? [],
|
|
25870
|
+
enabledFanModes: cfg.enabledFanModes ?? [],
|
|
25871
|
+
exposeVerticalSwing: cfg.exposeVerticalSwing ?? true,
|
|
25872
|
+
exposeHorizontalSwing: cfg.exposeHorizontalSwing ?? true
|
|
25873
|
+
});
|
|
25874
|
+
}
|
|
25875
|
+
async applySettingsPatch(patch) {
|
|
25876
|
+
const typed = greeAcSchema.partial().parse(patch);
|
|
25877
|
+
await this.config.setAll(typed);
|
|
25878
|
+
this.recomputeSlices();
|
|
25879
|
+
await this.refreshFeatures();
|
|
25880
|
+
}
|
|
26036
25881
|
};
|
|
25882
|
+
/** 'fan_only' → 'Fan only', 'medium_low' → 'Medium low'. Pure. */
|
|
25883
|
+
function titleCase(value) {
|
|
25884
|
+
const spaced = value.replace(/_/g, " ");
|
|
25885
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
25886
|
+
}
|
|
26037
25887
|
//#endregion
|
|
26038
25888
|
//#region src/devices/gree-toggle-device.ts
|
|
26039
25889
|
var SWITCH_CAP = "switch";
|
|
@@ -26043,9 +25893,10 @@ var SWITCH_COLD_START = {
|
|
|
26043
25893
|
};
|
|
26044
25894
|
/**
|
|
26045
25895
|
* Persisted config every Gree toggle accessory child carries. Mirrors
|
|
26046
|
-
* {@link greeAcSchema}: `greeMac` resolves the bound handle, `
|
|
26047
|
-
* the
|
|
26048
|
-
* which of the four boolean device flags this child
|
|
25896
|
+
* {@link greeAcSchema}: `greeMac` resolves the bound handle, `connectionKey`
|
|
25897
|
+
* selects the parent AC device's live connection, `system`/`integrationId` are
|
|
25898
|
+
* provenance. `toggle` selects which of the four boolean device flags this child
|
|
25899
|
+
* drives.
|
|
26049
25900
|
*/
|
|
26050
25901
|
var greeToggleSchema = object({
|
|
26051
25902
|
toggle: _enum([
|
|
@@ -26055,7 +25906,7 @@ var greeToggleSchema = object({
|
|
|
26055
25906
|
"freshAir"
|
|
26056
25907
|
]).describe("Which Gree boolean flag"),
|
|
26057
25908
|
greeMac: string().min(1).describe("Gree AC MAC address"),
|
|
26058
|
-
|
|
25909
|
+
connectionKey: string().min(1).describe("Per-device connection resolver key"),
|
|
26059
25910
|
system: literal("gree").optional(),
|
|
26060
25911
|
integrationId: string().optional()
|
|
26061
25912
|
});
|
|
@@ -26073,14 +25924,15 @@ var GreeToggleDevice = class extends BaseDevice$1 {
|
|
|
26073
25924
|
features = [];
|
|
26074
25925
|
toggle;
|
|
26075
25926
|
greeMac;
|
|
26076
|
-
|
|
25927
|
+
connectionKey;
|
|
26077
25928
|
stateChangedUnsub = null;
|
|
25929
|
+
surfaceUnsub = null;
|
|
26078
25930
|
constructor(ctx) {
|
|
26079
25931
|
const persisted = greeToggleSchema.parse(ctx.persistedConfig ?? {});
|
|
26080
25932
|
super(ctx, greeToggleSchema, { type: ctx.deviceMeta.type });
|
|
26081
25933
|
this.toggle = persisted.toggle;
|
|
26082
25934
|
this.greeMac = persisted.greeMac;
|
|
26083
|
-
this.
|
|
25935
|
+
this.connectionKey = persisted.connectionKey;
|
|
26084
25936
|
this.online = true;
|
|
26085
25937
|
this.updateSourceInfo({
|
|
26086
25938
|
id: `${this.greeMac}:${this.toggle}`,
|
|
@@ -26088,7 +25940,7 @@ var GreeToggleDevice = class extends BaseDevice$1 {
|
|
|
26088
25940
|
});
|
|
26089
25941
|
}
|
|
26090
25942
|
resolveAc() {
|
|
26091
|
-
return greeConnections.getDevice(this.
|
|
25943
|
+
return greeConnections.getDevice(this.connectionKey, this.greeMac);
|
|
26092
25944
|
}
|
|
26093
25945
|
requireAc() {
|
|
26094
25946
|
const ac = this.resolveAc();
|
|
@@ -26126,8 +25978,21 @@ var GreeToggleDevice = class extends BaseDevice$1 {
|
|
|
26126
25978
|
this.registerCap();
|
|
26127
25979
|
this.attachStateListener();
|
|
26128
25980
|
this.recomputeSlice();
|
|
25981
|
+
this.surfaceUnsub = greeConnections.onSurface(this.connectionKey, () => {
|
|
25982
|
+
this.attachStateListener();
|
|
25983
|
+
this.recomputeSlice();
|
|
25984
|
+
});
|
|
26129
25985
|
}
|
|
26130
25986
|
async removeDevice() {
|
|
25987
|
+
this.detachStateListener();
|
|
25988
|
+
if (this.surfaceUnsub) {
|
|
25989
|
+
try {
|
|
25990
|
+
this.surfaceUnsub();
|
|
25991
|
+
} catch {}
|
|
25992
|
+
this.surfaceUnsub = null;
|
|
25993
|
+
}
|
|
25994
|
+
}
|
|
25995
|
+
detachStateListener() {
|
|
26131
25996
|
if (this.stateChangedUnsub) {
|
|
26132
25997
|
try {
|
|
26133
25998
|
this.stateChangedUnsub();
|
|
@@ -26144,6 +26009,7 @@ var GreeToggleDevice = class extends BaseDevice$1 {
|
|
|
26144
26009
|
} });
|
|
26145
26010
|
return;
|
|
26146
26011
|
}
|
|
26012
|
+
this.detachStateListener();
|
|
26147
26013
|
const onState = () => {
|
|
26148
26014
|
try {
|
|
26149
26015
|
this.recomputeSlice();
|
|
@@ -26188,61 +26054,129 @@ var GreeToggleDevice = class extends BaseDevice$1 {
|
|
|
26188
26054
|
//#endregion
|
|
26189
26055
|
//#region src/devices/gree-container-device.ts
|
|
26190
26056
|
/**
|
|
26191
|
-
*
|
|
26192
|
-
*
|
|
26193
|
-
*
|
|
26194
|
-
*
|
|
26195
|
-
*
|
|
26196
|
-
|
|
26197
|
-
|
|
26198
|
-
greeMac: string().min(1).describe("Gree AC MAC address"),
|
|
26199
|
-
greeIp: string().optional().describe("Last known LAN IP"),
|
|
26200
|
-
brokerId: string().min(1).describe("Registry broker id"),
|
|
26201
|
-
integrationId: string().optional(),
|
|
26202
|
-
system: literal("gree").optional(),
|
|
26203
|
-
name: string().optional()
|
|
26204
|
-
});
|
|
26205
|
-
/**
|
|
26206
|
-
* Parent Container device for a single Gree AC. Owns no control caps itself — it
|
|
26207
|
-
* declares `getAccessoryChildren()` so the kernel auto-spawns the single
|
|
26208
|
-
* {@link GreeAcDevice} accessory child (Thermostat) and reconciles it across
|
|
26209
|
-
* reboots. Mirrors `DreoContainerDevice`.
|
|
26057
|
+
* Standalone-mode parent Container device for a single Gree AC (Reolink /
|
|
26058
|
+
* Ecowitt pattern). The operator-supplied CONNECTION lives on THIS device's
|
|
26059
|
+
* config; the device OWNS exactly one live `@apocaliss92/nodegree` client (via a
|
|
26060
|
+
* {@link GreeIntegrationManager}) keyed on its own `connectionKey` and published
|
|
26061
|
+
* on the in-process {@link greeConnections} resolver so its AC + toggle accessory
|
|
26062
|
+
* children read the live bound handle. There is NO broker and NO device-adoption
|
|
26063
|
+
* cap.
|
|
26210
26064
|
*
|
|
26211
|
-
*
|
|
26212
|
-
*
|
|
26213
|
-
*
|
|
26214
|
-
*
|
|
26215
|
-
*
|
|
26065
|
+
* It owns no control caps itself — it declares `getAccessoryChildren()` so the
|
|
26066
|
+
* kernel auto-spawns the single {@link GreeAcDevice} accessory child (Thermostat,
|
|
26067
|
+
* carrying `climate-control` + `fan-control` with the swing entities) plus
|
|
26068
|
+
* presence-gated {@link DeviceType.Switch} children for the panel-light / X-Fan /
|
|
26069
|
+
* health / fresh-air boolean flags (mirroring the HA Gree integration).
|
|
26070
|
+
* Capability presence is resolved from the bound handle's `AcCapabilities` (or
|
|
26071
|
+
* model defaults when no live handle).
|
|
26216
26072
|
*/
|
|
26217
26073
|
var GreeContainerDevice = class extends BaseDevice$1 {
|
|
26218
26074
|
features = [DeviceFeature.Resyncable];
|
|
26219
26075
|
greeMac;
|
|
26220
|
-
|
|
26076
|
+
connectionKey;
|
|
26221
26077
|
integrationId;
|
|
26078
|
+
connection;
|
|
26079
|
+
manager = null;
|
|
26222
26080
|
constructor(ctx) {
|
|
26223
|
-
const cfg =
|
|
26224
|
-
super(ctx,
|
|
26081
|
+
const cfg = greeAcDeviceSchema.parse(ctx.persistedConfig ?? {});
|
|
26082
|
+
super(ctx, greeAcDeviceSchema, { type: DeviceType.Container });
|
|
26225
26083
|
this.greeMac = cfg.greeMac;
|
|
26226
|
-
this.
|
|
26084
|
+
this.connectionKey = cfg.connectionKey;
|
|
26227
26085
|
this.integrationId = cfg.integrationId;
|
|
26228
|
-
this.
|
|
26086
|
+
this.connection = cfg.connection;
|
|
26087
|
+
this.online = greeConnections.getDevice(this.connectionKey, this.greeMac) !== null;
|
|
26229
26088
|
this.updateSourceInfo({
|
|
26230
26089
|
id: this.greeMac,
|
|
26231
26090
|
system: "gree"
|
|
26232
26091
|
});
|
|
26233
26092
|
}
|
|
26093
|
+
/** Adopt a pre-built manager (created by `onCreateDevice` so the live client
|
|
26094
|
+
* binds once at create time) — mirrors `EcowittGatewayDevice.adoptManager`. */
|
|
26095
|
+
adoptManager(manager) {
|
|
26096
|
+
this.manager = manager;
|
|
26097
|
+
}
|
|
26098
|
+
async onActivate() {
|
|
26099
|
+
await super.onActivate();
|
|
26100
|
+
if (this.manager === null) this.ensureManager().catch((err) => {
|
|
26101
|
+
this.ctx.logger.warn("Gree AC initial bind failed", { meta: {
|
|
26102
|
+
greeMac: this.greeMac,
|
|
26103
|
+
error: errMsg(err)
|
|
26104
|
+
} });
|
|
26105
|
+
});
|
|
26106
|
+
}
|
|
26107
|
+
async removeDevice() {
|
|
26108
|
+
const mgr = this.manager;
|
|
26109
|
+
this.manager = null;
|
|
26110
|
+
if (mgr) try {
|
|
26111
|
+
await mgr.stop();
|
|
26112
|
+
} catch (err) {
|
|
26113
|
+
this.ctx.logger.warn("Gree AC: client stop failed", { meta: {
|
|
26114
|
+
greeMac: this.greeMac,
|
|
26115
|
+
error: errMsg(err)
|
|
26116
|
+
} });
|
|
26117
|
+
}
|
|
26118
|
+
}
|
|
26119
|
+
async applySettingsPatch(patch) {
|
|
26120
|
+
if (!("connection" in patch)) return;
|
|
26121
|
+
const parsed = greeAcDeviceSchema.shape.connection.parse(patch["connection"]);
|
|
26122
|
+
await this.config.setAll({ connection: parsed });
|
|
26123
|
+
this.connection = parsed;
|
|
26124
|
+
const mgr = this.manager;
|
|
26125
|
+
if (mgr) try {
|
|
26126
|
+
await mgr.applyConnection(parsed);
|
|
26127
|
+
} catch (err) {
|
|
26128
|
+
this.ctx.logger.warn("Gree AC: applyConnection failed", { meta: {
|
|
26129
|
+
greeMac: this.greeMac,
|
|
26130
|
+
error: errMsg(err)
|
|
26131
|
+
} });
|
|
26132
|
+
}
|
|
26133
|
+
else await this.ensureManager().catch((err) => {
|
|
26134
|
+
this.ctx.logger.warn("Gree AC re-bind after settings change failed", { meta: {
|
|
26135
|
+
greeMac: this.greeMac,
|
|
26136
|
+
error: errMsg(err)
|
|
26137
|
+
} });
|
|
26138
|
+
});
|
|
26139
|
+
}
|
|
26140
|
+
/** Build + start the per-device nodegree client (if not already running). */
|
|
26141
|
+
async ensureManager() {
|
|
26142
|
+
if (this.manager !== null) return;
|
|
26143
|
+
const mgr = new GreeIntegrationManager({
|
|
26144
|
+
id: this.connectionKey,
|
|
26145
|
+
name: this.name,
|
|
26146
|
+
connection: this.connection,
|
|
26147
|
+
logger: this.ctx.logger,
|
|
26148
|
+
onConnected: () => this.setAcOnline(true),
|
|
26149
|
+
onDisconnected: () => this.setAcOnline(false),
|
|
26150
|
+
surfaceSink: greeConnections,
|
|
26151
|
+
expectMac: this.greeMac
|
|
26152
|
+
});
|
|
26153
|
+
this.manager = mgr;
|
|
26154
|
+
await mgr.start();
|
|
26155
|
+
}
|
|
26156
|
+
setAcOnline(online) {
|
|
26157
|
+
if (this.online === online) return;
|
|
26158
|
+
this.markOnline(online);
|
|
26159
|
+
this.setChildrenOnline(online).catch(() => {});
|
|
26160
|
+
}
|
|
26161
|
+
async setChildrenOnline(online) {
|
|
26162
|
+
const children = await this.ctx.devices.getChildren(this.id).catch(() => []);
|
|
26163
|
+
for (const child of children) {
|
|
26164
|
+
if (child.online === online) continue;
|
|
26165
|
+
child.markOnline(online);
|
|
26166
|
+
}
|
|
26167
|
+
}
|
|
26234
26168
|
getAccessoryChildren() {
|
|
26235
26169
|
return [{
|
|
26236
26170
|
stableIdSuffix: "ac",
|
|
26237
26171
|
meta: {
|
|
26238
|
-
type: DeviceType.
|
|
26172
|
+
type: DeviceType.Climate,
|
|
26239
26173
|
name: this.name,
|
|
26240
26174
|
linkDeviceId: this.id,
|
|
26241
26175
|
...this.integrationId !== void 0 ? { integrationId: this.integrationId } : {}
|
|
26242
26176
|
},
|
|
26243
26177
|
config: {
|
|
26244
26178
|
greeMac: this.greeMac,
|
|
26245
|
-
|
|
26179
|
+
connectionKey: this.connectionKey,
|
|
26246
26180
|
system: "gree",
|
|
26247
26181
|
...this.integrationId !== void 0 ? { integrationId: this.integrationId } : {}
|
|
26248
26182
|
},
|
|
@@ -26256,7 +26190,7 @@ var GreeContainerDevice = class extends BaseDevice$1 {
|
|
|
26256
26190
|
* defaults when no live handle is bound yet).
|
|
26257
26191
|
*/
|
|
26258
26192
|
toggleChildren() {
|
|
26259
|
-
const caps = greeConnections.getDevice(this.
|
|
26193
|
+
const caps = greeConnections.getDevice(this.connectionKey, this.greeMac)?.capabilities ?? DEFAULT_AC_CAPABILITIES;
|
|
26260
26194
|
return [
|
|
26261
26195
|
{
|
|
26262
26196
|
toggle: "light",
|
|
@@ -26292,7 +26226,7 @@ var GreeContainerDevice = class extends BaseDevice$1 {
|
|
|
26292
26226
|
const childConfig = {
|
|
26293
26227
|
toggle: d.toggle,
|
|
26294
26228
|
greeMac: this.greeMac,
|
|
26295
|
-
|
|
26229
|
+
connectionKey: this.connectionKey,
|
|
26296
26230
|
system: "gree",
|
|
26297
26231
|
...this.integrationId !== void 0 ? { integrationId: this.integrationId } : {}
|
|
26298
26232
|
};
|
|
@@ -26307,303 +26241,158 @@ var GreeContainerDevice = class extends BaseDevice$1 {
|
|
|
26307
26241
|
};
|
|
26308
26242
|
//#endregion
|
|
26309
26243
|
//#region src/addon.ts
|
|
26310
|
-
/** Default multi-broker config — a fresh install starts with no scopes. */
|
|
26311
|
-
var DEFAULTS = { brokers: [] };
|
|
26312
26244
|
/**
|
|
26313
|
-
* Gree device-provider addon (
|
|
26245
|
+
* Gree air-conditioner device-provider addon — `mode: standalone` (the Reolink /
|
|
26246
|
+
* Ecowitt pattern). No broker, no device-adoption cap. An AC is added as a DEVICE
|
|
26247
|
+
* via the manual-creation form: the CONNECTION (host IP + optional broadcastAddr
|
|
26248
|
+
* + UDP tuning) lives on the AC DEVICE config. `onCreateDevice` runs a DIRECTED
|
|
26249
|
+
* bind against the given host to learn the AC's durable identity (MAC) and prove
|
|
26250
|
+
* reachability, then creates a {@link GreeContainerDevice} that OWNS its own live
|
|
26251
|
+
* `@apocaliss92/nodegree` client and fans out its Thermostat + toggle accessory
|
|
26252
|
+
* children (climate-control with swing entities + the panel-light / X-Fan /
|
|
26253
|
+
* health / fresh-air switches — all preserved from the broker model).
|
|
26314
26254
|
*
|
|
26315
|
-
*
|
|
26316
|
-
*
|
|
26317
|
-
* {@link DeviceType.Container} parent that fans out a single {@link DeviceType.Thermostat}
|
|
26318
|
-
* accessory child carrying `climate-control` + `fan-control`. Modelled directly
|
|
26319
|
-
* on the Dreo / Dreame device-provider template: `broker` + `device-adoption` cap
|
|
26320
|
-
* providers for connection + adoption, the `device-provider` cap from the base
|
|
26321
|
-
* class, and an in-process connection resolver (`greeConnections`) the device
|
|
26322
|
-
* classes read.
|
|
26255
|
+
* `instanceMode: multiple` — many ACs may be added. Placement: any-node — Gree is
|
|
26256
|
+
* LOCAL UDP; the addon runs on whichever node shares the AC's subnet.
|
|
26323
26257
|
*
|
|
26324
|
-
*
|
|
26325
|
-
*
|
|
26326
|
-
*
|
|
26327
|
-
*
|
|
26258
|
+
* LAN UDP-broadcast auto-discovery is implemented via the `device-provider`
|
|
26259
|
+
* discovery surface (`supportsDiscovery`/`discoverDevices`/`adoptDiscoveredDevice`)
|
|
26260
|
+
* — the same one ONVIF uses and the aggregated "Scan network" modal fans out to.
|
|
26261
|
+
* Manual add-one-AC-by-IP (directed bind) remains available in parallel.
|
|
26328
26262
|
*/
|
|
26329
26263
|
var GreeProviderAddon = class extends BaseDeviceProvider {
|
|
26330
26264
|
addonId = "provider-gree";
|
|
26331
26265
|
providerName = "Gree";
|
|
26332
26266
|
deviceClasses = { [DeviceType.Container]: GreeContainerDevice };
|
|
26333
|
-
registry = null;
|
|
26334
26267
|
constructor() {
|
|
26335
|
-
super({
|
|
26268
|
+
super({});
|
|
26336
26269
|
}
|
|
26337
|
-
async
|
|
26338
|
-
|
|
26339
|
-
|
|
26340
|
-
|
|
26341
|
-
|
|
26342
|
-
|
|
26270
|
+
async supportsDiscovery() {
|
|
26271
|
+
return true;
|
|
26272
|
+
}
|
|
26273
|
+
async getDiscoveryParamsSchema() {
|
|
26274
|
+
return buildDiscoveryParamsFormSchema();
|
|
26275
|
+
}
|
|
26276
|
+
async discoverDevices(input) {
|
|
26277
|
+
const broadcastAddress = typeof input?.params?.["broadcastAddress"] === "string" ? input.params["broadcastAddress"].trim() : void 0;
|
|
26278
|
+
const timeoutMs = typeof input?.params?.["timeoutMs"] === "number" ? input.params["timeoutMs"] : void 0;
|
|
26279
|
+
const responders = await discoverGreeDevices({
|
|
26280
|
+
...broadcastAddress ? { broadcastAddress } : {},
|
|
26281
|
+
...timeoutMs ? { timeoutMs } : {}
|
|
26343
26282
|
});
|
|
26344
|
-
this.
|
|
26345
|
-
|
|
26283
|
+
this.ctx.logger.info("Gree discovery complete", { meta: {
|
|
26284
|
+
count: responders.length,
|
|
26285
|
+
broadcastAddress: broadcastAddress ?? "local"
|
|
26286
|
+
} });
|
|
26287
|
+
return responders.map((d) => {
|
|
26288
|
+
const displayName = d.name.length > 0 ? d.name : d.mac;
|
|
26289
|
+
return {
|
|
26290
|
+
stableId: `gree:${macKey(d.mac)}`,
|
|
26291
|
+
type: DeviceType.Container,
|
|
26292
|
+
suggestedName: displayName,
|
|
26293
|
+
prefilledConfig: {
|
|
26294
|
+
name: displayName,
|
|
26295
|
+
host: d.ip,
|
|
26296
|
+
...d.model !== void 0 ? { model: d.model } : {}
|
|
26297
|
+
}
|
|
26298
|
+
};
|
|
26346
26299
|
});
|
|
26347
|
-
await this.registry.restore(this.config.brokers);
|
|
26348
|
-
this.ctx.logger.info("Gree: provider initialised", { meta: { brokerCount: this.config.brokers.length } });
|
|
26349
|
-
await this.reconcileIntegrationsToBrokers();
|
|
26350
|
-
this.subscribeIntegrationLifecycle();
|
|
26351
|
-
return [
|
|
26352
|
-
...regs,
|
|
26353
|
-
{
|
|
26354
|
-
capability: brokerCapability,
|
|
26355
|
-
provider: this.buildBrokerProvider()
|
|
26356
|
-
},
|
|
26357
|
-
{
|
|
26358
|
-
capability: deviceAdoptionCapability,
|
|
26359
|
-
provider: this.buildAdoptionProvider()
|
|
26360
|
-
}
|
|
26361
|
-
];
|
|
26362
26300
|
}
|
|
26363
|
-
|
|
26364
|
-
|
|
26365
|
-
|
|
26366
|
-
|
|
26367
|
-
|
|
26368
|
-
const persisted = this.config.brokers;
|
|
26369
|
-
const liveIds = new Set(reg.list().map((b) => b.id));
|
|
26370
|
-
const persistedIds = new Set(persisted.map((e) => e.id));
|
|
26371
|
-
for (const liveId of liveIds) if (!persistedIds.has(liveId)) try {
|
|
26372
|
-
await reg.removeEntry(liveId);
|
|
26373
|
-
} catch (err) {
|
|
26374
|
-
this.ctx.logger.warn("Gree onConfigChanged: removeEntry failed", { meta: {
|
|
26375
|
-
brokerId: liveId,
|
|
26376
|
-
error: errMsg(err)
|
|
26377
|
-
} });
|
|
26378
|
-
}
|
|
26379
|
-
for (const entry of persisted) {
|
|
26380
|
-
if (entry.id && liveIds.has(entry.id)) {
|
|
26381
|
-
if (sameConnection(entry.connection, reg.getConnection(entry.id))) continue;
|
|
26382
|
-
try {
|
|
26383
|
-
await reg.updateEntry(entry.id, entry.connection);
|
|
26384
|
-
} catch (err) {
|
|
26385
|
-
this.ctx.logger.warn("Gree onConfigChanged: updateEntry failed", { meta: {
|
|
26386
|
-
brokerId: entry.id,
|
|
26387
|
-
error: errMsg(err)
|
|
26388
|
-
} });
|
|
26389
|
-
}
|
|
26390
|
-
continue;
|
|
26391
|
-
}
|
|
26392
|
-
try {
|
|
26393
|
-
const created = await reg.createEntry(entry.name, entry.connection);
|
|
26394
|
-
if (created.id !== entry.id) {
|
|
26395
|
-
const next = persisted.map((b) => b === entry ? {
|
|
26396
|
-
...b,
|
|
26397
|
-
id: created.id
|
|
26398
|
-
} : b);
|
|
26399
|
-
await this.updateGlobalSettings({ brokers: next });
|
|
26400
|
-
}
|
|
26401
|
-
} catch (err) {
|
|
26402
|
-
this.ctx.logger.warn("Gree onConfigChanged: failed to start new broker", { meta: {
|
|
26403
|
-
brokerName: entry.name,
|
|
26404
|
-
error: errMsg(err)
|
|
26405
|
-
} });
|
|
26406
|
-
}
|
|
26407
|
-
}
|
|
26301
|
+
async adoptDiscoveredDevice(input) {
|
|
26302
|
+
return this.createDevice({
|
|
26303
|
+
type: DeviceType.Container,
|
|
26304
|
+
config: input.candidate.prefilledConfig
|
|
26305
|
+
});
|
|
26408
26306
|
}
|
|
26409
|
-
async
|
|
26410
|
-
|
|
26411
|
-
await this.registry?.shutdown();
|
|
26412
|
-
} catch (err) {
|
|
26413
|
-
this.ctx.logger.warn("Gree: provider shutdown error", { meta: { error: errMsg(err) } });
|
|
26414
|
-
}
|
|
26415
|
-
this.registry = null;
|
|
26416
|
-
await super.onShutdown();
|
|
26307
|
+
async supportsManualCreation() {
|
|
26308
|
+
return true;
|
|
26417
26309
|
}
|
|
26418
|
-
|
|
26419
|
-
|
|
26420
|
-
return
|
|
26421
|
-
}
|
|
26422
|
-
|
|
26423
|
-
|
|
26424
|
-
|
|
26425
|
-
|
|
26426
|
-
|
|
26427
|
-
|
|
26428
|
-
|
|
26310
|
+
async onGetCreationSchema(type) {
|
|
26311
|
+
if (type !== DeviceType.Container) return null;
|
|
26312
|
+
return buildConnectionFormSchema();
|
|
26313
|
+
}
|
|
26314
|
+
async onCreateDevice(type, config) {
|
|
26315
|
+
if (type !== DeviceType.Container) throw new Error(`Gree provider does not support device type: ${type}`);
|
|
26316
|
+
const name = typeof config["name"] === "string" ? config["name"].trim() : "";
|
|
26317
|
+
if (!name) throw new Error("Air conditioner name is required");
|
|
26318
|
+
const connection = settingsToGreeConfig(config);
|
|
26319
|
+
if (connection.host.trim().length === 0) throw new Error("Air conditioner IP address is required");
|
|
26320
|
+
const bound = await bindOnce({
|
|
26321
|
+
connection,
|
|
26429
26322
|
logger: this.ctx.logger
|
|
26430
26323
|
});
|
|
26431
|
-
|
|
26432
|
-
|
|
26433
|
-
|
|
26434
|
-
|
|
26435
|
-
|
|
26436
|
-
|
|
26437
|
-
|
|
26438
|
-
|
|
26439
|
-
|
|
26440
|
-
|
|
26441
|
-
|
|
26324
|
+
const connectionKey = `gree:${macKey(bound.mac)}`;
|
|
26325
|
+
const deviceConfig = {
|
|
26326
|
+
greeMac: macKey(bound.mac),
|
|
26327
|
+
greeIp: bound.ip,
|
|
26328
|
+
connectionKey,
|
|
26329
|
+
connection,
|
|
26330
|
+
system: "gree",
|
|
26331
|
+
name
|
|
26332
|
+
};
|
|
26333
|
+
const manager = new GreeIntegrationManager({
|
|
26334
|
+
id: connectionKey,
|
|
26335
|
+
name,
|
|
26336
|
+
connection,
|
|
26337
|
+
logger: this.ctx.logger,
|
|
26338
|
+
onConnected: () => void 0,
|
|
26339
|
+
onDisconnected: () => void 0,
|
|
26340
|
+
surfaceSink: greeConnections,
|
|
26341
|
+
expectMac: bound.mac
|
|
26442
26342
|
});
|
|
26443
|
-
}
|
|
26444
|
-
/**
|
|
26445
|
-
* Link each surviving integration (carrying `{ brokerId }` in its settings) to
|
|
26446
|
-
* its broker so the generic `device-adoption` cap resolves the scope, and
|
|
26447
|
-
* cascade-clean brokers (+ their adopted devices) whose spawning integration
|
|
26448
|
-
* was deleted. Idempotent; runs on boot + on every integration lifecycle event.
|
|
26449
|
-
* Guarded so a failure never fails init.
|
|
26450
|
-
*/
|
|
26451
|
-
async reconcileIntegrationsToBrokers() {
|
|
26452
26343
|
try {
|
|
26453
|
-
|
|
26454
|
-
const surviving = new Set(mine.map((i) => i.id));
|
|
26455
|
-
for (const integration of mine) {
|
|
26456
|
-
const settings = await this.ctx.api.integrations.getSettings.query({ id: integration.id });
|
|
26457
|
-
const brokerId = typeof settings["brokerId"] === "string" ? settings["brokerId"] : void 0;
|
|
26458
|
-
if (brokerId === void 0) continue;
|
|
26459
|
-
if (!this.config.brokers.some((b) => b.id === brokerId)) {
|
|
26460
|
-
this.ctx.logger.warn("Gree integration→broker: linked broker not found", { meta: {
|
|
26461
|
-
integrationId: integration.id,
|
|
26462
|
-
brokerId
|
|
26463
|
-
} });
|
|
26464
|
-
continue;
|
|
26465
|
-
}
|
|
26466
|
-
this.requireRegistry().linkIntegration(integration.id, brokerId);
|
|
26467
|
-
}
|
|
26468
|
-
const toRemove = this.config.brokers.filter((b) => b.integrationId !== void 0 && !surviving.has(b.integrationId)).map((b) => b.id);
|
|
26469
|
-
if (toRemove.length === 0) return;
|
|
26470
|
-
for (const id of toRemove) try {
|
|
26471
|
-
await this.requireRegistry().removeEntry(id);
|
|
26472
|
-
await this.cascadeRemoveDevicesForBroker(id);
|
|
26473
|
-
} catch (err) {
|
|
26474
|
-
this.ctx.logger.warn("Gree integration→broker: broker cleanup failed", { meta: {
|
|
26475
|
-
brokerId: id,
|
|
26476
|
-
error: errMsg(err)
|
|
26477
|
-
} });
|
|
26478
|
-
}
|
|
26479
|
-
const nextBrokers = this.config.brokers.filter((b) => !toRemove.includes(b.id));
|
|
26480
|
-
await this.updateGlobalSettings({ brokers: nextBrokers });
|
|
26344
|
+
await manager.start();
|
|
26481
26345
|
} catch (err) {
|
|
26482
|
-
this.ctx.logger.warn("Gree
|
|
26346
|
+
this.ctx.logger.warn("Gree create: initial client start failed — device kept", { meta: {
|
|
26347
|
+
connectionKey,
|
|
26348
|
+
error: errMsg(err)
|
|
26349
|
+
} });
|
|
26483
26350
|
}
|
|
26484
|
-
|
|
26485
|
-
|
|
26486
|
-
|
|
26487
|
-
|
|
26488
|
-
if (typeof addonId === "string" && addonId !== this.ctx.id) return;
|
|
26489
|
-
this.reconcileIntegrationsToBrokers();
|
|
26490
|
-
};
|
|
26491
|
-
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationEnabled }, handler);
|
|
26492
|
-
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, handler);
|
|
26493
|
-
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, handler);
|
|
26494
|
-
}
|
|
26495
|
-
buildAdoptionProvider() {
|
|
26496
|
-
return buildGreeAdoptionProvider({
|
|
26497
|
-
registry: greeConnections,
|
|
26498
|
-
logger: this.ctx.logger,
|
|
26499
|
-
getBrokerIdForIntegration: async (id) => {
|
|
26500
|
-
const brokerId = (await this.ctx.api.integrations.getSettings.query({ id }))["brokerId"];
|
|
26501
|
-
if (typeof brokerId !== "string") throw new Error(`integration ${id} has no linked brokerId`);
|
|
26502
|
-
return brokerId;
|
|
26503
|
-
},
|
|
26504
|
-
listAdoptedGree: async () => {
|
|
26505
|
-
const reg = this.ctx.kernel.deviceRegistry;
|
|
26506
|
-
const devices = this.ctx.kernel.devices;
|
|
26507
|
-
if (!reg || !devices) return [];
|
|
26508
|
-
const out = [];
|
|
26509
|
-
for (const d of reg.getAllForAddon(this.addonId)) {
|
|
26510
|
-
if (d.parentDeviceId !== null) continue;
|
|
26511
|
-
const config = await devices.loadConfig(d.id).catch(() => ({}));
|
|
26512
|
-
out.push({
|
|
26513
|
-
id: d.id,
|
|
26514
|
-
config
|
|
26515
|
-
});
|
|
26516
|
-
}
|
|
26517
|
-
return out;
|
|
26518
|
-
},
|
|
26519
|
-
adoptDevice: async ({ greeMac, greeIp, brokerId, integrationId, name }) => {
|
|
26520
|
-
const devices = this.ctx.kernel.devices;
|
|
26521
|
-
if (!devices) throw new Error("gree adopt: kernel.devices unavailable");
|
|
26522
|
-
const config = {
|
|
26523
|
-
greeMac,
|
|
26524
|
-
greeIp,
|
|
26525
|
-
brokerId,
|
|
26526
|
-
system: "gree",
|
|
26527
|
-
integrationId,
|
|
26528
|
-
name
|
|
26529
|
-
};
|
|
26530
|
-
const stableId = this.generateStableId(DeviceType.Container, config);
|
|
26531
|
-
const device = await devices.create(stableId, GreeContainerDevice, config, null, {
|
|
26532
|
-
type: DeviceType.Container,
|
|
26533
|
-
name,
|
|
26534
|
-
integrationId
|
|
26535
|
-
});
|
|
26536
|
-
const children = await devices.getChildren(device.id);
|
|
26537
|
-
return {
|
|
26538
|
-
deviceId: device.id,
|
|
26539
|
-
accessoryDeviceIds: children.map((c) => c.id)
|
|
26540
|
-
};
|
|
26541
|
-
},
|
|
26542
|
-
removeDevice: async (id) => {
|
|
26543
|
-
await this.ctx.kernel.devices?.remove(id);
|
|
26351
|
+
return {
|
|
26352
|
+
meta: {
|
|
26353
|
+
type: DeviceType.Container,
|
|
26354
|
+
name
|
|
26544
26355
|
},
|
|
26545
|
-
|
|
26546
|
-
|
|
26547
|
-
if (
|
|
26548
|
-
return devices.loadConfig(id).catch(() => null);
|
|
26356
|
+
config: deviceConfig,
|
|
26357
|
+
onAfterCreate: async (device) => {
|
|
26358
|
+
if (device instanceof GreeContainerDevice) device.adoptManager(manager);
|
|
26549
26359
|
}
|
|
26550
|
-
}
|
|
26551
|
-
}
|
|
26552
|
-
globalSettingsSchema() {
|
|
26553
|
-
return this.schema({ sections: [{
|
|
26554
|
-
id: "gree-broker",
|
|
26555
|
-
title: "Gree discovery scope",
|
|
26556
|
-
description: "Gree scopes are managed in the External systems → Brokers tab.",
|
|
26557
|
-
columns: 1,
|
|
26558
|
-
fields: [{
|
|
26559
|
-
type: "info",
|
|
26560
|
-
key: "brokerHelp",
|
|
26561
|
-
label: "Scopes are managed separately",
|
|
26562
|
-
content: "This integration links to a Gree LAN discovery scope. Add, edit, or remove scopes from External systems → Brokers. Gree is local-only (UDP) — no cloud account is needed; a scope just stores the broadcast address + UDP settings."
|
|
26563
|
-
}]
|
|
26564
|
-
}] });
|
|
26565
|
-
}
|
|
26566
|
-
async supportsManualCreation() {
|
|
26567
|
-
return false;
|
|
26568
|
-
}
|
|
26569
|
-
async onGetCreationSchema(_type) {
|
|
26570
|
-
return null;
|
|
26571
|
-
}
|
|
26572
|
-
async onCreateDevice(_type, _config) {
|
|
26573
|
-
throw new Error("Gree devices are adopted via LAN discovery, not created manually");
|
|
26574
|
-
}
|
|
26575
|
-
setBrokerDevicesOnline(brokerId, online) {
|
|
26576
|
-
const reg = this.ctx.kernel.deviceRegistry;
|
|
26577
|
-
if (!reg) return;
|
|
26578
|
-
const n = setBrokerDevicesOnline(reg.getAllForAddon(this.addonId), brokerId, online);
|
|
26579
|
-
if (n > 0) this.ctx.logger.info("Gree: broker devices " + (online ? "online" : "offline"), { meta: {
|
|
26580
|
-
brokerId,
|
|
26581
|
-
count: n
|
|
26582
|
-
} });
|
|
26360
|
+
};
|
|
26583
26361
|
}
|
|
26584
|
-
|
|
26585
|
-
|
|
26586
|
-
|
|
26362
|
+
/**
|
|
26363
|
+
* AC stableId — derived from the AC's durable MAC identity (resolved by the
|
|
26364
|
+
* create-time bind), NOT a broker id. Re-adding the same physical AC reuses its
|
|
26365
|
+
* persisted row. Falls back to a timestamp only when no MAC is resolvable
|
|
26366
|
+
* (should not happen — `onCreateDevice` binds first).
|
|
26367
|
+
*/
|
|
26368
|
+
generateStableId(_type, config) {
|
|
26369
|
+
const mac = config?.["greeMac"];
|
|
26370
|
+
if (typeof mac === "string" && mac.length > 0) return `gree:${macKey(mac)}`;
|
|
26371
|
+
return `gree:${Date.now()}`;
|
|
26587
26372
|
}
|
|
26588
26373
|
};
|
|
26589
26374
|
//#endregion
|
|
26590
26375
|
exports.ADVERTISED_CAP_MODES = ADVERTISED_CAP_MODES;
|
|
26376
|
+
exports.DeviceType = DeviceType;
|
|
26591
26377
|
exports.GREE_FAN_PERCENTAGE_STEP = GREE_FAN_PERCENTAGE_STEP;
|
|
26592
26378
|
exports.GreeProviderAddon = GreeProviderAddon;
|
|
26593
26379
|
exports.SUPPORTED_CAP_MODES = SUPPORTED_CAP_MODES;
|
|
26380
|
+
exports.bindOnce = bindOnce;
|
|
26594
26381
|
exports.boolToHorizontalSwing = boolToHorizontalSwing;
|
|
26595
26382
|
exports.boolToVerticalSwing = boolToVerticalSwing;
|
|
26596
26383
|
exports.buildConnectionFormSchema = buildConnectionFormSchema;
|
|
26597
|
-
exports.buildGreeCandidates = buildGreeCandidates;
|
|
26598
26384
|
exports.capModeToLibMode = capModeToLibMode;
|
|
26599
26385
|
exports.fanSpeedToPercentage = fanSpeedToPercentage;
|
|
26600
|
-
exports.
|
|
26386
|
+
exports.greeAcDeviceSchema = greeAcDeviceSchema;
|
|
26601
26387
|
exports.greeConfigSchema = greeConfigSchema;
|
|
26602
26388
|
exports.horizontalSwingToBool = horizontalSwingToBool;
|
|
26603
26389
|
exports.isAutoFan = isAutoFan;
|
|
26604
26390
|
exports.libModeToCapMode = libModeToCapMode;
|
|
26605
26391
|
exports.oscillatingToVerticalSwing = oscillatingToVerticalSwing;
|
|
26606
26392
|
exports.percentageToFanSpeed = percentageToFanSpeed;
|
|
26393
|
+
exports.resolveBroadcastTarget = resolveBroadcastTarget;
|
|
26394
|
+
exports.sameConnection = sameConnection;
|
|
26395
|
+
exports.settingsToGreeConfig = settingsToGreeConfig;
|
|
26607
26396
|
exports.swingToOscillating = swingToOscillating;
|
|
26608
26397
|
exports.toNodegreeOptions = toNodegreeOptions;
|
|
26609
26398
|
exports.verticalSwingToBool = verticalSwingToBool;
|