@camstack/addon-provider-gree 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/addon.js +375 -21
- package/dist/addon.mjs +375 -21
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -4645,7 +4645,7 @@ function preprocess(fn, schema) {
|
|
|
4645
4645
|
});
|
|
4646
4646
|
}
|
|
4647
4647
|
//#endregion
|
|
4648
|
-
//#region ../types/dist/sleep-
|
|
4648
|
+
//#region ../types/dist/sleep-C2M2zF7x.mjs
|
|
4649
4649
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4650
4650
|
EventCategory["SystemBoot"] = "system.boot";
|
|
4651
4651
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -6213,6 +6213,12 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
|
6213
6213
|
DeviceType["Switch"] = "switch";
|
|
6214
6214
|
DeviceType["Sensor"] = "sensor";
|
|
6215
6215
|
DeviceType["Thermostat"] = "thermostat";
|
|
6216
|
+
/** Air-conditioner / heat-pump climate device (HVAC) — shares the
|
|
6217
|
+
* `climate-control` cap surface with `Thermostat` but renders a
|
|
6218
|
+
* dedicated AC-appropriate control UI (mode chips, fan speed,
|
|
6219
|
+
* independent vertical/horizontal swing). Sources: native Gree, and
|
|
6220
|
+
* reusable by other AC integrations. */
|
|
6221
|
+
DeviceType["Climate"] = "climate";
|
|
6216
6222
|
DeviceType["Button"] = "button";
|
|
6217
6223
|
/** Generic stateless event emitter — carries a device's EXACT declared
|
|
6218
6224
|
* event vocabulary verbatim (no normalization). Installed with the
|
|
@@ -9008,7 +9014,7 @@ var climateControlCapability = {
|
|
|
9008
9014
|
scope: "device",
|
|
9009
9015
|
deviceNative: true,
|
|
9010
9016
|
mode: "singleton",
|
|
9011
|
-
deviceTypes: [DeviceType.Thermostat],
|
|
9017
|
+
deviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
|
|
9012
9018
|
methods: {
|
|
9013
9019
|
setMode: method(object({
|
|
9014
9020
|
deviceId: number().int().nonnegative(),
|
|
@@ -13607,10 +13613,30 @@ var deviceProviderCapability = {
|
|
|
13607
13613
|
type: string()
|
|
13608
13614
|
}))),
|
|
13609
13615
|
supportsDiscovery: method(object({}), boolean()),
|
|
13610
|
-
|
|
13616
|
+
/**
|
|
13617
|
+
* Run a network scan. `params` carries optional provider-specific scan
|
|
13618
|
+
* inputs (e.g. a broadcast address / subnet for cross-subnet discovery),
|
|
13619
|
+
* shaped by `getDiscoveryParamsSchema`. Omitted for the generic scan
|
|
13620
|
+
* (provider uses its local-network default).
|
|
13621
|
+
*/
|
|
13622
|
+
discoverDevices: method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
|
|
13611
13623
|
kind: "mutation",
|
|
13612
13624
|
auth: "admin"
|
|
13613
13625
|
}),
|
|
13626
|
+
/**
|
|
13627
|
+
* Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
|
|
13628
|
+
* provider accepts (e.g. Gree's broadcast address for a different subnet).
|
|
13629
|
+
* `null` when the provider takes no extra scan params — the generic
|
|
13630
|
+
* aggregated scan never renders this; the per-integration scan does.
|
|
13631
|
+
*/
|
|
13632
|
+
getDiscoveryParamsSchema: method(object({}), CreationSchemaOutputSchema),
|
|
13633
|
+
/**
|
|
13634
|
+
* The DeviceType this provider creates via manual add (Camera for
|
|
13635
|
+
* Reolink/ONVIF, Container for Gree, Hub for Ecowitt). `null` when the
|
|
13636
|
+
* provider does not support manual creation. Lets the Add-Device dialog
|
|
13637
|
+
* pick the right type instead of assuming Camera.
|
|
13638
|
+
*/
|
|
13639
|
+
getManualCreationType: method(object({}), object({ deviceType: _enum(DeviceType).nullable() })),
|
|
13614
13640
|
adoptDiscoveredDevice: method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
|
|
13615
13641
|
kind: "mutation",
|
|
13616
13642
|
auth: "admin"
|
|
@@ -13734,9 +13760,23 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
13734
13760
|
async supportsDiscovery() {
|
|
13735
13761
|
return false;
|
|
13736
13762
|
}
|
|
13737
|
-
async discoverDevices() {
|
|
13763
|
+
async discoverDevices(_input) {
|
|
13738
13764
|
return [];
|
|
13739
13765
|
}
|
|
13766
|
+
/** Extra per-scan input form (e.g. a broadcast address for another subnet).
|
|
13767
|
+
* Null = no extra params. Override in providers that support scoped scans. */
|
|
13768
|
+
async getDiscoveryParamsSchema() {
|
|
13769
|
+
return null;
|
|
13770
|
+
}
|
|
13771
|
+
/**
|
|
13772
|
+
* The DeviceType this provider creates via manual add — derived from the
|
|
13773
|
+
* `deviceClasses` map (first registered type). `null` when manual creation is
|
|
13774
|
+
* unsupported. Lets the Add-Device dialog pick the right type per provider.
|
|
13775
|
+
*/
|
|
13776
|
+
async getManualCreationType() {
|
|
13777
|
+
if (!await this.supportsManualCreation()) return { deviceType: null };
|
|
13778
|
+
return { deviceType: Object.values(DeviceType).find((t) => this.deviceClasses[t] !== void 0) ?? null };
|
|
13779
|
+
}
|
|
13740
13780
|
async adoptDiscoveredDevice(_input) {
|
|
13741
13781
|
throw new Error(`${this.providerName} provider does not support discovery-based adoption`);
|
|
13742
13782
|
}
|
|
@@ -15581,7 +15621,10 @@ method(object({
|
|
|
15581
15621
|
}), FieldProbeResultSchema, {
|
|
15582
15622
|
kind: "mutation",
|
|
15583
15623
|
auth: "admin"
|
|
15584
|
-
}), method(
|
|
15624
|
+
}), method(object({
|
|
15625
|
+
addonId: string(),
|
|
15626
|
+
integrationId: string()
|
|
15627
|
+
}), object({ filters: array(AdoptionFilterSchema) }), { auth: "admin" }), method(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
|
|
15585
15628
|
addonId: string(),
|
|
15586
15629
|
integrationId: string()
|
|
15587
15630
|
}), AdoptionStatusSchema, {
|
|
@@ -15596,7 +15639,24 @@ method(object({
|
|
|
15596
15639
|
}), method(ResyncInputSchema, ResyncResultSchema, {
|
|
15597
15640
|
kind: "mutation",
|
|
15598
15641
|
auth: "admin"
|
|
15642
|
+
}), method(object({}), object({ providers: array(object({
|
|
15643
|
+
addonId: string(),
|
|
15644
|
+
label: string()
|
|
15645
|
+
})).readonly() }), { auth: "admin" }), method(object({}), object({ groups: array(object({
|
|
15646
|
+
addonId: string(),
|
|
15647
|
+
label: string(),
|
|
15648
|
+
candidates: array(DiscoveryCandidateSchema).readonly(),
|
|
15649
|
+
error: string().nullable()
|
|
15650
|
+
})).readonly() }), {
|
|
15651
|
+
kind: "mutation",
|
|
15652
|
+
auth: "admin"
|
|
15599
15653
|
}), method(object({
|
|
15654
|
+
addonId: string(),
|
|
15655
|
+
params: record(string(), unknown()).optional()
|
|
15656
|
+
}), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
|
|
15657
|
+
kind: "mutation",
|
|
15658
|
+
auth: "admin"
|
|
15659
|
+
}), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
|
|
15600
15660
|
deviceId: number(),
|
|
15601
15661
|
key: string(),
|
|
15602
15662
|
value: unknown()
|
|
@@ -20719,6 +20779,12 @@ Object.freeze({
|
|
|
20719
20779
|
addonId: null,
|
|
20720
20780
|
access: "create"
|
|
20721
20781
|
},
|
|
20782
|
+
"deviceManager.adoptionListCandidateFilters": {
|
|
20783
|
+
capName: "device-manager",
|
|
20784
|
+
capScope: "system",
|
|
20785
|
+
addonId: null,
|
|
20786
|
+
access: "view"
|
|
20787
|
+
},
|
|
20722
20788
|
"deviceManager.adoptionListCandidates": {
|
|
20723
20789
|
capName: "device-manager",
|
|
20724
20790
|
capScope: "system",
|
|
@@ -20767,12 +20833,30 @@ Object.freeze({
|
|
|
20767
20833
|
addonId: null,
|
|
20768
20834
|
access: "create"
|
|
20769
20835
|
},
|
|
20836
|
+
"deviceManager.discoverAllProviders": {
|
|
20837
|
+
capName: "device-manager",
|
|
20838
|
+
capScope: "system",
|
|
20839
|
+
addonId: null,
|
|
20840
|
+
access: "create"
|
|
20841
|
+
},
|
|
20770
20842
|
"deviceManager.discoverDevices": {
|
|
20771
20843
|
capName: "device-manager",
|
|
20772
20844
|
capScope: "system",
|
|
20773
20845
|
addonId: null,
|
|
20774
20846
|
access: "create"
|
|
20775
20847
|
},
|
|
20848
|
+
"deviceManager.discoverProvider": {
|
|
20849
|
+
capName: "device-manager",
|
|
20850
|
+
capScope: "system",
|
|
20851
|
+
addonId: null,
|
|
20852
|
+
access: "create"
|
|
20853
|
+
},
|
|
20854
|
+
"deviceManager.discoveryProviders": {
|
|
20855
|
+
capName: "device-manager",
|
|
20856
|
+
capScope: "system",
|
|
20857
|
+
addonId: null,
|
|
20858
|
+
access: "view"
|
|
20859
|
+
},
|
|
20776
20860
|
"deviceManager.enable": {
|
|
20777
20861
|
capName: "device-manager",
|
|
20778
20862
|
capScope: "system",
|
|
@@ -20923,6 +21007,18 @@ Object.freeze({
|
|
|
20923
21007
|
addonId: null,
|
|
20924
21008
|
access: "create"
|
|
20925
21009
|
},
|
|
21010
|
+
"deviceManager.providerCreationType": {
|
|
21011
|
+
capName: "device-manager",
|
|
21012
|
+
capScope: "system",
|
|
21013
|
+
addonId: null,
|
|
21014
|
+
access: "view"
|
|
21015
|
+
},
|
|
21016
|
+
"deviceManager.providerDiscoveryParamsSchema": {
|
|
21017
|
+
capName: "device-manager",
|
|
21018
|
+
capScope: "system",
|
|
21019
|
+
addonId: null,
|
|
21020
|
+
access: "view"
|
|
21021
|
+
},
|
|
20926
21022
|
"deviceManager.registerDevice": {
|
|
20927
21023
|
capName: "device-manager",
|
|
20928
21024
|
capScope: "system",
|
|
@@ -21139,6 +21235,18 @@ Object.freeze({
|
|
|
21139
21235
|
addonId: null,
|
|
21140
21236
|
access: "view"
|
|
21141
21237
|
},
|
|
21238
|
+
"deviceProvider.getDiscoveryParamsSchema": {
|
|
21239
|
+
capName: "device-provider",
|
|
21240
|
+
capScope: "system",
|
|
21241
|
+
addonId: null,
|
|
21242
|
+
access: "view"
|
|
21243
|
+
},
|
|
21244
|
+
"deviceProvider.getManualCreationType": {
|
|
21245
|
+
capName: "device-provider",
|
|
21246
|
+
capScope: "system",
|
|
21247
|
+
addonId: null,
|
|
21248
|
+
access: "view"
|
|
21249
|
+
},
|
|
21142
21250
|
"deviceProvider.getStatus": {
|
|
21143
21251
|
capName: "device-provider",
|
|
21144
21252
|
capScope: "system",
|
|
@@ -24128,6 +24236,34 @@ function buildConnectionFormSchema() {
|
|
|
24128
24236
|
]
|
|
24129
24237
|
}] };
|
|
24130
24238
|
}
|
|
24239
|
+
/**
|
|
24240
|
+
* Extra params form for a PER-INTEGRATION network scan. Gree's UDP broadcast
|
|
24241
|
+
* only reaches the addon node's local subnet; to find ACs on a DIFFERENT subnet
|
|
24242
|
+
* (e.g. 192.168.20.x) the operator supplies that subnet's directed-broadcast
|
|
24243
|
+
* address (192.168.20.255). Empty = default local-subnet broadcast.
|
|
24244
|
+
*/
|
|
24245
|
+
function buildDiscoveryParamsFormSchema() {
|
|
24246
|
+
return { sections: [{
|
|
24247
|
+
id: "scan",
|
|
24248
|
+
title: "Scan options",
|
|
24249
|
+
description: "Leave empty to scan the local subnet. To find air conditioners on a different subnet, enter that subnet’s broadcast address (e.g. 192.168.20.255).",
|
|
24250
|
+
columns: 1,
|
|
24251
|
+
fields: [{
|
|
24252
|
+
type: "text",
|
|
24253
|
+
key: "broadcastAddress",
|
|
24254
|
+
label: "Broadcast address",
|
|
24255
|
+
required: false,
|
|
24256
|
+
placeholder: "192.168.20.255"
|
|
24257
|
+
}, {
|
|
24258
|
+
type: "number",
|
|
24259
|
+
key: "timeoutMs",
|
|
24260
|
+
label: "UDP timeout (ms)",
|
|
24261
|
+
min: 500,
|
|
24262
|
+
max: 3e4,
|
|
24263
|
+
default: 3e3
|
|
24264
|
+
}]
|
|
24265
|
+
}] };
|
|
24266
|
+
}
|
|
24131
24267
|
//#endregion
|
|
24132
24268
|
//#region ../../node_modules/@apocaliss92/nodegree/dist/index.js
|
|
24133
24269
|
var GreeError = class extends Error {
|
|
@@ -24829,6 +24965,16 @@ function macKey(mac) {
|
|
|
24829
24965
|
*/
|
|
24830
24966
|
var GreeConnectionResolver = class {
|
|
24831
24967
|
#surfaces = /* @__PURE__ */ new Map();
|
|
24968
|
+
/**
|
|
24969
|
+
* Per-connection "surface (re)published" subscribers. An AC accessory child
|
|
24970
|
+
* activates from `restoreDevices` BEFORE its parent's manager finishes the
|
|
24971
|
+
* async bind, so `getDevice` is null at `onActivate` time and its
|
|
24972
|
+
* `stateChanged` listener never attaches → the climate slice stays frozen at
|
|
24973
|
+
* cold-start (`lastFetchedAt: 0`, no temperature). Children subscribe here and
|
|
24974
|
+
* (re)attach + recompute once the handle is published. A plain `Set` has no
|
|
24975
|
+
* listener cap. Mirrors the Ecowitt facade-resolver fan-out.
|
|
24976
|
+
*/
|
|
24977
|
+
#subs = /* @__PURE__ */ new Map();
|
|
24832
24978
|
/** Publish or remove the connection surface for a connection key. `null` removes. */
|
|
24833
24979
|
set(connectionKey, surface) {
|
|
24834
24980
|
if (surface === null) {
|
|
@@ -24836,6 +24982,24 @@ var GreeConnectionResolver = class {
|
|
|
24836
24982
|
return;
|
|
24837
24983
|
}
|
|
24838
24984
|
this.#surfaces.set(connectionKey, surface);
|
|
24985
|
+
const subs = this.#subs.get(connectionKey);
|
|
24986
|
+
if (subs) for (const cb of subs) try {
|
|
24987
|
+
cb();
|
|
24988
|
+
} catch {}
|
|
24989
|
+
}
|
|
24990
|
+
/** Subscribe to surface (re)publish for a connection key. Returns unsubscribe. */
|
|
24991
|
+
onSurface(connectionKey, cb) {
|
|
24992
|
+
let set = this.#subs.get(connectionKey);
|
|
24993
|
+
if (!set) {
|
|
24994
|
+
set = /* @__PURE__ */ new Set();
|
|
24995
|
+
this.#subs.set(connectionKey, set);
|
|
24996
|
+
}
|
|
24997
|
+
set.add(cb);
|
|
24998
|
+
return () => {
|
|
24999
|
+
const current = this.#subs.get(connectionKey);
|
|
25000
|
+
current?.delete(cb);
|
|
25001
|
+
if (current && current.size === 0) this.#subs.delete(connectionKey);
|
|
25002
|
+
};
|
|
24839
25003
|
}
|
|
24840
25004
|
/** The bind-scan rows for a connection key, or empty when unknown. */
|
|
24841
25005
|
discovered(connectionKey) {
|
|
@@ -24859,6 +25023,7 @@ var GreeConnectionResolver = class {
|
|
|
24859
25023
|
/** Remove all registered surfaces (called on full shutdown). */
|
|
24860
25024
|
clear() {
|
|
24861
25025
|
this.#surfaces.clear();
|
|
25026
|
+
this.#subs.clear();
|
|
24862
25027
|
}
|
|
24863
25028
|
};
|
|
24864
25029
|
/** The single in-process per-connection resolver shared between the AC device's
|
|
@@ -24869,7 +25034,7 @@ var greeConnections = new GreeConnectionResolver();
|
|
|
24869
25034
|
function defaultFacade(config) {
|
|
24870
25035
|
return new Nodegree(toNodegreeOptions(config));
|
|
24871
25036
|
}
|
|
24872
|
-
var DEFAULT_POLL_INTERVAL_MS =
|
|
25037
|
+
var DEFAULT_POLL_INTERVAL_MS = 1e4;
|
|
24873
25038
|
/**
|
|
24874
25039
|
* Wraps exactly one `@apocaliss92/nodegree` facade bound to a SINGLE Gree AC
|
|
24875
25040
|
* (standalone mode — the connection lives on the AC device). {@link start} runs a
|
|
@@ -25068,6 +25233,27 @@ function resolveBroadcastTarget(connection) {
|
|
|
25068
25233
|
* The returned identity is what {@link import('./config.js').greeAcDeviceSchema}
|
|
25069
25234
|
* persists on the new device; the live per-device manager re-binds on activate.
|
|
25070
25235
|
*/
|
|
25236
|
+
/**
|
|
25237
|
+
* One-shot LAN broadcast scan for Gree ACs — the network-discovery pass. Builds a
|
|
25238
|
+
* throwaway facade from default (or provided) UDP settings, runs the broadcast,
|
|
25239
|
+
* and returns every responder. No bind, no persistence. The provider's
|
|
25240
|
+
* `discoverDevices()` maps the responders to adoption candidates.
|
|
25241
|
+
*/
|
|
25242
|
+
async function discoverGreeDevices(input) {
|
|
25243
|
+
const connection = settingsToGreeConfig({
|
|
25244
|
+
...input.broadcastAddress ? { broadcastAddr: input.broadcastAddress } : {},
|
|
25245
|
+
...input.timeoutMs ? { timeoutMs: input.timeoutMs } : {}
|
|
25246
|
+
});
|
|
25247
|
+
const facade = (input.makeFacade ?? defaultFacade)(connection);
|
|
25248
|
+
try {
|
|
25249
|
+
const broadcast = resolveBroadcastTarget(connection);
|
|
25250
|
+
const opts = { timeoutMs: connection.timeoutMs };
|
|
25251
|
+
if (broadcast.length > 0) opts.broadcastAddr = broadcast;
|
|
25252
|
+
return await facade.discover(opts);
|
|
25253
|
+
} finally {
|
|
25254
|
+
await facade.close().catch(() => void 0);
|
|
25255
|
+
}
|
|
25256
|
+
}
|
|
25071
25257
|
async function bindOnce(input) {
|
|
25072
25258
|
const { connection, logger } = input;
|
|
25073
25259
|
const facade = (input.makeFacade ?? defaultFacade)(connection);
|
|
@@ -25366,7 +25552,19 @@ var greeAcSchema = object({
|
|
|
25366
25552
|
greeMac: string().min(1).describe("Gree AC MAC address"),
|
|
25367
25553
|
connectionKey: string().min(1).describe("Per-device connection resolver key"),
|
|
25368
25554
|
system: literal("gree").optional(),
|
|
25369
|
-
integrationId: string().optional()
|
|
25555
|
+
integrationId: string().optional(),
|
|
25556
|
+
/** Operator filter — HVAC modes exposed in the control UI (subset of the
|
|
25557
|
+
* model's supported modes). Empty / omitted ⇒ expose all. `off` is always
|
|
25558
|
+
* exposed (power). */
|
|
25559
|
+
enabledModes: array(string()).optional(),
|
|
25560
|
+
/** Operator filter — fan speeds exposed in the control UI (subset of
|
|
25561
|
+
* `GREE_FAN_MODES`). Empty / omitted ⇒ expose all. */
|
|
25562
|
+
enabledFanModes: array(string()).optional(),
|
|
25563
|
+
/** Operator filter — expose the vertical-swing toggle. Default true. */
|
|
25564
|
+
exposeVerticalSwing: boolean().optional(),
|
|
25565
|
+
/** Operator filter — expose the horizontal-swing toggle (model-permitting).
|
|
25566
|
+
* Default true. */
|
|
25567
|
+
exposeHorizontalSwing: boolean().optional()
|
|
25370
25568
|
});
|
|
25371
25569
|
/**
|
|
25372
25570
|
* One Gree air conditioner as a CamStack `Thermostat`-type accessory child.
|
|
@@ -25406,17 +25604,16 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
25406
25604
|
*/
|
|
25407
25605
|
get features() {
|
|
25408
25606
|
const caps = this.resolveAc()?.capabilities ?? getAcCapabilities();
|
|
25409
|
-
const
|
|
25410
|
-
|
|
25411
|
-
|
|
25412
|
-
|
|
25413
|
-
];
|
|
25414
|
-
if (caps.horizontalSwing) flags.push(DeviceFeature.ClimateSwingHorizontal);
|
|
25607
|
+
const cfg = this.config.values;
|
|
25608
|
+
const flags = [DeviceFeature.ClimateFanMode, DeviceFeature.ClimatePreset];
|
|
25609
|
+
if (cfg.exposeVerticalSwing ?? true) flags.push(DeviceFeature.ClimateSwingVertical);
|
|
25610
|
+
if (caps.horizontalSwing && (cfg.exposeHorizontalSwing ?? true)) flags.push(DeviceFeature.ClimateSwingHorizontal);
|
|
25415
25611
|
return flags;
|
|
25416
25612
|
}
|
|
25417
25613
|
greeMac;
|
|
25418
25614
|
connectionKey;
|
|
25419
25615
|
stateChangedUnsub = null;
|
|
25616
|
+
surfaceUnsub = null;
|
|
25420
25617
|
constructor(ctx) {
|
|
25421
25618
|
const persisted = greeAcSchema.parse(ctx.persistedConfig ?? {});
|
|
25422
25619
|
super(ctx, greeAcSchema, { type: ctx.deviceMeta.type });
|
|
@@ -25441,8 +25638,21 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
25441
25638
|
this.registerCaps();
|
|
25442
25639
|
this.attachStateListener();
|
|
25443
25640
|
this.recomputeSlices();
|
|
25641
|
+
this.surfaceUnsub = greeConnections.onSurface(this.connectionKey, () => {
|
|
25642
|
+
this.attachStateListener();
|
|
25643
|
+
this.recomputeSlices();
|
|
25644
|
+
});
|
|
25444
25645
|
}
|
|
25445
25646
|
async removeDevice() {
|
|
25647
|
+
this.detachStateListener();
|
|
25648
|
+
if (this.surfaceUnsub) {
|
|
25649
|
+
try {
|
|
25650
|
+
this.surfaceUnsub();
|
|
25651
|
+
} catch {}
|
|
25652
|
+
this.surfaceUnsub = null;
|
|
25653
|
+
}
|
|
25654
|
+
}
|
|
25655
|
+
detachStateListener() {
|
|
25446
25656
|
if (this.stateChangedUnsub) {
|
|
25447
25657
|
try {
|
|
25448
25658
|
this.stateChangedUnsub();
|
|
@@ -25450,12 +25660,16 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
25450
25660
|
this.stateChangedUnsub = null;
|
|
25451
25661
|
}
|
|
25452
25662
|
}
|
|
25663
|
+
/** Attach the live `stateChanged` listener to the bound AC handle. Idempotent:
|
|
25664
|
+
* drops any prior listener first, so it is safe to call on every surface
|
|
25665
|
+
* (re)publish. No-op until the handle is actually bound. */
|
|
25453
25666
|
attachStateListener() {
|
|
25454
25667
|
const ac = this.resolveAc();
|
|
25455
25668
|
if (!ac) {
|
|
25456
25669
|
this.ctx.logger.debug("GreeAcDevice: handle not present; no live listener yet", { meta: { greeMac: this.greeMac } });
|
|
25457
25670
|
return;
|
|
25458
25671
|
}
|
|
25672
|
+
this.detachStateListener();
|
|
25459
25673
|
const onState = () => {
|
|
25460
25674
|
try {
|
|
25461
25675
|
this.recomputeSlices();
|
|
@@ -25473,7 +25687,7 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
25473
25687
|
}
|
|
25474
25688
|
registerCaps() {
|
|
25475
25689
|
this.ctx.registerNativeCap(climateControlCapability, {
|
|
25476
|
-
getStatus: async () => this.runtimeState.getCapState(CLIMATE_CAP) ?? CLIMATE_COLD_START,
|
|
25690
|
+
getStatus: async () => this.runtimeState.getCapState(CLIMATE_CAP) ?? this.applyClimateFilters(CLIMATE_COLD_START),
|
|
25477
25691
|
setMode: async ({ mode }) => {
|
|
25478
25692
|
const ac = this.requireAc();
|
|
25479
25693
|
const libMode = capModeToLibMode(mode);
|
|
@@ -25520,7 +25734,7 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
25520
25734
|
await ac.setSwingHorizontal(boolToHorizontalSwing(on));
|
|
25521
25735
|
}
|
|
25522
25736
|
});
|
|
25523
|
-
this.runtimeState.setCapState(CLIMATE_CAP, CLIMATE_COLD_START);
|
|
25737
|
+
this.runtimeState.setCapState(CLIMATE_CAP, this.applyClimateFilters(CLIMATE_COLD_START));
|
|
25524
25738
|
this.ctx.registerNativeCap(fanControlCapability, {
|
|
25525
25739
|
getStatus: async () => this.runtimeState.getCapState(FAN_CAP) ?? FAN_COLD_START,
|
|
25526
25740
|
setPercentage: async ({ percentage }) => {
|
|
@@ -25567,7 +25781,7 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
25567
25781
|
swingHorizontal: ac.capabilities.horizontalSwing ? horizontalSwingToBool(ac.swingHorizontal) : null,
|
|
25568
25782
|
lastFetchedAt: now
|
|
25569
25783
|
};
|
|
25570
|
-
this.runtimeState.setCapState(CLIMATE_CAP, climateSlice);
|
|
25784
|
+
this.runtimeState.setCapState(CLIMATE_CAP, this.applyClimateFilters(climateSlice));
|
|
25571
25785
|
const fanSlice = {
|
|
25572
25786
|
percentage: fanSpeedToPercentage(ac.fanSpeed),
|
|
25573
25787
|
percentageStep: GREE_FAN_PERCENTAGE_STEP,
|
|
@@ -25579,7 +25793,97 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
25579
25793
|
};
|
|
25580
25794
|
this.runtimeState.setCapState(FAN_CAP, fanSlice);
|
|
25581
25795
|
}
|
|
25796
|
+
/**
|
|
25797
|
+
* Apply the operator's per-AC exposure filters to a climate slice: narrow
|
|
25798
|
+
* `availableModes` / `availableFanModes` to the enabled subsets (empty ⇒ all;
|
|
25799
|
+
* `off` is always kept) and null out a swing axis the operator has hidden.
|
|
25800
|
+
* Pure — returns a new slice, never mutates the input.
|
|
25801
|
+
*/
|
|
25802
|
+
applyClimateFilters(slice) {
|
|
25803
|
+
const cfg = this.config.values;
|
|
25804
|
+
const enabledModes = cfg.enabledModes;
|
|
25805
|
+
const enabledFanModes = cfg.enabledFanModes;
|
|
25806
|
+
const exposeV = cfg.exposeVerticalSwing ?? true;
|
|
25807
|
+
const exposeH = cfg.exposeHorizontalSwing ?? true;
|
|
25808
|
+
const availableModes = enabledModes && enabledModes.length > 0 ? slice.availableModes.filter((m) => m === "off" || enabledModes.includes(m)) : slice.availableModes;
|
|
25809
|
+
const availableFanModes = enabledFanModes && enabledFanModes.length > 0 ? slice.availableFanModes.filter((f) => enabledFanModes.includes(f)) : slice.availableFanModes;
|
|
25810
|
+
return {
|
|
25811
|
+
...slice,
|
|
25812
|
+
availableModes,
|
|
25813
|
+
availableFanModes,
|
|
25814
|
+
swingVertical: exposeV ? slice.swingVertical : null,
|
|
25815
|
+
swingHorizontal: exposeH ? slice.swingHorizontal : null
|
|
25816
|
+
};
|
|
25817
|
+
}
|
|
25818
|
+
/**
|
|
25819
|
+
* Per-AC exposure form (device-details settings). Lets the operator pick
|
|
25820
|
+
* which modes / fan speeds / swing axes appear in the AC control UI. An empty
|
|
25821
|
+
* multiselect means "expose all". The horizontal-swing toggle is only offered
|
|
25822
|
+
* when the model advertises horizontal swing.
|
|
25823
|
+
*/
|
|
25824
|
+
getSettingsUISchema() {
|
|
25825
|
+
const supportsHSwing = (this.resolveAc()?.capabilities ?? getAcCapabilities()).horizontalSwing;
|
|
25826
|
+
const cfg = this.config.values;
|
|
25827
|
+
const fields = [
|
|
25828
|
+
{
|
|
25829
|
+
type: "multiselect",
|
|
25830
|
+
key: "enabledModes",
|
|
25831
|
+
label: "Modes",
|
|
25832
|
+
description: "Modes shown in the control UI. Empty = all. “Off” is always available.",
|
|
25833
|
+
options: SUPPORTED_CAP_MODES.map((m) => ({
|
|
25834
|
+
value: m,
|
|
25835
|
+
label: titleCase(m)
|
|
25836
|
+
}))
|
|
25837
|
+
},
|
|
25838
|
+
{
|
|
25839
|
+
type: "multiselect",
|
|
25840
|
+
key: "enabledFanModes",
|
|
25841
|
+
label: "Fan speeds",
|
|
25842
|
+
description: "Fan speeds shown in the control UI. Empty = all.",
|
|
25843
|
+
options: GREE_FAN_MODES.map((f) => ({
|
|
25844
|
+
value: f,
|
|
25845
|
+
label: titleCase(f)
|
|
25846
|
+
}))
|
|
25847
|
+
},
|
|
25848
|
+
{
|
|
25849
|
+
type: "boolean",
|
|
25850
|
+
style: "switch",
|
|
25851
|
+
key: "exposeVerticalSwing",
|
|
25852
|
+
label: "Vertical swing",
|
|
25853
|
+
default: true
|
|
25854
|
+
}
|
|
25855
|
+
];
|
|
25856
|
+
if (supportsHSwing) fields.push({
|
|
25857
|
+
type: "boolean",
|
|
25858
|
+
style: "switch",
|
|
25859
|
+
key: "exposeHorizontalSwing",
|
|
25860
|
+
label: "Horizontal swing",
|
|
25861
|
+
default: true
|
|
25862
|
+
});
|
|
25863
|
+
return hydrateSchema({ sections: [{
|
|
25864
|
+
id: "exposure",
|
|
25865
|
+
title: "Exposed controls",
|
|
25866
|
+
description: "Choose which modes, fan speeds and swing axes appear in the air-conditioner control UI.",
|
|
25867
|
+
fields
|
|
25868
|
+
}] }, {
|
|
25869
|
+
enabledModes: cfg.enabledModes ?? [],
|
|
25870
|
+
enabledFanModes: cfg.enabledFanModes ?? [],
|
|
25871
|
+
exposeVerticalSwing: cfg.exposeVerticalSwing ?? true,
|
|
25872
|
+
exposeHorizontalSwing: cfg.exposeHorizontalSwing ?? true
|
|
25873
|
+
});
|
|
25874
|
+
}
|
|
25875
|
+
async applySettingsPatch(patch) {
|
|
25876
|
+
const typed = greeAcSchema.partial().parse(patch);
|
|
25877
|
+
await this.config.setAll(typed);
|
|
25878
|
+
this.recomputeSlices();
|
|
25879
|
+
await this.refreshFeatures();
|
|
25880
|
+
}
|
|
25582
25881
|
};
|
|
25882
|
+
/** 'fan_only' → 'Fan only', 'medium_low' → 'Medium low'. Pure. */
|
|
25883
|
+
function titleCase(value) {
|
|
25884
|
+
const spaced = value.replace(/_/g, " ");
|
|
25885
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
25886
|
+
}
|
|
25583
25887
|
//#endregion
|
|
25584
25888
|
//#region src/devices/gree-toggle-device.ts
|
|
25585
25889
|
var SWITCH_CAP = "switch";
|
|
@@ -25622,6 +25926,7 @@ var GreeToggleDevice = class extends BaseDevice$1 {
|
|
|
25622
25926
|
greeMac;
|
|
25623
25927
|
connectionKey;
|
|
25624
25928
|
stateChangedUnsub = null;
|
|
25929
|
+
surfaceUnsub = null;
|
|
25625
25930
|
constructor(ctx) {
|
|
25626
25931
|
const persisted = greeToggleSchema.parse(ctx.persistedConfig ?? {});
|
|
25627
25932
|
super(ctx, greeToggleSchema, { type: ctx.deviceMeta.type });
|
|
@@ -25673,8 +25978,21 @@ var GreeToggleDevice = class extends BaseDevice$1 {
|
|
|
25673
25978
|
this.registerCap();
|
|
25674
25979
|
this.attachStateListener();
|
|
25675
25980
|
this.recomputeSlice();
|
|
25981
|
+
this.surfaceUnsub = greeConnections.onSurface(this.connectionKey, () => {
|
|
25982
|
+
this.attachStateListener();
|
|
25983
|
+
this.recomputeSlice();
|
|
25984
|
+
});
|
|
25676
25985
|
}
|
|
25677
25986
|
async removeDevice() {
|
|
25987
|
+
this.detachStateListener();
|
|
25988
|
+
if (this.surfaceUnsub) {
|
|
25989
|
+
try {
|
|
25990
|
+
this.surfaceUnsub();
|
|
25991
|
+
} catch {}
|
|
25992
|
+
this.surfaceUnsub = null;
|
|
25993
|
+
}
|
|
25994
|
+
}
|
|
25995
|
+
detachStateListener() {
|
|
25678
25996
|
if (this.stateChangedUnsub) {
|
|
25679
25997
|
try {
|
|
25680
25998
|
this.stateChangedUnsub();
|
|
@@ -25691,6 +26009,7 @@ var GreeToggleDevice = class extends BaseDevice$1 {
|
|
|
25691
26009
|
} });
|
|
25692
26010
|
return;
|
|
25693
26011
|
}
|
|
26012
|
+
this.detachStateListener();
|
|
25694
26013
|
const onState = () => {
|
|
25695
26014
|
try {
|
|
25696
26015
|
this.recomputeSlice();
|
|
@@ -25850,7 +26169,7 @@ var GreeContainerDevice = class extends BaseDevice$1 {
|
|
|
25850
26169
|
return [{
|
|
25851
26170
|
stableIdSuffix: "ac",
|
|
25852
26171
|
meta: {
|
|
25853
|
-
type: DeviceType.
|
|
26172
|
+
type: DeviceType.Climate,
|
|
25854
26173
|
name: this.name,
|
|
25855
26174
|
linkDeviceId: this.id,
|
|
25856
26175
|
...this.integrationId !== void 0 ? { integrationId: this.integrationId } : {}
|
|
@@ -25936,9 +26255,10 @@ var GreeContainerDevice = class extends BaseDevice$1 {
|
|
|
25936
26255
|
* `instanceMode: multiple` — many ACs may be added. Placement: any-node — Gree is
|
|
25937
26256
|
* LOCAL UDP; the addon runs on whichever node shares the AC's subnet.
|
|
25938
26257
|
*
|
|
25939
|
-
* LAN UDP-broadcast auto-discovery
|
|
25940
|
-
*
|
|
25941
|
-
*
|
|
26258
|
+
* LAN UDP-broadcast auto-discovery is implemented via the `device-provider`
|
|
26259
|
+
* discovery surface (`supportsDiscovery`/`discoverDevices`/`adoptDiscoveredDevice`)
|
|
26260
|
+
* — the same one ONVIF uses and the aggregated "Scan network" modal fans out to.
|
|
26261
|
+
* Manual add-one-AC-by-IP (directed bind) remains available in parallel.
|
|
25942
26262
|
*/
|
|
25943
26263
|
var GreeProviderAddon = class extends BaseDeviceProvider {
|
|
25944
26264
|
addonId = "provider-gree";
|
|
@@ -25948,7 +26268,41 @@ var GreeProviderAddon = class extends BaseDeviceProvider {
|
|
|
25948
26268
|
super({});
|
|
25949
26269
|
}
|
|
25950
26270
|
async supportsDiscovery() {
|
|
25951
|
-
return
|
|
26271
|
+
return true;
|
|
26272
|
+
}
|
|
26273
|
+
async getDiscoveryParamsSchema() {
|
|
26274
|
+
return buildDiscoveryParamsFormSchema();
|
|
26275
|
+
}
|
|
26276
|
+
async discoverDevices(input) {
|
|
26277
|
+
const broadcastAddress = typeof input?.params?.["broadcastAddress"] === "string" ? input.params["broadcastAddress"].trim() : void 0;
|
|
26278
|
+
const timeoutMs = typeof input?.params?.["timeoutMs"] === "number" ? input.params["timeoutMs"] : void 0;
|
|
26279
|
+
const responders = await discoverGreeDevices({
|
|
26280
|
+
...broadcastAddress ? { broadcastAddress } : {},
|
|
26281
|
+
...timeoutMs ? { timeoutMs } : {}
|
|
26282
|
+
});
|
|
26283
|
+
this.ctx.logger.info("Gree discovery complete", { meta: {
|
|
26284
|
+
count: responders.length,
|
|
26285
|
+
broadcastAddress: broadcastAddress ?? "local"
|
|
26286
|
+
} });
|
|
26287
|
+
return responders.map((d) => {
|
|
26288
|
+
const displayName = d.name.length > 0 ? d.name : d.mac;
|
|
26289
|
+
return {
|
|
26290
|
+
stableId: `gree:${macKey(d.mac)}`,
|
|
26291
|
+
type: DeviceType.Container,
|
|
26292
|
+
suggestedName: displayName,
|
|
26293
|
+
prefilledConfig: {
|
|
26294
|
+
name: displayName,
|
|
26295
|
+
host: d.ip,
|
|
26296
|
+
...d.model !== void 0 ? { model: d.model } : {}
|
|
26297
|
+
}
|
|
26298
|
+
};
|
|
26299
|
+
});
|
|
26300
|
+
}
|
|
26301
|
+
async adoptDiscoveredDevice(input) {
|
|
26302
|
+
return this.createDevice({
|
|
26303
|
+
type: DeviceType.Container,
|
|
26304
|
+
config: input.candidate.prefilledConfig
|
|
26305
|
+
});
|
|
25952
26306
|
}
|
|
25953
26307
|
async supportsManualCreation() {
|
|
25954
26308
|
return true;
|
package/dist/addon.mjs
CHANGED
|
@@ -4644,7 +4644,7 @@ function preprocess(fn, schema) {
|
|
|
4644
4644
|
});
|
|
4645
4645
|
}
|
|
4646
4646
|
//#endregion
|
|
4647
|
-
//#region ../types/dist/sleep-
|
|
4647
|
+
//#region ../types/dist/sleep-C2M2zF7x.mjs
|
|
4648
4648
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4649
4649
|
EventCategory["SystemBoot"] = "system.boot";
|
|
4650
4650
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -6212,6 +6212,12 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
|
6212
6212
|
DeviceType["Switch"] = "switch";
|
|
6213
6213
|
DeviceType["Sensor"] = "sensor";
|
|
6214
6214
|
DeviceType["Thermostat"] = "thermostat";
|
|
6215
|
+
/** Air-conditioner / heat-pump climate device (HVAC) — shares the
|
|
6216
|
+
* `climate-control` cap surface with `Thermostat` but renders a
|
|
6217
|
+
* dedicated AC-appropriate control UI (mode chips, fan speed,
|
|
6218
|
+
* independent vertical/horizontal swing). Sources: native Gree, and
|
|
6219
|
+
* reusable by other AC integrations. */
|
|
6220
|
+
DeviceType["Climate"] = "climate";
|
|
6215
6221
|
DeviceType["Button"] = "button";
|
|
6216
6222
|
/** Generic stateless event emitter — carries a device's EXACT declared
|
|
6217
6223
|
* event vocabulary verbatim (no normalization). Installed with the
|
|
@@ -9007,7 +9013,7 @@ var climateControlCapability = {
|
|
|
9007
9013
|
scope: "device",
|
|
9008
9014
|
deviceNative: true,
|
|
9009
9015
|
mode: "singleton",
|
|
9010
|
-
deviceTypes: [DeviceType.Thermostat],
|
|
9016
|
+
deviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
|
|
9011
9017
|
methods: {
|
|
9012
9018
|
setMode: method(object({
|
|
9013
9019
|
deviceId: number().int().nonnegative(),
|
|
@@ -13606,10 +13612,30 @@ var deviceProviderCapability = {
|
|
|
13606
13612
|
type: string()
|
|
13607
13613
|
}))),
|
|
13608
13614
|
supportsDiscovery: method(object({}), boolean()),
|
|
13609
|
-
|
|
13615
|
+
/**
|
|
13616
|
+
* Run a network scan. `params` carries optional provider-specific scan
|
|
13617
|
+
* inputs (e.g. a broadcast address / subnet for cross-subnet discovery),
|
|
13618
|
+
* shaped by `getDiscoveryParamsSchema`. Omitted for the generic scan
|
|
13619
|
+
* (provider uses its local-network default).
|
|
13620
|
+
*/
|
|
13621
|
+
discoverDevices: method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
|
|
13610
13622
|
kind: "mutation",
|
|
13611
13623
|
auth: "admin"
|
|
13612
13624
|
}),
|
|
13625
|
+
/**
|
|
13626
|
+
* Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
|
|
13627
|
+
* provider accepts (e.g. Gree's broadcast address for a different subnet).
|
|
13628
|
+
* `null` when the provider takes no extra scan params — the generic
|
|
13629
|
+
* aggregated scan never renders this; the per-integration scan does.
|
|
13630
|
+
*/
|
|
13631
|
+
getDiscoveryParamsSchema: method(object({}), CreationSchemaOutputSchema),
|
|
13632
|
+
/**
|
|
13633
|
+
* The DeviceType this provider creates via manual add (Camera for
|
|
13634
|
+
* Reolink/ONVIF, Container for Gree, Hub for Ecowitt). `null` when the
|
|
13635
|
+
* provider does not support manual creation. Lets the Add-Device dialog
|
|
13636
|
+
* pick the right type instead of assuming Camera.
|
|
13637
|
+
*/
|
|
13638
|
+
getManualCreationType: method(object({}), object({ deviceType: _enum(DeviceType).nullable() })),
|
|
13613
13639
|
adoptDiscoveredDevice: method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
|
|
13614
13640
|
kind: "mutation",
|
|
13615
13641
|
auth: "admin"
|
|
@@ -13733,9 +13759,23 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
13733
13759
|
async supportsDiscovery() {
|
|
13734
13760
|
return false;
|
|
13735
13761
|
}
|
|
13736
|
-
async discoverDevices() {
|
|
13762
|
+
async discoverDevices(_input) {
|
|
13737
13763
|
return [];
|
|
13738
13764
|
}
|
|
13765
|
+
/** Extra per-scan input form (e.g. a broadcast address for another subnet).
|
|
13766
|
+
* Null = no extra params. Override in providers that support scoped scans. */
|
|
13767
|
+
async getDiscoveryParamsSchema() {
|
|
13768
|
+
return null;
|
|
13769
|
+
}
|
|
13770
|
+
/**
|
|
13771
|
+
* The DeviceType this provider creates via manual add — derived from the
|
|
13772
|
+
* `deviceClasses` map (first registered type). `null` when manual creation is
|
|
13773
|
+
* unsupported. Lets the Add-Device dialog pick the right type per provider.
|
|
13774
|
+
*/
|
|
13775
|
+
async getManualCreationType() {
|
|
13776
|
+
if (!await this.supportsManualCreation()) return { deviceType: null };
|
|
13777
|
+
return { deviceType: Object.values(DeviceType).find((t) => this.deviceClasses[t] !== void 0) ?? null };
|
|
13778
|
+
}
|
|
13739
13779
|
async adoptDiscoveredDevice(_input) {
|
|
13740
13780
|
throw new Error(`${this.providerName} provider does not support discovery-based adoption`);
|
|
13741
13781
|
}
|
|
@@ -15580,7 +15620,10 @@ method(object({
|
|
|
15580
15620
|
}), FieldProbeResultSchema, {
|
|
15581
15621
|
kind: "mutation",
|
|
15582
15622
|
auth: "admin"
|
|
15583
|
-
}), method(
|
|
15623
|
+
}), method(object({
|
|
15624
|
+
addonId: string(),
|
|
15625
|
+
integrationId: string()
|
|
15626
|
+
}), object({ filters: array(AdoptionFilterSchema) }), { auth: "admin" }), method(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
|
|
15584
15627
|
addonId: string(),
|
|
15585
15628
|
integrationId: string()
|
|
15586
15629
|
}), AdoptionStatusSchema, {
|
|
@@ -15595,7 +15638,24 @@ method(object({
|
|
|
15595
15638
|
}), method(ResyncInputSchema, ResyncResultSchema, {
|
|
15596
15639
|
kind: "mutation",
|
|
15597
15640
|
auth: "admin"
|
|
15641
|
+
}), method(object({}), object({ providers: array(object({
|
|
15642
|
+
addonId: string(),
|
|
15643
|
+
label: string()
|
|
15644
|
+
})).readonly() }), { auth: "admin" }), method(object({}), object({ groups: array(object({
|
|
15645
|
+
addonId: string(),
|
|
15646
|
+
label: string(),
|
|
15647
|
+
candidates: array(DiscoveryCandidateSchema).readonly(),
|
|
15648
|
+
error: string().nullable()
|
|
15649
|
+
})).readonly() }), {
|
|
15650
|
+
kind: "mutation",
|
|
15651
|
+
auth: "admin"
|
|
15598
15652
|
}), method(object({
|
|
15653
|
+
addonId: string(),
|
|
15654
|
+
params: record(string(), unknown()).optional()
|
|
15655
|
+
}), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
|
|
15656
|
+
kind: "mutation",
|
|
15657
|
+
auth: "admin"
|
|
15658
|
+
}), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
|
|
15599
15659
|
deviceId: number(),
|
|
15600
15660
|
key: string(),
|
|
15601
15661
|
value: unknown()
|
|
@@ -20718,6 +20778,12 @@ Object.freeze({
|
|
|
20718
20778
|
addonId: null,
|
|
20719
20779
|
access: "create"
|
|
20720
20780
|
},
|
|
20781
|
+
"deviceManager.adoptionListCandidateFilters": {
|
|
20782
|
+
capName: "device-manager",
|
|
20783
|
+
capScope: "system",
|
|
20784
|
+
addonId: null,
|
|
20785
|
+
access: "view"
|
|
20786
|
+
},
|
|
20721
20787
|
"deviceManager.adoptionListCandidates": {
|
|
20722
20788
|
capName: "device-manager",
|
|
20723
20789
|
capScope: "system",
|
|
@@ -20766,12 +20832,30 @@ Object.freeze({
|
|
|
20766
20832
|
addonId: null,
|
|
20767
20833
|
access: "create"
|
|
20768
20834
|
},
|
|
20835
|
+
"deviceManager.discoverAllProviders": {
|
|
20836
|
+
capName: "device-manager",
|
|
20837
|
+
capScope: "system",
|
|
20838
|
+
addonId: null,
|
|
20839
|
+
access: "create"
|
|
20840
|
+
},
|
|
20769
20841
|
"deviceManager.discoverDevices": {
|
|
20770
20842
|
capName: "device-manager",
|
|
20771
20843
|
capScope: "system",
|
|
20772
20844
|
addonId: null,
|
|
20773
20845
|
access: "create"
|
|
20774
20846
|
},
|
|
20847
|
+
"deviceManager.discoverProvider": {
|
|
20848
|
+
capName: "device-manager",
|
|
20849
|
+
capScope: "system",
|
|
20850
|
+
addonId: null,
|
|
20851
|
+
access: "create"
|
|
20852
|
+
},
|
|
20853
|
+
"deviceManager.discoveryProviders": {
|
|
20854
|
+
capName: "device-manager",
|
|
20855
|
+
capScope: "system",
|
|
20856
|
+
addonId: null,
|
|
20857
|
+
access: "view"
|
|
20858
|
+
},
|
|
20775
20859
|
"deviceManager.enable": {
|
|
20776
20860
|
capName: "device-manager",
|
|
20777
20861
|
capScope: "system",
|
|
@@ -20922,6 +21006,18 @@ Object.freeze({
|
|
|
20922
21006
|
addonId: null,
|
|
20923
21007
|
access: "create"
|
|
20924
21008
|
},
|
|
21009
|
+
"deviceManager.providerCreationType": {
|
|
21010
|
+
capName: "device-manager",
|
|
21011
|
+
capScope: "system",
|
|
21012
|
+
addonId: null,
|
|
21013
|
+
access: "view"
|
|
21014
|
+
},
|
|
21015
|
+
"deviceManager.providerDiscoveryParamsSchema": {
|
|
21016
|
+
capName: "device-manager",
|
|
21017
|
+
capScope: "system",
|
|
21018
|
+
addonId: null,
|
|
21019
|
+
access: "view"
|
|
21020
|
+
},
|
|
20925
21021
|
"deviceManager.registerDevice": {
|
|
20926
21022
|
capName: "device-manager",
|
|
20927
21023
|
capScope: "system",
|
|
@@ -21138,6 +21234,18 @@ Object.freeze({
|
|
|
21138
21234
|
addonId: null,
|
|
21139
21235
|
access: "view"
|
|
21140
21236
|
},
|
|
21237
|
+
"deviceProvider.getDiscoveryParamsSchema": {
|
|
21238
|
+
capName: "device-provider",
|
|
21239
|
+
capScope: "system",
|
|
21240
|
+
addonId: null,
|
|
21241
|
+
access: "view"
|
|
21242
|
+
},
|
|
21243
|
+
"deviceProvider.getManualCreationType": {
|
|
21244
|
+
capName: "device-provider",
|
|
21245
|
+
capScope: "system",
|
|
21246
|
+
addonId: null,
|
|
21247
|
+
access: "view"
|
|
21248
|
+
},
|
|
21141
21249
|
"deviceProvider.getStatus": {
|
|
21142
21250
|
capName: "device-provider",
|
|
21143
21251
|
capScope: "system",
|
|
@@ -24127,6 +24235,34 @@ function buildConnectionFormSchema() {
|
|
|
24127
24235
|
]
|
|
24128
24236
|
}] };
|
|
24129
24237
|
}
|
|
24238
|
+
/**
|
|
24239
|
+
* Extra params form for a PER-INTEGRATION network scan. Gree's UDP broadcast
|
|
24240
|
+
* only reaches the addon node's local subnet; to find ACs on a DIFFERENT subnet
|
|
24241
|
+
* (e.g. 192.168.20.x) the operator supplies that subnet's directed-broadcast
|
|
24242
|
+
* address (192.168.20.255). Empty = default local-subnet broadcast.
|
|
24243
|
+
*/
|
|
24244
|
+
function buildDiscoveryParamsFormSchema() {
|
|
24245
|
+
return { sections: [{
|
|
24246
|
+
id: "scan",
|
|
24247
|
+
title: "Scan options",
|
|
24248
|
+
description: "Leave empty to scan the local subnet. To find air conditioners on a different subnet, enter that subnet’s broadcast address (e.g. 192.168.20.255).",
|
|
24249
|
+
columns: 1,
|
|
24250
|
+
fields: [{
|
|
24251
|
+
type: "text",
|
|
24252
|
+
key: "broadcastAddress",
|
|
24253
|
+
label: "Broadcast address",
|
|
24254
|
+
required: false,
|
|
24255
|
+
placeholder: "192.168.20.255"
|
|
24256
|
+
}, {
|
|
24257
|
+
type: "number",
|
|
24258
|
+
key: "timeoutMs",
|
|
24259
|
+
label: "UDP timeout (ms)",
|
|
24260
|
+
min: 500,
|
|
24261
|
+
max: 3e4,
|
|
24262
|
+
default: 3e3
|
|
24263
|
+
}]
|
|
24264
|
+
}] };
|
|
24265
|
+
}
|
|
24130
24266
|
//#endregion
|
|
24131
24267
|
//#region ../../node_modules/@apocaliss92/nodegree/dist/index.js
|
|
24132
24268
|
var GreeError = class extends Error {
|
|
@@ -24828,6 +24964,16 @@ function macKey(mac) {
|
|
|
24828
24964
|
*/
|
|
24829
24965
|
var GreeConnectionResolver = class {
|
|
24830
24966
|
#surfaces = /* @__PURE__ */ new Map();
|
|
24967
|
+
/**
|
|
24968
|
+
* Per-connection "surface (re)published" subscribers. An AC accessory child
|
|
24969
|
+
* activates from `restoreDevices` BEFORE its parent's manager finishes the
|
|
24970
|
+
* async bind, so `getDevice` is null at `onActivate` time and its
|
|
24971
|
+
* `stateChanged` listener never attaches → the climate slice stays frozen at
|
|
24972
|
+
* cold-start (`lastFetchedAt: 0`, no temperature). Children subscribe here and
|
|
24973
|
+
* (re)attach + recompute once the handle is published. A plain `Set` has no
|
|
24974
|
+
* listener cap. Mirrors the Ecowitt facade-resolver fan-out.
|
|
24975
|
+
*/
|
|
24976
|
+
#subs = /* @__PURE__ */ new Map();
|
|
24831
24977
|
/** Publish or remove the connection surface for a connection key. `null` removes. */
|
|
24832
24978
|
set(connectionKey, surface) {
|
|
24833
24979
|
if (surface === null) {
|
|
@@ -24835,6 +24981,24 @@ var GreeConnectionResolver = class {
|
|
|
24835
24981
|
return;
|
|
24836
24982
|
}
|
|
24837
24983
|
this.#surfaces.set(connectionKey, surface);
|
|
24984
|
+
const subs = this.#subs.get(connectionKey);
|
|
24985
|
+
if (subs) for (const cb of subs) try {
|
|
24986
|
+
cb();
|
|
24987
|
+
} catch {}
|
|
24988
|
+
}
|
|
24989
|
+
/** Subscribe to surface (re)publish for a connection key. Returns unsubscribe. */
|
|
24990
|
+
onSurface(connectionKey, cb) {
|
|
24991
|
+
let set = this.#subs.get(connectionKey);
|
|
24992
|
+
if (!set) {
|
|
24993
|
+
set = /* @__PURE__ */ new Set();
|
|
24994
|
+
this.#subs.set(connectionKey, set);
|
|
24995
|
+
}
|
|
24996
|
+
set.add(cb);
|
|
24997
|
+
return () => {
|
|
24998
|
+
const current = this.#subs.get(connectionKey);
|
|
24999
|
+
current?.delete(cb);
|
|
25000
|
+
if (current && current.size === 0) this.#subs.delete(connectionKey);
|
|
25001
|
+
};
|
|
24838
25002
|
}
|
|
24839
25003
|
/** The bind-scan rows for a connection key, or empty when unknown. */
|
|
24840
25004
|
discovered(connectionKey) {
|
|
@@ -24858,6 +25022,7 @@ var GreeConnectionResolver = class {
|
|
|
24858
25022
|
/** Remove all registered surfaces (called on full shutdown). */
|
|
24859
25023
|
clear() {
|
|
24860
25024
|
this.#surfaces.clear();
|
|
25025
|
+
this.#subs.clear();
|
|
24861
25026
|
}
|
|
24862
25027
|
};
|
|
24863
25028
|
/** The single in-process per-connection resolver shared between the AC device's
|
|
@@ -24868,7 +25033,7 @@ var greeConnections = new GreeConnectionResolver();
|
|
|
24868
25033
|
function defaultFacade(config) {
|
|
24869
25034
|
return new Nodegree(toNodegreeOptions(config));
|
|
24870
25035
|
}
|
|
24871
|
-
var DEFAULT_POLL_INTERVAL_MS =
|
|
25036
|
+
var DEFAULT_POLL_INTERVAL_MS = 1e4;
|
|
24872
25037
|
/**
|
|
24873
25038
|
* Wraps exactly one `@apocaliss92/nodegree` facade bound to a SINGLE Gree AC
|
|
24874
25039
|
* (standalone mode — the connection lives on the AC device). {@link start} runs a
|
|
@@ -25067,6 +25232,27 @@ function resolveBroadcastTarget(connection) {
|
|
|
25067
25232
|
* The returned identity is what {@link import('./config.js').greeAcDeviceSchema}
|
|
25068
25233
|
* persists on the new device; the live per-device manager re-binds on activate.
|
|
25069
25234
|
*/
|
|
25235
|
+
/**
|
|
25236
|
+
* One-shot LAN broadcast scan for Gree ACs — the network-discovery pass. Builds a
|
|
25237
|
+
* throwaway facade from default (or provided) UDP settings, runs the broadcast,
|
|
25238
|
+
* and returns every responder. No bind, no persistence. The provider's
|
|
25239
|
+
* `discoverDevices()` maps the responders to adoption candidates.
|
|
25240
|
+
*/
|
|
25241
|
+
async function discoverGreeDevices(input) {
|
|
25242
|
+
const connection = settingsToGreeConfig({
|
|
25243
|
+
...input.broadcastAddress ? { broadcastAddr: input.broadcastAddress } : {},
|
|
25244
|
+
...input.timeoutMs ? { timeoutMs: input.timeoutMs } : {}
|
|
25245
|
+
});
|
|
25246
|
+
const facade = (input.makeFacade ?? defaultFacade)(connection);
|
|
25247
|
+
try {
|
|
25248
|
+
const broadcast = resolveBroadcastTarget(connection);
|
|
25249
|
+
const opts = { timeoutMs: connection.timeoutMs };
|
|
25250
|
+
if (broadcast.length > 0) opts.broadcastAddr = broadcast;
|
|
25251
|
+
return await facade.discover(opts);
|
|
25252
|
+
} finally {
|
|
25253
|
+
await facade.close().catch(() => void 0);
|
|
25254
|
+
}
|
|
25255
|
+
}
|
|
25070
25256
|
async function bindOnce(input) {
|
|
25071
25257
|
const { connection, logger } = input;
|
|
25072
25258
|
const facade = (input.makeFacade ?? defaultFacade)(connection);
|
|
@@ -25365,7 +25551,19 @@ var greeAcSchema = object({
|
|
|
25365
25551
|
greeMac: string().min(1).describe("Gree AC MAC address"),
|
|
25366
25552
|
connectionKey: string().min(1).describe("Per-device connection resolver key"),
|
|
25367
25553
|
system: literal("gree").optional(),
|
|
25368
|
-
integrationId: string().optional()
|
|
25554
|
+
integrationId: string().optional(),
|
|
25555
|
+
/** Operator filter — HVAC modes exposed in the control UI (subset of the
|
|
25556
|
+
* model's supported modes). Empty / omitted ⇒ expose all. `off` is always
|
|
25557
|
+
* exposed (power). */
|
|
25558
|
+
enabledModes: array(string()).optional(),
|
|
25559
|
+
/** Operator filter — fan speeds exposed in the control UI (subset of
|
|
25560
|
+
* `GREE_FAN_MODES`). Empty / omitted ⇒ expose all. */
|
|
25561
|
+
enabledFanModes: array(string()).optional(),
|
|
25562
|
+
/** Operator filter — expose the vertical-swing toggle. Default true. */
|
|
25563
|
+
exposeVerticalSwing: boolean().optional(),
|
|
25564
|
+
/** Operator filter — expose the horizontal-swing toggle (model-permitting).
|
|
25565
|
+
* Default true. */
|
|
25566
|
+
exposeHorizontalSwing: boolean().optional()
|
|
25369
25567
|
});
|
|
25370
25568
|
/**
|
|
25371
25569
|
* One Gree air conditioner as a CamStack `Thermostat`-type accessory child.
|
|
@@ -25405,17 +25603,16 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
25405
25603
|
*/
|
|
25406
25604
|
get features() {
|
|
25407
25605
|
const caps = this.resolveAc()?.capabilities ?? getAcCapabilities();
|
|
25408
|
-
const
|
|
25409
|
-
|
|
25410
|
-
|
|
25411
|
-
|
|
25412
|
-
];
|
|
25413
|
-
if (caps.horizontalSwing) flags.push(DeviceFeature.ClimateSwingHorizontal);
|
|
25606
|
+
const cfg = this.config.values;
|
|
25607
|
+
const flags = [DeviceFeature.ClimateFanMode, DeviceFeature.ClimatePreset];
|
|
25608
|
+
if (cfg.exposeVerticalSwing ?? true) flags.push(DeviceFeature.ClimateSwingVertical);
|
|
25609
|
+
if (caps.horizontalSwing && (cfg.exposeHorizontalSwing ?? true)) flags.push(DeviceFeature.ClimateSwingHorizontal);
|
|
25414
25610
|
return flags;
|
|
25415
25611
|
}
|
|
25416
25612
|
greeMac;
|
|
25417
25613
|
connectionKey;
|
|
25418
25614
|
stateChangedUnsub = null;
|
|
25615
|
+
surfaceUnsub = null;
|
|
25419
25616
|
constructor(ctx) {
|
|
25420
25617
|
const persisted = greeAcSchema.parse(ctx.persistedConfig ?? {});
|
|
25421
25618
|
super(ctx, greeAcSchema, { type: ctx.deviceMeta.type });
|
|
@@ -25440,8 +25637,21 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
25440
25637
|
this.registerCaps();
|
|
25441
25638
|
this.attachStateListener();
|
|
25442
25639
|
this.recomputeSlices();
|
|
25640
|
+
this.surfaceUnsub = greeConnections.onSurface(this.connectionKey, () => {
|
|
25641
|
+
this.attachStateListener();
|
|
25642
|
+
this.recomputeSlices();
|
|
25643
|
+
});
|
|
25443
25644
|
}
|
|
25444
25645
|
async removeDevice() {
|
|
25646
|
+
this.detachStateListener();
|
|
25647
|
+
if (this.surfaceUnsub) {
|
|
25648
|
+
try {
|
|
25649
|
+
this.surfaceUnsub();
|
|
25650
|
+
} catch {}
|
|
25651
|
+
this.surfaceUnsub = null;
|
|
25652
|
+
}
|
|
25653
|
+
}
|
|
25654
|
+
detachStateListener() {
|
|
25445
25655
|
if (this.stateChangedUnsub) {
|
|
25446
25656
|
try {
|
|
25447
25657
|
this.stateChangedUnsub();
|
|
@@ -25449,12 +25659,16 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
25449
25659
|
this.stateChangedUnsub = null;
|
|
25450
25660
|
}
|
|
25451
25661
|
}
|
|
25662
|
+
/** Attach the live `stateChanged` listener to the bound AC handle. Idempotent:
|
|
25663
|
+
* drops any prior listener first, so it is safe to call on every surface
|
|
25664
|
+
* (re)publish. No-op until the handle is actually bound. */
|
|
25452
25665
|
attachStateListener() {
|
|
25453
25666
|
const ac = this.resolveAc();
|
|
25454
25667
|
if (!ac) {
|
|
25455
25668
|
this.ctx.logger.debug("GreeAcDevice: handle not present; no live listener yet", { meta: { greeMac: this.greeMac } });
|
|
25456
25669
|
return;
|
|
25457
25670
|
}
|
|
25671
|
+
this.detachStateListener();
|
|
25458
25672
|
const onState = () => {
|
|
25459
25673
|
try {
|
|
25460
25674
|
this.recomputeSlices();
|
|
@@ -25472,7 +25686,7 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
25472
25686
|
}
|
|
25473
25687
|
registerCaps() {
|
|
25474
25688
|
this.ctx.registerNativeCap(climateControlCapability, {
|
|
25475
|
-
getStatus: async () => this.runtimeState.getCapState(CLIMATE_CAP) ?? CLIMATE_COLD_START,
|
|
25689
|
+
getStatus: async () => this.runtimeState.getCapState(CLIMATE_CAP) ?? this.applyClimateFilters(CLIMATE_COLD_START),
|
|
25476
25690
|
setMode: async ({ mode }) => {
|
|
25477
25691
|
const ac = this.requireAc();
|
|
25478
25692
|
const libMode = capModeToLibMode(mode);
|
|
@@ -25519,7 +25733,7 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
25519
25733
|
await ac.setSwingHorizontal(boolToHorizontalSwing(on));
|
|
25520
25734
|
}
|
|
25521
25735
|
});
|
|
25522
|
-
this.runtimeState.setCapState(CLIMATE_CAP, CLIMATE_COLD_START);
|
|
25736
|
+
this.runtimeState.setCapState(CLIMATE_CAP, this.applyClimateFilters(CLIMATE_COLD_START));
|
|
25523
25737
|
this.ctx.registerNativeCap(fanControlCapability, {
|
|
25524
25738
|
getStatus: async () => this.runtimeState.getCapState(FAN_CAP) ?? FAN_COLD_START,
|
|
25525
25739
|
setPercentage: async ({ percentage }) => {
|
|
@@ -25566,7 +25780,7 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
25566
25780
|
swingHorizontal: ac.capabilities.horizontalSwing ? horizontalSwingToBool(ac.swingHorizontal) : null,
|
|
25567
25781
|
lastFetchedAt: now
|
|
25568
25782
|
};
|
|
25569
|
-
this.runtimeState.setCapState(CLIMATE_CAP, climateSlice);
|
|
25783
|
+
this.runtimeState.setCapState(CLIMATE_CAP, this.applyClimateFilters(climateSlice));
|
|
25570
25784
|
const fanSlice = {
|
|
25571
25785
|
percentage: fanSpeedToPercentage(ac.fanSpeed),
|
|
25572
25786
|
percentageStep: GREE_FAN_PERCENTAGE_STEP,
|
|
@@ -25578,7 +25792,97 @@ var GreeAcDevice = class extends BaseDevice$1 {
|
|
|
25578
25792
|
};
|
|
25579
25793
|
this.runtimeState.setCapState(FAN_CAP, fanSlice);
|
|
25580
25794
|
}
|
|
25795
|
+
/**
|
|
25796
|
+
* Apply the operator's per-AC exposure filters to a climate slice: narrow
|
|
25797
|
+
* `availableModes` / `availableFanModes` to the enabled subsets (empty ⇒ all;
|
|
25798
|
+
* `off` is always kept) and null out a swing axis the operator has hidden.
|
|
25799
|
+
* Pure — returns a new slice, never mutates the input.
|
|
25800
|
+
*/
|
|
25801
|
+
applyClimateFilters(slice) {
|
|
25802
|
+
const cfg = this.config.values;
|
|
25803
|
+
const enabledModes = cfg.enabledModes;
|
|
25804
|
+
const enabledFanModes = cfg.enabledFanModes;
|
|
25805
|
+
const exposeV = cfg.exposeVerticalSwing ?? true;
|
|
25806
|
+
const exposeH = cfg.exposeHorizontalSwing ?? true;
|
|
25807
|
+
const availableModes = enabledModes && enabledModes.length > 0 ? slice.availableModes.filter((m) => m === "off" || enabledModes.includes(m)) : slice.availableModes;
|
|
25808
|
+
const availableFanModes = enabledFanModes && enabledFanModes.length > 0 ? slice.availableFanModes.filter((f) => enabledFanModes.includes(f)) : slice.availableFanModes;
|
|
25809
|
+
return {
|
|
25810
|
+
...slice,
|
|
25811
|
+
availableModes,
|
|
25812
|
+
availableFanModes,
|
|
25813
|
+
swingVertical: exposeV ? slice.swingVertical : null,
|
|
25814
|
+
swingHorizontal: exposeH ? slice.swingHorizontal : null
|
|
25815
|
+
};
|
|
25816
|
+
}
|
|
25817
|
+
/**
|
|
25818
|
+
* Per-AC exposure form (device-details settings). Lets the operator pick
|
|
25819
|
+
* which modes / fan speeds / swing axes appear in the AC control UI. An empty
|
|
25820
|
+
* multiselect means "expose all". The horizontal-swing toggle is only offered
|
|
25821
|
+
* when the model advertises horizontal swing.
|
|
25822
|
+
*/
|
|
25823
|
+
getSettingsUISchema() {
|
|
25824
|
+
const supportsHSwing = (this.resolveAc()?.capabilities ?? getAcCapabilities()).horizontalSwing;
|
|
25825
|
+
const cfg = this.config.values;
|
|
25826
|
+
const fields = [
|
|
25827
|
+
{
|
|
25828
|
+
type: "multiselect",
|
|
25829
|
+
key: "enabledModes",
|
|
25830
|
+
label: "Modes",
|
|
25831
|
+
description: "Modes shown in the control UI. Empty = all. “Off” is always available.",
|
|
25832
|
+
options: SUPPORTED_CAP_MODES.map((m) => ({
|
|
25833
|
+
value: m,
|
|
25834
|
+
label: titleCase(m)
|
|
25835
|
+
}))
|
|
25836
|
+
},
|
|
25837
|
+
{
|
|
25838
|
+
type: "multiselect",
|
|
25839
|
+
key: "enabledFanModes",
|
|
25840
|
+
label: "Fan speeds",
|
|
25841
|
+
description: "Fan speeds shown in the control UI. Empty = all.",
|
|
25842
|
+
options: GREE_FAN_MODES.map((f) => ({
|
|
25843
|
+
value: f,
|
|
25844
|
+
label: titleCase(f)
|
|
25845
|
+
}))
|
|
25846
|
+
},
|
|
25847
|
+
{
|
|
25848
|
+
type: "boolean",
|
|
25849
|
+
style: "switch",
|
|
25850
|
+
key: "exposeVerticalSwing",
|
|
25851
|
+
label: "Vertical swing",
|
|
25852
|
+
default: true
|
|
25853
|
+
}
|
|
25854
|
+
];
|
|
25855
|
+
if (supportsHSwing) fields.push({
|
|
25856
|
+
type: "boolean",
|
|
25857
|
+
style: "switch",
|
|
25858
|
+
key: "exposeHorizontalSwing",
|
|
25859
|
+
label: "Horizontal swing",
|
|
25860
|
+
default: true
|
|
25861
|
+
});
|
|
25862
|
+
return hydrateSchema({ sections: [{
|
|
25863
|
+
id: "exposure",
|
|
25864
|
+
title: "Exposed controls",
|
|
25865
|
+
description: "Choose which modes, fan speeds and swing axes appear in the air-conditioner control UI.",
|
|
25866
|
+
fields
|
|
25867
|
+
}] }, {
|
|
25868
|
+
enabledModes: cfg.enabledModes ?? [],
|
|
25869
|
+
enabledFanModes: cfg.enabledFanModes ?? [],
|
|
25870
|
+
exposeVerticalSwing: cfg.exposeVerticalSwing ?? true,
|
|
25871
|
+
exposeHorizontalSwing: cfg.exposeHorizontalSwing ?? true
|
|
25872
|
+
});
|
|
25873
|
+
}
|
|
25874
|
+
async applySettingsPatch(patch) {
|
|
25875
|
+
const typed = greeAcSchema.partial().parse(patch);
|
|
25876
|
+
await this.config.setAll(typed);
|
|
25877
|
+
this.recomputeSlices();
|
|
25878
|
+
await this.refreshFeatures();
|
|
25879
|
+
}
|
|
25581
25880
|
};
|
|
25881
|
+
/** 'fan_only' → 'Fan only', 'medium_low' → 'Medium low'. Pure. */
|
|
25882
|
+
function titleCase(value) {
|
|
25883
|
+
const spaced = value.replace(/_/g, " ");
|
|
25884
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
25885
|
+
}
|
|
25582
25886
|
//#endregion
|
|
25583
25887
|
//#region src/devices/gree-toggle-device.ts
|
|
25584
25888
|
var SWITCH_CAP = "switch";
|
|
@@ -25621,6 +25925,7 @@ var GreeToggleDevice = class extends BaseDevice$1 {
|
|
|
25621
25925
|
greeMac;
|
|
25622
25926
|
connectionKey;
|
|
25623
25927
|
stateChangedUnsub = null;
|
|
25928
|
+
surfaceUnsub = null;
|
|
25624
25929
|
constructor(ctx) {
|
|
25625
25930
|
const persisted = greeToggleSchema.parse(ctx.persistedConfig ?? {});
|
|
25626
25931
|
super(ctx, greeToggleSchema, { type: ctx.deviceMeta.type });
|
|
@@ -25672,8 +25977,21 @@ var GreeToggleDevice = class extends BaseDevice$1 {
|
|
|
25672
25977
|
this.registerCap();
|
|
25673
25978
|
this.attachStateListener();
|
|
25674
25979
|
this.recomputeSlice();
|
|
25980
|
+
this.surfaceUnsub = greeConnections.onSurface(this.connectionKey, () => {
|
|
25981
|
+
this.attachStateListener();
|
|
25982
|
+
this.recomputeSlice();
|
|
25983
|
+
});
|
|
25675
25984
|
}
|
|
25676
25985
|
async removeDevice() {
|
|
25986
|
+
this.detachStateListener();
|
|
25987
|
+
if (this.surfaceUnsub) {
|
|
25988
|
+
try {
|
|
25989
|
+
this.surfaceUnsub();
|
|
25990
|
+
} catch {}
|
|
25991
|
+
this.surfaceUnsub = null;
|
|
25992
|
+
}
|
|
25993
|
+
}
|
|
25994
|
+
detachStateListener() {
|
|
25677
25995
|
if (this.stateChangedUnsub) {
|
|
25678
25996
|
try {
|
|
25679
25997
|
this.stateChangedUnsub();
|
|
@@ -25690,6 +26008,7 @@ var GreeToggleDevice = class extends BaseDevice$1 {
|
|
|
25690
26008
|
} });
|
|
25691
26009
|
return;
|
|
25692
26010
|
}
|
|
26011
|
+
this.detachStateListener();
|
|
25693
26012
|
const onState = () => {
|
|
25694
26013
|
try {
|
|
25695
26014
|
this.recomputeSlice();
|
|
@@ -25849,7 +26168,7 @@ var GreeContainerDevice = class extends BaseDevice$1 {
|
|
|
25849
26168
|
return [{
|
|
25850
26169
|
stableIdSuffix: "ac",
|
|
25851
26170
|
meta: {
|
|
25852
|
-
type: DeviceType.
|
|
26171
|
+
type: DeviceType.Climate,
|
|
25853
26172
|
name: this.name,
|
|
25854
26173
|
linkDeviceId: this.id,
|
|
25855
26174
|
...this.integrationId !== void 0 ? { integrationId: this.integrationId } : {}
|
|
@@ -25935,9 +26254,10 @@ var GreeContainerDevice = class extends BaseDevice$1 {
|
|
|
25935
26254
|
* `instanceMode: multiple` — many ACs may be added. Placement: any-node — Gree is
|
|
25936
26255
|
* LOCAL UDP; the addon runs on whichever node shares the AC's subnet.
|
|
25937
26256
|
*
|
|
25938
|
-
* LAN UDP-broadcast auto-discovery
|
|
25939
|
-
*
|
|
25940
|
-
*
|
|
26257
|
+
* LAN UDP-broadcast auto-discovery is implemented via the `device-provider`
|
|
26258
|
+
* discovery surface (`supportsDiscovery`/`discoverDevices`/`adoptDiscoveredDevice`)
|
|
26259
|
+
* — the same one ONVIF uses and the aggregated "Scan network" modal fans out to.
|
|
26260
|
+
* Manual add-one-AC-by-IP (directed bind) remains available in parallel.
|
|
25941
26261
|
*/
|
|
25942
26262
|
var GreeProviderAddon = class extends BaseDeviceProvider {
|
|
25943
26263
|
addonId = "provider-gree";
|
|
@@ -25947,7 +26267,41 @@ var GreeProviderAddon = class extends BaseDeviceProvider {
|
|
|
25947
26267
|
super({});
|
|
25948
26268
|
}
|
|
25949
26269
|
async supportsDiscovery() {
|
|
25950
|
-
return
|
|
26270
|
+
return true;
|
|
26271
|
+
}
|
|
26272
|
+
async getDiscoveryParamsSchema() {
|
|
26273
|
+
return buildDiscoveryParamsFormSchema();
|
|
26274
|
+
}
|
|
26275
|
+
async discoverDevices(input) {
|
|
26276
|
+
const broadcastAddress = typeof input?.params?.["broadcastAddress"] === "string" ? input.params["broadcastAddress"].trim() : void 0;
|
|
26277
|
+
const timeoutMs = typeof input?.params?.["timeoutMs"] === "number" ? input.params["timeoutMs"] : void 0;
|
|
26278
|
+
const responders = await discoverGreeDevices({
|
|
26279
|
+
...broadcastAddress ? { broadcastAddress } : {},
|
|
26280
|
+
...timeoutMs ? { timeoutMs } : {}
|
|
26281
|
+
});
|
|
26282
|
+
this.ctx.logger.info("Gree discovery complete", { meta: {
|
|
26283
|
+
count: responders.length,
|
|
26284
|
+
broadcastAddress: broadcastAddress ?? "local"
|
|
26285
|
+
} });
|
|
26286
|
+
return responders.map((d) => {
|
|
26287
|
+
const displayName = d.name.length > 0 ? d.name : d.mac;
|
|
26288
|
+
return {
|
|
26289
|
+
stableId: `gree:${macKey(d.mac)}`,
|
|
26290
|
+
type: DeviceType.Container,
|
|
26291
|
+
suggestedName: displayName,
|
|
26292
|
+
prefilledConfig: {
|
|
26293
|
+
name: displayName,
|
|
26294
|
+
host: d.ip,
|
|
26295
|
+
...d.model !== void 0 ? { model: d.model } : {}
|
|
26296
|
+
}
|
|
26297
|
+
};
|
|
26298
|
+
});
|
|
26299
|
+
}
|
|
26300
|
+
async adoptDiscoveredDevice(input) {
|
|
26301
|
+
return this.createDevice({
|
|
26302
|
+
type: DeviceType.Container,
|
|
26303
|
+
config: input.candidate.prefilledConfig
|
|
26304
|
+
});
|
|
25951
26305
|
}
|
|
25952
26306
|
async supportsManualCreation() {
|
|
25953
26307
|
return true;
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ function buildGreeCandidates(input) {
|
|
|
18
18
|
out.push({
|
|
19
19
|
childNativeId: macKey(device.mac),
|
|
20
20
|
name: device.name.length > 0 ? device.name : device.mac,
|
|
21
|
-
type: require_addon.DeviceType.
|
|
21
|
+
type: require_addon.DeviceType.Climate,
|
|
22
22
|
status: "online",
|
|
23
23
|
metadata: {
|
|
24
24
|
serialNumber: device.mac,
|
package/dist/index.mjs
CHANGED
|
@@ -17,7 +17,7 @@ function buildGreeCandidates(input) {
|
|
|
17
17
|
out.push({
|
|
18
18
|
childNativeId: macKey(device.mac),
|
|
19
19
|
name: device.name.length > 0 ? device.name : device.mac,
|
|
20
|
-
type: DeviceType.
|
|
20
|
+
type: DeviceType.Climate,
|
|
21
21
|
status: "online",
|
|
22
22
|
metadata: {
|
|
23
23
|
serialNumber: device.mac,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-provider-gree",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Gree air-conditioner device-provider addon for CamStack — wraps the @apocaliss92/nodegree local-UDP client (LAN discovery + AES control), exposing climate-control and fan-control",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|