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" #error "No expected implementation available"
#endif #endif
template < typename T > template < typename T >
using awaitable_expected = boost::asio::awaitable< expected< T > >; using awaitable_expected = boost::asio::awaitable< expected< T > >;
template < typename T >
using awaitable = boost::asio::awaitable< T >;
using _void = expected< void >; using _void = expected< void >;
using ::boost::system::errc::make_error_code; using ::boost::system::errc::make_error_code;
@ -65,10 +67,10 @@ using ::boost::system::errc::make_error_code;
*_result; \ *_result; \
}) })
#define CHECK(failable) \ #define CHECK(failable) \
do { \ do { \
auto _result = await(failable); \ auto _result = await(failable); \
if(!_result) \ if(!_result) \
return unexpected(_result.error()); \ return unexpected(_result.error()); \
} while(0) } while(0)
@ -107,10 +109,10 @@ using ::boost::system::errc::make_error_code;
*_result; \ *_result; \
}) })
#define ASYNC_CHECK(failable) \ #define ASYNC_CHECK(failable) \
do { \ do { \
auto _result = co_await (failable); \ auto _result = co_await (failable); \
if(!_result) \ if(!_result) \
co_return unexpected(_result.error()); \ co_return unexpected(_result.error()); \
} while(0) } while(0)
@ -123,4 +125,13 @@ using ::boost::system::errc::make_error_code;
} \ } \
} while(0) } 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 } // 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 <ranczo-io/utils/config.hpp>
#include <sqlite3.h> #include <sqlite3.h>
namespace ranczo { 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 { std::string SettingsStore::Value::to_db_text() const {
using namespace boost::json; using namespace boost::json;
@ -36,7 +59,7 @@ std::string SettingsStore::Value::to_db_text() const {
return serialize(o); 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; using namespace boost::json;
value j; 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) { : main_exec_(main_exec), db_pool_(db_threads), db_path_(app_db_path) {
open_or_create(); open_or_create();
prepare_schema(); prepare_schema();
@ -100,7 +123,12 @@ SettingsStore::~SettingsStore() {
db_pool_.join(); 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); spdlog::debug("SettingsStore::contains_sync({}, {})", component, key);
const char * sql = const char * sql =
@ -112,8 +140,8 @@ bool SettingsStore::contains(const std::string & component, const std::string &
auto stmt_guard = make_stmt_guard(stmt); 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, 1, component.data(), -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, 2, key.data(), -1, SQLITE_TRANSIENT), "bind key");
int rc = sqlite3_step(stmt); int rc = sqlite3_step(stmt);
if(rc == SQLITE_ROW) { if(rc == SQLITE_ROW) {
@ -127,7 +155,12 @@ bool SettingsStore::contains(const std::string & component, const std::string &
return false; 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; auto caller_exec = co_await boost::asio::this_coro::executor;
bool exists = false; bool exists = false;
@ -147,7 +180,12 @@ awaitable_expected<bool> SettingsStore::async_contains(const std::string & compo
co_return exists; 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); spdlog::debug("SettingsStore::save_sync({}, {})", component, key);
const char * sql = 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"); check_sqlite(sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr), "prepare INSERT/UPDATE");
auto stmt_guard = make_stmt_guard(stmt); 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, 1, component.data(), -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, 2, key.data(), -1, SQLITE_TRANSIENT), "bind key");
std::string s = value.to_db_text(); std::string s = value.to_db_text();
check_sqlite(sqlite3_bind_text(stmt, 3, s.c_str(), -1, SQLITE_TRANSIENT), "bind value"); 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"); 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; auto caller_exec = co_await boost::asio::this_coro::executor;
// do puli DB // 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, awaitable_expected< std::optional< SettingsStore::Value > > SettingsStore::async_get(std::string_view component,
const std::string & key) { 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; auto caller_exec = co_await boost::asio::this_coro::executor;
std::optional< Value > result; std::optional< Value > result;
@ -248,7 +296,12 @@ awaitable_expected< std::optional< SettingsStore::Value > > SettingsStore::async
co_return result; 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); spdlog::debug("SettingsStore::get_sync({}, {})", component, key);
const char * sql = const char * sql =
@ -260,8 +313,8 @@ std::optional< SettingsStore::Value > SettingsStore::get(const std::string & com
auto stmt_guard = make_stmt_guard(stmt); 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, 1, component.data(), -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, 2, key.data(), -1, SQLITE_TRANSIENT), "bind key");
int rc = sqlite3_step(stmt); int rc = sqlite3_step(stmt);
if(rc == SQLITE_ROW) { if(rc == SQLITE_ROW) {

View File

@ -1,11 +1,15 @@
// settings_store.hpp // settings_store.hpp
#pragma once #pragma once
#include "tl/expected.hpp"
#include <boost/system/detail/errc.hpp>
#include <config.hpp> #include <config.hpp>
#include <cstdint> #include <cstdint>
#include <exception>
#include <optional> #include <optional>
#include <string> #include <string>
#include <string_view>
#include <type_traits> #include <type_traits>
#include <variant> #include <variant>
@ -38,6 +42,7 @@ class SettingsStore {
Value(const char * v) : data_(std::string(v)) {} Value(const char * v) : data_(std::string(v)) {}
Value(const std::string & v) : data_(v) {} Value(const std::string & v) : data_(v) {}
Value(std::string && v) : data_(std::move(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(const Json & v) : data_(v) {}
Value(Json && v) : data_(std::move(v)) {} Value(Json && v) : data_(std::move(v)) {}
Value(bool v) : data_(v) {} Value(bool v) : data_(v) {}
@ -64,7 +69,7 @@ class SettingsStore {
std::string to_db_text() const; std::string to_db_text() const;
// TEXT z DB -> Value // TEXT z DB -> Value
static Value from_db_text(const std::string & s); static Value from_db_text(std::string_view s);
private: private:
Variant data_; Variant data_;
@ -73,23 +78,21 @@ class SettingsStore {
// --------------------- Ctor / Dtor --------------------- // --------------------- Ctor / Dtor ---------------------
// app_db_path = plik SQLite dla danej aplikacji // 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(); ~SettingsStore();
bool contains(const std::string & component, const std::string & key) const; bool contains(std::string_view component, std::string_view key) const;
awaitable_expected< bool > async_contains(const std::string & component, const std::string & 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 // zwraca std::nullopt jeśli brak wpisu
std::optional< Value > get(const std::string & component, const std::string & key); awaitable_expected< bool > async_contains(std::string_view component, std::string_view key) const noexcept;
awaitable_expected< std::optional< Value > > async_get(const std::string & component, const std::string & key); 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;
// 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);
// overloady dla konkretnych typów: // overloady dla konkretnych typów:
template < typename T > 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 >) { if constexpr(std::is_same_v< int, T >) {
save(component, key, Value{int64_t{v}}); save(component, key, Value{int64_t{v}});
} else { } else {
@ -98,8 +101,12 @@ class SettingsStore {
} }
// overloady async_save dla typów prostych: // overloady async_save dla typów prostych:
template < typename T > template < typename T >
awaitable_expected< void > async_save(const std::string & component, const std::string & key, const T & v) { awaitable_expected< void > async_save(std::string_view component, std::string_view key, const T & v) noexcept {
co_return co_await async_save(component, key, Value{v}); 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: private:
@ -135,39 +142,59 @@ class ComponentSettingsStore {
// ------------- SYNCHRONICZNE API ------------- // ------------- SYNCHRONICZNE API -------------
// get_sync(component, key) -> get_sync(key) // 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); return store_.get(component_, key);
} }
template < typename T > template < typename T >
T get_value(const std::string & key) { T get_value(std::string_view key) {
BOOST_ASSERT(store_.contains(key)); BOOST_ASSERT(store_.contains(key));
return store_.get(component_, key).value().get_if< T >(); 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); co_return co_await store_.async_get(component_, key);
} }
template < typename T > 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); store_.save(component_, key, v);
} }
template < typename T > 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); 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); 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); co_return co_await store_.async_contains(component_, key);
} }
// dostęp do nazwy komponentu (opcjonalnie) // dostęp do nazwy komponentu (opcjonalnie)
const std::string & component() const noexcept { std::string_view component() const noexcept {
return component_; return component_;
} }

View File

@ -8,12 +8,13 @@
#include <boost/asio/steady_timer.hpp> #include <boost/asio/steady_timer.hpp>
#include <boost/smart_ptr/shared_ptr.hpp> #include <boost/smart_ptr/shared_ptr.hpp>
#include <boost/system/detail/errc.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/errc.hpp>
#include <boost/system/result.hpp> #include <boost/system/result.hpp>
#include <functional> #include <functional>
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <queue>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include "ranczo-io/utils/config.hpp" #include "ranczo-io/utils/config.hpp"
@ -36,21 +37,16 @@
#include <ranczo-io/utils/time.hpp> #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 { namespace ranczo {
enum class ThermostatState { Enabled, Disabled, Error }; enum class ThermostatState { Enabled, Disabled, Error };
enum class Trend { Fall, Const, Rise }; enum class Trend { Fall, Const, Rise };
std::optional< ThermostatState > ThermostatState_from_string(std::string_view state) { std::optional< ThermostatState > ThermostatState_from_string(std::optional< std::string > state) {
std::string s(state.begin(), state.end()); 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)); }); std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return static_cast< char >(std::tolower(c)); });
if(s == "enabled") if(s == "enabled")
@ -91,7 +87,7 @@ inline expected< T > readValue(const boost::json::value & jv, std::string_view k
if(!pv->is_string()) { if(!pv->is_string()) {
return unexpected{make_error_code(boost::system::errc::invalid_argument)}; 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) if(not v)
return unexpected{make_error_code(boost::system::errc::invalid_argument)}; return unexpected{make_error_code(boost::system::errc::invalid_argument)};
return *v; return *v;
@ -277,7 +273,8 @@ struct ThermometerMeasurements {
* @param epsilon_deg_per_min, trend specyfier * @param epsilon_deg_per_min, trend specyfier
* @return a trend if trent can be calculated or nullopt * @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()) { if(auto last = timeSinceLastRead(); not last.has_value()) {
spdlog::debug("No temperature samples available"); spdlog::debug("No temperature samples available");
return std::nullopt; 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 _tickTime{std::chrono::seconds(60)}; // minimalny czas ON/OFF
std::chrono::nanoseconds _slopeWindow{std::chrono::minutes(5)}; std::chrono::nanoseconds _slopeWindow{std::chrono::minutes(5)};
double _slopeDT_c{0.2}; // [°C / min] 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 --- // --- nowe helpery ---
awaitable_expected< void > controlLoop() { awaitable_expected< void > controlLoop() {
@ -387,9 +409,27 @@ struct RelayThermostat::Impl : private boost::noncopyable {
boost::asio::steady_timer timer(_io); boost::asio::steady_timer timer(_io);
for(;;) { 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)); timer.expires_after(duration_cast< steady_clock::duration >(_tickTime));
boost::system::error_code ec; boost::system::error_code ec;
@ -406,19 +446,6 @@ struct RelayThermostat::Impl : private boost::noncopyable {
awaitable_expected< void > applyControlStep() { awaitable_expected< void > applyControlStep() {
using namespace std::chrono; 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) { switch(_state) {
case ThermostatState::Error: case ThermostatState::Error:
ASYNC_CHECK(handleErrorState()); ASYNC_CHECK(handleErrorState());
@ -434,47 +461,72 @@ struct RelayThermostat::Impl : private boost::noncopyable {
co_return _void{}; 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; using namespace std::chrono;
// brak update'u temperatury // check if temperature is constantly read
if(dtLast > _sensorTimeout) { if(not checkLastTemperatureRead()) {
ASYNC_CHECK(error("temperature sensor timeout (> 5 minutes without update)")); 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 auto tempOpt = _thermo.currentTemperature();
if(_state == ThermostatState::Error) { if(!tempOpt) {
co_return _void{}; spdlog::warn("No temperature samples for {}/{}", _room, _zone);
co_return false;
} }
// aktualny trend temperatury (na konfigurowalnym oknie) auto trendOpt = _thermo.temperatureTrend(_slopeWindow, _slopeDT_c);
auto trendOpt = _thermo.temperatureTrend(duration_cast< seconds >(_slopeWindow), _slopeDT_c);
if(!trendOpt) { 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 trend = *trendOpt; // state should be cached
auto st = ASYNC_TRY(_relay->state()); auto mkerr = [](auto) { return make_error(RuntimeError::IoError); };
const bool relayOn = (st == Relay::State::On); 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 // 2a) relay OFF, a temperatura rośnie => przekaźnik zawiesił się na ON
if(!relayOn && trend == Trend::Rise) { 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 // 2b) relay ON, a trend != Rise => przekaźnik zawiesił się na OFF
if(relayOn && trend != Trend::Rise) { 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() { awaitable_expected< void > handleErrorState() {
auto st = ASYNC_TRY(_relay->state()); /// check preconditions, if ok release error state
if(st == Relay::State::On) { // only relay->state can fail
spdlog::warn("Forcing relay OFF in ERROR state for {}/{}", _room, _zone); auto preconditionsMet = ASYNC_TRY(preconditions());
ASYNC_CHECK_MSG(_relay->off(), "Emergency relay OFF failed!"); 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{}; co_return _void{};
} }
@ -483,7 +535,7 @@ struct RelayThermostat::Impl : private boost::noncopyable {
auto st = ASYNC_TRY(_relay->state()); auto st = ASYNC_TRY(_relay->state());
if(st == Relay::State::On) { if(st == Relay::State::On) {
spdlog::info("RelayThermostat disabling relay because thermostat is Disabled for {}/{}", _room, _zone); 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{}; co_return _void{};
} }
@ -491,10 +543,14 @@ struct RelayThermostat::Impl : private boost::noncopyable {
awaitable_expected< void > handleEnabledState() { awaitable_expected< void > handleEnabledState() {
using namespace std::chrono; using namespace std::chrono;
auto tempOpt = _thermo.currentTemperature(); auto preconditionsMet = ASYNC_TRY(preconditions());
if(!tempOpt) { if(not preconditionsMet) {
spdlog::warn("RelayThermostat turning set disabled state due to failed preconditions");
_state = ThermostatState::Error;
co_return _void{}; co_return _void{};
} }
auto tempOpt = _thermo.currentTemperature();
const double temp = *tempOpt; const double temp = *tempOpt;
const auto now = system_clock::now(); const auto now = system_clock::now();
@ -523,7 +579,7 @@ struct RelayThermostat::Impl : private boost::noncopyable {
temp, temp,
_targetTemperature, _targetTemperature,
_hysteresis); _hysteresis);
ASYNC_CHECK_MSG(_relay->off(), "Disabling relay failed"); ASYNC_CHECK_MSG(safe_off(), "Disabling relay failed");
_lastStateChange = now; _lastStateChange = now;
} }
} }
@ -531,6 +587,31 @@ struct RelayThermostat::Impl : private boost::noncopyable {
co_return _void{}; 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) { awaitable_expected< void > error(std::string_view reason) {
if(_state == ThermostatState::Error) { if(_state == ThermostatState::Error) {
spdlog::error("RelayThermostat {}/{} additional error while already in ERROR state: {}", _room, _zone, reason); 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); spdlog::error("RelayThermostat {}/{} entering ERROR state: {}", _room, _zone, reason);
_state = ThermostatState::Error; _state = ThermostatState::Error;
// Bezpieczeństwo: wyłącz przekaźnik natychmiastowo ASYNC_CHECK_MSG(safe_off(), "");
auto st = ASYNC_TRY(_relay->state());
if(st == Relay::State::On) {
ASYNC_CHECK(_relay->off());
}
// TODO: tu możesz wysłać komunikat MQTT, zapisać do DB itp. // TODO: tu możesz wysłać komunikat MQTT, zapisać do DB itp.
co_return _void{}; co_return _void{};
@ -575,41 +652,21 @@ struct RelayThermostat::Impl : private boost::noncopyable {
awaitable_expected< void > start() { awaitable_expected< void > start() {
using namespace std::placeholders; using namespace std::placeholders;
using namespace std::chrono;
spdlog::info("RelayThermostat::start room : {}", _room); spdlog::info("RelayThermostat::start room : {}", _room);
auto get_state = [this](std::string key, ThermostatState _default) -> awaitable_expected< ThermostatState > { auto toSec = [](auto t) { return seconds{t}.count(); };
auto contains = ASYNC_TRY(_settings.async_contains(key));
if(not contains) {
ASYNC_CHECK(_settings.async_save(key, ThermostatState_to_string(_default)));
}
/// TODO can throw _state = ThermostatState_from_string(
auto value = ASYNC_TRY(_settings.async_get(key)).value(); // we have this value at this point co_await _settings.async_get_store_default("state", ThermostatState_to_string(ThermostatState::Disabled)))
auto ThermostatStateStr = value.get_if< std::string >(); // TODO can be different type .value_or(ThermostatState::Disabled);
auto state = ThermostatState_from_string(ThermostatStateStr);
if(state)
co_return *state;
co_return unexpected{make_error_code(boost::system::errc::invalid_argument)};
};
auto get_double = [this](std::string key, double _default) -> awaitable_expected< double > { _targetTemperature = co_await _settings.async_get_store_default("target_temperature", 20.0);
auto contains = ASYNC_TRY(_settings.async_contains(key)); _hysteresis = co_await _settings.async_get_store_default("hysteresis", 2.0); // [°C]
if(not contains) { _tickTime = seconds{co_await _settings.async_get_store_default("tick_time_s", toSec(minutes{1}))};
ASYNC_CHECK(_settings.async_save(key, _default)); _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}))};
/// 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)};
// subscribe to a thermostat commands feed // subscribe to a thermostat commands feed
spdlog::info("RelayThermostat::start room : {} subscribe to mqtt", _room); 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, 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); spdlog::trace("RelayThermostat room {} subscribing to {}", _room, topic);
ASYNC_CHECK_MSG(_mqtt.subscribe(topic, std::move(cb)), "Heater faild to subscribe on: {}", topic); ASYNC_CHECK_MSG(_mqtt.subscribe(topic, std::move(cb)), "Heater faild to subscribe on: {}", topic);
co_return _void{}; co_return _void{};
@ -651,14 +708,20 @@ struct RelayThermostat::Impl : private boost::noncopyable {
// konkretna strefa: // konkretna strefa:
const auto zone_topic = topic::heating::subscribeToCommand(_room, _zone, Command::topic_suffix); 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); auto _result = Command::from_payload(data.request);
if(!_result) if(!_result)
co_return unexpected{_result.error()}; co_return unexpected{_result.error()};
auto cmd = *_result; 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{}; co_return _void{};
}; };
@ -668,54 +731,63 @@ struct RelayThermostat::Impl : private boost::noncopyable {
co_return _void{}; co_return _void{};
} }
// przeciążone handlery dla poszczególnych komend: template < typename T >
awaitable_expected< void > handle_command(const commands::TemperatureSetpointChange & cmd) { awaitable< void > update_config(std::string_view key, const T & value) noexcept {
spdlog::info("Heater target temperature update {} for {}/{}", _targetTemperature, _room, _zone); auto _result = co_await _settings.async_save("hysteresis", _hysteresis);
_targetTemperature = cmd.setpoint_c; if(not _result) {
co_return _void{}; 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); spdlog::info("Heater state update {} for {}/{}", static_cast< int >(cmd.state), _room, _zone);
// W stanie ERROR nie wolno włączyć przekaźnika (Enabled) // W stanie ERROR nie wolno włączyć przekaźnika (Enabled)
if(_state == ThermostatState::Error && cmd.state == ThermostatState::Enabled) { if(_state == ThermostatState::Error && cmd.state == ThermostatState::Enabled) {
spdlog::warn("Ignoring attempt to enable thermostat in ERROR state for {}/{}", _room, _zone); spdlog::warn("Ignoring attempt to enable thermostat in ERROR state for {}/{}", _room, _zone);
co_return _void{}; co_return false;
} }
_state = cmd.state; _state = cmd.state;
// Bezpiecznik: jeśli zostało ustawione Disabled wyłącz przekaźnik co_return true;
auto st = ASYNC_TRY(_relay->state());
if(_state == ThermostatState::Disabled && st == Relay::State::On) {
ASYNC_CHECK(_relay->off());
}
co_return _void{};
} }
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); spdlog::info("Heater histeresis update {} for {}/{}", cmd.histeresis, _room, _zone);
_hysteresis = cmd.histeresis; _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); spdlog::info("Heater tick time update {}ns for {}/{}", cmd.tickTime.count(), _room, _zone);
_tickTime = cmd.tickTime; _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); spdlog::info("Heater slope window update {}ns for {}/{}", cmd.window.count(), _room, _zone);
_slopeWindow = cmd.window; _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); spdlog::info("Heater slope temperature update {}C for {}/{}", cmd.dT_c, _room, _zone);
_slopeDT_c = cmd.dT_c; _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 { awaitable_expected< void > RelayThermostat::start() noexcept {
BOOST_ASSERT(_impl); BOOST_ASSERT(_impl);
return _impl->start(); return _impl->start();
} }