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 CHANGED
@@ -1033,7 +1033,7 @@ For issues and questions:
1033
1033
 
1034
1034
  ## πŸ“ˆ Changelog
1035
1035
 
1036
- ### [2.0.44] - 2026-03-15
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.44
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.44 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.
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.44",
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.44 (2026-03-15)
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":"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"}
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: comfort.active OR programs.active === comfortFeatureSuffix.
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: comfort.active=${comfortActive} programs.active=${activeProg?.properties?.value?.value} β†’ ${this.states.ExtendedHeatingActive}`);
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
- // Try comfort.deactivate first (clean method)
827
- const deactivateCmd = comfortProgram.commands?.['deactivate'];
828
- if (deactivateCmd?.isExecutable) {
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
- this.states.ExtendedHeatingActive = false;
832
- this.platform.log.info(`Heating circuit ${this.circuitNumber} extended heating deactivated`);
833
- return;
845
+ deactivated = true;
846
+ this.platform.log.info(`HC${this.circuitNumber} extended heating deactivated via comfort.deactivate`);
834
847
  }
835
848
  }
836
- // Fallback: setTemperature back to normal program temperature
837
- // This works even when deactivate is not executable β€” reduces comfort temp
838
- // back to normal level which effectively cancels the boost.
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, `heating.circuits.${this.circuitNumber}.operating.programs.${this.comfortFeatureSuffix}`, 'setTemperature', { targetTemperature: normalTemp });
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
- this.states.ExtendedHeatingActive = false;
848
- this.platform.log.info(`Heating circuit ${this.circuitNumber} extended heating cancelled via setTemperature β†’ ${normalTemp}Β°C`);
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
- // Final fallback via tryAlternativeExtendedHeating
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 mode');
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: read from programs.active === comfortFeatureSuffix OR comfort.active.
1495
- // NOTE: forcedLastFromSchedule.active is NOT a reliable indicator β€” it is also True during
1496
- // normal schedule transitions (e.g. maintaining 'normal' outside active slots).
1497
- // The authoritative source is programs.active value or comfort.active property.
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
- if (!extendedActive) {
1507
- }
1508
- this.platform.log.debug(`HC${this.circuitNumber} ExtendedHeating: comfort.active=${comfortActive} activeProgram=${activeProg?.properties?.value?.value} β†’ ${extendedActive}`);
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;
@@ -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.44";
12
+ export declare const PLUGIN_VERSION = "2.0.46";
13
13
  /**
14
14
  * Default configuration values
15
15
  */
package/dist/settings.js CHANGED
@@ -12,7 +12,7 @@ exports.PLUGIN_NAME = 'homebridge-viessmann-vicare';
12
12
  /**
13
13
  * Plugin version for User-Agent and logging
14
14
  */
15
- exports.PLUGIN_VERSION = '2.0.44';
15
+ exports.PLUGIN_VERSION = '2.0.46';
16
16
  /**
17
17
  * Default configuration values
18
18
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-viessmann-vicare",
3
- "version": "2.0.44",
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": {
@@ -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
- const sph = (burnerStarts && burnerHours && parseFloat(burnerHours) > 0) ? (parseFloat(burnerStarts)/parseFloat(burnerHours)).toFixed(2) : null;
91
- const effCls = sph ? (parseFloat(sph) < 2 ? 'good' : 'warn') : 'neutral';
92
- const effLabel = sph ? (parseFloat(sph) < 2 ? 'Good' : 'High cycling') : 'N/A';
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
- // Reconstruct ON/OFF edges from boilerRows sorted by time
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); // ignore <1 min noise
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 = DAYS * 24;
311
- const cyclesPerHour = cycleCount && reportHours ? (cycleCount / reportHours).toFixed(2) : null;
312
- const avgCycleDurNum = avgCycleDur ? parseFloat(avgCycleDur) : null;
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). Check system pressure, pump speed, or boiler minimum modulation setting.`
330
- : `High cycling rate β€” ${cyclesPerHour} cycles/hour. Consider increasing heating curve or minimum burner runtime.` });
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
- cyclingScore = (parseFloat(cyclesPerHour) * (10 / avgCycleDurNum)).toFixed(2);
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 < 1) { cyclingSeverity = 'Excellent'; cyclingSeverityCls = 'good'; }
363
- else if (sc2 < 3) { cyclingSeverity = 'Acceptable'; cyclingSeverityCls = 'warn'; }
364
- else { cyclingSeverity = 'Severe'; cyclingSeverityCls = 'bad'; }
365
- if (sc2 >= 3)
366
- insights.push({ type:'warn', text: `Cycling severity score ${cyclingScore} (severe). Boiler is cycling too frequently and too briefly β€” check minimum burner runtime setting or hydraulic balancing.` });
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
- <div class="grid">
814
- ${sc('Lifetime starts', burnerStarts)}
815
- ${sc('Running hours', burnerHours, 'h')}
816
- ${sc('Starts/hour', sph, '', badge(effCls, effLabel))}
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
- ${sc('Avg modulation', avgMod, '%')}
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
- ${cycleCount >= 3 ? `<div style="margin-top:18px"><div style="font-size:13px;font-weight:700;color:#1a1a2e;margin-bottom:10px">Cycle duration histogram</div><div class="ch"><canvas id="cCycleHist"></canvas></div><p class="note">Distribution of burner ON durations. Short cycles (&lt;5 min) indicate short-cycling.</p></div>` : ''}
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('Cycles/hour', cyclesPerHour, '', (excessiveCycling ? badge('warn','High') : badge('good','OK'))) : ''}
859
- ${cyclingScore ? sc('Cycling severity', cyclingSeverity, '', badge(cyclingSeverityCls, cyclingScore)) : ''}
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>