ranczo-io/services/floorheat_svc/heater_controller.cpp
2025-08-06 14:16:44 +02:00

221 lines
7.1 KiB
C++

#include "heater_controller.hpp"
#include "config.hpp"
#include <spdlog/spdlog.h>
#include <ranczo-io/utils/mqtt_client.hpp>
#include <ranczo-io/utils/mqtt_topic_builder.hpp>
#include <ranczo-io/utils/timer.hpp>
#include <algorithm>
#include <chrono>
#include <format>
#include <memory>
#include <string_view>
#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 {
using heater_expected_void = expected< void, boost::system::error_code >;
struct TemperatureMeasurement {
std::chrono::system_clock::time_point when;
double temperature_C;
};
enum Trend { Fall, Const, Rise };
struct ResistiveFloorHeater::Impl : private boost::noncopyable {
AsyncMqttClient _mqtt_client;
PeriodicTimer _tickTimer;
std::string _room;
std::chrono::system_clock::time_point _lastStateChange{std::chrono::system_clock::now()};
boost::circular_buffer< TemperatureMeasurement > _measurements;
bool _relayState{false};
double temperature{0.0};
double targetTemperature{0.0};
bool update = false;
bool enabled = false;
Impl(boost::asio::any_io_executor & io, std::string_view room)
: _mqtt_client{io},
_tickTimer{io, std::chrono::seconds{1}, [&]() { heaterControlLoopTick(); }},
_room{room.data(), room.size()},
_measurements{100} {}
~Impl() = default;
void setHEaterOn() {
_lastStateChange = std::chrono::system_clock::now();
_relayState = true;
}
void setHeaterOff() {
_lastStateChange = std::chrono::system_clock::now();
_relayState = false;
}
/// 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 boost::json::value & object) > cb) {
spdlog::trace("Heater subscribing to {}", topic);
ASYNC_CHECK_MSG(_mqtt_client.subscribe(topic, std::move(cb)), "Heater faild to subscribe on: {}", topic);
co_return heater_expected_void{};
}
inline double to_double(const boost::json::value & v) const {
if(v.is_double())
return v.as_double();
else if(v.is_int64())
return static_cast< double >(v.as_int64());
else if(v.is_uint64())
return static_cast< double >(v.as_uint64());
throw std::runtime_error("Invalid type for double conversion");
}
awaitable_expected< void > subscribeToTemperatureUpdate() {
auto topic = topic::temperature::floor(_room);
auto cb = [=, this](const boost::json::value & object) -> awaitable_expected< void > {
temperature = to_double(object.at("value"));
spdlog::trace("Heater temperature update {} for {}", temperature, _room);
_measurements.push_back({std::chrono::system_clock::now(), temperature});
update = true;
co_return heater_expected_void{};
};
ASYNC_CHECK(subscribe(topic, std::move(cb)));
co_return heater_expected_void{};
}
awaitable_expected< void > subscribeToCommandUpdate() {
auto topic = topic::heating::command(_room);
auto cb = [=, this](const boost::json::value & object) -> awaitable_expected< void > {
targetTemperature = to_double(object.at("value"));
spdlog::trace("Heater target temperature update {} for {}", targetTemperature, _room);
update = true;
co_return heater_expected_void{};
};
ASYNC_CHECK(subscribe(topic, std::move(cb)));
co_return heater_expected_void{};
}
awaitable_expected< void > subscribeToTargetProfileUpdate() {
auto topic = std::format("home/{}/floor/heating/profile/set", _room);
auto cb = [=, this](const boost::json::value & object) -> awaitable_expected< void > { //
spdlog::warn("not implemented");
co_return heater_expected_void{};
};
ASYNC_CHECK(subscribe(topic, std::move(cb)));
co_return heater_expected_void{};
}
awaitable_expected< void > start() {
ASYNC_CHECK_MSG(subscribeToTemperatureUpdate(), "subscribe to temp update failed");
ASYNC_CHECK_MSG(subscribeToCommandUpdate(), "subscribe to temp update failed");
ASYNC_CHECK_MSG(subscribeToTargetProfileUpdate(), "subscribe to profile update failed");
ASYNC_CHECK_MSG(_tickTimer.start(), "failed to start timer");
ASYNC_CHECK_MSG(_mqtt_client.listen(), "failed to listen");
co_return heater_expected_void{};
}
};
ResistiveFloorHeater::ResistiveFloorHeater(boost::asio::any_io_executor & io, std::string_view room)
: _impl{std::make_unique< Impl >(io, room)} {}
ResistiveFloorHeater::~ResistiveFloorHeater() = default;
awaitable_expected< void > ResistiveFloorHeater::start() noexcept {
BOOST_ASSERT(_impl);
return _impl->start();
}
} // namespace ranczo