ranczo-io/services/floorheat_svc/temperature_controller.cpp
2025-08-14 11:06:13 +02:00

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