homebridge-melcloud-control 4.3.3-beta.0 → 4.3.3-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.3.3-beta.0",
4
+ "version": "4.3.3-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",
package/src/constants.js CHANGED
@@ -107,8 +107,8 @@ export const HeatPump = {
107
107
  ForceDhwMapEnumToString: { 0: "Normal", 1: "Heat Now" },
108
108
  HolidayMapStringToEnum: { "Normal": 0, "Holiday": 1 },
109
109
  HolidayMapEnumToString: { 0: "Normal", 1: "Holiday" },
110
- ZoneOperationMapStringToEnum: { "Heat Thermostat": 0, "Heat Flow": 1, "Heat Curve": 2, "Cool Thermostat": 3, "Cool Flow": 4, "Flor Dry Up": 5, "Idle": 6 },
111
- ZoneOperationMapEnumToString: { 0: "Heat Thermostat", 1: "Heat Flow", 2: "Heat Curve", 3: "Cool Thermostat", 4: "Cool Flow", 5: "Flor Dry Up", 6: "Idle" },
110
+ OperationModeZoneMapStringToEnum: { "Heat Thermostat": 0, "Heat Flow": 1, "Heat Curve": 2, "Cool Thermostat": 3, "Cool Flow": 4, "Flor Dry Up": 5, "Idle": 6 },
111
+ OperationModeZoneMapEnumToString: { 0: "Heat Thermostat", 1: "Heat Flow", 2: "Heat Curve", 3: "Cool Thermostat", 4: "Cool Flow", 5: "Flor Dry Up", 6: "Idle" },
112
112
  EffectiveFlags: {
113
113
  Power: 1,
114
114
  OperationMode: 2,
@@ -1,3 +1,4 @@
1
+ import WebSocket from 'ws';
1
2
  import axios from 'axios';
2
3
  import EventEmitter from 'events';
3
4
  import ImpulseGenerator from './impulsegenerator.js';
@@ -26,6 +27,10 @@ class MelCloudAtw extends EventEmitter {
26
27
  //set default values
27
28
  this.deviceData = {};
28
29
  this.headers = {};
30
+ this.socket = null;
31
+ this.connecting = false;
32
+ this.socketConnected = false;
33
+ this.heartbeat = null;
29
34
 
30
35
  //lock flags
31
36
  this.locks = false;
@@ -51,20 +56,29 @@ class MelCloudAtw extends EventEmitter {
51
56
  }
52
57
  }
53
58
 
54
- async checkState() {
55
- try {
56
- //read device info from file
57
- const devicesData = await this.functions.readData(this.devicesFile, true);
58
- if (!devicesData) return;
59
+ cleanupSocket() {
60
+ if (this.heartbeat) {
61
+ clearInterval(this.heartbeat);
62
+ this.heartbeat = null;
63
+ }
59
64
 
60
- this.headers = devicesData.Headers;
61
- const deviceData = devicesData.Devices.find(device => device.DeviceID === this.deviceId);
65
+ if (this.socket) {
66
+ try { this.socket.close(); } catch { }
67
+ this.socket = null;
68
+ }
69
+
70
+ this.socketConnected = false;
71
+ }
72
+
73
+ async updateState(deviceData) {
74
+ try {
62
75
  if (this.accountType === 'melcloudhome') {
63
- deviceData.Scenes = devicesData.Scenes ?? [];
76
+ deviceData.Device.OperationMode = HeatPump.OperationModeMapStringToEnum[deviceData.Device.OperationMode] ?? deviceData.Device.OperationMode;
77
+ deviceData.Device.OperationModeZone1 = HeatPump.OperationModeZoneMapStringToEnum[deviceData.Device.OperationModeZone1] ?? deviceData.Device.OperationModeZone1;
78
+ deviceData.Device.OperationModeZone2 = HeatPump.OperationModeZoneMapStringToEnum[deviceData.Device.OperationModeZone2] ?? deviceData.Device.OperationModeZone2;
64
79
  }
65
80
  if (this.logDebug) this.emit('debug', `Device Data: ${JSON.stringify(deviceData, null, 2)}`);
66
81
 
67
- //device
68
82
  //device
69
83
  const serialNumber = deviceData.SerialNumber || '4.0.0';
70
84
  const firmwareAppVersion = deviceData.Device?.FirmwareAppVersion || '4.0.0';
@@ -123,6 +137,105 @@ class MelCloudAtw extends EventEmitter {
123
137
  };
124
138
  };
125
139
 
140
+ async checkState() {
141
+ try {
142
+ //read device info from file
143
+ const devicesData = await this.functions.readData(this.devicesFile, true);
144
+ if (!devicesData) return;
145
+
146
+ this.headers = devicesData.Headers;
147
+ const deviceData = devicesData.Devices.find(device => device.DeviceID === this.deviceId);
148
+ deviceData.Scenes = devicesData.Scenes ?? [];
149
+
150
+ //web cocket connection
151
+ if (this.accountType === 'melcloudhome' && !this.connecting && !this.socketConnected) {
152
+ this.connecting = true;
153
+
154
+ const url = `${ApiUrlsHome.WebSocketURL}${devicesData.WebSocketOptions.Hash}`;
155
+ try {
156
+ const socket = new WebSocket(url, { headers: devicesData.WebSocketOptions.Headers })
157
+ .on('error', (error) => {
158
+ if (this.logError) this.emit('error', `Socket error: ${error}`);
159
+ socket.close();
160
+ })
161
+ .on('close', () => {
162
+ if (this.logDebug) this.emit('debug', `Socket closed`);
163
+ this.cleanupSocket();
164
+ })
165
+ .on('open', () => {
166
+ this.socket = socket;
167
+ this.socketConnected = true;
168
+ this.connecting = false;
169
+ if (this.logSuccess) this.emit('success', `Socket Connect Success`);
170
+
171
+ // heartbeat
172
+ this.heartbeat = setInterval(() => {
173
+ if (socket.readyState === socket.OPEN) {
174
+ if (this.logDebug) this.emit('debug', `Socket send heartbeat`);
175
+ socket.ping();
176
+ }
177
+ }, 30000);
178
+ })
179
+ .on('pong', () => {
180
+ if (this.logDebug) this.emit('debug', `Socket received heartbeat`);
181
+ })
182
+ .on('message', async (message) => {
183
+ const parsedMessage = JSON.parse(message);
184
+ const stringifyMessage = JSON.stringify(parsedMessage, null, 2);
185
+ if (this.logDebug) this.emit('debug', `Incoming message: ${stringifyMessage}`);
186
+ if (parsedMessage.message === 'Forbidden') return;
187
+
188
+ const messageData = parsedMessage?.[0]?.Data;
189
+ if (!messageData) return;
190
+
191
+ let updateState = false;
192
+ const unitId = messageData?.id;
193
+ switch (unitId) {
194
+ case this.deviceId:
195
+ const messageType = parsedMessage[0].messageType;
196
+ switch (messageType) {
197
+ case 'unitStateChanged':
198
+ const settings = Object.fromEntries(
199
+ messageData.settings.map(({ name, value }) => {
200
+ let parsedValue = this.functions.convertValue(value);
201
+ return [name, parsedValue];
202
+ })
203
+ );
204
+ Object.assign(deviceData.Device, settings);
205
+ updateState = true;
206
+ break;
207
+ case 'unitWifiSignalChanged':
208
+ deviceData.Rssi = messageData.rssi;
209
+ updateState = true;
210
+ break;
211
+ default:
212
+ if (!this.logDebug) this.emit('debug', `Unit ${unitId}, received unknown message type: ${stringifyMessage}`);
213
+ return;
214
+ }
215
+ break;
216
+ default:
217
+ if (!this.logDebug) this.emit('debug', `Incoming unknown unit id: ${stringifyMessage}`);
218
+ return;
219
+ }
220
+
221
+ //update state
222
+ if (updateState) await this.updateState(deviceData);
223
+ });
224
+ } catch (error) {
225
+ if (this.logError) this.emit('error', `Socket connection failed: ${error}`);
226
+ this.cleanupSocket();
227
+ }
228
+ }
229
+
230
+ //update state
231
+ await this.updateState(deviceData);
232
+
233
+ return true;
234
+ } catch (error) {
235
+ throw new Error(`Check state error: ${error.message}`);
236
+ };
237
+ };
238
+
126
239
  async send(accountType, displayType, deviceData, flag, flagData) {
127
240
  try {
128
241
 
@@ -248,6 +361,7 @@ class MelCloudAtw extends EventEmitter {
248
361
  headers: headers,
249
362
  data: payload
250
363
  });
364
+
251
365
  this.updateData(deviceData, updateState);
252
366
  return true;
253
367
  default:
@@ -1,3 +1,4 @@
1
+ import WebSocket from 'ws';
1
2
  import axios from 'axios';
2
3
  import EventEmitter from 'events';
3
4
  import ImpulseGenerator from './impulsegenerator.js';
@@ -267,11 +267,11 @@ class MelCloudHome extends EventEmitter {
267
267
  '--no-zygote'
268
268
  ]
269
269
  });
270
- browser.on('disconnected', () => this.emit('debug', 'Browser disconnected'));
270
+ browser.on('disconnected', () => this.logDebug && this.emit('debug', 'Browser disconnected'));
271
271
 
272
272
  const page = await browser.newPage();
273
- page.on('error', error => this.emit('error', `Page crashed: ${error.message}`));
274
- page.on('pageerror', error => this.emit('error', `Browser error: ${error.message}`));
273
+ page.on('error', error => this.logError && this.emit('error', `Page crashed: ${error.message}`));
274
+ page.on('pageerror', error => this.logError && this.emit('error', `Browser error: ${error.message}`));
275
275
  page.setDefaultTimeout(GLOBAL_TIMEOUT);
276
276
  page.setDefaultNavigationTimeout(GLOBAL_TIMEOUT);
277
277