homebridge-melcloud-control 4.10.1-beta.1 → 4.10.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/index.js +78 -90
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -24,6 +24,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
24
24
  - For plugin < v4.6.0 use Homebridge UI <= v5.5.0
25
25
  - For plugin >= v4.6.0 use Homebridge UI >= v5.13.0
26
26
 
27
+ # [4.10.1] - (17.04.2026)
28
+
29
+ ## Changes
30
+
31
+ - fix RESTFul port assigned
32
+ - stability and performance improvements
33
+ - cleanup
34
+
27
35
  # [4.10.0] - (16.04.2026)
28
36
 
29
37
  ## Changes
package/index.js CHANGED
@@ -26,16 +26,18 @@ class MelCloudPlatform {
26
26
  }
27
27
 
28
28
  api.on('didFinishLaunching', () => {
29
+ const accountsName = [];
30
+
29
31
  // Each account is set up independently — a failure in one does not
30
32
  // block the others. Promise.allSettled runs all in parallel.
31
- const accountsName = [];
32
- const tasks = config.accounts.map(account =>
33
- this.setupAccount(account, accountsName, prefDir, log, api)
34
- );
35
- Promise.allSettled(tasks).then(results => {
33
+ Promise.allSettled(
34
+ config.accounts.map(account =>
35
+ this.setupAccount(account, accountsName, prefDir, log, api)
36
+ )
37
+ ).then(results => {
36
38
  results.forEach((result, i) => {
37
39
  if (result.status === 'rejected') {
38
- log.error(`Account[${i}] setup rejected: ${result.reason?.message ?? result.reason}`);
40
+ log.error(`Account[${i}] setup error: ${result.reason?.message ?? result.reason}`);
39
41
  }
40
42
  });
41
43
  });
@@ -47,17 +49,15 @@ class MelCloudPlatform {
47
49
  async setupAccount(account, accountsName, prefDir, log, api) {
48
50
  const { name, user, passwd, language, type } = account;
49
51
 
50
- // Skip disabled accounts silently
51
52
  if (type === 'disabled') return;
52
53
 
53
- // Validate required fields
54
- if (!name || !user || !passwd || !language || accountsName.includes(name)) {
54
+ if (!name || accountsName.includes(name) || !user || !passwd || !language) {
55
55
  const reason = !name ? 'name missing'
56
56
  : accountsName.includes(name) ? 'name duplicated'
57
57
  : !user ? 'user missing'
58
58
  : !passwd ? 'password missing'
59
59
  : 'language missing';
60
- log.warn(`Account ${name ?? '(unnamed)'}: ${reason} — will not be published`);
60
+ log.warn(`Account ${name ?? '(unnamed)'}: ${reason} — will not be published in the Home app`);
61
61
  return;
62
62
  }
63
63
  accountsName.push(name);
@@ -105,11 +105,11 @@ class MelCloudPlatform {
105
105
  log, api, impulseGenerator
106
106
  );
107
107
  } catch (error) {
108
- if (logLevel.error) log.error(`${name}, start error: ${error.message ?? error}, trying again in 120 s`);
108
+ if (logLevel.error) log.error(`${name}, Start impulse generator error, ${error.message ?? error}, trying again.`);
109
109
  }
110
110
  })
111
111
  .on('state', (state) => {
112
- if (logLevel.debug) log.info(`${name}, start impulse generator ${state ? 'started' : 'stopped'}`);
112
+ if (logLevel.debug) log.info(`${name}, Start impulse generator ${state ? 'started' : 'stopped'}.`);
113
113
  });
114
114
 
115
115
  await impulseGenerator.state(true, [{ name: 'start', sampling: 120_000 }]);
@@ -118,7 +118,6 @@ class MelCloudPlatform {
118
118
  // ── Connect, discover and register accessories for one account ────────────
119
119
 
120
120
  async startAccount(account, name, type, accountMelcloud, accountRefreshInterval, prefDir, logLevel, log, api, impulseGenerator) {
121
- // Determine timers and create the appropriate cloud class
122
121
  let timers;
123
122
  let melCloudClass;
124
123
 
@@ -128,145 +127,134 @@ class MelCloudPlatform {
128
127
  melCloudClass = new MelCloud(account, true);
129
128
  break;
130
129
  case 'melcloudhome':
131
- timers = [{ name: 'checkDevicesList', sampling: accountRefreshInterval }];
130
+ timers = [{ name: 'checkDevicesList', sampling: 10_000 }]; // fixed 100s interval for MELCloud Home, as it has its own internal timer
132
131
  melCloudClass = new MelCloudHome(account, true);
133
132
  break;
134
133
  default:
135
- if (logLevel.warn) log.warn(`${name}, unknown account type: ${type}`);
134
+ if (logLevel.warn) log.warn(`Unknown account type: ${type}.`);
136
135
  return;
137
136
  }
138
137
 
139
138
  melCloudClass
140
- .on('success', msg => log.success(`${name}, ${msg}`))
141
- .on('info', msg => log.info(`${name}, ${msg}`))
142
- .on('debug', msg => log.info(`${name}, debug: ${msg}`))
143
- .on('warn', msg => log.warn(`${name}, ${msg}`))
144
- .on('error', msg => log.error(`${name}, ${msg}`));
145
-
146
- // Connect (OAuth / token exchange)
147
- const connectData = await melCloudClass.connect();
148
- if (!connectData?.State) {
149
- if (logLevel.warn) log.warn(`${name}, ${connectData?.Status ?? 'connect failed'}`);
139
+ .on('success', (msg) => log.success(`${name}, ${msg}`))
140
+ .on('info', (msg) => log.info(`${name}, ${msg}`))
141
+ .on('debug', (msg) => log.info(`${name}, debug: ${msg}`))
142
+ .on('warn', (msg) => log.warn(`${name}, ${msg}`))
143
+ .on('error', (msg) => log.error(`${name}, ${msg}`));
144
+
145
+ // Connect
146
+ const melCloudAccountData = await melCloudClass.connect();
147
+ if (!melCloudAccountData?.State) {
148
+ if (logLevel.warn) log.warn(`${name}, ${melCloudAccountData?.Status ?? 'connect failed'}`);
150
149
  return;
151
150
  }
152
- if (logLevel.success) log.success(`${name}, ${connectData.Status}`);
151
+ if (logLevel.success) log.success(`${name}, ${melCloudAccountData.Status}`);
153
152
 
154
153
  // Discover devices
155
- const devicesData = await melCloudClass.checkDevicesList();
156
- if (!devicesData.State) {
157
- if (logLevel.warn) log.warn(`${name}, ${devicesData.Status}`);
154
+ const melCloudDevicesData = await melCloudClass.checkDevicesList();
155
+ if (!melCloudDevicesData.State) {
156
+ if (logLevel.warn) log.warn(`${name}, ${melCloudDevicesData.Status}`);
158
157
  return;
159
158
  }
160
- if (logLevel.debug) log.info(`${name}, ${devicesData.Status}`);
161
-
162
- // Filter to only the devices explicitly configured in the plugin config
163
- // and present in the API response. d.id is kept as string throughout.
164
- const apiDeviceIds = (devicesData.Devices ?? []).map(d => String(d.DeviceID));
159
+ if (logLevel.debug) log.info(`${name}, ${melCloudDevicesData.Status}`);
165
160
 
166
- const configuredDevices = [
167
- ...(account.ataDevices || []).filter(d => (d.displayType ?? 0) > 0 && apiDeviceIds.includes(String(d.id))),
168
- ...(account.atwDevices || []).filter(d => (d.displayType ?? 0) > 0 && apiDeviceIds.includes(String(d.id))),
169
- ...(account.ervDevices || []).filter(d => (d.displayType ?? 0) > 0 && apiDeviceIds.includes(String(d.id))),
170
- ];
161
+ // Filter configured devices — both sides coerced to string to avoid type mismatch
162
+ const devicesIds = (melCloudDevicesData.Devices ?? []).map(d => String(d.DeviceID));
163
+ const ataDevices = (account.ataDevices || []).filter(d => (d.displayType ?? 0) > 0 && devicesIds.includes(String(d.id)));
164
+ const atwDevices = (account.atwDevices || []).filter(d => (d.displayType ?? 0) > 0 && devicesIds.includes(String(d.id)));
165
+ const ervDevices = (account.ervDevices || []).filter(d => (d.displayType ?? 0) > 0 && devicesIds.includes(String(d.id)));
166
+ const devices = [...ataDevices, ...atwDevices, ...ervDevices];
171
167
 
172
- if (logLevel.debug) {
173
- const ata = (account.ataDevices || []).filter(d => (d.displayType ?? 0) > 0 && apiDeviceIds.includes(String(d.id))).length;
174
- const atw = (account.atwDevices || []).filter(d => (d.displayType ?? 0) > 0 && apiDeviceIds.includes(String(d.id))).length;
175
- const erv = (account.ervDevices || []).filter(d => (d.displayType ?? 0) > 0 && apiDeviceIds.includes(String(d.id))).length;
176
- log.info(`${name}, configured devices — ATA: ${ata}, ATW: ${atw}, ERV: ${erv}`);
177
- }
168
+ if (logLevel.debug) log.info(`${name}, found configured devices ATA: ${ataDevices.length}, ATW: ${atwDevices.length}, ERV: ${ervDevices.length}.`);
178
169
 
179
- // Register each configured device as a Homebridge accessory
180
- for (const [index, device] of configuredDevices.entries()) {
170
+ // Register each device as a Homebridge accessory
171
+ for (const [index, device] of devices.entries()) {
181
172
  await this.registerDevice({
182
- device, index, name, type, accountMelcloud,
173
+ account, device, index, name, type, accountMelcloud,
183
174
  prefDir, logLevel, log, api,
184
- melCloudClass, connectData, devicesData,
175
+ melCloudClass, melCloudAccountData, melCloudDevicesData,
185
176
  });
186
177
  }
187
178
 
188
- // Hand off periodic polling to the melcloud class and stop the startup generator
179
+ // Stop startup generator and hand off to the melcloud class generator
189
180
  await impulseGenerator.state(false);
190
181
  await melCloudClass.impulseGenerator.state(true, timers, false);
191
182
  }
192
183
 
193
184
  // ── Register a single device as a Homebridge accessory ───────────────────
194
185
 
195
- async registerDevice({ device, index, name, type, accountMelcloud, prefDir, logLevel, log, api, melCloudClass, connectData, devicesData }) {
196
- const deviceId = String(device.id);
186
+ async registerDevice({ account, device, index, name, type, accountMelcloud, prefDir, logLevel, log, api, melCloudClass, melCloudAccountData, melCloudDevicesData }) {
187
+ device.id = String(device.id);
188
+
197
189
  const deviceName = device.name;
198
190
  const deviceType = device.type;
199
191
  const deviceTypeString = DeviceType[deviceType] ?? `type${deviceType}`;
200
- const defaultTempsFile = join(prefDir, `${name}_${deviceId}_Temps`);
192
+ const defaultTempsFile = `${prefDir}/${name}_${device.id}_Temps`;
201
193
 
202
194
  // Find the matching API device — both sides coerced to string
203
- const apiDevice = devicesData.Devices.find(d => String(d.DeviceID) === deviceId);
204
- if (!apiDevice) {
205
- log.warn(`${name}, device ${deviceId} not found in API response skipping`);
195
+ const melCloudDeviceData = melCloudDevicesData.Devices.find(d => String(d.DeviceID) === device.id);
196
+ if (!melCloudDeviceData) {
197
+ log.warn(`${name}, device ${device.id} not found in API response, skipping`);
206
198
  return;
207
199
  }
208
200
 
209
- apiDevice.Scenes = devicesData.Scenes ?? [];
201
+ melCloudDeviceData.Scenes = melCloudDevicesData.Scenes ?? [];
210
202
 
211
- // Presets, schedules and scenes are filtered to only IDs that exist in the API
212
- const presetIds = (apiDevice.Presets ?? []).map(p => String(p.ID));
213
- const scheduleIds = (apiDevice.Schedule ?? []).map(s => String(s.Id));
214
- const sceneIds = (devicesData.Scenes ?? []).map(s => String(s.Id));
203
+ // Presets, schedules, scenes filtered to IDs present in the API response
204
+ const presetIds = (melCloudDeviceData.Presets ?? []).map(p => String(p.ID));
205
+ const schedulesIds = (melCloudDeviceData.Schedule ?? []).map(s => String(s.Id));
206
+ const scenesIds = (melCloudDevicesData.Scenes ?? []).map(s => String(s.Id));
215
207
 
216
208
  const presets = accountMelcloud ? (device.presets || []).filter(p => (p.displayType ?? 0) > 0 && presetIds.includes(String(p.id))) : [];
217
- const schedules = !accountMelcloud ? (device.schedules || []).filter(s => (s.displayType ?? 0) > 0 && scheduleIds.includes(String(s.id))) : [];
218
- const scenes = !accountMelcloud ? (device.scenes || []).filter(s => (s.displayType ?? 0) > 0 && sceneIds.includes(String(s.id))) : [];
209
+ const schedules = !accountMelcloud ? (device.schedules || []).filter(s => (s.displayType ?? 0) > 0 && schedulesIds.includes(String(s.id))) : [];
210
+ const scenes = !accountMelcloud ? (device.scenes || []).filter(s => (s.displayType ?? 0) > 0 && scenesIds.includes(String(s.id))) : [];
219
211
  const buttons = (device.buttonsSensors || []).filter(b => (b.displayType ?? 0) > 0);
220
212
 
221
- // REST port is stored on the device copy, not on the shared account object,
222
- // to prevent later iterations overwriting earlier devices' port values.
223
- device.id = deviceId;
224
- device.restFulPort = type === 'melcloudhome'
213
+ // Store port on device never mutate the shared account object
214
+ account.restFul.port = type === 'melcloudhome'
225
215
  ? `${3000}${index}`
226
- : deviceId.slice(-4).replace(/^0/, '9');
216
+ : (device.id).slice(-4).replace(/^0/, '9');
227
217
 
228
- // Write default temperature file for melcloudhome devices if not yet present
229
218
  if (type === 'melcloudhome') {
230
219
  try {
220
+ const temps = {
221
+ defaultCoolingSetTemperature: 24,
222
+ defaultHeatingSetTemperature: 20,
223
+ };
231
224
  if (!existsSync(defaultTempsFile)) {
232
- writeFileSync(defaultTempsFile, JSON.stringify({
233
- defaultCoolingSetTemperature: 24,
234
- defaultHeatingSetTemperature: 20,
235
- }, null, 2));
225
+ writeFileSync(defaultTempsFile, JSON.stringify(temps, null, 2));
236
226
  if (logLevel.debug) log.info(`${name}, default temperature file created: ${defaultTempsFile}`);
237
227
  }
238
228
  } catch (error) {
239
- if (logLevel.error) log.error(`${name}, ${deviceTypeString}, ${deviceName}, file init error: ${error.message}`);
240
- return; // skip this device — cannot proceed without the temps file
229
+ if (logLevel.error) log.error(`${name}, ${deviceTypeString}, ${deviceName}, File init error: ${error.message}`);
230
+ return;
241
231
  }
242
232
  }
243
233
 
244
- // Construct the device class
245
- const args = [api, device, presets, schedules, scenes, buttons, defaultTempsFile, melCloudClass, connectData, apiDevice];
234
+ // Construct the device class — original arg order preserved
246
235
  let deviceClass;
247
-
248
236
  switch (deviceType) {
249
- case 0: deviceClass = new DeviceAta(...args); break; // ATA
250
- case 1: deviceClass = new DeviceAtw(...args); break; // ATW
251
- case 2: return; // reserved — not implemented
252
- case 3: deviceClass = new DeviceErv(...args); break; // ERV
237
+ case 0: deviceClass = new DeviceAta(api, account, device, presets, schedules, scenes, buttons, defaultTempsFile, melCloudClass, melCloudAccountData, melCloudDeviceData); break; // ATA
238
+ case 1: deviceClass = new DeviceAtw(api, account, device, presets, schedules, scenes, buttons, defaultTempsFile, melCloudClass, melCloudAccountData, melCloudDeviceData); break; // ATW
239
+ case 2: return; // reserved
240
+ case 3: deviceClass = new DeviceErv(api, account, device, presets, schedules, scenes, buttons, defaultTempsFile, melCloudClass, melCloudAccountData, melCloudDeviceData); break; // ERV
253
241
  default:
254
- if (logLevel.warn) log.warn(`${name}, ${deviceTypeString}, ${deviceName}, unknown device type: ${deviceType}`);
242
+ if (logLevel.warn) log.warn(`${name}, ${deviceTypeString}, ${deviceName}, received unknown device type: ${deviceType}.`);
255
243
  return;
256
244
  }
257
245
 
258
246
  deviceClass
259
- .on('devInfo', info => log.info(info))
260
- .on('success', msg => log.success(`${name}, ${deviceTypeString}, ${deviceName}, ${msg}`))
261
- .on('info', msg => log.info(`${name}, ${deviceTypeString}, ${deviceName}, ${msg}`))
262
- .on('debug', msg => log.info(`${name}, ${deviceTypeString}, ${deviceName}, debug: ${msg}`))
263
- .on('warn', msg => log.warn(`${name}, ${deviceTypeString}, ${deviceName}, ${msg}`))
264
- .on('error', msg => log.error(`${name}, ${deviceTypeString}, ${deviceName}, ${msg}`));
247
+ .on('devInfo', (info) => log.info(info))
248
+ .on('success', (msg) => log.success(`${name}, ${deviceTypeString}, ${deviceName}, ${msg}`))
249
+ .on('info', (msg) => log.info(`${name}, ${deviceTypeString}, ${deviceName}, ${msg}`))
250
+ .on('debug', (msg) => log.info(`${name}, ${deviceTypeString}, ${deviceName}, debug: ${msg}`))
251
+ .on('warn', (msg) => log.warn(`${name}, ${deviceTypeString}, ${deviceName}, ${msg}`))
252
+ .on('error', (msg) => log.error(`${name}, ${deviceTypeString}, ${deviceName}, ${msg}`));
265
253
 
266
254
  const accessory = await deviceClass.start();
267
255
  if (accessory) {
268
256
  api.publishExternalAccessories(PluginName, [accessory]);
269
- if (logLevel.success) log.success(`${name}, ${deviceTypeString}, ${deviceName}, published as external accessory`);
257
+ if (logLevel.success) log.success(`${name}, ${deviceTypeString}, ${deviceName}, Published as external accessory.`);
270
258
  }
271
259
  }
272
260
 
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.1-beta.1",
4
+ "version": "4.10.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",