Add 'topic builder' add install step to cmake

This commit is contained in:
Bartosz Wieczorek 2025-08-06 08:57:01 +02:00
parent 9eb0723f1c
commit 2a55b5562a
6 changed files with 228 additions and 7 deletions

View File

@ -4,8 +4,15 @@ project(Ranczo-IO)
set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
# find_package(Protobuf REQUIRED) include(CheckIPOSupported)
# include_directories(${Protobuf_INCLUDE_DIRS}) check_ipo_supported(RESULT supported OUTPUT error)
if( supported )
message(STATUS "IPO / LTO enabled")
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
else()
message(STATUS "IPO / LTO not supported: <${error}>")
endif()
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
set(BOOST_ROOT /usr/local) set(BOOST_ROOT /usr/local)

View File

@ -17,3 +17,11 @@ target_link_libraries(ranczo-io_utils
Boost::json Boost::json
spdlog::spdlog spdlog::spdlog
) )
install(
TARGETS ranczo-io_utils
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

View File

@ -0,0 +1,184 @@
#pragma once
#include <array>
#include <string>
#include <string_view>
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) {
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::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;
}
// Full topic: home/<room>/<type>/<place>/<action>
inline std::string buildTopic(std::string_view room, std::string_view type, std::string_view place, std::string_view action) {
using namespace std::string_view_literals;
return make_topic("home"sv, room, type, place, action);
}
namespace topic {
namespace heating {
// Topic:
// home/<room>/heating/floor/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/command", payload, {
// response_topic: "client/myid/response",
// correlation_data: "uuid-123"
// });
inline std::string command(std::string_view room) {
using namespace std::string_view_literals;
return make_topic("home"sv, room, "heating"sv, "floor"sv, "command"sv);
}
// Topic:
// home/<room>/heating/floor/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) {
using namespace std::string_view_literals;
return make_topic("home"sv, room, "heating"sv, "floor"sv, "state"sv);
}
// Topic:
// home/<room>/heating/floor/error
//
// ➤ Purpose:
// Reports critical error conditions for floor heating in a room.
// Example errors: temperature spike, sensor missing, overheating.
// Payload: structured JSON with fields like:
// {
// "type": "temperature_spike",
// "message": "Floor temperature exceeded 40°C",
// "severity": "critical",
// "timestamp": "2025-08-06T14:22:10Z"
// }
//
// ➤ Direction: PUBLISH
// ➤ Retained: NO
// ➤ Request/Response: NO — event-driven reporting only.
inline std::string error(std::string_view room) {
using namespace std::string_view_literals;
return make_topic("home"sv, room, "heating"sv, "floor"sv, "error"sv);
}
// Topic:
// home/<room>/heating/floor/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) {
using namespace std::string_view_literals;
return make_topic("home"sv, room, "heating"sv, "floor"sv, "config"sv);
}
} // namespace heating
namespace temperature {
// Topic:
// home/<room>/sensor/floor/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) {
using namespace std::string_view_literals;
return make_topic("home"sv, room, "sensor"sv, "floor"sv, "temperature"sv);
}
// Topic:
// home/<room>/sensor/air/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) {
using namespace std::string_view_literals;
return make_topic("home"sv, room, "sensor"sv, "air"sv, "temperature"sv);
}
} // namespace temperature
} // namespace topic
} // namespace ranczo

View File

@ -8,3 +8,12 @@ target_link_libraries(ranczo-io_floorheating
PUBLIC PUBLIC
ranczo-io::utils ranczo-io::utils
) )
include(GNUInstallDirs)
install(
TARGETS ranczo-io_floorheating
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

View File

@ -1,8 +1,10 @@
#include "heater_controller.hpp" #include "heater_controller.hpp"
#include "config.hpp" #include "config.hpp"
#include <spdlog/spdlog.h>
#include <ranczo-io/utils/mqtt_client.hpp> #include <ranczo-io/utils/mqtt_client.hpp>
#include "spdlog/spdlog.h" #include <ranczo-io/utils/mqtt_topic_builder.hpp>
#include <ranczo-io/utils/timer.hpp> #include <ranczo-io/utils/timer.hpp>
#include <algorithm> #include <algorithm>
@ -150,7 +152,7 @@ struct ResistiveFloorHeater::Impl : private boost::noncopyable {
} }
awaitable_expected< void > subscribeToTemperatureUpdate() { awaitable_expected< void > subscribeToTemperatureUpdate() {
auto topic = std::format("home/{}/floor/sensor/temperature", _room); auto topic = topic::temperature::floor(_room);
auto cb = [=, this](const boost::json::value & object) -> awaitable_expected< void > { auto cb = [=, this](const boost::json::value & object) -> awaitable_expected< void > {
temperature = to_double(object.at("value")); temperature = to_double(object.at("value"));
@ -161,11 +163,12 @@ struct ResistiveFloorHeater::Impl : private boost::noncopyable {
}; };
ASYNC_CHECK(subscribe(topic, std::move(cb))); ASYNC_CHECK(subscribe(topic, std::move(cb)));
co_return heater_expected_void{}; co_return heater_expected_void{};
} }
awaitable_expected< void > subscribeToTargetTemperatureUpdate() { awaitable_expected< void > subscribeToCommandUpdate() {
auto topic = std::format("home/{}/floor/heating/temperature/command", _room); auto topic = topic::heating::command(_room);
auto cb = [=, this](const boost::json::value & object) -> awaitable_expected< void > { auto cb = [=, this](const boost::json::value & object) -> awaitable_expected< void > {
targetTemperature = to_double(object.at("value")); targetTemperature = to_double(object.at("value"));
@ -175,6 +178,7 @@ struct ResistiveFloorHeater::Impl : private boost::noncopyable {
}; };
ASYNC_CHECK(subscribe(topic, std::move(cb))); ASYNC_CHECK(subscribe(topic, std::move(cb)));
co_return heater_expected_void{}; co_return heater_expected_void{};
} }
@ -187,12 +191,13 @@ struct ResistiveFloorHeater::Impl : private boost::noncopyable {
}; };
ASYNC_CHECK(subscribe(topic, std::move(cb))); ASYNC_CHECK(subscribe(topic, std::move(cb)));
co_return heater_expected_void{}; co_return heater_expected_void{};
} }
awaitable_expected< void > start() { awaitable_expected< void > start() {
ASYNC_CHECK_MSG(subscribeToTemperatureUpdate(), "subscribe to temp update failed"); ASYNC_CHECK_MSG(subscribeToTemperatureUpdate(), "subscribe to temp update failed");
ASYNC_CHECK_MSG(subscribeToTargetTemperatureUpdate(), "subscribe to temp update failed"); ASYNC_CHECK_MSG(subscribeToCommandUpdate(), "subscribe to temp update failed");
ASYNC_CHECK_MSG(subscribeToTargetProfileUpdate(), "subscribe to profile update failed"); ASYNC_CHECK_MSG(subscribeToTargetProfileUpdate(), "subscribe to profile update failed");
ASYNC_CHECK_MSG(_tickTimer.start(), "failed to start timer"); ASYNC_CHECK_MSG(_tickTimer.start(), "failed to start timer");

View File

@ -10,3 +10,11 @@ target_link_libraries(ranczo-io_temperature
PUBLIC PUBLIC
ranczo-io::utils ranczo-io::utils
) )
install(
TARGETS ranczo-io_temperature
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)