@smarterhomeapp/homebridge-vantage 1.0.8 → 1.1.4
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 +68 -10
- package/config.schema.json +41 -11
- package/dist/homekitName.d.ts +2 -0
- package/dist/homekitName.js +17 -0
- package/dist/platform.d.ts +7 -0
- package/dist/platform.js +163 -19
- package/dist/platformAccessory.d.ts +3 -0
- package/dist/platformAccessory.js +70 -16
- package/dist/types.d.ts +19 -3
- package/dist/vantageInfusion.d.ts +51 -0
- package/dist/vantageInfusion.js +818 -169
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -11,8 +11,9 @@ This plugin replaces the one sold on smarterhome.io allowing anyone to download
|
|
|
11
11
|
- **Blind Control**: Window covering automation
|
|
12
12
|
- **Real-time Updates**: Live status updates from your InFusion controller
|
|
13
13
|
- **SSL Support**: Secure connections to your controller
|
|
14
|
-
- **Device Filtering**: Include
|
|
14
|
+
- **Device Filtering**: Include or exclude devices by type, omit list, or VID range
|
|
15
15
|
- **Safe Configuration Cache**: Downloads fresh controller configuration on every startup while keeping a persistent last-known-good fallback
|
|
16
|
+
- **Controller Discovery**: Uses the configured IP first, then mDNS if that address does not respond
|
|
16
17
|
|
|
17
18
|
## Installation
|
|
18
19
|
|
|
@@ -37,25 +38,37 @@ npm install -g @smarterhomeapp/homebridge-vantage
|
|
|
37
38
|
"username": "your_username",
|
|
38
39
|
"password": "your_password",
|
|
39
40
|
"usecache": true,
|
|
41
|
+
"forceSSL": false,
|
|
42
|
+
"includeLights": true,
|
|
43
|
+
"includeBlinds": true,
|
|
44
|
+
"includeThermostats": true,
|
|
45
|
+
"includeSensors": true,
|
|
40
46
|
"omit": "1234,5678",
|
|
41
|
-
"range": "
|
|
47
|
+
"range": "0-2290"
|
|
42
48
|
}
|
|
43
49
|
]
|
|
44
50
|
}
|
|
45
51
|
```
|
|
46
52
|
|
|
53
|
+
`platform` must stay `"VantageControls"`. `name` is only a label for logs and the Config UI.
|
|
54
|
+
|
|
47
55
|
## Configuration
|
|
48
56
|
|
|
49
57
|
| Parameter | Type | Required | Description |
|
|
50
58
|
|-----------|------|----------|-------------|
|
|
51
|
-
| `platform` | string | Yes | Must be "VantageControls" |
|
|
52
|
-
| `name` | string | Yes | Display name for
|
|
53
|
-
| `ipaddress` | string |
|
|
54
|
-
| `username` | string | No | Username for authentication |
|
|
55
|
-
| `password` | string | No | Password for authentication |
|
|
56
|
-
| `
|
|
59
|
+
| `platform` | string | Yes | Must be `"VantageControls"` |
|
|
60
|
+
| `name` | string | Yes | Display name for this platform entry |
|
|
61
|
+
| `ipaddress` | string | No | Controller IP. Leave blank to discover an InFusion controller with mDNS. If a configured IP stops responding, the plugin also tries mDNS. |
|
|
62
|
+
| `username` | string | No | Username for authentication. If login fails or times out, the plugin still attempts the configuration download. The common typo `admin` is retried as `administrator`. |
|
|
63
|
+
| `password` | string | No | Password for authentication. Used with `username`; a rejected login does not skip discovery. |
|
|
64
|
+
| `forceSSL` | boolean | No | Skip insecure ports and use 3010/2010 directly (default: false) |
|
|
65
|
+
| `usecache` | boolean | No | Maintain a persistent last-known-good configuration fallback while still attempting fresh controller discovery on every startup (default: true). If this is off and a live download fails, existing HomeKit accessories are kept. |
|
|
66
|
+
| `includeLights` | boolean | No | Include Vantage loads, dimmers, relays, RGB loads, and similar switch/light devices (default: true) |
|
|
67
|
+
| `includeBlinds` | boolean | No | Include Vantage blinds, shades, and window coverings (default: true) |
|
|
68
|
+
| `includeThermostats` | boolean | No | Include Vantage thermostats and HVAC zones (default: true) |
|
|
69
|
+
| `includeSensors` | boolean | No | Include dry contacts, temperature sensors, light sensors, and Omni sensors (default: true) |
|
|
57
70
|
| `omit` | string | No | Comma-separated list of VIDs to exclude |
|
|
58
|
-
| `range` | string | No |
|
|
71
|
+
| `range` | string | No | VID range using `min-max`, e.g. `1000-2000`. Legacy comma format, e.g. `1000,2000`, is still supported. |
|
|
59
72
|
|
|
60
73
|
## Configuration Discovery and Cache
|
|
61
74
|
|
|
@@ -65,6 +78,43 @@ When `usecache` is enabled, the persistent cache is only a fallback. If the cont
|
|
|
65
78
|
|
|
66
79
|
The cache is stored under Homebridge's configured storage directory in a plugin-specific `vantage` folder. Cache filenames are keyed by controller IP, for example `vantage-10.0.0.100.dc`, so multiple platform instances using different VID ranges for the same controller share one full-controller cache. Restarts naturally pick up controller/DC changes; users do not need to manually clear the cache.
|
|
67
80
|
|
|
81
|
+
The plugin also writes a copy to `/tmp/vantage.dc` for local tools such as Organize Home. That dump is updated from the configuration Homebridge actually uses: a successful live download, or the persistent last-known-good cache if live discovery fails. Incomplete or rejected downloads are not written there.
|
|
82
|
+
|
|
83
|
+
## HomeKit Accessory Limit and Child Bridges
|
|
84
|
+
|
|
85
|
+
HomeKit allows about 149 accessories on one bridge. If discovery finds more than that, split the controller with two `VantageControls` entries and different `range` values.
|
|
86
|
+
|
|
87
|
+
On Homebridge 2, every extra instance of the same dynamic platform must be a child bridge. Put `_bridge` on each split instance, with a unique username and port. Each child bridge is paired separately in the Home app.
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"platforms": [
|
|
92
|
+
{
|
|
93
|
+
"platform": "VantageControls",
|
|
94
|
+
"name": "Vantage Low",
|
|
95
|
+
"ipaddress": "192.168.1.100",
|
|
96
|
+
"range": "0-2290",
|
|
97
|
+
"_bridge": {
|
|
98
|
+
"username": "0E:AA:BB:CC:DD:02",
|
|
99
|
+
"port": 51839
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"platform": "VantageControls",
|
|
104
|
+
"name": "Vantage High",
|
|
105
|
+
"ipaddress": "192.168.1.100",
|
|
106
|
+
"range": "2291-999999999",
|
|
107
|
+
"_bridge": {
|
|
108
|
+
"username": "0E:AA:BB:CC:DD:03",
|
|
109
|
+
"port": 51840
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Homebridge 1.x still accepts one instance on the main bridge and one child bridge. That layout is rejected on Homebridge 2.
|
|
117
|
+
|
|
68
118
|
## Supported Devices
|
|
69
119
|
|
|
70
120
|
### Lighting
|
|
@@ -81,6 +131,13 @@ The cache is stored under Homebridge's configured storage directory in a plugin-
|
|
|
81
131
|
- **Blinds**: Position control (0-100%)
|
|
82
132
|
- **Shades**: Motorized window coverings
|
|
83
133
|
|
|
134
|
+
### Sensors
|
|
135
|
+
- **Dry contacts**: Contact sensors, including motion-style contacts
|
|
136
|
+
- **Temperature and Omni sensors**: Indoor temperature and similar Omni values
|
|
137
|
+
- **Light sensors**: Ambient light level
|
|
138
|
+
|
|
139
|
+
Load groups and blind groups are not exposed as HomeKit accessories. Use HomeKit rooms or scenes to group devices.
|
|
140
|
+
|
|
84
141
|
## Development
|
|
85
142
|
|
|
86
143
|
1. Clone the repository:
|
|
@@ -107,13 +164,14 @@ npm link
|
|
|
107
164
|
## Troubleshooting
|
|
108
165
|
|
|
109
166
|
### Connection Issues
|
|
110
|
-
- Verify the IP address is correct
|
|
167
|
+
- Verify the IP address is correct, or leave it blank so mDNS can find the controller
|
|
111
168
|
- Check if SSL is required (ports 3010/2010 vs 3001/2001)
|
|
112
169
|
- Ensure username/password are correct if authentication is enabled
|
|
113
170
|
|
|
114
171
|
### Device Not Appearing
|
|
115
172
|
- Check the `omit` and `range` parameters
|
|
116
173
|
- Verify the device VID is within the specified range
|
|
174
|
+
- On Homebridge 2, pair each child bridge in the Home app. The main hub may have no Vantage devices.
|
|
117
175
|
- Check Homebridge logs for discovery errors
|
|
118
176
|
|
|
119
177
|
### Performance Issues
|
package/config.schema.json
CHANGED
|
@@ -10,28 +10,27 @@
|
|
|
10
10
|
"name": {
|
|
11
11
|
"title": "Name",
|
|
12
12
|
"type": "string",
|
|
13
|
-
"default": "
|
|
14
|
-
"description": "A label for this platform entry."
|
|
13
|
+
"default": "Vantage Controls",
|
|
14
|
+
"description": "A label for this platform entry. This is not the Homebridge platform identifier."
|
|
15
15
|
},
|
|
16
16
|
"ipaddress": {
|
|
17
17
|
"title": "Controller IP",
|
|
18
18
|
"type": "string",
|
|
19
|
-
"format": "ipv4",
|
|
20
19
|
"placeholder": "192.168.1.50",
|
|
21
|
-
"description": "IP address of your Vantage InFusion controller."
|
|
20
|
+
"description": "IP address of your Vantage InFusion controller. Leave blank to discover an InFusion controller using mDNS."
|
|
22
21
|
},
|
|
23
22
|
"username": {
|
|
24
23
|
"title": "Username",
|
|
25
24
|
"type": "string",
|
|
26
25
|
"placeholder": "optional",
|
|
27
|
-
"description": "Only needed if your controller requires login."
|
|
26
|
+
"description": "Only needed if your controller requires login. If login is rejected or times out, discovery still continues."
|
|
28
27
|
},
|
|
29
28
|
"password": {
|
|
30
29
|
"title": "Password",
|
|
31
30
|
"type": "string",
|
|
32
31
|
"format": "password",
|
|
33
32
|
"placeholder": "optional",
|
|
34
|
-
"description": "Only needed if your controller requires login."
|
|
33
|
+
"description": "Only needed if your controller requires login. Used with Username."
|
|
35
34
|
},
|
|
36
35
|
"forceSSL": {
|
|
37
36
|
"title": "Force SSL",
|
|
@@ -43,7 +42,31 @@
|
|
|
43
42
|
"title": "Use Local Cache",
|
|
44
43
|
"type": "boolean",
|
|
45
44
|
"default": true,
|
|
46
|
-
"description": "
|
|
45
|
+
"description": "Maintain a persistent last-known-good configuration fallback while still attempting fresh discovery on startup."
|
|
46
|
+
},
|
|
47
|
+
"includeLights": {
|
|
48
|
+
"title": "Include Lights and Loads",
|
|
49
|
+
"type": "boolean",
|
|
50
|
+
"default": true,
|
|
51
|
+
"description": "Create HomeKit accessories for Vantage loads, dimmers, relays, RGB loads, and similar switch/light devices."
|
|
52
|
+
},
|
|
53
|
+
"includeBlinds": {
|
|
54
|
+
"title": "Include Blinds and Shades",
|
|
55
|
+
"type": "boolean",
|
|
56
|
+
"default": true,
|
|
57
|
+
"description": "Create HomeKit accessories for Vantage blinds, shades, and window coverings."
|
|
58
|
+
},
|
|
59
|
+
"includeThermostats": {
|
|
60
|
+
"title": "Include Thermostats",
|
|
61
|
+
"type": "boolean",
|
|
62
|
+
"default": true,
|
|
63
|
+
"description": "Create HomeKit accessories for Vantage thermostats and HVAC zones."
|
|
64
|
+
},
|
|
65
|
+
"includeSensors": {
|
|
66
|
+
"title": "Include Sensors and Contacts",
|
|
67
|
+
"type": "boolean",
|
|
68
|
+
"default": true,
|
|
69
|
+
"description": "Create HomeKit accessories for dry contacts, temperature sensors, light sensors, and Omni sensors."
|
|
47
70
|
},
|
|
48
71
|
"omit": {
|
|
49
72
|
"title": "Omit VIDs",
|
|
@@ -54,12 +77,12 @@
|
|
|
54
77
|
"range": {
|
|
55
78
|
"title": "VID Range",
|
|
56
79
|
"type": "string",
|
|
57
|
-
"default": "0
|
|
58
|
-
"placeholder": "0
|
|
59
|
-
"description": "Include only VIDs within this inclusive range, e.g. 0,5000. If you have more than 140
|
|
80
|
+
"default": "0-999999999",
|
|
81
|
+
"placeholder": "0-999999999",
|
|
82
|
+
"description": "Include only VIDs within this inclusive range, e.g. 0-5000. Legacy comma format such as 0,5000 is still supported. If you have more than 140 accessories, add another VantageControls platform with a different range. On Homebridge 2, every extra instance must be a child bridge."
|
|
60
83
|
}
|
|
61
84
|
},
|
|
62
|
-
"required": ["name"
|
|
85
|
+
"required": ["name"],
|
|
63
86
|
"additionalProperties": false
|
|
64
87
|
},
|
|
65
88
|
|
|
@@ -73,6 +96,13 @@
|
|
|
73
96
|
},
|
|
74
97
|
"forceSSL",
|
|
75
98
|
"usecache",
|
|
99
|
+
{
|
|
100
|
+
"type": "fieldset",
|
|
101
|
+
"title": "Device Types",
|
|
102
|
+
"expandable": true,
|
|
103
|
+
"expanded": true,
|
|
104
|
+
"items": ["includeLights", "includeBlinds", "includeThermostats", "includeSensors"]
|
|
105
|
+
},
|
|
76
106
|
{
|
|
77
107
|
"type": "fieldset",
|
|
78
108
|
"title": "Advanced",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sanitizeHomeKitName = sanitizeHomeKitName;
|
|
4
|
+
const HAP_NAME_PATTERN = /^[\p{L}\p{N}](?:[\p{L}\p{N}\p{Zs}\u2019'&!._:;()/,-]*[\p{L}\p{N}])?$/u;
|
|
5
|
+
/** HomeKit/HAP 2.3 accessory names must start and end with a letter or number. */
|
|
6
|
+
function sanitizeHomeKitName(raw, fallback = 'Vantage') {
|
|
7
|
+
let name = String(raw ?? '').normalize('NFC');
|
|
8
|
+
name = name.replace(/[()]/g, ' ');
|
|
9
|
+
name = name.replace(/[^\p{L}\p{N}\p{Zs}\u2019'&!._:;()/,-]+/gu, ' ');
|
|
10
|
+
name = name.replace(/\p{Zs}+/gu, ' ').trim();
|
|
11
|
+
name = name.replace(/^[^\p{L}\p{N}]+/u, '').replace(/[^\p{L}\p{N}]+$/u, '').trim();
|
|
12
|
+
if (name && HAP_NAME_PATTERN.test(name)) {
|
|
13
|
+
return name;
|
|
14
|
+
}
|
|
15
|
+
name = name.replace(/[^\p{L}\p{N}\p{Zs}]+/gu, ' ').replace(/\p{Zs}+/gu, ' ').trim();
|
|
16
|
+
return name || fallback;
|
|
17
|
+
}
|
package/dist/platform.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export declare class VantagePlatform implements DynamicPlatformPlugin {
|
|
|
9
9
|
private syncedOnce;
|
|
10
10
|
private accessoriesByVid;
|
|
11
11
|
private realtimeWired;
|
|
12
|
+
private invalidThermostatWarnings;
|
|
12
13
|
constructor(log: Logger, config: PlatformConfig, api: API);
|
|
13
14
|
configureAccessory(accessory: PlatformAccessory): void;
|
|
14
15
|
private syncAccessories;
|
|
@@ -16,6 +17,12 @@ export declare class VantagePlatform implements DynamicPlatformPlugin {
|
|
|
16
17
|
private rebuildAccessoryVidMap;
|
|
17
18
|
private wireRealtimeListeners;
|
|
18
19
|
private finalizeLogs;
|
|
20
|
+
private normalizeCurrentTemperature;
|
|
21
|
+
private normalizeTargetTemperature;
|
|
22
|
+
private isValidTargetTemperature;
|
|
23
|
+
private ensureValidThermostatSetpoints;
|
|
24
|
+
private resolveAutoModeTargetTemperature;
|
|
25
|
+
private logInvalidThermostatValue;
|
|
19
26
|
getInfusion(): VantageInfusion;
|
|
20
27
|
getApi(): API;
|
|
21
28
|
}
|
package/dist/platform.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.VantagePlatform = void 0;
|
|
|
4
4
|
const settings_1 = require("./settings");
|
|
5
5
|
const vantageInfusion_1 = require("./vantageInfusion");
|
|
6
6
|
const platformAccessory_1 = require("./platformAccessory");
|
|
7
|
+
const homekitName_1 = require("./homekitName");
|
|
7
8
|
class VantagePlatform {
|
|
8
9
|
constructor(log, config, api) {
|
|
9
10
|
this.log = log;
|
|
@@ -13,19 +14,24 @@ class VantagePlatform {
|
|
|
13
14
|
this.syncedOnce = false; // prevent double-sync when both event + promise fire
|
|
14
15
|
this.accessoriesByVid = new Map();
|
|
15
16
|
this.realtimeWired = false;
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
this.invalidThermostatWarnings = new Set();
|
|
18
|
+
if (!config) {
|
|
19
|
+
this.log.error(`[${settings_1.PLATFORM_NAME}] Missing platform config — plugin will not start.`);
|
|
18
20
|
return;
|
|
19
21
|
}
|
|
20
22
|
// Construct infusion
|
|
21
23
|
this.infusion = new vantageInfusion_1.VantageInfusion({
|
|
22
|
-
ipaddress: String(config.ipaddress),
|
|
24
|
+
ipaddress: config.ipaddress ? String(config.ipaddress) : '',
|
|
23
25
|
username: config.username ?? '',
|
|
24
26
|
password: config.password ?? '',
|
|
25
27
|
usecache: config.usecache ?? true,
|
|
26
28
|
omit: config.omit ?? '',
|
|
27
|
-
range: config.range ?? '0
|
|
29
|
+
range: config.range ?? '0-999999999',
|
|
28
30
|
forceSSL: config.forceSSL ?? false,
|
|
31
|
+
includeLights: config.includeLights ?? true,
|
|
32
|
+
includeBlinds: config.includeBlinds ?? true,
|
|
33
|
+
includeThermostats: config.includeThermostats ?? true,
|
|
34
|
+
includeSensors: config.includeSensors ?? true,
|
|
29
35
|
storagePath: this.api.user.storagePath(),
|
|
30
36
|
log: this.log,
|
|
31
37
|
});
|
|
@@ -79,7 +85,7 @@ class VantagePlatform {
|
|
|
79
85
|
const d = wanted.get(acc.UUID);
|
|
80
86
|
if (d) {
|
|
81
87
|
// Update name/context and re-bind
|
|
82
|
-
acc.displayName = d.name;
|
|
88
|
+
acc.displayName = (0, homekitName_1.sanitizeHomeKitName)(d.name, `VID ${d.vid}`);
|
|
83
89
|
acc.context.device = d;
|
|
84
90
|
new platformAccessory_1.VantagePlatformAccessory(this, acc);
|
|
85
91
|
seen.add(acc.UUID);
|
|
@@ -98,7 +104,7 @@ class VantagePlatform {
|
|
|
98
104
|
const uuid = uuidFor(d);
|
|
99
105
|
if (seen.has(uuid))
|
|
100
106
|
continue;
|
|
101
|
-
const acc = new this.api.platformAccessory(d.name, uuid);
|
|
107
|
+
const acc = new this.api.platformAccessory((0, homekitName_1.sanitizeHomeKitName)(d.name, `VID ${d.vid}`), uuid);
|
|
102
108
|
acc.context.device = d;
|
|
103
109
|
new platformAccessory_1.VantagePlatformAccessory(this, acc);
|
|
104
110
|
toRegister.push(acc);
|
|
@@ -234,11 +240,10 @@ class VantagePlatform {
|
|
|
234
240
|
if (!acc)
|
|
235
241
|
return;
|
|
236
242
|
const dev = acc.context.device;
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
this.log.warn(`Thermostat ${vid} reported invalid temperature ${temp}°C, capping at 100°C`);
|
|
243
|
+
const temperature = this.normalizeCurrentTemperature(temp);
|
|
244
|
+
if (temperature === undefined) {
|
|
245
|
+
this.logInvalidThermostatValue(`${vid}:current`, `Thermostat ${vid} reported invalid temperature ${temp}; ignoring update`);
|
|
246
|
+
return;
|
|
242
247
|
}
|
|
243
248
|
dev.temperature = temperature;
|
|
244
249
|
this.log.info(`thermostatIndoorTemperatureChange (VID=${vid}, Name=${dev.name}, Temp=${temperature})`);
|
|
@@ -274,35 +279,174 @@ class VantagePlatform {
|
|
|
274
279
|
dev.current = current; // Store the calculated current state
|
|
275
280
|
svc.updateCharacteristic(C.CurrentHeatingCoolingState, current);
|
|
276
281
|
svc.updateCharacteristic(C.TargetHeatingCoolingState, mode);
|
|
282
|
+
this.ensureValidThermostatSetpoints(dev, svc);
|
|
277
283
|
}
|
|
278
284
|
else {
|
|
279
|
-
|
|
285
|
+
const normalizedTarget = this.normalizeTargetTemperature(targetTemp);
|
|
286
|
+
if (normalizedTarget === undefined) {
|
|
287
|
+
this.logInvalidThermostatValue(`${vid}:target`, `Thermostat ${vid} reported invalid target temperature ${targetTemp}; ignoring setpoint update`);
|
|
288
|
+
svc.updateCharacteristic(C.TargetHeatingCoolingState, mode);
|
|
289
|
+
this.ensureValidThermostatSetpoints(dev, svc);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
dev.mode = mode;
|
|
293
|
+
dev.targetTemp = normalizedTarget;
|
|
280
294
|
if (mode === 1) {
|
|
281
|
-
dev.heating = Math.min(25,
|
|
295
|
+
dev.heating = Math.min(25, normalizedTarget); // HomeKit max is 25°C
|
|
282
296
|
svc.updateCharacteristic(C.HeatingThresholdTemperature, dev.heating);
|
|
283
297
|
}
|
|
284
298
|
else if (mode === 2) {
|
|
285
|
-
dev.cooling = Math.min(35,
|
|
299
|
+
dev.cooling = Math.min(35, normalizedTarget);
|
|
286
300
|
svc.updateCharacteristic(C.CoolingThresholdTemperature, dev.cooling);
|
|
287
301
|
}
|
|
288
302
|
// Update target temperature based on mode (from original logic)
|
|
289
|
-
if (dev.mode === 1)
|
|
303
|
+
if (dev.mode === 1 && this.isValidTargetTemperature(dev.heating)) {
|
|
290
304
|
dev.targetTemp = dev.heating;
|
|
291
|
-
|
|
305
|
+
}
|
|
306
|
+
else if (dev.mode === 2 && this.isValidTargetTemperature(dev.cooling)) {
|
|
292
307
|
dev.targetTemp = dev.cooling;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
308
|
+
}
|
|
309
|
+
else if (dev.mode === 3) {
|
|
310
|
+
const target = this.resolveAutoModeTargetTemperature(dev);
|
|
311
|
+
if (target !== undefined) {
|
|
312
|
+
dev.targetTemp = target;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (this.isValidTargetTemperature(dev.targetTemp)) {
|
|
316
|
+
svc.updateCharacteristic(C.TargetTemperature, dev.targetTemp);
|
|
317
|
+
}
|
|
296
318
|
svc.updateCharacteristic(C.TargetHeatingCoolingState, dev.mode);
|
|
297
319
|
// this.log.info(`After update - dev.mode=${dev.mode}, dev.targetTemp=${dev.targetTemp}, dev.heating=${dev.heating}, dev.cooling=${dev.cooling}`);
|
|
298
320
|
}
|
|
299
321
|
});
|
|
322
|
+
this.infusion.on('thermostatChildTemperatureChange', (vid, role, temperature) => {
|
|
323
|
+
const acc = this.accessoriesByVid.get(String(vid));
|
|
324
|
+
if (!acc)
|
|
325
|
+
return;
|
|
326
|
+
const dev = acc.context.device;
|
|
327
|
+
const svc = acc.getService(S.Thermostat);
|
|
328
|
+
if (!svc || !dev || dev.type !== 'thermostat')
|
|
329
|
+
return;
|
|
330
|
+
const normalized = role === 'indoor'
|
|
331
|
+
? this.normalizeCurrentTemperature(temperature)
|
|
332
|
+
: this.normalizeTargetTemperature(temperature);
|
|
333
|
+
if (normalized === undefined) {
|
|
334
|
+
this.logInvalidThermostatValue(`${vid}:${role}`, `Thermostat ${vid} reported invalid ${role} temperature ${temperature}; ignoring update`);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (role === 'indoor') {
|
|
338
|
+
dev.temperature = normalized;
|
|
339
|
+
svc.updateCharacteristic(C.CurrentTemperature, normalized);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (role === 'heat') {
|
|
343
|
+
dev.heating = normalized;
|
|
344
|
+
svc.updateCharacteristic(C.HeatingThresholdTemperature, Math.min(25, Math.max(10, normalized)));
|
|
345
|
+
if (dev.mode === 1) {
|
|
346
|
+
dev.targetTemp = normalized;
|
|
347
|
+
svc.updateCharacteristic(C.TargetTemperature, normalized);
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (role === 'cool') {
|
|
352
|
+
dev.cooling = normalized;
|
|
353
|
+
svc.updateCharacteristic(C.CoolingThresholdTemperature, Math.min(35, Math.max(10, normalized)));
|
|
354
|
+
if (dev.mode === 2) {
|
|
355
|
+
dev.targetTemp = normalized;
|
|
356
|
+
svc.updateCharacteristic(C.TargetTemperature, normalized);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
this.infusion.on('contactStatusChange', (vid, contact) => {
|
|
361
|
+
const acc = this.accessoriesByVid.get(String(vid));
|
|
362
|
+
if (!acc)
|
|
363
|
+
return;
|
|
364
|
+
const dev = acc.context.device;
|
|
365
|
+
if (!dev || dev.type !== 'contact')
|
|
366
|
+
return;
|
|
367
|
+
dev.contact = contact;
|
|
368
|
+
acc.getService(S.ContactSensor)?.updateCharacteristic(C.ContactSensorState, contact ? C.ContactSensorState.CONTACT_DETECTED : C.ContactSensorState.CONTACT_NOT_DETECTED);
|
|
369
|
+
});
|
|
370
|
+
this.infusion.on('temperatureSensorChange', (vid, temperature) => {
|
|
371
|
+
const acc = this.accessoriesByVid.get(String(vid));
|
|
372
|
+
if (!acc)
|
|
373
|
+
return;
|
|
374
|
+
const dev = acc.context.device;
|
|
375
|
+
if (!dev || dev.type !== 'temperatureSensor')
|
|
376
|
+
return;
|
|
377
|
+
const normalized = Math.min(100, Math.max(-50, Number(temperature)));
|
|
378
|
+
dev.temperature = normalized;
|
|
379
|
+
acc.getService(S.TemperatureSensor)?.updateCharacteristic(C.CurrentTemperature, normalized);
|
|
380
|
+
});
|
|
381
|
+
this.infusion.on('lightSensorChange', (vid, lightLevel) => {
|
|
382
|
+
const acc = this.accessoriesByVid.get(String(vid));
|
|
383
|
+
if (!acc)
|
|
384
|
+
return;
|
|
385
|
+
const dev = acc.context.device;
|
|
386
|
+
if (!dev || dev.type !== 'lightSensor')
|
|
387
|
+
return;
|
|
388
|
+
const normalized = Math.max(0.0001, Math.min(100000, Number(lightLevel)));
|
|
389
|
+
dev.lightLevel = normalized;
|
|
390
|
+
acc.getService(S.LightSensor)?.updateCharacteristic(C.CurrentAmbientLightLevel, normalized);
|
|
391
|
+
});
|
|
300
392
|
}
|
|
301
393
|
finalizeLogs() {
|
|
302
394
|
// Legacy “store/open for business” logs
|
|
303
395
|
this.log.warn('VantagePlatform for InFusion Controller (end configuration store)');
|
|
304
396
|
this.log.warn('VantagePlatform for InFusion Controller (is open for business)');
|
|
305
397
|
}
|
|
398
|
+
normalizeCurrentTemperature(value) {
|
|
399
|
+
const temperature = Number(value);
|
|
400
|
+
if (!Number.isFinite(temperature) || temperature <= -100 || temperature >= 900)
|
|
401
|
+
return undefined;
|
|
402
|
+
return Math.min(100, Math.max(-50, temperature));
|
|
403
|
+
}
|
|
404
|
+
normalizeTargetTemperature(value) {
|
|
405
|
+
const temperature = Number(value);
|
|
406
|
+
if (!Number.isFinite(temperature) || temperature < 1 || temperature >= 900)
|
|
407
|
+
return undefined;
|
|
408
|
+
return Math.min(38, Math.max(10, temperature));
|
|
409
|
+
}
|
|
410
|
+
isValidTargetTemperature(value) {
|
|
411
|
+
const temperature = Number(value);
|
|
412
|
+
return Number.isFinite(temperature) && temperature >= 10 && temperature <= 38;
|
|
413
|
+
}
|
|
414
|
+
ensureValidThermostatSetpoints(dev, svc) {
|
|
415
|
+
const C = this.api.hap.Characteristic;
|
|
416
|
+
if (!this.isValidTargetTemperature(dev.targetTemp)) {
|
|
417
|
+
dev.targetTemp = 20;
|
|
418
|
+
svc.updateCharacteristic(C.TargetTemperature, 20);
|
|
419
|
+
}
|
|
420
|
+
if (!this.isValidTargetTemperature(dev.heating)) {
|
|
421
|
+
dev.heating = 20;
|
|
422
|
+
svc.updateCharacteristic(C.HeatingThresholdTemperature, 20);
|
|
423
|
+
}
|
|
424
|
+
if (!this.isValidTargetTemperature(dev.cooling)) {
|
|
425
|
+
dev.cooling = 25;
|
|
426
|
+
svc.updateCharacteristic(C.CoolingThresholdTemperature, 25);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
resolveAutoModeTargetTemperature(dev) {
|
|
430
|
+
const heating = this.isValidTargetTemperature(dev.heating) ? Number(dev.heating) : undefined;
|
|
431
|
+
const cooling = this.isValidTargetTemperature(dev.cooling) ? Number(dev.cooling) : undefined;
|
|
432
|
+
if (heating === undefined && cooling === undefined)
|
|
433
|
+
return undefined;
|
|
434
|
+
if (heating === undefined)
|
|
435
|
+
return cooling;
|
|
436
|
+
if (cooling === undefined)
|
|
437
|
+
return heating;
|
|
438
|
+
const current = this.normalizeCurrentTemperature(dev.temperature);
|
|
439
|
+
if (current === undefined)
|
|
440
|
+
return cooling;
|
|
441
|
+
return current <= heating ? heating : cooling;
|
|
442
|
+
}
|
|
443
|
+
logInvalidThermostatValue(key, message) {
|
|
444
|
+
if (this.invalidThermostatWarnings.has(key)) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
this.invalidThermostatWarnings.add(key);
|
|
448
|
+
this.log.warn(`${message}. Future repeats will be suppressed.`);
|
|
449
|
+
}
|
|
306
450
|
// Exposed for accessory class
|
|
307
451
|
getInfusion() {
|
|
308
452
|
return this.infusion;
|