Add HTTP get
This commit is contained in:
parent
dbe89b64ca
commit
6da01a2f6b
@ -25,12 +25,12 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
|
|||||||
|
|
||||||
include(FetchContent)
|
include(FetchContent)
|
||||||
|
|
||||||
set(BOOST_INCLUDE_LIBRARIES asio assert core endian json mqtt5 fusion optional random range smart_ptr spirit type_traits)
|
set(BOOST_INCLUDE_LIBRARIES asio assert core endian json mqtt5 fusion optional random range smart_ptr spirit type_traits url)
|
||||||
set(BOOST_ENABLE_CMAKE ON)
|
set(BOOST_ENABLE_CMAKE ON)
|
||||||
set(Boost_NO_SYSTEM_PATHS ON)
|
set(Boost_NO_SYSTEM_PATHS ON)
|
||||||
set(BOOST_ROOT /usr/local/boost-1.89)
|
set(BOOST_ROOT /usr/local/boost-1.89)
|
||||||
|
|
||||||
find_package(Boost 1.89 REQUIRED COMPONENTS json mqtt5)
|
find_package(Boost 1.89 REQUIRED COMPONENTS json mqtt5 url)
|
||||||
# spdlog
|
# spdlog
|
||||||
|
|
||||||
include(CheckCXXSourceCompiles)
|
include(CheckCXXSourceCompiles)
|
||||||
|
|||||||
@ -4,6 +4,8 @@
|
|||||||
#include <boost/system/errc.hpp>
|
#include <boost/system/errc.hpp>
|
||||||
#include <boost/system/error_code.hpp>
|
#include <boost/system/error_code.hpp>
|
||||||
|
|
||||||
|
#include <memory_resource>
|
||||||
|
|
||||||
#if __has_include(<expected>)
|
#if __has_include(<expected>)
|
||||||
#include <expected>
|
#include <expected>
|
||||||
#elif __has_include(<tl/expected.hpp>)
|
#elif __has_include(<tl/expected.hpp>)
|
||||||
@ -14,6 +16,10 @@
|
|||||||
|
|
||||||
namespace ranczo {
|
namespace ranczo {
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
std::optional<T> from_string(std::optional< std::string_view > state,
|
||||||
|
std::pmr::memory_resource * mr = std::pmr::get_default_resource());
|
||||||
|
|
||||||
#if __has_include(<expected>)
|
#if __has_include(<expected>)
|
||||||
template < typename T >
|
template < typename T >
|
||||||
using expected = std::expected< T, boost::system::error_code >;
|
using expected = std::expected< T, boost::system::error_code >;
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
include(modbus.cmake)
|
include(modbus.cmake)
|
||||||
|
|
||||||
find_package(SQLite3)
|
find_package(SQLite3)
|
||||||
|
find_package(OpenSSL REQUIRED)
|
||||||
|
|
||||||
add_library(ranczo-io_utils
|
add_library(ranczo-io_utils
|
||||||
mqtt_client.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ranczo-io/utils/mqtt_client.hpp
|
mqtt_client.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ranczo-io/utils/mqtt_client.hpp
|
||||||
@ -9,6 +10,7 @@ add_library(ranczo-io_utils
|
|||||||
asio_watchdog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ranczo-io/utils/asio_watchdog.hpp
|
asio_watchdog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ranczo-io/utils/asio_watchdog.hpp
|
||||||
modbus.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ranczo-io/utils/modbus.hpp
|
modbus.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ranczo-io/utils/modbus.hpp
|
||||||
config.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ranczo-io/utils/config.hpp
|
config.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ranczo-io/utils/config.hpp
|
||||||
|
http.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ranczo-io/utils/http.hpp
|
||||||
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ranczo-io/utils/mqtt_topic_builder.hpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ranczo-io/utils/mqtt_topic_builder.hpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ranczo-io/utils/memory_resource.hpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ranczo-io/utils/memory_resource.hpp
|
||||||
@ -30,6 +32,9 @@ target_link_libraries(ranczo-io_utils
|
|||||||
fmt::fmt
|
fmt::fmt
|
||||||
modbus
|
modbus
|
||||||
date::date
|
date::date
|
||||||
|
OpenSSL::SSL #http
|
||||||
|
OpenSSL::Crypto
|
||||||
|
Boost::url
|
||||||
PRIVATE
|
PRIVATE
|
||||||
SQLite::SQLite3
|
SQLite::SQLite3
|
||||||
)
|
)
|
||||||
|
|||||||
176
libs/http.cpp
Normal file
176
libs/http.cpp
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
#include <boost/asio/redirect_error.hpp>
|
||||||
|
#include <boost/asio/use_awaitable.hpp>
|
||||||
|
#include <ranczo-io/utils/http.hpp>
|
||||||
|
|
||||||
|
#include <boost/asio/ssl.hpp>
|
||||||
|
#include <boost/beast/core.hpp>
|
||||||
|
#include <boost/beast/http.hpp>
|
||||||
|
#include <boost/beast/ssl.hpp>
|
||||||
|
#include <boost/url.hpp>
|
||||||
|
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace ranczo {
|
||||||
|
|
||||||
|
using tcp = boost::asio::ip::tcp;
|
||||||
|
namespace asio = boost::asio;
|
||||||
|
namespace beast = boost::beast;
|
||||||
|
namespace http = beast::http;
|
||||||
|
|
||||||
|
using PmrCharAlloc = std::pmr::polymorphic_allocator< char >;
|
||||||
|
using PmrFields = http::basic_fields< PmrCharAlloc >;
|
||||||
|
using PmrStringBody = http::basic_string_body< char, std::char_traits< char >, PmrCharAlloc >;
|
||||||
|
using PmrFlatBuffer = boost::beast::basic_flat_buffer< PmrCharAlloc >;
|
||||||
|
using PmrResponse = http::response< PmrStringBody, PmrFields >;
|
||||||
|
using PmrRequest = http::request< PmrStringBody, PmrFields >;
|
||||||
|
|
||||||
|
expected< HttpGetClient::ParsedUrl > HttpGetClient::parse_url_generic(std::string_view url) {
|
||||||
|
ParsedUrl out;
|
||||||
|
namespace urls = boost::urls;
|
||||||
|
|
||||||
|
auto r = urls::parse_uri(url);
|
||||||
|
if(!r) {
|
||||||
|
return unexpected(r.error());
|
||||||
|
}
|
||||||
|
|
||||||
|
urls::url_view u = *r;
|
||||||
|
|
||||||
|
out.scheme = std::string(u.scheme());
|
||||||
|
out.host = std::string(u.host());
|
||||||
|
|
||||||
|
// ścieżka
|
||||||
|
out.target = std::string(u.encoded_path());
|
||||||
|
if(out.target.empty())
|
||||||
|
out.target = "/";
|
||||||
|
|
||||||
|
// query (bez fragmentu)
|
||||||
|
if(!u.encoded_query().empty()) {
|
||||||
|
out.target += "?";
|
||||||
|
out.target += std::string(u.encoded_query());
|
||||||
|
}
|
||||||
|
|
||||||
|
// port
|
||||||
|
if(!u.port().empty()) {
|
||||||
|
out.port = u.port_number();
|
||||||
|
} else {
|
||||||
|
if(out.scheme == "https")
|
||||||
|
out.port = 443;
|
||||||
|
else
|
||||||
|
out.port = 80;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.use_ssl = (out.scheme == "https");
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
awaitable_expected< std::pmr::string > HttpGetClient::async_get(std::string_view url, std::pmr::memory_resource * mr) {
|
||||||
|
BOOST_ASSERT(mr);
|
||||||
|
namespace ssl = boost::asio::ssl;
|
||||||
|
|
||||||
|
PmrCharAlloc alloc{mr};
|
||||||
|
|
||||||
|
// 1. Parse URL
|
||||||
|
auto parsed_exp = parse_url_generic(url);
|
||||||
|
if(!parsed_exp) {
|
||||||
|
co_return unexpected(parsed_exp.error());
|
||||||
|
}
|
||||||
|
auto parsed = *parsed_exp;
|
||||||
|
|
||||||
|
beast::error_code ec;
|
||||||
|
|
||||||
|
// 2. Resolver
|
||||||
|
tcp::resolver resolver{executor()};
|
||||||
|
auto results = co_await resolver.async_resolve(parsed.host, std::to_string(parsed.port), asio::redirect_error(asio::use_awaitable, ec));
|
||||||
|
if(ec) {
|
||||||
|
co_return unexpected(ec);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Request na PMR
|
||||||
|
http::request< PmrStringBody, PmrFields > req{http::verb::get, parsed.target, 11, PmrStringBody::value_type{alloc}, PmrFields{alloc}};
|
||||||
|
req.set(http::field::host, parsed.host);
|
||||||
|
req.set(http::field::user_agent, "ranczo-http-client");
|
||||||
|
|
||||||
|
// 4. Wspólne rzeczy dla HTTP/HTTPS
|
||||||
|
PmrFlatBuffer buffer{alloc};
|
||||||
|
PmrResponse res{std::piecewise_construct, std::make_tuple(PmrStringBody::value_type{alloc}), std::make_tuple(PmrFields{alloc})};
|
||||||
|
|
||||||
|
if(!parsed.use_ssl) {
|
||||||
|
// ---------------------- HTTP (bez TLS) ----------------------
|
||||||
|
beast::tcp_stream stream{executor()};
|
||||||
|
stream.expires_after(_timeout);
|
||||||
|
|
||||||
|
co_await stream.async_connect(results, asio::redirect_error(asio::use_awaitable, ec));
|
||||||
|
if(ec) {
|
||||||
|
co_return unexpected(ec);
|
||||||
|
}
|
||||||
|
|
||||||
|
co_await http::async_write(stream, req, asio::redirect_error(asio::use_awaitable, ec));
|
||||||
|
if(ec) {
|
||||||
|
co_return unexpected(ec);
|
||||||
|
}
|
||||||
|
|
||||||
|
co_await http::async_read(stream, buffer, res, asio::redirect_error(asio::use_awaitable, ec));
|
||||||
|
if(ec) {
|
||||||
|
co_return unexpected(ec);
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.socket().shutdown(tcp::socket::shutdown_both, ec);
|
||||||
|
// błąd przy shutdown zwykle ignorujemy
|
||||||
|
} else {
|
||||||
|
// ---------------------- HTTPS (TLS) ----------------------
|
||||||
|
ssl::context ctx{ssl::context::tls_client};
|
||||||
|
|
||||||
|
// Uwaga: do prawdziwego użycia:
|
||||||
|
// - ctx.set_default_verify_paths();
|
||||||
|
// - ctx.set_verify_mode(ssl::verify_peer);
|
||||||
|
// Tu dla testów najprościej wyłączyć weryfikację:
|
||||||
|
ctx.set_verify_mode(ssl::verify_none);
|
||||||
|
|
||||||
|
beast::ssl_stream< beast::tcp_stream > stream{executor_, ctx};
|
||||||
|
|
||||||
|
auto & lowest = beast::get_lowest_layer(stream);
|
||||||
|
lowest.expires_after(_timeout);
|
||||||
|
|
||||||
|
co_await lowest.async_connect(results, asio::redirect_error(asio::use_awaitable, ec));
|
||||||
|
if(ec) {
|
||||||
|
co_return unexpected(ec);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (opcjonalnie) SNI – przydatne dla częsci serwerów
|
||||||
|
if(!SSL_set_tlsext_host_name(stream.native_handle(), parsed.host.c_str())) {
|
||||||
|
ec = beast::error_code(static_cast< int >(::ERR_get_error()), asio::error::get_ssl_category());
|
||||||
|
co_return unexpected(ec);
|
||||||
|
}
|
||||||
|
|
||||||
|
co_await stream.async_handshake(ssl::stream_base::client, asio::redirect_error(asio::use_awaitable, ec));
|
||||||
|
if(ec) {
|
||||||
|
co_return unexpected(ec);
|
||||||
|
}
|
||||||
|
|
||||||
|
co_await http::async_write(stream, req, asio::redirect_error(asio::use_awaitable, ec));
|
||||||
|
if(ec) {
|
||||||
|
co_return unexpected(ec);
|
||||||
|
}
|
||||||
|
|
||||||
|
co_await http::async_read(stream, buffer, res, asio::redirect_error(asio::use_awaitable, ec));
|
||||||
|
if(ec) {
|
||||||
|
co_return unexpected(ec);
|
||||||
|
}
|
||||||
|
|
||||||
|
// shutdown TLS
|
||||||
|
co_await stream.async_shutdown(asio::redirect_error(asio::use_awaitable, ec));
|
||||||
|
// tu też często dostaniesz EOF – można zignorować
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opcjonalnie możesz sprawdzić status:
|
||||||
|
// if (res.result() != http::status::ok) { ... }
|
||||||
|
|
||||||
|
// Body korzysta z tego samego PMR
|
||||||
|
std::pmr::string body{alloc};
|
||||||
|
body = res.body(); // kopia na tym samym allocatorze (bez dużych kosztów w mr)
|
||||||
|
|
||||||
|
co_return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ranczo
|
||||||
@ -123,8 +123,11 @@ awaitable_expected< T > ModbusTcpContext::call_with_lock(F && op) {
|
|||||||
out = ctx_.get();
|
out = ctx_.get();
|
||||||
return op(out);
|
return op(out);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
template < typename T, typename Maker >
|
template < typename T, typename Maker >
|
||||||
awaitable_expected< T > ModbusTcpContext::async_call(Maker && maker) {
|
awaitable_expected< T > ModbusTcpContext::async_call(Maker && maker) {
|
||||||
namespace asio = boost::asio;
|
namespace asio = boost::asio;
|
||||||
@ -155,6 +158,7 @@ awaitable_expected< uint16_t > ModbusDevice::async_read_holding_register(uint16_
|
|||||||
_log.error("modbus_set_slave address {} failed with {}", address, errno);
|
_log.error("modbus_set_slave address {} failed with {}", address, errno);
|
||||||
return unexpected(errno_errc());
|
return unexpected(errno_errc());
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t val = 0;
|
uint16_t val = 0;
|
||||||
int rc = ::modbus_read_registers(c, static_cast< int >(address), 1, &val);
|
int rc = ::modbus_read_registers(c, static_cast< int >(address), 1, &val);
|
||||||
if(rc == -1) {
|
if(rc == -1) {
|
||||||
@ -165,6 +169,29 @@ awaitable_expected< uint16_t > ModbusDevice::async_read_holding_register(uint16_
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
awaitable_expected<std::pmr::vector<uint16_t> > ModbusDevice::async_read_holding_registers(uint16_t address, std::size_t number, std::pmr::memory_resource *mr)
|
||||||
|
{
|
||||||
|
BOOST_ASSERT(mr);
|
||||||
|
address -= 1;
|
||||||
|
auto ctx = ctx_; // kopia shared_ptr dla bezpieczeństwa w tasku
|
||||||
|
co_return co_await ctx->call_with_lock< std::pmr::vector< std::uint16_t > >(
|
||||||
|
[this, address, mr, number](modbus_t * c) -> expected< std::pmr::vector< std::uint16_t > > {
|
||||||
|
if(::modbus_set_slave(c, unit_id_) == -1) {
|
||||||
|
_log.error("modbus_set_slave address {} failed with {}", address, errno);
|
||||||
|
return unexpected(errno_errc());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pmr::vector< std::uint16_t > val{mr};
|
||||||
|
val.resize(number);
|
||||||
|
int rc = ::modbus_read_registers(c, static_cast< int >(address), number, val.data());
|
||||||
|
if(rc == -1) {
|
||||||
|
_log.error("modbus_read_registers address {} failed with {}", address, errno);
|
||||||
|
return unexpected(errno_errc());
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
awaitable_expected< void > ModbusDevice::async_write_coil(uint16_t address, bool value) {
|
awaitable_expected< void > ModbusDevice::async_write_coil(uint16_t address, bool value) {
|
||||||
auto ctx = ctx_;
|
auto ctx = ctx_;
|
||||||
co_return co_await ctx->call_with_lock< void >([this, address, value](modbus_t * c) -> expected< void > {
|
co_return co_await ctx->call_with_lock< void >([this, address, value](modbus_t * c) -> expected< void > {
|
||||||
|
|||||||
84
libs/ranczo-io/utils/http.hpp
Normal file
84
libs/ranczo-io/utils/http.hpp
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <config.hpp>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <memory>
|
||||||
|
#include <memory_resource>
|
||||||
|
#include <ranczo-io/utils/logger.hpp>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include <boost/asio/ip/tcp.hpp>
|
||||||
|
|
||||||
|
namespace ranczo {
|
||||||
|
class BaseIO {
|
||||||
|
public:
|
||||||
|
using allocator_type = std::pmr::polymorphic_allocator< std::byte >;
|
||||||
|
|
||||||
|
BaseIO()
|
||||||
|
: resource_(std::pmr::get_default_resource()), _log{spdlog::default_logger(), "Client"}, executor_{boost::asio::system_executor()} {}
|
||||||
|
|
||||||
|
BaseIO(boost::asio::any_io_executor ex,
|
||||||
|
std::string_view log_name = "Client",
|
||||||
|
std::pmr::memory_resource * res = std::pmr::get_default_resource())
|
||||||
|
: resource_(res), _log{spdlog::default_logger(), log_name}, executor_{ex} {}
|
||||||
|
|
||||||
|
BaseIO(std::allocator_arg_t, const allocator_type & alloc, boost::asio::any_io_executor ex, std::string_view log_name = "Client")
|
||||||
|
: resource_(alloc.resource()), _log{spdlog::default_logger(), log_name}, executor_{ex} {}
|
||||||
|
|
||||||
|
allocator_type get_allocator() const noexcept {
|
||||||
|
return allocator_type{resource_};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pmr::memory_resource * get_resource() const noexcept {
|
||||||
|
return resource_;
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::asio::any_io_executor executor() const noexcept {
|
||||||
|
return executor_;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModuleLogger & log() noexcept {
|
||||||
|
return _log;
|
||||||
|
}
|
||||||
|
const ModuleLogger & log() const noexcept {
|
||||||
|
return _log;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModuleLogger _log;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// chronione, aby klasy pochodne mogły korzystać
|
||||||
|
std::pmr::memory_resource * resource_;
|
||||||
|
boost::asio::any_io_executor executor_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class HttpGetClient : public BaseIO {
|
||||||
|
public:
|
||||||
|
explicit HttpGetClient(boost::asio::any_io_executor executor)
|
||||||
|
: BaseIO{std::allocator_arg, allocator_type{std::pmr::get_default_resource()}, executor, "HttpClient"} {}
|
||||||
|
|
||||||
|
explicit HttpGetClient(std::allocator_arg_t t, const allocator_type & a, boost::asio::any_io_executor executor)
|
||||||
|
: BaseIO{t, a, executor, "HttpClient"} {}
|
||||||
|
|
||||||
|
struct ParsedUrl {
|
||||||
|
std::string scheme;
|
||||||
|
std::string host;
|
||||||
|
std::uint16_t port;
|
||||||
|
std::string target; // path + query
|
||||||
|
bool use_ssl;
|
||||||
|
};
|
||||||
|
/// Prosty helper: parsuje URL na scheme/host/port/target
|
||||||
|
expected< ParsedUrl > parse_url_generic(std::string_view url);
|
||||||
|
awaitable_expected< std::pmr::string > async_get(std::string_view url, std::pmr::memory_resource * mr);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::chrono::steady_clock::duration _timeout{std::chrono::seconds{5}};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ranczo
|
||||||
|
|
||||||
|
namespace std {
|
||||||
|
template <>
|
||||||
|
struct uses_allocator< ranczo::HttpGetClient, ranczo::HttpGetClient::allocator_type > : true_type {};
|
||||||
|
} // namespace std
|
||||||
@ -12,6 +12,7 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <memory_resource>
|
||||||
#include <string>
|
#include <string>
|
||||||
// libmodbus (C)
|
// libmodbus (C)
|
||||||
#include <modbus/modbus.h>
|
#include <modbus/modbus.h>
|
||||||
@ -78,10 +79,19 @@ class ModbusDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FC 0x03: pojedynczy holding register
|
// FC 0x03: pojedynczy holding register
|
||||||
awaitable_expected< std::uint16_t > async_read_holding_register(std::uint16_t address);
|
awaitable_expected< std::uint16_t > async_read_holding_register( //
|
||||||
|
std::uint16_t address);
|
||||||
|
|
||||||
|
// FC 0x03: wiele holding registers
|
||||||
|
awaitable_expected< std::pmr::vector< std::uint16_t > > async_read_holding_registers( //
|
||||||
|
std::uint16_t address,
|
||||||
|
std::size_t number,
|
||||||
|
std::pmr::memory_resource * mr = std::pmr::get_default_resource());
|
||||||
|
|
||||||
// FC 0x05 – ZAPIS CEWKI (coil/bit): true=ON, false=OFF
|
// FC 0x05 – ZAPIS CEWKI (coil/bit): true=ON, false=OFF
|
||||||
awaitable_expected< void > async_write_coil(std::uint16_t address, bool value);
|
awaitable_expected< void > async_write_coil( //
|
||||||
|
std::uint16_t address,
|
||||||
|
bool value);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static boost::system::error_code errno_errc() {
|
static boost::system::error_code errno_errc() {
|
||||||
|
|||||||
@ -7,7 +7,6 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Topic layout assumptions
|
* Topic layout assumptions
|
||||||
*
|
*
|
||||||
@ -41,7 +40,6 @@
|
|||||||
* Jest wyłącznie kanałem, na którym urządzenie publikuje swój stan.
|
* Jest wyłącznie kanałem, na którym urządzenie publikuje swój stan.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
namespace ranczo {
|
namespace ranczo {
|
||||||
template < typename... Args >
|
template < typename... Args >
|
||||||
constexpr size_t length(const Args &... args) {
|
constexpr size_t length(const Args &... args) {
|
||||||
@ -118,7 +116,7 @@ namespace topic {
|
|||||||
publishState(std::string_view room, int zone = 1, std::pmr::memory_resource * mr = std::pmr::get_default_resource()) {
|
publishState(std::string_view room, int zone = 1, std::pmr::memory_resource * mr = std::pmr::get_default_resource()) {
|
||||||
using namespace std::string_view_literals;
|
using namespace std::string_view_literals;
|
||||||
BOOST_ASSERT(mr);
|
BOOST_ASSERT(mr);
|
||||||
return buildPublishTopic(*mr, room, "heating"sv, "floor"sv, zone, "state"sv);
|
return buildPublishTopic(*mr, room, "heating"sv, "floor"sv, zone, "state"sv); // TODO powalczyć z prawidłowym nazwenictwem
|
||||||
}
|
}
|
||||||
} // namespace heating
|
} // namespace heating
|
||||||
|
|
||||||
@ -144,20 +142,46 @@ namespace topic {
|
|||||||
} // namespace temperature
|
} // namespace temperature
|
||||||
|
|
||||||
namespace utilities {
|
namespace utilities {
|
||||||
// home/utilities/power/<meter>/<kind>/<channel>
|
|
||||||
/*
|
/*
|
||||||
* <meter>: main, heating, housing
|
group – „rodzaj wielkości” (measurement / power / energy / flow / volume / …)
|
||||||
* <kind>: active, reactive, apparent, voltage, current, frequency, pf
|
medium – nośnik (electricity / water / gas / …)
|
||||||
* <channel>: total, L1, L2, L3 itp.
|
meter – który licznik (main / heating / housing / …)
|
||||||
|
measurement – co dokładnie (voltage / current / active / reactive / flow / volume / …)
|
||||||
|
channel – kanał / faza / obwód (L1 / L2 / total / loop1 / …)
|
||||||
|
|
||||||
|
home/utilities/flow /water /main/flow /heating_loop
|
||||||
|
home/utilities/flow /water /main/flow /domestic
|
||||||
|
home/utilities/power /electricity/main/active /L1
|
||||||
|
home/utilities/measurement/electricity/main/voltage/L1
|
||||||
|
|
||||||
|
# home/utilities/flow/water/main/flow/total
|
||||||
|
# group ^^^^ [flow | power | energy]
|
||||||
|
# medium ^^^^ [water | electricity]
|
||||||
|
# meter ^^^^ [main]
|
||||||
|
# measurement ^^^^ [flow | volume]
|
||||||
|
# channel ^^^^^ [domestic | well]
|
||||||
*/
|
*/
|
||||||
inline std::pmr::string
|
|
||||||
publishPowerReading(std::string_view meter, std::string_view kind, std::string_view channel, std::pmr::memory_resource * mr = std::pmr::get_default_resource()) {
|
inline std::pmr::string _make_topic(std::pmr::memory_resource * mr,
|
||||||
|
std::string_view group,
|
||||||
|
std::string_view medium,
|
||||||
|
std::string_view meter,
|
||||||
|
std::string_view measurement,
|
||||||
|
std::string_view channel) {
|
||||||
|
using namespace std::string_view_literals;
|
||||||
|
return make_topic(*mr, "home"sv, "utilities"sv, group, medium, meter, measurement, channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::pmr::string publishPowerReading(std::string_view meter,
|
||||||
|
std::string_view measuremetn,
|
||||||
|
std::string_view channel,
|
||||||
|
std::pmr::memory_resource * mr = std::pmr::get_default_resource()) {
|
||||||
BOOST_ASSERT(mr);
|
BOOST_ASSERT(mr);
|
||||||
BOOST_ASSERT(meter.size());
|
BOOST_ASSERT(meter.size());
|
||||||
BOOST_ASSERT(kind.size());
|
BOOST_ASSERT(kind.size());
|
||||||
BOOST_ASSERT(channel == "total" || channel == "L1" || channel == "L2" || channel == "L3");
|
BOOST_ASSERT(channel == "total" || channel == "L1" || channel == "L2" || channel == "L3");
|
||||||
using namespace std::string_view_literals;
|
using namespace std::string_view_literals;
|
||||||
return make_topic(*mr, "home"sv, "utilities"sv, "power"sv, "electricity"sv, meter, kind, channel);
|
return _make_topic(mr, "power"sv, "electricity"sv, meter, measuremetn, channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// home/utilities/energy/<meter>/<kind>/<channel>
|
// home/utilities/energy/<meter>/<kind>/<channel>
|
||||||
@ -166,18 +190,19 @@ namespace topic {
|
|||||||
* <kind>: tatol_active_energy
|
* <kind>: tatol_active_energy
|
||||||
* <channel>: total, L1, L2, L3 itp.
|
* <channel>: total, L1, L2, L3 itp.
|
||||||
*/
|
*/
|
||||||
inline std::pmr::string
|
inline std::pmr::string publishEnergyReading(std::string_view meter,
|
||||||
publishEnergyReading(std::string_view meter, std::string_view kind, std::string_view channel, std::pmr::memory_resource * mr = std::pmr::get_default_resource()) {
|
std::string_view measurement,
|
||||||
|
std::string_view channel,
|
||||||
|
std::pmr::memory_resource * mr = std::pmr::get_default_resource()) {
|
||||||
BOOST_ASSERT(mr);
|
BOOST_ASSERT(mr);
|
||||||
BOOST_ASSERT(meter.size());
|
BOOST_ASSERT(meter.size());
|
||||||
BOOST_ASSERT(kind.size());
|
BOOST_ASSERT(kind.size());
|
||||||
BOOST_ASSERT(channel == "total" || channel == "L1" || channel == "L2" || channel == "L3");
|
BOOST_ASSERT(channel == "total" || channel == "L1" || channel == "L2" || channel == "L3");
|
||||||
using namespace std::string_view_literals;
|
using namespace std::string_view_literals;
|
||||||
return make_topic(*mr, "home"sv, "utilities"sv, "energy"sv, "electricity"sv, meter, kind, channel);
|
return _make_topic(mr, "energy"sv, "electricity"sv, meter, measurement, channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace utilities
|
||||||
}
|
|
||||||
} // namespace topic
|
} // namespace topic
|
||||||
|
|
||||||
} // namespace ranczo
|
} // namespace ranczo
|
||||||
|
|||||||
@ -7,6 +7,6 @@ include(GNUInstallDirs)
|
|||||||
|
|
||||||
add_subdirectory(temperature_svc)
|
add_subdirectory(temperature_svc)
|
||||||
add_subdirectory(floorheat_svc)
|
add_subdirectory(floorheat_svc)
|
||||||
# add_subdirectory(energymeter_svc)
|
add_subdirectory(energymeter_svc)
|
||||||
add_subdirectory(output_svc)
|
add_subdirectory(output_svc)
|
||||||
add_subdirectory(input_svc)
|
add_subdirectory(input_svc)
|
||||||
|
|||||||
@ -0,0 +1,207 @@
|
|||||||
|
|
||||||
|
#include "ranczo-io/utils/date_utils.hpp"
|
||||||
|
#include "ranczo-io/utils/modbus.hpp"
|
||||||
|
#include <boost/asio/any_io_executor.hpp>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <modbus/modbus.h>
|
||||||
|
|
||||||
|
namespace ranczo {
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────────────────
|
||||||
|
// Rejestry energomierza
|
||||||
|
// ───────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
struct Register {
|
||||||
|
const char * measurement_name;
|
||||||
|
const char * unit;
|
||||||
|
const char * register_name;
|
||||||
|
std::uint16_t offset; // offset w słowach 16-bit od _baseAddress
|
||||||
|
|
||||||
|
float (*read)(const std::uint16_t *);
|
||||||
|
float (*total)(float lhs, float rhs);
|
||||||
|
|
||||||
|
float do_read(const std::uint16_t * data) const {
|
||||||
|
return read(data + offset);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
inline float read_float(const std::uint16_t * data) {
|
||||||
|
return modbus_get_float_abcd(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline float from_kilo(const std::uint16_t * data) {
|
||||||
|
return read_float(data) * 1000.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline float add(float lhs, float rhs) {
|
||||||
|
return lhs + rhs;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline float avg(float lhs, float rhs) {
|
||||||
|
return (lhs + rhs) * 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────────────────
|
||||||
|
// Konkrety: odczyty "live"
|
||||||
|
// ───────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class EnergymeterReadings {
|
||||||
|
public:
|
||||||
|
EnergymeterReadings(boost::asio::any_io_executor ex,
|
||||||
|
ModbusDevice & modbus,
|
||||||
|
std::span< const Register > regs,
|
||||||
|
const char * name,
|
||||||
|
std::uint16_t baseAddress)
|
||||||
|
: _ex(ex),
|
||||||
|
_modbus(modbus),
|
||||||
|
_registers(regs),
|
||||||
|
_name(name),
|
||||||
|
_baseAddress(baseAddress),
|
||||||
|
_housingUsage(regs.size(), 0.0f),
|
||||||
|
_heatingUsage(regs.size(), 0.0f) {}
|
||||||
|
|
||||||
|
// slave 1: "housing", slave 2: "heating"
|
||||||
|
ranczo::awaitable_expected< void > publish() {
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
const std::size_t LiveRegistersSize = _registers.size() * 2;
|
||||||
|
|
||||||
|
// std::vector< std::uint16_t > housingRaw(LiveRegistersSize);
|
||||||
|
// std::vector< std::uint16_t > heatingRaw(LiveRegistersSize);
|
||||||
|
|
||||||
|
// const auto housingTimepoint = date::to_iso_timestamp(std::chrono::system_clock::now());
|
||||||
|
// auto r1 =
|
||||||
|
// co_await _modbus.async_read_holding_registers(_baseAddress, static_cast< std::uint16_t >(LiveRegistersSize), );
|
||||||
|
// if(!r1) {
|
||||||
|
// spdlog::error("Modbus read (housing) failed: {}", r1.error().message());
|
||||||
|
// co_return unexpected(r1.error());
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const auto heatingTimepoint = date::to_iso_timestamp(std::chrono::system_clock::now());
|
||||||
|
// auto r2 =
|
||||||
|
// co_await _modbus.read_holding_registers(2, _baseAddress, static_cast< std::uint16_t >(LiveRegistersSize), heatingRaw.data());
|
||||||
|
// if(!r2) {
|
||||||
|
// spdlog::error("Modbus read (heating) failed: {}", r2.error().message());
|
||||||
|
// co_return unexpected(r2.error());
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const auto totalTimepoint = date::to_iso_timestamp(std::chrono::system_clock::now());
|
||||||
|
|
||||||
|
// auto housingPrev = _housingUsage.begin();
|
||||||
|
// auto heatingPrev = _heatingUsage.begin();
|
||||||
|
|
||||||
|
// for(const auto & reg : _registers) {
|
||||||
|
// const float housingValue = reg.do_read(housingRaw.data());
|
||||||
|
// const float heatingValue = reg.do_read(heatingRaw.data());
|
||||||
|
// const float totalValue = reg.total(heatingValue, housingValue);
|
||||||
|
|
||||||
|
// const bool housingUpdated = housingValue != *housingPrev;
|
||||||
|
// const bool heatingUpdated = heatingValue != *heatingPrev;
|
||||||
|
// const bool totalUpdated = housingUpdated || heatingUpdated;
|
||||||
|
|
||||||
|
// *housingPrev = housingValue;
|
||||||
|
// *heatingPrev = heatingValue;
|
||||||
|
// ++housingPrev;
|
||||||
|
// ++heatingPrev;
|
||||||
|
// }
|
||||||
|
|
||||||
|
co_return ranczo::expected< void >{};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
boost::asio::any_io_executor _ex;
|
||||||
|
|
||||||
|
ModbusDevice &_modbus;
|
||||||
|
std::span< const Register > _registers;
|
||||||
|
const char * _name;
|
||||||
|
const std::uint16_t _baseAddress;
|
||||||
|
|
||||||
|
std::vector< float > _housingUsage;
|
||||||
|
std::vector< float > _heatingUsage;
|
||||||
|
};
|
||||||
|
|
||||||
|
class EnergymeterLiveReading : public EnergymeterReadings {
|
||||||
|
private:
|
||||||
|
static constexpr std::uint16_t _baseAddressLive = 0x000E;
|
||||||
|
|
||||||
|
static constexpr Register liveRegisters_[] = {
|
||||||
|
{"Voltage", "V", "L1", std::uint16_t{0x000E} - _baseAddressLive, read_float, avg},
|
||||||
|
{"Voltage", "V", "L2", std::uint16_t{0x0010} - _baseAddressLive, read_float, avg},
|
||||||
|
{"Voltage", "V", "L3", std::uint16_t{0x0012} - _baseAddressLive, read_float, avg},
|
||||||
|
{"Frequency", "Hz", "Grid", std::uint16_t{0x0014} - _baseAddressLive, read_float, avg},
|
||||||
|
{"Current", "A", "L1", std::uint16_t{0x0016} - _baseAddressLive, read_float, add},
|
||||||
|
{"Current", "A", "L2", std::uint16_t{0x0018} - _baseAddressLive, read_float, add},
|
||||||
|
{"Current", "A", "L3", std::uint16_t{0x001A} - _baseAddressLive, read_float, add},
|
||||||
|
{"ActivePower", "W", "Total", std::uint16_t{0x001C} - _baseAddressLive, from_kilo, add},
|
||||||
|
{"ActivePower", "W", "L1", std::uint16_t{0x001E} - _baseAddressLive, from_kilo, add},
|
||||||
|
{"ActivePower", "W", "L2", std::uint16_t{0x0020} - _baseAddressLive, from_kilo, add},
|
||||||
|
{"ActivePower", "W", "L3", std::uint16_t{0x0022} - _baseAddressLive, from_kilo, add},
|
||||||
|
{"ReactivePower", "Var", "Total", std::uint16_t{0x0024} - _baseAddressLive, from_kilo, add},
|
||||||
|
{"ReactivePower", "Var", "L1", std::uint16_t{0x0026} - _baseAddressLive, from_kilo, add},
|
||||||
|
{"ReactivePower", "Var", "L2", std::uint16_t{0x0028} - _baseAddressLive, from_kilo, add},
|
||||||
|
{"ReactivePower", "Var", "L3", std::uint16_t{0x002A} - _baseAddressLive, from_kilo, add},
|
||||||
|
{"ApparentPower", "VA", "Total", std::uint16_t{0x002C} - _baseAddressLive, from_kilo, add},
|
||||||
|
{"ApparentPower", "VA", "L1", std::uint16_t{0x002E} - _baseAddressLive, from_kilo, add},
|
||||||
|
{"ApparentPower", "VA", "L2", std::uint16_t{0x0030} - _baseAddressLive, from_kilo, add},
|
||||||
|
{"ApparentPower", "VA", "L3", std::uint16_t{0x0032} - _baseAddressLive, from_kilo, add},
|
||||||
|
{"PowerFactor", "", "Total", std::uint16_t{0x0034} - _baseAddressLive, read_float, avg},
|
||||||
|
{"PowerFactor", "", "L1", std::uint16_t{0x0036} - _baseAddressLive, read_float, avg},
|
||||||
|
{"PowerFactor", "", "L2", std::uint16_t{0x0038} - _baseAddressLive, read_float, avg},
|
||||||
|
{"PowerFactor", "", "L3", std::uint16_t{0x003A} - _baseAddressLive, read_float, avg},
|
||||||
|
};
|
||||||
|
|
||||||
|
public:
|
||||||
|
EnergymeterLiveReading(boost::asio::any_io_executor ex, ModbusDevice & modbus)
|
||||||
|
: EnergymeterReadings(ex,
|
||||||
|
modbus,
|
||||||
|
std::span< const Register >(liveRegisters_, std::size(liveRegisters_)),
|
||||||
|
"current",
|
||||||
|
_baseAddressLive) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────────────────
|
||||||
|
// Konkrety: odczyty "total"
|
||||||
|
// ───────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class EnergymeterTotalReading : public EnergymeterReadings {
|
||||||
|
private:
|
||||||
|
static constexpr std::uint16_t _baseAddressTotal = 0x0100;
|
||||||
|
|
||||||
|
static constexpr Register totalRegisters_[] = {
|
||||||
|
{"ActiveEnergy", "Wh", "Total", std::uint16_t{0x0100} - _baseAddressTotal, from_kilo, add},
|
||||||
|
{"ActiveEnergy", "Wh", "L1", std::uint16_t{0x0102} - _baseAddressTotal, from_kilo, add},
|
||||||
|
{"ActiveEnergy", "Wh", "L2", std::uint16_t{0x0104} - _baseAddressTotal, from_kilo, add},
|
||||||
|
{"ActiveEnergy", "Wh", "L3", std::uint16_t{0x0106} - _baseAddressTotal, from_kilo, add},
|
||||||
|
{"ForwardActiveEnergy", "Wh", "Total", std::uint16_t{0x0108} - _baseAddressTotal, from_kilo, add},
|
||||||
|
{"ForwardActiveEnergy", "Wh", "L1", std::uint16_t{0x010A} - _baseAddressTotal, from_kilo, add},
|
||||||
|
{"ForwardActiveEnergy", "Wh", "L2", std::uint16_t{0x010C} - _baseAddressTotal, from_kilo, add},
|
||||||
|
{"ForwardActiveEnergy", "Wh", "L3", std::uint16_t{0x010E} - _baseAddressTotal, from_kilo, add},
|
||||||
|
{"ReverseActiveEnergy", "Wh", "Total", std::uint16_t{0x0110} - _baseAddressTotal, from_kilo, add},
|
||||||
|
{"ReverseActiveEnergy", "Wh", "L1", std::uint16_t{0x0112} - _baseAddressTotal, from_kilo, add},
|
||||||
|
{"ReverseActiveEnergy", "Wh", "L2", std::uint16_t{0x0114} - _baseAddressTotal, from_kilo, add},
|
||||||
|
{"ReverseActiveEnergy", "Wh", "L3", std::uint16_t{0x0116} - _baseAddressTotal, from_kilo, add},
|
||||||
|
{"ReactiveEnergy", "Varh", "Total", std::uint16_t{0x0118} - _baseAddressTotal, from_kilo, add},
|
||||||
|
{"ReactiveEnergy", "Varh", "L1", std::uint16_t{0x011A} - _baseAddressTotal, from_kilo, add},
|
||||||
|
{"ReactiveEnergy", "Varh", "L2", std::uint16_t{0x011C} - _baseAddressTotal, from_kilo, add},
|
||||||
|
{"ReactiveEnergy", "Varh", "L3", std::uint16_t{0x011E} - _baseAddressTotal, from_kilo, add},
|
||||||
|
{"ForwardReactiveEnergy", "Varh", "Total", std::uint16_t{0x0120} - _baseAddressTotal, from_kilo, add},
|
||||||
|
{"ForwardReactiveEnergy", "Varh", "L1", std::uint16_t{0x0122} - _baseAddressTotal, from_kilo, add},
|
||||||
|
{"ForwardReactiveEnergy", "Varh", "L2", std::uint16_t{0x0124} - _baseAddressTotal, from_kilo, add},
|
||||||
|
{"ForwardReactiveEnergy", "Varh", "L3", std::uint16_t{0x0126} - _baseAddressTotal, from_kilo, add},
|
||||||
|
{"ReverseReactiveEnergy", "Varh", "Total", std::uint16_t{0x0128} - _baseAddressTotal, from_kilo, add},
|
||||||
|
{"ReverseReactiveEnergy", "Varh", "L1", std::uint16_t{0x012A} - _baseAddressTotal, from_kilo, add},
|
||||||
|
{"ReverseReactiveEnergy", "Varh", "L2", std::uint16_t{0x012C} - _baseAddressTotal, from_kilo, add},
|
||||||
|
{"ReverseReactiveEnergy", "Varh", "L3", std::uint16_t{0x012E} - _baseAddressTotal, from_kilo, add},
|
||||||
|
};
|
||||||
|
|
||||||
|
public:
|
||||||
|
EnergymeterTotalReading(boost::asio::any_io_executor ex, ModbusDevice & modbus)
|
||||||
|
: EnergymeterReadings(ex,
|
||||||
|
modbus,
|
||||||
|
std::span< const Register >(totalRegisters_, std::size(totalRegisters_)),
|
||||||
|
"total",
|
||||||
|
_baseAddressTotal) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ranczo
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
@ -1,413 +1,40 @@
|
|||||||
|
|
||||||
#include <array>
|
|
||||||
#include <chrono>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <span>
|
|
||||||
#include <string>
|
|
||||||
#include <string_view>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <boost/asio.hpp>
|
#include <boost/asio.hpp>
|
||||||
#include <boost/system/error_code.hpp>
|
#include <boost/system/error_code.hpp>
|
||||||
|
|
||||||
#include <fmt/core.h>
|
#include <fmt/core.h>
|
||||||
#include <modbus/modbus.h>
|
#include <iostream>
|
||||||
|
#include <memory_resource>
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
#include <config.hpp>
|
#include <config.hpp>
|
||||||
|
|
||||||
// ───────────────────────────────────────────────────────────────────────────
|
#include <ranczo-io/utils/http.hpp>
|
||||||
// Pomocnicze: timestamp ISO
|
|
||||||
// ───────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
inline std::string to_iso_timestamp()
|
namespace ranczo {
|
||||||
{
|
|
||||||
using clock = std::chrono::system_clock;
|
|
||||||
auto now = clock::now();
|
|
||||||
std::time_t t = clock::to_time_t(now);
|
|
||||||
|
|
||||||
std::tm tm{};
|
} // namespace ranczo
|
||||||
#if defined(_WIN32)
|
|
||||||
gmtime_s(&tm, &t);
|
|
||||||
#else
|
|
||||||
gmtime_r(&t, &tm);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
char buf[32];
|
int main() {
|
||||||
std::strftime(buf, sizeof(buf), "%FT%TZ", &tm);
|
boost::asio::io_context io;
|
||||||
return std::string(buf);
|
|
||||||
|
boost::asio::co_spawn(
|
||||||
|
io,
|
||||||
|
[&]() -> ranczo::awaitable< void > {
|
||||||
|
|
||||||
|
ranczo::HttpGetClient cli{io.get_executor()};
|
||||||
|
auto r = co_await cli.async_get("http://192.168.20.11:80/state", std::pmr::get_default_resource());
|
||||||
|
|
||||||
|
if(!r) {
|
||||||
|
spdlog::error("HTTP GET failed: {}", r.error().message());
|
||||||
|
} else {
|
||||||
|
std::cout << *r;
|
||||||
|
// spdlog::info("Response: {}", *r);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ───────────────────────────────────────────────────────────────────────────
|
co_return;
|
||||||
// Rejestry energomierza
|
|
||||||
// ───────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
struct Register {
|
|
||||||
const char* measurement_name;
|
|
||||||
const char* unit;
|
|
||||||
const char* register_name;
|
|
||||||
std::uint16_t offset; // offset w słowach 16-bit od _baseAddress
|
|
||||||
|
|
||||||
float (*read)(const std::uint16_t*);
|
|
||||||
float (*total)(float lhs, float rhs);
|
|
||||||
|
|
||||||
float do_read(const std::uint16_t* data) const {
|
|
||||||
return read(data + offset);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
inline float read_float(const std::uint16_t* data) {
|
|
||||||
return modbus_get_float_abcd(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline float from_kilo(const std::uint16_t* data) {
|
|
||||||
return read_float(data) * 1000.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline float add(float lhs, float rhs) {
|
|
||||||
return lhs + rhs;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline float avg(float lhs, float rhs) {
|
|
||||||
return (lhs + rhs) * 0.5f;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ───────────────────────────────────────────────────────────────────────────
|
|
||||||
// Formatowanie topiców / payloadów MQTT
|
|
||||||
// ───────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
inline std::size_t topic_to(
|
|
||||||
char* buffer,
|
|
||||||
const Register& reg,
|
|
||||||
std::string_view type,
|
|
||||||
std::string_view event)
|
|
||||||
{
|
|
||||||
const auto end = fmt::format_to(
|
|
||||||
buffer,
|
|
||||||
"home/utilities/electricity/{}/{}/{}/{}",
|
|
||||||
type,
|
|
||||||
event,
|
|
||||||
reg.measurement_name,
|
|
||||||
reg.register_name);
|
|
||||||
|
|
||||||
*end = '\0';
|
|
||||||
auto len = static_cast<std::size_t>(std::distance(buffer, end) + 1);
|
|
||||||
spdlog::debug("MQTT topic: {}", buffer);
|
|
||||||
return len;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::size_t payload_to(
|
|
||||||
std::uint8_t* buffer,
|
|
||||||
const Register& reg,
|
|
||||||
float value,
|
|
||||||
bool update,
|
|
||||||
std::string_view ts)
|
|
||||||
{
|
|
||||||
const auto end = fmt::format_to(
|
|
||||||
buffer,
|
|
||||||
R"json({{"value":{},"unit":"{}","source":"energymeter_service","update":{},"timestamp":"{}"}})json",
|
|
||||||
value,
|
|
||||||
reg.unit,
|
|
||||||
update ? "true" : "false",
|
|
||||||
ts);
|
|
||||||
|
|
||||||
auto len = static_cast<std::size_t>(std::distance(buffer, end));
|
|
||||||
return len;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ───────────────────────────────────────────────────────────────────────────
|
|
||||||
// Bazowa klasa odczytu energomierza (async, z boost::asio)
|
|
||||||
// ───────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
class EnergymeterReadings {
|
|
||||||
public:
|
|
||||||
EnergymeterReadings(
|
|
||||||
boost::asio::any_io_executor ex,
|
|
||||||
MqttClient& mqtt,
|
|
||||||
ModbusClient& modbus,
|
|
||||||
std::span<const Register> regs,
|
|
||||||
const char* name,
|
|
||||||
std::uint16_t baseAddress)
|
|
||||||
: _ex(ex)
|
|
||||||
, _mqtt(mqtt)
|
|
||||||
, _modbus(modbus)
|
|
||||||
, _registers(regs)
|
|
||||||
, _name(name)
|
|
||||||
, _baseAddress(baseAddress)
|
|
||||||
, _housingUsage(regs.size(), 0.0f)
|
|
||||||
, _heatingUsage(regs.size(), 0.0f)
|
|
||||||
{}
|
|
||||||
|
|
||||||
// slave 1: "housing", slave 2: "heating"
|
|
||||||
awaitable_expected<void> publish()
|
|
||||||
{
|
|
||||||
using namespace std::chrono_literals;
|
|
||||||
|
|
||||||
const std::size_t LiveRegistersSize = _registers.size() * 2;
|
|
||||||
|
|
||||||
std::vector<std::uint16_t> housingRaw(LiveRegistersSize);
|
|
||||||
std::vector<std::uint16_t> heatingRaw(LiveRegistersSize);
|
|
||||||
|
|
||||||
const auto housingTimepoint = to_iso_timestamp();
|
|
||||||
auto r1 = co_await _modbus.read_holding_registers(
|
|
||||||
1, _baseAddress, static_cast<std::uint16_t>(LiveRegistersSize), housingRaw.data());
|
|
||||||
if (!r1) {
|
|
||||||
spdlog::error("Modbus read (housing) failed: {}", r1.error().message());
|
|
||||||
co_return unexpected(r1.error());
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto heatingTimepoint = to_iso_timestamp();
|
|
||||||
auto r2 = co_await _modbus.read_holding_registers(
|
|
||||||
2, _baseAddress, static_cast<std::uint16_t>(LiveRegistersSize), heatingRaw.data());
|
|
||||||
if (!r2) {
|
|
||||||
spdlog::error("Modbus read (heating) failed: {}", r2.error().message());
|
|
||||||
co_return unexpected(r2.error());
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto totalTimepoint = to_iso_timestamp();
|
|
||||||
|
|
||||||
auto housingPrev = _housingUsage.begin();
|
|
||||||
auto heatingPrev = _heatingUsage.begin();
|
|
||||||
|
|
||||||
std::uint8_t payloadBuffer[256];
|
|
||||||
char topicBuffer[256];
|
|
||||||
|
|
||||||
for (const auto& reg : _registers) {
|
|
||||||
const float housingValue = reg.do_read(housingRaw.data());
|
|
||||||
const float heatingValue = reg.do_read(heatingRaw.data());
|
|
||||||
const float totalValue = reg.total(heatingValue, housingValue);
|
|
||||||
|
|
||||||
const bool housingUpdated = housingValue != *housingPrev;
|
|
||||||
const bool heatingUpdated = heatingValue != *heatingPrev;
|
|
||||||
const bool totalUpdated = housingUpdated || heatingUpdated;
|
|
||||||
|
|
||||||
*housingPrev = housingValue;
|
|
||||||
*heatingPrev = heatingValue;
|
|
||||||
++housingPrev;
|
|
||||||
++heatingPrev;
|
|
||||||
|
|
||||||
auto doPublish = [&](float value,
|
|
||||||
bool updated,
|
|
||||||
std::string_view ts,
|
|
||||||
const char* type) -> awaitable_expected<void>
|
|
||||||
{
|
|
||||||
const std::size_t topicLen = topic_to(topicBuffer, reg, _name, type);
|
|
||||||
const std::size_t payloadLen = payload_to(
|
|
||||||
payloadBuffer, reg, value, updated, ts);
|
|
||||||
|
|
||||||
std::string_view topic(topicBuffer, topicLen - 1); // bez '\0'
|
|
||||||
std::string_view payload(
|
|
||||||
reinterpret_cast<const char*>(payloadBuffer),
|
|
||||||
payloadLen);
|
|
||||||
|
|
||||||
auto res = co_await _mqtt.publish(topic, payload, 0);
|
|
||||||
if (!res) {
|
|
||||||
spdlog::warn("MQTT publish failed on topic {}: {}",
|
|
||||||
topic, res.error().message());
|
|
||||||
co_return unexpected(res.error());
|
|
||||||
}
|
|
||||||
|
|
||||||
co_return expected<void>{};
|
|
||||||
};
|
|
||||||
|
|
||||||
// housing
|
|
||||||
(void) co_await doPublish(housingValue, housingUpdated, housingTimepoint, "housing");
|
|
||||||
// heating
|
|
||||||
(void) co_await doPublish(heatingValue, heatingUpdated, heatingTimepoint, "heating");
|
|
||||||
// total
|
|
||||||
(void) co_await doPublish(totalValue, totalUpdated, totalTimepoint, "ALL");
|
|
||||||
}
|
|
||||||
|
|
||||||
co_return expected<void>{};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
|
||||||
boost::asio::any_io_executor _ex;
|
|
||||||
MqttClient& _mqtt;
|
|
||||||
ModbusClient& _modbus;
|
|
||||||
|
|
||||||
std::span<const Register> _registers;
|
|
||||||
const char* _name;
|
|
||||||
const std::uint16_t _baseAddress;
|
|
||||||
|
|
||||||
std::vector<float> _housingUsage;
|
|
||||||
std::vector<float> _heatingUsage;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ───────────────────────────────────────────────────────────────────────────
|
|
||||||
// Konkrety: odczyty "live"
|
|
||||||
// ───────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
class EnergymeterLiveReading : public EnergymeterReadings {
|
|
||||||
private:
|
|
||||||
static constexpr std::uint16_t _baseAddressLive = 0x000E;
|
|
||||||
|
|
||||||
static constexpr Register liveRegisters_[] = {
|
|
||||||
{"Voltage", "V", "L1", std::uint16_t{0x000E} - _baseAddressLive, read_float, avg},
|
|
||||||
{"Voltage", "V", "L2", std::uint16_t{0x0010} - _baseAddressLive, read_float, avg},
|
|
||||||
{"Voltage", "V", "L3", std::uint16_t{0x0012} - _baseAddressLive, read_float, avg},
|
|
||||||
{"Frequency", "Hz", "Grid", std::uint16_t{0x0014} - _baseAddressLive, read_float, avg},
|
|
||||||
{"Current", "A", "L1", std::uint16_t{0x0016} - _baseAddressLive, read_float, add},
|
|
||||||
{"Current", "A", "L2", std::uint16_t{0x0018} - _baseAddressLive, read_float, add},
|
|
||||||
{"Current", "A", "L3", std::uint16_t{0x001A} - _baseAddressLive, read_float, add},
|
|
||||||
{"ActivePower", "W", "Total", std::uint16_t{0x001C} - _baseAddressLive, from_kilo, add},
|
|
||||||
{"ActivePower", "W", "L1", std::uint16_t{0x001E} - _baseAddressLive, from_kilo, add},
|
|
||||||
{"ActivePower", "W", "L2", std::uint16_t{0x0020} - _baseAddressLive, from_kilo, add},
|
|
||||||
{"ActivePower", "W", "L3", std::uint16_t{0x0022} - _baseAddressLive, from_kilo, add},
|
|
||||||
{"ReactivePower", "Var", "Total", std::uint16_t{0x0024} - _baseAddressLive, from_kilo, add},
|
|
||||||
{"ReactivePower", "Var", "L1", std::uint16_t{0x0026} - _baseAddressLive, from_kilo, add},
|
|
||||||
{"ReactivePower", "Var", "L2", std::uint16_t{0x0028} - _baseAddressLive, from_kilo, add},
|
|
||||||
{"ReactivePower", "Var", "L3", std::uint16_t{0x002A} - _baseAddressLive, from_kilo, add},
|
|
||||||
{"ApparentPower", "VA", "Total", std::uint16_t{0x002C} - _baseAddressLive, from_kilo, add},
|
|
||||||
{"ApparentPower", "VA", "L1", std::uint16_t{0x002E} - _baseAddressLive, from_kilo, add},
|
|
||||||
{"ApparentPower", "VA", "L2", std::uint16_t{0x0030} - _baseAddressLive, from_kilo, add},
|
|
||||||
{"ApparentPower", "VA", "L3", std::uint16_t{0x0032} - _baseAddressLive, from_kilo, add},
|
|
||||||
{"PowerFactor", "", "Total", std::uint16_t{0x0034} - _baseAddressLive, read_float, avg},
|
|
||||||
{"PowerFactor", "", "L1", std::uint16_t{0x0036} - _baseAddressLive, read_float, avg},
|
|
||||||
{"PowerFactor", "", "L2", std::uint16_t{0x0038} - _baseAddressLive, read_float, avg},
|
|
||||||
{"PowerFactor", "", "L3", std::uint16_t{0x003A} - _baseAddressLive, read_float, avg},
|
|
||||||
};
|
|
||||||
|
|
||||||
public:
|
|
||||||
EnergymeterLiveReading(
|
|
||||||
boost::asio::any_io_executor ex,
|
|
||||||
MqttClient& mqtt,
|
|
||||||
ModbusClient& modbus)
|
|
||||||
: EnergymeterReadings(
|
|
||||||
ex,
|
|
||||||
mqtt,
|
|
||||||
modbus,
|
|
||||||
std::span<const Register>(liveRegisters_, std::size(liveRegisters_)),
|
|
||||||
"current",
|
|
||||||
_baseAddressLive)
|
|
||||||
{}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ───────────────────────────────────────────────────────────────────────────
|
|
||||||
// Konkrety: odczyty "total"
|
|
||||||
// ───────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
class EnergymeterTotalReading : public EnergymeterReadings {
|
|
||||||
private:
|
|
||||||
static constexpr std::uint16_t _baseAddressTotal = 0x0100;
|
|
||||||
|
|
||||||
static constexpr Register totalRegisters_[] = {
|
|
||||||
{"ActiveEnergy", "Wh", "Total", std::uint16_t{0x0100} - _baseAddressTotal, from_kilo, add},
|
|
||||||
{"ActiveEnergy", "Wh", "L1", std::uint16_t{0x0102} - _baseAddressTotal, from_kilo, add},
|
|
||||||
{"ActiveEnergy", "Wh", "L2", std::uint16_t{0x0104} - _baseAddressTotal, from_kilo, add},
|
|
||||||
{"ActiveEnergy", "Wh", "L3", std::uint16_t{0x0106} - _baseAddressTotal, from_kilo, add},
|
|
||||||
{"ForwardActiveEnergy", "Wh", "Total", std::uint16_t{0x0108} - _baseAddressTotal, from_kilo, add},
|
|
||||||
{"ForwardActiveEnergy", "Wh", "L1", std::uint16_t{0x010A} - _baseAddressTotal, from_kilo, add},
|
|
||||||
{"ForwardActiveEnergy", "Wh", "L2", std::uint16_t{0x010C} - _baseAddressTotal, from_kilo, add},
|
|
||||||
{"ForwardActiveEnergy", "Wh", "L3", std::uint16_t{0x010E} - _baseAddressTotal, from_kilo, add},
|
|
||||||
{"ReverseActiveEnergy", "Wh", "Total", std::uint16_t{0x0110} - _baseAddressTotal, from_kilo, add},
|
|
||||||
{"ReverseActiveEnergy", "Wh", "L1", std::uint16_t{0x0112} - _baseAddressTotal, from_kilo, add},
|
|
||||||
{"ReverseActiveEnergy", "Wh", "L2", std::uint16_t{0x0114} - _baseAddressTotal, from_kilo, add},
|
|
||||||
{"ReverseActiveEnergy", "Wh", "L3", std::uint16_t{0x0116} - _baseAddressTotal, from_kilo, add},
|
|
||||||
{"ReactiveEnergy", "Varh", "Total", std::uint16_t{0x0118} - _baseAddressTotal, from_kilo, add},
|
|
||||||
{"ReactiveEnergy", "Varh", "L1", std::uint16_t{0x011A} - _baseAddressTotal, from_kilo, add},
|
|
||||||
{"ReactiveEnergy", "Varh", "L2", std::uint16_t{0x011C} - _baseAddressTotal, from_kilo, add},
|
|
||||||
{"ReactiveEnergy", "Varh", "L3", std::uint16_t{0x011E} - _baseAddressTotal, from_kilo, add},
|
|
||||||
{"ForwardReactiveEnergy", "Varh", "Total", std::uint16_t{0x0120} - _baseAddressTotal, from_kilo, add},
|
|
||||||
{"ForwardReactiveEnergy", "Varh", "L1", std::uint16_t{0x0122} - _baseAddressTotal, from_kilo, add},
|
|
||||||
{"ForwardReactiveEnergy", "Varh", "L2", std::uint16_t{0x0124} - _baseAddressTotal, from_kilo, add},
|
|
||||||
{"ForwardReactiveEnergy", "Varh", "L3", std::uint16_t{0x0126} - _baseAddressTotal, from_kilo, add},
|
|
||||||
{"ReverseReactiveEnergy", "Varh", "Total", std::uint16_t{0x0128} - _baseAddressTotal, from_kilo, add},
|
|
||||||
{"ReverseReactiveEnergy", "Varh", "L1", std::uint16_t{0x012A} - _baseAddressTotal, from_kilo, add},
|
|
||||||
{"ReverseReactiveEnergy", "Varh", "L2", std::uint16_t{0x012C} - _baseAddressTotal, from_kilo, add},
|
|
||||||
{"ReverseReactiveEnergy", "Varh", "L3", std::uint16_t{0x012E} - _baseAddressTotal, from_kilo, add},
|
|
||||||
};
|
|
||||||
|
|
||||||
public:
|
|
||||||
EnergymeterTotalReading(
|
|
||||||
boost::asio::any_io_executor ex,
|
|
||||||
MqttClient& mqtt,
|
|
||||||
ModbusClient& modbus)
|
|
||||||
: EnergymeterReadings(
|
|
||||||
ex,
|
|
||||||
mqtt,
|
|
||||||
modbus,
|
|
||||||
std::span<const Register>(totalRegisters_, std::size(totalRegisters_)),
|
|
||||||
"total",
|
|
||||||
_baseAddressTotal)
|
|
||||||
{}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ───────────────────────────────────────────────────────────────────────────
|
|
||||||
// Serwis: dwa taski asynchroniczne (live + total) na timerach
|
|
||||||
// ───────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
class EnergymeterService {
|
|
||||||
public:
|
|
||||||
EnergymeterService(
|
|
||||||
boost::asio::any_io_executor ex,
|
|
||||||
MqttClient& mqtt,
|
|
||||||
ModbusClient& modbus)
|
|
||||||
: _ex(ex)
|
|
||||||
, _live(ex, mqtt, modbus)
|
|
||||||
, _total(ex, mqtt, modbus)
|
|
||||||
{}
|
|
||||||
|
|
||||||
// uruchamia oba taski; wołaj np. przez co_spawn(service.run(), detached)
|
|
||||||
awaitable<void> run()
|
|
||||||
{
|
|
||||||
using boost::asio::co_spawn;
|
|
||||||
using boost::asio::detached;
|
|
||||||
|
|
||||||
co_spawn(_ex,
|
|
||||||
[this]() -> awaitable<void> {
|
|
||||||
co_await live_loop();
|
|
||||||
},
|
},
|
||||||
detached);
|
boost::asio::detached);
|
||||||
|
|
||||||
co_spawn(_ex,
|
io.run();
|
||||||
[this]() -> awaitable<void> {
|
|
||||||
co_await total_loop();
|
|
||||||
},
|
|
||||||
detached);
|
|
||||||
|
|
||||||
co_return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
|
||||||
awaitable<void> live_loop()
|
|
||||||
{
|
|
||||||
using namespace std::chrono_literals;
|
|
||||||
boost::asio::steady_timer timer(_ex);
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
timer.expires_after(3s);
|
|
||||||
boost::system::error_code ec;
|
|
||||||
co_await timer.async_wait(boost::asio::redirect_error(boost::asio::use_awaitable, ec));
|
|
||||||
if (ec == boost::asio::error::operation_aborted)
|
|
||||||
co_return;
|
|
||||||
|
|
||||||
auto res = co_await _live.publish();
|
|
||||||
if (!res) {
|
|
||||||
spdlog::warn("Live reading publish error: {}", res.error().message());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
awaitable<void> total_loop()
|
|
||||||
{
|
|
||||||
using namespace std::chrono_literals;
|
|
||||||
boost::asio::steady_timer timer(_ex);
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
timer.expires_after(60s);
|
|
||||||
boost::system::error_code ec;
|
|
||||||
co_await timer.async_wait(boost::asio::redirect_error(boost::asio::use_awaitable, ec));
|
|
||||||
if (ec == boost::asio::error::operation_aborted)
|
|
||||||
co_return;
|
|
||||||
|
|
||||||
auto res = co_await _total.publish();
|
|
||||||
if (!res) {
|
|
||||||
spdlog::warn("Total reading publish error: {}", res.error().message());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
boost::asio::any_io_executor _ex;
|
|
||||||
EnergymeterLiveReading _live;
|
|
||||||
EnergymeterTotalReading _total;
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
add_executable(ranczo-io_floorheating
|
add_executable(ranczo-io_floorheating
|
||||||
main.cpp
|
main.cpp
|
||||||
temperature_controller.hpp temperature_controller.cpp
|
temperature_controller.hpp temperature_controller.cpp
|
||||||
|
temperature_measurements.hpp temperature_measurements.cpp
|
||||||
relay.hpp relay.cpp
|
relay.hpp relay.cpp
|
||||||
thermometer.hpp thermometer.cpp
|
thermometer.hpp thermometer.cpp
|
||||||
|
|
||||||
|
|||||||
@ -40,6 +40,20 @@ namespace ranczo {
|
|||||||
/// * Zapis danych w DB
|
/// * Zapis danych w DB
|
||||||
/// * Zapis ustawień
|
/// * Zapis ustawień
|
||||||
/// * Nasłuchiwanie na MQTT
|
/// * Nasłuchiwanie na MQTT
|
||||||
|
/// static constexpr std::array< std::tuple< int, quantity< Resistance >, Line >, 8 > _idToResistance{
|
||||||
|
// std::tuple{4, 38.9915503757583 * boost::units::si::ohm, Line::L1},
|
||||||
|
// std::tuple{5, 116.973061767177 * boost::units::si::ohm, Line::L1},
|
||||||
|
// std::tuple{25, 38.1843638931974 * boost::units::si::ohm, Line::L2}, // 2/9 playroom
|
||||||
|
// std::tuple{26, 49.9384951729712 * boost::units::si::ohm, Line::L3}, // 2/10 aska
|
||||||
|
// std::tuple{27, 50.8739911417796 * boost::units::si::ohm, Line::L3}, // 2/11 maciej
|
||||||
|
// std::tuple{28, 76.6462545974082 * boost::units::si::ohm, Line::L2}, // 2/12 office
|
||||||
|
// std::tuple{29, 94.2874894960184 * boost::units::si::ohm, Line::L1}, // 2/13 bathroom 3
|
||||||
|
// std::tuple{49, 38.9915503757583 * boost::units::si::ohm, Line::L1}};// 16/1 utility
|
||||||
|
// 16/11 bathroom_1
|
||||||
|
|
||||||
|
// TODO subscribe to home/utilities/power/electricity/main/active/[L1|L2|L3] and listen to energy usage, disable some mats when energy usage is too high
|
||||||
|
// TODO subscribe to home/utilities/powerline/electricity/main/voltage/L1 and listen to energy usage, disable some mats when energy usage is too high
|
||||||
|
// TODO Procedure to check the
|
||||||
|
|
||||||
} // namespace ranczo
|
} // namespace ranczo
|
||||||
|
|
||||||
@ -272,4 +286,3 @@ int main() {
|
|||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,6 @@
|
|||||||
#include <boost/asio/experimental/parallel_group.hpp>
|
#include <boost/asio/experimental/parallel_group.hpp>
|
||||||
#include <boost/asio/redirect_error.hpp>
|
#include <boost/asio/redirect_error.hpp>
|
||||||
#include <boost/asio/steady_timer.hpp>
|
#include <boost/asio/steady_timer.hpp>
|
||||||
#include <boost/circular_buffer.hpp>
|
|
||||||
#include <boost/core/noncopyable.hpp>
|
#include <boost/core/noncopyable.hpp>
|
||||||
#include <boost/json/object.hpp>
|
#include <boost/json/object.hpp>
|
||||||
#include <boost/json/storage_ptr.hpp>
|
#include <boost/json/storage_ptr.hpp>
|
||||||
@ -32,6 +31,9 @@
|
|||||||
#include <ranczo-io/utils/memory_resource.hpp>
|
#include <ranczo-io/utils/memory_resource.hpp>
|
||||||
#include <ranczo-io/utils/mqtt_client.hpp>
|
#include <ranczo-io/utils/mqtt_client.hpp>
|
||||||
#include <ranczo-io/utils/mqtt_topic_builder.hpp>
|
#include <ranczo-io/utils/mqtt_topic_builder.hpp>
|
||||||
|
#include <services/floorheat_svc/temperature_measurements.hpp>
|
||||||
|
|
||||||
|
#include <boost/units/systems/si/resistance.hpp>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
@ -41,10 +43,6 @@
|
|||||||
|
|
||||||
namespace ranczo {
|
namespace ranczo {
|
||||||
|
|
||||||
template<typename T>
|
|
||||||
std::optional<T> from_string(std::optional< std::string_view > state,
|
|
||||||
std::pmr::memory_resource * mr = std::pmr::get_default_resource());
|
|
||||||
|
|
||||||
enum class ThermostatState { Enabled, Disabled, Error };
|
enum class ThermostatState { Enabled, Disabled, Error };
|
||||||
std::string to_string(ThermostatState state) {
|
std::string to_string(ThermostatState state) {
|
||||||
switch(state) {
|
switch(state) {
|
||||||
@ -58,8 +56,7 @@ std::string to_string(ThermostatState state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
std::optional< ThermostatState > from_string(std::optional< std::string_view > state,
|
std::optional< ThermostatState > from_string(std::optional< std::string_view > state, std::pmr::memory_resource * mr) {
|
||||||
std::pmr::memory_resource * mr) {
|
|
||||||
BOOST_ASSERT(mr);
|
BOOST_ASSERT(mr);
|
||||||
|
|
||||||
if(not state) {
|
if(not state) {
|
||||||
@ -77,38 +74,6 @@ std::optional< ThermostatState > from_string(std::optional< std::string_view > s
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Trend { Fall, Const, Rise };
|
|
||||||
|
|
||||||
std::pmr::string to_string(Trend state) {
|
|
||||||
switch(state) {
|
|
||||||
case Trend::Fall:
|
|
||||||
return "Fall";
|
|
||||||
case Trend::Const:
|
|
||||||
return "Const";
|
|
||||||
default:
|
|
||||||
return "Rise";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
template < >
|
|
||||||
std::optional< Trend > from_string(std::optional< std::string_view > state,
|
|
||||||
std::pmr::memory_resource * mr) {
|
|
||||||
BOOST_ASSERT(mr);
|
|
||||||
|
|
||||||
if(not state) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
std::pmr::string s(state->begin(), state->end(), mr);
|
|
||||||
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return static_cast< char >(std::tolower(c)); });
|
|
||||||
|
|
||||||
if(s == "fall")
|
|
||||||
return Trend::Fall;
|
|
||||||
if(s == "const")
|
|
||||||
return Trend::Const;
|
|
||||||
if(s == "rise")
|
|
||||||
return Trend::Rise;
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
template < typename T >
|
template < typename T >
|
||||||
inline expected< T > readValue(const boost::json::value & jv, std::string_view key) {
|
inline expected< T > readValue(const boost::json::value & jv, std::string_view key) {
|
||||||
if(auto * obj = jv.if_object()) {
|
if(auto * obj = jv.if_object()) {
|
||||||
@ -247,173 +212,6 @@ namespace commands {
|
|||||||
};
|
};
|
||||||
} // namespace commands
|
} // namespace commands
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The ThermometerMeasurements class. It listens to thermometer async temperature update and stores those as a linear history
|
|
||||||
* @todo We can read the history of measurements from PG database that stores them,
|
|
||||||
* optional as we will not reload this service often so we can store our own measurements for couple of minutes
|
|
||||||
*/
|
|
||||||
struct ThermometerMeasurements {
|
|
||||||
struct Measurement {
|
|
||||||
std::chrono::system_clock::time_point when;
|
|
||||||
Thermometer::ThermometerData data;
|
|
||||||
};
|
|
||||||
executor & _io;
|
|
||||||
ModuleLogger _log;
|
|
||||||
std::unique_ptr< Thermometer > _sensor;
|
|
||||||
boost::circular_buffer< Measurement > _history;
|
|
||||||
|
|
||||||
ThermometerMeasurements(executor & io, std::unique_ptr< Thermometer > sensor)
|
|
||||||
: _io{io}, _log{spdlog::default_logger(), "ThermometerMeasurements"}, _sensor{std::move(sensor)}, _history{200} {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief start the service, we can't make it happen in ctor so we need a different approach
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
awaitable_expected< void > subscribeToTemperatureUpdate() {
|
|
||||||
return _sensor->on_update([&](auto data) { return this->temperatureUpdateCallback(data); });
|
|
||||||
}
|
|
||||||
|
|
||||||
awaitable_expected< void > temperatureUpdateCallback(Thermometer::ThermometerData data) {
|
|
||||||
// circular buffer, no need to clean
|
|
||||||
_log.debug("Got new sample: {} total: {}", data.temp_c(), _history.size());
|
|
||||||
/// TODO thermometer sometimes gives a temp like 80C which is a fluck, we need to filter this
|
|
||||||
_history.push_back({std::chrono::system_clock::now(), data});
|
|
||||||
co_return _void{};
|
|
||||||
}
|
|
||||||
|
|
||||||
awaitable_expected< void > start() {
|
|
||||||
BOOST_ASSERT(_sensor);
|
|
||||||
|
|
||||||
// subscribe to a thermometer readings
|
|
||||||
ASYNC_CHECK_LOG(subscribeToTemperatureUpdate(), "subscribtion to temperature stream failed");
|
|
||||||
|
|
||||||
// spins own mqtt listener 'thread' and executes on update callback on every temperature update
|
|
||||||
boost::asio::co_spawn(_io, _sensor->listen(), boost::asio::detached);
|
|
||||||
co_return _void{};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief timeSinceLastRead
|
|
||||||
* @return a time that passed since last temperature read or nullopt if no measurements are available
|
|
||||||
*/
|
|
||||||
std::optional< std::chrono::nanoseconds > timeSinceLastRead() const noexcept {
|
|
||||||
if(_history.size() == 0)
|
|
||||||
return std::nullopt;
|
|
||||||
|
|
||||||
auto timeDiff = std::chrono::system_clock::now() - _history.back().when;
|
|
||||||
|
|
||||||
if(timeDiff < std::chrono::nanoseconds{0}) {
|
|
||||||
memory_resource::MonotonicStackResource< 64 > mr;
|
|
||||||
auto now_ts = date::to_iso_timestamp(std::chrono::system_clock::now(), &mr);
|
|
||||||
auto meas_ts = date::to_iso_timestamp(_history.back().when, &mr);
|
|
||||||
_log.warn("measurements are from the future (measurement: {}, now: {})", meas_ts, now_ts);
|
|
||||||
}
|
|
||||||
|
|
||||||
return timeDiff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief currentTemperature
|
|
||||||
* @return temperature or nothing if no temp is yet available
|
|
||||||
*/
|
|
||||||
std::optional< double > currentTemperature() const noexcept {
|
|
||||||
if(auto last = timeSinceLastRead(); not last.has_value()) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
return _history.back().data.temp_c();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t size() const noexcept {
|
|
||||||
return _history.size();
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @brief temperatureTrend
|
|
||||||
* @param window which should be used to get the trend
|
|
||||||
* @param epsilon_deg_per_min, trend specyfier
|
|
||||||
* @return a trend if trent can be calculated or nullopt
|
|
||||||
*/
|
|
||||||
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()) {
|
|
||||||
_log.debug("No temperature samples available");
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(_history.size() < 2) {
|
|
||||||
_log.debug("Too few samples in the last {} minutes, no trend can be calculated",
|
|
||||||
std::chrono::duration_cast< std::chrono::duration< double, std::ratio< 60 > > >(window).count());
|
|
||||||
return Trend::Const;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto now = std::chrono::system_clock::now();
|
|
||||||
const auto window_start = now - window;
|
|
||||||
|
|
||||||
struct Point {
|
|
||||||
double t_s;
|
|
||||||
double temp;
|
|
||||||
std::chrono::system_clock::time_point when;
|
|
||||||
};
|
|
||||||
std::vector< Point > pts;
|
|
||||||
pts.reserve(_history.size());
|
|
||||||
|
|
||||||
for(const auto & m : _history) {
|
|
||||||
if(m.when < window_start || m.when > now)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
double t_s = std::chrono::duration< double >(m.when - window_start).count();
|
|
||||||
double y = m.data.temp_c();
|
|
||||||
pts.push_back({t_s, y, m.when});
|
|
||||||
}
|
|
||||||
|
|
||||||
if(pts.size() < 2) {
|
|
||||||
_log.debug("Too few samples in the last {} minutes, no trend can be calculated",
|
|
||||||
std::chrono::duration_cast< std::chrono::duration< double, std::ratio< 60 > > >(window).count());
|
|
||||||
return Trend::Const;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Posortuj czasowo (przyda się w fallbacku)
|
|
||||||
std::sort(pts.begin(), pts.end(), [](const Point & a, const Point & b) { return a.when < b.when; });
|
|
||||||
// Regresja liniowa y = a + b * t
|
|
||||||
double sum_t{};
|
|
||||||
double sum_y{};
|
|
||||||
double sum_tt{};
|
|
||||||
double sum_ty{};
|
|
||||||
for(const auto & p : pts) {
|
|
||||||
sum_t += p.t_s;
|
|
||||||
sum_y += p.temp;
|
|
||||||
sum_tt += p.t_s * p.t_s;
|
|
||||||
sum_ty += p.t_s * p.temp;
|
|
||||||
}
|
|
||||||
|
|
||||||
const double n = static_cast< double >(pts.size());
|
|
||||||
const double denom = (n * sum_tt - sum_t * sum_t);
|
|
||||||
|
|
||||||
double slope_deg_per_s = 0.0;
|
|
||||||
if(denom != 0.0) {
|
|
||||||
slope_deg_per_s = (n * sum_ty - sum_t * sum_y) / denom; // b
|
|
||||||
} else {
|
|
||||||
// fallback: pochylona między pierwszą i ostatnią próbką
|
|
||||||
const double dt_s = std::chrono::duration< double >(pts.back().when - pts.front().when).count();
|
|
||||||
if(dt_s <= 0.0) {
|
|
||||||
_log.warn("No time spread in samples (dt = {}).", dt_s);
|
|
||||||
return Trend::Const;
|
|
||||||
}
|
|
||||||
slope_deg_per_s = (pts.back().temp - pts.front().temp) / dt_s;
|
|
||||||
}
|
|
||||||
|
|
||||||
const double slope_deg_per_min = slope_deg_per_s * 60.0;
|
|
||||||
|
|
||||||
_log.debug(
|
|
||||||
"Trend (5 min): samples={}, slope={:.4f} °C/min, threshold={:.4f} °C/min", pts.size(), slope_deg_per_min, epsilon_deg_per_min);
|
|
||||||
|
|
||||||
if(slope_deg_per_min > epsilon_deg_per_min)
|
|
||||||
return Trend::Rise;
|
|
||||||
if(slope_deg_per_min < -epsilon_deg_per_min)
|
|
||||||
return Trend::Fall;
|
|
||||||
return Trend::Const;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
enum RuntimeError {
|
enum RuntimeError {
|
||||||
NoTemperatureMeasurements = 1,
|
NoTemperatureMeasurements = 1,
|
||||||
IoError,
|
IoError,
|
||||||
@ -509,6 +307,9 @@ struct RelayThermostat::Impl : private boost::noncopyable {
|
|||||||
double _slopeDT_c{0.2}; // [°C / min]
|
double _slopeDT_c{0.2}; // [°C / min]
|
||||||
std::chrono::nanoseconds _sensorTimeout{std::chrono::minutes(5)}; // max time to wait for temperature
|
std::chrono::nanoseconds _sensorTimeout{std::chrono::minutes(5)}; // max time to wait for temperature
|
||||||
|
|
||||||
|
/// TODO dodanie resistance
|
||||||
|
///
|
||||||
|
|
||||||
// additional variables
|
// additional variables
|
||||||
Timer _statusTimer; // sends status of current endpoint
|
Timer _statusTimer; // sends status of current endpoint
|
||||||
std::chrono::system_clock::time_point _startTP; // used only to prevent warnings at start of procedure
|
std::chrono::system_clock::time_point _startTP; // used only to prevent warnings at start of procedure
|
||||||
@ -997,8 +798,7 @@ struct RelayThermostat::Impl : private boost::noncopyable {
|
|||||||
|
|
||||||
auto toSec = [](auto t) { return seconds{t}.count(); };
|
auto toSec = [](auto t) { return seconds{t}.count(); };
|
||||||
|
|
||||||
_state = from_string<ThermostatState>(
|
_state = from_string< ThermostatState >(co_await _settings.async_get_store_default("state", to_string(ThermostatState::Disabled)))
|
||||||
co_await _settings.async_get_store_default("state", to_string(ThermostatState::Disabled)))
|
|
||||||
.value_or(ThermostatState::Disabled);
|
.value_or(ThermostatState::Disabled);
|
||||||
|
|
||||||
_targetTemperature = co_await _settings.async_get_store_default("target_temperature", 20.0);
|
_targetTemperature = co_await _settings.async_get_store_default("target_temperature", 20.0);
|
||||||
@ -1007,6 +807,7 @@ struct RelayThermostat::Impl : private boost::noncopyable {
|
|||||||
_slopeWindow = seconds{co_await _settings.async_get_store_default("slope_window_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]
|
_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}))};
|
_sensorTimeout = seconds{co_await _settings.async_get_store_default("sensor_timeout_s", toSec(minutes{5}))};
|
||||||
|
/// TODO sprawdzić czy inne parametry również trzeba/można odczytać
|
||||||
|
|
||||||
// subscribe to a thermostat commands feed
|
// subscribe to a thermostat commands feed
|
||||||
_log.info("Start: subscribe to mqtt");
|
_log.info("Start: subscribe to mqtt");
|
||||||
@ -1195,7 +996,7 @@ struct RelayThermostat::Impl : private boost::noncopyable {
|
|||||||
|
|
||||||
auto trend = _thermo.temperatureTrend(_slopeWindow, _slopeDT_c);
|
auto trend = _thermo.temperatureTrend(_slopeWindow, _slopeDT_c);
|
||||||
if(trend) {
|
if(trend) {
|
||||||
obj["current_trend"] = to_string(*trend);
|
obj["temperature_trend"] = to_string(*trend);
|
||||||
}
|
}
|
||||||
obj["measurements_size"] = _thermo.size();
|
obj["measurements_size"] = _thermo.size();
|
||||||
obj["hysteresis"] = _hysteresis;
|
obj["hysteresis"] = _hysteresis;
|
||||||
|
|||||||
133
services/floorheat_svc/temperature_measurements.cpp
Normal file
133
services/floorheat_svc/temperature_measurements.cpp
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
#include "temperature_measurements.hpp"
|
||||||
|
#include "ranczo-io/utils/date_utils.hpp"
|
||||||
|
#include <boost/asio/co_spawn.hpp>
|
||||||
|
#include <boost/asio/detached.hpp>
|
||||||
|
|
||||||
|
namespace ranczo {
|
||||||
|
|
||||||
|
ThermometerMeasurements::ThermometerMeasurements(executor & io, std::unique_ptr< Thermometer > sensor)
|
||||||
|
: _io{io}, _log{spdlog::default_logger(), "ThermometerMeasurements"}, _sensor{std::move(sensor)}, _history{200} {}
|
||||||
|
|
||||||
|
awaitable_expected< void > ThermometerMeasurements::temperatureUpdateCallback(Thermometer::ThermometerData data) {
|
||||||
|
// circular buffer, no need to clean
|
||||||
|
_log.debug("Got new sample: {} total: {}", data.temp_c(), _history.size());
|
||||||
|
/// TODO thermometer sometimes gives a temp like 80C which is a fluck, we need to filter this
|
||||||
|
_history.push_back({std::chrono::system_clock::now(), data});
|
||||||
|
co_return _void{};
|
||||||
|
}
|
||||||
|
|
||||||
|
awaitable_expected< void > ThermometerMeasurements::start() {
|
||||||
|
BOOST_ASSERT(_sensor);
|
||||||
|
|
||||||
|
// subscribe to a thermometer readings
|
||||||
|
ASYNC_CHECK_LOG(subscribeToTemperatureUpdate(), "subscribtion to temperature stream failed");
|
||||||
|
|
||||||
|
// spins own mqtt listener 'thread' and executes on update callback on every temperature update
|
||||||
|
boost::asio::co_spawn(_io, _sensor->listen(), boost::asio::detached);
|
||||||
|
co_return _void{};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional< Trend > ThermometerMeasurements::temperatureTrend(std::chrono::nanoseconds window, double epsilon_deg_per_min) const {
|
||||||
|
if(auto last = timeSinceLastRead(); not last.has_value()) {
|
||||||
|
_log.debug("No temperature samples available");
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(_history.size() < 2) {
|
||||||
|
_log.debug("Too few samples in the last {} minutes, no trend can be calculated",
|
||||||
|
std::chrono::duration_cast< std::chrono::duration< double, std::ratio< 60 > > >(window).count());
|
||||||
|
return Trend::Const;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto now = std::chrono::system_clock::now();
|
||||||
|
const auto window_start = now - window;
|
||||||
|
|
||||||
|
struct Point {
|
||||||
|
double t_s;
|
||||||
|
double temp;
|
||||||
|
std::chrono::system_clock::time_point when;
|
||||||
|
};
|
||||||
|
std::vector< Point > pts; // pmr vector
|
||||||
|
pts.reserve(_history.size());
|
||||||
|
|
||||||
|
for(const auto & m : _history) {
|
||||||
|
if(m.when < window_start || m.when > now)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
double t_s = std::chrono::duration< double >(m.when - window_start).count();
|
||||||
|
double y = m.data.temp_c();
|
||||||
|
pts.push_back({t_s, y, m.when});
|
||||||
|
}
|
||||||
|
|
||||||
|
if(pts.size() < 2) {
|
||||||
|
_log.debug("Too few samples in the last {} minutes, no trend can be calculated",
|
||||||
|
std::chrono::duration_cast< std::chrono::duration< double, std::ratio< 60 > > >(window).count());
|
||||||
|
return Trend::Const;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Posortuj czasowo (przyda się w fallbacku)
|
||||||
|
std::sort(pts.begin(), pts.end(), [](const Point & a, const Point & b) { return a.when < b.when; });
|
||||||
|
// Regresja liniowa y = a + b * t
|
||||||
|
double sum_t{};
|
||||||
|
double sum_y{};
|
||||||
|
double sum_tt{};
|
||||||
|
double sum_ty{};
|
||||||
|
for(const auto & p : pts) {
|
||||||
|
sum_t += p.t_s;
|
||||||
|
sum_y += p.temp;
|
||||||
|
sum_tt += p.t_s * p.t_s;
|
||||||
|
sum_ty += p.t_s * p.temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double n = static_cast< double >(pts.size());
|
||||||
|
const double denom = (n * sum_tt - sum_t * sum_t);
|
||||||
|
|
||||||
|
double slope_deg_per_s = 0.0;
|
||||||
|
if(denom != 0.0) {
|
||||||
|
slope_deg_per_s = (n * sum_ty - sum_t * sum_y) / denom; // b
|
||||||
|
} else {
|
||||||
|
// fallback: pochylona między pierwszą i ostatnią próbką
|
||||||
|
const double dt_s = std::chrono::duration< double >(pts.back().when - pts.front().when).count();
|
||||||
|
if(dt_s <= 0.0) {
|
||||||
|
_log.warn("No time spread in samples (dt = {}).", dt_s);
|
||||||
|
return Trend::Const;
|
||||||
|
}
|
||||||
|
slope_deg_per_s = (pts.back().temp - pts.front().temp) / dt_s;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double slope_deg_per_min = slope_deg_per_s * 60.0;
|
||||||
|
|
||||||
|
_log.debug(
|
||||||
|
"Trend (5 min): samples={}, slope={:.4f} °C/min, threshold={:.4f} °C/min", pts.size(), slope_deg_per_min, epsilon_deg_per_min);
|
||||||
|
|
||||||
|
if(slope_deg_per_min > epsilon_deg_per_min)
|
||||||
|
return Trend::Rise;
|
||||||
|
if(slope_deg_per_min < -epsilon_deg_per_min)
|
||||||
|
return Trend::Fall;
|
||||||
|
return Trend::Const;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional< double > ThermometerMeasurements::currentTemperature() const {
|
||||||
|
if(auto last = timeSinceLastRead(); not last.has_value()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return _history.back().data.temp_c();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional< std::chrono::nanoseconds > ThermometerMeasurements::timeSinceLastRead() const {
|
||||||
|
if(_history.size() == 0)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
auto timeDiff = std::chrono::system_clock::now() - _history.back().when;
|
||||||
|
|
||||||
|
if(timeDiff < std::chrono::nanoseconds{0}) {
|
||||||
|
memory_resource::MonotonicStackResource< 64 > mr;
|
||||||
|
auto now_ts = date::to_iso_timestamp(std::chrono::system_clock::now(), &mr);
|
||||||
|
auto meas_ts = date::to_iso_timestamp(_history.back().when, &mr);
|
||||||
|
_log.warn("measurements are from the future (measurement: {}, now: {})", meas_ts, now_ts);
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeDiff;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ranczo
|
||||||
106
services/floorheat_svc/temperature_measurements.hpp
Normal file
106
services/floorheat_svc/temperature_measurements.hpp
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "config.hpp"
|
||||||
|
#include "thermometer.hpp"
|
||||||
|
|
||||||
|
#include <boost/circular_buffer.hpp>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace ranczo {
|
||||||
|
class Thermometer;
|
||||||
|
|
||||||
|
enum class Trend { Fall, Const, Rise };
|
||||||
|
|
||||||
|
inline std::pmr::string to_string(Trend state) {
|
||||||
|
switch(state) {
|
||||||
|
case Trend::Fall:
|
||||||
|
return "Fall";
|
||||||
|
case Trend::Const:
|
||||||
|
return "Const";
|
||||||
|
default:
|
||||||
|
return "Rise";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
template <>
|
||||||
|
inline std::optional< Trend > from_string(std::optional< std::string_view > state, std::pmr::memory_resource * mr) {
|
||||||
|
BOOST_ASSERT(mr);
|
||||||
|
|
||||||
|
if(not state) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
std::pmr::string s(state->begin(), state->end(), mr);
|
||||||
|
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return static_cast< char >(std::tolower(c)); });
|
||||||
|
|
||||||
|
if(s == "fall")
|
||||||
|
return Trend::Fall;
|
||||||
|
if(s == "const")
|
||||||
|
return Trend::Const;
|
||||||
|
if(s == "rise")
|
||||||
|
return Trend::Rise;
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The ThermometerMeasurements class. It listens to thermometer async temperature update and stores those as a linear history
|
||||||
|
* @todo We can read the history of measurements from PG database that stores them,
|
||||||
|
* optional as we will not reload this service often so we can store our own measurements for couple of minutes
|
||||||
|
*/
|
||||||
|
struct ThermometerMeasurements {
|
||||||
|
struct Measurement {
|
||||||
|
std::chrono::system_clock::time_point when;
|
||||||
|
Thermometer::ThermometerData data;
|
||||||
|
};
|
||||||
|
executor & _io;
|
||||||
|
ModuleLogger _log;
|
||||||
|
std::unique_ptr< Thermometer > _sensor;
|
||||||
|
boost::circular_buffer< Measurement > _history;
|
||||||
|
|
||||||
|
ThermometerMeasurements(executor & io, std::unique_ptr< Thermometer > sensor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief start the service, we can't make it happen in ctor so we need a different approach
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
awaitable_expected< void > subscribeToTemperatureUpdate() {
|
||||||
|
return _sensor->on_update([&](auto data) { return this->temperatureUpdateCallback(data); });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief temperatureUpdateCallback
|
||||||
|
* @param data
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
awaitable_expected< void > temperatureUpdateCallback(Thermometer::ThermometerData data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief start
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
awaitable_expected< void > start();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief timeSinceLastRead
|
||||||
|
* @return a time that passed since last temperature read or nullopt if no measurements are available
|
||||||
|
*/
|
||||||
|
std::optional< std::chrono::nanoseconds > timeSinceLastRead() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief currentTemperature
|
||||||
|
* @return temperature or nothing if no temp is yet available
|
||||||
|
*/
|
||||||
|
std::optional< double > currentTemperature() const;
|
||||||
|
|
||||||
|
std::size_t size() const noexcept {
|
||||||
|
return _history.size();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @brief temperatureTrend
|
||||||
|
* @param window which should be used to get the trend
|
||||||
|
* @param epsilon_deg_per_min, trend specyfier
|
||||||
|
* @return a trend if trent can be calculated or nullopt
|
||||||
|
*/
|
||||||
|
std::optional< Trend > temperatureTrend(std::chrono::nanoseconds window = std::chrono::minutes(5),
|
||||||
|
double epsilon_deg_per_min = 0.2) const;
|
||||||
|
};
|
||||||
|
} // namespace ranczo
|
||||||
Loading…
Reference in New Issue
Block a user