@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.mjs
CHANGED
|
@@ -14782,83 +14782,34 @@ var GetStateInputSchema = object({
|
|
|
14782
14782
|
* HA: entity_id (returns the cached entity state). */
|
|
14783
14783
|
key: string()
|
|
14784
14784
|
});
|
|
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
|
-
};
|
|
14785
|
+
method(ListInputSchema, array(BrokerInfoSchema$1)), method(GetInputSchema, BrokerInfoSchema$1.nullable()), method(_void(), array(BrokerProviderInfoSchema), { auth: "admin" }), method(AddInputSchema, AddResultSchema, {
|
|
14786
|
+
kind: "mutation",
|
|
14787
|
+
auth: "admin"
|
|
14788
|
+
}), method(RemoveInputSchema, _void(), {
|
|
14789
|
+
kind: "mutation",
|
|
14790
|
+
auth: "admin"
|
|
14791
|
+
}), method(GetInputSchema, TestConnectionResultSchema, {
|
|
14792
|
+
kind: "mutation",
|
|
14793
|
+
auth: "admin"
|
|
14794
|
+
}), method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }), method(object({
|
|
14795
|
+
id: string(),
|
|
14796
|
+
settings: SettingsRecordSchema$1
|
|
14797
|
+
}), _void(), {
|
|
14798
|
+
kind: "mutation",
|
|
14799
|
+
auth: "admin"
|
|
14800
|
+
}), method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }), method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }), method(TestSettingsInputSchema, TestSettingsResultSchema, {
|
|
14801
|
+
kind: "mutation",
|
|
14802
|
+
auth: "admin"
|
|
14803
|
+
}), method(PublishInputSchema, unknown(), {
|
|
14804
|
+
kind: "mutation",
|
|
14805
|
+
auth: "admin"
|
|
14806
|
+
}), method(SubscribeInputSchema, SubscribeResultSchema, {
|
|
14807
|
+
kind: "mutation",
|
|
14808
|
+
auth: "admin"
|
|
14809
|
+
}), method(UnsubscribeInputSchema, _void(), {
|
|
14810
|
+
kind: "mutation",
|
|
14811
|
+
auth: "admin"
|
|
14812
|
+
}), method(GetStateInputSchema, unknown().nullable()), method(_void(), RegistryStatusSchema);
|
|
14862
14813
|
DeviceType.Camera;
|
|
14863
14814
|
/**
|
|
14864
14815
|
* `custom-model-registry` — collection cap exposing operator-registered
|
|
@@ -15094,36 +15045,19 @@ var ResyncResultSchema = object({
|
|
|
15094
15045
|
* provider re-derived the device. 0/absent for a normal incremental re-sync. */
|
|
15095
15046
|
removedChildren: number().int().nonnegative().optional()
|
|
15096
15047
|
});
|
|
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
|
-
};
|
|
15048
|
+
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, {
|
|
15049
|
+
kind: "mutation",
|
|
15050
|
+
auth: "admin"
|
|
15051
|
+
}), method(AdoptInputSchema, AdoptResultSchema, {
|
|
15052
|
+
kind: "mutation",
|
|
15053
|
+
auth: "admin"
|
|
15054
|
+
}), method(ReleaseInputSchema, _void(), {
|
|
15055
|
+
kind: "mutation",
|
|
15056
|
+
auth: "admin"
|
|
15057
|
+
}), method(ResyncInputSchema, ResyncResultSchema, {
|
|
15058
|
+
kind: "mutation",
|
|
15059
|
+
auth: "admin"
|
|
15060
|
+
});
|
|
15127
15061
|
/**
|
|
15128
15062
|
* `device-export` — collection cap for addons that export camstack
|
|
15129
15063
|
* devices to external ecosystems (HomeAssistant via MQTT discovery,
|
|
@@ -18313,6 +18247,17 @@ var AvailableIntegrationTypeSchema = object({
|
|
|
18313
18247
|
iconUrl: string().nullable(),
|
|
18314
18248
|
color: string(),
|
|
18315
18249
|
instanceMode: string(),
|
|
18250
|
+
/**
|
|
18251
|
+
* Integration wizard `mode` (LOCKED MODEL): `standalone` (create
|
|
18252
|
+
* immediately then add devices, no config step/button), `account` (config
|
|
18253
|
+
* step), or `broker` (broker step). Derived server-side by
|
|
18254
|
+
* `getAvailableTypes` when the addon manifest omits an explicit `mode`.
|
|
18255
|
+
*/
|
|
18256
|
+
mode: _enum([
|
|
18257
|
+
"standalone",
|
|
18258
|
+
"account",
|
|
18259
|
+
"broker"
|
|
18260
|
+
]),
|
|
18316
18261
|
discoveryMode: string(),
|
|
18317
18262
|
/**
|
|
18318
18263
|
* Which integration-marker cap the addon declared, so the wizard can
|
|
@@ -24031,6 +23976,158 @@ object({
|
|
|
24031
23976
|
schemaVersion: literal(1)
|
|
24032
23977
|
});
|
|
24033
23978
|
//#endregion
|
|
23979
|
+
//#region src/config.ts
|
|
23980
|
+
/**
|
|
23981
|
+
* The AES negotiation modes the wrapped `@apocaliss92/nodegree` client accepts.
|
|
23982
|
+
* Mirrors the library's `encryption` union — kept local so the addon validates
|
|
23983
|
+
* the operator-supplied value at the system boundary without importing a runtime
|
|
23984
|
+
* value the library does not export. `auto` tries V2 (GCM) then V1 (ECB).
|
|
23985
|
+
*/
|
|
23986
|
+
var GREE_ENCRYPTION_MODES = [
|
|
23987
|
+
"auto",
|
|
23988
|
+
"v1",
|
|
23989
|
+
"v2"
|
|
23990
|
+
];
|
|
23991
|
+
var GreeEncryptionSchema = _enum(GREE_ENCRYPTION_MODES);
|
|
23992
|
+
/**
|
|
23993
|
+
* Operator-supplied CONNECTION for ONE Gree air conditioner (standalone mode —
|
|
23994
|
+
* the Reolink / Ecowitt pattern). Gree is LOCAL-ONLY (a directed-broadcast bind
|
|
23995
|
+
* handshake + per-device AES control over UDP), so the connection carries no
|
|
23996
|
+
* credentials — only the AC's LAN address and the UDP tuning knobs. The
|
|
23997
|
+
* `broadcastAddr` is the directed target the bind scan is aimed at: point it at
|
|
23998
|
+
* the AC's own IP (unicast-directed) or the subnet broadcast (e.g.
|
|
23999
|
+
* `192.168.1.255`). Empty = the library's default global broadcast.
|
|
24000
|
+
*/
|
|
24001
|
+
var greeConfigSchema = object({
|
|
24002
|
+
/** The AC's LAN IP address — the directed-bind target. Required for a manual
|
|
24003
|
+
* standalone add (the operator types it). */
|
|
24004
|
+
host: string().default("").describe("Air conditioner LAN IP address"),
|
|
24005
|
+
/** Directed broadcast address for the bind scan (e.g. `192.168.1.255`). Empty =
|
|
24006
|
+
* the library's default global broadcast; when a `host` is set the scan is
|
|
24007
|
+
* aimed at it directly. */
|
|
24008
|
+
broadcastAddr: string().default("").describe("Directed broadcast address for the bind scan"),
|
|
24009
|
+
/** UDP request timeout in ms. */
|
|
24010
|
+
timeoutMs: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(500).max(3e4).default(3e3)).describe("UDP request timeout (ms)"),
|
|
24011
|
+
/** Retry count per UDP request. */
|
|
24012
|
+
retries: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(0).max(10).default(3)).describe("Retries per UDP request"),
|
|
24013
|
+
/** AES negotiation mode. */
|
|
24014
|
+
encryption: GreeEncryptionSchema.default("auto").describe("AES cipher negotiation")
|
|
24015
|
+
});
|
|
24016
|
+
/**
|
|
24017
|
+
* Build the `NodegreeOptions` the wrapped client constructor expects from the
|
|
24018
|
+
* validated connection. Pure: same config in → same options out. `host` and
|
|
24019
|
+
* `broadcastAddr` are NOT constructor options — they are passed to `discover()`
|
|
24020
|
+
* per scan — so they are intentionally omitted here.
|
|
24021
|
+
*/
|
|
24022
|
+
function toNodegreeOptions(config) {
|
|
24023
|
+
return {
|
|
24024
|
+
timeoutMs: config.timeoutMs,
|
|
24025
|
+
retries: config.retries,
|
|
24026
|
+
encryption: config.encryption
|
|
24027
|
+
};
|
|
24028
|
+
}
|
|
24029
|
+
/**
|
|
24030
|
+
* Coerce a loose settings blob (the manual-creation form values, or a persisted
|
|
24031
|
+
* `connection` blob) through the connection schema, applying all defaults.
|
|
24032
|
+
* Throws a `ZodError` on invalid input so the caller surfaces a clear error at
|
|
24033
|
+
* the system boundary.
|
|
24034
|
+
*/
|
|
24035
|
+
function settingsToGreeConfig(settings) {
|
|
24036
|
+
return greeConfigSchema.parse(settings ?? {});
|
|
24037
|
+
}
|
|
24038
|
+
/**
|
|
24039
|
+
* Persisted config for a Gree AC {@link import('@camstack/types').DeviceType.Container}
|
|
24040
|
+
* device. The operator-supplied CONNECTION lives directly on the device (Reolink
|
|
24041
|
+
* pattern) — there is no broker registry. The device owns its own live
|
|
24042
|
+
* `@apocaliss92/nodegree` client keyed on its own `connectionKey`; its AC +
|
|
24043
|
+
* toggle accessory children resolve the bound handle via that key.
|
|
24044
|
+
*
|
|
24045
|
+
* `greeMac` is the durable device identity (resolved by the create-time bind);
|
|
24046
|
+
* `greeIp` is the last-known LAN address; `connectionKey` is the per-device
|
|
24047
|
+
* connection-resolver key (the device's own stableId — decoupled from any
|
|
24048
|
+
* broker); `connection` is the operator's UDP settings; `system`/`name` are
|
|
24049
|
+
* provenance.
|
|
24050
|
+
*/
|
|
24051
|
+
var greeAcDeviceSchema = object({
|
|
24052
|
+
/** Durable AC identity (MAC, lowercased) resolved by the create-time bind. */
|
|
24053
|
+
greeMac: string().min(1).describe("Gree AC MAC address"),
|
|
24054
|
+
/** Last-known LAN IP (re-bound on each activate). */
|
|
24055
|
+
greeIp: string().optional().describe("Last known LAN IP"),
|
|
24056
|
+
/** Per-device connection-resolver key (the device's own stableId). */
|
|
24057
|
+
connectionKey: string().min(1).describe("Per-device connection resolver key"),
|
|
24058
|
+
/** The operator-supplied UDP connection settings the device's client dials. */
|
|
24059
|
+
connection: greeConfigSchema,
|
|
24060
|
+
system: literal("gree").optional(),
|
|
24061
|
+
integrationId: string().optional(),
|
|
24062
|
+
name: string().optional()
|
|
24063
|
+
});
|
|
24064
|
+
/**
|
|
24065
|
+
* Hand-written connection form for the AC device-creation UI (standalone mode —
|
|
24066
|
+
* Reolink pattern). The connection lives on the AC DEVICE config, not in a broker
|
|
24067
|
+
* registry. A `name` field is included so the operator names the AC at creation
|
|
24068
|
+
* time (mirrors the Reolink / Ecowitt creation form).
|
|
24069
|
+
*/
|
|
24070
|
+
function buildConnectionFormSchema() {
|
|
24071
|
+
return { sections: [{
|
|
24072
|
+
id: "identity",
|
|
24073
|
+
title: "Air conditioner",
|
|
24074
|
+
description: "Gree air conditioners are controlled directly over your LAN (no cloud). Enter the AC's IP address; CamStack binds to it over UDP.",
|
|
24075
|
+
columns: 1,
|
|
24076
|
+
fields: [{
|
|
24077
|
+
type: "text",
|
|
24078
|
+
key: "name",
|
|
24079
|
+
label: "Name",
|
|
24080
|
+
required: true,
|
|
24081
|
+
placeholder: "Living room AC"
|
|
24082
|
+
}, {
|
|
24083
|
+
type: "text",
|
|
24084
|
+
key: "host",
|
|
24085
|
+
label: "IP address",
|
|
24086
|
+
required: true,
|
|
24087
|
+
placeholder: "192.168.1.50"
|
|
24088
|
+
}]
|
|
24089
|
+
}, {
|
|
24090
|
+
id: "advanced",
|
|
24091
|
+
title: "Advanced (UDP)",
|
|
24092
|
+
columns: 2,
|
|
24093
|
+
fields: [
|
|
24094
|
+
{
|
|
24095
|
+
type: "text",
|
|
24096
|
+
key: "broadcastAddr",
|
|
24097
|
+
label: "Broadcast address (optional)",
|
|
24098
|
+
required: false,
|
|
24099
|
+
placeholder: "192.168.1.255"
|
|
24100
|
+
},
|
|
24101
|
+
{
|
|
24102
|
+
type: "number",
|
|
24103
|
+
key: "timeoutMs",
|
|
24104
|
+
label: "UDP timeout (ms)",
|
|
24105
|
+
min: 500,
|
|
24106
|
+
max: 3e4,
|
|
24107
|
+
default: 3e3
|
|
24108
|
+
},
|
|
24109
|
+
{
|
|
24110
|
+
type: "number",
|
|
24111
|
+
key: "retries",
|
|
24112
|
+
label: "Retries",
|
|
24113
|
+
min: 0,
|
|
24114
|
+
max: 10,
|
|
24115
|
+
default: 3
|
|
24116
|
+
},
|
|
24117
|
+
{
|
|
24118
|
+
type: "select",
|
|
24119
|
+
key: "encryption",
|
|
24120
|
+
label: "Encryption",
|
|
24121
|
+
default: "auto",
|
|
24122
|
+
options: GREE_ENCRYPTION_MODES.map((m) => ({
|
|
24123
|
+
value: m,
|
|
24124
|
+
label: m.toUpperCase()
|
|
24125
|
+
}))
|
|
24126
|
+
}
|
|
24127
|
+
]
|
|
24128
|
+
}] };
|
|
24129
|
+
}
|
|
24130
|
+
//#endregion
|
|
24034
24131
|
//#region ../../node_modules/@apocaliss92/nodegree/dist/index.js
|
|
24035
24132
|
var GreeError = class extends Error {
|
|
24036
24133
|
constructor(message) {
|
|
@@ -24712,191 +24809,79 @@ var Nodegree = class {
|
|
|
24712
24809
|
}
|
|
24713
24810
|
};
|
|
24714
24811
|
//#endregion
|
|
24715
|
-
//#region src/
|
|
24716
|
-
/**
|
|
24717
|
-
|
|
24718
|
-
|
|
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
|
-
};
|
|
24812
|
+
//#region src/gree-gateway.ts
|
|
24813
|
+
/** Lowercase a MAC for stable map keying (Gree echoes mixed-case MACs). */
|
|
24814
|
+
function macKey(mac) {
|
|
24815
|
+
return mac.toLowerCase();
|
|
24759
24816
|
}
|
|
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
24817
|
/**
|
|
24772
|
-
*
|
|
24773
|
-
* the
|
|
24774
|
-
*
|
|
24818
|
+
* Per-connection registry that device classes use to reach a bound Gree AC handle
|
|
24819
|
+
* (and the bind-scan rows) for a given AC device (standalone mode — the
|
|
24820
|
+
* connection lives on the AC device, not a broker).
|
|
24821
|
+
*
|
|
24822
|
+
* The kernel constructs device classes with only a `DeviceContext` — it cannot
|
|
24823
|
+
* thread the handle in as a constructor arg. Like the Ecowitt / Dreame addon's
|
|
24824
|
+
* facade resolver, we keep it simple and in-process: the AC device's integration
|
|
24825
|
+
* manager owns the connection surface per `connectionKey` (the device's own
|
|
24826
|
+
* stableId) and publishes it here; the AC's accessory children resolve their live
|
|
24827
|
+
* `AcDevice` handle by `(connectionKey, mac)`.
|
|
24775
24828
|
*/
|
|
24776
|
-
|
|
24777
|
-
|
|
24829
|
+
var GreeConnectionResolver = class {
|
|
24830
|
+
#surfaces = /* @__PURE__ */ new Map();
|
|
24831
|
+
/** Publish or remove the connection surface for a connection key. `null` removes. */
|
|
24832
|
+
set(connectionKey, surface) {
|
|
24833
|
+
if (surface === null) {
|
|
24834
|
+
this.#surfaces.delete(connectionKey);
|
|
24835
|
+
return;
|
|
24836
|
+
}
|
|
24837
|
+
this.#surfaces.set(connectionKey, surface);
|
|
24838
|
+
}
|
|
24839
|
+
/** The bind-scan rows for a connection key, or empty when unknown. */
|
|
24840
|
+
discovered(connectionKey) {
|
|
24841
|
+
return this.#surfaces.get(connectionKey)?.discovered ?? [];
|
|
24842
|
+
}
|
|
24843
|
+
/** True when the connection has a published surface (i.e. it is bound). */
|
|
24844
|
+
has(connectionKey) {
|
|
24845
|
+
return this.#surfaces.has(connectionKey);
|
|
24846
|
+
}
|
|
24847
|
+
/** The active connection keys (one entry per published surface). */
|
|
24848
|
+
list() {
|
|
24849
|
+
return Array.from(this.#surfaces.keys()).map((id) => ({ id }));
|
|
24850
|
+
}
|
|
24851
|
+
/**
|
|
24852
|
+
* Resolve the bound {@link AcDevice} handle for a `(connectionKey, mac)` pair,
|
|
24853
|
+
* or null when the connection is unknown or the AC has not been bound.
|
|
24854
|
+
*/
|
|
24855
|
+
getDevice(connectionKey, mac) {
|
|
24856
|
+
return this.#surfaces.get(connectionKey)?.handles.get(macKey(mac)) ?? null;
|
|
24857
|
+
}
|
|
24858
|
+
/** Remove all registered surfaces (called on full shutdown). */
|
|
24859
|
+
clear() {
|
|
24860
|
+
this.#surfaces.clear();
|
|
24861
|
+
}
|
|
24862
|
+
};
|
|
24863
|
+
/** The single in-process per-connection resolver shared between the AC device's
|
|
24864
|
+
* manager and its accessory children. */
|
|
24865
|
+
var greeConnections = new GreeConnectionResolver();
|
|
24866
|
+
//#endregion
|
|
24867
|
+
//#region src/gree-integration-manager.ts
|
|
24868
|
+
function defaultFacade(config) {
|
|
24869
|
+
return new Nodegree(toNodegreeOptions(config));
|
|
24778
24870
|
}
|
|
24871
|
+
var DEFAULT_POLL_INTERVAL_MS = 3e4;
|
|
24779
24872
|
/**
|
|
24780
|
-
*
|
|
24781
|
-
*
|
|
24782
|
-
*
|
|
24783
|
-
|
|
24784
|
-
|
|
24785
|
-
|
|
24786
|
-
|
|
24787
|
-
|
|
24788
|
-
|
|
24789
|
-
|
|
24790
|
-
|
|
24791
|
-
|
|
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
|
-
//#region src/gree-gateway.ts
|
|
24833
|
-
/** Lowercase a MAC for stable map keying (Gree echoes mixed-case MACs). */
|
|
24834
|
-
function macKey$2(mac) {
|
|
24835
|
-
return mac.toLowerCase();
|
|
24836
|
-
}
|
|
24837
|
-
/**
|
|
24838
|
-
* Per-broker registry that device classes use to reach a bound Gree AC handle
|
|
24839
|
-
* (and the discovery rows) for a given discovery scope.
|
|
24840
|
-
*
|
|
24841
|
-
* 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
|
-
* `dreameFacades`, we keep it simple and in-process: the integration manager
|
|
24844
|
-
* owns the connection surface per registered broker and publishes it here;
|
|
24845
|
-
* device classes resolve their live `AcDevice` handle by `(brokerId, mac)`.
|
|
24846
|
-
*/
|
|
24847
|
-
var GreeConnectionResolver = class {
|
|
24848
|
-
#surfaces = /* @__PURE__ */ new Map();
|
|
24849
|
-
/** Publish or remove the connection surface for a broker id. `null` removes. */
|
|
24850
|
-
set(brokerId, surface) {
|
|
24851
|
-
if (surface === null) {
|
|
24852
|
-
this.#surfaces.delete(brokerId);
|
|
24853
|
-
return;
|
|
24854
|
-
}
|
|
24855
|
-
this.#surfaces.set(brokerId, surface);
|
|
24856
|
-
}
|
|
24857
|
-
/** The discovery rows for a broker id, or empty when unknown. */
|
|
24858
|
-
discovered(brokerId) {
|
|
24859
|
-
return this.#surfaces.get(brokerId)?.discovered ?? [];
|
|
24860
|
-
}
|
|
24861
|
-
/** True when the broker has a published surface (i.e. its scope is active). */
|
|
24862
|
-
has(brokerId) {
|
|
24863
|
-
return this.#surfaces.has(brokerId);
|
|
24864
|
-
}
|
|
24865
|
-
/** The active broker ids (one entry per published surface). */
|
|
24866
|
-
list() {
|
|
24867
|
-
return Array.from(this.#surfaces.keys()).map((id) => ({ id }));
|
|
24868
|
-
}
|
|
24869
|
-
/**
|
|
24870
|
-
* Resolve the bound {@link AcDevice} handle for a `(brokerId, mac)` pair, or
|
|
24871
|
-
* null when the broker is unknown or the AC has not been bound.
|
|
24872
|
-
*/
|
|
24873
|
-
getDevice(brokerId, mac) {
|
|
24874
|
-
return this.#surfaces.get(brokerId)?.handles.get(macKey$2(mac)) ?? null;
|
|
24875
|
-
}
|
|
24876
|
-
/** Remove all registered surfaces (called on full shutdown). */
|
|
24877
|
-
clear() {
|
|
24878
|
-
this.#surfaces.clear();
|
|
24879
|
-
}
|
|
24880
|
-
};
|
|
24881
|
-
/** The single in-process per-broker connection resolver shared between the
|
|
24882
|
-
* manager and device instances. */
|
|
24883
|
-
var greeConnections = new GreeConnectionResolver();
|
|
24884
|
-
//#endregion
|
|
24885
|
-
//#region src/gree-integration-manager.ts
|
|
24886
|
-
function defaultFacade(config) {
|
|
24887
|
-
return new Nodegree(toNodegreeOptions(config));
|
|
24888
|
-
}
|
|
24889
|
-
var DEFAULT_POLL_INTERVAL_MS = 3e4;
|
|
24890
|
-
/**
|
|
24891
|
-
* Wraps exactly one `@apocaliss92/nodegree` facade (= one LAN discovery scope)
|
|
24892
|
-
* with observable status + lifecycle. {@link start} runs a discovery scan and
|
|
24893
|
-
* binds (creates) an {@link AcDevice} handle per found AC, publishing the
|
|
24894
|
-
* connection surface on the shared resolver and starting per-AC polling.
|
|
24895
|
-
* {@link stop} closes the facade; {@link applyConnection} does both atomically
|
|
24896
|
-
* when the operator changes the scope settings.
|
|
24897
|
-
*
|
|
24898
|
-
* Mirrors `DreameIntegrationManager`. Binding is best-effort per AC so one
|
|
24899
|
-
* offline unit does not fail the whole scope.
|
|
24873
|
+
* Wraps exactly one `@apocaliss92/nodegree` facade bound to a SINGLE Gree AC
|
|
24874
|
+
* (standalone mode — the connection lives on the AC device). {@link start} runs a
|
|
24875
|
+
* DIRECTED bind scan aimed at the configured `host` (falling back to the
|
|
24876
|
+
* `broadcastAddr` / global broadcast), matches the responder for that host,
|
|
24877
|
+
* binds (creates) its {@link AcDevice} handle, publishes the connection surface
|
|
24878
|
+
* on the shared resolver keyed by the device's `connectionKey`, and starts
|
|
24879
|
+
* polling. {@link stop} closes the facade; {@link applyConnection} does both
|
|
24880
|
+
* atomically when the operator changes the connection.
|
|
24881
|
+
*
|
|
24882
|
+
* {@link bindOnce} is the create-time helper: a static one-shot directed scan
|
|
24883
|
+
* that returns the AC's durable identity (MAC/ip/name) WITHOUT holding a handle,
|
|
24884
|
+
* so `onCreateDevice` can persist the identity before the device exists.
|
|
24900
24885
|
*/
|
|
24901
24886
|
var GreeIntegrationManager = class {
|
|
24902
24887
|
#id;
|
|
@@ -24908,6 +24893,8 @@ var GreeIntegrationManager = class {
|
|
|
24908
24893
|
#surfaceSink;
|
|
24909
24894
|
#pollIntervalMs;
|
|
24910
24895
|
#makeFacade;
|
|
24896
|
+
/** When set, only the responder for this MAC is bound (standalone: one AC). */
|
|
24897
|
+
#expectMac;
|
|
24911
24898
|
#facade = null;
|
|
24912
24899
|
#handles = /* @__PURE__ */ new Map();
|
|
24913
24900
|
#status = "disconnected";
|
|
@@ -24924,6 +24911,7 @@ var GreeIntegrationManager = class {
|
|
|
24924
24911
|
this.#surfaceSink = options.surfaceSink;
|
|
24925
24912
|
this.#pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
24926
24913
|
this.#makeFacade = options.makeFacade ?? defaultFacade;
|
|
24914
|
+
this.#expectMac = options.expectMac !== void 0 ? macKey(options.expectMac) : null;
|
|
24927
24915
|
}
|
|
24928
24916
|
/** The connection settings the manager is currently configured with. */
|
|
24929
24917
|
getConnection() {
|
|
@@ -24937,6 +24925,7 @@ var GreeIntegrationManager = class {
|
|
|
24937
24925
|
kind: "gree",
|
|
24938
24926
|
status: this.#status,
|
|
24939
24927
|
info: {
|
|
24928
|
+
host: this.#connection.host,
|
|
24940
24929
|
broadcastAddr: this.#connection.broadcastAddr,
|
|
24941
24930
|
deviceCount: this.#deviceCount
|
|
24942
24931
|
},
|
|
@@ -24945,14 +24934,14 @@ var GreeIntegrationManager = class {
|
|
|
24945
24934
|
};
|
|
24946
24935
|
}
|
|
24947
24936
|
/**
|
|
24948
|
-
* Build the facade, run a
|
|
24949
|
-
*
|
|
24950
|
-
* on
|
|
24951
|
-
*
|
|
24937
|
+
* Build the facade, run a DIRECTED bind scan (aimed at `host`), bind the
|
|
24938
|
+
* responder for the expected AC, and publish the connection surface. Sets
|
|
24939
|
+
* `connected` on a successful bind. Surfaces a clear bind error on
|
|
24940
|
+
* {@link GreeAuthError}.
|
|
24952
24941
|
*/
|
|
24953
24942
|
async start() {
|
|
24954
24943
|
if (this.#facade !== null) {
|
|
24955
|
-
this.#logger.warn("GreeIntegrationManager: start() called while running — stop first", { tags: {
|
|
24944
|
+
this.#logger.warn("GreeIntegrationManager: start() called while running — stop first", { tags: { connectionKey: this.#id } });
|
|
24956
24945
|
return;
|
|
24957
24946
|
}
|
|
24958
24947
|
this.#status = "connecting";
|
|
@@ -24961,18 +24950,19 @@ var GreeIntegrationManager = class {
|
|
|
24961
24950
|
this.#facade = facade;
|
|
24962
24951
|
try {
|
|
24963
24952
|
const discovered = await facade.discover(this.#discoverOpts());
|
|
24953
|
+
const matched = this.#selectResponders(discovered);
|
|
24964
24954
|
const handles = /* @__PURE__ */ new Map();
|
|
24965
|
-
for (const dev of
|
|
24955
|
+
for (const dev of matched) try {
|
|
24966
24956
|
const ac = await facade.createAc({
|
|
24967
24957
|
ip: dev.ip,
|
|
24968
24958
|
port: dev.port,
|
|
24969
24959
|
mac: dev.mac
|
|
24970
24960
|
});
|
|
24971
24961
|
if (this.#pollIntervalMs > 0) ac.startPolling(this.#pollIntervalMs);
|
|
24972
|
-
handles.set(macKey
|
|
24962
|
+
handles.set(macKey(dev.mac), ac);
|
|
24973
24963
|
} catch (err) {
|
|
24974
24964
|
this.#logger.warn("GreeIntegrationManager: bind (createAc) failed", {
|
|
24975
|
-
tags: {
|
|
24965
|
+
tags: { connectionKey: this.#id },
|
|
24976
24966
|
meta: {
|
|
24977
24967
|
mac: dev.mac,
|
|
24978
24968
|
ip: dev.ip,
|
|
@@ -24982,22 +24972,24 @@ var GreeIntegrationManager = class {
|
|
|
24982
24972
|
}
|
|
24983
24973
|
this.#handles = handles;
|
|
24984
24974
|
const surface = {
|
|
24985
|
-
discovered,
|
|
24975
|
+
discovered: matched,
|
|
24986
24976
|
handles
|
|
24987
24977
|
};
|
|
24988
24978
|
this.#surfaceSink.set(this.#id, surface);
|
|
24989
24979
|
this.#deviceCount = handles.size;
|
|
24990
|
-
this.#status = "connected";
|
|
24991
|
-
this.#error = null;
|
|
24980
|
+
this.#status = handles.size > 0 ? "connected" : "error";
|
|
24981
|
+
this.#error = handles.size > 0 ? null : "AC not reachable — bind returned no handle";
|
|
24992
24982
|
this.#lastCheckedAt = Date.now();
|
|
24993
|
-
|
|
24994
|
-
|
|
24995
|
-
|
|
24996
|
-
|
|
24997
|
-
|
|
24998
|
-
|
|
24999
|
-
|
|
25000
|
-
|
|
24983
|
+
if (handles.size > 0) {
|
|
24984
|
+
this.#onConnected(this.#id);
|
|
24985
|
+
this.#logger.info("GreeIntegrationManager: connected", {
|
|
24986
|
+
tags: { connectionKey: this.#id },
|
|
24987
|
+
meta: {
|
|
24988
|
+
discovered: discovered.length,
|
|
24989
|
+
bound: handles.size
|
|
24990
|
+
}
|
|
24991
|
+
});
|
|
24992
|
+
} else this.#onDisconnected(this.#id);
|
|
25001
24993
|
} catch (err) {
|
|
25002
24994
|
this.#status = "error";
|
|
25003
24995
|
this.#error = err instanceof GreeAuthError ? "device bind failed — check the AC is reachable" : errMsg(err);
|
|
@@ -25005,7 +24997,7 @@ var GreeIntegrationManager = class {
|
|
|
25005
24997
|
this.#surfaceSink.set(this.#id, null);
|
|
25006
24998
|
this.#onDisconnected(this.#id);
|
|
25007
24999
|
this.#logger.warn("GreeIntegrationManager: scan failed", {
|
|
25008
|
-
tags: {
|
|
25000
|
+
tags: { connectionKey: this.#id },
|
|
25009
25001
|
meta: { error: this.#error }
|
|
25010
25002
|
});
|
|
25011
25003
|
throw err;
|
|
@@ -25025,7 +25017,7 @@ var GreeIntegrationManager = class {
|
|
|
25025
25017
|
await facade.close();
|
|
25026
25018
|
} catch (err) {
|
|
25027
25019
|
this.#logger.warn("GreeIntegrationManager: facade close failed", {
|
|
25028
|
-
tags: {
|
|
25020
|
+
tags: { connectionKey: this.#id },
|
|
25029
25021
|
meta: { error: errMsg(err) }
|
|
25030
25022
|
});
|
|
25031
25023
|
}
|
|
@@ -25037,534 +25029,87 @@ var GreeIntegrationManager = class {
|
|
|
25037
25029
|
this.#connection = conn;
|
|
25038
25030
|
await this.start();
|
|
25039
25031
|
}
|
|
25032
|
+
/** Keep only the responder(s) this manager should bind: the one matching the
|
|
25033
|
+
* expected MAC when set, else the one matching the configured host, else all. */
|
|
25034
|
+
#selectResponders(discovered) {
|
|
25035
|
+
if (this.#expectMac !== null) return discovered.filter((d) => macKey(d.mac) === this.#expectMac);
|
|
25036
|
+
const host = this.#connection.host.trim();
|
|
25037
|
+
if (host.length > 0) {
|
|
25038
|
+
const byHost = discovered.filter((d) => d.ip === host);
|
|
25039
|
+
if (byHost.length > 0) return byHost;
|
|
25040
|
+
}
|
|
25041
|
+
return [...discovered];
|
|
25042
|
+
}
|
|
25040
25043
|
#discoverOpts() {
|
|
25041
25044
|
const out = { timeoutMs: this.#connection.timeoutMs };
|
|
25042
|
-
|
|
25045
|
+
const broadcast = resolveBroadcastTarget(this.#connection);
|
|
25046
|
+
if (broadcast.length > 0) out.broadcastAddr = broadcast;
|
|
25043
25047
|
return out;
|
|
25044
25048
|
}
|
|
25045
25049
|
};
|
|
25046
|
-
/** True when two configs are the "same connection" (no facade rebuild needed). */
|
|
25047
|
-
function sameConnection(a, b) {
|
|
25048
|
-
if (a === null || b === null) return false;
|
|
25049
|
-
return a.broadcastAddr === b.broadcastAddr && a.timeoutMs === b.timeoutMs && a.retries === b.retries && a.encryption === b.encryption;
|
|
25050
|
-
}
|
|
25051
|
-
//#endregion
|
|
25052
|
-
//#region src/gree-broker-registry.ts
|
|
25053
|
-
/**
|
|
25054
|
-
* Manages N live Gree discovery scopes ("brokers"), each backed by a
|
|
25055
|
-
* {@link GreeIntegrationManager}. Allocates stable ids (`gree_001`, …), supports
|
|
25056
|
-
* CRUD, an integration FK index for cascade-delete, and lifecycle helpers.
|
|
25057
|
-
* Mirrors `DreoBrokerRegistry`.
|
|
25058
|
-
*/
|
|
25059
|
-
var GreeBrokerRegistry = class {
|
|
25060
|
-
#logger;
|
|
25061
|
-
#onBrokerConnected;
|
|
25062
|
-
#onBrokerDisconnected;
|
|
25063
|
-
#makeManager;
|
|
25064
|
-
#managers = /* @__PURE__ */ new Map();
|
|
25065
|
-
#integrationToBroker = /* @__PURE__ */ new Map();
|
|
25066
|
-
#nextId = 1;
|
|
25067
|
-
constructor(logger, deps = {}) {
|
|
25068
|
-
this.#logger = logger;
|
|
25069
|
-
this.#onBrokerConnected = deps.onBrokerConnected ?? (() => void 0);
|
|
25070
|
-
this.#onBrokerDisconnected = deps.onBrokerDisconnected ?? (() => void 0);
|
|
25071
|
-
this.#makeManager = deps.makeManager ?? ((opts) => new GreeIntegrationManager({
|
|
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
|
-
}));
|
|
25080
|
-
}
|
|
25081
|
-
/** Restore persisted broker entries on boot (best-effort per manager). */
|
|
25082
|
-
async restore(entries) {
|
|
25083
|
-
for (const entry of entries) this.#seedCounter(entry.id);
|
|
25084
|
-
for (const entry of entries) try {
|
|
25085
|
-
await this.#startManager(entry);
|
|
25086
|
-
} catch (err) {
|
|
25087
|
-
this.#logger.warn("GreeBrokerRegistry: failed to restore manager", {
|
|
25088
|
-
tags: { brokerId: entry.id },
|
|
25089
|
-
meta: { error: errMsg(err) }
|
|
25090
|
-
});
|
|
25091
|
-
}
|
|
25092
|
-
}
|
|
25093
|
-
/** Stop all managers and clear state. */
|
|
25094
|
-
async shutdown() {
|
|
25095
|
-
const ids = Array.from(this.#managers.keys());
|
|
25096
|
-
await Promise.all(ids.map(async (id) => {
|
|
25097
|
-
try {
|
|
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);
|
|
25133
|
-
try {
|
|
25134
|
-
await mgr.stop();
|
|
25135
|
-
} catch (err) {
|
|
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);
|
|
25146
|
-
return {
|
|
25147
|
-
id,
|
|
25148
|
-
name: mgr.getInfo().name,
|
|
25149
|
-
connection
|
|
25150
|
-
};
|
|
25151
|
-
}
|
|
25152
|
-
linkIntegration(integrationId, brokerId) {
|
|
25153
|
-
this.#integrationToBroker.set(integrationId, brokerId);
|
|
25154
|
-
}
|
|
25155
|
-
getBrokerIdByIntegrationId(integrationId) {
|
|
25156
|
-
const brokerId = this.#integrationToBroker.get(integrationId);
|
|
25157
|
-
if (brokerId === void 0) throw new Error(`GreeBrokerRegistry: no broker linked for integration id "${integrationId}"`);
|
|
25158
|
-
return brokerId;
|
|
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
25050
|
/**
|
|
25211
|
-
*
|
|
25212
|
-
*
|
|
25213
|
-
*
|
|
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).
|
|
25051
|
+
* Resolve the directed-scan target for a connection: the explicit
|
|
25052
|
+
* `broadcastAddr` when set, else the AC `host` (unicast-directed scan), else the
|
|
25053
|
+
* empty string (library default global broadcast).
|
|
25217
25054
|
*/
|
|
25218
|
-
function
|
|
25219
|
-
const
|
|
25220
|
-
|
|
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();
|
|
25055
|
+
function resolveBroadcastTarget(connection) {
|
|
25056
|
+
const broadcast = connection.broadcastAddr.trim();
|
|
25057
|
+
if (broadcast.length > 0) return broadcast;
|
|
25058
|
+
return connection.host.trim();
|
|
25330
25059
|
}
|
|
25331
25060
|
/**
|
|
25332
|
-
*
|
|
25333
|
-
*
|
|
25334
|
-
*
|
|
25335
|
-
*
|
|
25336
|
-
|
|
25337
|
-
|
|
25338
|
-
|
|
25339
|
-
|
|
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).
|
|
25061
|
+
* Create-time one-shot directed bind: build a throwaway facade, run a directed
|
|
25062
|
+
* scan (aimed at the connection's `host`/`broadcastAddr`), bind the matching
|
|
25063
|
+
* responder to prove the AC is reachable and learn its durable identity, then
|
|
25064
|
+
* close the facade. Returns the AC's identity (MAC/ip/name/model). Throws when no
|
|
25065
|
+
* AC responds or the bind fails — surfaced to the operator in the Add modal.
|
|
25066
|
+
*
|
|
25067
|
+
* The returned identity is what {@link import('./config.js').greeAcDeviceSchema}
|
|
25068
|
+
* persists on the new device; the live per-device manager re-binds on activate.
|
|
25382
25069
|
*/
|
|
25383
|
-
function
|
|
25384
|
-
const {
|
|
25385
|
-
|
|
25386
|
-
|
|
25387
|
-
|
|
25388
|
-
|
|
25070
|
+
async function bindOnce(input) {
|
|
25071
|
+
const { connection, logger } = input;
|
|
25072
|
+
const facade = (input.makeFacade ?? defaultFacade)(connection);
|
|
25073
|
+
try {
|
|
25074
|
+
const broadcast = resolveBroadcastTarget(connection);
|
|
25075
|
+
const opts = { timeoutMs: connection.timeoutMs };
|
|
25076
|
+
if (broadcast.length > 0) opts.broadcastAddr = broadcast;
|
|
25077
|
+
const discovered = await facade.discover(opts);
|
|
25078
|
+
const host = connection.host.trim();
|
|
25079
|
+
const target = host.length > 0 ? discovered.find((d) => d.ip === host) ?? discovered[0] : discovered[0];
|
|
25080
|
+
if (target === void 0) throw new Error(host.length > 0 ? `no Gree AC responded at ${host}` : "no Gree AC responded to the discovery scan");
|
|
25081
|
+
const ac = await facade.createAc({
|
|
25082
|
+
ip: target.ip,
|
|
25083
|
+
port: target.port,
|
|
25084
|
+
mac: target.mac
|
|
25389
25085
|
});
|
|
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;
|
|
25533
25086
|
try {
|
|
25534
|
-
|
|
25535
|
-
|
|
25536
|
-
|
|
25537
|
-
|
|
25538
|
-
|
|
25539
|
-
|
|
25540
|
-
|
|
25541
|
-
|
|
25542
|
-
|
|
25543
|
-
|
|
25544
|
-
|
|
25087
|
+
ac.stopPolling();
|
|
25088
|
+
} catch {}
|
|
25089
|
+
return {
|
|
25090
|
+
mac: target.mac,
|
|
25091
|
+
ip: target.ip,
|
|
25092
|
+
port: target.port,
|
|
25093
|
+
name: target.name.length > 0 ? target.name : target.mac,
|
|
25094
|
+
...target.model !== void 0 ? { model: target.model } : {}
|
|
25095
|
+
};
|
|
25096
|
+
} catch (err) {
|
|
25097
|
+
const message = err instanceof GreeAuthError ? "device bind failed — check the AC is reachable and the encryption mode" : errMsg(err);
|
|
25098
|
+
logger.warn("gree bindOnce failed", { meta: {
|
|
25099
|
+
host: connection.host,
|
|
25100
|
+
error: message
|
|
25101
|
+
} });
|
|
25102
|
+
throw new Error(message);
|
|
25103
|
+
} finally {
|
|
25104
|
+
try {
|
|
25105
|
+
await facade.close();
|
|
25106
|
+
} catch {}
|
|
25545
25107
|
}
|
|
25546
|
-
return removed;
|
|
25547
25108
|
}
|
|
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;
|
|
25109
|
+
/** True when two configs are the "same connection" (no facade rebuild needed). */
|
|
25110
|
+
function sameConnection(a, b) {
|
|
25111
|
+
if (a === null || b === null) return false;
|
|
25112
|
+
return a.host === b.host && a.broadcastAddr === b.broadcastAddr && a.timeoutMs === b.timeoutMs && a.retries === b.retries && a.encryption === b.encryption;
|
|
25568
25113
|
}
|
|
25569
25114
|
//#endregion
|
|
25570
25115
|
//#region src/gree-domain-mapping.ts
|
|
@@ -25812,12 +25357,13 @@ var FAN_COLD_START = {
|
|
|
25812
25357
|
};
|
|
25813
25358
|
/**
|
|
25814
25359
|
* Persisted config every Gree AC accessory child carries. `greeMac` resolves the
|
|
25815
|
-
* bound handle from the connection resolver; `
|
|
25816
|
-
*
|
|
25360
|
+
* bound handle from the connection resolver; `connectionKey` selects the parent
|
|
25361
|
+
* AC device's live connection (its own stableId); `system`/`integrationId` are
|
|
25362
|
+
* provenance.
|
|
25817
25363
|
*/
|
|
25818
25364
|
var greeAcSchema = object({
|
|
25819
25365
|
greeMac: string().min(1).describe("Gree AC MAC address"),
|
|
25820
|
-
|
|
25366
|
+
connectionKey: string().min(1).describe("Per-device connection resolver key"),
|
|
25821
25367
|
system: literal("gree").optional(),
|
|
25822
25368
|
integrationId: string().optional()
|
|
25823
25369
|
});
|
|
@@ -25868,13 +25414,13 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
25868
25414
|
return flags;
|
|
25869
25415
|
}
|
|
25870
25416
|
greeMac;
|
|
25871
|
-
|
|
25417
|
+
connectionKey;
|
|
25872
25418
|
stateChangedUnsub = null;
|
|
25873
25419
|
constructor(ctx) {
|
|
25874
25420
|
const persisted = greeAcSchema.parse(ctx.persistedConfig ?? {});
|
|
25875
25421
|
super(ctx, greeAcSchema, { type: ctx.deviceMeta.type });
|
|
25876
25422
|
this.greeMac = persisted.greeMac;
|
|
25877
|
-
this.
|
|
25423
|
+
this.connectionKey = persisted.connectionKey;
|
|
25878
25424
|
this.online = true;
|
|
25879
25425
|
this.updateSourceInfo({
|
|
25880
25426
|
id: this.greeMac,
|
|
@@ -25882,7 +25428,7 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
25882
25428
|
});
|
|
25883
25429
|
}
|
|
25884
25430
|
resolveAc() {
|
|
25885
|
-
return greeConnections.getDevice(this.
|
|
25431
|
+
return greeConnections.getDevice(this.connectionKey, this.greeMac);
|
|
25886
25432
|
}
|
|
25887
25433
|
requireAc() {
|
|
25888
25434
|
const ac = this.resolveAc();
|
|
@@ -26042,9 +25588,10 @@ var SWITCH_COLD_START = {
|
|
|
26042
25588
|
};
|
|
26043
25589
|
/**
|
|
26044
25590
|
* 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
|
|
25591
|
+
* {@link greeAcSchema}: `greeMac` resolves the bound handle, `connectionKey`
|
|
25592
|
+
* selects the parent AC device's live connection, `system`/`integrationId` are
|
|
25593
|
+
* provenance. `toggle` selects which of the four boolean device flags this child
|
|
25594
|
+
* drives.
|
|
26048
25595
|
*/
|
|
26049
25596
|
var greeToggleSchema = object({
|
|
26050
25597
|
toggle: _enum([
|
|
@@ -26054,7 +25601,7 @@ var greeToggleSchema = object({
|
|
|
26054
25601
|
"freshAir"
|
|
26055
25602
|
]).describe("Which Gree boolean flag"),
|
|
26056
25603
|
greeMac: string().min(1).describe("Gree AC MAC address"),
|
|
26057
|
-
|
|
25604
|
+
connectionKey: string().min(1).describe("Per-device connection resolver key"),
|
|
26058
25605
|
system: literal("gree").optional(),
|
|
26059
25606
|
integrationId: string().optional()
|
|
26060
25607
|
});
|
|
@@ -26072,14 +25619,14 @@ var GreeToggleDevice = class extends BaseDevice$1 {
|
|
|
26072
25619
|
features = [];
|
|
26073
25620
|
toggle;
|
|
26074
25621
|
greeMac;
|
|
26075
|
-
|
|
25622
|
+
connectionKey;
|
|
26076
25623
|
stateChangedUnsub = null;
|
|
26077
25624
|
constructor(ctx) {
|
|
26078
25625
|
const persisted = greeToggleSchema.parse(ctx.persistedConfig ?? {});
|
|
26079
25626
|
super(ctx, greeToggleSchema, { type: ctx.deviceMeta.type });
|
|
26080
25627
|
this.toggle = persisted.toggle;
|
|
26081
25628
|
this.greeMac = persisted.greeMac;
|
|
26082
|
-
this.
|
|
25629
|
+
this.connectionKey = persisted.connectionKey;
|
|
26083
25630
|
this.online = true;
|
|
26084
25631
|
this.updateSourceInfo({
|
|
26085
25632
|
id: `${this.greeMac}:${this.toggle}`,
|
|
@@ -26087,7 +25634,7 @@ var GreeToggleDevice = class extends BaseDevice$1 {
|
|
|
26087
25634
|
});
|
|
26088
25635
|
}
|
|
26089
25636
|
resolveAc() {
|
|
26090
|
-
return greeConnections.getDevice(this.
|
|
25637
|
+
return greeConnections.getDevice(this.connectionKey, this.greeMac);
|
|
26091
25638
|
}
|
|
26092
25639
|
requireAc() {
|
|
26093
25640
|
const ac = this.resolveAc();
|
|
@@ -26187,49 +25734,117 @@ var GreeToggleDevice = class extends BaseDevice$1 {
|
|
|
26187
25734
|
//#endregion
|
|
26188
25735
|
//#region src/devices/gree-container-device.ts
|
|
26189
25736
|
/**
|
|
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`.
|
|
25737
|
+
* Standalone-mode parent Container device for a single Gree AC (Reolink /
|
|
25738
|
+
* Ecowitt pattern). The operator-supplied CONNECTION lives on THIS device's
|
|
25739
|
+
* config; the device OWNS exactly one live `@apocaliss92/nodegree` client (via a
|
|
25740
|
+
* {@link GreeIntegrationManager}) keyed on its own `connectionKey` and published
|
|
25741
|
+
* on the in-process {@link greeConnections} resolver so its AC + toggle accessory
|
|
25742
|
+
* children read the live bound handle. There is NO broker and NO device-adoption
|
|
25743
|
+
* cap.
|
|
26209
25744
|
*
|
|
26210
|
-
*
|
|
26211
|
-
*
|
|
26212
|
-
*
|
|
26213
|
-
*
|
|
26214
|
-
*
|
|
25745
|
+
* It owns no control caps itself — it declares `getAccessoryChildren()` so the
|
|
25746
|
+
* kernel auto-spawns the single {@link GreeAcDevice} accessory child (Thermostat,
|
|
25747
|
+
* carrying `climate-control` + `fan-control` with the swing entities) plus
|
|
25748
|
+
* presence-gated {@link DeviceType.Switch} children for the panel-light / X-Fan /
|
|
25749
|
+
* health / fresh-air boolean flags (mirroring the HA Gree integration).
|
|
25750
|
+
* Capability presence is resolved from the bound handle's `AcCapabilities` (or
|
|
25751
|
+
* model defaults when no live handle).
|
|
26215
25752
|
*/
|
|
26216
25753
|
var GreeContainerDevice = class extends BaseDevice$1 {
|
|
26217
25754
|
features = [DeviceFeature.Resyncable];
|
|
26218
25755
|
greeMac;
|
|
26219
|
-
|
|
25756
|
+
connectionKey;
|
|
26220
25757
|
integrationId;
|
|
25758
|
+
connection;
|
|
25759
|
+
manager = null;
|
|
26221
25760
|
constructor(ctx) {
|
|
26222
|
-
const cfg =
|
|
26223
|
-
super(ctx,
|
|
25761
|
+
const cfg = greeAcDeviceSchema.parse(ctx.persistedConfig ?? {});
|
|
25762
|
+
super(ctx, greeAcDeviceSchema, { type: DeviceType.Container });
|
|
26224
25763
|
this.greeMac = cfg.greeMac;
|
|
26225
|
-
this.
|
|
25764
|
+
this.connectionKey = cfg.connectionKey;
|
|
26226
25765
|
this.integrationId = cfg.integrationId;
|
|
26227
|
-
this.
|
|
25766
|
+
this.connection = cfg.connection;
|
|
25767
|
+
this.online = greeConnections.getDevice(this.connectionKey, this.greeMac) !== null;
|
|
26228
25768
|
this.updateSourceInfo({
|
|
26229
25769
|
id: this.greeMac,
|
|
26230
25770
|
system: "gree"
|
|
26231
25771
|
});
|
|
26232
25772
|
}
|
|
25773
|
+
/** Adopt a pre-built manager (created by `onCreateDevice` so the live client
|
|
25774
|
+
* binds once at create time) — mirrors `EcowittGatewayDevice.adoptManager`. */
|
|
25775
|
+
adoptManager(manager) {
|
|
25776
|
+
this.manager = manager;
|
|
25777
|
+
}
|
|
25778
|
+
async onActivate() {
|
|
25779
|
+
await super.onActivate();
|
|
25780
|
+
if (this.manager === null) this.ensureManager().catch((err) => {
|
|
25781
|
+
this.ctx.logger.warn("Gree AC initial bind failed", { meta: {
|
|
25782
|
+
greeMac: this.greeMac,
|
|
25783
|
+
error: errMsg(err)
|
|
25784
|
+
} });
|
|
25785
|
+
});
|
|
25786
|
+
}
|
|
25787
|
+
async removeDevice() {
|
|
25788
|
+
const mgr = this.manager;
|
|
25789
|
+
this.manager = null;
|
|
25790
|
+
if (mgr) try {
|
|
25791
|
+
await mgr.stop();
|
|
25792
|
+
} catch (err) {
|
|
25793
|
+
this.ctx.logger.warn("Gree AC: client stop failed", { meta: {
|
|
25794
|
+
greeMac: this.greeMac,
|
|
25795
|
+
error: errMsg(err)
|
|
25796
|
+
} });
|
|
25797
|
+
}
|
|
25798
|
+
}
|
|
25799
|
+
async applySettingsPatch(patch) {
|
|
25800
|
+
if (!("connection" in patch)) return;
|
|
25801
|
+
const parsed = greeAcDeviceSchema.shape.connection.parse(patch["connection"]);
|
|
25802
|
+
await this.config.setAll({ connection: parsed });
|
|
25803
|
+
this.connection = parsed;
|
|
25804
|
+
const mgr = this.manager;
|
|
25805
|
+
if (mgr) try {
|
|
25806
|
+
await mgr.applyConnection(parsed);
|
|
25807
|
+
} catch (err) {
|
|
25808
|
+
this.ctx.logger.warn("Gree AC: applyConnection failed", { meta: {
|
|
25809
|
+
greeMac: this.greeMac,
|
|
25810
|
+
error: errMsg(err)
|
|
25811
|
+
} });
|
|
25812
|
+
}
|
|
25813
|
+
else await this.ensureManager().catch((err) => {
|
|
25814
|
+
this.ctx.logger.warn("Gree AC re-bind after settings change failed", { meta: {
|
|
25815
|
+
greeMac: this.greeMac,
|
|
25816
|
+
error: errMsg(err)
|
|
25817
|
+
} });
|
|
25818
|
+
});
|
|
25819
|
+
}
|
|
25820
|
+
/** Build + start the per-device nodegree client (if not already running). */
|
|
25821
|
+
async ensureManager() {
|
|
25822
|
+
if (this.manager !== null) return;
|
|
25823
|
+
const mgr = new GreeIntegrationManager({
|
|
25824
|
+
id: this.connectionKey,
|
|
25825
|
+
name: this.name,
|
|
25826
|
+
connection: this.connection,
|
|
25827
|
+
logger: this.ctx.logger,
|
|
25828
|
+
onConnected: () => this.setAcOnline(true),
|
|
25829
|
+
onDisconnected: () => this.setAcOnline(false),
|
|
25830
|
+
surfaceSink: greeConnections,
|
|
25831
|
+
expectMac: this.greeMac
|
|
25832
|
+
});
|
|
25833
|
+
this.manager = mgr;
|
|
25834
|
+
await mgr.start();
|
|
25835
|
+
}
|
|
25836
|
+
setAcOnline(online) {
|
|
25837
|
+
if (this.online === online) return;
|
|
25838
|
+
this.markOnline(online);
|
|
25839
|
+
this.setChildrenOnline(online).catch(() => {});
|
|
25840
|
+
}
|
|
25841
|
+
async setChildrenOnline(online) {
|
|
25842
|
+
const children = await this.ctx.devices.getChildren(this.id).catch(() => []);
|
|
25843
|
+
for (const child of children) {
|
|
25844
|
+
if (child.online === online) continue;
|
|
25845
|
+
child.markOnline(online);
|
|
25846
|
+
}
|
|
25847
|
+
}
|
|
26233
25848
|
getAccessoryChildren() {
|
|
26234
25849
|
return [{
|
|
26235
25850
|
stableIdSuffix: "ac",
|
|
@@ -26241,7 +25856,7 @@ var GreeContainerDevice = class extends BaseDevice$1 {
|
|
|
26241
25856
|
},
|
|
26242
25857
|
config: {
|
|
26243
25858
|
greeMac: this.greeMac,
|
|
26244
|
-
|
|
25859
|
+
connectionKey: this.connectionKey,
|
|
26245
25860
|
system: "gree",
|
|
26246
25861
|
...this.integrationId !== void 0 ? { integrationId: this.integrationId } : {}
|
|
26247
25862
|
},
|
|
@@ -26255,7 +25870,7 @@ var GreeContainerDevice = class extends BaseDevice$1 {
|
|
|
26255
25870
|
* defaults when no live handle is bound yet).
|
|
26256
25871
|
*/
|
|
26257
25872
|
toggleChildren() {
|
|
26258
|
-
const caps = greeConnections.getDevice(this.
|
|
25873
|
+
const caps = greeConnections.getDevice(this.connectionKey, this.greeMac)?.capabilities ?? DEFAULT_AC_CAPABILITIES;
|
|
26259
25874
|
return [
|
|
26260
25875
|
{
|
|
26261
25876
|
toggle: "light",
|
|
@@ -26291,7 +25906,7 @@ var GreeContainerDevice = class extends BaseDevice$1 {
|
|
|
26291
25906
|
const childConfig = {
|
|
26292
25907
|
toggle: d.toggle,
|
|
26293
25908
|
greeMac: this.greeMac,
|
|
26294
|
-
|
|
25909
|
+
connectionKey: this.connectionKey,
|
|
26295
25910
|
system: "gree",
|
|
26296
25911
|
...this.integrationId !== void 0 ? { integrationId: this.integrationId } : {}
|
|
26297
25912
|
};
|
|
@@ -26306,284 +25921,100 @@ var GreeContainerDevice = class extends BaseDevice$1 {
|
|
|
26306
25921
|
};
|
|
26307
25922
|
//#endregion
|
|
26308
25923
|
//#region src/addon.ts
|
|
26309
|
-
/** Default multi-broker config — a fresh install starts with no scopes. */
|
|
26310
|
-
var DEFAULTS = { brokers: [] };
|
|
26311
25924
|
/**
|
|
26312
|
-
* Gree device-provider addon (
|
|
25925
|
+
* Gree air-conditioner device-provider addon — `mode: standalone` (the Reolink /
|
|
25926
|
+
* Ecowitt pattern). No broker, no device-adoption cap. An AC is added as a DEVICE
|
|
25927
|
+
* via the manual-creation form: the CONNECTION (host IP + optional broadcastAddr
|
|
25928
|
+
* + UDP tuning) lives on the AC DEVICE config. `onCreateDevice` runs a DIRECTED
|
|
25929
|
+
* bind against the given host to learn the AC's durable identity (MAC) and prove
|
|
25930
|
+
* reachability, then creates a {@link GreeContainerDevice} that OWNS its own live
|
|
25931
|
+
* `@apocaliss92/nodegree` client and fans out its Thermostat + toggle accessory
|
|
25932
|
+
* children (climate-control with swing entities + the panel-light / X-Fan /
|
|
25933
|
+
* health / fresh-air switches — all preserved from the broker model).
|
|
26313
25934
|
*
|
|
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.
|
|
25935
|
+
* `instanceMode: multiple` — many ACs may be added. Placement: any-node — Gree is
|
|
25936
|
+
* LOCAL UDP; the addon runs on whichever node shares the AC's subnet.
|
|
26322
25937
|
*
|
|
26323
|
-
*
|
|
26324
|
-
*
|
|
26325
|
-
*
|
|
26326
|
-
* The operator points each scope's `broadcastAddr` at the right subnet.
|
|
25938
|
+
* LAN UDP-broadcast auto-discovery of multiple ACs is deliberately NOT
|
|
25939
|
+
* implemented here — that is the later Discovery-cap phase. Phase 1 is
|
|
25940
|
+
* add-one-AC-by-IP with a directed bind.
|
|
26327
25941
|
*/
|
|
26328
25942
|
var GreeProviderAddon = class extends BaseDeviceProvider {
|
|
26329
25943
|
addonId = "provider-gree";
|
|
26330
25944
|
providerName = "Gree";
|
|
26331
25945
|
deviceClasses = { [DeviceType.Container]: GreeContainerDevice };
|
|
26332
|
-
registry = null;
|
|
26333
25946
|
constructor() {
|
|
26334
|
-
super({
|
|
26335
|
-
}
|
|
26336
|
-
async onInitialize() {
|
|
26337
|
-
const regs = await super.onInitialize();
|
|
26338
|
-
this.registry = new GreeBrokerRegistry(this.ctx.logger);
|
|
26339
|
-
this.registry.setOnBrokerConnected((brokerId) => {
|
|
26340
|
-
this.ctx.logger.info("Gree: broker connected", { meta: { brokerId } });
|
|
26341
|
-
this.setBrokerDevicesOnline(brokerId, true);
|
|
26342
|
-
});
|
|
26343
|
-
this.registry.setOnBrokerDisconnected((brokerId) => {
|
|
26344
|
-
this.setBrokerDevicesOnline(brokerId, false);
|
|
26345
|
-
});
|
|
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
|
-
];
|
|
25947
|
+
super({});
|
|
26361
25948
|
}
|
|
26362
|
-
|
|
26363
|
-
|
|
26364
|
-
async onConfigChanged() {
|
|
26365
|
-
const reg = this.registry;
|
|
26366
|
-
if (!reg) return;
|
|
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
|
-
}
|
|
25949
|
+
async supportsDiscovery() {
|
|
25950
|
+
return false;
|
|
26407
25951
|
}
|
|
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();
|
|
25952
|
+
async supportsManualCreation() {
|
|
25953
|
+
return true;
|
|
26416
25954
|
}
|
|
26417
|
-
|
|
26418
|
-
|
|
26419
|
-
return
|
|
26420
|
-
}
|
|
26421
|
-
|
|
26422
|
-
|
|
26423
|
-
|
|
26424
|
-
|
|
26425
|
-
|
|
26426
|
-
|
|
26427
|
-
|
|
25955
|
+
async onGetCreationSchema(type) {
|
|
25956
|
+
if (type !== DeviceType.Container) return null;
|
|
25957
|
+
return buildConnectionFormSchema();
|
|
25958
|
+
}
|
|
25959
|
+
async onCreateDevice(type, config) {
|
|
25960
|
+
if (type !== DeviceType.Container) throw new Error(`Gree provider does not support device type: ${type}`);
|
|
25961
|
+
const name = typeof config["name"] === "string" ? config["name"].trim() : "";
|
|
25962
|
+
if (!name) throw new Error("Air conditioner name is required");
|
|
25963
|
+
const connection = settingsToGreeConfig(config);
|
|
25964
|
+
if (connection.host.trim().length === 0) throw new Error("Air conditioner IP address is required");
|
|
25965
|
+
const bound = await bindOnce({
|
|
25966
|
+
connection,
|
|
26428
25967
|
logger: this.ctx.logger
|
|
26429
25968
|
});
|
|
26430
|
-
|
|
26431
|
-
|
|
26432
|
-
|
|
26433
|
-
|
|
26434
|
-
|
|
26435
|
-
|
|
26436
|
-
|
|
26437
|
-
|
|
26438
|
-
|
|
26439
|
-
|
|
26440
|
-
|
|
25969
|
+
const connectionKey = `gree:${macKey(bound.mac)}`;
|
|
25970
|
+
const deviceConfig = {
|
|
25971
|
+
greeMac: macKey(bound.mac),
|
|
25972
|
+
greeIp: bound.ip,
|
|
25973
|
+
connectionKey,
|
|
25974
|
+
connection,
|
|
25975
|
+
system: "gree",
|
|
25976
|
+
name
|
|
25977
|
+
};
|
|
25978
|
+
const manager = new GreeIntegrationManager({
|
|
25979
|
+
id: connectionKey,
|
|
25980
|
+
name,
|
|
25981
|
+
connection,
|
|
25982
|
+
logger: this.ctx.logger,
|
|
25983
|
+
onConnected: () => void 0,
|
|
25984
|
+
onDisconnected: () => void 0,
|
|
25985
|
+
surfaceSink: greeConnections,
|
|
25986
|
+
expectMac: bound.mac
|
|
26441
25987
|
});
|
|
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
25988
|
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 });
|
|
25989
|
+
await manager.start();
|
|
26480
25990
|
} catch (err) {
|
|
26481
|
-
this.ctx.logger.warn("Gree
|
|
25991
|
+
this.ctx.logger.warn("Gree create: initial client start failed — device kept", { meta: {
|
|
25992
|
+
connectionKey,
|
|
25993
|
+
error: errMsg(err)
|
|
25994
|
+
} });
|
|
26482
25995
|
}
|
|
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);
|
|
25996
|
+
return {
|
|
25997
|
+
meta: {
|
|
25998
|
+
type: DeviceType.Container,
|
|
25999
|
+
name
|
|
26543
26000
|
},
|
|
26544
|
-
|
|
26545
|
-
|
|
26546
|
-
if (
|
|
26547
|
-
return devices.loadConfig(id).catch(() => null);
|
|
26001
|
+
config: deviceConfig,
|
|
26002
|
+
onAfterCreate: async (device) => {
|
|
26003
|
+
if (device instanceof GreeContainerDevice) device.adoptManager(manager);
|
|
26548
26004
|
}
|
|
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
|
-
} });
|
|
26005
|
+
};
|
|
26582
26006
|
}
|
|
26583
|
-
|
|
26584
|
-
|
|
26585
|
-
|
|
26007
|
+
/**
|
|
26008
|
+
* AC stableId — derived from the AC's durable MAC identity (resolved by the
|
|
26009
|
+
* create-time bind), NOT a broker id. Re-adding the same physical AC reuses its
|
|
26010
|
+
* persisted row. Falls back to a timestamp only when no MAC is resolvable
|
|
26011
|
+
* (should not happen — `onCreateDevice` binds first).
|
|
26012
|
+
*/
|
|
26013
|
+
generateStableId(_type, config) {
|
|
26014
|
+
const mac = config?.["greeMac"];
|
|
26015
|
+
if (typeof mac === "string" && mac.length > 0) return `gree:${macKey(mac)}`;
|
|
26016
|
+
return `gree:${Date.now()}`;
|
|
26586
26017
|
}
|
|
26587
26018
|
};
|
|
26588
26019
|
//#endregion
|
|
26589
|
-
export { GreeProviderAddon,
|
|
26020
|
+
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 };
|