refactor
This commit is contained in:
parent
f4f0613b2e
commit
549ddfc615
@ -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>
|
||||
|
||||
@ -1 +1,3 @@
|
||||
add_subdirectory(floorheat_hub)
|
||||
add_subdirectory(floorheat_svc)
|
||||
add_subdirectory(temperature_svc)
|
||||
add_subdirectory(output_svc)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
|
||||
|
||||
@ -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"));
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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.
|
||||
|
||||
0
services/output_svc/CMakeLists.txt
Normal file
0
services/output_svc/CMakeLists.txt
Normal file
0
services/temperature_svc/CMakeLists.txt
Normal file
0
services/temperature_svc/CMakeLists.txt
Normal file
Loading…
Reference in New Issue
Block a user