From b01271fa3b0ffa8af803ac374aeeb377d0769a5e Mon Sep 17 00:00:00 2001 From: Bartosz Wieczorek Date: Tue, 18 Nov 2025 14:58:48 +0100 Subject: [PATCH] Add more logic to temperature controller --- config.hpp | 29 +- libs/config.cpp | 83 ++++- libs/ranczo-io/utils/config.hpp | 69 ++-- .../floorheat_svc/temperature_controller.cpp | 303 +++++++++++------- 4 files changed, 323 insertions(+), 161 deletions(-) diff --git a/config.hpp b/config.hpp index 8f1867f..e8af12e 100644 --- a/config.hpp +++ b/config.hpp @@ -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 diff --git a/libs/config.cpp b/libs/config.cpp index 61b2f5e..0d61134 100644 --- a/libs/config.cpp +++ b/libs/config.cpp @@ -1,8 +1,31 @@ +#include "config.hpp" +#include +#include +#include #include #include 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 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 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) { diff --git a/libs/ranczo-io/utils/config.hpp b/libs/ranczo-io/utils/config.hpp index 0ef60ac..b4d448a 100644 --- a/libs/ranczo-io/utils/config.hpp +++ b/libs/ranczo-io/utils/config.hpp @@ -1,11 +1,15 @@ // settings_store.hpp #pragma once +#include "tl/expected.hpp" +#include #include #include +#include #include #include +#include #include #include @@ -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_; } diff --git a/services/floorheat_svc/temperature_controller.cpp b/services/floorheat_svc/temperature_controller.cpp index 88dbce3..0c20927 100644 --- a/services/floorheat_svc/temperature_controller.cpp +++ b/services/floorheat_svc/temperature_controller.cpp @@ -8,12 +8,13 @@ #include #include #include +#include +#include #include #include #include #include #include -#include #include #include "ranczo-io/utils/config.hpp" @@ -36,21 +37,16 @@ #include -/* - * 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(); }