iobroker.airzone 1.0.1 → 2.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/Airzone.png CHANGED
Binary file
@@ -0,0 +1,78 @@
1
+ 'use strict';
2
+
3
+ const AsyncRequest = require('../Utils/asyncRequest');
4
+ const System = require('./System')
5
+
6
+ // Allow to connect to Airzone local API
7
+
8
+ let log;
9
+ let adapter;
10
+ class AirzoneLocalApi {
11
+ constructor(a, local_ip)
12
+ {
13
+ adapter = a;
14
+ log = a.log;
15
+ this.local_ip = local_ip;
16
+ }
17
+
18
+ async init(system_id) {
19
+
20
+ this.system = new System(adapter, this, system_id);
21
+ await this.system.init();
22
+ }
23
+
24
+ async update() {
25
+ if(this.system == undefined)
26
+ return;
27
+
28
+ await this.system.update();
29
+ }
30
+
31
+ logInfo(msg) {
32
+ log.info(msg);
33
+ }
34
+
35
+ logError(msg) {
36
+ log.error(msg);
37
+ }
38
+
39
+ async getZoneState() {
40
+ var url = "http://"+this.local_ip+":3000/api/v1/hvac";
41
+ const data = JSON.stringify({"systemID":this.system?.id, "ZoneID":0});
42
+ var response = await AsyncRequest.jsonPostRequest(url, data);
43
+
44
+ var errors = response["errors"];
45
+ if(errors)
46
+ {
47
+ this.logError("Failed to get zone state: (statusCode: "+response["statusCode"]+") - "+response["errors"]);
48
+ return undefined;
49
+ }
50
+ var body = response["body"];
51
+ var zones = JSON.parse(body)["data"];
52
+
53
+ return zones;
54
+ }
55
+
56
+ async sendUpdate(zoneid, key, value)
57
+ {
58
+ try
59
+ {
60
+ var url = "http://"+this.local_ip+":3000/api/v1/hvac";
61
+ var payload = '{\"systemID\":'+this.system?.id+', \"ZoneID\":'+zoneid+', \"'+key+'\":'+value+'}';
62
+ var response = await AsyncRequest.jsonPutRequest(url, payload);
63
+ var errors = response["errors"];
64
+ if(errors)
65
+ {
66
+ this.logError("Failed to update '"+key+"' with value '"+value+"': (statusCode: "+response["statusCode"]+") - "+response["errors"]);
67
+ return undefined;
68
+ }
69
+ var body = response["body"];
70
+ var responseData = JSON.parse(body)["data"];
71
+ return responseData.hasOwnProperty(key);
72
+ }
73
+ catch (e) {
74
+ this.logError('error during sendUpdate '+e+'\r\n'+e.stack);
75
+ }
76
+ }
77
+ }
78
+ module.exports = AirzoneLocalApi;
@@ -0,0 +1,14 @@
1
+ module.exports = Object.freeze({
2
+ MODES_CONVERTER : {
3
+ "1": {"name": "Stop"},
4
+ "2": {"name": "Cooling"},
5
+ "3": {"name": "Heating"},
6
+ "4": {"name": "Fan"},
7
+ "5": {"name": "Dry"}
8
+ },
9
+
10
+ UNIT_CONVERTER : {
11
+ "0": {"name": "Celsius", "unit": "°C"},
12
+ "1": {"name": "Fahrenheit", "unit": "°F"}
13
+ },
14
+ });
@@ -0,0 +1,150 @@
1
+ const Zone = require('./Zone')
2
+ const AsyncRequest = require('../Utils/asyncRequest');
3
+ const Constants = require('./Constants');
4
+
5
+ class System {
6
+ constructor(adapter, localApi, id)
7
+ {
8
+ this.adapter = adapter;
9
+ this.localApi = localApi;
10
+ this.id = id;
11
+ }
12
+
13
+ /**
14
+ * Initialize the system with the data from the airzone local api
15
+ */
16
+ async init() {
17
+ this.path = "System"+this.id;
18
+ await this.adapter.setObjectNotExistsAsync(this.path, {
19
+ type: 'device',
20
+ common: {
21
+ name: this.path,
22
+ type: 'object',
23
+ read: true,
24
+ write: false,
25
+ },
26
+ native: {},
27
+ });
28
+
29
+ await this.adapter.createProperty(this.path, 'mode_raw', 'string', true, true, 'state');
30
+ await this.adapter.createProperty(this.path, 'mode', 'string', true, false, 'text');
31
+
32
+ this.adapter.subscribeState(this.path+'.mode_raw', this, this.reactToModeRawChange);
33
+
34
+ var masterZone = await this.load_zones(this.path);
35
+ if(masterZone == undefined)
36
+ return false;
37
+
38
+ await this.updateData(masterZone);
39
+
40
+ return true;
41
+ }
42
+
43
+ /**
44
+ * Synchronized the system data from airzone into the iobroker data points
45
+ */
46
+ async updateData(masterZoneData)
47
+ {
48
+ if(masterZoneData == undefined)
49
+ {
50
+ this.localApi.logError("Missing master Zone");
51
+ return;
52
+ }
53
+
54
+ this.mode_raw = masterZoneData["mode"];
55
+ await this.adapter.updatePropertyValue(this.path, 'mode_raw', this.mode_raw);
56
+ this.mode = Constants.MODES_CONVERTER[this.mode_raw]["name"];
57
+ await this.adapter.updatePropertyValue(this.path, 'mode', this.mode);
58
+ }
59
+
60
+ /**
61
+ * Synchronized the system data from airzone into the iobroker data points and call update for all sub zones
62
+ */
63
+ async update() {
64
+ var masterZoneData = await this.update_zones();
65
+
66
+ await this.updateData(masterZoneData);
67
+ }
68
+
69
+ /**
70
+ * Load and initialize the zones of this system from airzone local api
71
+ */
72
+ async load_zones(path) {
73
+
74
+ var zones_relations = await this.localApi.getZoneState();
75
+ if(zones_relations == undefined)
76
+ return undefined;
77
+
78
+ var masterZoneData = undefined;
79
+ this.zones = [];
80
+ for (let index = 0; index < zones_relations.length; index++) {
81
+ var zoneData = zones_relations[index];
82
+ var zone = new Zone(this.adapter, this.localApi);
83
+ await zone.init(path, zoneData)
84
+ this.zones[index] = zone;
85
+
86
+ if(zoneData.hasOwnProperty("mode"))
87
+ {
88
+ masterZoneData = zoneData;
89
+ this.masterZoneId = this.zones[index].id;
90
+ }
91
+ }
92
+
93
+ return masterZoneData;
94
+ }
95
+
96
+ /**
97
+ * Update zones with the current zone data from airzone local api
98
+ */
99
+ async update_zones() {
100
+
101
+ var zones_relations = await this.localApi.getZoneState();
102
+ if(zones_relations == undefined)
103
+ return undefined;
104
+
105
+ if(this.zones == undefined)
106
+ return undefined;
107
+
108
+ var masterZoneData = undefined;
109
+
110
+ for (let index = 0; index < zones_relations.length; index++) {
111
+ var zoneData = zones_relations[index];
112
+ var zId = zoneData["zoneID"];
113
+
114
+ if(zoneData.hasOwnProperty("mode"))
115
+ masterZoneData = zoneData;
116
+
117
+ for(let i = 0;i<this.zones.length;i++) {
118
+ if(this.zones[i].id == zId) {
119
+ await this.zones[i].updateData(zoneData);
120
+ break;
121
+ }
122
+ }
123
+ }
124
+
125
+ return masterZoneData;
126
+ }
127
+
128
+ /**
129
+ * Is called when the state of mode was changed
130
+ */
131
+ async reactToModeRawChange(self, id, state) {
132
+
133
+ if(state.val == 0)
134
+ {
135
+ self.zones.forEach(zone => {
136
+ zone.turn_off();
137
+ });
138
+ }
139
+
140
+ self.sendEvent('mode', state.val);
141
+ }
142
+
143
+ /**
144
+ * Send event to the airzone local api
145
+ */
146
+ async sendEvent(option, value) {
147
+ await this.localApi.sendUpdate(this.masterZoneId, option, value)
148
+ }
149
+ }
150
+ module.exports = System;
@@ -0,0 +1,116 @@
1
+ const Constants = require('./Constants');
2
+
3
+ class Zone {
4
+ constructor(adapter, localApi)
5
+ {
6
+ this.adapter = adapter;
7
+ this.localApi = localApi;
8
+ }
9
+
10
+ /**
11
+ * Initialize the zone with the data from the airzone local api
12
+ */
13
+ async init(path, zoneData) {
14
+ this.id = parseInt(zoneData['zoneID']);
15
+ this.name = zoneData.hasOwnProperty('name') ? zoneData['name'] : '';
16
+ this.min_temp = zoneData['minTemp'];
17
+ this.max_temp = zoneData['maxTemp'];
18
+
19
+ this.path = path+'.Zone'+this.id;
20
+ await this.adapter.setObjectNotExistsAsync(this.path, {
21
+ type: 'device',
22
+ common: {
23
+ name: 'Zone_'+this.id,
24
+ type: 'object',
25
+ read: true,
26
+ write: false,
27
+ },
28
+ native: {},
29
+ });
30
+
31
+ var unitRaw = zoneData['units'];
32
+ var unitName = Constants.UNIT_CONVERTER[unitRaw]["name"];
33
+ var unitUnit = Constants.UNIT_CONVERTER[unitRaw]["unit"];
34
+
35
+ await this.adapter.createPropertyAndInit(this.path, 'id', 'number', true, false, this.id, 'number');
36
+ await this.adapter.createPropertyAndInit(this.path, 'name', 'string', true, false, this.name, 'text');
37
+ await this.adapter.createPropertyAndInit(this.path, 'min_temp', 'number', true, false, this.min_temp, 'value.min');
38
+ await this.adapter.createPropertyAndInit(this.path, 'max_temp', 'number', true, false, this.max_temp, 'value.max');
39
+ await this.adapter.createPropertyAndInit(this.path, 'unitRaw', 'string', true, false, unitRaw, 'number');
40
+ await this.adapter.createPropertyAndInit(this.path, 'unitName', 'string', true, false, unitName, 'text');
41
+ await this.adapter.createPropertyAndInit(this.path, 'unit', 'string', true, false, unitUnit, 'text');
42
+ await this.adapter.createProperty(this.path, 'is_on', 'boolean', true, false, 'switch.power');
43
+ await this.adapter.createUnitProperty(this.path, 'current_temperature', 'number', 0, 100, unitUnit, true, false, 'value.temperature');
44
+ await this.adapter.createUnitProperty(this.path, 'current_humidity', 'number', 0, 100, '%', true, false, 'value.humidity');
45
+ await this.adapter.createUnitProperty(this.path, 'target_temperature', 'number', this.min_temp, this.max_temp, unitUnit, true, true, 'state');
46
+
47
+ // Register callbacks to react on value changes
48
+ this.adapter.subscribeState(this.path+'.target_temperature', this, this.reactToTargetTemperatureChange);
49
+ this.adapter.subscribeState(this.path+'.is_on', this, this.reactToIsOnChange);
50
+
51
+ await this.updateData(zoneData);
52
+ }
53
+
54
+ /**
55
+ * Synchronized the zone data from airzone into the iobroker data points
56
+ */
57
+ async updateData(zoneData)
58
+ {
59
+ this.current_temperature = zoneData['roomTemp'];
60
+ await this.adapter.updatePropertyValue(this.path, 'current_temperature', this.current_temperature);
61
+
62
+ this.current_humidity = zoneData['humidity'];
63
+ await this.adapter.updatePropertyValue(this.path, 'current_humidity', this.current_humidity);
64
+
65
+ this.target_temperature = zoneData['setpoint'];
66
+ await this.adapter.updatePropertyValue(this.path, 'target_temperature', this.target_temperature);
67
+
68
+ this.is_on = zoneData['on'] == '1';
69
+ await this.adapter.updatePropertyValue(this.path, 'is_on', this.is_on);
70
+ }
71
+
72
+ /**
73
+ * Is called when the state of target_temperature was changed
74
+ */
75
+ async reactToTargetTemperatureChange(self, id, state) {
76
+ var temperature = state.val;
77
+ if(self.min_temp != undefined && temperature < self.min_temp)
78
+ temperature = self.min_temp;
79
+ if(self.max_temp != undefined && temperature > self.max_temp)
80
+ temperature = self.max_temp;
81
+
82
+ await self.sendEvent('setpoint', temperature);
83
+ }
84
+
85
+ /**
86
+ * Is called when the state of is_on was changed
87
+ */
88
+ async reactToIsOnChange(self, id, state) {
89
+ if(state.val)
90
+ await self.turn_on();
91
+ else
92
+ await self.turn_off();
93
+ }
94
+
95
+ /**
96
+ * Send event to the airzone cloud
97
+ */
98
+ async sendEvent(option, value) {
99
+ await this.localApi.sendUpdate(this.id, option, value)
100
+ }
101
+
102
+ /**
103
+ * Turn zone on
104
+ */
105
+ async turn_on() {
106
+ await this.sendEvent('on', 1);
107
+ }
108
+
109
+ /**
110
+ * Turn zone off
111
+ */
112
+ async turn_off() {
113
+ await this.sendEvent('on', 0);
114
+ }
115
+ }
116
+ module.exports = Zone;
package/README.md CHANGED
@@ -16,6 +16,18 @@ Control and monitor airzone devices with ioBroker.
16
16
  **Tests:**
