@lv426/homebridge-systemair-enhanced 1.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.
Files changed (4) hide show
  1. package/README.md +94 -0
  2. package/icon.png +0 -0
  3. package/index.js +282 -0
  4. package/package.json +38 -0
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # Homebridge Systemair Ventilator Plugin
2
+
3
+ This is a Homebridge plugin for controlling a Systemair Ventilator through its web interface then into HomeKit. You must have a SAVEConnect WIFI module connected for this to work. You will need the IP address of the SAVEConnect device and include it in the plugin settings.
4
+
5
+ This plugin provides comprehensive control of your Systemair ventilator including:
6
+
7
+ - **Fan Speed Control**: Adjust ventilation speeds with multiple settings
8
+ - **Temperature Control**: Set target temperature and view current temperature
9
+ - **Humidity Monitoring**: Monitor current humidity levels
10
+ - **Thermostat Interface**: Control heating/cooling/auto modes through HomeKit's thermostat interface
11
+
12
+ # Installation:
13
+
14
+ If Homebridge is not already installed, use the following command to install it globally:
15
+
16
+ 1. Install Homebridge:
17
+
18
+ ```bash
19
+ npm install -g homebridge
20
+
21
+ ```
22
+
23
+ 2. Install the Plugin
24
+
25
+ ```bash
26
+ npm install -g @lv426/homebridge-systemair-enhanced
27
+ Run the following command to install the Homebridge Systemair Ventilator plugin:
28
+
29
+ ```
30
+
31
+ 3. Configure the Plugin
32
+ Edit the Homebridge config.json file to include the plugin. Add the following under "accessories":
33
+
34
+ ```yaml
35
+ {
36
+ "accessories": [
37
+ {
38
+ "accessory": "SystemairVentilatorEnhanced",
39
+ "name": "Living Room Ventilator",
40
+ "ip": "x.x.x.x"
41
+ }
42
+ ]
43
+ }
44
+ Replace add your Systemair IP with the actual IP address of your Systemair SAVEConnect device.
45
+
46
+ ```
47
+
48
+ 4. Restart Homebridge
49
+ Restart Homebridge for the changes to take effect:
50
+ ```bash
51
+ sudo systemctl restart homebridge
52
+ ```
53
+
54
+ # Features:
55
+
56
+ Control fan speeds with three settings: Low, Medium, and High.
57
+ Adjust the target temperature directly from HomeKit.
58
+ View current temperature and humidity readings.
59
+ Control heating/cooling/auto modes through the thermostat interface.
60
+ Automatically handles fan activation and speed synchronization.
61
+ Create scenes for Low, Normal, and High with the following mappings:
62
+
63
+ speed === 1 maps to 0% (Off)
64
+ speed === 2 maps to 16% (Low)
65
+ speed === 3 maps to 50% (Normal)
66
+ speed === 4 maps to 83% (High)
67
+
68
+ # Troubleshooting:
69
+
70
+ If the ventilator doesn't respond, check the following:
71
+
72
+ Ensure the IP address in the configuration is correct.
73
+ Verify that the SAVEConnect WIFI module is online.
74
+ Check Homebridge logs for error messages.
75
+
76
+ **Note**: The plugin creates multiple HomeKit accessories:
77
+
78
+ - **Fan**: Controls ventilation speed and on/off state
79
+ - **Thermostat**: Controls target temperature and heating/cooling modes
80
+ - **Humidity Sensor**: Displays current humidity levels
81
+ - **Refresh Switch**: Triggers refresh mode
82
+ - **Timer Display**: Shows remaining timer (displayed as battery level)
83
+
84
+ ## Parameter IDs:
85
+
86
+ The plugin uses the following Systemair SAVEConnect parameter IDs:
87
+
88
+ - **1130**: Fan speed control (0=off, 2=low, 3=medium, 4=high)
89
+ - **1110**: Timer remaining time
90
+ - **1120**: Current temperature sensor reading
91
+ - **2000**: Target temperature setting
92
+ - **1125**: Current humidity sensor reading
93
+
94
+ **Important**: These parameter IDs are based on common Systemair configurations but may vary by device model. If temperature or humidity readings are not working correctly, you may need to modify the parameter IDs in the code to match your specific device's API configuration.
package/icon.png ADDED
Binary file
package/index.js ADDED
@@ -0,0 +1,282 @@
1
+ const axios = require('axios');
2
+
3
+ let Service, Characteristic;
4
+
5
+ module.exports = (homebridge) => {
6
+ Service = homebridge.hap.Service;
7
+ Characteristic = homebridge.hap.Characteristic;
8
+ homebridge.registerAccessory(
9
+ '@lv426/homebridge-systemair-enhanced',
10
+ 'SystemairVentilatorEnhanced',
11
+ SystemairVentilator
12
+ );
13
+ };
14
+
15
+ class SystemairVentilator {
16
+ constructor(log, config, api) {
17
+ this.log = log;
18
+ this.config = config;
19
+ this.api = api;
20
+
21
+ // Parameter IDs for Systemair SAVEConnect API
22
+ // These may need adjustment based on your specific device model
23
+ this.PARAM_IDS = {
24
+ FAN_SPEED: '1130', // Fan speed control (0=off, 2=low, 3=medium, 4=high)
25
+ TIMER: '1110', // Timer remaining time
26
+ CURRENT_TEMP: '12543', // Current temperature sensor reading
27
+ TARGET_TEMP: '2000', // Target temperature setting
28
+ HUMIDITY: '12135', // Current humidity sensor reading
29
+ };
30
+
31
+ this.axiosInstance = axios.create({
32
+ timeout: 20000, // 20 seconds timeout
33
+ });
34
+
35
+ // Fan service
36
+ this.fanService = new Service.Fanv2(this.config.name + ' Fan');
37
+
38
+ // Refresh service
39
+ this.refreshService = new Service.Switch(this.config.name + ' Refresh');
40
+
41
+ // Timer service (using BatteryService instead of LightSensor)
42
+ this.timerService = new Service.BatteryService(this.config.name + ' Timer');
43
+
44
+ // Thermostat service
45
+ this.thermostatService = new Service.Thermostat(
46
+ this.config.name + ' Thermostat'
47
+ );
48
+
49
+ // Humidity sensor service
50
+ this.humiditySensorService = new Service.HumiditySensor(
51
+ this.config.name + ' Humidity'
52
+ );
53
+
54
+ this.setupCharacteristics();
55
+ }
56
+
57
+ setupCharacteristics() {
58
+ // Fan characteristics
59
+ this.fanService
60
+ .getCharacteristic(Characteristic.Active)
61
+ .onSet(this.setActive.bind(this))
62
+ .onGet(this.getActive.bind(this));
63
+
64
+ this.fanService
65
+ .getCharacteristic(Characteristic.RotationSpeed)
66
+ .onSet(this.setRotationSpeed.bind(this))
67
+ .onGet(this.getRotationSpeed.bind(this));
68
+
69
+ // Refresh characteristic
70
+ this.refreshService
71
+ .getCharacteristic(Characteristic.On)
72
+ .onSet(this.setRefresh.bind(this));
73
+
74
+ // Timer characteristic (using Battery Level to store remaining time)
75
+ this.timerService
76
+ .getCharacteristic(Characteristic.BatteryLevel)
77
+ .onGet(this.getTimer.bind(this));
78
+
79
+ // Thermostat characteristics
80
+ this.thermostatService
81
+ .getCharacteristic(Characteristic.CurrentTemperature)
82
+ .onGet(this.getCurrentTemperature.bind(this));
83
+
84
+ this.thermostatService
85
+ .getCharacteristic(Characteristic.TargetTemperature)
86
+ .onGet(this.getTargetTemperature.bind(this))
87
+ .onSet(this.setTargetTemperature.bind(this))
88
+ .setProps({
89
+ minValue: 10,
90
+ maxValue: 38,
91
+ minStep: 0.5,
92
+ });
93
+
94
+ // Humidity sensor characteristic
95
+ this.humiditySensorService
96
+ .getCharacteristic(Characteristic.CurrentRelativeHumidity)
97
+ .onGet(this.getCurrentRelativeHumidity.bind(this));
98
+ }
99
+
100
+ async retryRequest(url, retries = 3) {
101
+ for (let i = 0; i < retries; i++) {
102
+ try {
103
+ return await this.axiosInstance.get(url);
104
+ } catch (error) {
105
+ if (i === retries - 1) {
106
+ this.log(`Retry failed: ${error.message}`);
107
+ throw error;
108
+ }
109
+ this.log(
110
+ `Retrying request (${i + 1}/${retries}) due to: ${error.message}`
111
+ );
112
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // 1s delay before retry
113
+ }
114
+ }
115
+ }
116
+
117
+ async setActive(value) {
118
+ const url = `http://${this.config.ip}/mwrite?{"${
119
+ this.PARAM_IDS.FAN_SPEED
120
+ }":${value ? '1' : '0'}}`;
121
+ this.log(`SetActive: Sending request to ${url}`);
122
+ await this.retryRequest(url);
123
+ this.log(`SetActive: Successfully set to ${value ? 'ON' : 'OFF'}`);
124
+ }
125
+
126
+ async getActive() {
127
+ const url = `http://${this.config.ip}/mread?{"${this.PARAM_IDS.FAN_SPEED}":1}`;
128
+ this.log(`GetActive: Sending request to ${url}`);
129
+ const response = await this.retryRequest(url);
130
+ const isActive = response.data[this.PARAM_IDS.FAN_SPEED] > 0;
131
+ this.log(`GetActive: Current state is ${isActive ? 'ON' : 'OFF'}`);
132
+ return isActive ? 1 : 0;
133
+ }
134
+
135
+ async setRotationSpeed(value) {
136
+ let speed;
137
+ if (value === 0) {
138
+ speed = 0;
139
+ } else if (value <= 16) {
140
+ speed = 2;
141
+ } else if (value <= 50) {
142
+ speed = 3;
143
+ } else {
144
+ speed = 4;
145
+ }
146
+
147
+ const url = `http://${this.config.ip}/mwrite?{"${this.PARAM_IDS.FAN_SPEED}":${speed}}`;
148
+ this.log(`SetRotationSpeed: Setting speed to ${speed} (value: ${value}%)`);
149
+ await this.retryRequest(url);
150
+ this.log(`SetRotationSpeed: Successfully set to speed ${speed}`);
151
+ }
152
+
153
+ async getRotationSpeed() {
154
+ const url = `http://${this.config.ip}/mread?{"${this.PARAM_IDS.FAN_SPEED}":1}`;
155
+ this.log(`GetRotationSpeed: Sending request to ${url}`);
156
+ const response = await this.retryRequest(url);
157
+ const speed = response.data[this.PARAM_IDS.FAN_SPEED];
158
+ let percentage = speed === 2 ? 16 : speed === 3 ? 50 : speed === 4 ? 83 : 0;
159
+ this.log(
160
+ `GetRotationSpeed: Current speed is ${speed} (value: ${percentage}%)`
161
+ );
162
+ return percentage;
163
+ }
164
+
165
+ async setRefresh(value) {
166
+ if (value) {
167
+ const writeUrl = `http://${this.config.ip}/mwrite?{"${this.PARAM_IDS.FAN_SPEED}":2,"1161":4,"${this.PARAM_IDS.TARGET_TEMP}":180,"2504":0,"16100":0}`;
168
+ this.log(`Refresh: Sending request to ${writeUrl}`);
169
+ await this.retryRequest(writeUrl);
170
+ this.log(`Refresh: Successfully started refresh mode.`);
171
+ setTimeout(() => {
172
+ this.refreshService
173
+ .getCharacteristic(Characteristic.On)
174
+ .updateValue(false);
175
+ }, 1000);
176
+ }
177
+ }
178
+
179
+ async getTimer() {
180
+ const url = `http://${this.config.ip}/mread?{"${this.PARAM_IDS.TIMER}":2}`;
181
+ this.log(`Timer: Fetching timer value from ${url}`);
182
+ try {
183
+ const response = await this.retryRequest(url);
184
+ let timerValue = response.data[this.PARAM_IDS.TIMER]; // Extract timer value
185
+
186
+ // Ensure timer value is valid for HomeKit (0 - 100% battery level range)
187
+ if (timerValue < 0) {
188
+ timerValue = 0;
189
+ } else if (timerValue > 100) {
190
+ timerValue = 100; // Max HomeKit battery level
191
+ }
192
+
193
+ this.log(`Timer: Current remaining time is ${timerValue} minutes.`);
194
+ return timerValue; // Return valid percentage
195
+ } catch (error) {
196
+ this.log(`Timer: Error - ${error.message}`);
197
+ return 0; // Default to 0% if an error occurs
198
+ }
199
+ }
200
+
201
+ // Thermostat methods
202
+ async getCurrentTemperature() {
203
+ const url = `http://${this.config.ip}/mread?{"${this.PARAM_IDS.CURRENT_TEMP}":1}`;
204
+ this.log(`getCurrentTemperature: Fetching from ${url}`);
205
+ try {
206
+ const response = await this.retryRequest(url);
207
+ let temperature = response.data[this.PARAM_IDS.CURRENT_TEMP] || 20; // Default to 20°C if no data
208
+
209
+ // Ensure temperature is within HomeKit range (-270 to 100°C)
210
+ temperature = Math.max(-270, Math.min(100, temperature));
211
+
212
+ this.log(
213
+ `getCurrentTemperature: Current temperature is ${temperature}°C`
214
+ );
215
+ return temperature;
216
+ } catch (error) {
217
+ this.log(`getCurrentTemperature: Error - ${error.message}`);
218
+ return 20; // Default to 20°C if an error occurs
219
+ }
220
+ }
221
+
222
+ async getTargetTemperature() {
223
+ const url = `http://${this.config.ip}/mread?{"${this.PARAM_IDS.TARGET_TEMP}":1}`;
224
+ this.log(`getTargetTemperature: Fetching from ${url}`);
225
+ try {
226
+ const response = await this.retryRequest(url);
227
+ let temperature = response.data[this.PARAM_IDS.TARGET_TEMP] || 20; // Default to 20°C if no data
228
+
229
+ // Ensure temperature is within HomeKit target range (10-38°C)
230
+ temperature = Math.max(10, Math.min(38, temperature));
231
+
232
+ this.log(`getTargetTemperature: Target temperature is ${temperature}°C`);
233
+ return temperature;
234
+ } catch (error) {
235
+ this.log(`getTargetTemperature: Error - ${error.message}`);
236
+ return 20; // Default to 20°C if an error occurs
237
+ }
238
+ }
239
+
240
+ async setTargetTemperature(value) {
241
+ // Ensure the value is within valid range
242
+ value = Math.max(10, Math.min(38, value));
243
+
244
+ const url = `http://${this.config.ip}/mwrite?{"${this.PARAM_IDS.TARGET_TEMP}":${value}}`;
245
+ this.log(`setTargetTemperature: Setting target temperature to ${value}°C`);
246
+ try {
247
+ await this.retryRequest(url);
248
+ this.log(`setTargetTemperature: Successfully set to ${value}°C`);
249
+ } catch (error) {
250
+ this.log(`setTargetTemperature: Error - ${error.message}`);
251
+ }
252
+ }
253
+
254
+ // Humidity sensor method
255
+ async getCurrentRelativeHumidity() {
256
+ const url = `http://${this.config.ip}/mread?{"${this.PARAM_IDS.HUMIDITY}":1}`;
257
+ this.log(`getCurrentRelativeHumidity: Fetching from ${url}`);
258
+ try {
259
+ const response = await this.retryRequest(url);
260
+ let humidity = response.data[this.PARAM_IDS.HUMIDITY] || 50; // Default to 50% if no data
261
+
262
+ // Ensure humidity is within valid range (0-100%)
263
+ humidity = Math.max(0, Math.min(100, humidity));
264
+
265
+ this.log(`getCurrentRelativeHumidity: Current humidity is ${humidity}%`);
266
+ return humidity;
267
+ } catch (error) {
268
+ this.log(`getCurrentRelativeHumidity: Error - ${error.message}`);
269
+ return 50; // Default to 50% if an error occurs
270
+ }
271
+ }
272
+
273
+ getServices() {
274
+ return [
275
+ this.fanService,
276
+ this.refreshService,
277
+ this.timerService,
278
+ this.thermostatService,
279
+ this.humiditySensorService,
280
+ ];
281
+ }
282
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@lv426/homebridge-systemair-enhanced",
3
+ "version": "1.0.0",
4
+ "description": "An enhanced Homebridge plugin for controlling SystemAir Ventilators, which includes Temperature controls and Humidity readings",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "author": "foxhoundn",
10
+ "license": "MIT",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/foxhoundn/homebridge-systemair-ventilator.git"
14
+ },
15
+ "keywords": [
16
+ "homebridge-plugin",
17
+ "homebridge",
18
+ "systemair",
19
+ "ventilator",
20
+ "fan",
21
+ "thermostat"
22
+ ],
23
+ "engines": {
24
+ "node": ">=12.0.0",
25
+ "homebridge": ">=2.0.0"
26
+ },
27
+ "dependencies": {
28
+ "axios": "^1.4.0"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "homebridge": {
34
+ "pluginAlias": "SystemairVentilatorEnhanced",
35
+ "pluginType": "accessory",
36
+ "icon": "https://raw.githubusercontent.com/foxhoundn/homebridge-systemair-ventilator/master/icon.png"
37
+ }
38
+ }