From ae0a3279ec31e1144cd9c2166352c0154be7bf44 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Fri, 8 Mar 2019 15:41:36 +0100 Subject: [PATCH] WIP: drexel und weiss experience --- .../connection/nymeaconnection.cpp | 4 +- libnymea-app-core/devicesproxy.cpp | 19 + libnymea-app-core/devicesproxy.h | 6 + libnymea-common/types/states.h | 2 +- nymea-app/images.qrc | 1 + nymea-app/resources.qrc | 1 + nymea-app/ui/Nymea.qml | 1 - nymea-app/ui/components/Dial.qml | 14 +- nymea-app/ui/components/ThrottledSlider.qml | 1 + .../ui/devicepages/HeatingDevicePage.qml | 6 +- nymea-app/ui/experiences/heating/Main.qml | 385 ++++++++++++++++++ nymea-app/ui/images/ventilation.svg | 29 ++ 12 files changed, 456 insertions(+), 13 deletions(-) create mode 100644 nymea-app/ui/experiences/heating/Main.qml create mode 100644 nymea-app/ui/images/ventilation.svg diff --git a/libnymea-app-core/connection/nymeaconnection.cpp b/libnymea-app-core/connection/nymeaconnection.cpp index 8a89783f..8142ec09 100644 --- a/libnymea-app-core/connection/nymeaconnection.cpp +++ b/libnymea-app-core/connection/nymeaconnection.cpp @@ -137,7 +137,7 @@ Connection *NymeaConnection::currentConnection() const void NymeaConnection::sendData(const QByteArray &data) { if (connected()) { - qDebug() << "sending data:" << data; +// qDebug() << "sending data:" << data; m_currentTransport->sendData(data); } else { qWarning() << "Connection: Not connected. Cannot send."; @@ -382,7 +382,7 @@ void NymeaConnection::updateActiveBearers() QList configs = m_networkConfigManager->allConfigurations(QNetworkConfiguration::Active); // qDebug() << "Network configuations:" << configs.count(); foreach (const QNetworkConfiguration &config, configs) { - qDebug() << "Candidate network config:" << config.name() << config.bearerTypeFamily() << config.bearerTypeName(); +// qDebug() << "Candidate network config:" << config.name() << config.bearerTypeFamily() << config.bearerTypeName(); // NOTE: iOS doesn't correctly report bearer types. It'll be Unknown all the time. Let's hardcode it to WiFi for that... #if defined(Q_OS_IOS) diff --git a/libnymea-app-core/devicesproxy.cpp b/libnymea-app-core/devicesproxy.cpp index 0e9c7368..b3a08153 100644 --- a/libnymea-app-core/devicesproxy.cpp +++ b/libnymea-app-core/devicesproxy.cpp @@ -101,6 +101,20 @@ void DevicesProxy::setFilterTagId(const QString &filterTag) } } +QString DevicesProxy::filterDeviceClassId() const +{ + return m_filterDeviceClassId; +} + +void DevicesProxy::setFilterDeviceClassId(const QString &filterDeviceClassId) +{ + if (m_filterDeviceClassId != filterDeviceClassId) { + m_filterDeviceClassId = filterDeviceClassId; + emit filterDeviceClassIdChanged(); + invalidateFilter(); + } +} + QStringList DevicesProxy::shownInterfaces() const { return m_shownInterfaces; @@ -231,6 +245,11 @@ bool DevicesProxy::filterAcceptsRow(int source_row, const QModelIndex &source_pa return false; } } + if (!m_filterDeviceClassId.isEmpty()) { + if (device->deviceClassId() != m_filterDeviceClassId) { + return false; + } + } DeviceClass *deviceClass = m_engine->deviceManager()->deviceClasses()->getDeviceClass(device->deviceClassId()); if (!m_shownInterfaces.isEmpty()) { bool foundMatch = false; diff --git a/libnymea-app-core/devicesproxy.h b/libnymea-app-core/devicesproxy.h index 63dfca95..4ab74807 100644 --- a/libnymea-app-core/devicesproxy.h +++ b/libnymea-app-core/devicesproxy.h @@ -38,6 +38,7 @@ class DevicesProxy : public QSortFilterProxyModel Q_PROPERTY(Engine* engine READ engine WRITE setEngine NOTIFY engineChanged) Q_PROPERTY(DevicesProxy *parentProxy READ parentProxy WRITE setParentProxy NOTIFY parentProxyChanged) Q_PROPERTY(QString filterTagId READ filterTagId WRITE setFilterTagId NOTIFY filterTagIdChanged) + Q_PROPERTY(QString filterDeviceClassId READ filterDeviceClassId WRITE setFilterDeviceClassId NOTIFY filterDeviceClassIdChanged) Q_PROPERTY(QStringList shownInterfaces READ shownInterfaces WRITE setShownInterfaces NOTIFY shownInterfacesChanged) Q_PROPERTY(QStringList hiddenInterfaces READ hiddenInterfaces WRITE setHiddenInterfaces NOTIFY hiddenInterfacesChanged) Q_PROPERTY(QString nameFilter READ nameFilter WRITE setNameFilter NOTIFY nameFilterChanged) @@ -62,6 +63,9 @@ public: QString filterTagId() const; void setFilterTagId(const QString &filterTag); + QString filterDeviceClassId() const; + void setFilterDeviceClassId(const QString &filterDeviceClassId); + QStringList shownInterfaces() const; void setShownInterfaces(const QStringList &shownInterfaces); @@ -86,6 +90,7 @@ signals: void engineChanged(); void parentProxyChanged(); void filterTagIdChanged(); + void filterDeviceClassIdChanged(); void shownInterfacesChanged(); void hiddenInterfacesChanged(); void nameFilterChanged(); @@ -100,6 +105,7 @@ private: Engine *m_engine = nullptr; DevicesProxy *m_parentProxy = nullptr; QString m_filterTagId; + QString m_filterDeviceClassId; QStringList m_shownInterfaces; QStringList m_hiddenInterfaces; QString m_nameFilter; diff --git a/libnymea-common/types/states.h b/libnymea-common/types/states.h index 51b7794b..c54ee50b 100644 --- a/libnymea-common/types/states.h +++ b/libnymea-common/types/states.h @@ -37,7 +37,7 @@ public: StateTypeIdRole }; - explicit States(QObject *parent = 0); + explicit States(QObject *parent = nullptr); QList states(); diff --git a/nymea-app/images.qrc b/nymea-app/images.qrc index 68f4fa09..effa074f 100644 --- a/nymea-app/images.qrc +++ b/nymea-app/images.qrc @@ -170,5 +170,6 @@ ui/images/sensors/presence.svg ui/images/powersocket.svg ui/images/dial.svg + ui/images/ventilation.svg diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 05bbdfb4..4e25a7e4 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -165,5 +165,6 @@ ui/thingconfiguration/ConfigureThingPage.qml ui/connection/CertificateDialog.qml ui/experiences/garagegates/Main.qml + ui/experiences/heating/Main.qml diff --git a/nymea-app/ui/Nymea.qml b/nymea-app/ui/Nymea.qml index b1eacadd..bd342dee 100644 --- a/nymea-app/ui/Nymea.qml +++ b/nymea-app/ui/Nymea.qml @@ -50,7 +50,6 @@ ApplicationWindow { RootItem { id: rootItem anchors.fill: parent - anchors.topMargin: PlatformHelper.getSafeAreaMargins(app) } NymeaDiscovery { diff --git a/nymea-app/ui/components/Dial.qml b/nymea-app/ui/components/Dial.qml index ff3f55f6..56395aac 100644 --- a/nymea-app/ui/components/Dial.qml +++ b/nymea-app/ui/components/Dial.qml @@ -24,8 +24,8 @@ ColumnLayout { } readonly property State deviceState: device && stateType ? device.states.getState(stateType.id) : null - readonly property double from: dial.stateType.minValue - readonly property double to: dial.stateType.maxValue + readonly property double from: dial.stateType ? dial.stateType.minValue : 0 + readonly property double to: dial.stateType ? dial.stateType.maxValue : 100 readonly property double anglePerStep: maxAngle / dial.steps readonly property double startAngle: -(dial.steps * dial.anglePerStep) / 2 @@ -90,7 +90,7 @@ ColumnLayout { Label { id: topLabel Layout.fillWidth: true - text: rotateMouseArea.currentValue + dial.stateType.unitString + text: rotateMouseArea.currentValue + (dial.stateType ? dial.stateType.unitString : "") font.pixelSize: app.largeFont * 1.5 horizontalAlignment: Text.AlignHCenter visible: dial.showValueLabel && dial.stateType !== null @@ -101,7 +101,6 @@ ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true - Item { id: innerDial @@ -110,7 +109,6 @@ ColumnLayout { anchors.centerIn: parent rotation: dial.startAngle - Rectangle { anchors.fill: rotationButton radius: height / 2 @@ -167,7 +165,7 @@ ColumnLayout { width: parent.width height: width radius: width / 2 - color: dial.angleToValue(parent.rotation) <= dial.deviceState.value ? d.poweredColor : d.offColor + color: dial.deviceState && dial.angleToValue(parent.rotation) <= dial.deviceState.value ? d.poweredColor : d.offColor Behavior on color { ColorAnimation { duration: 200 } } } } @@ -247,8 +245,8 @@ ColumnLayout { dragging = false; } - readonly property int decimals: dial.stateType.type.toLowerCase() === "int" ? 0 : 1 - property var currentValue: dial.deviceState.value.toFixed(decimals) + readonly property int decimals: dial.stateType && dial.stateType.type.toLowerCase() === "int" ? 0 : 1 + property var currentValue: dial.deviceState ? dial.deviceState.value.toFixed(decimals) : 0 property date lastVibration: new Date() property int startX property int startY diff --git a/nymea-app/ui/components/ThrottledSlider.qml b/nymea-app/ui/components/ThrottledSlider.qml index 434889a2..8bcc5598 100644 --- a/nymea-app/ui/components/ThrottledSlider.qml +++ b/nymea-app/ui/components/ThrottledSlider.qml @@ -10,6 +10,7 @@ Item { property alias from: slider.from property alias to: slider.to property alias stepSize: slider.stepSize + property alias snapMode: slider.snapMode readonly property real rawValue: slider.value diff --git a/nymea-app/ui/devicepages/HeatingDevicePage.qml b/nymea-app/ui/devicepages/HeatingDevicePage.qml index 5171c499..d0b137f6 100644 --- a/nymea-app/ui/devicepages/HeatingDevicePage.qml +++ b/nymea-app/ui/devicepages/HeatingDevicePage.qml @@ -23,6 +23,9 @@ DevicePageBase { readonly property StateType boostStateType: device.deviceClass.stateTypes.findByName("boost") readonly property State boostState: boostStateType ? device.states.getState(boostStateType.id) : null + Component.onCompleted: { + print("d:", root.device, root.targetTemperatureStateType, root.percentageStateType) + } GridLayout { anchors.fill: parent @@ -33,10 +36,11 @@ DevicePageBase { id: dial Layout.fillWidth: true Layout.fillHeight: true - visible: root.targetTemperatureStateType || root.percentageStateType +// visible: root.targetTemperatureStateType || root.percentageStateType device: root.device stateType: root.targetTemperatureStateType ? root.targetTemperatureStateType : root.percentageStateType + } Rectangle { diff --git a/nymea-app/ui/experiences/heating/Main.qml b/nymea-app/ui/experiences/heating/Main.qml new file mode 100644 index 00000000..29127a59 --- /dev/null +++ b/nymea-app/ui/experiences/heating/Main.qml @@ -0,0 +1,385 @@ +import QtQuick 2.3 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import "qrc:/ui/components" +import Nymea 1.0 +import QtGraphicalEffects 1.0 + +Item { + id: root + readonly property string title: qsTr("Heating") + readonly property string icon: Qt.resolvedUrl("qrc:/ui/images/radiator.svg") + + readonly property Device duwWpDevice: duwWpFilterModel.count > 0 ? duwWpFilterModel.get(0) : null + readonly property Device duwLuDevice: duwLuFilterModel.count > 0 ? duwLuFilterModel.get(0) : null + + readonly property State temperatureState: duwWpDevice ? duwWpDevice.states.getState(duwWpDevice.deviceClass.stateTypes.findByName("temperature").id) : null + readonly property State targetTemperatureState: duwWpDevice ? duwWpDevice.states.getState(duwWpDevice.deviceClass.stateTypes.findByName("targetTemperature").id) : null + readonly property State ventilationModeState: duwLuDevice ? duwLuDevice.states.getState(duwLuDevice.deviceClass.stateTypes.findByName("ventilationMode").id) : null + + function ventilationModeToSliderValue(ventilationMode) { + switch (ventilationMode) { + case "Automatic": + case "Party": + return 0 + case "Manual level 0": + return 0; + case "Manual level 1": + return 1; + case "Manual level 2": + return 2; + case "Manual level 3": + return 3; + } + return 0; + } + function ventilationModeToUiMode(ventilationMode) { + switch (ventilationMode) { + case "Automatic": + return 0 + case "Party": + return 1; + case "Manual level 0": + case "Manual level 1": + case "Manual level 2": + case "Manual level 3": + return 2; + } + } + + function uiModeToVentilationMode(uiMode, sliderValue) { + switch (uiMode) { + case 0: + return "Automatic"; + case 1: + return "Party"; + case 2: + return "Manual level " + Math.floor(sliderValue) + } + } + + function setVentilationMode(uiModeIndex, sliderIndex) { + var params =[]; + var param = {}; + param["paramTypeId"] = root.ventilationModeState.stateTypeId + param["value"] = root.uiModeToVentilationMode(uiModeIndex, sliderIndex) + params.push(param) + engine.deviceManager.executeAction(root.duwLuDevice.id, root.ventilationModeState.stateTypeId, params) + } + + function setTargetTemp(targetTemp) { + // We don't want to spam with set value calls so we're going to queue them up and only send one at a time + if (d.pendingCallId != -1) { + d.queuedTargetTemp = targetTemp; + d.setTempPending = true; + return; + } + var params = [] + var param = {} + param["paramTypeId"] = root.targetTemperatureState.stateTypeId + param["value"] = targetTemp + params.push(param) + d.pendingCallId = engine.deviceManager.executeAction(root.duwWpDevice.id, root.targetTemperatureState.stateTypeId, params) + d.setTempPending = false; + } + + Connections { + target: engine.deviceManager + onExecuteActionReply: { + print("executeActionReply:", params["id"]) + if (params["id"] === d.pendingCallId) { + d.pendingCallId = -1; + if (d.setTempPending) { + setTargetTemp(d.queuedTargetTemp) + } + } + } + } + + QtObject { + id: d + property int pendingCallId: -1 + property bool setTempPending: false + property real queuedTargetTemp: 0 + } + + DevicesProxy { + id: duwWpFilterModel + engine: _engine + filterDeviceClassId: "e548f962-92db-4110-8279-10fbcde35f93" + } + + DevicesProxy { + id: duwLuFilterModel + engine: _engine + filterDeviceClassId: "0de8e21e-392a-4790-a78a-b1a7eaa7571b" + } + + EmptyViewPlaceholder { + anchors.centerIn: parent + width: parent.width - app.margins * 2 + text: qsTr("There is no drexel und weiss heating system set up yet.") + imageSource: "qrc:/ui/images/radiator.svg" + buttonText: qsTr("Set up now") + visible: duwWpFilterModel.count === 0 && !engine.deviceManager.fetchingData + } + + + Item { + id: mainView + anchors.fill: parent + visible: root.duwWpDevice !== null + + ColumnLayout { + anchors.fill: parent + anchors.margins: app.margins + + RowLayout { + spacing: app.margins + ColorIcon { + Layout.preferredHeight: app.iconSize + Layout.preferredWidth: app.iconSize + name: "qrc:/ui/images/weathericons/wind.svg" + color: app.accentColor + } + Led { + } + } + + Label { + text: qsTr("Current temperature") + font.pixelSize: app.smallFont + } + + RowLayout { + ColorIcon { + Layout.preferredHeight: app.iconSize + Layout.preferredWidth: app.iconSize + name: "qrc:/ui/images/sensors/temperature.svg" + color: app.accentColor + } + Label { + text: root.temperatureState ? root.temperatureState.value.toFixed(1) + "°C" : "N/A" + Layout.fillWidth: true + font.pixelSize: app.largeFont * 2 + } + } + + Item { + Layout.preferredHeight: app.margins * 2 + Layout.fillWidth: true + } + + Label { + text: qsTr("Temperature, °C") + font.pixelSize: app.largeFont + } + Label { + text: (d.pendingCallId !== -1 || d.setTempPending) ? d.queuedTargetTemp.toFixed(1) : + root.targetTemperatureState ? root.targetTemperatureState.value.toFixed(1) : "N/A" + font.pixelSize: app.largeFont * 4 + } + + Item { + Layout.preferredHeight: app.margins * 2 + Layout.fillWidth: true + } + ColorIcon { + Layout.preferredHeight: app.iconSize * 1.5 + Layout.preferredWidth: height + Layout.leftMargin: width + color: app.accentColor + name: "qrc:/ui/images/share.svg" + } + Label { + text: qsTr("Automate this thing") + color: app.accentColor + font.pixelSize: app.smallFont + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + } + + RowLayout { + Layout.leftMargin: parent.width * .05 + Layout.rightMargin: parent.width * .2 + spacing: app.margins + ColorIcon { + Layout.preferredHeight: app.iconSize + Layout.preferredWidth: app.iconSize + color: app.accentColor + name: "qrc:/ui/images/ventilation.svg" + } + + RowLayout { + Layout.fillWidth: true + Layout.maximumHeight: app.iconSize + spacing: 0 + + Repeater { + model: ListModel { + ListElement { text: qsTr("Auto") } + ListElement { text: qsTr("Party") } + ListElement { text: qsTr("Manual") } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + border.width: 1 + border.color: app.accentColor + color: root.ventilationModeState && root.ventilationModeToUiMode(root.ventilationModeState.value) === index ? app.accentColor : "transparent" + Label { + anchors.centerIn: parent + text: model.text + font.pixelSize: app.smallFont + } + MouseArea { + anchors.fill: parent + onClicked: { + root.setVentilationMode(index, ventilationSlider.value) + } + } + } + } + } + } + + Slider { + id: ventilationSlider + Layout.fillWidth: true + Layout.leftMargin: parent.width * .05 + Layout.rightMargin: parent.width * .05 + from: 0 + to: 4 + stepSize: 1 + live: false + snapMode: Slider.SnapAlways + enabled: root.ventilationModeState && root.ventilationModeToUiMode(root.ventilationModeState.value) === 2 + opacity: enabled ? 1 : .2 + value: root.ventilationModeState ? root.ventilationModeToSliderValue(root.ventilationModeState.value) : 0 + onMoved: root.setVentilationMode(2, ventilationSlider.value) + + } + + ProgressButton { + imageSource: "qrc:/ui/images/system-shutdown.svg" + Layout.preferredHeight: app.iconSize * 1.5 + Layout.preferredWidth: height + Layout.alignment: Qt.AlignHCenter + } + + Label { + text: qsTr("Hold to turn off") + font.pixelSize: app.smallFont + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + } + } + + Item { + height: parent.height * .85 + width: height + anchors.left: parent.right + anchors.leftMargin: -width * .25 + anchors.top: parent.top + anchors.topMargin: -height * .05 + z: -1 + + Rectangle { + id: outerRadius + anchors.fill: parent + radius: width / 2 + border.width: 3 + color: "transparent" + border.color: app.accentColor + } + + Glow { + anchors.fill: parent + source: outerRadius +// color: "#f45b69" + color: Qt.rgba(app.accentColor.r, app.accentColor.g, app.accentColor.b, .5) + radius: 8 + samples: 17 + spread: 0.5 + } + + Rectangle { + id: innerRadius + anchors.fill: parent + anchors.margins: parent.width * .02 + radius: width / 2 + border.width: 2 + color: "transparent" + border.color: app.accentColor + + Repeater { + id: ticksRepeater + model: 180 + + Item { + height: isBold ? 3 : 2 + width: parent.width - 2 + anchors.centerIn: parent + rotation: index * 360 / ticksRepeater.count + readonly property int isBold: index % 10 === 0 +// Rectangle { anchors.fill: parent; color: "blue" } + + Rectangle { height: parent.height; width: parent.isBold ? 20 : 10; color: app.accentColor } + + } + } + + } + + MouseArea { + anchors.fill: parent + + property real startAnglePress + property real startAngleDial + property real startTemp + + onPressed: { + startAnglePress = calculateAngle(mouseX, mouseY) + startAngleDial = innerRadius.rotation + startTemp = root.targetTemperatureState.value + + print("angle:", calculateAngle(mouseX, mouseY)) + } + + onPositionChanged: { + var currentAngle = calculateAngle(mouseX, mouseY) + var angleDiff = currentAngle - startAnglePress + + var tempDiff = Math.round(angleDiff / 2) / 10 + var newTemp = startTemp + tempDiff + + innerRadius.rotation = startAngleDial + angleDiff + + print("new degree value", newTemp) + root.setTargetTemp(newTemp); + } + + function calculateAngle(mouseX, mouseY) { + // transform coords to center of dial + mouseX -= width / 2 + mouseY -= height / 2 + + var rad = Math.atan(mouseY / mouseX); + var angle = rad * 180 / Math.PI + + angle += 90; + + if (mouseX < 0 && mouseY >= 0) angle = 180 + angle; + if (mouseX < 0 && mouseY < 0) angle = 180 + angle; + + return angle; + } + } + } + } +} diff --git a/nymea-app/ui/images/ventilation.svg b/nymea-app/ui/images/ventilation.svg new file mode 100644 index 00000000..d6740d1c --- /dev/null +++ b/nymea-app/ui/images/ventilation.svg @@ -0,0 +1,29 @@ + + + + +settings + + +