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 + } } } } } +