289 lines
12 KiB
C++
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;
|
|
}
|