Add 'topic builder' add install step to cmake
This commit is contained in:
parent
9eb0723f1c
commit
2a55b5562a
@ -4,8 +4,15 @@ project(Ranczo-IO)
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# find_package(Protobuf REQUIRED)
|
||||
# include_directories(${Protobuf_INCLUDE_DIRS})
|
||||
include(CheckIPOSupported)
|
||||
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)
|
||||
set(BOOST_ROOT /usr/local)
|
||||
|
||||
@ -17,3 +17,11 @@ target_link_libraries(ranczo-io_utils
|
||||
Boost::json
|
||||
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}
|
||||
)
|
||||
|
||||
184
libs/ranczo-io/utils/mqtt_topic_builder.hpp
Normal file
184
libs/ranczo-io/utils/mqtt_topic_builder.hpp
Normal 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
|
||||
@ -8,3 +8,12 @@ target_link_libraries(ranczo-io_floorheating
|
||||
PUBLIC
|
||||
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}
|
||||
)
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
#include "heater_controller.hpp"
|
||||
|
||||
#include "config.hpp"
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#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 <algorithm>
|
||||
@ -150,7 +152,7 @@ struct ResistiveFloorHeater::Impl : private boost::noncopyable {
|
||||
}
|
||||
|
||||
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 > {
|
||||
temperature = to_double(object.at("value"));
|
||||
@ -161,11 +163,12 @@ struct ResistiveFloorHeater::Impl : private boost::noncopyable {
|
||||
};
|
||||
|
||||
ASYNC_CHECK(subscribe(topic, std::move(cb)));
|
||||
|
||||
co_return heater_expected_void{};
|
||||
}
|
||||
|
||||
awaitable_expected< void > subscribeToTargetTemperatureUpdate() {
|
||||
auto topic = std::format("home/{}/floor/heating/temperature/command", _room);
|
||||
awaitable_expected< void > subscribeToCommandUpdate() {
|
||||
auto topic = topic::heating::command(_room);
|
||||
|
||||
auto cb = [=, this](const boost::json::value & object) -> awaitable_expected< void > {
|
||||
targetTemperature = to_double(object.at("value"));
|
||||
@ -175,6 +178,7 @@ struct ResistiveFloorHeater::Impl : private boost::noncopyable {
|
||||
};
|
||||
|
||||
ASYNC_CHECK(subscribe(topic, std::move(cb)));
|
||||
|
||||
co_return heater_expected_void{};
|
||||
}
|
||||
|
||||
@ -187,12 +191,13 @@ struct ResistiveFloorHeater::Impl : private boost::noncopyable {
|
||||
};
|
||||
|
||||
ASYNC_CHECK(subscribe(topic, std::move(cb)));
|
||||
|
||||
co_return heater_expected_void{};
|
||||
}
|
||||
|
||||
awaitable_expected< void > start() {
|
||||
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(_tickTimer.start(), "failed to start timer");
|
||||
|
||||
@ -10,3 +10,11 @@ target_link_libraries(ranczo-io_temperature
|
||||
PUBLIC
|
||||
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}
|
||||
)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user