@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 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/exclude specific devices by VID range
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": "1000,2000"
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 the platform |
53
- | `ipaddress` | string | Yes | IP address of your InFusion controller |
54
- | `username` | string | No | Username for authentication |
55
- | `password` | string | No | Password for authentication |
56
- | `usecache` | boolean | No | Maintain a persistent last-known-good configuration fallback while still attempting fresh controller discovery on every startup (default: true) |
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 | Comma-separated VID range (min,max) |
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
@@ -10,28 +10,27 @@
10
10
  "name": {
11
11
  "title": "Name",
12
12
  "type": "string",
13
- "default": "VantageControls",
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": "Use previously downloaded configuration (if present) to speed up startup."
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,999999999",
58
- "placeholder": "0,999999999",
59
- "description": "Include only VIDs within this inclusive range, e.g. 0,5000. If you have more than 140 loads, add 2 instances of the vantage platform with different ranges."
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", "ipaddress"],
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,2 @@
1
+ /** HomeKit/HAP 2.3 accessory names must start and end with a letter or number. */
2
+ export declare function sanitizeHomeKitName(raw: string, fallback?: string): string;
@@ -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
+ }
@@ -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
- if (!config || !config.ipaddress) {
17
- this.log.error(`[${settings_1.PLATFORM_NAME}] Missing required config "ipaddress" — plugin will not start.`);
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,999999999',
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
- let temperature = Number(temp);
238
- // Validate temperature range (from original code)
239
- if (temperature > 100) {
240
- temperature = 100;
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
- dev.targetTemp = Math.min(38, targetTemp);
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, targetTemp); // HomeKit max is 25°C
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, targetTemp);
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
- else if (dev.mode === 2)
305
+ }
306
+ else if (dev.mode === 2 && this.isValidTargetTemperature(dev.cooling)) {
292
307
  dev.targetTemp = dev.cooling;
293
- else if (dev.mode === 3)
294
- dev.targetTemp = (dev.temperature <= dev.heating) ? dev.heating : dev.cooling;
295
- svc.updateCharacteristic(C.TargetTemperature, dev.targetTemp);
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;
@@ -9,4 +9,7 @@ export declare class VantagePlatformAccessory {
9
9
  /** Call this both on create and on restore */
10
10
  registerServices(): void;
11
11
  private safeRemoveCharacteristic;
12
+ private hapTargetTemperature;
13
+ private hapHeatingThreshold;
14
+ private hapCoolingThreshold;
12
15
  }