// 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. * * nymea-app 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 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. If not, see . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Qt5Compat.GraphicalEffects import Nymea import "../components" import "../customviews" ThingPageBase { id: root readonly property bool isEnergyMeter: root.thing && root.thing.thingClass.interfaces.indexOf("energymeter") >= 0 readonly property bool isConsumer: root.thing && root.thing.thingClass.interfaces.indexOf("smartmeterconsumer") >= 0 readonly property bool isProducer: root.thing && root.thing.thingClass.interfaces.indexOf("smartmeterproducer") >= 0 readonly property bool isBattery: root.thing && root.thing.thingClass.interfaces.indexOf("energystorage") >= 0 readonly property bool isEvCharger: root.thing && root.thing.thingClass.interfaces.indexOf("evcharger") >= 0 readonly property State currentPowerState: root.thing.stateByName("currentPower") // meters, producers, consumers readonly property State totalEnergyConsumedState: isEnergyMeter || isConsumer ? root.thing.stateByName("totalEnergyConsumed") : null readonly property StateType totalEnergyConsumedStateType: isEnergyMeter || isConsumer ? root.thing.thingClass.stateTypes.findByName("totalEnergyConsumed") : null readonly property State totalEnergyProducedState: isEnergyMeter || isProducer ? root.thing.stateByName("totalEnergyProduced") : null readonly property StateType totalEnergyProducedStateType: isEnergyMeter || isProducer ? root.thing.thingClass.stateTypes.findByName("totalEnergyProduced") : null // Battery related states readonly property State batteryLevelState: isBattery ? root.thing.stateByName("batteryLevel") : null readonly property State batteryCriticalState: isBattery ? root.thing.stateByName("batteryCritical") : null readonly property State chargingState: isBattery ? root.thing.stateByName("chargingState") : null readonly property State capacityState: isBattery ? root.thing.stateByName("capacity") : null property bool isProduction: currentPowerState.value < 0 property bool isConsumption: currentPowerState.value > 0 property double normalizedValue: isProducer ? Math.max(0, -currentPowerState.value) : Math.abs(currentPowerState.value) property double cleanValue: (normalizedValue / (normalizedValue > 1000 ? 1000 : 1)).toFixed(1) property string unit: normalizedValue > 1000 ? "kW" : "W" readonly property bool isCharging: root.chargingState && root.chargingState.value === "charging" readonly property bool isDischarging: root.chargingState && root.chargingState.value === "discharging" readonly property date now: d.now readonly property date startTime: new Date(now.getTime() - 24 * 60 * 60 * 1000) QtObject { id: d property date now: new Date() } Timer { interval: 60000 repeat: true running: true onTriggered: d.now = new Date() } GridLayout { id: contentGrid anchors.fill: parent columns: app.landscape ? 2 : 1 CircleBackground { id: background Layout.fillWidth: true Layout.fillHeight: true Layout.margins: Style.hugeMargins iconSource: "" onColor: { if (root.isBattery) { if (root.isCharging) { return Style.purple } if (root.isDischarging) { return Style.orange } return Style.green } if (root.isEnergyMeter) { return root.currentPowerState.value < 0 ? Style.yellow : Style.red } return root.currentPowerState.value < 0 ? Style.green : Style.blue } Behavior on onColor { ColorAnimation { duration: Style.fastAnimationDuration } } Rectangle { id: mask anchors.centerIn: parent width: background.contentItem.width height: background.contentItem.height radius: width / 2 visible: false } Item { id: juice anchors.fill: parent Rectangle { anchors.centerIn: parent width: background.contentItem.width height: background.contentItem.height property real progress: root.batteryLevelState ? root.batteryLevelState.value / 100 : 0 anchors.verticalCenterOffset: height * (1 - progress) color: background.onColor visible: root.isBattery } RadialGradient { id: gradient anchors.centerIn: parent width: background.contentItem.width height: background.contentItem.height property real progress: Math.abs(root.currentPowerState.value) / 10000 visible: root.currentPowerState.value !== 0 Behavior on gradientRatio { NumberAnimation { duration: Style.sleepyAnimationDuration; easing.type: Easing.InOutQuad } } property real gradientRatio: (1 - progress) * 0.1 gradient: Gradient{ GradientStop { position: .399 + gradient.gradientRatio; color: "transparent" } GradientStop { position: .5; color: background.onColor } } } ColumnLayout { anchors.centerIn: parent Label { Layout.fillWidth: true horizontalAlignment: Text.AlignHCenter font: Style.largeFont text: "%1 %2".arg(root.cleanValue).arg(root.unit) } Label { Layout.fillWidth: true horizontalAlignment: Text.AlignHCenter text: { if (root.chargingState) { switch (root.chargingState.value) { case "idle": return qsTr("Idle") case "charging": return qsTr("Charging") case "discharging": return qsTr("Discharging") } } if (root.isProducer) { return root.currentPowerState.value < 0 ? qsTr("Producing") : qsTr("Idle") } if (root.isConsumer) { return qsTr("Consuming") } return root.currentPowerState.value < 0 ? qsTr("Returning") : qsTr("Obtaining") } font: Style.smallFont } Label { Layout.fillWidth: true horizontalAlignment: Text.AlignHCenter font: Style.bigFont visible: batteryLevelState text: "%1 %".arg(batteryLevelState ? batteryLevelState.value : "") } } } OpacityMask { anchors.centerIn: parent width: background.contentItem.width height: background.contentItem.height source: ShaderEffectSource { sourceItem: juice hideSource: true sourceRect: Qt.rect(background.contentItem.x, background.contentItem.y, background.contentItem.width, background.contentItem.height) } maskSource: mask } } ColumnLayout { id: textLayout Layout.fillHeight: true Layout.fillWidth: true Layout.margins: Style.bigMargins spacing: Style.margins Label { Layout.fillWidth: true wrapMode: Text.WordWrap horizontalAlignment: Text.AlignHCenter textFormat: Text.RichText property double availableWh: isBattery ? root.capacityState.value * 1000 * root.batteryLevelState.value / 100 : 0 property double remainingWh: isCharging ? root.capacityState.value * 1000 - availableWh : availableWh property double remainingHours: isBattery ? remainingWh / Math.abs(root.currentPowerState.value) : 0 property date endTime: isBattery ? new Date(new Date().getTime() + remainingHours * 60 * 60 * 1000) : new Date() property int n: Math.round(remainingHours) text: root.isConsumer ? qsTr("Total consumption: %1 kWh").arg('' + root.totalEnergyConsumedState.value.toFixed(2) + "") : root.isProducer ? qsTr("Total production: %1 kWh").arg('' + root.totalEnergyProducedState.value.toFixed(2) + "") : root.isEnergyMeter ? qsTr("Total acquisition: %1 kWh").arg('' + root.totalEnergyConsumedState.value.toFixed(2) + "") + "
" + qsTr("Total return: %1 kWh").arg('' + root.totalEnergyProducedState.value.toFixed(2) + "") : root.isBattery && root.batteryLevelState.value === 0 ? qsTr("The battery is empty") : root.isBattery && root.isCharging ? qsTr("At the current rate, the battery will be fully charged at %1.").arg('' + endTime.toLocaleTimeString(Locale.ShortFormat) + "") : root.isBattery && root.isDischarging ? qsTr("At the current rate, the battery will last until %1.").arg('' + endTime.toLocaleTimeString(Locale.ShortFormat) + "") : "" } } } }