Merge PR #132: Add support for the thermostat interface
This commit is contained in:
commit
ba9cac9b1b
@ -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");
|
||||
}
|
||||
|
||||
@ -169,5 +169,6 @@
|
||||
<file>ui/images/stock_video.svg</file>
|
||||
<file>ui/images/sensors/presence.svg</file>
|
||||
<file>ui/images/powersocket.svg</file>
|
||||
<file>ui/images/dial.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@ -5,3 +5,4 @@ PlatformHelper::PlatformHelper(QObject *parent) : QObject(parent)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
};
|
||||
|
||||
@ -44,6 +44,24 @@ QString PlatformHelperAndroid::deviceManufacturer() const
|
||||
return QAndroidJniObject::callStaticObjectMethod<jstring>("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<void>("vibrate","(I)V", duration);
|
||||
}
|
||||
|
||||
void PlatformHelperAndroid::permissionRequestFinished(const QtAndroid::PermissionResultMap &result)
|
||||
{
|
||||
foreach (const QString &key, result.keys()) {
|
||||
|
||||
@ -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 &);
|
||||
|
||||
|
||||
@ -38,3 +38,8 @@ QString PlatformHelperGeneric::deviceManufacturer() const
|
||||
{
|
||||
return QSysInfo::productType();
|
||||
}
|
||||
|
||||
void PlatformHelperGeneric::vibrate(PlatformHelper::HapticsFeedback feedbyckType)
|
||||
{
|
||||
Q_UNUSED(feedbyckType)
|
||||
}
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -147,5 +147,19 @@
|
||||
<file>ui/devicepages/PowersocketDevicePage.qml</file>
|
||||
<file>ui/devicelistpages/PowerSocketsDeviceListPage.qml</file>
|
||||
<file>ui/components/GroupedListView.qml</file>
|
||||
<file>ui/devicepages/HeatingDevicePage.qml</file>
|
||||
<file>ui/delegates/statedelegates/LedDelegate.qml</file>
|
||||
<file>ui/delegates/statedelegates/LabelDelegate.qml</file>
|
||||
<file>ui/delegates/statedelegates/NumberLabelDelegate.qml</file>
|
||||
<file>ui/delegates/statedelegates/TextFieldDelegate.qml</file>
|
||||
<file>ui/delegates/statedelegates/ListDelegate.qml</file>
|
||||
<file>ui/delegates/statedelegates/CheckboxDelegate.qml</file>
|
||||
<file>ui/delegates/statedelegates/SwitchDelegate.qml</file>
|
||||
<file>ui/delegates/statedelegates/SpinBoxDelegate.qml</file>
|
||||
<file>ui/delegates/statedelegates/ComboBoxDelegate.qml</file>
|
||||
<file>ui/delegates/statedelegates/ColorDelegate.qml</file>
|
||||
<file>ui/delegates/statedelegates/DateTimeDelegate.qml</file>
|
||||
<file>ui/components/Dial.qml</file>
|
||||
<file>ui/delegates/statedelegates/SliderDelegate.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@ -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) {
|
||||
|
||||
296
nymea-app/ui/components/Dial.qml
Normal file
296
nymea-app/ui/components/Dial.qml
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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,9 +106,6 @@ ItemDelegate {
|
||||
id: sliderComponent
|
||||
RowLayout {
|
||||
spacing: app.margins
|
||||
Label {
|
||||
text: root.paramType.minValue
|
||||
}
|
||||
Slider {
|
||||
id: slider
|
||||
Layout.fillWidth: true
|
||||
@ -114,12 +113,13 @@ ItemDelegate {
|
||||
to: root.paramType.maxValue
|
||||
value: root.param.value
|
||||
stepSize: {
|
||||
switch (root.paramType.type.toLowerCase()) {
|
||||
case "int":
|
||||
return 1;
|
||||
var ret = 1
|
||||
for (var i = 0; i < decimals; i++) {
|
||||
ret /= 10;
|
||||
}
|
||||
return 0.01;
|
||||
return ret;
|
||||
}
|
||||
property int decimals: root.paramType.type.toLocaleLowerCase() === "int" ? 0 : 1
|
||||
|
||||
onMoved: {
|
||||
var newValue
|
||||
@ -134,7 +134,7 @@ ItemDelegate {
|
||||
}
|
||||
}
|
||||
Label {
|
||||
text: root.paramType.maxValue
|
||||
text: root.param.value.toFixed(slider.decimals) + root.paramType.unitString
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
12
nymea-app/ui/delegates/statedelegates/CheckboxDelegate.qml
Normal file
12
nymea-app/ui/delegates/statedelegates/CheckboxDelegate.qml
Normal file
@ -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
|
||||
}
|
||||
61
nymea-app/ui/delegates/statedelegates/ColorDelegate.qml
Normal file
61
nymea-app/ui/delegates/statedelegates/ColorDelegate.qml
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
nymea-app/ui/delegates/statedelegates/ComboBoxDelegate.qml
Normal file
17
nymea-app/ui/delegates/statedelegates/ComboBoxDelegate.qml
Normal file
@ -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)
|
||||
}
|
||||
12
nymea-app/ui/delegates/statedelegates/DateTimeDelegate.qml
Normal file
12
nymea-app/ui/delegates/statedelegates/DateTimeDelegate.qml
Normal file
@ -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
|
||||
}
|
||||
12
nymea-app/ui/delegates/statedelegates/LabelDelegate.qml
Normal file
12
nymea-app/ui/delegates/statedelegates/LabelDelegate.qml
Normal file
@ -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
|
||||
}
|
||||
11
nymea-app/ui/delegates/statedelegates/LedDelegate.qml
Normal file
11
nymea-app/ui/delegates/statedelegates/LedDelegate.qml
Normal file
@ -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
|
||||
}
|
||||
11
nymea-app/ui/delegates/statedelegates/ListDelegate.qml
Normal file
11
nymea-app/ui/delegates/statedelegates/ListDelegate.qml
Normal file
@ -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(", ")
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
57
nymea-app/ui/delegates/statedelegates/SliderDelegate.qml
Normal file
57
nymea-app/ui/delegates/statedelegates/SliderDelegate.qml
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
16
nymea-app/ui/delegates/statedelegates/SpinBoxDelegate.qml
Normal file
16
nymea-app/ui/delegates/statedelegates/SpinBoxDelegate.qml
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
15
nymea-app/ui/delegates/statedelegates/SwitchDelegate.qml
Normal file
15
nymea-app/ui/delegates/statedelegates/SwitchDelegate.qml
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
11
nymea-app/ui/delegates/statedelegates/TextFieldDelegate.qml
Normal file
11
nymea-app/ui/delegates/statedelegates/TextFieldDelegate.qml
Normal file
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,89 +113,129 @@ DevicePageBase {
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: parent.width / 2
|
||||
text: stateDelegate.stateType.displayName
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
Loader {
|
||||
id: stateDelegateLoader
|
||||
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 spinBoxComponent;
|
||||
}
|
||||
return numberLabelComponent;
|
||||
case "color":
|
||||
return colorComponent;
|
||||
default:
|
||||
print("Unhandled state type", stateType.displayName, stateType.type)
|
||||
}
|
||||
|
||||
print("GenericDevicePage: unhandled entry", stateDelegate.stateType.displayName)
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -215,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
|
||||
@ -347,154 +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) / 10
|
||||
editable: true
|
||||
onValueModified: {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
81
nymea-app/ui/devicepages/HeatingDevicePage.qml
Normal file
81
nymea-app/ui/devicepages/HeatingDevicePage.qml
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
170
nymea-app/ui/images/dial.svg
Normal file
170
nymea-app/ui/images/dial.svg
Normal file
@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
viewBox="0 0 96 96.000001"
|
||||
version="1.1"
|
||||
id="svg4874"
|
||||
height="96"
|
||||
width="96"
|
||||
sodipodi:docname="dial.svg"
|
||||
inkscape:version="0.92.3 (2405546, 2018-03-11)">
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="2792"
|
||||
inkscape:window-height="1698"
|
||||
id="namedview842"
|
||||
showgrid="true"
|
||||
inkscape:zoom="6.9947917"
|
||||
inkscape:cx="77.052782"
|
||||
inkscape:cy="81.297134"
|
||||
inkscape:window-x="88"
|
||||
inkscape:window-y="44"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg4874">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid844" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs4876" />
|
||||
<metadata
|
||||
id="metadata4879">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<rect
|
||||
transform="rotate(90)"
|
||||
y="-95.999985"
|
||||
x="8.5593265e-06"
|
||||
height="96"
|
||||
width="96"
|
||||
id="rect4782"
|
||||
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:3.99920893;marker:none;enable-background:accumulate" />
|
||||
<circle
|
||||
cy="48"
|
||||
cx="48"
|
||||
id="path833"
|
||||
style="fill:none;fill-opacity:1;stroke:#808080;stroke-width:3.99999976;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
r="34.642204" />
|
||||
<circle
|
||||
r="2.5"
|
||||
cy="-28.688278"
|
||||
cx="19.022213"
|
||||
id="circle944"
|
||||
style="opacity:1;fill:#808080;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
transform="rotate(70)" />
|
||||
<circle
|
||||
style="opacity:1;fill:#808080;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle950"
|
||||
cx="25.123938"
|
||||
cy="-5.916328"
|
||||
r="2.5"
|
||||
transform="rotate(50)" />
|
||||
<circle
|
||||
r="2.5"
|
||||
cy="17.56922"
|
||||
cx="23.06922"
|
||||
id="circle956"
|
||||
style="opacity:1;fill:#808080;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
transform="rotate(30)" />
|
||||
<circle
|
||||
style="opacity:1;fill:#808080;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle962"
|
||||
cx="13.105885"
|
||||
cy="38.935661"
|
||||
r="2.5"
|
||||
transform="rotate(10)" />
|
||||
<circle
|
||||
r="2.5"
|
||||
cy="55.605885"
|
||||
cx="-3.5643404"
|
||||
id="circle968"
|
||||
style="opacity:1;fill:#808080;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
transform="rotate(-10)" />
|
||||
<circle
|
||||
style="opacity:1;fill:#808080;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle974"
|
||||
cx="-24.93078"
|
||||
cy="65.569221"
|
||||
r="2.5"
|
||||
transform="rotate(-30)" />
|
||||
<circle
|
||||
r="2.5"
|
||||
cy="-17.56922"
|
||||
cx="-108.06922"
|
||||
id="circle980"
|
||||
style="opacity:1;fill:#808080;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
transform="rotate(-150)" />
|
||||
<circle
|
||||
style="opacity:1;fill:#808080;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle986"
|
||||
cx="-98.105888"
|
||||
cy="-38.935661"
|
||||
r="2.5"
|
||||
transform="rotate(-170)" />
|
||||
<circle
|
||||
r="2.5"
|
||||
cy="-55.605885"
|
||||
cx="-81.435661"
|
||||
id="circle992"
|
||||
style="opacity:1;fill:#808080;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
transform="rotate(170)" />
|
||||
<circle
|
||||
style="opacity:1;fill:#808080;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle998"
|
||||
cx="-60.069218"
|
||||
cy="-65.569221"
|
||||
r="2.5"
|
||||
transform="rotate(150)" />
|
||||
<circle
|
||||
style="opacity:1;fill:#808080;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle1010"
|
||||
cx="-13.811721"
|
||||
cy="-61.522213"
|
||||
r="2.5"
|
||||
transform="rotate(110)" />
|
||||
<circle
|
||||
style="opacity:1;fill:#808080;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path936"
|
||||
cx="5.5"
|
||||
cy="-48"
|
||||
r="2.5"
|
||||
transform="rotate(90)" />
|
||||
<circle
|
||||
r="2.5"
|
||||
cy="-67.62394"
|
||||
cx="-36.583672"
|
||||
id="circle1024"
|
||||
style="opacity:1;fill:#808080;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
transform="rotate(130)" />
|
||||
<rect
|
||||
y="-25.083672"
|
||||
x="65.12394"
|
||||
height="17"
|
||||
width="5"
|
||||
id="rect1028"
|
||||
style="opacity:1;fill:#808080;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
transform="rotate(40)"
|
||||
rx="3"
|
||||
ry="3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.5 KiB |
@ -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 = []
|
||||
|
||||
@ -85,4 +85,5 @@
|
||||
<!-- The following comment will be replaced upon deployment with default features based on the dependencies of the application.
|
||||
Remove the comment if you do not require these default features. -->
|
||||
<!-- %%INSERT_FEATURES -->
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
</manifest>
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Security/Security.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#include <QtDebug>
|
||||
#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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user