This commit is contained in:
Bartosz Wieczorek 2025-08-05 14:37:36 +02:00
parent f4f0613b2e
commit 549ddfc615
9 changed files with 137 additions and 125 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE QtCreatorProject>
<!-- Written by QtCreator 17.0.0, 2025-08-05T11:13:08. -->
<!-- Written by QtCreator 17.0.0, 2025-08-05T14:36:20. -->
<qtcreator>
<data>
<variable>EnvironmentId</variable>

View File

@ -1 +1,3 @@
add_subdirectory(floorheat_hub)
add_subdirectory(floorheat_svc)
add_subdirectory(temperature_svc)
add_subdirectory(output_svc)

View File

@ -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

View File

@ -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/<room>/temperature/command` | Set target temperature and mode | `response-topic`, `correlation-data` |
| `home/heating/<room>/profile/command` | ❌ Not handled (external services like NodeRed can publish here) | |
| `home/sensor/<room>/temperature/state` | Floor temperature readings | — |
| `home/sensor/<room>/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/<room>/floor/temperature/command` | Set target floor temperature |
| `home/sensor/<room>/floor/temperature/state` | Floor temperature sensor input |
| `home/sensor/<room>/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/<room>/temperature/state` | Current status including temperature and heating state | ✅ Yes |
| `home/heating/<room>/temperature/result` | Response to a `command` request | ❌ No |
| `home/error/floorheating/<room>` | Critical error reporting (e.g., overheating) | ❌ No |
| `home/control/kill_switch` | Initiate a global emergency shutdown (optional) | ❌ No |
| `home/heating/<room>/config` | Optional auto-discovery data | ✅ Yes |
| `home/heating/<room>/floor/temperature/state` | Periodic state report (current, target, heating status) | ✅ Yes |
| `home/heating/<room>/floor/temperature/result` | Response to `/command` (uses MQTT v5 correlation-data) | ❌ No |
| `home/error/floorheating/<room>` | Critical error events (e.g., temperature spike) | ❌ No |
| `home/control/kill_switch` | Global kill message (optional, emitted by this service) | ❌ No |
| `home/heating/<room>/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/<room>/temperature/state
home/heating/<room>/floor/temperature/state
```
### Example Payload:
**Example payload:**
```json
{
@ -77,62 +69,45 @@ home/heating/<room>/temperature/state
---
## 🔃 Command Handling
### `temperature/command`
When a message is received on:
## 🧭 Command Handling
### Topic:
```
home/heating/<room>/temperature/command
home/heating/<room>/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/<room>/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/<room>
```
```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/<room>/floor/temperature/state
```
Payload:
```json
{
"value": 22.4,
"timestamp": "2025-08-05T22:00:00Z"
}
```
### Air temperature (optional)
Topic:
```
home/sensor/<room>/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/<id>/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
```

View File

@ -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"));

View File

@ -17,10 +17,10 @@
namespace ranczo {
/// TODO
/// * Przypisanie przełącznika do maty grzewczej
/// * Przypisanie przełącznika do maty grzewczej
/// * Zapis danych w DB
/// * Zapis ustawień
/// * Nasłuchiwanie 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 możliwości i wtedy można wykonywać zadania równloegle
/// 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));
// PIĘTRO
// 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));

View File

@ -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.

View File

View File