Add check for format library

This commit is contained in:
Bartosz Wieczorek 2025-11-27 13:40:45 +01:00
parent 31e6e73526
commit b4bbdb3ed3
8 changed files with 200 additions and 231 deletions

View File

@ -30,7 +30,24 @@ set(BOOST_ROOT /usr/local/boost-1.89)
find_package(Boost 1.89 REQUIRED COMPONENTS json mqtt5)
# spdlog
set(SPDLOG_USE_STD_FORMAT ON)
include(CheckCXXSourceCompiles)
set(CHECK_STD_FORMAT_SRC "
#include <format>
int main() { std::string s = std::format(\"{} {}\", 1, 2); return 0; } "
)
check_cxx_source_compiles("${CHECK_STD_FORMAT_SRC}" HAS_STD_FORMAT)
if (HAS_STD_FORMAT)
message(STATUS "Compiler supports <format> → SPDLOG_USE_STD_FORMAT=ON")
set(SPDLOG_USE_STD_FORMAT ON CACHE BOOL "" FORCE)
else()
message(STATUS "Compiler does NOT support <format> → SPDLOG_USE_STD_FORMAT=OFF")
set(SPDLOG_USE_STD_FORMAT OFF CACHE BOOL "" FORCE)
endif()
FetchContent_Declare(
spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog

View File

@ -1,3 +1,4 @@
#include <functional>
#include <optional>
#include <ranczo-io/utils/mqtt_client.hpp>
@ -304,15 +305,15 @@ struct AsyncMqttClient::AsyncMqttClientImpl {
if(responseTopic) {
_log.trace("got response topic, handling it properly");
// --- PMR dla response ---
std::optional< boost::json::value > response;
// --- PMR dla response ---
ResponseData response;
std::array< std::uint8_t, 2048 > responseBuffer{0};
std::pmr::monotonic_buffer_resource mr_resp{responseBuffer.data(), responseBuffer.size()};
json::pmr_memory_resource_adapter adapter_resp(&mr_resp);
boost::json::storage_ptr sp_resp(&adapter_resp);
response.emplace(boost::json::object_kind, sp_resp);
boost::json::value r(sp_resp);
response = std::ref(r); // make a reference wrapper object
// wywołanie callbacka z możliwością wypełnienia response
if(auto result = co_await handler(cbdata, response); !result) {
_log.warn("callback error: {}", result.error().message());
@ -328,7 +329,7 @@ struct AsyncMqttClient::AsyncMqttClientImpl {
}
}else{
// wywołanie callbacka z pustym response
std::optional< boost::json::value > response;
ResponseData response;
if(auto result = co_await handler(cbdata, response); !result) {
_log.warn("callback error: {}", result.error().message());
}

View File

@ -1,5 +1,6 @@
#pragma once
#include <functional>
#include <ranczo-io/utils/logger.hpp>
#include <boost/json/value.hpp>
@ -31,8 +32,8 @@ class AsyncMqttClient {
};
struct SubscribtionToken;
using ResponseData = std::optional< boost::json::value >;
using ResponseData = std::optional< std::reference_wrapper< boost::json::value > >;
using callback_t = std::function< awaitable_expected< void >(const CallbackData & value, ResponseData & response) >;
struct AsyncMqttClientImpl;

View File

@ -1,35 +1,56 @@
#pragma once
#include <boost/assert.hpp>
#include <array>
#include <boost/smart_ptr/shared_ptr.hpp>
#include <memory_resource>
#include <string>
#include <string_view>
/*
"routing": {
"topics": {
// Full topic: home/<room>/<type>/<place>/<number>/<action>
"light_set": "home/{room}/output/light/{N}/set",
"light_status": "home/{room}/output/light/{N}/status",
"floor_set": "home/{room}/heating/floor/{N}/set",
"floor_status": "home/{room}/heating/floor/{N}/status",
"floor_temp": "home/{room}/sensor/floor/{N}/temperature",
"air_temp": "home/{room}/sensor/air/{N}/temperature"
}
*/
/**
* Topic layout assumptions
*
* General pattern:
* home/<room>/<type>/<place>/<N>/<action>[/command|/state]
*
* - Urządzenia sterowane (heating, relay, thermostat, etc.):
* - <action>/state aktualny stan publikowany przez urządzenie
* - <action>/command polecenia z zewnątrz (HA, Node-RED, itp.)
*
* - Sensory tylko-do-odczytu (temperature, humidity, etc.):
* - home/<room>/sensor/<place>/<N>/temperature
* publikowane wyłącznie przez sensor, brak /command.
*
* Przykłady:
*
* 1) Termostat zadana temperatura:
* home/office/heating/floor/1/setpoint/state
* termostat publikuje aktualny setpoint
*
* home/office/heating/floor/1/setpoint/command
* dowolny byt (HA, Node-RED) może ustawić nowy setpoint
*
* 2) Sensor temperatury podłogi:
* home/office/sensor/floor/2/temperature
* sensor publikuje bieżącą temperaturę
* nie istnieje topic .../temperature/command
*
* Uwaga:
* - Topic .../<action>/state dostępny jedynie dla urządzeń które moge być sterowane
* Jest wyłącznie kanałem, na którym urządzenie publikuje swój stan.
*/
namespace ranczo {
template < typename... Args >
constexpr size_t length(const Args &... args) {
return (args.size() + ...);
}
static_assert(length(std::string_view{"asdf"}, std::string_view{"asdfgh"}) == 10);
// Główna funkcja 'join'
template < typename... Args >
std::string make_topic(const Args &... args) {
std::pmr::string make_topic(std::pmr::memory_resource & mr, const Args &... args) {
using namespace std::string_literals;
constexpr size_t count = sizeof...(Args);
if constexpr(count == 0) {
@ -41,182 +62,85 @@ std::string make_topic(const Args &... args) {
std::array< std::string_view, count > segments{std::string_view(args)...};
size_t total_size = length(args...) + segments.size();
std::string result;
result.reserve(total_size);
result.append(segments[0]);
for(size_t i = 1; i < count; ++i) {
result.append(separator);
result.append(segments[i]);
}
return result;
}
template < typename... Args >
std::pmr::string make_topic(std::pmr::memory_resource &mr, const Args &... args) {
using namespace std::string_literals;
constexpr size_t count = sizeof...(Args);
if constexpr(count == 0) {
return "";
}
auto separator = R"(/)"s;
std::array< std::string_view, count > segments{std::string_view(args)...};
size_t total_size = length(args...) + segments.size();
std::pmr::string result{&mr};
result.reserve(total_size);
result.append(segments[0]);
for(size_t i = 1; i < count; ++i) {
result.append(separator);
result.append(segments[i]);
}
return result;
}
// Full topic: home/<room>/<type>/<place>/{N}/<action>
inline std::string buildTopic(std::string_view room, std::string_view type, std::string_view place, uint number, std::string_view action) {
using namespace std::string_view_literals;
return make_topic("home"sv, room, type, place, std::to_string(number), action);
}
inline std::pmr::string buildTopic(std::pmr::memory_resource &mr, std::string_view room, std::string_view type, std::string_view place, uint number, std::string_view action) {
inline std::pmr::string buildPublishTopic(std::pmr::memory_resource & mr,
std::string_view room,
std::string_view type,
std::string_view place,
uint number,
std::string_view action) {
using namespace std::string_view_literals;
return make_topic(mr, "home"sv, room, type, place, std::to_string(number), action);
}
enum CommandType { state, command };
// Full topic: home/<room>/<type>/<place>/{N}/<action>/[state|command]
inline std::pmr::string buildCommandTopic(std::pmr::memory_resource & mr,
std::string_view room,
std::string_view type,
std::string_view place,
uint number,
std::string_view action,
CommandType commandType) {
using namespace std::string_view_literals;
if(commandType == CommandType::state)
return make_topic(mr, "home"sv, room, type, place, std::to_string(number), action, "state"sv);
return make_topic(mr, "home"sv, room, type, place, std::to_string(number), action, "command"sv);
}
namespace topic {
namespace heating {
// Topic:
// home/<room>/heating/floor/<N>/command
//
// ➤ Purpose:
// Accepts commands to set target floor temperature or heating mode.
// Payload: JSON with fields like {"target": 23.0, "mode": "manual"}
//
// ➤ Direction: SUBSCRIBE
// ➤ Request/Response: YES (ideal for MQTT v5 request/response pattern)
//
// ➤ Example:
// mqttClient.publish("home/bathroom/heating/floor/<N>/command", payload, {
// response_topic: "client/myid/response",
// correlation_data: "uuid-123"
// });
inline std::string subscribeToCommand(std::string_view room, int zone, std::string_view command) {
// home/<room>/heating/floor/<N>/<command>/[command|status]
inline std::pmr::string subscribeToControl(std::string_view room,
int zone,
std::string_view command,
std::pmr::memory_resource * mr = std::pmr::get_default_resource()) {
using namespace std::string_view_literals;
return buildTopic(room, "heating"sv, "floor"sv, zone, command);
BOOST_ASSERT(mr);
return buildCommandTopic(*mr, room, "heating"sv, "floor"sv, zone, command, CommandType::command);
}
// Topic:
// home/<room>/heating/floor/<N>/state
//
// ➤ Purpose:
// Publishes the current heating state periodically (every 60s or on change).
// Includes fields like:
// {
// "current": 21.4,
// "target": 23.0,
// "heating": true,
// "mode": "manual",
// "timestamp": "2025-08-06T12:34:56Z"
// }
//
// ➤ Direction: PUBLISH
// ➤ Retained: YES
// ➤ Request/Response: NOT used — this is a one-way state feed.
inline std::string state(std::string_view room, int zone =1) {
inline std::pmr::string
publishState(std::string_view room, int zone = 1, std::pmr::memory_resource * mr = std::pmr::get_default_resource()) {
using namespace std::string_view_literals;
return buildTopic( room, "heating"sv, "floor"sv, zone,"state"sv);
}
inline std::pmr::string state(std::pmr::memory_resource&mr, std::string_view room, int zone =1) {
using namespace std::string_view_literals;
return buildTopic(mr, room, "heating"sv, "floor"sv, zone, "state"sv);
}
// Topic:
// home/<room>/heating/floor/<N>/config
//
// ➤ Purpose:
// Publishes discovery metadata for Home Assistant or external tooling.
// JSON payload compatible with Home Assistant MQTT discovery:
// {
// "temperature_state_topic": "...",
// "temperature_command_topic": "...",
// "unique_id": "...",
// ...
// }
//
// ➤ Direction: PUBLISH
// ➤ Retained: YES
// ➤ Request/Response: NO — usually published once at startup.
//
// ➤ Tip:
// Consider sending this automatically on boot or after config change.
inline std::string config(std::string_view room, int zone =1) {
using namespace std::string_view_literals;
return buildTopic(room, "heating"sv, "floor"sv, zone, "config"sv);
BOOST_ASSERT(mr);
return buildPublishTopic(*mr, room, "heating"sv, "floor"sv, zone, "state"sv);
}
} // namespace heating
namespace temperature {
// Topic:
// home/<room>/sensor/floor/<N>/temperature
//
// ➤ Purpose:
// Publishes the current floor temperature measured by a sensor.
// Payload example:
// {
// "value": 22.4,
// "unit": "°C",
// "timestamp": "2025-08-06T14:35:00Z"
// }
//
// ➤ Direction: PUBLISH
// ➤ Retained: Optional (depending on freshness expectations)
// ➤ Request/Response: NO — this is state-only (event or periodic)
//
// ➤ Tip:
// Can be consumed by heating services, dashboards, or Home Assistant
inline std::string floor(std::string_view room, int zone) {
BOOST_ASSERT(zone>0);// no broadcast
inline std::pmr::string
publishFloor(std::string_view room, int zone, std::pmr::memory_resource * mr = std::pmr::get_default_resource()) {
BOOST_ASSERT(zone > 0); // no broadcast
BOOST_ASSERT(mr);
using namespace std::string_view_literals;
return buildTopic(room, "sensor"sv, "floor"sv, zone, "temperature"sv);
}
inline std::pmr::string floor(std::pmr::memory_resource&mr, std::string_view room, int zone) {
BOOST_ASSERT(zone>0);// no broadcast
using namespace std::string_view_literals;
return buildTopic(mr, room, "sensor"sv, "floor"sv, zone, "temperature"sv);
return buildPublishTopic(*mr, room, "sensor"sv, "floor"sv, zone, "temperature"sv);
}
// Topic:
// home/<room>/sensor/air/<N>/temperature
//
// ➤ Purpose:
// Publishes the current air temperature in the room.
// Payload format is the same as for floor temperature:
// {
// "value": 23.1,
// "unit": "°C",
// "timestamp": "2025-08-06T14:35:10Z"
// }
//
// ➤ Direction: PUBLISH
// ➤ Retained: Optional (usually not unless used for dashboards)
// ➤ Request/Response: NO — meant for passive state reporting
inline std::string air(std::string_view room, int zone) {
BOOST_ASSERT(zone>0);// no broadcast
inline std::pmr::string
publishAir(std::string_view room, int zone, std::pmr::memory_resource * mr = std::pmr::get_default_resource()) {
BOOST_ASSERT(zone > 0); // no broadcast
BOOST_ASSERT(mr);
using namespace std::string_view_literals;
return buildTopic(room, "sensor"sv, "air"sv,zone, "temperature"sv);
return buildPublishTopic(*mr, room, "sensor"sv, "air"sv, zone, "temperature"sv);
}
inline std::pmr::string air(std::pmr::memory_resource&mr, std::string_view room, int zone) {
BOOST_ASSERT(zone>0);// no broadcast
using namespace std::string_view_literals;
return buildTopic(mr, room, "sensor"sv, "air"sv, zone, "temperature"sv);
}
} // namespace temperature
} // namespace topic

View File

@ -103,7 +103,7 @@ inline awaitable_expected< void > forward_floor_temperature_all_rooms(AsyncMqttC
if(remap.contains(room))
room = remap.at(room);
std::string dst = topic::temperature::floor(room, zone);
auto dst = topic::temperature::publishFloor(room, zone);
spdlog::info("Republishing temperature from: {} to: {}", data.topic, dst);
ASYNC_CHECK(mqtt.publish(dst, data.request));
co_return _void{};

View File

@ -22,6 +22,8 @@
#include <boost/system/result.hpp>
#include <chrono>
#include <memory_resource>
#include <optional>
#include <ranczo-io/utils/asio_watchdog.hpp>
#include <ranczo-io/utils/config.hpp>
#include <ranczo-io/utils/date_utils.hpp>
@ -31,7 +33,6 @@
#include <ranczo-io/utils/mqtt_client.hpp>
#include <ranczo-io/utils/mqtt_topic_builder.hpp>
#include <stdexcept>
#include <string>
#include <string_view>
#include <type_traits>
@ -40,37 +41,12 @@
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 Trend { Fall, Const, Rise };
std::pmr::string Trend_to_string(Trend state) {
switch(state) {
case Trend::Fall:
return "Fall";
case Trend::Const:
return "Const";
default:
return "Rise";
}
}
std::optional< ThermostatState > ThermostatState_from_string(std::optional< std::string > state) {
if(not state) {
return std::nullopt;
}
std::string s(state->begin(), state->end());
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return static_cast< char >(std::tolower(c)); });
if(s == "enabled")
return ThermostatState::Enabled;
if(s == "disabled")
return ThermostatState::Disabled;
if(s == "error")
return ThermostatState::Error;
return std::nullopt;
}
std::string ThermostatState_to_string(ThermostatState state) {
std::string to_string(ThermostatState state) {
switch(state) {
case ThermostatState::Enabled:
return "Enabled";
@ -81,12 +57,58 @@ std::string ThermostatState_to_string(ThermostatState state) {
}
}
/**
* @brief readValue
* @param jv
* @param key
* @return
*/
template<>
std::optional< ThermostatState > 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 == "enabled")
return ThermostatState::Enabled;
if(s == "disabled")
return ThermostatState::Disabled;
if(s == "error")
return ThermostatState::Error;
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 >
inline expected< T > readValue(const boost::json::value & jv, std::string_view key) {
if(auto * obj = jv.if_object()) {
@ -99,7 +121,7 @@ inline expected< T > readValue(const boost::json::value & jv, std::string_view k
if(!pv->is_string()) {
return unexpected{make_error_code(boost::system::errc::invalid_argument)};
}
auto v = ThermostatState_from_string(std::make_optional< std::string >(pv->as_string()));
auto v = from_string<T>(std::make_optional< std::string >(pv->as_string()));
if(not v)
return unexpected{make_error_code(boost::system::errc::invalid_argument)};
return *v;
@ -975,8 +997,8 @@ struct RelayThermostat::Impl : private boost::noncopyable {
auto toSec = [](auto t) { return seconds{t}.count(); };
_state = ThermostatState_from_string(
co_await _settings.async_get_store_default("state", ThermostatState_to_string(ThermostatState::Disabled)))
_state = from_string<ThermostatState>(
co_await _settings.async_get_store_default("state", to_string(ThermostatState::Disabled)))
.value_or(ThermostatState::Disabled);
_targetTemperature = co_await _settings.async_get_store_default("target_temperature", 20.0);
@ -988,7 +1010,7 @@ struct RelayThermostat::Impl : private boost::noncopyable {
// subscribe to a thermostat commands feed
_log.info("Start: subscribe to mqtt");
ASYNC_CHECK_LOG(subscribeToAllCommands(), "subscribe to command stream failed"); // todo pass logger??
ASYNC_CHECK_LOG(subscribeToAllCommands(), "subscribe to command stream failed");
// detaching listening thread
_log.info("Start: listen");
@ -1027,24 +1049,28 @@ struct RelayThermostat::Impl : private boost::noncopyable {
template < typename Command >
awaitable_expected< void > subscribeCommand() {
// broadcast: zone 0
const auto broadcast_topic = topic::heating::subscribeToCommand(_room, 0, Command::topic_suffix);
const auto broadcast_topic = topic::heating::subscribeToControl(_room, 0, Command::topic_suffix);
// konkretna strefa:
const auto zone_topic = topic::heating::subscribeToCommand(_room, _zone, Command::topic_suffix);
const auto zone_topic = topic::heating::subscribeToControl(_room, _zone, Command::topic_suffix) ;
auto cb = [this](const AsyncMqttClient::CallbackData & data, AsyncMqttClient::ResponseData & resp) -> awaitable_expected< void > {
auto _result = Command::from_payload(data.request);
if(!_result)
co_return unexpected{_result.error()};
co_return unexpected{_result.error()}; //
auto cmd = *_result;
/// 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) {
_log.trace("command {} request has a response topic, write response", Command::topic_suffix);
(*resp) = boost::json::object{{"status", status ? "ok" : "nok"}, {"details", "heater updated"}};
auto expected = co_await handle_command(cmd);
if(expected) {
if(resp.has_value()) {
_log.trace("command {} request has a response topic, write response", Command::topic_suffix);
(*resp).get() = boost::json::object{{"status", *expected ? "ok" : "nok"}, {"details", "heater updated"}};
}
} else {
_log.error("command {} has failed", Command::topic_suffix);
if(resp.has_value()) {
_log.trace("command {} request has a response topic, write response", Command::topic_suffix);
(*resp).get() = boost::json::object{{"status", "nok"}, {"details", expected.error().what()}};
}
}
co_return _void{};
@ -1070,7 +1096,7 @@ struct RelayThermostat::Impl : private boost::noncopyable {
_log.info("Heater target temperature update {}", _targetTemperature);
_targetTemperature = cmd.setpoint_c;
co_await update_config("target_temperature", _targetTemperature);
co_return true;
co_return ASYNC_TRY(publish_status());
}
awaitable_expected< bool > handle_command(const commands::StateChange & cmd) {
@ -1083,8 +1109,8 @@ struct RelayThermostat::Impl : private boost::noncopyable {
}
_state = cmd.state;
co_return true;
co_await update_config("state", to_string(_state));
co_return ASYNC_TRY(publish_status());
}
awaitable_expected< bool > handle_command(const commands::HisteresisChange & cmd) {
@ -1092,28 +1118,28 @@ struct RelayThermostat::Impl : private boost::noncopyable {
_hysteresis = cmd.histeresis;
/// TODO check if histeresis has ok value
co_await update_config("hysteresis", _hysteresis);
co_return true;
co_return ASYNC_TRY(publish_status());
}
awaitable_expected< bool > handle_command(const commands::TickTimeChange & cmd) {
_log.info("Heater tick time update {}ns", cmd.tickTime.count());
_tickTime = cmd.tickTime;
co_await update_config("tick_time_s", std::chrono::duration_cast< std::chrono::seconds >(_tickTime).count());
co_return true;
co_return ASYNC_TRY(publish_status());
}
awaitable_expected< bool > handle_command(const commands::SlopeWindowChange & cmd) {
_log.info("Heater slope window update {}ns", cmd.window.count());
_slopeWindow = cmd.window;
co_await update_config("slope_window_s", std::chrono::duration_cast< std::chrono::seconds >(_slopeWindow).count());
co_return true;
co_return ASYNC_TRY(publish_status());
}
awaitable_expected< bool > handle_command(const commands::SlopeTemperatureDiffChange & cmd) {
_log.info("Heater slope temperature update {}C", cmd.dT_c);
_slopeDT_c = cmd.dT_c;
co_await update_config("slope_delta_t", _slopeDT_c);
co_return true;
co_return ASYNC_TRY(publish_status());
}
awaitable_expected< bool > handle_command(const commands::StatusRequest & cmd) {
@ -1169,7 +1195,7 @@ struct RelayThermostat::Impl : private boost::noncopyable {
auto trend = _thermo.temperatureTrend(_slopeWindow, _slopeDT_c);
if(trend) {
obj["current_trend"] = Trend_to_string(*trend);
obj["current_trend"] = to_string(*trend);
}
obj["measurements_size"] = _thermo.size();
obj["hysteresis"] = _hysteresis;
@ -1181,7 +1207,7 @@ struct RelayThermostat::Impl : private boost::noncopyable {
obj["slope_window_s"] = duration_cast< seconds_d >(_slopeWindow).count();
obj["sensor_timeout_s"] = duration_cast< seconds_d >(_sensorTimeout).count();
auto topic = topic::heating::state(mr, _room, _zone);
auto topic = topic::heating::publishState(_room, _zone, &mr);
ASYNC_CHECK(_mqtt.publish(topic, obj));
co_return true;
}

View File

@ -18,7 +18,7 @@ awaitable_expected< void > MqttThermometer::on_update(async_cb handler) noexcept
_log.trace("on_update procedure start");
handler_ = std::move(handler);
const auto topic =
cfg_.type == "floor" ? topic::temperature::floor(cfg_.room, cfg_.zone) : topic::temperature::air(cfg_.room, cfg_.zone);
cfg_.type == "floor" ? topic::temperature::publishFloor(cfg_.room, cfg_.zone) : topic::temperature::publishAir(cfg_.room, cfg_.zone);
_log.trace("on_update procedure subscribe on topic: {}", topic);
_subscribtionToken = ASYNC_TRY(mqtt_.subscribe(topic, [this](const AsyncMqttClient::CallbackData & data, AsyncMqttClient::ResponseData &resp ) -> awaitable_expected< void > {

View File

@ -49,7 +49,7 @@ MqttMeasurementPublisher::publish_measurement(std::string_view sensor_type, std:
const auto &[room, type] = _mapping.find(sensor_id)->second;
auto topic = ranczo::topic::temperature::floor(mr, room, 1);
auto topic = ranczo::topic::temperature::publishFloor(room, 1, &mr);
using namespace std::chrono;
const auto now = date::floor< std::chrono::milliseconds >(system_clock::now());