221 lines
7.1 KiB
C++
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
|