@mxtommy/kip 4.7.0-beta.9 → 4.8.0-beta.1

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +19 -16
  3. package/package.json +31 -42
  4. package/plugin/history-series.service.js +25 -3
  5. package/plugin/index.js +123 -37
  6. package/plugin/kip-series-contract.js +8 -0
  7. package/plugin/openApi.json +19 -2
  8. package/public/3rdpartylicenses.txt +0 -26
  9. package/public/assets/help-docs/dashboards.md +2 -0
  10. package/public/assets/svg/icons.svg +12 -0
  11. package/public/assets/svg/symbols.svg +68 -0
  12. package/public/{chunk-2GOHQZH5.js → chunk-2TP7C66X.js} +2 -2
  13. package/public/chunk-4KHUCTYN.js +3 -0
  14. package/public/{chunk-FYDLTNP4.js → chunk-6ISGIPNP.js} +1 -1
  15. package/public/{chunk-PV5PXDO5.js → chunk-7JGTAL26.js} +7 -7
  16. package/public/{chunk-POMIQBAL.js → chunk-C23GLDFH.js} +1 -1
  17. package/public/{chunk-BGGO4PGD.js → chunk-CB4E7PPA.js} +1 -1
  18. package/public/{chunk-AZC2WKQI.js → chunk-DNM5XUIF.js} +1 -1
  19. package/public/{chunk-HSKVTFFQ.js → chunk-HF7V6XFA.js} +1 -1
  20. package/public/{chunk-4YDVZHMH.js → chunk-HXR2BKDL.js} +1 -1
  21. package/public/{chunk-MXUEYEZU.js → chunk-ILQQCGMJ.js} +1 -1
  22. package/public/chunk-IXUKD73N.js +5 -0
  23. package/public/{chunk-CSIELI2Z.js → chunk-JMZYPL54.js} +1 -1
  24. package/public/{chunk-AQROQY2F.js → chunk-KJCBQE6W.js} +1 -1
  25. package/public/chunk-LEY6MANN.js +16 -0
  26. package/public/{chunk-PUPM3HUQ.js → chunk-O5BTKN5D.js} +1 -1
  27. package/public/chunk-OH5KNIQ7.js +50 -0
  28. package/public/{chunk-PTLDR7X7.js → chunk-Q32FSCUX.js} +1 -1
  29. package/public/chunk-RIX4VQJ2.js +1 -0
  30. package/public/{chunk-IENESD5Q.js → chunk-SHJMXSDM.js} +1 -1
  31. package/public/{chunk-SUWMN3AE.js → chunk-T4VTC6GW.js} +1 -1
  32. package/public/{chunk-WJFXI5PQ.js → chunk-VFJD3XKT.js} +1 -1
  33. package/public/{chunk-BQPPRM7O.js → chunk-VFZSH4TC.js} +1 -1
  34. package/public/{chunk-YY4ZUJFI.js → chunk-W6MCE3GH.js} +1 -1
  35. package/public/chunk-WAOG456B.js +2 -0
  36. package/public/chunk-XBOM6DMF.js +9 -0
  37. package/public/index.html +1 -1
  38. package/public/main-BECMST5R.js +1 -0
  39. package/public/polyfills-BWA36QKG.js +1 -0
  40. package/public/chunk-CHJNKZ4A.js +0 -50
  41. package/public/chunk-CLSJS3SX.js +0 -3
  42. package/public/chunk-CRS5IXO7.js +0 -2
  43. package/public/chunk-JFTWNT5T.js +0 -1
  44. package/public/chunk-M37BLWHF.js +0 -5
  45. package/public/chunk-P7YPDCAJ.js +0 -9
  46. package/public/chunk-PZ6I6W3H.js +0 -16
  47. package/public/main-4UU5FOPF.js +0 -1
  48. package/public/polyfills-L4FJGPOC.js +0 -2
package/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ # v4.7.0
2
+ ## New Features
3
+ * Battery Monitor Widget: Stay on top of your vessel’s power system with a dedicated compact Battery Monitor Widget. Instantly view individual batteries or whole banks, including State of Charge, remaining capacity, remaining time, voltage, current, power flow, and temperature. Batteries are detected automatically, with Signal K Zones support for clear warning and alarm visibility at a glance.
4
+ ## Improvements
5
+ * Faster first-load widget data display so dashboards feel more immediate and alive
6
+ * Smoother dashboard transitions with reduced startup animation on Radial, Linear, Compass, Compact Linear, Windsteer, Racesteer, and Autopilot widgets
7
+ * More robust History API integration using the server-provided HistoryAPI type and improved registration cleanup. Thanks to @tkurki
8
+ * Newly added widgets now open their options automatically, speeding up setup and reducing extra taps
9
+ ## Fixes
10
+ * Fixed the plugin-config-data directory location. Fixes #1006
11
+ * Fixed documentation link references
1
12
  # v4.6.0
2
13
  ## Improvements
3
14
  * Built-in Time-Series storage and History-API provider now use the native node:sqlite feature, eliminating binary and external dependencies.
package/README.md CHANGED
@@ -96,31 +96,34 @@ Organize your dashboards and access tools.
96
96
 
97
97
  ## Widget Library
98
98
  All KIP widgets are visual presentation controls that are very versatile, with multiple advanced configuration options available to suit your needs:
