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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "displayName": "MELCloud Control",
3
3
  "name": "homebridge-melcloud-control",
4
- "version": "4.10.20-beta.0",
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(
@@ -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
- payload.setTemperature = (deviceData.Device.DefaultCoolingSetTemperature + deviceData.Device.DefaultHeatingSetTemperature) / 2;
219
+ changes.setTemperature = (deviceData.Device.DefaultCoolingSetTemperature + deviceData.Device.DefaultHeatingSetTemperature) / 2;
213
220
  }
214
221
 
215
- flag = !flag ? AirConditioner.EffectiveFlags.Power : AirConditioner.EffectiveFlags.Power + flag;
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
- ...payload,
218
- power: payload.power !== false,
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
- payload.setTemperature = (deviceData.Device.DefaultCoolingSetTemperature + deviceData.Device.DefaultHeatingSetTemperature) / 2;
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 (payload.setFanSpeed != null) payload.setFanSpeed = String(payload.setFanSpeed);
305
- if (payload.operationMode != null) payload.operationMode = AirConditioner.OperationModeMapEnumToString[payload.operationMode];
306
- if (payload.vaneHorizontalDirection != null) payload.vaneHorizontalDirection = AirConditioner.VaneHorizontalDirectionMapEnumToString[payload.vaneHorizontalDirection];
307
- if (payload.vaneVerticalDirection != null) payload.vaneVerticalDirection = AirConditioner.VaneVerticalDirectionMapEnumToString[payload.vaneVerticalDirection];
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:
@@ -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];
@@ -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
- payload.setTemperature = deviceData.Device.DefaultHeatingSetTemperature;
198
+ changes.setTemperature = deviceData.Device.DefaultHeatingSetTemperature;
192
199
  break;
193
200
  case 1: //BYPASS
194
- payload.setTemperature = deviceData.Device.DefaultCoolingSetTemperature;
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
- payload.setTemperature = setTemperature;
205
+ changes.setTemperature = setTemperature;
199
206
  break;
200
207
  };
201
208
  case 2: //Thermostat
202
- payload.setTemperature = deviceData.Device.SetTemperature;
209
+ changes.setTemperature = deviceData.Device.SetTemperature;
203
210
  break;
204
211
  };
205
212
 
206
- //device state
207
- flag = !flag ? Ventilation.EffectiveFlags.Power : Ventilation.EffectiveFlags.Power + flag;
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
- ...payload,
210
- power: payload.power !== false,
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
- payload.setTemperature = (deviceData.Device.DefaultCoolingSetTemperature + deviceData.Device.DefaultHeatingSetTemperature) / 2;
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 (payload.setFanSpeed != null) payload.setFanSpeed = String(payload.setFanSpeed);
271
- if (payload.operationMode != null) payload.operationMode = Ventilation.OperationModeMapEnumToString[payload.operationMode];
272
- if (payload.ventilationMode != null) payload.ventilationMode = Ventilation.VentilationModeMapEnumToString[payload.ventilationMode];
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)}`);