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