99
- - **Numeric display**: Create gauges to display any numerical data sent by your system: SOG, depth, wind speed, VMG, refrigerator temperature, weather data, etc.
100
- - **Text display**: Create gauges to display any textual data sent by your system: MPPT state, vessel details, next waypoint, Fusion radio song information, noon and sun phases, any system component configuration detail or status available, etc.
101
- - **Label**: A static text widget.
102
- - **Date display**: A timezone-aware control with flexible presentation formatting support.
103
- - **Position display**: Position coordinates in textual format.
104
- - **Zones State Panel**: Monitor the health/state of multiple sensors and devices at a glance. Configure multiple paths per panel; each control uses KIP’s zone severity colors and status messages (driven by Signal K metadata zones) so warnings and alarms stand out immediately.
105
- - **Boolean Control Panel**: A switchboard to configure and operate remote devices: light switches, bilge pumps, solenoids, or any Signal K path that supports boolean PUT operations.
106
- - **Slider**: A versatile control that allows users to adjust values within a defined range by sliding. Commonly used for settings like light intensity, volume control, or any parameter requiring fine-tuned adjustments.
107
- - **Multi State Switch**: Lists all available device/path operating modes/states (e.g., On, Off, Charge Only, Invert Only), highlights the current state, and lets you select a new state to send to the device and see the result.
99
+ - **Numeric**: Create gauges to display any numerical data sent by your system: SOG, depth, wind speed, VMG, refrigerator temperature, weather data, etc.
100
+ - **Text**: Create gauges to display any textual data sent by your system: MPPT state, vessel details, next waypoint, Fusion radio song information, noon and sun phases, any system component configuration detail or status available, etc.
101
+ - **Date & Time**: A timezone-aware control with flexible presentation formatting support.
102
+ - **Position**: Position coordinates in textual format.
108
103
  - **Simple Linear gauge**: A visual display for electrical numerical data: chargers, MPPT, shunt, etc.
109
104
  - **Linear gauge**: Visually display any numerical data on a vertical or horizontal scale: tank and reservoir levels, battery remaining capacity, etc.
110
105
  - **Radial gauge**: Visually display any numerical data on a radial scale: boat speed, wind speed, engine RPM, etc.
111
106
  - **Compass gauge**: A card or marine compass to display directional data such as heading, bearing to next waypoint, wind angle, etc.
112
107
  - **Radial and linear Steel gauge**: Old-school look & feel gauges.
113
108
  - **Level gauge**: Dual-scale heel angle indicator combining a high‑precision ±5° fine level with a wide ±40° coarse arc for fast trim tuning and broader heel / sea‑state monitoring.
114
- - **Pitch & Roll**: Horizon-style attitude indicator showing live pitch and roll for monitoring trim, heel, and sea-state response.
115
- - **Wind Steering Display**: Your typical sailboat wind gauge.
116
- - **Freeboard-SK Chart Plotter**: A high-quality Signal K implementation of the Freeboard integration widget.
109
+ - **Pitch & Roll gauge**: Horizon-style attitude indicator showing live pitch and roll for monitoring trim, heel, and sea-state response.
110
+ - **Battery Monitor**: Stay on top of batteries or whole banks state with a compact view displaying State of Charge, remaining capacity, remaining time, voltage, current, power flow, and temperature.
111
+ - **Solar Charger**: Track solar generation and charging performance at a glance with live panel output, battery-side metrics, and clear charger and relay status indicators.
112
+ - **Windsteer**: Combines wind, wind sectors, heading, COG, and waypoint info for sailboat wind steering.
113
+ - **Racesteer**: Sailboat race steering display fusing polar performance data with live conditions for optimal tactics.
114
+ - **Countdown Timer**: A simple start sequences timer.
115
+ - **Racer Start Timer**: Advanced race countdown timer with OCS (On Course Side) detection and automatic dashboard switching.
116
+ - **Start Line Insight**: Analyze and visualize the start line for tactical racing advantage, including favored end and distance-to-line.
117
+ - **Boolean Control Panel**: A digital switchboard to configure and operate remote devices: lights, bilge pumps, solenoids, or any Signal K compatible device that supports On/Off operations.
118
+ - **Slider**: A versatile control that allows users to adjust values within a defined range by sliding. Commonly used for settings like light intensity, volume control, or any parameter requiring fine-tuned adjustments.
119
+ - **Multi State Switch**: Lists all available device modes/states (e.g., On, Off, Charge Only, Invert Only), highlights the current state, and lets you select a new state to send to the device and see the result.
120
+ - **Zones State Panel**: Monitor the health/state of multiple sensors and devices at a glance. Configure multiple paths per panel; each control uses KIP’s zone severity colors and status messages so warnings and alarms stand out immediately.
121
+ - **Freeboard-SK Chart Plotter**: High-quality Signal K chartplotter integration widget.
117
122
  - **Autopilot Head**: Operate your autopilot from any device remotely.
118
- - **Data Chart**: Visualize data trends over time.
119
123
  - **AIS Radar**: Display AIS targets with range rings, interactive target details, and quick zoom and filtering controls.
120
- - **Race Timer**: Track regatta start sequences.
121
- - **Start Line Insight**: Analyze and visualize the start line for tactical racing advantage, including favored end and distance-to-line.
122
- - **Racer Start Timer**: Advanced race countdown timer with OCS (On Course Side) detection and automatic dashboard switching.
124
+ - **Data Chart**: Visualize data trends over time.
123
125
  - **Embedded Webpage**: A powerful way to display web-based apps published on your Signal K server, such as Grafana and Node-RED dashboards, or your own standalone web app.
