From 6b95be2e40554dd83fa362e4181ca0f6ace58afa Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Thu, 24 Jan 2019 23:33:24 +0100 Subject: [PATCH 1/3] go back to using a slider instead of the spinner --- nymea-app/ui/components/ThrottledSlider.qml | 3 +++ .../ui/devicepages/GenericDevicePage.qml | 21 +++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/nymea-app/ui/components/ThrottledSlider.qml b/nymea-app/ui/components/ThrottledSlider.qml index 80e45af6..434889a2 100644 --- a/nymea-app/ui/components/ThrottledSlider.qml +++ b/nymea-app/ui/components/ThrottledSlider.qml @@ -10,6 +10,9 @@ Item { property alias from: slider.from property alias to: slider.to property alias stepSize: slider.stepSize + + readonly property real rawValue: slider.value + signal moved(real value); Slider { diff --git a/nymea-app/ui/devicepages/GenericDevicePage.qml b/nymea-app/ui/devicepages/GenericDevicePage.qml index 7bf37dba..2c9cc36c 100644 --- a/nymea-app/ui/devicepages/GenericDevicePage.qml +++ b/nymea-app/ui/devicepages/GenericDevicePage.qml @@ -113,11 +113,13 @@ DevicePageBase { Label { Layout.fillWidth: true + Layout.minimumWidth: parent.width / 2 text: stateDelegate.stateType.displayName elide: Text.ElideRight } Loader { id: stateDelegateLoader + Layout.fillWidth: true sourceComponent: { switch (stateType.type.toLowerCase()) { case "string": @@ -144,7 +146,8 @@ DevicePageBase { } if (stateDelegate.writable) { - return spinBoxComponent; + return sliderComponent; +// return spinBoxComponent; } return numberLabelComponent; case "color": @@ -157,6 +160,11 @@ DevicePageBase { } } + Label { + visible: stateDelegateLoader.sourceComponent === sliderComponent + text: stateDelegate.deviceState.value + } + Label { visible: stateDelegate.stateType.unit !== Types.UnitUnixTime && stateDelegate.stateType.unitString.length > 0 text: stateDelegate.stateType.unitString @@ -412,7 +420,7 @@ DevicePageBase { SpinBox { width: 150 signal changed(var value) - stepSize: (to - from) / 10 + stepSize: (to - from) / 20 editable: true onValueModified: { changed(value) @@ -420,6 +428,15 @@ DevicePageBase { } } + Component { + id: sliderComponent + ThrottledSlider { + signal changed(var value) + stepSize: 1 + onMoved: changed(value) + } + } + Component { id: comboBoxComponent ComboBox { From d81f609614924a306e6b278561afe21ae9dee1e8 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Mon, 28 Jan 2019 13:30:41 +0100 Subject: [PATCH 2/3] fix paramDelegate for double --- nymea-app/ui/delegates/ParamDelegate.qml | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/nymea-app/ui/delegates/ParamDelegate.qml b/nymea-app/ui/delegates/ParamDelegate.qml index c488f9f9..1f354f9d 100644 --- a/nymea-app/ui/delegates/ParamDelegate.qml +++ b/nymea-app/ui/delegates/ParamDelegate.qml @@ -31,6 +31,7 @@ ItemDelegate { id: loader Layout.fillWidth: sourceComponent === textFieldComponent sourceComponent: { + print("loading paramdelegate:", root.writable, root.paramType.type) if (!root.writable) { return stringComponent; } @@ -39,7 +40,8 @@ ItemDelegate { case "bool": return boolComponent; case "int": - return spinnerComponent; + case "double": + return sliderComponent; case "string": case "qstring": if (root.paramType.allowedValues.length > 0) { @@ -104,22 +106,14 @@ ItemDelegate { id: sliderComponent RowLayout { spacing: app.margins - Label { - text: root.paramType.minValue - } Slider { id: slider Layout.fillWidth: true from: root.paramType.minValue to: root.paramType.maxValue value: root.param.value - stepSize: { - switch (root.paramType.type.toLowerCase()) { - case "int": - return 1; - } - return 0.01; - } + stepSize: 1 / (10 * decimals) + property int decimals: root.paramType.type.toLocaleLowerCase() === "int" ? 0 : 1 onMoved: { var newValue @@ -134,7 +128,7 @@ ItemDelegate { } } Label { - text: root.paramType.maxValue + text: root.param.value.toFixed(slider.decimals) + root.paramType.unitString } } From f095a2ac15d63049a46b616f35e61193fa04b6b9 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Mon, 28 Jan 2019 00:09:43 +0100 Subject: [PATCH 3/3] Add support for the thermostat interface --- libnymea-app-core/devicemanager.cpp | 5 +- nymea-app/images.qrc | 1 + nymea-app/platformhelper.cpp | 1 + nymea-app/platformhelper.h | 9 + .../android/platformhelperandroid.cpp | 18 + .../android/platformhelperandroid.h | 2 + .../generic/platformhelpergeneric.cpp | 5 + .../generic/platformhelpergeneric.h | 1 + .../ios/platformhelperios.cpp | 16 + .../ios/platformhelperios.h | 6 + nymea-app/resources.qrc | 14 + nymea-app/ui/Nymea.qml | 10 +- nymea-app/ui/components/Dial.qml | 296 ++++++++++++++++ nymea-app/ui/delegates/ParamDelegate.qml | 8 +- .../statedelegates/CheckboxDelegate.qml | 12 + .../statedelegates/ColorDelegate.qml | 61 ++++ .../statedelegates/ComboBoxDelegate.qml | 17 + .../statedelegates/DateTimeDelegate.qml | 12 + .../statedelegates/LabelDelegate.qml | 12 + .../delegates/statedelegates/LedDelegate.qml | 11 + .../delegates/statedelegates/ListDelegate.qml | 11 + .../statedelegates/NumberLabelDelegate.qml | 12 + .../statedelegates/SliderDelegate.qml | 57 +++ .../statedelegates/SpinBoxDelegate.qml | 16 + .../statedelegates/SwitchDelegate.qml | 15 + .../statedelegates/TextFieldDelegate.qml | 11 + .../devicelistpages/SensorsDeviceListPage.qml | 7 + .../ui/devicepages/GenericDevicePage.qml | 334 ++++++------------ .../ui/devicepages/HeatingDevicePage.qml | 81 +++++ nymea-app/ui/devicepages/LightDevicePage.qml | 130 ++++--- nymea-app/ui/images/dial.svg | 170 +++++++++ .../ui/mainviews/DevicesPageDelegate.qml | 3 + packaging/android/AndroidManifest.xml | 1 + .../src/io/guh/nymeaapp/NymeaAppActivity.java | 7 + packaging/ios/platformhelperios.mm | 34 ++ 35 files changed, 1111 insertions(+), 295 deletions(-) create mode 100644 nymea-app/ui/components/Dial.qml create mode 100644 nymea-app/ui/delegates/statedelegates/CheckboxDelegate.qml create mode 100644 nymea-app/ui/delegates/statedelegates/ColorDelegate.qml create mode 100644 nymea-app/ui/delegates/statedelegates/ComboBoxDelegate.qml create mode 100644 nymea-app/ui/delegates/statedelegates/DateTimeDelegate.qml create mode 100644 nymea-app/ui/delegates/statedelegates/LabelDelegate.qml create mode 100644 nymea-app/ui/delegates/statedelegates/LedDelegate.qml create mode 100644 nymea-app/ui/delegates/statedelegates/ListDelegate.qml create mode 100644 nymea-app/ui/delegates/statedelegates/NumberLabelDelegate.qml create mode 100644 nymea-app/ui/delegates/statedelegates/SliderDelegate.qml create mode 100644 nymea-app/ui/delegates/statedelegates/SpinBoxDelegate.qml create mode 100644 nymea-app/ui/delegates/statedelegates/SwitchDelegate.qml create mode 100644 nymea-app/ui/delegates/statedelegates/TextFieldDelegate.qml create mode 100644 nymea-app/ui/devicepages/HeatingDevicePage.qml create mode 100644 nymea-app/ui/images/dial.svg diff --git a/libnymea-app-core/devicemanager.cpp b/libnymea-app-core/devicemanager.cpp index d647a182..4196a1f9 100644 --- a/libnymea-app-core/devicemanager.cpp +++ b/libnymea-app-core/devicemanager.cpp @@ -298,7 +298,7 @@ void DeviceManager::editDeviceResponse(const QVariantMap ¶ms) void DeviceManager::executeActionResponse(const QVariantMap ¶ms) { - qDebug() << "Execute Action response" << params; +// qDebug() << "Execute Action response" << params; emit executeActionReply(params); } @@ -371,7 +371,7 @@ void DeviceManager::editDevice(const QUuid &deviceId, const QString &name) int DeviceManager::executeAction(const QUuid &deviceId, const QUuid &actionTypeId, const QVariantList ¶ms) { - qDebug() << "JsonRpc: execute action " << deviceId.toString() << actionTypeId.toString() << params; +// qDebug() << "JsonRpc: execute action " << deviceId.toString() << actionTypeId.toString() << params; QVariantMap p; p.insert("deviceId", deviceId.toString()); p.insert("actionTypeId", actionTypeId.toString()); @@ -379,6 +379,5 @@ int DeviceManager::executeAction(const QUuid &deviceId, const QUuid &actionTypeI p.insert("params", params); } - qDebug() << "Params:" << p; return m_jsonClient->sendCommand("Actions.ExecuteAction", p, this, "executeActionResponse"); } diff --git a/nymea-app/images.qrc b/nymea-app/images.qrc index e83d1940..68f4fa09 100644 --- a/nymea-app/images.qrc +++ b/nymea-app/images.qrc @@ -169,5 +169,6 @@ ui/images/stock_video.svg ui/images/sensors/presence.svg ui/images/powersocket.svg + ui/images/dial.svg diff --git a/nymea-app/platformhelper.cpp b/nymea-app/platformhelper.cpp index 2158ef74..de4fd04f 100644 --- a/nymea-app/platformhelper.cpp +++ b/nymea-app/platformhelper.cpp @@ -5,3 +5,4 @@ PlatformHelper::PlatformHelper(QObject *parent) : QObject(parent) { } + diff --git a/nymea-app/platformhelper.h b/nymea-app/platformhelper.h index c936e9c8..a5a4a22e 100644 --- a/nymea-app/platformhelper.h +++ b/nymea-app/platformhelper.h @@ -13,6 +13,13 @@ class PlatformHelper : public QObject Q_PROPERTY(QString machineHostname READ machineHostname CONSTANT) public: + enum HapticsFeedback { + HapticsFeedbackSelection, + HapticsFeedbackImpact, + HapticsFeedbackNotification + }; + Q_ENUM(HapticsFeedback) + explicit PlatformHelper(QObject *parent = nullptr); virtual ~PlatformHelper() = default; @@ -24,6 +31,8 @@ public: virtual QString deviceModel() const = 0; virtual QString deviceManufacturer() const = 0; + Q_INVOKABLE virtual void vibrate(HapticsFeedback feedbackType) = 0; + signals: void permissionsRequestFinished(); }; diff --git a/nymea-app/platformintegration/android/platformhelperandroid.cpp b/nymea-app/platformintegration/android/platformhelperandroid.cpp index d9f6d4d1..cde0ece6 100644 --- a/nymea-app/platformintegration/android/platformhelperandroid.cpp +++ b/nymea-app/platformintegration/android/platformhelperandroid.cpp @@ -44,6 +44,24 @@ QString PlatformHelperAndroid::deviceManufacturer() const return QAndroidJniObject::callStaticObjectMethod("io/guh/nymeaapp/NymeaAppActivity","deviceManufacturer").toString(); } +void PlatformHelperAndroid::vibrate(PlatformHelper::HapticsFeedback feedbackType) +{ + int duration; + switch (feedbackType) { + case HapticsFeedbackSelection: + duration = 15; + break; + case HapticsFeedbackImpact: + duration = 25; + break; + case HapticsFeedbackNotification: + duration = 500; + break; + } + + QtAndroid::androidActivity().callMethod("vibrate","(I)V", duration); +} + void PlatformHelperAndroid::permissionRequestFinished(const QtAndroid::PermissionResultMap &result) { foreach (const QString &key, result.keys()) { diff --git a/nymea-app/platformintegration/android/platformhelperandroid.h b/nymea-app/platformintegration/android/platformhelperandroid.h index f35e7eb3..06dc5053 100644 --- a/nymea-app/platformintegration/android/platformhelperandroid.h +++ b/nymea-app/platformintegration/android/platformhelperandroid.h @@ -19,6 +19,8 @@ public: QString deviceModel() const override; QString deviceManufacturer() const override; + Q_INVOKABLE void vibrate(HapticsFeedback feedbackType) override; + private: static void permissionRequestFinished(const QtAndroid::PermissionResultMap &); diff --git a/nymea-app/platformintegration/generic/platformhelpergeneric.cpp b/nymea-app/platformintegration/generic/platformhelpergeneric.cpp index 51d35d8b..9c9c7145 100644 --- a/nymea-app/platformintegration/generic/platformhelpergeneric.cpp +++ b/nymea-app/platformintegration/generic/platformhelpergeneric.cpp @@ -38,3 +38,8 @@ QString PlatformHelperGeneric::deviceManufacturer() const { return QSysInfo::productType(); } + +void PlatformHelperGeneric::vibrate(PlatformHelper::HapticsFeedback feedbyckType) +{ + Q_UNUSED(feedbyckType) +} diff --git a/nymea-app/platformintegration/generic/platformhelpergeneric.h b/nymea-app/platformintegration/generic/platformhelpergeneric.h index c0583a2d..82bb4461 100644 --- a/nymea-app/platformintegration/generic/platformhelpergeneric.h +++ b/nymea-app/platformintegration/generic/platformhelpergeneric.h @@ -18,6 +18,7 @@ public: virtual QString deviceModel() const override; virtual QString deviceManufacturer() const override; + Q_INVOKABLE virtual void vibrate(HapticsFeedback feedbyckType) override; signals: public slots: diff --git a/nymea-app/platformintegration/ios/platformhelperios.cpp b/nymea-app/platformintegration/ios/platformhelperios.cpp index 19c0bbca..3885d710 100644 --- a/nymea-app/platformintegration/ios/platformhelperios.cpp +++ b/nymea-app/platformintegration/ios/platformhelperios.cpp @@ -47,3 +47,19 @@ QString PlatformHelperIOS::deviceManufacturer() const { return QString("iPhone"); } + +void PlatformHelperIOS::vibrate(PlatformHelper::HapticsFeedback feedbackType) +{ + switch (feedbackType) { + case HapticsFeedbackSelection: + generateSelectionFeedback(); + break; + case HapticsFeedbackImpact: + generateImpactFeedback(); + break; + case HapticsFeedbackNotification: + generateNotificationFeedback(); + break; + } +} + diff --git a/nymea-app/platformintegration/ios/platformhelperios.h b/nymea-app/platformintegration/ios/platformhelperios.h index 26813f40..b8af806d 100644 --- a/nymea-app/platformintegration/ios/platformhelperios.h +++ b/nymea-app/platformintegration/ios/platformhelperios.h @@ -19,10 +19,16 @@ public: virtual QString deviceModel() const override; virtual QString deviceManufacturer() const override; + Q_INVOKABLE virtual void vibrate(HapticsFeedback feedbackType) override; + private: // defined in platformhelperios.mm QString readKeyChainEntry(const QString &service, const QString &key); void writeKeyChainEntry(const QString &service, const QString &key, const QString &value); + + void generateSelectionFeedback(); + void generateImpactFeedback(); + void generateNotificationFeedback(); }; #endif // PLATFORMHELPERIOS_H diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index a83a2858..8197c0b2 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -147,5 +147,19 @@ ui/devicepages/PowersocketDevicePage.qml ui/devicelistpages/PowerSocketsDeviceListPage.qml ui/components/GroupedListView.qml + ui/devicepages/HeatingDevicePage.qml + ui/delegates/statedelegates/LedDelegate.qml + ui/delegates/statedelegates/LabelDelegate.qml + ui/delegates/statedelegates/NumberLabelDelegate.qml + ui/delegates/statedelegates/TextFieldDelegate.qml + ui/delegates/statedelegates/ListDelegate.qml + ui/delegates/statedelegates/CheckboxDelegate.qml + ui/delegates/statedelegates/SwitchDelegate.qml + ui/delegates/statedelegates/SpinBoxDelegate.qml + ui/delegates/statedelegates/ComboBoxDelegate.qml + ui/delegates/statedelegates/ColorDelegate.qml + ui/delegates/statedelegates/DateTimeDelegate.qml + ui/components/Dial.qml + ui/delegates/statedelegates/SliderDelegate.qml diff --git a/nymea-app/ui/Nymea.qml b/nymea-app/ui/Nymea.qml index 003f012e..8883c19e 100644 --- a/nymea-app/ui/Nymea.qml +++ b/nymea-app/ui/Nymea.qml @@ -54,7 +54,7 @@ ApplicationWindow { rootItem.handleCloseEvent(close) } - property var supportedInterfaces: ["light", "weather", "media", "garagegate", "awning", "shutter", "blind", "heating", "powersocket", "sensor", "smartmeter", "evcharger", "accesscontrol", "button", "notifications", "inputtrigger", "outputtrigger", "gateway"] + property var supportedInterfaces: ["light", "weather", "media", "garagegate", "awning", "shutter", "blind", "powersocket", "heating", "sensor", "smartmeter", "evcharger", "accesscontrol", "button", "notifications", "inputtrigger", "outputtrigger", "gateway"] function interfaceToString(name) { switch(name) { case "light": @@ -206,6 +206,8 @@ ApplicationWindow { case "heating": case "extendedheating": return Qt.resolvedUrl("images/radiator.svg") + case "thermostat": + return Qt.resolvedUrl("images/dial.svg") case "evcharger": case "extendedevcharger": return Qt.resolvedUrl("images/ev-charger.svg") @@ -231,7 +233,9 @@ ApplicationWindow { "smartmeterproducer": "lightgreen", "smartmeterconsumer": "orange", "extendedsmartmeterproducer": "blue", - "extendedsmartmeterconsumer": "blue" + "extendedsmartmeterconsumer": "blue", + "heating" : "gainsboro", + "thermostat": "dodgerblue" } function interfaceToColor(name) { @@ -278,6 +282,8 @@ ApplicationWindow { page = "ButtonDevicePage.qml"; } else if (interfaceList.indexOf("weather") >= 0) { page = "WeatherDevicePage.qml"; + } else if (interfaceList.indexOf("heating") >= 0 || interfaceList.indexOf("thermostat") >= 0) { + page = "HeatingDevicePage.qml"; } else if (interfaceList.indexOf("sensor") >= 0) { page = "SensorDevicePage.qml"; } else if (interfaceList.indexOf("inputtrigger") >= 0) { diff --git a/nymea-app/ui/components/Dial.qml b/nymea-app/ui/components/Dial.qml new file mode 100644 index 00000000..d9fc343e --- /dev/null +++ b/nymea-app/ui/components/Dial.qml @@ -0,0 +1,296 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.2 +import Nymea 1.0 +import QtQuick.Layouts 1.2 +import QtQuick.Controls.Material 2.2 + +ColumnLayout { + id: dial + + property Device device: null + property StateType stateType: null + + property bool showValueLabel: true + property int steps: 10 + property color color: app.accentColor + property int maxAngle: 235 + + // value : max = angle : maxAngle + function valueToAngle(value) { + return (value - from) * maxAngle / (to - from) + } + function angleToValue(angle) { + return (to - from) * angle / maxAngle + from + } + + 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 anglePerStep: maxAngle / dial.steps + readonly property double startAngle: -(dial.steps * dial.anglePerStep) / 2 + + readonly property StateType powerStateType: dial.device.deviceClass.stateTypes.findByName("power") + readonly property State powerState: powerStateType ? dial.device.states.getState(powerStateType.id) : null + + QtObject { + id: d + property int pendingActionId: -1 + property real valueCache: 0 + property bool valueCacheDirty: false + + property bool busy: rotateMouseArea.pressed || pendingActionId != -1 || valueCacheDirty + + property color onColor: dial.color + property color offColor: "#808080" + property color poweredColor: dial.powerStateType + ? (dial.powerState.value === true ? onColor : offColor) + : onColor + + + function enqueueSetValue(value) { + if (d.pendingActionId == -1) { + executeAction(value); + return; + } else { + valueCache = value + valueCacheDirty = true; + } + } + + function executeAction(value) { + var params = [] + var param = {} + param["paramTypeId"] = dial.stateType.id + param["value"] = value + params.push(param) + d.pendingActionId = engine.deviceManager.executeAction(dial.device.id, dial.stateType.id, params) + } + } + Connections { + target: engine.deviceManager + onExecuteActionReply: { + d.pendingActionId = -1 + if (d.valueCacheDirty) { + d.executeAction(d.valueCache) + d.valueCacheDirty = false; + } + } + } + + Component.onCompleted: rotationButton.rotation = dial.valueToAngle(dial.deviceState.value) + Connections { + target: dial.deviceState + onValueChanged: { + if (!d.busy) { + rotationButton.rotation = dial.valueToAngle(dial.deviceState.value) + } + } + } + + Label { + id: topLabel + Layout.fillWidth: true + text: rotateMouseArea.currentValue + dial.stateType.unitString + font.pixelSize: app.largeFont * 1.5 + horizontalAlignment: Text.AlignHCenter + visible: dial.showValueLabel && dial.stateType !== null + } + + Item { + id: buttonContainer + Layout.fillWidth: true + Layout.fillHeight: true + + Item { + id: innerDial + + height: Math.min(parent.height, parent.width) * .9 + width: height + anchors.centerIn: parent + rotation: dial.startAngle + + Rectangle { + height: parent.height * .8 + width: height + anchors.centerIn: parent + radius: height / 2 + border.color: app.foregroundColor + opacity: .3 + border.width: width * .025 + color: "transparent" + } + + Rectangle { + anchors.fill: rotationButton + radius: height / 2 + border.color: app.foregroundColor + border.width: 2 + color: "transparent" + opacity: rotateMouseArea.pressed && !rotateMouseArea.grabbed ? .7 : 1 + } + + Item { + id: rotationButton + height: parent.height * .75 + width: height + anchors.centerIn: parent + visible: dial.stateType !== null + Behavior on rotation { + NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } + enabled: !rotateMouseArea.pressed && !d.busy + } + + Item { + id: handle + anchors.horizontalCenter: parent.horizontalCenter + height: parent.height * .3 + width: height + + Rectangle { + height: parent.height * .6 + width: innerDial.width * 0.02 + radius: width / 2 + anchors.centerIn: parent + color: d.poweredColor + Behavior on color { ColorAnimation { duration: 200 } } + } + } + } + + Repeater { + id: indexLEDs + model: dial.steps + 1 + + Item { + height: parent.height + width: parent.width * .04 + anchors.centerIn: parent + rotation: dial.anglePerStep * index + visible: dial.stateType !== null + + Rectangle { + width: parent.width + height: width + radius: width / 2 + color: dial.angleToValue(parent.rotation) <= dial.deviceState.value ? d.poweredColor : d.offColor + Behavior on color { ColorAnimation { duration: 200 } } + } + } + } + } + + Label { + anchors { left: innerDial.left; bottom: innerDial.bottom; bottomMargin: innerDial.height * .1 } + text: "MIN" + font.pixelSize: innerDial.height * .06 + visible: dial.stateType !== null + } + + Label { + anchors { right: innerDial.right; bottom: innerDial.bottom; bottomMargin: innerDial.height * .1 } + text: "MAX" + font.pixelSize: innerDial.height * .06 + visible: dial.stateType !== null + } + + ColorIcon { + anchors.centerIn: innerDial + height: innerDial.height * .2 + width: height + name: "../images/system-shutdown.svg" + visible: dial.powerStateType !== null + color: d.poweredColor + Behavior on color { ColorAnimation { duration: 200 } } + } + + MouseArea { + id: rotateMouseArea + anchors.fill: innerDial + onPressedChanged: PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackImpact) + + property bool grabbed: false + onPressed: { + var mappedToHandle = mapToItem(handle, mouseX, mouseY); + if (mappedToHandle.x >= 0 + && mappedToHandle.x < handle.width + && mappedToHandle.y >= 0 + && mappedToHandle.y < handle.height + ) { + grabbed = true; + return; + } + } + onCanceled: grabbed = false; + + property bool dragging: false + onReleased: { + grabbed = false; + if (dial.powerStateType && !dragging) { + var params = [] + var param = {} + param["paramTypeId"] = dial.powerStateType.id + param["value"] = !dial.powerState.value + params.push(param) + engine.deviceManager.executeAction(dial.device.id, dial.powerStateType.id, params) + } + dragging = false; + } + + readonly property int decimals: dial.stateType.type.toLowerCase() === "int" ? 0 : 1 + property var currentValue: dial.deviceState.value.toFixed(decimals) + property date lastVibration: new Date() + onPositionChanged: { + if (!grabbed) { + return; + } + var angle = calculateAngle(mouseX, mouseY) + angle = (360 + angle - dial.startAngle) % 360; + + if (angle > 360 - ((360 - dial.maxAngle) / 2)) { + angle = 0; + } else if (angle > dial.maxAngle) { + angle = dial.maxAngle + } + + var newValue = Math.round(dial.angleToValue(angle) * 2) / 2; + rotationButton.rotation = angle; + newValue = newValue.toFixed(decimals) + + if (newValue != currentValue) { + if (!dragging) { + dragging = true; + } + + currentValue = newValue; + if (newValue <= dial.stateType.minValue || newValue >= dial.stateType.maxValue) { + PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackImpact) + } else { + if (lastVibration.getTime() + 35 < new Date()) { + PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection) + } + lastVibration = new Date() + } + d.enqueueSetValue(newValue); + } + } + + function calculateAngle(mouseX, mouseY) { + // transform coords to center of dial + mouseX -= innerDial.width / 2 + mouseY -= innerDial.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/delegates/ParamDelegate.qml b/nymea-app/ui/delegates/ParamDelegate.qml index 1f354f9d..a76cf1a9 100644 --- a/nymea-app/ui/delegates/ParamDelegate.qml +++ b/nymea-app/ui/delegates/ParamDelegate.qml @@ -112,7 +112,13 @@ ItemDelegate { from: root.paramType.minValue to: root.paramType.maxValue value: root.param.value - stepSize: 1 / (10 * decimals) + stepSize: { + var ret = 1 + for (var i = 0; i < decimals; i++) { + ret /= 10; + } + return ret; + } property int decimals: root.paramType.type.toLocaleLowerCase() === "int" ? 0 : 1 onMoved: { diff --git a/nymea-app/ui/delegates/statedelegates/CheckboxDelegate.qml b/nymea-app/ui/delegates/statedelegates/CheckboxDelegate.qml new file mode 100644 index 00000000..1942a410 --- /dev/null +++ b/nymea-app/ui/delegates/statedelegates/CheckboxDelegate.qml @@ -0,0 +1,12 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../../components" + +CheckBox { + property var value + checked: value === true + enabled: false +} diff --git a/nymea-app/ui/delegates/statedelegates/ColorDelegate.qml b/nymea-app/ui/delegates/statedelegates/ColorDelegate.qml new file mode 100644 index 00000000..18083220 --- /dev/null +++ b/nymea-app/ui/delegates/statedelegates/ColorDelegate.qml @@ -0,0 +1,61 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../../components" + +Item { + id: colorComponentItem + implicitWidth: app.iconSize * 2 + implicitHeight: app.iconSize + property var value + signal changed(var value) + + Pane { + anchors.fill: parent + topPadding: 0 + bottomPadding: 0 + leftPadding: 0 + rightPadding: 0 + Material.elevation: 1 + contentItem: Rectangle { + color: colorComponentItem.value + + MouseArea { + anchors.fill: parent + onClicked: { + var pos = colorComponentItem.mapToItem(root, 0, colorComponentItem.height) + print("opening", colorComponentItem.value) + var colorPicker = colorPickerComponent.createObject(root, {preferredY: pos.y, colorValue: colorComponentItem.value }) + colorPicker.open() + } + } + } + } + + Component { + id: colorPickerComponent + Dialog { + id: colorPickerDialog + modal: true + x: (parent.width - width) / 2 + y: Math.min(preferredY, parent.height - height) + width: parent.width - app.margins * 2 + height: 200 + padding: app.margins + property var colorValue + property int preferredY: 0 + contentItem: ColorPicker { + color: colorPickerDialog.colorValue + property var lastSentTime: new Date() + onColorChanged: { + var currentTime = new Date(); + if (pressed && currentTime - lastSentTime > 200) { + colorComponentItem.changed(color); + } + } + } + } + } +} diff --git a/nymea-app/ui/delegates/statedelegates/ComboBoxDelegate.qml b/nymea-app/ui/delegates/statedelegates/ComboBoxDelegate.qml new file mode 100644 index 00000000..0b5662d7 --- /dev/null +++ b/nymea-app/ui/delegates/statedelegates/ComboBoxDelegate.qml @@ -0,0 +1,17 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../../components" + +ComboBox { + property var value + property var possibleValues + + signal changed(var value) + model: possibleValues + currentIndex: possibleValues.indexOf(value) + onActivated: changed(model[index]) + Component.onCompleted: print("completed. values", possibleValues, "value", value) +} diff --git a/nymea-app/ui/delegates/statedelegates/DateTimeDelegate.qml b/nymea-app/ui/delegates/statedelegates/DateTimeDelegate.qml new file mode 100644 index 00000000..ca427599 --- /dev/null +++ b/nymea-app/ui/delegates/statedelegates/DateTimeDelegate.qml @@ -0,0 +1,12 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../../components" + +Label { + property var value + text: Qt.formatDateTime(new Date(value * 1000), Qt.DefaultLocaleShortDate) + horizontalAlignment: Text.AlignRight +} diff --git a/nymea-app/ui/delegates/statedelegates/LabelDelegate.qml b/nymea-app/ui/delegates/statedelegates/LabelDelegate.qml new file mode 100644 index 00000000..8ddf0103 --- /dev/null +++ b/nymea-app/ui/delegates/statedelegates/LabelDelegate.qml @@ -0,0 +1,12 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../../components" + +Label { + property var value + text: value + horizontalAlignment: Text.AlignRight +} diff --git a/nymea-app/ui/delegates/statedelegates/LedDelegate.qml b/nymea-app/ui/delegates/statedelegates/LedDelegate.qml new file mode 100644 index 00000000..22fb94a5 --- /dev/null +++ b/nymea-app/ui/delegates/statedelegates/LedDelegate.qml @@ -0,0 +1,11 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../../components" + +Led { + property bool value + on: value === true +} diff --git a/nymea-app/ui/delegates/statedelegates/ListDelegate.qml b/nymea-app/ui/delegates/statedelegates/ListDelegate.qml new file mode 100644 index 00000000..66c659e5 --- /dev/null +++ b/nymea-app/ui/delegates/statedelegates/ListDelegate.qml @@ -0,0 +1,11 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../../components" + +Label { + property var value + text: value.join(", ") +} diff --git a/nymea-app/ui/delegates/statedelegates/NumberLabelDelegate.qml b/nymea-app/ui/delegates/statedelegates/NumberLabelDelegate.qml new file mode 100644 index 00000000..2ac99a57 --- /dev/null +++ b/nymea-app/ui/delegates/statedelegates/NumberLabelDelegate.qml @@ -0,0 +1,12 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../../components" + +Label { + property var value + text: Math.round(value * 100) / 100 + horizontalAlignment: Text.AlignRight +} diff --git a/nymea-app/ui/delegates/statedelegates/SliderDelegate.qml b/nymea-app/ui/delegates/statedelegates/SliderDelegate.qml new file mode 100644 index 00000000..54dc2848 --- /dev/null +++ b/nymea-app/ui/delegates/statedelegates/SliderDelegate.qml @@ -0,0 +1,57 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../../components" + +RowLayout { + id: root + width: 150 + signal changed(var value) + + property var value + property alias from: slider.from + property alias to: slider.to + + property StateType stateType + + readonly property int decimals: root.stateType.type.toLowerCase() === "int" ? 0 : 1 + + Slider { + id: slider + Layout.fillWidth: true + value: root.value + stepSize: { + var ret = 1 + for (var i = 0; i < root.decimals; i++) { + ret /= 10; + } + return ret; + } + property var lastVibration: new Date() + property var lastChange: root.value + onMoved: { + // Emits moved more often than stepsize, we only want to act when we actually emitted value change + if (value === lastChange) { + return; + } + lastChange = value; + + if (value === from || value === to) { + PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackImpact) + } else { + if (lastVibration.getTime() + 35 < new Date()) { + PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection) + } + lastVibration = new Date() + } + + + root.changed(value) + } + } + Label { + text: slider.value.toFixed(root.decimals) + } +} diff --git a/nymea-app/ui/delegates/statedelegates/SpinBoxDelegate.qml b/nymea-app/ui/delegates/statedelegates/SpinBoxDelegate.qml new file mode 100644 index 00000000..9b6e47fb --- /dev/null +++ b/nymea-app/ui/delegates/statedelegates/SpinBoxDelegate.qml @@ -0,0 +1,16 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../../components" + +SpinBox { + width: 150 + signal changed(var value) + stepSize: (to - from) / 10 + editable: true + onValueModified: { + changed(value) + } +} diff --git a/nymea-app/ui/delegates/statedelegates/SwitchDelegate.qml b/nymea-app/ui/delegates/statedelegates/SwitchDelegate.qml new file mode 100644 index 00000000..f4ccbab1 --- /dev/null +++ b/nymea-app/ui/delegates/statedelegates/SwitchDelegate.qml @@ -0,0 +1,15 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../../components" + +Switch { + property var value + signal changed(var value) + checked: value === true + onClicked: { + changed(checked) + } +} diff --git a/nymea-app/ui/delegates/statedelegates/TextFieldDelegate.qml b/nymea-app/ui/delegates/statedelegates/TextFieldDelegate.qml new file mode 100644 index 00000000..35fc098e --- /dev/null +++ b/nymea-app/ui/delegates/statedelegates/TextFieldDelegate.qml @@ -0,0 +1,11 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../../components" + +TextField { + property var value + text: value +} diff --git a/nymea-app/ui/devicelistpages/SensorsDeviceListPage.qml b/nymea-app/ui/devicelistpages/SensorsDeviceListPage.qml index 4f0d60d3..541e1044 100644 --- a/nymea-app/ui/devicelistpages/SensorsDeviceListPage.qml +++ b/nymea-app/ui/devicelistpages/SensorsDeviceListPage.qml @@ -84,6 +84,8 @@ DeviceListPageBase { ListElement { interfaceName: "co2sensor"; stateName: "co2" } ListElement { interfaceName: "daylightsensor"; stateName: "daylight" } ListElement { interfaceName: "presencesensor"; stateName: "isPresent" } + ListElement { interfaceName: "heating"; stateName: "power" } + ListElement { interfaceName: "thermostat"; stateName: "targetTemperature" } } delegate: RowLayout { @@ -114,9 +116,14 @@ DeviceListPageBase { font.pixelSize: app.smallFont } Led { + id: led visible: sensorValueDelegate.stateType && sensorValueDelegate.stateType.type.toLowerCase() == "bool" on: visible && sensorValueDelegate.stateValue.value === true } + Item { + Layout.preferredWidth: led.width + visible: led.visible + } } } } diff --git a/nymea-app/ui/devicepages/GenericDevicePage.qml b/nymea-app/ui/devicepages/GenericDevicePage.qml index 2c9cc36c..f2d0aae7 100644 --- a/nymea-app/ui/devicepages/GenericDevicePage.qml +++ b/nymea-app/ui/devicepages/GenericDevicePage.qml @@ -120,90 +120,122 @@ DevicePageBase { Loader { id: stateDelegateLoader Layout.fillWidth: true - sourceComponent: { - switch (stateType.type.toLowerCase()) { - case "string": - if (stateDelegate.writable) { - if (stateDelegate.stateType.allowedValues !== undefined) { - return comboBoxComponent - } - return textFieldComponent; - } else { - return labelComponent; - } - case "stringlist": - return listComponent; - case "bool": - if (stateDelegate.writable) { - return switchComponent; - } else { - return ledComponent; - } - case "int": - case "double": - if (stateDelegate.stateType.unit === Types.UnitUnixTime) { - return dateTimeComponent; - } - - if (stateDelegate.writable) { - return sliderComponent; -// return spinBoxComponent; - } - return numberLabelComponent; - case "color": - return colorComponent; - default: - print("Unhandled state type", stateType.displayName, stateType.type) - } - - print("GenericDevicePage: unhandled entry", stateDelegate.stateType.displayName) - } } - - Label { - visible: stateDelegateLoader.sourceComponent === sliderComponent - text: stateDelegate.deviceState.value - } - Label { visible: stateDelegate.stateType.unit !== Types.UnitUnixTime && stateDelegate.stateType.unitString.length > 0 text: stateDelegate.stateType.unitString } + Component.onCompleted: updateLoader() + onStateTypeChanged: updateLoader(); + + function updateLoader() { + if (stateDelegate.stateType == null) { + return; + } + + var isWritable = root.deviceClass.actionTypes.getActionType(stateType.id) !== null; + + var sourceComp; + switch (stateDelegate.stateType.type.toLowerCase()) { + case "string": + if (isWritable) { + if (stateDelegate.stateType.allowedValues !== undefined) { + sourceComp = "ComboBoxDelegate.qml" + } else { + sourceComp = "TextFieldDelegate.qml"; + } + } else { + sourceComp = "LabelDelegate.qml"; + } + break; + case "stringlist": + sourceComp = "ListDelegate.qml"; + break; + case "bool": + if (isWritable) { + sourceComp = "SwitchDelegate.qml"; + } else { + sourceComp = "LedDelegate.qml"; + } + break; + case "int": + case "double": + if (stateDelegate.stateType.unit === Types.UnitUnixTime) { + sourceComp = "DateTimeDelegate.qml"; + } else if (isWritable) { + sourceComp = "SliderDelegate.qml"; +// sourceComp = "SpinBoxDelegate.qml"; + } else { + sourceComp = "NumberLabelDelegate.qml"; + } + break; + case "color": + sourceComp = "ColorDelegate.qml"; + break; + } + if (!sourceComp) { + sourceComp = "LabelDelegate.qml"; + print("GenericDevicePage: unhandled entry", stateDelegate.stateType.displayName) + } + + stateDelegateLoader.setSource("../delegates/statedelegates/" + sourceComp, + { +// value: root.device.states.getState(stateType.id).value, + possibleValues: stateDelegate.stateType.allowedValues, + from: stateDelegate.stateType.minValue, + to: stateDelegate.stateType.maxValue, + stateType: stateDelegate.stateType + }) + } + + property int pendingActionId: -1 + property real valueCache: 0 + property bool valueCacheDirty: false + + function enqueueSetValue(value) { + if (pendingActionId == -1) { + executeAction(value); + return; + } else { + valueCache = value + valueCacheDirty = true; + } + } + + function executeAction(value) { + var params = [] + var param1 = {} + param1["paramTypeId"] = stateDelegate.stateType.id + param1["value"] = value; + params.push(param1) + var actionId = root.executeAction(stateDelegate.stateType.id, params); + stateDelegate.pendingActionId = actionId + } + Binding { target: stateDelegateLoader.item property: "value" - value: root.device.states.getState(stateDelegate.stateType.id).value - } - Binding { - target: stateDelegateLoader.item && stateDelegateLoader.item.hasOwnProperty("possibleValues") ? stateDelegateLoader.item : null - property: "possibleValues" - value: stateDelegate.stateType.allowedValues - } - Binding { - target: stateDelegateLoader.item && stateDelegateLoader.item.hasOwnProperty("from") ? stateDelegateLoader.item : null - property: "from" - value: stateDelegate.stateType.minValue !== undefined ? stateDelegate.stateType.minValue : -999999999999 - } - Binding { - target: stateDelegateLoader.item && stateDelegateLoader.item.hasOwnProperty("to") ? stateDelegateLoader.item : null - property: "to" - value: stateDelegate.stateType.maxValue !== undefined ? stateDelegate.stateType.maxValue : 999999999999 - } - Binding { - target: stateDelegateLoader.item && stateDelegateLoader.item.hasOwnProperty("actionTypeId") ? stateDelegateLoader.item : null - property: "actionTypeId" - value: stateDelegate.stateType.id + value: stateDelegate.deviceState.value + when: !stateDelegate.valueCacheDirty && stateDelegate.pendingActionId === -1 } + Connections { target: stateDelegateLoader.item && stateDelegateLoader.item.hasOwnProperty("changed") ? stateDelegateLoader.item : null onChanged: { - var params = [] - var param1 = {} - param1["paramTypeId"] = stateDelegate.stateType.id - param1["value"] = value; - params.push(param1) - root.executeAction(stateDelegate.stateType.id, params); + stateDelegate.enqueueSetValue(value) + } + } + Connections { + target: engine.deviceManager + onExecuteActionReply: { + if (stateDelegate.pendingActionId === params.id) { + stateDelegate.pendingActionId = -1 + if (stateDelegate.valueCacheDirty) { + stateDelegate.executeAction(stateDelegate.valueCache) + stateDelegate.valueCacheDirty = false; + } + } } } } @@ -223,7 +255,6 @@ DevicePageBase { target: engine.deviceManager onExecuteActionReply: { if (params["id"] === actionDelegate.pendingActionId) { - print("blubb!") pendingTimer.start(); actionDelegate.lastSuccess = params["params"]["deviceError"] === "DeviceErrorNoError" actionDelegate.pendingActionId = -1 @@ -355,163 +386,4 @@ DevicePageBase { } } } - - Component { - id: ledComponent - Led { - property bool value - on: value === true - } - } - - Component { - id: labelComponent - Label { - property var value - text: value - } - } - Component { - id: numberLabelComponent - Label { - property var value - text: Math.round(value * 100) / 100 - } - } - Component { - id: textFieldComponent - TextField { - property var value - text: value - } - } - - Component { - id: listComponent - Label { - property var value - text: value.join(", ") - } - } - - Component { - id: checkBoxComponent - CheckBox { - property var value - checked: value === true - enabled: false - } - } - - Component { - id: switchComponent - Switch { - property var value - signal changed(var value) - checked: value === true - onClicked: { - changed(checked) - } - } - } - - Component { - id: spinBoxComponent - SpinBox { - width: 150 - signal changed(var value) - stepSize: (to - from) / 20 - editable: true - onValueModified: { - changed(value) - } - } - } - - Component { - id: sliderComponent - ThrottledSlider { - signal changed(var value) - stepSize: 1 - onMoved: changed(value) - } - } - - Component { - id: comboBoxComponent - ComboBox { - property var value - property var possibleValues - - signal changed(var value) - model: possibleValues - onActivated: changed(model[index]) - } - } - - Component { - id: colorComponent - Item { - id: colorComponentItem - implicitWidth: app.iconSize * 2 - implicitHeight: app.iconSize - property var value - signal changed(var value) - - Pane { - anchors.fill: parent - topPadding: 0 - bottomPadding: 0 - leftPadding: 0 - rightPadding: 0 - Material.elevation: 1 - contentItem: Rectangle { - color: colorComponentItem.value - - MouseArea { - anchors.fill: parent - onClicked: { - var pos = colorComponentItem.mapToItem(root, 0, colorComponentItem.height) - print("opening", colorComponentItem.value) - var colorPicker = colorPickerComponent.createObject(root, {preferredY: pos.y, colorValue: colorComponentItem.value }) - colorPicker.open() - } - } - } - } - - Component { - id: colorPickerComponent - Dialog { - id: colorPickerDialog - modal: true - x: (parent.width - width) / 2 - y: Math.min(preferredY, parent.height - height) - width: parent.width - app.margins * 2 - height: 200 - padding: app.margins - property var colorValue - property int preferredY: 0 - contentItem: ColorPicker { - color: colorPickerDialog.colorValue - property var lastSentTime: new Date() - onColorChanged: { - var currentTime = new Date(); - if (pressed && currentTime - lastSentTime > 200) { - colorComponentItem.changed(color); - } - } - } - } - } - } - } - - Component { - id: dateTimeComponent - Label { - property var value - text: Qt.formatDateTime(new Date(value * 1000), Qt.DefaultLocaleShortDate) - } - } } diff --git a/nymea-app/ui/devicepages/HeatingDevicePage.qml b/nymea-app/ui/devicepages/HeatingDevicePage.qml new file mode 100644 index 00000000..64afdbd7 --- /dev/null +++ b/nymea-app/ui/devicepages/HeatingDevicePage.qml @@ -0,0 +1,81 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.2 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../components" +import "../customviews" + +DevicePageBase { + id: root + + readonly property bool landscape: width > height + + readonly property StateType targetTemperatureStateType: device.deviceClass.stateTypes.findByName("targetTemperature") + readonly property State targetTemperatureState: targetTemperatureStateType ? device.states.getState(targetTemperatureStateType.id) : null + readonly property StateType powerStateType: deviceClass.stateTypes.findByName("power") + readonly property State powerState: powerStateType ? device.states.getState(powerStateType.id) : null + readonly property StateType temperatureStateType: device.deviceClass.stateTypes.findByName("temperature") + readonly property State temperatureState: temperatureStateType ? device.states.getState(temperatureStateType.id) : null + readonly property StateType percentageStateType: device.deviceClass.stateTypes.findByName("percentage") + readonly property State percentageState: percentageStateType ? device.states.getState(percentageStateType.id) : null + // TODO: should this be an interface? e.g. extendedthermostat + readonly property StateType boostStateType: device.deviceClass.stateTypes.findByName("boost") + readonly property State boostState: boostStateType ? device.states.getState(boostStateType.id) : null + + + GridLayout { + anchors.fill: parent + anchors.margins: app.margins + columns: app.landscape ? 2 : 1 + + Dial { + id: dial + Layout.fillWidth: true + Layout.fillHeight: true + visible: root.targetTemperatureStateType || root.percentageStateType + + device: root.device + stateType: root.targetTemperatureStateType ? root.targetTemperatureStateType : root.percentageStateType + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 50 + visible: root.boostStateType + border.color: boostMouseArea.pressed || root.boostStateType && root.boostState.value === true ? app.accentColor : app.foregroundColor + border.width: 1 + radius: height / 2 + color: root.boostStateType && root.boostState.value === true ? app.accentColor : "transparent" + + Row { + anchors.centerIn: parent + spacing: app.margins / 2 + ColorIcon { + height: app.iconSize + width: app.iconSize + name: "../images/sensors/temperature.svg" + color: root.boostStateType && root.boostState.value === true ? "red" : keyColor + } + + Label { + text: qsTr("Boost") + anchors.verticalCenter: parent.verticalCenter + } + } + MouseArea { + id: boostMouseArea + anchors.fill: parent + onPressedChanged: PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackImpact) + onClicked: { + var params = [] + var param = {} + param["paramTypeId"] = root.boostStateType.id + param["value"] = !root.boostState.value + params.push(param) + engine.deviceManager.executeAction(root.device.id, root.boostStateType.id, params); + } + } + } + } +} diff --git a/nymea-app/ui/devicepages/LightDevicePage.qml b/nymea-app/ui/devicepages/LightDevicePage.qml index 612efc36..41f63742 100644 --- a/nymea-app/ui/devicepages/LightDevicePage.qml +++ b/nymea-app/ui/devicepages/LightDevicePage.qml @@ -33,45 +33,59 @@ DevicePageBase { columnSpacing: app.margins Layout.alignment: Qt.AlignCenter - Item { - Layout.preferredWidth: Math.max(app.iconSize * 4, parent.width / 5) + Dial { + Layout.minimumWidth: app.landscape ? parent.width / 3 :app.iconSize * 4 Layout.preferredHeight: width + Layout.fillWidth: true Layout.topMargin: app.margins Layout.bottomMargin: app.landscape ? app.margins : 0 Layout.alignment: Qt.AlignCenter - Layout.rowSpan: app.landscape ? 4 : 1 + Layout.rowSpan: app.landscape ? 3 : 1 Layout.fillHeight: true - - AbstractButton { - height: Math.min(parent.height, parent.width) - width: height - anchors.centerIn: parent - Rectangle { - anchors.fill: parent - color: "white" - border.color: root.powerState.value === true ? app.accentColor : bulbIcon.keyColor - border.width: 4 - radius: width / 2 - } - - ColorIcon { - id: bulbIcon - anchors.fill: parent - anchors.margins: app.margins * 1.5 - name: root.powerState.value === true ? "../images/light-on.svg" : "../images/light-off.svg" - color: root.powerState.value === true ? app.accentColor : keyColor - } - onClicked: { - var params = [] - var param = {} - param["paramTypeId"] = root.powerActionType.paramTypes.get(0).id; - param["value"] = !root.powerState.value; - params.push(param) - engine.deviceManager.executeAction(root.device.id, root.powerStateType.id, params); - } - } + device: root.device + stateType: root.brightnessStateType + showValueLabel: false } +// Item { +// Layout.preferredWidth: Math.max(app.iconSize * 4, parent.width / 5) +// Layout.preferredHeight: width +// Layout.topMargin: app.margins +// Layout.bottomMargin: app.landscape ? app.margins : 0 +// Layout.alignment: Qt.AlignCenter +// Layout.rowSpan: app.landscape ? 4 : 1 +// Layout.fillHeight: true + +// AbstractButton { +// height: Math.min(parent.height, parent.width) +// width: height +// anchors.centerIn: parent +// Rectangle { +// anchors.fill: parent +// color: "white" +// border.color: root.powerState.value === true ? app.accentColor : bulbIcon.keyColor +// border.width: 4 +// radius: width / 2 +// } + +// ColorIcon { +// id: bulbIcon +// anchors.fill: parent +// anchors.margins: app.margins * 1.5 +// name: root.powerState.value === true ? "../images/light-on.svg" : "../images/light-off.svg" +// color: root.powerState.value === true ? app.accentColor : keyColor +// } +// onClicked: { +// var params = [] +// var param = {} +// param["paramTypeId"] = root.powerActionType.paramTypes.get(0).id; +// param["value"] = !root.powerState.value; +// params.push(param) +// engine.deviceManager.executeAction(root.device.id, root.powerStateType.id, params); +// } +// } +// } + RowLayout { Layout.fillHeight: true @@ -121,34 +135,34 @@ DevicePageBase { } } - Rectangle { - color: "blue" - Layout.fillWidth: true - Layout.fillHeight: true - Layout.minimumHeight: 20 - Layout.preferredHeight: 20 - visible: root.brightnessStateType +// Rectangle { +// color: "blue" +// Layout.fillWidth: true +// Layout.fillHeight: true +// Layout.minimumHeight: 20 +// Layout.preferredHeight: 20 +// visible: root.brightnessStateType - Pane { - anchors { left: parent.left; right: parent.right; verticalCenter: parent.verticalCenter } - height: parent.height - Material.elevation: 1 - padding: 0 +// Pane { +// anchors { left: parent.left; right: parent.right; verticalCenter: parent.verticalCenter } +// height: parent.height +// Material.elevation: 1 +// padding: 0 - BrightnessSlider { - anchors.fill: parent - brightness: root.brightnessState ? root.brightnessState.value : 0 - onMoved: { - var params = [] - var param = {} - param["paramTypeId"] = root.brightnessActionType.paramTypes.get(0).id; - param["value"] = brightness; - params.push(param) - engine.deviceManager.executeAction(root.device.id, root.brightnessActionType.id, params); - } - } - } - } +// BrightnessSlider { +// anchors.fill: parent +// brightness: root.brightnessState ? root.brightnessState.value : 0 +// onMoved: { +// var params = [] +// var param = {} +// param["paramTypeId"] = root.brightnessActionType.paramTypes.get(0).id; +// param["value"] = brightness; +// params.push(param) +// engine.deviceManager.executeAction(root.device.id, root.brightnessActionType.id, params); +// } +// } +// } +// } Rectangle { diff --git a/nymea-app/ui/images/dial.svg b/nymea-app/ui/images/dial.svg new file mode 100644 index 00000000..d73f77d6 --- /dev/null +++ b/nymea-app/ui/images/dial.svg @@ -0,0 +1,170 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/mainviews/DevicesPageDelegate.qml b/nymea-app/ui/mainviews/DevicesPageDelegate.qml index 23cb44fe..df8cda1e 100644 --- a/nymea-app/ui/mainviews/DevicesPageDelegate.qml +++ b/nymea-app/ui/mainviews/DevicesPageDelegate.qml @@ -16,6 +16,7 @@ MainPageTile { onClicked: { var page; switch (model.name) { + case "heating": case "sensor": page = "SensorsDeviceListPage.qml" break; @@ -91,6 +92,7 @@ MainPageTile { case "smartmeterproducer": case "extendedsmartmeterconsumer": case "extendedsmartmeterproducer": + case "heating": return sensorComponent; // return labelComponent; @@ -430,6 +432,7 @@ MainPageTile { ListElement { ifaceName: "noisesensor"; stateName: "noise" } ListElement { ifaceName: "smartmeterconsumer"; stateName: "totalEnergyConsumed" } ListElement { ifaceName: "smartmeterproducer"; stateName: "totalEnergyProduced" } + ListElement { ifaceName: "thermostat"; stateName: "targetTemperature" } } function findSensors(deviceClass) { var ret = [] diff --git a/packaging/android/AndroidManifest.xml b/packaging/android/AndroidManifest.xml index d911e010..f59d1f71 100644 --- a/packaging/android/AndroidManifest.xml +++ b/packaging/android/AndroidManifest.xml @@ -85,4 +85,5 @@ + diff --git a/packaging/android/src/io/guh/nymeaapp/NymeaAppActivity.java b/packaging/android/src/io/guh/nymeaapp/NymeaAppActivity.java index c1f57fe8..6b6785ae 100644 --- a/packaging/android/src/io/guh/nymeaapp/NymeaAppActivity.java +++ b/packaging/android/src/io/guh/nymeaapp/NymeaAppActivity.java @@ -6,6 +6,7 @@ import android.os.Bundle; import android.os.Build; import android.telephony.TelephonyManager; import android.provider.Settings.Secure; +import android.os.Vibrator; //import com.google.firebase.messaging.MessageForwardingService; @@ -27,6 +28,12 @@ public class NymeaAppActivity extends org.qtproject.qt5.android.bindings.QtActiv return Build.MODEL; } + public void vibrate(int duration) + { + Vibrator v = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); + v.vibrate(duration); + } + // // The key in the intent's extras that maps to the incoming message's message ID. Only sent by // // the server, GmsCore sends EXTRA_MESSAGE_ID_KEY below. Server can't send that as it would get // // stripped by the client. diff --git a/packaging/ios/platformhelperios.mm b/packaging/ios/platformhelperios.mm index d7d58519..f7f5c919 100644 --- a/packaging/ios/platformhelperios.mm +++ b/packaging/ios/platformhelperios.mm @@ -1,10 +1,12 @@ #import #import +#import #include #include "platformintegration/ios/platformhelperios.h" + QString PlatformHelperIOS::readKeyChainEntry(const QString &service, const QString &key) { NSDictionary *const query = @{ @@ -65,3 +67,35 @@ void PlatformHelperIOS::writeKeyChainEntry(const QString &service, const QString qWarning() << "Error storing value in keycahin" << status; } } + + +void PlatformHelperIOS::generateSelectionFeedback() +{ + UISelectionFeedbackGenerator *generator = [[UISelectionFeedbackGenerator alloc] init]; + [generator prepare]; + [generator selectionChanged]; + generator = nil; +} + +void PlatformHelperIOS::generateImpactFeedback() +{ + // UIImpactFeedbackStyleLight + // UIImpactFeedbackStyleMedium + // UIImpactFeedbackStyleHeavy + UIImpactFeedbackGenerator *generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight]; + [generator prepare]; + [generator impactOccurred]; + generator = nil; +} + +void PlatformHelperIOS::generateNotificationFeedback() +{ +// UINotificationFeedbackTypeSuccess +// UINotificationFeedbackTypeWarning +// UINotificationFeedbackTypeError + + UINotificationFeedbackGenerator *generator = [[UINotificationFeedbackGenerator alloc] init]; + [generator prepare]; + [generator notificationOccurred:UINotificationFeedbackTypeSuccess]; + generator = nil; +}