From: Bas Schouten Date: Tue, 23 Apr 2024 10:08:52 +0000 (+0200) Subject: Update with new content for the office thermostat and use WebSockets directly to... X-Git-Url: https://git.basschouten.com/?a=commitdiff_plain;h=05e86b1bdf52daf21682b7f82c13300edb1cc08c;p=qthomecontrol.git Update with new content for the office thermostat and use WebSockets directly to OpenHAB instead of MQTT. --- diff --git a/.gitmodules b/.gitmodules index e0e40ec..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "paho.mqtt.c"] - path = paho.mqtt.c - url = https://github.com/eclipse/paho.mqtt.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 11378e0..ba339e4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,26 +5,51 @@ project(qthomecontrol VERSION 0.1 LANGUAGES CXX) set(CMAKE_AUTOMOC ON) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(Qt6 6.5 COMPONENTS Quick REQUIRED) +find_package(Qt6 6.5 COMPONENTS Quick QuickControls2 WebSockets REQUIRED) qt_standard_project_setup(REQUIRES 6.5) -if (UNIX) - add_subdirectory ("paho.mqtt.c") -endif(UNIX) - qt_add_executable(appqthomecontrol main.cpp - mqttinterface.cpp - mqttinterface.h + openhabinterface.cpp + openhabinterface.h + backlightcontroller.h + backlightcontroller.cpp ) qt_add_qml_module(appqthomecontrol URI qthomecontrol VERSION 1.0 - QML_FILES Main.qml + QML_FILES MainBedroom.qml MainOffice.qml SOURCES - mqttinterface.h mqttinterface.cpp + openhabinterface.h openhabinterface.cpp + backlightcontroller.h backlightcontroller.cpp + itemdefinitions.h +) + +qt_add_resources(appqthomecontrol "configuration" + PREFIX "/" + FILES + qtquickcontrols2.conf +) + +qt_add_resources(appqthomecontrol "images" + PREFIX "/" + FILES + images/temperature.png + images/humidity.png + images/light.png + images/energy.png + images/frontdoor.png + images/moon.png + images/pressure.png +) + +qt_add_resources(appqthomecontrol "fonts" + PREFIX "/" + FILES + fonts/digital-7.ttf + ) set_target_properties(appqthomecontrol PROPERTIES @@ -35,17 +60,14 @@ set_target_properties(appqthomecontrol PROPERTIES WIN32_EXECUTABLE TRUE ) -target_include_directories (appqthomecontrol PRIVATE paho.mqtt.c/src) - target_link_libraries(appqthomecontrol PRIVATE Qt6::Quick) -if (WIN32) - target_link_libraries (appqthomecontrol PRIVATE ${CMAKE_SOURCE_DIR}/paho.mqtt.c/src/Release/paho-mqtt3a.lib ${CMAKE_SOURCE_DIR}/paho.mqtt.c/src/Release/paho-mqtt3c.lib) -endif (WIN32) -if (UNIX) - target_link_libraries (appqthomecontrol PRIVATE paho-mqtt3a paho-mqtt3c) -endif(UNIX) +target_link_libraries (appqthomecontrol PRIVATE Qt6::QuickControls2 Qt6::WebSockets) + +if (USE_PWM) + add_compile_definitions(USE_PWM) +endif(USE_PWM) install(TARGETS appqthomecontrol BUNDLE DESTINATION . diff --git a/Main.qml b/Main.qml deleted file mode 100644 index 7b52a7a..0000000 --- a/Main.qml +++ /dev/null @@ -1,56 +0,0 @@ -import QtQuick -import QtQuick.Controls 6.3 -import QtQuick.Layouts 6.3 -import qthomecontrol - -Window { - width: 720 - height: 720 - visible: true - color: "#000000" - title: qsTr("Home Control") - - MQTTInterface { - id: mqttinterface - } - - Slider { - id: slider - x: 13 - y: 534 - width: 695 - height: 178 - live: true - antialiasing: false - topPadding: 0 - orientation: Qt.Horizontal - snapMode: RangeSlider.SnapOnRelease - stepSize: 1 - to: 100 - value: 0 - } - - Text { - id: text1 - x: 206 - y: 114 - width: 309 - height: 129 - color: "#ffffff" - text: mqttinterface.bedroomTemperature.toFixed(1) + " °C" - font.pixelSize: 92 - horizontalAlignment: Text.AlignHCenter - } - - Text { - id: text2 - x: 206 - y: 285 - width: 309 - height: 129 - color: "#ffffff" - text: mqttinterface.bedroomHumidity.toFixed(0) + " %" - font.pixelSize: 92 - horizontalAlignment: Text.AlignHCenter - } -} diff --git a/MainBedroom.qml b/MainBedroom.qml new file mode 100644 index 0000000..28526c2 --- /dev/null +++ b/MainBedroom.qml @@ -0,0 +1,360 @@ +import QtQuick +import QtQuick.Controls 6.3 +import QtQuick.Layouts 6.3 +import qthomecontrol + +Window { + width: 720 + height: 720 + visible: true + title: qsTr("Home Control") + visibility: "FullScreen" + color: Material.backgroundColor + + OpenHABInterface { + id: openHABInterface + } + + BackLightController { + id: backlightController + } + + FontLoader { id: digital; source: "qrc:/fonts/digital-7.ttf" } + + Label { + id: textTime + x: 620 + y: 10 + width: 90 + height: 50 + font.pixelSize: 36 + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignTop + } + + + Timer { + id: timer + interval: 1000 + repeat: true + running: true + + onTriggered: + { + textTime.text = Qt.formatTime(new Date(),"hh:mm") + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + propagateComposedEvents: true + onPositionChanged: { + backlightController.hadInteraction(); + mouse.accepted = false; + } + onClicked: { + backlightController.hadInteraction(); + mouse.accepted = false; + } + onPressed: { + backlightController.hadInteraction(); + mouse.accepted = false; + } + onReleased: mouse.accepted = false; + onDoubleClicked: mouse.accepted = false; + onPressAndHold: mouse.accepted = false; + z: 100 + } + + GroupBox { + id: groupBox + x: 15 + y: 10 + width: 483 + height: 430 + label: Label { + id: bedroomLabel + text: "Bedroom" + font.pixelSize: 32 + } + Slider { + id: slider + x: 245 + y: 83 + width: 190 + height: 25 + live: true + antialiasing: true + topPadding: 0 + bottomPadding: 0 + orientation: Qt.Horizontal + snapMode: RangeSlider.SnapOnRelease + stepSize: 1 + to: 100 + value: openHABInterface.bedroomDimmer + scale: 1.5 + onMoved: + { + openHABInterface.bedroomDimmer = slider.value + } + } + + Label { + id: textTemp + x: 50 + y: 0 + width: 100 + height: 60 + text: openHABInterface.bedroomTemperature.toFixed(1) + font.pixelSize: 52 + font.family: digital.font.family + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignBottom + } + Label { + id: unitTemp + x: 155 + y: 0 + width: 30 + height: 60 + text: "°C" + font.pixelSize: 32 + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignBottom + } + + Label { + id: textHumid + x: 50 + y: 60 + width: 100 + height: 60 + text: openHABInterface.bedroomHumidity.toFixed(0) + font.pixelSize: 52 + font.family: digital.font.family + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignBottom + } + Label { + id: unitHumid + x: 155 + y: 60 + width: 30 + height: 60 + text: "%" + font.pixelSize: 32 + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignBottom + } + + Label { + id: textLight + x: 250 + y: 0 + width: 100 + height: 60 + text: openHABInterface.bedroomDimmer.toFixed(0) + font.pixelSize: 52 + font.family: digital.font.family + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignBottom + } + Label { + id: unitDimmer + x: 357 + y: 0 + width: 30 + height: 60 + text: "%" + font.pixelSize: 32 + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignBottom + } + + Image { + id: tempImage + x: 10 + y: 0 + width: 54 + height: 60 + source: "qrc:/images/temperature.png" + fillMode: Image.PreserveAspectFit + } + Image { + id: humidImage + x: 10 + y: 60 + width: 54 + height: 60 + source: "qrc:/images/humidity.png" + fillMode: Image.PreserveAspectFit + } + Image { + id: lightImage + x: 235 + y: 0 + width: 54 + height: 60 + source: "qrc:/images/light.png" + fillMode: Image.PreserveAspectFit + } + Dial { + id: dialAC + x: 35 + y: 230 + inputMode: Dial.Vertical + scale: 1.5 + + from: 16 + value: openHABInterface.bedroomACSetPoint + to: 24 + stepSize: 0.5 + snapMode: Dial.SnapAlways + + onMoved: { + openHABInterface.bedroomACSetPoint = dialAC.value + } + + Label { + x: 0 + width: dialAC.width + y: 0 + height: dialAC.height + id: thermoSetLabel + text: dialAC.value.toFixed(1) + font.pixelSize: 32 + font.family: digital.font.family + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + Dial { + id: dialFloor + x: 50 + dialFloor.width * 1.5 + y: 230 + inputMode: Dial.Vertical + scale: 1.5 + + from: 16 + value: openHABInterface.bedroomFloorSetPoint + to: 24 + stepSize: 0.5 + snapMode: Dial.SnapAlways + + onMoved: { + openHABInterface.bedroomFloorSetPoint = dialFloor.value + } + + Label { + x: 0 + width: dialFloor.width + y: 0 + height: dialFloor.height + id: thermoFloorSetLabel + text: dialFloor.value.toFixed(1) + font.pixelSize: 32 + font.family: digital.font.family + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + + + Switch { + id: switchAC + x: 35 + width: dialAC.width + y: 143 + checked: openHABInterface.bedroomACToggle + scale: 1.5 + onToggled: + { + openHABInterface.bedroomACToggle = switchAC.checked + } + } + + } + + RoundButton { + id: allLightsOffButton + x: 610 + y: 610 + width: 100 + height: 100 + icon.source: "qrc:/images/moon.png" + icon.name: "night" + icon.width: 64 + icon.height: 64 + onClicked: + { + openHABInterface.signalItem("BedroomGoodnight") + } + } + + + Image { + id: outdoorTempImage + x: 10 + y: 590 + width: 54 + height: 60 + source: "qrc:/images/frontdoor.png" + fillMode: Image.PreserveAspectFit + } + Label { + id: outdoorTemp + x: 70 + y: 590 + width: 100 + height: 60 + text: openHABInterface.outdoorTemperature.toFixed(1) + font.pixelSize: 52 + font.family: digital.font.family + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignBottom + } + Label { + id: unitOutdoorTemp + x: 175 + y: 590 + width: 30 + height: 60 + text: "°C" + font.pixelSize: 32 + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignBottom + } + Image { + id: powerImage + x: 10 + y: 650 + width: 54 + height: 60 + source: "qrc:/images/energy.png" + fillMode: Image.PreserveAspectFit + } + Label { + id: textPower + x: 70 + y: 650 + width: 100 + height: 60 + text: openHABInterface.mainPowerUsage < 1000 ? openHABInterface.mainPowerUsage.toFixed(0) : (openHABInterface.mainPowerUsage / 1000).toFixed(2) + font.pixelSize: 52 + font.family: digital.font.family + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignBottom + } + Label { + id: unitPower + x: 175 + y: 650 + width: 30 + height: 60 + text: openHABInterface.mainPowerUsage < 1000 ? "W" : "kW" + font.pixelSize: 32 + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignBottom + } +} diff --git a/MainOffice.qml b/MainOffice.qml new file mode 100644 index 0000000..c77b76e --- /dev/null +++ b/MainOffice.qml @@ -0,0 +1,376 @@ +import QtQuick +import QtQuick.Controls 6.3 +import QtQuick.Layouts 6.3 +import qthomecontrol + +Window { + width: 720 + height: 720 + visible: true + title: qsTr("Home Control") + visibility: "FullScreen" + color: Material.backgroundColor + + OpenHABInterface { + id: openHABInterface + } + + BackLightController { + id: backlightController + } + + FontLoader { id: digital; source: "qrc:/fonts/digital-7.ttf" } + + Label { + id: textTime + x: 620 + y: 10 + width: 90 + height: 50 + font.pixelSize: 36 + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignTop + } + + + Timer { + id: timer + interval: 1000 + repeat: true + running: true + + onTriggered: + { + textTime.text = Qt.formatTime(new Date(),"hh:mm") + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + propagateComposedEvents: true + onPositionChanged: { + backlightController.hadInteraction(); + mouse.accepted = false; + } + onClicked: { + backlightController.hadInteraction(); + mouse.accepted = false; + } + onPressed: { + backlightController.hadInteraction(); + mouse.accepted = false; + } + onReleased: mouse.accepted = false; + onDoubleClicked: mouse.accepted = false; + onPressAndHold: mouse.accepted = false; + z: 100 + } + + GroupBox { + id: groupBox + x: 15 + y: 10 + width: 483 + height: 490 + label: Label { + id: officeLabel + text: "Office" + font.pixelSize: 32 + } + Slider { + id: slider + x: 245 + y: 83 + width: 190 + height: 25 + live: true + antialiasing: true + topPadding: 0 + bottomPadding: 0 + orientation: Qt.Horizontal + snapMode: RangeSlider.SnapOnRelease + stepSize: 1 + to: 100 + value: openHABInterface.officeDimmer + scale: 1.5 + onMoved: + { + openHABInterface.officeDimmer = slider.value + } + } + + Label { + id: textTemp + x: 50 + y: 0 + width: 100 + height: 60 + text: openHABInterface.officeTemperature.toFixed(1) + font.pixelSize: 52 + font.family: digital.font.family + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignBottom + } + Label { + id: unitTemp + x: 155 + y: 0 + width: 30 + height: 60 + text: "°C" + font.pixelSize: 32 + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignBottom + } + + Label { + id: textHumid + x: 50 + y: 60 + width: 100 + height: 60 + text: openHABInterface.officeHumidity.toFixed(0) + font.pixelSize: 52 + font.family: digital.font.family + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignBottom + } + Label { + id: unitHumid + x: 155 + y: 60 + width: 30 + height: 60 + text: "%" + font.pixelSize: 32 + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignBottom + } + + Label { + id: textPressure + x: 50 + y: 120 + width: 150 + height: 60 + text: openHABInterface.officePressure.toFixed(1) + font.pixelSize: 52 + font.family: digital.font.family + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignBottom + } + Label { + id: unitPressure + x: 205 + y: 120 + width: 30 + height: 60 + text: "hPa" + font.pixelSize: 32 + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignBottom + } + + Label { + id: textLight + x: 250 + y: 0 + width: 100 + height: 60 + text: openHABInterface.officeDimmer.toFixed(0) + font.pixelSize: 52 + font.family: digital.font.family + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignBottom + } + Label { + id: unitDimmer + x: 357 + y: 0 + width: 30 + height: 60 + text: "%" + font.pixelSize: 32 + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignBottom + } + + Image { + id: tempImage + x: 10 + y: 0 + width: 54 + height: 60 + source: "qrc:/images/temperature.png" + fillMode: Image.PreserveAspectFit + } + Image { + id: humidImage + x: 10 + y: 60 + width: 54 + height: 60 + source: "qrc:/images/humidity.png" + fillMode: Image.PreserveAspectFit + } + Image { + id: pressureImage + x: 10 + y: 120 + width: 54 + height: 60 + source: "qrc:/images/pressure.png" + fillMode: Image.PreserveAspectFit + } + Image { + id: lightImage + x: 235 + y: 0 + width: 54 + height: 60 + source: "qrc:/images/light.png" + fillMode: Image.PreserveAspectFit + } + Dial { + id: dialAC + x: 35 + y: 290 + inputMode: Dial.Vertical + scale: 1.5 + + from: 16 + value: openHABInterface.officeACSetPoint + to: 26 + stepSize: 0.5 + snapMode: Dial.SnapAlways + + onMoved: { + openHABInterface.officeACSetPoint = dialAC.value + } + + Label { + x: 0 + width: dialAC.width + y: 0 + height: dialAC.height + id: thermoSetLabel + text: dialAC.value.toFixed(1) + font.pixelSize: 32 + font.family: digital.font.family + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + Dial { + id: dialFloor + x: 50 + dialFloor.width * 1.5 + y: 290 + inputMode: Dial.Vertical + scale: 1.5 + + from: 16 + value: openHABInterface.officeFloorSetPoint + to: 24 + stepSize: 0.5 + snapMode: Dial.SnapAlways + + onMoved: { + openHABInterface.officeFloorSetPoint = dialFloor.value + } + + Label { + x: 0 + width: dialFloor.width + y: 0 + height: dialFloor.height + id: thermoFloorSetLabel + text: dialFloor.value.toFixed(1) + font.pixelSize: 32 + font.family: digital.font.family + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + + + Switch { + id: switchAC + x: 35 + width: dialAC.width + y: 203 + checked: openHABInterface.officeACToggle + scale: 1.5 + onToggled: + { + openHABInterface.officeACToggle = switchAC.checked + } + } + + } + + Image { + id: outdoorTempImage + x: 10 + y: 590 + width: 54 + height: 60 + source: "qrc:/images/frontdoor.png" + fillMode: Image.PreserveAspectFit + } + Label { + id: outdoorTemp + x: 70 + y: 590 + width: 100 + height: 60 + text: openHABInterface.outdoorTemperature.toFixed(1) + font.pixelSize: 52 + font.family: digital.font.family + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignBottom + } + Label { + id: unitOutdoorTemp + x: 175 + y: 590 + width: 30 + height: 60 + text: "°C" + font.pixelSize: 32 + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignBottom + } + Image { + id: powerImage + x: 10 + y: 650 + width: 54 + height: 60 + source: "qrc:/images/energy.png" + fillMode: Image.PreserveAspectFit + } + Label { + id: textPower + x: 70 + y: 650 + width: 100 + height: 60 + text: openHABInterface.mainPowerUsage < 1000 ? openHABInterface.mainPowerUsage.toFixed(0) : (openHABInterface.mainPowerUsage / 1000).toFixed(2) + font.pixelSize: 52 + font.family: digital.font.family + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignBottom + } + Label { + id: unitPower + x: 175 + y: 650 + width: 30 + height: 60 + text: openHABInterface.mainPowerUsage < 1000 ? "W" : "kW" + font.pixelSize: 32 + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignBottom + } +} diff --git a/backlightcontroller.cpp b/backlightcontroller.cpp new file mode 100644 index 0000000..3cb8f92 --- /dev/null +++ b/backlightcontroller.cpp @@ -0,0 +1,91 @@ +#include "backlightcontroller.h" +#ifdef USE_PWM +#define PWM_PERIOD "500000" +#define PWM_DUTYCYCLE_DIM "380000" +#define PWM_DUTYCYCLE_ON "100000" + +#include +#include +#include +#include +#include +#endif + +BackLightController::BackLightController(QObject *parent) + : QObject{parent} +{ +#ifdef USE_PWM + int fd; + + fd = open("/sys/class/pwm/pwmchip0/export", O_WRONLY); + if (-1 == fd) { + fprintf(stderr, "Failed to open export for writing.\n"); + } + write(fd, "0", 2); + close(fd); + + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + fd = open("/sys/class/pwm/pwmchip0/pwm0/period", O_WRONLY); + if (-1 == fd) { + fprintf(stderr, "Failed to open PWM period for writing.\n"); + } + write(fd, PWM_PERIOD, strlen(PWM_PERIOD)); + close(fd); + + fd = open("/sys/class/pwm/pwmchip0/pwm0/duty_cycle", O_WRONLY); + if (-1 == fd) { + fprintf(stderr, "Failed to open PWM duty cycle for writing.\n"); + } + write(fd, PWM_DUTYCYCLE_DIM, strlen(PWM_DUTYCYCLE_DIM)); + close(fd); + + fd = open("/sys/class/pwm/pwmchip0/pwm0/enable", O_WRONLY); + if (-1 == fd) { + fprintf(stderr, "Failed to open PWM for writing.\n"); + } + write(fd, "1", 2); + close(fd); +#endif + updateBackLight(false); +} + +void +BackLightController::hadInteraction() +{ + updateBackLight(true); + mOffTimer.reset(new QTimer(this)); + mOffTimer->callOnTimeout(this, &BackLightController::backlightTimedOut); + mOffTimer->setSingleShot(true); + mOffTimer->start(10000); +} + +void +BackLightController::backlightTimedOut() +{ + updateBackLight(false); +} + +void +BackLightController::updateBackLight(bool aState) +{ + if (mBackLightIsHigh == aState) { + return; + } + +#ifdef USE_PWM + int fd; + fd = open("/sys/class/pwm/pwmchip0/pwm0/duty_cycle", O_WRONLY); + if (-1 == fd) { + fprintf(stderr, "Failed to open PWM duty cycle for writing.\n"); + } + write(fd, aState ? PWM_DUTYCYCLE_ON : PWM_DUTYCYCLE_DIM, aState ? strlen(PWM_DUTYCYCLE_ON) : strlen(PWM_DUTYCYCLE_DIM)); + close(fd); +#endif + if (aState) { + // turn on backlight + mBackLightIsHigh = true; + } else { + mBackLightIsHigh = false; + } +} diff --git a/backlightcontroller.h b/backlightcontroller.h new file mode 100644 index 0000000..19b1788 --- /dev/null +++ b/backlightcontroller.h @@ -0,0 +1,27 @@ +#ifndef BACKLIGHTCONTROLLER_H +#define BACKLIGHTCONTROLLER_H + +#include +#include +#include +#include + +class BackLightController : public QObject +{ + Q_OBJECT + QML_ELEMENT +public: + explicit BackLightController(QObject *parent = nullptr); + +public slots: + void hadInteraction(); + void backlightTimedOut(); + +private: + void updateBackLight(bool aState); + + bool mBackLightIsHigh = false; + QScopedPointer mOffTimer; +}; + +#endif // BACKLIGHTCONTROLLER_H diff --git a/fonts/DigitalDream.ttf b/fonts/DigitalDream.ttf new file mode 100644 index 0000000..25ab5de Binary files /dev/null and b/fonts/DigitalDream.ttf differ diff --git a/fonts/digital-7.ttf b/fonts/digital-7.ttf new file mode 100644 index 0000000..5dbe6f9 Binary files /dev/null and b/fonts/digital-7.ttf differ diff --git a/images/energy.png b/images/energy.png new file mode 100644 index 0000000..07f0773 Binary files /dev/null and b/images/energy.png differ diff --git a/images/frontdoor.png b/images/frontdoor.png new file mode 100644 index 0000000..d0378b6 Binary files /dev/null and b/images/frontdoor.png differ diff --git a/images/humidity.png b/images/humidity.png new file mode 100644 index 0000000..00f58df Binary files /dev/null and b/images/humidity.png differ diff --git a/images/light.png b/images/light.png new file mode 100644 index 0000000..2e5b762 Binary files /dev/null and b/images/light.png differ diff --git a/images/moon.png b/images/moon.png new file mode 100644 index 0000000..bbe7893 Binary files /dev/null and b/images/moon.png differ diff --git a/images/pressure.png b/images/pressure.png new file mode 100644 index 0000000..d91d298 Binary files /dev/null and b/images/pressure.png differ diff --git a/images/temperature.png b/images/temperature.png new file mode 100644 index 0000000..2fddbbe Binary files /dev/null and b/images/temperature.png differ diff --git a/itemdefinitions.h b/itemdefinitions.h new file mode 100644 index 0000000..756bae7 --- /dev/null +++ b/itemdefinitions.h @@ -0,0 +1,15 @@ +DEFINE_ITEM(bedroomTemperature, Bedroom_Thermostat_Temperature, float, temperature) +DEFINE_ITEM(bedroomHumidity, Bedroom_Thermostat_Humidity, float, humidity) +DEFINE_ITEM(bedroomDimmer, BedroomLight_Dimmer, float, dimmer) +DEFINE_ITEM(bedroomACToggle, ACDeviceSlaapkamer_Power, bool, toggle) +DEFINE_ITEM(bedroomACSetPoint, AC_Device__Slaapkamer_Set_Temperature, float, temperatureWithUnit) +DEFINE_ITEM(bedroomFloorSetPoint, Bedroom_Thermostat_Setpoint, float, temperature) +DEFINE_ITEM(officeTemperature, Office_Thermostat_Temperature, float, temperature) +DEFINE_ITEM(officeHumidity, Office_Thermostat_Humidity, float, humidityWithUnit) +DEFINE_ITEM(officePressure, Office_Thermostat_Pressure, float, pressure) +DEFINE_ITEM(officeDimmer, OfficeLight_Dimmer, float, dimmer) +DEFINE_ITEM(officeACToggle, ACDeviceOffice_Power, bool, toggle) +DEFINE_ITEM(officeACSetPoint, ACDeviceOffice_SetTemperature, float, temperatureWithUnit) +DEFINE_ITEM(officeFloorSetPoint, Office_Thermostat_Setpoint, float, temperature) +DEFINE_ITEM(mainPowerUsage, MainElectricityMeter_ActualPowerDelivery, float, power) +DEFINE_ITEM(outdoorTemperature, Nibe_BT1_Outdoor_Temperature_Sensor_Nibe_BT1_Outdoor_Temperature, float, temperature) diff --git a/main.cpp b/main.cpp index 6086143..099898f 100644 --- a/main.cpp +++ b/main.cpp @@ -1,16 +1,31 @@ #include #include - +#include +#include +#include int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); + //app.setOverrideCursor(QCursor(Qt::BlankCursor)); + + QString panelString("MainBedroom"); + if (argc >= 2) { + for (int i = 0; i < argc; i++) { + if (!strncasecmp(argv[i], "-p", 2)) { + if (!strncasecmp(argv[++i], "office", 6)) { + panelString = "MainOffice"; + } + } + } + } + QQmlApplicationEngine engine; QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, &app, []() { QCoreApplication::exit(-1); }, Qt::QueuedConnection); - engine.loadFromModule("qthomecontrol", "Main"); + engine.loadFromModule("qthomecontrol", panelString); return app.exec(); } diff --git a/mqttinterface.cpp b/mqttinterface.cpp deleted file mode 100644 index 2958895..0000000 --- a/mqttinterface.cpp +++ /dev/null @@ -1,115 +0,0 @@ -#include "mqttinterface.h" - -using namespace std; - -void delivered(void* context, MQTTClient_deliveryToken dt) -{ - MQTTInterface* thermostat = static_cast(context); - //thermostat->DeliverToken(dt); -} - -int msgarrvd(void* context, char* topicName, int topicLen, MQTTClient_message* message) -{ - MQTTInterface* thermostat = static_cast(context); - - if (string(topicName) == "bedroom/thermostat/temperature/raw") { - float temperature = stof(string((char*)message->payload)); - thermostat->updateBedroomTemperature(temperature); - } else if (string(topicName) == "bedroom/thermostat/humidity/raw") { - float humidity = stof(string((char*)message->payload)); - thermostat->updateBedroomHumidity(humidity); - } - - MQTTClient_freeMessage(&message); - MQTTClient_free(topicName); - - - return 1; -} - -void connlost(void* context, char* cause) -{ - MQTTInterface* thermostat = static_cast(context); - - printf("\nConnection lost\n"); - printf(" cause: %s\n", cause); - - //thermostat->ShouldReconnect(); -} - -#define QOS 1 - -MQTTInterface::MQTTInterface(QObject *parent) - : QObject{parent} -{ - MQTTClient_message pubmsg = MQTTClient_message_initializer; - MQTTClient_deliveryToken token; - - int rc; - MQTTClient_create(&mClient, "tcp://10.0.1.225:1883", "bedroom-thermostat-fe", - MQTTCLIENT_PERSISTENCE_NONE, NULL); - - if ((rc = MQTTClient_setCallbacks(mClient, this, connlost, msgarrvd, delivered)) != MQTTCLIENT_SUCCESS) - { - printf("Failed to set callbacks, return code %d\n", rc); - return; - } - - connectToServer(); - - char cbuf[256]; - sprintf(cbuf, "%.1f", 15.3); - pubmsg.payload = (void*)cbuf; - pubmsg.payloadlen = strlen(cbuf); - pubmsg.qos = QOS; - pubmsg.retained = 0; - MQTTClient_publishMessage(mClient, "newtest/test", &pubmsg, &token); -} - -void -MQTTInterface::updateBedroomTemperature(float aTemperature) -{ - { - unique_lock lk(mDataMutex); - mBedroomTemperature = aTemperature; - } - - emit bedroomTemperatureChanged(); -} - -void -MQTTInterface::updateBedroomHumidity(float aHumidity) -{ - { - unique_lock lk(mDataMutex); - mBedroomHumidity = aHumidity; - } - - emit bedroomHumidityChanged(); -} - -void -MQTTInterface::connectToServer() -{ - int rc = 0; - MQTTClient_disconnect(mClient, 10000); - - MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer; - conn_opts.keepAliveInterval = 20; - conn_opts.cleansession = 1; - - if ((rc = MQTTClient_connect(mClient, &conn_opts)) != MQTTCLIENT_SUCCESS) - { - printf("Failed to connect, return code %d\n", rc); - return; - } - - if ((rc = MQTTClient_subscribe(mClient, "bedroom/thermostat/temperature/raw", QOS)) != MQTTCLIENT_SUCCESS) - { - printf("Failed to subscribe, return code %d\n", rc); - } - if ((rc = MQTTClient_subscribe(mClient, "bedroom/thermostat/humidity/raw", QOS)) != MQTTCLIENT_SUCCESS) - { - printf("Failed to subscribe, return code %d\n", rc); - } -} diff --git a/mqttinterface.h b/mqttinterface.h deleted file mode 100644 index 1b54a9a..0000000 --- a/mqttinterface.h +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef MQTTINTERFACE_H -#define MQTTINTERFACE_H - -#include -#include -#include - -#include "MQTTClient.h" - -class MQTTInterface : public QObject -{ - Q_OBJECT - Q_PROPERTY(qreal bedroomTemperature READ bedroomTemperature NOTIFY bedroomTemperatureChanged); - Q_PROPERTY(qreal bedroomHumidity READ bedroomHumidity NOTIFY bedroomHumidityChanged); - QML_ELEMENT - -public: - explicit MQTTInterface(QObject *parent = nullptr); - - qreal bedroomTemperature() { - std::unique_lock lk(mDataMutex); - return mBedroomTemperature; - } - qreal bedroomHumidity() { - std::unique_lock lk(mDataMutex); - return mBedroomHumidity; - } - - void updateBedroomTemperature(float aTemperature); - void updateBedroomHumidity(float aHumidity); - -signals: - void bedroomTemperatureChanged(); - void bedroomHumidityChanged(); - -private: - void connectToServer(); - - qreal mBedroomTemperature = 10; - qreal mBedroomHumidity = 50; - MQTTClient mClient; - std::mutex mDataMutex; -}; - -#endif // MQTTINTERFACE_H diff --git a/openhabinterface.cpp b/openhabinterface.cpp new file mode 100644 index 0000000..c09cf03 --- /dev/null +++ b/openhabinterface.cpp @@ -0,0 +1,131 @@ +#include "openhabinterface.h" + +#include +#include + +using namespace std; + +QJsonObject createCommandMessage(const char* aItemName) { + QJsonObject jsonObject; + jsonObject["type"] = "ItemCommandEvent"; + jsonObject["topic"] = QString("openhab/items/") + aItemName + QString("/command"); + jsonObject["source"] = "BedroomHomeControl"; + return jsonObject; +} + +OpenHABInterface::OpenHABInterface(QObject *parent) + : QObject{parent} +{ +#define DEFINE_ITEM(name, itemName, ctype, type) \ + mItems.push_back(ItemDescription{ &m_##name, "openhab/items/"#itemName"/state", #itemName, std::bind(&OpenHABInterface::name##Changed, this), ItemType::type }); +#include "itemdefinitions.h" +#undef DEFINE_ITEM + + connect(&mOpenHABSocket, &QWebSocket::errorOccurred, this, &OpenHABInterface::socketError); + connect(&mOpenHABSocket, &QWebSocket::textMessageReceived, this, &OpenHABInterface::openHABMessageReceived); + connectToServer(); + + for (const ItemDescription& item : mItems) { + if (item.mType == ItemType::toggle) { + *reinterpret_cast(item.mValue) = false; + } else { + *reinterpret_cast(item.mValue) = 0; + } + } +} + +void +OpenHABInterface::socketError(QAbstractSocket::SocketError aError) +{ +} + +void +OpenHABInterface::openHABMessageReceived(const QString& aMessage) +{ + QJsonDocument receivedMessage = QJsonDocument::fromJson(aMessage.toUtf8().data()); + + if (!receivedMessage["topic"].isString()) { + return; + } + + const QString& topic = receivedMessage["topic"].toString(); + + for (const ItemDescription& item : mItems) { + if (topic == item.mOpenHABState) { + if (item.mType == ItemType::dimmer || item.mType == ItemType::temperature || item.mType == ItemType::humidity || item.mType == ItemType::pressure) { + QJsonDocument payload = QJsonDocument::fromJson(receivedMessage["payload"].toString().toUtf8().data()); + float value = payload["value"].toString().toFloat(); + *reinterpret_cast(item.mValue) = value; + emit item.mSignal(); + } else if (item.mType == ItemType::humidityWithUnit) { + QJsonDocument payload = QJsonDocument::fromJson(receivedMessage["payload"].toString().toUtf8().data()); + QString valueString = payload["value"].toString(); + valueString.chop(2); + float value = valueString.toFloat(); + *reinterpret_cast(item.mValue) = value; + emit item.mSignal(); + } else if (item.mType == ItemType::temperatureWithUnit) { + QJsonDocument payload = QJsonDocument::fromJson(receivedMessage["payload"].toString().toUtf8().data()); + QString valueString = payload["value"].toString(); + valueString.chop(3); + float value = valueString.toFloat(); + *reinterpret_cast(item.mValue) = value; + emit item.mSignal(); + } else if (item.mType == ItemType::power) { + QJsonDocument payload = QJsonDocument::fromJson(receivedMessage["payload"].toString().toUtf8().data()); + QString valueString = payload["value"].toString(); + valueString.chop(3); + float value = valueString.toFloat() * 1000; + *reinterpret_cast(item.mValue) = value; + emit item.mSignal(); + } else if (item.mType == ItemType::toggle) { + QJsonDocument payload = QJsonDocument::fromJson(receivedMessage["payload"].toString().toUtf8().data()); + *reinterpret_cast(item.mValue) = payload["value"].toString() == "ON"; + emit item.mSignal(); + } + } + } +} + +void +OpenHABInterface::signalItem(const QString& aItem) +{ + QJsonObject commandObject = createCommandMessage(aItem.toStdString().c_str()); + commandObject["payload"] = "{\"type\":\"OnOff\",\"value\":\"OFF\"}"; + QJsonDocument commandDocument; + commandDocument.setObject(commandObject); + mOpenHABSocket.sendTextMessage(QString::fromUtf8(commandDocument.toJson())); +} + +void +OpenHABInterface::writeValue(const char* aItemName, ItemType aType, float aValue) +{ + QJsonObject commandObject = createCommandMessage(aItemName); + + if (aType == ItemType::dimmer) { + commandObject["payload"] = "{\"type\":\"Percent\",\"value\":\"" + QString::number(aValue, 'f', 0) + "\"}"; + } else if (aType == ItemType::temperature) { + commandObject["payload"] = "{\"type\":\"Decimal\",\"value\":\"" + QString::number(aValue, 'f', 1) + "\"}"; + } else if (aType == ItemType::temperatureWithUnit) { + commandObject["payload"] = "{\"type\":\"Quantity\",\"value\":\"" + QString::number(aValue, 'f', 1) + " °C\"}"; + } + QJsonDocument commandDocument; + commandDocument.setObject(commandObject); + mOpenHABSocket.sendTextMessage(QString::fromUtf8(commandDocument.toJson())); +} + +void +OpenHABInterface::writeValue(const char* aItemName, ItemType aType, bool aValue) +{ + QJsonObject commandObject = createCommandMessage(aItemName); + commandObject["payload"] = "{\"type\":\"OnOff\",\"value\":\"" + QString(aValue ? "ON" : "OFF") + "\"}"; + QJsonDocument commandDocument; + commandDocument.setObject(commandObject); + mOpenHABSocket.sendTextMessage(QString::fromUtf8(commandDocument.toJson())); +} + +void +OpenHABInterface::connectToServer() +{ + mOpenHABSocket.open(QUrl("ws://10.0.1.225:8080/ws")); +} diff --git a/openhabinterface.h b/openhabinterface.h new file mode 100644 index 0000000..d3c0b75 --- /dev/null +++ b/openhabinterface.h @@ -0,0 +1,102 @@ +#ifndef OPENHABINTERFACE_H +#define OPENHABINTERFACE_H + +#include +#include +#include +#include +#include + +class OpenHABInterface : public QObject +{ + Q_OBJECT +#define DEFINE_ITEM(name, itemName, ctype, type) \ + Q_PROPERTY(ctype name READ name WRITE set##name NOTIFY name##Changed); +#include "itemdefinitions.h" +#undef DEFINE_ITEM + QML_ELEMENT + + enum class ItemType { + temperature, + temperatureWithUnit, + humidity, + humidityWithUnit, + dimmer, + toggle, + power, + pressure + }; + +public: + explicit OpenHABInterface(QObject *parent = nullptr); + +#define DEFINE_ITEM(name, itemName, ctype, type) \ + ctype name() { \ + std::unique_lock lk(mDataMutex); \ + return m_##name; \ + } \ + void set##name(ctype aValue) { \ + { \ + std::unique_lock lk(mDataMutex); \ + m_##name = aValue; \ + } \ +\ + writeValue(#itemName, ItemType::type, aValue); \ + emit name##Changed(); \ + } +#include "itemdefinitions.h" +#undef DEFINE_ITEM + +public slots: + void socketError(QAbstractSocket::SocketError aError); + void openHABMessageReceived(const QString& message); + void signalItem(const QString& item); + +signals: +// Ugh, Qt doesn't understand macros inside here. +//#define DEFINE_ITEM(name, itemName, type) \ +// void name##Changed(); +//#include "itemdefinitions.h" +//#undef DEFINE_ITEM + void bedroomTemperatureChanged(); + void bedroomHumidityChanged(); + void bedroomDimmerChanged(); + void bedroomACToggleChanged(); + void bedroomACSetPointChanged(); + void bedroomFloorSetPointChanged(); + void officeTemperatureChanged(); + void officeHumidityChanged(); + void officePressureChanged(); + void officeDimmerChanged(); + void officeACToggleChanged(); + void officeACSetPointChanged(); + void officeFloorSetPointChanged(); + void mainPowerUsageChanged(); + void outdoorTemperatureChanged(); + +private: + void connectToServer(); + + void writeValue(const char* aItemName, ItemType aItemType, float aValue); + void writeValue(const char* aItemName, ItemType aItemType, bool aValue); + + +#define DEFINE_ITEM(name, itemName, ctype, type) \ + ctype m_##name; +#include "itemdefinitions.h" +#undef DEFINE_ITEM + + struct ItemDescription { + void* mValue; + const char* mOpenHABState; + const char* mOpenHABCommand; + std::function mSignal; + ItemType mType; + }; + + std::mutex mDataMutex; + QWebSocket mOpenHABSocket; + std::vector mItems; +}; + +#endif // OPENHABINTERFACE_H diff --git a/paho.mqtt.c b/paho.mqtt.c deleted file mode 160000 index f7799da..0000000 --- a/paho.mqtt.c +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f7799da95e347bbc930b201b52a1173ebbad45a7 diff --git a/qtquickcontrols2.conf b/qtquickcontrols2.conf new file mode 100644 index 0000000..e1c0add --- /dev/null +++ b/qtquickcontrols2.conf @@ -0,0 +1,5 @@ +[Controls] +Style=Material + +[Material] +Theme=Dark diff --git a/resources.qrc b/resources.qrc new file mode 100644 index 0000000..4fbf17b --- /dev/null +++ b/resources.qrc @@ -0,0 +1,5 @@ + + + qtquickcontrols2.conf + +