From a3efbe110c2713d51a8529d95511a07e0b45b18f Mon Sep 17 00:00:00 2001 From: Martin Lukas Date: Tue, 10 Sep 2024 06:31:29 +0200 Subject: [PATCH] Introduce overview for states and events of things Signed-off-by: Martin Lukas --- nymea-app/resources.qrc | 1 + nymea-app/ui/components/ThingInfoPane.qml | 2 +- .../ui/devicepages/DeviceDetailsPage.qml | 238 ++++++++++++++++++ .../thingconfiguration/ConfigureThingPage.qml | 6 + 4 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 nymea-app/ui/devicepages/DeviceDetailsPage.qml diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 3667d88c..a31fc172 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -318,5 +318,6 @@ ui/mainviews/dashboard/DashboardSensorDelegate.qml ui/devicepages/ThingStatusPage.qml ui/customviews/MultiStateChart.qml + ui/devicepages/DeviceDetailsPage.qml diff --git a/nymea-app/ui/components/ThingInfoPane.qml b/nymea-app/ui/components/ThingInfoPane.qml index 725a95c9..58126561 100644 --- a/nymea-app/ui/components/ThingInfoPane.qml +++ b/nymea-app/ui/components/ThingInfoPane.qml @@ -13,7 +13,7 @@ InfoPaneBase { readonly property bool setupFailure: root.thing.setupStatus == Thing.ThingSetupStatusFailed readonly property State batteryState: root.thing.stateByName("batteryLevel") readonly property State batteryCriticalState: root.thing.stateByName("batteryCritical") - readonly property State connectedState: root.thing.thingClass.interfaces.indexOf("connectable") >= 0 && root.thing.stateByName("connected") + readonly property bool connectedState: root.thing.thingClass.interfaces.indexOf("connectable") >= 0 && root.thing.stateByName("connected") readonly property State signalStrengthState: root.thing.stateByName("signalStrength") readonly property State updateStatusState: root.thing.stateByName("updateStatus") readonly property State childLockState: root.thing.stateByName("childLock") diff --git a/nymea-app/ui/devicepages/DeviceDetailsPage.qml b/nymea-app/ui/devicepages/DeviceDetailsPage.qml new file mode 100644 index 00000000..d3ad2d9f --- /dev/null +++ b/nymea-app/ui/devicepages/DeviceDetailsPage.qml @@ -0,0 +1,238 @@ +import QtQuick 2.5 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import Nymea 1.0 +import "../components" +import "../delegates" + +ThingPageBase { + id: root + + ListView { + id: flickable + anchors.fill: parent + clip: true + + SwipeDelegateGroup {} + + section.property: "type" + section.delegate: ListSectionHeader { + text: { + switch (parseInt(section)) { + case ThingModel.TypeStateType: + return qsTr("States") + case ThingModel.TypeEventType: + return qsTr("Events") + } + } + } + + model: ThingModel { + thing: root.thing + } + delegate: SwipeDelegate { + id: delegate + width: parent.width + + readonly property StateType stateType: model.type === ThingModel.TypeStateType ? root.thing.thingClass.stateTypes.getStateType(model.id) : null + readonly property EventType eventType: model.type === ThingModel.TypeEventType ? root.thing.thingClass.eventTypes.getEventType(model.id) : null + + Layout.fillWidth: true + bottomPadding: 0 + + contentItem: Loader { + id: inlineLoader + Layout.fillWidth: true + Layout.preferredHeight: Style.smallDelegateHeight + sourceComponent: { + switch (model.type) { + case ThingModel.TypeStateType: + return stateComponent; + case ThingModel.TypeEventType: + return eventComponent; + } + } + + Binding { + target: inlineLoader.item + when: model.type === ThingModel.TypeStateType + property: "stateType" + value: delegate.stateType + } + Binding { + target: inlineLoader.item + when: model.type === ThingModel.TypeEventType + property: "eventType" + value: delegate.eventType + } + } + } + } + + Component { + id: stateComponent + RowLayout { + id: stateDelegate + property StateType stateType: null + readonly property State thingState: stateType ? root.thing.states.getState(stateType.id) : null + + Label { + Layout.fillWidth: true + Layout.minimumWidth: parent.width / 2 + text: stateDelegate.stateType.displayName + elide: Text.ElideRight + } + Loader { + id: stateDelegateLoader + Layout.fillWidth: true + } + Label { + visible: text.length > 0 && stateDelegate.stateType.unit !== Types.UnitUnixTime + text: Types.toUiUnit(stateDelegate.stateType.unit) + } + + Component.onCompleted: updateLoader() + onStateTypeChanged: updateLoader(); + + function updateLoader() { + if (stateDelegate.stateType == null) { + return; + } + + var sourceComp; + switch (stateDelegate.stateType.type.toLowerCase()) { + case "string": + sourceComp = "LabelDelegate.qml"; + break; + case "stringlist": + sourceComp = "ListDelegate.qml"; + break; + case "bool": + sourceComp = "LedDelegate.qml"; + break; + case "int": + case "uint": + case "double": + if (stateDelegate.stateType.unit === Types.UnitUnixTime) { + sourceComp = "DateTimeDelegate.qml"; + } else { + sourceComp = "NumberLabelDelegate.qml"; + } + break; + case "color": + sourceComp = "ColorDelegate.qml"; + break; + } + if (!sourceComp) { + sourceComp = "LabelDelegate.qml"; + print("GenericThingPage: unhandled entry", stateDelegate.stateType.displayName) + } + + var minValue = stateDelegate.stateType.minValue !== undefined + ? stateDelegate.stateType.minValue + : stateDelegate.stateType.type.toLowerCase() === "uint" + ? 0 + : -2000000000; // As per QML spec + var maxValue = stateDelegate.stateType.maxValue !== undefined + ? stateDelegate.stateType.maxValue + : 2000000000; + print("pushing delegate for", stateDelegate.stateType.name, "from:", minValue, "to:", maxValue, "possible:", stateDelegate.stateType.possibleValuesDisplayNames) + stateDelegateLoader.setSource("../delegates/statedelegates/" + sourceComp, + { + value: root.thing.states.getState(stateType.id).value, + possibleValues: stateDelegate.stateType.possibleValues, + possibleValuesDisplayNames: stateDelegate.stateType.possibleValuesDisplayNames, + from: minValue, + to: maxValue, + unit: stateDelegate.stateType.unit, + writable: false, + stateType: stateDelegate.stateType + }) + + } + + Binding { + target: stateDelegateLoader.item + property: "value" + value: stateDelegate.thingState.value + } + Binding { + target: stateDelegateLoader.item.hasOwnProperty("from") ? stateDelegateLoader.item : null + property: "from" + value: stateDelegate.thingState.minValue + } + Binding { + target: stateDelegateLoader.item.hasOwnProperty("to") ? stateDelegateLoader.item : null + property: "to" + value: stateDelegate.thingState.maxValue + } + Binding { + target: stateDelegateLoader.item.hasOwnProperty("possibleValues") ? stateDelegateLoader.item : null + property: "possibleValues" + value: stateDelegate.thingState.possibleValues + } + Binding { + target: stateDelegateLoader.item.hasOwnProperty("possibleValuesDisplayNames") ? stateDelegateLoader.item : null + property: "possibleValuesDisplayNames" + value: { + print("updating displayNames", stateDelegate.thingState.possibleValues) + var ret = [] + for (var i = 0; i < stateDelegate.thingState.possibleValues.length; i++) { + var possibleValue = stateDelegate.thingState.possibleValues[i] + var idx = stateDelegate.stateType.possibleValues.indexOf(possibleValue) + print("value:", possibleValue, idx) + if (idx >= 0) { + ret.push(stateDelegate.stateType.possibleValuesDisplayNames[idx]) + } else { + ret.push(possibleValue) + } + } + return ret + } + } + Binding { + target: stateDelegateLoader.item.hasOwnProperty("unit") ? stateDelegateLoader.item : null + property: "unit" + value: stateDelegate.stateType.unit + } + } + } + + Component { + id: eventComponent + RowLayout { + id: eventComponentItem + property EventType eventType: null + + Label { + Layout.fillWidth: true + text: eventComponentItem.eventType.displayName + } + Rectangle { + id: flashlight + Layout.preferredHeight: Style.iconSize * .8 + Layout.preferredWidth: height + color: "lightgray" + radius: width / 2 + border.color: Style.foregroundColor + border.width: 1 + + SequentialAnimation on color { + id: flashlightAnimation + running: false + ColorAnimation { to: "lightgreen"; duration: 100 } + ColorAnimation { to: "lightgray"; duration: 500 } + } + } + Connections { + target: root.thing + onEventTriggered: { + if (eventTypeId === eventComponentItem.eventType.id) { + flashlightAnimation.start(); + } + } + } + } + } +} diff --git a/nymea-app/ui/thingconfiguration/ConfigureThingPage.qml b/nymea-app/ui/thingconfiguration/ConfigureThingPage.qml index 10c33f8c..40c634d4 100644 --- a/nymea-app/ui/thingconfiguration/ConfigureThingPage.qml +++ b/nymea-app/ui/thingconfiguration/ConfigureThingPage.qml @@ -73,6 +73,8 @@ SettingsPageBase { if (!root.thing.isChild || root.thingClass.createMethods.indexOf("CreateMethodAuto") < 0) { deviceMenu.addItem(menuEntryComponent.createObject(deviceMenu, {text: qsTr("Reconfigure"), iconSource: "../images/configure.svg", functionName: "reconfigureThing"})) } + + deviceMenu.addItem(menuEntryComponent.createObject(deviceMenu, {text: qsTr("Details"), iconSource: "../images/info.svg", functionName: "thingDetails"})) } function renameThing() { @@ -91,6 +93,10 @@ SettingsPageBase { configPage.aborted.connect(function() {pageStack.pop(root)}) } + function thingDetails() { + var detailsPage = pageStack.push(Qt.resolvedUrl("qrc:/ui/devicepages/DeviceDetailsPage.qml"), {thing: root.thing}) + } + Component { id: menuEntryComponent IconMenuItem {