126
+ - **Label**: A static text widget.
124
127
 
125
128
  Get the latest version of KIP to see what's new!
126
129
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mxtommy/kip",
3
- "version": "4.7.0-beta.9",
3
+ "version": "4.8.0-beta.1",
4
4
  "description": "An advanced and versatile marine instrumentation package to display Signal K data.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -51,84 +51,73 @@
51
51
  ],
52
52
  "signalk-plugin-enabled-by-default": true,
53
53
  "scripts": {
54
- "test": "CI=1 ng test --watch=false",
54
+ "test": "ng test --watch=false",
55
55
  "test:plugin": "npm run build:plugin && node --test --test-concurrency=1 kip-plugin/tests/index.test.cjs",
56
56
  "test:interactive": "ng test",
57
- "test:headless": "CI=1 ng test --browsers=ChromeHeadless --watch=false",
57
+ "test:headless": "CI=1 ng test --watch=false",
58
58
  "lint": "ng lint",
59
59
  "dev": "ng serve --configuration=dev --serve-path=/@mxtommy/kip/",
60
60
  "build:plugin": "tsc -p ./kip-plugin/tsconfig.plugin.json",
61
61
  "build:dev": "ng build --configuration=dev",
62
62
  "build:prod": "ng build --configuration=production",
63
63
  "build:all": "npm run build:plugin && npm run build:prod",
64
- "e2e": "ng e2e",
65
64
  "generate:widget": "npx schematics ./tools/schematics/collection.json:create-host2-widget --dry-run=false"
66
65
  },
67
66
  "schematics": "tools/schematics/collection.json",
68
67
  "devDependencies": {
69
- "@angular/cdk": "21.2.1",
70
- "@angular/common": "21.2.1",
71
- "@angular/compiler": "21.2.1",
72
- "@angular/core": "21.2.1",
73
- "@angular/forms": "21.2.1",
74
- "@angular/material": "21.2.1",
75
- "@angular/animations": "21.2.1",
76
- "@angular/platform-browser": "21.2.1",
77
- "@angular/platform-browser-dynamic": "21.2.1",
78
- "@angular/router": "21.2.1",
79
- "@angular-devkit/build-angular": "^21.2.1",
68
+ "@angular-devkit/build-angular": "^21.2.3",
80
69
  "@angular-devkit/schematics-cli": "^20.1.6",
81
- "@angular/build": "^21.2.1",
82
- "@angular/cli": "^21.2.1",
83
- "@angular/compiler-cli": "21.2.1",
84
- "@angular/language-service": "21.2.1",
70
+ "@angular/animations": "21.2.5",
71
+ "@angular/build": "^21.2.3",
72
+ "@angular/cdk": "21.2.3",
73
+ "@angular/cli": "^21.2.3",
74
+ "@angular/common": "21.2.5",
75
+ "@angular/compiler": "21.2.5",
76
+ "@angular/compiler-cli": "21.2.5",
77
+ "@angular/core": "21.2.5",
78
+ "@angular/forms": "21.2.5",
79
+ "@angular/language-service": "21.2.5",
80
+ "@angular/material": "21.2.3",
81
+ "@angular/platform-browser": "21.2.5",
82
+ "@angular/platform-browser-dynamic": "21.2.5",
83
+ "@angular/router": "21.2.5",
84
+ "@aziham/chartjs-plugin-streaming": "^3.5.1",
85
+ "@godind/ng-canvas-gauges": "^6.2.1",
85
86
  "@types/canvas-gauges": "^2.1.8",
86
87
  "@types/d3": "^7.4.3",
87
- "@types/jasmine": "~3.6.0",
88
- "@types/jasminewd2": "^2.0.9",
89
88
  "@types/js-quantities": "^1.6.6",
90
89
  "@types/lodash-es": "^4.17.9",
91
90
  "@types/node": "^24.1.0",
92
- "angular-eslint": "21.3.0",
93
- "codelyzer": "^6.0.0",
94
- "eslint": "^9.29.0",
95
- "jasmine-core": "~4.0.1",
96
- "jasmine-spec-reporter": "~5.0.0",
97
- "karma": "^6.4.4",
98
- "karma-chrome-launcher": "~3.1.0",
99
- "karma-cli": "~2.0.0",
100
- "karma-coverage": "^2.2.0",
101
- "karma-jasmine": "~4.0.0",
102
- "karma-jasmine-html-reporter": "^1.6.0",
103
- "karma-spec-reporter": "^0.0.36",
104
- "ng-packagr": "^21.0.1",
105
- "protractor": "~7.0.0",
106
- "pwa-asset-generator": "^8.1.1",
107
- "sass": "^1.49.9",
108
- "ts-node": "^10.9.2",
109
- "typescript": "^5.9.3",
110
- "@aziham/chartjs-plugin-streaming": "^3.5.1",
111
- "@godind/ng-canvas-gauges": "^6.2.1",
91
+ "@vitest/coverage-v8": "^4.1.0",
112
92
  "@zakj/no-sleep": "^0.13.5",
93
+ "angular-eslint": "21.3.1",
113
94
  "chart.js": "^4.5.1",
114
95
  "chartjs-adapter-date-fns": "^3.0.0",
115
96
  "chartjs-plugin-annotation": "^3.0.1",
116
97
  "clipboard": "^2.0.11",
98
+ "codelyzer": "^6.0.0",
117
99
  "compare-versions": "^6.1.1",
118
100
  "core-js": "^3.13.1",
119
101
  "d3": "^7.9.0",
120
102
  "date-fns": "^2.30.0",
103
+ "eslint": "^9.29.0",
121
104
  "gridstack": "^12.3.3",
122
105
  "js-quantities": "^1.8.0",
106
+ "jsdom": "^26.1.0",
123
107
  "lodash-es": "^4.17.23",
108
+ "ng-packagr": "^21.0.1",
124
109
  "ngx-markdown": "^21.0.1",
125
110
  "prismjs": "^1.30.0",
111
+ "pwa-asset-generator": "^8.1.1",
126
112
  "rxjs": "^7.8.2",
113
+ "sass": "^1.49.9",
127
114
  "screenfull": "^6.0.2",
128
115
  "sk-ais-status-plugin": "^1.0.0",
129
116
  "steelseries": "^2.0.9",
117
+ "ts-node": "^10.9.2",
130
118
  "tslib": "^2.6.2",
131
- "zone.js": "^0.15.1"
119
+ "typescript": "^5.9.3",
120
+ "vitest": "^4.1.0"
132
121
  },
