314 lines
16 KiB
Markdown
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
|
|
}
|
|
``` |