homebridge-viessmann-vicare 2.0.52 → 2.0.53

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,6 +1033,11 @@ For issues and questions:
1033
1033
 
1034
1034
  ## 📈 Changelog
1035
1035
 
1036
+ ### [2.0.53] - 2026-03-17
1037
+ - feat: viessmann-sync-events.js — fetches burner ON/OFF events from API events-history with second-precision timestamps
1038
+ - fix: Device Messages section now correctly inside max-width container
1039
+ - fix: viessmann-sync-events.js added to npm package files
1040
+
1036
1041
  ### [2.0.52] - 2026-03-17
1037
1042
  - feat: hourly burner heatmap in report (24-cell grid, runtime %, outdoor temp on hover)
1038
1043
  - feat: daily thermal efficiency chart from CSV (heat_heating_day_kwh / gas × 10.55)
package/SETUP-GUIDE.md CHANGED
@@ -1,4 +1,4 @@
1
- # Complete Setup Guide - v2.0.52
1
+ # Complete Setup Guide - v2.0.53
2
2
 
3
3
  ## Overview
4
4
 
@@ -1228,6 +1228,10 @@ sudo systemctl restart homebridge
1228
1228
 
1229
1229
  ## Changelog
1230
1230
 
1231
+ ### v2.0.53 (2026-03-17)
1232
+ - feat: new script viessmann-sync-events.js for precise burner event history from API
1233
+ - fix: Device Messages layout fixed (inside wrap container)
1234
+
1231
1235
  ### v2.0.52 (2026-03-17)
1232
1236
  - feat: 3 new report sections — hourly heatmap, daily efficiency chart, energy flow
1233
1237
  - feat: icons on all report section headers
@@ -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.52";
12
+ export declare const PLUGIN_VERSION = "2.0.53";
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.52';
15
+ exports.PLUGIN_VERSION = '2.0.53';
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.52",
3
+ "version": "2.0.53",
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": {
@@ -51,7 +51,8 @@
51
51
  "README.md",
52
52
  "SETUP-GUIDE.md",
53
53
  "viessmann-report.js",
54
- "viessmann-report-server.js"
54
+ "viessmann-report-server.js",
55
+ "viessmann-sync-events.js"
55
56
  ],
