ranczo-io/services/floorheat_svc/main.cpp
Bartosz Wieczorek 6da01a2f6b Add HTTP get
2025-12-12 16:57:05 +01:00

289 lines
12 KiB
C++

#include <map>
#include <optional>
#include <ranczo-io/utils/mqtt_client.hpp>
#include <boost/algorithm/string/compare.hpp>
#include <boost/algorithm/string/split.hpp>
#include <boost/asio/associated_executor.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/signal_set.hpp>
#include <boost/asio/strand.hpp>
#include <boost/asio/this_coro.hpp>
#include <boost/asio/use_future.hpp>
#include <boost/json/serialize.hpp>
#include <boost/regex/config.hpp>
#include <spdlog/spdlog.h>
#include "config.hpp"
#include "ranczo-io/utils/config.hpp"
#include "services/floorheat_svc/relay.hpp"
#include "services/floorheat_svc/thermometer.hpp"
#include "temperature_controller.hpp"
#include <ranczo-io/utils/modbus.hpp>
#include <ranczo-io/utils/mqtt_client.hpp>
#include <ranczo-io/utils/mqtt_topic_builder.hpp>
#include <csignal>
#include <memory>
#include <string>
#include <utility>
namespace ranczo {
/// TODO
/// * Przypisanie przełącznika do maty grzewczej
/// * Zapis danych w DB
/// * Zapis ustawień
/// * 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
// Modified completion token that will prevent co_await from throwing exceptions.
using namespace std::chrono_literals;
using namespace std::string_view_literals;
struct ZoneInfo {
std::string room;
int zone; // no _zone
};
std::optional< ZoneInfo > parse_zone_string(const std::string & input) {
const std::string suffix = "_zone";
ZoneInfo result;
auto pos = input.find(suffix);
if(pos != std::string::npos) {
// cutout room
result.room = input.substr(0, pos);
// cutout number after "_zone"
std::string number_part = input.substr(pos + suffix.size());
if(!number_part.empty()) {
try {
result.zone = std::stoi(number_part);
return result;
} catch(...) {
spdlog::warn("Zone in {} topic is not correct", input);
return std::nullopt; // NaN
}
}
}
return std::nullopt;
}
namespace ranczo {
inline awaitable_expected< void > forward_floor_temperature_all_rooms(AsyncMqttClient & mqtt) {
const std::string filter = "home/+/floor/temperature";
spdlog::info("Registering subscribtion for {}", filter);
auto ec = co_await mqtt.subscribe(filter, [&mqtt](const AsyncMqttClient::CallbackData & data, AsyncMqttClient::ResponseData &resp) -> awaitable_expected< void > {
spdlog::debug("Got temperature on old topic: {}, trying to republish", data.topic);
std::string room;
{
// topic format: home/{room}/floor/temperature
std::vector< std::string > parts{};
boost::algorithm::split(parts, data.topic, [](auto ch) { return ch == '/'; });
if(parts.size() == 4) // got ok topic
room = parts.at(1);
}
auto publish = [&](auto room, auto zone) -> awaitable_expected< void > {
std::map< std::string, std::string > remap{{"bathroom_1", "bathroom_guest"},
{"bathroom_2", "bathroom_private"},
{"bathroom_3", "bathroom_up"},
{"askaRoom", "aska_room"},
{"maciejRoom", "maciej_room"},
{"utilityRoom", "utility_room"}};
if(remap.contains(room))
room = remap.at(room);
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{};
};
if(!room.empty()) {
auto zone = parse_zone_string(room);
if(zone) {
ASYNC_CHECK(publish(zone->room, zone->zone));
} else {
ASYNC_CHECK(publish(room, 1));
}
}
co_return _void{};
});
spdlog::trace("Subscribtion pass, checking for errors");
if(!ec) {
spdlog::error("Got error from subscribe: {}", ec.error().message());
}
spdlog::trace("All good, returning");
co_return _void{};
}
} // namespace ranczo
int main() {
using namespace ranczo;
spdlog::set_level(spdlog::level::info);
std::vector< std::shared_ptr< TemperatureController > > _heaters;
boost::asio::io_context io_context;
// Register signal handler
spdlog::info("Registering signal handlers");
// promisa do „zatrzymaj program”
std::promise< void > stop_promise;
auto stop_future = stop_promise.get_future();
boost::asio::executor_work_guard< boost::asio::io_context::executor_type > work_guard = boost::asio::make_work_guard(io_context);
boost::asio::any_io_executor io_executor = io_context.get_executor();
SettingsStore store{"floorheat_svc.db", io_executor, 1};
ComponentSettingsStore mqttconfig{store, "mqtt"};
if(not mqttconfig.contains("host"))
mqttconfig.save("host", "192.168.10.10");
if(not mqttconfig.contains("port"))
mqttconfig.save("port", 2502);
spdlog::info("Create Modbus TCP context");
auto modbus_relayBoardsCtx = ModbusTcpContext::create(io_context, // używamy istniejącego io_context
mqttconfig.get_value< std::string >("host"), // host Modbus TCP
mqttconfig.get_value< int >("port"), // port Modbus TCP
1 // rozmiar puli dla libmodbus
);
auto fut = boost::asio::co_spawn(
io_executor,
[modbus_relayBoardsCtx]() -> awaitable_expected< void > { co_return co_await modbus_relayBoardsCtx->async_connect(); },
boost::asio::use_future);
std::jthread io_thread([&] {
spdlog::info("io_context thread started");
io_context.run();
spdlog::info("io_context thread finished");
});
auto result = fut.get(); // odbieramy wynik connecta
if(!result) {
spdlog::error("Modbus connect failed: {} ({})", result.error().message(), result.error().value());
return 1; // nie kontynuujemy dalszej konfiguracji
}
spdlog::info("Modbus connected successfully");
// create a modbus client to use with all internal objects
AsyncMqttClient mqttClient{io_executor};
// send messages are not received by the sending client so we need to create another to act as proxy
AsyncMqttClient mqttProxyClient{io_executor};
co_spawn(io_executor, forward_floor_temperature_all_rooms(mqttProxyClient), boost::asio::detached);
std::map< int, std::shared_ptr< ModbusDevice > > modbusDevices;
auto make_relay = [&](int board, int channel) {
BOOST_ASSERT(board > 0 and board < 256);
BOOST_ASSERT(channel > 0 and channel <= 16);
std::shared_ptr< ModbusDevice > device;
if(auto it = modbusDevices.find(board); it != modbusDevices.end()) {
device = it->second;
spdlog::info("Modbus device found, reusing {}", device.use_count());
} else {
spdlog::info("Modbus device not found");
const auto & [dev, ok] =
modbusDevices.emplace(std::make_pair(board, std::make_shared< ModbusDevice >(modbus_relayBoardsCtx, board)));
device = dev->second;
}
spdlog::debug("make_relay : {}/{} ", board, channel);
return std::make_unique< ModbusRelay >(io_executor, device, channel);
};
auto make_ramp_thermostat = [&](auto room, int zone, std::unique_ptr< Relay > relay) {
spdlog::debug("make_ramp_thermostat {} / {}: create MqttThermometer", room, zone);
MqttThermometer::Settings settings{.room = room.data(), .zone = zone};
auto thermo = std::make_unique< MqttThermometer >(io_executor, mqttClient, settings);
spdlog::debug("make_ramp_thermostat {} / {} : create Relay Thermostat", room, zone);
return std::make_shared< RelayThermostat >(io_executor, mqttClient, store, std::move(relay), std::move(thermo), room, zone);
};
// // Floor 0
_heaters.emplace_back(make_ramp_thermostat("livingroom"sv, 1, make_relay(16, 2))); // 3 strefy
_heaters.emplace_back(make_ramp_thermostat("livingroom"sv, 2, make_relay(16, 10)));
_heaters.emplace_back(make_ramp_thermostat("livingroom"sv, 3, make_relay(16, 9)));
_heaters.emplace_back(make_ramp_thermostat("corridor"sv, 1, make_relay(16, 12)));
_heaters.emplace_back(make_ramp_thermostat("utility_room"sv, 1, make_relay(16, 1)));
_heaters.emplace_back(make_ramp_thermostat("wardrobe"sv, 1, make_relay(16, 5)));
_heaters.emplace_back(make_ramp_thermostat("bathroom_private"sv, 1, make_relay(16, 12)));
_heaters.emplace_back(make_ramp_thermostat("bathroom_guest"sv, 1, make_relay(16, 11)));
_heaters.emplace_back(make_ramp_thermostat("beadroom"sv, 1, make_relay(16, 6))); // 2 strefy
_heaters.emplace_back(make_ramp_thermostat("beadroom"sv, 2, make_relay(16, 7)));
// Floor 1
_heaters.emplace_back(make_ramp_thermostat("office"sv, 1, make_relay(2, 12)));
/// TODO fizycznie podłączyć czujnik temperatury
// _heaters.emplace_back(relayThermostatFactory("bathroom_up"sv, 1, relay(0,0)));
_heaters.emplace_back(make_ramp_thermostat("aska_room"sv, 1, make_relay(2, 10)));
_heaters.emplace_back(make_ramp_thermostat("maciej_room"sv, 1, make_relay(2, 11)));
_heaters.emplace_back(make_ramp_thermostat("playroom"sv, 1, make_relay(2, 9)));
/// TODO czujnik temperatury
// _heaters.emplace_back(relayThermostatFactory("corridor_up"sv, 1, relay(0, 0)));
boost::asio::signal_set signals(io_context, SIGINT, SIGTERM);
signals.async_wait([&](const boost::system::error_code & ec, int signo) {
if(ec)
return; // canceled podczas shutdown
spdlog::warn("Signal {} received", signo);
// Zainicjuj graceful shutdown na wątku io_context (bezpiecznie):
boost::asio::post(io_context, [&] {
spdlog::info("Graceful shutdown start");
_heaters.clear(); // remove all heaters
mqttProxyClient.cancel();
mqttClient.cancel();
modbusDevices.clear();
work_guard.reset();
io_context.stop(); // ok na tym samym wątku
spdlog::info("Graceful shutdown posted");
});
stop_promise.set_value(); // pobudzi main
});
for(auto & heater : _heaters) {
co_spawn(io_context, heater->start(), boost::asio::detached);
}
if(io_thread.joinable())
io_thread.join();
return 0;
}