--- /dev/null
+.vs/*
+out/*
\ No newline at end of file
--- /dev/null
+[submodule "paho.mqtt.c"]
+ path = paho.mqtt.c
+ url = https://github.com/eclipse/paho.mqtt.c
--- /dev/null
+# CMakeList.txt : Top-level CMake project file, do global configuration
+# and include sub-projects here.
+#
+cmake_minimum_required (VERSION 3.8)
+
+project ("MQTTThermostat")
+
+# Include sub-projects.
+add_subdirectory ("paho.mqtt.c")
+add_subdirectory ("MQTTThermostat")
+
+# Directory with configuration files
+set (DAEMON_CONF_DIR "/etc")
+
+# Directory with systemd unit files
+set (SYSTEMD_UNIT_DIR "/usr/lib/systemd/system/")
+
+# Macro for installing configuration files
+function(install_conf src dest)
+ if(NOT IS_ABSOLUTE "${src}")
+ set(src "${CMAKE_CURRENT_SOURCE_DIR}/${src}")
+ endif()
+ get_filename_component(src_name "${src}" NAME)
+ if (NOT IS_ABSOLUTE "${dest}")
+ set(dest "${CMAKE_INSTALL_PREFIX}/${dest}")
+ endif()
+ install(CODE "
+ if(NOT EXISTS \"\$ENV{DESTDIR}${dest}/${src_name}\")
+ #file(INSTALL \"${src}\" DESTINATION \"${dest}\")
+ message(STATUS \"Installing: \$ENV{DESTDIR}${dest}/${src_name}\")
+ execute_process(COMMAND \${CMAKE_COMMAND} -E copy \"${src}\"
+ \"\$ENV{DESTDIR}${dest}/${src_name}\"
+ RESULT_VARIABLE copy_result
+ ERROR_VARIABLE error_output)
+ if(copy_result)
+ message(FATAL_ERROR \${error_output})
+ endif()
+ else()
+ message(STATUS \"Skipping : \$ENV{DESTDIR}${dest}/${src_name}\")
+ endif()
+ ")
+endfunction(install_conf)
+
+# Install configuration file
+install_conf (./mqttthermostat.conf ${DAEMON_CONF_DIR})
+
+# Install systemd unit files
+install_conf (./mqttthermostat.service ${SYSTEMD_UNIT_DIR})
\ No newline at end of file
--- /dev/null
+{
+ "version": 3,
+ "configurePresets": [
+ {
+ "name": "windows-base",
+ "hidden": true,
+ "generator": "Ninja",
+ "binaryDir": "${sourceDir}/out/build/${presetName}",
+ "installDir": "${sourceDir}/out/install/${presetName}",
+ "cacheVariables": {
+ "CMAKE_C_COMPILER": "cl.exe",
+ "CMAKE_CXX_COMPILER": "cl.exe"
+ },
+ "condition": {
+ "type": "equals",
+ "lhs": "${hostSystemName}",
+ "rhs": "Windows"
+ }
+ },
+ {
+ "name": "x64-debug",
+ "displayName": "x64 Debug",
+ "inherits": "windows-base",
+ "architecture": {
+ "value": "x64",
+ "strategy": "external"
+ },
+ "cacheVariables": {
+ "CMAKE_BUILD_TYPE": "Debug"
+ }
+ },
+ {
+ "name": "x64-release",
+ "displayName": "x64 Release",
+ "inherits": "x64-debug",
+ "cacheVariables": {
+ "CMAKE_BUILD_TYPE": "Release"
+ }
+ },
+ {
+ "name": "x86-debug",
+ "displayName": "x86 Debug",
+ "inherits": "windows-base",
+ "architecture": {
+ "value": "x86",
+ "strategy": "external"
+ },
+ "cacheVariables": {
+ "CMAKE_BUILD_TYPE": "Debug"
+ }
+ },
+ {
+ "name": "x86-release",
+ "displayName": "x86 Release",
+ "inherits": "x86-debug",
+ "cacheVariables": {
+ "CMAKE_BUILD_TYPE": "Release"
+ }
+ }
+ ]
+}
--- /dev/null
+# CMakeList.txt : CMake project for MQTTThermostat, include source and define
+# project specific logic here.
+#
+
+
+# Add source to this project's executable.
+if (WIN32)
+add_executable (mqttthermostat "MQTTThermostat.cpp" "MQTTThermostat.h" "TemperatureSupplier.h" "HeatingController.h" "maintest.cpp")
+target_link_libraries (mqttthermostat PRIVATE paho-mqtt3a-static paho-mqtt3c-static)
+endif (WIN32)
+if (UNIX)
+add_executable (mqttthermostat "MQTTThermostat.cpp" "MQTTThermostat.h" "TemperatureSupplier.h" "HeatingController.h" "maindaemon.cpp")
+target_link_libraries (mqttthermostat PRIVATE paho-mqtt3a paho-mqtt3c)
+endif (UNIX)
+
+target_include_directories (mqttthermostat PRIVATE ../paho.mqtt.c/src)
+
+install(TARGETS mqttthermostat
+ RUNTIME DESTINATION bin
+ LIBRARY DESTINATION lib
+ ARCHIVE DESTINATION lib/myproject)
+
+if (CMAKE_VERSION VERSION_GREATER 3.12)
+ set_property(TARGET mqttthermostat PROPERTY CXX_STANDARD 20)
+endif()
+
--- /dev/null
+#pragma once
+
+#include <stdint.h>
+#include <stdio.h>
+
+class HeatingController
+{
+public:
+ virtual ~HeatingController() {}
+
+ virtual void setHeatingActive(bool aActive) = 0;
+};
+
+class DummyHeatingController : public HeatingController
+{
+public:
+ virtual void setHeatingActive(bool aActive) {
+ if (aActive) {
+ printf("Heating activated!\n");
+ }
+ else {
+ printf("Heating deactivated!\n");
+ }
+ }
+};
\ No newline at end of file
--- /dev/null
+// MQTTThermostat.cpp : Defines the entry point for the application.
+//
+
+#include <cstring>
+#include <iostream>
+#include <chrono>
+#include <thread>
+
+#include "MQTTThermostat.h"
+#include "MQTTClient.h"
+
+using namespace std;
+
+#define ADDRESS "tcp://10.0.1.213:1883"
+
+#define QOS 1
+#define TIMEOUT 10000L
+
+void delivered(void* context, MQTTClient_deliveryToken dt)
+{
+ MQTTThermostat* thermostat = static_cast<MQTTThermostat*>(context);
+ thermostat->DeliverToken(dt);
+}
+
+int msgarrvd(void* context, char* topicName, int topicLen, MQTTClient_message* message)
+{
+ MQTTThermostat* thermostat = static_cast<MQTTThermostat*>(context);
+ printf("Message arrived\n");
+ printf(" topic: %s\n", topicName);
+ printf(" message: %.*s\n", message->payloadlen, (char*)message->payload);
+
+ float temperatureSet = stof(string((char*)message->payload));
+ MQTTClient_freeMessage(&message);
+ MQTTClient_free(topicName);
+
+ thermostat->SetSetpoint(temperatureSet);
+
+ return 1;
+}
+
+void connlost(void* context, char* cause)
+{
+ MQTTThermostat* thermostat = static_cast<MQTTThermostat*>(context);
+
+ printf("\nConnection lost\n");
+ printf(" cause: %s\n", cause);
+
+ thermostat->ShouldReconnect();
+}
+
+void MQTTThermostat::Start()
+{
+ mShuttingDown = false;
+
+ MQTTClient_message pubmsg = MQTTClient_message_initializer;
+ MQTTClient_deliveryToken token;
+ int rc;
+ MQTTClient_create(&mClient, ADDRESS, mClientID.c_str(),
+ 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;
+ }
+
+ while (true) {
+ {
+ unique_lock<mutex> lock(mSetpointMutex);
+ if (mSetpointUpdatePending) {
+ processSetpointUpdate();
+ }
+ if (mShuttingDown) {
+ break;
+ }
+
+ mSetpointUpdateCV.wait_for(lock, mMeasurementInterval);
+
+ if (mShuttingDown) {
+ break;
+ }
+
+ if (!mTemperatureSupplier) {
+ continue;
+ }
+
+ if (mShouldReconnect) {
+ if (!connectToServer()) {
+ continue;
+ }
+
+ mShouldReconnect = false;
+ }
+
+ if (mSetpointUpdatePending) {
+ processSetpointUpdate();
+ }
+ }
+
+ float newTemp = mTemperatureSupplier->getTemperature();
+ uint32_t newHumidity = mTemperatureSupplier->getHumidity();
+
+ if (newHumidity != mLastHumidity) {
+ mLastHumidity = newHumidity;
+ char cbuf[256];
+ sprintf(cbuf, "%d", mLastHumidity);
+ pubmsg.payload = (void*)cbuf;
+ pubmsg.payloadlen = strlen(cbuf);
+ pubmsg.qos = QOS;
+ pubmsg.retained = 0;
+ string topic = mTopic;
+ topic.append("humidity/raw");
+ MQTTClient_publishMessage(mClient, topic.c_str(), &pubmsg, &token);
+ WaitForDelivery(token);
+ }
+
+ if (newTemp < mLastTemperature + 0.1 && newTemp > mLastTemperature - 0.1) {
+ continue;
+ }
+
+ if (mLastTemperature > mCurrentSetpoint && newTemp < mCurrentSetpoint) {
+ mHeatingController->setHeatingActive(true);
+ }
+ else if (mLastTemperature < mCurrentSetpoint && newTemp > mCurrentSetpoint) {
+ mHeatingController->setHeatingActive(false);
+ }
+
+ mLastTemperature = newTemp;
+ char cbuf[256];
+ sprintf(cbuf, "%.1f", mLastTemperature);
+ pubmsg.payload = (void*)cbuf;
+ pubmsg.payloadlen = strlen(cbuf);
+ pubmsg.qos = QOS;
+ pubmsg.retained = 0;
+ string topic = mTopic;
+ topic.append("temperature/raw");
+ MQTTClient_publishMessage(mClient, topic.c_str(), &pubmsg, &token);
+ WaitForDelivery(token);
+ }
+
+ MQTTClient_disconnect(mClient, 10000);
+ MQTTClient_destroy(&mClient);
+}
+
+void MQTTThermostat::DeliverToken(MQTTClient_deliveryToken& aToken)
+{
+ unique_lock<mutex> lock(mDeliveryMutex);
+ mDeliveredTokens.push(aToken);
+ mDeliveryCV.notify_one();
+}
+
+void MQTTThermostat::WaitForDelivery(MQTTClient_deliveryToken& aToken)
+{
+ while (true) {
+ unique_lock<mutex> lock(mDeliveryMutex);
+ while (mDeliveredTokens.size()) {
+ if (mDeliveredTokens.top() == aToken) {
+ mDeliveredTokens = stack<MQTTClient_deliveryToken>();
+ return;
+ }
+ mDeliveredTokens.pop();
+ }
+ if (mDeliveryCV.wait_for(lock, 1s) == cv_status::timeout) {
+ printf("Wait for delivery timed out.\n");
+ mShouldReconnect = true;
+ return;
+ }
+ }
+}
+
+void MQTTThermostat::SetSetpoint(float aTemperature)
+{
+ unique_lock<mutex> lock(mSetpointMutex);
+ mSetpointUpdatePending = true;
+ mCurrentSetpoint = aTemperature;
+ mSetpointUpdateCV.notify_one();
+}
+
+void MQTTThermostat::Shutdown()
+{
+ unique_lock<mutex> lock(mSetpointMutex);
+ mShuttingDown = true;
+ mSetpointUpdateCV.notify_one();
+}
+
+bool MQTTThermostat::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 false;
+ }
+
+ string topic = mTopic;
+ topic.append("setpoint/set");
+ if ((rc = MQTTClient_subscribe(mClient, topic.c_str(), QOS)) != MQTTCLIENT_SUCCESS)
+ {
+ printf("Failed to subscribe, return code %d\n", rc);
+ rc = EXIT_FAILURE;
+ }
+
+ return true;
+}
+
+void MQTTThermostat::processSetpointUpdate()
+{
+ if (mCurrentSetpoint > mLastTemperature) {
+ mHeatingController->setHeatingActive(true);
+ }
+ else {
+ mHeatingController->setHeatingActive(false);
+ }
+ if (announceSetpoint()) {
+ mSetpointUpdatePending = false;
+ }
+}
+
+bool MQTTThermostat::announceSetpoint()
+{
+ MQTTClient_message pubmsg = MQTTClient_message_initializer;
+ MQTTClient_deliveryToken token;
+
+ int rc;
+
+ char cbuf[256];
+ sprintf(cbuf, "%.1f", mCurrentSetpoint);
+ pubmsg.payload = (void*)cbuf;
+ pubmsg.payloadlen = strlen(cbuf);
+ pubmsg.qos = QOS;
+ pubmsg.retained = 0;
+ string topic = mTopic;
+ topic.append("setpoint/raw");
+ if (MQTTClient_publishMessage(mClient, topic.c_str(), &pubmsg, &token) != MQTTCLIENT_SUCCESS) {
+ return false;
+ }
+
+ WaitForDelivery(token);
+ return true;
+}
--- /dev/null
+// MQTTThermostat.h : Include file for standard system include files,
+// or project specific include files.
+
+#pragma once
+
+#include <iostream>
+#include <memory>
+#include <stack>
+#include <mutex>
+#include <condition_variable>
+#include <chrono>
+#include <string>
+
+#include "MQTTClient.h"
+#include "TemperatureSupplier.h"
+#include "HeatingController.h"
+
+class MQTTThermostat
+{
+public:
+ MQTTThermostat(const MQTTThermostat&) = delete;
+ MQTTThermostat& operator=(const MQTTThermostat&) = delete;
+ MQTTThermostat(MQTTThermostat&&) = delete;
+ MQTTThermostat& operator=(MQTTThermostat&) = delete;
+
+ static auto& instance()
+ {
+ static MQTTThermostat instance;
+ return instance;
+ }
+
+ void Start();
+
+ void DeliverToken(MQTTClient_deliveryToken& aToken);
+
+ void WaitForDelivery(MQTTClient_deliveryToken& aToken);
+
+ void SetClientID(const std::string& aID) { mClientID = aID; }
+ void SetTemperatureSupplier(std::unique_ptr<TemperatureSupplier>&& aSupplier) { mTemperatureSupplier = std::move(aSupplier); }
+ void SetHeatingController(std::unique_ptr<HeatingController>&& aController) { mHeatingController = std::move(aController); }
+ void SetMeasurementInterval(std::chrono::milliseconds aMS) { mMeasurementInterval = aMS; }
+ void SetTopic(const std::string& aTopic) { mTopic = aTopic; }
+
+ void SetSetpoint(float aTemperature);
+
+ void Shutdown();
+
+ void ShouldReconnect() { mShouldReconnect = true; }
+
+private:
+ MQTTThermostat() = default;
+
+ bool connectToServer();
+
+ void processSetpointUpdate();
+
+ bool announceSetpoint();
+
+ std::unique_ptr<TemperatureSupplier> mTemperatureSupplier;
+ std::unique_ptr<HeatingController> mHeatingController;
+ float mLastTemperature = 0.0;
+ uint32_t mLastHumidity = 0;
+ float mCurrentSetpoint = 19.0;
+ bool mShouldReconnect = true;
+ bool mSetpointUpdatePending = true;
+ bool mShuttingDown = false;
+ MQTTClient mClient;
+ std::string mClientID;
+ std::stack<MQTTClient_deliveryToken> mDeliveredTokens;
+ std::mutex mDeliveryMutex;
+ std::mutex mSetpointMutex;
+ std::condition_variable mDeliveryCV;
+ std::condition_variable mSetpointUpdateCV;
+ std::chrono::milliseconds mMeasurementInterval;
+ std::string mTopic;
+};
\ No newline at end of file
--- /dev/null
+#pragma once
+
+#include <stdint.h>
+
+class TemperatureSupplier
+{
+public:
+ virtual ~TemperatureSupplier() {}
+
+ virtual float getTemperature() = 0;
+ virtual uint32_t getHumidity() = 0;
+};
+
+class DummyTemperatureSupplier : public TemperatureSupplier
+{
+public:
+ virtual float getTemperature() {
+ mCurrentTemperature += 0.01;
+ return mCurrentTemperature;
+ }
+ virtual uint32_t getHumidity() {
+ return mCurrentHumidity++;
+ }
+private:
+ float mCurrentTemperature = 0;
+ uint32_t mCurrentHumidity = 0;
+};
\ No newline at end of file
--- /dev/null
+#include <memory>
+#include <chrono>
+#include <iostream>
+#include <fstream>
+#include <sstream>
+#include <string>
+#include <string.h>
+#include <stdio.h>
+#include <getopt.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <signal.h>
+
+#include "MQTTThermostat.h"
+
+using namespace std;
+
+// Linux only Daemon code.
+
+#define LOG(msg) logStream << msg << "\n"; logStream.flush();
+
+static int pid_fd;
+static ofstream logStream;
+static string pidFile = "~/mqttthermostat.lock";
+static string configFile = "~/mqttthermostat.conf";
+
+static bool shuttingDown = false;
+
+void handle_signal(int sig)
+{
+ if (sig == SIGINT) {
+ LOG("Stopping daemon ...");
+ shuttingDown = true;
+ MQTTThermostat::instance().Shutdown();
+
+ /* Unlock and close lockfile */
+ if (pid_fd != -1) {
+ lockf(pid_fd, F_ULOCK, 0);
+ close(pid_fd);
+ }
+ /* Try to delete lockfile */
+ unlink(pidFile.c_str());
+
+ /* Reset signal handling to default behavior */
+ signal(SIGINT, SIG_DFL);
+ }
+ else if (sig == SIGHUP) {
+ LOG("Reloading daemon config file ...");
+ MQTTThermostat::instance().Shutdown();
+ }
+ else if (sig == SIGCHLD) {
+ LOG("Received SIGCHLD signal");
+ }
+}
+
+void read_config_file()
+{
+ ifstream config(configFile, ios::in);
+ if (!config) {
+ std::cerr << "Failure to open config file!\n";
+ exit(EXIT_FAILURE);
+ }
+
+ string line, topic, clientid;
+ unique_ptr<TemperatureSupplier> tempSupplier;
+ unique_ptr<HeatingController> heatingController;
+ uint32_t measurementInterval;
+
+ while (getline(config, line)) {
+ istringstream config_line(line);
+ string key;
+ if (std::getline(config_line, key, '='))
+ {
+ std::string value;
+ if (getline(config_line, value)) {
+ if (key == "temperature supplier") {
+ if (value.rfind("dummy", 0) == 0) {
+ tempSupplier = make_unique<DummyTemperatureSupplier>();
+ }
+ else {
+ LOG("Unknown temperature supplier specified.");
+ }
+ }
+ else if (key == "heating controller") {
+ if (value.rfind("dummy", 0) == 0) {
+ heatingController = make_unique<DummyHeatingController>();
+ }
+ else {
+ LOG("Unknown heating controller specified.");
+ }
+ }
+ else if (key == "measurement interval") {
+ measurementInterval = stoi(value);
+ }
+ else if (key == "topic") {
+ topic = value;
+ }
+ else if (key == "clientid") {
+ clientid = value;
+ }
+ }
+ }
+ }
+
+ if (!tempSupplier || !heatingController) {
+ LOG("No valid temperature supplier or heating controller found. Exiting.");
+ exit(EXIT_FAILURE);
+ }
+
+ logStream << "CONFIG: topic=" << topic << ", client ID=" << clientid << ", measurement interval=" << measurementInterval << "ms.\n";
+ logStream.flush();
+
+
+ MQTTThermostat::instance().SetTemperatureSupplier(std::move(tempSupplier));
+ MQTTThermostat::instance().SetHeatingController(std::move(heatingController));
+ MQTTThermostat::instance().SetMeasurementInterval(chrono::milliseconds{ measurementInterval });
+ MQTTThermostat::instance().SetTopic(topic);
+ MQTTThermostat::instance().SetClientID(clientid);
+}
+
+void daemonize()
+{
+ pid_t pid, sid;
+
+ pid = fork();
+ if (pid < 0) {
+ exit(EXIT_FAILURE);
+ }
+
+ if (pid > 0) {
+ exit(EXIT_SUCCESS);
+ }
+
+ sid = setsid();
+ if (sid < 0) {
+ LOG("Failed to get unique SID.");
+ exit(EXIT_FAILURE);
+ }
+
+ /* Ignore signal sent from child to parent process */
+ signal(SIGCHLD, SIG_IGN);
+
+ /* Fork off for the second time*/
+ pid = fork();
+
+ /* An error occurred */
+ if (pid < 0) {
+ exit(EXIT_FAILURE);
+ }
+
+ /* Success: Let the parent terminate */
+ if (pid > 0) {
+ exit(EXIT_SUCCESS);
+ }
+
+ umask(0);
+
+ if ((chdir("/")) < 0) {
+ LOG("Failed to changedir to root.");
+ exit(EXIT_FAILURE);
+ }
+
+ long fd;
+ /* Close all open file descriptors */
+ for (fd = sysconf(_SC_OPEN_MAX); fd > 0; fd--) {
+ close(fd);
+ }
+
+ /* Try to write PID of daemon to lockfile */
+ char str[256];
+ uint pid_fd = open(pidFile.c_str(), O_RDWR | O_CREAT, 0640);
+ if (pid_fd < 0) {
+ /* Can't open lockfile */
+ exit(EXIT_FAILURE);
+ }
+ if (lockf(pid_fd, F_TLOCK, 0) < 0) {
+ /* Can't lock file */
+ exit(EXIT_FAILURE);
+ }
+ /* Get current PID */
+ sprintf(str, "%d\n", getpid());
+ /* Write PID to lockfile */
+ write(pid_fd, str, strlen(str));
+}
+
+int main(int argc, char* argv[])
+{
+ static struct option long_options[] = {
+ {"conf_file", required_argument, 0, 'c'},
+ {"test_conf", required_argument, 0, 't'},
+ {"log_file", required_argument, 0, 'l'},
+ {"daemon", no_argument, 0, 'd'},
+ {"pid_file", required_argument, 0, 'p'},
+ {NULL, 0, 0, 0}
+ };
+ int value, option_index = 0, ret;
+ string logFile;
+ int start_daemonized = 0;
+
+ /* Try to process all command line arguments */
+ while ((value = getopt_long(argc, argv, "c:l:p:dh", long_options, &option_index)) != -1) {
+ switch (value) {
+ case 'c':
+ configFile = string(optarg);
+ break;
+ case 'l':
+ logFile = string(optarg);
+ break;
+ case 'p':
+ pidFile = string(optarg);
+ break;
+ case 'd':
+ start_daemonized = 1;
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (start_daemonized) {
+ daemonize();
+ }
+
+ signal(SIGINT, handle_signal);
+ signal(SIGHUP, handle_signal);
+
+ if (!logFile.empty()) {
+ logStream.open(logFile, ios::out | ios::app | ios::ate);
+ }
+
+ if (!logStream) {
+ std::cerr << "Failure to open log file!\n";
+ exit(EXIT_FAILURE);
+ }
+
+ LOG("Starting MQTTThermostat daemon...");
+
+ while (!shuttingDown) {
+ read_config_file();
+ MQTTThermostat::instance().Start();
+ }
+
+ return 0;
+}
--- /dev/null
+#include <memory>
+#include <chrono>
+#include <iostream>
+#include <fstream>
+#include <string>
+
+#include "MQTTThermostat.h"
+
+using namespace std;
+
+// Linux only Daemon code.
+
+int main()
+{
+ MQTTThermostat::instance().SetTemperatureSupplier(make_unique<DummyTemperatureSupplier>());
+ MQTTThermostat::instance().SetHeatingController(make_unique<DummyHeatingController>());
+ MQTTThermostat::instance().SetMeasurementInterval(2000ms);
+ MQTTThermostat::instance().SetTopic("bedroom-staging/thermostat/");
+ MQTTThermostat::instance().SetClientID("bedroom-staging");
+ MQTTThermostat::instance().Start();
+
+ return 0;
+}
--- /dev/null
+temperature supplier=dummy
+heating controller=dummy
+measurement interval=2000
+topic=staging/thermostat/
+clientid=staging
\ No newline at end of file
--- /dev/null
+[Unit]
+Description=MQTT Thermostat Service
+
+[Service]
+Type=forking
+PIDFile=/run/mqttthermostat.pid
+ExecStart=/usr/bin/mqttthermostat \
+ --conf_file /etc/mqttthermostat.conf \
+ --log_file /var/log/mqttthermostat.log \
+ --pid_file /run/mqttthermostat.pid \
+ --daemon
+User=daemoner
+ExecReload=/bin/kill -HUP $MAINPID
+
+[Install]
+WantedBy=multi-user.target
\ No newline at end of file
--- /dev/null
+Subproject commit f7799da95e347bbc930b201b52a1173ebbad45a7