From 05d1938167b04d184f3d5438dd1672d08e50e9a8 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Mon, 4 Jan 2021 00:39:51 +0100 Subject: [PATCH 1/2] Improve energy views --- nymea-app/ui/StyleBase.qml | 4 +- nymea-app/ui/components/HeaderButton.qml | 4 +- nymea-app/ui/customviews/GenericTypeGraph.qml | 594 +++++++++--------- .../ui/devicepages/SmartMeterDevicePage.qml | 69 +- 4 files changed, 341 insertions(+), 330 deletions(-) diff --git a/nymea-app/ui/StyleBase.qml b/nymea-app/ui/StyleBase.qml index 0c057361..30acdd64 100644 --- a/nymea-app/ui/StyleBase.qml +++ b/nymea-app/ui/StyleBase.qml @@ -35,9 +35,9 @@ Item { "presencesensor": "darkblue", "closablesensor": "green", "smartmeterproducer": "lightgreen", + "extendedsmartmeterproducer": "lightgreen", "smartmeterconsumer": "orange", - "extendedsmartmeterproducer": "blue", - "extendedsmartmeterconsumer": "blue", + "extendedsmartmeterconsumer": "orange", "heating" : "gainsboro", "thermostat": "dodgerblue", "irrigation": "lightblue", diff --git a/nymea-app/ui/components/HeaderButton.qml b/nymea-app/ui/components/HeaderButton.qml index fe0f7cf1..92723c4d 100644 --- a/nymea-app/ui/components/HeaderButton.qml +++ b/nymea-app/ui/components/HeaderButton.qml @@ -36,8 +36,8 @@ ToolButton { property alias color: image.color contentItem: Item { - height: 20 - width: 20 + height: app.iconSize + width: app.iconSize ColorIcon { id: image anchors.fill: parent diff --git a/nymea-app/ui/customviews/GenericTypeGraph.qml b/nymea-app/ui/customviews/GenericTypeGraph.qml index f9bb17b4..2a729252 100644 --- a/nymea-app/ui/customviews/GenericTypeGraph.qml +++ b/nymea-app/ui/customviews/GenericTypeGraph.qml @@ -72,11 +72,27 @@ Item { viewStartTime: xAxis.min } - ColumnLayout { + ChartView { + id: chartView anchors.fill: parent - spacing: 0 + margins.top: app.iconSize + app.margins + margins.bottom: app.margins / 2 + margins.left: 0 + margins.right: 0 + backgroundColor: Style.tileBackgroundColor + backgroundRoundness: Style.tileRadius + legend.visible: false + legend.labelColor: Style.foregroundColor + + titleColor: Style.foregroundColor + titleFont.pixelSize: app.largeFont + + animationDuration: 300 + animationOptions: ChartView.SeriesAnimations + RowLayout { - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins + anchors { left: parent.left; top: parent.top; right: parent.right; topMargin: app.margins / 2; leftMargin: app.margins; rightMargin: app.margins } + ColorIcon { Layout.preferredHeight: app.iconSize Layout.preferredWidth: app.iconSize @@ -84,21 +100,13 @@ Item { visible: root.iconSource.length > 0 color: root.color } - - Led { - visible: root.stateType.type.toLowerCase() === "bool" - state: root.valueState.value === true ? "on" : "off" - } - Label { Layout.fillWidth: true text: root.stateType.type.toLowerCase() === "bool" - ? root.stateType.displayName - : 1.0 * Math.round(Types.toUiValue(root.valueState.value, root.stateType.unit) * Math.pow(10, root.roundTo)) / Math.pow(10, root.roundTo) + " " + Types.toUiUnit(root.stateType.unit) + ? root.stateType.displayName + : 1.0 * Math.round(Types.toUiValue(root.valueState.value, root.stateType.unit) * Math.pow(10, root.roundTo)) / Math.pow(10, root.roundTo) + " " + Types.toUiUnit(root.stateType.unit) font.pixelSize: app.largeFont } - - HeaderButton { imageSource: "../images/zoom-out.svg" onClicked: { @@ -106,7 +114,6 @@ Item { xAxis.min = newTime; } } - HeaderButton { imageSource: "../images/zoom-in.svg" enabled: xAxis.timeDiff > (60 * 30) @@ -117,333 +124,318 @@ Item { } } - ChartView { - id: chartView - Layout.fillWidth: true - Layout.fillHeight: true - margins.top: 0 - margins.bottom: 0 - margins.left: 0 - margins.right: 0 - backgroundColor: Material.background - legend.visible: false - legend.labelColor: Style.foregroundColor - - animationDuration: 300 - animationOptions: ChartView.SeriesAnimations - - ValueAxis { - id: yAxis - max: { - switch (root.stateType.type.toLowerCase()) { - case "bool": - return 1; - default: - Math.ceil(logsModelNg.maxValue + Math.abs(logsModelNg.maxValue * .05)) - } - } - min: Math.floor(logsModelNg.minValue - Math.abs(logsModelNg.minValue * .05)) -// onMinChanged: applyNiceNumbers(); -// onMaxChanged: applyNiceNumbers(); - labelsFont.pixelSize: app.smallFont - labelFormat: { - switch (root.stateType.type.toLowerCase()) { - case "bool": - return "x"; - default: - return "%d"; - } - } - labelsColor: Style.foregroundColor - tickCount: root.stateType.type.toLowerCase() === "bool" ? 2 : chartView.height / 40 - color: Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, .2) - gridLineColor: color - } - - ValueAxis { - id: connectedAxis - min: 0 - max: 1 - visible: false - } - - DateTimeAxis { - id: xAxis - gridVisible: false - color: Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, .2) - tickCount: chartView.width / 70 - labelsFont.pixelSize: app.smallFont - labelsColor: Style.foregroundColor - property int timeDiff: (xAxis.max.getTime() - xAxis.min.getTime()) / 1000 - - function getTimeSpanString() { - var td = Math.round(timeDiff) - if (td < 60) { - return qsTr("%n seconds", "", td); - } - td = Math.round(td / 60) - if (td < 60) { - return qsTr("%n minutes", "", td); - } - td = Math.round(td / 60) - if (td < 48) { - return qsTr("%n hours", "", td); - } - td = Math.round(td / 24); - if (td < 14) { - return qsTr("%n days", "", td); - } - td = Math.round(td / 7) - if (td < 9) { - return qsTr("%n weeks", "", td); - } - td = Math.round(td * 7 / 30) - if (td < 24) { - return qsTr("%n months", "", td); - } - td = Math.round(td * 30 / 356) - return qsTr("%n years", "", td) - } - - titleText: { - if (xAxis.min.getYear() === xAxis.max.getYear() - && xAxis.min.getMonth() === xAxis.max.getMonth() - && xAxis.min.getDate() === xAxis.max.getDate()) { - return Qt.formatDate(xAxis.min) + " (" + getTimeSpanString() + ")" - } - return Qt.formatDate(xAxis.min) + " - " + Qt.formatDate(xAxis.max) + " (" + getTimeSpanString() + ")" - } - titleBrush: Style.foregroundColor - format: { - if (timeDiff < 60) { // one minute - return "mm:ss" - } - if (timeDiff < 60 * 60) { // one hour - return "hh:mm" - } - if (timeDiff < 60 * 60 * 24 * 2) { // two day - return "hh:mm" - } - if (timeDiff < 60 * 60 * 24 * 7) { // one week - return "ddd hh:mm" - } - if (timeDiff < 60 * 60 * 24 * 7 * 30) { // one month - return "dd.MM." - } - return "MMM yy" - } - - min: { - var date = new Date(); - date.setTime(date.getTime() - (1000 * 60 * 60 * 6) + 2000); - return date; - } - max: { - var date = new Date(); - date.setTime(date.getTime() + 2000) - return date; + ValueAxis { + id: yAxis + max: { + switch (root.stateType.type.toLowerCase()) { + case "bool": + return 1; + default: + Math.ceil(logsModelNg.maxValue + Math.abs(logsModelNg.maxValue * .05)) } } - - AreaSeries { - axisX: xAxis - axisY: connectedAxis - name: qsTr("Not connected") - visible: root.hasConnectable - upperSeries: LineSeries { - XYPoint {x: xAxis.min.getTime(); y: 1} - XYPoint {x: xAxis.max.getTime(); y: 1} + min: Math.floor(logsModelNg.minValue - Math.abs(logsModelNg.minValue * .05)) + // onMinChanged: applyNiceNumbers(); + // onMaxChanged: applyNiceNumbers(); + labelsFont.pixelSize: app.smallFont + labelFormat: { + switch (root.stateType.type.toLowerCase()) { + case "bool": + return "x"; + default: + return "%d"; } + } + labelsColor: Style.foregroundColor + tickCount: root.stateType.type.toLowerCase() === "bool" ? 2 : chartView.height / 40 + color: Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, .2) + gridLineColor: color + } - lowerSeries: LineSeries { - id: connectedLineSeries - onPointAdded: { - var newPoint = connectedLineSeries.at(index) -// print("pointadded", newPoint.x, newPoint.y) - } + ValueAxis { + id: connectedAxis + min: 0 + max: 1 + visible: false + } + DateTimeAxis { + id: xAxis + gridVisible: false + color: Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, .2) + tickCount: chartView.width / 70 + labelsFont.pixelSize: app.smallFont + labelsColor: Style.foregroundColor + property int timeDiff: (xAxis.max.getTime() - xAxis.min.getTime()) / 1000 + + function getTimeSpanString() { + var td = Math.round(timeDiff) + if (td < 60) { + return qsTr("%n seconds", "", td); } - color: "#55ff0000" - borderWidth: 0 + td = Math.round(td / 60) + if (td < 60) { + return qsTr("%n minutes", "", td); + } + td = Math.round(td / 60) + if (td < 48) { + return qsTr("%n hours", "", td); + } + td = Math.round(td / 24); + if (td < 14) { + return qsTr("%n days", "", td); + } + td = Math.round(td / 7) + if (td < 9) { + return qsTr("%n weeks", "", td); + } + td = Math.round(td * 7 / 30) + if (td < 24) { + return qsTr("%n months", "", td); + } + td = Math.round(td * 30 / 356) + return qsTr("%n years", "", td) } - AreaSeries { - id: mainSeries - axisX: xAxis - axisY: yAxis - name: root.stateType.displayName - borderColor: root.color - borderWidth: 4 - lowerSeries: LineSeries { - id: lineSeries0 - XYPoint { x: xAxis.max.getTime(); y: 0 } - XYPoint { x: xAxis.min.getTime(); y: 0 } + titleText: { + if (xAxis.min.getYear() === xAxis.max.getYear() + && xAxis.min.getMonth() === xAxis.max.getMonth() + && xAxis.min.getDate() === xAxis.max.getDate()) { + return Qt.formatDate(xAxis.min) + " (" + getTimeSpanString() + ")" + } + return Qt.formatDate(xAxis.min) + " - " + Qt.formatDate(xAxis.max) + " (" + getTimeSpanString() + ")" + } + titleBrush: Style.foregroundColor + format: { + if (timeDiff < 60) { // one minute + return "mm:ss" + } + if (timeDiff < 60 * 60) { // one hour + return "hh:mm" + } + if (timeDiff < 60 * 60 * 24 * 2) { // two day + return "hh:mm" + } + if (timeDiff < 60 * 60 * 24 * 7) { // one week + return "ddd hh:mm" + } + if (timeDiff < 60 * 60 * 24 * 7 * 30) { // one month + return "dd.MM." + } + return "MMM yy" + } + + min: { + var date = new Date(); + date.setTime(date.getTime() - (1000 * 60 * 60 * 6) + 2000); + return date; + } + max: { + var date = new Date(); + date.setTime(date.getTime() + 2000) + return date; + } + } + + AreaSeries { + axisX: xAxis + axisY: connectedAxis + name: qsTr("Not connected") + visible: root.hasConnectable + upperSeries: LineSeries { + XYPoint {x: xAxis.min.getTime(); y: 1} + XYPoint {x: xAxis.max.getTime(); y: 1} + } + + lowerSeries: LineSeries { + id: connectedLineSeries + onPointAdded: { + var newPoint = connectedLineSeries.at(index) + // print("pointadded", newPoint.x, newPoint.y) } - upperSeries: LineSeries { - id: lineSeries1 - onPointAdded: { - var newPoint = lineSeries1.at(index) -// print("pointadded", newPoint.x, newPoint.y) + } + color: "#55ff0000" + borderWidth: 0 + } - if (newPoint.x > lineSeries0.at(0).x) { - lineSeries0.replace(0, newPoint.x, 0) - } - if (newPoint.x < lineSeries0.at(1).x) { - lineSeries0.replace(1, newPoint.x, 0) - } + AreaSeries { + id: mainSeries + axisX: xAxis + axisY: yAxis + name: root.stateType.displayName + borderColor: root.color + borderWidth: 4 + lowerSeries: LineSeries { + id: lineSeries0 + XYPoint { x: xAxis.max.getTime(); y: 0 } + XYPoint { x: xAxis.min.getTime(); y: 0 } + } - if (newPoint.x <= xAxis.max.getTime() || logsModelNg.busy) { - return; - } - - var diffMaxToNew = newPoint.x - xAxis.max.getTime(); - if (diffMaxToNew < 1000 * 60 * 5) { - chartView.animationOptions = ChartView.NoAnimation - var newMin = xAxis.min.getTime() + diffMaxToNew; - xAxis.max = new Date(newPoint.x); - xAxis.min = new Date(newMin) - chartView.animationOptions = ChartView.SeriesAnimations - } + upperSeries: LineSeries { + id: lineSeries1 + onPointAdded: { + var newPoint = lineSeries1.at(index) + // print("pointadded", newPoint.x, newPoint.y) + if (newPoint.x > lineSeries0.at(0).x) { + lineSeries0.replace(0, newPoint.x, 0) + } + if (newPoint.x < lineSeries0.at(1).x) { + lineSeries0.replace(1, newPoint.x, 0) } - } - color: Qt.rgba(root.color.r, root.color.g, root.color.b, .3) - onHovered: { - markClosestPoint(point) - } - function markClosestPoint(point) { - if (lineSeries1.count == 0) { + if (newPoint.x <= xAxis.max.getTime() || logsModelNg.busy) { return; } - if (lineSeries1.count == 1) { - selectedHighlights.removePoints(0, selectedHighlights.count) - selectedHighlights.append(lineSeries1.at(0).x, lineSeries1.at(1).y) - return; + var diffMaxToNew = newPoint.x - xAxis.max.getTime(); + if (diffMaxToNew < 1000 * 60 * 5) { + chartView.animationOptions = ChartView.NoAnimation + var newMin = xAxis.min.getTime() + diffMaxToNew; + xAxis.max = new Date(newPoint.x); + xAxis.min = new Date(newMin) + chartView.animationOptions = ChartView.SeriesAnimations } - var searchIndex = Math.floor(lineSeries1.count / 2) - var previousIndex = 0; - var nextIndex = lineSeries1.count - 1; + } + } + color: Qt.rgba(root.color.r, root.color.g, root.color.b, .3) + onHovered: { + markClosestPoint(point) + } - while (previousIndex + 1 != nextIndex) { - if (point.x < lineSeries1.at(searchIndex).x) { - previousIndex = searchIndex; - } else if (point.x > lineSeries1.at(searchIndex).x) { - nextIndex = searchIndex; - } - searchIndex = previousIndex + Math.floor((nextIndex - previousIndex) / 2); - } - var diffToPrevious = Math.abs(point.x - lineSeries1.at(previousIndex).x) - var diffToNext = Math.abs(point.x - lineSeries1.at(nextIndex).x) - var closestPoint = diffToPrevious < diffToNext ? lineSeries1.at(previousIndex) : lineSeries1.at(nextIndex); + function markClosestPoint(point) { + if (lineSeries1.count == 0) { + return; + } + if (lineSeries1.count == 1) { selectedHighlights.removePoints(0, selectedHighlights.count) - selectedHighlights.append(closestPoint.x, closestPoint.y) + selectedHighlights.append(lineSeries1.at(0).x, lineSeries1.at(1).y) + return; } - } - ScatterSeries { - id: selectedHighlights - color: root.color - markerSize: 10 - borderWidth: 2 - borderColor: root.color - axisX: xAxis - axisY: yAxis - pointLabelsVisible: root.stateType.type.toLowerCase() !== "bool" - pointLabelsColor: Style.foregroundColor - pointLabelsFont.pixelSize: app.smallFont - pointLabelsFormat: "@yPoint" - pointLabelsClipping: false - } + var searchIndex = Math.floor(lineSeries1.count / 2) + var previousIndex = 0; + var nextIndex = lineSeries1.count - 1; - BusyIndicator { - anchors.centerIn: parent - visible: logsModelNg.busy - } - - - MouseArea { - id: scrollMouseArea - x: chartView.plotArea.x - y: chartView.plotArea.y - width: chartView.plotArea.width - height: chartView.plotArea.height - property int lastX: 0 - property int startX: 0 - preventStealing: false - - property bool autoScroll: true - - function scrollRightLimited(dx) { - chartView.animationOptions = ChartView.NoAnimation - var now = new Date() - // if we're already at the limit, don't even start scrolling - if (dx < 0 || xAxis.max < now) { - chartView.scrollRight(dx) + while (previousIndex + 1 != nextIndex) { + if (point.x < lineSeries1.at(searchIndex).x) { + previousIndex = searchIndex; + } else if (point.x > lineSeries1.at(searchIndex).x) { + nextIndex = searchIndex; } - // figure out if we scrolled too far - var overshoot = xAxis.max.getTime() - now.getTime() -// print("overshoot is:", overshoot, "oldMax", xAxis.max, "newMax", now, "oldMin", xAxis.min, "newMin", new Date(xAxis.min.getTime() - overshoot)) - if (overshoot > 0) { - var range = xAxis.max - xAxis.min - xAxis.max = now - xAxis.min = new Date(xAxis.max.getTime() - range) - } - // If the user scrolled closer than 5 pixels to the right edge, enable autoscroll - autoScroll = overshoot > -5; - - chartView.animationOptions = ChartView.SeriesAnimations + searchIndex = previousIndex + Math.floor((nextIndex - previousIndex) / 2); } + var diffToPrevious = Math.abs(point.x - lineSeries1.at(previousIndex).x) + var diffToNext = Math.abs(point.x - lineSeries1.at(nextIndex).x) + var closestPoint = diffToPrevious < diffToNext ? lineSeries1.at(previousIndex) : lineSeries1.at(nextIndex); - function zoomInLimited(dy) { - chartView.animationOptions = ChartView.NoAnimation - var oldMax = xAxis.max; - chartView.scrollRight(dy); - xAxis.min = new Date(xAxis.min.getTime() - xAxis.timeDiff * 1000 * 2) - chartView.animationOptions = ChartView.SeriesAnimations + selectedHighlights.removePoints(0, selectedHighlights.count) + selectedHighlights.append(closestPoint.x, closestPoint.y) + } + } + + ScatterSeries { + id: selectedHighlights + color: root.color + markerSize: 10 + borderWidth: 2 + borderColor: root.color + axisX: xAxis + axisY: yAxis + pointLabelsVisible: root.stateType.type.toLowerCase() !== "bool" + pointLabelsColor: Style.foregroundColor + pointLabelsFont.pixelSize: app.smallFont + pointLabelsFormat: "@yPoint" + pointLabelsClipping: false + } + + BusyIndicator { + anchors.centerIn: parent + visible: logsModelNg.busy + } + + + MouseArea { + id: scrollMouseArea + x: chartView.plotArea.x + y: chartView.plotArea.y + width: chartView.plotArea.width + height: chartView.plotArea.height + property int lastX: 0 + property int startX: 0 + preventStealing: false + + property bool autoScroll: true + + function scrollRightLimited(dx) { + chartView.animationOptions = ChartView.NoAnimation + var now = new Date() + // if we're already at the limit, don't even start scrolling + if (dx < 0 || xAxis.max < now) { + chartView.scrollRight(dx) } + // figure out if we scrolled too far + var overshoot = xAxis.max.getTime() - now.getTime() + // print("overshoot is:", overshoot, "oldMax", xAxis.max, "newMax", now, "oldMin", xAxis.min, "newMin", new Date(xAxis.min.getTime() - overshoot)) + if (overshoot > 0) { + var range = xAxis.max - xAxis.min + xAxis.max = now + xAxis.min = new Date(xAxis.max.getTime() - range) + } + // If the user scrolled closer than 5 pixels to the right edge, enable autoscroll + autoScroll = overshoot > -5; - onPressed: { + chartView.animationOptions = ChartView.SeriesAnimations + } + + function zoomInLimited(dy) { + chartView.animationOptions = ChartView.NoAnimation + var oldMax = xAxis.max; + chartView.scrollRight(dy); + xAxis.min = new Date(xAxis.min.getTime() - xAxis.timeDiff * 1000 * 2) + chartView.animationOptions = ChartView.SeriesAnimations + } + + onPressed: { + lastX = mouse.x + startX = mouse.x + } + onClicked: { + var pt = chartView.mapToValue(Qt.point(mouse.x + chartView.plotArea.x, mouse.y + chartView.plotArea.y), mainSeries) + mainSeries.markClosestPoint(pt) + } + + onWheel: { + scrollRightLimited(-wheel.pixelDelta.x) + // zoomInLimited(wheel.pixelDelta.y) + } + + onPositionChanged: { + if (lastX !== mouse.x) { + scrollRightLimited(lastX - mouseX) lastX = mouse.x - startX = mouse.x - } - onClicked: { - var pt = chartView.mapToValue(Qt.point(mouse.x + chartView.plotArea.x, mouse.y + chartView.plotArea.y), mainSeries) - mainSeries.markClosestPoint(pt) } - onWheel: { - scrollRightLimited(-wheel.pixelDelta.x) -// zoomInLimited(wheel.pixelDelta.y) + if (Math.abs(startX - mouse.x) > 10) { + preventStealing = true; } + } - onPositionChanged: { - if (lastX !== mouse.x) { - scrollRightLimited(lastX - mouseX) - lastX = mouse.x - } - - if (Math.abs(startX - mouse.x) > 10) { - preventStealing = true; - } - } - - onReleased: preventStealing = false; + onReleased: preventStealing = false; - Timer { - running: scrollMouseArea.autoScroll - interval: 1000 - repeat: true - onTriggered: { - scrollMouseArea.scrollRightLimited(10) - } + Timer { + running: scrollMouseArea.autoScroll + interval: 1000 + repeat: true + onTriggered: { + scrollMouseArea.scrollRightLimited(10) } } } } } + diff --git a/nymea-app/ui/devicepages/SmartMeterDevicePage.qml b/nymea-app/ui/devicepages/SmartMeterDevicePage.qml index 30733fc8..08924e43 100644 --- a/nymea-app/ui/devicepages/SmartMeterDevicePage.qml +++ b/nymea-app/ui/devicepages/SmartMeterDevicePage.qml @@ -38,6 +38,11 @@ import "../customviews" DevicePageBase { 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") + Flickable { anchors.fill: parent contentHeight: contentGrid.height @@ -45,34 +50,48 @@ DevicePageBase { GridLayout { id: contentGrid - width: parent.width - columns: Math.min(width / 300, contentModel.count) + width: parent.width - app.margins + anchors.horizontalCenter: parent.horizontalCenter + columns: 1 - Repeater { - model: ListModel { - id: contentModel - Component.onCompleted: { - if (root.deviceClass.interfaces.indexOf("extendedsmartmeterproducer") >= 0 - || root.deviceClass.interfaces.indexOf("extendedsmartmeterconsumer") >= 0) { - append( {interface: "extendedsmartmeterproducer", stateTypeName: "currentPower" }) - } - if (root.deviceClass.interfaces.indexOf("smartmeterproducer") >= 0) { - append( {interface: "smartmeterproducer", stateTypeName: "totalEnergyProduced" }) - } - if (root.deviceClass.interfaces.indexOf("smartmeterconsumer") >= 0) { - append( {interface: "smartmeterconsumer", stateTypeName: "totalEnergyConsumed" }) - } - print("shown graphs are", count) + BigTile { + Layout.preferredWidth: contentGrid.width / contentGrid.columns + showHeader: false + contentItem: RowLayout { + ColorIcon { + Layout.preferredHeight: app.iconSize + Layout.preferredWidth: app.iconSize + name: app.interfaceToIcon("smartmeterconsumer") + color: app.interfaceToColor("smartmeterconsumer") + } + + Label { + Layout.fillWidth: true + text: root.totalEnergyConsumedState.value.toFixed(2) + " " + root.totalEnergyConsumedStateType.unitString + font.pixelSize: app.largeFont + } + ColorIcon { + Layout.preferredHeight: app.iconSize + Layout.preferredWidth: app.iconSize + name: app.interfaceToIcon("smartmeterproducer") + color: app.interfaceToColor("smartmeterproducer") + } + + Label { + Layout.fillWidth: true + text: root.totalEnergyProducedState.value.toFixed(2) + " " + root.totalEnergyProducedStateType.unitString + font.pixelSize: app.largeFont } } - delegate: GenericTypeGraph { - Layout.preferredWidth: contentGrid.width / contentGrid.columns - device: root.device - stateType: root.deviceClass.stateTypes.findByName(model.stateTypeName) - color: app.interfaceToColor(model.interface) - iconSource: app.interfaceToIcon(model.interface) - roundTo: 5 - } + } + + GenericTypeGraph { + Layout.preferredWidth: contentGrid.width / contentGrid.columns + device: root.device + stateType: root.deviceClass.stateTypes.findByName("currentPower") + color: app.interfaceToColor("smartmeterconsumer") + iconSource: app.interfaceToIcon("smartmeterconsumer") + roundTo: 5 } } } From c1b3d5aed0d0d54982944a1deac2c57759298828 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Wed, 6 Jan 2021 19:19:17 +0100 Subject: [PATCH 2/2] Add watersensor interface --- .../connection/discovery/upnpdiscovery.cpp | 2 +- libnymea-app/models/logsmodel.cpp | 2 +- libnymea-app/models/logsmodelng.cpp | 2 +- libnymea-app/models/xyseriesadapter.cpp | 28 ++- libnymea-app/types/interfaces.cpp | 2 + nymea-app/images.qrc | 1 + nymea-app/ui/Nymea.qml | 27 +++ nymea-app/ui/StyleBase.qml | 13 +- nymea-app/ui/components/SmartMeterChart.qml | 3 +- nymea-app/ui/customviews/GenericTypeGraph.qml | 9 +- nymea-app/ui/delegates/InterfaceTile.qml | 2 +- .../devicelistpages/SensorsDeviceListPage.qml | 7 +- .../SmartMeterDeviceListPage.qml | 4 +- nymea-app/ui/devicepages/SensorDevicePage.qml | 32 +++- .../ui/devicepages/SmartMeterDevicePage.qml | 21 ++- nymea-app/ui/images/sensors/water.svg | 177 ++++++++++++++++++ nymea-app/ui/mainviews/EnergyView.qml | 11 +- 17 files changed, 312 insertions(+), 31 deletions(-) create mode 100644 nymea-app/ui/images/sensors/water.svg diff --git a/libnymea-app/connection/discovery/upnpdiscovery.cpp b/libnymea-app/connection/discovery/upnpdiscovery.cpp index 35c9a636..1de798ca 100644 --- a/libnymea-app/connection/discovery/upnpdiscovery.cpp +++ b/libnymea-app/connection/discovery/upnpdiscovery.cpp @@ -140,7 +140,7 @@ void UpnpDiscovery::readData() data.resize(socket->pendingDatagramSize()); socket->readDatagram(data.data(), data.size(), &hostAddress, &port); - qDebug() << "Received UPnP datagram:" << data; +// qDebug() << "Received UPnP datagram:" << data; // if the data contains the HTTP OK header... if (data.contains("HTTP/1.1 200 OK")) { diff --git a/libnymea-app/models/logsmodel.cpp b/libnymea-app/models/logsmodel.cpp index 89d53d23..b4baa62a 100644 --- a/libnymea-app/models/logsmodel.cpp +++ b/libnymea-app/models/logsmodel.cpp @@ -322,7 +322,7 @@ void LogsModel::fetchMore(const QModelIndex &parent) params.insert("limit", m_blockSize); params.insert("offset", m_list.count()); - qDebug() << "Fetching logs from" << m_startTime.toString() << "to" << m_endTime.toString() << "with offset" << m_list.count() << "and limit" << m_blockSize; +// qDebug() << "Fetching logs from" << m_startTime.toString() << "to" << m_endTime.toString() << "with offset" << m_list.count() << "and limit" << m_blockSize; m_engine->jsonRpcClient()->sendCommand("Logging.GetLogEntries", params, this, "logsReply"); // qDebug() << "GetLogEntries called"; diff --git a/libnymea-app/models/logsmodelng.cpp b/libnymea-app/models/logsmodelng.cpp index d9585380..bb1a4995 100644 --- a/libnymea-app/models/logsmodelng.cpp +++ b/libnymea-app/models/logsmodelng.cpp @@ -418,7 +418,7 @@ void LogsModelNg::fetchMore(const QModelIndex &parent) params.insert("limit", m_blockSize); params.insert("offset", m_list.count()); - qDebug() << "Fetching logs:" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson()); +// qDebug() << "Fetching logs:" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson()); m_engine->jsonRpcClient()->sendCommand("Logging.GetLogEntries", params, this, "logsReply"); // qDebug() << "GetLogEntries called"; diff --git a/libnymea-app/models/xyseriesadapter.cpp b/libnymea-app/models/xyseriesadapter.cpp index 325df9d5..3727e472 100644 --- a/libnymea-app/models/xyseriesadapter.cpp +++ b/libnymea-app/models/xyseriesadapter.cpp @@ -46,12 +46,34 @@ void XYSeriesAdapter::setBaseSeries(QtCharts::QXYSeries *series) connect(m_baseSeries, &QtCharts::QXYSeries::pointAdded, this, [=](int index){ if (m_series->count() > index) { - m_series->replace(index, m_series->at(index).x(), calculateSampleValue(index)); + qreal value = calculateSampleValue(index); + m_series->replace(index, m_series->at(index).x(), value); + if (value < m_minValue) { + m_minValue = value; + qDebug() << "New min:" << m_minValue; + emit minValueChanged(); + } + if (value > m_maxValue) { + m_maxValue = value; + qDebug() << "New max:" << m_maxValue; + emit maxValueChanged(); + } } }); connect(m_baseSeries, &QtCharts::QXYSeries::pointReplaced, this, [=](int index){ if (m_series->count() > index) { - m_series->replace(index, m_series->at(index).x(), calculateSampleValue(index)); + qreal value = calculateSampleValue(index); + m_series->replace(index, m_series->at(index).x(), value); + if (value < m_minValue) { + m_minValue = value; + qDebug() << "New min:" << m_minValue; + emit minValueChanged(); + } + if (value > m_maxValue) { + m_maxValue = value; + qDebug() << "New max:" << m_maxValue; + emit maxValueChanged(); + } } }); } @@ -155,10 +177,12 @@ void XYSeriesAdapter::logEntryAdded(LogEntry *entry) if (value < m_minValue) { m_minValue = value; +// qDebug() << "New min:" << m_minValue; emit minValueChanged(); } if (value > m_maxValue) { m_maxValue = value; +// qDebug() << "New max:" << m_maxValue; emit maxValueChanged(); } } diff --git a/libnymea-app/types/interfaces.cpp b/libnymea-app/types/interfaces.cpp index 8ddd2fa8..83e95886 100644 --- a/libnymea-app/types/interfaces.cpp +++ b/libnymea-app/types/interfaces.cpp @@ -297,6 +297,8 @@ Interfaces::Interfaces(QObject *parent) : QAbstractListModel(parent) addInterface("wirelessconnectable", tr("Wireless devices"), {"connectable"}); addStateType("wirelessconnectable", "signalStrength", QVariant::UInt, false, tr("Signal strength"), tr("Signal strength changed")); + addInterface("watersensor", tr("Water sensors"), {"sensor"}); + addStateType("watersensor", "watterDetected", QVariant::Double, false, tr("Water detected"), tr("Water detected changed")); } int Interfaces::rowCount(const QModelIndex &parent) const diff --git a/nymea-app/images.qrc b/nymea-app/images.qrc index 9ee9ca9c..792e16ed 100644 --- a/nymea-app/images.qrc +++ b/nymea-app/images.qrc @@ -253,5 +253,6 @@ ui/images/media/ambeo.svg ui/images/thermostat/cooling.svg ui/images/thermostat/heating.svg + ui/images/sensors/water.svg diff --git a/nymea-app/ui/Nymea.qml b/nymea-app/ui/Nymea.qml index 11982c8e..41494ca3 100644 --- a/nymea-app/ui/Nymea.qml +++ b/nymea-app/ui/Nymea.qml @@ -291,6 +291,8 @@ ApplicationWindow { return Qt.resolvedUrl("images/sensors/closable.svg") case "windspeedsensor": return Qt.resolvedUrl("images/sensors/windspeed.svg") + case "watersensor": + return Qt.resolvedUrl("images/sensors/water.svg") case "media": case "mediacontroller": case "mediaplayer": @@ -390,6 +392,31 @@ ApplicationWindow { id: styleBase } + function stateColor(stateName) { + // Try to load color map from style + if (Style.stateColors[stateName]) { + return Style.stateColors[stateName]; + } + + if (styleBase.stateColors[stateName]) { + return styleBase.stateColors[stateName]; + } + console.warn("stateColor(): Color not set for state", stateName) + return "grey"; + } + + function stateIcon(stateName) { + var iconMap = { + "currentPower": "energy.svg", + "totalEnergyConsumed": "smartmeter.svg", + "totalEnergyProduced": "smartmeter.svg", + } + if (!iconMap[stateName]) { + console.warn("stateIcon(): Icon not set for state", stateName) + } + return Qt.resolvedUrl("images/" + iconMap[stateName]); + } + function interfaceToColor(name) { // Try to load color map from style if (Style.interfaceColors[name]) { diff --git a/nymea-app/ui/StyleBase.qml b/nymea-app/ui/StyleBase.qml index 30acdd64..ef40cfb4 100644 --- a/nymea-app/ui/StyleBase.qml +++ b/nymea-app/ui/StyleBase.qml @@ -36,12 +36,19 @@ Item { "closablesensor": "green", "smartmeterproducer": "lightgreen", "extendedsmartmeterproducer": "lightgreen", - "smartmeterconsumer": "orange", - "extendedsmartmeterconsumer": "orange", + "smartmeterconsumer": "deepskyblue", + "extendedsmartmeterconsumer": "deepskyblue", "heating" : "gainsboro", "thermostat": "dodgerblue", "irrigation": "lightblue", "windspeedsensor": "blue", - "ventilation": "lightblue" + "ventilation": "lightblue", + "watersensor": "aqua" + } + + property var stateColors: { + "totalEnergyConsumed": "orange", + "totalEnergyProduced": "lightgreen", + "currentPower": "deepskyblue", } } diff --git a/nymea-app/ui/components/SmartMeterChart.qml b/nymea-app/ui/components/SmartMeterChart.qml index 1d14fee1..49b46b74 100644 --- a/nymea-app/ui/components/SmartMeterChart.qml +++ b/nymea-app/ui/components/SmartMeterChart.qml @@ -37,7 +37,8 @@ import Nymea 1.0 ChartView { id: chart - backgroundColor: Style.backgroundColor + backgroundColor: Style.tileBackgroundColor + backgroundRoundness: Style.tileRadius theme: ChartView.ChartThemeLight legend.labelColor: Style.foregroundColor legend.font.pixelSize: app.smallFont diff --git a/nymea-app/ui/customviews/GenericTypeGraph.qml b/nymea-app/ui/customviews/GenericTypeGraph.qml index 2a729252..0d6240e4 100644 --- a/nymea-app/ui/customviews/GenericTypeGraph.qml +++ b/nymea-app/ui/customviews/GenericTypeGraph.qml @@ -44,13 +44,15 @@ Item { property Device device: null property StateType stateType: null property int roundTo: 2 + property color color: Style.accentColor + property string iconSource: "" + property alias title: titleLabel.text + readonly property var valueState: device.states.getState(stateType.id) readonly property var deviceClass: engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); readonly property bool hasConnectable: deviceClass.interfaces.indexOf("connectable") >= 0 readonly property var connectedStateType: hasConnectable ? deviceClass.stateTypes.findByName("connected") : null - property color color: Style.accentColor - property string iconSource: "" LogsModelNg { id: logsModelNg @@ -91,7 +93,7 @@ Item { animationOptions: ChartView.SeriesAnimations RowLayout { - anchors { left: parent.left; top: parent.top; right: parent.right; topMargin: app.margins / 2; leftMargin: app.margins; rightMargin: app.margins } + anchors { left: parent.left; top: parent.top; right: parent.right; topMargin: app.margins / 2; leftMargin: app.margins * 1.5; rightMargin: app.margins } ColorIcon { Layout.preferredHeight: app.iconSize @@ -101,6 +103,7 @@ Item { color: root.color } Label { + id: titleLabel Layout.fillWidth: true text: root.stateType.type.toLowerCase() === "bool" ? root.stateType.displayName diff --git a/nymea-app/ui/delegates/InterfaceTile.qml b/nymea-app/ui/delegates/InterfaceTile.qml index ee04f920..fbf478fe 100644 --- a/nymea-app/ui/delegates/InterfaceTile.qml +++ b/nymea-app/ui/delegates/InterfaceTile.qml @@ -627,6 +627,7 @@ MainPageTile { ListElement { ifaceName: "presencesensor"; stateName: "isPresent" } ListElement { ifaceName: "closablesensor"; stateName: "closed" } ListElement { ifaceName: "lightsensor"; stateName: "lightIntensity" } + ListElement { ifaceName: "watersensor"; stateName: "waterDetected" } ListElement { ifaceName: "co2sensor"; stateName: "co2" } ListElement { ifaceName: "conductivity"; stateName: "conductivity" } ListElement { ifaceName: "noisesensor"; stateName: "noise" } @@ -717,6 +718,5 @@ MainPageTile { } } } - } } diff --git a/nymea-app/ui/devicelistpages/SensorsDeviceListPage.qml b/nymea-app/ui/devicelistpages/SensorsDeviceListPage.qml index 6be739f9..f6443a81 100644 --- a/nymea-app/ui/devicelistpages/SensorsDeviceListPage.qml +++ b/nymea-app/ui/devicelistpages/SensorsDeviceListPage.qml @@ -85,6 +85,7 @@ DeviceListPageBase { ListElement { interfaceName: "closablesensor"; stateName: "closed" } ListElement { interfaceName: "heating"; stateName: "power" } ListElement { interfaceName: "thermostat"; stateName: "targetTemperature" } + ListElement { interfaceName: "watersensor"; stateName: "waterDetected" } } delegate: RowLayout { @@ -124,10 +125,12 @@ DeviceListPageBase { switch (model.interfaceName) { case "closablesensor": return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? qsTr("Closed") : qsTr("Open"); - case "preencesensor": + case "presencesensor": return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? qsTr("Presence") : qsTr("Vacant"); case "daylightsensor": return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? qsTr("Daytime") : qsTr("Nighttime"); + case "watersensor": + return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? qsTr("Wet") : qsTr("Dry"); case "heating": return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? qsTr("On") : qsTr("Off"); default: @@ -144,7 +147,7 @@ DeviceListPageBase { } Led { id: led - visible: sensorValueDelegate.stateType && sensorValueDelegate.stateType.type.toLowerCase() === "bool" && ["presencesensor", "daylightsensor", "heating", "closablesensor"].indexOf(model.interfaceName) < 0 + visible: sensorValueDelegate.stateType && sensorValueDelegate.stateType.type.toLowerCase() === "bool" && ["presencesensor", "daylightsensor", "heating", "closablesensor", "watersensor"].indexOf(model.interfaceName) < 0 state: visible && sensorValueDelegate.stateValue.value === true ? "on" : "off" } Item { diff --git a/nymea-app/ui/devicelistpages/SmartMeterDeviceListPage.qml b/nymea-app/ui/devicelistpages/SmartMeterDeviceListPage.qml index df5688af..cf0dcef3 100644 --- a/nymea-app/ui/devicelistpages/SmartMeterDeviceListPage.qml +++ b/nymea-app/ui/devicelistpages/SmartMeterDeviceListPage.qml @@ -103,8 +103,8 @@ DeviceListPageBase { Layout.preferredHeight: app.iconSize Layout.preferredWidth: height Layout.alignment: Qt.AlignVCenter - color: app.interfaceToColor(model.interfaceName) - name: app.interfaceToIcon(model.interfaceName) + color: app.stateColor(model.stateName) + name: app.stateIcon(model.stateName) } Label { diff --git a/nymea-app/ui/devicepages/SensorDevicePage.qml b/nymea-app/ui/devicepages/SensorDevicePage.qml index 224379a8..46998c90 100644 --- a/nymea-app/ui/devicepages/SensorDevicePage.qml +++ b/nymea-app/ui/devicepages/SensorDevicePage.qml @@ -41,18 +41,22 @@ DevicePageBase { Flickable { id: listView anchors { fill: parent } + topMargin: app.margins / 2 interactive: contentHeight > height contentHeight: contentGrid.implicitHeight GridLayout { id: contentGrid - width: parent.width + width: parent.width - app.margins + anchors.horizontalCenter: parent.horizontalCenter columns: Math.ceil(width / 600) + rowSpacing: 0 + columnSpacing: 0 Repeater { model: ListModel { Component.onCompleted: { - var supportedInterfaces = ["temperaturesensor", "humiditysensor", "pressuresensor", "moisturesensor", "lightsensor", "conductivitysensor", "noisesensor", "co2sensor", "presencesensor", "daylightsensor", "closablesensor"] + var supportedInterfaces = ["temperaturesensor", "humiditysensor", "pressuresensor", "moisturesensor", "lightsensor", "conductivitysensor", "noisesensor", "co2sensor", "presencesensor", "daylightsensor", "closablesensor", "watersensor"] for (var i = 0; i < supportedInterfaces.length; i++) { if (root.deviceClass.interfaces.indexOf(supportedInterfaces[i]) >= 0) { append({name: supportedInterfaces[i]}); @@ -67,6 +71,7 @@ DevicePageBase { Layout.preferredHeight: item.implicitHeight property StateType stateType: root.deviceClass.stateTypes.findByName(interfaceStateMap[modelData]) + property State state: root.thing.stateByName(interfaceStateMap[modelData]) property string interfaceName: modelData // sourceComponent: stateType && stateType.type.toLowerCase() === "bool" ? boolComponent : graphComponent @@ -83,7 +88,8 @@ DevicePageBase { "co2sensor": "co2", "presencesensor": "isPresent", "daylightsensor": "daylight", - "closablesensor": "closed" + "closablesensor": "closed", + "watersensor": "waterDetected" } } @@ -96,12 +102,32 @@ DevicePageBase { id: graphComponent GenericTypeGraph { + id: graph device: root.device color: app.interfaceToColor(interfaceName) iconSource: app.interfaceToIcon(interfaceName) implicitHeight: width * .6 property string interfaceName: parent.interfaceName stateType: parent.stateType + property State state: parent.state + + Binding { + target: graph + property: "title" + when: ["presencesensor", "daylightsensor", "closablesensor", "watersensor"].indexOf(graph.interfaceName) >= 0 + value: { + switch (graph.interfaceName) { + case "presencesensor": + return graph.state.value === true ? qsTr("Presence") : qsTr("Vacant") + case "daylightsensor": + return graph.state.value === true ? qsTr("Daytimet") : qsTr("Nighttime") + case "closablesensor": + return graph.state.value === true ? qsTr("Closed") : qsTr("Open") + case "watersensor": + return graph.state.value === true ? qsTr("Wet") : qsTr("Dry") + } + } + } } } diff --git a/nymea-app/ui/devicepages/SmartMeterDevicePage.qml b/nymea-app/ui/devicepages/SmartMeterDevicePage.qml index 08924e43..73f76fb3 100644 --- a/nymea-app/ui/devicepages/SmartMeterDevicePage.qml +++ b/nymea-app/ui/devicepages/SmartMeterDevicePage.qml @@ -45,6 +45,7 @@ DevicePageBase { Flickable { anchors.fill: parent + topMargin: app.margins / 2 contentHeight: contentGrid.height interactive: contentHeight > height @@ -56,13 +57,17 @@ DevicePageBase { BigTile { Layout.preferredWidth: contentGrid.width / contentGrid.columns - showHeader: false + showHeader: true + header: Label { + text: qsTr("Total energy consumption") + } + contentItem: RowLayout { ColorIcon { Layout.preferredHeight: app.iconSize Layout.preferredWidth: app.iconSize - name: app.interfaceToIcon("smartmeterconsumer") - color: app.interfaceToColor("smartmeterconsumer") + name: app.stateIcon("totalEnergyConsumed") + color: app.stateColor("totalEnergyConsumed") } Label { @@ -73,14 +78,16 @@ DevicePageBase { ColorIcon { Layout.preferredHeight: app.iconSize Layout.preferredWidth: app.iconSize - name: app.interfaceToIcon("smartmeterproducer") - color: app.interfaceToColor("smartmeterproducer") + 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 } } } @@ -89,8 +96,8 @@ DevicePageBase { Layout.preferredWidth: contentGrid.width / contentGrid.columns device: root.device stateType: root.deviceClass.stateTypes.findByName("currentPower") - color: app.interfaceToColor("smartmeterconsumer") - iconSource: app.interfaceToIcon("smartmeterconsumer") + color: app.stateColor("currentPower") + iconSource: app.stateIcon("currentPower") roundTo: 5 } } diff --git a/nymea-app/ui/images/sensors/water.svg b/nymea-app/ui/images/sensors/water.svg new file mode 100644 index 00000000..fee3e80a --- /dev/null +++ b/nymea-app/ui/images/sensors/water.svg @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/mainviews/EnergyView.qml b/nymea-app/ui/mainviews/EnergyView.qml index f6d34874..168d40a7 100644 --- a/nymea-app/ui/mainviews/EnergyView.qml +++ b/nymea-app/ui/mainviews/EnergyView.qml @@ -79,7 +79,8 @@ MainViewBase { legend.alignment: Qt.AlignBottom legend.font.pixelSize: app.smallFont legend.visible: false - backgroundColor: Style.backgroundColor + backgroundColor: Style.tileBackgroundColor + backgroundRoundness: Style.tileRadius titleColor: Style.foregroundColor title: qsTr("Power usage history") @@ -150,7 +151,7 @@ MainViewBase { } Component.onCompleted: { - print("creating series") + print("creating series", consumer.thing.name, index) seriesAdapter.ensureSamples(xAxis.min, xAxis.max) var areaSeries = chartView.createSeries(ChartView.SeriesTypeArea, consumer.thing.name, xAxis, yAxis) areaSeries.upperSeries = upperSeries; @@ -179,8 +180,8 @@ MainViewBase { ValueAxis { id: yAxis readonly property XYSeriesAdapter adapter: consumersRepeater.itemAt(consumersRepeater.count - 1).adapter; - max: Math.ceil(adapter.maxValue + Math.abs(adapter.maxValue * .05)) - min: Math.floor(adapter.minValue - Math.abs(adapter.minValue * .05)) + max: Math.ceil(Math.max(adapter.maxValue * 0.95, adapter.maxValue * 1.05)) + min: Math.floor(Math.min(adapter.minValue * 0.95, adapter.minValue * 1.05)) // This seems to crash occationally // onMinChanged: applyNiceNumbers(); // onMaxChanged: applyNiceNumbers(); @@ -354,6 +355,8 @@ MainViewBase { SmartMeterChart { Layout.fillWidth: true Layout.preferredHeight: width * .7 + backgroundColor: Style.tileBackgroundColor + backgroundRoundness: Style.tileRadius meters: producers title: qsTr("Total produced energy") visible: producers.count > 0