@lv426/homebridge-systemair-enhanced 1.0.6 → 2.0.1
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 +36 -0
- package/LICENSE +21 -0
- package/README.md +107 -67
- package/config.schema.json +98 -0
- package/dist/config.js +54 -0
- package/dist/config.js.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/modes.js +79 -0
- package/dist/modes.js.map +1 -0
- package/dist/names.js +55 -0
- package/dist/names.js.map +1 -0
- package/dist/registers.js +58 -0
- package/dist/registers.js.map +1 -0
- package/dist/saveConnect.js +65 -0
- package/dist/saveConnect.js.map +1 -0
- package/dist/state.js +179 -0
- package/dist/state.js.map +1 -0
- package/dist/ventilator.js +405 -0
- package/dist/ventilator.js.map +1 -0
- package/package.json +36 -11
- package/index.js +0 -333
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 2.0.1
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Every tile read "Ventilation" in the Home app.** Since iOS 16 the Home app shows a tile's own name only from its ConfiguredName, which the plugin now sets on each one: "Ventilation Refresh", "Ventilation Fan" and so on. A name you give a tile in the Home app is kept across restarts.
|
|
8
|
+
|
|
9
|
+
## 2.0.0
|
|
10
|
+
|
|
11
|
+
A rewrite: faster, a switch for every mode, timers, and the fan's real level.
|
|
12
|
+
|
|
13
|
+
### New
|
|
14
|
+
|
|
15
|
+
- **A switch per mode** for scenes and automations: Auto, Visitors (the unit's Crowded), Refresh, Fireplace, Away and Holiday. Only one is on at a time; turning the one that's on off goes back to what it interrupted (Auto, or the manual level you had), as the unit itself does when a mode's time is up.
|
|
16
|
+
- **Refresh and Fireplace timers**: set how long the mode lasts from the Home app, and watch it count down.
|
|
17
|
+
- **The fan shows the level it really runs at**, Auto included: Low 16%, Normal 50%, High 83%, Maximum 100%. The top of the slider starts Refresh, the only way to Maximum. The fan's Auto setting switches back to Auto.
|
|
18
|
+
- **Outdoor temperature** and **filter life** tiles.
|
|
19
|
+
- **Settings in Homebridge's web interface**: which modes and timers to show, your own names for the modes, how often the unit is read.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **The unit is read in one request every 10 seconds** and HomeKit is told about each change. 1.x asked the unit separately for every value the Home app wanted, waiting up to 20 seconds each, so tiles could show "No Response".
|
|
24
|
+
- **The Refresh switch stays on while Refresh runs**, and turning it off ends Refresh. In 1.x it turned itself off after a second.
|
|
25
|
+
- **The thermostat's range is the unit's**, 12–30 °C in half degrees.
|
|
26
|
+
- The Timer tile (time left shown as a battery level) is gone: the timers count down instead.
|
|
27
|
+
- Written in TypeScript, with tests; needs Homebridge 1.8 or 2.x and Node 20 or newer.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **Refresh no longer changes your settings.** 1.x sent the unit's whole Home form to start it, which also set the temperature to 18 °C, the manual level to Low and ECO mode off.
|
|
32
|
+
- **During Refresh the fan showed 16%** (the manual Low setting) while the unit ran at Maximum.
|
|
33
|
+
|
|
34
|
+
## 1.0.6 and earlier
|
|
35
|
+
|
|
36
|
+
See [npm](https://www.npmjs.com/package/@lv426/homebridge-systemair-enhanced?activeTab=versions).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 foxhoundn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,94 +1,134 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="icon.png" width="120" alt="Systemair Ventilator for Homebridge" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">Systemair Ventilator for Homebridge</h1>
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
Your Systemair SAVE ventilation in Apple Home: modes for scenes, timers, the fan's real level,<br />
|
|
9
|
+
temperature, humidity, the outdoor temperature and the filter.
|
|
10
|
+
</p>
|
|
4
11
|
|
|
5
|
-
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://www.npmjs.com/package/@lv426/homebridge-systemair-enhanced"><img src="https://img.shields.io/npm/v/@lv426/homebridge-systemair-enhanced?label=npm" alt="npm version" /></a>
|
|
14
|
+
<a href="https://github.com/Foxhoundn/homebridge-systemair-enhanced/actions/workflows/ci.yml"><img src="https://github.com/Foxhoundn/homebridge-systemair-enhanced/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
|
|
15
|
+
<img src="https://img.shields.io/badge/homebridge-1.8%20%7C%202.x-blueviolet" alt="Homebridge 1.8 and 2.x" />
|
|
16
|
+
<a href="LICENSE"><img src="https://img.shields.io/npm/l/@lv426/homebridge-systemair-enhanced" alt="MIT licence" /></a>
|
|
17
|
+
</p>
|
|
6
18
|
|
|
7
|
-
|
|
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
|
|
19
|
+
---
|
|
11
20
|
|
|
12
|
-
|
|
21
|
+
It talks to the unit's **SAVE Connect** module the same way the module's own web page does, so there's no cloud, no account and no Modbus wiring: if you can open the unit's web page in a browser, this plugin can run it.
|
|
13
22
|
|
|
14
|
-
|
|
23
|
+
## What you get in the Home app
|
|
15
24
|
|
|
16
|
-
|
|
25
|
+
| Tile | What it does |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| **Fan** | Shows the level the fans really run at, Auto included. Drag it to run them manually at Low, Normal or High; drag it to the top to start Refresh; switch it back to Auto, or off. |
|
|
28
|
+
| **Auto, Visitors, Refresh, Fireplace, Away, Holiday** | A switch per mode, for scenes and automations. Only one is on at a time. Turning the one that's on off goes back to what it interrupted: Auto, or the manual level you had. |
|
|
29
|
+
| **Refresh timer, Fireplace timer** | Set how long the mode lasts (the tile's Duration), turn it on, and watch it count down. |
|
|
30
|
+
| **Thermostat** | The temperature in the house and the setpoint (12–30 °C). |
|
|
31
|
+
| **Eco Mode** | The unit's ECO mode. |
|
|
32
|
+
| **Humidity** | Relative humidity of the extract air. |
|
|
33
|
+
| **Outdoor** | The unit's outdoor temperature sensor. |
|
|
34
|
+
| **Filter** | How much filter life is left; the Home app tells you when it's time to change it. |
|
|
17
35
|
|
|
18
|
-
|
|
19
|
-
npm install -g homebridge
|
|
36
|
+
The unit is read in one request every 10 seconds, and HomeKit is told about each change, so the Home app opens straight away and automations see changes within seconds.
|
|
20
37
|
|
|
21
|
-
|
|
38
|
+
### The fan slider
|
|
22
39
|
|
|
23
|
-
|
|
40
|
+
The slider shows and sets the unit's airflow levels:
|
|
24
41
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
42
|
+
| Slider | The unit runs | Shown as |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| 0% | Off (Manual) | 0% |
|
|
45
|
+
| 1–33% | Manual, **Low** | 16% |
|
|
46
|
+
| 34–66% | Manual, **Normal** | 50% |
|
|
47
|
+
| 67–91% | Manual, **High** | 83% |
|
|
48
|
+
| 92–100% | **Refresh**, at Maximum for its set time, then back to what it was doing | 100% |
|
|
28
49
|
|
|
29
|
-
|
|
50
|
+
Manual mode can't go above High: Maximum is reached only by modes like Refresh. Turning the fans off needs **Manual fan stop** allowed on the unit.
|
|
30
51
|
|
|
31
|
-
|
|
32
|
-
Edit the Homebridge config.json file to include the plugin. Add the following under "accessories":
|
|
52
|
+
### The modes
|
|
33
53
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
]
|
|
43
|
-
}
|
|
44
|
-
Replace add your Systemair IP with the actual IP address of your Systemair SAVEConnect device.
|
|
54
|
+
| Mode | What the unit does | How long |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| **Auto** | Follows its week schedule and demand control. | — |
|
|
57
|
+
| **Visitors** (the unit's *Crowded*) | More air for a full house. | 1–8 hours |
|
|
58
|
+
| **Refresh** | As much air as it can, to air the house out. | 1–240 minutes |
|
|
59
|
+
| **Fireplace** | Slight overpressure, so the fireplace draws well. | 1–60 minutes |
|
|
60
|
+
| **Away** | Less air while you're out. | 1–72 hours |
|
|
61
|
+
| **Holiday** | Less air for longer, while you're away for days. | 1–365 days |
|
|
45
62
|
|
|
46
|
-
|
|
63
|
+
How long each mode lasts is the unit's own setting: the timer tiles change it for Refresh and Fireplace, and the unit's web page for the rest. When a mode's time is up, the unit goes back to the mode it interrupted. Changing a timer's minutes while its mode runs saves them for next time; the running mode keeps its countdown.
|
|
47
64
|
|
|
48
|
-
|
|
49
|
-
Restart Homebridge for the changes to take effect:
|
|
50
|
-
```bash
|
|
51
|
-
sudo systemctl restart homebridge
|
|
52
|
-
```
|
|
65
|
+
*Cooker hood* and *Vacuum cleaner* modes are started by switches wired to the unit, so HomeKit can't start them.
|
|
53
66
|
|
|
54
|
-
|
|
67
|
+
## Install
|
|
55
68
|
|
|
56
|
-
|
|
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:
|
|
69
|
+
Search for **Systemair Ventilator** under **Plugins** in Homebridge's web interface and install it, or:
|
|
62
70
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
speed === 4 maps to 83% (High)
|
|
71
|
+
```bash
|
|
72
|
+
npm install -g @lv426/homebridge-systemair-enhanced
|
|
73
|
+
```
|
|
67
74
|
|
|
68
|
-
|
|
75
|
+
You need a Systemair SAVE unit with a **SAVE Connect** module on your network, and its address (the one you open its web page at). Give it a fixed address in your router so it doesn't move.
|
|
69
76
|
|
|
70
|
-
|
|
77
|
+
## Settings
|
|
71
78
|
|
|
72
|
-
|
|
73
|
-
Verify that the SAVEConnect WIFI module is online.
|
|
74
|
-
Check Homebridge logs for error messages.
|
|
79
|
+
Fill them in under the plugin's **Settings** in Homebridge's web interface, or add this to `config.json`:
|
|
75
80
|
|
|
76
|
-
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"accessories": [
|
|
84
|
+
{
|
|
85
|
+
"accessory": "SystemairVentilatorEnhanced",
|
|
86
|
+
"name": "Ventilation",
|
|
87
|
+
"ip": "192.168.1.50"
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
```
|
|
77
92
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
| Setting | What it does | Default |
|
|
94
|
+
| --- | --- | --- |
|
|
95
|
+
| `name` | Each tile is named after it: "Ventilation Fan", "Ventilation Refresh". | `Ventilation` |
|
|
96
|
+
| `ip` | The SAVE Connect module's address. | — |
|
|
97
|
+
| `modeSwitches` | The modes shown as switches: `auto`, `crowded`, `refresh`, `fireplace`, `away`, `holiday`. | all six |
|
|
98
|
+
| `timers` | The modes with a timer tile: `refresh`, `fireplace`. | both |
|
|
99
|
+
| `modeNames` | Your own names, for example `{ "refresh": "Air out" }`. | Crowded is `Visitors`; the rest as on the unit |
|
|
100
|
+
| `eco` | Show the ECO mode switch. | `true` |
|
|
101
|
+
| `outdoorTemperature` | Show the outdoor temperature. | `true` |
|
|
102
|
+
| `filter` | Show the filter. | `true` |
|
|
103
|
+
| `pollSeconds` | How often the unit is read (at least 5). | `10` |
|
|
83
104
|
|
|
84
|
-
##
|
|
105
|
+
## Upgrading from 1.x
|
|
85
106
|
|
|
86
|
-
The
|
|
107
|
+
The accessory and the tiles 1.x had (the fan, the Refresh and Eco Mode switches, the thermostat and humidity) stay the same to HomeKit, so scenes and automations that use them keep working. The new tiles appear next to them. See the [changelog](CHANGELOG.md) for what changed.
|
|
87
108
|
|
|
88
|
-
|
|
89
|
-
- **1110**: Timer remaining time
|
|
90
|
-
- **1120**: Current temperature sensor reading
|
|
91
|
-
- **2000**: Target temperature setting
|
|
92
|
-
- **1125**: Current humidity sensor reading
|
|
109
|
+
## How it talks to the unit
|
|
93
110
|
|
|
94
|
-
|
|
111
|
+
Like the unit's own web page: `GET /mread?{"1160":1}` reads registers and `GET /mwrite?{"1161":4}` writes them, one request at a time. The registers and modes are listed in [`src/registers.ts`](src/registers.ts) and [`src/modes.ts`](src/modes.ts), each as the web page names it.
|
|
112
|
+
|
|
113
|
+
## Development
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npm install
|
|
117
|
+
npm run check # typecheck and tests
|
|
118
|
+
npm run build
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The tests cover the register mapping (from a real unit's replies), the client and the accessory on Homebridge's own HomeKit library.
|
|
122
|
+
|
|
123
|
+
**Releasing:** bump the version and push to `master`; GitHub Actions publishes it to npm, tags it, creates the GitHub release from the [changelog](CHANGELOG.md) and posts to Discord.
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npm version patch # or minor, major, or prerelease --preid beta
|
|
127
|
+
git push --follow-tags
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
A prerelease (`2.1.0-beta.1`) is published under npm's `beta` tag.
|
|
131
|
+
|
|
132
|
+
## Licence
|
|
133
|
+
|
|
134
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pluginAlias": "SystemairVentilatorEnhanced",
|
|
3
|
+
"pluginType": "accessory",
|
|
4
|
+
"singular": false,
|
|
5
|
+
"headerDisplay": "Systemair SAVE ventilation through its SAVE Connect module: the fan, a switch per mode for scenes, timers for Refresh and Fireplace, temperature, humidity and the filter.",
|
|
6
|
+
"schema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"name": {
|
|
10
|
+
"title": "Name",
|
|
11
|
+
"type": "string",
|
|
12
|
+
"default": "Ventilation",
|
|
13
|
+
"required": true,
|
|
14
|
+
"description": "Each tile is named after it: \"Ventilation Fan\", \"Ventilation Refresh\"…"
|
|
15
|
+
},
|
|
16
|
+
"ip": {
|
|
17
|
+
"title": "SAVE Connect address",
|
|
18
|
+
"type": "string",
|
|
19
|
+
"required": true,
|
|
20
|
+
"placeholder": "192.168.68.57",
|
|
21
|
+
"description": "The address you open the unit's web page at."
|
|
22
|
+
},
|
|
23
|
+
"pollSeconds": {
|
|
24
|
+
"title": "Read the unit every (seconds)",
|
|
25
|
+
"type": "integer",
|
|
26
|
+
"default": 10,
|
|
27
|
+
"minimum": 5
|
|
28
|
+
},
|
|
29
|
+
"modeSwitches": {
|
|
30
|
+
"title": "Modes to show as switches",
|
|
31
|
+
"description": "For scenes and automations. Only one is on at a time; turning the one that's on off goes back to Auto.",
|
|
32
|
+
"type": "array",
|
|
33
|
+
"uniqueItems": true,
|
|
34
|
+
"default": ["auto", "crowded", "refresh", "fireplace", "away", "holiday"],
|
|
35
|
+
"items": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"enum": ["auto", "crowded", "refresh", "fireplace", "away", "holiday"],
|
|
38
|
+
"enumNames": ["Auto", "Visitors (Crowded)", "Refresh", "Fireplace", "Away", "Holiday"]
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"timers": {
|
|
42
|
+
"title": "Modes with a timer",
|
|
43
|
+
"description": "A timer tile sets how long the mode lasts (the Home app's Duration, up to an hour) and counts down while it runs.",
|
|
44
|
+
"type": "array",
|
|
45
|
+
"uniqueItems": true,
|
|
46
|
+
"default": ["refresh", "fireplace"],
|
|
47
|
+
"items": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"enum": ["refresh", "fireplace"],
|
|
50
|
+
"enumNames": ["Refresh", "Fireplace"]
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"modeNames": {
|
|
54
|
+
"title": "Your own names for the modes",
|
|
55
|
+
"type": "object",
|
|
56
|
+
"properties": {
|
|
57
|
+
"auto": { "title": "Auto", "type": "string", "placeholder": "Auto" },
|
|
58
|
+
"crowded": { "title": "Crowded", "type": "string", "placeholder": "Visitors" },
|
|
59
|
+
"refresh": { "title": "Refresh", "type": "string", "placeholder": "Refresh" },
|
|
60
|
+
"fireplace": { "title": "Fireplace", "type": "string", "placeholder": "Fireplace" },
|
|
61
|
+
"away": { "title": "Away", "type": "string", "placeholder": "Away" },
|
|
62
|
+
"holiday": { "title": "Holiday", "type": "string", "placeholder": "Holiday" }
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"eco": {
|
|
66
|
+
"title": "Show the ECO mode switch",
|
|
67
|
+
"type": "boolean",
|
|
68
|
+
"default": true
|
|
69
|
+
},
|
|
70
|
+
"outdoorTemperature": {
|
|
71
|
+
"title": "Show the outdoor temperature",
|
|
72
|
+
"type": "boolean",
|
|
73
|
+
"default": true
|
|
74
|
+
},
|
|
75
|
+
"filter": {
|
|
76
|
+
"title": "Show the filter (time to change it)",
|
|
77
|
+
"type": "boolean",
|
|
78
|
+
"default": true
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"layout": [
|
|
83
|
+
"name",
|
|
84
|
+
"ip",
|
|
85
|
+
{
|
|
86
|
+
"type": "fieldset",
|
|
87
|
+
"title": "Modes",
|
|
88
|
+
"expandable": true,
|
|
89
|
+
"items": ["modeSwitches", "timers", "modeNames"]
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"type": "fieldset",
|
|
93
|
+
"title": "More",
|
|
94
|
+
"expandable": true,
|
|
95
|
+
"items": ["eco", "outdoorTemperature", "filter", "pollSeconds"]
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TIMER_MAX_SECONDS = exports.DEFAULT_MODE_NAMES = exports.DEFAULT_TIMERS = exports.DEFAULT_MODE_SWITCHES = void 0;
|
|
4
|
+
exports.readConfig = readConfig;
|
|
5
|
+
const modes_js_1 = require("./modes.js");
|
|
6
|
+
exports.DEFAULT_MODE_SWITCHES = [
|
|
7
|
+
'auto',
|
|
8
|
+
'crowded',
|
|
9
|
+
'refresh',
|
|
10
|
+
'fireplace',
|
|
11
|
+
'away',
|
|
12
|
+
'holiday',
|
|
13
|
+
];
|
|
14
|
+
exports.DEFAULT_TIMERS = ['refresh', 'fireplace'];
|
|
15
|
+
/** What people call the unit's Crowded mode. */
|
|
16
|
+
exports.DEFAULT_MODE_NAMES = { crowded: 'Visitors' };
|
|
17
|
+
/** The Home app counts a valve's duration down from at most an hour. */
|
|
18
|
+
exports.TIMER_MAX_SECONDS = 3600;
|
|
19
|
+
const MIN_POLL_SECONDS = 5;
|
|
20
|
+
const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
21
|
+
function modeList(value, allowed, fallback) {
|
|
22
|
+
if (!Array.isArray(value))
|
|
23
|
+
return fallback;
|
|
24
|
+
return allowed.filter((id) => value.includes(id));
|
|
25
|
+
}
|
|
26
|
+
/** The plugin's settings from Homebridge's config, with defaults for what's left out. */
|
|
27
|
+
function readConfig(raw) {
|
|
28
|
+
const config = isRecord(raw) ? raw : {};
|
|
29
|
+
const ip = typeof config.ip === 'string' ? config.ip.trim() : '';
|
|
30
|
+
if (!ip)
|
|
31
|
+
throw new Error('Set "ip" to the SAVE Connect module\'s address in the plugin settings');
|
|
32
|
+
const poll = Number(config.pollSeconds);
|
|
33
|
+
const names = isRecord(config.modeNames) ? config.modeNames : {};
|
|
34
|
+
const settable = modes_js_1.SETTABLE_MODES.map((mode) => mode.id).filter((id) => id !== 'manual');
|
|
35
|
+
const timed = modes_js_1.MODES.filter((mode) => mode.duration?.unit === 'minutes').map((mode) => mode.id);
|
|
36
|
+
return {
|
|
37
|
+
name: typeof config.name === 'string' && config.name.trim() ? config.name.trim() : 'Ventilation',
|
|
38
|
+
ip,
|
|
39
|
+
pollSeconds: Number.isFinite(poll) && poll >= MIN_POLL_SECONDS ? poll : 10,
|
|
40
|
+
modeSwitches: modeList(config.modeSwitches, settable, exports.DEFAULT_MODE_SWITCHES),
|
|
41
|
+
timers: modeList(config.timers, timed, exports.DEFAULT_TIMERS),
|
|
42
|
+
modeNames: {
|
|
43
|
+
...exports.DEFAULT_MODE_NAMES,
|
|
44
|
+
...Object.fromEntries(modes_js_1.MODES.flatMap((mode) => {
|
|
45
|
+
const name = names[mode.id];
|
|
46
|
+
return typeof name === 'string' && name.trim() ? [[mode.id, name.trim()]] : [];
|
|
47
|
+
})),
|
|
48
|
+
},
|
|
49
|
+
eco: config.eco !== false,
|
|
50
|
+
outdoorTemperature: config.outdoorTemperature !== false,
|
|
51
|
+
filter: config.filter !== false,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const ventilator_js_1 = require("./ventilator.js");
|
|
3
|
+
/** The accessory's name in Homebridge's config (`"accessory": "SystemairVentilatorEnhanced"`). */
|
|
4
|
+
const ACCESSORY_NAME = 'SystemairVentilatorEnhanced';
|
|
5
|
+
module.exports = (api) => {
|
|
6
|
+
api.registerAccessory(ACCESSORY_NAME, ventilator_js_1.SystemairVentilator);
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,mDAAsD;AAEtD,kGAAkG;AAClG,MAAM,cAAc,GAAG,6BAA6B,CAAC;AAErD,iBAAS,CAAC,GAAQ,EAAQ,EAAE;IAC1B,GAAG,CAAC,iBAAiB,CAAC,cAAc,EAAE,mCAAmB,CAAC,CAAC;AAC7D,CAAC,CAAC"}
|
package/dist/modes.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SETTABLE_MODES = exports.MODES = void 0;
|
|
4
|
+
exports.modeById = modeById;
|
|
5
|
+
exports.modeByCurrent = modeByCurrent;
|
|
6
|
+
exports.durationSeconds = durationSeconds;
|
|
7
|
+
exports.durationValue = durationValue;
|
|
8
|
+
/**
|
|
9
|
+
* The unit's modes. Requests and durations are the web page's own (its mode menu and timer
|
|
10
|
+
* pop-up); Cooker hood and Vacuum cleaner come from Systemair's Modbus list: they're started by
|
|
11
|
+
* switches wired to the unit, so they can be seen but not set.
|
|
12
|
+
*/
|
|
13
|
+
exports.MODES = [
|
|
14
|
+
{ id: 'auto', name: 'Auto', current: 0, request: 1 },
|
|
15
|
+
{ id: 'manual', name: 'Manual', current: 1, request: 2 },
|
|
16
|
+
{
|
|
17
|
+
id: 'crowded',
|
|
18
|
+
name: 'Crowded',
|
|
19
|
+
current: 2,
|
|
20
|
+
request: 3,
|
|
21
|
+
duration: { register: 1104, unit: 'hours', min: 1, max: 8 },
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'refresh',
|
|
25
|
+
name: 'Refresh',
|
|
26
|
+
current: 3,
|
|
27
|
+
request: 4,
|
|
28
|
+
duration: { register: 1103, unit: 'minutes', min: 1, max: 240 },
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'fireplace',
|
|
32
|
+
name: 'Fireplace',
|
|
33
|
+
current: 4,
|
|
34
|
+
request: 5,
|
|
35
|
+
duration: { register: 1102, unit: 'minutes', min: 1, max: 60 },
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'away',
|
|
39
|
+
name: 'Away',
|
|
40
|
+
current: 5,
|
|
41
|
+
request: 6,
|
|
42
|
+
duration: { register: 1101, unit: 'hours', min: 1, max: 72 },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'holiday',
|
|
46
|
+
name: 'Holiday',
|
|
47
|
+
current: 6,
|
|
48
|
+
request: 7,
|
|
49
|
+
duration: { register: 1100, unit: 'days', min: 1, max: 365 },
|
|
50
|
+
},
|
|
51
|
+
{ id: 'cookerHood', name: 'Cooker hood', current: 7 },
|
|
52
|
+
{ id: 'vacuumCleaner', name: 'Vacuum cleaner', current: 8 },
|
|
53
|
+
];
|
|
54
|
+
function modeById(id) {
|
|
55
|
+
const mode = exports.MODES.find((item) => item.id === id);
|
|
56
|
+
if (!mode)
|
|
57
|
+
throw new Error(`Unknown mode ${id}`);
|
|
58
|
+
return mode;
|
|
59
|
+
}
|
|
60
|
+
function modeByCurrent(value) {
|
|
61
|
+
return exports.MODES.find((mode) => mode.current === value);
|
|
62
|
+
}
|
|
63
|
+
/** The modes HomeKit can switch on (everything but those only a wired input starts). */
|
|
64
|
+
exports.SETTABLE_MODES = exports.MODES.filter((mode) => mode.request !== undefined);
|
|
65
|
+
const UNIT_SECONDS = {
|
|
66
|
+
minutes: 60,
|
|
67
|
+
hours: 3600,
|
|
68
|
+
days: 86_400,
|
|
69
|
+
};
|
|
70
|
+
/** A duration in the unit's own terms (35 minutes) as seconds. */
|
|
71
|
+
function durationSeconds(duration, value) {
|
|
72
|
+
return value * UNIT_SECONDS[duration.unit];
|
|
73
|
+
}
|
|
74
|
+
/** Seconds as the unit keeps the duration: whole minutes, hours or days, within its limits. */
|
|
75
|
+
function durationValue(duration, seconds) {
|
|
76
|
+
const value = Math.round(seconds / UNIT_SECONDS[duration.unit]);
|
|
77
|
+
return Math.min(duration.max, Math.max(duration.min, value));
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=modes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"modes.js","sourceRoot":"","sources":["../src/modes.ts"],"names":[],"mappings":"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"}
|
package/dist/names.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TileNames = void 0;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
/**
|
|
9
|
+
* The names you give the tiles in the Home app (their ConfiguredName), kept in a small file in
|
|
10
|
+
* Homebridge's storage so a restart doesn't put the plugin's names back. Without a file (tests),
|
|
11
|
+
* they're kept only while the plugin runs.
|
|
12
|
+
*/
|
|
13
|
+
class TileNames {
|
|
14
|
+
accessory;
|
|
15
|
+
file;
|
|
16
|
+
names = {};
|
|
17
|
+
constructor(
|
|
18
|
+
/** Which accessory's names: one file can hold several ventilators'. */
|
|
19
|
+
accessory, file) {
|
|
20
|
+
this.accessory = accessory;
|
|
21
|
+
this.file = file;
|
|
22
|
+
if (!file)
|
|
23
|
+
return;
|
|
24
|
+
try {
|
|
25
|
+
const all = JSON.parse(node_fs_1.default.readFileSync(file, 'utf8'));
|
|
26
|
+
const own = all && typeof all === 'object' ? all[accessory] : undefined;
|
|
27
|
+
if (own && typeof own === 'object')
|
|
28
|
+
this.names = Object.fromEntries(Object.entries(own).filter((entry) => typeof entry[1] === 'string'));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// No file yet, or one this plugin can't read: start from the plugin's names.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
get(tile) {
|
|
35
|
+
return this.names[tile];
|
|
36
|
+
}
|
|
37
|
+
set(tile, name) {
|
|
38
|
+
this.names[tile] = name;
|
|
39
|
+
if (!this.file)
|
|
40
|
+
return;
|
|
41
|
+
let all = {};
|
|
42
|
+
try {
|
|
43
|
+
const read = JSON.parse(node_fs_1.default.readFileSync(this.file, 'utf8'));
|
|
44
|
+
if (read && typeof read === 'object')
|
|
45
|
+
all = read;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Nothing to keep from before.
|
|
49
|
+
}
|
|
50
|
+
all[this.accessory] = this.names;
|
|
51
|
+
node_fs_1.default.writeFileSync(this.file, `${JSON.stringify(all, null, 2)}\n`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.TileNames = TileNames;
|
|
55
|
+
//# sourceMappingURL=names.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"names.js","sourceRoot":"","sources":["../src/names.ts"],"names":[],"mappings":";;;;;;AAAA,sDAAyB;AAEzB;;;;GAIG;AACH;IAKqB,SAAS;IACT,IAAI;IALf,KAAK,GAA2B,EAAE,CAAC;IAE3C;IACE,uEAAuE;IACtD,SAAiB,EACjB,IAAa;yBADb,SAAS;oBACT,IAAI;QAErB,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,IAAI,CAAC;YACH,MAAM,GAAG,GAAY,IAAI,CAAC,KAAK,CAAC,iBAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;YAC/D,MAAM,GAAG,GAAG,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAE,GAA+B,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YACrG,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;gBAChC,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,WAAW,CAC7B,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAA6B,EAAE,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,CAC/F,CAAC;QACN,CAAC;QAAC,MAAM,CAAC;YACP,6EAA6E;QAC/E,CAAC;IACH,CAAC;IAEM,GAAG,CAAC,IAAY;QACrB,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAEM,GAAG,CAAC,IAAY,EAAE,IAAY;QACnC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;QACxB,IAAI,CAAC,IAAI,CAAC,IAAI;YAAE,OAAO;QACvB,IAAI,GAAG,GAA4B,EAAE,CAAC;QACtC,IAAI,CAAC;YACH,MAAM,IAAI,GAAY,IAAI,CAAC,KAAK,CAAC,iBAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;YACrE,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;gBAAE,GAAG,GAAG,IAA+B,CAAC;QAC9E,CAAC;QAAC,MAAM,CAAC;YACP,+BAA+B;QACjC,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC;QACjC,iBAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;IACnE,CAAC;CACF"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SETPOINT_RANGE = exports.FAN_LEVELS = exports.REGISTER = void 0;
|
|
4
|
+
exports.fanLevel = fanLevel;
|
|
5
|
+
/**
|
|
6
|
+
* Systemair SAVE Connect registers, as the unit's own web page uses them (its `scripts.js`):
|
|
7
|
+
* `GET /mread?{"1160":1}` reads (register → how many registers), `GET /mwrite?{"1161":4}` writes.
|
|
8
|
+
* Temperatures are tenths of a degree; a time split over two registers is seconds (low, high).
|
|
9
|
+
*/
|
|
10
|
+
exports.REGISTER = {
|
|
11
|
+
/** Write to change the mode: see `MODES[].request`. */
|
|
12
|
+
modeRequest: 1161,
|
|
13
|
+
/** The mode it's in now: see `MODES[].current`. */
|
|
14
|
+
modeCurrent: 1160,
|
|
15
|
+
/** Manual mode's airflow: 0 off, 2 low, 3 normal, 4 high. */
|
|
16
|
+
manualLevel: 1130,
|
|
17
|
+
/** Time left in a timed mode, seconds (with 1111). */
|
|
18
|
+
remaining: 1110,
|
|
19
|
+
/** Whether Manual may stop the fans altogether (1 yes). */
|
|
20
|
+
manualStopAllowed: 1352,
|
|
21
|
+
/** What the fans actually run at, percent. */
|
|
22
|
+
supplyFanOutput: 14000,
|
|
23
|
+
extractFanOutput: 14001,
|
|
24
|
+
/** Each airflow level's supply and extract percentages, Minimum to Maximum (1400–1409). */
|
|
25
|
+
fanLevelPercents: 1400,
|
|
26
|
+
/** Extract air temperature (what the house is at), tenths of °C. */
|
|
27
|
+
extractTemperature: 12543,
|
|
28
|
+
outdoorTemperature: 12101,
|
|
29
|
+
supplyTemperature: 12102,
|
|
30
|
+
/** Temperature setpoint, tenths of °C (12–30 °C on the unit). */
|
|
31
|
+
setpoint: 2000,
|
|
32
|
+
/** Relative humidity, percent. */
|
|
33
|
+
humidity: 12135,
|
|
34
|
+
/** ECO mode: 1 on. */
|
|
35
|
+
eco: 2504,
|
|
36
|
+
/** How often the filter is changed, months. */
|
|
37
|
+
filterPeriod: 7000,
|
|
38
|
+
/** Time left until the filter change, seconds (with 7005). */
|
|
39
|
+
filterRemaining: 7004,
|
|
40
|
+
};
|
|
41
|
+
/** The unit's airflow levels, lowest first. */
|
|
42
|
+
exports.FAN_LEVELS = [
|
|
43
|
+
{ id: 'off', name: 'Off', value: 0, speed: 0, manual: true },
|
|
44
|
+
{ id: 'minimum', name: 'Minimum', value: 1, speed: 8, supplyRegister: 1400, manual: false },
|
|
45
|
+
{ id: 'low', name: 'Low', value: 2, speed: 16, supplyRegister: 1402, manual: true },
|
|
46
|
+
{ id: 'normal', name: 'Normal', value: 3, speed: 50, supplyRegister: 1404, manual: true },
|
|
47
|
+
{ id: 'high', name: 'High', value: 4, speed: 83, supplyRegister: 1406, manual: true },
|
|
48
|
+
{ id: 'maximum', name: 'Maximum', value: 5, speed: 100, supplyRegister: 1408, manual: false },
|
|
49
|
+
];
|
|
50
|
+
function fanLevel(id) {
|
|
51
|
+
const level = exports.FAN_LEVELS.find((item) => item.id === id);
|
|
52
|
+
if (!level)
|
|
53
|
+
throw new Error(`Unknown fan level ${id}`);
|
|
54
|
+
return level;
|
|
55
|
+
}
|
|
56
|
+
/** The unit's setpoint range, °C. */
|
|
57
|
+
exports.SETPOINT_RANGE = { min: 12, max: 30 };
|
|
58
|
+
//# sourceMappingURL=registers.js.map
|