diff --git a/libnymea-app/models/logsmodel.cpp b/libnymea-app/models/logsmodel.cpp
index 9d301cda..5ac3fd6e 100644
--- a/libnymea-app/models/logsmodel.cpp
+++ b/libnymea-app/models/logsmodel.cpp
@@ -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) {
diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc
index fd26f6ce..444ec17b 100644
--- a/nymea-app/resources.qrc
+++ b/nymea-app/resources.qrc
@@ -257,5 +257,6 @@
ui/components/CircleBackground.qml
ui/devicepages/CoolingThingPage.qml
ui/devicepages/EvChargerThingPage.qml
+ ui/components/BlurredLabel.qml
diff --git a/nymea-app/ui/StyleBase.qml b/nymea-app/ui/StyleBase.qml
index d6e75302..fd8792c3 100644
--- a/nymea-app/ui/StyleBase.qml
+++ b/nymea-app/ui/StyleBase.qml
@@ -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
}
diff --git a/nymea-app/ui/components/BlurredLabel.qml b/nymea-app/ui/components/BlurredLabel.qml
new file mode 100644
index 00000000..1bda1a5c
--- /dev/null
+++ b/nymea-app/ui/components/BlurredLabel.qml
@@ -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 } }
+ }
+}
diff --git a/nymea-app/ui/components/CircleBackground.qml b/nymea-app/ui/components/CircleBackground.qml
index 8731e98b..8a92e217 100644
--- a/nymea-app/ui/components/CircleBackground.qml
+++ b/nymea-app/ui/components/CircleBackground.qml
@@ -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 } }
}
diff --git a/nymea-app/ui/customviews/GenericTypeGraph.qml b/nymea-app/ui/customviews/GenericTypeGraph.qml
index 2a095495..6e8c8f61 100644
--- a/nymea-app/ui/customviews/GenericTypeGraph.qml
+++ b/nymea-app/ui/customviews/GenericTypeGraph.qml
@@ -40,6 +40,7 @@ import QtCharts 2.2
Item {
id: root
implicitHeight: width * .6
+ implicitWidth: 400
property Thing thing: null
property StateType stateType: null
diff --git a/nymea-app/ui/devicelistpages/SmartMeterDeviceListPage.qml b/nymea-app/ui/devicelistpages/SmartMeterDeviceListPage.qml
index 018591d9..698fb7ab 100644
--- a/nymea-app/ui/devicelistpages/SmartMeterDeviceListPage.qml
+++ b/nymea-app/ui/devicelistpages/SmartMeterDeviceListPage.qml
@@ -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
}
}
}
diff --git a/nymea-app/ui/devicelistpages/ThingsListPageBase.qml b/nymea-app/ui/devicelistpages/ThingsListPageBase.qml
index d2c608b3..badddc15 100644
--- a/nymea-app/ui/devicelistpages/ThingsListPageBase.qml
+++ b/nymea-app/ui/devicelistpages/ThingsListPageBase.qml
@@ -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)})
}
diff --git a/nymea-app/ui/devicepages/DeviceLogPage.qml b/nymea-app/ui/devicepages/DeviceLogPage.qml
index 515bd77a..39b3cd66 100644
--- a/nymea-app/ui/devicepages/DeviceLogPage.qml
+++ b/nymea-app/ui/devicepages/DeviceLogPage.qml
@@ -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 {
diff --git a/nymea-app/ui/devicepages/SmartMeterDevicePage.qml b/nymea-app/ui/devicepages/SmartMeterDevicePage.qml
index 5eee9825..cd492682 100644
--- a/nymea-app/ui/devicepages/SmartMeterDevicePage.qml
+++ b/nymea-app/ui/devicepages/SmartMeterDevicePage.qml
@@ -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('' + endTime.toLocaleTimeString(Locale.ShortFormat) + "")
+ : isDischarging ? qsTr("At the current rate, the battery will last until %1.").arg('' + endTime.toLocaleTimeString(Locale.ShortFormat) + "")
+ : 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 consumed in the last 24 hours.").arg('' + (totalPeriodConsumption).toFixed(1) + '')
+ : qsTr("A total of %1 kWh has been obtained in the last 24 hours.").arg('' + (totalPeriodConsumption).toFixed(1) + '')
+ 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 produced in the last 24 hours.").arg('' + (totalPeriodProduction).toFixed(1) + '')
+ : qsTr("A total of %1 kWh has been returned in the last 24 hours.").arg('' + (totalPeriodProduction).toFixed(1) + '')
+ 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
+ }
}
}
}
}
+