This repository has been archived on 2026-05-31. You can view files and clone it, but cannot push or open issues or pull requests.
2026-01-07 16:52:35 +01:00

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