homebridge-ge-ac 1.1.0 → 1.2.0
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/README.md +4 -1
- package/lib/accessory.js +37 -8
- package/lib/platform.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,7 +48,10 @@ the unit echoes the change back on `publish#erd`.
|
|
|
48
48
|
Setpoint changes **stick**.
|
|
49
49
|
- **Mode switches** with real names: Cool, Fan Only, Energy Saver, Dry. (No bogus "Heat" — these
|
|
50
50
|
are cooling-only units. Each switch sets `ConfiguredName`, so HomeKit never shows "Switch 1–5".)
|
|
51
|
-
- **Fan speed**
|
|
51
|
+
- **Fan speed** as a dedicated **Fan** accessory — an **Auto ⇄ Manual** toggle plus a Low/Med/High
|
|
52
|
+
speed slider, mirroring the GE app's Auto/Low/Med/High. (Apple's Home app won't render fan speed on
|
|
53
|
+
a thermostat tile, so it's exposed as its own Fan service on the same accessory — it groups with the
|
|
54
|
+
AC rather than appearing as the thermostat's rotation-speed slider.)
|
|
52
55
|
- **Live updates** — state is pushed from the WebSocket subscription, so HomeKit reflects changes
|
|
53
56
|
made in the GE app or on the unit, in real time.
|
|
54
57
|
- Resilient: app‑level keepalive, OAuth token refresh ahead of expiry, and exponential‑backoff
|
package/lib/accessory.js
CHANGED
|
@@ -77,9 +77,9 @@ class ACAccessory {
|
|
|
77
77
|
.onSet(this._wrap(async (v) => { await this.client.setErd(this.mac, C.ERD.HEAT_TARGET_TEMP, C.encodeTempF(this._clampF(v))); }));
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
// Apple's Home app does NOT render RotationSpeed on a HeaterCooler, so fan speed lives on a
|
|
81
|
+
// dedicated Fanv2 service (below). Strip any stale cached RotationSpeed off the HeaterCooler.
|
|
82
|
+
if (hc.testCharacteristic(this.Ch.RotationSpeed)) hc.removeCharacteristic(hc.getCharacteristic(this.Ch.RotationSpeed));
|
|
83
83
|
|
|
84
84
|
hc.getCharacteristic(this.Ch.TemperatureDisplayUnits)
|
|
85
85
|
.onGet(() => (this.client.getState(this.mac, C.ERD.TEMP_UNIT) === '01') ? this.Ch.TemperatureDisplayUnits.CELSIUS : this.Ch.TemperatureDisplayUnits.FAHRENHEIT);
|
|
@@ -106,6 +106,27 @@ class ACAccessory {
|
|
|
106
106
|
return { sw, modeHex };
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
+
// ---- Fan (Fanv2): Auto/Manual toggle + Low/Med/High slider, bound to fan ERD 0x7A00 ----
|
|
110
|
+
// Apple Home renders this as a proper Fan tile (with an Auto switch), unlike a HeaterCooler's
|
|
111
|
+
// RotationSpeed. Same accessory, so it groups with the thermostat + mode switches.
|
|
112
|
+
const fan = accessory.getService(this.S.Fanv2) || accessory.addService(this.S.Fanv2, `${name} Fan`, 'GEAC_FANV2');
|
|
113
|
+
this.fan = fan;
|
|
114
|
+
this._configuredName(fan, 'Fan');
|
|
115
|
+
const TFS = this.Ch.TargetFanState, CFS = this.Ch.CurrentFanState, ACT = this.Ch.Active;
|
|
116
|
+
fan.getCharacteristic(ACT)
|
|
117
|
+
.onGet(() => this._power() ? ACT.ACTIVE : ACT.INACTIVE)
|
|
118
|
+
.onSet(this._wrap(async (v) => { await this.client.setErd(this.mac, C.ERD.POWER, v === ACT.ACTIVE ? C.POWER.ON : C.POWER.OFF); }));
|
|
119
|
+
fan.getCharacteristic(CFS)
|
|
120
|
+
.onGet(() => this._power() ? CFS.BLOWING_AIR : CFS.INACTIVE);
|
|
121
|
+
fan.getCharacteristic(TFS)
|
|
122
|
+
.onGet(() => this._fanIsAuto() ? TFS.AUTO : TFS.MANUAL)
|
|
123
|
+
.onSet(this._wrap(async (v) => {
|
|
124
|
+
await this.client.setErd(this.mac, C.ERD.FAN, v === TFS.AUTO ? C.FAN.AUTO : this._fanFromPct(this._fanSpeedPct()));
|
|
125
|
+
}));
|
|
126
|
+
fan.getCharacteristic(this.Ch.RotationSpeed) // continuous slider; onSet maps to Low(<=34)/Med(<=67)/High
|
|
127
|
+
.onGet(() => this._fanSpeedPct())
|
|
128
|
+
.onSet(this._wrap(async (pct) => { await this.client.setErd(this.mac, C.ERD.FAN, this._fanFromPct(pct)); }));
|
|
129
|
+
|
|
109
130
|
// live updates
|
|
110
131
|
this._onErd = (m) => { if (m === this.mac) this._pushAll(); };
|
|
111
132
|
client.on('erd', this._onErd);
|
|
@@ -133,12 +154,14 @@ class ACAccessory {
|
|
|
133
154
|
if (this._isHeatMode()) return (this._ambientF() < this._setpointF(C.ERD.HEAT_TARGET_TEMP)) ? CHCS.HEATING : CHCS.IDLE;
|
|
134
155
|
return (this._ambientF() > this._setpointF(C.ERD.TARGET_TEMP)) ? CHCS.COOLING : CHCS.IDLE;
|
|
135
156
|
}
|
|
136
|
-
|
|
157
|
+
// GE fan enum: AUTO=1 LOW=2 LOW_AUTO=3 MED=4 MED_AUTO=5 HIGH=8 HIGH_AUTO=9. The low bit = "auto".
|
|
158
|
+
_fanIsAuto() { const n = C.decodeInt(this.client.getState(this.mac, C.ERD.FAN)); return n == null || (n & 1) === 1; }
|
|
159
|
+
_fanSpeedPct() {
|
|
137
160
|
const n = C.decodeInt(this.client.getState(this.mac, C.ERD.FAN));
|
|
138
|
-
if (n == null) return
|
|
139
|
-
if (n
|
|
161
|
+
if (n == null) return 33;
|
|
162
|
+
if (n & 8) return 100; if (n & 4) return 66; if (n & 2) return 33; return 33; // pure AUTO(1): default Low position
|
|
140
163
|
}
|
|
141
|
-
_fanFromPct(p) { if (p <= 0) return C.FAN.AUTO; if (p <=
|
|
164
|
+
_fanFromPct(p) { if (p <= 0) return C.FAN.AUTO; if (p <= 34) return C.FAN.LOW; if (p <= 67) return C.FAN.MED; return C.FAN.HIGH; }
|
|
142
165
|
|
|
143
166
|
_pushAll() {
|
|
144
167
|
const u = (c, v) => { try { this.hc.updateCharacteristic(c, v); } catch (e) { /* ignore */ } };
|
|
@@ -148,7 +171,13 @@ class ACAccessory {
|
|
|
148
171
|
u(this.Ch.CurrentTemperature, C.fToC(this._ambientF()));
|
|
149
172
|
u(this.Ch.CoolingThresholdTemperature, this._threshC(C.ERD.TARGET_TEMP));
|
|
150
173
|
if (this.heatCapable) u(this.Ch.HeatingThresholdTemperature, this._threshC(C.ERD.HEAT_TARGET_TEMP));
|
|
151
|
-
|
|
174
|
+
if (this.fan) {
|
|
175
|
+
const uf = (c, v) => { try { this.fan.updateCharacteristic(c, v); } catch (e) { /* ignore */ } };
|
|
176
|
+
uf(this.Ch.Active, this._power() ? this.Ch.Active.ACTIVE : this.Ch.Active.INACTIVE);
|
|
177
|
+
uf(this.Ch.CurrentFanState, this._power() ? this.Ch.CurrentFanState.BLOWING_AIR : this.Ch.CurrentFanState.INACTIVE);
|
|
178
|
+
uf(this.Ch.TargetFanState, this._fanIsAuto() ? this.Ch.TargetFanState.AUTO : this.Ch.TargetFanState.MANUAL);
|
|
179
|
+
uf(this.Ch.RotationSpeed, this._fanSpeedPct());
|
|
180
|
+
}
|
|
152
181
|
for (const { sw, modeHex } of this.switches) {
|
|
153
182
|
try { sw.updateCharacteristic(this.Ch.On, this._power() && this._mode() === modeHex); } catch (e) { /* ignore */ }
|
|
154
183
|
}
|
package/lib/platform.js
CHANGED
|
@@ -57,6 +57,7 @@ class GEACPlatform {
|
|
|
57
57
|
this.log.info(`Restoring AC "${name}" (${mac}) from cache`);
|
|
58
58
|
accessory.context.mac = mac;
|
|
59
59
|
new ACAccessory(this, accessory, this.client, mac);
|
|
60
|
+
this.api.updatePlatformAccessories([accessory]); // persist service changes (e.g. added Fan, removed stale chars)
|
|
60
61
|
} else {
|
|
61
62
|
this.log.info(`Adding AC "${name}" (${mac})`);
|
|
62
63
|
accessory = new this.api.platformAccessory(name, uuid);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-ge-ac",
|
|
3
3
|
"displayName": "GE Profile AC (SmartHQ WebSocket)",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"description": "Homebridge plugin for GE SmartHQ Wi-Fi window air conditioners over GE's realtime WebSocket channel (reliable setpoint/mode/fan control). Built and tested on the cooling-only GE Profile AHTT06BC; likely works on similar GE Wi-Fi window ACs (unverified).",
|
|
6
6
|
"author": "Fabricore",
|
|
7
7
|
"license": "ISC",
|