228 lines
7.2 KiB
C++
228 lines
7.2 KiB
C++
#include "temperature_controller.hpp"
|
|
|
|
#include "config.hpp"
|
|
#include <boost/asio/co_spawn.hpp>
|
|
#include <boost/asio/detached.hpp>
|
|
#include <boost/asio/experimental/parallel_group.hpp>
|
|
#include <boost/asio/steady_timer.hpp>
|
|
#include <boost/system/detail/errc.hpp>
|
|
#include <boost/system/errc.hpp>
|
|
#include <boost/system/result.hpp>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <spdlog/spdlog.h>
|
|
|
|
#include "thermometer.hpp"
|
|
|
|
#include <ranczo-io/utils/mqtt_client.hpp>
|
|
#include <ranczo-io/utils/mqtt_topic_builder.hpp>
|
|
#include <ranczo-io/utils/json_helpers.hpp>
|
|
|
|
#include <chrono>
|
|
|
|
#include <boost/asio/awaitable.hpp>
|
|
#include <boost/circular_buffer.hpp>
|
|
#include <boost/core/noncopyable.hpp>
|
|
#include <boost/json/object.hpp>
|
|
|
|
/*
|
|
* TODO
|
|
* * odbieranie configa
|
|
* * KLASA do sterowania przekaźnikiem
|
|
* * KLASA do sterowania temperaturą
|
|
* * zapis do bazy danych
|
|
* * historia użycie i monitorowanie temperatury
|
|
* * Handle ERRORS
|
|
*/
|
|
|
|
namespace ranczo {
|
|
struct TemperatureMeasurement {
|
|
std::chrono::system_clock::time_point when;
|
|
double temperature_C;
|
|
};
|
|
|
|
enum Trend { Fall, Const, Rise };
|
|
|
|
struct RelayThermostat::Impl : private boost::noncopyable {
|
|
executor & _io;
|
|
|
|
AsyncMqttClient & _mqtt;
|
|
|
|
std::unique_ptr< Relay > _relay;
|
|
std::unique_ptr< Thermometer > _temp;
|
|
|
|
boost::asio::steady_timer _tickTimer;
|
|
std::chrono::system_clock::time_point _lastStateChange{std::chrono::system_clock::now()};
|
|
|
|
std::string _room;
|
|
boost::circular_buffer< TemperatureMeasurement > _measurements;
|
|
|
|
double temperature{0.0};
|
|
double targetTemperature{0.0};
|
|
|
|
bool update = false;
|
|
bool enabled = false;
|
|
|
|
Impl(executor & io,
|
|
AsyncMqttClient & mqtt,
|
|
std::unique_ptr< Relay > relay,
|
|
std::unique_ptr< Thermometer > thermometer,
|
|
std::string_view room)
|
|
: _io{io}, _mqtt{mqtt}, _relay{std::move(relay)}, _temp{std::move(thermometer)}, _tickTimer{_io}, _room{room}, _measurements{100} {
|
|
BOOST_ASSERT(_relay);
|
|
BOOST_ASSERT(not _room.empty());
|
|
}
|
|
|
|
~Impl() = default;
|
|
|
|
void setHEaterOn() {
|
|
_lastStateChange = std::chrono::system_clock::now();
|
|
}
|
|
|
|
void setHeaterOff() {
|
|
_lastStateChange = std::chrono::system_clock::now();
|
|
}
|
|
|
|
/// TODO sprawdź po 1 min czy temp faktycznie spada jeśli jest off czy rośnie jeśli jest on
|
|
Trend getTempTrendFromLastChange() const {
|
|
if(_measurements.size() < 2) {
|
|
return Const;
|
|
}
|
|
|
|
const TemperatureMeasurement * closestMeasurement = nullptr;
|
|
|
|
auto it = std::lower_bound(
|
|
_measurements.begin(), _measurements.end(), _lastStateChange, [](const TemperatureMeasurement & measurement, const auto & tp) {
|
|
return measurement.when < tp;
|
|
});
|
|
|
|
// no valid measurements found
|
|
if(it == _measurements.end()) {
|
|
return Const;
|
|
}
|
|
|
|
// Compare the closest temperature to the most recent temperature
|
|
double temperatureAtChange = it->temperature_C;
|
|
double latestTemperature = _measurements.back().temperature_C;
|
|
|
|
if(latestTemperature > temperatureAtChange) {
|
|
return Rise;
|
|
} else if(latestTemperature < temperatureAtChange) {
|
|
return Fall;
|
|
} else {
|
|
return Const;
|
|
}
|
|
}
|
|
|
|
void goToEmergencyMode() {
|
|
enabled = false;
|
|
}
|
|
|
|
void heaterControlLoopTick() {
|
|
if(update == true) {
|
|
/// TODO if temp to low enable heater
|
|
/// TODO if temp to high disable heater
|
|
spdlog::debug("heaterControlLoopTick got update");
|
|
|
|
switch(getTempTrendFromLastChange()) {
|
|
case Const:
|
|
spdlog::debug("No temp change");
|
|
break;
|
|
case Fall:
|
|
spdlog::debug("Temp FALL");
|
|
break;
|
|
case Rise:
|
|
spdlog::debug("Temp RISE");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// auto avg = _measurements.size() ? std::accumulate(measurements.begin(), measurements.end(), 0.0) `/ _measurements.size() :
|
|
// 0.0; spdlog::info("got readout nr {} last temp {} avg {}", _measurements.size(), _measurements.back().temperature_C, avg);
|
|
update = false;
|
|
}
|
|
}
|
|
|
|
awaitable_expected< void > subscribe(std::string_view topic,
|
|
std::function< awaitable_expected< void >(const AsyncMqttClient::CallbackData&) > cb) {
|
|
/// TODO fix
|
|
spdlog::trace("RelayThermostat room {} subscribing to {}", _room, topic);
|
|
ASYNC_CHECK_MSG(_mqtt.subscribe(topic, std::move(cb)), "Heater faild to subscribe on: {}", topic);
|
|
co_return _void{};
|
|
}
|
|
|
|
inline expected< double, boost::system::error_code > get_value(const boost::json::value & jv, std::string_view key) {
|
|
if(auto * obj = jv.if_object()) {
|
|
if(auto * pv = obj->if_contains(key)) {
|
|
auto ovalue = json::as_number(*pv).value();
|
|
if(ovalue)
|
|
return ovalue;
|
|
}
|
|
}
|
|
return unexpected{make_error_code(boost::system::errc::invalid_argument)};
|
|
}
|
|
|
|
awaitable_expected< void > subscribeToCommandUpdate() {
|
|
auto topic = topic::heating::command(_room);
|
|
|
|
auto cb = [=, this](const AsyncMqttClient::CallbackData&data) -> awaitable_expected< void > {
|
|
targetTemperature = TRY(get_value(data.payload, "value"));
|
|
spdlog::trace("Heater target temperature update {} for {}", targetTemperature, _room);
|
|
update = true;
|
|
co_return _void{};
|
|
};
|
|
|
|
ASYNC_CHECK(subscribe(topic, std::move(cb)));
|
|
co_return _void{};
|
|
}
|
|
|
|
awaitable_expected<void> temperatureUpdate(const Thermometer::ThermometerData &data){
|
|
spdlog::info("Got temperature update for: {}", _room);
|
|
|
|
/// TODO sprawdzenie czy temp < treshold
|
|
/// TODO ustawienie przekaźniczka
|
|
/// TODO update watchdog'a
|
|
|
|
co_return _void{};
|
|
}
|
|
|
|
awaitable_expected< void > start() {
|
|
using namespace std::placeholders;
|
|
|
|
// subscribe to a thermometer readings
|
|
ASYNC_CHECK(_temp->on_update(std::bind(&Impl::temperatureUpdate, this, _1)));
|
|
|
|
// subscribe to a thermostat commands feed
|
|
ASYNC_CHECK_MSG(subscribeToCommandUpdate(), "subscribe to command stream failed");
|
|
|
|
/// TODO subscribe to energy measurements
|
|
|
|
// detaching listening thread
|
|
boost::asio::co_spawn(_io, _temp->listen(), boost::asio::detached);
|
|
|
|
// ASYNC_CHECK_MSG(_tickTimer.start(), "failed to start timer");
|
|
// ASYNC_CHECK_MSG(_mqtt.listen(), "failed to listen");
|
|
|
|
co_return _void{};
|
|
}
|
|
};
|
|
|
|
RelayThermostat::RelayThermostat(executor & io, AsyncMqttClient &mqtt, std::unique_ptr< Relay > relay, std::unique_ptr<Thermometer> thermometer, std::string_view room)
|
|
: _impl{std::make_unique< Impl >(io, mqtt, std::move(relay), std::move(thermometer), room)} {
|
|
}
|
|
|
|
RelayThermostat::~RelayThermostat() = default;
|
|
awaitable_expected< void > RelayThermostat::start() noexcept {
|
|
BOOST_ASSERT(_impl);
|
|
|
|
return _impl->start();
|
|
}
|
|
|
|
void RelayThermostat::stop() noexcept
|
|
{
|
|
BOOST_ASSERT(_impl);
|
|
}
|
|
|
|
} // namespace ranczo
|