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.
- package/CHANGELOG.md +8 -0
- package/index.js +78 -90
- 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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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
|
-
|
|
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},
|
|
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},
|
|
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:
|
|
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(
|
|
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
|
|
147
|
-
const
|
|
148
|
-
if (!
|
|
149
|
-
if (logLevel.warn) log.warn(`${name}, ${
|
|
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}, ${
|
|
151
|
+
if (logLevel.success) log.success(`${name}, ${melCloudAccountData.Status}`);
|
|
153
152
|
|
|
154
153
|
// Discover devices
|
|
155
|
-
const
|
|
156
|
-
if (!
|
|
157
|
-
if (logLevel.warn) log.warn(`${name}, ${
|
|
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}, ${
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
180
|
-
for (const [index, device] of
|
|
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,
|
|
175
|
+
melCloudClass, melCloudAccountData, melCloudDevicesData,
|
|
185
176
|
});
|
|
186
177
|
}
|
|
187
178
|
|
|
188
|
-
//
|
|
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,
|
|
196
|
-
|
|
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 =
|
|
192
|
+
const defaultTempsFile = `${prefDir}/${name}_${device.id}_Temps`;
|
|
201
193
|
|
|
202
194
|
// Find the matching API device — both sides coerced to string
|
|
203
|
-
const
|
|
204
|
-
if (!
|
|
205
|
-
log.warn(`${name}, device ${
|
|
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
|
-
|
|
201
|
+
melCloudDeviceData.Scenes = melCloudDevicesData.Scenes ?? [];
|
|
210
202
|
|
|
211
|
-
// Presets, schedules
|
|
212
|
-
const presetIds = (
|
|
213
|
-
const
|
|
214
|
-
const
|
|
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 &&
|
|
218
|
-
const scenes = !accountMelcloud ? (device.scenes || []).filter(s => (s.displayType ?? 0) > 0 &&
|
|
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
|
-
//
|
|
222
|
-
|
|
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
|
-
:
|
|
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},
|
|
240
|
-
return;
|
|
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(
|
|
250
|
-
case 1: deviceClass = new DeviceAtw(
|
|
251
|
-
case 2: return;
|
|
252
|
-
case 3: deviceClass = new DeviceErv(
|
|
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},
|
|
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
|
|
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",
|