iobroker.airzone 2.0.3 → 3.0.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/.github/ISSUE_TEMPLATE/bug_report.md +32 -32
- package/CHANGELOG.md +7 -0
- package/LocalApi/AirzoneLocalApi.js +94 -25
- package/LocalApi/Constants.js +53 -9
- package/LocalApi/IAQSensor.js +123 -0
- package/LocalApi/System.js +36 -37
- package/LocalApi/Zone.js +163 -17
- package/README.md +17 -0
- package/Utils/asyncRequest.js +118 -75
- package/eslint.config.js +30 -0
- package/io-package.json +41 -6
- package/main.js +62 -41
- package/main.test.js +13 -13
- package/package.json +41 -42
- package/test-integration.js +144 -0
- package/test-standalone.js +114 -0
package/LocalApi/Zone.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
const Constants = require('./Constants');
|
|
2
4
|
|
|
3
5
|
class Zone {
|
|
4
6
|
constructor(adapter, localApi)
|
|
5
7
|
{
|
|
6
8
|
this.adapter = adapter;
|
|
7
|
-
this.localApi = localApi;
|
|
9
|
+
this.localApi = localApi;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
/**
|
|
@@ -14,8 +16,9 @@ class Zone {
|
|
|
14
16
|
this.id = parseInt(zoneData['zoneID']);
|
|
15
17
|
this.name = zoneData.hasOwnProperty('name') ? zoneData['name'] : '';
|
|
16
18
|
this.min_temp = zoneData['minTemp'];
|
|
17
|
-
this.max_temp = zoneData['maxTemp'];
|
|
18
|
-
|
|
19
|
+
this.max_temp = zoneData['maxTemp'];
|
|
20
|
+
this.isMaster = zoneData['master'] === 1;
|
|
21
|
+
|
|
19
22
|
this.path = path+'.Zone'+this.id;
|
|
20
23
|
await this.adapter.setObjectNotExistsAsync(this.path, {
|
|
21
24
|
type: 'device',
|
|
@@ -28,25 +31,62 @@ class Zone {
|
|
|
28
31
|
native: {},
|
|
29
32
|
});
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
const unitRaw = zoneData['units'];
|
|
35
|
+
const unitName = Constants.UNIT_CONVERTER[unitRaw]?.['name'] || 'Unknown';
|
|
36
|
+
const unitUnit = Constants.UNIT_CONVERTER[unitRaw]?.['unit'] || '°C';
|
|
34
37
|
|
|
35
38
|
await this.adapter.createPropertyAndInit(this.path, 'id', 'number', true, false, this.id, 'number');
|
|
36
|
-
|
|
39
|
+
// Name is writable so user can set custom zone names (API may not return names)
|
|
40
|
+
await this.adapter.createPropertyAndInit(this.path, 'name', 'string', true, true, this.name || `Zone ${this.id}`, 'text');
|
|
37
41
|
await this.adapter.createPropertyAndInit(this.path, 'min_temp', 'number', true, false, this.min_temp, 'value.min');
|
|
38
42
|
await this.adapter.createPropertyAndInit(this.path, 'max_temp', 'number', true, false, this.max_temp, 'value.max');
|
|
39
|
-
await this.adapter.createPropertyAndInit(this.path, 'unitRaw', '
|
|
43
|
+
await this.adapter.createPropertyAndInit(this.path, 'unitRaw', 'number', true, false, unitRaw, 'value');
|
|
40
44
|
await this.adapter.createPropertyAndInit(this.path, 'unitName', 'string', true, false, unitName, 'text');
|
|
41
45
|
await this.adapter.createPropertyAndInit(this.path, 'unit', 'string', true, false, unitUnit, 'text');
|
|
42
|
-
await this.adapter.
|
|
46
|
+
await this.adapter.createPropertyAndInit(this.path, 'master', 'boolean', true, false, this.isMaster, 'indicator');
|
|
47
|
+
await this.adapter.createProperty(this.path, 'is_on', 'boolean', true, true, 'switch.power');
|
|
43
48
|
await this.adapter.createUnitProperty(this.path, 'current_temperature', 'number', 0, 100, unitUnit, true, false, 'value.temperature');
|
|
44
49
|
await this.adapter.createUnitProperty(this.path, 'current_humidity', 'number', 0, 100, '%', true, false, 'value.humidity');
|
|
45
50
|
await this.adapter.createUnitProperty(this.path, 'target_temperature', 'number', this.min_temp, this.max_temp, unitUnit, true, true, 'value.temperature');
|
|
46
|
-
|
|
51
|
+
|
|
52
|
+
// Mode property
|
|
53
|
+
await this.adapter.createProperty(this.path, 'mode', 'number', true, true, 'level.mode.hvac');
|
|
54
|
+
await this.adapter.createProperty(this.path, 'modeName', 'string', true, false, 'text');
|
|
55
|
+
|
|
56
|
+
// Double setpoint support (if available)
|
|
57
|
+
if (zoneData.hasOwnProperty('setpoint_air_cool')) {
|
|
58
|
+
await this.adapter.createUnitProperty(this.path, 'setpoint_cool', 'number', this.min_temp, this.max_temp, unitUnit, true, true, 'value.temperature');
|
|
59
|
+
await this.adapter.createUnitProperty(this.path, 'setpoint_heat', 'number', this.min_temp, this.max_temp, unitUnit, true, true, 'value.temperature');
|
|
60
|
+
this.adapter.subscribeState(this.path+'.setpoint_cool', this, this.reactToSetpointCoolChange);
|
|
61
|
+
this.adapter.subscribeState(this.path+'.setpoint_heat', this, this.reactToSetpointHeatChange);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Fan speed (if available)
|
|
65
|
+
if (zoneData.hasOwnProperty('speed')) {
|
|
66
|
+
await this.adapter.createProperty(this.path, 'fan_speed', 'number', true, true, 'level');
|
|
67
|
+
await this.adapter.createProperty(this.path, 'fan_speed_name', 'string', true, false, 'text');
|
|
68
|
+
this.adapter.subscribeState(this.path+'.fan_speed', this, this.reactToFanSpeedChange);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Sleep timer (if available)
|
|
72
|
+
if (zoneData.hasOwnProperty('sleep')) {
|
|
73
|
+
await this.adapter.createProperty(this.path, 'sleep_timer', 'number', true, true, 'level.timer');
|
|
74
|
+
this.adapter.subscribeState(this.path+'.sleep_timer', this, this.reactToSleepTimerChange);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Air quality (if available)
|
|
78
|
+
if (zoneData.hasOwnProperty('air_quality')) {
|
|
79
|
+
await this.adapter.createProperty(this.path, 'air_quality', 'number', true, false, 'value');
|
|
80
|
+
await this.adapter.createProperty(this.path, 'air_quality_name', 'string', true, false, 'text');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Error indicators
|
|
84
|
+
await this.adapter.createProperty(this.path, 'errors', 'string', true, false, 'text');
|
|
85
|
+
|
|
47
86
|
// Register callbacks to react on value changes
|
|
48
87
|
this.adapter.subscribeState(this.path+'.target_temperature', this, this.reactToTargetTemperatureChange);
|
|
49
88
|
this.adapter.subscribeState(this.path+'.is_on', this, this.reactToIsOnChange);
|
|
89
|
+
this.adapter.subscribeState(this.path+'.mode', this, this.reactToModeChange);
|
|
50
90
|
|
|
51
91
|
await this.updateData(zoneData);
|
|
52
92
|
}
|
|
@@ -55,7 +95,12 @@ class Zone {
|
|
|
55
95
|
* Synchronized the zone data from airzone into the iobroker data points
|
|
56
96
|
*/
|
|
57
97
|
async updateData(zoneData)
|
|
58
|
-
{
|
|
98
|
+
{
|
|
99
|
+
// Only update name if API returns one (don't overwrite user-set names)
|
|
100
|
+
if (zoneData['name']) {
|
|
101
|
+
await this.adapter.updatePropertyValue(this.path, 'name', zoneData['name']);
|
|
102
|
+
}
|
|
103
|
+
|
|
59
104
|
this.current_temperature = zoneData['roomTemp'];
|
|
60
105
|
await this.adapter.updatePropertyValue(this.path, 'current_temperature', this.current_temperature);
|
|
61
106
|
|
|
@@ -65,15 +110,61 @@ class Zone {
|
|
|
65
110
|
this.target_temperature = zoneData['setpoint'];
|
|
66
111
|
await this.adapter.updatePropertyValue(this.path, 'target_temperature', this.target_temperature);
|
|
67
112
|
|
|
68
|
-
this.is_on = zoneData['on']
|
|
113
|
+
this.is_on = zoneData['on'] === 1;
|
|
69
114
|
await this.adapter.updatePropertyValue(this.path, 'is_on', this.is_on);
|
|
70
|
-
|
|
115
|
+
|
|
116
|
+
// Update mode
|
|
117
|
+
const mode = zoneData['mode'];
|
|
118
|
+
if (mode !== undefined) {
|
|
119
|
+
await this.adapter.updatePropertyValue(this.path, 'mode', mode);
|
|
120
|
+
const modeName = Constants.MODES_CONVERTER[String(mode)]?.['name'] || 'Unknown';
|
|
121
|
+
await this.adapter.updatePropertyValue(this.path, 'modeName', modeName);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Update double setpoint values (if available)
|
|
125
|
+
if (zoneData.hasOwnProperty('setpoint_air_cool')) {
|
|
126
|
+
await this.adapter.updatePropertyValue(this.path, 'setpoint_cool', zoneData['setpoint_air_cool']);
|
|
127
|
+
}
|
|
128
|
+
if (zoneData.hasOwnProperty('setpoint_air_heat')) {
|
|
129
|
+
await this.adapter.updatePropertyValue(this.path, 'setpoint_heat', zoneData['setpoint_air_heat']);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Update fan speed (if available)
|
|
133
|
+
if (zoneData.hasOwnProperty('speed')) {
|
|
134
|
+
const speed = zoneData['speed'];
|
|
135
|
+
await this.adapter.updatePropertyValue(this.path, 'fan_speed', speed);
|
|
136
|
+
const speedName = Constants.FAN_SPEED_CONVERTER[String(speed)]?.['name'] || 'Unknown';
|
|
137
|
+
await this.adapter.updatePropertyValue(this.path, 'fan_speed_name', speedName);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Update sleep timer (if available)
|
|
141
|
+
if (zoneData.hasOwnProperty('sleep')) {
|
|
142
|
+
await this.adapter.updatePropertyValue(this.path, 'sleep_timer', zoneData['sleep']);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Update air quality (if available)
|
|
146
|
+
if (zoneData.hasOwnProperty('air_quality')) {
|
|
147
|
+
const quality = zoneData['air_quality'];
|
|
148
|
+
await this.adapter.updatePropertyValue(this.path, 'air_quality', quality);
|
|
149
|
+
const qualityName = Constants.IAQ_SCORE_CONVERTER[String(quality)]?.['name'] || 'Unknown';
|
|
150
|
+
await this.adapter.updatePropertyValue(this.path, 'air_quality_name', qualityName);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Update errors (if present)
|
|
154
|
+
if (zoneData.hasOwnProperty('errors')) {
|
|
155
|
+
const errors = zoneData['errors'];
|
|
156
|
+
const errorStr = Array.isArray(errors) ? errors.join(', ') : String(errors);
|
|
157
|
+
await this.adapter.updatePropertyValue(this.path, 'errors', errorStr);
|
|
158
|
+
} else {
|
|
159
|
+
await this.adapter.updatePropertyValue(this.path, 'errors', '');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
71
162
|
|
|
72
163
|
/**
|
|
73
164
|
* Is called when the state of target_temperature was changed
|
|
74
165
|
*/
|
|
75
166
|
async reactToTargetTemperatureChange(self, id, state) {
|
|
76
|
-
|
|
167
|
+
let temperature = state.val;
|
|
77
168
|
if(self.min_temp != undefined && temperature < self.min_temp)
|
|
78
169
|
temperature = self.min_temp;
|
|
79
170
|
if(self.max_temp != undefined && temperature > self.max_temp)
|
|
@@ -85,18 +176,73 @@ class Zone {
|
|
|
85
176
|
/**
|
|
86
177
|
* Is called when the state of is_on was changed
|
|
87
178
|
*/
|
|
88
|
-
async reactToIsOnChange(self, id, state) {
|
|
179
|
+
async reactToIsOnChange(self, id, state) {
|
|
89
180
|
if(state.val)
|
|
90
181
|
await self.turn_on();
|
|
91
182
|
else
|
|
92
183
|
await self.turn_off();
|
|
93
184
|
}
|
|
94
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Is called when the mode was changed
|
|
188
|
+
*/
|
|
189
|
+
async reactToModeChange(self, id, state) {
|
|
190
|
+
await self.sendEvent('mode', state.val);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Is called when the cooling setpoint was changed
|
|
195
|
+
*/
|
|
196
|
+
async reactToSetpointCoolChange(self, id, state) {
|
|
197
|
+
let temperature = state.val;
|
|
198
|
+
if(self.min_temp != undefined && temperature < self.min_temp)
|
|
199
|
+
temperature = self.min_temp;
|
|
200
|
+
if(self.max_temp != undefined && temperature > self.max_temp)
|
|
201
|
+
temperature = self.max_temp;
|
|
202
|
+
await self.sendEvent('setpoint_air_cool', temperature);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Is called when the heating setpoint was changed
|
|
207
|
+
*/
|
|
208
|
+
async reactToSetpointHeatChange(self, id, state) {
|
|
209
|
+
let temperature = state.val;
|
|
210
|
+
if(self.min_temp != undefined && temperature < self.min_temp)
|
|
211
|
+
temperature = self.min_temp;
|
|
212
|
+
if(self.max_temp != undefined && temperature > self.max_temp)
|
|
213
|
+
temperature = self.max_temp;
|
|
214
|
+
await self.sendEvent('setpoint_air_heat', temperature);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Is called when the fan speed was changed
|
|
219
|
+
*/
|
|
220
|
+
async reactToFanSpeedChange(self, id, state) {
|
|
221
|
+
const speed = Math.max(0, Math.min(7, Math.round(state.val)));
|
|
222
|
+
await self.sendEvent('speed', speed);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Is called when the sleep timer was changed
|
|
227
|
+
*/
|
|
228
|
+
async reactToSleepTimerChange(self, id, state) {
|
|
229
|
+
// Sleep timer values: 0 = off, 30, 60, 90 minutes
|
|
230
|
+
const validValues = [0, 30, 60, 90];
|
|
231
|
+
let value = state.val;
|
|
232
|
+
if (!validValues.includes(value)) {
|
|
233
|
+
// Find the closest valid value
|
|
234
|
+
value = validValues.reduce((prev, curr) =>
|
|
235
|
+
Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
await self.sendEvent('sleep', value);
|
|
239
|
+
}
|
|
240
|
+
|
|
95
241
|
/**
|
|
96
242
|
* Send event to the airzone cloud
|
|
97
243
|
*/
|
|
98
244
|
async sendEvent(option, value) {
|
|
99
|
-
await this.localApi.sendUpdate(this.id, option, value)
|
|
245
|
+
await this.localApi.sendUpdate(this.id, option, value);
|
|
100
246
|
}
|
|
101
247
|
|
|
102
248
|
/**
|
|
@@ -109,7 +255,7 @@ class Zone {
|
|
|
109
255
|
/**
|
|
110
256
|
* Turn zone off
|
|
111
257
|
*/
|
|
112
|
-
|
|
258
|
+
async turn_off() {
|
|
113
259
|
await this.sendEvent('on', 0);
|
|
114
260
|
}
|
|
115
261
|
}
|
package/README.md
CHANGED
|
@@ -11,6 +11,23 @@ Control and monitor airzone devices with ioBroker.
|
|
|
11
11
|
[](https://travis-ci.com/github/SilentPhoenix11/ioBroker.airzone)
|
|
12
12
|
|
|
13
13
|
## Changelog
|
|
14
|
+
### 3.0.0
|
|
15
|
+
* (SilentPhoenix11) **BREAKING**: Requires Node.js >= 18 and js-controller >= 5.0.0
|
|
16
|
+
* (SilentPhoenix11) Migrated from deprecated `request` to `axios` HTTP client
|
|
17
|
+
* (SilentPhoenix11) Updated to @iobroker/adapter-core 3.x
|
|
18
|
+
* (SilentPhoenix11) Added `info.connection` state for connection status
|
|
19
|
+
* (SilentPhoenix11) Added support for fan speed control (`fanSpeed`, `fanSpeedRaw`)
|
|
20
|
+
* (SilentPhoenix11) Added sleep timer support (`sleepTime`)
|
|
21
|
+
* (SilentPhoenix11) Added double setpoint support (`setpointCool`, `setpointHeat`)
|
|
22
|
+
* (SilentPhoenix11) Added zone mode control (`mode`, `modeRaw`)
|
|
23
|
+
* (SilentPhoenix11) Added zone error tracking (`errors`)
|
|
24
|
+
* (SilentPhoenix11) Zone names are now editable
|
|
25
|
+
* (SilentPhoenix11) Added IAQ sensor support (CO2, PM2.5, PM10, TVOC)
|
|
26
|
+
* (SilentPhoenix11) Added API version and webserver info endpoints
|
|
27
|
+
* (SilentPhoenix11) Fixed HTTP parser issue with Airzone devices (non-standard LF line endings)
|
|
28
|
+
* (SilentPhoenix11) Uses adapter timers instead of global setTimeout
|
|
29
|
+
* (SilentPhoenix11) Migrated to ESLint 9 flat config
|
|
30
|
+
|
|
14
31
|
### 2.0.3
|
|
15
32
|
* (SilentPhoenix11) Small fixes
|
|
16
33
|
|
package/Utils/asyncRequest.js
CHANGED
|
@@ -1,94 +1,137 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const axios = require('axios');
|
|
4
|
+
|
|
5
|
+
// Create axios instance with default config for Airzone API
|
|
6
|
+
const axiosInstance = axios.create({
|
|
7
|
+
timeout: 10000,
|
|
8
|
+
headers: {
|
|
9
|
+
'Content-Type': 'application/json'
|
|
10
|
+
}
|
|
11
|
+
});
|
|
4
12
|
|
|
5
13
|
class AsyncRequest {
|
|
6
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Perform a POST request to the Airzone API
|
|
17
|
+
* @param {string} url - The URL to send the request to
|
|
18
|
+
* @param {object} data - The data to send (will be serialized to JSON)
|
|
19
|
+
* @returns {Promise<object>} - Response object with statusCode and body/errors
|
|
20
|
+
*/
|
|
7
21
|
static async jsonPostRequest(url, data) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
uri: url,
|
|
12
|
-
headers: {
|
|
13
|
-
'Content-Type': 'application/json'
|
|
14
|
-
},
|
|
15
|
-
body: data
|
|
16
|
-
});
|
|
22
|
+
try {
|
|
23
|
+
// Parse data if it's a string
|
|
24
|
+
const requestData = typeof data === 'string' ? JSON.parse(data) : data;
|
|
17
25
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
var errorMsg = JSON.parse(body)["errors"];
|
|
28
|
-
if(errorMsg)
|
|
29
|
-
result = JSON.stringify({statusCode:response.statusCode,errors:errorMsg});
|
|
30
|
-
else
|
|
31
|
-
result = JSON.stringify({statusCode:response.statusCode,body:response.body});
|
|
32
|
-
}
|
|
26
|
+
const response = await axiosInstance.post(url, requestData);
|
|
27
|
+
|
|
28
|
+
// Check for API-level errors in response
|
|
29
|
+
if (response.data && response.data.errors) {
|
|
30
|
+
return {
|
|
31
|
+
statusCode: response.status,
|
|
32
|
+
errors: response.data.errors
|
|
33
|
+
};
|
|
34
|
+
}
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
return {
|
|
37
|
+
statusCode: response.status,
|
|
38
|
+
body: JSON.stringify(response.data)
|
|
39
|
+
};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error.response) {
|
|
42
|
+
// Server responded with error status
|
|
43
|
+
const errorData = error.response.data;
|
|
44
|
+
return {
|
|
45
|
+
statusCode: error.response.status,
|
|
46
|
+
errors: errorData?.errors || errorData?.message || error.message
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// Network error or timeout
|
|
50
|
+
return {
|
|
51
|
+
statusCode: 0,
|
|
52
|
+
errors: error.message
|
|
53
|
+
};
|
|
54
|
+
}
|
|
35
55
|
}
|
|
36
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Perform a PUT request to the Airzone API
|
|
59
|
+
* @param {string} url - The URL to send the request to
|
|
60
|
+
* @param {object} data - The data to send (will be serialized to JSON)
|
|
61
|
+
* @returns {Promise<object>} - Response object with statusCode and body/errors
|
|
62
|
+
*/
|
|
37
63
|
static async jsonPutRequest(url, data) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
uri: url,
|
|
42
|
-
headers: {
|
|
43
|
-
'Content-Type': 'application/json'
|
|
44
|
-
},
|
|
45
|
-
body: data
|
|
46
|
-
});
|
|
64
|
+
try {
|
|
65
|
+
// Parse data if it's a string
|
|
66
|
+
const requestData = typeof data === 'string' ? JSON.parse(data) : data;
|
|
47
67
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if(response.error)
|
|
51
|
-
{
|
|
52
|
-
result = JSON.stringify({statusCode:response.statusCode,errors:error});
|
|
53
|
-
}
|
|
54
|
-
else
|
|
55
|
-
{
|
|
56
|
-
var body = response.body;
|
|
57
|
-
var errorMsg = JSON.parse(body)["errors"];
|
|
58
|
-
if(errorMsg)
|
|
59
|
-
result = JSON.stringify({statusCode:response.statusCode,errors:errorMsg});
|
|
60
|
-
else
|
|
61
|
-
result = JSON.stringify({statusCode:response.statusCode,body:response.body});
|
|
62
|
-
}
|
|
68
|
+
const response = await axiosInstance.put(url, requestData);
|
|
63
69
|
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
// Check for API-level errors in response
|
|
71
|
+
if (response.data && response.data.errors) {
|
|
72
|
+
return {
|
|
73
|
+
statusCode: response.status,
|
|
74
|
+
errors: response.data.errors
|
|
75
|
+
};
|
|
76
|
+
}
|
|
66
77
|
|
|
78
|
+
return {
|
|
79
|
+
statusCode: response.status,
|
|
80
|
+
body: JSON.stringify(response.data)
|
|
81
|
+
};
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error.response) {
|
|
84
|
+
// Server responded with error status
|
|
85
|
+
const errorData = error.response.data;
|
|
86
|
+
return {
|
|
87
|
+
statusCode: error.response.status,
|
|
88
|
+
errors: errorData?.errors || errorData?.message || error.message
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// Network error or timeout
|
|
92
|
+
return {
|
|
93
|
+
statusCode: 0,
|
|
94
|
+
errors: error.message
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
67
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Perform a GET request to the Airzone API
|
|
101
|
+
* @param {string} url - The URL to send the request to
|
|
102
|
+
* @returns {Promise<object>} - Response object with statusCode and body/errors
|
|
103
|
+
*/
|
|
68
104
|
static async jsonGetRequest(url) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
method: 'GET',
|
|
72
|
-
uri: url
|
|
73
|
-
});
|
|
105
|
+
try {
|
|
106
|
+
const response = await axiosInstance.get(url);
|
|
74
107
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
{
|
|
83
|
-
var body = response.body;
|
|
84
|
-
var errorMsg = JSON.parse(body)["errors"];
|
|
85
|
-
if(errorMsg)
|
|
86
|
-
result = JSON.stringify({statusCode:response.statusCode,errors:errorMsg});
|
|
87
|
-
else
|
|
88
|
-
result = JSON.stringify({statusCode:response.statusCode,body:response.body});
|
|
89
|
-
}
|
|
108
|
+
// Check for API-level errors in response
|
|
109
|
+
if (response.data && response.data.errors) {
|
|
110
|
+
return {
|
|
111
|
+
statusCode: response.status,
|
|
112
|
+
errors: response.data.errors
|
|
113
|
+
};
|
|
114
|
+
}
|
|
90
115
|
|
|
91
|
-
|
|
116
|
+
return {
|
|
117
|
+
statusCode: response.status,
|
|
118
|
+
body: JSON.stringify(response.data)
|
|
119
|
+
};
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error.response) {
|
|
122
|
+
// Server responded with error status
|
|
123
|
+
const errorData = error.response.data;
|
|
124
|
+
return {
|
|
125
|
+
statusCode: error.response.status,
|
|
126
|
+
errors: errorData?.errors || errorData?.message || error.message
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// Network error or timeout
|
|
130
|
+
return {
|
|
131
|
+
statusCode: 0,
|
|
132
|
+
errors: error.message
|
|
133
|
+
};
|
|
134
|
+
}
|
|
92
135
|
}
|
|
93
136
|
}
|
|
94
137
|
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const js = require('@eslint/js');
|
|
2
|
+
const globals = require('globals');
|
|
3
|
+
|
|
4
|
+
module.exports = [
|
|
5
|
+
js.configs.recommended,
|
|
6
|
+
{
|
|
7
|
+
languageOptions: {
|
|
8
|
+
ecmaVersion: 2022,
|
|
9
|
+
sourceType: 'commonjs',
|
|
10
|
+
globals: {
|
|
11
|
+
...globals.node,
|
|
12
|
+
...globals.mocha
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
rules: {
|
|
16
|
+
'indent': ['error', 4, { 'SwitchCase': 1 }],
|
|
17
|
+
'no-console': 'off',
|
|
18
|
+
'no-var': 'error',
|
|
19
|
+
'prefer-const': 'error',
|
|
20
|
+
'no-trailing-spaces': 'error',
|
|
21
|
+
'quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': true }],
|
|
22
|
+
'semi': ['error', 'always'],
|
|
23
|
+
'no-prototype-builtins': 'off',
|
|
24
|
+
'no-unused-vars': ['error', { 'argsIgnorePattern': '^_', 'varsIgnorePattern': '^_', 'caughtErrorsIgnorePattern': '^_' }]
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
ignores: ['node_modules/**', '.git/**', 'gulpfile.js', 'admin/words.js']
|
|
29
|
+
}
|
|
30
|
+
];
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common":{
|
|
3
3
|
"name":"airzone",
|
|
4
|
-
"version":"
|
|
4
|
+
"version":"3.0.0",
|
|
5
5
|
"news":{
|
|
6
|
+
"3.0.0":{
|
|
7
|
+
"en": "Migrated from request to axios, Node.js 18+, added info.connection, extended API support",
|
|
8
|
+
"de": "Migration von request zu axios, Node.js 18+, info.connection hinzugefügt, erweiterte API-Unterstützung",
|
|
9
|
+
"ru": "Переход с request на axios, Node.js 18+, добавлен info.connection, расширенная поддержка API",
|
|
10
|
+
"pt": "Migração de request para axios, Node.js 18+, info.connection adicionado, suporte estendido à API",
|
|
11
|
+
"nl": "Migratie van request naar axios, Node.js 18+, info.connection toegevoegd, uitgebreide API-ondersteuning",
|
|
12
|
+
"fr": "Migration de request vers axios, Node.js 18+, info.connection ajouté, support API étendu",
|
|
13
|
+
"it": "Migrazione da request ad axios, Node.js 18+, aggiunto info.connection, supporto API esteso",
|
|
14
|
+
"es": "Migración de request a axios, Node.js 18+, info.connection añadido, soporte API extendido",
|
|
15
|
+
"pl": "Migracja z request do axios, Node.js 18+, dodano info.connection, rozszerzona obsługa API",
|
|
16
|
+
"zh-cn": "从request迁移到axios,Node.js 18+,添加info.connection,扩展API支持"
|
|
17
|
+
},
|
|
6
18
|
"1.0.0":{
|
|
7
19
|
"en": "initial release",
|
|
8
20
|
"de": "Erstveröffentlichung",
|
|
@@ -156,13 +168,14 @@
|
|
|
156
168
|
"loglevel":"info",
|
|
157
169
|
"mode":"daemon",
|
|
158
170
|
"type":"climate-control",
|
|
159
|
-
|
|
171
|
+
"compact":false,
|
|
160
172
|
"connectionType":"local",
|
|
161
|
-
|
|
162
|
-
"materialize":true,
|
|
173
|
+
"dataSource":"poll",
|
|
174
|
+
"materialize":true,
|
|
175
|
+
"nodeProcessParams": ["--insecure-http-parser"],
|
|
163
176
|
"dependencies":[
|
|
164
177
|
{
|
|
165
|
-
"js-controller":">=
|
|
178
|
+
"js-controller":">=5.0.0"
|
|
166
179
|
}
|
|
167
180
|
]
|
|
168
181
|
},
|
|
@@ -174,5 +187,27 @@
|
|
|
174
187
|
"encryptedNative": [],
|
|
175
188
|
"protectedNative": [],
|
|
176
189
|
"objects":[],
|
|
177
|
-
"instanceObjects":[
|
|
190
|
+
"instanceObjects":[
|
|
191
|
+
{
|
|
192
|
+
"_id": "info",
|
|
193
|
+
"type": "channel",
|
|
194
|
+
"common": {
|
|
195
|
+
"name": "Information"
|
|
196
|
+
},
|
|
197
|
+
"native": {}
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"_id": "info.connection",
|
|
201
|
+
"type": "state",
|
|
202
|
+
"common": {
|
|
203
|
+
"role": "indicator.connected",
|
|
204
|
+
"name": "Device or service connected",
|
|
205
|
+
"type": "boolean",
|
|
206
|
+
"read": true,
|
|
207
|
+
"write": false,
|
|
208
|
+
"def": false
|
|
209
|
+
},
|
|
210
|
+
"native": {}
|
|
211
|
+
}
|
|
212
|
+
]
|
|
178
213
|
}
|