firmware-base/docs/amp-budget.md

314 lines
16 KiB
Markdown

# Amperage Budget Manager Design
## 1. Introduction
This document outlines the design for the `AmperageBudgetManager` component. This component is responsible for managing a group of `OmronE5` temperature controllers (representing heating partitions) to ensure their total power consumption does not exceed a predefined budget. It achieves this by serializing the activation (`run()`) of the controllers based on heating demand (PV < SP) and available budget, using a fair scheduling algorithm.
## 2. Goals
* Limit the instantaneous total power consumption of a group of `OmronE5` devices.
* Provide a configurable power budget (in Watts).
* Implement a fair scheduling mechanism (round-robin with preemption) to ensure all partitions needing heat get a chance to run over time.
* Integrate seamlessly with the existing `PHApp` and `OmronE5` components.
* Control `OmronE5` devices using their existing `run()` and `stop()` methods.
## 3. Component Architecture
### 3.1 Class Diagram
```mermaid
classDiagram
Component <|-- AmperageBudgetManager
AmperageBudgetManager o-- "0..*" ManagedDevice : contains
ManagedDevice o-- OmronE5 : references
PHApp o-- AmperageBudgetManager : owns
PHApp o-- "*" OmronE5 : owns
class Component {
+setup() virtual
+loop() virtual
+info() virtual
}
class OmronE5 {
+run() bool
+stop() bool
+getPV(uint16_t&) bool
+getSP(uint16_t&) bool
+isRunning() bool
+getConsumption() uint32_t
-_consumption : uint32_t
}
class ManagedDevice {
+OmronE5* device
+ManagedState state
+uint8_t originalIndex
}
class AmperageBudgetManager {
+AmperageBudgetManager(uint32_t wattBudget)
+addManagedDevice(OmronE5* device)
+setup() override
+loop() override
+info() override
-uint32_t _wattBudget
-ManagedDevice _managedDevices[]
-uint8_t _numDevices
-uint8_t _maxDevices
-uint8_t _nextDeviceIndex
-ManagedState state // Enum definition reference
-_allocateBudget()
}
class PHApp {
+setup()
+loop()
}
enum ManagedState {
UNKNOWN
IDLE
REQUESTING_HEAT
HEATING
}
AmperageBudgetManager ..> ManagedState : uses
ManagedDevice ..> ManagedState : uses
```
### 3.2 Data Structures
* **`_wattBudget`**: `uint32_t` - The maximum combined wattage allowed for managed devices in the `HEATING` state.
* **`_managedDevices`**: An array or similar structure holding pointers to the managed `OmronE5` instances along with their current state (`ManagedState`) and original registration order for stable sorting.
* **`_nextDeviceIndex`**: `uint8_t` - Index used for the round-robin starting point.
* **`ManagedState`**: Enum (`UNKNOWN`, `IDLE`, `REQUESTING_HEAT`, `HEATING`) - Tracks the state of each managed device from the budget manager's perspective.
### 3.3 Key Methods
* **`AmperageBudgetManager(uint32_t wattBudget)`**: Constructor, sets the budget.
* **`addManagedDevice(OmronE5* device)`**: Registers an `OmronE5` instance to be managed.
* **`setup()`**: Initializes the manager, sets initial states.
* **`loop()`**: The core logic, executed repeatedly. Checks device status, calculates needs, allocates budget, and sends `run()`/`stop()` commands.
* **`info()`**: Prints debugging information about managed devices and budget status.
* **`_allocateBudget()`**: Internal helper encapsulating the budget allocation logic described in section 4.
## 4. Scheduling Logic (Round-Robin with Preemption)
The `loop()` method executes the following logic periodically:
1. **Identify Needs & Stop Unneeded:**
* Iterate through all `_managedDevices`.
* For each device, get its current PV, SP, and `isRunning()` status.
* If `PV >= SP` (wants to stop) and its state is `HEATING` or `REQUESTING_HEAT`:
* Call `device->stop()`.
* Set its state to `IDLE`.
* If `PV < SP` (wants to run):
* If its state is `IDLE`, change state to `REQUESTING_HEAT`.
* Keep track of devices currently in `HEATING` state and those in `REQUESTING_HEAT`.
2. **Prioritize and Allocate Budget (`_allocateBudget`):**
* Create a list `potentialRunners` containing all devices currently in the `REQUESTING_HEAT` or `HEATING` state.
* Sort `potentialRunners` based on their original registration order, but starting the comparison cycle from `_nextDeviceIndex` to implement round-robin priority. (Effectively, devices closer to `_nextDeviceIndex` in the circular list get higher priority for this cycle).
* Initialize `currentWattage = 0`.
* Create an empty list `willRun`.
* Iterate through the prioritized `potentialRunners`:
* Get the device's consumption: `device->getConsumption()`.
* If `currentWattage + device->getConsumption() <= _wattBudget`:
* Add the device to the `willRun` list.
* `currentWattage += device->getConsumption()`.
3. **Apply Changes:**
* Iterate through all `_managedDevices` again:
* If the device is in the `willRun` list:
* If its state was `REQUESTING_HEAT`, call `device->run()`.
* Set its state to `HEATING`.
* Else (device is *not* in `willRun` list):
* If its state was `HEATING`, call `device->stop()`.
* Set its state to `REQUESTING_HEAT` (if PV < SP) or `IDLE` (if PV >= SP - handled in step 1).
4. **Update Robin Index:**
* Increment `_nextDeviceIndex` (wrapping around) to ensure the priority shifts in the next cycle: `_nextDeviceIndex = (_nextDeviceIndex + 1) % _numDevices`.
### 4.1 State Diagram (for a single managed OmronE5)
```mermaid
stateDiagram-v2
[*] --> IDLE : Initialized / PV >= SP
IDLE --> REQUESTING_HEAT : PV < SP detected
IDLE --> IDLE : PV >= SP
REQUESTING_HEAT --> HEATING : Budget allocated & run() called
REQUESTING_HEAT --> IDLE : PV >= SP detected & stop() called
REQUESTING_HEAT --> REQUESTING_HEAT : Budget not available
HEATING --> IDLE : PV >= SP detected & stop() called
HEATING --> REQUESTING_HEAT : Budget revoked & stop() called
HEATING --> HEATING : Budget remains allocated & PV < SP
```
### 4.2 Sequence Diagram (Simplified `loop` cycle)
```mermaid
sequenceDiagram
participant L as loop()
participant BM as AmperageBudgetManager
participant O1 as OmronE5_1
participant O2 as OmronE5_2
participant O3 as OmronE5_3
loop over Managed Devices
L->>O1: getPV(), getSP(), isRunning()
L->>BM: Update O1 State (e.g., IDLE to REQUESTING_HEAT)
L->>O2: getPV(), getSP(), isRunning()
L->>BM: Update O2 State (e.g., HEATING to IDLE)
L->>O2: stop()
L->>O3: getPV(), getSP(), isRunning()
L->>BM: Update O3 State (e.g., HEATING remains HEATING)
end
L->>BM: _allocateBudget()
BM->>O1: getConsumption()
BM->>O3: getConsumption()
BM-->>L: Return willRun = [O1, O3]
loop over Managed Devices
L->>BM: Check if O1 in willRun -> Yes
BM->>O1: run() (if state was REQUESTING_HEAT)
BM->>BM: Set O1 state = HEATING
L->>BM: Check if O2 in willRun -> No
BM->>BM: Set O2 state = IDLE (already stopped)
L->>BM: Check if O3 in willRun -> Yes
BM->>BM: Set O3 state = HEATING (no change needed)
end
L->>BM: Increment _nextDeviceIndex
```
## 5. Configuration and Integration
* **Instantiation:** An instance of `AmperageBudgetManager` should be created in `PHApp`.
```cpp
// In PHApp.h (example)
#include "components/AmperageBudgetManager.h"
// ...
AmperageBudgetManager* budgetManager;
// In PHApp::setup() (example)
_amperageBudget = new AmperageBudgetManager(10000); // Example: 10kW budget
_amperageBudget->addManagedDevice(_omron1); // Assuming _omron1 is an OmronE5*
_amperageBudget->addManagedDevice(_omron2);
// ... add other Omrons
_amperageBudget->setup();
```
* **Budget Value:** The power budget (Watts) is passed to the constructor. This could be made configurable via Modbus or REST API by adding setter methods and potentially exposing them through `PHApp`.
* **Device Consumption:** The manager relies on `OmronE5::getConsumption()`. This method needs to be added to the `OmronE5` class, returning the `_consumption` value (which might also need to be made configurable per `OmronE5` instance).
* **PHApp Loop:** `AmperageBudgetManager::loop()` must be called from `PHApp::loop()`.
```cpp
// In PHApp::loop()
if (_amperageBudget) {
_amperageBudget->loop();
}
```
## 6. Potential Issues and Refinements
* **SP Reachability:** If the budget is too low relative to the heat loss and the number/power of partitions, the system might struggle or fail to reach the Set Point (SP) on all devices. This requires careful tuning of the budget.
* **Rapid Cycling:** If the budget forces frequent starting/stopping, it might cause wear on relays (if used by the Omron output). Hysteresis could be added (e.g., require PV to be `SP + delta` before stopping, or `SP - delta` before wanting heat).
* **Consumption Accuracy:** The accuracy depends on the configured `_consumption` value in each `OmronE5`. If actual consumption varies significantly, the budget management might be inaccurate.
* **Error Handling:** The design assumes `OmronE5` methods succeed. Robust error handling (checking return values of `run()`, `stop()`, `getPV`, etc.) should be added.
* **Dynamic Budget:** The budget could be adjusted dynamically based on external factors (e.g., total system load).
* **Advanced Scheduling:** More complex scheduling (e.g., prioritizing devices further from SP) could be implemented if simple round-robin proves insufficient.
## 7. Mathematical Modeling and Optimization Considerations
This section explores the mathematical underpinnings and potential optimizations for the budget management system.
### 7.1 Core Budget Constraint
Let N be the number of managed `OmronE5` devices. Let P_i be the power consumption (Watts) of device i when heating, obtained via `device[i]->getConsumption()`. Let S_i(t) be the state of device i at time t, where S_i(t) = 1 if the device is actively heating (state `HEATING`) and S_i(t) = 0 otherwise (`IDLE`, `REQUESTING_HEAT`, or stopped by the manager).
The fundamental constraint enforced by the `AmperageBudgetManager` at any given time t is:
```
Sum(S_i(t) * P_i for i=1 to N) <= W_budget
```
where W_budget is the configured maximum power budget.
The scheduling algorithm (Section 4, Step 2) effectively selects the subset of devices H(t) = { i | S_i(t) = 1 } such that this inequality holds, prioritizing devices based on need (PV_i < SP_i) and the round-robin index.
### 7.2 Error Sources
Potential sources of error can affect the system's ability to precisely meet the budget or achieve optimal heating:
1. **Consumption Estimation Error (epsilon_P):** The actual power P_i_actual drawn by a heater might differ from the configured P_i. The total actual power is `Sum(P_i_actual for i in H(t))`. The budget calculation error is `Sum(P_i - P_i_actual for i in H(t))`.
* *Mitigation:* Calibration, using more accurate power measurement if available, adding a safety margin to W_budget.
2. **Measurement Delay (tau_m):** Time lag between a temperature change, its measurement by the `OmronE5` (PV update), and the `AmperageBudgetManager` reading it.
3. **Control Delay (tau_c):** Time lag between the manager deciding to change a device's state (`run()`/`stop()`) and the command being executed and having an effect (e.g., RS485 communication delay, relay actuation time).
4. **Sampling Rate:** The `loop()` frequency of the `AmperageBudgetManager` determines how quickly changes are detected and acted upon. A slower rate increases the effective delay.
### 7.3 Thermal Inertia and Dynamics
Each heating partition i has thermal characteristics. A simplified model might be a first-order system:
```
C_i * d(PV_i)/dt = Q_heat_i(t) - Q_loss_i(t)
```
where:
* C_i is the thermal capacitance of partition i.
* PV_i is the process variable (temperature) of partition i.
* Q_heat_i(t) is the heat input rate. Q_heat_i(t) = P_i if S_i(t) = 1, and 0 otherwise (assuming idealized heater).
* Q_loss_i(t) is the heat loss rate, often modeled as `U_i * (PV_i(t) - T_ambient)`, where U_i is a heat transfer coefficient and T_ambient is the ambient temperature.
**Implications:**
* **Lag:** Due to C_i, the temperature PV_i changes gradually in response to Q_heat_i(t). Stopping heat doesn't instantly stop the temperature rise, potentially leading to overshoot, especially if the Omron's internal PID isn't tuned for intermittent operation.
* **Interdependence:** Heat loss Q_loss_i(t) might depend not only on T_ambient but also on the temperatures of adjacent partitions (PV_j), creating thermal coupling.
* **SP Reachability:** If the average heat input allowed by the budget over time (Avg_Q_heat_i) is less than the heat loss at the setpoint (Q_loss_i(SP_i)), the partition may never reach SP_i.
```
Avg_Q_heat_i = P_i * (Average Duty Cycle for i) < Q_loss_i(SP_i)
```
* **Minimum Effective On-Time (tau_min_on_i):** Due to thermal capacitance (C_i) and initial heat loss rate (Q_loss_i(t)), there might be a minimum duration tau_min_on_i for which a heater i must be active (S_i(t) = 1) to produce a significant or useful increase in PV_i. Activating a heater for durations less than tau_min_on_i could be inefficient, consuming power without meaningfully contributing to reaching the setpoint. The scheduling algorithm should ideally ensure that when a device is granted budget, it runs for at least this duration, or consider this constraint when deciding which devices to activate.
### 7.4 Optimization Objectives
Beyond simply staying within budget, optimization could target:
1. **Minimize Time-to-Setpoint:** Reduce the total time for all devices requesting heat to reach their respective SP_i.
2. **Maximize Fairness:** Ensure all devices requesting heat receive a proportional amount of heating time over a longer window, preventing starvation.
3. **Minimize Overshoot:** Reduce the amount PV_i exceeds SP_i after heating stops.
4. **Minimize Control Effort:** Reduce the frequency of `run()`/`stop()` commands to minimize wear on physical components (like relays).
5. **Maximize Weighted Priority:** Allow certain devices to have higher priority in budget allocation.
### 7.5 Potential Control Improvements
1. **Hysteresis:** Introduce a deadband around SP. Only request heat if `PV_i < SP_i - delta_lower` and only stop requesting heat if `PV_i > SP_i + delta_upper`. This reduces rapid cycling near the setpoint.
2. **Priority Scheduling:** Instead of pure round-robin, prioritize devices based on:
* Error magnitude: `SP_i - PV_i`
* Time since last heated
* Configured static priority
3. **Predictive Control:** Model the thermal dynamics (C_i, U_i) and predict future PV_i to make more informed decisions about when to start/stop heating, potentially anticipating overshoot or allocating budget more effectively.
4. **Duty Cycle Modulation:** If the heating elements support it (unlikely with simple `run`/`stop`), modulate the *power* P_i rather than just on/off state, allowing finer budget control.
5. **Adaptive Budget:** Adjust W_budget based on overall system state or external inputs.
## 8. Required Changes to OmronE5
* Add a public method `uint32_t getConsumption() const;` to `OmronE5.h`.
* Implement `OmronE5::getConsumption()` in `OmronE5.cpp` to return the value of the `_consumption` member variable.
```cpp
// In OmronE5.h
public:
// ... other methods ...
uint32_t getConsumption() const; // Add this
// In OmronE5.cpp
uint32_t OmronE5::getConsumption() const {
return _consumption; // Assuming _consumption holds Watts
}
```