@camstack/addon-provider-gree 0.1.8 → 0.1.9
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 +560 -1125
- package/dist/addon.mjs +555 -1124
- package/dist/index.js +38 -2
- package/dist/index.mjs +34 -2
- package/package.json +2 -8
package/dist/addon.js
CHANGED
|
@@ -14783,83 +14783,34 @@ var GetStateInputSchema = object({
|
|
|
14783
14783
|
* HA: entity_id (returns the cached entity state). */
|
|
14784
14784
|
key: string()
|
|
14785
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
|
-
}),
|
|
14815
|
-
/** Read the persisted settings record for a broker (kind-specific
|
|
14816
|
-
* shape). Admin-only — settings may contain secrets. Returns `null`
|
|
14817
|
-
* when the broker id is unknown to the provider (the collection
|
|
14818
|
-
* fallback may route a foreign id to the first provider). */
|
|
14819
|
-
getSettings: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
|
|
14820
|
-
/** Overwrite the persisted settings record. The kind-specific
|
|
14821
|
-
* provider validates the shape and applies the change (reconnects
|
|
14822
|
-
* if credentials changed). */
|
|
14823
|
-
setSettings: method(object({
|
|
14824
|
-
id: string(),
|
|
14825
|
-
settings: SettingsRecordSchema$1
|
|
14826
|
-
}), _void(), {
|
|
14827
|
-
kind: "mutation",
|
|
14828
|
-
auth: "admin"
|
|
14829
|
-
}),
|
|
14830
|
-
/** Returns the kind-specific connection config the consumer needs
|
|
14831
|
-
* to open its own client (MQTT pattern: `{url, username, password,
|
|
14832
|
-
* clientIdPrefix}`). HA providers MAY return the auth envelope
|
|
14833
|
-
* but typical HA consumers use `publish` / `subscribe` instead.
|
|
14834
|
-
* Returns `null` when the broker id is unknown to the provider. */
|
|
14835
|
-
getBrokerConfig: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
|
|
14836
|
-
getSettingsSchema: method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }),
|
|
14837
|
-
testSettings: method(TestSettingsInputSchema, TestSettingsResultSchema, {
|
|
14838
|
-
kind: "mutation",
|
|
14839
|
-
auth: "admin"
|
|
14840
|
-
}),
|
|
14841
|
-
publish: method(PublishInputSchema, unknown(), {
|
|
14842
|
-
kind: "mutation",
|
|
14843
|
-
auth: "admin"
|
|
14844
|
-
}),
|
|
14845
|
-
subscribe: method(SubscribeInputSchema, SubscribeResultSchema, {
|
|
14846
|
-
kind: "mutation",
|
|
14847
|
-
auth: "admin"
|
|
14848
|
-
}),
|
|
14849
|
-
unsubscribe: method(UnsubscribeInputSchema, _void(), {
|
|
14850
|
-
kind: "mutation",
|
|
14851
|
-
auth: "admin"
|
|
14852
|
-
}),
|
|
14853
|
-
/** Read the broker's cached state for a key. Returns `null` when
|
|
14854
|
-
* unknown to the broker (never published / unknown entity). */
|
|
14855
|
-
getState: method(GetStateInputSchema, unknown().nullable()),
|
|
14856
|
-
/** Status method — explicit registration with a `z.void()` input so
|
|
14857
|
-
* the codegen-generated tRPC router types its input as
|
|
14858
|
-
* `{addonId?: string, nodeId?: string}` (system-scoped collection
|
|
14859
|
-
* shape) instead of the device-scoped `{deviceId}` fallback. */
|
|
14860
|
-
getStatus: method(_void(), RegistryStatusSchema)
|
|
14861
|
-
}
|
|
14862
|
-
};
|
|
14786
|
+
method(ListInputSchema, array(BrokerInfoSchema$1)), method(GetInputSchema, BrokerInfoSchema$1.nullable()), method(_void(), array(BrokerProviderInfoSchema), { auth: "admin" }), method(AddInputSchema, AddResultSchema, {
|
|
14787
|
+
kind: "mutation",
|
|
14788
|
+
auth: "admin"
|
|
14789
|
+
}), method(RemoveInputSchema, _void(), {
|
|
14790
|
+
kind: "mutation",
|
|
14791
|
+
auth: "admin"
|
|
14792
|
+
}), method(GetInputSchema, TestConnectionResultSchema, {
|
|
14793
|
+
kind: "mutation",
|
|
14794
|
+
auth: "admin"
|
|
14795
|
+
}), method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }), method(object({
|
|
14796
|
+
id: string(),
|
|
14797
|
+
settings: SettingsRecordSchema$1
|
|
14798
|
+
}), _void(), {
|
|
14799
|
+
kind: "mutation",
|
|
14800
|
+
auth: "admin"
|
|
14801
|
+
}), method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }), method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }), method(TestSettingsInputSchema, TestSettingsResultSchema, {
|
|
14802
|
+
kind: "mutation",
|
|
14803
|
+
auth: "admin"
|
|
14804
|
+
}), method(PublishInputSchema, unknown(), {
|
|
14805
|
+
kind: "mutation",
|
|
14806
|
+
auth: "admin"
|
|
14807
|
+
}), method(SubscribeInputSchema, SubscribeResultSchema, {
|
|
14808
|
+
kind: "mutation",
|
|
14809
|
+
auth: "admin"
|
|
14810
|
+
}), method(UnsubscribeInputSchema, _void(), {
|
|
14811
|
+
kind: "mutation",
|
|
14812
|
+
auth: "admin"
|
|
14813
|
+
}), method(GetStateInputSchema, unknown().nullable()), method(_void(), RegistryStatusSchema);
|
|
14863
14814
|
DeviceType.Camera;
|
|
14864
14815
|
/**
|
|
14865
14816
|
* `custom-model-registry` — collection cap exposing operator-registered
|
|
@@ -15095,36 +15046,19 @@ var ResyncResultSchema = object({
|
|
|
15095
15046
|
* provider re-derived the device. 0/absent for a normal incremental re-sync. */
|
|
15096
15047
|
removedChildren: number().int().nonnegative().optional()
|
|
15097
15048
|
});
|
|
15098
|
-
|
|
15099
|
-
|
|
15100
|
-
|
|
15101
|
-
|
|
15102
|
-
|
|
15103
|
-
|
|
15104
|
-
|
|
15105
|
-
|
|
15106
|
-
|
|
15107
|
-
|
|
15108
|
-
|
|
15109
|
-
|
|
15110
|
-
|
|
15111
|
-
kind: "mutation",
|
|
15112
|
-
auth: "admin"
|
|
15113
|
-
}),
|
|
15114
|
-
adopt: method(AdoptInputSchema, AdoptResultSchema, {
|
|
15115
|
-
kind: "mutation",
|
|
15116
|
-
auth: "admin"
|
|
15117
|
-
}),
|
|
15118
|
-
release: method(ReleaseInputSchema, _void(), {
|
|
15119
|
-
kind: "mutation",
|
|
15120
|
-
auth: "admin"
|
|
15121
|
-
}),
|
|
15122
|
-
resync: method(ResyncInputSchema, ResyncResultSchema, {
|
|
15123
|
-
kind: "mutation",
|
|
15124
|
-
auth: "admin"
|
|
15125
|
-
})
|
|
15126
|
-
}
|
|
15127
|
-
};
|
|
15049
|
+
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, {
|
|
15050
|
+
kind: "mutation",
|
|
15051
|
+
auth: "admin"
|
|
15052
|
+
}), method(AdoptInputSchema, AdoptResultSchema, {
|
|
15053
|
+
kind: "mutation",
|
|
15054
|
+
auth: "admin"
|
|
15055
|
+
}), method(ReleaseInputSchema, _void(), {
|
|
15056
|
+
kind: "mutation",
|
|
15057
|
+
auth: "admin"
|
|
15058
|
+
}), method(ResyncInputSchema, ResyncResultSchema, {
|
|
15059
|
+
kind: "mutation",
|
|
15060
|
+
auth: "admin"
|
|
15061
|
+
});
|
|
15128
15062
|
/**
|
|
15129
15063
|
* `device-export` — collection cap for addons that export camstack
|
|
15130
15064
|
* devices to external ecosystems (HomeAssistant via MQTT discovery,
|
|
@@ -18314,6 +18248,17 @@ var AvailableIntegrationTypeSchema = object({
|
|
|
18314
18248
|
iconUrl: string().nullable(),
|
|
18315
18249
|
color: string(),
|
|
18316
18250
|
instanceMode: string(),
|
|
18251
|
+
/**
|
|
18252
|
+
* Integration wizard `mode` (LOCKED MODEL): `standalone` (create
|
|
18253
|
+
* immediately then add devices, no config step/button), `account` (config
|
|
18254
|
+
* step), or `broker` (broker step). Derived server-side by
|
|
18255
|
+
* `getAvailableTypes` when the addon manifest omits an explicit `mode`.
|
|
18256
|
+
*/
|
|
18257
|
+
mode: _enum([
|
|
18258
|
+
"standalone",
|
|
18259
|
+
"account",
|
|
18260
|
+
"broker"
|
|
18261
|
+
]),
|
|
18317
18262
|
discoveryMode: string(),
|
|
18318
18263
|
/**
|
|
18319
18264
|
* Which integration-marker cap the addon declared, so the wizard can
|
|
@@ -24032,6 +23977,158 @@ object({
|
|
|
24032
23977
|
schemaVersion: literal(1)
|
|
24033
23978
|
});
|
|
24034
23979
|
//#endregion
|
|
23980
|
+
//#region src/config.ts
|
|
23981
|
+
/**
|
|
23982
|
+
* The AES negotiation modes the wrapped `@apocaliss92/nodegree` client accepts.
|
|
23983
|
+
* Mirrors the library's `encryption` union — kept local so the addon validates
|
|
23984
|
+
* the operator-supplied value at the system boundary without importing a runtime
|
|
23985
|
+
* value the library does not export. `auto` tries V2 (GCM) then V1 (ECB).
|
|
23986
|
+
*/
|
|
23987
|
+
var GREE_ENCRYPTION_MODES = [
|
|
23988
|
+
"auto",
|
|
23989
|
+
"v1",
|
|
23990
|
+
"v2"
|
|
23991
|
+
];
|
|
23992
|
+
var GreeEncryptionSchema = _enum(GREE_ENCRYPTION_MODES);
|
|
23993
|
+
/**
|
|
23994
|
+
* Operator-supplied CONNECTION for ONE Gree air conditioner (standalone mode —
|
|
23995
|
+
* the Reolink / Ecowitt pattern). Gree is LOCAL-ONLY (a directed-broadcast bind
|
|
23996
|
+
* handshake + per-device AES control over UDP), so the connection carries no
|
|
23997
|
+
* credentials — only the AC's LAN address and the UDP tuning knobs. The
|
|
23998
|
+
* `broadcastAddr` is the directed target the bind scan is aimed at: point it at
|
|
23999
|
+
* the AC's own IP (unicast-directed) or the subnet broadcast (e.g.
|
|
24000
|
+
* `192.168.1.255`). Empty = the library's default global broadcast.
|
|
24001
|
+
*/
|
|
24002
|
+
var greeConfigSchema = object({
|
|
24003
|
+
/** The AC's LAN IP address — the directed-bind target. Required for a manual
|
|
24004
|
+
* standalone add (the operator types it). */
|
|
24005
|
+
host: string().default("").describe("Air conditioner LAN IP address"),
|
|
24006
|
+
/** Directed broadcast address for the bind scan (e.g. `192.168.1.255`). Empty =
|
|
24007
|
+
* the library's default global broadcast; when a `host` is set the scan is
|
|
24008
|
+
* aimed at it directly. */
|
|
24009
|
+
broadcastAddr: string().default("").describe("Directed broadcast address for the bind scan"),
|
|
24010
|
+
/** UDP request timeout in ms. */
|
|
24011
|
+
timeoutMs: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(500).max(3e4).default(3e3)).describe("UDP request timeout (ms)"),
|
|
24012
|
+
/** Retry count per UDP request. */
|
|
24013
|
+
retries: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(0).max(10).default(3)).describe("Retries per UDP request"),
|
|
24014
|
+
/** AES negotiation mode. */
|
|
24015
|
+
encryption: GreeEncryptionSchema.default("auto").describe("AES cipher negotiation")
|
|
24016
|
+
});
|
|
24017
|
+
/**
|
|
24018
|
+
* Build the `NodegreeOptions` the wrapped client constructor expects from the
|
|
24019
|
+
* validated connection. Pure: same config in → same options out. `host` and
|
|
24020
|
+
* `broadcastAddr` are NOT constructor options — they are passed to `discover()`
|
|
24021
|
+
* per scan — so they are intentionally omitted here.
|
|
24022
|
+
*/
|
|
24023
|
+
function toNodegreeOptions(config) {
|
|
24024
|
+
return {
|
|
24025
|
+
timeoutMs: config.timeoutMs,
|
|
24026
|
+
retries: config.retries,
|
|
24027
|
+
encryption: config.encryption
|
|
24028
|
+
};
|
|
24029
|
+
}
|
|
24030
|
+
/**
|
|
24031
|
+
* Coerce a loose settings blob (the manual-creation form values, or a persisted
|
|
24032
|
+
* `connection` blob) through the connection schema, applying all defaults.
|
|
24033
|
+
* Throws a `ZodError` on invalid input so the caller surfaces a clear error at
|
|
24034
|
+
* the system boundary.
|
|
24035
|
+
*/
|
|
24036
|
+
function settingsToGreeConfig(settings) {
|
|
24037
|
+
return greeConfigSchema.parse(settings ?? {});
|
|
24038
|
+
}
|
|
24039
|
+
/**
|
|
24040
|
+
* Persisted config for a Gree AC {@link import('@camstack/types').DeviceType.Container}
|
|
24041
|
+
* device. The operator-supplied CONNECTION lives directly on the device (Reolink
|
|
24042
|
+
* pattern) — there is no broker registry. The device owns its own live
|
|
24043
|
+
* `@apocaliss92/nodegree` client keyed on its own `connectionKey`; its AC +
|
|
24044
|
+
* toggle accessory children resolve the bound handle via that key.
|
|
24045
|
+
*
|
|
24046
|
+
* `greeMac` is the durable device identity (resolved by the create-time bind);
|
|
24047
|
+
* `greeIp` is the last-known LAN address; `connectionKey` is the per-device
|
|
24048
|
+
* connection-resolver key (the device's own stableId — decoupled from any
|
|
24049
|
+
* broker); `connection` is the operator's UDP settings; `system`/`name` are
|
|
24050
|
+
* provenance.
|
|
24051
|
+
*/
|
|
24052
|
+
var greeAcDeviceSchema = object({
|
|
24053
|
+
/** Durable AC identity (MAC, lowercased) resolved by the create-time bind. */
|
|
24054
|
+
greeMac: string().min(1).describe("Gree AC MAC address"),
|
|
24055
|
+
/** Last-known LAN IP (re-bound on each activate). */
|
|
24056
|
+
greeIp: string().optional().describe("Last known LAN IP"),
|
|
24057
|
+
/** Per-device connection-resolver key (the device's own stableId). */
|
|
24058
|
+
connectionKey: string().min(1).describe("Per-device connection resolver key"),
|
|
24059
|
+
/** The operator-supplied UDP connection settings the device's client dials. */
|
|
24060
|
+
connection: greeConfigSchema,
|
|
24061
|
+
system: literal("gree").optional(),
|
|
24062
|
+
integrationId: string().optional(),
|
|
24063
|
+
name: string().optional()
|
|
24064
|
+
});
|
|
24065
|
+
/**
|
|
24066
|
+
* Hand-written connection form for the AC device-creation UI (standalone mode —
|
|
24067
|
+
* Reolink pattern). The connection lives on the AC DEVICE config, not in a broker
|
|
24068
|
+
* registry. A `name` field is included so the operator names the AC at creation
|
|
24069
|
+
* time (mirrors the Reolink / Ecowitt creation form).
|
|
24070
|
+
*/
|
|
24071
|
+
function buildConnectionFormSchema() {
|
|
24072
|
+
return { sections: [{
|
|
24073
|
+
id: "identity",
|
|
24074
|
+
title: "Air conditioner",
|
|
24075
|
+
description: "Gree air conditioners are controlled directly over your LAN (no cloud). Enter the AC's IP address; CamStack binds to it over UDP.",
|
|
24076
|
+
columns: 1,
|
|
24077
|
+
fields: [{
|
|
24078
|
+
type: "text",
|
|
24079
|
+
key: "name",
|
|
24080
|
+
label: "Name",
|
|
24081
|
+
required: true,
|
|
24082
|
+
placeholder: "Living room AC"
|
|
24083
|
+
}, {
|
|
24084
|
+
type: "text",
|
|
24085
|
+
key: "host",
|
|
24086
|
+
label: "IP address",
|
|
24087
|
+
required: true,
|
|
24088
|
+
placeholder: "192.168.1.50"
|
|
24089
|
+
}]
|
|
24090
|
+
}, {
|
|
24091
|
+
id: "advanced",
|
|
24092
|
+
title: "Advanced (UDP)",
|
|
24093
|
+
columns: 2,
|
|
24094
|
+
fields: [
|
|
24095
|
+
{
|
|
24096
|
+
type: "text",
|
|
24097
|
+
key: "broadcastAddr",
|
|
24098
|
+
label: "Broadcast address (optional)",
|
|
24099
|
+
required: false,
|
|
24100
|
+
placeholder: "192.168.1.255"
|
|
24101
|
+
},
|
|
24102
|
+
{
|
|
24103
|
+
type: "number",
|
|
24104
|
+
key: "timeoutMs",
|
|
24105
|
+
label: "UDP timeout (ms)",
|
|
24106
|
+
min: 500,
|
|
24107
|
+
max: 3e4,
|
|
24108
|
+
default: 3e3
|
|
24109
|
+
},
|
|
24110
|
+
{
|
|
24111
|
+
type: "number",
|
|
24112
|
+
key: "retries",
|
|
24113
|
+
label: "Retries",
|
|
24114
|
+
min: 0,
|
|
24115
|
+
max: 10,
|
|
24116
|
+
default: 3
|
|
24117
|
+
},
|
|
24118
|
+
{
|
|
24119
|
+
type: "select",
|
|
24120
|
+
key: "encryption",
|
|
24121
|
+
label: "Encryption",
|
|
24122
|
+
default: "auto",
|
|
24123
|
+
options: GREE_ENCRYPTION_MODES.map((m) => ({
|
|
24124
|
+
value: m,
|
|
24125
|
+
label: m.toUpperCase()
|
|
24126
|
+
}))
|
|
24127
|
+
}
|
|
24128
|
+
]
|
|
24129
|
+
}] };
|
|
24130
|
+
}
|
|
24131
|
+
//#endregion
|
|
24035
24132
|
//#region ../../node_modules/@apocaliss92/nodegree/dist/index.js
|
|
24036
24133
|
var GreeError = class extends Error {
|
|
24037
24134
|
constructor(message) {
|
|
@@ -24713,191 +24810,79 @@ var Nodegree = class {
|
|
|
24713
24810
|
}
|
|
24714
24811
|
};
|
|
24715
24812
|
//#endregion
|
|
24716
|
-
//#region src/
|
|
24717
|
-
/**
|
|
24718
|
-
|
|
24719
|
-
|
|
24720
|
-
* the operator-supplied value at the system boundary without importing a runtime
|
|
24721
|
-
* value the library does not export. `auto` tries V2 (GCM) then V1 (ECB).
|
|
24722
|
-
*/
|
|
24723
|
-
var GREE_ENCRYPTION_MODES = [
|
|
24724
|
-
"auto",
|
|
24725
|
-
"v1",
|
|
24726
|
-
"v2"
|
|
24727
|
-
];
|
|
24728
|
-
var GreeEncryptionSchema = _enum(GREE_ENCRYPTION_MODES);
|
|
24729
|
-
/**
|
|
24730
|
-
* Operator-supplied settings for ONE Gree "broker" (= one LAN discovery scope on
|
|
24731
|
-
* one node). Gree is LOCAL-ONLY (UDP broadcast discovery + per-device AES
|
|
24732
|
-
* control), so a broker carries no credentials — only the broadcast target and
|
|
24733
|
-
* the UDP tuning knobs. Each node running the addon owns its own socket; the
|
|
24734
|
-
* `broadcastAddr` lets the operator point discovery at the right subnet
|
|
24735
|
-
* (directed broadcast) when the node has multiple interfaces.
|
|
24736
|
-
*/
|
|
24737
|
-
var greeConfigSchema = object({
|
|
24738
|
-
/** Directed broadcast address for discovery (e.g. `192.168.1.255`). Empty =
|
|
24739
|
-
* the library's default global broadcast. */
|
|
24740
|
-
broadcastAddr: string().default("").describe("Directed broadcast address for discovery"),
|
|
24741
|
-
/** UDP request timeout in ms. */
|
|
24742
|
-
timeoutMs: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(500).max(3e4).default(3e3)).describe("UDP request timeout (ms)"),
|
|
24743
|
-
/** Retry count per UDP request. */
|
|
24744
|
-
retries: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(0).max(10).default(3)).describe("Retries per UDP request"),
|
|
24745
|
-
/** AES negotiation mode. */
|
|
24746
|
-
encryption: GreeEncryptionSchema.default("auto").describe("AES cipher negotiation")
|
|
24747
|
-
});
|
|
24748
|
-
/**
|
|
24749
|
-
* Build the `NodegreeOptions` the wrapped client constructor expects from the
|
|
24750
|
-
* validated addon config. Pure: same config in → same options out. The
|
|
24751
|
-
* `broadcastAddr` is NOT a constructor option — it is passed to `discover()` per
|
|
24752
|
-
* scan — so it is intentionally omitted here.
|
|
24753
|
-
*/
|
|
24754
|
-
function toNodegreeOptions(config) {
|
|
24755
|
-
return {
|
|
24756
|
-
timeoutMs: config.timeoutMs,
|
|
24757
|
-
retries: config.retries,
|
|
24758
|
-
encryption: config.encryption
|
|
24759
|
-
};
|
|
24813
|
+
//#region src/gree-gateway.ts
|
|
24814
|
+
/** Lowercase a MAC for stable map keying (Gree echoes mixed-case MACs). */
|
|
24815
|
+
function macKey(mac) {
|
|
24816
|
+
return mac.toLowerCase();
|
|
24760
24817
|
}
|
|
24761
|
-
/** Top-level addon config — an ordered list of broker entries (default empty). */
|
|
24762
|
-
var greeAddonConfigSchema = object({ brokers: array(object({
|
|
24763
|
-
/** Stable opaque identifier — e.g. 'gree_001', 'gree_002'. */
|
|
24764
|
-
id: string().min(1),
|
|
24765
|
-
/** Human-readable label shown in the admin UI. */
|
|
24766
|
-
name: string().min(1),
|
|
24767
|
-
/** Validated discovery-scope settings. */
|
|
24768
|
-
connection: greeConfigSchema,
|
|
24769
|
-
/** FK to the spawning integration — auto-cleanup on integration delete. */
|
|
24770
|
-
integrationId: string().optional()
|
|
24771
|
-
})).default([]) });
|
|
24772
24818
|
/**
|
|
24773
|
-
*
|
|
24774
|
-
* the
|
|
24775
|
-
*
|
|
24819
|
+
* Per-connection registry that device classes use to reach a bound Gree AC handle
|
|
24820
|
+
* (and the bind-scan rows) for a given AC device (standalone mode — the
|
|
24821
|
+
* connection lives on the AC device, not a broker).
|
|
24822
|
+
*
|
|
24823
|
+
* The kernel constructs device classes with only a `DeviceContext` — it cannot
|
|
24824
|
+
* thread the handle in as a constructor arg. Like the Ecowitt / Dreame addon's
|
|
24825
|
+
* facade resolver, we keep it simple and in-process: the AC device's integration
|
|
24826
|
+
* manager owns the connection surface per `connectionKey` (the device's own
|
|
24827
|
+
* stableId) and publishes it here; the AC's accessory children resolve their live
|
|
24828
|
+
* `AcDevice` handle by `(connectionKey, mac)`.
|
|
24776
24829
|
*/
|
|
24777
|
-
|
|
24778
|
-
|
|
24830
|
+
var GreeConnectionResolver = class {
|
|
24831
|
+
#surfaces = /* @__PURE__ */ new Map();
|
|
24832
|
+
/** Publish or remove the connection surface for a connection key. `null` removes. */
|
|
24833
|
+
set(connectionKey, surface) {
|
|
24834
|
+
if (surface === null) {
|
|
24835
|
+
this.#surfaces.delete(connectionKey);
|
|
24836
|
+
return;
|
|
24837
|
+
}
|
|
24838
|
+
this.#surfaces.set(connectionKey, surface);
|
|
24839
|
+
}
|
|
24840
|
+
/** The bind-scan rows for a connection key, or empty when unknown. */
|
|
24841
|
+
discovered(connectionKey) {
|
|
24842
|
+
return this.#surfaces.get(connectionKey)?.discovered ?? [];
|
|
24843
|
+
}
|
|
24844
|
+
/** True when the connection has a published surface (i.e. it is bound). */
|
|
24845
|
+
has(connectionKey) {
|
|
24846
|
+
return this.#surfaces.has(connectionKey);
|
|
24847
|
+
}
|
|
24848
|
+
/** The active connection keys (one entry per published surface). */
|
|
24849
|
+
list() {
|
|
24850
|
+
return Array.from(this.#surfaces.keys()).map((id) => ({ id }));
|
|
24851
|
+
}
|
|
24852
|
+
/**
|
|
24853
|
+
* Resolve the bound {@link AcDevice} handle for a `(connectionKey, mac)` pair,
|
|
24854
|
+
* or null when the connection is unknown or the AC has not been bound.
|
|
24855
|
+
*/
|
|
24856
|
+
getDevice(connectionKey, mac) {
|
|
24857
|
+
return this.#surfaces.get(connectionKey)?.handles.get(macKey(mac)) ?? null;
|
|
24858
|
+
}
|
|
24859
|
+
/** Remove all registered surfaces (called on full shutdown). */
|
|
24860
|
+
clear() {
|
|
24861
|
+
this.#surfaces.clear();
|
|
24862
|
+
}
|
|
24863
|
+
};
|
|
24864
|
+
/** The single in-process per-connection resolver shared between the AC device's
|
|
24865
|
+
* manager and its accessory children. */
|
|
24866
|
+
var greeConnections = new GreeConnectionResolver();
|
|
24867
|
+
//#endregion
|
|
24868
|
+
//#region src/gree-integration-manager.ts
|
|
24869
|
+
function defaultFacade(config) {
|
|
24870
|
+
return new Nodegree(toNodegreeOptions(config));
|
|
24779
24871
|
}
|
|
24872
|
+
var DEFAULT_POLL_INTERVAL_MS = 3e4;
|
|
24780
24873
|
/**
|
|
24781
|
-
*
|
|
24782
|
-
*
|
|
24783
|
-
*
|
|
24784
|
-
|
|
24785
|
-
|
|
24786
|
-
|
|
24787
|
-
|
|
24788
|
-
|
|
24789
|
-
|
|
24790
|
-
|
|
24791
|
-
|
|
24792
|
-
|
|
24793
|
-
key: "broadcastAddr",
|
|
24794
|
-
label: "Broadcast address (optional)",
|
|
24795
|
-
required: false,
|
|
24796
|
-
placeholder: "192.168.1.255"
|
|
24797
|
-
}]
|
|
24798
|
-
}, {
|
|
24799
|
-
id: "advanced",
|
|
24800
|
-
title: "Advanced (UDP)",
|
|
24801
|
-
columns: 2,
|
|
24802
|
-
fields: [
|
|
24803
|
-
{
|
|
24804
|
-
type: "number",
|
|
24805
|
-
key: "timeoutMs",
|
|
24806
|
-
label: "UDP timeout (ms)",
|
|
24807
|
-
min: 500,
|
|
24808
|
-
max: 3e4,
|
|
24809
|
-
default: 3e3
|
|
24810
|
-
},
|
|
24811
|
-
{
|
|
24812
|
-
type: "number",
|
|
24813
|
-
key: "retries",
|
|
24814
|
-
label: "Retries",
|
|
24815
|
-
min: 0,
|
|
24816
|
-
max: 10,
|
|
24817
|
-
default: 3
|
|
24818
|
-
},
|
|
24819
|
-
{
|
|
24820
|
-
type: "select",
|
|
24821
|
-
key: "encryption",
|
|
24822
|
-
label: "Encryption",
|
|
24823
|
-
default: "auto",
|
|
24824
|
-
options: GREE_ENCRYPTION_MODES.map((m) => ({
|
|
24825
|
-
value: m,
|
|
24826
|
-
label: m.toUpperCase()
|
|
24827
|
-
}))
|
|
24828
|
-
}
|
|
24829
|
-
]
|
|
24830
|
-
}] };
|
|
24831
|
-
}
|
|
24832
|
-
//#endregion
|
|
24833
|
-
//#region src/gree-gateway.ts
|
|
24834
|
-
/** Lowercase a MAC for stable map keying (Gree echoes mixed-case MACs). */
|
|
24835
|
-
function macKey$2(mac) {
|
|
24836
|
-
return mac.toLowerCase();
|
|
24837
|
-
}
|
|
24838
|
-
/**
|
|
24839
|
-
* Per-broker registry that device classes use to reach a bound Gree AC handle
|
|
24840
|
-
* (and the discovery rows) for a given discovery scope.
|
|
24841
|
-
*
|
|
24842
|
-
* The kernel constructs device classes with only a `DeviceContext` — it cannot
|
|
24843
|
-
* thread the handle in as a constructor arg. Like the Dreame addon's
|
|
24844
|
-
* `dreameFacades`, we keep it simple and in-process: the integration manager
|
|
24845
|
-
* owns the connection surface per registered broker and publishes it here;
|
|
24846
|
-
* device classes resolve their live `AcDevice` handle by `(brokerId, mac)`.
|
|
24847
|
-
*/
|
|
24848
|
-
var GreeConnectionResolver = class {
|
|
24849
|
-
#surfaces = /* @__PURE__ */ new Map();
|
|
24850
|
-
/** Publish or remove the connection surface for a broker id. `null` removes. */
|
|
24851
|
-
set(brokerId, surface) {
|
|
24852
|
-
if (surface === null) {
|
|
24853
|
-
this.#surfaces.delete(brokerId);
|
|
24854
|
-
return;
|
|
24855
|
-
}
|
|
24856
|
-
this.#surfaces.set(brokerId, surface);
|
|
24857
|
-
}
|
|
24858
|
-
/** The discovery rows for a broker id, or empty when unknown. */
|
|
24859
|
-
discovered(brokerId) {
|
|
24860
|
-
return this.#surfaces.get(brokerId)?.discovered ?? [];
|
|
24861
|
-
}
|
|
24862
|
-
/** True when the broker has a published surface (i.e. its scope is active). */
|
|
24863
|
-
has(brokerId) {
|
|
24864
|
-
return this.#surfaces.has(brokerId);
|
|
24865
|
-
}
|
|
24866
|
-
/** The active broker ids (one entry per published surface). */
|
|
24867
|
-
list() {
|
|
24868
|
-
return Array.from(this.#surfaces.keys()).map((id) => ({ id }));
|
|
24869
|
-
}
|
|
24870
|
-
/**
|
|
24871
|
-
* Resolve the bound {@link AcDevice} handle for a `(brokerId, mac)` pair, or
|
|
24872
|
-
* null when the broker is unknown or the AC has not been bound.
|
|
24873
|
-
*/
|
|
24874
|
-
getDevice(brokerId, mac) {
|
|
24875
|
-
return this.#surfaces.get(brokerId)?.handles.get(macKey$2(mac)) ?? null;
|
|
24876
|
-
}
|
|
24877
|
-
/** Remove all registered surfaces (called on full shutdown). */
|
|
24878
|
-
clear() {
|
|
24879
|
-
this.#surfaces.clear();
|
|
24880
|
-
}
|
|
24881
|
-
};
|
|
24882
|
-
/** The single in-process per-broker connection resolver shared between the
|
|
24883
|
-
* manager and device instances. */
|
|
24884
|
-
var greeConnections = new GreeConnectionResolver();
|
|
24885
|
-
//#endregion
|
|
24886
|
-
//#region src/gree-integration-manager.ts
|
|
24887
|
-
function defaultFacade(config) {
|
|
24888
|
-
return new Nodegree(toNodegreeOptions(config));
|
|
24889
|
-
}
|
|
24890
|
-
var DEFAULT_POLL_INTERVAL_MS = 3e4;
|
|
24891
|
-
/**
|
|
24892
|
-
* Wraps exactly one `@apocaliss92/nodegree` facade (= one LAN discovery scope)
|
|
24893
|
-
* with observable status + lifecycle. {@link start} runs a discovery scan and
|
|
24894
|
-
* binds (creates) an {@link AcDevice} handle per found AC, publishing the
|
|
24895
|
-
* connection surface on the shared resolver and starting per-AC polling.
|
|
24896
|
-
* {@link stop} closes the facade; {@link applyConnection} does both atomically
|
|
24897
|
-
* when the operator changes the scope settings.
|
|
24898
|
-
*
|
|
24899
|
-
* Mirrors `DreameIntegrationManager`. Binding is best-effort per AC so one
|
|
24900
|
-
* offline unit does not fail the whole scope.
|
|
24874
|
+
* Wraps exactly one `@apocaliss92/nodegree` facade bound to a SINGLE Gree AC
|
|
24875
|
+
* (standalone mode — the connection lives on the AC device). {@link start} runs a
|
|
24876
|
+
* DIRECTED bind scan aimed at the configured `host` (falling back to the
|
|
24877
|
+
* `broadcastAddr` / global broadcast), matches the responder for that host,
|
|
24878
|
+
* binds (creates) its {@link AcDevice} handle, publishes the connection surface
|
|
24879
|
+
* on the shared resolver keyed by the device's `connectionKey`, and starts
|
|
24880
|
+
* polling. {@link stop} closes the facade; {@link applyConnection} does both
|
|
24881
|
+
* atomically when the operator changes the connection.
|
|
24882
|
+
*
|
|
24883
|
+
* {@link bindOnce} is the create-time helper: a static one-shot directed scan
|
|
24884
|
+
* that returns the AC's durable identity (MAC/ip/name) WITHOUT holding a handle,
|
|
24885
|
+
* so `onCreateDevice` can persist the identity before the device exists.
|
|
24901
24886
|
*/
|
|
24902
24887
|
var GreeIntegrationManager = class {
|
|
24903
24888
|
#id;
|
|
@@ -24909,6 +24894,8 @@ var GreeIntegrationManager = class {
|
|
|
24909
24894
|
#surfaceSink;
|
|
24910
24895
|
#pollIntervalMs;
|
|
24911
24896
|
#makeFacade;
|
|
24897
|
+
/** When set, only the responder for this MAC is bound (standalone: one AC). */
|
|
24898
|
+
#expectMac;
|
|
24912
24899
|
#facade = null;
|
|
24913
24900
|
#handles = /* @__PURE__ */ new Map();
|
|
24914
24901
|
#status = "disconnected";
|
|
@@ -24925,6 +24912,7 @@ var GreeIntegrationManager = class {
|
|
|
24925
24912
|
this.#surfaceSink = options.surfaceSink;
|
|
24926
24913
|
this.#pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
24927
24914
|
this.#makeFacade = options.makeFacade ?? defaultFacade;
|
|
24915
|
+
this.#expectMac = options.expectMac !== void 0 ? macKey(options.expectMac) : null;
|
|
24928
24916
|
}
|
|
24929
24917
|
/** The connection settings the manager is currently configured with. */
|
|
24930
24918
|
getConnection() {
|
|
@@ -24938,6 +24926,7 @@ var GreeIntegrationManager = class {
|
|
|
24938
24926
|
kind: "gree",
|
|
24939
24927
|
status: this.#status,
|
|
24940
24928
|
info: {
|
|
24929
|
+
host: this.#connection.host,
|
|
24941
24930
|
broadcastAddr: this.#connection.broadcastAddr,
|
|
24942
24931
|
deviceCount: this.#deviceCount
|
|
24943
24932
|
},
|
|
@@ -24946,14 +24935,14 @@ var GreeIntegrationManager = class {
|
|
|
24946
24935
|
};
|
|
24947
24936
|
}
|
|
24948
24937
|
/**
|
|
24949
|
-
* Build the facade, run a
|
|
24950
|
-
*
|
|
24951
|
-
* on
|
|
24952
|
-
*
|
|
24938
|
+
* Build the facade, run a DIRECTED bind scan (aimed at `host`), bind the
|
|
24939
|
+
* responder for the expected AC, and publish the connection surface. Sets
|
|
24940
|
+
* `connected` on a successful bind. Surfaces a clear bind error on
|
|
24941
|
+
* {@link GreeAuthError}.
|
|
24953
24942
|
*/
|
|
24954
24943
|
async start() {
|
|
24955
24944
|
if (this.#facade !== null) {
|
|
24956
|
-
this.#logger.warn("GreeIntegrationManager: start() called while running — stop first", { tags: {
|
|
24945
|
+
this.#logger.warn("GreeIntegrationManager: start() called while running — stop first", { tags: { connectionKey: this.#id } });
|
|
24957
24946
|
return;
|
|
24958
24947
|
}
|
|
24959
24948
|
this.#status = "connecting";
|
|
@@ -24962,18 +24951,19 @@ var GreeIntegrationManager = class {
|
|
|
24962
24951
|
this.#facade = facade;
|
|
24963
24952
|
try {
|
|
24964
24953
|
const discovered = await facade.discover(this.#discoverOpts());
|
|
24954
|
+
const matched = this.#selectResponders(discovered);
|
|
24965
24955
|
const handles = /* @__PURE__ */ new Map();
|
|
24966
|
-
for (const dev of
|
|
24956
|
+
for (const dev of matched) try {
|
|
24967
24957
|
const ac = await facade.createAc({
|
|
24968
24958
|
ip: dev.ip,
|
|
24969
24959
|
port: dev.port,
|
|
24970
24960
|
mac: dev.mac
|
|
24971
24961
|
});
|
|
24972
24962
|
if (this.#pollIntervalMs > 0) ac.startPolling(this.#pollIntervalMs);
|
|
24973
|
-
handles.set(macKey
|
|
24963
|
+
handles.set(macKey(dev.mac), ac);
|
|
24974
24964
|
} catch (err) {
|
|
24975
24965
|
this.#logger.warn("GreeIntegrationManager: bind (createAc) failed", {
|
|
24976
|
-
tags: {
|
|
24966
|
+
tags: { connectionKey: this.#id },
|
|
24977
24967
|
meta: {
|
|
24978
24968
|
mac: dev.mac,
|
|
24979
24969
|
ip: dev.ip,
|
|
@@ -24983,22 +24973,24 @@ var GreeIntegrationManager = class {
|
|
|
24983
24973
|
}
|
|
24984
24974
|
this.#handles = handles;
|
|
24985
24975
|
const surface = {
|
|
24986
|
-
discovered,
|
|
24976
|
+
discovered: matched,
|
|
24987
24977
|
handles
|
|
24988
24978
|
};
|
|
24989
24979
|
this.#surfaceSink.set(this.#id, surface);
|
|
24990
24980
|
this.#deviceCount = handles.size;
|
|
24991
|
-
this.#status = "connected";
|
|
24992
|
-
this.#error = null;
|
|
24981
|
+
this.#status = handles.size > 0 ? "connected" : "error";
|
|
24982
|
+
this.#error = handles.size > 0 ? null : "AC not reachable — bind returned no handle";
|
|
24993
24983
|
this.#lastCheckedAt = Date.now();
|
|
24994
|
-
|
|
24995
|
-
|
|
24996
|
-
|
|
24997
|
-
|
|
24998
|
-
|
|
24999
|
-
|
|
25000
|
-
|
|
25001
|
-
|
|
24984
|
+
if (handles.size > 0) {
|
|
24985
|
+
this.#onConnected(this.#id);
|
|
24986
|
+
this.#logger.info("GreeIntegrationManager: connected", {
|
|
24987
|
+
tags: { connectionKey: this.#id },
|
|
24988
|
+
meta: {
|
|
24989
|
+
discovered: discovered.length,
|
|
24990
|
+
bound: handles.size
|
|
24991
|
+
}
|
|
24992
|
+
});
|
|
24993
|
+
} else this.#onDisconnected(this.#id);
|
|
25002
24994
|
} catch (err) {
|
|
25003
24995
|
this.#status = "error";
|
|
25004
24996
|
this.#error = err instanceof GreeAuthError ? "device bind failed — check the AC is reachable" : errMsg(err);
|
|
@@ -25006,7 +24998,7 @@ var GreeIntegrationManager = class {
|
|
|
25006
24998
|
this.#surfaceSink.set(this.#id, null);
|
|
25007
24999
|
this.#onDisconnected(this.#id);
|
|
25008
25000
|
this.#logger.warn("GreeIntegrationManager: scan failed", {
|
|
25009
|
-
tags: {
|
|
25001
|
+
tags: { connectionKey: this.#id },
|
|
25010
25002
|
meta: { error: this.#error }
|
|
25011
25003
|
});
|
|
25012
25004
|
throw err;
|
|
@@ -25026,7 +25018,7 @@ var GreeIntegrationManager = class {
|
|
|
25026
25018
|
await facade.close();
|
|
25027
25019
|
} catch (err) {
|
|
25028
25020
|
this.#logger.warn("GreeIntegrationManager: facade close failed", {
|
|
25029
|
-
tags: {
|
|
25021
|
+
tags: { connectionKey: this.#id },
|
|
25030
25022
|
meta: { error: errMsg(err) }
|
|
25031
25023
|
});
|
|
25032
25024
|
}
|
|
@@ -25038,534 +25030,87 @@ var GreeIntegrationManager = class {
|
|
|
25038
25030
|
this.#connection = conn;
|
|
25039
25031
|
await this.start();
|
|
25040
25032
|
}
|
|
25033
|
+
/** Keep only the responder(s) this manager should bind: the one matching the
|
|
25034
|
+
* expected MAC when set, else the one matching the configured host, else all. */
|
|
25035
|
+
#selectResponders(discovered) {
|
|
25036
|
+
if (this.#expectMac !== null) return discovered.filter((d) => macKey(d.mac) === this.#expectMac);
|
|
25037
|
+
const host = this.#connection.host.trim();
|
|
25038
|
+
if (host.length > 0) {
|
|
25039
|
+
const byHost = discovered.filter((d) => d.ip === host);
|
|
25040
|
+
if (byHost.length > 0) return byHost;
|
|
25041
|
+
}
|
|
25042
|
+
return [...discovered];
|
|
25043
|
+
}
|
|
25041
25044
|
#discoverOpts() {
|
|
25042
25045
|
const out = { timeoutMs: this.#connection.timeoutMs };
|
|
25043
|
-
|
|
25046
|
+
const broadcast = resolveBroadcastTarget(this.#connection);
|
|
25047
|
+
if (broadcast.length > 0) out.broadcastAddr = broadcast;
|
|
25044
25048
|
return out;
|
|
25045
25049
|
}
|
|
25046
25050
|
};
|
|
25047
|
-
/** True when two configs are the "same connection" (no facade rebuild needed). */
|
|
25048
|
-
function sameConnection(a, b) {
|
|
25049
|
-
if (a === null || b === null) return false;
|
|
25050
|
-
return a.broadcastAddr === b.broadcastAddr && a.timeoutMs === b.timeoutMs && a.retries === b.retries && a.encryption === b.encryption;
|
|
25051
|
-
}
|
|
25052
|
-
//#endregion
|
|
25053
|
-
//#region src/gree-broker-registry.ts
|
|
25054
|
-
/**
|
|
25055
|
-
* Manages N live Gree discovery scopes ("brokers"), each backed by a
|
|
25056
|
-
* {@link GreeIntegrationManager}. Allocates stable ids (`gree_001`, …), supports
|
|
25057
|
-
* CRUD, an integration FK index for cascade-delete, and lifecycle helpers.
|
|
25058
|
-
* Mirrors `DreoBrokerRegistry`.
|
|
25059
|
-
*/
|
|
25060
|
-
var GreeBrokerRegistry = class {
|
|
25061
|
-
#logger;
|
|
25062
|
-
#onBrokerConnected;
|
|
25063
|
-
#onBrokerDisconnected;
|
|
25064
|
-
#makeManager;
|
|
25065
|
-
#managers = /* @__PURE__ */ new Map();
|
|
25066
|
-
#integrationToBroker = /* @__PURE__ */ new Map();
|
|
25067
|
-
#nextId = 1;
|
|
25068
|
-
constructor(logger, deps = {}) {
|
|
25069
|
-
this.#logger = logger;
|
|
25070
|
-
this.#onBrokerConnected = deps.onBrokerConnected ?? (() => void 0);
|
|
25071
|
-
this.#onBrokerDisconnected = deps.onBrokerDisconnected ?? (() => void 0);
|
|
25072
|
-
this.#makeManager = deps.makeManager ?? ((opts) => new GreeIntegrationManager({
|
|
25073
|
-
id: opts.id,
|
|
25074
|
-
name: opts.name,
|
|
25075
|
-
connection: opts.connection,
|
|
25076
|
-
logger: opts.logger,
|
|
25077
|
-
onConnected: opts.onConnected,
|
|
25078
|
-
onDisconnected: opts.onDisconnected,
|
|
25079
|
-
surfaceSink: greeConnections
|
|
25080
|
-
}));
|
|
25081
|
-
}
|
|
25082
|
-
/** Restore persisted broker entries on boot (best-effort per manager). */
|
|
25083
|
-
async restore(entries) {
|
|
25084
|
-
for (const entry of entries) this.#seedCounter(entry.id);
|
|
25085
|
-
for (const entry of entries) try {
|
|
25086
|
-
await this.#startManager(entry);
|
|
25087
|
-
} catch (err) {
|
|
25088
|
-
this.#logger.warn("GreeBrokerRegistry: failed to restore manager", {
|
|
25089
|
-
tags: { brokerId: entry.id },
|
|
25090
|
-
meta: { error: errMsg(err) }
|
|
25091
|
-
});
|
|
25092
|
-
}
|
|
25093
|
-
}
|
|
25094
|
-
/** Stop all managers and clear state. */
|
|
25095
|
-
async shutdown() {
|
|
25096
|
-
const ids = Array.from(this.#managers.keys());
|
|
25097
|
-
await Promise.all(ids.map(async (id) => {
|
|
25098
|
-
try {
|
|
25099
|
-
await this.#managers.get(id)?.stop();
|
|
25100
|
-
} catch (err) {
|
|
25101
|
-
this.#logger.warn("GreeBrokerRegistry: shutdown stop failed", {
|
|
25102
|
-
tags: { brokerId: id },
|
|
25103
|
-
meta: { error: errMsg(err) }
|
|
25104
|
-
});
|
|
25105
|
-
}
|
|
25106
|
-
}));
|
|
25107
|
-
this.#managers.clear();
|
|
25108
|
-
this.#integrationToBroker.clear();
|
|
25109
|
-
greeConnections.clear();
|
|
25110
|
-
}
|
|
25111
|
-
setOnBrokerConnected(cb) {
|
|
25112
|
-
this.#onBrokerConnected = cb;
|
|
25113
|
-
}
|
|
25114
|
-
setOnBrokerDisconnected(cb) {
|
|
25115
|
-
this.#onBrokerDisconnected = cb;
|
|
25116
|
-
}
|
|
25117
|
-
async createEntry(name, connection, opts = {}) {
|
|
25118
|
-
const id = this.#allocateId();
|
|
25119
|
-
const entry = {
|
|
25120
|
-
id,
|
|
25121
|
-
name,
|
|
25122
|
-
connection,
|
|
25123
|
-
...opts.integrationId !== void 0 ? { integrationId: opts.integrationId } : {}
|
|
25124
|
-
};
|
|
25125
|
-
await this.#startManager(entry);
|
|
25126
|
-
if (opts.integrationId !== void 0) this.#integrationToBroker.set(opts.integrationId, id);
|
|
25127
|
-
return entry;
|
|
25128
|
-
}
|
|
25129
|
-
async removeEntry(id) {
|
|
25130
|
-
const mgr = this.#managers.get(id);
|
|
25131
|
-
if (!mgr) throw new Error(`GreeBrokerRegistry: unknown broker id "${id}"`);
|
|
25132
|
-
for (const [integrationId, brokerId] of this.#integrationToBroker.entries()) if (brokerId === id) this.#integrationToBroker.delete(integrationId);
|
|
25133
|
-
this.#managers.delete(id);
|
|
25134
|
-
try {
|
|
25135
|
-
await mgr.stop();
|
|
25136
|
-
} catch (err) {
|
|
25137
|
-
this.#logger.warn("GreeBrokerRegistry: removeEntry stop failed", {
|
|
25138
|
-
tags: { brokerId: id },
|
|
25139
|
-
meta: { error: errMsg(err) }
|
|
25140
|
-
});
|
|
25141
|
-
}
|
|
25142
|
-
}
|
|
25143
|
-
async updateEntry(id, connection) {
|
|
25144
|
-
const mgr = this.#managers.get(id);
|
|
25145
|
-
if (!mgr) throw new Error(`GreeBrokerRegistry: unknown broker id "${id}"`);
|
|
25146
|
-
await mgr.applyConnection(connection);
|
|
25147
|
-
return {
|
|
25148
|
-
id,
|
|
25149
|
-
name: mgr.getInfo().name,
|
|
25150
|
-
connection
|
|
25151
|
-
};
|
|
25152
|
-
}
|
|
25153
|
-
linkIntegration(integrationId, brokerId) {
|
|
25154
|
-
this.#integrationToBroker.set(integrationId, brokerId);
|
|
25155
|
-
}
|
|
25156
|
-
getBrokerIdByIntegrationId(integrationId) {
|
|
25157
|
-
const brokerId = this.#integrationToBroker.get(integrationId);
|
|
25158
|
-
if (brokerId === void 0) throw new Error(`GreeBrokerRegistry: no broker linked for integration id "${integrationId}"`);
|
|
25159
|
-
return brokerId;
|
|
25160
|
-
}
|
|
25161
|
-
list() {
|
|
25162
|
-
return Array.from(this.#managers.values()).map((m) => m.getInfo());
|
|
25163
|
-
}
|
|
25164
|
-
get(id) {
|
|
25165
|
-
return this.#managers.get(id)?.getInfo() ?? null;
|
|
25166
|
-
}
|
|
25167
|
-
getConnection(id) {
|
|
25168
|
-
return this.#managers.get(id)?.getConnection() ?? null;
|
|
25169
|
-
}
|
|
25170
|
-
size() {
|
|
25171
|
-
return this.#managers.size;
|
|
25172
|
-
}
|
|
25173
|
-
connectedCount() {
|
|
25174
|
-
let count = 0;
|
|
25175
|
-
for (const mgr of this.#managers.values()) if (mgr.getInfo().status === "connected") count++;
|
|
25176
|
-
return count;
|
|
25177
|
-
}
|
|
25178
|
-
async #startManager(entry) {
|
|
25179
|
-
if (this.#managers.has(entry.id)) {
|
|
25180
|
-
if (entry.integrationId !== void 0) this.#integrationToBroker.set(entry.integrationId, entry.id);
|
|
25181
|
-
return;
|
|
25182
|
-
}
|
|
25183
|
-
const mgr = this.#makeManager({
|
|
25184
|
-
id: entry.id,
|
|
25185
|
-
name: entry.name,
|
|
25186
|
-
connection: entry.connection,
|
|
25187
|
-
logger: this.#logger,
|
|
25188
|
-
onConnected: (id) => this.#onBrokerConnected(id),
|
|
25189
|
-
onDisconnected: (id) => this.#onBrokerDisconnected(id)
|
|
25190
|
-
});
|
|
25191
|
-
this.#managers.set(entry.id, mgr);
|
|
25192
|
-
if (entry.integrationId !== void 0) this.#integrationToBroker.set(entry.integrationId, entry.id);
|
|
25193
|
-
await mgr.start();
|
|
25194
|
-
}
|
|
25195
|
-
#allocateId() {
|
|
25196
|
-
const id = `gree_${String(this.#nextId).padStart(3, "0")}`;
|
|
25197
|
-
this.#nextId++;
|
|
25198
|
-
return id;
|
|
25199
|
-
}
|
|
25200
|
-
#seedCounter(id) {
|
|
25201
|
-
const match = /^gree_(\d+)$/.exec(id);
|
|
25202
|
-
if (match === null || match[1] === void 0) return;
|
|
25203
|
-
const n = parseInt(match[1], 10);
|
|
25204
|
-
if (!isNaN(n)) this.#nextId = Math.max(this.#nextId, n + 1);
|
|
25205
|
-
}
|
|
25206
|
-
};
|
|
25207
|
-
//#endregion
|
|
25208
|
-
//#region src/gree-broker-provider.ts
|
|
25209
|
-
/** Kind tag registered by this provider — matches the `listProviders` entry. */
|
|
25210
|
-
var GREE_KIND = "gree";
|
|
25211
25051
|
/**
|
|
25212
|
-
*
|
|
25213
|
-
*
|
|
25214
|
-
*
|
|
25215
|
-
*
|
|
25216
|
-
* Gree carries no secret — a "broker" is just a LAN discovery scope — so the
|
|
25217
|
-
* settings getters expose the connection verbatim (no password redaction).
|
|
25052
|
+
* Resolve the directed-scan target for a connection: the explicit
|
|
25053
|
+
* `broadcastAddr` when set, else the AC `host` (unicast-directed scan), else the
|
|
25054
|
+
* empty string (library default global broadcast).
|
|
25218
25055
|
*/
|
|
25219
|
-
function
|
|
25220
|
-
const
|
|
25221
|
-
|
|
25222
|
-
return
|
|
25223
|
-
list: async ({ kind }) => {
|
|
25224
|
-
if (kind && kind !== GREE_KIND) return [];
|
|
25225
|
-
return registry.list().map((info) => ({
|
|
25226
|
-
...info,
|
|
25227
|
-
addonId: ownerAddonId
|
|
25228
|
-
}));
|
|
25229
|
-
},
|
|
25230
|
-
get: async ({ id }) => {
|
|
25231
|
-
const info = registry.get(id);
|
|
25232
|
-
return info ? {
|
|
25233
|
-
...info,
|
|
25234
|
-
addonId: ownerAddonId
|
|
25235
|
-
} : null;
|
|
25236
|
-
},
|
|
25237
|
-
listProviders: async () => [{
|
|
25238
|
-
addonId: ownerAddonId,
|
|
25239
|
-
kinds: [{
|
|
25240
|
-
kind: GREE_KIND,
|
|
25241
|
-
label: "Gree"
|
|
25242
|
-
}]
|
|
25243
|
-
}],
|
|
25244
|
-
add: async ({ kind, name, settings }) => {
|
|
25245
|
-
if (kind !== GREE_KIND) throw new Error(`provider-gree: only kind '${GREE_KIND}' is handled here (got '${kind}')`);
|
|
25246
|
-
const entry = await registry.createEntry(name, settingsToGreeConfig(settings));
|
|
25247
|
-
await persistBrokers([...getBrokers(), entry]);
|
|
25248
|
-
return { id: entry.id };
|
|
25249
|
-
},
|
|
25250
|
-
remove: async ({ id }) => {
|
|
25251
|
-
if (!owns(id)) return;
|
|
25252
|
-
await registry.removeEntry(id);
|
|
25253
|
-
await cascadeRemoveDevices(id).catch((err) => {
|
|
25254
|
-
logger.warn("gree: broker cascade-remove threw", {
|
|
25255
|
-
tags: { brokerId: id },
|
|
25256
|
-
meta: { error: errMsg(err) }
|
|
25257
|
-
});
|
|
25258
|
-
});
|
|
25259
|
-
await persistBrokers(getBrokers().filter((b) => b.id !== id));
|
|
25260
|
-
},
|
|
25261
|
-
testConnection: async ({ id }) => {
|
|
25262
|
-
if (!owns(id)) return {
|
|
25263
|
-
ok: false,
|
|
25264
|
-
error: "unknown broker"
|
|
25265
|
-
};
|
|
25266
|
-
const info = registry.get(id);
|
|
25267
|
-
if (!info) return {
|
|
25268
|
-
ok: false,
|
|
25269
|
-
error: "unknown broker"
|
|
25270
|
-
};
|
|
25271
|
-
if (info.status === "connected") return {
|
|
25272
|
-
ok: true,
|
|
25273
|
-
latencyMs: info.lastCheckedAt ? Math.max(0, Date.now() - info.lastCheckedAt) : 0
|
|
25274
|
-
};
|
|
25275
|
-
return {
|
|
25276
|
-
ok: false,
|
|
25277
|
-
error: info.error ?? `status: ${info.status}`
|
|
25278
|
-
};
|
|
25279
|
-
},
|
|
25280
|
-
getSettings: async ({ id }) => {
|
|
25281
|
-
const entry = getBrokers().find((b) => b.id === id);
|
|
25282
|
-
if (!entry) return null;
|
|
25283
|
-
return { ...entry.connection };
|
|
25284
|
-
},
|
|
25285
|
-
setSettings: async ({ id, settings }) => {
|
|
25286
|
-
if (!owns(id)) return;
|
|
25287
|
-
if (!getBrokers().find((b) => b.id === id)) return;
|
|
25288
|
-
const parsed = settingsToGreeConfig(settings);
|
|
25289
|
-
const updated = await registry.updateEntry(id, parsed);
|
|
25290
|
-
await persistBrokers(getBrokers().map((b) => b.id === id ? updated : b));
|
|
25291
|
-
},
|
|
25292
|
-
getBrokerConfig: async ({ id }) => {
|
|
25293
|
-
const entry = getBrokers().find((b) => b.id === id);
|
|
25294
|
-
if (!entry) return null;
|
|
25295
|
-
return { ...entry.connection };
|
|
25296
|
-
},
|
|
25297
|
-
getSettingsSchema: async ({ kind }) => {
|
|
25298
|
-
if (kind !== GREE_KIND) return null;
|
|
25299
|
-
return buildConnectionFormSchema();
|
|
25300
|
-
},
|
|
25301
|
-
testSettings: async ({ kind, settings }) => {
|
|
25302
|
-
if (kind !== GREE_KIND) return {
|
|
25303
|
-
ok: false,
|
|
25304
|
-
error: `unsupported kind: ${kind}`
|
|
25305
|
-
};
|
|
25306
|
-
try {
|
|
25307
|
-
settingsToGreeConfig(settings);
|
|
25308
|
-
return { ok: true };
|
|
25309
|
-
} catch (err) {
|
|
25310
|
-
return {
|
|
25311
|
-
ok: false,
|
|
25312
|
-
error: errMsg(err)
|
|
25313
|
-
};
|
|
25314
|
-
}
|
|
25315
|
-
},
|
|
25316
|
-
publish: async () => null,
|
|
25317
|
-
subscribe: async () => ({ subscriptionId: "" }),
|
|
25318
|
-
unsubscribe: async () => void 0,
|
|
25319
|
-
getState: async () => null,
|
|
25320
|
-
getStatus: async () => ({
|
|
25321
|
-
brokerCount: registry.size(),
|
|
25322
|
-
connectedCount: registry.connectedCount()
|
|
25323
|
-
})
|
|
25324
|
-
};
|
|
25325
|
-
}
|
|
25326
|
-
//#endregion
|
|
25327
|
-
//#region src/gree-discovery.ts
|
|
25328
|
-
/** Lowercase a MAC for stable keying (Gree echoes mixed-case MACs). */
|
|
25329
|
-
function macKey$1(mac) {
|
|
25330
|
-
return mac.toLowerCase();
|
|
25056
|
+
function resolveBroadcastTarget(connection) {
|
|
25057
|
+
const broadcast = connection.broadcastAddr.trim();
|
|
25058
|
+
if (broadcast.length > 0) return broadcast;
|
|
25059
|
+
return connection.host.trim();
|
|
25331
25060
|
}
|
|
25332
25061
|
/**
|
|
25333
|
-
*
|
|
25334
|
-
*
|
|
25335
|
-
*
|
|
25336
|
-
*
|
|
25337
|
-
|
|
25338
|
-
|
|
25339
|
-
|
|
25340
|
-
|
|
25341
|
-
const adoptedId = input.adopted.get(macKey$1(device.mac)) ?? null;
|
|
25342
|
-
out.push({
|
|
25343
|
-
childNativeId: macKey$1(device.mac),
|
|
25344
|
-
name: device.name.length > 0 ? device.name : device.mac,
|
|
25345
|
-
type: DeviceType.Thermostat,
|
|
25346
|
-
status: "online",
|
|
25347
|
-
metadata: {
|
|
25348
|
-
serialNumber: device.mac,
|
|
25349
|
-
...device.model !== void 0 ? { model: device.model } : {}
|
|
25350
|
-
},
|
|
25351
|
-
alreadyAdopted: adoptedId !== null,
|
|
25352
|
-
adoptedDeviceId: adoptedId,
|
|
25353
|
-
capabilities: ["climate-control", "fan-control"]
|
|
25354
|
-
});
|
|
25355
|
-
}
|
|
25356
|
-
return out;
|
|
25357
|
-
}
|
|
25358
|
-
//#endregion
|
|
25359
|
-
//#region src/gree-adoption-provider.ts
|
|
25360
|
-
/** The single granularity this provider advertises: whole-device adoption. */
|
|
25361
|
-
var DEVICES_FILTER = {
|
|
25362
|
-
id: "devices",
|
|
25363
|
-
label: "Devices",
|
|
25364
|
-
isDefault: true
|
|
25365
|
-
};
|
|
25366
|
-
function macKey(mac) {
|
|
25367
|
-
return mac.toLowerCase();
|
|
25368
|
-
}
|
|
25369
|
-
/** Build a `mac → CamStack deviceId` map for a single broker. */
|
|
25370
|
-
async function adoptedMapForBroker(brokerId, listAdoptedGree) {
|
|
25371
|
-
const all = await listAdoptedGree();
|
|
25372
|
-
const map = /* @__PURE__ */ new Map();
|
|
25373
|
-
for (const device of all) if (device.config["system"] === "gree" && device.config["brokerId"] === brokerId) {
|
|
25374
|
-
const mac = device.config["greeMac"];
|
|
25375
|
-
if (typeof mac === "string") map.set(macKey(mac), device.id);
|
|
25376
|
-
}
|
|
25377
|
-
return map;
|
|
25378
|
-
}
|
|
25379
|
-
/**
|
|
25380
|
-
* Construct the `device-adoption` cap provider for the Gree addon. Pure builder:
|
|
25381
|
-
* all side-effecting deps are injected. Mirrors `buildDreoAdoptionProvider` with
|
|
25382
|
-
* a single `devices` granularity (one Container per discovered AC).
|
|
25062
|
+
* Create-time one-shot directed bind: build a throwaway facade, run a directed
|
|
25063
|
+
* scan (aimed at the connection's `host`/`broadcastAddr`), bind the matching
|
|
25064
|
+
* responder to prove the AC is reachable and learn its durable identity, then
|
|
25065
|
+
* close the facade. Returns the AC's identity (MAC/ip/name/model). Throws when no
|
|
25066
|
+
* AC responds or the bind fails — surfaced to the operator in the Add modal.
|
|
25067
|
+
*
|
|
25068
|
+
* The returned identity is what {@link import('./config.js').greeAcDeviceSchema}
|
|
25069
|
+
* persists on the new device; the live per-device manager re-binds on activate.
|
|
25383
25070
|
*/
|
|
25384
|
-
function
|
|
25385
|
-
const {
|
|
25386
|
-
|
|
25387
|
-
|
|
25388
|
-
|
|
25389
|
-
|
|
25071
|
+
async function bindOnce(input) {
|
|
25072
|
+
const { connection, logger } = input;
|
|
25073
|
+
const facade = (input.makeFacade ?? defaultFacade)(connection);
|
|
25074
|
+
try {
|
|
25075
|
+
const broadcast = resolveBroadcastTarget(connection);
|
|
25076
|
+
const opts = { timeoutMs: connection.timeoutMs };
|
|
25077
|
+
if (broadcast.length > 0) opts.broadcastAddr = broadcast;
|
|
25078
|
+
const discovered = await facade.discover(opts);
|
|
25079
|
+
const host = connection.host.trim();
|
|
25080
|
+
const target = host.length > 0 ? discovered.find((d) => d.ip === host) ?? discovered[0] : discovered[0];
|
|
25081
|
+
if (target === void 0) throw new Error(host.length > 0 ? `no Gree AC responded at ${host}` : "no Gree AC responded to the discovery scan");
|
|
25082
|
+
const ac = await facade.createAc({
|
|
25083
|
+
ip: target.ip,
|
|
25084
|
+
port: target.port,
|
|
25085
|
+
mac: target.mac
|
|
25390
25086
|
});
|
|
25391
|
-
}
|
|
25392
|
-
function applyCandidateTextFilter(cands, filterText) {
|
|
25393
|
-
let filtered = [...cands];
|
|
25394
|
-
if (filterText === void 0) return filtered;
|
|
25395
|
-
const { search, adoptedOnly, unadoptedOnly } = filterText;
|
|
25396
|
-
if (search !== void 0 && search.length > 0) {
|
|
25397
|
-
const lower = search.toLowerCase();
|
|
25398
|
-
filtered = filtered.filter((c) => c.name.toLowerCase().includes(lower) || (c.metadata.model?.toLowerCase().includes(lower) ?? false) || c.childNativeId.toLowerCase().includes(lower));
|
|
25399
|
-
}
|
|
25400
|
-
if (adoptedOnly === true) filtered = filtered.filter((c) => c.alreadyAdopted);
|
|
25401
|
-
if (unadoptedOnly === true) filtered = filtered.filter((c) => !c.alreadyAdopted);
|
|
25402
|
-
return filtered;
|
|
25403
|
-
}
|
|
25404
|
-
return {
|
|
25405
|
-
listCandidateFilters: async () => ({ filters: [DEVICES_FILTER] }),
|
|
25406
|
-
listCandidates: async ({ integrationId, page, pageSize, filterText }) => {
|
|
25407
|
-
const filtered = applyCandidateTextFilter(await allCandidatesForBroker(await getBrokerIdForIntegration(integrationId)), filterText);
|
|
25408
|
-
const start = (page - 1) * pageSize;
|
|
25409
|
-
return {
|
|
25410
|
-
candidates: filtered.slice(start, start + pageSize),
|
|
25411
|
-
totalCount: filtered.length,
|
|
25412
|
-
page,
|
|
25413
|
-
pageSize
|
|
25414
|
-
};
|
|
25415
|
-
},
|
|
25416
|
-
getCandidate: async ({ integrationId, childNativeId }) => {
|
|
25417
|
-
return (await allCandidatesForBroker(await getBrokerIdForIntegration(integrationId))).find((c) => c.childNativeId === childNativeId) ?? null;
|
|
25418
|
-
},
|
|
25419
|
-
getStatus: async () => {
|
|
25420
|
-
try {
|
|
25421
|
-
const brokers = registry.list();
|
|
25422
|
-
let candidateCount = 0;
|
|
25423
|
-
for (const broker of brokers) candidateCount += (await allCandidatesForBroker(broker.id)).length;
|
|
25424
|
-
const adoptedCount = (await listAdoptedGree()).filter((d) => d.config["system"] === "gree").length;
|
|
25425
|
-
return {
|
|
25426
|
-
lastDiscoveryAt: Date.now(),
|
|
25427
|
-
candidateCount,
|
|
25428
|
-
adoptedCount,
|
|
25429
|
-
lastError: null
|
|
25430
|
-
};
|
|
25431
|
-
} catch (err) {
|
|
25432
|
-
logger.warn("gree adoption: getStatus failed", { meta: { error: errMsg(err) } });
|
|
25433
|
-
return {
|
|
25434
|
-
lastDiscoveryAt: null,
|
|
25435
|
-
candidateCount: 0,
|
|
25436
|
-
adoptedCount: 0,
|
|
25437
|
-
lastError: errMsg(err)
|
|
25438
|
-
};
|
|
25439
|
-
}
|
|
25440
|
-
},
|
|
25441
|
-
refresh: async ({ integrationId }) => {
|
|
25442
|
-
const brokerId = await getBrokerIdForIntegration(integrationId);
|
|
25443
|
-
const candidateCount = (await allCandidatesForBroker(brokerId)).length;
|
|
25444
|
-
const adoptedCount = (await listAdoptedGree()).filter((d) => d.config["system"] === "gree" && d.config["brokerId"] === brokerId).length;
|
|
25445
|
-
return {
|
|
25446
|
-
lastDiscoveryAt: Date.now(),
|
|
25447
|
-
candidateCount,
|
|
25448
|
-
adoptedCount,
|
|
25449
|
-
lastError: null
|
|
25450
|
-
};
|
|
25451
|
-
},
|
|
25452
|
-
adopt: async ({ integrationId, childNativeIds, perCandidate }) => {
|
|
25453
|
-
const brokerId = await getBrokerIdForIntegration(integrationId);
|
|
25454
|
-
if (!registry.has(brokerId)) throw new Error(`gree adopt: broker ${brokerId} not active`);
|
|
25455
|
-
const devices = registry.discovered(brokerId);
|
|
25456
|
-
const adopted = [];
|
|
25457
|
-
let failures = 0;
|
|
25458
|
-
for (const childNativeId of childNativeIds) try {
|
|
25459
|
-
const dev = devices.find((d) => macKey(d.mac) === macKey(childNativeId));
|
|
25460
|
-
if (dev === void 0) {
|
|
25461
|
-
logger.warn("gree adopt: device not found on broker — skipping", { meta: {
|
|
25462
|
-
childNativeId,
|
|
25463
|
-
brokerId
|
|
25464
|
-
} });
|
|
25465
|
-
failures++;
|
|
25466
|
-
continue;
|
|
25467
|
-
}
|
|
25468
|
-
const name = perCandidate?.[childNativeId]?.name ?? dev.name ?? dev.mac;
|
|
25469
|
-
const candidate = buildGreeCandidates({
|
|
25470
|
-
devices: [dev],
|
|
25471
|
-
adopted: /* @__PURE__ */ new Map()
|
|
25472
|
-
})[0];
|
|
25473
|
-
if (candidate === void 0) {
|
|
25474
|
-
failures++;
|
|
25475
|
-
continue;
|
|
25476
|
-
}
|
|
25477
|
-
const { deviceId, accessoryDeviceIds } = await adoptDevice({
|
|
25478
|
-
greeMac: dev.mac,
|
|
25479
|
-
greeIp: dev.ip,
|
|
25480
|
-
brokerId,
|
|
25481
|
-
integrationId,
|
|
25482
|
-
type: candidate.type,
|
|
25483
|
-
name
|
|
25484
|
-
});
|
|
25485
|
-
adopted.push({
|
|
25486
|
-
childNativeId: macKey(childNativeId),
|
|
25487
|
-
parentDeviceId: deviceId,
|
|
25488
|
-
accessoryDeviceIds
|
|
25489
|
-
});
|
|
25490
|
-
} catch (err) {
|
|
25491
|
-
logger.warn("gree adopt: failed to adopt device", { meta: {
|
|
25492
|
-
childNativeId,
|
|
25493
|
-
brokerId,
|
|
25494
|
-
error: errMsg(err)
|
|
25495
|
-
} });
|
|
25496
|
-
failures++;
|
|
25497
|
-
}
|
|
25498
|
-
if (adopted.length === 0 && failures > 0) throw new Error(`gree adopt: all ${failures} adopt(s) failed`);
|
|
25499
|
-
return { adopted };
|
|
25500
|
-
},
|
|
25501
|
-
release: async ({ camDeviceId }) => {
|
|
25502
|
-
await removeDevice(camDeviceId);
|
|
25503
|
-
},
|
|
25504
|
-
resync: async ({ camDeviceId }) => {
|
|
25505
|
-
const cfg = await findDeviceConfig(camDeviceId);
|
|
25506
|
-
if (cfg === null) throw new Error(`gree resync: device ${camDeviceId} not found`);
|
|
25507
|
-
if (cfg["system"] !== "gree") throw new Error(`gree resync: device ${camDeviceId} is not a Gree device`);
|
|
25508
|
-
const brokerId = String(cfg["brokerId"]);
|
|
25509
|
-
const greeMac = String(cfg["greeMac"]);
|
|
25510
|
-
if (!registry.discovered(brokerId).some((d) => macKey(d.mac) === macKey(greeMac))) throw new Error(`gree resync: device ${greeMac} no longer present on broker ${brokerId}`);
|
|
25511
|
-
return {
|
|
25512
|
-
changed: false,
|
|
25513
|
-
rebuiltChildren: 0
|
|
25514
|
-
};
|
|
25515
|
-
}
|
|
25516
|
-
};
|
|
25517
|
-
}
|
|
25518
|
-
//#endregion
|
|
25519
|
-
//#region src/gree-broker-device-cascade.ts
|
|
25520
|
-
/**
|
|
25521
|
-
* Remove every adopted Gree PARENT device (children cascade via the kernel)
|
|
25522
|
-
* whose persisted config carries `{ system: 'gree', brokerId }`. Used when a
|
|
25523
|
-
* broker is removed (broker.remove) and when its spawning integration is
|
|
25524
|
-
* deleted. Best-effort per device; returns the count removed. Mirrors the
|
|
25525
|
-
* Dreo cascade helper.
|
|
25526
|
-
*/
|
|
25527
|
-
async function cascadeRemoveDevicesForBroker(input) {
|
|
25528
|
-
const { reg, devices, addonId, brokerId, logger } = input;
|
|
25529
|
-
let removed = 0;
|
|
25530
|
-
for (const d of reg.getAllForAddon(addonId)) {
|
|
25531
|
-
if (d.parentDeviceId !== null) continue;
|
|
25532
|
-
const cfg = await devices.loadConfig(d.id).catch(() => ({}));
|
|
25533
|
-
if (cfg["system"] !== "gree" || cfg["brokerId"] !== brokerId) continue;
|
|
25534
25087
|
try {
|
|
25535
|
-
|
|
25536
|
-
|
|
25537
|
-
|
|
25538
|
-
|
|
25539
|
-
|
|
25540
|
-
|
|
25541
|
-
|
|
25542
|
-
|
|
25543
|
-
|
|
25544
|
-
|
|
25545
|
-
|
|
25088
|
+
ac.stopPolling();
|
|
25089
|
+
} catch {}
|
|
25090
|
+
return {
|
|
25091
|
+
mac: target.mac,
|
|
25092
|
+
ip: target.ip,
|
|
25093
|
+
port: target.port,
|
|
25094
|
+
name: target.name.length > 0 ? target.name : target.mac,
|
|
25095
|
+
...target.model !== void 0 ? { model: target.model } : {}
|
|
25096
|
+
};
|
|
25097
|
+
} catch (err) {
|
|
25098
|
+
const message = err instanceof GreeAuthError ? "device bind failed — check the AC is reachable and the encryption mode" : errMsg(err);
|
|
25099
|
+
logger.warn("gree bindOnce failed", { meta: {
|
|
25100
|
+
host: connection.host,
|
|
25101
|
+
error: message
|
|
25102
|
+
} });
|
|
25103
|
+
throw new Error(message);
|
|
25104
|
+
} finally {
|
|
25105
|
+
try {
|
|
25106
|
+
await facade.close();
|
|
25107
|
+
} catch {}
|
|
25546
25108
|
}
|
|
25547
|
-
return removed;
|
|
25548
25109
|
}
|
|
25549
|
-
|
|
25550
|
-
|
|
25551
|
-
|
|
25552
|
-
|
|
25553
|
-
* `devices` is the addon-scoped device list; every Gree top-level Container
|
|
25554
|
-
* carries `{ system: 'gree', brokerId }` in its config blob.
|
|
25555
|
-
*
|
|
25556
|
-
* Churn-free: skips any device already in the target state. Returns the count of
|
|
25557
|
-
* devices actually transitioned. Mirrors the Dreo offline helper.
|
|
25558
|
-
*/
|
|
25559
|
-
function setBrokerDevicesOnline(devices, brokerId, online) {
|
|
25560
|
-
let count = 0;
|
|
25561
|
-
for (const dev of devices) {
|
|
25562
|
-
if (dev.config.get("system") !== "gree") continue;
|
|
25563
|
-
if (dev.config.get("brokerId") !== brokerId) continue;
|
|
25564
|
-
if (dev.online === online) continue;
|
|
25565
|
-
dev.markOnline(online);
|
|
25566
|
-
count += 1;
|
|
25567
|
-
}
|
|
25568
|
-
return count;
|
|
25110
|
+
/** True when two configs are the "same connection" (no facade rebuild needed). */
|
|
25111
|
+
function sameConnection(a, b) {
|
|
25112
|
+
if (a === null || b === null) return false;
|
|
25113
|
+
return a.host === b.host && a.broadcastAddr === b.broadcastAddr && a.timeoutMs === b.timeoutMs && a.retries === b.retries && a.encryption === b.encryption;
|
|
25569
25114
|
}
|
|
25570
25115
|
//#endregion
|
|
25571
25116
|
//#region src/gree-domain-mapping.ts
|
|
@@ -25813,12 +25358,13 @@ var FAN_COLD_START = {
|
|
|
25813
25358
|
};
|
|
25814
25359
|
/**
|
|
25815
25360
|
* Persisted config every Gree AC accessory child carries. `greeMac` resolves the
|
|
25816
|
-
* bound handle from the connection resolver; `
|
|
25817
|
-
*
|
|
25361
|
+
* bound handle from the connection resolver; `connectionKey` selects the parent
|
|
25362
|
+
* AC device's live connection (its own stableId); `system`/`integrationId` are
|
|
25363
|
+
* provenance.
|
|
25818
25364
|
*/
|
|
25819
25365
|
var greeAcSchema = object({
|
|
25820
25366
|
greeMac: string().min(1).describe("Gree AC MAC address"),
|
|
25821
|
-
|
|
25367
|
+
connectionKey: string().min(1).describe("Per-device connection resolver key"),
|
|
25822
25368
|
system: literal("gree").optional(),
|
|
25823
25369
|
integrationId: string().optional()
|
|
25824
25370
|
});
|
|
@@ -25869,13 +25415,13 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
25869
25415
|
return flags;
|
|
25870
25416
|
}
|
|
25871
25417
|
greeMac;
|
|
25872
|
-
|
|
25418
|
+
connectionKey;
|
|
25873
25419
|
stateChangedUnsub = null;
|
|
25874
25420
|
constructor(ctx) {
|
|
25875
25421
|
const persisted = greeAcSchema.parse(ctx.persistedConfig ?? {});
|
|
25876
25422
|
super(ctx, greeAcSchema, { type: ctx.deviceMeta.type });
|
|
25877
25423
|
this.greeMac = persisted.greeMac;
|
|
25878
|
-
this.
|
|
25424
|
+
this.connectionKey = persisted.connectionKey;
|
|
25879
25425
|
this.online = true;
|
|
25880
25426
|
this.updateSourceInfo({
|
|
25881
25427
|
id: this.greeMac,
|
|
@@ -25883,7 +25429,7 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
25883
25429
|
});
|
|
25884
25430
|
}
|
|
25885
25431
|
resolveAc() {
|
|
25886
|
-
return greeConnections.getDevice(this.
|
|
25432
|
+
return greeConnections.getDevice(this.connectionKey, this.greeMac);
|
|
25887
25433
|
}
|
|
25888
25434
|
requireAc() {
|
|
25889
25435
|
const ac = this.resolveAc();
|
|
@@ -26043,9 +25589,10 @@ var SWITCH_COLD_START = {
|
|
|
26043
25589
|
};
|
|
26044
25590
|
/**
|
|
26045
25591
|
* Persisted config every Gree toggle accessory child carries. Mirrors
|
|
26046
|
-
* {@link greeAcSchema}: `greeMac` resolves the bound handle, `
|
|
26047
|
-
* the
|
|
26048
|
-
* which of the four boolean device flags this child
|
|
25592
|
+
* {@link greeAcSchema}: `greeMac` resolves the bound handle, `connectionKey`
|
|
25593
|
+
* selects the parent AC device's live connection, `system`/`integrationId` are
|
|
25594
|
+
* provenance. `toggle` selects which of the four boolean device flags this child
|
|
25595
|
+
* drives.
|
|
26049
25596
|
*/
|
|
26050
25597
|
var greeToggleSchema = object({
|
|
26051
25598
|
toggle: _enum([
|
|
@@ -26055,7 +25602,7 @@ var greeToggleSchema = object({
|
|
|
26055
25602
|
"freshAir"
|
|
26056
25603
|
]).describe("Which Gree boolean flag"),
|
|
26057
25604
|
greeMac: string().min(1).describe("Gree AC MAC address"),
|
|
26058
|
-
|
|
25605
|
+
connectionKey: string().min(1).describe("Per-device connection resolver key"),
|
|
26059
25606
|
system: literal("gree").optional(),
|
|
26060
25607
|
integrationId: string().optional()
|
|
26061
25608
|
});
|
|
@@ -26073,14 +25620,14 @@ var GreeToggleDevice = class extends BaseDevice$1 {
|
|
|
26073
25620
|
features = [];
|
|
26074
25621
|
toggle;
|
|
26075
25622
|
greeMac;
|
|
26076
|
-
|
|
25623
|
+
connectionKey;
|
|
26077
25624
|
stateChangedUnsub = null;
|
|
26078
25625
|
constructor(ctx) {
|
|
26079
25626
|
const persisted = greeToggleSchema.parse(ctx.persistedConfig ?? {});
|
|
26080
25627
|
super(ctx, greeToggleSchema, { type: ctx.deviceMeta.type });
|
|
26081
25628
|
this.toggle = persisted.toggle;
|
|
26082
25629
|
this.greeMac = persisted.greeMac;
|
|
26083
|
-
this.
|
|
25630
|
+
this.connectionKey = persisted.connectionKey;
|
|
26084
25631
|
this.online = true;
|
|
26085
25632
|
this.updateSourceInfo({
|
|
26086
25633
|
id: `${this.greeMac}:${this.toggle}`,
|
|
@@ -26088,7 +25635,7 @@ var GreeToggleDevice = class extends BaseDevice$1 {
|
|
|
26088
25635
|
});
|
|
26089
25636
|
}
|
|
26090
25637
|
resolveAc() {
|
|
26091
|
-
return greeConnections.getDevice(this.
|
|
25638
|
+
return greeConnections.getDevice(this.connectionKey, this.greeMac);
|
|
26092
25639
|
}
|
|
26093
25640
|
requireAc() {
|
|
26094
25641
|
const ac = this.resolveAc();
|
|
@@ -26188,49 +25735,117 @@ var GreeToggleDevice = class extends BaseDevice$1 {
|
|
|
26188
25735
|
//#endregion
|
|
26189
25736
|
//#region src/devices/gree-container-device.ts
|
|
26190
25737
|
/**
|
|
26191
|
-
*
|
|
26192
|
-
*
|
|
26193
|
-
*
|
|
26194
|
-
*
|
|
26195
|
-
*
|
|
26196
|
-
|
|
26197
|
-
|
|
26198
|
-
greeMac: string().min(1).describe("Gree AC MAC address"),
|
|
26199
|
-
greeIp: string().optional().describe("Last known LAN IP"),
|
|
26200
|
-
brokerId: string().min(1).describe("Registry broker id"),
|
|
26201
|
-
integrationId: string().optional(),
|
|
26202
|
-
system: literal("gree").optional(),
|
|
26203
|
-
name: string().optional()
|
|
26204
|
-
});
|
|
26205
|
-
/**
|
|
26206
|
-
* Parent Container device for a single Gree AC. Owns no control caps itself — it
|
|
26207
|
-
* declares `getAccessoryChildren()` so the kernel auto-spawns the single
|
|
26208
|
-
* {@link GreeAcDevice} accessory child (Thermostat) and reconciles it across
|
|
26209
|
-
* reboots. Mirrors `DreoContainerDevice`.
|
|
25738
|
+
* Standalone-mode parent Container device for a single Gree AC (Reolink /
|
|
25739
|
+
* Ecowitt pattern). The operator-supplied CONNECTION lives on THIS device's
|
|
25740
|
+
* config; the device OWNS exactly one live `@apocaliss92/nodegree` client (via a
|
|
25741
|
+
* {@link GreeIntegrationManager}) keyed on its own `connectionKey` and published
|
|
25742
|
+
* on the in-process {@link greeConnections} resolver so its AC + toggle accessory
|
|
25743
|
+
* children read the live bound handle. There is NO broker and NO device-adoption
|
|
25744
|
+
* cap.
|
|
26210
25745
|
*
|
|
26211
|
-
*
|
|
26212
|
-
*
|
|
26213
|
-
*
|
|
26214
|
-
*
|
|
26215
|
-
*
|
|
25746
|
+
* It owns no control caps itself — it declares `getAccessoryChildren()` so the
|
|
25747
|
+
* kernel auto-spawns the single {@link GreeAcDevice} accessory child (Thermostat,
|
|
25748
|
+
* carrying `climate-control` + `fan-control` with the swing entities) plus
|
|
25749
|
+
* presence-gated {@link DeviceType.Switch} children for the panel-light / X-Fan /
|
|
25750
|
+
* health / fresh-air boolean flags (mirroring the HA Gree integration).
|
|
25751
|
+
* Capability presence is resolved from the bound handle's `AcCapabilities` (or
|
|
25752
|
+
* model defaults when no live handle).
|
|
26216
25753
|
*/
|
|
26217
25754
|
var GreeContainerDevice = class extends BaseDevice$1 {
|
|
26218
25755
|
features = [DeviceFeature.Resyncable];
|
|
26219
25756
|
greeMac;
|
|
26220
|
-
|
|
25757
|
+
connectionKey;
|
|
26221
25758
|
integrationId;
|
|
25759
|
+
connection;
|
|
25760
|
+
manager = null;
|
|
26222
25761
|
constructor(ctx) {
|
|
26223
|
-
const cfg =
|
|
26224
|
-
super(ctx,
|
|
25762
|
+
const cfg = greeAcDeviceSchema.parse(ctx.persistedConfig ?? {});
|
|
25763
|
+
super(ctx, greeAcDeviceSchema, { type: DeviceType.Container });
|
|
26225
25764
|
this.greeMac = cfg.greeMac;
|
|
26226
|
-
this.
|
|
25765
|
+
this.connectionKey = cfg.connectionKey;
|
|
26227
25766
|
this.integrationId = cfg.integrationId;
|
|
26228
|
-
this.
|
|
25767
|
+
this.connection = cfg.connection;
|
|
25768
|
+
this.online = greeConnections.getDevice(this.connectionKey, this.greeMac) !== null;
|
|
26229
25769
|
this.updateSourceInfo({
|
|
26230
25770
|
id: this.greeMac,
|
|
26231
25771
|
system: "gree"
|
|
26232
25772
|
});
|
|
26233
25773
|
}
|
|
25774
|
+
/** Adopt a pre-built manager (created by `onCreateDevice` so the live client
|
|
25775
|
+
* binds once at create time) — mirrors `EcowittGatewayDevice.adoptManager`. */
|
|
25776
|
+
adoptManager(manager) {
|
|
25777
|
+
this.manager = manager;
|
|
25778
|
+
}
|
|
25779
|
+
async onActivate() {
|
|
25780
|
+
await super.onActivate();
|
|
25781
|
+
if (this.manager === null) this.ensureManager().catch((err) => {
|
|
25782
|
+
this.ctx.logger.warn("Gree AC initial bind failed", { meta: {
|
|
25783
|
+
greeMac: this.greeMac,
|
|
25784
|
+
error: errMsg(err)
|
|
25785
|
+
} });
|
|
25786
|
+
});
|
|
25787
|
+
}
|
|
25788
|
+
async removeDevice() {
|
|
25789
|
+
const mgr = this.manager;
|
|
25790
|
+
this.manager = null;
|
|
25791
|
+
if (mgr) try {
|
|
25792
|
+
await mgr.stop();
|
|
25793
|
+
} catch (err) {
|
|
25794
|
+
this.ctx.logger.warn("Gree AC: client stop failed", { meta: {
|
|
25795
|
+
greeMac: this.greeMac,
|
|
25796
|
+
error: errMsg(err)
|
|
25797
|
+
} });
|
|
25798
|
+
}
|
|
25799
|
+
}
|
|
25800
|
+
async applySettingsPatch(patch) {
|
|
25801
|
+
if (!("connection" in patch)) return;
|
|
25802
|
+
const parsed = greeAcDeviceSchema.shape.connection.parse(patch["connection"]);
|
|
25803
|
+
await this.config.setAll({ connection: parsed });
|
|
25804
|
+
this.connection = parsed;
|
|
25805
|
+
const mgr = this.manager;
|
|
25806
|
+
if (mgr) try {
|
|
25807
|
+
await mgr.applyConnection(parsed);
|
|
25808
|
+
} catch (err) {
|
|
25809
|
+
this.ctx.logger.warn("Gree AC: applyConnection failed", { meta: {
|
|
25810
|
+
greeMac: this.greeMac,
|
|
25811
|
+
error: errMsg(err)
|
|
25812
|
+
} });
|
|
25813
|
+
}
|
|
25814
|
+
else await this.ensureManager().catch((err) => {
|
|
25815
|
+
this.ctx.logger.warn("Gree AC re-bind after settings change failed", { meta: {
|
|
25816
|
+
greeMac: this.greeMac,
|
|
25817
|
+
error: errMsg(err)
|
|
25818
|
+
} });
|
|
25819
|
+
});
|
|
25820
|
+
}
|
|
25821
|
+
/** Build + start the per-device nodegree client (if not already running). */
|
|
25822
|
+
async ensureManager() {
|
|
25823
|
+
if (this.manager !== null) return;
|
|
25824
|
+
const mgr = new GreeIntegrationManager({
|
|
25825
|
+
id: this.connectionKey,
|
|
25826
|
+
name: this.name,
|
|
25827
|
+
connection: this.connection,
|
|
25828
|
+
logger: this.ctx.logger,
|
|
25829
|
+
onConnected: () => this.setAcOnline(true),
|
|
25830
|
+
onDisconnected: () => this.setAcOnline(false),
|
|
25831
|
+
surfaceSink: greeConnections,
|
|
25832
|
+
expectMac: this.greeMac
|
|
25833
|
+
});
|
|
25834
|
+
this.manager = mgr;
|
|
25835
|
+
await mgr.start();
|
|
25836
|
+
}
|
|
25837
|
+
setAcOnline(online) {
|
|
25838
|
+
if (this.online === online) return;
|
|
25839
|
+
this.markOnline(online);
|
|
25840
|
+
this.setChildrenOnline(online).catch(() => {});
|
|
25841
|
+
}
|
|
25842
|
+
async setChildrenOnline(online) {
|
|
25843
|
+
const children = await this.ctx.devices.getChildren(this.id).catch(() => []);
|
|
25844
|
+
for (const child of children) {
|
|
25845
|
+
if (child.online === online) continue;
|
|
25846
|
+
child.markOnline(online);
|
|
25847
|
+
}
|
|
25848
|
+
}
|
|
26234
25849
|
getAccessoryChildren() {
|
|
26235
25850
|
return [{
|
|
26236
25851
|
stableIdSuffix: "ac",
|
|
@@ -26242,7 +25857,7 @@ var GreeContainerDevice = class extends BaseDevice$1 {
|
|
|
26242
25857
|
},
|
|
26243
25858
|
config: {
|
|
26244
25859
|
greeMac: this.greeMac,
|
|
26245
|
-
|
|
25860
|
+
connectionKey: this.connectionKey,
|
|
26246
25861
|
system: "gree",
|
|
26247
25862
|
...this.integrationId !== void 0 ? { integrationId: this.integrationId } : {}
|
|
26248
25863
|
},
|
|
@@ -26256,7 +25871,7 @@ var GreeContainerDevice = class extends BaseDevice$1 {
|
|
|
26256
25871
|
* defaults when no live handle is bound yet).
|
|
26257
25872
|
*/
|
|
26258
25873
|
toggleChildren() {
|
|
26259
|
-
const caps = greeConnections.getDevice(this.
|
|
25874
|
+
const caps = greeConnections.getDevice(this.connectionKey, this.greeMac)?.capabilities ?? DEFAULT_AC_CAPABILITIES;
|
|
26260
25875
|
return [
|
|
26261
25876
|
{
|
|
26262
25877
|
toggle: "light",
|
|
@@ -26292,7 +25907,7 @@ var GreeContainerDevice = class extends BaseDevice$1 {
|
|
|
26292
25907
|
const childConfig = {
|
|
26293
25908
|
toggle: d.toggle,
|
|
26294
25909
|
greeMac: this.greeMac,
|
|
26295
|
-
|
|
25910
|
+
connectionKey: this.connectionKey,
|
|
26296
25911
|
system: "gree",
|
|
26297
25912
|
...this.integrationId !== void 0 ? { integrationId: this.integrationId } : {}
|
|
26298
25913
|
};
|
|
@@ -26307,303 +25922,123 @@ var GreeContainerDevice = class extends BaseDevice$1 {
|
|
|
26307
25922
|
};
|
|
26308
25923
|
//#endregion
|
|
26309
25924
|
//#region src/addon.ts
|
|
26310
|
-
/** Default multi-broker config — a fresh install starts with no scopes. */
|
|
26311
|
-
var DEFAULTS = { brokers: [] };
|
|
26312
25925
|
/**
|
|
26313
|
-
* Gree device-provider addon (
|
|
25926
|
+
* Gree air-conditioner device-provider addon — `mode: standalone` (the Reolink /
|
|
25927
|
+
* Ecowitt pattern). No broker, no device-adoption cap. An AC is added as a DEVICE
|
|
25928
|
+
* via the manual-creation form: the CONNECTION (host IP + optional broadcastAddr
|
|
25929
|
+
* + UDP tuning) lives on the AC DEVICE config. `onCreateDevice` runs a DIRECTED
|
|
25930
|
+
* bind against the given host to learn the AC's durable identity (MAC) and prove
|
|
25931
|
+
* reachability, then creates a {@link GreeContainerDevice} that OWNS its own live
|
|
25932
|
+
* `@apocaliss92/nodegree` client and fans out its Thermostat + toggle accessory
|
|
25933
|
+
* children (climate-control with swing entities + the panel-light / X-Fan /
|
|
25934
|
+
* health / fresh-air switches — all preserved from the broker model).
|
|
26314
25935
|
*
|
|
26315
|
-
*
|
|
26316
|
-
*
|
|
26317
|
-
* {@link DeviceType.Container} parent that fans out a single {@link DeviceType.Thermostat}
|
|
26318
|
-
* accessory child carrying `climate-control` + `fan-control`. Modelled directly
|
|
26319
|
-
* on the Dreo / Dreame device-provider template: `broker` + `device-adoption` cap
|
|
26320
|
-
* providers for connection + adoption, the `device-provider` cap from the base
|
|
26321
|
-
* class, and an in-process connection resolver (`greeConnections`) the device
|
|
26322
|
-
* classes read.
|
|
25936
|
+
* `instanceMode: multiple` — many ACs may be added. Placement: any-node — Gree is
|
|
25937
|
+
* LOCAL UDP; the addon runs on whichever node shares the AC's subnet.
|
|
26323
25938
|
*
|
|
26324
|
-
*
|
|
26325
|
-
*
|
|
26326
|
-
*
|
|
26327
|
-
* The operator points each scope's `broadcastAddr` at the right subnet.
|
|
25939
|
+
* LAN UDP-broadcast auto-discovery of multiple ACs is deliberately NOT
|
|
25940
|
+
* implemented here — that is the later Discovery-cap phase. Phase 1 is
|
|
25941
|
+
* add-one-AC-by-IP with a directed bind.
|
|
26328
25942
|
*/
|
|
26329
25943
|
var GreeProviderAddon = class extends BaseDeviceProvider {
|
|
26330
25944
|
addonId = "provider-gree";
|
|
26331
25945
|
providerName = "Gree";
|
|
26332
25946
|
deviceClasses = { [DeviceType.Container]: GreeContainerDevice };
|
|
26333
|
-
registry = null;
|
|
26334
25947
|
constructor() {
|
|
26335
|
-
super({
|
|
26336
|
-
}
|
|
26337
|
-
async onInitialize() {
|
|
26338
|
-
const regs = await super.onInitialize();
|
|
26339
|
-
this.registry = new GreeBrokerRegistry(this.ctx.logger);
|
|
26340
|
-
this.registry.setOnBrokerConnected((brokerId) => {
|
|
26341
|
-
this.ctx.logger.info("Gree: broker connected", { meta: { brokerId } });
|
|
26342
|
-
this.setBrokerDevicesOnline(brokerId, true);
|
|
26343
|
-
});
|
|
26344
|
-
this.registry.setOnBrokerDisconnected((brokerId) => {
|
|
26345
|
-
this.setBrokerDevicesOnline(brokerId, false);
|
|
26346
|
-
});
|
|
26347
|
-
await this.registry.restore(this.config.brokers);
|
|
26348
|
-
this.ctx.logger.info("Gree: provider initialised", { meta: { brokerCount: this.config.brokers.length } });
|
|
26349
|
-
await this.reconcileIntegrationsToBrokers();
|
|
26350
|
-
this.subscribeIntegrationLifecycle();
|
|
26351
|
-
return [
|
|
26352
|
-
...regs,
|
|
26353
|
-
{
|
|
26354
|
-
capability: brokerCapability,
|
|
26355
|
-
provider: this.buildBrokerProvider()
|
|
26356
|
-
},
|
|
26357
|
-
{
|
|
26358
|
-
capability: deviceAdoptionCapability,
|
|
26359
|
-
provider: this.buildAdoptionProvider()
|
|
26360
|
-
}
|
|
26361
|
-
];
|
|
25948
|
+
super({});
|
|
26362
25949
|
}
|
|
26363
|
-
|
|
26364
|
-
|
|
26365
|
-
async onConfigChanged() {
|
|
26366
|
-
const reg = this.registry;
|
|
26367
|
-
if (!reg) return;
|
|
26368
|
-
const persisted = this.config.brokers;
|
|
26369
|
-
const liveIds = new Set(reg.list().map((b) => b.id));
|
|
26370
|
-
const persistedIds = new Set(persisted.map((e) => e.id));
|
|
26371
|
-
for (const liveId of liveIds) if (!persistedIds.has(liveId)) try {
|
|
26372
|
-
await reg.removeEntry(liveId);
|
|
26373
|
-
} catch (err) {
|
|
26374
|
-
this.ctx.logger.warn("Gree onConfigChanged: removeEntry failed", { meta: {
|
|
26375
|
-
brokerId: liveId,
|
|
26376
|
-
error: errMsg(err)
|
|
26377
|
-
} });
|
|
26378
|
-
}
|
|
26379
|
-
for (const entry of persisted) {
|
|
26380
|
-
if (entry.id && liveIds.has(entry.id)) {
|
|
26381
|
-
if (sameConnection(entry.connection, reg.getConnection(entry.id))) continue;
|
|
26382
|
-
try {
|
|
26383
|
-
await reg.updateEntry(entry.id, entry.connection);
|
|
26384
|
-
} catch (err) {
|
|
26385
|
-
this.ctx.logger.warn("Gree onConfigChanged: updateEntry failed", { meta: {
|
|
26386
|
-
brokerId: entry.id,
|
|
26387
|
-
error: errMsg(err)
|
|
26388
|
-
} });
|
|
26389
|
-
}
|
|
26390
|
-
continue;
|
|
26391
|
-
}
|
|
26392
|
-
try {
|
|
26393
|
-
const created = await reg.createEntry(entry.name, entry.connection);
|
|
26394
|
-
if (created.id !== entry.id) {
|
|
26395
|
-
const next = persisted.map((b) => b === entry ? {
|
|
26396
|
-
...b,
|
|
26397
|
-
id: created.id
|
|
26398
|
-
} : b);
|
|
26399
|
-
await this.updateGlobalSettings({ brokers: next });
|
|
26400
|
-
}
|
|
26401
|
-
} catch (err) {
|
|
26402
|
-
this.ctx.logger.warn("Gree onConfigChanged: failed to start new broker", { meta: {
|
|
26403
|
-
brokerName: entry.name,
|
|
26404
|
-
error: errMsg(err)
|
|
26405
|
-
} });
|
|
26406
|
-
}
|
|
26407
|
-
}
|
|
25950
|
+
async supportsDiscovery() {
|
|
25951
|
+
return false;
|
|
26408
25952
|
}
|
|
26409
|
-
async
|
|
26410
|
-
|
|
26411
|
-
await this.registry?.shutdown();
|
|
26412
|
-
} catch (err) {
|
|
26413
|
-
this.ctx.logger.warn("Gree: provider shutdown error", { meta: { error: errMsg(err) } });
|
|
26414
|
-
}
|
|
26415
|
-
this.registry = null;
|
|
26416
|
-
await super.onShutdown();
|
|
25953
|
+
async supportsManualCreation() {
|
|
25954
|
+
return true;
|
|
26417
25955
|
}
|
|
26418
|
-
|
|
26419
|
-
|
|
26420
|
-
return
|
|
26421
|
-
}
|
|
26422
|
-
|
|
26423
|
-
|
|
26424
|
-
|
|
26425
|
-
|
|
26426
|
-
|
|
26427
|
-
|
|
26428
|
-
|
|
25956
|
+
async onGetCreationSchema(type) {
|
|
25957
|
+
if (type !== DeviceType.Container) return null;
|
|
25958
|
+
return buildConnectionFormSchema();
|
|
25959
|
+
}
|
|
25960
|
+
async onCreateDevice(type, config) {
|
|
25961
|
+
if (type !== DeviceType.Container) throw new Error(`Gree provider does not support device type: ${type}`);
|
|
25962
|
+
const name = typeof config["name"] === "string" ? config["name"].trim() : "";
|
|
25963
|
+
if (!name) throw new Error("Air conditioner name is required");
|
|
25964
|
+
const connection = settingsToGreeConfig(config);
|
|
25965
|
+
if (connection.host.trim().length === 0) throw new Error("Air conditioner IP address is required");
|
|
25966
|
+
const bound = await bindOnce({
|
|
25967
|
+
connection,
|
|
26429
25968
|
logger: this.ctx.logger
|
|
26430
25969
|
});
|
|
26431
|
-
|
|
26432
|
-
|
|
26433
|
-
|
|
26434
|
-
|
|
26435
|
-
|
|
26436
|
-
|
|
26437
|
-
|
|
26438
|
-
|
|
26439
|
-
|
|
26440
|
-
|
|
26441
|
-
|
|
25970
|
+
const connectionKey = `gree:${macKey(bound.mac)}`;
|
|
25971
|
+
const deviceConfig = {
|
|
25972
|
+
greeMac: macKey(bound.mac),
|
|
25973
|
+
greeIp: bound.ip,
|
|
25974
|
+
connectionKey,
|
|
25975
|
+
connection,
|
|
25976
|
+
system: "gree",
|
|
25977
|
+
name
|
|
25978
|
+
};
|
|
25979
|
+
const manager = new GreeIntegrationManager({
|
|
25980
|
+
id: connectionKey,
|
|
25981
|
+
name,
|
|
25982
|
+
connection,
|
|
25983
|
+
logger: this.ctx.logger,
|
|
25984
|
+
onConnected: () => void 0,
|
|
25985
|
+
onDisconnected: () => void 0,
|
|
25986
|
+
surfaceSink: greeConnections,
|
|
25987
|
+
expectMac: bound.mac
|
|
26442
25988
|
});
|
|
26443
|
-
}
|
|
26444
|
-
/**
|
|
26445
|
-
* Link each surviving integration (carrying `{ brokerId }` in its settings) to
|
|
26446
|
-
* its broker so the generic `device-adoption` cap resolves the scope, and
|
|
26447
|
-
* cascade-clean brokers (+ their adopted devices) whose spawning integration
|
|
26448
|
-
* was deleted. Idempotent; runs on boot + on every integration lifecycle event.
|
|
26449
|
-
* Guarded so a failure never fails init.
|
|
26450
|
-
*/
|
|
26451
|
-
async reconcileIntegrationsToBrokers() {
|
|
26452
25989
|
try {
|
|
26453
|
-
|
|
26454
|
-
const surviving = new Set(mine.map((i) => i.id));
|
|
26455
|
-
for (const integration of mine) {
|
|
26456
|
-
const settings = await this.ctx.api.integrations.getSettings.query({ id: integration.id });
|
|
26457
|
-
const brokerId = typeof settings["brokerId"] === "string" ? settings["brokerId"] : void 0;
|
|
26458
|
-
if (brokerId === void 0) continue;
|
|
26459
|
-
if (!this.config.brokers.some((b) => b.id === brokerId)) {
|
|
26460
|
-
this.ctx.logger.warn("Gree integration→broker: linked broker not found", { meta: {
|
|
26461
|
-
integrationId: integration.id,
|
|
26462
|
-
brokerId
|
|
26463
|
-
} });
|
|
26464
|
-
continue;
|
|
26465
|
-
}
|
|
26466
|
-
this.requireRegistry().linkIntegration(integration.id, brokerId);
|
|
26467
|
-
}
|
|
26468
|
-
const toRemove = this.config.brokers.filter((b) => b.integrationId !== void 0 && !surviving.has(b.integrationId)).map((b) => b.id);
|
|
26469
|
-
if (toRemove.length === 0) return;
|
|
26470
|
-
for (const id of toRemove) try {
|
|
26471
|
-
await this.requireRegistry().removeEntry(id);
|
|
26472
|
-
await this.cascadeRemoveDevicesForBroker(id);
|
|
26473
|
-
} catch (err) {
|
|
26474
|
-
this.ctx.logger.warn("Gree integration→broker: broker cleanup failed", { meta: {
|
|
26475
|
-
brokerId: id,
|
|
26476
|
-
error: errMsg(err)
|
|
26477
|
-
} });
|
|
26478
|
-
}
|
|
26479
|
-
const nextBrokers = this.config.brokers.filter((b) => !toRemove.includes(b.id));
|
|
26480
|
-
await this.updateGlobalSettings({ brokers: nextBrokers });
|
|
25990
|
+
await manager.start();
|
|
26481
25991
|
} catch (err) {
|
|
26482
|
-
this.ctx.logger.warn("Gree
|
|
25992
|
+
this.ctx.logger.warn("Gree create: initial client start failed — device kept", { meta: {
|
|
25993
|
+
connectionKey,
|
|
25994
|
+
error: errMsg(err)
|
|
25995
|
+
} });
|
|
26483
25996
|
}
|
|
26484
|
-
|
|
26485
|
-
|
|
26486
|
-
|
|
26487
|
-
|
|
26488
|
-
if (typeof addonId === "string" && addonId !== this.ctx.id) return;
|
|
26489
|
-
this.reconcileIntegrationsToBrokers();
|
|
26490
|
-
};
|
|
26491
|
-
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationEnabled }, handler);
|
|
26492
|
-
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, handler);
|
|
26493
|
-
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, handler);
|
|
26494
|
-
}
|
|
26495
|
-
buildAdoptionProvider() {
|
|
26496
|
-
return buildGreeAdoptionProvider({
|
|
26497
|
-
registry: greeConnections,
|
|
26498
|
-
logger: this.ctx.logger,
|
|
26499
|
-
getBrokerIdForIntegration: async (id) => {
|
|
26500
|
-
const brokerId = (await this.ctx.api.integrations.getSettings.query({ id }))["brokerId"];
|
|
26501
|
-
if (typeof brokerId !== "string") throw new Error(`integration ${id} has no linked brokerId`);
|
|
26502
|
-
return brokerId;
|
|
26503
|
-
},
|
|
26504
|
-
listAdoptedGree: async () => {
|
|
26505
|
-
const reg = this.ctx.kernel.deviceRegistry;
|
|
26506
|
-
const devices = this.ctx.kernel.devices;
|
|
26507
|
-
if (!reg || !devices) return [];
|
|
26508
|
-
const out = [];
|
|
26509
|
-
for (const d of reg.getAllForAddon(this.addonId)) {
|
|
26510
|
-
if (d.parentDeviceId !== null) continue;
|
|
26511
|
-
const config = await devices.loadConfig(d.id).catch(() => ({}));
|
|
26512
|
-
out.push({
|
|
26513
|
-
id: d.id,
|
|
26514
|
-
config
|
|
26515
|
-
});
|
|
26516
|
-
}
|
|
26517
|
-
return out;
|
|
26518
|
-
},
|
|
26519
|
-
adoptDevice: async ({ greeMac, greeIp, brokerId, integrationId, name }) => {
|
|
26520
|
-
const devices = this.ctx.kernel.devices;
|
|
26521
|
-
if (!devices) throw new Error("gree adopt: kernel.devices unavailable");
|
|
26522
|
-
const config = {
|
|
26523
|
-
greeMac,
|
|
26524
|
-
greeIp,
|
|
26525
|
-
brokerId,
|
|
26526
|
-
system: "gree",
|
|
26527
|
-
integrationId,
|
|
26528
|
-
name
|
|
26529
|
-
};
|
|
26530
|
-
const stableId = this.generateStableId(DeviceType.Container, config);
|
|
26531
|
-
const device = await devices.create(stableId, GreeContainerDevice, config, null, {
|
|
26532
|
-
type: DeviceType.Container,
|
|
26533
|
-
name,
|
|
26534
|
-
integrationId
|
|
26535
|
-
});
|
|
26536
|
-
const children = await devices.getChildren(device.id);
|
|
26537
|
-
return {
|
|
26538
|
-
deviceId: device.id,
|
|
26539
|
-
accessoryDeviceIds: children.map((c) => c.id)
|
|
26540
|
-
};
|
|
26541
|
-
},
|
|
26542
|
-
removeDevice: async (id) => {
|
|
26543
|
-
await this.ctx.kernel.devices?.remove(id);
|
|
25997
|
+
return {
|
|
25998
|
+
meta: {
|
|
25999
|
+
type: DeviceType.Container,
|
|
26000
|
+
name
|
|
26544
26001
|
},
|
|
26545
|
-
|
|
26546
|
-
|
|
26547
|
-
if (
|
|
26548
|
-
return devices.loadConfig(id).catch(() => null);
|
|
26002
|
+
config: deviceConfig,
|
|
26003
|
+
onAfterCreate: async (device) => {
|
|
26004
|
+
if (device instanceof GreeContainerDevice) device.adoptManager(manager);
|
|
26549
26005
|
}
|
|
26550
|
-
}
|
|
26551
|
-
}
|
|
26552
|
-
globalSettingsSchema() {
|
|
26553
|
-
return this.schema({ sections: [{
|
|
26554
|
-
id: "gree-broker",
|
|
26555
|
-
title: "Gree discovery scope",
|
|
26556
|
-
description: "Gree scopes are managed in the External systems → Brokers tab.",
|
|
26557
|
-
columns: 1,
|
|
26558
|
-
fields: [{
|
|
26559
|
-
type: "info",
|
|
26560
|
-
key: "brokerHelp",
|
|
26561
|
-
label: "Scopes are managed separately",
|
|
26562
|
-
content: "This integration links to a Gree LAN discovery scope. Add, edit, or remove scopes from External systems → Brokers. Gree is local-only (UDP) — no cloud account is needed; a scope just stores the broadcast address + UDP settings."
|
|
26563
|
-
}]
|
|
26564
|
-
}] });
|
|
26565
|
-
}
|
|
26566
|
-
async supportsManualCreation() {
|
|
26567
|
-
return false;
|
|
26568
|
-
}
|
|
26569
|
-
async onGetCreationSchema(_type) {
|
|
26570
|
-
return null;
|
|
26571
|
-
}
|
|
26572
|
-
async onCreateDevice(_type, _config) {
|
|
26573
|
-
throw new Error("Gree devices are adopted via LAN discovery, not created manually");
|
|
26574
|
-
}
|
|
26575
|
-
setBrokerDevicesOnline(brokerId, online) {
|
|
26576
|
-
const reg = this.ctx.kernel.deviceRegistry;
|
|
26577
|
-
if (!reg) return;
|
|
26578
|
-
const n = setBrokerDevicesOnline(reg.getAllForAddon(this.addonId), brokerId, online);
|
|
26579
|
-
if (n > 0) this.ctx.logger.info("Gree: broker devices " + (online ? "online" : "offline"), { meta: {
|
|
26580
|
-
brokerId,
|
|
26581
|
-
count: n
|
|
26582
|
-
} });
|
|
26006
|
+
};
|
|
26583
26007
|
}
|
|
26584
|
-
|
|
26585
|
-
|
|
26586
|
-
|
|
26008
|
+
/**
|
|
26009
|
+
* AC stableId — derived from the AC's durable MAC identity (resolved by the
|
|
26010
|
+
* create-time bind), NOT a broker id. Re-adding the same physical AC reuses its
|
|
26011
|
+
* persisted row. Falls back to a timestamp only when no MAC is resolvable
|
|
26012
|
+
* (should not happen — `onCreateDevice` binds first).
|
|
26013
|
+
*/
|
|
26014
|
+
generateStableId(_type, config) {
|
|
26015
|
+
const mac = config?.["greeMac"];
|
|
26016
|
+
if (typeof mac === "string" && mac.length > 0) return `gree:${macKey(mac)}`;
|
|
26017
|
+
return `gree:${Date.now()}`;
|
|
26587
26018
|
}
|
|
26588
26019
|
};
|
|
26589
26020
|
//#endregion
|
|
26590
26021
|
exports.ADVERTISED_CAP_MODES = ADVERTISED_CAP_MODES;
|
|
26022
|
+
exports.DeviceType = DeviceType;
|
|
26591
26023
|
exports.GREE_FAN_PERCENTAGE_STEP = GREE_FAN_PERCENTAGE_STEP;
|
|
26592
26024
|
exports.GreeProviderAddon = GreeProviderAddon;
|
|
26593
26025
|
exports.SUPPORTED_CAP_MODES = SUPPORTED_CAP_MODES;
|
|
26026
|
+
exports.bindOnce = bindOnce;
|
|
26594
26027
|
exports.boolToHorizontalSwing = boolToHorizontalSwing;
|
|
26595
26028
|
exports.boolToVerticalSwing = boolToVerticalSwing;
|
|
26596
26029
|
exports.buildConnectionFormSchema = buildConnectionFormSchema;
|
|
26597
|
-
exports.buildGreeCandidates = buildGreeCandidates;
|
|
26598
26030
|
exports.capModeToLibMode = capModeToLibMode;
|
|
26599
26031
|
exports.fanSpeedToPercentage = fanSpeedToPercentage;
|
|
26600
|
-
exports.
|
|
26032
|
+
exports.greeAcDeviceSchema = greeAcDeviceSchema;
|
|
26601
26033
|
exports.greeConfigSchema = greeConfigSchema;
|
|
26602
26034
|
exports.horizontalSwingToBool = horizontalSwingToBool;
|
|
26603
26035
|
exports.isAutoFan = isAutoFan;
|
|
26604
26036
|
exports.libModeToCapMode = libModeToCapMode;
|
|
26605
26037
|
exports.oscillatingToVerticalSwing = oscillatingToVerticalSwing;
|
|
26606
26038
|
exports.percentageToFanSpeed = percentageToFanSpeed;
|
|
26039
|
+
exports.resolveBroadcastTarget = resolveBroadcastTarget;
|
|
26040
|
+
exports.sameConnection = sameConnection;
|
|
26041
|
+
exports.settingsToGreeConfig = settingsToGreeConfig;
|
|
26607
26042
|
exports.swingToOscillating = swingToOscillating;
|
|
26608
26043
|
exports.toNodegreeOptions = toNodegreeOptions;
|
|
26609
26044
|
exports.verticalSwingToBool = verticalSwingToBool;
|