@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.
- package/CHANGELOG.md +11 -0
- package/README.md +19 -16
- package/package.json +31 -42
- package/plugin/history-series.service.js +25 -3
- package/plugin/index.js +123 -37
- package/plugin/kip-series-contract.js +8 -0
- package/plugin/openApi.json +19 -2
- package/public/3rdpartylicenses.txt +0 -26
- package/public/assets/help-docs/dashboards.md +2 -0
- package/public/assets/svg/icons.svg +12 -0
- package/public/assets/svg/symbols.svg +68 -0
- package/public/{chunk-2GOHQZH5.js → chunk-2TP7C66X.js} +2 -2
- package/public/chunk-4KHUCTYN.js +3 -0
- package/public/{chunk-FYDLTNP4.js → chunk-6ISGIPNP.js} +1 -1
- package/public/{chunk-PV5PXDO5.js → chunk-7JGTAL26.js} +7 -7
- package/public/{chunk-POMIQBAL.js → chunk-C23GLDFH.js} +1 -1
- package/public/{chunk-BGGO4PGD.js → chunk-CB4E7PPA.js} +1 -1
- package/public/{chunk-AZC2WKQI.js → chunk-DNM5XUIF.js} +1 -1
- package/public/{chunk-HSKVTFFQ.js → chunk-HF7V6XFA.js} +1 -1
- package/public/{chunk-4YDVZHMH.js → chunk-HXR2BKDL.js} +1 -1
- package/public/{chunk-MXUEYEZU.js → chunk-ILQQCGMJ.js} +1 -1
- package/public/chunk-IXUKD73N.js +5 -0
- package/public/{chunk-CSIELI2Z.js → chunk-JMZYPL54.js} +1 -1
- package/public/{chunk-AQROQY2F.js → chunk-KJCBQE6W.js} +1 -1
- package/public/chunk-LEY6MANN.js +16 -0
- package/public/{chunk-PUPM3HUQ.js → chunk-O5BTKN5D.js} +1 -1
- package/public/chunk-OH5KNIQ7.js +50 -0
- package/public/{chunk-PTLDR7X7.js → chunk-Q32FSCUX.js} +1 -1
- package/public/chunk-RIX4VQJ2.js +1 -0
- package/public/{chunk-IENESD5Q.js → chunk-SHJMXSDM.js} +1 -1
- package/public/{chunk-SUWMN3AE.js → chunk-T4VTC6GW.js} +1 -1
- package/public/{chunk-WJFXI5PQ.js → chunk-VFJD3XKT.js} +1 -1
- package/public/{chunk-BQPPRM7O.js → chunk-VFZSH4TC.js} +1 -1
- package/public/{chunk-YY4ZUJFI.js → chunk-W6MCE3GH.js} +1 -1
- package/public/chunk-WAOG456B.js +2 -0
- package/public/chunk-XBOM6DMF.js +9 -0
- package/public/index.html +1 -1
- package/public/main-BECMST5R.js +1 -0
- package/public/polyfills-BWA36QKG.js +1 -0
- package/public/chunk-CHJNKZ4A.js +0 -50
- package/public/chunk-CLSJS3SX.js +0 -3
- package/public/chunk-CRS5IXO7.js +0 -2
- package/public/chunk-JFTWNT5T.js +0 -1
- package/public/chunk-M37BLWHF.js +0 -5
- package/public/chunk-P7YPDCAJ.js +0 -9
- package/public/chunk-PZ6I6W3H.js +0 -16
- package/public/main-4UU5FOPF.js +0 -1
- 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
|
|
100
|
-
- **Text
|
|
101
|
-
- **
|
|
102
|
-
- **
|
|
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
|
-
- **
|
|
116
|
-
- **
|
|
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
|
-
- **
|
|
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.
|
|
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": "
|
|
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 --
|
|
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/
|
|
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/
|
|
82
|
-
"@angular/
|
|
83
|
-
"@angular/
|
|
84
|
-
"@angular/
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
|
|
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 (
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
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
|
-
...
|
|
1246
|
+
...preservedTemplateConcreteSeries
|
|
1162
1247
|
]);
|
|
1163
1248
|
const result = simulated.reconcileSeries(expandedPayload);
|
|
1164
1249
|
const nextSeries = simulated.listSeries();
|
|
1165
1250
|
await storageService.replaceSeriesDefinitions(nextSeries);
|
|
1166
|
-
|
|
1167
|
-
|
|
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
|
}
|
package/plugin/openApi.json
CHANGED
|
@@ -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" />
|