133
122
  "dependencies": {
134
123
  "@signalk/server-api": "^2.22.0"
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HistorySeriesService = exports.isKipTemplateSeriesDefinition = exports.isKipSeriesEnabled = exports.isKipConcreteSeriesDefinition = void 0;
3
+ exports.HistorySeriesService = exports.isKipTemplateSeriesDefinition = exports.isKipSolarTemplateSeriesDefinition = exports.isKipSeriesEnabled = exports.isKipConcreteSeriesDefinition = exports.isKipBmsTemplateSeriesDefinition = void 0;
4
4
  const kip_series_contract_1 = require("./kip-series-contract");
5
+ Object.defineProperty(exports, "isKipBmsTemplateSeriesDefinition", { enumerable: true, get: function () { return kip_series_contract_1.isKipBmsTemplateSeriesDefinition; } });
5
6
  Object.defineProperty(exports, "isKipConcreteSeriesDefinition", { enumerable: true, get: function () { return kip_series_contract_1.isKipConcreteSeriesDefinition; } });
6
7
  Object.defineProperty(exports, "isKipSeriesEnabled", { enumerable: true, get: function () { return kip_series_contract_1.isKipSeriesEnabled; } });
8
+ Object.defineProperty(exports, "isKipSolarTemplateSeriesDefinition", { enumerable: true, get: function () { return kip_series_contract_1.isKipSolarTemplateSeriesDefinition; } });
7
9
  Object.defineProperty(exports, "isKipTemplateSeriesDefinition", { enumerable: true, get: function () { return kip_series_contract_1.isKipTemplateSeriesDefinition; } });
8
10
  /**
9
11
  * Manages history capture series definitions and serves History API-compatible query results.
@@ -327,6 +329,7 @@ class HistorySeriesService {
327
329
  && leftComparable.path === rightComparable.path
328
330
  && leftComparable.expansionMode === rightComparable.expansionMode
329
331
  && this.areStringArraysEquivalent(leftComparable.allowedBatteryIds, rightComparable.allowedBatteryIds)
332
+ && this.areStringArraysEquivalent(leftComparable.allowedSolarIds, rightComparable.allowedSolarIds)
330
333
  && leftComparable.source === rightComparable.source
331
334
  && leftComparable.context === rightComparable.context
332
335
  && leftComparable.timeScale === rightComparable.timeScale
@@ -342,6 +345,7 @@ class HistorySeriesService {
342
345
  return {
343
346
  ...comparable,
344
347
  allowedBatteryIds: this.normalizeComparableStringArray(comparable.allowedBatteryIds),
348
+ allowedSolarIds: this.normalizeComparableStringArray(comparable.allowedSolarIds),
345
349
  methods: this.normalizeComparableStringArray(comparable.methods)
346
350
  };
347
351
  }
@@ -390,10 +394,16 @@ class HistorySeriesService {
390
394
  if (expansionMode === 'bms-battery-tree' && ownerWidgetSelector !== 'widget-bms') {
391
395
  throw new Error('BMS template series must use ownerWidgetSelector "widget-bms"');
392
396
  }
397
+ if (expansionMode === 'solar-tree' && ownerWidgetSelector !== 'widget-solar-charger') {
398
+ throw new Error('Solar template series must use ownerWidgetSelector "widget-solar-charger"');
399
+ }
393
400
  const normalizedMethods = this.normalizeComparableStringArray(input.methods);
394
401
  const normalizedAllowedBatteryIds = expansionMode === 'bms-battery-tree'
395
402
  ? this.normalizeComparableStringArray(input.allowedBatteryIds)
396
403
  : undefined;
404
+ const normalizedAllowedSolarIds = expansionMode === 'solar-tree'
405
+ ? this.normalizeComparableStringArray(input.allowedSolarIds)
406
+ : undefined;
397
407
  const isDataWidget = this.isChartWidget(ownerWidgetSelector, ownerWidgetUuid);
398
408
  const retentionMs = this.resolveRetentionMs(input);
399
409
  let sampleTime;
@@ -428,14 +438,26 @@ class HistorySeriesService {
428
438
  ...normalizedBase,
429
439
  ownerWidgetSelector: 'widget-bms',
430
440
  expansionMode,
431
- allowedBatteryIds: normalizedAllowedBatteryIds ?? null
441
+ allowedBatteryIds: normalizedAllowedBatteryIds ?? null,
442
+ allowedSolarIds: null
443
+ };
444
+ return templateSeries;
445
+ }
446
+ if (expansionMode === 'solar-tree') {
447
+ const templateSeries = {
448
+ ...normalizedBase,
449
+ ownerWidgetSelector: 'widget-solar-charger',
450
+ expansionMode,
451
+ allowedBatteryIds: null,
452
+ allowedSolarIds: normalizedAllowedSolarIds ?? null
432
453
  };
433
454
  return templateSeries;
434
455
  }
435
456
  const concreteSeries = {
436
457
  ...normalizedBase,
437
458
  expansionMode: null,
438
- allowedBatteryIds: null
459
+ allowedBatteryIds: null,
460
+ allowedSolarIds: null
439
461
  };
440
462
  return concreteSeries;
441
463
  }
package/plugin/index.js CHANGED
@@ -88,7 +88,8 @@ const start = (server) => {
88
88
  }
89
89
  };
90
90
  const historySeries = new history_series_service_1.HistorySeriesService(() => Date.now(), typeof server.selfId === 'string' && server.selfId.trim().length > 0 ? `vessels.${server.selfId.trim()}` : null);
91
- const storageService = new sqlite_history_storage_service_1.SqliteHistoryStorageService(server.getDataDirPath());
91
+ // Constructed in plugin.start server.getDataDirPath() is only available after SK fully initializes
92
+ let storageService;
92
93
  let retentionSweepTimer = null;
93
94
  let storageFlushTimer = null;
94
95
  let sqliteInitializationPromise = null;
@@ -178,6 +179,30 @@ const start = (server) => {
178
179
  .filter(id => /^[a-z0-9_-]+$/i.test(id))
179
180
  .sort((left, right) => left.localeCompare(right));
180
181
  }
182
+ function resolveSolarIdsFromSelfPath() {
183
+ const solarPath = server.getSelfPath('electrical.solar');
184
+ const readCandidate = (node) => {
185
+ if (!node || typeof node !== 'object' || Array.isArray(node)) {
186
+ return null;
187
+ }
188
+ const root = node;
189
+ if (Object.prototype.hasOwnProperty.call(root, 'value')) {
190
+ const value = root.value;
191
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
192
+ return value;
193
+ }
194
+ return null;
195
+ }
196
+ return root;
197
+ };
198
+ const candidates = readCandidate(solarPath);
199
+ if (!candidates) {
200
+ return [];
201
+ }
202
+ return Object.keys(candidates)
203
+ .filter(id => /^[a-z0-9_-]+$/i.test(id))
204
+ .sort((left, right) => left.localeCompare(right));
205
+ }
181
206
  function getExistingConcreteBmsSeries(templateSeries, existingSeries) {
182
207
  return existingSeries
183
208
  .filter(series => series.ownerWidgetUuid === templateSeries.ownerWidgetUuid)
@@ -185,6 +210,14 @@ const start = (server) => {
185
210
  .filter(series => series.seriesId !== templateSeries.seriesId)
186
211
  .map(series => ({ ...series }));
187
212
  }
213
+ function getExistingConcreteSolarSeries(templateSeries, existingSeries) {
214
+ return existingSeries
215
+ .filter(series => series.ownerWidgetUuid === templateSeries.ownerWidgetUuid)
216
+ .filter(history_series_service_1.isKipConcreteSeriesDefinition)
217
+ .filter(series => series.seriesId !== templateSeries.seriesId)
218
+ .filter(series => series.path.startsWith('electrical.solar.'))
219
+ .map(series => ({ ...series }));
220
+ }
188
221
  function mergeSeriesDefinitions(series) {
189
222
  const mergedById = new Map();
190
223
  series.forEach(item => {
@@ -194,46 +227,90 @@ const start = (server) => {
194
227
  }
195
228
  function expandTemplateSeriesDefinitions(payload, existingSeries = []) {
196
229
  const bmsMetrics = ['capacity.stateOfCharge', 'current'];
230
+ const solarMetrics = ['current', 'panelPower'];
197
231
  const expandedById = new Map();
198
232
  const discoveredBatteryIds = resolveBmsBatteryIdsFromSelfPath();
233
+ const discoveredSolarIds = resolveSolarIdsFromSelfPath();
199
234
  payload.forEach(series => {
200
235
  if (!(0, history_series_service_1.isKipTemplateSeriesDefinition)(series)) {
201
236
  expandedById.set(series.seriesId, series);
202
237
  return;
203
238
  }
204
- if (discoveredBatteryIds.length === 0) {
205
- getExistingConcreteBmsSeries(series, existingSeries).forEach(existing => {
206
- expandedById.set(existing.seriesId, existing);
239
+ if ((0, history_series_service_1.isKipBmsTemplateSeriesDefinition)(series)) {
240
+ if (discoveredBatteryIds.length === 0) {
241
+ getExistingConcreteBmsSeries(series, existingSeries).forEach(existing => {
242
+ expandedById.set(existing.seriesId, existing);
243
+ });
244
+ return;
245
+ }
246
+ const allowedBatteryIds = Array.isArray(series.allowedBatteryIds)
247
+ ? series.allowedBatteryIds
248
+ .filter((id) => typeof id === 'string')
249
+ .map(id => id.trim())
250
+ .filter(id => id.length > 0)
251
+ : [];
252
+ const allowedSet = allowedBatteryIds.length > 0 ? new Set(allowedBatteryIds) : null;
253
+ const batteryIds = discoveredBatteryIds.filter(id => !allowedSet || allowedSet.has(id));
254
+ if (batteryIds.length === 0) {
255
+ return;
256
+ }
257
+ const source = series.source ?? 'default';
258
+ const sourceKey = slugify(source || 'default') || 'default';
259
+ batteryIds.forEach(batteryId => {
260
+ bmsMetrics.forEach(metric => {
261
+ const path = `self.electrical.batteries.${batteryId}.${metric}`;
262
+ const seriesId = `${series.ownerWidgetUuid}:bms:${batteryId}:${metric}:${sourceKey}`;
263
+ expandedById.set(seriesId, {
264
+ ...series,
265
+ seriesId,
266
+ datasetUuid: `${series.ownerWidgetUuid}:bms:${batteryId}:${metric}:${sourceKey}`,
267
+ path,
268
+ retentionDurationMs: Number.isFinite(series.retentionDurationMs) ? series.retentionDurationMs : 24 * 60 * 60 * 1000,
269
+ expansionMode: null,
270
+ allowedBatteryIds: null,
271
+ allowedSolarIds: null
272
+ });
273
+ });
207
274
  });
208
275
  return;
209
276
  }
210
- const allowedBatteryIds = Array.isArray(series.allowedBatteryIds)
211
- ? series.allowedBatteryIds
212
- .filter((id) => typeof id === 'string')
213
- .map(id => id.trim())
214
- .filter(id => id.length > 0)
215
- : [];
216
- const allowedSet = allowedBatteryIds.length > 0 ? new Set(allowedBatteryIds) : null;
217
- const batteryIds = discoveredBatteryIds.filter(id => !allowedSet || allowedSet.has(id));
218
- if (batteryIds.length === 0) {
219
- return;
220
- }
221
- const source = series.source ?? 'default';
222
- const sourceKey = slugify(source || 'default') || 'default';
223
- batteryIds.forEach(batteryId => {
224
- bmsMetrics.forEach(metric => {
225
- const path = `self.electrical.batteries.${batteryId}.${metric}`;
226
- const seriesId = `${series.ownerWidgetUuid}:bms:${batteryId}:${metric}:${sourceKey}`;
227
- expandedById.set(seriesId, {
228
- ...series,
229
- seriesId,
230
- datasetUuid: `${series.ownerWidgetUuid}:bms:${batteryId}:${metric}:${sourceKey}`,
231
- path,
232
- retentionDurationMs: Number.isFinite(series.retentionDurationMs) ? series.retentionDurationMs : 24 * 60 * 60 * 1000,
233
- expansionMode: null
277
+ if ((0, history_series_service_1.isKipSolarTemplateSeriesDefinition)(series)) {
278
+ if (discoveredSolarIds.length === 0) {
279
+ getExistingConcreteSolarSeries(series, existingSeries).forEach(existing => {
280
+ expandedById.set(existing.seriesId, existing);
281
+ });
282
+ return;
283
+ }
284
+ const allowedSolarIds = Array.isArray(series.allowedSolarIds)
285
+ ? series.allowedSolarIds
286
+ .filter((id) => typeof id === 'string')
287
+ .map(id => id.trim())
288
+ .filter(id => id.length > 0)
289
+ : [];
290
+ const allowedSet = allowedSolarIds.length > 0 ? new Set(allowedSolarIds) : null;
291
+ const chargerIds = discoveredSolarIds.filter(id => !allowedSet || allowedSet.has(id));
292
+ if (chargerIds.length === 0) {
293
+ return;
294
+ }
295
+ const source = series.source ?? 'default';
296
+ const sourceKey = slugify(source || 'default') || 'default';
297
+ chargerIds.forEach(chargerId => {
298
+ solarMetrics.forEach(metric => {
299
+ const path = `self.electrical.solar.${chargerId}.${metric}`;
300
+ const seriesId = `${series.ownerWidgetUuid}:solar:${chargerId}:${metric}:${sourceKey}`;
301
+ expandedById.set(seriesId, {
302
+ ...series,
303
+ seriesId,
304
+ datasetUuid: `${series.ownerWidgetUuid}:solar:${chargerId}:${metric}:${sourceKey}`,
305
+ path,
306
+ retentionDurationMs: Number.isFinite(series.retentionDurationMs) ? series.retentionDurationMs : 24 * 60 * 60 * 1000,
307
+ expansionMode: null,
308
+ allowedBatteryIds: null,
309
+ allowedSolarIds: null
310
+ });
234
311
  });
235
312
  });
236
- });
313
+ }
237
314
  });
238
315
  return Array.from(expandedById.values());
239
316
  }
@@ -682,6 +759,7 @@ const start = (server) => {
682
759
  description: 'KIP server plugin',
683
760
  start: async (settings) => {
684
761
  server.debug('[KIP][LIFECYCLE] start');
762
+ storageService = new sqlite_history_storage_service_1.SqliteHistoryStorageService(server.getDataDirPath());
685
763
  modeConfig = resolveHistoryModeConfig(settings);
686
764
  // Overwrite runtime-detected properties in modeConfig
687
765
  modeConfig.nodeSqliteAvailable = await detectSqliteRuntime();
@@ -1151,20 +1229,28 @@ const start = (server) => {
1151
1229
  ...series
1152
1230
  }));
1153
1231
  const isBatteryDiscoveryUnavailable = resolveBmsBatteryIdsFromSelfPath().length === 0;
1154
- const preservedBmsSeries = isBatteryDiscoveryUnavailable
1155
- ? scopedPayload
1156
- .filter(history_series_service_1.isKipTemplateSeriesDefinition)
1157
- .flatMap(series => currentSeries.filter(current => current.ownerWidgetUuid === series.ownerWidgetUuid && (0, history_series_service_1.isKipConcreteSeriesDefinition)(current) && current.seriesId !== series.seriesId))
1158
- : [];
1232
+ const isSolarDiscoveryUnavailable = resolveSolarIdsFromSelfPath().length === 0;
1233
+ const preservedTemplateConcreteSeries = scopedPayload
1234
+ .filter(history_series_service_1.isKipTemplateSeriesDefinition)
1235
+ .flatMap(series => {
1236
+ if ((0, history_series_service_1.isKipBmsTemplateSeriesDefinition)(series) && isBatteryDiscoveryUnavailable) {
1237
+ return getExistingConcreteBmsSeries(series, currentSeries);
1238
+ }
1239
+ if ((0, history_series_service_1.isKipSolarTemplateSeriesDefinition)(series) && isSolarDiscoveryUnavailable) {
1240
+ return getExistingConcreteSolarSeries(series, currentSeries);
1241
+ }
1242
+ return [];
1243
+ });
1159
1244
  const expandedPayload = mergeSeriesDefinitions([
1160
1245
  ...expandTemplateSeriesDefinitions(scopedPayload, currentSeries),
1161
- ...preservedBmsSeries
1246
+ ...preservedTemplateConcreteSeries
1162
1247
  ]);
1163
1248
  const result = simulated.reconcileSeries(expandedPayload);
1164
1249
  const nextSeries = simulated.listSeries();
1165
1250
  await storageService.replaceSeriesDefinitions(nextSeries);
1166
- const seriesOutsideScope = historySeries.listSeries();
1167
- historySeries.reconcileSeries([...seriesOutsideScope, ...nextSeries]);
1251
+ // /series/reconcile expects the full desired set for KIP-managed series.
1252
+ // Keep in-memory state aligned with persisted state to avoid reintroducing stale series.
1253
+ historySeries.reconcileSeries(nextSeries);
1168
1254
  server.debug(`[KIP][SERIES_RECONCILE] created=${result.created} updated=${result.updated} deleted=${result.deleted} total=${result.total}`);
1169
1255
  rebuildSeriesCaptureSubscriptions();
1170
1256
  return sendOk(res, result);
@@ -1,11 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isKipTemplateSeriesDefinition = isKipTemplateSeriesDefinition;
4
+ exports.isKipBmsTemplateSeriesDefinition = isKipBmsTemplateSeriesDefinition;
5
+ exports.isKipSolarTemplateSeriesDefinition = isKipSolarTemplateSeriesDefinition;
4
6
  exports.isKipConcreteSeriesDefinition = isKipConcreteSeriesDefinition;
5
7
  exports.isKipSeriesEnabled = isKipSeriesEnabled;
6
8
  function isKipTemplateSeriesDefinition(series) {
9
+ return series.expansionMode === 'bms-battery-tree' || series.expansionMode === 'solar-tree';
10
+ }
11
+ function isKipBmsTemplateSeriesDefinition(series) {
7
12
  return series.expansionMode === 'bms-battery-tree';
8
13
  }
14
+ function isKipSolarTemplateSeriesDefinition(series) {
15
+ return series.expansionMode === 'solar-tree';
16
+ }
9
17
  function isKipConcreteSeriesDefinition(series) {
10
18
  return series.expansionMode == null;
11
19
  }
@@ -224,6 +224,14 @@
224
224
  "type": "string"
225
225
  },
226
226
  "description": "Concrete series do not use battery filters and should leave this null."
227
+ },
228
+ "allowedSolarIds": {
229
+ "type": "array",
230
+ "nullable": true,
231
+ "items": {
232
+ "type": "string"
233
+ },
234
+ "description": "Concrete series do not use solar charger filters and should leave this null."
227
235
  }
228
236
  }
229
237
  }
@@ -240,13 +248,15 @@
240
248
  "ownerWidgetSelector": {
241
249
  "type": "string",
242
250
  "enum": [
243
- "widget-bms"
251
+ "widget-bms",
252
+ "widget-solar-charger"
244
253
  ]
245
254
  },
246
255
  "expansionMode": {
247
256
  "type": "string",
248
257
  "enum": [
249
- "bms-battery-tree"
258
+ "bms-battery-tree",
259
+ "solar-tree"
250
260
  ]
251
261
  },
252
262
  "allowedBatteryIds": {
@@ -255,6 +265,13 @@
255
265
  "items": {
256
266
  "type": "string"
257
267
  }
268
+ },
269
+ "allowedSolarIds": {
270
+ "type": "array",
271
+ "nullable": true,
272
+ "items": {
273
+ "type": "string"
274
+ }
258
275
  }
259
276
  },
260
277
  "required": [
@@ -1061,32 +1061,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1061
1061
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
1062
1062
  THE SOFTWARE.
1063
1063
 
1064
- --------------------------------------------------------------------------------
1065
- Package: zone.js
1066
- License: "MIT"
1067
-
1068
- The MIT License
1069
-
1070
- Copyright (c) 2010-2025 Google LLC. https://angular.dev/license
1071
-
1072
- Permission is hereby granted, free of charge, to any person obtaining a copy
1073
- of this software and associated documentation files (the "Software"), to deal
1074
- in the Software without restriction, including without limitation the rights
1075
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1076
- copies of the Software, and to permit persons to whom the Software is
1077
- furnished to do so, subject to the following conditions:
1078
-
1079
- The above copyright notice and this permission notice shall be included in
1080
- all copies or substantial portions of the Software.
1081
-
1082
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1083
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1084
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1085
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1086
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1087
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
1088
- THE SOFTWARE.
1089
-
1090
1064
  --------------------------------------------------------------------------------
1091
1065
  Package: prismjs
1092
1066
  License: "MIT"
@@ -81,6 +81,8 @@ KIP widgets turn Signal K data into readable visuals and controls. Available wid
81
81
  - **Classic Steel** – Traditional steel-look linear & radial gauges with range sizes and zone highlights.
82
82
  - **Windsteer** – Combines wind, wind sectors, heading, COG, and waypoint info for wind steering.
83
83
  - **Wind Trends** – Real-time True Wind trends with dual axes for direction and speed, live values, and averages.
84
+ - **Battery Monitor** - Display batteries or whole banks state State of Charge, remaining capacity, remaining time, voltage, current, power flow, and temperature.
85
+ - **Solar Charger**- Track solar generation and charging performance at a glance with live panel output, battery-side metrics, and clear charger and relay status indicators.
84
86
  - **Freeboard-SK** – Adds the Freeboard-SK chart plotter as a widget with automatic sign-in.
85
87
  - **Autopilot Head** – Typical autopilot controls for compatible Signal K Autopilot devices.
86
88
  - **Realtime Data Chart** – Visualizes data on a real-time chart with actuals, averages, and min/max.
@@ -348,6 +348,18 @@
348
348
  <svg id="power_renewal" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
349
349
  <path fill="currentColor" d="M4.06445 13C4.55672 16.9461 7.92051 20 12 20c2.3364 -0.0002 4.4372 -1.0031 5.8994 -2.6006L15.5 15h6v6l-2.1855 -2.1855C17.4894 20.7734 14.8887 21.9998 12 22c-5.18532 0 -9.44843 -3.9467 -9.9502 -9zM12.5 10H16l-4.5 9v-5H8l4.5 -9zM12 2c5.1851 0.00028 9.4485 3.94685 9.9502 9h-2.0147C19.4434 7.05399 16.0793 4.00027 12 4 9.66354 4 7.56264 5.00292 6.10059 6.60059L8.5 9h-6V3l2.18457 2.18457C6.50965 3.22563 9.11128 2 12 2" stroke-width="1"></path>
350
350
  </svg>
351
+ <svg id="solar_charger" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24">
352
+ <path fill="currentColor" stroke="currentColor" stroke-width="1" d="M7.666666666666667 2a3.8333333333333335 3.8333333333333335 0 0 0 7.666666666666667 0z"></path>
353
+ <path stroke="currentColor" d="M3.8333333333333335 2.875h0.9583333333333334" stroke-width="1.5"></path>
354
+ <path stroke="currentColor" d="M18.208333333333336 2.875h0.9583333333333334" stroke-width="1.5"></path>
355
+ <path stroke="currentColor" d="M11.5 8.625v0.9583333333333334" stroke-width="1.5"></path>
356
+ <path stroke="currentColor" d="m16.483333333333334 6.9 0.6775416666666667 0.6775416666666667" stroke-width="1.5"></path>
357
+ <path stroke="currentColor" d="m6.516666666666667 6.9 -0.6708333333333333 0.6708333333333333" stroke-width="1.5"></path>
358
+ <path fill="var(--mat-sys-primary)" fill-opacity="0.5" stroke="var(--mat-sys-primary)" stroke-opacity="1" d="M4.1016666666666675 20.125h14.796666666666667a0.9583333333333334 0.9583333333333334 0 0 0 0.9295833333333333 -1.1912083333333334l-1.4375 -5.75a0.9583333333333334 0.9583333333333334 0 0 0 -0.9295833333333333 -0.7254583333333333H5.5391666666666675a0.9583333333333334 0.9583333333333334 0 0 0 -0.9295833333333333 0.7254583333333333l-1.4375 5.75A0.9583333333333334 0.9583333333333334 0 0 0 4.1016666666666675 20.125z" stroke-width="1"></path>
359
+ <path stroke="var(--mat-sys-primary)" stroke-width="1" d="M3.8333333333333335 16.291666666666668h15.333333333333334"></path>
360
+ <path stroke="var(--mat-sys-primary)" stroke-width="1" d="m9.583333333333334 12.458333333333334 -0.9583333333333334 7.666666666666667"></path>
361
+ <path stroke="var(--mat-sys-primary)" stroke-width="1" d="m13.416666666666668 12.458333333333334 0.9583333333333334 7.666666666666667"></path>
362
+ </svg>
351
363
  <svg id="dashboard-beating-starboard" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
352
364
  <g id="g9" transform="matrix(-0.70710678,0.70710678,0.70710678,0.70710678,8.8211956,-6.2237242)">
353
365
  <path d="M 12,5.9999999 C 16,9.777778 17.777778,15.444445 16,23 H 8.0000001 C 6.2222223,15.444445 8.0000001,9.777778 12,5.9999999 Z" fill="var(--mat-sys-tertiary)" id="path1" style="stroke-width:0.229061" />