homebridge-melcloud-control 4.10.19 → 4.10.20-beta.1

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.19",
4
+ "version": "4.10.20-beta.1",
5
5
  "description": "Homebridge plugin to control Mitsubishi Air Conditioner, Heat Pump and Energy Recovery Ventilation.",
6
6
  "license": "MIT",
7
7
  "author": "grzegorz914",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@homebridge/plugin-ui-utils": "^2.2.6",
39
- "mqtt": "^5.15.2",
39
+ "mqtt": "^5.16.0",
40
40
  "axios": "^1.20.0",
41
41
  "axios-cookiejar-support": "^7.0.0",
42
42
  "tough-cookie": "^6.0.2",
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,33 @@ class MelCloudAta extends EventEmitter {
207
207
  payload = { data: payload.LoginData };
208
208
  path = ApiUrls.Post.UpdateApplicationOptions;
209
209
  break;
210
- default:
210
+ default: {
211
+ const changes = this.functions.filterChanges(payload, deviceData.Device);
212
+ if (Object.keys(changes).length === 0) {
213
+ if (this.logDebug) this.emit('debug', `Ignore send, no changes: ${JSON.stringify(payload)}`);
214
+ return true;
215
+ }
216
+
211
217
  if (displayType === 1 && deviceData.Device.OperationMode === 8) {
212
- payload.setTemperature = (deviceData.Device.DefaultCoolingSetTemperature + deviceData.Device.DefaultHeatingSetTemperature) / 2;
218
+ changes.setTemperature = (deviceData.Device.DefaultCoolingSetTemperature + deviceData.Device.DefaultHeatingSetTemperature) / 2;
213
219
  }
214
220
 
215
- flag = !flag ? AirConditioner.EffectiveFlags.Power : AirConditioner.EffectiveFlags.Power + flag;
221
+ //rebuild effective flags from only the fields that actually changed, Power always stays forced
222
+ const flagBits = {
223
+ operationMode: AirConditioner.EffectiveFlags.OperationMode,
224
+ setTemperature: AirConditioner.EffectiveFlags.SetTemperature,
225
+ setFanSpeed: AirConditioner.EffectiveFlags.SetFanSpeed,
226
+ vaneVerticalDirection: AirConditioner.EffectiveFlags.VaneVertical,
227
+ vaneHorizontalDirection: AirConditioner.EffectiveFlags.VaneHorizontal,
228
+ prohibitSetTemperature: AirConditioner.EffectiveFlags.Prohibit,
229
+ prohibitOperationMode: AirConditioner.EffectiveFlags.Prohibit,
230
+ prohibitPower: AirConditioner.EffectiveFlags.Prohibit,
231
+ };
232
+ flag = Object.keys(changes).reduce((acc, key) => acc | (flagBits[key] ?? 0), AirConditioner.EffectiveFlags.Power);
233
+
216
234
  payload = this.functions.toPascalCaseKeys({
217
- ...payload,
218
- power: payload.power !== false,
235
+ ...changes,
236
+ power: changes.power !== false,
219
237
  deviceID: deviceData.Device.DeviceID,
220
238
  effectiveFlags: flag,
221
239
  hasPendingCommand: true,
@@ -224,6 +242,7 @@ class MelCloudAta extends EventEmitter {
224
242
  path = ApiUrls.Post.Ata;
225
243
  update = true;
226
244
  break;
245
+ }
227
246
  }
228
247
 
229
248
  if (this.logDebug) this.emit('debug', `Send data: ${JSON.stringify(payload, null, 2)}`);
@@ -288,9 +307,15 @@ class MelCloudAta extends EventEmitter {
288
307
  if (scene) scene.Enabled = payload.enabled;
289
308
  payload = {};
290
309
  break;
291
- default:
310
+ default: {
311
+ const changes = this.functions.filterChanges(payload, deviceData.Device);
312
+ if (Object.keys(changes).length === 0) {
313
+ if (this.logDebug) this.emit('debug', `Ignore send, no changes: ${JSON.stringify(payload)}`);
314
+ return true;
315
+ }
316
+
292
317
  if (displayType === 1 && deviceData.Device.OperationMode === 8) {
293
- payload.setTemperature = (deviceData.Device.DefaultCoolingSetTemperature + deviceData.Device.DefaultHeatingSetTemperature) / 2;
318
+ changes.setTemperature = (deviceData.Device.DefaultCoolingSetTemperature + deviceData.Device.DefaultHeatingSetTemperature) / 2;
294
319
 
295
320
  if (this.deviceData.Device.DefaultCoolingSetTemperature !== deviceData.Device.DefaultCoolingSetTemperature || this.deviceData.Device.DefaultHeatingSetTemperature !== deviceData.Device.DefaultHeatingSetTemperature) {
296
321
  const temps = {
@@ -301,22 +326,17 @@ class MelCloudAta extends EventEmitter {
301
326
  }
302
327
  }
303
328
 
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
- });
329
+ if (changes.setFanSpeed != null) changes.setFanSpeed = String(changes.setFanSpeed);
330
+ if (changes.operationMode != null) changes.operationMode = AirConditioner.OperationModeMapEnumToString[changes.operationMode];
331
+ if (changes.vaneHorizontalDirection != null) changes.vaneHorizontalDirection = AirConditioner.VaneHorizontalDirectionMapEnumToString[changes.vaneHorizontalDirection];
332
+ if (changes.vaneVerticalDirection != null) changes.vaneVerticalDirection = AirConditioner.VaneVerticalDirectionMapEnumToString[changes.vaneVerticalDirection];
315
333
 
334
+ payload = changes;
316
335
  method = 'PUT';
317
336
  path = ApiUrls.Home.Put.Ata.replace('deviceid', deviceData.DeviceID);
318
337
  deviceData.Device = { ...deviceData.Device, ...payload };
319
338
  break;
339
+ }
320
340
  }
321
341
 
322
342
  //send payload
@@ -187,6 +187,11 @@ class MelCloudAtw extends EventEmitter {
187
187
  path = ApiUrls.Post.UpdateApplicationOptions;
188
188
  break;
189
189
  default:
190
+ if (!this.functions.hasChanges(payload, deviceData.Device)) {
191
+ if (this.logDebug) this.emit('debug', `Ignore send, no changes: ${JSON.stringify(payload)}`);
192
+ return true;
193
+ }
194
+
190
195
  flag = !flag ? HeatPump.EffectiveFlags.Power : HeatPump.EffectiveFlags.Power + flag;
191
196
  payload = this.functions.toPascalCaseKeys({
192
197
  ...payload,
@@ -252,6 +257,11 @@ class MelCloudAtw extends EventEmitter {
252
257
  payload = {};
253
258
  break;
254
259
  default:
260
+ if (!this.functions.hasChanges(payload, deviceData.Device)) {
261
+ if (this.logDebug) this.emit('debug', `Ignore send, no changes: ${JSON.stringify(payload)}`);
262
+ return true;
263
+ }
264
+
255
265
  if (payload.operationMode != null) payload.operationMode = HeatPump.OperationModeMapEnumToString[payload.operationMode];
256
266
  if (payload.operationModeZone1 != null) payload.operationModeZone1 = HeatPump.OperationModeZoneMapEnumToString[payload.operationModeZone1];
257
267
  if (payload.operationModeZone2 != null) payload.operationModeZone2 = HeatPump.OperationModeZoneMapEnumToString[payload.operationModeZone2];
@@ -182,32 +182,49 @@ class MelCloudErv extends EventEmitter {
182
182
  payload = { data: payload.LoginData };
183
183
  path = ApiUrls.Post.UpdateApplicationOptions;
184
184
  break;
185
- default:
185
+ default: {
186
+ const changes = this.functions.filterChanges(payload, deviceData.Device);
187
+ if (Object.keys(changes).length === 0) {
188
+ if (this.logDebug) this.emit('debug', `Ignore send, no changes: ${JSON.stringify(payload)}`);
189
+ return true;
190
+ }
191
+
186
192
  //set target temp based on display mode and ventilation mode
187
193
  switch (displayType) {
188
194
  case 1: //Heather/Cooler
189
195
  switch (deviceData.Device.VentilationMode) {
190
196
  case 0: //LOSNAY
191
- payload.setTemperature = deviceData.Device.DefaultHeatingSetTemperature;
197
+ changes.setTemperature = deviceData.Device.DefaultHeatingSetTemperature;
192
198
  break;
193
199
  case 1: //BYPASS
194
- payload.setTemperature = deviceData.Device.DefaultCoolingSetTemperature;
200
+ changes.setTemperature = deviceData.Device.DefaultCoolingSetTemperature;
195
201
  break;
196
202
  case 2: //AUTO
197
203
  const setTemperature = (deviceData.Device.DefaultCoolingSetTemperature + deviceData.Device.DefaultHeatingSetTemperature) / 2;
198
- payload.setTemperature = setTemperature;
204
+ changes.setTemperature = setTemperature;
199
205
  break;
200
206
  };
201
207
  case 2: //Thermostat
202
- payload.setTemperature = deviceData.Device.SetTemperature;
208
+ changes.setTemperature = deviceData.Device.SetTemperature;
203
209
  break;
204
210
  };
205
211
 
206
- //device state
207
- flag = !flag ? Ventilation.EffectiveFlags.Power : Ventilation.EffectiveFlags.Power + flag;
212
+ //rebuild effective flags from only the fields that actually changed, Power always stays forced
213
+ const flagBits = {
214
+ operationMode: Ventilation.EffectiveFlags.OperationMode,
215
+ ventilationMode: Ventilation.EffectiveFlags.VentilationMode,
216
+ setFanSpeed: Ventilation.EffectiveFlags.SetFanSpeed,
217
+ setTemperature: Ventilation.EffectiveFlags.SetTemperature,
218
+ nightPurgeMode: Ventilation.EffectiveFlags.NightPurgeMode,
219
+ prohibitSetTemperature: Ventilation.EffectiveFlags.Prohibit,
220
+ prohibitOperationMode: Ventilation.EffectiveFlags.Prohibit,
221
+ prohibitPower: Ventilation.EffectiveFlags.Prohibit,
222
+ };
223
+ flag = Object.keys(changes).reduce((acc, key) => acc | (flagBits[key] ?? 0), Ventilation.EffectiveFlags.Power);
224
+
208
225
  payload = this.functions.toPascalCaseKeys({
209
- ...payload,
210
- power: payload.power !== false,
226
+ ...changes,
227
+ power: changes.power !== false,
211
228
  deviceID: deviceData.Device.DeviceID,
212
229
  effectiveFlags: flag,
213
230
  hasPendingCommand: true,
@@ -215,6 +232,7 @@ class MelCloudErv extends EventEmitter {
215
232
  path = ApiUrls.Post.Erv;
216
233
  update = true;
217
234
  break;
235
+ }
218
236
  }
219
237
 
220
238
  if (this.logDebug) this.emit('debug', `Send data: ${JSON.stringify(payload, null, 2)}`);
@@ -254,9 +272,15 @@ class MelCloudErv extends EventEmitter {
254
272
  if (scene) scene.Enabled = payload.enabled;
255
273
  payload = {};
256
274
  break;
257
- default:
275
+ default: {
276
+ const changes = this.functions.filterChanges(payload, deviceData.Device);
277
+ if (Object.keys(changes).length === 0) {
278
+ if (this.logDebug) this.emit('debug', `Ignore send, no changes: ${JSON.stringify(payload)}`);
279
+ return true;
280
+ }
281
+
258
282
  if (displayType === 1 && deviceData.Device.VentilationMode === 2) {
259
- payload.setTemperature = (deviceData.Device.DefaultCoolingSetTemperature + deviceData.Device.DefaultHeatingSetTemperature) / 2;
283
+ changes.setTemperature = (deviceData.Device.DefaultCoolingSetTemperature + deviceData.Device.DefaultHeatingSetTemperature) / 2;
260
284
 
261
285
  if (this.deviceData.Device.DefaultCoolingSetTemperature !== deviceData.Device.DefaultCoolingSetTemperature || this.deviceData.Device.DefaultHeatingSetTemperature !== deviceData.Device.DefaultHeatingSetTemperature) {
262
286
  const temps = {
@@ -267,21 +291,16 @@ class MelCloudErv extends EventEmitter {
267
291
  }
268
292
  }
269
293
 
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
- });
294
+ if (changes.setFanSpeed != null) changes.setFanSpeed = String(changes.setFanSpeed);
295
+ if (changes.operationMode != null) changes.operationMode = Ventilation.OperationModeMapEnumToString[changes.operationMode];
296
+ if (changes.ventilationMode != null) changes.ventilationMode = Ventilation.VentilationModeMapEnumToString[changes.ventilationMode];
280
297
 
298
+ payload = changes;
281
299
  method = 'PUT';
282
300
  path = ApiUrls.Home.Put.Erv.replace('deviceid', deviceData.DeviceID);
283
301
  deviceData.Device = { ...deviceData.Device, ...payload };
284
- break
302
+ break;
303
+ }
285
304
  }
286
305
 
287
306
  if (this.logDebug) this.emit('debug', `Send data: ${JSON.stringify(payload, null, 2)}`);