Compare commits
10 Commits
2d6d3c982e
...
e5f280c0b5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5f280c0b5 | ||
|
|
28e77c2b25 | ||
|
|
b9cf03896e | ||
|
|
d2df602c29 | ||
|
|
4384c4e6a8 | ||
|
|
2c235b75aa | ||
|
|
79d8d661f6 | ||
|
|
506e551f4c | ||
|
|
ecfae4deb9 | ||
|
|
17b7f0b9db |
265
.clang-format
Normal file
265
.clang-format
Normal file
@ -0,0 +1,265 @@
|
||||
---
|
||||
Language: Cpp
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignArrayOfStructures: None
|
||||
AlignConsecutiveAssignments:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
PadOperators: false
|
||||
AlignConsecutiveBitFields:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
PadOperators: false
|
||||
AlignConsecutiveDeclarations:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
PadOperators: false
|
||||
AlignConsecutiveMacros:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
PadOperators: false
|
||||
AlignConsecutiveShortCaseStatements:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCaseColons: false
|
||||
AlignEscapedNewlines: DontAlign
|
||||
AlignOperands: Align
|
||||
AlignTrailingComments:
|
||||
Kind: Always
|
||||
OverEmptyLines: 0
|
||||
AllowAllArgumentsOnNextLine: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: true
|
||||
AllowShortBlocksOnASingleLine: Never
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortEnumsOnASingleLine: true
|
||||
AllowShortFunctionsOnASingleLine: Inline
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
AllowShortLambdasOnASingleLine: All
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterDefinitionReturnType: None
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: Yes
|
||||
AttributeMacros:
|
||||
- __capability
|
||||
BinPackArguments: false
|
||||
BinPackParameters: false
|
||||
BitFieldColonSpacing: Both
|
||||
BraceWrapping:
|
||||
AfterCaseLabel: false
|
||||
AfterClass: true
|
||||
AfterControlStatement: Never
|
||||
AfterEnum: false
|
||||
AfterExternBlock: false
|
||||
AfterFunction: true
|
||||
AfterNamespace: false
|
||||
AfterObjCDeclaration: false
|
||||
AfterStruct: true
|
||||
AfterUnion: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: false
|
||||
BeforeLambdaBody: false
|
||||
BeforeWhile: false
|
||||
IndentBraces: false
|
||||
SplitEmptyFunction: false
|
||||
SplitEmptyRecord: false
|
||||
SplitEmptyNamespace: false
|
||||
BreakAfterAttributes: Never
|
||||
BreakAfterJavaFieldAnnotations: false
|
||||
BreakArrays: true
|
||||
BreakBeforeBinaryOperators: All
|
||||
BreakBeforeConceptDeclarations: Always
|
||||
BreakBeforeBraces: Custom
|
||||
BreakBeforeInlineASMColon: OnlyMultiline
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializers: BeforeComma
|
||||
BreakInheritanceList: BeforeColon
|
||||
BreakStringLiterals: true
|
||||
ColumnLimit: 180
|
||||
CommentPragmas: '^ IWYU pragma:'
|
||||
CompactNamespaces: false
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: true
|
||||
DerivePointerAlignment: true
|
||||
DisableFormat: false
|
||||
EmptyLineAfterAccessModifier: Never
|
||||
EmptyLineBeforeAccessModifier: LogicalBlock
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
FixNamespaceComments: true
|
||||
ForEachMacros:
|
||||
- forever
|
||||
- foreach
|
||||
- Q_FOREACH
|
||||
- BOOST_FOREACH
|
||||
IfMacros:
|
||||
- KJ_IF_MAYBE
|
||||
IncludeBlocks: Preserve
|
||||
IncludeCategories:
|
||||
- Regex: '^<Q.*'
|
||||
Priority: 200
|
||||
SortPriority: 200
|
||||
CaseSensitive: true
|
||||
IncludeIsMainRegex: '(Test)?$'
|
||||
IncludeIsMainSourceRegex: ''
|
||||
IndentAccessModifiers: false
|
||||
IndentCaseBlocks: false
|
||||
IndentCaseLabels: false
|
||||
IndentExternBlock: AfterExternBlock
|
||||
IndentGotoLabels: true
|
||||
IndentPPDirectives: None
|
||||
IndentRequiresClause: true
|
||||
IndentWidth: 4
|
||||
IndentWrappedFunctionNames: false
|
||||
InsertBraces: false
|
||||
InsertNewlineAtEOF: false
|
||||
InsertTrailingCommas: None
|
||||
IntegerLiteralSeparator:
|
||||
Binary: 0
|
||||
BinaryMinDigits: 0
|
||||
Decimal: 0
|
||||
DecimalMinDigits: 0
|
||||
Hex: 0
|
||||
HexMinDigits: 0
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||
KeepEmptyLinesAtEOF: false
|
||||
LambdaBodyIndentation: Signature
|
||||
LineEnding: DeriveLF
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: None
|
||||
ObjCBinPackProtocolList: Auto
|
||||
ObjCBlockIndentWidth: 4
|
||||
ObjCBreakBeforeNestedBlockParam: true
|
||||
ObjCSpaceAfterProperty: false
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PackConstructorInitializers: BinPack
|
||||
PenaltyBreakAssignment: 150
|
||||
PenaltyBreakBeforeFirstCallParameter: 300
|
||||
PenaltyBreakComment: 500
|
||||
PenaltyBreakFirstLessLess: 400
|
||||
PenaltyBreakOpenParenthesis: 0
|
||||
PenaltyBreakString: 600
|
||||
PenaltyBreakTemplateDeclaration: 10
|
||||
PenaltyExcessCharacter: 50
|
||||
PenaltyIndentedWhitespace: 0
|
||||
PenaltyReturnTypeOnItsOwnLine: 300
|
||||
PointerAlignment: Right
|
||||
PPIndentWidth: -1
|
||||
QualifierAlignment: Leave
|
||||
ReferenceAlignment: Pointer
|
||||
ReflowComments: false
|
||||
RemoveBracesLLVM: false
|
||||
RemoveParentheses: Leave
|
||||
RemoveSemicolon: false
|
||||
RequiresClausePosition: OwnLine
|
||||
RequiresExpressionIndentation: OuterScope
|
||||
SeparateDefinitionBlocks: Leave
|
||||
ShortNamespaceLines: 1
|
||||
SortIncludes: CaseSensitive
|
||||
SortJavaStaticImport: Before
|
||||
SortUsingDeclarations: Lexicographic
|
||||
SpaceAfterCStyleCast: true
|
||||
SpaceAfterLogicalNot: false
|
||||
SpaceAfterTemplateKeyword: false
|
||||
SpaceAroundPointerQualifiers: Default
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeCaseColon: false
|
||||
SpaceBeforeCpp11BracedList: false
|
||||
SpaceBeforeCtorInitializerColon: true
|
||||
SpaceBeforeInheritanceColon: true
|
||||
SpaceBeforeJsonColon: false
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceBeforeParensOptions:
|
||||
AfterControlStatements: true
|
||||
AfterForeachMacros: true
|
||||
AfterFunctionDefinitionName: false
|
||||
AfterFunctionDeclarationName: false
|
||||
AfterIfMacros: true
|
||||
AfterOverloadedOperator: false
|
||||
AfterRequiresInClause: false
|
||||
AfterRequiresInExpression: false
|
||||
BeforeNonEmptyParentheses: false
|
||||
SpaceBeforeRangeBasedForLoopColon: true
|
||||
SpaceBeforeSquareBrackets: false
|
||||
SpaceInEmptyBlock: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: Never
|
||||
SpacesInContainerLiterals: false
|
||||
SpacesInLineCommentPrefix:
|
||||
Minimum: 1
|
||||
Maximum: -1
|
||||
SpacesInParens: Never
|
||||
SpacesInParensOptions:
|
||||
InCStyleCasts: false
|
||||
InConditionalStatements: false
|
||||
InEmptyParentheses: false
|
||||
Other: false
|
||||
SpacesInSquareBrackets: false
|
||||
Standard: Auto
|
||||
StatementAttributeLikeMacros:
|
||||
- Q_EMIT
|
||||
StatementMacros:
|
||||
- Q_UNUSED
|
||||
- QT_REQUIRE_VERSION
|
||||
- Q_CLASSINFO
|
||||
- Q_ENUM
|
||||
- Q_ENUM_NS
|
||||
- Q_FLAG
|
||||
- Q_FLAG_NS
|
||||
- Q_GADGET
|
||||
- Q_GADGET_EXPORT
|
||||
- Q_INTERFACES
|
||||
- Q_MOC_INCLUDE
|
||||
- Q_NAMESPACE
|
||||
- Q_NAMESPACE_EXPORT
|
||||
- Q_OBJECT
|
||||
- Q_PROPERTY
|
||||
- Q_REVISION
|
||||
- Q_DISABLE_COPY
|
||||
- Q_SET_OBJECT_NAME
|
||||
- QT_BEGIN_NAMESPACE
|
||||
- QT_END_NAMESPACE
|
||||
- QML_ADDED_IN_MINOR_VERSION
|
||||
- QML_ANONYMOUS
|
||||
- QML_ATTACHED
|
||||
- QML_DECLARE_TYPE
|
||||
- QML_DECLARE_TYPEINFO
|
||||
- QML_ELEMENT
|
||||
- QML_EXTENDED
|
||||
- QML_EXTENDED_NAMESPACE
|
||||
- QML_EXTRA_VERSION
|
||||
- QML_FOREIGN
|
||||
- QML_FOREIGN_NAMESPACE
|
||||
- QML_IMPLEMENTS_INTERFACES
|
||||
- QML_INTERFACE
|
||||
- QML_NAMED_ELEMENT
|
||||
- QML_REMOVED_IN_MINOR_VERSION
|
||||
- QML_SINGLETON
|
||||
- QML_UNAVAILABLE
|
||||
- QML_UNCREATABLE
|
||||
- QML_VALUE_TYPE
|
||||
TabWidth: 4
|
||||
UseTab: Never
|
||||
VerilogBreakBetweenInstancePorts: true
|
||||
WhitespaceSensitiveMacros:
|
||||
- BOOST_PP_STRINGIZE
|
||||
- CF_SWIFT_NAME
|
||||
- NS_SWIFT_NAME
|
||||
- PP_STRINGIZE
|
||||
- STRINGIZE
|
||||
...
|
||||
|
||||
285
dashboard/app.js
285
dashboard/app.js
@ -46,6 +46,7 @@ class DashboardApp {
|
||||
this.tokenRefreshTimer = null;
|
||||
this.refreshInFlight = false;
|
||||
this.chargers = new Map();
|
||||
this.expandedChargers = new Set();
|
||||
this.cars = new Map();
|
||||
this.sessions = [];
|
||||
this.activePanel = null;
|
||||
@ -64,15 +65,12 @@ class DashboardApp {
|
||||
{ key: 'name', label: 'Name' },
|
||||
{ key: 'assignedCar', label: 'Car' },
|
||||
{ key: 'energyManagerMode', label: 'Energy manager mode' },
|
||||
{ key: 'connected', label: 'Connected' },
|
||||
{ key: 'connected', label: 'Reachable' },
|
||||
{ key: 'status', label: 'Status' },
|
||||
{ key: 'chargingCurrent', label: 'Charging current' },
|
||||
{ key: 'chargingPhases', label: 'Charging phases' },
|
||||
{ key: 'currentPower', label: 'Current power' },
|
||||
{ key: 'sessionEnergy', label: 'Session energy' },
|
||||
{ key: 'version', label: 'Version' },
|
||||
{ key: 'temperature', label: 'Temperature' },
|
||||
{ key: 'digitalInputMode', label: 'Digital input' }
|
||||
{ key: 'sessionEnergy', label: 'Session energy' }
|
||||
];
|
||||
|
||||
this.translateDocument();
|
||||
@ -135,8 +133,9 @@ class DashboardApp {
|
||||
'chargers.columns.name': 'Name',
|
||||
'chargers.columns.car': 'Car',
|
||||
'chargers.columns.energyManagerMode': 'Energy manager mode',
|
||||
'chargers.columns.connected': 'Connected',
|
||||
'chargers.columns.connected': 'Reachable',
|
||||
'chargers.columns.status': 'Status',
|
||||
'chargers.columns.lastStatusUpdate': 'Last status update',
|
||||
'chargers.columns.chargingCurrent': 'Charging current',
|
||||
'chargers.columns.chargingPhases': 'Charging phases',
|
||||
'chargers.columns.currentPower': 'Current power',
|
||||
@ -144,6 +143,14 @@ class DashboardApp {
|
||||
'chargers.columns.version': 'Version',
|
||||
'chargers.columns.temperature': 'Temperature',
|
||||
'chargers.columns.digitalInputMode': 'Digital input',
|
||||
'chargerStatus.Init': 'Initializing',
|
||||
'chargerStatus.A1': 'Charger ready',
|
||||
'chargerStatus.A2': 'Charger ready',
|
||||
'chargerStatus.B1': 'Car connected, autorization required',
|
||||
'chargerStatus.B2': 'Car connected',
|
||||
'chargerStatus.C1': 'Charging pause, car ready',
|
||||
'chargerStatus.C2': 'Charging',
|
||||
'chargerStatus.F': 'Error',
|
||||
|
||||
'sessions.history': 'History',
|
||||
'sessions.title': 'Charging sessions',
|
||||
@ -151,7 +158,7 @@ class DashboardApp {
|
||||
'sessions.allCars': 'All cars',
|
||||
'sessions.filterStartDate': 'Start date',
|
||||
'sessions.filterEndDate': 'End date',
|
||||
'sessions.fetch': 'Fetch sessions',
|
||||
'sessions.fetch': 'Refresh view',
|
||||
'sessions.downloadCsv': 'Download CSV',
|
||||
'sessions.helper': 'Optionally filter charging sessions by car and time range before downloading.',
|
||||
'sessions.columns.name': 'Name',
|
||||
@ -186,6 +193,7 @@ class DashboardApp {
|
||||
'help.referenceBullet1': 'The dashboard keeps one row per charger ID and updates it with backend notifications.',
|
||||
'help.referenceBullet2': 'Columns follow the order defined by <code>EvDashEngine::packCharger</code> so new properties show up automatically.',
|
||||
'help.referenceBullet3': 'Branding (colours, fonts) is managed via CSS variables at the top of this file for easy overrides.',
|
||||
'help.referenceBullet4': 'Select a charger row to expand additional charger details.',
|
||||
|
||||
'easterEgg.hiddenTreat': 'Hidden treat',
|
||||
'easterEgg.title': 'Grid Dash',
|
||||
@ -274,8 +282,9 @@ class DashboardApp {
|
||||
'chargers.columns.name': 'Name',
|
||||
'chargers.columns.car': 'Fahrzeug',
|
||||
'chargers.columns.energyManagerMode': 'Energiemanager-Modus',
|
||||
'chargers.columns.connected': 'Verbunden',
|
||||
'chargers.columns.connected': 'Erreichbar',
|
||||
'chargers.columns.status': 'Status',
|
||||
'chargers.columns.lastStatusUpdate': 'Letzte Statusaktualisierung',
|
||||
'chargers.columns.chargingCurrent': 'Ladestrom',
|
||||
'chargers.columns.chargingPhases': 'Ladephasen',
|
||||
'chargers.columns.currentPower': 'Aktuelle Leistung',
|
||||
@ -283,6 +292,14 @@ class DashboardApp {
|
||||
'chargers.columns.version': 'Version',
|
||||
'chargers.columns.temperature': 'Temperatur',
|
||||
'chargers.columns.digitalInputMode': 'Digitaler Eingang',
|
||||
'chargerStatus.Init': 'Initialisierung',
|
||||
'chargerStatus.A1': 'Ladestation bereit',
|
||||
'chargerStatus.A2': 'Ladestation bereit',
|
||||
'chargerStatus.B1': 'Fahrzeug verbunden, Autorisierung erforderlich',
|
||||
'chargerStatus.B2': 'Fahrzeug verbunden',
|
||||
'chargerStatus.C1': 'Ladepause, Fahrzeug bereit',
|
||||
'chargerStatus.C2': 'Laden',
|
||||
'chargerStatus.F': 'Fehler',
|
||||
|
||||
'sessions.history': 'Historie',
|
||||
'sessions.title': 'Ladevorgänge',
|
||||
@ -290,7 +307,7 @@ class DashboardApp {
|
||||
'sessions.allCars': 'Alle Fahrzeuge',
|
||||
'sessions.filterStartDate': 'Startdatum',
|
||||
'sessions.filterEndDate': 'Enddatum',
|
||||
'sessions.fetch': 'Ladevorgänge laden',
|
||||
'sessions.fetch': 'Ansicht aktualisieren',
|
||||
'sessions.downloadCsv': 'CSV herunterladen',
|
||||
'sessions.helper': 'Optional nach Fahrzeug und Zeitraum filtern, bevor du die CSV herunterlädst.',
|
||||
'sessions.columns.name': 'Name',
|
||||
@ -325,6 +342,7 @@ class DashboardApp {
|
||||
'help.referenceBullet1': 'Das Dashboard hält eine Zeile pro Ladestations-ID und aktualisiert sie über Backend-Benachrichtigungen.',
|
||||
'help.referenceBullet2': 'Die Spalten folgen der Reihenfolge aus <code>EvDashEngine::packCharger</code>, sodass neue Eigenschaften automatisch erscheinen.',
|
||||
'help.referenceBullet3': 'Branding (Farben, Schrift) wird über CSS-Variablen am Anfang dieser Datei gesteuert.',
|
||||
'help.referenceBullet4': 'Wähle eine Ladestationszeile aus, um zusätzliche Details einzublenden.',
|
||||
|
||||
'easterEgg.hiddenTreat': 'Verstecktes Extra',
|
||||
'easterEgg.title': 'Grid Dash',
|
||||
@ -494,6 +512,27 @@ class DashboardApp {
|
||||
this.stopEasterEggGame();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elements.chargerTableBody) {
|
||||
this.elements.chargerTableBody.addEventListener('click', event => {
|
||||
const targetRow = event.target ? event.target.closest('tr[data-charger-id]') : null;
|
||||
if (!targetRow || !targetRow.dataset || !targetRow.dataset.chargerId)
|
||||
return;
|
||||
this.toggleChargerDetails(targetRow.dataset.chargerId);
|
||||
});
|
||||
|
||||
this.elements.chargerTableBody.addEventListener('keydown', event => {
|
||||
if (!event || (event.key !== 'Enter' && event.key !== ' '))
|
||||
return;
|
||||
|
||||
const targetRow = event.target ? event.target.closest('tr[data-charger-id]') : null;
|
||||
if (!targetRow || !targetRow.dataset || !targetRow.dataset.chargerId)
|
||||
return;
|
||||
|
||||
event.preventDefault();
|
||||
this.toggleChargerDetails(targetRow.dataset.chargerId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
initializePanelNavigation() {
|
||||
@ -1125,8 +1164,12 @@ class DashboardApp {
|
||||
|
||||
let row = this.findChargerRow(key);
|
||||
if (!row || forceCreate) {
|
||||
if (row && row.parentElement)
|
||||
if (row && row.parentElement) {
|
||||
const detailsRow = this.findChargerDetailsRow(key);
|
||||
if (detailsRow && detailsRow.parentElement)
|
||||
detailsRow.parentElement.removeChild(detailsRow);
|
||||
row.parentElement.removeChild(row);
|
||||
}
|
||||
row = this.buildChargerRow(charger);
|
||||
this.elements.chargerTableBody.appendChild(row);
|
||||
} else {
|
||||
@ -1140,12 +1183,17 @@ class DashboardApp {
|
||||
});
|
||||
}
|
||||
|
||||
this.syncChargerDetailsVisibility(key);
|
||||
this.toggleChargerEmptyState();
|
||||
}
|
||||
|
||||
buildChargerRow(charger) {
|
||||
const row = document.createElement('tr');
|
||||
row.classList.add('charger-row');
|
||||
row.dataset.chargerId = this.getChargerKey(charger) || '';
|
||||
row.tabIndex = 0;
|
||||
row.setAttribute('role', 'button');
|
||||
row.setAttribute('aria-expanded', 'false');
|
||||
this.chargerColumns.forEach(column => {
|
||||
if (column.hidden)
|
||||
return;
|
||||
@ -1157,6 +1205,141 @@ class DashboardApp {
|
||||
return row;
|
||||
}
|
||||
|
||||
toggleChargerDetails(chargerId) {
|
||||
if (!chargerId)
|
||||
return;
|
||||
|
||||
const key = this.getChargerKey(chargerId);
|
||||
if (!key)
|
||||
return;
|
||||
|
||||
if (this.expandedChargers.has(key))
|
||||
this.collapseChargerDetails(key);
|
||||
else
|
||||
this.expandChargerDetails(key);
|
||||
}
|
||||
|
||||
expandChargerDetails(chargerId) {
|
||||
if (!chargerId)
|
||||
return;
|
||||
|
||||
const key = this.getChargerKey(chargerId);
|
||||
if (!key)
|
||||
return;
|
||||
|
||||
this.expandedChargers.add(key);
|
||||
this.syncChargerDetailsVisibility(key);
|
||||
}
|
||||
|
||||
collapseChargerDetails(chargerId) {
|
||||
if (!chargerId)
|
||||
return;
|
||||
|
||||
const key = this.getChargerKey(chargerId);
|
||||
if (!key)
|
||||
return;
|
||||
|
||||
this.expandedChargers.delete(key);
|
||||
this.syncChargerDetailsVisibility(key);
|
||||
}
|
||||
|
||||
syncChargerDetailsVisibility(chargerId) {
|
||||
if (!chargerId || !this.elements.chargerTableBody)
|
||||
return;
|
||||
|
||||
const key = this.getChargerKey(chargerId);
|
||||
if (!key)
|
||||
return;
|
||||
|
||||
const isExpanded = this.expandedChargers.has(key);
|
||||
const row = this.findChargerRow(key);
|
||||
if (row) {
|
||||
row.classList.toggle('is-expanded', isExpanded);
|
||||
row.setAttribute('aria-expanded', isExpanded ? 'true' : 'false');
|
||||
}
|
||||
|
||||
const detailsRow = this.findChargerDetailsRow(key);
|
||||
if (!isExpanded) {
|
||||
if (detailsRow && detailsRow.parentElement)
|
||||
detailsRow.parentElement.removeChild(detailsRow);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!row)
|
||||
return;
|
||||
|
||||
const ensured = detailsRow || this.buildChargerDetailsRow(key);
|
||||
if (ensured && ensured !== detailsRow) {
|
||||
this.elements.chargerTableBody.insertBefore(ensured, row.nextSibling);
|
||||
}
|
||||
|
||||
this.updateChargerDetailsRow(key);
|
||||
}
|
||||
|
||||
buildChargerDetailsRow(chargerId) {
|
||||
const row = document.createElement('tr');
|
||||
row.classList.add('charger-details-row');
|
||||
row.dataset.chargerDetailsFor = this.getChargerKey(chargerId) || '';
|
||||
|
||||
const cell = document.createElement('td');
|
||||
cell.colSpan = this.getVisibleChargerColumnCount();
|
||||
const list = document.createElement('dl');
|
||||
list.className = 'charger-details-list';
|
||||
cell.appendChild(list);
|
||||
row.appendChild(cell);
|
||||
return row;
|
||||
}
|
||||
|
||||
updateChargerDetailsRow(chargerId) {
|
||||
if (!chargerId)
|
||||
return;
|
||||
|
||||
const key = this.getChargerKey(chargerId);
|
||||
if (!key || !this.expandedChargers.has(key))
|
||||
return;
|
||||
|
||||
const charger = this.chargers && this.chargers.get(key) ? this.chargers.get(key) : null;
|
||||
const detailsRow = this.findChargerDetailsRow(key);
|
||||
if (!detailsRow)
|
||||
return;
|
||||
|
||||
const list = detailsRow.querySelector('dl.charger-details-list');
|
||||
if (!list)
|
||||
return;
|
||||
|
||||
const items = [
|
||||
{ label: this.t('chargers.columns.lastStatusUpdate'), key: 'lastStatusUpdate' },
|
||||
{ label: this.t('chargers.columns.version'), key: 'version' },
|
||||
{ label: this.t('chargers.columns.temperature'), key: 'temperature' },
|
||||
{ label: this.t('chargers.columns.digitalInputMode'), key: 'digitalInputMode' }
|
||||
];
|
||||
|
||||
list.innerHTML = '';
|
||||
items.forEach(item => {
|
||||
const term = document.createElement('dt');
|
||||
term.textContent = item.label;
|
||||
const description = document.createElement('dd');
|
||||
const value = charger && Object.prototype.hasOwnProperty.call(charger, item.key) ? charger[item.key] : null;
|
||||
description.textContent = this.formatChargerValue(item.key, value);
|
||||
list.appendChild(term);
|
||||
list.appendChild(description);
|
||||
});
|
||||
}
|
||||
|
||||
findChargerDetailsRow(chargerId) {
|
||||
if (!this.elements.chargerTableBody || !chargerId)
|
||||
return null;
|
||||
|
||||
const normalizedId = typeof CSS !== 'undefined' && CSS.escape
|
||||
? CSS.escape(String(chargerId))
|
||||
: String(chargerId).replace(/"/g, '\\"');
|
||||
return this.elements.chargerTableBody.querySelector(`tr[data-charger-details-for="${normalizedId}"]`);
|
||||
}
|
||||
|
||||
getVisibleChargerColumnCount() {
|
||||
return this.chargerColumns.filter(column => !column.hidden).length;
|
||||
}
|
||||
|
||||
renderCellValue(cell, key, value) {
|
||||
if (!cell)
|
||||
return;
|
||||
@ -1184,10 +1367,14 @@ class DashboardApp {
|
||||
if (!key)
|
||||
return;
|
||||
|
||||
this.expandedChargers.delete(key);
|
||||
this.chargers.delete(key);
|
||||
const row = this.findChargerRow(key);
|
||||
const detailsRow = this.findChargerDetailsRow(key);
|
||||
if (row && row.parentElement)
|
||||
row.parentElement.removeChild(row);
|
||||
if (detailsRow && detailsRow.parentElement)
|
||||
detailsRow.parentElement.removeChild(detailsRow);
|
||||
|
||||
this.toggleChargerEmptyState();
|
||||
}
|
||||
@ -1196,7 +1383,8 @@ class DashboardApp {
|
||||
if (!this.elements.chargerTableBody)
|
||||
return;
|
||||
|
||||
const rows = this.elements.chargerTableBody.querySelectorAll('tr[data-charger-id]');
|
||||
this.expandedChargers.clear();
|
||||
const rows = this.elements.chargerTableBody.querySelectorAll('tr[data-charger-id], tr[data-charger-details-for]');
|
||||
rows.forEach(row => {
|
||||
if (row.parentElement)
|
||||
row.parentElement.removeChild(row);
|
||||
@ -1337,10 +1525,54 @@ class DashboardApp {
|
||||
return unit ? `${rounded} ${unit}` : String(rounded);
|
||||
}
|
||||
|
||||
formatNumberMaxDecimals(value, unit, decimals = 2) {
|
||||
if (!Number.isFinite(value))
|
||||
return '—';
|
||||
|
||||
const factor = 10 ** decimals;
|
||||
const rounded = Math.round(value * factor) / factor;
|
||||
let text = rounded.toFixed(decimals).replace(/\.?0+$/, '');
|
||||
if (text === '-0')
|
||||
text = '0';
|
||||
return unit ? `${text} ${unit}` : text;
|
||||
}
|
||||
|
||||
coerceFiniteNumber(value) {
|
||||
if (typeof value === 'number')
|
||||
return Number.isFinite(value) ? value : null;
|
||||
|
||||
if (typeof value !== 'string')
|
||||
return null;
|
||||
|
||||
const normalized = value.trim().replace(',', '.');
|
||||
if (!normalized)
|
||||
return null;
|
||||
|
||||
const parsed = Number.parseFloat(normalized);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
formatChargerValue(key, value) {
|
||||
if (value === null || value === undefined || value === '')
|
||||
return '—';
|
||||
|
||||
if (key === 'status') {
|
||||
const code = String(value).trim();
|
||||
const statusKeys = {
|
||||
Init: 'chargerStatus.Init',
|
||||
A1: 'chargerStatus.A1',
|
||||
A2: 'chargerStatus.A2',
|
||||
B1: 'chargerStatus.B1',
|
||||
B2: 'chargerStatus.B2',
|
||||
C1: 'chargerStatus.C1',
|
||||
C2: 'chargerStatus.C2',
|
||||
F: 'chargerStatus.F'
|
||||
};
|
||||
if (code in statusKeys)
|
||||
return `${code}: ${this.t(statusKeys[code])}`;
|
||||
return code || '—';
|
||||
}
|
||||
|
||||
if (key === 'energyManagerMode') {
|
||||
const modes = {
|
||||
0: this.t('energyManagerMode.quick'),
|
||||
@ -1364,12 +1596,37 @@ class DashboardApp {
|
||||
return Number.isFinite(value) ? this.t('value.unknownWithValue', { value }) : '—';
|
||||
}
|
||||
|
||||
if ((key === 'currentPower' || key === 'sessionEnergy') && typeof value === 'number') {
|
||||
if (key === 'lastStatusUpdate') {
|
||||
const numeric = typeof value === 'string' ? Number.parseFloat(value) : value;
|
||||
if (!Number.isFinite(numeric))
|
||||
return '—';
|
||||
|
||||
const ms = numeric > 1e12 ? numeric : numeric * 1000;
|
||||
const date = new Date(ms);
|
||||
if (Number.isNaN(date.getTime()))
|
||||
return '—';
|
||||
|
||||
const pad = part => String(part).padStart(2, '0');
|
||||
const day = pad(date.getDate());
|
||||
const month = pad(date.getMonth() + 1);
|
||||
const year = date.getFullYear();
|
||||
const hours = pad(date.getHours());
|
||||
const minutes = pad(date.getMinutes());
|
||||
const seconds = pad(date.getSeconds());
|
||||
|
||||
return `${day}.${month}.${year} ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
if (key === 'currentPower' || key === 'sessionEnergy') {
|
||||
const numericValue = this.coerceFiniteNumber(value);
|
||||
if (numericValue === null)
|
||||
return typeof value === 'string' ? value : '—';
|
||||
|
||||
const unit = key === 'currentPower' ? 'kW' : 'kWh';
|
||||
if (key === 'currentPower')
|
||||
return this.formatNumber(value / 1000, unit);
|
||||
return numericValue >= 0 && numericValue < 50 ? `0 ${unit}` : this.formatNumberMaxDecimals(numericValue / 1000, unit, 2);
|
||||
|
||||
return this.formatNumber(value, unit);
|
||||
return this.formatNumber(numericValue, unit);
|
||||
}
|
||||
|
||||
if (typeof value === 'boolean')
|
||||
|
||||
@ -343,6 +343,42 @@
|
||||
background: #f9fbfd;
|
||||
}
|
||||
|
||||
.chargers-table tbody tr[data-charger-id] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chargers-table tbody tr[data-charger-id]:hover,
|
||||
.chargers-table tbody tr[data-charger-id].is-expanded {
|
||||
background: rgba(88, 97, 107, 0.08);
|
||||
}
|
||||
|
||||
.charger-details-row td {
|
||||
border-bottom: 1px solid #e4e9f2;
|
||||
padding: 0.75rem;
|
||||
background: rgba(88, 97, 107, 0.04);
|
||||
}
|
||||
|
||||
.charger-details-list {
|
||||
margin: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(120px, 220px) minmax(0, 1fr);
|
||||
row-gap: 0.25rem;
|
||||
column-gap: 1rem;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.charger-details-list dt {
|
||||
font-weight: 600;
|
||||
color: #1f2a37;
|
||||
}
|
||||
|
||||
.charger-details-list dd {
|
||||
margin: 0;
|
||||
color: #1f2a37;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.data-table .empty-row td {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
@ -707,20 +743,17 @@
|
||||
<th scope="col" data-i18n="chargers.columns.name">Name</th>
|
||||
<th scope="col" data-i18n="chargers.columns.car">Car</th>
|
||||
<th scope="col" data-i18n="chargers.columns.energyManagerMode">Energy manager mode</th>
|
||||
<th scope="col" data-i18n="chargers.columns.connected">Connected</th>
|
||||
<th scope="col" data-i18n="chargers.columns.connected">Reachable</th>
|
||||
<th scope="col" data-i18n="chargers.columns.status">Status</th>
|
||||
<th scope="col" data-i18n="chargers.columns.chargingCurrent">Charging current</th>
|
||||
<th scope="col" data-i18n="chargers.columns.chargingPhases">Charging phases</th>
|
||||
<th scope="col" data-i18n="chargers.columns.currentPower">Current power</th>
|
||||
<th scope="col" data-i18n="chargers.columns.sessionEnergy">Session energy</th>
|
||||
<th scope="col" data-i18n="chargers.columns.version">Version</th>
|
||||
<th scope="col" data-i18n="chargers.columns.temperature">Temperature</th>
|
||||
<th scope="col" data-i18n="chargers.columns.digitalInputMode">Digital input</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="chargerTableBody">
|
||||
<tr id="chargerEmptyRow" class="empty-row">
|
||||
<td colspan="12" data-i18n="chargers.empty">No chargers loaded yet.</td>
|
||||
<td colspan="9" data-i18n="chargers.empty">No chargers loaded yet.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -744,7 +777,7 @@
|
||||
<input type="date" id="sessionStartFilter" name="sessionStartFilter">
|
||||
<label class="sr-only" for="sessionEndFilter" data-i18n="sessions.filterEndDate">End date</label>
|
||||
<input type="date" id="sessionEndFilter" name="sessionEndFilter">
|
||||
<button type="button" id="fetchSessionsButton" class="primary" data-i18n="sessions.fetch">Fetch sessions</button>
|
||||
<button type="button" id="fetchSessionsButton" class="primary" data-i18n="sessions.fetch">Refresh view</button>
|
||||
<button type="button" id="downloadSessionsButton" data-i18n="sessions.downloadCsv">Download CSV</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -825,6 +858,7 @@
|
||||
<li data-i18n="help.referenceBullet1">The dashboard keeps one row per charger ID and updates it with backend notifications.</li>
|
||||
<li data-i18n="help.referenceBullet2" data-i18n-mode="html">Columns follow the order defined by <code>EvDashEngine::packCharger</code> so new properties show up automatically.</li>
|
||||
<li data-i18n="help.referenceBullet3">Branding (colours, fonts) is managed via CSS variables at the top of this file for easy overrides.</li>
|
||||
<li data-i18n="help.referenceBullet4">Select a charger row to expand additional charger details.</li>
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
@ -1,5 +1,17 @@
|
||||
nymea-experience-plugin-evdash (0.1.0) UNRELEASED; urgency=medium
|
||||
nymea-experience-plugin-evdash (1.14.2) noble; urgency=medium
|
||||
|
||||
|
||||
-- jenkins <developer@nymea.io> Thu, 19 Feb 2026 12:14:19 +0100
|
||||
|
||||
nymea-experience-plugin-evdash (1.14.1) noble; urgency=medium
|
||||
|
||||
|
||||
-- jenkins <developer@nymea.io> Thu, 29 Jan 2026 21:30:11 +0100
|
||||
|
||||
nymea-experience-plugin-evdash (1.14.0) noble; urgency=medium
|
||||
|
||||
[ Simon Stürz ]
|
||||
* Initial release.
|
||||
* Initial version of the EV Dash experience
|
||||
|
||||
-- Simon Stürz <simon.stuerz@nymea.io> Mon, 10 Nov 2025 10:37:23 +0100
|
||||
-- jenkins <developer@nymea.io> Thu, 15 Jan 2026 15:30:17 +0100
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
Source: nymea-experience-plugin-evdash
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Maintainer: nymea GmbH <developer@nymea.io>
|
||||
Maintainer: chargebyte austria GmbH <developer@nymea.io>
|
||||
Standards-Version: 4.6.0
|
||||
Homepage: https://nymea.io
|
||||
Build-Depends: debhelper,
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: nymea-experience-plugin-evdash
|
||||
Upstream-Contact: nymea GmbH <contact@nymea.io>
|
||||
Copyright: 2013-2025, nymea GmbH
|
||||
Upstream-Contact: chargebyte austria GmbH <contact@nymea.io>
|
||||
Download: http://www.github.com/nymea/nymea-experience-plugin-evdash
|
||||
Source: https://github.com/nymea/nymea-experience-plugin-evdash.git
|
||||
|
||||
@ -11,5 +10,5 @@ License: GPL-3
|
||||
|
||||
Files: *
|
||||
License: GPL-3
|
||||
Copyright: 2013-2025, nymea GmbH <contact@nymea.io>
|
||||
|
||||
Copyright: 2013-2024, nymea GmbH <contact@nymea.io>
|
||||
2024-2025, chargebyte austria GmbH <contact@nymea.io>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
Source: nymea-experience-plugin-evdash
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Maintainer: nymea GmbH <developer@nymea.io>
|
||||
Maintainer: chargebyte austria GmbH <developer@nymea.io>
|
||||
Standards-Version: 4.7.2
|
||||
Homepage: https://nymea.io
|
||||
Build-Depends: debhelper,
|
||||
|
||||
@ -25,27 +25,24 @@
|
||||
#include "chargingsessionsdbusinterfaceclient.h"
|
||||
|
||||
#include <QDBusArgument>
|
||||
#include <QDBusInterface>
|
||||
#include <QDBusConnectionInterface>
|
||||
#include <QDBusError>
|
||||
#include <QDBusInterface>
|
||||
#include <QDBusPendingCall>
|
||||
#include <QDBusPendingCallWatcher>
|
||||
#include <QDBusPendingReply>
|
||||
#include <QDBusReply>
|
||||
#include <QDBusError>
|
||||
#include <QDBusServiceWatcher>
|
||||
|
||||
static const QString kDbusService = QStringLiteral("io.nymea.energy.chargingsessions");
|
||||
static const QString kDbusPath = QStringLiteral("/io/nymea/energy/chargingsessions");
|
||||
static const QString kDbusInterface = QStringLiteral("io.nymea.energy.chargingsessions");
|
||||
|
||||
ChargingSessionsDBusInterfaceClient::ChargingSessionsDBusInterfaceClient(QObject *parent) :
|
||||
QObject(parent),
|
||||
m_connection(QDBusConnection::systemBus())
|
||||
ChargingSessionsDBusInterfaceClient::ChargingSessionsDBusInterfaceClient(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_connection(QDBusConnection::systemBus())
|
||||
{
|
||||
m_serviceWatcher = new QDBusServiceWatcher(kDbusService,
|
||||
m_connection,
|
||||
QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration,
|
||||
this);
|
||||
m_serviceWatcher = new QDBusServiceWatcher(kDbusService, m_connection, QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration, this);
|
||||
connect(m_serviceWatcher, &QDBusServiceWatcher::serviceRegistered, this, &ChargingSessionsDBusInterfaceClient::onServiceRegistered);
|
||||
connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &ChargingSessionsDBusInterfaceClient::onServiceUnregistered);
|
||||
|
||||
|
||||
@ -24,12 +24,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QDBusConnection>
|
||||
#include <QList>
|
||||
#include <QVariantMap>
|
||||
#include <QStringList>
|
||||
#include <QDBusServiceWatcher>
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
#include <QVariantMap>
|
||||
|
||||
class QDBusInterface;
|
||||
class QDBusPendingCallWatcher;
|
||||
|
||||
@ -24,12 +24,12 @@
|
||||
|
||||
#include "energymanagerdbusclient.h"
|
||||
|
||||
#include <QDBusArgument>
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusConnectionInterface>
|
||||
#include <QDBusInterface>
|
||||
#include <QDBusArgument>
|
||||
#include <QDBusMetaType>
|
||||
#include <QDBusMessage>
|
||||
#include <QDBusMetaType>
|
||||
#include <QDBusPendingCall>
|
||||
#include <QDBusPendingCallWatcher>
|
||||
#include <QDBusReply>
|
||||
@ -39,19 +39,16 @@ static const QString kDbusService = QStringLiteral("io.nymea.energymanager");
|
||||
static const QString kDbusPath = QStringLiteral("/io/nymea/energymanager");
|
||||
static const QString kDbusInterface = QStringLiteral("io.nymea.energymanager");
|
||||
|
||||
EnergyManagerDbusClient::EnergyManagerDbusClient(QObject *parent) :
|
||||
QObject(parent),
|
||||
m_connection(QDBusConnection::systemBus())
|
||||
EnergyManagerDbusClient::EnergyManagerDbusClient(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_connection(QDBusConnection::systemBus())
|
||||
{
|
||||
if (!m_connection.isConnected()) {
|
||||
emit errorOccurred(QStringLiteral("DBus system bus not connected"));
|
||||
return;
|
||||
}
|
||||
|
||||
m_serviceWatcher = new QDBusServiceWatcher(kDbusService,
|
||||
m_connection,
|
||||
QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration,
|
||||
this);
|
||||
m_serviceWatcher = new QDBusServiceWatcher(kDbusService, m_connection, QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration, this);
|
||||
connect(m_serviceWatcher, &QDBusServiceWatcher::serviceRegistered, this, &EnergyManagerDbusClient::onServiceRegistered);
|
||||
connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &EnergyManagerDbusClient::onServiceUnregistered);
|
||||
|
||||
@ -61,9 +58,7 @@ EnergyManagerDbusClient::EnergyManagerDbusClient(QObject *parent) :
|
||||
}
|
||||
}
|
||||
|
||||
EnergyManagerDbusClient::~EnergyManagerDbusClient()
|
||||
{
|
||||
}
|
||||
EnergyManagerDbusClient::~EnergyManagerDbusClient() {}
|
||||
|
||||
QVariantList EnergyManagerDbusClient::chargingInfos() const
|
||||
{
|
||||
|
||||
@ -25,9 +25,9 @@
|
||||
#ifndef ENERGYMANAGERDBUSCLIENT_H
|
||||
#define ENERGYMANAGERDBUSCLIENT_H
|
||||
|
||||
#include <QDBusConnection>
|
||||
#include <QObject>
|
||||
#include <QVariantList>
|
||||
#include <QDBusConnection>
|
||||
|
||||
class QDBusInterface;
|
||||
class QDBusPendingCallWatcher;
|
||||
|
||||
@ -23,34 +23,34 @@
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#include "evdashengine.h"
|
||||
#include "chargingsessionsdbusinterfaceclient.h"
|
||||
#include "energymanagerdbusclient.h"
|
||||
#include "evdashsettings.h"
|
||||
#include "evdashwebserverresource.h"
|
||||
#include "energymanagerdbusclient.h"
|
||||
#include "chargingsessionsdbusinterfaceclient.h"
|
||||
|
||||
#include <integrations/thingmanager.h>
|
||||
#include <logging/logengine.h>
|
||||
|
||||
#include <QWebSocket>
|
||||
#include <QWebSocketServer>
|
||||
#include <QHostAddress>
|
||||
#include <QWebSocket>
|
||||
#include <QWebSocketProtocol>
|
||||
#include <QWebSocketServer>
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QJsonParseError>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonParseError>
|
||||
#include <QUuid>
|
||||
|
||||
#include <QLoggingCategory>
|
||||
Q_DECLARE_LOGGING_CATEGORY(dcEvDashExperience)
|
||||
|
||||
EvDashEngine::EvDashEngine(ThingManager *thingManager, LogEngine *logEngine, EvDashWebServerResource *webServerResource, QObject *parent)
|
||||
: QObject{parent},
|
||||
m_thingManager{thingManager},
|
||||
m_logEngine{logEngine},
|
||||
m_webServerResource{webServerResource}
|
||||
: QObject{parent}
|
||||
, m_thingManager{thingManager}
|
||||
, m_logEngine{logEngine}
|
||||
, m_webServerResource{webServerResource}
|
||||
{
|
||||
Things configuredThings = m_thingManager->configuredThings();
|
||||
foreach (Thing *thing, configuredThings) {
|
||||
@ -72,18 +72,16 @@ EvDashEngine::EvDashEngine(ThingManager *thingManager, LogEngine *logEngine, EvD
|
||||
// Setup websocket server
|
||||
m_webSocketServer = new QWebSocketServer(QStringLiteral("EvDashEngine"), QWebSocketServer::NonSecureMode, this);
|
||||
|
||||
connect(m_webSocketServer, &QWebSocketServer::newConnection, this, [this](){
|
||||
connect(m_webSocketServer, &QWebSocketServer::newConnection, this, [this]() {
|
||||
QWebSocket *socket = m_webSocketServer->nextPendingConnection();
|
||||
if (!socket) {
|
||||
qCWarning(dcEvDashExperience()) << "Interface: Received new connection but socket was null";
|
||||
return;
|
||||
}
|
||||
|
||||
connect(socket, &QWebSocket::textMessageReceived, this, [this, socket](const QString &message) {
|
||||
processTextMessage(socket, message);
|
||||
});
|
||||
connect(socket, &QWebSocket::textMessageReceived, this, [this, socket](const QString &message) { processTextMessage(socket, message); });
|
||||
|
||||
connect(socket, &QWebSocket::disconnected, this, [this, socket](){
|
||||
connect(socket, &QWebSocket::disconnected, this, [this, socket]() {
|
||||
m_clients.removeAll(socket);
|
||||
m_authenticatedClients.remove(socket);
|
||||
qCDebug(dcEvDashExperience()) << "WebSocket client disconnected" << socket->peerAddress().toString() << "Remaining clients:" << m_clients.count();
|
||||
@ -107,23 +105,20 @@ EvDashEngine::EvDashEngine(ThingManager *thingManager, LogEngine *logEngine, EvD
|
||||
|
||||
// ChargingSessions client for fetching charging sessions
|
||||
m_chargingSessionsClient = new ChargingSessionsDBusInterfaceClient(this);
|
||||
connect(m_chargingSessionsClient, &ChargingSessionsDBusInterfaceClient::sessionsReceived,
|
||||
this, &EvDashEngine::onSessionsReceived);
|
||||
|
||||
connect(m_chargingSessionsClient, &ChargingSessionsDBusInterfaceClient::errorOccurred,
|
||||
this, &EvDashEngine::onSessionsError);
|
||||
connect(m_chargingSessionsClient, &ChargingSessionsDBusInterfaceClient::sessionsReceived, this, &EvDashEngine::onSessionsReceived);
|
||||
|
||||
connect(m_chargingSessionsClient, &ChargingSessionsDBusInterfaceClient::errorOccurred, this, &EvDashEngine::onSessionsError);
|
||||
|
||||
// Energy manager client for associated cars and current mode
|
||||
m_energyManagerClient = new EnergyManagerDbusClient(this);
|
||||
connect(m_energyManagerClient, &EnergyManagerDbusClient::chargingInfosUpdated, this, [](const QVariantList &chargingInfos){
|
||||
connect(m_energyManagerClient, &EnergyManagerDbusClient::chargingInfosUpdated, this, [](const QVariantList &chargingInfos) {
|
||||
qCDebug(dcEvDashExperience()) << "ChargingInfos:";
|
||||
foreach (const QVariant &ciVariant, chargingInfos) {
|
||||
qCDebug(dcEvDashExperience()) << "-->" << ciVariant.toMap();
|
||||
}
|
||||
});
|
||||
|
||||
connect(m_energyManagerClient, &EnergyManagerDbusClient::chargingInfoAdded, this, [this](const QVariantMap &chargingInfo){
|
||||
connect(m_energyManagerClient, &EnergyManagerDbusClient::chargingInfoAdded, this, [this](const QVariantMap &chargingInfo) {
|
||||
qCDebug(dcEvDashExperience()) << "ChargingInfo added:" << chargingInfo;
|
||||
Thing *charger = m_thingManager->findConfiguredThing(chargingInfo.value("evChargerId").toUuid());
|
||||
if (charger) {
|
||||
@ -131,7 +126,7 @@ EvDashEngine::EvDashEngine(ThingManager *thingManager, LogEngine *logEngine, EvD
|
||||
}
|
||||
});
|
||||
|
||||
connect(m_energyManagerClient, &EnergyManagerDbusClient::chargingInfoChanged, this, [this](const QVariantMap &chargingInfo){
|
||||
connect(m_energyManagerClient, &EnergyManagerDbusClient::chargingInfoChanged, this, [this](const QVariantMap &chargingInfo) {
|
||||
qCDebug(dcEvDashExperience()) << "ChargingInfo changed:" << chargingInfo;
|
||||
Thing *charger = m_thingManager->findConfiguredThing(chargingInfo.value("evChargerId").toUuid());
|
||||
if (charger) {
|
||||
@ -139,11 +134,11 @@ EvDashEngine::EvDashEngine(ThingManager *thingManager, LogEngine *logEngine, EvD
|
||||
}
|
||||
});
|
||||
|
||||
connect(m_energyManagerClient, &EnergyManagerDbusClient::chargingInfoRemoved, this, [](const QString &evChargerId){
|
||||
connect(m_energyManagerClient, &EnergyManagerDbusClient::chargingInfoRemoved, this, [](const QString &evChargerId) {
|
||||
qCDebug(dcEvDashExperience()) << "ChargingInfo removed:" << evChargerId;
|
||||
});
|
||||
|
||||
connect(m_energyManagerClient, &EnergyManagerDbusClient::errorOccurred, this, [](const QString &errorMessage){
|
||||
connect(m_energyManagerClient, &EnergyManagerDbusClient::errorOccurred, this, [](const QString &errorMessage) {
|
||||
qCWarning(dcEvDashExperience()) << "Energy manager DBus client error occurred:" << errorMessage;
|
||||
});
|
||||
|
||||
@ -196,6 +191,7 @@ void EvDashEngine::onThingAdded(Thing *thing)
|
||||
m_chargers.append(thing);
|
||||
monitorChargerThing(thing);
|
||||
sendNotification("ChargerAdded", packCharger(thing));
|
||||
verifyChargerStatusChanged(thing);
|
||||
}
|
||||
|
||||
if (isCarThing(thing)) {
|
||||
@ -210,8 +206,9 @@ void EvDashEngine::onThingRemoved(const ThingId &thingId)
|
||||
foreach (Thing *thing, m_chargers) {
|
||||
if (thing->id() == thingId) {
|
||||
qCDebug(dcEvDashExperience()) << "Charger has been removed.";
|
||||
m_chargers.removeAll(thing);
|
||||
sendNotification("ChargerRemoved", packCharger(thing));
|
||||
m_chargers.removeAll(thing);
|
||||
m_chargersStatusChangedCache.remove(thing);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -228,8 +225,10 @@ void EvDashEngine::onThingRemoved(const ThingId &thingId)
|
||||
|
||||
void EvDashEngine::onThingChanged(Thing *thing)
|
||||
{
|
||||
if (isChargerThing(thing))
|
||||
if (isChargerThing(thing)) {
|
||||
sendNotification("ChargerChanged", packCharger(thing));
|
||||
verifyChargerStatusChanged(thing);
|
||||
}
|
||||
|
||||
if (isCarThing(thing))
|
||||
sendNotification("CarChanged", packCar(thing));
|
||||
@ -237,27 +236,63 @@ void EvDashEngine::onThingChanged(Thing *thing)
|
||||
|
||||
void EvDashEngine::monitorChargerThing(Thing *thing)
|
||||
{
|
||||
connect(thing, &Thing::stateValueChanged, this, [this, thing](const StateTypeId &stateTypeId, const QVariant &value, const QVariant &minValue, const QVariant &maxValue, const QVariantList &possibleValues){
|
||||
Q_UNUSED(stateTypeId)
|
||||
Q_UNUSED(value)
|
||||
Q_UNUSED(minValue)
|
||||
Q_UNUSED(maxValue)
|
||||
Q_UNUSED(possibleValues)
|
||||
connect(thing,
|
||||
&Thing::stateValueChanged,
|
||||
this,
|
||||
[this, thing](const StateTypeId &stateTypeId, const QVariant &value, const QVariant &minValue, const QVariant &maxValue, const QVariantList &possibleValues) {
|
||||
Q_UNUSED(stateTypeId)
|
||||
Q_UNUSED(value)
|
||||
Q_UNUSED(minValue)
|
||||
Q_UNUSED(maxValue)
|
||||
Q_UNUSED(possibleValues)
|
||||
|
||||
onThingChanged(thing);
|
||||
});
|
||||
onThingChanged(thing);
|
||||
});
|
||||
}
|
||||
|
||||
void EvDashEngine::monitorCarThing(Thing *thing)
|
||||
{
|
||||
connect(thing, &Thing::stateValueChanged, this, [this, thing](const StateTypeId &stateTypeId, const QVariant &value, const QVariant &minValue, const QVariant &maxValue, const QVariantList &possibleValues){
|
||||
Q_UNUSED(stateTypeId)
|
||||
Q_UNUSED(value)
|
||||
Q_UNUSED(minValue)
|
||||
Q_UNUSED(maxValue)
|
||||
Q_UNUSED(possibleValues)
|
||||
connect(thing,
|
||||
&Thing::stateValueChanged,
|
||||
this,
|
||||
[this, thing](const StateTypeId &stateTypeId, const QVariant &value, const QVariant &minValue, const QVariant &maxValue, const QVariantList &possibleValues) {
|
||||
Q_UNUSED(stateTypeId)
|
||||
Q_UNUSED(value)
|
||||
Q_UNUSED(minValue)
|
||||
Q_UNUSED(maxValue)
|
||||
Q_UNUSED(possibleValues)
|
||||
|
||||
onThingChanged(thing);
|
||||
onThingChanged(thing);
|
||||
});
|
||||
}
|
||||
|
||||
void EvDashEngine::verifyChargerStatusChanged(Thing *charger)
|
||||
{
|
||||
QString stateName = "status";
|
||||
QString source = QString("state-%1-%2").arg(charger->id().toString(QUuid::WithBraces), stateName);
|
||||
LogFetchJob *job = m_logEngine->fetchLogEntries({source}, {stateName}, {}, {}, {}, Types::SampleRateAny, Qt::DescendingOrder, 0, 1);
|
||||
connect(job, &LogFetchJob::finished, charger, [this, charger, stateName](const LogEntries &entries) {
|
||||
if (entries.isEmpty()) {
|
||||
qCDebug(dcEvDashExperience()) << "Last state change of" << charger->name() << stateName << "unknown";
|
||||
// Forget any cached values, the database did not return any information...
|
||||
m_chargersStatusChangedCache.remove(charger);
|
||||
return;
|
||||
}
|
||||
|
||||
qint64 lastChangeTimestamp = entries.first().timestamp().toSecsSinceEpoch();
|
||||
qCDebug(dcEvDashExperience()) << "Last state change" << charger->name() << stateName << entries.first().timestamp().toString() << entries.first().values().first();
|
||||
|
||||
if (!m_chargersStatusChangedCache.contains(charger)) {
|
||||
m_chargersStatusChangedCache.insert(charger, lastChangeTimestamp);
|
||||
sendNotification("ChargerChanged", packCharger(charger));
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_chargersStatusChangedCache.value(charger) != lastChangeTimestamp) {
|
||||
m_chargersStatusChangedCache[charger] = lastChangeTimestamp;
|
||||
sendNotification("ChargerChanged", packCharger(charger));
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -368,10 +403,7 @@ QJsonObject EvDashEngine::handleApiRequest(QWebSocket *socket, const QJsonObject
|
||||
|
||||
m_authenticatedClients.insert(socket, token);
|
||||
|
||||
QJsonObject responsePayload {
|
||||
{QStringLiteral("authenticated"), true},
|
||||
{QStringLiteral("timestamp"), QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs)}
|
||||
};
|
||||
QJsonObject responsePayload{{QStringLiteral("authenticated"), true}, {QStringLiteral("timestamp"), QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs)}};
|
||||
return createSuccessResponse(requestId, responsePayload);
|
||||
}
|
||||
|
||||
@ -387,7 +419,6 @@ QJsonObject EvDashEngine::handleApiRequest(QWebSocket *socket, const QJsonObject
|
||||
}
|
||||
|
||||
if (action.compare(QStringLiteral("GetChargers"), Qt::CaseInsensitive) == 0) {
|
||||
|
||||
QJsonObject payload;
|
||||
QJsonArray chargerList;
|
||||
foreach (Thing *thing, m_thingManager->configuredThings()) {
|
||||
@ -451,7 +482,6 @@ void EvDashEngine::sendReply(QWebSocket *socket, QJsonObject response) const
|
||||
void EvDashEngine::sendNotification(const QString ¬ification, QJsonObject payload) const
|
||||
{
|
||||
// Send to all active clients
|
||||
|
||||
for (QWebSocket *client : qAsConst(m_clients)) {
|
||||
if (m_authenticatedClients.value(client).isEmpty())
|
||||
continue;
|
||||
@ -497,7 +527,6 @@ QJsonObject EvDashEngine::packCharger(Thing *charger) const
|
||||
foreach (const QVariant &chargingInfoVariant, m_energyManagerClient->chargingInfos()) {
|
||||
QVariantMap chargingInfo = chargingInfoVariant.toMap();
|
||||
if (chargingInfo.value("evChargerId").toUuid() == charger->id()) {
|
||||
|
||||
// Set assigned car name
|
||||
if (chargingInfo.value("assignedCarId").toString().isEmpty()) {
|
||||
chargerObject.insert("assignedCar", "");
|
||||
@ -521,28 +550,6 @@ QJsonObject EvDashEngine::packCharger(Thing *charger) const
|
||||
chargerObject.insert("pluggedIn", charger->stateValue("pluggedIn").toBool());
|
||||
chargerObject.insert("chargingAllowed", charger->stateValue("power").toBool());
|
||||
|
||||
QString stateName = "currentPower";
|
||||
chargerObject.insert(stateName, charger->stateValue(stateName).toString());
|
||||
QString source = QString("state-%1-%2").arg(charger->id().toString(QUuid::WithBraces), stateName);
|
||||
LogFetchJob *job = m_logEngine->fetchLogEntries(
|
||||
{source},
|
||||
{stateName},
|
||||
{}, {}, {}, // start/end/filter
|
||||
Types::SampleRateAny,
|
||||
Qt::DescendingOrder,
|
||||
0, 1 // offset, limit
|
||||
);
|
||||
|
||||
connect(job, &LogFetchJob::finished, this, [charger, stateName](const LogEntries &entries) {
|
||||
if (entries.isEmpty()) {
|
||||
qCDebug(dcEvDashExperience()) << "##### Last state change of" << charger->name() << stateName << "unknwon";
|
||||
return;
|
||||
}
|
||||
|
||||
//qint64 lastChangeMs = entries.first().timestamp().toMSecsSinceEpoch();
|
||||
qCDebug(dcEvDashExperience()) << "##### Last state change" << charger->name() << stateName << entries.first().timestamp().toString() << entries.first().values().first();
|
||||
});
|
||||
|
||||
if (charger->hasState("currentVersion"))
|
||||
chargerObject.insert("version", charger->stateValue("currentVersion").toDouble());
|
||||
|
||||
@ -559,10 +566,11 @@ QJsonObject EvDashEngine::packCharger(Thing *charger) const
|
||||
if (charger->hasState("error"))
|
||||
chargerObject.insert("error", charger->stateValue("error").toString());
|
||||
|
||||
if (charger->hasState("status")) {
|
||||
if (charger->hasState("status"))
|
||||
chargerObject.insert("status", charger->stateValue("status").toString());
|
||||
|
||||
|
||||
}
|
||||
if (m_chargersStatusChangedCache.contains(charger))
|
||||
chargerObject.insert("lastStatusUpdate", m_chargersStatusChangedCache.value(charger));
|
||||
|
||||
if (charger->hasState("digitalInputMode"))
|
||||
chargerObject.insert("digitalInputMode", charger->stateValue("digitalInputMode").toInt());
|
||||
@ -618,6 +626,7 @@ QStringList EvDashEngine::carThingIdsForCharger(const QString &chargerId) const
|
||||
const QString assignedCarId = chargingInfo.value(QStringLiteral("assignedCarId")).toString();
|
||||
if (!assignedCarId.isEmpty())
|
||||
carThingIds.append(assignedCarId);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@ -640,6 +649,7 @@ void EvDashEngine::onSessionsReceived(const QList<QVariantMap> &sessions)
|
||||
QPointer<QWebSocket> socket = m_pendingChargingSessionsRequests.take(requestId);
|
||||
if (!socket)
|
||||
continue;
|
||||
|
||||
sendReply(socket, createSuccessResponse(requestId, payload));
|
||||
}
|
||||
|
||||
@ -655,6 +665,7 @@ void EvDashEngine::onSessionsError(const QString &errorMessage)
|
||||
QPointer<QWebSocket> socket = m_pendingChargingSessionsRequests.take(requestId);
|
||||
if (!socket)
|
||||
continue;
|
||||
|
||||
sendReply(socket, createErrorResponse(requestId, errorMessage));
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,9 +25,9 @@
|
||||
#ifndef EVDASHENGINE_H
|
||||
#define EVDASHENGINE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QHash>
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QStringList>
|
||||
|
||||
@ -47,13 +47,7 @@ class EvDashEngine : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum EvDashError {
|
||||
EvDashErrorNoError = 0,
|
||||
EvDashErrorBackendError,
|
||||
EvDashErrorDuplicateUser,
|
||||
EvDashErrorUserNotFound,
|
||||
EvDashErrorBadPassword
|
||||
};
|
||||
enum EvDashError { EvDashErrorNoError = 0, EvDashErrorBackendError, EvDashErrorDuplicateUser, EvDashErrorUserNotFound, EvDashErrorBadPassword };
|
||||
Q_ENUM(EvDashError)
|
||||
|
||||
explicit EvDashEngine(ThingManager *thingManager, LogEngine *logEngine, EvDashWebServerResource *webServerResource, QObject *parent = nullptr);
|
||||
@ -86,11 +80,15 @@ private:
|
||||
QList<QWebSocket *> m_clients;
|
||||
QHash<QWebSocket *, QString> m_authenticatedClients;
|
||||
|
||||
QList<Thing *> m_chargers;
|
||||
void monitorChargerThing(Thing *thing);
|
||||
QList<Thing *> m_cars;
|
||||
QList<Thing *> m_chargers;
|
||||
|
||||
void monitorChargerThing(Thing *thing);
|
||||
void monitorCarThing(Thing *thing);
|
||||
|
||||
QHash<Thing *, qint64> m_chargersStatusChangedCache;
|
||||
void verifyChargerStatusChanged(Thing *charger);
|
||||
|
||||
// Pending requests waiting for charging sessions data to return
|
||||
QHash<QString, QPointer<QWebSocket>> m_pendingChargingSessionsRequests;
|
||||
QStringList carThingIdsForCharger(const QString &chargerId) const;
|
||||
@ -114,7 +112,6 @@ private:
|
||||
QJsonObject packCar(Thing *car) const;
|
||||
void onSessionsReceived(const QList<QVariantMap> &sessions);
|
||||
void onSessionsError(const QString &errorMessage);
|
||||
|
||||
};
|
||||
|
||||
#endif // EVDASHENGINE_H
|
||||
|
||||
@ -30,42 +30,45 @@
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(dcEvDashExperience)
|
||||
|
||||
EvDashJsonHandler::EvDashJsonHandler(EvDashEngine *engine, EvDashWebServerResource *resource, QObject *parent):
|
||||
JsonHandler{parent},
|
||||
m_engine{engine},
|
||||
m_resource{resource}
|
||||
EvDashJsonHandler::EvDashJsonHandler(EvDashEngine *engine, EvDashWebServerResource *resource, QObject *parent)
|
||||
: JsonHandler{parent}
|
||||
, m_engine{engine}
|
||||
, m_resource{resource}
|
||||
{
|
||||
registerEnum<EvDashEngine::EvDashError>();
|
||||
|
||||
QString description;
|
||||
QVariantMap params, returns;
|
||||
|
||||
|
||||
params.clear(); returns.clear();
|
||||
params.clear();
|
||||
returns.clear();
|
||||
description = "Get the enabled status of EV Dash service.";
|
||||
returns.insert("enabled", enumValueName(Bool));
|
||||
registerMethod("GetEnabled", description, params, returns);
|
||||
|
||||
params.clear(); returns.clear();
|
||||
params.clear();
|
||||
returns.clear();
|
||||
description = "Enable/Disable the EV Dash service.";
|
||||
params.insert("enabled", enumValueName(Bool));
|
||||
returns.insert("evDashError", enumRef<EvDashEngine::EvDashError>());
|
||||
registerMethod("SetEnabled", description, params, returns);
|
||||
|
||||
|
||||
params.clear(); returns.clear();
|
||||
params.clear();
|
||||
returns.clear();
|
||||
description = "Get the list of available users names.";
|
||||
returns.insert("usernames", enumValueName(StringList));
|
||||
registerMethod("GetUsers", description, params, returns);
|
||||
|
||||
params.clear(); returns.clear();
|
||||
params.clear();
|
||||
returns.clear();
|
||||
description = "Add a new user with the given username and password.";
|
||||
params.insert("username", enumValueName(String));
|
||||
params.insert("password", enumValueName(String));
|
||||
returns.insert("evDashError", enumRef<EvDashEngine::EvDashError>());
|
||||
registerMethod("AddUser", description, params, returns);
|
||||
|
||||
params.clear(); returns.clear();
|
||||
params.clear();
|
||||
returns.clear();
|
||||
description = "Remove the user with the given username.";
|
||||
params.insert("username", enumValueName(String));
|
||||
returns.insert("evDashError", enumRef<EvDashEngine::EvDashError>());
|
||||
@ -77,27 +80,21 @@ EvDashJsonHandler::EvDashJsonHandler(EvDashEngine *engine, EvDashWebServerResour
|
||||
params.insert("enabled", enumValueName(Bool));
|
||||
registerNotification("EnabledChanged", description, params);
|
||||
|
||||
connect(m_engine, &EvDashEngine::enabledChanged, this, [this](bool enabled){
|
||||
emit EnabledChanged({{"enabled", enabled}});
|
||||
});
|
||||
connect(m_engine, &EvDashEngine::enabledChanged, this, [this](bool enabled) { emit EnabledChanged({{"enabled", enabled}}); });
|
||||
|
||||
params.clear();
|
||||
description = "Emitted whenever a new username has been added for the dashboard.";
|
||||
params.insert("username", enumValueName(String));
|
||||
registerNotification("UserAdded", description, params);
|
||||
|
||||
connect(m_resource, &EvDashWebServerResource::userAdded, this, [this](const QString &username){
|
||||
emit UserAdded({{"username", username}});
|
||||
});
|
||||
connect(m_resource, &EvDashWebServerResource::userAdded, this, [this](const QString &username) { emit UserAdded({{"username", username}}); });
|
||||
|
||||
params.clear();
|
||||
description = "Emitted whenever a username has been removed from the dashboard.";
|
||||
params.insert("username", enumValueName(String));
|
||||
registerNotification("UserRemoved", description, params);
|
||||
|
||||
connect(m_resource, &EvDashWebServerResource::userRemoved, this, [this](const QString &username){
|
||||
emit UserRemoved({{"username", username}});
|
||||
});
|
||||
connect(m_resource, &EvDashWebServerResource::userRemoved, this, [this](const QString &username) { emit UserRemoved({{"username", username}}); });
|
||||
}
|
||||
|
||||
QString EvDashJsonHandler::name() const
|
||||
@ -157,6 +154,4 @@ JsonReply *EvDashJsonHandler::RemoveUser(const QVariantMap ¶ms)
|
||||
QVariantMap returns;
|
||||
returns.insert("evDashError", enumValueName(error));
|
||||
return createReply(returns);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -56,7 +56,6 @@ signals:
|
||||
private:
|
||||
EvDashEngine *m_engine = nullptr;
|
||||
EvDashWebServerResource *m_resource = nullptr;
|
||||
|
||||
};
|
||||
|
||||
#endif // ENERGYJSONHANDLER_H
|
||||
|
||||
@ -28,6 +28,4 @@
|
||||
|
||||
EvDashSettings::EvDashSettings()
|
||||
: QSettings(NymeaSettings::settingsPath() + "/evdash.conf", QSettings::IniFormat)
|
||||
{
|
||||
|
||||
}
|
||||
{}
|
||||
|
||||
@ -32,7 +32,6 @@ class EvDashSettings : public QSettings
|
||||
{
|
||||
public:
|
||||
explicit EvDashSettings();
|
||||
|
||||
};
|
||||
|
||||
#endif // EVDASHSETTINGS_H
|
||||
|
||||
@ -25,13 +25,13 @@
|
||||
#include "evdashwebserverresource.h"
|
||||
#include "evdashsettings.h"
|
||||
|
||||
#include <QUuid>
|
||||
#include <QCryptographicHash>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QRegularExpression>
|
||||
#include <QCryptographicHash>
|
||||
#include <QUuid>
|
||||
|
||||
#include <QLoggingCategory>
|
||||
Q_DECLARE_LOGGING_CATEGORY(dcEvDashExperience)
|
||||
@ -191,10 +191,7 @@ HttpReply *EvDashWebServerResource::handleLoginRequest(const HttpRequest &reques
|
||||
const QJsonDocument requestDoc = QJsonDocument::fromJson(request.payload(), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !requestDoc.isObject()) {
|
||||
qCWarning(dcEvDashExperience()) << "Invalid login payload" << parseError.errorString();
|
||||
QJsonObject errorPayload {
|
||||
{QStringLiteral("success"), false},
|
||||
{QStringLiteral("error"), QStringLiteral("invalidRequest")}
|
||||
};
|
||||
QJsonObject errorPayload{{QStringLiteral("success"), false}, {QStringLiteral("error"), QStringLiteral("invalidRequest")}};
|
||||
return HttpReply::createJsonReply(QJsonDocument(errorPayload), HttpReply::BadRequest);
|
||||
}
|
||||
|
||||
@ -203,21 +200,14 @@ HttpReply *EvDashWebServerResource::handleLoginRequest(const HttpRequest &reques
|
||||
const QString password = requestObject.value(QStringLiteral("password")).toString();
|
||||
|
||||
if (!verifyCredentials(username, password)) {
|
||||
QJsonObject response {
|
||||
{QStringLiteral("success"), false},
|
||||
{QStringLiteral("error"), QStringLiteral("unauthorized")}
|
||||
};
|
||||
QJsonObject response{{QStringLiteral("success"), false}, {QStringLiteral("error"), QStringLiteral("unauthorized")}};
|
||||
return HttpReply::createJsonReply(QJsonDocument(response), HttpReply::Unauthorized);
|
||||
}
|
||||
|
||||
const QString token = issueToken(username);
|
||||
const TokenInfo tokenInfo = m_activeTokens.value(token);
|
||||
|
||||
QJsonObject payload {
|
||||
{QStringLiteral("success"), true},
|
||||
{QStringLiteral("token"), token},
|
||||
{QStringLiteral("expiresAt"), tokenInfo.expiresAt.toString(Qt::ISODateWithMs)}
|
||||
};
|
||||
QJsonObject payload{{QStringLiteral("success"), true}, {QStringLiteral("token"), token}, {QStringLiteral("expiresAt"), tokenInfo.expiresAt.toString(Qt::ISODateWithMs)}};
|
||||
|
||||
return HttpReply::createJsonReply(QJsonDocument(payload));
|
||||
}
|
||||
@ -233,10 +223,7 @@ HttpReply *EvDashWebServerResource::handleRefreshRequest(const HttpRequest &requ
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument requestDoc = QJsonDocument::fromJson(request.payload(), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !requestDoc.isObject()) {
|
||||
QJsonObject errorPayload {
|
||||
{QStringLiteral("success"), false},
|
||||
{QStringLiteral("error"), QStringLiteral("invalidRequest")}
|
||||
};
|
||||
QJsonObject errorPayload{{QStringLiteral("success"), false}, {QStringLiteral("error"), QStringLiteral("invalidRequest")}};
|
||||
return HttpReply::createJsonReply(QJsonDocument(errorPayload), HttpReply::BadRequest);
|
||||
}
|
||||
|
||||
@ -245,10 +232,7 @@ HttpReply *EvDashWebServerResource::handleRefreshRequest(const HttpRequest &requ
|
||||
const QJsonObject requestObject = requestDoc.object();
|
||||
const QString token = requestObject.value(QStringLiteral("token")).toString();
|
||||
if (token.isEmpty() || !m_activeTokens.contains(token)) {
|
||||
QJsonObject response {
|
||||
{QStringLiteral("success"), false},
|
||||
{QStringLiteral("error"), QStringLiteral("unauthorized")}
|
||||
};
|
||||
QJsonObject response{{QStringLiteral("success"), false}, {QStringLiteral("error"), QStringLiteral("unauthorized")}};
|
||||
return HttpReply::createJsonReply(QJsonDocument(response), HttpReply::Unauthorized);
|
||||
}
|
||||
|
||||
@ -256,11 +240,7 @@ HttpReply *EvDashWebServerResource::handleRefreshRequest(const HttpRequest &requ
|
||||
info.expiresAt = QDateTime::currentDateTimeUtc().addSecs(s_tokenLifetimeSeconds);
|
||||
m_activeTokens.insert(token, info);
|
||||
|
||||
QJsonObject payload {
|
||||
{QStringLiteral("success"), true},
|
||||
{QStringLiteral("token"), token},
|
||||
{QStringLiteral("expiresAt"), info.expiresAt.toString(Qt::ISODateWithMs)}
|
||||
};
|
||||
QJsonObject payload{{QStringLiteral("success"), true}, {QStringLiteral("token"), token}, {QStringLiteral("expiresAt"), info.expiresAt.toString(Qt::ISODateWithMs)}};
|
||||
|
||||
return HttpReply::createJsonReply(QJsonDocument(payload));
|
||||
}
|
||||
|
||||
@ -25,9 +25,9 @@
|
||||
#ifndef EVDASHWEBSERVERRESOURCE_H
|
||||
#define EVDASHWEBSERVERRESOURCE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QDateTime>
|
||||
#include <QHash>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include <webserver/webserverresource.h>
|
||||
@ -57,12 +57,14 @@ signals:
|
||||
void userRemoved(const QString &username);
|
||||
|
||||
private:
|
||||
struct TokenInfo {
|
||||
struct TokenInfo
|
||||
{
|
||||
QString username;
|
||||
QDateTime expiresAt;
|
||||
};
|
||||
|
||||
struct UserInfo {
|
||||
struct UserInfo
|
||||
{
|
||||
QString username;
|
||||
QByteArray passwordHash;
|
||||
QByteArray passwordSalt;
|
||||
@ -84,8 +86,6 @@ private:
|
||||
bool verifyStaticFile(const QString &fileName);
|
||||
|
||||
bool verifyCredentials(const QString &username, const QString &password) const;
|
||||
|
||||
|
||||
};
|
||||
|
||||
#endif // EVDASHWEBSERVERRESOURCE_H
|
||||
|
||||
@ -31,13 +31,9 @@
|
||||
#include <jsonrpc/jsonrpcserver.h>
|
||||
#include <loggingcategories.h>
|
||||
|
||||
|
||||
NYMEA_LOGGING_CATEGORY(dcEvDashExperience, "EvDashExperience")
|
||||
|
||||
ExperiencePluginEvDash::ExperiencePluginEvDash()
|
||||
{
|
||||
|
||||
}
|
||||
ExperiencePluginEvDash::ExperiencePluginEvDash() {}
|
||||
|
||||
void ExperiencePluginEvDash::init()
|
||||
{
|
||||
|
||||
@ -34,7 +34,7 @@ Q_DECLARE_LOGGING_CATEGORY(dcEvDashExperience)
|
||||
class EvDashEngine;
|
||||
class EvDashWebServerResource;
|
||||
|
||||
class ExperiencePluginEvDash: public ExperiencePlugin
|
||||
class ExperiencePluginEvDash : public ExperiencePlugin
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PLUGIN_METADATA(IID "io.nymea.ExperiencePlugin")
|
||||
@ -50,7 +50,6 @@ public:
|
||||
private:
|
||||
EvDashEngine *m_engine = nullptr;
|
||||
EvDashWebServerResource *m_resource = nullptr;
|
||||
|
||||
};
|
||||
|
||||
#endif // EXPERIENCEPLUGINEVDASH_H
|
||||
|
||||
@ -9,7 +9,7 @@ PKGCONFIG += nymea
|
||||
RESOURCES += ../dashboard.qrc
|
||||
|
||||
QT -= gui
|
||||
QT += network sql websockets dbus
|
||||
QT += network websockets dbus
|
||||
|
||||
HEADERS += experiencepluginevdash.h \
|
||||
energymanagerdbusclient.h \
|
||||
|
||||
Reference in New Issue
Block a user