homebridge-viessmann-vicare 2.0.44 β 2.0.46
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 +7 -1
- package/SETUP-GUIDE.md +10 -4
- package/dist/accessories/heating-circuit-accessory.d.ts.map +1 -1
- package/dist/accessories/heating-circuit-accessory.js +49 -34
- package/dist/settings.d.ts +1 -1
- package/dist/settings.js +1 -1
- package/package.json +1 -1
- package/viessmann-report.js +158 -30
package/README.md
CHANGED
|
@@ -1033,7 +1033,7 @@ For issues and questions:
|
|
|
1033
1033
|
|
|
1034
1034
|
## π Changelog
|
|
1035
1035
|
|
|
1036
|
-
### [2.0.
|
|
1036
|
+
### [2.0.46] - 2026-03-15
|
|
1037
1037
|
#### Fixed
|
|
1038
1038
|
- **Extended Heating state: HomeKit OFF while ViCare ON** β confirmed via live API: `forcedLastFromSchedule.active=True` is a schedule management artifact (always present), not an Extended Heating indicator. State now reads `comfort.active OR (programs.active === comfortFeatureSuffix)`. Deactivation uses `comfort.setTemperature` as fallback when `deactivate` is not executable (Vitodens).
|
|
1039
1039
|
- **Extended Heating / comfort program: API-driven feature discovery** β removed hardcoded candidate list `['comfort', 'comfortHeating']`. Plugin now discovers the comfort program by scanning actual device features for any enabled `programs.*` that has an `activate` command, excluding known non-comfort programs. Works for Vitodens (`programs.comfort`), Vitocal gen3 (`programs.comfortHeating`), and any future device model without code changes.
|
|
@@ -1042,6 +1042,12 @@ For issues and questions:
|
|
|
1042
1042
|
- **Device messages written at startup** β `setupDeviceAccessories` now calls `writeDeviceMessages` so the file exists immediately on startup, not only after the first update cycle.
|
|
1043
1043
|
- **Compressor setpoint path: dynamic** β `heating.compressors.0.speed.setpoint` was hardcoded. Now derived from resolved `hpPaths.compressorMod` by replacing `.current` with `.setpoint` β correct for any device/compressor index.
|
|
1044
1044
|
|
|
1045
|
+
### [2.0.45] - 2026-03-15
|
|
1046
|
+
*(published separately)*
|
|
1047
|
+
|
|
1048
|
+
### [2.0.44] - 2026-03-15
|
|
1049
|
+
*(published separately)*
|
|
1050
|
+
|
|
1045
1051
|
### [2.0.43] - 2026-03-15
|
|
1046
1052
|
*(published separately)*
|
|
1047
1053
|
|
package/SETUP-GUIDE.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Complete Setup Guide - v2.0.
|
|
1
|
+
# Complete Setup Guide - v2.0.46
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
This guide will walk you through setting up the Viessmann ViCare plugin v2.0.
|
|
5
|
+
This guide will walk you through setting up the Viessmann ViCare plugin v2.0.46 for Homebridge, including all the advanced features like intelligent caching, rate limiting protection, comprehensive configuration options, **complete localization support with custom names**, **CSV history logging**, **HTML diagnostic reports**, **energy system monitoring** (PV, battery, wallbox), and **heating schedule awareness** with visual bands in the HTML report, and **heat pump (WΓ€rmepumpe) support** with automatic device detection.
|
|
6
6
|
|
|
7
7
|
## Prerequisites
|
|
8
8
|
|
|
@@ -1157,7 +1157,7 @@ When reporting issues, include:
|
|
|
1157
1157
|
|
|
1158
1158
|
```json
|
|
1159
1159
|
{
|
|
1160
|
-
"plugin_version": "2.0.
|
|
1160
|
+
"plugin_version": "2.0.46",
|
|
1161
1161
|
"homebridge_version": "1.8.x",
|
|
1162
1162
|
"node_version": "18.x.x",
|
|
1163
1163
|
"heating_system": "Viessmann Model",
|
|
@@ -1228,7 +1228,7 @@ sudo systemctl restart homebridge
|
|
|
1228
1228
|
|
|
1229
1229
|
## Changelog
|
|
1230
1230
|
|
|
1231
|
-
### v2.0.
|
|
1231
|
+
### v2.0.46 (2026-03-15)
|
|
1232
1232
|
**Multi-device robustness + API-driven feature discovery:**
|
|
1233
1233
|
- **Comfort program discovery: API-driven** β scans actual device features instead of hardcoded name list. Supports Vitodens, Vitocal gen3, and future devices automatically.
|
|
1234
1234
|
- **HC program normalisation: pattern-based** β `comfort*`/`normal*`/`reduced*` prefix matching instead of fixed map.
|
|
@@ -1236,6 +1236,12 @@ sudo systemctl restart homebridge
|
|
|
1236
1236
|
- **Device messages written at startup** β available immediately, not only after first update cycle.
|
|
1237
1237
|
- **Compressor setpoint path: dynamic** β derived from resolved `hpPaths.compressorMod`, no hardcoded device index.
|
|
1238
1238
|
|
|
1239
|
+
### v2.0.45 (2026-03-15)
|
|
1240
|
+
*(published separately)*
|
|
1241
|
+
|
|
1242
|
+
### v2.0.44 (2026-03-15)
|
|
1243
|
+
*(published separately)*
|
|
1244
|
+
|
|
1239
1245
|
### v2.0.43 (2026-03-15)
|
|
1240
1246
|
*(published separately)*
|
|
1241
1247
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"heating-circuit-accessory.d.ts","sourceRoot":"","sources":["../../src/accessories/heating-circuit-accessory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAE7E,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,eAAe,EAA2B,MAAM,aAAa,CAAC;AAInI,qBAAa,gCAAgC;IAuEzC,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,aAAa;IA3EhC,OAAO,CAAC,mBAAmB,CAAU;IACrC,OAAO,CAAC,kBAAkB,CAAU;IAGpC,OAAO,CAAC,cAAc,CAAC,CAAU;IACjC,OAAO,CAAC,oBAAoB,CAAC,CAAU;IACvC,OAAO,CAAC,sBAAsB,CAAC,CAAU;IAGzC,OAAO,CAAC,cAAc,CAAC,CAAU;IACjC,OAAO,CAAC,cAAc,CAAC,CAAU;IACjC,OAAO,CAAC,cAAc,CAAC,CAAU;IAEjC,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,iBAAiB,CAAgB;IACzC,OAAO,CAAC,wBAAwB,CAAgB;IAChD,OAAO,CAAC,0BAA0B,CAAS;IAC3C,OAAO,CAAC,sBAAsB,CAAuB;IACrD,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,cAAc,CAAY;IAElC,OAAO,CAAC,oBAAoB,CAAa;IAIzC,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,mBAAmB,CAAK;IAChC,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,mBAAmB,CAAiC;IAC5D,OAAO,CAAC,mBAAmB,CAAiC;IAC5D,OAAO,CAAC,sBAAsB,CAAiC;IAC/D,OAAO,CAAC,sBAAsB,CAAiC;IAC/D,OAAO,CAAC,mBAAmB,CAAiC;IAC5D,OAAO,CAAC,mBAAmB,CAAiC;IAG5D,OAAO,CAAC,wBAAwB,CAAS;IAEzC,OAAO,CAAC,MAAM,CAeZ;IAGF,OAAO,CAAC,mBAAmB,CAIzB;IAEF,OAAO,CAAC,aAAa,CAAC,CAAyB;IAG/C,OAAO,CAAC,eAAe,CAAyE;IAChG,OAAO,CAAC,oBAAoB,CAA8C;gBAGvD,QAAQ,EAAE,iBAAiB,EAC3B,SAAS,EAAE,iBAAiB,EAC5B,YAAY,EAAE,qBAAqB,EACnC,OAAO,EAAE,gBAAgB,EACzB,MAAM,EAAE,eAAe,EACvB,aAAa,EAAE,MAAM;YA0B1B,sBAAsB;IAyBpC,OAAO,CAAC,mBAAmB;
|
|
1
|
+
{"version":3,"file":"heating-circuit-accessory.d.ts","sourceRoot":"","sources":["../../src/accessories/heating-circuit-accessory.ts"],"names":[],"mappings":"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"}
|
|
@@ -247,14 +247,15 @@ class ViessmannHeatingCircuitAccessory {
|
|
|
247
247
|
(hasForcedActivate && hasForcedDeactivate);
|
|
248
248
|
if (hasAnyMethod) {
|
|
249
249
|
this.availableQuickSelections.push('extendedHeating');
|
|
250
|
-
// Initial state:
|
|
251
|
-
// NOTE: forcedLastFromSchedule.active is NOT used β it is True during normal
|
|
252
|
-
// schedule transitions and does not indicate Extended Heating is active.
|
|
250
|
+
// Initial state: OR of all three signals (same logic as update cycle).
|
|
253
251
|
const comfortActive = comfortProgram.properties?.active?.value || false;
|
|
252
|
+
const forcedActive = forcedProgram?.properties?.active?.value || false;
|
|
254
253
|
const activeProg = features.find(f => f.feature === `${circuitPrefix}.operating.programs.active`);
|
|
255
254
|
const activeIsComfort = activeProg?.properties?.value?.value === this.comfortFeatureSuffix;
|
|
256
|
-
this.states.ExtendedHeatingActive = comfortActive || activeIsComfort;
|
|
257
|
-
this.platform.log.debug(`HC${this.circuitNumber} ExtendedHeating initial
|
|
255
|
+
this.states.ExtendedHeatingActive = comfortActive || forcedActive || activeIsComfort;
|
|
256
|
+
this.platform.log.debug(`HC${this.circuitNumber} ExtendedHeating initial:` +
|
|
257
|
+
` comfort=${comfortActive} forced=${forcedActive} active=${activeProg?.properties?.value?.value}` +
|
|
258
|
+
` β ${this.states.ExtendedHeatingActive}`);
|
|
258
259
|
const activateExecutable = hasActivate?.isExecutable || false;
|
|
259
260
|
const deactivateExecutable = hasDeactivate?.isExecutable || false;
|
|
260
261
|
const setTempExecutable = hasSetTemperature?.isExecutable || false;
|
|
@@ -823,40 +824,45 @@ class ViessmannHeatingCircuitAccessory {
|
|
|
823
824
|
}
|
|
824
825
|
else {
|
|
825
826
|
// DEACTIVATING Extended Heating
|
|
826
|
-
//
|
|
827
|
-
|
|
828
|
-
|
|
827
|
+
// Strategy: try all deactivation methods in order of likelihood to work.
|
|
828
|
+
// On Vitodens: forcedLastFromSchedule is the real mechanism β try it first.
|
|
829
|
+
// On other devices: comfort.deactivate may work directly.
|
|
830
|
+
const circuitPfx = `heating.circuits.${this.circuitNumber}`;
|
|
831
|
+
const forcedProgramNow = features.find(f => f.feature === `${circuitPfx}.operating.programs.forcedLastFromSchedule`);
|
|
832
|
+
let deactivated = false;
|
|
833
|
+
// Method 1: forcedLastFromSchedule.deactivate (Vitodens and others using this mechanism)
|
|
834
|
+
if (!deactivated && forcedProgramNow?.commands?.deactivate?.isExecutable) {
|
|
835
|
+
const success = await this.platform.viessmannAPI.executeCommand(this.installation.id, this.gateway.serial, this.device.id, `${circuitPfx}.operating.programs.forcedLastFromSchedule`, 'deactivate', {});
|
|
836
|
+
if (success) {
|
|
837
|
+
deactivated = true;
|
|
838
|
+
this.platform.log.info(`HC${this.circuitNumber} extended heating deactivated via forcedLastFromSchedule`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
// Method 2: comfort.deactivate (devices where comfort is the primary mechanism)
|
|
842
|
+
if (!deactivated && comfortProgram.commands?.deactivate?.isExecutable) {
|
|
829
843
|
const success = await this.executeComfortCommand('deactivate', comfortProgram, false);
|
|
830
844
|
if (success) {
|
|
831
|
-
|
|
832
|
-
this.platform.log.info(`
|
|
833
|
-
return;
|
|
845
|
+
deactivated = true;
|
|
846
|
+
this.platform.log.info(`HC${this.circuitNumber} extended heating deactivated via comfort.deactivate`);
|
|
834
847
|
}
|
|
835
848
|
}
|
|
836
|
-
//
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
const setTempCmd = comfortProgram.commands?.['setTemperature'];
|
|
840
|
-
if (setTempCmd?.isExecutable) {
|
|
841
|
-
const normalProgram = features.find(f => f.feature === `heating.circuits.${this.circuitNumber}.operating.programs.normal`);
|
|
849
|
+
// Method 3: comfort.setTemperature back to normal level
|
|
850
|
+
if (!deactivated && comfortProgram.commands?.setTemperature?.isExecutable) {
|
|
851
|
+
const normalProgram = features.find(f => f.feature === `${circuitPfx}.operating.programs.normal`);
|
|
842
852
|
const normalTemp = normalProgram?.properties?.temperature?.value
|
|
843
853
|
?? this.programTemperatures.normal
|
|
844
854
|
?? 20;
|
|
845
|
-
const success = await this.platform.viessmannAPI.executeCommand(this.installation.id, this.gateway.serial, this.device.id,
|
|
855
|
+
const success = await this.platform.viessmannAPI.executeCommand(this.installation.id, this.gateway.serial, this.device.id, `${circuitPfx}.operating.programs.${this.comfortFeatureSuffix}`, 'setTemperature', { targetTemperature: normalTemp });
|
|
846
856
|
if (success) {
|
|
847
|
-
|
|
848
|
-
this.platform.log.info(`
|
|
849
|
-
return;
|
|
857
|
+
deactivated = true;
|
|
858
|
+
this.platform.log.info(`HC${this.circuitNumber} extended heating cancelled via setTemperature β ${normalTemp}Β°C`);
|
|
850
859
|
}
|
|
851
860
|
}
|
|
852
|
-
|
|
853
|
-
const success = await this.tryAlternativeExtendedHeating(false, comfortProgram, features);
|
|
854
|
-
if (success) {
|
|
861
|
+
if (deactivated) {
|
|
855
862
|
this.states.ExtendedHeatingActive = false;
|
|
856
|
-
this.platform.log.info(`Heating circuit ${this.circuitNumber} extended heating deactivated using alternative method`);
|
|
857
863
|
}
|
|
858
864
|
else {
|
|
859
|
-
throw new Error('Failed to deactivate extended heating
|
|
865
|
+
throw new Error('Failed to deactivate extended heating β no executable command found');
|
|
860
866
|
}
|
|
861
867
|
}
|
|
862
868
|
}
|
|
@@ -1491,21 +1497,30 @@ class ViessmannHeatingCircuitAccessory {
|
|
|
1491
1497
|
}
|
|
1492
1498
|
}
|
|
1493
1499
|
}
|
|
1494
|
-
// Extended heating state
|
|
1495
|
-
//
|
|
1496
|
-
//
|
|
1497
|
-
//
|
|
1500
|
+
// Extended heating state β three signals in OR, all verified via live API:
|
|
1501
|
+
//
|
|
1502
|
+
// Vitodens (Ciriè, confirmed):
|
|
1503
|
+
// programs.active stays 'normal', comfort.active stays False
|
|
1504
|
+
// forcedLastFromSchedule.active = True when ON, False when OFF β real indicator here
|
|
1505
|
+
//
|
|
1506
|
+
// Vitocal gen3 / other devices:
|
|
1507
|
+
// programs.active may change to comfortFeatureSuffix
|
|
1508
|
+
// comfort.active may become True
|
|
1509
|
+
//
|
|
1510
|
+
// Using OR of all three signals covers all known device behaviours without assumptions.
|
|
1498
1511
|
const comfortProgram = features.find(f => f.feature === `${circuitPrefix}.operating.programs.${this.comfortFeatureSuffix}`);
|
|
1512
|
+
const forcedProgram = features.find(f => f.feature === `${circuitPrefix}.operating.programs.forcedLastFromSchedule`);
|
|
1499
1513
|
const activeProg = features.find(f => f.feature === `${circuitPrefix}.operating.programs.active`);
|
|
1500
1514
|
const comfortActive = comfortProgram?.properties?.active?.value ?? false;
|
|
1515
|
+
const forcedActive = forcedProgram?.properties?.active?.value ?? false;
|
|
1501
1516
|
const activeIsComfort = activeProg?.properties?.value?.value === this.comfortFeatureSuffix;
|
|
1502
|
-
const extendedActive = comfortActive || activeIsComfort;
|
|
1517
|
+
const extendedActive = comfortActive || forcedActive || activeIsComfort;
|
|
1503
1518
|
if (extendedActive !== this.states.ExtendedHeatingActive) {
|
|
1504
1519
|
this.states.ExtendedHeatingActive = extendedActive;
|
|
1505
1520
|
anyProgramStateChanged = true;
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1521
|
+
this.platform.log.debug(`HC${this.circuitNumber} ExtendedHeating:` +
|
|
1522
|
+
` comfort=${comfortActive} forced=${forcedActive} active=${activeProg?.properties?.value?.value}` +
|
|
1523
|
+
` β ${extendedActive}`);
|
|
1509
1524
|
if (extendedActive) {
|
|
1510
1525
|
this.states.HolidayActive = false;
|
|
1511
1526
|
this.states.HolidayAtHomeActive = false;
|
package/dist/settings.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export declare const PLUGIN_NAME = "homebridge-viessmann-vicare";
|
|
|
9
9
|
/**
|
|
10
10
|
* Plugin version for User-Agent and logging
|
|
11
11
|
*/
|
|
12
|
-
export declare const PLUGIN_VERSION = "2.0.
|
|
12
|
+
export declare const PLUGIN_VERSION = "2.0.46";
|
|
13
13
|
/**
|
|
14
14
|
* Default configuration values
|
|
15
15
|
*/
|
package/dist/settings.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-viessmann-vicare",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.46",
|
|
4
4
|
"description": "Complete Homebridge plugin for Viessmann ViCare heating systems with full control capabilities, advanced rate limiting protection, and intelligent cache management",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
package/viessmann-report.js
CHANGED
|
@@ -87,9 +87,30 @@ const avgWallboxPwr = avg(energyRows.filter(r => r.wallbox_charging==='true'), '
|
|
|
87
87
|
const lb = boilerRows[boilerRows.length-1] || {};
|
|
88
88
|
const burnerStarts = lb.burner_starts || null;
|
|
89
89
|
const burnerHours = lb.burner_hours || null;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
|
|
91
|
+
// Use DELTA of burner_starts/hours within the report period β not the lifetime ratio.
|
|
92
|
+
// The CSV has a column mapping issue in early rows (starts=hours value) and the
|
|
93
|
+
// 15-min sample rate misses ~97% of real cycles. API counter delta is the only
|
|
94
|
+
// reliable source.
|
|
95
|
+
// Guard: only rows that have BOTH columns (plugin >= v2.0.40).
|
|
96
|
+
const validBurnerRows = boilerRows.filter(r => r.burner_starts && r.burner_hours && parseFloat(r.burner_hours) > 0);
|
|
97
|
+
const firstVBR = validBurnerRows[0] || null;
|
|
98
|
+
const lastVBR = validBurnerRows[validBurnerRows.length-1] || null;
|
|
99
|
+
const deltaStarts = (firstVBR && lastVBR) ? Math.max(0, parseInt(lastVBR.burner_starts) - parseInt(firstVBR.burner_starts)) : null;
|
|
100
|
+
const deltaHours = (firstVBR && lastVBR) ? Math.max(0, parseFloat(lastVBR.burner_hours) - parseFloat(firstVBR.burner_hours)) : null;
|
|
101
|
+
|
|
102
|
+
// Starts/hour from delta β precise (firmware counter, not CSV edges)
|
|
103
|
+
const sph = (deltaStarts !== null && deltaHours !== null && deltaHours > 0)
|
|
104
|
+
? (deltaStarts / deltaHours).toFixed(1)
|
|
105
|
+
: null;
|
|
106
|
+
const avgCycleDurReal = (deltaStarts !== null && deltaHours !== null && deltaStarts > 0)
|
|
107
|
+
? (deltaHours * 60 / deltaStarts).toFixed(1)
|
|
108
|
+
: null;
|
|
109
|
+
const realCycleCount = deltaStarts;
|
|
110
|
+
const realAvgDur = avgCycleDurReal;
|
|
111
|
+
|
|
112
|
+
const effCls = sph ? (parseFloat(sph) < 3 ? 'good' : parseFloat(sph) < 6 ? 'warn' : 'bad') : 'neutral';
|
|
113
|
+
const effLabel = sph ? (parseFloat(sph) < 3 ? 'Normal' : parseFloat(sph) < 6 ? 'High' : 'Severe') : 'N/A';
|
|
93
114
|
|
|
94
115
|
// --- Flow temperature stats (from hc0 rows) ---
|
|
95
116
|
const avgFlow = avg(hcRows, 'flow_temp');
|
|
@@ -123,6 +144,54 @@ const gasBarDhw = gasDays.map(d => +gasPerDay[d].dhw.toFixed(2));
|
|
|
123
144
|
const gasLineTotal = gasDays.map(d => +(gasPerDay[d].heating + gasPerDay[d].dhw).toFixed(2));
|
|
124
145
|
const hasGasChart = gasDays.length >= 1;
|
|
125
146
|
|
|
147
|
+
// --- API Summary data (from viessmann-history-explore-*.json if present) ---
|
|
148
|
+
let apiSummary = null;
|
|
149
|
+
try {
|
|
150
|
+
const exploreFile = require('path').join(HB_PATH, `viessmann-history-explore-${INSTALLATION_ID || 'all'}.json`);
|
|
151
|
+
if (require('fs').existsSync(exploreFile)) {
|
|
152
|
+
const raw = JSON.parse(require('fs').readFileSync(exploreFile, 'utf8'));
|
|
153
|
+
// Find device with heating features (device id = '0' usually)
|
|
154
|
+
const devKey = Object.keys(raw.devices || {}).find(k => {
|
|
155
|
+
const d = raw.devices[k];
|
|
156
|
+
return d.historyFeatures && d.historyFeatures.some(f => f.feature.includes('gas.consumption'));
|
|
157
|
+
});
|
|
158
|
+
if (devKey) {
|
|
159
|
+
const dev = raw.devices[devKey];
|
|
160
|
+
const feat = (name) => dev.historyFeatures.find(f => f.feature === name);
|
|
161
|
+
const val = (name, prop) => { const f = feat(name); return f?.samples?.[prop]?.value ?? null; };
|
|
162
|
+
const unit = (name, prop) => { const f = feat(name); return f?.samples?.[prop]?.unit ?? ''; };
|
|
163
|
+
apiSummary = {
|
|
164
|
+
timestamp: raw.timestamp,
|
|
165
|
+
gasHeatMonth: val('heating.gas.consumption.summary.heating', 'currentMonth'),
|
|
166
|
+
gasHeatYear: val('heating.gas.consumption.summary.heating', 'currentYear'),
|
|
167
|
+
gasHeat7d: val('heating.gas.consumption.summary.heating', 'lastSevenDays'),
|
|
168
|
+
gasDhwMonth: val('heating.gas.consumption.summary.dhw', 'currentMonth'),
|
|
169
|
+
gasDhwYear: val('heating.gas.consumption.summary.dhw', 'currentYear'),
|
|
170
|
+
heatProdHeatMonth:val('heating.heat.production.summary.heating', 'currentMonth'),
|
|
171
|
+
heatProdHeatYear: val('heating.heat.production.summary.heating', 'currentYear'),
|
|
172
|
+
heatProdDhwMonth: val('heating.heat.production.summary.dhw', 'currentMonth'),
|
|
173
|
+
heatProdDhwYear: val('heating.heat.production.summary.dhw', 'currentYear'),
|
|
174
|
+
pwrConsHeatMonth: val('heating.power.consumption.summary.heating','currentMonth'),
|
|
175
|
+
pwrConsHeatYear: val('heating.power.consumption.summary.heating','currentYear'),
|
|
176
|
+
burnerLifeStarts: val('heating.burners.0.statistics', 'starts'),
|
|
177
|
+
burnerLifeHours: val('heating.burners.0.statistics', 'hours'),
|
|
178
|
+
};
|
|
179
|
+
// Thermal efficiency = heat produced / (gas consumed Γ PCS)
|
|
180
|
+
const GAS_PCS_EFF = 10.55;
|
|
181
|
+
if (apiSummary.heatProdHeatYear && apiSummary.gasHeatYear && apiSummary.gasHeatYear > 0) {
|
|
182
|
+
apiSummary.thermalEffYear = Math.min(110, Math.round(
|
|
183
|
+
(apiSummary.heatProdHeatYear / (apiSummary.gasHeatYear * GAS_PCS_EFF)) * 100
|
|
184
|
+
));
|
|
185
|
+
}
|
|
186
|
+
if (apiSummary.heatProdHeatMonth && apiSummary.gasHeatMonth && apiSummary.gasHeatMonth > 0) {
|
|
187
|
+
apiSummary.thermalEffMonth = Math.min(110, Math.round(
|
|
188
|
+
(apiSummary.heatProdHeatMonth / (apiSummary.gasHeatMonth * GAS_PCS_EFF)) * 100
|
|
189
|
+
));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} catch(e) { /* explore file not present β skip */ }
|
|
194
|
+
|
|
126
195
|
// --- Heating schedule ---
|
|
127
196
|
const SCHED_FILE = SCHED_FILE_PATH;
|
|
128
197
|
let heatingSchedule = null;
|
|
@@ -244,7 +313,9 @@ const avgHeatDemand = activeBurnerRows.length
|
|
|
244
313
|
: null;
|
|
245
314
|
|
|
246
315
|
// --- Burner cycle analysis ---
|
|
247
|
-
//
|
|
316
|
+
// PRIMARY source: API counter delta (burner_starts / burner_hours) β captures ALL ignitions.
|
|
317
|
+
// CSV edge detection misses ~97% of cycles because refresh is 15 min but cycles avg 6 min.
|
|
318
|
+
// We keep edge detection only for the histogram (visual distribution of detected cycles).
|
|
248
319
|
const sortedBoiler = [...boilerRows].sort((a,b) => new Date(a.timestamp) - new Date(b.timestamp));
|
|
249
320
|
const cycles = [];
|
|
250
321
|
let cycleStart = null;
|
|
@@ -253,15 +324,18 @@ for (let i = 0; i < sortedBoiler.length; i++) {
|
|
|
253
324
|
if (on && cycleStart === null) cycleStart = new Date(sortedBoiler[i].timestamp);
|
|
254
325
|
if (!on && cycleStart !== null) {
|
|
255
326
|
const durationMin = (new Date(sortedBoiler[i].timestamp) - cycleStart) / 60000;
|
|
256
|
-
if (durationMin >= 1) cycles.push(durationMin);
|
|
327
|
+
if (durationMin >= 1) cycles.push(durationMin);
|
|
257
328
|
cycleStart = null;
|
|
258
329
|
}
|
|
259
330
|
}
|
|
331
|
+
// cycleCount from CSV edges (used only for histogram β severely underestimates real count)
|
|
260
332
|
const cycleCount = cycles.length;
|
|
261
333
|
const avgCycleDur = cycleCount ? (cycles.reduce((a,b)=>a+b,0)/cycleCount).toFixed(0) : null;
|
|
262
334
|
const shortestCycle = cycleCount ? Math.min(...cycles).toFixed(0) : null;
|
|
263
335
|
const shortCycleCls = shortestCycle ? (parseFloat(shortestCycle) < 5 ? 'warn' : 'good') : 'neutral';
|
|
264
336
|
|
|
337
|
+
// realCycleCount, realAvgDur, sph already defined above from delta calculation
|
|
338
|
+
|
|
265
339
|
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
266
340
|
// HEATING SYSTEM ASSISTANT
|
|
267
341
|
// boilerNominalPowerKW: CLI param > env var > default 0 (kW-based cards hidden)
|
|
@@ -306,12 +380,16 @@ const peakLoadKW = (heatLossCoeff !== null && avgRoomNum !== null)
|
|
|
306
380
|
// Boiler sizing check
|
|
307
381
|
const boilerOversized = (hasBoilerKW && peakLoadKW !== null && BOILER_KW > parseFloat(peakLoadKW) * 2);
|
|
308
382
|
|
|
309
|
-
// Cycle diagnostics
|
|
310
|
-
const reportHours
|
|
311
|
-
|
|
312
|
-
const
|
|
383
|
+
// Cycle diagnostics β use API delta values (real), not CSV edge counts (severely undersampled)
|
|
384
|
+
const reportHours = DAYS * 24;
|
|
385
|
+
// cyclesPerHour: prefer real API delta; CSV edge fallback only if no delta available
|
|
386
|
+
const cyclesPerHour = sph || (cycleCount && reportHours ? (cycleCount / reportHours).toFixed(2) : null);
|
|
387
|
+
// avgCycleDurNum: prefer real API-derived value
|
|
388
|
+
const avgCycleDurNum = realAvgDur ? parseFloat(realAvgDur) : (avgCycleDur ? parseFloat(avgCycleDur) : null);
|
|
313
389
|
const shortCycling = avgCycleDurNum !== null && avgCycleDurNum < 5;
|
|
314
390
|
const excessiveCycling = cyclesPerHour !== null && parseFloat(cyclesPerHour) > 6;
|
|
391
|
+
// Note: with real API data, Vitodens typically shows 6-12 starts/hour in partial load,
|
|
392
|
+
// which is expected behavior. Only flag if combined with short avg duration.
|
|
315
393
|
|
|
316
394
|
// Flow temp heuristic
|
|
317
395
|
const avgFlowNum = avgFlow ? parseFloat(avgFlow) : null;
|
|
@@ -326,8 +404,8 @@ const inefficientOp = avgModNum !== null && avgCycleDurNum !== null && avgModNum
|
|
|
326
404
|
const insights = [];
|
|
327
405
|
if (shortCycling || excessiveCycling)
|
|
328
406
|
insights.push({ type:'warn', text: shortCycling
|
|
329
|
-
? `Short cycling detected β avg cycle ${avgCycleDur} min (ideal > 10 min).
|
|
330
|
-
: `High cycling rate β ${cyclesPerHour}
|
|
407
|
+
? `Short cycling detected β avg cycle ${realAvgDur ?? avgCycleDur} min (ideal > 10 min). With ${cyclesPerHour} starts/hour, the boiler is starting too frequently. Check minimum modulation setting (technician), system hydraulic balance, and pump speed.`
|
|
408
|
+
: `High cycling rate β ${cyclesPerHour} starts/hour from API counter. Consider raising the heating curve setpoint or requesting minimum burner runtime calibration.` });
|
|
331
409
|
if (inefficientOp)
|
|
332
410
|
insights.push({ type:'warn', text: `Boiler running at low modulation (avg ${avgMod}%) with short cycles. Consider lowering the heating curve to reduce cycling.` });
|
|
333
411
|
if (highFlowTemp)
|
|
@@ -357,13 +435,17 @@ if (roomTemps.length >= 10) {
|
|
|
357
435
|
// score = cyclesPerHour Γ (10 / avgCycleDuration) β <1 excellent, 1-3 ok, >3 severe
|
|
358
436
|
let cyclingScore = null, cyclingSeverity = null, cyclingSeverityCls = 'neutral';
|
|
359
437
|
if (cyclesPerHour && avgCycleDurNum && avgCycleDurNum > 0) {
|
|
360
|
-
|
|
438
|
+
// Score uses real API counter data β not CSV edge count
|
|
439
|
+
cyclingScore = (parseFloat(cyclesPerHour) * (10 / avgCycleDurNum)).toFixed(1);
|
|
361
440
|
const sc2 = parseFloat(cyclingScore);
|
|
362
|
-
if (sc2 <
|
|
363
|
-
else if (sc2 <
|
|
364
|
-
else
|
|
365
|
-
|
|
366
|
-
|
|
441
|
+
if (sc2 < 2) { cyclingSeverity = 'Good'; cyclingSeverityCls = 'good'; }
|
|
442
|
+
else if (sc2 < 5) { cyclingSeverity = 'Moderate'; cyclingSeverityCls = 'warn'; }
|
|
443
|
+
else if (sc2 < 10){ cyclingSeverity = 'High'; cyclingSeverityCls = 'warn'; }
|
|
444
|
+
else { cyclingSeverity = 'Severe'; cyclingSeverityCls = 'bad'; }
|
|
445
|
+
if (sc2 >= 10)
|
|
446
|
+
insights.push({ type:'warn', text: `Cycling severity score ${cyclingScore} β severe. The boiler starts ${cyclesPerHour} times/hour with avg cycle duration ${avgCycleDurNum.toFixed(1)} min. Check minimum burner runtime, system pressure, and pump speed. A technician should calibrate minimum modulation.` });
|
|
447
|
+
else if (sc2 >= 5)
|
|
448
|
+
insights.push({ type:'warn', text: `Cycling severity score ${cyclingScore} β high. Boiler starts ${cyclesPerHour} times/hour (avg ${avgCycleDurNum.toFixed(1)} min/cycle). Consider increasing heating curve or requesting minimum modulation calibration.` });
|
|
367
449
|
}
|
|
368
450
|
|
|
369
451
|
// ββ Min modulation check βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -810,24 +892,33 @@ footer{text-align:center;font-size:10px;color:#bbb;padding:16px}
|
|
|
810
892
|
|
|
811
893
|
<div class="box">
|
|
812
894
|
<h2>Boiler β Burner</h2>
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
${sc('
|
|
895
|
+
<!-- KPI row 1: real cycle metrics from API counter -->
|
|
896
|
+
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#999;margin-bottom:8px">Cycle performance (from API counters β precise)</div>
|
|
897
|
+
<div class="grid" style="margin-bottom:6px">
|
|
898
|
+
${realCycleCount !== null ? sc('Cycles in period', realCycleCount, '', badge(effCls, effLabel)) : ''}
|
|
899
|
+
${sph ? sc('Starts/hour', sph, '/h', badge(effCls, parseFloat(sph)<3?'Normal':parseFloat(sph)<6?'High':'Severe')) : ''}
|
|
900
|
+
${realAvgDur ? sc('Avg cycle duration', realAvgDur, ' min', badge(parseFloat(realAvgDur)<5?'warn':parseFloat(realAvgDur)<10?'warn':'good', parseFloat(realAvgDur)<5?'Short cycling':parseFloat(realAvgDur)<10?'Check':'OK')) : ''}
|
|
901
|
+
${burnerStarts ? sc('Lifetime starts', burnerStarts) : ''}
|
|
902
|
+
${burnerHours ? sc('Lifetime hours', burnerHours, 'h') : ''}
|
|
817
903
|
${sc('Burner active', burnerOnPct, '% samples')}
|
|
818
|
-
|
|
904
|
+
</div>
|
|
905
|
+
<p class="note" style="margin-bottom:14px">β‘ Cycle count uses <strong>API firmware counters</strong> (burner_starts delta) β captures all ignitions regardless of 15-min CSV sample rate. CSV edge detection would miss ~97% of cycles at this cycle frequency.</p>
|
|
906
|
+
|
|
907
|
+
<!-- KPI row 2: modulation + gas -->
|
|
908
|
+
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#999;margin-bottom:8px">Modulation & gas (period)</div>
|
|
909
|
+
<div class="grid" style="margin-bottom:6px">
|
|
910
|
+
${sc('Avg modulation (active)', avgMod, '%', badge(parseFloat(avgMod||0)<15?'warn':'good', parseFloat(avgMod||0)<15?'Low mod':'OK'))}
|
|
819
911
|
${sc('Max modulation', maxMod, '%')}
|
|
820
912
|
${avgHeatDemand ? sc('Avg heat demand', avgHeatDemand, ' kW') : ''}
|
|
821
913
|
${hasGasData ? sc('Gas heating today', gasHeatingToday, ' mΒ³') : ''}
|
|
822
914
|
${hasGasData && gasDhwToday ? sc('Gas DHW today', gasDhwToday, ' mΒ³') : ''}
|
|
823
915
|
${hasGasData && gasTotalToday ? sc('Gas total today', gasTotalToday, ' mΒ³') : ''}
|
|
824
|
-
${cycleCount > 0 ? sc('Cycles (period)', cycleCount) : ''}
|
|
825
|
-
${avgCycleDur ? sc('Avg cycle', avgCycleDur, ' min') : ''}
|
|
826
|
-
${shortestCycle ? sc('Shortest cycle', shortestCycle, ' min', badge(shortCycleCls, parseFloat(shortestCycle)<5?'β Short':'OK')) : ''}
|
|
827
916
|
</div>
|
|
917
|
+
|
|
828
918
|
${boilerRows.length < 5 ? `<p class="note">Only ${boilerRows.length} samples β data will accumulate over time (~1 every 15 min).</p>` : ''}
|
|
829
|
-
${boilerRows.length >= 2 ? `<div class="ch"><canvas id="cMod"></canvas></div><div class="ch" style="margin-top:14px"><canvas id="cBurner"></canvas></div
|
|
830
|
-
|
|
919
|
+
${boilerRows.length >= 2 ? `<div class="ch"><canvas id="cMod"></canvas></div><div class="ch" style="margin-top:14px"><canvas id="cBurner"></canvas></div>
|
|
920
|
+
<p class="note">β οΈ Burner ON/OFF bars show only cycles visible within 15-min sampling interval. Actual cycle count (${realCycleCount ?? '?'}) is ${realCycleCount && cycleCount ? Math.round(realCycleCount/Math.max(cycleCount,1)) : '~100'}Γ higher β see API counter KPIs above.</p>` : ''}
|
|
921
|
+
${cycleCount >= 3 ? `<div style="margin-top:18px"><div style="font-size:13px;font-weight:700;color:#1a1a2e;margin-bottom:10px">Cycle duration histogram (sampled)</div><div class="ch"><canvas id="cCycleHist"></canvas></div><p class="note">Distribution of burner ON durations visible in CSV samples. Actual cycle duration from API counters: avg ${realAvgDur ?? '?'} min. Histogram shows only ~${cycleCount} of ${realCycleCount ?? '?'} real cycles.</p></div>` : ''}
|
|
831
922
|
${hasGasChart ? `<div style="margin-top:18px"><div style="font-size:13px;font-weight:700;color:#1a1a2e;margin-bottom:10px">Daily gas consumption (mΒ³)</div><div class="ch-tall"><canvas id="cGas"></canvas></div><p class="note">Stacked bars: heating (dark blue) + DHW (teal). Red line: daily total. Today's bar shows current accumulated value.</p></div>` : ''}
|
|
832
923
|
</div>
|
|
833
924
|
|
|
@@ -836,9 +927,9 @@ footer{text-align:center;font-size:10px;color:#bbb;padding:16px}
|
|
|
836
927
|
<div class="grid">
|
|
837
928
|
${sc('Avg room temp', avgRoom, 'Β°C')}
|
|
838
929
|
${sc('Avg setpoint', avgTarget, 'Β°C')}
|
|
839
|
-
${avgFlow ? sc('Avg flow temp', avgFlow, 'Β°C') : ''}
|
|
840
930
|
${maxFlow ? sc('Max flow temp', maxFlow, 'Β°C') : ''}
|
|
841
931
|
${condensingPct !== null ? sc('Condensing mode', condensingPct, '% time', badge(condensingCls, condensingLabel)) : ''}
|
|
932
|
+
${avgFlow ? sc('Avg flow temp', avgFlow, 'Β°C', badge(parseFloat(avgFlow||99)<45?'good':parseFloat(avgFlow||99)<55?'warn':'neutral', parseFloat(avgFlow||99)<45?'Excellent':parseFloat(avgFlow||99)<55?'Condensing':'High')) : ''}
|
|
842
933
|
${scheduleToday ? sc('Today\'s schedule', scheduleToday) : ''}
|
|
843
934
|
</div>
|
|
844
935
|
${progDist.length ? `<div style="margin-bottom:16px"><div class="sl" style="margin-bottom:8px">Program distribution</div>
|
|
@@ -855,8 +946,9 @@ footer{text-align:center;font-size:10px;color:#bbb;padding:16px}
|
|
|
855
946
|
${peakLoadKW ? sc('Est. peak load', peakLoadKW, ' kW', '<span style=\"font-size:10px;color:#888\">at '+DESIGN_TEMP+'Β°C outdoor</span>') : ''}
|
|
856
947
|
${hasBoilerKW ? sc('Boiler nominal', BOILER_KW, ' kW', boilerOversized ? badge('warn','Oversized') : badge('good','OK')) : ''}
|
|
857
948
|
${houseEff ? sc('House efficiency', houseEff.label, '', badge(houseEff.cls, heatLossCoeff+' kW/Β°C')) : ''}
|
|
858
|
-
${cyclesPerHour ? sc('
|
|
859
|
-
${
|
|
949
|
+
${cyclesPerHour ? sc('Starts/hour', cyclesPerHour, '/h', badge(effCls, parseFloat(cyclesPerHour)<3?'Normal':parseFloat(cyclesPerHour)<6?'High':'Severe')) : ''}
|
|
950
|
+
${realAvgDur ? sc('Avg cycle duration', realAvgDur, ' min', badge(parseFloat(realAvgDur)<5?'warn':parseFloat(realAvgDur)<10?'warn':'good', parseFloat(realAvgDur)<5?'Short':parseFloat(realAvgDur)<10?'Check':'OK')) : ''}
|
|
951
|
+
${cyclingScore ? sc('Cycling score', cyclingSeverity, '', badge(cyclingSeverityCls, cyclingScore)) : ''}
|
|
860
952
|
${comfortStddev ? sc('Comfort stability', comfortRating, '', badge(comfortCls, 'Β±'+comfortStddev+'Β°C')) : ''}
|
|
861
953
|
${gasEfficiencyPct ? sc('Est. efficiency', gasEfficiencyPct, '%') : ''}
|
|
862
954
|
${heatCurveCorr ? sc('Heating curve', heatCurveBehaviour, '', badge(heatCurveCls, 'r='+heatCurveCorr)) : ''}
|
|
@@ -886,6 +978,42 @@ footer{text-align:center;font-size:10px;color:#bbb;padding:16px}
|
|
|
886
978
|
${dhwRows.length >= 2 ? `<div class="ch-tall"><canvas id="cDhw"></canvas></div>` : ''}
|
|
887
979
|
</div>
|
|
888
980
|
|
|
981
|
+
${apiSummary ? `
|
|
982
|
+
<div class="box">
|
|
983
|
+
<h2>π Energy Summary (from Viessmann API)</h2>
|
|
984
|
+
<p class="note" style="margin-bottom:14px">Official aggregated data from Viessmann cloud API. Data snapshot: ${apiSummary.timestamp ? new Date(apiSummary.timestamp).toLocaleString('en-GB') : 'N/A'}. Run <code>viessmann-explore-history.js</code> to refresh.</p>
|
|
985
|
+
|
|
986
|
+
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#999;margin-bottom:8px">Gas consumption (mΒ³)</div>
|
|
987
|
+
<div class="grid" style="margin-bottom:16px">
|
|
988
|
+
${apiSummary.gasHeat7d !== null ? sc('Heating last 7 days', apiSummary.gasHeat7d, ' mΒ³') : ''}
|
|
989
|
+
${apiSummary.gasHeatMonth !== null ? sc('Heating this month', apiSummary.gasHeatMonth,' mΒ³') : ''}
|
|
990
|
+
${apiSummary.gasHeatYear !== null ? sc('Heating this year', apiSummary.gasHeatYear, ' mΒ³') : ''}
|
|
991
|
+
${apiSummary.gasDhwMonth !== null ? sc('DHW this month', apiSummary.gasDhwMonth, ' mΒ³') : ''}
|
|
992
|
+
${apiSummary.gasDhwYear !== null ? sc('DHW this year', apiSummary.gasDhwYear, ' mΒ³') : ''}
|
|
993
|
+
${(apiSummary.gasHeatMonth !== null && apiSummary.gasDhwMonth !== null) ? sc('Total this month', +(apiSummary.gasHeatMonth + apiSummary.gasDhwMonth).toFixed(1), ' mΒ³') : ''}
|
|
994
|
+
${(apiSummary.gasHeatYear !== null && apiSummary.gasDhwYear !== null) ? sc('Total this year', +(apiSummary.gasHeatYear + apiSummary.gasDhwYear).toFixed(1), ' mΒ³') : ''}
|
|
995
|
+
</div>
|
|
996
|
+
|
|
997
|
+
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#999;margin-bottom:8px">Heat produced (kWh)</div>
|
|
998
|
+
<div class="grid" style="margin-bottom:16px">
|
|
999
|
+
${apiSummary.heatProdHeatMonth !== null ? sc('Heating this month', apiSummary.heatProdHeatMonth, ' kWh') : ''}
|
|
1000
|
+
${apiSummary.heatProdHeatYear !== null ? sc('Heating this year', apiSummary.heatProdHeatYear, ' kWh') : ''}
|
|
1001
|
+
${apiSummary.heatProdDhwMonth !== null ? sc('DHW this month', apiSummary.heatProdDhwMonth, ' kWh') : ''}
|
|
1002
|
+
${apiSummary.heatProdDhwYear !== null ? sc('DHW this year', apiSummary.heatProdDhwYear, ' kWh') : ''}
|
|
1003
|
+
${apiSummary.pwrConsHeatMonth !== null ? sc('Pump power month', apiSummary.pwrConsHeatMonth, ' kWh') : ''}
|
|
1004
|
+
${apiSummary.pwrConsHeatYear !== null ? sc('Pump power year', apiSummary.pwrConsHeatYear, ' kWh') : ''}
|
|
1005
|
+
</div>
|
|
1006
|
+
|
|
1007
|
+
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#999;margin-bottom:8px">Thermal efficiency (heat produced / gas input Γ PCS 10.55 kWh/mΒ³)</div>
|
|
1008
|
+
<div class="grid">
|
|
1009
|
+
${apiSummary.thermalEffMonth !== undefined ? sc('Efficiency this month', apiSummary.thermalEffMonth, '%', badge(apiSummary.thermalEffMonth >= 90 ? 'good' : 'warn', apiSummary.thermalEffMonth >= 90 ? 'Condensing β' : 'Check')) : ''}
|
|
1010
|
+
${apiSummary.thermalEffYear !== undefined ? sc('Efficiency this year', apiSummary.thermalEffYear, '%', badge(apiSummary.thermalEffYear >= 90 ? 'good' : 'warn', apiSummary.thermalEffYear >= 90 ? 'Condensing β' : 'Check')) : ''}
|
|
1011
|
+
${apiSummary.burnerLifeStarts !== null ? sc('Lifetime starts', apiSummary.burnerLifeStarts) : ''}
|
|
1012
|
+
${apiSummary.burnerLifeHours !== null ? sc('Lifetime hours', apiSummary.burnerLifeHours, 'h') : ''}
|
|
1013
|
+
</div>
|
|
1014
|
+
<p class="note" style="margin-top:10px">Thermal efficiency >100% is possible for condensing boilers (latent heat recovery). Values >105% may indicate rounding in Viessmann API data.</p>
|
|
1015
|
+
</div>` : ''}
|
|
1016
|
+
|
|
889
1017
|
${gasForecast ? `
|
|
890
1018
|
<div class="box">
|
|
891
1019
|
<h2>β½ Gas Forecast</h2>
|