@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.
- package/README.md +94 -0
- package/icon.png +0 -0
- package/index.js +282 -0
- 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
|
+
}
|