458 lines
17 KiB
QML
458 lines
17 KiB
QML
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
*
|
|
* Copyright (C) 2013 - 2024, nymea GmbH
|
|
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
|
|
*
|
|
* This file is part of nymea-app.
|
|
*
|
|
* nymea-app is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* nymea-app is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with nymea-app. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import QtQuick.Controls
|
|
import QtQuick.Controls.Material
|
|
import Qt5Compat.GraphicalEffects
|
|
import Nymea
|
|
|
|
import "qrc:/ui/components"
|
|
|
|
Item {
|
|
id: root
|
|
readonly property string title: qsTr("Celsi°s")
|
|
readonly property string icon: Qt.resolvedUrl("qrc:/icons/radiator.svg")
|
|
|
|
readonly property Thing duwWpDevice: duwWpFilterModel.count > 0 ? duwWpFilterModel.get(0) : null
|
|
readonly property Thing duwLuDevice: duwLuFilterModel.count > 0 ? duwLuFilterModel.get(0) : null
|
|
|
|
readonly property State temperatureState: duwWpDevice ? duwWpDevice.states.getState(duwWpDevice.thingClass.stateTypes.findByName("temperature").id) : null
|
|
readonly property State targetTemperatureState: duwWpDevice ? duwWpDevice.states.getState(duwWpDevice.thingClass.stateTypes.findByName("targetTemperature").id) : null
|
|
readonly property State co2LevelState: duwLuDevice ? duwLuDevice.states.getState(duwLuDevice.thingClass.stateTypes.findByName("co2").id) : null
|
|
readonly property State ventilationModeState: duwLuDevice ? duwLuDevice.states.getState(duwLuDevice.thingClass.stateTypes.findByName("ventilationMode").id) : null
|
|
readonly property State ventilationLevelState: duwLuDevice ? duwLuDevice.states.getState(duwLuDevice.thingClass.stateTypes.findByName("activeVentilationLevel").id) : null
|
|
|
|
function ventilationModeToSliderValue(ventilationMode) {
|
|
switch (ventilationMode) {
|
|
case "Automatic":
|
|
case "Party":
|
|
return 0
|
|
case "Manual level 0":
|
|
return 0;
|
|
case "Manual level 1":
|
|
return 1;
|
|
case "Manual level 2":
|
|
return 2;
|
|
case "Manual level 3":
|
|
return 3;
|
|
}
|
|
return 0;
|
|
}
|
|
function ventilationModeToUiMode(ventilationMode) {
|
|
switch (ventilationMode) {
|
|
case "Automatic":
|
|
return 0
|
|
case "Party":
|
|
return 1;
|
|
case "Manual level 0":
|
|
case "Manual level 1":
|
|
case "Manual level 2":
|
|
case "Manual level 3":
|
|
return 2;
|
|
}
|
|
}
|
|
|
|
function uiModeToVentilationMode(uiMode, sliderValue) {
|
|
switch (uiMode) {
|
|
case 0:
|
|
return "Automatic";
|
|
case 1:
|
|
return "Party";
|
|
case 2:
|
|
return "Manual level " + Math.floor(sliderValue)
|
|
}
|
|
}
|
|
|
|
function setVentilationMode(uiModeIndex, sliderIndex) {
|
|
var params =[];
|
|
var param = {};
|
|
param["paramTypeId"] = root.ventilationModeState.stateTypeId
|
|
param["value"] = root.uiModeToVentilationMode(uiModeIndex, sliderIndex)
|
|
params.push(param)
|
|
engine.thingManager.executeAction(root.duwLuDevice.id, root.ventilationModeState.stateTypeId, params)
|
|
}
|
|
|
|
function setTargetTemp(targetTemp) {
|
|
// We don't want to spam with set value calls so we're going to queue them up and only send one at a time
|
|
d.queuedTargetTemp = targetTemp;
|
|
if (d.pendingCallId != -1) {
|
|
d.setTempPending = true;
|
|
return;
|
|
}
|
|
var params = []
|
|
var param = {}
|
|
param["paramTypeId"] = root.targetTemperatureState.stateTypeId
|
|
param["value"] = targetTemp
|
|
params.push(param)
|
|
d.pendingCallId = engine.thingManager.executeAction(root.duwWpDevice.id, root.targetTemperatureState.stateTypeId, params)
|
|
d.setTempPending = false;
|
|
}
|
|
|
|
Connections {
|
|
target: engine.thingManager
|
|
onExecuteActionReply: (commandId, thingError, displayMessage) => {
|
|
print("executeActionReply:", commandId)
|
|
if (commandId === d.pendingCallId) {
|
|
d.pendingCallId = -1;
|
|
if (d.setTempPending) {
|
|
setTargetTemp(d.queuedTargetTemp)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
QtObject {
|
|
id: d
|
|
property int pendingCallId: -1
|
|
property bool setTempPending: false
|
|
property real queuedTargetTemp: 0
|
|
}
|
|
|
|
ThingsProxy {
|
|
id: duwWpFilterModel
|
|
engine: _engine
|
|
shownThingClassIds: ["e548f962-92db-4110-8279-10fbcde35f93"]
|
|
}
|
|
|
|
ThingsProxy {
|
|
id: duwLuFilterModel
|
|
engine: _engine
|
|
shownThingClassIds: ["0de8e21e-392a-4790-a78a-b1a7eaa7571b"]
|
|
}
|
|
|
|
EmptyViewPlaceholder {
|
|
anchors.centerIn: parent
|
|
width: parent.width - app.margins * 2
|
|
text: qsTr("There is no drexel und weiss heating system set up yet.")
|
|
imageSource: "qrc:/icons/radiator.svg"
|
|
buttonVisible: false
|
|
buttonText: qsTr("Set up now")
|
|
visible: duwWpFilterModel.count === 0 && !engine.thingManager.fetchingData
|
|
}
|
|
|
|
|
|
Item {
|
|
id: mainView
|
|
anchors.fill: parent
|
|
visible: root.duwWpDevice !== null
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: app.margins
|
|
|
|
ColumnLayout {
|
|
|
|
Label {
|
|
text: qsTr("Current air quality")
|
|
font.pixelSize: app.smallFont
|
|
}
|
|
|
|
RowLayout {
|
|
spacing: app.margins
|
|
ColorIcon {
|
|
Layout.preferredHeight: Style.iconSize
|
|
Layout.preferredWidth: Style.iconSize
|
|
name: "qrc:/icons/weathericons/wind.svg"
|
|
color: Style.accentColor
|
|
}
|
|
Led {
|
|
state: {
|
|
if (!root.co2LevelState) {
|
|
return "off"
|
|
}
|
|
if (root.co2LevelState.value < 600) {
|
|
return "green"
|
|
}
|
|
if (root.co2LevelState.value < 1200) {
|
|
return "orange"
|
|
}
|
|
return "red"
|
|
}
|
|
}
|
|
}
|
|
|
|
Label {
|
|
text: qsTr("Current temperature")
|
|
font.pixelSize: app.smallFont
|
|
}
|
|
|
|
RowLayout {
|
|
ColorIcon {
|
|
Layout.preferredHeight: Style.iconSize
|
|
Layout.preferredWidth: Style.iconSize
|
|
name: "qrc:/icons/sensors/temperature.svg"
|
|
color: Style.accentColor
|
|
}
|
|
Label {
|
|
text: root.temperatureState ? root.temperatureState.value.toFixed(1) + "°C" : "N/A"
|
|
Layout.fillWidth: true
|
|
font.pixelSize: app.largeFont * 1.5
|
|
}
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
|
|
Label {
|
|
text: qsTr("Temperature, °C")
|
|
font.pixelSize: app.largeFont
|
|
}
|
|
Label {
|
|
text: (d.pendingCallId !== -1 || d.setTempPending) ? d.queuedTargetTemp.toFixed(1) :
|
|
root.targetTemperatureState ? root.targetTemperatureState.value.toFixed(1) : "N/A"
|
|
font.pixelSize: app.largeFont * 3
|
|
}
|
|
}
|
|
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: false
|
|
Layout.bottomMargin: app.margins
|
|
ColorIcon {
|
|
Layout.preferredHeight: Style.iconSize //* 1.5
|
|
Layout.preferredWidth: height
|
|
Layout.alignment: Qt.AlignHCenter
|
|
color: Style.accentColor
|
|
name: "qrc:/icons/magic.svg"
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: pageStack.push("qrc:/ui/magic/ThingRulesPage.qml", {thing: root.duwWpDevice})
|
|
}
|
|
}
|
|
Label {
|
|
text: qsTr("Automate this thing")
|
|
color: Style.accentColor
|
|
font.pixelSize: app.smallFont
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
|
|
RowLayout {
|
|
Layout.leftMargin: parent.width * .05
|
|
Layout.rightMargin: parent.width * .2
|
|
spacing: app.margins
|
|
ColorIcon {
|
|
Layout.preferredHeight: Style.iconSize
|
|
Layout.preferredWidth: Style.iconSize
|
|
color: Style.accentColor
|
|
name: "qrc:/icons/ventilation.svg"
|
|
PropertyAnimation on rotation {
|
|
running: root.ventilationLevelState !== null
|
|
duration: root.ventilationLevelState !== null && root.ventilationLevelState.value > 0
|
|
? 2000 / root.ventilationLevelState.value
|
|
: 0
|
|
from: 0
|
|
to: 360
|
|
loops: Animation.Infinite
|
|
onDurationChanged: {
|
|
running = false;
|
|
running = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Layout.maximumHeight: Style.iconSize
|
|
spacing: 0
|
|
|
|
Repeater {
|
|
model: ListModel {
|
|
ListElement { text: qsTr("Auto") }
|
|
ListElement { text: qsTr("Party") }
|
|
ListElement { text: qsTr("Manual") }
|
|
}
|
|
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
border.width: 1
|
|
border.color: Style.accentColor
|
|
color: root.ventilationModeState && root.ventilationModeToUiMode(root.ventilationModeState.value) === index ? Style.accentColor : "transparent"
|
|
Label {
|
|
anchors.centerIn: parent
|
|
text: model.text
|
|
font.pixelSize: app.smallFont
|
|
}
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: {
|
|
root.setVentilationMode(index, ventilationSlider.value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Slider {
|
|
id: ventilationSlider
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: parent.width * .05
|
|
Layout.rightMargin: parent.width * .05
|
|
from: 0
|
|
to: 3
|
|
stepSize: 1
|
|
live: false
|
|
snapMode: Slider.SnapAlways
|
|
enabled: root.ventilationModeState && root.ventilationModeToUiMode(root.ventilationModeState.value) === 2
|
|
opacity: enabled ? 1 : .2
|
|
value: root.ventilationModeState ? root.ventilationModeToSliderValue(root.ventilationModeState.value) : 0
|
|
onMoved: root.setVentilationMode(2, valueAt(visualPosition))
|
|
}
|
|
}
|
|
|
|
|
|
// ProgressButton {
|
|
// imageSource: "qrc:/icons/system-shutdown.svg"
|
|
// Layout.preferredHeight: Style.iconSize * 1.5
|
|
// Layout.preferredWidth: height
|
|
// Layout.alignment: Qt.AlignHCenter
|
|
// }
|
|
|
|
// Label {
|
|
// text: qsTr("Hold to turn off")
|
|
// font.pixelSize: app.smallFont
|
|
// Layout.fillWidth: true
|
|
// horizontalAlignment: Text.AlignHCenter
|
|
// }
|
|
}
|
|
|
|
Item {
|
|
height: parent.height * .85
|
|
width: height
|
|
anchors.left: parent.right
|
|
anchors.leftMargin: -width * .25
|
|
anchors.top: parent.top
|
|
anchors.topMargin: -height * .05
|
|
z: -1
|
|
|
|
Rectangle {
|
|
id: outerRadius
|
|
anchors.fill: parent
|
|
radius: width / 2
|
|
border.width: 3
|
|
color: "transparent"
|
|
border.color: Style.accentColor
|
|
}
|
|
|
|
Glow {
|
|
anchors.fill: parent
|
|
source: outerRadius
|
|
// color: "#f45b69"
|
|
color: Qt.rgba(Style.accentColor.r, Style.accentColor.g, Style.accentColor.b, .5)
|
|
radius: 8
|
|
samples: 17
|
|
spread: 0.5
|
|
}
|
|
|
|
Rectangle {
|
|
id: innerRadius
|
|
anchors.fill: parent
|
|
anchors.margins: parent.width * .02
|
|
radius: width / 2
|
|
border.width: 2
|
|
color: "transparent"
|
|
border.color: Style.accentColor
|
|
|
|
Repeater {
|
|
id: ticksRepeater
|
|
model: 180
|
|
|
|
Item {
|
|
height: isBold ? 3 : 2
|
|
width: parent.width - 2
|
|
anchors.centerIn: parent
|
|
rotation: index * 360 / ticksRepeater.count
|
|
readonly property int isBold: index % 10 === 0
|
|
// Rectangle { anchors.fill: parent; color: "blue" }
|
|
|
|
Rectangle { height: parent.height; width: parent.isBold ? 20 : 10; color: Style.accentColor }
|
|
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
preventStealing: true
|
|
|
|
property real startAnglePress
|
|
property real startAngleDial
|
|
property real startTemp
|
|
|
|
property real lastValue
|
|
|
|
onPressed: {
|
|
startAnglePress = calculateAngle(mouseX, mouseY)
|
|
startAngleDial = innerRadius.rotation
|
|
startTemp = root.targetTemperatureState.value
|
|
lastValue = startTemp
|
|
|
|
print("angle:", calculateAngle(mouseX, mouseY))
|
|
}
|
|
|
|
onPositionChanged: {
|
|
var currentAngle = calculateAngle(mouseX, mouseY)
|
|
var angleDiff = currentAngle - startAnglePress
|
|
|
|
var tempDiff = Math.round(angleDiff / 2) / 10
|
|
var newTemp = startTemp + tempDiff
|
|
|
|
innerRadius.rotation = startAngleDial + angleDiff
|
|
|
|
if (lastValue.toFixed(1) === newTemp.toFixed(1)) {
|
|
return;
|
|
}
|
|
lastValue = newTemp
|
|
|
|
print("degree value changed", newTemp, lastValue)
|
|
PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection)
|
|
root.setTargetTemp(newTemp);
|
|
}
|
|
|
|
function calculateAngle(mouseX, mouseY) {
|
|
// transform coords to center of dial
|
|
mouseX -= width / 2
|
|
mouseY -= height / 2
|
|
|
|
var rad = Math.atan(mouseY / mouseX);
|
|
var angle = rad * 180 / Math.PI
|
|
|
|
angle += 90;
|
|
|
|
if (mouseX < 0 && mouseY >= 0) angle = 180 + angle;
|
|
if (mouseX < 0 && mouseY < 0) angle = 180 + angle;
|
|
|
|
return angle;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|