56
57
  "funding": {
57
58
  "type": "individual",
@@ -1213,8 +1213,6 @@ ${energyRows.length >= 1 ? `
1213
1213
  ${hasWallbox && energyRows.length >= 2 ? `<div class="ch" style="margin-top:14px"><canvas id="cWallbox"></canvas></div>` : ''}
1214
1214
  </div>` : ''}
1215
1215
 
1216
- </div>
1217
-
1218
1216
  <div class="box" id="device-messages">
1219
1217
  <h2>🔔 Device Messages</h2>
1220
1218
  <p class="note" style="margin-bottom:12px">Status, info and fault codes reported by the device. Translated from Viessmann service documentation.</p>
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * viessmann-sync-events.js
4
+ *
5
+ * Fetches events-history from Viessmann API and writes burner ON/OFF events
6
+ * directly into the CSV history file with precise timestamps.
7
+ *
8
+ * Uses S.6 active=true/false (ignition start/end) as the definitive burner state.
9
+ * Also writes S.39 (heating demand) and S.1 (DHW demand) as context events.
10
+ *
11
+ * Usage:
12
+ * node viessmann-sync-events.js --installation 2045571 [--days 7] [--path /var/lib/homebridge]
13
+ *
14
+ * Run via cron daily (e.g. 04:00) to backfill the previous day:
15
+ * 0 4 * * * node /usr/local/lib/node_modules/homebridge-viessmann-vicare/viessmann-sync-events.js --installation 2045571
16
+ */
17
+
18
+ 'use strict';
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const https = require('https');
22
+ const http = require('http');
23
+
24
+ // ── CLI args ───────────────────────────────────────────────────────────────
25
+ const args = process.argv.slice(2);
26
+ const getArg = (flag, def) => { const i = args.indexOf(flag); return i !== -1 && args[i+1] ? args[i+1] : def; };
27
+ const HB_PATH = getArg('--path', '/var/lib/homebridge');
28
+ const INSTALLATION = getArg('--installation', '');
29
+ const DAYS = parseInt(getArg('--days', '7'), 10);
30
+ const DRY_RUN = args.includes('--dry-run');
31
+
32
+ if (!INSTALLATION) {
33
+ console.error('ERROR: --installation <ID> is required');
34
+ process.exit(1);
35
+ }
36
+
37
+ // ── CSV config ─────────────────────────────────────────────────────────────
38
+ const CSV_FILE = path.join(HB_PATH, `viessmann-history-${INSTALLATION}.csv`);
39
+ const TOKEN_FILE = path.join(HB_PATH, 'viessmann-tokens.json');
40
+
41
+ const CSV_COLUMNS = [
42
+ 'timestamp', 'accessory', 'event_type',
43
+ 'burner_active', 'modulation',
44
+ 'room_temp', 'target_temp', 'outside_temp', 'outside_humidity',
45
+ 'dhw_temp', 'dhw_target', 'program', 'mode',
46
+ 'burner_starts', 'burner_hours', 'burner_starts_today', 'burner_hours_today',
47
+ 'flow_temp',
48
+ 'gas_heating_day_m3', 'gas_dhw_day_m3',
49
+ 'gas_heating_month_m3', 'gas_dhw_month_m3',
50
+ 'heat_heating_day_kwh', 'heat_dhw_day_kwh',
51
+ 'heat_heating_month_kwh', 'heat_dhw_month_kwh',
52
+ 'pv_production_w', 'pv_daily_kwh',
53
+ 'battery_level', 'battery_charging_w', 'battery_discharging_w',
54
+ 'grid_feedin_w', 'grid_draw_w',
55
+ 'wallbox_charging', 'wallbox_power_w',
56
+ ];
57
+
58
+ // ── Token ──────────────────────────────────────────────────────────────────
59
+ function getToken() {
60
+ try {
61
+ const tokens = JSON.parse(fs.readFileSync(TOKEN_FILE, 'utf8'));
62
+ const key = Object.keys(tokens)[0];
63
+ return tokens[key].accessToken;
64
+ } catch(e) {
65
+ console.error('ERROR: Cannot read token from', TOKEN_FILE, e.message);
66
+ process.exit(1);
67
+ }
68
+ }
69
+
70
+ // ── HTTP helper ────────────────────────────────────────────────────────────
71
+ function apiGet(url, token) {
72
+ return new Promise((resolve, reject) => {
73
+ const lib = url.startsWith('https') ? https : http;
74
+ const req = lib.get(url, {
75
+ headers: { 'Authorization': 'Bearer ' + token, 'Accept': 'application/json' }
76
+ }, (res) => {
77
+ let body = '';
78
+ res.on('data', chunk => body += chunk);
79
+ res.on('end', () => {
80
+ try { resolve(JSON.parse(body)); }
81
+ catch(e) { reject(new Error('JSON parse error: ' + body.slice(0, 200))); }
82
+ });
83
+ });
84
+ req.on('error', reject);
85
+ req.setTimeout(30000, () => { req.destroy(); reject(new Error('Timeout')); });
86
+ });
87
+ }
88
+
89
+ // ── Fetch all events for the last N days ──────────────────────────────────
90
+ async function fetchEvents(token, installationId, days) {
91
+ const cutoff = new Date();
92
+ cutoff.setDate(cutoff.getDate() - days);
93
+ const cutoffTs = cutoff.getTime();
94
+
95
+ const base = `https://api.viessmann-climatesolutions.com/iot/v2/events-history/installations/${installationId}/events`;
96
+ let cursor = null;
97
+ let allEvents = [];
98
+ let page = 0;
99
+
100
+ while (true) {
101
+ const url = base + '?limit=1000' + (cursor ? '&cursor=' + cursor : '');
102
+ console.log(` Fetching page ${++page}${cursor ? ' (cursor)' : ''}...`);
103
+
104
+ const data = await apiGet(url, token);
105
+ const events = data.data || [];
106
+
107
+ if (!events.length) break;
108
+
109
+ // Events are newest-first — stop when we go past the cutoff
110
+ let doneEarly = false;
111
+ for (const e of events) {
112
+ const ts = new Date(e.eventTimestamp || e.createdAt).getTime();
113
+ if (ts < cutoffTs) { doneEarly = true; break; }
114
+ allEvents.push(e);
115
+ }
116
+
117
+ if (doneEarly) break;
118
+
119
+ cursor = data.cursor?.next;
120
+ if (!cursor) break;
121
+
122
+ // Rate limit protection: 1 req/sec
123
+ await new Promise(r => setTimeout(r, 1100));
124
+ }
125
+
126
+ console.log(` Total events fetched: ${allEvents.length}`);
127
+ return allEvents;
128
+ }
129
+
130
+ // ── Parse events into CSV rows ─────────────────────────────────────────────
131
+ function eventsToCsvRows(events) {
132
+ const rows = [];
133
+
134
+ for (const e of events) {
135
+ const body = e.body || {};
136
+ const code = body.errorCode || '';
137
+ const active= body.active;
138
+ const ts = e.eventTimestamp || e.createdAt;
139
+
140
+ // S.6 = Ignition (most precise burner ON/OFF signal)
141
+ if (code === 'S.6') {
142
+ const row = {};
143
+ CSV_COLUMNS.forEach(c => row[c] = '');
144
+ row.timestamp = ts;
145
+ row.accessory = 'boiler';
146
+ row.event_type = active ? 'burner_on' : 'burner_off';
147
+ row.burner_active = active ? 'true' : 'false';
148
+ rows.push(row);
149
+ }
150
+
151
+ // S.29 = Central heating demand
152
+ // S.1 = DHW demand
153
+ // S.39 = Heating demand (variant — write as context event)
154
+ if (code === 'S.29' || code === 'S.1' || code === 'S.39') {
155
+ const row = {};
156
+ CSV_COLUMNS.forEach(c => row[c] = '');
157
+ row.timestamp = ts;
158
+ row.accessory = 'boiler';
159
+ row.event_type = active
160
+ ? (code === 'S.1' ? 'dhw_demand_on' : 'heat_demand_on')
161
+ : (code === 'S.1' ? 'dhw_demand_off' : 'heat_demand_off');
162
+ row.burner_active = ''; // demand ≠ ignition — don't set burner_active
163
+ rows.push(row);
164
+ }
165
+ }
166
+
167
+ return rows;
168
+ }
169
+
170
+ // ── Read existing CSV timestamps ────────────────────────────────────────────
171
+ function readExistingEventTimestamps() {
172
+ if (!fs.existsSync(CSV_FILE)) return new Set();
173
+ const lines = fs.readFileSync(CSV_FILE, 'utf8').trim().split('\n');
174
+ const header = lines[0].split(',');
175
+ const tsIdx = header.indexOf('timestamp');
176
+ const etIdx = header.indexOf('event_type');
177
+ const existing = new Set();
178
+ for (let i = 1; i < lines.length; i++) {
179
+ const vals = lines[i].split(',');
180
+ const et = etIdx >= 0 ? vals[etIdx] : '';
181
+ // Only track event rows (not snapshots)
182
+ if (et && et !== 'snapshot') {
183
+ existing.add(vals[tsIdx] + '|' + et);
184
+ }
185
+ }
186
+ return existing;
187
+ }
188
+
189
+ // ── Append rows to CSV ─────────────────────────────────────────────────────
190
+ function appendToCsv(rows) {
191
+ const exists = fs.existsSync(CSV_FILE);
192
+ if (!exists) {
193
+ fs.writeFileSync(CSV_FILE, CSV_COLUMNS.join(',') + '\n', 'utf8');
194
+ console.log(' Created new CSV:', CSV_FILE);
195
+ }
196
+
197
+ const lines = rows.map(row =>
198
+ CSV_COLUMNS.map(c => row[c] ?? '').join(',')
199
+ ).join('\n') + '\n';
200
+
201
+ fs.appendFileSync(CSV_FILE, lines, 'utf8');
202
+ }
203
+
204
+ // ── Sort CSV by timestamp after insert ────────────────────────────────────
205
+ function sortCsv() {
206
+ const lines = fs.readFileSync(CSV_FILE, 'utf8').trim().split('\n');
207
+ const header = lines[0];
208
+ const data = lines.slice(1).filter(l => l.trim());
209
+ data.sort((a, b) => {
210
+ const ta = a.split(',')[0];
211
+ const tb = b.split(',')[0];
212
+ return ta < tb ? -1 : ta > tb ? 1 : 0;
213
+ });
214
+ fs.writeFileSync(CSV_FILE, header + '\n' + data.join('\n') + '\n', 'utf8');
215
+ }
216
+
217
+ // ── Main ───────────────────────────────────────────────────────────────────
218
+ async function main() {
219
+ console.log(`[viessmann-sync-events] installation=${INSTALLATION} days=${DAYS} path=${HB_PATH}${DRY_RUN ? ' DRY-RUN' : ''}`);
220
+
221
+ const token = getToken();
222
+
223
+ console.log('Fetching events-history from API...');
224
+ const events = await fetchEvents(token, INSTALLATION, DAYS);
225
+
226
+ if (!events.length) {
227
+ console.log('No events found for the period.');
228
+ return;
229
+ }
230
+
231
+ console.log('Parsing events into CSV rows...');
232
+ const newRows = eventsToCsvRows(events);
233
+ console.log(` Parsed ${newRows.length} rows (S.6 ignition + demand events)`);
234
+
235
+ // Deduplicate against existing CSV
236
+ const existing = readExistingEventTimestamps();
237
+ const toAdd = newRows.filter(r => !existing.has(r.timestamp + '|' + r.event_type));
238
+ console.log(` New rows to add: ${toAdd.length} (skipping ${newRows.length - toAdd.length} already present)`);
239
+
240
+ // Summary
241
+ const byType = {};
242
+ toAdd.forEach(r => { byType[r.event_type] = (byType[r.event_type]||0)+1; });
243
+ console.log(' By type:', JSON.stringify(byType));
244
+
245
+ if (DRY_RUN) {
246
+ console.log('DRY-RUN: not writing to CSV');
247
+ toAdd.slice(0, 5).forEach(r =>
248
+ console.log(' ', r.timestamp, r.event_type)
249
+ );
250
+ return;
251
+ }
252
+
253
+ if (!toAdd.length) {
254
+ console.log('Nothing to add.');
255
+ return;
256
+ }
257
+
258
+ console.log('Writing to CSV...');
259
+ appendToCsv(toAdd);
260
+
261
+ console.log('Sorting CSV by timestamp...');
262
+ sortCsv();
263
+
264
+ // Final stats
265
+ const lines = fs.readFileSync(CSV_FILE, 'utf8').trim().split('\n');
266
+ console.log(`[viessmann-sync-events] Done. CSV now has ${lines.length - 1} rows.`);
267
+ }
268
+
269
+ main().catch(e => {
270
+ console.error('[viessmann-sync-events] FATAL:', e.message);
271
+ process.exit(1);
272
+ });