]> git.basschouten.com Git - mqttthermostat.git/commitdiff
Initial commit.
authorBas Schouten <bas@basschouten.com>
Mon, 30 Jan 2023 03:38:57 +0000 (04:38 +0100)
committerBas Schouten <bas@basschouten.com>
Mon, 30 Jan 2023 03:38:57 +0000 (04:38 +0100)
14 files changed:
.gitignore [new file with mode: 0644]
.gitmodules [new file with mode: 0644]
CMakeLists.txt [new file with mode: 0644]
CMakePresets.json [new file with mode: 0644]
MQTTThermostat/CMakeLists.txt [new file with mode: 0644]
MQTTThermostat/HeatingController.h [new file with mode: 0644]
MQTTThermostat/MQTTThermostat.cpp [new file with mode: 0644]
MQTTThermostat/MQTTThermostat.h [new file with mode: 0644]
MQTTThermostat/TemperatureSupplier.h [new file with mode: 0644]
MQTTThermostat/maindaemon.cpp [new file with mode: 0644]
MQTTThermostat/maintest.cpp [new file with mode: 0644]
mqttthermostat.conf [new file with mode: 0644]
mqttthermostat.service [new file with mode: 0644]
paho.mqtt.c [new submodule]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..dd2f824
--- /dev/null
@@ -0,0 +1,2 @@
+.vs/*
+out/*
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
new file mode 100644 (file)
index 0000000..e0e40ec
--- /dev/null
@@ -0,0 +1,3 @@
+[submodule "paho.mqtt.c"]
+       path = paho.mqtt.c
+       url = https://github.com/eclipse/paho.mqtt.c
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644 (file)
index 0000000..748e67f
--- /dev/null
@@ -0,0 +1,48 @@
+# 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
diff --git a/CMakePresets.json b/CMakePresets.json
new file mode 100644 (file)
index 0000000..d3c8220
--- /dev/null
@@ -0,0 +1,61 @@
+{
+    "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"
+            }
+        }
+    ]
+}
diff --git a/MQTTThermostat/CMakeLists.txt b/MQTTThermostat/CMakeLists.txt
new file mode 100644 (file)
index 0000000..f62271c
--- /dev/null
@@ -0,0 +1,26 @@
+# 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()
+
diff --git a/MQTTThermostat/HeatingController.h b/MQTTThermostat/HeatingController.h
new file mode 100644 (file)
index 0000000..bb48085
--- /dev/null
@@ -0,0 +1,25 @@
+#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
diff --git a/MQTTThermostat/MQTTThermostat.cpp b/MQTTThermostat/MQTTThermostat.cpp
new file mode 100644 (file)
index 0000000..02f551e
--- /dev/null
@@ -0,0 +1,246 @@
+// 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;
+}
diff --git a/MQTTThermostat/MQTTThermostat.h b/MQTTThermostat/MQTTThermostat.h
new file mode 100644 (file)
index 0000000..7d0d791
--- /dev/null
@@ -0,0 +1,76 @@
+// 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
diff --git a/MQTTThermostat/TemperatureSupplier.h b/MQTTThermostat/TemperatureSupplier.h
new file mode 100644 (file)
index 0000000..5a563ed
--- /dev/null
@@ -0,0 +1,27 @@
+#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
diff --git a/MQTTThermostat/maindaemon.cpp b/MQTTThermostat/maindaemon.cpp
new file mode 100644 (file)
index 0000000..25706c7
--- /dev/null
@@ -0,0 +1,245 @@
+#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;
+}
diff --git a/MQTTThermostat/maintest.cpp b/MQTTThermostat/maintest.cpp
new file mode 100644 (file)
index 0000000..af528bc
--- /dev/null
@@ -0,0 +1,23 @@
+#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;
+}
diff --git a/mqttthermostat.conf b/mqttthermostat.conf
new file mode 100644 (file)
index 0000000..1a4e000
--- /dev/null
@@ -0,0 +1,5 @@
+temperature supplier=dummy
+heating controller=dummy
+measurement interval=2000
+topic=staging/thermostat/
+clientid=staging
\ No newline at end of file
diff --git a/mqttthermostat.service b/mqttthermostat.service
new file mode 100644 (file)
index 0000000..2d74c83
--- /dev/null
@@ -0,0 +1,16 @@
+[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
diff --git a/paho.mqtt.c b/paho.mqtt.c
new file mode 160000 (submodule)
index 0000000..f7799da
--- /dev/null
@@ -0,0 +1 @@
+Subproject commit f7799da95e347bbc930b201b52a1173ebbad45a7