homebridge-viessmann-vicare 2.0.70 → 2.0.71

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.
@@ -520,10 +520,10 @@
520
520
  "reportServerTimeout": {
521
521
  "title": "Report Server Generation Timeout",
522
522
  "type": "integer",
523
- "default": 300,
524
- "minimum": 60,
525
- "maximum": 1800,
526
- "description": "Maximum time in seconds allowed for viessmann-report.js to generate a report. Increase for large CSV files (90+ days). Default: 300s (5 min). Range: 601800s."
523
+ "default": 600,
524
+ "minimum": 120,
525
+ "maximum": 3600,
526
+ "description": "Maximum time in seconds for report generation. RPi 3 benchmark: ~365s for 90 days / ~1460s for 365 days. Default: 600s. Range: 1203600s."
527
527
  }
528
528
  },
529
529
  "required": [
@@ -57,6 +57,7 @@ export declare class ViessmannPlatform implements DynamicPlatformPlugin {
57
57
  private setupRoomSensorDiscovery;
58
58
  updateAllDevices(): Promise<void>;
59
59
  private updateCycleComplete;
60
+ private writeApiStatusFile;
60
61
  private adjustRefreshInterval;
61
62
  private sleep;
62
63
  private startHealthMonitoring;
@@ -1 +1 @@
1
- {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"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"}
package/dist/platform.js CHANGED
@@ -41,6 +41,7 @@ const dhw_accessory_1 = require("./accessories/dhw-accessory");
41
41
  const heating_circuit_accessory_1 = require("./accessories/heating-circuit-accessory");
42
42
  const energy_accessory_1 = require("./accessories/energy-accessory");
43
43
  const room_sensor_accessory_1 = require("./accessories/room-sensor-accessory");
44
+ const fs = __importStar(require("fs"));
44
45
  const settings_1 = require("./settings");
45
46
  class ViessmannPlatform {
46
47
  log;
@@ -864,9 +865,51 @@ class ViessmannPlatform {
864
865
  const healthScore = this.viessmannAPI.getAPIHealthScore();
865
866
  const healthStatus = this.viessmannAPI.getAPIHealthStatus();
866
867
  this.log.debug(`📊 API Health: ${healthScore}/100 (${healthStatus})`);
868
+ this.writeApiStatusFile(healthScore, healthStatus);
867
869
  }
868
870
  }
869
871
  }
872
+ // ── API Status File ─────────────────────────────────────────────────────────
873
+ // Written after each successful update cycle.
874
+ // Read by viessmann-report-server.js and auth-manager status page to show
875
+ // API usage percentage, health score, and rate limit info in the web UI.
876
+ // Viessmann documented daily limit: 1450 requests/day.
877
+ writeApiStatusFile(healthScore, healthStatus) {
878
+ try {
879
+ const storagePath = this.api.user.storagePath();
880
+ const metrics = this.viessmannAPI.getAPIMetrics();
881
+ const rateLimitSt = this.viessmannAPI.getRateLimitStatus();
882
+ const DAILY_LIMIT = 1450; // Viessmann API documented daily quota
883
+ const status = {
884
+ timestamp: new Date().toISOString(),
885
+ healthScore,
886
+ healthStatus,
887
+ totalRequests: metrics.totalRequests,
888
+ successfulRequests: metrics.successfulRequests,
889
+ failedRequests: metrics.failedRequests,
890
+ rateLimitHits: metrics.rateLimitHits,
891
+ errorRate: parseFloat(metrics.errorRate.toFixed(1)),
892
+ avgResponseTime: Math.round(metrics.averageResponseTime),
893
+ requestsPerMinute: metrics.requestsPerMinute,
894
+ isRateLimited: rateLimitSt.isLimited,
895
+ rateLimitWaitSec: rateLimitSt.waitSeconds ?? 0,
896
+ dailyQuotaExceeded: rateLimitSt.dailyQuotaExceeded,
897
+ // Estimated daily usage — resets with metrics (every 24h)
898
+ // requestsPerMinute × 60 × 24 gives a rough projection
899
+ dailyEstimatedReqs: Math.round(metrics.requestsPerMinute * 60 * 24),
900
+ dailyLimitTotal: DAILY_LIMIT,
901
+ dailyUsagePct: Math.min(100, parseFloat((metrics.totalRequests / DAILY_LIMIT * 100).toFixed(1))),
902
+ pluginVersion: '2.0.71',
903
+ refreshInterval: this.config.refreshInterval || 120000,
904
+ };
905
+ const filePath = require('path').join(storagePath, 'viessmann-api-status.json');
906
+ fs.writeFileSync(filePath, JSON.stringify(status, null, 2), 'utf8');
907
+ this.log.debug(`📊 API status written: health=${healthScore}/100, dailyUsage=${status.dailyUsagePct}%`);
908
+ }
909
+ catch (e) {
910
+ this.log.debug(`📊 Could not write API status file: ${e.message}`);
911
+ }
912
+ }
870
913
  adjustRefreshInterval(increase) {
871
914
  if (this.config.enableRateLimitProtection === false) {
872
915
  return;
@@ -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.70";
12
+ export declare const PLUGIN_VERSION = "2.0.71";
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.70';
15
+ exports.PLUGIN_VERSION = '2.0.71';
16
16
  /**
17
17
  * Default configuration values
18
18
  */
@@ -61,7 +61,7 @@ exports.DEFAULT_CONFIG = {
61
61
  deviceUpdateDelay: 1000,
62
62
  userAgent: `homebridge-viessmann-vicare/${exports.PLUGIN_VERSION}`,
63
63
  },
64
- reportServerTimeout: 300, // seconds — max time for report generation (default 5 min)
64
+ reportServerTimeout: 600, // seconds — default 10 min; RPi3: ~365s/90days ~1460s/365days
65
65
  monitoring: {
66
66
  enableHealthReports: true,
67
67
  healthReportInterval: 3600000, // 1 hour
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-viessmann-vicare",
3
- "version": "2.0.70",
3
+ "version": "2.0.71",
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": {
@@ -195,7 +195,7 @@ function generateReport(params) {
195
195
 
196
196
  // ── UI HTML ────────────────────────────────────────────────────────────────
197
197
 
198
- function buildUI(installations) {
198
+ function buildUI(installations, apiStatus) {
199
199
  const today = new Date().toLocaleDateString('en-GB', { day:'2-digit', month:'short', year:'numeric' });
200
200
  const instOptions = installations.length === 0
201
201
  ? '<option value="">No CSV found — check path</option>'
@@ -203,6 +203,51 @@ function buildUI(installations) {
203
203
  '<option value="' + id + '">' + (id ? 'Installation ' + id : 'Default (no ID)') + '</option>'
204
204
  ).join('');
205
205
 
206
+
207
+ // Build API status card from shared status file
208
+ const apiCardHtml = (() => {
209
+ if (!apiStatus) {
210
+ return `<div class="api-card">
211
+ <div class="api-card-title">🌐 API Usage</div>
212
+ <div class="api-row"><span class="api-lbl">Status</span><span class="api-val" style="color:var(--muted)">No data yet — waiting for first update cycle</span></div>
213
+ </div>`;
214
+ }
215
+ const pct = apiStatus.dailyUsagePct ?? 0;
216
+ const health = apiStatus.healthScore ?? 0;
217
+ const pctCls = pct > 80 ? 'bad' : pct > 50 ? 'warn' : 'ok';
218
+ const hCls = health >= 85 ? 'ok' : health >= 60 ? 'warn' : 'bad';
219
+ const rlTxt = apiStatus.isRateLimited
220
+ ? `<span class="api-val bad">🚫 Rate limited — wait ${apiStatus.rateLimitWaitSec}s</span>`
221
+ : (apiStatus.dailyQuotaExceeded
222
+ ? `<span class="api-val bad">🚫 Daily quota exceeded</span>`
223
+ : `<span class="api-val ok">✓ OK</span>`);
224
+ const age = Math.round((Date.now() - new Date(apiStatus.timestamp).getTime()) / 1000);
225
+ const ageStr = age < 120 ? `${age}s ago` : `${Math.round(age/60)}m ago`;
226
+ return `<div class="api-card">
227
+ <div class="api-card-title">🌐 API Usage (Viessmann limit: ${apiStatus.dailyLimitTotal ?? 1450} req/day)</div>
228
+ <div class="api-row">
229
+ <span class="api-lbl">Daily usage (est.)</span>
230
+ <span class="api-val">
231
+ <span class="bar-wrap"><span class="bar-fill ${pctCls}" style="width:${Math.min(pct,100)}%"></span></span>
232
+ <span class="${pctCls}">${pct}%</span>
233
+ &nbsp;<span style="color:var(--muted);font-size:10px">(~${apiStatus.dailyEstimatedReqs ?? '?'} req/day)</span>
234
+ </span>
235
+ </div>
236
+ <div class="api-row">
237
+ <span class="api-lbl">Health score</span>
238
+ <span class="api-val">
239
+ <span class="bar-wrap"><span class="bar-fill ${hCls}" style="width:${health}%"></span></span>
240
+ <span class="${hCls}">${health}/100</span>
241
+ &nbsp;<span style="color:var(--muted);font-size:10px">${apiStatus.healthStatus ?? ''}</span>
242
+ </span>
243
+ </div>
244
+ <div class="api-row"><span class="api-lbl">Rate limit</span>${rlTxt}</div>
245
+ <div class="api-row"><span class="api-lbl">Avg response</span><span class="api-val">${apiStatus.avgResponseTime ?? 0} ms</span></div>
246
+ <div class="api-row"><span class="api-lbl">Error rate</span><span class="api-val ${apiStatus.errorRate > 10 ? 'bad' : apiStatus.errorRate > 5 ? 'warn' : 'ok'}">${apiStatus.errorRate ?? 0}%</span></div>
247
+ <div class="api-row"><span class="api-lbl">Last updated</span><span class="api-val" style="color:var(--muted)">${ageStr}</span></div>
248
+ </div>`;
249
+ })();
250
+
206
251
  return `<!DOCTYPE html>
207
252
  <html lang="en">
208
253
  <head>
@@ -256,6 +301,16 @@ input:focus,select:focus{outline:none;border-color:var(--accent)}
256
301
  @keyframes spin{to{transform:rotate(360deg)}}
257
302
  .spinner{display:inline-block;width:13px;height:13px;border:2px solid rgba(249,115,22,.3);border-top-color:var(--accent);border-radius:50%;animation:spin .7s linear infinite;vertical-align:middle;margin-right:6px}
258
303
  footer{margin-top:36px;font-family:'Space Mono',monospace;font-size:10px;color:var(--muted);text-align:center;opacity:.4}
304
+ .api-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:16px 20px;width:100%;max-width:560px;margin-bottom:14px}
305
+ .api-card-title{font-family:'Space Mono',monospace;font-size:10px;text-transform:uppercase;letter-spacing:2px;color:var(--accent);margin-bottom:12px}
306
+ .api-row{display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid var(--border);font-size:12px}
307
+ .api-row:last-child{border-bottom:none}
308
+ .api-lbl{color:var(--muted);font-family:'Space Mono',monospace;font-size:11px}
309
+ .api-val{font-weight:600;font-family:'Space Mono',monospace;font-size:11px}
310
+ .api-val.ok{color:var(--good)}.api-val.warn{color:var(--warn)}.api-val.bad{color:var(--bad)}
311
+ .bar-wrap{width:100px;height:6px;background:var(--border);border-radius:3px;overflow:hidden;display:inline-block;vertical-align:middle;margin-right:6px}
312
+ .bar-fill{height:100%;border-radius:3px}
313
+ .bar-fill.ok{background:var(--good)}.bar-fill.warn{background:var(--warn)}.bar-fill.bad{background:var(--bad)}
259
314
  </style>
260
315
  </head>
261
316
  <body>
@@ -345,6 +400,7 @@ footer{margin-top:36px;font-family:'Space Mono',monospace;font-size:10px;color:v
345
400
  Generate Report
346
401
  </button>
347
402
  <div id="status"></div>
403
+ ${apiCardHtml}
348
404
  <footer>homebridge-viessmann-vicare &nbsp;·&nbsp; report server &nbsp;·&nbsp; ${HB_PATH} &nbsp;·&nbsp; ${LAN_IP}:${PORT}</footer>
349
405
 
350
406
  <script>
@@ -424,7 +480,8 @@ const server = http.createServer(async (req, res) => {
424
480
 
425
481
  if (pathname === '/' || pathname === '') {
426
482
  const installations = detectInstallations();
427
- const html = buildUI(installations);
483
+ const apiStatus = readApiStatus(HB_PATH);
484
+ const html = buildUI(installations, apiStatus);
428
485
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
429
486
  res.end(html);
430
487
  dbg(`← 200 / (${Date.now()-t0}ms, ${installations.length} installation(s))`);