Add more logic to temperature controller

This commit is contained in:
Bartosz Wieczorek 2025-11-18 14:58:48 +01:00
parent 7c969cff91
commit b01271fa3b
4 changed files with 323 additions and 161 deletions

View File

@ -31,10 +31,12 @@ using unexpected = tl::unexpected< T >;
#error "No expected implementation available"
#endif
template < typename T >
using awaitable_expected = boost::asio::awaitable< expected< T > >;
template < typename T >
using awaitable = boost::asio::awaitable< T >;
using _void = expected< void >;
using ::boost::system::errc::make_error_code;
@ -65,10 +67,10 @@ using ::boost::system::errc::make_error_code;
*_result; \
})
#define CHECK(failable) \
do { \
auto _result = await(failable); \
if(!_result) \
#define CHECK(failable) \
do { \
auto _result = await(failable); \
if(!_result) \
return unexpected(_result.error()); \
} while(0)
@ -107,10 +109,10 @@ using ::boost::system::errc::make_error_code;
*_result; \
})
#define ASYNC_CHECK(failable) \
do { \
auto _result = co_await (failable); \
if(!_result) \
#define ASYNC_CHECK(failable) \
do { \
auto _result = co_await (failable); \
if(!_result) \
co_return unexpected(_result.error()); \
} while(0)
@ -123,4 +125,13 @@ using ::boost::system::errc::make_error_code;
} \
} while(0)
#define ASYNC_CHECK_TRANSFORM_ERROR(failable, transform) \
do { \
auto _result = co_await (failable); \
if(!_result) { \
spdlog::error(__VA_ARGS__); \
co_return unexpected{transform(_result.error())}; \
} \
} while(0)
} // namespace ranczo

View File

