From 4f6f1ffbdcd52a68c021feaec24e7f7d15b81463 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Fri, 22 Oct 2021 18:00:21 +0200 Subject: [PATCH] Improve sensor views --- libnymea-app/types/interfaces.cpp | 5 +- nymea-app/images.qrc | 1 + nymea-app/ui/MainPage.qml | 3 +- nymea-app/ui/Nymea.qml | 5 +- nymea-app/ui/StyleBase.qml | 1 + nymea-app/ui/components/CircleBackground.qml | 6 +- .../ui/customviews/ThermostatController.qml | 8 +- nymea-app/ui/delegates/InterfaceTile.qml | 1 + nymea-app/ui/delegates/ThingTile.qml | 3 + .../devicelistpages/SensorsDeviceListPage.qml | 7 +- nymea-app/ui/devicepages/SensorDevicePage.qml | 254 ++++++++++++++---- nymea-app/ui/images/sensors/fire.svg | 193 +++++++++++++ 12 files changed, 431 insertions(+), 56 deletions(-) create mode 100644 nymea-app/ui/images/sensors/fire.svg diff --git a/libnymea-app/types/interfaces.cpp b/libnymea-app/types/interfaces.cpp index 9b287486..a9efdec5 100644 --- a/libnymea-app/types/interfaces.cpp +++ b/libnymea-app/types/interfaces.cpp @@ -316,7 +316,10 @@ Interfaces::Interfaces(QObject *parent) : QAbstractListModel(parent) 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")); + addStateType("watersensor", "waterDetected", QVariant::Double, false, tr("Water detected"), tr("Water detected changed")); + + addInterface("firesensor", tr("Fire sensors"), {"sensor"}); + addStateType("firesensor", "fireDetected", QVariant::Double, false, tr("Fire detected"), tr("Fire detected changed")); } int Interfaces::rowCount(const QModelIndex &parent) const diff --git a/nymea-app/images.qrc b/nymea-app/images.qrc index 83ffcd02..56d4b97f 100644 --- a/nymea-app/images.qrc +++ b/nymea-app/images.qrc @@ -273,5 +273,6 @@ ui/images/contact-group.svg ui/images/zigbee/TI.svg ui/images/car.svg + ui/images/sensors/fire.svg diff --git a/nymea-app/ui/MainPage.qml b/nymea-app/ui/MainPage.qml index 33ed2d8c..acd17b56 100644 --- a/nymea-app/ui/MainPage.qml +++ b/nymea-app/ui/MainPage.qml @@ -91,6 +91,7 @@ Page { width: visible ? implicitWidth : 0 HeaderButton { + id: button imageSource: "../images/system-update.svg" color: Style.accentColor visible: updatesModel.count > 0 || engine.systemController.updateRunning @@ -101,7 +102,7 @@ Page { duration: 2000 loops: Animation.Infinite running: engine.systemController.updateRunning - onStopped: icon.rotation = 0; + onStopped: button.rotation = 0; } PackagesFilterModel { id: updatesModel diff --git a/nymea-app/ui/Nymea.qml b/nymea-app/ui/Nymea.qml index 01cb1805..af7eadec 100644 --- a/nymea-app/ui/Nymea.qml +++ b/nymea-app/ui/Nymea.qml @@ -311,6 +311,8 @@ ApplicationWindow { return Qt.resolvedUrl("images/sensors/water.svg") case "waterlevelsensor": return Qt.resolvedUrl("images/sensors/water.svg") + case "firesensor": + return Qt.resolvedUrl("images/sensors/fire.svg") case "o2sensor": return Qt.resolvedUrl("images/sensors/o2.svg") case "phsensor": @@ -450,14 +452,11 @@ ApplicationWindow { function interfaceToColor(name) { // Try to load color map from style - print("checking color for name", name) if (Style.interfaceColors[name]) { - print("have color for name", name, Style.interfaceColors[name]) return Style.interfaceColors[name]; } if (styleBase.interfaceColors[name]) { - print("have color for name", name, styleBase.interfaceColors[name]) return styleBase.interfaceColors[name]; } diff --git a/nymea-app/ui/StyleBase.qml b/nymea-app/ui/StyleBase.qml index fb4c9271..a425bffa 100644 --- a/nymea-app/ui/StyleBase.qml +++ b/nymea-app/ui/StyleBase.qml @@ -111,6 +111,7 @@ Item { "ventilation": lightBlue, "watersensor": lightBlue, "waterlevelsensor": lightBlue, + "firesensor": red, "phsensor": green, "o2sensor": lightBlue, "orpsensor": yellow, diff --git a/nymea-app/ui/components/CircleBackground.qml b/nymea-app/ui/components/CircleBackground.qml index b5a467f1..2722800d 100644 --- a/nymea-app/ui/components/CircleBackground.qml +++ b/nymea-app/ui/components/CircleBackground.qml @@ -44,6 +44,11 @@ Item { property bool on: false property alias showOnGradient: opacityMask.visible + property bool showProgress: false + property double progressFrom: 0 + property double progressTo: 100 + property double progress: 50 + readonly property Item contentItem: background signal clicked() @@ -95,7 +100,6 @@ Item { source: gradient maskSource: mask Behavior on opacity { NumberAnimation { duration: Style.animationDuration } } - } Item { diff --git a/nymea-app/ui/customviews/ThermostatController.qml b/nymea-app/ui/customviews/ThermostatController.qml index 5aa6dbdc..15b34f2e 100644 --- a/nymea-app/ui/customviews/ThermostatController.qml +++ b/nymea-app/ui/customviews/ThermostatController.qml @@ -134,22 +134,22 @@ Item { } ctx.beginPath(); - ctx.font = "" + app.hugeFont + "px " + Style.fontFamily; + ctx.font = "" + Style.hugeFont.pixelSize + "px " + Style.fontFamily; ctx.fillStyle = Style.foregroundColor; var roundedTargetTemp = Types.toUiValue(currentValue, root.targetTemperatureStateType.unit) roundedTargetTemp = roundToPrecision(roundedTargetTemp).toFixed(1) + "°" var size = ctx.measureText(roundedTargetTemp) - ctx.text(roundedTargetTemp, center.x - size.width / 2, center.y + app.hugeFont / 2); + ctx.text(roundedTargetTemp, center.x - size.width / 2, center.y + Style.hugeFont.pixelSize / 2); ctx.fill(); ctx.closePath(); if (root.temperatureState) { ctx.beginPath(); - ctx.font = "" + app.largeFont + "px " + Style.fontFamily; + ctx.font = "" + Style.bigFont.pixelSize + "px " + Style.fontFamily; var roundedTemp = Types.toUiValue(root.temperatureState.value, root.temperatureStateType.unit) roundedTemp = roundToPrecision(roundedTemp) + "°" size = ctx.measureText(roundedTemp) - ctx.text(roundedTemp, center.x - size.width / 2, center.y + app.hugeFont + app.margins); + ctx.text(roundedTemp, center.x - size.width / 2, center.y + Style.hugeFont.pixelSize + Style.margins); ctx.fill(); ctx.closePath(); } diff --git a/nymea-app/ui/delegates/InterfaceTile.qml b/nymea-app/ui/delegates/InterfaceTile.qml index 1982f4ec..3a988f31 100644 --- a/nymea-app/ui/delegates/InterfaceTile.qml +++ b/nymea-app/ui/delegates/InterfaceTile.qml @@ -318,6 +318,7 @@ MainPageTile { ListElement { ifaceName: "lightsensor"; stateName: "lightIntensity" } ListElement { ifaceName: "watersensor"; stateName: "waterDetected" } ListElement { ifaceName: "waterlevelsensor"; stateName: "waterLevel" } + ListElement { ifaceName: "firesensor"; stateName: "fireDetected" } ListElement { ifaceName: "cosensor"; stateName: "co" } ListElement { ifaceName: "co2sensor"; stateName: "co2" } ListElement { ifaceName: "gassensor"; stateName: "gas" } diff --git a/nymea-app/ui/delegates/ThingTile.qml b/nymea-app/ui/delegates/ThingTile.qml index 0d759e2d..22e806fd 100644 --- a/nymea-app/ui/delegates/ThingTile.qml +++ b/nymea-app/ui/delegates/ThingTile.qml @@ -274,6 +274,9 @@ MainPageTile { if (thing.thingClass.interfaces.indexOf("waterlevelsensor") >= 0) { tmp.push({iface: "waterlevelsensor", state: "waterLevel"}) } + if (thing.thingClass.interfaces.indexOf("firesensor") >= 0) { + tmp.push({iface: "firesensor", state: "fireDetected"}) + } if (thing.thingClass.interfaces.indexOf("weather") >= 0) { tmp.push({iface: "temperaturesensor", state: "temperature"}); diff --git a/nymea-app/ui/devicelistpages/SensorsDeviceListPage.qml b/nymea-app/ui/devicelistpages/SensorsDeviceListPage.qml index 42a11d24..8109035c 100644 --- a/nymea-app/ui/devicelistpages/SensorsDeviceListPage.qml +++ b/nymea-app/ui/devicelistpages/SensorsDeviceListPage.qml @@ -90,6 +90,7 @@ ThingsListPageBase { ListElement { interfaceName: "thermostat"; stateName: "targetTemperature" } ListElement { interfaceName: "watersensor"; stateName: "waterDetected" } ListElement { interfaceName: "waterlevelsensor"; stateName: "waterLevel" } + ListElement { interfaceName: "firesensor"; stateName: "fireDetected" } ListElement { interfaceName: "o2sensor"; stateName: "o2saturation" } ListElement { interfaceName: "phsensor"; stateName: "ph" } ListElement { interfaceName: "orpsensor"; stateName: "orp" } @@ -111,6 +112,8 @@ ThingsListPageBase { switch (model.interfaceName) { case "closablesensor": return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? Style.green : Style.red; + case "firesensor": + return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? Style.red : Style.iconColor; default: return app.interfaceToColor(model.interfaceName) } @@ -138,6 +141,8 @@ ThingsListPageBase { 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 "firesensor": + return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? qsTr("Fire") : qsTr("No fire"); case "heating": return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? qsTr("On") : qsTr("Off"); default: @@ -154,7 +159,7 @@ ThingsListPageBase { } Led { id: led - visible: sensorValueDelegate.stateType && sensorValueDelegate.stateType.type.toLowerCase() === "bool" && ["presencesensor", "daylightsensor", "heating", "closablesensor", "watersensor"].indexOf(model.interfaceName) < 0 + visible: sensorValueDelegate.stateType && sensorValueDelegate.stateType.type.toLowerCase() === "bool" && ["presencesensor", "daylightsensor", "heating", "closablesensor", "watersensor", "firesensor"].indexOf(model.interfaceName) < 0 state: visible && sensorValueDelegate.stateValue.value === true ? "on" : "off" } Item { diff --git a/nymea-app/ui/devicepages/SensorDevicePage.qml b/nymea-app/ui/devicepages/SensorDevicePage.qml index a6a0858e..84b8672a 100644 --- a/nymea-app/ui/devicepages/SensorDevicePage.qml +++ b/nymea-app/ui/devicepages/SensorDevicePage.qml @@ -38,6 +38,42 @@ import "../customviews" ThingPageBase { id: root + property var interfaceStateMap: { + "temperaturesensor": "temperature", + "humiditysensor": "humidity", + "pressuresensor": "pressure", + "moisturesensor": "moisture", + "lightsensor": "lightIntensity", + "conductivitysensor": "conductivity", + "noisesensor": "noise", + "cosensor": "co", + "co2sensor": "co2", + "gassensor": "gas", + "presencesensor": "isPresent", + "daylightsensor": "daylight", + "closablesensor": "closed", + "watersensor": "waterDetected", + "firesensor": "fireDetected", + "waterlevelsensor": "waterLevel", + "phsensor": "ph", + "o2sensor": "o2saturation", + "orpsensor": "orp", + "airquality": "airQuality", + "indoorairquality": "indoorAirQuality" + } + + ListModel { + id: sensorsModel + Component.onCompleted: { + var supportedInterfaces = Object.keys(interfaceStateMap) + for (var i = 0; i < supportedInterfaces.length; i++) { + if (root.thingClass.interfaces.indexOf(supportedInterfaces[i]) >= 0) { + append({name: supportedInterfaces[i]}); + } + } + } + } + Flickable { id: listView anchors { fill: parent } @@ -45,65 +81,129 @@ ThingPageBase { interactive: contentHeight > height contentHeight: contentGrid.implicitHeight - GridLayout { + ColumnLayout { id: contentGrid - width: parent.width - app.margins - anchors.horizontalCenter: parent.horizontalCenter - columns: Math.ceil(width / 600) - rowSpacing: 0 - columnSpacing: 0 + width: parent.width + spacing: Style.margins - Repeater { - model: ListModel { - Component.onCompleted: { - var supportedInterfaces = ["temperaturesensor", "humiditysensor", "pressuresensor", "moisturesensor", "lightsensor", "conductivitysensor", "noisesensor", "cosensor", "co2sensor", "gassensor", "presencesensor", "daylightsensor", "closablesensor", "watersensor", "phsensor", "o2sensor", "orpsensor", "waterlevelsensor"] - for (var i = 0; i < supportedInterfaces.length; i++) { - if (root.thingClass.interfaces.indexOf(supportedInterfaces[i]) >= 0) { - append({name: supportedInterfaces[i]}); + Flow { + id: flow + Layout.fillWidth: true + Layout.leftMargin: sensorsModel.count == 1 ? Style.hugeMargins : Style.bigMargins + Layout.rightMargin: sensorsModel.count == 1 ? Style.hugeMargins : Style.bigMargins + Layout.preferredHeight: cellWidth * Math.min(400, Math.ceil(flowRepeater.count / columns)) + + property int columns: Math.min(flowRepeater.count, Math.floor(listView.width / 150)) + property int cellWidth: width / columns + property int totalRows: flowRepeater.count / columns + +// columns: 2// Math.ceil(width / 600) +// columnSpacing: Style.margins +// rowSpacing: Style.margins + + Repeater { + id: flowRepeater + model: sensorsModel + + delegate: Item { + width: Math.floor(flow.width / itemsInRow) + height: Math.min(400, flow.cellWidth) + property int row: Math.floor(index / flow.columns) + property int itemsInRow: row < flow.totalRows ? flow.columns : (flowRepeater.count % flow.columns) + + CircleBackground { + id: background + anchors.centerIn: parent + width: Math.min(parent.width, parent.height) - Style.margins + height: width + + readonly property StateType sensorStateType: root.thing.thingClass.stateTypes.findByName(interfaceStateMap[modelData]) + readonly property State sensorState: root.thing.stateByName(interfaceStateMap[modelData]) + + onColor: app.interfaceToColor(modelData) + on: sensorStateType.type.toLowerCase() == "bool" && sensorState.value === true + iconSource: [ + "closablesensor", + "presencesensor", + "daylightsensor", + "firesensor", + "watersensor" + ].indexOf(modelData) >= 0 ? app.interfaceToIcon(modelData) : "" + + Loader { + anchors.centerIn: parent + width: background.contentItem.width + height: background.contentItem.height + property StateType stateType: root.thingClass.stateTypes.findByName(interfaceStateMap[modelData]) + property State state: root.thing.stateByName(interfaceStateMap[modelData]) + property string interfaceName: modelData + property var minValue: { + if (["temperaturesensor"].indexOf(modelData) >= 0) { + return Types.toUiValue(-50, Types.UnitDegreeCelsius) + } + return state.minValue + } + property var maxValue: { + if (["temperaturesensor"].indexOf(modelData) >= 0) { + return Types.toUiValue(50, Types.UnitDegreeCelsius) + } + return state.maxValue + } + + sourceComponent: { + var handledInterfaces = [ + "humiditysensor", + "o2sensor", + "temperaturesensor", + "moisturesensor", + "co2sensor", + "conductivitysensor", + "cosensor", + "co2sensor", + "gassensor", + "lightsensor", + "orpsensor", + "phsensor", + "pressuresensor", + "waterlevelsensor", + "windspeedsensor" + ] + if (handledInterfaces.indexOf(modelData) >= 0) { + return progressComponent + } + } } } } } + } - delegate: Loader { - id: loader - Layout.fillWidth: true - Layout.preferredHeight: item.implicitHeight - property StateType stateType: root.thingClass.stateTypes.findByName(interfaceStateMap[modelData]) - property State state: root.thing.stateByName(interfaceStateMap[modelData]) - property string interfaceName: modelData + GridLayout { + columns: Math.ceil(width / 600) + rowSpacing: 0 + columnSpacing: 0 - // sourceComponent: stateType && stateType.type.toLowerCase() === "bool" ? boolComponent : graphComponent - sourceComponent: graphComponent + Repeater { + model: sensorsModel + + delegate: Loader { + id: loader + Layout.fillWidth: true + Layout.preferredHeight: item.implicitHeight + + property StateType stateType: root.thingClass.stateTypes.findByName(interfaceStateMap[modelData]) + property State state: root.thing.stateByName(interfaceStateMap[modelData]) + property string interfaceName: modelData + + // sourceComponent: stateType && stateType.type.toLowerCase() === "bool" ? boolComponent : graphComponent + sourceComponent: graphComponent - property var interfaceStateMap: { - "temperaturesensor": "temperature", - "humiditysensor": "humidity", - "pressuresensor": "pressure", - "moisturesensor": "moisture", - "lightsensor": "lightIntensity", - "conductivitysensor": "conductivity", - "noisesensor": "noise", - "cosensor": "co", - "co2sensor": "co2", - "gassensor": "gas", - "presencesensor": "isPresent", - "daylightsensor": "daylight", - "closablesensor": "closed", - "watersensor": "waterDetected", - "waterlevelsensor": "waterLevel", - "phsensor": "ph", - "o2sensor": "o2saturation", - "orpsensor": "orp" } } - } } - - Component { id: graphComponent @@ -120,7 +220,7 @@ ThingPageBase { Binding { target: graph property: "title" - when: ["presencesensor", "daylightsensor", "closablesensor", "watersensor"].indexOf(graph.interfaceName) >= 0 + when: ["presencesensor", "daylightsensor", "closablesensor", "watersensor", "firesensor"].indexOf(graph.interfaceName) >= 0 value: { switch (graph.interfaceName) { case "presencesensor": @@ -131,6 +231,8 @@ ThingPageBase { return graph.state.value === true ? qsTr("Closed") : qsTr("Open") case "watersensor": return graph.state.value === true ? qsTr("Wet") : qsTr("Dry") + case "firesensor": + return graph.state.value === true ? qsTr("Fire") : qsTr("No fire") } } } @@ -225,5 +327,67 @@ ThingPageBase { } } } + + Component { + id: progressComponent + Canvas { + id: progressCanvas + property string interfaceName: parent.interfaceName + property StateType stateType: parent.stateType + property State state: parent.state + property var minValue: parent.minValue + property var maxValue: parent.maxValue + + Label { + anchors.centerIn: parent + width: parent.width * 0.6 + text: Types.toUiValue(progressCanvas.state.value, progressCanvas.stateType.unit).toFixed(1) + " " + Types.toUiUnit(progressCanvas.stateType.unit) + font.pixelSize: Math.min(Style.hugeFont.pixelSize, parent.height / 6) + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + onPaint: { + var ctx = getContext("2d"); + ctx.reset(); + + ctx.beginPath() + ctx.fillStyle = Style.foregroundColor + + ctx.translate(width / 2, height / 2) + ctx.rotate(135 * Math.PI / 180) + + ctx.lineCap = "round" + ctx.lineWidth = width * .1 + + ctx.beginPath() + ctx.strokeStyle = Style.tileOverlayColor + var startAngle = 0 + var endAngle = 270 + var radStart = startAngle * Math.PI/180; + var radEnd = endAngle * Math.PI/180; + ctx.arc(0, 0, width / 2 - ctx.lineWidth / 2, radStart, radEnd) + ctx.stroke() + ctx.closePath() + + ctx.beginPath() + ctx.strokeStyle = app.interfaceToColor(progressCanvas.interfaceName) + var progress = (progressCanvas.state.value - progressCanvas.minValue) / (progressCanvas.maxValue - progressCanvas.minValue) + radEnd *= progress + ctx.arc(0, 0, width / 2 - ctx.lineWidth / 2, radStart, radEnd) + ctx.stroke() + ctx.closePath() + } + + ColorIcon { + anchors.centerIn: parent + anchors.verticalCenterOffset: progressCanvas.height / 2 - height + name: app.interfaceToIcon(progressCanvas.interfaceName) + size: Math.min(Style.bigIconSize, parent.height / 5) + color: app.interfaceToColor(progressCanvas.interfaceName) + } + } + } + } diff --git a/nymea-app/ui/images/sensors/fire.svg b/nymea-app/ui/images/sensors/fire.svg new file mode 100644 index 00000000..bff79b13 --- /dev/null +++ b/nymea-app/ui/images/sensors/fire.svg @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + +