homebridge-melcloud-control 4.10.20-beta.0 → 4.10.20-beta.2
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/package.json +1 -1
- package/src/deviceata.js +0 -6
- package/src/functions.js +19 -0
- package/src/melcloudata.js +41 -19
- package/src/melcloudatw.js +12 -0
- package/src/melclouderv.js +43 -22
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"displayName": "MELCloud Control",
|
|
3
3
|
"name": "homebridge-melcloud-control",
|
|
4
|
-
"version": "4.10.20-beta.
|
|
4
|
+
"version": "4.10.20-beta.2",
|
|
5
5
|
"description": "Homebridge plugin to control Mitsubishi Air Conditioner, Heat Pump and Energy Recovery Ventilation.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "grzegorz914",
|
package/src/deviceata.js
CHANGED
|
@@ -316,12 +316,6 @@ class DeviceAta extends EventEmitter {
|
|
|
316
316
|
})
|
|
317
317
|
.onSet(async (value) => {
|
|
318
318
|
try {
|
|
319
|
-
//ignore fan speed pushed by Apple Home together with powering the unit on from off, it's the dial's rest position, not user intent
|
|
320
|
-
if (!this.accessory.power) {
|
|
321
|
-
if (this.logDebug) this.emit('debug', `Ignore set fan speed while unit is off: ${value}`);
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
319
|
const max = numberOfFanSpeeds;
|
|
326
320
|
const minValue = supportsAutomaticFanSpeed ? 0 : 1;
|
|
327
321
|
const clampedValue = Math.min(Math.max(value, minValue), max);
|
package/src/functions.js
CHANGED
|
@@ -77,6 +77,25 @@ class Functions extends EventEmitter {
|
|
|
77
77
|
);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
//returns only the payload keys whose value differs from the matching PascalCase field in current device state
|
|
81
|
+
filterChanges(payload, current) {
|
|
82
|
+
if (!payload || typeof payload !== 'object' || !current) return { ...payload };
|
|
83
|
+
|
|
84
|
+
const changes = {};
|
|
85
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
86
|
+
if (value === undefined) continue;
|
|
87
|
+
const pascalKey = key.charAt(0).toUpperCase() + key.slice(1);
|
|
88
|
+
if (current[pascalKey] !== value) changes[key] = value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return changes;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
//true if at least one key in payload differs from the matching PascalCase field in current device state
|
|
95
|
+
hasChanges(payload, current) {
|
|
96
|
+
return Object.keys(this.filterChanges(payload, current)).length > 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
80
99
|
toPascalCaseKeys(object) {
|
|
81
100
|
if (object !== null && typeof object === 'object') {
|
|
82
101
|
return Object.fromEntries(
|
package/src/melcloudata.js
CHANGED
|
@@ -207,15 +207,34 @@ class MelCloudAta extends EventEmitter {
|
|
|
207
207
|
payload = { data: payload.LoginData };
|
|
208
208
|
path = ApiUrls.Post.UpdateApplicationOptions;
|
|
209
209
|
break;
|
|
210
|
-
default:
|
|
210
|
+
default: {
|
|
211
|
+
//compare against the API wrapper's own live-polled state, not the caller's closure-captured (possibly stale) deviceData
|
|
212
|
+
const changes = this.functions.filterChanges(payload, this.deviceData.Device);
|
|
213
|
+
if (Object.keys(changes).length === 0) {
|
|
214
|
+
if (this.logDebug) this.emit('debug', `Ignore send, no changes: ${JSON.stringify(payload)}`);
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
|
|
211
218
|
if (displayType === 1 && deviceData.Device.OperationMode === 8) {
|
|
212
|
-
|
|
219
|
+
changes.setTemperature = (deviceData.Device.DefaultCoolingSetTemperature + deviceData.Device.DefaultHeatingSetTemperature) / 2;
|
|
213
220
|
}
|
|
214
221
|
|
|
215
|
-
|
|
222
|
+
//rebuild effective flags from only the fields that actually changed, Power always stays forced
|
|
223
|
+
const flagBits = {
|
|
224
|
+
operationMode: AirConditioner.EffectiveFlags.OperationMode,
|
|
225
|
+
setTemperature: AirConditioner.EffectiveFlags.SetTemperature,
|
|
226
|
+
setFanSpeed: AirConditioner.EffectiveFlags.SetFanSpeed,
|
|
227
|
+
vaneVerticalDirection: AirConditioner.EffectiveFlags.VaneVertical,
|
|
228
|
+
vaneHorizontalDirection: AirConditioner.EffectiveFlags.VaneHorizontal,
|
|
229
|
+
prohibitSetTemperature: AirConditioner.EffectiveFlags.Prohibit,
|
|
230
|
+
prohibitOperationMode: AirConditioner.EffectiveFlags.Prohibit,
|
|
231
|
+
prohibitPower: AirConditioner.EffectiveFlags.Prohibit,
|
|
232
|
+
};
|
|
233
|
+
flag = Object.keys(changes).reduce((acc, key) => acc | (flagBits[key] ?? 0), AirConditioner.EffectiveFlags.Power);
|
|
234
|
+
|
|
216
235
|
payload = this.functions.toPascalCaseKeys({
|
|
217
|
-
...
|
|
218
|
-
power:
|
|
236
|
+
...changes,
|
|
237
|
+
power: changes.power !== false,
|
|
219
238
|
deviceID: deviceData.Device.DeviceID,
|
|
220
239
|
effectiveFlags: flag,
|
|
221
240
|
hasPendingCommand: true,
|
|
@@ -224,6 +243,7 @@ class MelCloudAta extends EventEmitter {
|
|
|
224
243
|
path = ApiUrls.Post.Ata;
|
|
225
244
|
update = true;
|
|
226
245
|
break;
|
|
246
|
+
}
|
|
227
247
|
}
|
|
228
248
|
|
|
229
249
|
if (this.logDebug) this.emit('debug', `Send data: ${JSON.stringify(payload, null, 2)}`);
|
|
@@ -288,9 +308,16 @@ class MelCloudAta extends EventEmitter {
|
|
|
288
308
|
if (scene) scene.Enabled = payload.enabled;
|
|
289
309
|
payload = {};
|
|
290
310
|
break;
|
|
291
|
-
default:
|
|
311
|
+
default: {
|
|
312
|
+
//compare against the API wrapper's own live-polled state, not the caller's closure-captured (possibly stale) deviceData
|
|
313
|
+
const changes = this.functions.filterChanges(payload, this.deviceData.Device);
|
|
314
|
+
if (Object.keys(changes).length === 0) {
|
|
315
|
+
if (this.logDebug) this.emit('debug', `Ignore send, no changes: ${JSON.stringify(payload)}`);
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
|
|
292
319
|
if (displayType === 1 && deviceData.Device.OperationMode === 8) {
|
|
293
|
-
|
|
320
|
+
changes.setTemperature = (deviceData.Device.DefaultCoolingSetTemperature + deviceData.Device.DefaultHeatingSetTemperature) / 2;
|
|
294
321
|
|
|
295
322
|
if (this.deviceData.Device.DefaultCoolingSetTemperature !== deviceData.Device.DefaultCoolingSetTemperature || this.deviceData.Device.DefaultHeatingSetTemperature !== deviceData.Device.DefaultHeatingSetTemperature) {
|
|
296
323
|
const temps = {
|
|
@@ -301,26 +328,21 @@ class MelCloudAta extends EventEmitter {
|
|
|
301
328
|
}
|
|
302
329
|
}
|
|
303
330
|
|
|
304
|
-
if (
|
|
305
|
-
if (
|
|
306
|
-
if (
|
|
307
|
-
if (
|
|
308
|
-
|
|
309
|
-
//cleanup undefined
|
|
310
|
-
Object.keys(payload).forEach(key => {
|
|
311
|
-
if (payload[key] === undefined) {
|
|
312
|
-
delete payload[key];
|
|
313
|
-
}
|
|
314
|
-
});
|
|
331
|
+
if (changes.setFanSpeed != null) changes.setFanSpeed = String(changes.setFanSpeed);
|
|
332
|
+
if (changes.operationMode != null) changes.operationMode = AirConditioner.OperationModeMapEnumToString[changes.operationMode];
|
|
333
|
+
if (changes.vaneHorizontalDirection != null) changes.vaneHorizontalDirection = AirConditioner.VaneHorizontalDirectionMapEnumToString[changes.vaneHorizontalDirection];
|
|
334
|
+
if (changes.vaneVerticalDirection != null) changes.vaneVerticalDirection = AirConditioner.VaneVerticalDirectionMapEnumToString[changes.vaneVerticalDirection];
|
|
315
335
|
|
|
336
|
+
payload = changes;
|
|
316
337
|
method = 'PUT';
|
|
317
338
|
path = ApiUrls.Home.Put.Ata.replace('deviceid', deviceData.DeviceID);
|
|
318
339
|
deviceData.Device = { ...deviceData.Device, ...payload };
|
|
319
340
|
break;
|
|
341
|
+
}
|
|
320
342
|
}
|
|
321
343
|
|
|
322
344
|
//send payload
|
|
323
|
-
if (this.logDebug) this.emit('debug', `Send data: ${JSON.stringify(payload, null, 2)}`);
|
|
345
|
+
if (!this.logDebug) this.emit('debug', `Send data: ${JSON.stringify(payload, null, 2)}`);
|
|
324
346
|
await this.client(path, { method: method, data: payload });
|
|
325
347
|
return true;
|
|
326
348
|
default:
|
package/src/melcloudatw.js
CHANGED
|
@@ -187,6 +187,12 @@ class MelCloudAtw extends EventEmitter {
|
|
|
187
187
|
path = ApiUrls.Post.UpdateApplicationOptions;
|
|
188
188
|
break;
|
|
189
189
|
default:
|
|
190
|
+
//compare against the API wrapper's own live-polled state, not the caller's closure-captured (possibly stale) deviceData
|
|
191
|
+
if (!this.functions.hasChanges(payload, this.deviceData.Device)) {
|
|
192
|
+
if (this.logDebug) this.emit('debug', `Ignore send, no changes: ${JSON.stringify(payload)}`);
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
190
196
|
flag = !flag ? HeatPump.EffectiveFlags.Power : HeatPump.EffectiveFlags.Power + flag;
|
|
191
197
|
payload = this.functions.toPascalCaseKeys({
|
|
192
198
|
...payload,
|
|
@@ -252,6 +258,12 @@ class MelCloudAtw extends EventEmitter {
|
|
|
252
258
|
payload = {};
|
|
253
259
|
break;
|
|
254
260
|
default:
|
|
261
|
+
//compare against the API wrapper's own live-polled state, not the caller's closure-captured (possibly stale) deviceData
|
|
262
|
+
if (!this.functions.hasChanges(payload, this.deviceData.Device)) {
|
|
263
|
+
if (this.logDebug) this.emit('debug', `Ignore send, no changes: ${JSON.stringify(payload)}`);
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
|
|
255
267
|
if (payload.operationMode != null) payload.operationMode = HeatPump.OperationModeMapEnumToString[payload.operationMode];
|
|
256
268
|
if (payload.operationModeZone1 != null) payload.operationModeZone1 = HeatPump.OperationModeZoneMapEnumToString[payload.operationModeZone1];
|
|
257
269
|
if (payload.operationModeZone2 != null) payload.operationModeZone2 = HeatPump.OperationModeZoneMapEnumToString[payload.operationModeZone2];
|
package/src/melclouderv.js
CHANGED
|
@@ -182,32 +182,50 @@ class MelCloudErv extends EventEmitter {
|
|
|
182
182
|
payload = { data: payload.LoginData };
|
|
183
183
|
path = ApiUrls.Post.UpdateApplicationOptions;
|
|
184
184
|
break;
|
|
185
|
-
default:
|
|
185
|
+
default: {
|
|
186
|
+
//compare against the API wrapper's own live-polled state, not the caller's closure-captured (possibly stale) deviceData
|
|
187
|
+
const changes = this.functions.filterChanges(payload, this.deviceData.Device);
|
|
188
|
+
if (Object.keys(changes).length === 0) {
|
|
189
|
+
if (this.logDebug) this.emit('debug', `Ignore send, no changes: ${JSON.stringify(payload)}`);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
186
193
|
//set target temp based on display mode and ventilation mode
|
|
187
194
|
switch (displayType) {
|
|
188
195
|
case 1: //Heather/Cooler
|
|
189
196
|
switch (deviceData.Device.VentilationMode) {
|
|
190
197
|
case 0: //LOSNAY
|
|
191
|
-
|
|
198
|
+
changes.setTemperature = deviceData.Device.DefaultHeatingSetTemperature;
|
|
192
199
|
break;
|
|
193
200
|
case 1: //BYPASS
|
|
194
|
-
|
|
201
|
+
changes.setTemperature = deviceData.Device.DefaultCoolingSetTemperature;
|
|
195
202
|
break;
|
|
196
203
|
case 2: //AUTO
|
|
197
204
|
const setTemperature = (deviceData.Device.DefaultCoolingSetTemperature + deviceData.Device.DefaultHeatingSetTemperature) / 2;
|
|
198
|
-
|
|
205
|
+
changes.setTemperature = setTemperature;
|
|
199
206
|
break;
|
|
200
207
|
};
|
|
201
208
|
case 2: //Thermostat
|
|
202
|
-
|
|
209
|
+
changes.setTemperature = deviceData.Device.SetTemperature;
|
|
203
210
|
break;
|
|
204
211
|
};
|
|
205
212
|
|
|
206
|
-
//
|
|
207
|
-
|
|
213
|
+
//rebuild effective flags from only the fields that actually changed, Power always stays forced
|
|
214
|
+
const flagBits = {
|
|
215
|
+
operationMode: Ventilation.EffectiveFlags.OperationMode,
|
|
216
|
+
ventilationMode: Ventilation.EffectiveFlags.VentilationMode,
|
|
217
|
+
setFanSpeed: Ventilation.EffectiveFlags.SetFanSpeed,
|
|
218
|
+
setTemperature: Ventilation.EffectiveFlags.SetTemperature,
|
|
219
|
+
nightPurgeMode: Ventilation.EffectiveFlags.NightPurgeMode,
|
|
220
|
+
prohibitSetTemperature: Ventilation.EffectiveFlags.Prohibit,
|
|
221
|
+
prohibitOperationMode: Ventilation.EffectiveFlags.Prohibit,
|
|
222
|
+
prohibitPower: Ventilation.EffectiveFlags.Prohibit,
|
|
223
|
+
};
|
|
224
|
+
flag = Object.keys(changes).reduce((acc, key) => acc | (flagBits[key] ?? 0), Ventilation.EffectiveFlags.Power);
|
|
225
|
+
|
|
208
226
|
payload = this.functions.toPascalCaseKeys({
|
|
209
|
-
...
|
|
210
|
-
power:
|
|
227
|
+
...changes,
|
|
228
|
+
power: changes.power !== false,
|
|
211
229
|
deviceID: deviceData.Device.DeviceID,
|
|
212
230
|
effectiveFlags: flag,
|
|
213
231
|
hasPendingCommand: true,
|
|
@@ -215,6 +233,7 @@ class MelCloudErv extends EventEmitter {
|
|
|
215
233
|
path = ApiUrls.Post.Erv;
|
|
216
234
|
update = true;
|
|
217
235
|
break;
|
|
236
|
+
}
|
|
218
237
|
}
|
|
219
238
|
|
|
220
239
|
if (this.logDebug) this.emit('debug', `Send data: ${JSON.stringify(payload, null, 2)}`);
|
|
@@ -254,9 +273,16 @@ class MelCloudErv extends EventEmitter {
|
|
|
254
273
|
if (scene) scene.Enabled = payload.enabled;
|
|
255
274
|
payload = {};
|
|
256
275
|
break;
|
|
257
|
-
default:
|
|
276
|
+
default: {
|
|
277
|
+
//compare against the API wrapper's own live-polled state, not the caller's closure-captured (possibly stale) deviceData
|
|
278
|
+
const changes = this.functions.filterChanges(payload, this.deviceData.Device);
|
|
279
|
+
if (Object.keys(changes).length === 0) {
|
|
280
|
+
if (this.logDebug) this.emit('debug', `Ignore send, no changes: ${JSON.stringify(payload)}`);
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
258
284
|
if (displayType === 1 && deviceData.Device.VentilationMode === 2) {
|
|
259
|
-
|
|
285
|
+
changes.setTemperature = (deviceData.Device.DefaultCoolingSetTemperature + deviceData.Device.DefaultHeatingSetTemperature) / 2;
|
|
260
286
|
|
|
261
287
|
if (this.deviceData.Device.DefaultCoolingSetTemperature !== deviceData.Device.DefaultCoolingSetTemperature || this.deviceData.Device.DefaultHeatingSetTemperature !== deviceData.Device.DefaultHeatingSetTemperature) {
|
|
262
288
|
const temps = {
|
|
@@ -267,21 +293,16 @@ class MelCloudErv extends EventEmitter {
|
|
|
267
293
|
}
|
|
268
294
|
}
|
|
269
295
|
|
|
270
|
-
if (
|
|
271
|
-
if (
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
//cleanup undefined
|
|
275
|
-
Object.keys(payload).forEach(key => {
|
|
276
|
-
if (payload[key] === undefined) {
|
|
277
|
-
delete payload[key];
|
|
278
|
-
}
|
|
279
|
-
});
|
|
296
|
+
if (changes.setFanSpeed != null) changes.setFanSpeed = String(changes.setFanSpeed);
|
|
297
|
+
if (changes.operationMode != null) changes.operationMode = Ventilation.OperationModeMapEnumToString[changes.operationMode];
|
|
298
|
+
if (changes.ventilationMode != null) changes.ventilationMode = Ventilation.VentilationModeMapEnumToString[changes.ventilationMode];
|
|
280
299
|
|
|
300
|
+
payload = changes;
|
|
281
301
|
method = 'PUT';
|
|
282
302
|
path = ApiUrls.Home.Put.Erv.replace('deviceid', deviceData.DeviceID);
|
|
283
303
|
deviceData.Device = { ...deviceData.Device, ...payload };
|
|
284
|
-
break
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
285
306
|
}
|
|
286
307
|
|
|
287
308
|
if (this.logDebug) this.emit('debug', `Send data: ${JSON.stringify(payload, null, 2)}`);
|