@ -1,8 +1,31 @@
#include "config.hpp"
#include <boost/system/errc.hpp>
#include <boost/uuid/basic_random_generator.hpp>
#include <exception>
#include <ranczo-io/utils/config.hpp>
#include <sqlite3.h>
namespace ranczo {
/// UWAGA:
/// Funkcja zakłada, że pod adresem data()[size()] znajduje się legalna pamięć.
/// To jest bezpieczne tylko, jeśli string_view pochodzi z:
/// - C-stringa
/// - std::string
/// - innego bufora, który ma '\0' za końcem
///
/// Jeśli string_view zawiera dokładnie taki fragment:
/// "abc" → OK
/// "abc\0xyz" → będzie wykrywać '\0' po size()==3 → OK
/// Jeśli string_view powstał z danych binary/packed → UWAŻAJ.
inline bool is_null_terminated(std::string_view sv) {
if(sv.empty())
return true; // pusty string_view jest "C-stringiem"
const char * ptr = sv.data();
return ptr[sv.size()] == '\0';
}
std::string SettingsStore::Value::to_db_text() const {
using namespace boost::json;
@ -36,7 +59,7 @@ std::string SettingsStore::Value::to_db_text() const {
return serialize(o);
}
SettingsStore::Value SettingsStore::Value::from_db_text(const std::string & s) {
SettingsStore::Value SettingsStore::Value::from_db_text(std::string_view s) {
using namespace boost::json;
value j;
@ -86,7 +109,7 @@ SettingsStore::Value SettingsStore::Value::from_db_text(const std::string & s) {
}
}
SettingsStore::SettingsStore(const std::string & app_db_path, executor_type main_exec, std::size_t db_threads)
SettingsStore::SettingsStore(std::string_view app_db_path, executor_type main_exec, std::size_t db_threads)
: main_exec_(main_exec), db_pool_(db_threads), db_path_(app_db_path) {
open_or_create();
prepare_schema();
@ -100,7 +123,12 @@ SettingsStore::~SettingsStore() {
db_pool_.join();
}
bool SettingsStore::contains(const std::string & component, const std::string & key) const {
bool SettingsStore::contains(std::string_view component, std::string_view key) const {
BOOST_ASSERT(component.size() < 256);
BOOST_ASSERT(key.size() < 256);
BOOST_ASSERT(is_null_terminated(component));
BOOST_ASSERT(is_null_terminated(key));
spdlog::debug("SettingsStore::contains_sync({}, {})", component, key);
const char * sql =
@ -112,8 +140,8 @@ bool SettingsStore::contains(const std::string & component, const std::string &
auto stmt_guard = make_stmt_guard(stmt);
check_sqlite(sqlite3_bind_text(stmt, 1, component.c_str(), -1, SQLITE_TRANSIENT), "bind component");
check_sqlite(sqlite3_bind_text(stmt, 2, key.c_str(), -1, SQLITE_TRANSIENT), "bind key");
check_sqlite(sqlite3_bind_text(stmt, 1, component.data(), -1, SQLITE_TRANSIENT), "bind component");
check_sqlite(sqlite3_bind_text(stmt, 2, key.data(), -1, SQLITE_TRANSIENT), "bind key");
int rc = sqlite3_step(stmt);
if(rc == SQLITE_ROW) {
@ -127,7 +155,12 @@ bool SettingsStore::contains(const std::string & component, const std::string &
return false;
}
awaitable_expected<bool> SettingsStore::async_contains(const std::string & component, const std::string & key) const {
awaitable_expected< bool > SettingsStore::async_contains(std::string_view component, std::string_view key) const noexcept {
BOOST_ASSERT(component.size() < 256);
BOOST_ASSERT(key.size() < 256);
BOOST_ASSERT(is_null_terminated(component));
BOOST_ASSERT(is_null_terminated(key));
auto caller_exec = co_await boost::asio::this_coro::executor;
bool exists = false;
@ -147,7 +180,12 @@ awaitable_expected<bool> SettingsStore::async_contains(const std::string & compo
co_return exists;
}
void SettingsStore::save(const std::string & component, const std::string & key, const Value & value) {
void SettingsStore::save(std::string_view component, std::string_view key, const Value & value) {
BOOST_ASSERT(component.size() < 256);
BOOST_ASSERT(key.size() < 256);
BOOST_ASSERT(is_null_terminated(component));
BOOST_ASSERT(is_null_terminated(key));
spdlog::debug("SettingsStore::save_sync({}, {})", component, key);
const char * sql =
@ -160,8 +198,8 @@ void SettingsStore::save(const std::string & component, const std::string & key,
check_sqlite(sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr), "prepare INSERT/UPDATE");
auto stmt_guard = make_stmt_guard(stmt);
check_sqlite(sqlite3_bind_text(stmt, 1, component.c_str(), -1, SQLITE_TRANSIENT), "bind component");
check_sqlite(sqlite3_bind_text(stmt, 2, key.c_str(), -1, SQLITE_TRANSIENT), "bind key");
check_sqlite(sqlite3_bind_text(stmt, 1, component.data(), -1, SQLITE_TRANSIENT), "bind component");
check_sqlite(sqlite3_bind_text(stmt, 2, key.data(), -1, SQLITE_TRANSIENT), "bind key");
std::string s = value.to_db_text();
check_sqlite(sqlite3_bind_text(stmt, 3, s.c_str(), -1, SQLITE_TRANSIENT), "bind value");
@ -170,7 +208,12 @@ void SettingsStore::save(const std::string & component, const std::string & key,
check_sqlite(rc == SQLITE_DONE ? SQLITE_OK : rc, "step INSERT/UPDATE");
}
awaitable_expected< void > SettingsStore::async_save(const std::string & component, const std::string & key, const Value & value) {
awaitable_expected< void > SettingsStore::async_save(std::string_view component, std::string_view key, const Value & value) noexcept {
BOOST_ASSERT(component.size() < 256);
BOOST_ASSERT(key.size() < 256);
BOOST_ASSERT(is_null_terminated(component));
BOOST_ASSERT(is_null_terminated(key));
auto caller_exec = co_await boost::asio::this_coro::executor;
// do puli DB
@ -226,8 +269,13 @@ void SettingsStore::check_sqlite(int rc, const char * what) {
}
}
awaitable_expected< std::optional< SettingsStore::Value > > SettingsStore::async_get(const std::string & component,
const std::string & key) {
awaitable_expected< std::optional< SettingsStore::Value > > SettingsStore::async_get(std::string_view component,
std::string_view key) noexcept {
BOOST_ASSERT(component.size() < 256);
BOOST_ASSERT(key.size() < 256);
BOOST_ASSERT(is_null_terminated(component));
BOOST_ASSERT(is_null_terminated(key));
auto caller_exec = co_await boost::asio::this_coro::executor;
std::optional< Value > result;
@ -248,7 +296,12 @@ awaitable_expected< std::optional< SettingsStore::Value > > SettingsStore::async
co_return result;
}
std::optional< SettingsStore::Value > SettingsStore::get(const std::string & component, const std::string & key) {
std::optional< SettingsStore::Value > SettingsStore::get(std::string_view component, std::string_view key) {
BOOST_ASSERT(component.size() < 256);
BOOST_ASSERT(key.size() < 256);
BOOST_ASSERT(is_null_terminated(component));
BOOST_ASSERT(is_null_terminated(key));
spdlog::debug("SettingsStore::get_sync({}, {})", component, key);
const char * sql =
@ -260,8 +313,8 @@ std::optional< SettingsStore::Value > SettingsStore::get(const std::string & com
auto stmt_guard = make_stmt_guard(stmt);
check_sqlite(sqlite3_bind_text(stmt, 1, component.c_str(), -1, SQLITE_TRANSIENT), "bind component");
check_sqlite(sqlite3_bind_text(stmt, 2, key.c_str(), -1, SQLITE_TRANSIENT), "bind key");
check_sqlite(sqlite3_bind_text(stmt, 1, component.data(), -1, SQLITE_TRANSIENT), "bind component");
check_sqlite(sqlite3_bind_text(stmt, 2, key.data(), -1, SQLITE_TRANSIENT), "bind key");
int rc = sqlite3_step(stmt);
if(rc == SQLITE_ROW) {

View File

@ -1,11 +1,15 @@
// settings_store.hpp
#pragma once
#include "tl/expected.hpp"
#include <boost/system/detail/errc.hpp>
#include <config.hpp>
#include <cstdint>
#include <exception>
#include <optional>
#include <string>
#include <string_view>
#include <type_traits>
#include <variant>
@ -38,6 +42,7 @@ class SettingsStore {
Value(const char * v) : data_(std::string(v)) {}
Value(const std::string & v) : data_(v) {}
Value(std::string && v) : data_(std::move(v)) {}
Value(std::string_view v) : data_(std::string{v.data(), v.size()}) {}
Value(const Json & v) : data_(v) {}
Value(Json && v) : data_(std::move(v)) {}
Value(bool v) : data_(v) {}
@ -64,7 +69,7 @@ class SettingsStore {
std::string to_db_text() const;
// TEXT z DB -> Value
static Value from_db_text(const std::string & s);
static Value from_db_text(std::string_view s);
private:
Variant data_;
@ -73,23 +78,21 @@ class SettingsStore {
// --------------------- Ctor / Dtor ---------------------
// app_db_path = plik SQLite dla danej aplikacji
explicit SettingsStore(const std::string & app_db_path, executor_type main_exec, std::size_t db_threads = 1);
explicit SettingsStore(std::string_view app_db_path, executor_type main_exec, std::size_t db_threads = 1);
~SettingsStore();
bool contains(const std::string & component, const std::string & key) const;
awaitable_expected< bool > async_contains(const std::string & component, const std::string & key) const;
bool contains(std::string_view component, std::string_view key) const;
std::optional< Value > get(std::string_view component, std::string_view key);
void save(std::string_view component, std::string_view key, const Value & value);
// zwraca std::nullopt jeśli brak wpisu
std::optional< Value > get(const std::string & component, const std::string & key);
awaitable_expected< std::optional< Value > > async_get(const std::string & component, const std::string & key);
// zapis Value
void save(const std::string & component, const std::string & key, const Value & value);
awaitable_expected< void > async_save(const std::string & component, const std::string & key, const Value & value);
awaitable_expected< bool > async_contains(std::string_view component, std::string_view key) const noexcept;
awaitable_expected< std::optional< Value > > async_get(std::string_view component, std::string_view key) noexcept;
awaitable_expected< void > async_save(std::string_view component, std::string_view key, const Value & value) noexcept;
// overloady dla konkretnych typów:
template < typename T >
void save(const std::string & component, const std::string & key, const T & v) {
void save(std::string_view component, std::string_view key, const T & v) {
if constexpr(std::is_same_v< int, T >) {
save(component, key, Value{int64_t{v}});
} else {
@ -98,8 +101,12 @@ class SettingsStore {
}
// overloady async_save dla typów prostych:
template < typename T >
awaitable_expected< void > async_save(const std::string & component, const std::string & key, const T & v) {
co_return co_await async_save(component, key, Value{v});
awaitable_expected< void > async_save(std::string_view component, std::string_view key, const T & v) noexcept {
if constexpr(std::is_same_v< int, T >) {
co_return co_await async_save(component, key, Value{int64_t{v}});
} else {
co_return co_await async_save(component, key, Value{v});
}
}
private:
@ -135,39 +142,59 @@ class ComponentSettingsStore {
// ------------- SYNCHRONICZNE API -------------
// get_sync(component, key) -> get_sync(key)
std::optional< Value > get(const std::string & key) {
std::optional< Value > get(std::string_view key) {
return store_.get(component_, key);
}
template < typename T >
T get_value(const std::string & key) {
T get_value(std::string_view key) {
BOOST_ASSERT(store_.contains(key));
return store_.get(component_, key).value().get_if< T >();
}
awaitable_expected< std::optional< Value > > async_get(const std::string & key) {
template < typename T >
awaitable< T > async_get_store_default(std::string_view key, const T & _default) {
auto contains = co_await async_contains(key);
if(not contains) {
auto status = co_await async_save(key, _default);
if(not status) {
spdlog::warn("Cant save {}/{} to configuration store, returning default", component_, key);
co_return _default;
}
}
// storage contains value, this should be safe
auto value = co_await async_get(key);
if(not value) {
spdlog::error("Component {} should contain {} but it dosn't, returning default", component_, key);
co_return _default;
}
co_return (**value).template get_if< T >(); // TODO can be different type that T
}
awaitable_expected< std::optional< Value > > async_get(std::string_view key) {
co_return co_await store_.async_get(component_, key);
}
template < typename T >
void save(const std::string & key, const T & v) {
void save(std::string_view key, const T & v) {
store_.save(component_, key, v);
}
template < typename T >
awaitable_expected< void > async_save(const std::string & key, const T & v) {
awaitable_expected< void > async_save(std::string_view key, const T & v) {
co_return co_await store_.async_save(component_, key, v);
}
bool contains(const std::string & key) const {
bool contains(std::string_view key) const {
return store_.contains(component_, key);
}
awaitable_expected< bool > async_contains(const std::string & key) {
awaitable< bool > async_contains(std::string_view key) {
co_return co_await store_.async_contains(component_, key);
}
// dostęp do nazwy komponentu (opcjonalnie)
const std::string & component() const noexcept {
std::string_view component() const noexcept {
return component_;
}

View File

@ -8,12 +8,13 @@
#include <boost/asio/steady_timer.hpp>
#include <boost/smart_ptr/shared_ptr.hpp>
#include <boost/system/detail/errc.hpp>
#include <boost/system/detail/error_category.hpp>
#include <boost/system/detail/error_code.hpp>
#include <boost/system/errc.hpp>
#include <boost/system/result.hpp>
#include <functional>
#include <memory>
#include <optional>
#include <queue>
#include <spdlog/spdlog.h>
#include "ranczo-io/utils/config.hpp"
@ -36,21 +37,16 @@
#include <ranczo-io/utils/time.hpp>
/*
* TODO odbieranie i zapisywanie konfiguracji na dysku.
* Dopisać klase która będzie handlowała aktualną konfiguracją
*
* TODO dopisać maszynę stanów, idealnie korzystając z boost::sml
* Maszyna powinna zarządzać przejściami pomiedzy stanami w regulatorze
*/
namespace ranczo {
enum class ThermostatState { Enabled, Disabled, Error };
enum class Trend { Fall, Const, Rise };
std::optional< ThermostatState > ThermostatState_from_string(std::string_view state) {
std::string s(state.begin(), state.end());
std::optional< ThermostatState > ThermostatState_from_string(std::optional< std::string > state) {
if(not state) {
return std::nullopt;
}
std::string s(state->begin(), state->end());
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return static_cast< char >(std::tolower(c)); });
if(s == "enabled")
@ -91,7 +87,7 @@ inline expected< T > readValue(const boost::json::value & jv, std::string_view k
if(!pv->is_string()) {
return unexpected{make_error_code(boost::system::errc::invalid_argument)};
}
auto v = ThermostatState_from_string(pv->as_string());
auto v = ThermostatState_from_string(std::make_optional< std::string >(pv->as_string()));
if(not v)
return unexpected{make_error_code(boost::system::errc::invalid_argument)};
return *v;
@ -277,7 +273,8 @@ struct ThermometerMeasurements {
* @param epsilon_deg_per_min, trend specyfier
* @return a trend if trent can be calculated or nullopt
*/
std::optional< Trend > temperatureTrend(std::chrono::seconds window = std::chrono::minutes(5), double epsilon_deg_per_min = 0.2) const {
std::optional< Trend > temperatureTrend(std::chrono::nanoseconds window = std::chrono::minutes(5),
double epsilon_deg_per_min = 0.2) const {
if(auto last = timeSinceLastRead(); not last.has_value()) {
spdlog::debug("No temperature samples available");
return std::nullopt;
@ -378,7 +375,32 @@ struct RelayThermostat::Impl : private boost::noncopyable {
std::chrono::nanoseconds _tickTime{std::chrono::seconds(60)}; // minimalny czas ON/OFF
std::chrono::nanoseconds _slopeWindow{std::chrono::minutes(5)};
double _slopeDT_c{0.2}; // [°C / min]
std::chrono::minutes _sensorTimeout{std::chrono::minutes(5)};
std::chrono::nanoseconds _sensorTimeout{std::chrono::minutes(5)};
enum RuntimeError { NoTemperatureMeasurements = 1, IoError };
struct ErrorCategory : public boost::system::error_category {
// error_category interface
public:
const char * name() const noexcept override {
return "ErrorCategory";
}
std::string message(int ev) const override {
RuntimeError control = static_cast< RuntimeError >(ev);
switch(control) {
case RuntimeError::NoTemperatureMeasurements:
return "NoTemperatureMeasurements";
break;
case RuntimeError::IoError:
return "IoError";
default:
break;
}
}
};
static boost::system::error_code make_error(RuntimeError ec) {
static auto TemperatureControlerCategory = ErrorCategory{};
return boost::system::error_code{static_cast< int >(ec), TemperatureControlerCategory};
}
// --- nowe helpery ---
awaitable_expected< void > controlLoop() {
@ -387,9 +409,27 @@ struct RelayThermostat::Impl : private boost::noncopyable {
boost::asio::steady_timer timer(_io);
for(;;) {
ASYNC_CHECK(applyControlStep());
/// TODO this should be a good point for handling errors
// we got couple of errors to handle
// 1. We cannot disable relay
// 2. We have a relay disabled but temperature rises
// 3. other error, relay state is not knows etc
auto expectedStep = co_await applyControlStep();
if(not expectedStep) {
RuntimeError err = static_cast< RuntimeError >(expectedStep.error().value());
switch(err) {
case RuntimeError::NoTemperatureMeasurements:
/// TODO disable relay
/// TODO set state error
break;
default:
break;
}
/// He can handle uncought error here
/// All of them should be 'panic mode'
}
// odczekaj minimalny tick
// Wait tick
timer.expires_after(duration_cast< steady_clock::duration >(_tickTime));
boost::system::error_code ec;
@ -406,19 +446,6 @@ struct RelayThermostat::Impl : private boost::noncopyable {
awaitable_expected< void > applyControlStep() {
using namespace std::chrono;
// 1) sprawdzenie czy mamy odczyty temperatury
auto dtLastOpt = _thermo.timeSinceLastRead();
if(!dtLastOpt) {
spdlog::debug("No temperature samples yet for {}/{}", _room, _zone);
co_return _void{};
}
auto dtLast = *dtLastOpt;
// 2) spójne sprawdzenie timeoutu, trendu i ewentualnych stuck-relay
ASYNC_CHECK(checkStateAndTrend(dtLast));
// 3) logika zależna od stanu termostatu
switch(_state) {
case ThermostatState::Error:
ASYNC_CHECK(handleErrorState());
@ -434,47 +461,72 @@ struct RelayThermostat::Impl : private boost::noncopyable {
co_return _void{};
}
awaitable_expected< void > checkStateAndTrend(std::chrono::nanoseconds dtLast) {
bool checkLastTemperatureRead() {
// brak update'u temperatury
auto dtLast = _thermo.timeSinceLastRead();
if(dtLast > _sensorTimeout) {
return false;
}
return true;
}
awaitable_expected< bool > preconditions() {
using namespace std::chrono;
// brak update'u temperatury
if(dtLast > _sensorTimeout) {
ASYNC_CHECK(error("temperature sensor timeout (> 5 minutes without update)"));
// check if temperature is constantly read
if(not checkLastTemperatureRead()) {
spdlog::warn("temperature sensor timeout (> 5 minutes without update) for {}/{}", _room, _zone);
co_return false;
}
// jeśli już jesteśmy w ERROR, to nie ma sensu dalej analizować trendu
if(_state == ThermostatState::Error) {
co_return _void{};
auto tempOpt = _thermo.currentTemperature();
if(!tempOpt) {
spdlog::warn("No temperature samples for {}/{}", _room, _zone);
co_return false;
}
// aktualny trend temperatury (na konfigurowalnym oknie)
auto trendOpt = _thermo.temperatureTrend(duration_cast< seconds >(_slopeWindow), _slopeDT_c);
auto trendOpt = _thermo.temperatureTrend(_slopeWindow, _slopeDT_c);
if(!trendOpt) {
co_return _void{};
spdlog::warn("No temperature samples for {}/{} for last {}s",
_room,
_zone,
std::chrono::duration_cast< std::chrono::seconds >(_slopeWindow).count());
co_return false;
}
auto trend = *trendOpt;
auto st = ASYNC_TRY(_relay->state());
const bool relayOn = (st == Relay::State::On);
auto trend = *trendOpt;
// state should be cached
auto mkerr = [](auto) { return make_error(RuntimeError::IoError); };
const bool relayOn = ASYNC_TRY_TRANSFORM_ERROR(_relay->state(), mkerr) == Relay::State::On;
// 2a) relay OFF, a temperatura rośnie => przekaźnik zawiesił się na ON
if(!relayOn && trend == Trend::Rise) {
ASYNC_CHECK(error("relay stuck ON: temperature rising while relay is commanded OFF"));
spdlog::warn("relay stuck ON: temperature rising while relay is commanded OFF for {}/{}", _room, _zone);
co_return false;
}
// 2b) relay ON, a trend != Rise => przekaźnik zawiesił się na OFF
if(relayOn && trend != Trend::Rise) {
ASYNC_CHECK(error("relay stuck OFF: temperature not rising while relay is commanded ON"));
spdlog::warn("relay stuck OFF: temperature not rising while relay is commanded ON for {}/{}", _room, _zone);
co_return false;
}
co_return _void{};
co_return true;
}
awaitable_expected< void > handleErrorState() {
auto st = ASYNC_TRY(_relay->state());
if(st == Relay::State::On) {
spdlog::warn("Forcing relay OFF in ERROR state for {}/{}", _room, _zone);
ASYNC_CHECK_MSG(_relay->off(), "Emergency relay OFF failed!");
/// check preconditions, if ok release error state
// only relay->state can fail
auto preconditionsMet = ASYNC_TRY(preconditions());
if(preconditionsMet) {
spdlog::info("Switching back to Enabled due to met preconditions {}/{}", _room, _zone);
_state = ThermostatState::Enabled; // should be from previous
} else {
auto relay_on = ASYNC_TRY(_relay->state()) == Relay::State::On;
if(relay_on) {
spdlog::warn("Forcing relay OFF in ERROR state for {}/{}", _room, _zone);
ASYNC_CHECK_MSG(_relay->off(), "Emergency relay OFF failed!");
}
}
co_return _void{};
}
@ -483,7 +535,7 @@ struct RelayThermostat::Impl : private boost::noncopyable {
auto st = ASYNC_TRY(_relay->state());
if(st == Relay::State::On) {
spdlog::info("RelayThermostat disabling relay because thermostat is Disabled for {}/{}", _room, _zone);
ASYNC_CHECK_MSG(_relay->off(), "relay OFF failed");
ASYNC_CHECK_MSG(safe_off(), "relay OFF failed"); // TODO basically PANIC mode on fail
}
co_return _void{};
}
@ -491,10 +543,14 @@ struct RelayThermostat::Impl : private boost::noncopyable {
awaitable_expected< void > handleEnabledState() {
using namespace std::chrono;
auto tempOpt = _thermo.currentTemperature();
if(!tempOpt) {
auto preconditionsMet = ASYNC_TRY(preconditions());
if(not preconditionsMet) {
spdlog::warn("RelayThermostat turning set disabled state due to failed preconditions");
_state = ThermostatState::Error;
co_return _void{};
}
auto tempOpt = _thermo.currentTemperature();
const double temp = *tempOpt;
const auto now = system_clock::now();
@ -523,7 +579,7 @@ struct RelayThermostat::Impl : private boost::noncopyable {
temp,
_targetTemperature,
_hysteresis);
ASYNC_CHECK_MSG(_relay->off(), "Disabling relay failed");
ASYNC_CHECK_MSG(safe_off(), "Disabling relay failed");
_lastStateChange = now;
}
}
@ -531,6 +587,31 @@ struct RelayThermostat::Impl : private boost::noncopyable {
co_return _void{};
}
awaitable_expected< void > safe_off() {
auto retries = co_await _settings.async_get_store_default("retries", 3);
while(retries) {
// Bezpieczeństwo: wyłącz przekaźnik natychmiastowo
auto expectedState = co_await _relay->state();
if(!expectedState) {
spdlog::warn("Cant get relay {} state", this->_room);
--retries;
continue;
}
auto st = *expectedState;
if(st == Relay::State::On) {
auto expectedOff = co_await _relay->off();
if(!expectedOff) {
spdlog::warn("Cant turn off relay {}", this->_room);
--retries;
continue;
}
}
}
co_return _void{};
}
awaitable_expected< void > error(std::string_view reason) {
if(_state == ThermostatState::Error) {
spdlog::error("RelayThermostat {}/{} additional error while already in ERROR state: {}", _room, _zone, reason);
@ -540,11 +621,7 @@ struct RelayThermostat::Impl : private boost::noncopyable {
spdlog::error("RelayThermostat {}/{} entering ERROR state: {}", _room, _zone, reason);
_state = ThermostatState::Error;
// Bezpieczeństwo: wyłącz przekaźnik natychmiastowo
auto st = ASYNC_TRY(_relay->state());
if(st == Relay::State::On) {
ASYNC_CHECK(_relay->off());
}
ASYNC_CHECK_MSG(safe_off(), "");
// TODO: tu możesz wysłać komunikat MQTT, zapisać do DB itp.
co_return _void{};
@ -575,41 +652,21 @@ struct RelayThermostat::Impl : private boost::noncopyable {
awaitable_expected< void > start() {
using namespace std::placeholders;
using namespace std::chrono;
spdlog::info("RelayThermostat::start room : {}", _room);
auto get_state = [this](std::string key, ThermostatState _default) -> awaitable_expected< ThermostatState > {
auto contains = ASYNC_TRY(_settings.async_contains(key));
if(not contains) {
ASYNC_CHECK(_settings.async_save(key, ThermostatState_to_string(_default)));
}
auto toSec = [](auto t) { return seconds{t}.count(); };
/// TODO can throw
auto value = ASYNC_TRY(_settings.async_get(key)).value(); // we have this value at this point
auto ThermostatStateStr = value.get_if< std::string >(); // TODO can be different type
auto state = ThermostatState_from_string(ThermostatStateStr);
if(state)
co_return *state;
co_return unexpected{make_error_code(boost::system::errc::invalid_argument)};
};
_state = ThermostatState_from_string(
co_await _settings.async_get_store_default("state", ThermostatState_to_string(ThermostatState::Disabled)))
.value_or(ThermostatState::Disabled);
auto get_double = [this](std::string key, double _default) -> awaitable_expected< double > {
auto contains = ASYNC_TRY(_settings.async_contains(key));
if(not contains) {
ASYNC_CHECK(_settings.async_save(key, _default));
}
/// TODO can throw
auto value = ASYNC_TRY(_settings.async_get(key)).value(); // we have this value at this point
co_return value.get_if< decltype(_default) >(); // TODO can be different type
};
_state = ASYNC_TRY(get_state("state", ThermostatState::Disabled));
_targetTemperature = ASYNC_TRY(get_double("target_temperature", 20.0));
_hysteresis = ASYNC_TRY(get_double("hysteresis", 2)); // [°C]
std::chrono::nanoseconds _tickTime{std::chrono::seconds(60)}; // minimalny czas ON/OFF
std::chrono::nanoseconds _slopeWindow{std::chrono::minutes(5)};
_slopeDT_c = ASYNC_TRY(get_double("slope_delta_t", 1)); // [°C / min]
std::chrono::minutes _sensorTimeout{std::chrono::minutes(5)};
_targetTemperature = co_await _settings.async_get_store_default("target_temperature", 20.0);
_hysteresis = co_await _settings.async_get_store_default("hysteresis", 2.0); // [°C]
_tickTime = seconds{co_await _settings.async_get_store_default("tick_time_s", toSec(minutes{1}))};
_slopeWindow = seconds{co_await _settings.async_get_store_default("slope_window_s", toSec(minutes{1}))};
_slopeDT_c = co_await _settings.async_get_store_default("slope_delta_t", 1); // [°C / min]
_sensorTimeout = seconds{co_await _settings.async_get_store_default("sensor_timeout_s", toSec(minutes{5}))};
// subscribe to a thermostat commands feed
spdlog::info("RelayThermostat::start room : {} subscribe to mqtt", _room);
@ -626,7 +683,7 @@ struct RelayThermostat::Impl : private boost::noncopyable {
}
awaitable_expected< void > subscribe(std::string_view topic,
std::function< awaitable_expected< void >(const AsyncMqttClient::CallbackData &, AsyncMqttClient::ResponseData &resp) > cb) {
std::function< awaitable_expected< void >(const AsyncMqttClient::CallbackData &, AsyncMqttClient::ResponseData & resp) > cb) {
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{};
@ -651,14 +708,20 @@ struct RelayThermostat::Impl : private boost::noncopyable {
// konkretna strefa:
const auto zone_topic = topic::heating::subscribeToCommand(_room, _zone, Command::topic_suffix);
auto cb = [this](const AsyncMqttClient::CallbackData & data, AsyncMqttClient::ResponseData &resp) -> awaitable_expected< void > {
auto cb = [this](const AsyncMqttClient::CallbackData & data, AsyncMqttClient::ResponseData & resp) -> awaitable_expected< void > {
auto _result = Command::from_payload(data.request);
if(!_result)
co_return unexpected{_result.error()};
auto cmd = *_result;
ASYNC_CHECK(handle_command(cmd));
/// TODO command can "throw an unexpected, this should be handled here
/// TODO command can return a true/false status for ok/nok case
auto status = ASYNC_TRY(handle_command(cmd));
if(resp) {
(*resp) = boost::json::object{{"status", status ? "ok" : "nok"}, {"details", "heater updated"}};
}
co_return _void{};
};
@ -668,54 +731,63 @@ struct RelayThermostat::Impl : private boost::noncopyable {
co_return _void{};
}
// przeciążone handlery dla poszczególnych komend:
awaitable_expected< void > handle_command(const commands::TemperatureSetpointChange & cmd) {
spdlog::info("Heater target temperature update {} for {}/{}", _targetTemperature, _room, _zone);
_targetTemperature = cmd.setpoint_c;
co_return _void{};
template < typename T >
awaitable< void > update_config(std::string_view key, const T & value) noexcept {
auto _result = co_await _settings.async_save("hysteresis", _hysteresis);
if(not _result) {
spdlog::warn("Failed to update configuration paremeter {} for {}/{}", key, _room, _zone);
}
co_return;
}
awaitable_expected< void > handle_command(const commands::StateChange & cmd) {
// przeciążone handlery dla poszczególnych komend:
awaitable_expected< bool > handle_command(const commands::TemperatureSetpointChange & cmd) {
spdlog::info("Heater target temperature update {} for {}/{}", _targetTemperature, _room, _zone);
_targetTemperature = cmd.setpoint_c;
co_await update_config("target_temperature", _targetTemperature);
co_return true;
}
awaitable_expected< bool > handle_command(const commands::StateChange & cmd) {
spdlog::info("Heater state update {} for {}/{}", static_cast< int >(cmd.state), _room, _zone);
// W stanie ERROR nie wolno włączyć przekaźnika (Enabled)
if(_state == ThermostatState::Error && cmd.state == ThermostatState::Enabled) {
spdlog::warn("Ignoring attempt to enable thermostat in ERROR state for {}/{}", _room, _zone);
co_return _void{};
co_return false;
}
_state = cmd.state;
// Bezpiecznik: jeśli zostało ustawione Disabled wyłącz przekaźnik
auto st = ASYNC_TRY(_relay->state());
if(_state == ThermostatState::Disabled && st == Relay::State::On) {
ASYNC_CHECK(_relay->off());
}
co_return _void{};
co_return true;
}
awaitable_expected< void > handle_command(const commands::HisteresisChange & cmd) {
awaitable_expected< bool > handle_command(const commands::HisteresisChange & cmd) {
spdlog::info("Heater histeresis update {} for {}/{}", cmd.histeresis, _room, _zone);
_hysteresis = cmd.histeresis;
co_return _void{};
/// TODO check if histeresis has ok value
co_await update_config("hysteresis", _hysteresis);
co_return true;
}
awaitable_expected< void > handle_command(const commands::TickTimeChange & cmd) {
awaitable_expected< bool > handle_command(const commands::TickTimeChange & cmd) {
spdlog::info("Heater tick time update {}ns for {}/{}", cmd.tickTime.count(), _room, _zone);
_tickTime = cmd.tickTime;
co_return _void{};
co_await update_config("tick_time_s", std::chrono::duration_cast< std::chrono::seconds >(_tickTime).count());
co_return true;
}
awaitable_expected< void > handle_command(const commands::SlopeWindowChange & cmd) {
awaitable_expected< bool > handle_command(const commands::SlopeWindowChange & cmd) {
spdlog::info("Heater slope window update {}ns for {}/{}", cmd.window.count(), _room, _zone);
_slopeWindow = cmd.window;
co_return _void{};
co_await update_config("slope_window_s", std::chrono::duration_cast< std::chrono::seconds >(_slopeWindow).count());
co_return true;
}
awaitable_expected< void > handle_command(const commands::SlopeTemperatureDiffChange & cmd) {
awaitable_expected< bool > handle_command(const commands::SlopeTemperatureDiffChange & cmd) {
spdlog::info("Heater slope temperature update {}C for {}/{}", cmd.dT_c, _room, _zone);
_slopeDT_c = cmd.dT_c;
co_return _void{};
co_await update_config("slope_delta_t", _slopeDT_c);
co_return true;
}
};
@ -732,7 +804,6 @@ RelayThermostat::~RelayThermostat() = default;
awaitable_expected< void > RelayThermostat::start() noexcept {
BOOST_ASSERT(_impl);
return _impl->start();
}