nymea-app-energy-overlay/common/ui/components/ChargePointControl.qml

875 lines
34 KiB
QML

// SPDX-License-Identifier: GPL-3.0-or-later
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea-app-energy-overlay.
*
* nymea-app-energy-overlay is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* nymea-app-energy-overlay is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License
* along with nymea-app-energy-overlay. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Material
import QtQuick.Layouts
import QtQuick.Window
import Qt5Compat.GraphicalEffects
import Nymea
import "qrc:/ui/components/"
import "qrc:/styles/light"
import "../components"
ColumnLayout {
id: root
property Thing evCharger
property NymeaEnergy nymeaEnergy
property ChargingConfiguration chargingConfiguration
// Objects
readonly property Thing car: engine.thingManager.things.getThing(chargingConfiguration.assignedCarId)
readonly property ChargingState chargingState: nymeaEnergy.chargingStates.count > 0 && evCharger ?
nymeaEnergy.chargingStates.getChargingState(evCharger.id) : null
// States
readonly property State wallBoxPowerState: evCharger ? evCharger.stateByName("power") : null
readonly property State wallBoxChargingState: evCharger ? evCharger.stateByName("charging") : null
readonly property State wallBoxPluggedInState: evCharger ? evCharger.stateByName("pluggedIn") : null
readonly property State wallBoxDesiredPhaseCountState: evCharger ? evCharger.stateByName("desiredPhaseCount") : null
readonly property State wallBoxConnectedState: evCharger ? evCharger.stateByName("connected") : null
readonly property State wallBoxCurrentPowerState: evCharger ? evCharger.stateByName("currentPower") : null
readonly property State wallBoxSessionEnergyState: evCharger ? evCharger.stateByName("sessionEnergy") : null
readonly property State wallBoxMaxAmpereState: evCharger ? evCharger.stateByName("maxChargingCurrent") : null
readonly property bool hasEnergyMetering: wallBoxCurrentPowerState != null
readonly property bool hasSessionEnergy: wallBoxSessionEnergyState != null
readonly property bool wallBoxConnected: wallBoxConnectedState ? wallBoxConnectedState.value : false
readonly property bool wallBoxCharging: wallBoxConnectedState ? wallBoxChargingState.value : false
readonly property bool carConnected: wallBoxPluggedInState ? wallBoxPluggedInState.value : false
readonly property bool phaseSwitchingAvailable: wallBoxDesiredPhaseCountState != null &&
wallBoxDesiredPhaseCountState.possibleValues.length > 1
readonly property bool surplusChargingAvailable: car !== null &&
chargingConfiguration.chargingMode !== NymeaEnergyUtils.ChargingModeNormal &&
rootMeter !== null
readonly property bool spotmarketChargingAvailable: car !== null &&
chargingConfiguration.chargingMode !== NymeaEnergyUtils.ChargingModeNormal &&
nymeaEnergy.spotMarketEnabled &&
engine.jsonRpcClient.experiences["NymeaEnergy"] >= 0.6
property color chargerStateColor: {
// Plugged in but not charging, or preparing for charging
if (!wallBoxChargingState.value && wallBoxPluggedInState.value)
return Style.yellow
if (wallBoxChargingState.value) {
return Style.green
} else {
return Style.gray
}
}
Connections {
id: chargingConfigurationConnections
target: root.chargingConfiguration
onChargingModeChanged: {
// Handle mode tabs
switch (chargingConfiguration.chargingMode) {
case NymeaEnergyUtils.ChargingModeEco:
modeTabBar.currentIndex = 0
useTargetTimeSwitch.checked = false
break;
case NymeaEnergyUtils.ChargingModeEcoWithTargetTime:
modeTabBar.currentIndex = 0
useTargetTimeSwitch.checked = true
break;
case NymeaEnergyUtils.ChargingModeNormal:
modeTabBar.currentIndex = 1
break;
}
}
onAssignedCarIdChanged: {
if (chargingConfiguration.assignedCarId === "{00000000-0000-0000-0000-000000000000}") {
carComboBox.currentIndex = -1
} else {
for (var i = 0; i < carsProxy.count; i++) {
if (carsProxy.get(i).id === chargingConfiguration.assignedCarId) {
carComboBox.currentIndex = i
}
}
}
}
}
QtObject {
id: internal
property int pendingCommand: -1
}
Connections {
id: nymeaEnergyConnections
target: nymeaEnergy
onSetChargingModeReply: (commandId) => {
if (commandId === internal.pendingCommand) {
internal.pendingCommand = -1
}
}
}
Connections {
target: engine.thingManager
onExecuteActionReply: (commandId) => {
if (commandId === internal.pendingCommand) {
internal.pendingCommand = -1
}
}
}
Connections {
target: wallBoxDesiredPhaseCountState
onValueChanged: {
phaseCountSwitch.checked = root.wallBoxDesiredPhaseCountState.value > 1
}
}
// Car selection
RowLayout {
spacing: Style.margins
Layout.fillWidth: true
Rectangle {
Layout.preferredWidth: Style.largeIconSize
Layout.preferredHeight: Style.largeIconSize
Layout.alignment: Qt.AlignVCenter
radius: Style.smallCornerRadius
color: "transparent"
border.width: 1
border.color: Style.foregroundColor
ColorIcon {
anchors.centerIn: parent
width: Style.iconSize
height: width
source: carConnected ? "/images/car-plugged-in.svg" : "/images/car-plugged-out.svg"
}
}
OverlayComboBoxCar {
id: carComboBox
Layout.fillWidth: true
model: carsProxy
textRole: "name"
iconSource: "/images/car.svg"
iconSourceActive: "/images/car-filled.svg"
displayText: currentIndex < 0 ? qsTr("Select a car") : currentText
currentIndex: carsProxy.indexOf(carsProxy.getThing(chargingConfiguration.assignedCarId))
actionRequired: currentIndex < 0
onCurrentIndexChanged: {
if (currentIndex < 0)
return
console.log("Car selection current index changed:", currentIndex)
nymeaEnergy.assignCar(evCharger.id, carsProxy.get(currentIndex).id)
}
}
}
TabBar {
id: modeTabBar
Layout.fillWidth: true
currentIndex: root.chargingConfiguration.chargingMode == NymeaEnergyUtils.ChargingModeNormal ? 1 : 0
background: Rectangle {
implicitHeight: 40
color: "transparent"
}
OverlayTabButton {
text: qsTr("Eco")
activeTab: modeTabBar.currentIndex == TabBar.index
iconSource: activeTab ? "/images/eco-charging-filled.svg" : "/images/eco-charging.svg"
onActiveTabChanged: {
if (activeTab && evCharger && root.chargingConfiguration && internal.pendingCommand < 0) {
internal.pendingCommand = nymeaEnergy.setChargingModeEco(evCharger.id)
}
}
}
OverlayTabButton {
text: qsTr("Quick")
activeTab: modeTabBar.currentIndex == TabBar.index
iconSource: activeTab ? "/images/quick-charging-filled.svg" : "/images/quick-charging.svg"
onActiveTabChanged: {
if (activeTab && evCharger && root.chargingConfiguration && internal.pendingCommand < 0) {
internal.pendingCommand = nymeaEnergy.setChargingModeManual(evCharger.id)
}
}
}
}
StackLayout {
id: modeStackLayout
Layout.fillHeight: true
Layout.fillWidth: true
Layout.minimumWidth: 250
Layout.minimumHeight: Layout.minimumWidth
currentIndex: modeTabBar.currentIndex
ColumnLayout {
id: ecoTab
Layout.fillHeight: true
Layout.fillWidth: true
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: Style.delegateHeight
Label {
Layout.fillWidth: true
text: qsTr("Use target time")
}
OverlaySwitch {
id: useTargetTimeSwitch
Layout.alignment: Qt.AlignVCenter
checked: root.chargingConfiguration.chargingMode === NymeaEnergyUtils.ChargingModeEcoWithTargetTime
onCheckedChanged: {
if (!root.chargingConfiguration)
return
if (internal.pendingCommand < 0) {
if (checked) {
var endDateTime = new Date()
endDateTime.setDate(endDateTime.getDate() + 1)
endDateTime.setHours(7, 0, 0, 0)
internal.pendingCommand = root.nymeaEnergy.setChargingModeEcoWithTargetDateTime(root.evCharger.id, endDateTime,
root.chargingConfiguration.targetPercentage);
} else {
internal.pendingCommand = root.nymeaEnergy.setChargingModeEco(root.evCharger.id);
}
}
}
}
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
//Rectangle { anchors.fill: parent; color: "green"; opacity: 0.2 }
readonly property real itemSize: Math.min(width, height)
EcoModeControl {
id: ecoModeControl
anchors.centerIn: parent
width: parent.itemSize
height: width
enabled: wallBoxConnected
}
Binding {
target: ecoModeControl
property: "nymeaEnergy"
value: root.nymeaEnergy
}
Binding {
target: ecoModeControl
property: "evCharger"
value: root.evCharger
}
Binding {
target: ecoModeControl
property: "car"
value: root.car
}
Binding {
target: ecoModeControl
property: "chargingConfiguration"
value: root.chargingConfiguration
}
Binding {
target: ecoModeControl
property: "chargingState"
value: root.chargingState
}
Binding {
target: ecoModeControl
property: "chargerStateColor"
value: root.chargerStateColor
}
Binding {
target: ecoModeControl
property: "busy"
value: root.wallBoxCharging
}
}
}
ColumnLayout {
id: quickTab
Layout.fillHeight: true
Layout.fillWidth: true
RowLayout {
id: phaseSwitchingLayout
Layout.preferredHeight: Style.delegateHeight
Layout.fillWidth: true
Label {
Layout.fillWidth: true
text: qsTr("Charging phases")
opacity: phaseSwitchingAvailable ? 1 : 0
}
PhaseCountSwitch {
id: phaseCountSwitch
Layout.alignment: Qt.AlignVCenter
visible: phaseSwitchingAvailable
enabled: root.wallBoxConnected
checked: root.wallBoxDesiredPhaseCountState.value > 1
onCheckedChanged: {
if (internal.pendingCommand < 0) {
var desiredPhaseCount = root.wallBoxDesiredPhaseCountState && checked ? 3 : 1
console.log("Setting desired phase count to " + desiredPhaseCount)
internal.pendingCommand = root.evCharger.executeAction("desiredPhaseCount", [
{ paramName: "desiredPhaseCount", value: desiredPhaseCount }
])
}
}
//Component.onCompleted: checked = (root.wallBoxDesiredPhaseCountState && root.wallBoxDesiredPhaseCountState.value === 3)
}
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
readonly property real itemSize: Math.min(width, height)
QuickModeControl {
id: quickModeControl
anchors.centerIn: parent
width: parent.itemSize
height: width
enabled: wallBoxConnected
}
Binding {
target: quickModeControl
property: "nymeaEnergy"
value: root.nymeaEnergy
}
Binding {
target: quickModeControl
property: "evCharger"
value: root.evCharger
}
Binding {
target: quickModeControl
property: "car"
value: root.car
}
Binding {
target: quickModeControl
property: "chargingConfiguration"
value: root.chargingConfiguration
}
Binding {
target: ecoModeControl
property: "chargingState"
value: root.chargingState
}
Binding {
target: quickModeControl
property: "chargerStateColor"
value: root.chargerStateColor
}
Binding {
target: quickModeControl
property: "busy"
value: root.wallBoxCharging
}
}
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.smallDelegateHeight
RowLayout {
id: sessionInfoRow
anchors.fill: parent
visible: root.wallBoxConnected
SessionInfoItem {
Layout.fillWidth: true
//available: wallBoxPluggedInState && wallBoxPluggedInState.value && hasEnergyMetering
title: qsTr("Power")
value: {
if (hasEnergyMetering) {
if (Math.abs(wallBoxCurrentPowerState.value) < 1000) {
return Math.abs(Math.round(wallBoxCurrentPowerState.value * 100)) / 100
} else {
return Math.round(Math.abs(wallBoxCurrentPowerState.value / 10)) / 100
}
}
if (wallBoxMaxAmpereState) {
return wallBoxMaxAmpereState.value
}
return 0
}
unit: {
if (hasEnergyMetering) {
if (wallBoxCurrentPowerState && Math.abs(wallBoxCurrentPowerState.value) < 1000) {
return "W"
} else {
return "kW"
}
}
return "A"
}
}
SessionInfoItem {
Layout.fillWidth: true
visible: hasSessionEnergy
title: qsTr("Energy")
value: wallBoxSessionEnergyState ? Math.abs(Math.round(wallBoxSessionEnergyState.value * 100)) / 100 : "--"
unit: "kWh"
}
}
}
ChargerPanelInfosProxy {
id: panelInfosProxy
chargerConnected: root.wallBoxConnected
carConnected: root.carConnected
chargingState: root.chargingState
chargingConfiguration: root.chargingConfiguration
Component.onCompleted: {
// We add the different message types here since they are easier to translate in QML. The entire
// logic which message will be shown where is in the proxy filter implementation
chargerPanelInfos.addPanelInfo(ChargerPanelInfo.TypeChargerNotConnectedWarning,
ChargerPanelInfo.IconTypeError,
qsTr("The charger is not available"),
qsTr("Please make sure the charger is connected and reachable."));
chargerPanelInfos.addPanelInfo(ChargerPanelInfo.TypeNoCarSelected,
ChargerPanelInfo.IconTypeError,
qsTr("No car assigned"),
qsTr("The eco mode is only available if there is a car assigned to the charger. Assign an existing car to the charger or add a new one."));
chargerPanelInfos.addPanelInfo(ChargerPanelInfo.TypeOverloadProtectionActive,
ChargerPanelInfo.IconTypeWarning,
qsTr("Overload protection is active"),
qsTr("One or more phases have reached the limit and charging has been throttled."));
chargerPanelInfos.addPanelInfo(ChargerPanelInfo.TypeIdleRecommended,
ChargerPanelInfo.IconTypeInfo,
qsTr("Car not plugged in"),
qsTr("The car is currently not plugged into the charger."));
chargerPanelInfos.addPanelInfo(ChargerPanelInfo.TypeTargetPercentageReached,
ChargerPanelInfo.IconTypeLock,
// Max length
qsTr("Charging target reached"),
qsTr("Continue charging has been disabled in the charger settings."));
chargerPanelInfos.addPanelInfo(ChargerPanelInfo.TypePowerLockActive,
ChargerPanelInfo.IconTypeLock,
qsTr("Charging mode locked for %1"),
qsTr("To avoid frequent charging interruptions, the current charging mode is fixed.\n\nBackground: Some vehicles classify the energy source as unreliable in the event of frequent interruptions and block the charging process."));
chargerPanelInfos.addPanelInfo(ChargerPanelInfo.TypeTargetTimeOvershot,
ChargerPanelInfo.IconTypeWarning,
qsTr("Unreachable charging target"),
qsTr("It is not possible to reach the desired target in time. It would take additional %1 to reach the desired target."));
chargerPanelInfos.addPanelInfo(ChargerPanelInfo.TypeBatteryLevelConsiderationActive,
ChargerPanelInfo.IconTypeInfo,
qsTr("Prefer the storage up to %1%"),
qsTr("The home storage system will be charged up to %1%. As soon as this charge level has been reached, excess energy will be used to charge the vehicle."));
chargerPanelInfos.addPanelInfo(ChargerPanelInfo.TypeSurplusCharging,
ChargerPanelInfo.IconTypeSurplus,
qsTr("Charging from surplus"),
qsTr("The car is currently charging using surplus energy."));
chargerPanelInfos.addPanelInfo(ChargerPanelInfo.TypeSpotMarketCharging,
ChargerPanelInfo.IconTypeSpotMarket,
qsTr("Charging from spot market"),
qsTr("The car is currently charging from cheap spot market energy."));
chargerPanelInfos.addPanelInfo(ChargerPanelInfo.TypeTimeRequirementCharging,
ChargerPanelInfo.IconTypeTimeRequirement,
qsTr("Forced charging"),
qsTr("Charging as fast as possible in order to meet the charging target in time."));
chargerPanelInfos.addPanelInfo(ChargerPanelInfo.TypeSurplusRecommended,
ChargerPanelInfo.IconTypeSurplus,
qsTr("Surplus available"),
qsTr("Charging is recommended."));
chargerPanelInfos.addPanelInfo(ChargerPanelInfo.TypeSpotMarketRecommended,
ChargerPanelInfo.IconTypeSpotMarket,
qsTr("Cheap energy price"),
qsTr("Charging is recommended."));
}
}
// < 1.0
ChargerPanelInfosProxy {
id: panelInfosProxyOld
chargerConnected: root.wallBoxConnected
carConnected: root.carConnected
chargingState: root.chargingState
chargingConfiguration: root.chargingConfiguration
Component.onCompleted: {
// We add the different message types here since they are easier to translate in QML. The entire
// logic which message will be shown where is in the proxy filter implementation
chargerPanelInfos.addPanelInfo(ChargerPanelInfo.TypeChargerNotConnectedWarning,
ChargerPanelInfo.IconTypeError,
qsTr("The charger is not available"),
qsTr("Please make sure the charger is connected and reachable."));
chargerPanelInfos.addPanelInfo(ChargerPanelInfo.TypeNoCarSelected,
ChargerPanelInfo.IconTypeError,
qsTr("No car assigned"),
qsTr("The eco mode is only available if there is a car assigned to the charger. Assign an existing car to the charger or add a new one."));
}
}
Item {
id: infoPanel
property ChargerPanelInfosProxy infoProxy: engine.jsonRpcClient.experiences["NymeaEnergy"] >= 1.0 ? panelInfosProxy : panelInfosProxyOld
property int indicatorHeight: 16
property int indicatorSize: 4
property bool panelVisible: infoProxy.count > 0
Layout.fillWidth: true
Layout.minimumHeight: Style.delegateHeight + indicatorHeight
Rectangle {
id: infoPanelBackground
visible: infoPanel.panelVisible
anchors.fill: parent
radius: Style.smallCornerRadius
color: Style.tooltipBackgroundColor
}
Column {
anchors.fill: parent
anchors.leftMargin: Style.margins
anchors.rightMargin: Style.margins
visible: infoPanel.panelVisible
Item {
width: parent.width
height: infoPanel.indicatorHeight
Row {
anchors.verticalCenter: parent.verticalCenter
spacing: infoPanel.indicatorSize
visible: infoPanel.infoProxy.count > 1
Repeater {
model: infoPanel.infoProxy
delegate: Rectangle {
height: infoPanel.indicatorSize
width: infoPanel.indicatorSize
radius: infoPanel.indicatorSize / 2
color: {
switch (model.iconType) {
case ChargerPanelInfo.IconTypeWarning:
return Style.orange
case ChargerPanelInfo.IconTypeError:
return Style.red
default:
return Style.iconColor
}
}
}
}
}
}
ListView {
id: infoPanelListView
width: parent.width
height: Style.smallDelegateHeight
clip: true
interactive: false
model: infoPanel.infoProxy
delegate: RowLayout {
width: infoPanel.width
height: Style.smallDelegateHeight
ColorIcon {
id: infoPanelIcon
source: {
switch (model.iconType) {
case ChargerPanelInfo.IconTypeInfo:
return "qrc:/icons/info.svg"
case ChargerPanelInfo.IconTypeWarning:
return "qrc:/icons/dialog-warning-symbolic.svg"
case ChargerPanelInfo.IconTypeLock:
return "qrc:/icons/lock-closed.svg"
case ChargerPanelInfo.IconTypeError:
return "qrc:/icons/dialog-error-symbolic.svg"
case ChargerPanelInfo.IconTypeSurplus:
return "qrc:/images/pv-available.svg"
default:
return "qrc:/icons/info.svg"
}
}
color: {
switch (model.iconType) {
case ChargerPanelInfo.IconTypeWarning:
return Style.orange
case ChargerPanelInfo.IconTypeError:
return Style.red
default:
return Style.iconColor
}
}
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: Style.iconSize
Layout.preferredHeight: Style.iconSize
}
Label {
id: infoLabel
text: model.text
wrapMode: Text.WordWrap
Layout.maximumWidth: infoPanelListView.width - infoPanelIcon.width - Style.margins
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
}
}
}
MouseArea {
anchors.fill: parent
enabled: infoPanel.panelVisible
onClicked: {
var popup = infoPanelDialogComponent.createObject(
root,
{
panelY: infoPanel.y + infoPanel.height,
infoProxy: infoPanel.infoProxy,
x: infoPanel.x,
width: infoPanel.width,
})
popup.open()
}
}
Component {
id: infoPanelDialogComponent
Dialog {
id: infoPanelDialog
property ChargerPanelInfosProxy infoProxy: null
property real panelY: 0
height: Math.min(dialogListView.contentHeight + Style.smallDelegateHeight, panelY)
y: panelY - height
background: Rectangle {
id: infoPanelBackground
anchors.fill: parent
radius: Style.smallCornerRadius
color: Style.tooltipBackgroundColor
}
ColumnLayout {
anchors.fill: parent
anchors.topMargin: -Style.margins
anchors.bottomMargin: -Style.margins
spacing: 0
ListView {
id: dialogListView
Layout.fillHeight: true
Layout.fillWidth: true
clip: true
interactive: true
spacing: 0
model: infoProxy
delegate: Item {
implicitWidth: itemColumn.width
implicitHeight: itemColumn.implicitHeight
ColumnLayout {
id: itemColumn
width: parent.width
spacing: 0
RowLayout {
id: textRowLayout
Layout.fillWidth: true
Layout.preferredHeight: Style.delegateHeight
ColorIcon {
id: infoIcon
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: Style.iconSize
Layout.preferredHeight: Style.iconSize
source: {
switch (model.iconType) {
case ChargerPanelInfo.IconTypeInfo:
return "qrc:/icons/info.svg"
case ChargerPanelInfo.IconTypeWarning:
return "qrc:/icons/dialog-warning-symbolic.svg"
case ChargerPanelInfo.IconTypeLock:
return "qrc:/icons/lock-closed.svg"
case ChargerPanelInfo.IconTypeError:
return "qrc:/icons/dialog-error-symbolic.svg"
case ChargerPanelInfo.IconTypeSurplus:
return "qrc:/images/pv-available.svg"
default:
return "qrc:/icons/info.svg"
}
}
color: {
switch (model.iconType) {
case ChargerPanelInfo.IconTypeWarning:
return Style.orange
case ChargerPanelInfo.IconTypeError:
return Style.red
default:
return Style.iconColor
}
}
}
Label {
id: infoLabel
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
text: model.text
wrapMode: Text.WordWrap
}
}
Label {
id: descriptionLabel
visible: model.description.length > 0
Layout.preferredWidth: dialogListView.width - Style.iconSize - textRowLayout.spacing - Style.margins
Layout.leftMargin: Style.iconSize + textRowLayout.spacing
Layout.rightMargin: Style.margins
text: model.description
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignLeft
wrapMode: Text.WordWrap
}
}
}
}
}
}
}
}
}