homebridge-nest-accfactory 0.3.0 → 0.3.2
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 +31 -0
- package/README.md +31 -25
- package/config.schema.json +46 -22
- package/dist/HomeKitDevice.js +523 -281
- package/dist/HomeKitHistory.js +357 -341
- package/dist/config.js +69 -87
- package/dist/consts.js +160 -0
- package/dist/devices.js +40 -48
- package/dist/ffmpeg.js +297 -0
- package/dist/index.js +3 -3
- package/dist/nexustalk.js +182 -149
- package/dist/plugins/camera.js +1164 -933
- package/dist/plugins/doorbell.js +26 -32
- package/dist/plugins/floodlight.js +11 -24
- package/dist/plugins/heatlink.js +411 -5
- package/dist/plugins/lock.js +309 -0
- package/dist/plugins/protect.js +240 -71
- package/dist/plugins/tempsensor.js +159 -35
- package/dist/plugins/thermostat.js +891 -455
- package/dist/plugins/weather.js +128 -33
- package/dist/protobuf/nest/services/apigateway.proto +1 -1
- package/dist/protobuf/nestlabs/gateway/v2.proto +1 -1
- package/dist/protobuf/root.proto +1 -0
- package/dist/rtpmuxer.js +186 -0
- package/dist/streamer.js +490 -248
- package/dist/system.js +1741 -2868
- package/dist/utils.js +327 -0
- package/dist/webrtc.js +358 -229
- package/package.json +19 -16
package/dist/plugins/doorbell.js
CHANGED
|
@@ -8,24 +8,33 @@
|
|
|
8
8
|
import { setTimeout, clearTimeout } from 'node:timers';
|
|
9
9
|
|
|
10
10
|
// Define external module requirements
|
|
11
|
-
import NestCamera from './camera.js';
|
|
11
|
+
import NestCamera, { processRawData } from './camera.js';
|
|
12
|
+
export { processRawData };
|
|
12
13
|
|
|
13
14
|
export default class NestDoorbell extends NestCamera {
|
|
14
15
|
static TYPE = 'Doorbell';
|
|
15
|
-
static VERSION = '2025.
|
|
16
|
+
static VERSION = '2025.07.24'; // Code version
|
|
16
17
|
|
|
17
18
|
doorbellTimer = undefined; // Cooldown timer for doorbell events
|
|
18
19
|
switchService = undefined; // HomeKit switch for enabling/disabling chime
|
|
19
20
|
|
|
20
|
-
constructor(accessory, api, log, eventEmitter, deviceData) {
|
|
21
|
-
super(accessory, api, log, eventEmitter, deviceData);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
21
|
// Class functions
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
onAdd() {
|
|
23
|
+
// Setup motion services. This needs to be done before we setup the HomeKit doorbell controller
|
|
24
|
+
if (this.motionServices === undefined) {
|
|
25
|
+
this.createCameraMotionServices();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Setup HomeKit doorbell controller
|
|
29
|
+
// Need to cleanup the CameraOperatingMode service. This is to allow seamless configuration
|
|
30
|
+
// switching between enabling hksv or not
|
|
31
|
+
// Thanks to @bcullman (Brad Ullman) for catching this
|
|
32
|
+
this.accessory.removeService(this.accessory.getService(this.hap.Service.CameraOperatingMode));
|
|
33
|
+
if (this.controller === undefined) {
|
|
34
|
+
// Establish the "camera" controller here as a doorbell specific one
|
|
35
|
+
// when onAdd is called for the base camera class, this will cconfigure our camera controller established here
|
|
36
|
+
this.controller = new this.hap.DoorbellController(this.generateControllerOptions());
|
|
37
|
+
}
|
|
29
38
|
|
|
30
39
|
if (this.deviceData?.has_indoor_chime === true && this.deviceData?.chimeSwitch === true) {
|
|
31
40
|
// Add service to allow automation and enabling/disabling indoor chiming.
|
|
@@ -37,7 +46,7 @@ export default class NestDoorbell extends NestCamera {
|
|
|
37
46
|
onSet: (value) => {
|
|
38
47
|
if (value !== this.deviceData.indoor_chime_enabled) {
|
|
39
48
|
// only change indoor chime status value if different than on-device
|
|
40
|
-
this.
|
|
49
|
+
this.message(NestDoorbell.SET, { uuid: this.deviceData.nest_google_uuid, indoor_chime_enabled: value });
|
|
41
50
|
|
|
42
51
|
this?.log?.info?.('Indoor chime on "%s" was turned', this.deviceData.description, value === true ? 'on' : 'off');
|
|
43
52
|
}
|
|
@@ -61,26 +70,19 @@ export default class NestDoorbell extends NestCamera {
|
|
|
61
70
|
this.switchService !== undefined && this.postSetupDetail('Chime switch');
|
|
62
71
|
}
|
|
63
72
|
|
|
64
|
-
|
|
65
|
-
super.removeDevice();
|
|
66
|
-
|
|
73
|
+
onRemove() {
|
|
67
74
|
clearTimeout(this.doorbellTimer);
|
|
68
75
|
this.doorbellTimer = undefined;
|
|
69
76
|
|
|
70
|
-
|
|
71
|
-
this.accessory.removeService(this.switchService);
|
|
72
|
-
}
|
|
77
|
+
this.accessory.removeService(this.switchService);
|
|
73
78
|
this.switchService = undefined;
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
|
|
81
|
+
onUpdate(deviceData) {
|
|
77
82
|
if (typeof deviceData !== 'object' || this.controller === undefined) {
|
|
78
83
|
return;
|
|
79
84
|
}
|
|
80
85
|
|
|
81
|
-
// Get the camera class todo all its updates first, then we'll handle the doorbell specific stuff
|
|
82
|
-
super.updateDevice(deviceData);
|
|
83
|
-
|
|
84
86
|
if (this.switchService !== undefined) {
|
|
85
87
|
// Update status of indoor chime enable/disable switch
|
|
86
88
|
this.switchService.updateCharacteristic(this.hap.Characteristic.On, deviceData.indoor_chime_enabled);
|
|
@@ -105,17 +107,9 @@ export default class NestDoorbell extends NestCamera {
|
|
|
105
107
|
this.controller.ringDoorbell();
|
|
106
108
|
}
|
|
107
109
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
time: Math.floor(Date.now() / 1000),
|
|
112
|
-
status: 1,
|
|
113
|
-
});
|
|
114
|
-
this.historyService.addHistory(this.controller.doorbellService, {
|
|
115
|
-
time: Math.floor(Date.now() / 1000),
|
|
116
|
-
status: 0,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
110
|
+
// Record a doorbell press and unpress event to our history
|
|
111
|
+
this.history(this.controller.doorbellService, { status: 1 }, { timegap: 2, force: true });
|
|
112
|
+
this.history(this.controller.doorbellService, { status: 0 }, { timegap: 2, force: true });
|
|
119
113
|
}
|
|
120
114
|
});
|
|
121
115
|
}
|
|
@@ -5,23 +5,17 @@
|
|
|
5
5
|
'use strict';
|
|
6
6
|
|
|
7
7
|
// Define external module requirements
|
|
8
|
-
import NestCamera from './camera.js';
|
|
8
|
+
import NestCamera, { processRawData } from './camera.js';
|
|
9
|
+
export { processRawData };
|
|
9
10
|
|
|
10
11
|
export default class NestFloodlight extends NestCamera {
|
|
11
12
|
static TYPE = 'FloodlightCamera';
|
|
12
|
-
static VERSION = '2025.
|
|
13
|
+
static VERSION = '2025.07.25'; // Code version
|
|
13
14
|
|
|
14
15
|
lightService = undefined; // HomeKit light
|
|
15
16
|
|
|
16
|
-
constructor(accessory, api, log, eventEmitter, deviceData) {
|
|
17
|
-
super(accessory, api, log, eventEmitter, deviceData);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
17
|
// Class functions
|
|
21
|
-
|
|
22
|
-
// Call parent to setup the common camera things. Once we return, we can add in the specifics for our floodlight
|
|
23
|
-
super.setupDevice();
|
|
24
|
-
|
|
18
|
+
onAdd() {
|
|
25
19
|
if (this.deviceData.has_light === true) {
|
|
26
20
|
// Add service to for a light, including brightness control
|
|
27
21
|
this.lightService = this.addHKService(this.hap.Service.Lightbulb, '', 1);
|
|
@@ -29,7 +23,7 @@ export default class NestFloodlight extends NestCamera {
|
|
|
29
23
|
props: { minStep: 10 }, // Light only goes in 10% increments
|
|
30
24
|
onSet: (value) => {
|
|
31
25
|
if (value !== this.deviceData.light_brightness) {
|
|
32
|
-
this.
|
|
26
|
+
this.message(NestFloodlight.SET, { uuid: this.deviceData.nest_google_uuid, light_brightness: value });
|
|
33
27
|
|
|
34
28
|
this?.log?.info?.('Floodlight brightness on "%s" was set to "%s %"', this.deviceData.description, value);
|
|
35
29
|
}
|
|
@@ -42,7 +36,7 @@ export default class NestFloodlight extends NestCamera {
|
|
|
42
36
|
this.addHKCharacteristic(this.lightService, this.hap.Characteristic.On, {
|
|
43
37
|
onSet: (value) => {
|
|
44
38
|
if (value !== this.deviceData.light_enabled) {
|
|
45
|
-
this.
|
|
39
|
+
this.message(NestFloodlight.SET, { uuid: this.deviceData.nest_google_uuid, light_enabled: value });
|
|
46
40
|
|
|
47
41
|
this?.log?.info?.('Floodlight on "%s" was turned', this.deviceData.description, value === true ? 'on' : 'off');
|
|
48
42
|
}
|
|
@@ -58,30 +52,23 @@ export default class NestFloodlight extends NestCamera {
|
|
|
58
52
|
if (this.lightService !== undefined) {
|
|
59
53
|
this.accessory.removeService(this.lightService);
|
|
60
54
|
}
|
|
61
|
-
this.lightService
|
|
55
|
+
this.lightService = undefined;
|
|
62
56
|
}
|
|
63
57
|
|
|
64
58
|
// Extra setup details for output
|
|
65
|
-
this.lightService !== undefined && this.
|
|
59
|
+
this.lightService !== undefined && this.postSetupDetail('Light support');
|
|
66
60
|
}
|
|
67
61
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (this.lightService !== undefined) {
|
|
72
|
-
this.accessory.removeService(this.lightService);
|
|
73
|
-
}
|
|
62
|
+
onRemove() {
|
|
63
|
+
this.accessory.removeService(this.lightService);
|
|
74
64
|
this.lightService = undefined;
|
|
75
65
|
}
|
|
76
66
|
|
|
77
|
-
|
|
67
|
+
onUpdate(deviceData) {
|
|
78
68
|
if (typeof deviceData !== 'object' || this.controller === undefined) {
|
|
79
69
|
return;
|
|
80
70
|
}
|
|
81
71
|
|
|
82
|
-
// Get the camera class todo all its updates first, then we'll handle the doorbell specific stuff
|
|
83
|
-
super.updateDevice(deviceData);
|
|
84
|
-
|
|
85
72
|
if (this.lightService !== undefined) {
|
|
86
73
|
// Update status of light, including brightness
|
|
87
74
|
this.lightService.updateCharacteristic(this.hap.Characteristic.On, deviceData.light_enabled);
|
package/dist/plugins/heatlink.js
CHANGED
|
@@ -5,13 +5,419 @@
|
|
|
5
5
|
'use strict';
|
|
6
6
|
|
|
7
7
|
// Define our modules
|
|
8
|
-
import
|
|
8
|
+
import HomeKitDevice from '../HomeKitDevice.js';
|
|
9
|
+
import { processCommonData, adjustTemperature, parseDurationToSeconds } from '../utils.js';
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
// Define constants
|
|
12
|
+
import {
|
|
13
|
+
DATA_SOURCE,
|
|
14
|
+
DEVICE_TYPE,
|
|
15
|
+
HOTWATER_MAX_TEMPERATURE,
|
|
16
|
+
HOTWATER_MIN_TEMPERATURE,
|
|
17
|
+
PROTOBUF_RESOURCES,
|
|
18
|
+
HOTWATER_BOOST_TIMES,
|
|
19
|
+
} from '../consts.js';
|
|
20
|
+
|
|
21
|
+
export default class NestHeatlink extends HomeKitDevice {
|
|
11
22
|
static TYPE = 'Heatlink';
|
|
12
|
-
static VERSION = '2025.
|
|
23
|
+
static VERSION = '2025.08.08'; // Code version
|
|
24
|
+
|
|
25
|
+
thermostatService = undefined; // Hotwater temperature control
|
|
26
|
+
switchService = undefined; // Hotwater heating boost control
|
|
27
|
+
|
|
28
|
+
onAdd() {
|
|
29
|
+
// Patch to avoid characteristic errors when setting initial property ranges
|
|
30
|
+
this.hap.Characteristic.TargetTemperature.prototype.getDefaultValue = () => {
|
|
31
|
+
return this.deviceData.hotwaterMinTemp; // start at minimum heating threshold
|
|
32
|
+
};
|
|
33
|
+
this.hap.Characteristic.TargetHeatingCoolingState.prototype.getDefaultValue = () => {
|
|
34
|
+
return this.hap.Characteristic.TargetHeatingCoolingState.HEAT; // Only heating
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// If the heatlink supports hotwater temperature control
|
|
38
|
+
// Setup the thermostat service if not already present on the accessory, and link it to the Eve app if configured to do so
|
|
39
|
+
if (this.deviceData?.has_hot_water_control === true && this.deviceData?.has_hot_water_temperature === true) {
|
|
40
|
+
this.#setupHotwaterTemperature();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (this.deviceData?.has_hot_water_control === false || this.deviceData?.has_hot_water_temperature === false) {
|
|
44
|
+
// No longer have hotwater temperature control configured and service present, so removed it
|
|
45
|
+
this.thermostatService = this.accessory.getService(this.hap.Service.Thermostat);
|
|
46
|
+
if (this.thermostatService !== undefined) {
|
|
47
|
+
this.accessory.removeService(this.thermostatService);
|
|
48
|
+
}
|
|
49
|
+
this.thermostatService = undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Setup hotwater boost heating service if supported by the thermostat and not already present on the accessory
|
|
53
|
+
if (this.deviceData?.has_hot_water_control === true) {
|
|
54
|
+
this.#setupHotwaterBoost();
|
|
55
|
+
}
|
|
56
|
+
if (this.deviceData?.has_hot_water_control === false) {
|
|
57
|
+
// No longer have hotwater heating configured and service present, so removed it
|
|
58
|
+
this.switchService = this.accessory.getService(this.hap.Service.Switch);
|
|
59
|
+
if (this.switchService !== undefined) {
|
|
60
|
+
this.accessory.removeService(this.switchService);
|
|
61
|
+
}
|
|
62
|
+
this.switchService = undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Extra setup details for output
|
|
66
|
+
this.thermostatService !== undefined &&
|
|
67
|
+
this.postSetupDetail('Temperature control (' + this.deviceData.hotwaterMinTemp + '–' + this.deviceData.hotwaterMaxTemp + '°C)');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
onRemove() {
|
|
71
|
+
this.accessory.removeService(this.thermostatService);
|
|
72
|
+
this.accessory.removeService(this.switchService);
|
|
73
|
+
this.thermostatService = undefined;
|
|
74
|
+
this.switchService = undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
onUpdate(deviceData) {
|
|
78
|
+
if (typeof deviceData !== 'object') {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// TODO dynamic changes to hotwater setup ie: boost control and temperature control
|
|
13
83
|
|
|
14
|
-
|
|
15
|
-
|
|
84
|
+
if (this.thermostatService !== undefined) {
|
|
85
|
+
// Update when we have hot water temperature control
|
|
86
|
+
this.thermostatService.updateCharacteristic(
|
|
87
|
+
this.hap.Characteristic.TemperatureDisplayUnits,
|
|
88
|
+
deviceData.temperature_scale.toUpperCase() === 'C'
|
|
89
|
+
? this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS
|
|
90
|
+
: this.hap.Characteristic.TemperatureDisplayUnits.FAHRENHEIT,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
this.thermostatService.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, deviceData.current_water_temperature);
|
|
94
|
+
this.thermostatService.updateCharacteristic(this.hap.Characteristic.TargetTemperature, deviceData.hot_water_temperature);
|
|
95
|
+
this.thermostatService.updateCharacteristic(
|
|
96
|
+
this.hap.Characteristic.CurrentHeatingCoolingState,
|
|
97
|
+
deviceData.hot_water_active === true
|
|
98
|
+
? this.hap.Characteristic.CurrentHeatingCoolingState.HEAT
|
|
99
|
+
: this.hap.Characteristic.CurrentHeatingCoolingState.OFF,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
this.thermostatService.updateCharacteristic(this.hap.Characteristic.StatusActive, deviceData.online === true);
|
|
103
|
+
|
|
104
|
+
// Log thermostat metrics to history only if changed to previous recording
|
|
105
|
+
this.history(this.thermostatService, {
|
|
106
|
+
status: deviceData.hot_water_active === true ? 2 : 0, // 2 - heating water, 0 - not heating water
|
|
107
|
+
temperature: deviceData.current_water_temperature,
|
|
108
|
+
target: deviceData.hot_water_temperature,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Update our internal data with properties Eve will need to process then Notify Eve App of device status changes if linked
|
|
112
|
+
this.deviceData.online = deviceData.online;
|
|
113
|
+
this.historyService?.updateEveHome?.(this.thermostatService);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (this.switchService !== undefined) {
|
|
117
|
+
// Hotwater boost status on or off
|
|
118
|
+
this.switchService.updateCharacteristic(this.hap.Characteristic.On, deviceData.hot_water_boost_active === true);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
onMessage(type, message) {
|
|
123
|
+
if (typeof type !== 'string' || type === '' || message === null || typeof message !== 'object' || message?.constructor !== Object) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (type === HomeKitDevice?.HISTORY?.GET) {
|
|
128
|
+
// Extend Eve Thermo GET payload with device state
|
|
129
|
+
message.attached = this.deviceData.online === true;
|
|
130
|
+
return message;
|
|
131
|
+
}
|
|
16
132
|
}
|
|
133
|
+
|
|
134
|
+
#setupHotwaterTemperature() {
|
|
135
|
+
this.thermostatService = this.addHKService(this.hap.Service.Thermostat, '', 1, { messages: this.message.bind(this) });
|
|
136
|
+
this.thermostatService.setPrimaryService();
|
|
137
|
+
|
|
138
|
+
this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.StatusActive);
|
|
139
|
+
|
|
140
|
+
this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.TemperatureDisplayUnits, {
|
|
141
|
+
onSet: (value) => {
|
|
142
|
+
let unit = value === this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS ? 'C' : 'F';
|
|
143
|
+
|
|
144
|
+
this.message(HomeKitDevice.SET, {
|
|
145
|
+
uuid: this.deviceData.associated_thermostat,
|
|
146
|
+
temperature_scale: unit,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
this.thermostatService.updateCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits, value);
|
|
150
|
+
|
|
151
|
+
this?.log?.info?.('Set temperature units on heatlink "%s" to "%s"', this.deviceData.description, unit === 'C' ? '°C' : '°F');
|
|
152
|
+
},
|
|
153
|
+
onGet: () => {
|
|
154
|
+
return this.deviceData.temperature_scale.toUpperCase() === 'C'
|
|
155
|
+
? this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS
|
|
156
|
+
: this.hap.Characteristic.TemperatureDisplayUnits.FAHRENHEIT;
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.CurrentTemperature, {
|
|
161
|
+
props: { minStep: 0.5 },
|
|
162
|
+
onGet: () => {
|
|
163
|
+
return this.deviceData.current_water_temperature;
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.TargetTemperature, {
|
|
168
|
+
props: {
|
|
169
|
+
minStep: 0.5,
|
|
170
|
+
minValue: this.deviceData.hotwaterMinTemp,
|
|
171
|
+
maxValue: this.deviceData.hotwaterMaxTemp,
|
|
172
|
+
},
|
|
173
|
+
onSet: (value) => {
|
|
174
|
+
if (value !== this.deviceData.hot_water_temperature) {
|
|
175
|
+
this.message(HomeKitDevice.SET, { uuid: this.deviceData.associated_thermostat, hot_water_temperature: value });
|
|
176
|
+
|
|
177
|
+
this?.log?.info?.('Set hotwater boiler temperature on heatlink "%s" to "%s °C"', this.deviceData.description, value);
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
onGet: () => {
|
|
181
|
+
return this.deviceData.hot_water_temperature;
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// We only support heating for this thermostat service
|
|
186
|
+
this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.TargetHeatingCoolingState, {
|
|
187
|
+
props: {
|
|
188
|
+
validValues: [this.hap.Characteristic.TargetHeatingCoolingState.HEAT],
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#setupHotwaterBoost() {
|
|
194
|
+
this.switchService = this.addHKService(this.hap.Service.Switch, '', 1);
|
|
195
|
+
|
|
196
|
+
this.addHKCharacteristic(this.switchService, this.hap.Characteristic.On, {
|
|
197
|
+
onSet: (value) => {
|
|
198
|
+
if (value !== this.deviceData.hot_water_boost_active) {
|
|
199
|
+
this.message(HomeKitDevice.SET, {
|
|
200
|
+
uuid: this.deviceData.associated_thermostat,
|
|
201
|
+
hot_water_boost_active: { state: value === true, time: this.deviceData.hotwaterBoostTime },
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
this.switchService.updateCharacteristic(this.hap.Characteristic.On, value);
|
|
205
|
+
|
|
206
|
+
this?.log?.info?.(
|
|
207
|
+
'Set hotwater boost heating on heatlink "%s" to "%s"',
|
|
208
|
+
this.deviceData.description,
|
|
209
|
+
value === true
|
|
210
|
+
? 'On for ' +
|
|
211
|
+
(this.deviceData.hotwaterBoostTime >= 3600
|
|
212
|
+
? Math.floor(this.deviceData.hotwaterBoostTime / 3600) +
|
|
213
|
+
' hr' +
|
|
214
|
+
(Math.floor(this.deviceData.hotwaterBoostTime / 3600) > 1 ? 's ' : ' ')
|
|
215
|
+
: '') +
|
|
216
|
+
Math.floor((this.deviceData.hotwaterBoostTime % 3600) / 60) +
|
|
217
|
+
' min' +
|
|
218
|
+
(Math.floor((this.deviceData.hotwaterBoostTime % 3600) / 60) !== 1 ? 's' : '')
|
|
219
|
+
: 'Off',
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
onGet: () => {
|
|
224
|
+
return this.deviceData?.hot_water_boost_active === true;
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Function to process our RAW Nest or Google for this device type
|
|
231
|
+
export function processRawData(log, rawData, config, deviceType = undefined) {
|
|
232
|
+
if (
|
|
233
|
+
rawData === null ||
|
|
234
|
+
typeof rawData !== 'object' ||
|
|
235
|
+
rawData?.constructor !== Object ||
|
|
236
|
+
typeof config !== 'object' ||
|
|
237
|
+
config?.constructor !== Object
|
|
238
|
+
) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Process data for any heatlink devices we have in the raw data
|
|
243
|
+
// We do this using any thermostat data
|
|
244
|
+
let devices = {};
|
|
245
|
+
Object.entries(rawData)
|
|
246
|
+
.filter(
|
|
247
|
+
([key, value]) =>
|
|
248
|
+
key.startsWith('device.') === true ||
|
|
249
|
+
(key.startsWith('DEVICE_') === true && PROTOBUF_RESOURCES.THERMOSTAT.includes(value.value?.device_info?.typeName) === true),
|
|
250
|
+
)
|
|
251
|
+
.forEach(([object_key, value]) => {
|
|
252
|
+
let tempDevice = {};
|
|
253
|
+
try {
|
|
254
|
+
if (
|
|
255
|
+
value?.source === DATA_SOURCE.GOOGLE &&
|
|
256
|
+
value.value?.configuration_done?.deviceReady === true &&
|
|
257
|
+
typeof rawData?.[value.value?.device_info?.pairerId?.resourceId] === 'object' &&
|
|
258
|
+
['HEAT_LINK_CONNECTION_TYPE_ON_OFF', 'HEAT_LINK_CONNECTION_TYPE_OPENTHERM'].some(
|
|
259
|
+
(type) =>
|
|
260
|
+
value.value?.heat_link_settings?.heatConnectionType === type ||
|
|
261
|
+
value.value?.heat_link_settings?.hotWaterConnectionType === type,
|
|
262
|
+
) === true &&
|
|
263
|
+
(value.value?.heat_link?.heatLinkModel?.value?.trim?.() ?? '') !== '' &&
|
|
264
|
+
(value.value?.heat_link?.heatLinkSerialNumber?.value?.trim?.() ?? '') !== '' &&
|
|
265
|
+
(value.value?.heat_link?.heatLinkSwVersion?.value?.trim?.() ?? '') !== ''
|
|
266
|
+
) {
|
|
267
|
+
tempDevice = processCommonData(
|
|
268
|
+
object_key,
|
|
269
|
+
{
|
|
270
|
+
type: DEVICE_TYPE.HEATLINK,
|
|
271
|
+
model:
|
|
272
|
+
value.value.heat_link.heatLinkModel.value.startsWith('Amber-2') === true
|
|
273
|
+
? 'Heatlink for Learning Thermostat (3rd gen, EU)'
|
|
274
|
+
: value.value.heat_link.heatLinkModel.value.startsWith('Amber-1') === true
|
|
275
|
+
? 'Heatlink for Learning Thermostat (2nd gen, EU)'
|
|
276
|
+
: value.value.heat_link.heatLinkModel.value.includes('Agate') === true
|
|
277
|
+
? 'Heatlink for Thermostat E (1st gen, EU)'
|
|
278
|
+
: 'Heatlink (unknown - ' + value.value.heat_link.heatLinkModel + ')',
|
|
279
|
+
serialNumber: value.value.heat_link.heatLinkSerialNumber.value,
|
|
280
|
+
softwareVersion: value.value.heat_link.heatLinkSwVersion.value,
|
|
281
|
+
associated_thermostat: object_key, // Thermostat linked to
|
|
282
|
+
temperature_scale: value.value?.display_settings?.temperatureScale === 'TEMPERATURE_SCALE_F' ? 'F' : 'C',
|
|
283
|
+
online: value.value?.liveness?.status === 'LIVENESS_DEVICE_STATUS_ONLINE', // Use thermostat online status
|
|
284
|
+
has_hot_water_control: value.value?.hvac_equipment_capabilities?.hasHotWaterControl === true,
|
|
285
|
+
hot_water_active: value.value?.hot_water_trait?.boilerActive === true,
|
|
286
|
+
hot_water_boost_active:
|
|
287
|
+
isNaN(value.value?.hot_water_settings?.boostTimerEnd?.seconds) === false &&
|
|
288
|
+
Number(value.value.hot_water_settings.boostTimerEnd.seconds) > 0,
|
|
289
|
+
has_hot_water_temperature: value.value?.hvac_equipment_capabilities?.hasHotWaterTemperature === true,
|
|
290
|
+
current_water_temperature:
|
|
291
|
+
isNaN(value.value?.hot_water_trait?.temperature?.value) === false
|
|
292
|
+
? adjustTemperature(Number(value.value.hot_water_trait.temperature.value), 'C', 'C', true)
|
|
293
|
+
: 0.0,
|
|
294
|
+
hot_water_temperature:
|
|
295
|
+
isNaN(value.value?.hot_water_settings?.temperature?.value) === false
|
|
296
|
+
? adjustTemperature(Number(value.value.hot_water_settings.temperature.value), 'C', 'C', true)
|
|
297
|
+
: 0.0,
|
|
298
|
+
description: String(value.value?.label?.label ?? ''),
|
|
299
|
+
location: String(
|
|
300
|
+
[
|
|
301
|
+
...Object.values(
|
|
302
|
+
rawData?.[value.value?.device_info?.pairerId?.resourceId]?.value?.located_annotations?.predefinedWheres || {},
|
|
303
|
+
),
|
|
304
|
+
...Object.values(
|
|
305
|
+
rawData?.[value.value?.device_info?.pairerId?.resourceId]?.value?.located_annotations?.customWheres || {},
|
|
306
|
+
),
|
|
307
|
+
].find((where) => where?.whereId?.resourceId === value.value?.device_located_settings?.whereAnnotationRid?.resourceId)
|
|
308
|
+
?.label?.literal ?? '',
|
|
309
|
+
),
|
|
310
|
+
},
|
|
311
|
+
config,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (
|
|
316
|
+
value?.source === DATA_SOURCE.NEST &&
|
|
317
|
+
typeof rawData?.['track.' + value.value?.serial_number] === 'object' &&
|
|
318
|
+
typeof rawData?.['link.' + value.value?.serial_number] === 'object' &&
|
|
319
|
+
typeof rawData?.['shared.' + value.value?.serial_number] === 'object' &&
|
|
320
|
+
typeof rawData?.['where.' + rawData?.['link.' + value.value?.serial_number]?.value?.structure?.split?.('.')[1]] === 'object' &&
|
|
321
|
+
['onoff', 'opentherm'].some(
|
|
322
|
+
(type) => value?.value?.heat_link_heat_type === type || value?.value?.heat_link_hot_water_type === type,
|
|
323
|
+
) === true &&
|
|
324
|
+
(value.value?.heat_link_model?.trim?.() ?? '') !== '' &&
|
|
325
|
+
(value.value?.heat_link_serial_number?.trim?.() ?? '') !== '' &&
|
|
326
|
+
(value.value?.heat_link_sw_version?.trim?.() ?? '') !== ''
|
|
327
|
+
) {
|
|
328
|
+
tempDevice = processCommonData(
|
|
329
|
+
object_key,
|
|
330
|
+
{
|
|
331
|
+
type: DEVICE_TYPE.HEATLINK,
|
|
332
|
+
model:
|
|
333
|
+
value.value.heat_link_model.startsWith('Amber-2') === true
|
|
334
|
+
? 'Heatlink for Learning Thermostat (3rd gen, EU)'
|
|
335
|
+
: value.value.heat_link_model.startsWith('Amber-1') === true
|
|
336
|
+
? 'Heatlink for Learning Thermostat (2nd gen, EU)'
|
|
337
|
+
: value.value.heat_link_model.includes('Agate') === true
|
|
338
|
+
? 'Heatlink for Thermostat E (1st gen, EU)'
|
|
339
|
+
: 'Heatlink (unknown - ' + value.value.heat_link_model + ')',
|
|
340
|
+
serialNumber: value.value.heat_link_serial_number,
|
|
341
|
+
softwareVersion: value.value.heat_link_sw_version,
|
|
342
|
+
associated_thermostat: object_key, // Thermostat linked to
|
|
343
|
+
temperature_scale: value.value.temperature_scale.toUpperCase() === 'F' ? 'F' : 'C',
|
|
344
|
+
online: rawData?.['track.' + value.value.serial_number]?.value?.online === true, // Use thermostat online status
|
|
345
|
+
has_hot_water_control: value.value.has_hot_water_control === true,
|
|
346
|
+
hot_water_active: value.value?.hot_water_active === true,
|
|
347
|
+
hot_water_boost_active:
|
|
348
|
+
isNaN(value.value?.hot_water_boost_time_to_end) === false && Number(value.value.hot_water_boost_time_to_end) > 0,
|
|
349
|
+
has_hot_water_temperature: value.value?.has_hot_water_temperature === true,
|
|
350
|
+
hot_water_temperature:
|
|
351
|
+
isNaN(value.value?.hot_water_temperature) === false
|
|
352
|
+
? adjustTemperature(Number(value.value.hot_water_temperature), 'C', 'C', true)
|
|
353
|
+
: 0.0,
|
|
354
|
+
current_water_temperature:
|
|
355
|
+
isNaN(value.value?.current_water_temperature) === false
|
|
356
|
+
? adjustTemperature(Number(value.value.current_water_temperature), 'C', 'C', true)
|
|
357
|
+
: 0.0,
|
|
358
|
+
description: String(rawData?.['shared.' + value.value.serial_number]?.value?.name ?? ''),
|
|
359
|
+
location: String(
|
|
360
|
+
rawData?.[
|
|
361
|
+
'where.' + rawData?.['link.' + value.value.serial_number]?.value?.structure?.split?.('.')[1]
|
|
362
|
+
]?.value?.wheres?.find((where) => where?.where_id === value.value.where_id)?.name ?? '',
|
|
363
|
+
),
|
|
364
|
+
},
|
|
365
|
+
config,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
// eslint-disable-next-line no-unused-vars
|
|
369
|
+
} catch (error) {
|
|
370
|
+
log?.debug?.('Error processing heatlink data for "%s"', object_key);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (
|
|
374
|
+
Object.entries(tempDevice).length !== 0 &&
|
|
375
|
+
typeof devices[tempDevice.serialNumber] === 'undefined' &&
|
|
376
|
+
(deviceType === undefined || (typeof deviceType === 'string' && deviceType !== '' && tempDevice.type === deviceType))
|
|
377
|
+
) {
|
|
378
|
+
let deviceOptions = config?.devices?.find(
|
|
379
|
+
(device) => device?.serialNumber?.toUpperCase?.() === tempDevice?.serialNumber?.toUpperCase?.(),
|
|
380
|
+
);
|
|
381
|
+
// Insert any extra options we've read in from configuration file for this device
|
|
382
|
+
tempDevice.eveHistory = config.options.eveHistory === true || deviceOptions?.eveHistory === true;
|
|
383
|
+
|
|
384
|
+
// Process hotwater boost time.. we only allow values matching app
|
|
385
|
+
tempDevice.hotwaterBoostTime = parseDurationToSeconds(deviceOptions?.hotwaterBoostTime, {
|
|
386
|
+
defaultValue: 1800, // 30 mins
|
|
387
|
+
min: 1800, // 30 mins
|
|
388
|
+
max: 7200, // 2 hrs
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
tempDevice.hotwaterBoostTime = HOTWATER_BOOST_TIMES.reduce((a, b) =>
|
|
392
|
+
Math.abs(tempDevice.hotwaterBoostTime - a) < Math.abs(tempDevice.hotwaterBoostTime - b) ? a : b,
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
tempDevice.hotwaterMinTemp =
|
|
396
|
+
isNaN(deviceOptions?.hotwaterMinTemp) === false
|
|
397
|
+
? adjustTemperature(deviceOptions.hotwaterMinTemp, 'C', 'C', true)
|
|
398
|
+
: typeof deviceOptions?.hotwaterMinTemp === 'string' && /^([0-9.]+)\s*([CF])$/i.test(deviceOptions.hotwaterMinTemp)
|
|
399
|
+
? adjustTemperature(
|
|
400
|
+
parseFloat(deviceOptions.hotwaterMinTemp.match(/^([0-9.]+)\s*([CF])$/i)[1]),
|
|
401
|
+
deviceOptions.hotwaterMinTemp.match(/^([0-9.]+)\s*([CF])$/i)[2],
|
|
402
|
+
'C',
|
|
403
|
+
true,
|
|
404
|
+
)
|
|
405
|
+
: HOTWATER_MIN_TEMPERATURE; // 30c minimum
|
|
406
|
+
|
|
407
|
+
tempDevice.hotwaterMaxTemp =
|
|
408
|
+
isNaN(deviceOptions?.hotwaterMaxTemp) === false
|
|
409
|
+
? adjustTemperature(deviceOptions.hotwaterMaxTemp, 'C', 'C', true)
|
|
410
|
+
: typeof deviceOptions?.hotwaterMaxTemp === 'string' && /^([0-9.]+)\s*([CF])$/i.test(deviceOptions.hotwaterMaxTemp)
|
|
411
|
+
? adjustTemperature(
|
|
412
|
+
parseFloat(deviceOptions.hotwaterMaxTemp.match(/^([0-9.]+)\s*([CF])$/i)[1]),
|
|
413
|
+
deviceOptions.hotwaterMaxTemp.match(/^([0-9.]+)\s*([CF])$/i)[2],
|
|
414
|
+
'C',
|
|
415
|
+
true,
|
|
416
|
+
)
|
|
417
|
+
: HOTWATER_MAX_TEMPERATURE; // 70c maximum
|
|
418
|
+
devices[tempDevice.serialNumber] = tempDevice; // Store processed device
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
return devices;
|
|
17
423
|
}
|