homebridge-nuheat2 1.2.4-beta.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/CHANGELOG.md +89 -0
- package/README.md +120 -0
- package/config.schema.json +101 -0
- package/index.js +360 -0
- package/lib/NuHeatAPI.js +696 -0
- package/lib/NuHeatGroup.js +74 -0
- package/lib/NuHeatListener.js +133 -0
- package/lib/NuHeatThermostat.js +247 -0
- package/lib/logger.js +35 -0
- package/lib/settings.js +19 -0
- package/package.json +51 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project should be documented in this file
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [1.2.4-beta.0] - 2026-04-11
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Delay platform startup until Homebridge finishes restoring cached accessories
|
|
12
|
+
- Allow overriding Nuheat OAuth client settings through config or environment variables
|
|
13
|
+
- Improve SignalR reconnection handling and token refresh behavior
|
|
14
|
+
- Add basic regression tests and modernize package metadata for Homebridge 1.8+ and 2.0 betas
|
|
15
|
+
- Publish the maintained fork under the new npm package identity `homebridge-nuheat2`
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Correct manual-mode thermostat mapping so HomeKit no longer snaps back to off
|
|
20
|
+
- Fix thermostat online-state handling so status updates no longer rely on an assignment bug
|
|
21
|
+
- Pin transitive websocket and cookie dependencies away from known vulnerable versions
|
|
22
|
+
|
|
23
|
+
## [1.2.3] - 2024-11-04
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Bug when using away mode switches
|
|
28
|
+
- Variable handling in some debug logging
|
|
29
|
+
|
|
30
|
+
## [1.2.2] - 2023-06-13
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- variable typo
|
|
35
|
+
|
|
36
|
+
## [1.2.1] - 2023-06-01
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- hold length bug
|
|
41
|
+
|
|
42
|
+
## [1.2.0] - 2023-05-26
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- client secret for api auth
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- async updates from the api to reduce cookie creation
|
|
51
|
+
|
|
52
|
+
## [1.1.4] - 2023-05-12
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
- error handling when unable to get access token, as to not crash homebridge
|
|
57
|
+
|
|
58
|
+
## [1.1.3] - 2023-04-17
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
- typo in away mode
|
|
63
|
+
|
|
64
|
+
## [1.1.2] - 2022-12-24
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- some unhandled api auth error
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
|
|
72
|
+
- some debug logging code
|
|
73
|
+
|
|
74
|
+
## [1.1.1] - 2022-11-17
|
|
75
|
+
|
|
76
|
+
### Fixed
|
|
77
|
+
|
|
78
|
+
- changed the 'homebridge-nuheat' name to be lower case so HOOBS handles properly
|
|
79
|
+
|
|
80
|
+
## [1.1.0] - 2022-11-15
|
|
81
|
+
|
|
82
|
+
### Added
|
|
83
|
+
|
|
84
|
+
- Added `homebridge-ui` support with auto detection
|
|
85
|
+
- Added Away Mode switches for groups
|
|
86
|
+
|
|
87
|
+
### Changed
|
|
88
|
+
|
|
89
|
+
- Changed the underlying API to use nuheats new api system
|
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# homebridge-nuheat2
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/homebridge-nuheat2)
|
|
4
|
+
[](https://www.npmjs.com/package/homebridge-nuheat2)
|
|
5
|
+
|
|
6
|
+
Homebridge platform plugin for Nuheat Signature floor-heating thermostats.
|
|
7
|
+
|
|
8
|
+
This fork focuses on modernizing the plugin for current Homebridge releases, improving runtime stability, and preparing for Homebridge 2.0 while keeping the existing `NuHeat` platform configuration intact.
|
|
9
|
+
|
|
10
|
+
## Highlights
|
|
11
|
+
|
|
12
|
+
- Automatically discovers thermostats on the authenticated Nuheat account
|
|
13
|
+
- Optionally creates HomeKit switches for Nuheat group away mode
|
|
14
|
+
- Supports permanent, scheduled, and timed holds
|
|
15
|
+
- Uses Nuheat's OAuth-based API instead of legacy site scraping
|
|
16
|
+
- Includes compatibility improvements for Homebridge 1.8+ and 2.0 betas
|
|
17
|
+
- Allows advanced OAuth overrides for long-term API stability
|
|
18
|
+
|
|
19
|
+
## Compatibility
|
|
20
|
+
|
|
21
|
+
- Homebridge: `^1.8.0 || ^2.0.0-beta.0`
|
|
22
|
+
- Node.js: `^18.20.4 || ^20.18.0 || ^22 || ^24`
|
|
23
|
+
|
|
24
|
+
For current Homebridge 2.0 betas, use Node 22 or 24.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
Install Homebridge first:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install -g homebridge
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Then install the plugin:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install -g homebridge-nuheat2
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The published package name for this maintained fork is `homebridge-nuheat2`. The Homebridge platform name in config remains `NuHeat`.
|
|
41
|
+
|
|
42
|
+
## Configuration
|
|
43
|
+
|
|
44
|
+
Most users should configure the plugin through Homebridge Config UI X, but the equivalent JSON looks like this:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"platform": "NuHeat",
|
|
49
|
+
"name": "NuHeat",
|
|
50
|
+
"email": "email@address.com",
|
|
51
|
+
"password": "password123",
|
|
52
|
+
"devices": [{ "serialNumber": "1111111" }, { "serialNumber": "2222222" }],
|
|
53
|
+
"autoPopulateAwayModeSwitches": true,
|
|
54
|
+
"holdLength": 1440,
|
|
55
|
+
"refresh": 60
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Options
|
|
60
|
+
|
|
61
|
+
- `platform`: Must be `NuHeat`
|
|
62
|
+
- `name`: Display name used in Homebridge logs
|
|
63
|
+
- `email`: MyNuheat account email address
|
|
64
|
+
- `password`: MyNuheat account password
|
|
65
|
+
- `devices`: Optional list of thermostats to expose
|
|
66
|
+
- `serialNumber`: Thermostat serial number from MyNuheat
|
|
67
|
+
- `autoPopulateAwayModeSwitches`: Automatically expose switches for all groups on the account
|
|
68
|
+
- `groups`: Optional allow-list of groups to expose as away-mode switches
|
|
69
|
+
- `groupName`: Group name as shown in MyNuheat
|
|
70
|
+
- `holdLength`: Hold duration in minutes
|
|
71
|
+
- `refresh`: Poll interval in seconds, default `60`
|
|
72
|
+
- `debug`: Enables verbose logging
|
|
73
|
+
- `clientId`: Optional advanced override for the Nuheat OAuth client ID
|
|
74
|
+
- `clientSecret`: Optional advanced override for the Nuheat OAuth client secret
|
|
75
|
+
- `redirectUri`: Optional advanced override for the Nuheat OAuth redirect URI, default `http://localhost`
|
|
76
|
+
|
|
77
|
+
### Hold Length Behavior
|
|
78
|
+
|
|
79
|
+
- `0`: hold until the next scheduled event
|
|
80
|
+
- `1-1439`: timed hold for the configured number of minutes
|
|
81
|
+
- `1440`: permanent hold
|
|
82
|
+
|
|
83
|
+
### Device Discovery
|
|
84
|
+
|
|
85
|
+
If `devices` is omitted or empty, the plugin will automatically expose every thermostat on the authenticated account.
|
|
86
|
+
|
|
87
|
+
If `groups` is omitted and `autoPopulateAwayModeSwitches` is enabled, the plugin will automatically expose away-mode switches for all groups on the account.
|
|
88
|
+
|
|
89
|
+
## Nuheat API Access
|
|
90
|
+
|
|
91
|
+
Nuheat's public OpenAPI documentation indicates that third-party developers should request their own API credentials:
|
|
92
|
+
|
|
93
|
+
- [Nuheat OpenAPI docs](https://api.mynuheat.com/)
|
|
94
|
+
- [Nuheat API access request page](https://www.nuheat.com/openapi)
|
|
95
|
+
|
|
96
|
+
This fork still supports the legacy built-in OAuth client settings as a fallback, but using your own `clientId` and `clientSecret` is the recommended long-term path.
|
|
97
|
+
|
|
98
|
+
## What's New In This Fork
|
|
99
|
+
|
|
100
|
+
- Fixed the manual-mode thermostat issue where HomeKit could immediately snap back to `Off`
|
|
101
|
+
- Hardened online-state parsing and general accessory refresh behavior
|
|
102
|
+
- Delayed platform startup until Homebridge finishes restoring cached accessories
|
|
103
|
+
- Improved SignalR reconnect handling
|
|
104
|
+
- Added regression tests for the key thermostat behavior fixes
|
|
105
|
+
- Updated package metadata and dependency overrides for a cleaner modern release
|
|
106
|
+
- Published under the maintainer-owned package identity `homebridge-nuheat2`
|
|
107
|
+
|
|
108
|
+
## Development
|
|
109
|
+
|
|
110
|
+
Run the test suite with:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
npm test
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Roadmap
|
|
117
|
+
|
|
118
|
+
- Validate the plugin against an official Nuheat API client registration
|
|
119
|
+
- Verify group and away-mode behavior against current live API responses
|
|
120
|
+
- Revisit whether SignalR notifications can reduce polling further in real-world deployments
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pluginAlias": "NuHeat",
|
|
3
|
+
"pluginType": "platform",
|
|
4
|
+
"singular": false,
|
|
5
|
+
"schema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"name": {
|
|
9
|
+
"title": "Name",
|
|
10
|
+
"type": "string",
|
|
11
|
+
"required": true,
|
|
12
|
+
"default": "NuHeat"
|
|
13
|
+
},
|
|
14
|
+
"email": {
|
|
15
|
+
"title": "email",
|
|
16
|
+
"type": "string",
|
|
17
|
+
"required": true,
|
|
18
|
+
"format": "email"
|
|
19
|
+
},
|
|
20
|
+
"password": {
|
|
21
|
+
"title": "password",
|
|
22
|
+
"type": "string",
|
|
23
|
+
"required": true
|
|
24
|
+
},
|
|
25
|
+
"clientId": {
|
|
26
|
+
"title": "Nuheat Client ID",
|
|
27
|
+
"type": "string",
|
|
28
|
+
"description": "Optional advanced override. Leave blank to use the built-in default client ID."
|
|
29
|
+
},
|
|
30
|
+
"clientSecret": {
|
|
31
|
+
"title": "Nuheat Client Secret",
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "Optional advanced override. Leave blank to use the built-in default client secret."
|
|
34
|
+
},
|
|
35
|
+
"redirectUri": {
|
|
36
|
+
"title": "Nuheat Redirect URI",
|
|
37
|
+
"type": "string",
|
|
38
|
+
"description": "Optional advanced override for OAuth redirect URI.",
|
|
39
|
+
"placeholder": "http://localhost"
|
|
40
|
+
},
|
|
41
|
+
"devices": {
|
|
42
|
+
"title": "Devices",
|
|
43
|
+
"type": "array",
|
|
44
|
+
"items": {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"properties": {
|
|
47
|
+
"serialNumber": {
|
|
48
|
+
"title": "Serial Number",
|
|
49
|
+
"type": "string",
|
|
50
|
+
"required": true
|
|
51
|
+
},
|
|
52
|
+
"disabled": {
|
|
53
|
+
"title": "Disabled",
|
|
54
|
+
"type": "boolean"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"autoPopulateAwayModeSwitches": {
|
|
60
|
+
"title": "Auto populate Away Mode switches for all available groups",
|
|
61
|
+
"type": "boolean"
|
|
62
|
+
},
|
|
63
|
+
"groups": {
|
|
64
|
+
"title": "Groups",
|
|
65
|
+
"type": "array",
|
|
66
|
+
"items": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"properties": {
|
|
69
|
+
"groupName": {
|
|
70
|
+
"title": "Group Name",
|
|
71
|
+
"type": "string",
|
|
72
|
+
"required": true
|
|
73
|
+
},
|
|
74
|
+
"disabled": {
|
|
75
|
+
"title": "Disabled",
|
|
76
|
+
"type": "boolean"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"holdLength": {
|
|
82
|
+
"title": "Hold Length (in minutes)",
|
|
83
|
+
"type": "integer",
|
|
84
|
+
"description": "If set to 0, set point changes will hold until the next scheduled event. If set to 1440 (default), set point changes will be permanent. Set to any value in between for a timed hold.",
|
|
85
|
+
"placeholder": 1440
|
|
86
|
+
},
|
|
87
|
+
"refresh": {
|
|
88
|
+
"title": "Refresh Interval (in seconds)",
|
|
89
|
+
"type": "integer",
|
|
90
|
+
"placeholder": 60,
|
|
91
|
+
"minimum": 1
|
|
92
|
+
},
|
|
93
|
+
"debug": {
|
|
94
|
+
"title": "Enable debug logs",
|
|
95
|
+
"type": "boolean"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"form": null,
|
|
100
|
+
"display": null
|
|
101
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
let NuHeatAPI = require("./lib/NuHeatAPI.js");
|
|
4
|
+
let NuHeatGroup = require("./lib/NuHeatGroup.js");
|
|
5
|
+
let NuHeatThermostat = require("./lib/NuHeatThermostat.js");
|
|
6
|
+
let NuHeatListener = require("./lib/NuHeatListener.js");
|
|
7
|
+
const logger = require("./lib/logger");
|
|
8
|
+
|
|
9
|
+
let Homebridge, PlatformAccessory, Service, Characteristic, UUIDGen;
|
|
10
|
+
const PLUGIN_NAME = "homebridge-nuheat2";
|
|
11
|
+
const PLATFORM_NAME = "NuHeat";
|
|
12
|
+
|
|
13
|
+
module.exports = function (homebridge) {
|
|
14
|
+
Homebridge = homebridge;
|
|
15
|
+
PlatformAccessory = homebridge.platformAccessory;
|
|
16
|
+
Characteristic = homebridge.hap.Characteristic;
|
|
17
|
+
Service = homebridge.hap.Service;
|
|
18
|
+
UUIDGen = homebridge.hap.uuid;
|
|
19
|
+
|
|
20
|
+
homebridge.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, NuHeatPlatform, true);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
class NuHeatPlatform {
|
|
24
|
+
constructor(log, config, api) {
|
|
25
|
+
if (!config) {
|
|
26
|
+
log.warn("Ignoring NuHeat Platform setup because it is not configured");
|
|
27
|
+
this.disabled = true;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if ((!config.Email && !config.email) || !config.password) {
|
|
32
|
+
log.warn(
|
|
33
|
+
"Ignoring NuHeat Platform setup because it is not configured properly. Missing email or password",
|
|
34
|
+
);
|
|
35
|
+
this.disabled = true;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.config = config;
|
|
40
|
+
this.config.email = this.config.Email || this.config.email;
|
|
41
|
+
this.config.holdLength = Math.min(
|
|
42
|
+
1440,
|
|
43
|
+
Math.max(0, this.config.holdLength || 1440),
|
|
44
|
+
);
|
|
45
|
+
this.api = api;
|
|
46
|
+
this.accessories = [];
|
|
47
|
+
this.log = new logger.Logger(log, this.config.debug || false);
|
|
48
|
+
this.refreshTimer = null;
|
|
49
|
+
this.didFinishLaunching = false;
|
|
50
|
+
|
|
51
|
+
if (this.api?.on) {
|
|
52
|
+
// Modern Homebridge startup restores cached accessories first, then calls didFinishLaunching.
|
|
53
|
+
this.api.on("didFinishLaunching", async () => {
|
|
54
|
+
this.didFinishLaunching = true;
|
|
55
|
+
await this.setupPlatform();
|
|
56
|
+
});
|
|
57
|
+
this.api.on("shutdown", () => {
|
|
58
|
+
this.teardown();
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
this.setupPlatform();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
configureAccessory(accessory) {
|
|
66
|
+
this.accessories.push({ uuid: accessory.UUID, accessory });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async setupPlatform() {
|
|
70
|
+
if (this.disabled) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (this.api?.on && !this.didFinishLaunching) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.log.info("Logging into NuHeat...");
|
|
79
|
+
this.NuHeatAPI = new NuHeatAPI(
|
|
80
|
+
this.config.email,
|
|
81
|
+
this.config.password,
|
|
82
|
+
this.log,
|
|
83
|
+
{
|
|
84
|
+
clientId: this.config.clientId,
|
|
85
|
+
clientSecret: this.config.clientSecret,
|
|
86
|
+
redirectUri: this.config.redirectUri,
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (await this.NuHeatAPI.returnAccessToken()) {
|
|
91
|
+
await this.setupGroups();
|
|
92
|
+
await this.setupThermostats();
|
|
93
|
+
this.cleanupRemovedAccessories();
|
|
94
|
+
|
|
95
|
+
if (this.refreshTimer) {
|
|
96
|
+
clearInterval(this.refreshTimer);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this.refreshTimer = setInterval(
|
|
100
|
+
this.refreshAccessories.bind(this),
|
|
101
|
+
(this.config.refresh || 60) * 1000,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (!this.NuHeatListener) {
|
|
105
|
+
this.NuHeatListener = new NuHeatListener(this.NuHeatAPI, this);
|
|
106
|
+
this.NuHeatListener.connect();
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
this.log.error(
|
|
110
|
+
"Unable to acquire an access token. We will try again later.",
|
|
111
|
+
);
|
|
112
|
+
setTimeout(
|
|
113
|
+
this.setupPlatform.bind(this),
|
|
114
|
+
(this.config.refresh || 60) * 1000,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async setupGroups() {
|
|
120
|
+
const groupArray = this.config.groups || [];
|
|
121
|
+
if (!(this.config.autoPopulateAwayModeSwitches || groupArray.length > 0)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const response = await this.NuHeatAPI.refreshGroups();
|
|
126
|
+
if (!response) {
|
|
127
|
+
this.log.error("Error getting data from NuHeatAPI");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (groupArray.length === 0) {
|
|
132
|
+
this.log.info(
|
|
133
|
+
"No groups defined in config. Auto populating away mode switches by pulling all groups from the account.",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await Promise.all(
|
|
138
|
+
response.map((deviceData) => {
|
|
139
|
+
if (
|
|
140
|
+
!(
|
|
141
|
+
groupArray.length === 0 ||
|
|
142
|
+
groupArray.find(
|
|
143
|
+
(device) =>
|
|
144
|
+
device.groupName == deviceData.groupName && !device.disabled,
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const uuid = UUIDGen.generate(deviceData.groupId.toString());
|
|
152
|
+
let deviceAccessory = false;
|
|
153
|
+
|
|
154
|
+
if (this.accessories.find((accessory) => accessory.uuid === uuid)) {
|
|
155
|
+
deviceAccessory = this.accessories.find(
|
|
156
|
+
(accessory) => accessory.uuid === uuid,
|
|
157
|
+
).accessory;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!deviceAccessory) {
|
|
161
|
+
this.log.info("Creating new away mode switch", deviceData.groupName);
|
|
162
|
+
const accessory = new PlatformAccessory(deviceData.groupName, uuid);
|
|
163
|
+
accessory.addService(
|
|
164
|
+
Service.Switch,
|
|
165
|
+
deviceData.groupName + " Away Mode",
|
|
166
|
+
);
|
|
167
|
+
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
|
|
168
|
+
accessory,
|
|
169
|
+
]);
|
|
170
|
+
deviceAccessory = accessory;
|
|
171
|
+
this.accessories.push({ uuid });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this.accessories.find(
|
|
175
|
+
(accessory) => accessory.uuid === uuid,
|
|
176
|
+
).accessory = new NuHeatGroup(
|
|
177
|
+
this.log,
|
|
178
|
+
deviceData,
|
|
179
|
+
deviceAccessory instanceof NuHeatGroup
|
|
180
|
+
? deviceAccessory.accessory
|
|
181
|
+
: deviceAccessory,
|
|
182
|
+
this.NuHeatAPI,
|
|
183
|
+
Homebridge,
|
|
184
|
+
);
|
|
185
|
+
this.accessories.find(
|
|
186
|
+
(accessory) => accessory.uuid === uuid,
|
|
187
|
+
).existsInConfig = true;
|
|
188
|
+
this.log.info("Loaded away mode switch", deviceData.groupName);
|
|
189
|
+
this.accessories
|
|
190
|
+
.find((accessory) => accessory.uuid === uuid)
|
|
191
|
+
.accessory.updateValues(deviceData);
|
|
192
|
+
}),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async setupThermostats() {
|
|
197
|
+
const deviceArray = this.config.devices || [];
|
|
198
|
+
const response = await this.NuHeatAPI.refreshThermostats();
|
|
199
|
+
|
|
200
|
+
if (!response) {
|
|
201
|
+
this.log.error("Error getting data from NuHeatAPI");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (deviceArray.length === 0) {
|
|
206
|
+
this.log.info(
|
|
207
|
+
"No devices defined in config. Auto populating thermostats by pulling everything from the account.",
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
await Promise.all(
|
|
212
|
+
response.map((deviceData) => {
|
|
213
|
+
if (
|
|
214
|
+
!(
|
|
215
|
+
deviceArray.length === 0 ||
|
|
216
|
+
deviceArray.find(
|
|
217
|
+
(device) =>
|
|
218
|
+
device.serialNumber == deviceData.serialNumber &&
|
|
219
|
+
!device.disabled,
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const uuid = UUIDGen.generate(deviceData.serialNumber.toString());
|
|
227
|
+
let deviceAccessory = false;
|
|
228
|
+
|
|
229
|
+
if (this.accessories.find((accessory) => accessory.uuid === uuid)) {
|
|
230
|
+
deviceAccessory = this.accessories.find(
|
|
231
|
+
(accessory) => accessory.uuid === uuid,
|
|
232
|
+
).accessory;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!deviceAccessory) {
|
|
236
|
+
this.log.info(
|
|
237
|
+
"Creating new thermostat for serial number: " +
|
|
238
|
+
deviceData.serialNumber,
|
|
239
|
+
);
|
|
240
|
+
const accessory = new PlatformAccessory(deviceData.name, uuid);
|
|
241
|
+
accessory.addService(Service.Thermostat, deviceData.name);
|
|
242
|
+
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
|
|
243
|
+
accessory,
|
|
244
|
+
]);
|
|
245
|
+
deviceAccessory = accessory;
|
|
246
|
+
this.accessories.push({ uuid });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
this.accessories.find(
|
|
250
|
+
(accessory) => accessory.uuid === uuid,
|
|
251
|
+
).accessory = new NuHeatThermostat(
|
|
252
|
+
this.log,
|
|
253
|
+
deviceData,
|
|
254
|
+
this.config.holdLength,
|
|
255
|
+
deviceAccessory instanceof NuHeatThermostat
|
|
256
|
+
? deviceAccessory.accessory
|
|
257
|
+
: deviceAccessory,
|
|
258
|
+
this.NuHeatAPI,
|
|
259
|
+
Homebridge,
|
|
260
|
+
);
|
|
261
|
+
this.accessories.find(
|
|
262
|
+
(accessory) => accessory.uuid === uuid,
|
|
263
|
+
).existsInConfig = true;
|
|
264
|
+
this.log.info(
|
|
265
|
+
"Loaded thermostat " +
|
|
266
|
+
deviceData.serialNumber +
|
|
267
|
+
" " +
|
|
268
|
+
deviceData.name,
|
|
269
|
+
);
|
|
270
|
+
this.accessories
|
|
271
|
+
.find((accessory) => accessory.uuid === uuid)
|
|
272
|
+
.accessory.updateValues(deviceData);
|
|
273
|
+
}),
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
cleanupRemovedAccessories() {
|
|
278
|
+
this.accessories.forEach(function (thisAccessory) {
|
|
279
|
+
if (thisAccessory.existsInConfig !== true) {
|
|
280
|
+
try {
|
|
281
|
+
this.log.info(
|
|
282
|
+
"Deleting removed accessory",
|
|
283
|
+
thisAccessory.accessory
|
|
284
|
+
.getService(Service.AccessoryInformation)
|
|
285
|
+
.getCharacteristic(Characteristic.Name)
|
|
286
|
+
.getValue(),
|
|
287
|
+
);
|
|
288
|
+
} catch {
|
|
289
|
+
this.log.info("Deleting removed accessory");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
|
|
293
|
+
thisAccessory.accessory,
|
|
294
|
+
]);
|
|
295
|
+
}
|
|
296
|
+
}, this);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async refreshAccessories() {
|
|
300
|
+
await this.refreshGroups();
|
|
301
|
+
await this.refreshThermostats();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async refreshGroups() {
|
|
305
|
+
this.log.debug("Trying to refresh groups.");
|
|
306
|
+
const response = await this.NuHeatAPI.refreshGroups();
|
|
307
|
+
|
|
308
|
+
if (!response) {
|
|
309
|
+
this.log.error("Error getting data from NuHeatAPI in group refresh");
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
response.forEach(function (deviceData) {
|
|
314
|
+
const thisAccessory = this.accessories.find(
|
|
315
|
+
(accessory) =>
|
|
316
|
+
accessory.uuid === UUIDGen.generate(deviceData.groupId.toString()),
|
|
317
|
+
);
|
|
318
|
+
if (thisAccessory) {
|
|
319
|
+
thisAccessory.accessory.updateValues(deviceData);
|
|
320
|
+
}
|
|
321
|
+
}, this);
|
|
322
|
+
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async refreshThermostats() {
|
|
327
|
+
this.log.debug("Trying to refresh thermostats.");
|
|
328
|
+
const response = await this.NuHeatAPI.refreshThermostats();
|
|
329
|
+
|
|
330
|
+
if (!response) {
|
|
331
|
+
this.log.error("Error getting data from NuHeatAPI in thermostat refresh");
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
response.forEach(function (deviceData) {
|
|
336
|
+
const thisAccessory = this.accessories.find(
|
|
337
|
+
(accessory) =>
|
|
338
|
+
accessory.uuid ===
|
|
339
|
+
UUIDGen.generate(deviceData.serialNumber.toString()),
|
|
340
|
+
);
|
|
341
|
+
if (thisAccessory) {
|
|
342
|
+
thisAccessory.accessory.updateValues(deviceData);
|
|
343
|
+
}
|
|
344
|
+
}, this);
|
|
345
|
+
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
teardown() {
|
|
350
|
+
if (this.refreshTimer) {
|
|
351
|
+
clearInterval(this.refreshTimer);
|
|
352
|
+
this.refreshTimer = null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (this.NuHeatListener) {
|
|
356
|
+
this.NuHeatListener.disconnect();
|
|
357
|
+
this.NuHeatListener = null;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|