Merge PR #132: Add support for the thermostat interface

This commit is contained in:
Jenkins 2019-01-29 12:55:43 +01:00
commit ba9cac9b1b
36 changed files with 1118 additions and 288 deletions

View File

@ -298,7 +298,7 @@ void DeviceManager::editDeviceResponse(const QVariantMap &params)
void DeviceManager::executeActionResponse(const QVariantMap &params)
{
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 &params)
{
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");
}

View File

@ -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>

View File

@ -5,3 +5,4 @@ PlatformHelper::PlatformHelper(QObject *parent) : QObject(parent)
{
}

View File

@ -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();
};

View File

@ -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()) {

View File

@ -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 &);

View File

@ -38,3 +38,8 @@ QString PlatformHelperGeneric::deviceManufacturer() const
{
return QSysInfo::productType();
}
void PlatformHelperGeneric::vibrate(PlatformHelper::HapticsFeedback feedbyckType)
{
Q_UNUSED(feedbyckType)
}

View File

@ -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:

View File

@ -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;
}
}

View File

@ -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

View File

@ -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>

View File

@ -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) {

View 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;
}
}
}
}

View File

@ -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 {

View File

@ -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
}
}

View 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
}

View 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);
}
}
}
}
}
}

View 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)
}

View 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
}

View 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
}

View 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
}

View 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(", ")
}

View 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: Math.round(value * 100) / 100
horizontalAlignment: Text.AlignRight
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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
}

View File

@ -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
}
}
}
}

View File

@ -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)
}
}
}

View 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);
}
}
}
}
}

View File

@ -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 {

View 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

View File

@ -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 = []

View File

@ -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>

View File

@ -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.

View File

@ -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;
}