Improve energy pages

This commit is contained in:
Michael Zanetti 2021-09-23 12:52:03 +02:00
parent a4336ae1c0
commit dc399f28ae
10 changed files with 348 additions and 105 deletions

View File

@ -330,7 +330,7 @@ void LogsModel::fetchMore(const QModelIndex &parent)
Q_UNUSED(parent)
if (!m_engine) {
qCWarning(dcLogEngine()) << objectName() << "Cannot update. Engine not set";
qCDebug(dcLogEngine()) << objectName() << "Cannot update yet. Engine not set";
return;
}
if (m_busyInternal) {

View File

@ -257,5 +257,6 @@
<file>ui/components/CircleBackground.qml</file>
<file>ui/devicepages/CoolingThingPage.qml</file>
<file>ui/devicepages/EvChargerThingPage.qml</file>
<file>ui/components/BlurredLabel.qml</file>
</qresource>
</RCC>

View File

@ -91,7 +91,8 @@ Item {
"o2sensor": "lightblue",
"orpsensor": "yellow",
"powersocket": "aquamarine",
"evcharger": "limegreen"
"evcharger": "limegreen",
"energystorage": "limegreen"
}
property var stateColors: {
@ -111,4 +112,5 @@ Item {
readonly property int fastAnimationDuration: 100
readonly property int animationDuration: 150
readonly property int slowAnimationDuration: 300
readonly property int sleepyAnimationDuration: 2000
}

View File

@ -0,0 +1,37 @@
import QtQuick 2.5
import QtQuick.Controls 2.1
import QtGraphicalEffects 1.0
import Nymea 1.0
Item {
id: root
implicitWidth: label.implicitWidth + Style.margins * 2
implicitHeight: label.implicitHeight + Style.margins * 2
property alias text: label.text
property alias wrapMode: label.wrapMode
property alias horizontalAlignment: label.horizontalAlignment
property alias textFormat: label.textFormat
property bool blurred: false
Label {
id: label
anchors.fill: parent
}
ShaderEffectSource {
id: effectSource
anchors.fill: parent
sourceItem: label
hideSource: true
visible: false
}
FastBlur {
anchors.fill: parent
source: effectSource
radius: root.blurred ? 32 : 0
Behavior on radius { NumberAnimation { duration: Style.animationDuration } }
}
}

View File

@ -56,6 +56,13 @@ Item {
radius: width / 2
color: Style.tileBackgroundColor
}
Rectangle {
id: mask
anchors.fill: background
radius: height / 2
visible: false
color: "red"
}
MouseArea {
anchors.fill: background
@ -85,7 +92,7 @@ Item {
opacity: root.on ? 1 : 0
anchors.fill: gradient
source: gradient
maskSource: background
maskSource: mask
Behavior on opacity { NumberAnimation { duration: Style.animationDuration } }
}

View File

@ -40,6 +40,7 @@ import QtCharts 2.2
Item {
id: root
implicitHeight: width * .6
implicitWidth: 400
property Thing thing: null
property StateType stateType: null

View File

@ -66,52 +66,71 @@ ThingsListPageBase {
thing: root.thingsProxy.getThing(model.id)
onClicked: {
enterPage(index)
enterPage(index, ["energystorage", "smartmeter"])
}
contentItem: GridLayout {
contentItem: RowLayout {
id: dataGrid
columns: Math.floor(contentItem.width / 120)
Repeater {
model: ListModel {
Component.onCompleted: {
if (itemDelegate.thing.thingClass.stateTypes.findByName("totalEnergyConsumed") !== null) {
append( {stateName: "totalEnergyConsumed" })
property State currentPowerState: itemDelegate.thing.stateByName("currentPower")
property bool isEnergyMeter: itemDelegate.thing.thingClass.interfaces.indexOf("energymeter") >= 0
property bool isBattery: itemDelegate.thing.thingClass.interfaces.indexOf("energystorage") >= 0
property bool isEvCharger: itemDelegate.thing.thingClass.interfaces.indexOf("evcharger") >= 0
property bool isProducer: itemDelegate.thing.thingClass.interfaces.indexOf("smartmeterproducer") >= 0
property bool isConsumer: itemDelegate.thing.thingClass.interfaces.indexOf("smartmeterconsumer") >= 0
property bool isProduction: currentPowerState.value < 0
property bool isConsumption: currentPowerState.value > 0
property double absValue: Math.abs(currentPowerState.value)
property double cleanVale: (absValue / (absValue > 1000 ? 1000 : 1)).toFixed(1)
property string unit: absValue > 1000 ? "kW" : "W"
ColorIcon {
name: app.stateIcon("currentPower")
color: app.stateColor("currentPower")
size: Style.iconSize
}
Label {
Layout.fillWidth: true
text: {
if (dataGrid.isEnergyMeter) {
if (dataGrid.isProduction) {
return qsTr("%1 %2 returning").arg(dataGrid.cleanVale).arg(dataGrid.unit)
} else {
return qsTr("%1 %2 obtaining").arg(dataGrid.cleanVale).arg(dataGrid.unit)
}
if (itemDelegate.thing.thingClass.stateTypes.findByName("totalEnergyProduced") !== null) {
append( {stateName: "totalEnergyProduced" })
} else if (dataGrid.isBattery || dataGrid.isEvCharger) {
if (dataGrid.isProduction) {
return qsTr("%1 %2 discharging").arg(dataGrid.cleanVale).arg(dataGrid.unit)
} else {
return qsTr("%1 %2 charging").arg(dataGrid.cleanVale).arg(dataGrid.unit)
}
if (itemDelegate.thing.thingClass.stateTypes.findByName("currentPower") !== null) {
append({stateName: "currentPower"});
} else if (dataGrid.isProducer && !dataGrid.isConsumer) {
if (dataGrid.isProduction) {
return qsTr("%1 %2 producing").arg(dataGrid.cleanVale).arg(dataGrid.unit)
} else {
return qsTr("%1 %2 idling").arg(Math.max(0, dataGrid.cleanVale)).arg(dataGrid.unit)
}
} else if (dataGrid.isConsumer && !dataGrid.isProducer) {
if (dataGrid.isProduction) {
return qsTr("%1 %2 idling").arg(Math.max(0, dataGrid.cleanVale)).arg(dataGrid.unit)
} else {
return qsTr("%1 %2 consuming").arg(dataGrid.cleanVale).arg(dataGrid.unit)
}
} else {
if (dataGrid.isProduction) {
return qsTr("%1 %2 producing").arg(dataGrid.cleanVale).arg(dataGrid.unit)
} else {
return qsTr("%1 %2 consuming").arg(dataGrid.cleanVale).arg(dataGrid.unit)
}
}
}
delegate: RowLayout {
id: sensorValueDelegate
Layout.preferredWidth: contentItem.width / dataGrid.columns
property StateType stateType: itemDelegate.thing.thingClass.stateTypes.findByName(model.stateName)
property State stateValue: stateType ? itemDelegate.thing.states.getState(stateType.id) : null
ColorIcon {
Layout.preferredHeight: Style.iconSize
Layout.preferredWidth: height
Layout.alignment: Qt.AlignVCenter
color: app.stateColor(model.stateName)
name: app.stateIcon(model.stateName)
}
Label {
Layout.fillWidth: true
text: sensorValueDelegate.stateValue
? "%1 %2".arg((1.0 * Math.round(Types.toUiValue(sensorValueDelegate.stateValue.value, sensorValueDelegate.stateType.unit) * 100000) / 100000).toFixed(3)).arg(Types.toUiUnit(sensorValueDelegate.stateType.unit))
: ""
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
font.pixelSize: app.smallFont
}
}
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
font.pixelSize: app.smallFont
}
}
}

View File

@ -44,9 +44,14 @@ Page {
property ThingsProxy thingsProxy: thingsProxyInternal
function enterPage(index) {
function enterPage(index, interfaces) {
if (interfaces === undefined) {
interfaces = root.shownInterfaces
}
var thing = thingsProxy.get(index);
var page = NymeaUtils.interfaceListToDevicePage(root.shownInterfaces);
print("matching interfaces", interfaces)
var page = NymeaUtils.interfaceListToDevicePage(interfaces);
pageStack.push(Qt.resolvedUrl("../devicepages/" + page), {thing: thingsProxy.get(index)})
}

View File

@ -236,6 +236,8 @@ Page {
return boolComponent;
case "color":
return colorComponent
case "double":
return floatLabelComponent;
default:
if (entryDelegate.stateType.unit == Types.UnitUnixTime) {
return dateTimeComponent
@ -278,6 +280,17 @@ Page {
}
}
Component {
id: floatLabelComponent
Label {
property double value
property string unitString
text: value.toFixed(value > 1000 ? 0 : 2) + " " + unitString
font.pixelSize: app.smallFont
elide: Text.ElideRight
}
}
Component {
id: dateTimeComponent
Label {

View File

@ -31,6 +31,7 @@
import QtQuick 2.5
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.1
import QtGraphicalEffects 1.0
import Nymea 1.0
import "../components"
import "../customviews"
@ -38,75 +39,232 @@ import "../customviews"
ThingPageBase {
id: root
readonly property State totalEnergyConsumedState: root.thing.stateByName("totalEnergyConsumed")
readonly property StateType totalEnergyConsumedStateType: root.thing.thingClass.stateTypes.findByName("totalEnergyConsumed")
readonly property State totalEnergyProducedState: root.thing.stateByName("totalEnergyProduced")
readonly property StateType totalEnergyProducedStateType: root.thing.thingClass.stateTypes.findByName("totalEnergyProduced")
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.thingClass.interfaces.indexOf("smartmeterproducer") >= 0
readonly property bool isBattery: root.thing && root.thingClass.interfaces.indexOf("energystorage") >= 0
Flickable {
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
readonly property real currentPower: currentPowerState ? currentPowerState.value : 0
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
topMargin: app.margins / 2
contentHeight: contentGrid.height
interactive: contentHeight > height
columns: app.landscape ? 2 : 1
GridLayout {
id: contentGrid
width: parent.width - app.margins
anchors.horizontalCenter: parent.horizontalCenter
columns: 1
CircleBackground {
id: background
Layout.fillWidth: true
Layout.preferredHeight: width
Layout.leftMargin: app.landscape ? Style.margins : Style.hugeMargins
Layout.rightMargin: Style.hugeMargins
Layout.topMargin: Style.hugeMargins
Layout.bottomMargin: app.landscape ? Style.hugeMargins : Style.margins
iconSource: ""
onColor: batteryLevelState
? currentPower < 0 ? "crimson" : "limegreen"
: currentPower < 0 ? "limegreen" : "crimson"
BigTile {
Layout.preferredWidth: contentGrid.width / contentGrid.columns
showHeader: true
header: Label {
text: qsTr("Total energy consumption")
}
onPressAndHold: {
var contextMenuComponent = Qt.createComponent("../components/ThingContextMenu.qml");
var contextMenu = contextMenuComponent.createObject(root, { thing: root.thing })
contextMenu.x = Qt.binding(function() { return (root.width - contextMenu.width) / 2 })
contextMenu.open()
}
contentItem: RowLayout {
ColorIcon {
Layout.preferredHeight: Style.iconSize
Layout.preferredWidth: Style.iconSize
name: app.stateIcon("totalEnergyConsumed")
color: app.stateColor("totalEnergyConsumed")
}
Label {
Layout.fillWidth: true
text: root.totalEnergyConsumedState.value.toFixed(2) + " " + root.totalEnergyConsumedStateType.unitString
font.pixelSize: app.largeFont
}
ColorIcon {
Layout.preferredHeight: Style.iconSize
Layout.preferredWidth: Style.iconSize
name: app.stateIcon("totalEnergyProduced")
color: app.stateColor("totalEnergyProduced")
visible: root.totalEnergyProducedState !== null
}
Label {
Layout.fillWidth: true
text: root.totalEnergyProducedState.value.toFixed(2) + " " + root.totalEnergyProducedStateType.unitString
font.pixelSize: app.largeFont
visible: root.totalEnergyProducedState !== null
}
}
Rectangle {
id: mask
anchors.centerIn: parent
width: background.contentItem.width
height: background.contentItem.height
radius: width / 2
visible: false
}
GenericTypeGraph {
Layout.preferredWidth: contentGrid.width / contentGrid.columns
thing: root.thing
stateType: root.thingClass.stateTypes.findByName("currentPower")
color: app.stateColor("currentPower")
iconSource: app.stateIcon("currentPower")
roundTo: 5
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: batteryCriticalState && batteryCriticalState.value ? "crimson" : "limegreen"
visible: root.batteryLevelState
}
RadialGradient {
id: gradient
anchors.centerIn: parent
width: background.contentItem.width
height: background.contentItem.height
property real progress: Math.abs(root.currentPower) / 10000
visible: currentPower != 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.hugeFont
property bool toKilos: currentPower >= 1000
property double value: Math.abs(currentPower / (toKilos ? 1000 : 1))
text: "%1 %2".arg(value.toFixed(toKilos ? 2 : 1)).arg(toKilos ? "kW" : "W")
}
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 qsTr("Production")
}
return root.currentPower < 0 ? qsTr("Return") : qsTr("Consumption")
}
font: Style.smallFont
}
Label {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
font: Style.hugeFont
visible: batteryLevelState
text: "%1 %".arg(batteryLevelState ? batteryLevelState.value : "")
}
}
}
OpacityMask {
anchors.fill: background
source: ShaderEffectSource {
sourceItem: juice
hideSource: true
}
maskSource: mask
}
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
Layout.margins: Style.bigMargins
ColumnLayout {
id: textLayout
anchors.fill: parent
spacing: Style.margins
Label {
Layout.fillWidth: true
visible: isBattery
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
textFormat: Text.RichText
property bool isCharging: root.chargingState && root.chargingState.value === "charging"
property bool isDischarging: root.chargingState && root.chargingState.value === "discharging"
property double availableWh: isBattery ? root.capacityState.value * 1000 * root.batteryLevelState.value / 100 : 0
property double remainingWh: isCharging ? root.capacityState.value - availableWh : availableWh
property double remainingHours: isBattery ? remainingWh / Math.abs(root.currentPower) : 0
property date endTime: isBattery ? new Date(new Date().getTime() + remainingHours * 60 * 60 * 1000) : new Date()
property int n: Math.round(remainingHours)
text: isCharging ? qsTr("At the current rate, the battery will be fully charged at %1.").arg('<span style="font-size:' + Style.bigFont.pixelSize + 'px">' + endTime.toLocaleTimeString(Locale.ShortFormat) + "</span>")
: isDischarging ? qsTr("At the current rate, the battery will last until %1.").arg('<span style="font-size:' + Style.bigFont.pixelSize + 'px">' + endTime.toLocaleTimeString(Locale.ShortFormat) + "</span>")
: qsTr("The battery is fully charged")
}
BlurredLabel {
Layout.fillWidth: true
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
visible: isEnergyMeter || isConsumer
blurred: periodConsumptionModel.busy
text: isConsumer ?
qsTr("A total of %1 kWh has been <b>consumed</b> in the last 24 hours.").arg('<span style="font-size:' + Style.bigFont.pixelSize + 'px">' + (totalPeriodConsumption).toFixed(1) + '</span>')
: qsTr("A total of %1 kWh has been <b>obtained</b> in the last 24 hours.").arg('<span style="font-size:' + Style.bigFont.pixelSize + 'px">' + (totalPeriodConsumption).toFixed(1) + '</span>')
textFormat: Text.RichText
LogsModel {
id: periodConsumptionModel
objectName: "Root meter model"
engine: root.isEnergyMeter ? _engine : null
thingId: root.thing.id
typeIds: isEnergyMeter ? [root.totalEnergyConsumedStateType.id] : []
viewStartTime: root.startTime
live: true
}
property LogEntry logEntryAtStart: periodConsumptionModel.busy ? null : periodConsumptionModel.findClosest(periodConsumptionModel.viewStartTime)
property double totalPeriodConsumption: logEntryAtStart && totalEnergyConsumedState ? totalEnergyConsumedState.value - logEntryAtStart.value : 0
}
BlurredLabel {
visible: isEnergyMeter || isProducer
Layout.fillWidth: true
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
blurred: periodProductionModel.busy
text: isProducer ?
qsTr("A total of %1 kWh has been <b>produced</b> in the last 24 hours.").arg('<span style="font-size:' + Style.bigFont.pixelSize + 'px">' + (totalPeriodProduction).toFixed(1) + '</span>')
: qsTr("A total of %1 kWh has been <b>returned</b> in the last 24 hours.").arg('<span style="font-size:' + Style.bigFont.pixelSize + 'px">' + (totalPeriodProduction).toFixed(1) + '</span>')
textFormat: Text.RichText
LogsModel {
id: periodProductionModel
engine: root.isEnergyMeter ? _engine : null
thingId: root.thing.id
typeIds: isEnergyMeter ? [root.totalEnergyProducedStateType.id] : []
viewStartTime: root.startTime
live: true
}
property LogEntry logEntryAtStart: periodProductionModel.busy ? null : periodProductionModel.findClosest(periodProductionModel.viewStartTime)
property double totalPeriodProduction: logEntryAtStart && totalEnergyProducedState ? totalEnergyProducedState.value - logEntryAtStart.value : 0
}
}
}
}
}