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