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"
|
#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
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user