Add more logic to temperature controller
This commit is contained in:
parent
7c969cff91
commit
b01271fa3b
29
config.hpp
29
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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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_;
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user