@homebridge-plugins/homebridge-tado 9.2.0 → 9.3.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 +15 -1
- package/config.schema.json +6 -1
- package/package.json +4 -4
- package/src/accessories/heatercooler.js +84 -50
- package/src/helper/handler.js +190 -60
- package/src/helper/update-buffer.js +23 -6
- package/src/tado/tado-config.js +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
# v9.3.0 - 2026-06-18
|
|
4
|
+
- Fix: Treat tado zone id `0` as a valid zone id when resolving and updating configured zones (#169)
|
|
5
|
+
- Fix: Resolve missing `HOT_WATER` zone ids for temperature-controlled boiler `HeaterCooler` accessories before sending overlays (#169)
|
|
6
|
+
- Fix: Prevent invalid overlay requests from being sent when a zone id cannot be resolved (#169)
|
|
7
|
+
- Fix: Use a valid configured `HOT_WATER` target temperature when Apple Home turns the accessory back on without providing one (#169)
|
|
8
|
+
- Fix: Clamp only `HOT_WATER` target temperatures to the configured supported range before sending overlays (#169)
|
|
9
|
+
- Fix: Apply `HeaterCooler` temperature limits before restoring cached threshold values to avoid HomeKit range warnings for `HOT_WATER` zones (#169)
|
|
10
|
+
- Improve: Refine update buffer handling for accessory state and temperature updates
|
|
11
|
+
- Update dependencies
|
|
12
|
+
|
|
13
|
+
# v9.2.1 - 2026-06-06
|
|
14
|
+
- Fix: HeaterCooler temperature limits are no longer overridden by an unset or zero `minValue`/`maxValue` from config (#169)
|
|
15
|
+
- Fix: `accTypeBoiler` is now hidden in the UI when `boilerTempSupport` is enabled, as temperature-controlled hot water devices are always exposed as HeaterCooler (#169)
|
|
16
|
+
|
|
3
17
|
# v9.2.0 - 2026-06-05
|
|
4
|
-
- Fix: Incorrect warning "CoolingThresholdTemperature exceeded maximum of 35" for HOT_WATER zones with `boilerTempSupport: true` (#169)
|
|
18
|
+
- Fix: Incorrect warning "CoolingThresholdTemperature exceeded maximum of 35" for `HOT_WATER` zones with `boilerTempSupport: true` (#169)
|
|
5
19
|
- Fix: Config UI: show AC-specific fields for AIR_CONDITIONING zones (#173)
|
|
6
20
|
- Update dependencies
|
|
7
21
|
|
package/config.schema.json
CHANGED
|
@@ -682,7 +682,12 @@
|
|
|
682
682
|
},
|
|
683
683
|
"items": [
|
|
684
684
|
"homes[].zones[].boilerTempSupport",
|
|
685
|
-
|
|
685
|
+
{
|
|
686
|
+
"key": "homes[].zones[].accTypeBoiler",
|
|
687
|
+
"condition": {
|
|
688
|
+
"functionBody": "try { return !model.homes[arrayIndices[0]].zones[arrayIndices[1]].boilerTempSupport } catch(e){ return false }"
|
|
689
|
+
}
|
|
690
|
+
}
|
|
686
691
|
]
|
|
687
692
|
},
|
|
688
693
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homebridge-plugins/homebridge-tado",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.3.0",
|
|
4
4
|
"description": "Homebridge plugin for controlling tado° devices.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -35,15 +35,15 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@homebridge/plugin-ui-utils": "^2.2.4",
|
|
37
37
|
"fakegato-history": "^0.6.7",
|
|
38
|
-
"form-data": "^4.0.
|
|
38
|
+
"form-data": "^4.0.6",
|
|
39
39
|
"fs-extra": "^11.3.5",
|
|
40
40
|
"got": "^15.0.5",
|
|
41
41
|
"moment": "^2.30.1"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"eslint": "^10.
|
|
44
|
+
"eslint": "^10.5.0",
|
|
45
45
|
"@eslint/js": "^10.0.1",
|
|
46
46
|
"globals": "^17.6.0",
|
|
47
|
-
"prettier": "^3.8.
|
|
47
|
+
"prettier": "^3.8.4"
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -16,13 +16,73 @@ export default class HeaterCoolerAccessory {
|
|
|
16
16
|
|
|
17
17
|
this.autoDelayTimeout = null;
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
const temperatureConfig = this.getTemperatureConfig();
|
|
20
|
+
const isHotWater = this.accessory.context.config.type === 'HOT_WATER';
|
|
21
|
+
const defaultTemperature = isHotWater ? temperatureConfig.minValue : 20;
|
|
22
|
+
const clampToRange = isHotWater;
|
|
23
|
+
|
|
24
|
+
this.updateBuffer = new TadoUpdateBuffer(
|
|
25
|
+
(target, value) => {
|
|
26
|
+
return this.deviceHandler.setStates(this.accessory, this.accessories, target, value);
|
|
27
|
+
},
|
|
28
|
+
preferSiriTemperature,
|
|
29
|
+
defaultTemperature,
|
|
30
|
+
temperatureConfig.minValue,
|
|
31
|
+
temperatureConfig.maxValue,
|
|
32
|
+
clampToRange
|
|
33
|
+
);
|
|
22
34
|
|
|
23
35
|
this.getService();
|
|
24
36
|
}
|
|
25
37
|
|
|
38
|
+
getTemperatureConfig() {
|
|
39
|
+
let minValue =
|
|
40
|
+
this.accessory.context.config.type === 'HOT_WATER'
|
|
41
|
+
? this.accessory.context.config.temperatureUnit === 'FAHRENHEIT'
|
|
42
|
+
? 86
|
|
43
|
+
: 30
|
|
44
|
+
: this.accessory.context.config.temperatureUnit === 'FAHRENHEIT'
|
|
45
|
+
? 41
|
|
46
|
+
: 5;
|
|
47
|
+
|
|
48
|
+
let maxValue =
|
|
49
|
+
this.accessory.context.config.type === 'HOT_WATER'
|
|
50
|
+
? this.accessory.context.config.temperatureUnit === 'FAHRENHEIT'
|
|
51
|
+
? 149
|
|
52
|
+
: 65
|
|
53
|
+
: this.accessory.context.config.temperatureUnit === 'FAHRENHEIT'
|
|
54
|
+
? 77
|
|
55
|
+
: 25;
|
|
56
|
+
|
|
57
|
+
//only override temperature limits if config value is explicitly and correctly set
|
|
58
|
+
if (
|
|
59
|
+
this.accessory.context.config.minValue != null &&
|
|
60
|
+
this.accessory.context.config.minValue > 0 &&
|
|
61
|
+
this.accessory.context.config.minValue < maxValue
|
|
62
|
+
) {
|
|
63
|
+
minValue = this.accessory.context.config.minValue;
|
|
64
|
+
}
|
|
65
|
+
if (
|
|
66
|
+
this.accessory.context.config.maxValue != null &&
|
|
67
|
+
this.accessory.context.config.maxValue > 0 &&
|
|
68
|
+
this.accessory.context.config.maxValue > minValue
|
|
69
|
+
) {
|
|
70
|
+
maxValue = this.accessory.context.config.maxValue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const minStep = parseFloat(
|
|
74
|
+
(this.accessory.context.config.minStep &&
|
|
75
|
+
!isNaN(this.accessory.context.config.minStep) &&
|
|
76
|
+
this.accessory.context.config.minStep > 0 &&
|
|
77
|
+
this.accessory.context.config.minStep <= 1
|
|
78
|
+
? parseFloat(this.accessory.context.config.minStep)
|
|
79
|
+
: 1
|
|
80
|
+
).toFixed(2)
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return { minValue, maxValue, minStep };
|
|
84
|
+
}
|
|
85
|
+
|
|
26
86
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
|
|
27
87
|
// Services
|
|
28
88
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
|
|
@@ -149,41 +209,7 @@ export default class HeaterCoolerAccessory {
|
|
|
149
209
|
);
|
|
150
210
|
}
|
|
151
211
|
|
|
152
|
-
|
|
153
|
-
this.accessory.context.config.type === 'HOT_WATER'
|
|
154
|
-
? this.accessory.context.config.temperatureUnit === 'CELSIUS'
|
|
155
|
-
? 30
|
|
156
|
-
: 86
|
|
157
|
-
: this.accessory.context.config.temperatureUnit === 'CELSIUS'
|
|
158
|
-
? 5
|
|
159
|
-
: 41;
|
|
160
|
-
|
|
161
|
-
let maxValue =
|
|
162
|
-
this.accessory.context.config.type === 'HOT_WATER'
|
|
163
|
-
? this.accessory.context.config.temperatureUnit === 'CELSIUS'
|
|
164
|
-
? 65
|
|
165
|
-
: 149
|
|
166
|
-
: this.accessory.context.config.temperatureUnit === 'CELSIUS'
|
|
167
|
-
? 25
|
|
168
|
-
: 77;
|
|
169
|
-
|
|
170
|
-
minValue = this.accessory.context.config.minValue < maxValue ? this.accessory.context.config.minValue : minValue;
|
|
171
|
-
|
|
172
|
-
maxValue = this.accessory.context.config.maxValue > minValue ? this.accessory.context.config.maxValue : maxValue;
|
|
173
|
-
|
|
174
|
-
console.log('Before MINSTEP: ' + this.accessory.context.config.minStep, this.accessory.displayName);
|
|
175
|
-
|
|
176
|
-
let minStep = parseFloat(
|
|
177
|
-
(this.accessory.context.config.minStep &&
|
|
178
|
-
!isNaN(this.accessory.context.config.minStep) &&
|
|
179
|
-
this.accessory.context.config.minStep > 0 &&
|
|
180
|
-
this.accessory.context.config.minStep <= 1
|
|
181
|
-
? parseFloat(this.accessory.context.config.minStep)
|
|
182
|
-
: 1
|
|
183
|
-
).toFixed(2)
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
console.log('After MINSTEP: ' + minStep, this.accessory.displayName);
|
|
212
|
+
const { minValue, maxValue, minStep } = this.getTemperatureConfig();
|
|
187
213
|
|
|
188
214
|
let maxState = this.accessory.context.config.type === 'HOT_WATER' ? 2 : 3;
|
|
189
215
|
|
|
@@ -220,30 +246,38 @@ export default class HeaterCoolerAccessory {
|
|
|
220
246
|
maxValue: 255,
|
|
221
247
|
});
|
|
222
248
|
|
|
223
|
-
|
|
224
|
-
service.getCharacteristic(this.api.hap.Characteristic.HeatingThresholdTemperature).updateValue(minValue);
|
|
249
|
+
const heatingThreshold = service.getCharacteristic(this.api.hap.Characteristic.HeatingThresholdTemperature);
|
|
225
250
|
|
|
226
|
-
|
|
227
|
-
service.getCharacteristic(this.api.hap.Characteristic.HeatingThresholdTemperature).updateValue(maxValue);
|
|
228
|
-
|
|
229
|
-
service.getCharacteristic(this.api.hap.Characteristic.HeatingThresholdTemperature).setProps({
|
|
251
|
+
heatingThreshold.setProps({
|
|
230
252
|
minValue: minValue,
|
|
231
253
|
maxValue: maxValue,
|
|
232
254
|
minStep: minStep,
|
|
233
255
|
});
|
|
234
256
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
257
|
+
let heatingThresholdValue = Number(heatingThreshold.value);
|
|
258
|
+
|
|
259
|
+
if (!Number.isFinite(heatingThresholdValue) || heatingThresholdValue < minValue)
|
|
260
|
+
heatingThreshold.updateValue(minValue);
|
|
238
261
|
|
|
239
|
-
|
|
240
|
-
|
|
262
|
+
if (Number.isFinite(heatingThresholdValue) && heatingThresholdValue > maxValue)
|
|
263
|
+
heatingThreshold.updateValue(maxValue);
|
|
241
264
|
|
|
242
|
-
|
|
265
|
+
if (this.accessory.context.config.type === 'HEATING') {
|
|
266
|
+
const coolingThreshold = service.getCharacteristic(this.api.hap.Characteristic.CoolingThresholdTemperature);
|
|
267
|
+
|
|
268
|
+
coolingThreshold.setProps({
|
|
243
269
|
minValue: minValue,
|
|
244
270
|
maxValue: maxValue,
|
|
245
271
|
minStep: minStep,
|
|
246
272
|
});
|
|
273
|
+
|
|
274
|
+
let coolingThresholdValue = Number(coolingThreshold.value);
|
|
275
|
+
|
|
276
|
+
if (!Number.isFinite(coolingThresholdValue) || coolingThresholdValue < minValue)
|
|
277
|
+
coolingThreshold.updateValue(minValue);
|
|
278
|
+
|
|
279
|
+
if (Number.isFinite(coolingThresholdValue) && coolingThresholdValue > maxValue)
|
|
280
|
+
coolingThreshold.updateValue(maxValue);
|
|
247
281
|
}
|
|
248
282
|
|
|
249
283
|
if (!service.testCharacteristic(this.api.hap.Characteristic.ValvePosition))
|
package/src/helper/handler.js
CHANGED
|
@@ -30,6 +30,162 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
30
30
|
return Object.keys(helpers[config.homeId].activeSettingStateRuns).length > 0;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
|
|
34
|
+
function _hasZoneId(zoneId) {
|
|
35
|
+
return zoneId !== undefined && zoneId !== null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function _getExpectedAccessoryName(zone) {
|
|
39
|
+
return config.homeName + ' ' + zone.name + (zone.type === 'HEATING' ? ' Heater' : zone.type === 'AIR_CONDITIONING' ? ' AC' : ' Boiler');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function _getAccessoryNameSuffix(zone) {
|
|
43
|
+
return zone.name + (zone.type === 'HEATING' ? ' Heater' : zone.type === 'AIR_CONDITIONING' ? ' AC' : ' Boiler');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function _isAccessoryForZone(accessory, zone) {
|
|
47
|
+
return (
|
|
48
|
+
accessory &&
|
|
49
|
+
zone &&
|
|
50
|
+
accessory.context.config.type === zone.type &&
|
|
51
|
+
(accessory.displayName === _getExpectedAccessoryName(zone) ||
|
|
52
|
+
accessory.displayName === _getAccessoryNameSuffix(zone) ||
|
|
53
|
+
accessory.displayName.endsWith(' ' + _getAccessoryNameSuffix(zone)))
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function _findConfiguredZoneForAccessory(accessory) {
|
|
58
|
+
if (!config.zones || !config.zones.length) return;
|
|
59
|
+
|
|
60
|
+
return config.zones.find((zone) => _isAccessoryForZone(accessory, zone));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function _getZoneIdForAccessory(accessory) {
|
|
64
|
+
if (_hasZoneId(accessory.context.config.zoneId)) return accessory.context.config.zoneId;
|
|
65
|
+
|
|
66
|
+
let configuredZone = _findConfiguredZoneForAccessory(accessory);
|
|
67
|
+
|
|
68
|
+
if (configuredZone && _hasZoneId(configuredZone.id)) {
|
|
69
|
+
accessory.context.config.zoneId = configuredZone.id;
|
|
70
|
+
return configuredZone.id;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const allZones = (await tado.getZones(config.homeId)) || [];
|
|
74
|
+
|
|
75
|
+
for (const [index, zone] of config.zones.entries()) {
|
|
76
|
+
const foundZone = allZones.find((zoneWithID) => zoneWithID.name === zone.name && zoneWithID.type === zone.type);
|
|
77
|
+
|
|
78
|
+
if (foundZone && _hasZoneId(foundZone.id)) {
|
|
79
|
+
config.zones[index].id = foundZone.id;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
configuredZone = _findConfiguredZoneForAccessory(accessory);
|
|
84
|
+
|
|
85
|
+
if (configuredZone && _hasZoneId(configuredZone.id)) {
|
|
86
|
+
accessory.context.config.zoneId = configuredZone.id;
|
|
87
|
+
return configuredZone.id;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
Logger.error(`Cannot set state for ${accessory.displayName}: missing tado zone id. Please run reconfigure or add the correct zone id to the zone config.`);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function _getTemperatureLimits(accessory) {
|
|
95
|
+
let min =
|
|
96
|
+
accessory.context.config.type === 'HOT_WATER'
|
|
97
|
+
? accessory.context.config.temperatureUnit === 'FAHRENHEIT'
|
|
98
|
+
? 86
|
|
99
|
+
: 30
|
|
100
|
+
: accessory.context.config.temperatureUnit === 'FAHRENHEIT'
|
|
101
|
+
? 41
|
|
102
|
+
: 5;
|
|
103
|
+
|
|
104
|
+
let max =
|
|
105
|
+
accessory.context.config.type === 'HOT_WATER'
|
|
106
|
+
? accessory.context.config.temperatureUnit === 'FAHRENHEIT'
|
|
107
|
+
? 149
|
|
108
|
+
: 65
|
|
109
|
+
: accessory.context.config.temperatureUnit === 'FAHRENHEIT'
|
|
110
|
+
? 77
|
|
111
|
+
: 25;
|
|
112
|
+
|
|
113
|
+
if (
|
|
114
|
+
accessory.context.config.minValue != null &&
|
|
115
|
+
accessory.context.config.minValue > 0 &&
|
|
116
|
+
accessory.context.config.minValue < max
|
|
117
|
+
) {
|
|
118
|
+
min = accessory.context.config.minValue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (
|
|
122
|
+
accessory.context.config.maxValue != null &&
|
|
123
|
+
accessory.context.config.maxValue > 0 &&
|
|
124
|
+
accessory.context.config.maxValue > min
|
|
125
|
+
) {
|
|
126
|
+
max = accessory.context.config.maxValue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { min, max };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function _normalizeTemperatureForAccessory(accessory, temp) {
|
|
133
|
+
if (temp === null || temp === undefined) return temp;
|
|
134
|
+
|
|
135
|
+
const numericTemp = Number(temp);
|
|
136
|
+
if (!Number.isFinite(numericTemp)) return temp;
|
|
137
|
+
|
|
138
|
+
if (accessory.context.config.type !== 'HOT_WATER') return parseFloat(numericTemp.toFixed(2));
|
|
139
|
+
|
|
140
|
+
const { min, max } = _getTemperatureLimits(accessory);
|
|
141
|
+
|
|
142
|
+
return parseFloat(Math.min(Math.max(numericTemp, min), max).toFixed(2));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function _setZoneOverlay(accessory, power, temp, mode) {
|
|
146
|
+
const zoneId = await _getZoneIdForAccessory(accessory);
|
|
147
|
+
if (!_hasZoneId(zoneId)) return false;
|
|
148
|
+
|
|
149
|
+
await tado.setZoneOverlay(
|
|
150
|
+
config.homeId,
|
|
151
|
+
zoneId,
|
|
152
|
+
power,
|
|
153
|
+
_normalizeTemperatureForAccessory(accessory, temp),
|
|
154
|
+
mode,
|
|
155
|
+
accessory.context.config.temperatureUnit
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function _setACZoneOverlay(accessory, power, acMode, temp, fanSpeed, swing, mode) {
|
|
162
|
+
const zoneId = await _getZoneIdForAccessory(accessory);
|
|
163
|
+
if (!_hasZoneId(zoneId)) return false;
|
|
164
|
+
|
|
165
|
+
await tado.setACZoneOverlay(
|
|
166
|
+
config.homeId,
|
|
167
|
+
zoneId,
|
|
168
|
+
power,
|
|
169
|
+
acMode,
|
|
170
|
+
_normalizeTemperatureForAccessory(accessory, temp),
|
|
171
|
+
fanSpeed,
|
|
172
|
+
swing,
|
|
173
|
+
mode,
|
|
174
|
+
accessory.context.config.temperatureUnit
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function _clearZoneOverlay(accessory) {
|
|
181
|
+
const zoneId = await _getZoneIdForAccessory(accessory);
|
|
182
|
+
if (!_hasZoneId(zoneId)) return false;
|
|
183
|
+
|
|
184
|
+
await tado.clearZoneOverlay(config.homeId, zoneId);
|
|
185
|
+
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
33
189
|
async function setStates(accessory, accs, target, value) {
|
|
34
190
|
let zoneUpdated = false;
|
|
35
191
|
accessories = accs.filter((acc) => acc && acc.context.config.homeName === config.homeName);
|
|
@@ -80,27 +236,18 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
80
236
|
// Use AC-specific overlay for AIR_CONDITIONING zones
|
|
81
237
|
if (accessory.context.config.type === 'AIR_CONDITIONING') {
|
|
82
238
|
zoneUpdated = true;
|
|
83
|
-
await
|
|
84
|
-
|
|
85
|
-
accessory.context.config.zoneId,
|
|
239
|
+
await _setACZoneOverlay(
|
|
240
|
+
accessory,
|
|
86
241
|
power,
|
|
87
242
|
'COOL', // Default AC mode for OFF state
|
|
88
243
|
temp,
|
|
89
244
|
null, // No fan speed for AC units
|
|
90
245
|
'OFF', // Default swing
|
|
91
|
-
mode
|
|
92
|
-
accessory.context.config.temperatureUnit
|
|
246
|
+
mode
|
|
93
247
|
);
|
|
94
248
|
} else {
|
|
95
249
|
zoneUpdated = true;
|
|
96
|
-
await
|
|
97
|
-
config.homeId,
|
|
98
|
-
accessory.context.config.zoneId,
|
|
99
|
-
power,
|
|
100
|
-
temp,
|
|
101
|
-
mode,
|
|
102
|
-
accessory.context.config.temperatureUnit
|
|
103
|
-
);
|
|
250
|
+
await _setZoneOverlay(accessory, power, temp, mode);
|
|
104
251
|
}
|
|
105
252
|
} else {
|
|
106
253
|
let mode =
|
|
@@ -133,7 +280,7 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
133
280
|
accessory.context.config.subtype.includes('heatercooler'))
|
|
134
281
|
) {
|
|
135
282
|
zoneUpdated = true;
|
|
136
|
-
await
|
|
283
|
+
await _clearZoneOverlay(accessory);
|
|
137
284
|
return;
|
|
138
285
|
}
|
|
139
286
|
|
|
@@ -149,27 +296,18 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
149
296
|
let acMode = value === 1 ? 'HEAT' : value === 2 ? 'COOL' : 'COOL';
|
|
150
297
|
|
|
151
298
|
zoneUpdated = true;
|
|
152
|
-
await
|
|
153
|
-
|
|
154
|
-
accessory.context.config.zoneId,
|
|
299
|
+
await _setACZoneOverlay(
|
|
300
|
+
accessory,
|
|
155
301
|
power,
|
|
156
302
|
acMode,
|
|
157
303
|
temp,
|
|
158
304
|
null, // No fan speed for AC units
|
|
159
305
|
'OFF', // Default swing
|
|
160
|
-
mode
|
|
161
|
-
accessory.context.config.temperatureUnit
|
|
306
|
+
mode
|
|
162
307
|
);
|
|
163
308
|
} else {
|
|
164
309
|
zoneUpdated = true;
|
|
165
|
-
await
|
|
166
|
-
config.homeId,
|
|
167
|
-
accessory.context.config.zoneId,
|
|
168
|
-
power,
|
|
169
|
-
temp,
|
|
170
|
-
mode,
|
|
171
|
-
accessory.context.config.temperatureUnit
|
|
172
|
-
);
|
|
310
|
+
await _setZoneOverlay(accessory, power, temp, mode);
|
|
173
311
|
}
|
|
174
312
|
|
|
175
313
|
helpers[config.homeId].delayTimer[accessory.displayName] = null;
|
|
@@ -196,7 +334,7 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
196
334
|
accessory.context.config.subtype.includes('heatercooler'))
|
|
197
335
|
) {
|
|
198
336
|
zoneUpdated = true;
|
|
199
|
-
await
|
|
337
|
+
await _clearZoneOverlay(accessory);
|
|
200
338
|
return;
|
|
201
339
|
}
|
|
202
340
|
|
|
@@ -259,27 +397,18 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
259
397
|
}
|
|
260
398
|
|
|
261
399
|
zoneUpdated = true;
|
|
262
|
-
await
|
|
263
|
-
|
|
264
|
-
accessory.context.config.zoneId,
|
|
400
|
+
await _setACZoneOverlay(
|
|
401
|
+
accessory,
|
|
265
402
|
power,
|
|
266
403
|
acMode,
|
|
267
404
|
temp,
|
|
268
405
|
null, // No fan speed for AC units
|
|
269
406
|
'OFF', // Default swing
|
|
270
|
-
mode
|
|
271
|
-
accessory.context.config.temperatureUnit
|
|
407
|
+
mode
|
|
272
408
|
);
|
|
273
409
|
} else {
|
|
274
410
|
zoneUpdated = true;
|
|
275
|
-
await
|
|
276
|
-
config.homeId,
|
|
277
|
-
accessory.context.config.zoneId,
|
|
278
|
-
power,
|
|
279
|
-
temp,
|
|
280
|
-
mode,
|
|
281
|
-
accessory.context.config.temperatureUnit
|
|
282
|
-
);
|
|
411
|
+
await _setZoneOverlay(accessory, power, temp, mode);
|
|
283
412
|
}
|
|
284
413
|
}
|
|
285
414
|
|
|
@@ -293,7 +422,7 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
293
422
|
let temp = null;
|
|
294
423
|
let power = value ? 'ON' : 'OFF';
|
|
295
424
|
|
|
296
|
-
if (faucetService) faucetService.getCharacteristic(
|
|
425
|
+
if (faucetService) faucetService.getCharacteristic(api.hap.Characteristic.InUse).updateValue(value);
|
|
297
426
|
|
|
298
427
|
let mode =
|
|
299
428
|
accessory.context.config.mode === 'TIMER'
|
|
@@ -303,27 +432,18 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
303
432
|
// Use AC-specific overlay for AIR_CONDITIONING zones
|
|
304
433
|
if (accessory.context.config.type === 'AIR_CONDITIONING') {
|
|
305
434
|
zoneUpdated = true;
|
|
306
|
-
await
|
|
307
|
-
|
|
308
|
-
accessory.context.config.zoneId,
|
|
435
|
+
await _setACZoneOverlay(
|
|
436
|
+
accessory,
|
|
309
437
|
power,
|
|
310
438
|
'COOL', // Default AC mode for switch/faucet
|
|
311
439
|
temp,
|
|
312
440
|
null, // No fan speed for AC units
|
|
313
441
|
'OFF', // Default swing
|
|
314
|
-
mode
|
|
315
|
-
accessory.context.config.temperatureUnit
|
|
442
|
+
mode
|
|
316
443
|
);
|
|
317
444
|
} else {
|
|
318
445
|
zoneUpdated = true;
|
|
319
|
-
await
|
|
320
|
-
config.homeId,
|
|
321
|
-
accessory.context.config.zoneId,
|
|
322
|
-
power,
|
|
323
|
-
temp,
|
|
324
|
-
mode,
|
|
325
|
-
accessory.context.config.temperatureUnit
|
|
326
|
-
);
|
|
446
|
+
await _setZoneOverlay(accessory, power, temp, mode);
|
|
327
447
|
}
|
|
328
448
|
|
|
329
449
|
break;
|
|
@@ -899,7 +1019,7 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
899
1019
|
let inOffMode = 0;
|
|
900
1020
|
let inAutoMode = 0;
|
|
901
1021
|
|
|
902
|
-
let zonesWithoutID = config.zones.filter((zone) => zone && !zone.id);
|
|
1022
|
+
let zonesWithoutID = config.zones.filter((zone) => zone && !_hasZoneId(zone.id));
|
|
903
1023
|
|
|
904
1024
|
if (zonesWithoutID.length) {
|
|
905
1025
|
const allZones = (await tado.getZones(config.homeId)) || [];
|
|
@@ -917,11 +1037,11 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
917
1037
|
for (const [index, zone] of config.zones.entries()) {
|
|
918
1038
|
allZones.forEach((zoneWithID) => {
|
|
919
1039
|
if (zoneWithID.name === zone.name && zoneWithID.type === zone.type) {
|
|
920
|
-
const
|
|
921
|
-
(acc) => acc && acc.displayName === config.homeName + ' ' + zone.name + ' Heater'
|
|
922
|
-
);
|
|
1040
|
+
const zoneAccessories = accessories.filter((acc) => _isAccessoryForZone(acc, zone));
|
|
923
1041
|
|
|
924
|
-
|
|
1042
|
+
zoneAccessories.forEach((acc) => {
|
|
1043
|
+
acc.context.config.zoneId = zoneWithID.id;
|
|
1044
|
+
});
|
|
925
1045
|
|
|
926
1046
|
config.zones[index].id = zoneWithID.id;
|
|
927
1047
|
config.zones[index].battery = !config.zones[index].noBattery
|
|
@@ -949,9 +1069,19 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
949
1069
|
}
|
|
950
1070
|
|
|
951
1071
|
for (const zone of config.zones) {
|
|
1072
|
+
if (!_hasZoneId(zone.id)) {
|
|
1073
|
+
Logger.warn(`Skipping zone ${zone.name}: missing tado zone id.`);
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
952
1077
|
const zoneState = zoneStates[zone.id.toString()];
|
|
953
1078
|
Logger.debug(`Update state of zone ${zone.id} to:`, zoneState);
|
|
954
1079
|
|
|
1080
|
+
if (!zoneState || !zoneState.setting) {
|
|
1081
|
+
Logger.warn(`Skipping zone ${zone.name}: missing tado zone state.`);
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
955
1085
|
let currentState, targetState, currentTemp, targetTemp, humidity, active, battery, tempEqual;
|
|
956
1086
|
|
|
957
1087
|
if (zoneState.setting.type === 'HEATING') {
|
|
@@ -2,7 +2,7 @@ import { hrtime } from "process";
|
|
|
2
2
|
import Logger from '../helper/logger.js';
|
|
3
3
|
|
|
4
4
|
export class TadoUpdateBuffer {
|
|
5
|
-
constructor(sendUpdateFn, preferSiriTemperature = false) {
|
|
5
|
+
constructor(sendUpdateFn, preferSiriTemperature = false, defaultTemperature = 20, minTemperature = 5, maxTemperature = null, clampToRange = false) {
|
|
6
6
|
this.preferSiriTemperature = !!preferSiriTemperature;
|
|
7
7
|
this.sendUpdateFn = sendUpdateFn;
|
|
8
8
|
this.delay = 400;
|
|
@@ -10,7 +10,12 @@ export class TadoUpdateBuffer {
|
|
|
10
10
|
this.pendingState = null;
|
|
11
11
|
this.pendingTemperature = null;
|
|
12
12
|
this.lastUpdateTime = null;
|
|
13
|
-
this.
|
|
13
|
+
this.minTemperature = Number.isFinite(Number(minTemperature)) ? Number(minTemperature) : 5;
|
|
14
|
+
this.maxTemperature = Number.isFinite(Number(maxTemperature)) ? Number(maxTemperature) : null;
|
|
15
|
+
this.clampToRange = !!clampToRange;
|
|
16
|
+
|
|
17
|
+
const normalizedDefaultTemperature = this._normalizeTemperature(defaultTemperature);
|
|
18
|
+
this.lastTemperature = normalizedDefaultTemperature ?? (this.clampToRange ? this.minTemperature : 20);
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
setState(value) {
|
|
@@ -25,6 +30,16 @@ export class TadoUpdateBuffer {
|
|
|
25
30
|
Logger.debug("[TadoUpdateBuffer] setTemperature", value, hrtime.bigint());
|
|
26
31
|
}
|
|
27
32
|
|
|
33
|
+
_normalizeTemperature(value) {
|
|
34
|
+
const temp = Number(value);
|
|
35
|
+
if (!Number.isFinite(temp)) return null;
|
|
36
|
+
|
|
37
|
+
if (temp < this.minTemperature) return this.clampToRange ? this.minTemperature : null;
|
|
38
|
+
if (this.maxTemperature !== null && temp > this.maxTemperature) return this.clampToRange ? this.maxTemperature : temp;
|
|
39
|
+
|
|
40
|
+
return parseFloat(temp.toFixed(2));
|
|
41
|
+
}
|
|
42
|
+
|
|
28
43
|
_schedule() {
|
|
29
44
|
if (this.timer) clearTimeout(this.timer);
|
|
30
45
|
this.timer = setTimeout(() => this._apply(), this.delay);
|
|
@@ -33,15 +48,17 @@ export class TadoUpdateBuffer {
|
|
|
33
48
|
_apply() {
|
|
34
49
|
this.timer = null;
|
|
35
50
|
const state = this.pendingState;
|
|
36
|
-
const
|
|
37
|
-
const
|
|
51
|
+
const rawTemp = this.pendingTemperature;
|
|
52
|
+
const siriAutoTemperature = Number(rawTemp) === 5;
|
|
53
|
+
const temp = siriAutoTemperature ? 5 : this._normalizeTemperature(rawTemp);
|
|
54
|
+
const tempSet = temp !== null;
|
|
38
55
|
this.pendingState = null;
|
|
39
56
|
this.pendingTemperature = null;
|
|
40
|
-
if (tempSet) this.lastTemperature = temp;
|
|
57
|
+
if (tempSet && !siriAutoTemperature) this.lastTemperature = temp;
|
|
41
58
|
|
|
42
59
|
//Siri temperature heuristic
|
|
43
60
|
if (this.preferSiriTemperature && state === 3 && tempSet) {
|
|
44
|
-
if (
|
|
61
|
+
if (siriAutoTemperature) {
|
|
45
62
|
//set auto mode on
|
|
46
63
|
Logger.debug("[TadoUpdateBuffer] preferSiriTemperature active but temp=5 -> treat as auto mode");
|
|
47
64
|
return this.sendUpdateFn("State", 3);
|
package/src/tado/tado-config.js
CHANGED
|
@@ -15,6 +15,8 @@ const hashCode = (s) =>
|
|
|
15
15
|
const devices = new Map();
|
|
16
16
|
const deviceHandler = new Map();
|
|
17
17
|
|
|
18
|
+
const hasZoneId = (zoneId) => zoneId !== undefined && zoneId !== null;
|
|
19
|
+
|
|
18
20
|
let telegram;
|
|
19
21
|
|
|
20
22
|
export default {
|
|
@@ -554,6 +556,13 @@ export default {
|
|
|
554
556
|
zone.type = 'HEATING';
|
|
555
557
|
}
|
|
556
558
|
|
|
559
|
+
if (!hasZoneId(zone.id)) {
|
|
560
|
+
Logger.warn(
|
|
561
|
+
'There is no tado zone id configured for this zone. The plugin will try to resolve it from the tado API at runtime. Run reconfigure to persist it in the config.',
|
|
562
|
+
zone.name
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
557
566
|
if (!error) {
|
|
558
567
|
activeZones += 1;
|
|
559
568
|
|
|
@@ -582,6 +591,7 @@ export default {
|
|
|
582
591
|
? 'zone-faucet'
|
|
583
592
|
: 'zone-switch';
|
|
584
593
|
|
|
594
|
+
// boilerTempSupport always takes precedence over accTypeBoiler
|
|
585
595
|
config.subtype = zone.boilerTempSupport ? 'zone-heatercooler-boiler' : config.subtype;
|
|
586
596
|
|
|
587
597
|
config.zoneId = zone.id;
|
|
@@ -878,7 +888,7 @@ export default {
|
|
|
878
888
|
config.name = name;
|
|
879
889
|
config.subtype = 'extra-cntrlswitch';
|
|
880
890
|
config.runningInformation = home.extras.runningInformation;
|
|
881
|
-
config.rooms = home.zones.filter((zne) => zne && zne.id);
|
|
891
|
+
config.rooms = home.zones.filter((zne) => zne && hasZoneId(zne.id));
|
|
882
892
|
config.switches = validSwitches;
|
|
883
893
|
config.model = 'Central Switch';
|
|
884
894
|
config.serialNumber = hashCode(name);
|