17
17
 
18
18
  ## Changelog
19
+ ### 2.0.0
20
+ * (SilentPhoenix11) Using the local API instead of cloud web service.
21
+
22
+ ### 1.0.4
23
+ * (SilentPhoenix11) Small fixes
24
+
25
+ ### 1.0.3
26
+ * (SilentPhoenix11) Fix of "SyntaxError: Unexpected end of JSON input" when calling "askSystemInfo"
27
+
28
+ ### 1.0.2
29
+ * (SilentPhoenix11) Corrections after code review by Apollon77
30
+
19
31
  ### 1.0.1
20
32
  * (SilentPhoenix11) Various corrections in the project structure
21
33
 
@@ -23,7 +23,8 @@ class AsyncRequest {
23
23
  }
24
24
  else
25
25
  {
26
- var errorMsg = JSON.parse(response.body)["errors"];
26
+ var body = response.body;
27
+ var errorMsg = JSON.parse(body)["errors"];
27
28
  if(errorMsg)
28
29
  result = JSON.stringify({statusCode:response.statusCode,errors:errorMsg});
29
30
  else
@@ -33,6 +34,37 @@ class AsyncRequest {
33
34
  return JSON.parse(result);
34
35
  }
35
36
 
37
+ static async jsonPutRequest(url, data) {
38
+
39
+ const response = await asyncRequest({
40
+ method: 'PUT',
41
+ uri: url,
42
+ headers: {
43
+ 'Content-Type': 'application/json'
44
+ },
45
+ body: data
46
+ });
47
+
48
+ var result;
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
+ }
63
+
64
+ return JSON.parse(result);
65
+ }
66
+
67
+
36
68
  static async jsonGetRequest(url) {
37
69
 
38
70
  const response = await asyncRequest({
@@ -48,7 +80,8 @@ class AsyncRequest {
48
80
  }
49
81
  else
50
82
  {
51
- var errorMsg = JSON.parse(response.body)["errors"];
83
+ var body = response.body;
84
+ var errorMsg = JSON.parse(body)["errors"];
52
85
  if(errorMsg)
53
86
  result = JSON.stringify({statusCode:response.statusCode,errors:errorMsg});
54
87
  else
package/admin/Airzone.png CHANGED
Binary file
Binary file
@@ -1,105 +1,98 @@
1
- <html>
2
-
3
- <head>
4
-
5
- <!-- Load ioBroker scripts and styles-->
6
- <link rel="stylesheet" type="text/css" href="../../css/adapter.css" />
7
- <link rel="stylesheet" type="text/css" href="../../lib/css/materialize.css">
8
-
9
- <script type="text/javascript" src="../../lib/js/jquery-3.2.1.min.js"></script>
10
- <script type="text/javascript" src="../../socket.io/socket.io.js"></script>
11
-
12
- <script type="text/javascript" src="../../js/translate.js"></script>
13
- <script type="text/javascript" src="../../lib/js/materialize.js"></script>
14
- <script type="text/javascript" src="../../js/adapter-settings.js"></script>
15
-
16
- <!-- Load our own files -->
17
- <link rel="stylesheet" type="text/css" href="style.css" />
18
- <script type="text/javascript" src="words.js"></script>
19
-
20
- <script type="text/javascript">
21
- // This will be called by the admin adapter when the settings page loads
22
- function load(settings, onChange) {
23
- // example: select elements with id=key and class=value and insert value
24
- if (!settings) return;
25
- $('.value').each(function () {
26
- var $key = $(this);
27
- var id = $key.attr('id');
28
- if ($key.attr('type') === 'checkbox') {
29
- // do not call onChange direct, because onChange could expect some arguments
30
- $key.prop('checked', settings[id])
31
- .on('change', () => onChange())
32
- ;
33
- } else {
34
- // do not call onChange direct, because onChange could expect some arguments
35
- $key.val(settings[id])
36
- .on('change', () => onChange())
37
- .on('keyup', () => onChange())
38
- ;
39
- }
40
- });
41
- onChange(false);
42
- // reinitialize all the Materialize labels on the page if you are dynamically adding inputs:
43
- if (M) M.updateTextFields();
44
- }
45
-
46
- // This will be called by the admin adapter when the user presses the save button
47
- function save(callback) {
48
- // example: select elements with class=value and build settings object
49
- var obj = {};
50
- $('.value').each(function () {
51
- var $this = $(this);
52
- if ($this.attr('type') === 'checkbox') {
53
- obj[$this.attr('id')] = $this.prop('checked');
54
- } else {
55
- obj[$this.attr('id')] = $this.val();
56
- }
57
- });
58
- callback(obj);
59
- }
60
- </script>
61
-
62
- </head>
63
-
64
- <body>
65
-
66
- <div class="m adapter-container">
67
-
68
- <div class="row">
69
- <div class="col s12 m4 l2">
70
- <img src="Airzone.png" class="logo" style="width: 150px;">
71
- </div>
72
- </div>
73
-
74
- <!-- Put your content here -->
75
-
76
- <!-- For example columns with settings: -->
77
- <div class="row">
78
- <div class="col s6 input-field">
79
- <input type="text" class="value" id="username"/>
80
- <label for="username">Username</label>
81
- </div>
82
-
83
- <div class="col s6 input-field">
84
- <input type="password" class="value" id="password" />
85
- <label for="password">Password</label>
86
- </div>
87
- </div>
88
- <div class="row">
89
- <div class="col s6 input-field">
90
- <input type="text" class="value" id="base_url" />
91
- <label for="base_url">Base url</label><span class="helper-text translate">s (default is https://airzonecloud.com)</span>
92
- </div>
93
-
94
- <div class="input-field col s12 m4 l3">
95
- <input placeholder="12" name="synchronization" type="number" id="sync_time" class="value validate" />
96
- <label data-error="numbers only" data-success="ok" class="translate active" for="sync_time">Synctime</label><span class="helper-text translate">s (default is 12)</span>
97
- </div>
98
-
99
- </div>
100
-
101
- </div>
102
-
103
- </body>
104
-
1
+ <html>
2
+
3
+ <head>
4
+
5
+ <!-- Load ioBroker scripts and styles-->
6
+ <link rel="stylesheet" type="text/css" href="../../css/adapter.css" />
7
+ <link rel="stylesheet" type="text/css" href="../../lib/css/materialize.css">
8
+
9
+ <script type="text/javascript" src="../../lib/js/jquery-3.2.1.min.js"></script>
10
+ <script type="text/javascript" src="../../socket.io/socket.io.js"></script>
11
+
12
+ <script type="text/javascript" src="../../js/translate.js"></script>
13
+ <script type="text/javascript" src="../../lib/js/materialize.js"></script>
14
+ <script type="text/javascript" src="../../js/adapter-settings.js"></script>
15
+
16
+ <!-- Load our own files -->
17
+ <link rel="stylesheet" type="text/css" href="style.css" />
18
+ <script type="text/javascript" src="words.js"></script>
19
+
20
+ <script type="text/javascript">
21
+ // This will be called by the admin adapter when the settings page loads
22
+ function load(settings, onChange) {
23
+ // example: select elements with id=key and class=value and insert value
24
+ if (!settings) return;
25
+ $('.value').each(function () {
26
+ var $key = $(this);
27
+ var id = $key.attr('id');
28
+ if ($key.attr('type') === 'checkbox') {
29
+ // do not call onChange direct, because onChange could expect some arguments
30
+ $key.prop('checked', settings[id])
31
+ .on('change', () => onChange())
32
+ ;
33
+ } else {
34
+ // do not call onChange direct, because onChange could expect some arguments
35
+ $key.val(settings[id])
36
+ .on('change', () => onChange())
37
+ .on('keyup', () => onChange())
38
+ ;
39
+ }
40
+ });
41
+ onChange(false);
42
+ // reinitialize all the Materialize labels on the page if you are dynamically adding inputs:
43
+ if (M) M.updateTextFields();
44
+ }
45
+
46
+ // This will be called by the admin adapter when the user presses the save button
47
+ function save(callback) {
48
+ // example: select elements with class=value and build settings object
49
+ var obj = {};
50
+ $('.value').each(function () {
51
+ var $this = $(this);
52
+ if ($this.attr('type') === 'checkbox') {
53
+ obj[$this.attr('id')] = $this.prop('checked');
54
+ } else {
55
+ obj[$this.attr('id')] = $this.val();
56
+ }
57
+ });
58
+ callback(obj);
59
+ }
60
+ </script>
61
+
62
+ </head>
63
+
64
+ <body>
65
+
66
+ <div class="m adapter-container">
67
+
68
+ <div class="row">
69
+ <div class="col s12 m4 l2">
70
+ <img src="Airzone.png" class="logo" style="width: 198px;">
71
+ </div>
72
+ </div>
73
+
74
+ <div class="row">
75
+ <div class="input-field col s12 m4 l3">
76
+ <input placeholder="192.168.178.1" type="text" minlength="7" maxlength="15" size="15" pattern="^((\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.){3}(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$" class="value validate" id="local_ip">
77
+ <label data-error="invalid format" data-success="ok" for="local_ip" class="translate">Local IP</label>
78
+ </div>
79
+ </div>
80
+ <div class="row">
81
+ <div class="input-field col s12 m4 l3">
82
+ <input placeholder="1" type="number" class="value validate" id="system_id" min="1"/>
83
+ <label data-error="numbers only" data-success="ok" class="translate active" for="system_id" class="translate">System Id</label>
84
+ <span class="helper-text translate">(default is 1)</span>
85
+ </div>
86
+ </div>
87
+ <div class="row">
88
+ <div class="input-field col s12 m4 l3">
89
+ <input placeholder="5" type="number" id="sync_time" class="value validate" min="1"/>
90
+ <label data-error="numbers only" data-success="ok" class="translate active" for="sync_time" class="translate">Sync time</label>
91
+ <span class="helper-text translate">(default is 5 s, minimum is 1 s)</span>
92
+ </div>
93
+ </div>
94
+ </div>
95
+
96
+ </body>
97
+
105
98
  </html>