homebridge-melcloud-control 2.4.0 → 2.5.0-beta.0
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 +1 -1
- package/config.schema.json +1 -0
- package/homebridge-ui/public/homebridge-melcloud-control.png +0 -0
- package/homebridge-ui/public/index.html +196 -0
- package/homebridge-ui/server.js +71 -0
- package/index.js +3 -3
- package/package.json +3 -1
- package/src/melcloud.js +13 -13
package/CHANGELOG.md
CHANGED
package/config.schema.json
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
"pluginType": "platform",
|
|
4
4
|
"singular": true,
|
|
5
5
|
"fixArrays": true,
|
|
6
|
+
"customUi": true,
|
|
6
7
|
"headerDisplay": "This plugin works with Mitsubishi Air Conditioner, Heat Pump and Energy Recovery Ventilation connected to MELCloud service. Devices are exposed to HomeKit as separate accessories and each needs to be manually paired.",
|
|
7
8
|
"footerDisplay": "For documentation please see [GitHub repository](https://github.com/grzegorz914/homebridge-melcloud-control).",
|
|
8
9
|
"schema": {
|
|
Binary file
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
<div>
|
|
2
|
+
<img src="homebridge-melcloud-control.png" style="text-align: center;" height="150" />
|
|
3
|
+
</div>
|
|
4
|
+
<div id="accounts">
|
|
5
|
+
</div>
|
|
6
|
+
<div id="melCloudAccount" style="display: none;" class="card card-body">
|
|
7
|
+
<form id="configForm">
|
|
8
|
+
<div class="text-center">
|
|
9
|
+
<label id="accountName" style="font-size: 23px">Account Name</label><br>
|
|
10
|
+
<label id="info" style="text-align: center; font-size: 17px;">MELCloud Account</label><br><br>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="form-group">
|
|
13
|
+
<label for="name">Account Name</label>
|
|
14
|
+
<input id="name" type="text" class="form-control"><br>
|
|
15
|
+
<label for="user">MELCloud User</label>
|
|
16
|
+
<input id="user" type="text" class="form-control"><br>
|
|
17
|
+
<label for="passwd">MELCloud Password</label>
|
|
18
|
+
<input id="passwd" type="text" class="form-control"><br>
|
|
19
|
+
<label for="language">Language</label>
|
|
20
|
+
<input id="language" type="text" class="form-control"><br>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="text-center">
|
|
23
|
+
<button id="logIn" type="button" class="btn btn-primary">Log In</button>
|
|
24
|
+
<button id="getDevices" type="button" class="btn btn-primary">Get Devices</button>
|
|
25
|
+
<button id="configButton" type="button" class="btn btn-primary"><i class="fas fa-gear"></i></button>
|
|
26
|
+
</div>
|
|
27
|
+
</form>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<script>
|
|
31
|
+
(async () => {
|
|
32
|
+
//get the plugin config array
|
|
33
|
+
const pluginConfig = await homebridge.getPluginConfig();
|
|
34
|
+
|
|
35
|
+
if (!pluginConfig.length) {
|
|
36
|
+
pluginConfig.push({});
|
|
37
|
+
await homebridge.updatePluginConfig(pluginConfig);
|
|
38
|
+
homebridge.showSchemaForm();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
//get accounts count
|
|
43
|
+
const accountsCount = pluginConfig[0].accounts.length;
|
|
44
|
+
this.deviceIndex = 0;
|
|
45
|
+
for (let i = 0; i < accountsCount; i++) {
|
|
46
|
+
//create buttons
|
|
47
|
+
const button = document.createElement("button");
|
|
48
|
+
button.setAttribute("type", "button");
|
|
49
|
+
button.setAttribute("id", `button${i}`);
|
|
50
|
+
button.setAttribute("class", "btn btn-primary");
|
|
51
|
+
button.innerText = pluginConfig[0].accounts[i].name;
|
|
52
|
+
document.getElementById("accounts").appendChild(button);
|
|
53
|
+
|
|
54
|
+
//get actuall value on account select
|
|
55
|
+
document.getElementById(`button${i}`).addEventListener('click', async () => {
|
|
56
|
+
for (let j = 0; j < accountsCount; j++) {
|
|
57
|
+
const setRemoveAtribute = j === i ? document.getElementById(`button${j}`).setAttribute("class", "btn btn-secondary") : document.getElementById(`button${j}`).setAttribute("class", "btn btn-primary");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
document.getElementById('accountName').value = pluginConfig[0].accounts[i].name || '';
|
|
61
|
+
document.getElementById('name').value = pluginConfig[0].accounts[i].name || '';
|
|
62
|
+
document.getElementById('user').value = pluginConfig[0].accounts[i].user || '';
|
|
63
|
+
document.getElementById('passwd').value = pluginConfig[0].accounts[i].passwd || '';
|
|
64
|
+
document.getElementById('lnguage').value = pluginConfig[0].accounts[i].language || '';
|
|
65
|
+
|
|
66
|
+
const accountConfigured = pluginConfig[0].accounts[i].name && pluginConfig[0].accounts[i].user && pluginConfig[0].accounts[i].passwd && pluginConfig[0].accounts[i].language;
|
|
67
|
+
const setButtonName = accountConfigured ? document.getElementById('logIn').removeAttribute('disabled') : document.getElementById('logIn').setAttribute('disabled');
|
|
68
|
+
await homebridge.updatePluginConfig(pluginConfig);
|
|
69
|
+
this.deviceIndex = i;
|
|
70
|
+
});
|
|
71
|
+
const click = i === accountsCount - 1 ? document.getElementById(`button0`).click() : false;
|
|
72
|
+
const update = i === accountsCount - 1 ? await homebridge.updatePluginConfig(pluginConfig) : false;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
//load melCloudAccount form
|
|
76
|
+
document.getElementById('melCloudAccount').style.display = 'block';
|
|
77
|
+
|
|
78
|
+
//watch for changes to the form
|
|
79
|
+
document.getElementById('configForm').addEventListener('input', async () => {
|
|
80
|
+
pluginConfig[0].accounts[this.deviceIndex].name = document.querySelector('#name').value;
|
|
81
|
+
pluginConfig[0].accounts[this.deviceIndex].user = document.querySelector('#user').value;
|
|
82
|
+
pluginConfig[0].accounts[this.deviceIndex].passwd = document.querySelector('#passwd').value;
|
|
83
|
+
pluginConfig[0].accounts[this.deviceIndex].language = document.querySelector('#language').value;
|
|
84
|
+
|
|
85
|
+
const accountConfigured = pluginConfig[0].accounts[i].name && pluginConfig[0].accounts[i].user && pluginConfig[0].accounts[i].passwd && pluginConfig[0].accounts[i].language;
|
|
86
|
+
const setButtonMode = accountConfigured ? document.getElementById('logIn').removeAttribute('disabled') : document.getElementById('logIn').setAttribute('disabled');
|
|
87
|
+
|
|
88
|
+
await homebridge.updatePluginConfig(pluginConfig);
|
|
89
|
+
await homebridge.savePluginConfig(pluginConfig);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
//watch for changes to the config button
|
|
93
|
+
document.getElementById('configButton').addEventListener('click', async () => {
|
|
94
|
+
const showHideSettings = this.configButtonState ? homebridge.hideSchemaForm() : homebridge.showSchemaForm();
|
|
95
|
+
this.configButtonState = !this.configButtonState;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
//watch for click on the login button
|
|
99
|
+
document.getElementById('logIn').addEventListener('click', async () => {
|
|
100
|
+
homebridge.showSpinner();
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
await homebridge.updatePluginConfig(pluginConfig)
|
|
104
|
+
document.getElementById('info').innerHTML = '';
|
|
105
|
+
|
|
106
|
+
const accountName = pluginConfig[0].accounts[this.deviceIndex].name;
|
|
107
|
+
const user = pluginConfig[0].accounts[this.deviceIndex].user;
|
|
108
|
+
const passwd = pluginConfig[0].accounts[this.deviceIndex].passwd;
|
|
109
|
+
const language = pluginConfig[0].accounts[this.deviceIndex].language;
|
|
110
|
+
const enableDebugMode = pluginConfig[0].accounts[this.deviceIndex].enableDebugMode;
|
|
111
|
+
const refreshInterval = pluginConfig[0].accounts[this.deviceIndex].refreshInterval;
|
|
112
|
+
|
|
113
|
+
const payload = {
|
|
114
|
+
mode: 'connect',
|
|
115
|
+
accountName: accountName,
|
|
116
|
+
user: user,
|
|
117
|
+
passwd: passwd,
|
|
118
|
+
language: language,
|
|
119
|
+
enableDebugMode: enableDebugMode,
|
|
120
|
+
refreshInterval: refreshInterval
|
|
121
|
+
};
|
|
122
|
+
const response = await homebridge.request('/connect', payload);
|
|
123
|
+
const info = response.info;
|
|
124
|
+
const status = response.status;
|
|
125
|
+
const data = response.data;
|
|
126
|
+
const error = response.error;
|
|
127
|
+
|
|
128
|
+
switch (status) {
|
|
129
|
+
case 0:
|
|
130
|
+
document.getElementById('info').innerHTML = info;
|
|
131
|
+
document.getElementById('getDevices').removeAttribute('disabled');
|
|
132
|
+
break;
|
|
133
|
+
case 2:
|
|
134
|
+
document.getElementById('getDevices').setAttribute('disabled');
|
|
135
|
+
document.getElementById('info').innerHTML = `MELCloud error: ${JSON.stringify(error, null, 2)}`;
|
|
136
|
+
document.getElementById('info').style.color = 'red';
|
|
137
|
+
break;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
homebridge.hideSpinner();
|
|
141
|
+
} catch (e) {
|
|
142
|
+
homebridge.toast.error(`Data error: ${e.message}`);
|
|
143
|
+
} finally {
|
|
144
|
+
homebridge.hideSpinner();
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
//get devices
|
|
149
|
+
document.getElementById('getDevices').addEventListener('click', async () => {
|
|
150
|
+
homebridge.showSpinner();
|
|
151
|
+
try {
|
|
152
|
+
const devicesId = [];
|
|
153
|
+
|
|
154
|
+
const payload = {
|
|
155
|
+
mode: 'getdevices'
|
|
156
|
+
};
|
|
157
|
+
const response = await homebridge.request('/getDevices', payload);
|
|
158
|
+
|
|
159
|
+
const info = response.info;
|
|
160
|
+
const status = response.status;
|
|
161
|
+
const devices = response.data;
|
|
162
|
+
const error = response.error;
|
|
163
|
+
|
|
164
|
+
switch (status) {
|
|
165
|
+
case 1:
|
|
166
|
+
//get device info fom devices
|
|
167
|
+
for (const deviceInfo of devices) {
|
|
168
|
+
const deviceId = deviceInfo.DeviceID;
|
|
169
|
+
const deviceName = deviceInfo.DeviceName;
|
|
170
|
+
|
|
171
|
+
//prepare device if not in devices array
|
|
172
|
+
if (!devicesId.includes(deviceId)) {
|
|
173
|
+
devicesId.push(deviceId);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
document.getElementById('info').innerHTML = `${info} Found: ${devicesId.length} devices. Now You can start configure it.`;
|
|
178
|
+
await homebridge.updatePluginConfig(pluginConfig);
|
|
179
|
+
await homebridge.savePluginConfig(pluginConfig);
|
|
180
|
+
break;
|
|
181
|
+
case 2:
|
|
182
|
+
document.getElementById('info').innerHTML = `MELCloud error: ${JSON.stringify(error, null, 2)}`;
|
|
183
|
+
document.getElementById('info').style.color = 'red';
|
|
184
|
+
break;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
homebridge.hideSpinner();
|
|
188
|
+
} catch (e) {
|
|
189
|
+
homebridge.toast.error(`Get devices error: ${e.message}`);
|
|
190
|
+
} finally {
|
|
191
|
+
homebridge.hideSpinner();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
})();
|
|
196
|
+
</script>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { HomebridgePluginUiServer, RequestError } = require('@homebridge/plugin-ui-utils');
|
|
4
|
+
const MelCloud = require('../src/melcloud.js')
|
|
5
|
+
|
|
6
|
+
class PluginUiServer extends HomebridgePluginUiServer {
|
|
7
|
+
constructor() {
|
|
8
|
+
super();
|
|
9
|
+
|
|
10
|
+
//get devices
|
|
11
|
+
this.onRequest('/getdevices', this.start.bind(this));
|
|
12
|
+
|
|
13
|
+
//connect
|
|
14
|
+
this.onRequest('/connect', this.start.bind(this));
|
|
15
|
+
|
|
16
|
+
//this MUST be called when you are ready to accept requests
|
|
17
|
+
this.ready();
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
async start(payload) {
|
|
21
|
+
|
|
22
|
+
const mode = payload.mode;
|
|
23
|
+
const accountName = payload.accountName;
|
|
24
|
+
const user = payload.user;
|
|
25
|
+
const passwd = payload.passwd;
|
|
26
|
+
const language = payload.language;
|
|
27
|
+
const enableDebugMode = payload.enableDebugMode;
|
|
28
|
+
const refreshInterval = payload.refreshInterval;
|
|
29
|
+
const accountInfoFile = `${this.homebridgeStoragePath}/melcloud/${accountName}_Account`;
|
|
30
|
+
const deviceInfoFile = `${this.homebridgeStoragePath}/melcloud/${accountName}_Device_`;
|
|
31
|
+
const melCloud = new MelCloud(user, passwd, language, accountInfoFile, deviceInfoFile, enableDebugMode, refreshInterval);
|
|
32
|
+
|
|
33
|
+
let data = {};
|
|
34
|
+
switch (mode) {
|
|
35
|
+
case 'connect':
|
|
36
|
+
try {
|
|
37
|
+
const response = await melCloud.connect();
|
|
38
|
+
return data = {
|
|
39
|
+
info: 'Connect to MELCloud success, now pres the button Get Devices.',
|
|
40
|
+
status: 1,
|
|
41
|
+
data: response
|
|
42
|
+
};
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return data = {
|
|
45
|
+
info: 'Connect error:',
|
|
46
|
+
status: 2,
|
|
47
|
+
data: error
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
case 'getdevices':
|
|
51
|
+
try {
|
|
52
|
+
const response = await melCloud.chackDevicesList();
|
|
53
|
+
return data = {
|
|
54
|
+
info: 'Get devices success.',
|
|
55
|
+
status: 0,
|
|
56
|
+
data: response
|
|
57
|
+
};
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return data = {
|
|
60
|
+
info: 'Get devices error:',
|
|
61
|
+
status: 2,
|
|
62
|
+
data: error
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
(() => {
|
|
70
|
+
return new PluginUiServer();
|
|
71
|
+
})();
|
package/index.js
CHANGED
|
@@ -62,6 +62,7 @@ class MelCloudPlatform {
|
|
|
62
62
|
const prefDir = path.join(api.user.storagePath(), 'melcloud');
|
|
63
63
|
const accountInfoFile = `${prefDir}/${accountName}_Account`;
|
|
64
64
|
const buildingsFile = `${prefDir}/${accountName}_Buildings`;
|
|
65
|
+
const deviceInfoFile = `${prefDir}/${accountName}_Device_`;
|
|
65
66
|
|
|
66
67
|
//create directory if it doesn't exist
|
|
67
68
|
try {
|
|
@@ -77,7 +78,7 @@ class MelCloudPlatform {
|
|
|
77
78
|
const deviceRefreshInterval = account.deviceRefreshInterval * 1000 || 5000;
|
|
78
79
|
|
|
79
80
|
//melcloud account
|
|
80
|
-
const melCloud = new MelCloud(
|
|
81
|
+
const melCloud = new MelCloud(user, passwd, language, accountInfoFile, buildingsFile, deviceInfoFile, enableDebugMode, refreshInterval);
|
|
81
82
|
melCloud.on('checkDevicesListComplete', (accountInfo, contextKey, deviceInfo) => {
|
|
82
83
|
const deviceId = deviceInfo.DeviceID.toString();
|
|
83
84
|
const deviceType = deviceInfo.Type;
|
|
@@ -202,9 +203,8 @@ class MelCloudPlatform {
|
|
|
202
203
|
})
|
|
203
204
|
.on('error', async (error) => {
|
|
204
205
|
log.error(`Account ${accountName}, ${error}, check again in: ${refreshInterval / 1000}s.`);
|
|
205
|
-
melCloud.impulseGenerator.stop();
|
|
206
206
|
await new Promise(resolve => setTimeout(resolve, refreshInterval));
|
|
207
|
-
melCloud.connect();
|
|
207
|
+
await melCloud.connect();
|
|
208
208
|
});
|
|
209
209
|
};
|
|
210
210
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"displayName": "MELCloud Control",
|
|
3
3
|
"name": "homebridge-melcloud-control",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.5.0-beta.0",
|
|
5
5
|
"description": "Homebridge plugin to control Mitsubishi Air Conditioner, Heat Pump and Energy Recovery Ventilation.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "grzegorz914",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"main": "index.js",
|
|
20
20
|
"files": [
|
|
21
21
|
"src",
|
|
22
|
+
"homebridge-ui",
|
|
22
23
|
"index.js",
|
|
23
24
|
"config.schema.json",
|
|
24
25
|
"package.json",
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
"homebridge": ">=2.0.0"
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
35
|
+
"@homebridge/plugin-ui-utils": "^1.0.3",
|
|
34
36
|
"async-mqtt": "^2.6.3",
|
|
35
37
|
"axios": "^1.7.5",
|
|
36
38
|
"express": "^4.19.2"
|
package/src/melcloud.js
CHANGED
|
@@ -8,13 +8,12 @@ const ImpulseGenerator = require('./impulsegenerator.js');
|
|
|
8
8
|
const CONSTANTS = require('./constants.json');
|
|
9
9
|
|
|
10
10
|
class MelCloud extends EventEmitter {
|
|
11
|
-
constructor(
|
|
11
|
+
constructor(user, passwd, language, accountInfoFile, deviceInfoFile, buildingsFile, enableDebugMode, refreshInterval) {
|
|
12
12
|
super();
|
|
13
|
-
this.prefDir = prefDir;
|
|
14
|
-
this.accountName = accountName;
|
|
15
13
|
this.enableDebugMode = enableDebugMode;
|
|
16
14
|
this.accountInfoFile = accountInfoFile;
|
|
17
15
|
this.buildingsFile = buildingsFile;
|
|
16
|
+
this.deviceInfoFile = deviceInfoFile;
|
|
18
17
|
this.refreshInterval = refreshInterval;
|
|
19
18
|
this.devicesId = [];
|
|
20
19
|
|
|
@@ -46,9 +45,6 @@ class MelCloud extends EventEmitter {
|
|
|
46
45
|
const debug = this.enableDebugMode ? this.emit('debug', `Connecting to MELCloud.`) : false;
|
|
47
46
|
|
|
48
47
|
try {
|
|
49
|
-
//create timers array
|
|
50
|
-
const timers = [{ name: 'checkDevicesList', sampling: this.refreshInterval }];
|
|
51
|
-
|
|
52
48
|
const axiosInstanceLogin = axios.create({
|
|
53
49
|
method: 'POST',
|
|
54
50
|
baseURL: CONSTANTS.ApiUrls.BaseURL,
|
|
@@ -83,9 +79,6 @@ class MelCloud extends EventEmitter {
|
|
|
83
79
|
return;
|
|
84
80
|
};
|
|
85
81
|
|
|
86
|
-
//emit connect success
|
|
87
|
-
this.emit('success', `Connect to MELCloud Success.`)
|
|
88
|
-
|
|
89
82
|
//create axios instance get
|
|
90
83
|
this.axiosInstanceGet = axios.create({
|
|
91
84
|
method: 'GET',
|
|
@@ -130,8 +123,13 @@ class MelCloud extends EventEmitter {
|
|
|
130
123
|
//check devices list
|
|
131
124
|
await this.chackDevicesList();
|
|
132
125
|
|
|
126
|
+
//emit connect success
|
|
127
|
+
this.emit('success', `Connect to MELCloud Success.`)
|
|
128
|
+
|
|
133
129
|
//start impulse generator
|
|
134
|
-
this.
|
|
130
|
+
const timers = [{ name: 'checkDevicesList', sampling: this.refreshInterval }];
|
|
131
|
+
this.impulseGenerator.start(timers)
|
|
132
|
+
|
|
135
133
|
return true;
|
|
136
134
|
} catch (error) {
|
|
137
135
|
this.emit('error', `Connect to MELCloud error: ${error.message ?? error}.`);
|
|
@@ -183,7 +181,7 @@ class MelCloud extends EventEmitter {
|
|
|
183
181
|
const deviceName = deviceInfo.DeviceName;
|
|
184
182
|
|
|
185
183
|
//save every device info to the file
|
|
186
|
-
const deviceInfoFile = `${this.
|
|
184
|
+
const deviceInfoFile = `${this.deviceInfoFile}${deviceId}`;
|
|
187
185
|
await this.saveData(deviceInfoFile, deviceInfo);
|
|
188
186
|
const debug = this.enableDebugMode ? this.emit('debug', `Device: ${deviceName} info saved.`) : false;
|
|
189
187
|
|
|
@@ -192,9 +190,11 @@ class MelCloud extends EventEmitter {
|
|
|
192
190
|
this.devicesId.push(deviceId);
|
|
193
191
|
this.emit('checkDevicesListComplete', this.accountInfo, this.contextKey, deviceInfo);
|
|
194
192
|
}
|
|
195
|
-
}
|
|
196
|
-
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
return devices;
|
|
197
196
|
} catch (error) {
|
|
197
|
+
this.impulseGenerator.stop();
|
|
198
198
|
throw new Error(`Scanning for devices error: ${error.message ?? error}.`);
|
|
199
199
|
};
|
|
200
200
|
}
|