diff --git a/CMakeLists.txt.user b/CMakeLists.txt.user index f8eeef1..10aa36c 100644 --- a/CMakeLists.txt.user +++ b/CMakeLists.txt.user @@ -1,6 +1,6 @@ - + EnvironmentId diff --git a/services/CMakeLists.txt b/services/CMakeLists.txt index 85d90b5..adf32a4 100644 --- a/services/CMakeLists.txt +++ b/services/CMakeLists.txt @@ -1 +1,3 @@ -add_subdirectory(floorheat_hub) +add_subdirectory(floorheat_svc) +add_subdirectory(temperature_svc) +add_subdirectory(output_svc) diff --git a/services/floorheat_svc/CMakeLists.txt b/services/floorheat_svc/CMakeLists.txt index a6a3924..9238338 100644 --- a/services/floorheat_svc/CMakeLists.txt +++ b/services/floorheat_svc/CMakeLists.txt @@ -14,7 +14,6 @@ target_include_directories(ranczo-io_floorheating target_link_libraries(ranczo-io_floorheating PUBLIC - # ${Protobuf_LIBRARIES} Boost::mqtt5 Boost::system Boost::json diff --git a/services/floorheat_svc/README.md b/services/floorheat_svc/README.md index 152ca54..55504a2 100644 --- a/services/floorheat_svc/README.md +++ b/services/floorheat_svc/README.md @@ -1,69 +1,61 @@ # `floorheating_svc` – Floor Heating Control Service -`floorheating_svc` is a service responsible for controlling electric underfloor heating zones throughout a smart home system. It uses MQTT v5 for communication and operates entirely asynchronously using Boost.Asio. +`floorheating_svc` is a service responsible for controlling **electric floor heating zones** in a smart home. It uses **MQTT v5** for communication and is implemented using **Boost.Asio** with asynchronous logic. -The service manages: -- Target temperature control per room -- Integration with floor and air temperature sensors (via MQTT) -- Secure relay control (via `io_output_svc`) -- Error detection (e.g., overheat) and safe shutdown +The service: +- Maintains and applies target **floor** temperature per room +- Reads temperature sensor data via MQTT (floor = required, air = optional) +- Sends regular state updates +- Handles critical errors (e.g., overheating) +- Listens to global `kill_switch` for emergency shutdown --- -## 🔧 MQTT Topic Structure +## 🔧 Design Philosophy -### Subscribed Topics +- Each room (zone) is represented by a separate object +- Relay outputs are **not controlled directly**, but via a separate service (`io_output_svc`) +- The floor heater only controls floor temperature — it **does not control air temperature** +- **Profiles, scheduling, automation** are delegated to external tools (e.g., Node-RED) -| Topic | Purpose | MQTT v5 Properties | -|-------|---------|--------------------| -| `home/heating//temperature/command` | Set target temperature and mode | `response-topic`, `correlation-data` | -| `home/heating//profile/command` | ❌ Not handled (external services like NodeRed can publish here) | | -| `home/sensor//temperature/state` | Floor temperature readings | — | -| `home/sensor//air_temperature/state` | Air temperature readings (optional) | — | -| `home/control/kill_switch` | Global emergency shutdown | — | +--- -### Published Topics +## 📚 MQTT Topics + +### 📥 Subscribed Topics (Inputs) + +| Topic | Purpose | +|-------|---------| +| `home/heating//floor/temperature/command` | Set target floor temperature | +| `home/sensor//floor/temperature/state` | Floor temperature sensor input | +| `home/sensor//air/temperature/state` | (Optional) air temperature sensor | +| `home/control/kill_switch` | Global emergency shutdown | + +> All subscriptions use `no_local = yes` to avoid receiving own messages. + +--- + +### 📤 Published Topics (Outputs) | Topic | Purpose | Retained | |-------|---------|----------| -| `home/heating//temperature/state` | Current status including temperature and heating state | ✅ Yes | -| `home/heating//temperature/result` | Response to a `command` request | ❌ No | -| `home/error/floorheating/` | Critical error reporting (e.g., overheating) | ❌ No | -| `home/control/kill_switch` | Initiate a global emergency shutdown (optional) | ❌ No | -| `home/heating//config` | Optional auto-discovery data | ✅ Yes | +| `home/heating//floor/temperature/state` | Periodic state report (current, target, heating status) | ✅ Yes | +| `home/heating//floor/temperature/result` | Response to `/command` (uses MQTT v5 correlation-data) | ❌ No | +| `home/error/floorheating/` | Critical error events (e.g., temperature spike) | ❌ No | +| `home/control/kill_switch` | Global kill message (optional, emitted by this service) | ❌ No | +| `home/heating//floor/config` | (Optional) auto-discovery data | ✅ Yes | --- -## 🌡 Temperature Sensors +## 🔁 Periodic State Updates -The service expects sensor data to arrive via MQTT from other components (e.g., Modbus, Zigbee, ADCs). - -### Example Topics -- `home/sensor/bathroom/temperature/state` -- `home/sensor/livingroom/air_temperature/state` - -### Payload Example - -```json -{ - "value": 21.7, - "timestamp": "2025-08-05T21:10:10Z" -} -``` - -> Floor and air temperatures are cached internally for each zone. Missing sensor updates for over 2 minutes will be treated as a warning. - ---- - -## 🔁 Periodic State Reporting - -The service publishes a status update every **60 seconds** per room to: +Every **60 seconds**, the service publishes a message to: ``` -home/heating//temperature/state +home/heating//floor/temperature/state ``` -### Example Payload: +**Example payload:** ```json { @@ -77,62 +69,45 @@ home/heating//temperature/state --- -## 🔃 Command Handling - -### `temperature/command` - -When a message is received on: +## 🧭 Command Handling +### Topic: ``` -home/heating//temperature/command +home/heating//floor/temperature/command ``` -The payload might look like: +**Example payload:** ```json { - "target": 22.5, + "target": 23.0, "mode": "manual" } ``` -The service: -- Sets the new target temperature -- Switches to manual mode -- Responds to the specified `response-topic` with the same `correlation-data` (MQTT v5 only) +**Expected response:** -### Response Example - -``` -home/heating//temperature/result -``` +Published to the specified `response-topic`, with matching `correlation-data` (MQTT v5): ```json { "status": "ok", - "applied": 22.5 + "applied": 23.0 } ``` --- -## ❌ Unsupported Features +## ⚠️ Error Detection & Safety -- `profile/command` is **not handled** inside the service. -- Scheduling, scenes, and automations should be implemented in external orchestrators like **NodeRed**. - ---- - -## 🛑 Error Handling and Safety - -### Overheat or sensor failure - -When a critical error is detected (e.g., floor temperature exceeds safe threshold): - -1. The service **publishes** an error: +### Trigger conditions: +- Floor temperature exceeds **critical threshold** (e.g., > 40°C) +- Missing floor sensor updates (e.g., no message for 2+ minutes) +### Actions: +1. Publish error: ``` -home/error/floorheating/bathroom +home/error/floorheating/ ``` ```json @@ -144,8 +119,7 @@ home/error/floorheating/bathroom } ``` -2. Then it **publishes** a global shutdown signal (optional): - +2. Trigger global shutdown: ``` home/control/kill_switch ``` @@ -153,53 +127,90 @@ home/control/kill_switch ```json { "reason": "critical_overheat", + "targets": ["floorheating"], "source": "floorheating_svc", - "targets": ["floorheating"] + "timestamp": "2025-08-05T22:30:10Z" } ``` -3. The service: - - Immediately **turns off all outputs** - - **Stops operation** and enters a `disabled` state - - Requires **manual restart** to resume (restart of process/container) +3. Internally: + - All outputs are disabled + - Heating logic is halted + - **Manual restart** is required to resume operation --- -## 🧪 Testing via CLI +## 🧪 Sensor Integration -You can test the service manually via `mosquitto_pub`: +### Floor temperature (mandatory) +Topic: +``` +home/sensor//floor/temperature/state +``` + +Payload: +```json +{ + "value": 22.4, + "timestamp": "2025-08-05T22:00:00Z" +} +``` + +### Air temperature (optional) +Topic: +``` +home/sensor//air/temperature/state +``` + +Payload: +```json +{ + "value": 23.1, + "timestamp": "2025-08-05T22:00:05Z" +} +``` + +> Only floor temperature is used for heating logic. Air temperature may be logged or used for auxiliary conditions. + +--- + +## 🚫 Unsupported Features + +| Feature | Status | +|--------|--------| +| Profile switching (`/profile/command`) | ❌ Not supported | +| Scheduling / automation | ❌ Not implemented internally | +| Direct relay access | ❌ Delegated to `io_output_svc` | +| Scene control | ❌ Should be handled externally | + +--- + +## 📌 Operational Notes + +- One shared MQTT client instance is used across all zone objects. +- The service handles messages using `boost::asio::awaitable` and `std::expected`-like error handling. +- Own MQTT messages are ignored via `no_local = yes` (available in MQTT v5 only). +- All state and sensor inputs are cached internally with timestamps. +- Fail-safe design: on any error, heating stops immediately and cannot restart automatically. + +--- + +## 🧪 Manual Testing Example ```bash -mosquitto_pub -t home/heating/bathroom/temperature/command \ - -m '{"target": 21.5, "mode": "manual"}' \ +mosquitto_pub \ + -t home/heating/bathroom/floor/temperature/command \ + -m '{"target": 22.5, "mode": "manual"}' \ -V mqttv5 \ - -D response-topic home/heating/bathroom/temperature/result + -D response-topic home/heating/bathroom/floor/temperature/result \ + -D correlation-data test-uuid-1234 ``` --- -## 🔒 Notes +## 📁 File placement -- Actual relay control is delegated to `io_output_svc` via MQTT topics like `home/output//command` -- The service does **not** directly access Modbus, Zigbee or hardware interfaces -- Designed to be restart-safe and isolated per zone - ---- - -## 🧭 Future Ideas - -- [ ] Track runtime stats (heating time, energy estimation) -- [ ] Publish `diagnostics` per room -- [ ] Add dry-run mode for development -- [ ] Send alerts to `notification_svc` on errors - ---- - -## 📁 File format info - -This document is valid `.md` or `.txt` format and can be placed directly in your repository: +This file should be placed in the project repo at: ``` services/floorheating_svc/README.md ``` - - diff --git a/services/floorheat_svc/heater.cpp b/services/floorheat_svc/heater.cpp index 7a95b57..c903339 100644 --- a/services/floorheat_svc/heater.cpp +++ b/services/floorheat_svc/heater.cpp @@ -164,7 +164,7 @@ struct ResistiveFloorHeater::Impl : private boost::noncopyable { } awaitable_expected< void > subscribeToTargetTemperatureUpdate() { - auto topic = std::format("home/{}/floor/heating/targetTemperature/set", _room); + auto topic = std::format("home/heating/{}/floor/temperature/command", _room); auto cb = [=, this](const boost::json::value & object) { targetTemperature = to_double(object.at("value")); diff --git a/services/floorheat_svc/main.cpp b/services/floorheat_svc/main.cpp index 6c0d0cd..b05f22d 100644 --- a/services/floorheat_svc/main.cpp +++ b/services/floorheat_svc/main.cpp @@ -17,10 +17,10 @@ namespace ranczo { /// TODO -/// * Przypisanie przecznika do maty grzewczej +/// * Przypisanie przełącznika do maty grzewczej /// * Zapis danych w DB -/// * Zapis ustawie -/// * Nasuchiwanie na MQTT +/// * Zapis ustawień +/// * Nasłuchiwanie na MQTT } // namespace ranczo @@ -36,8 +36,8 @@ int main() { boost::asio::io_context io_service; - /// Strand powoduje e zadania do niego przypisane zostaj wykonane sekwencyjnie, - /// get_executor pobrany z io_service nie daje takiej moliwoci i wtedy mona wykonywa zadania rwnloegle + /// Strand powoduje że zadania do niego przypisane zostają wykonane sekwencyjnie, + /// get_executor pobrany z io_service nie daje takiej możliwości i wtedy można wykonywać zadania równloegle boost::asio::any_io_executor io_executor = boost::asio::make_strand(io_service); // PARTER @@ -52,7 +52,7 @@ int main() { _heaters.emplace_back(std::make_shared< ranczo::ResistiveFloorHeater >(io_executor, "livingroom_zone2"sv)); _heaters.emplace_back(std::make_shared< ranczo::ResistiveFloorHeater >(io_executor, "livingroom_zone3"sv)); - // PITRO + // PIĘTRO _heaters.emplace_back(std::make_shared< ranczo::ResistiveFloorHeater >(io_executor, "askaRoom"sv)); _heaters.emplace_back(std::make_shared< ranczo::ResistiveFloorHeater >(io_executor, "maciejRoom"sv)); _heaters.emplace_back(std::make_shared< ranczo::ResistiveFloorHeater >(io_executor, "playroom"sv)); diff --git a/services/floorheat_svc/mqtt_client.cpp b/services/floorheat_svc/mqtt_client.cpp index 00e6040..286f84a 100644 --- a/services/floorheat_svc/mqtt_client.cpp +++ b/services/floorheat_svc/mqtt_client.cpp @@ -77,10 +77,10 @@ struct AsyncMqttClient::AsyncMqttClientImpl { // Configure the request to subscribe to a Topic. boost::mqtt5::subscribe_topic sub_topic = boost::mqtt5::subscribe_topic{topic.data(), boost::mqtt5::subscribe_options{ - boost::mqtt5::qos_e::exactly_once, // All messages will arrive at QoS 2. - boost::mqtt5::no_local_e::no, // Forward message from Clients with same ID. - boost::mqtt5::retain_as_published_e::retain, // Keep the original RETAIN flag. - boost::mqtt5::retain_handling_e::send // Send retained messages when the subscription is established. + .max_qos = boost::mqtt5::qos_e::exactly_once, // All messages will arrive at QoS 2. + .no_local = boost::mqtt5::no_local_e::yes, // Forward message from Clients with same ID. + .retain_as_published = boost::mqtt5::retain_as_published_e::retain, // Keep the original RETAIN flag. + .retain_handling = boost::mqtt5::retain_handling_e::send // Send retained messages when the subscription is established. }}; // Subscribe to a single Topic. diff --git a/services/output_svc/CMakeLists.txt b/services/output_svc/CMakeLists.txt new file mode 100644 index 0000000..e69de29 diff --git a/services/temperature_svc/CMakeLists.txt b/services/temperature_svc/CMakeLists.txt new file mode 100644 index 0000000..e69de29