Skip to content

Commit 257a7d0

Browse files
authored
Update README to reflect current config values and scheduler behaviour (#28)
- Correct config code block: HEADROOM_TARGET_KWH 10.2→12.0, MORNING_HIGH_SOC_THRESHOLD_PERCENT 95→55, and four other stale values - Update HEADROOM_TARGET_KWH description to battery-capacity formula (BATTERY_KWH × 0.5) - Update MORNING_HIGH_SOC_THRESHOLD_PERCENT description to document 55% trigger and 10% gap above 45% floor - Update quick examples and export-to-grid formula section to use 12.0 kWh - Rewrite export-to-grid formula from old physics model to battery-capacity model - Rewrite state machine flowchart: remove bogus PreRestoreMode save step, fix orphaned EvaluateToGreen reference, add auto-extension path, add mid-period high-SOC safety export node - Update key state flows description to match revised flowchart
1 parent c472c0a commit 257a7d0

1 file changed

Lines changed: 55 additions & 47 deletions

File tree

‎README.md‎

Lines changed: 55 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ POLL_INTERVAL_MINUTES = 5
297297
FORECAST_REFRESH_INTERVAL_MINUTES = 30
298298
FORECAST_SOLAR_ARCHIVE_ENABLED = True
299299
FORECAST_SOLAR_ARCHIVE_INTERVAL_MINUTES = 30
300-
MAX_PRE_PERIOD_WINDOW_MINUTES = 120
300+
MAX_PRE_PERIOD_WINDOW_MINUTES = 180
301301
FULL_SIMULATION_MODE = True
302302
NIGHT_MODE_ENABLED = True
303303
LOCAL_TIMEZONE = "Europe/Dublin"
@@ -306,19 +306,19 @@ QUARTZ_GREEN_CAPACITY_FRACTION = 0.40
306306
FORECAST_SOLAR_POWER_MULTIPLIER = 1.53
307307
CHEAP_RATE_START_HOUR = 23
308308
CHEAP_RATE_END_HOUR = 8
309-
HEADROOM_TARGET_KWH = 10.2
309+
HEADROOM_TARGET_KWH = 12.0
310310
AMBER_HEADROOM_FRACTION = 0.25
311311
ENABLE_PRE_CHEAP_RATE_BATTERY_BRIDGE = True
312312
ESTIMATED_HOME_LOAD_KW = 0.8
313313
BRIDGE_BATTERY_RESERVE_KWH = 1.0
314314
MORNING_HIGH_SOC_PROTECTION_ENABLED = True
315-
MORNING_HIGH_SOC_THRESHOLD_PERCENT = 95.0
315+
MORNING_HIGH_SOC_THRESHOLD_PERCENT = 55.0
316316
LIVE_CLIPPING_RISK_VALID_PERIODS = "M,A"
317-
LIVE_CLIPPING_RISK_SOC_THRESHOLD_PERCENT = 90.0
318-
LIVE_CLIPPING_RISK_SOLAR_TRIGGER_KW = 4.0
317+
LIVE_CLIPPING_RISK_SOC_THRESHOLD_PERCENT = 50.0
318+
LIVE_CLIPPING_RISK_SOLAR_TRIGGER_KW = 3.2
319319
ENABLE_SUMMER_PRE_SUNRISE_DISCHARGE = True
320320
PRE_SUNRISE_DISCHARGE_MONTHS = "4,5,6,7,8,9"
321-
PRE_SUNRISE_DISCHARGE_LEAD_MINUTES = 90
321+
PRE_SUNRISE_DISCHARGE_LEAD_MINUTES = 120
322322
```
323323

324324
Meaning:
@@ -340,13 +340,13 @@ Meaning:
340340
- `QUARTZ_RED_CAPACITY_FRACTION`: lower Quartz status threshold as a fraction of configured array capacity
341341
- `QUARTZ_GREEN_CAPACITY_FRACTION`: upper Quartz status threshold as a fraction of configured array capacity
342342
- `FORECAST_SOLAR_POWER_MULTIPLIER`: scalar applied to Forecast.Solar watts before period status/value normalization; use this to correct persistent local bias (for example, historical under-forecasting)
343-
- `HEADROOM_TARGET_KWH`: fixed battery headroom target for Green periods (10.2 kWh = surplus capacity × 3 hours)
343+
- `HEADROOM_TARGET_KWH`: fixed battery headroom target for Green periods (12.0 kWh = BATTERY_KWH × 0.5)
344344
- `AMBER_HEADROOM_FRACTION`: fraction of battery capacity to maintain as free headroom before Amber periods (default `0.25`); `AMBER_HEADROOM_TARGET_KWH` is derived as `BATTERY_KWH * AMBER_HEADROOM_FRACTION`. Set to `0.0` to disable Amber headroom
345345
- `ENABLE_PRE_CHEAP_RATE_BATTERY_BRIDGE`: when enabled, Evening decisions avoid charge-oriented behavior before cheap-rate starts if battery can bridge the expected load
346346
- `ESTIMATED_HOME_LOAD_KW`: average household load estimate used to calculate whether current battery energy can cover consumption until cheap-rate begins
347347
- `BRIDGE_BATTERY_RESERVE_KWH`: safety buffer to keep in battery when evaluating bridge sufficiency
348348
- `MORNING_HIGH_SOC_PROTECTION_ENABLED`: enables high-SOC export protection rule for selected daytime periods
349-
- `MORNING_HIGH_SOC_THRESHOLD_PERCENT`: SOC threshold for the high-SOC export protection rule
349+
- `MORNING_HIGH_SOC_THRESHOLD_PERCENT`: SOC trigger threshold for the mid-period high-SOC safety export (default 55%, which is 10% above the 45% export floor set by `DAYTIME_TIMED_EXPORT_MIN_SOC_PERCENT`)
350350
- `LIVE_CLIPPING_RISK_VALID_PERIODS`: comma-separated period codes where live clipping-risk Amber→Green promotion is active (`M`=Morning, `A`=Afternoon, `E`=Evening). Only applies to the intra-tick live solar check.
351351
- `LIVE_CLIPPING_RISK_SOC_THRESHOLD_PERCENT`: SOC threshold for live clipping-risk Amber→Green promotion
352352
- `LIVE_CLIPPING_RISK_SOLAR_TRIGGER_KW`: rolling live-solar kW threshold for live clipping-risk promotion
@@ -483,8 +483,8 @@ For night:
483483

484484
### Quick examples
485485

486-
- Green + headroom < 10.2 kWh -> `GRID_EXPORT` (insufficient space for incoming solar).
487-
- Green + headroom >= 10.2 kWh -> Follow forecast mode (battery can absorb the solar).
486+
- Green + headroom < 12.0 kWh -> `GRID_EXPORT` (insufficient space for incoming solar).
487+
- Green + headroom >= 12.0 kWh -> Follow forecast mode (battery can absorb the solar).
488488
- Amber + Peak tariff -> `SELF_POWERED` (peak override wins).
489489
- Red + normal Day tariff -> `SELF_POWERED` (default forecast mapping).
490490

@@ -503,94 +503,102 @@ The runtime scheduler in `main.py` operates as a state machine running every 5 m
503503
```mermaid
504504
stateDiagram-v2
505505
[*] --> SchedulerLoop
506-
506+
507507
SchedulerLoop --> DayBoundaryCheck: Every 5 min tick
508-
508+
509509
DayBoundaryCheck --> RefreshDailyData: New calendar day?
510510
DayBoundaryCheck --> ForecastRefresh: Check intra-day refresh needed
511-
511+
512512
RefreshDailyData --> TimedExportCheck: Reset period action flags
513513
ForecastRefresh --> TimedExportCheck
514-
514+
515515
TimedExportCheck --> TimedExportActive: Timed export in progress?
516516
TimedExportCheck --> NightCheck: No active timed export
517-
518-
TimedExportActive --> RestoreWindow: Restore time reached OR SOC floor hit?
519-
RestoreWindow --> PreRestoreMode: Save current inverter mode
520-
PreRestoreMode --> Restore: Switch back to pre-export mode
517+
518+
TimedExportActive --> RestoreCheck: Restore time reached OR SOC floor hit?
519+
RestoreCheck --> AutoExtend: Window expired but SOC still above floor?
520+
AutoExtend --> BumpRestoreAt: YES - extend window (bump restore_at forward)
521+
BumpRestoreAt --> SkipNormalDecisions
522+
AutoExtend --> Restore: NO - restore to pre-export mode (saved at export start)
521523
Restore --> SkipNormalDecisions: Resume normal decisions next tick
522524
TimedExportActive --> SkipNormalDecisions: Still in export window
523-
525+
524526
SkipNormalDecisions --> SleepWithinTolerance
525-
527+
526528
NightCheck --> IsNight: Night mode enabled & in night window?
527529
NightCheck --> DaytimeEvaluation: Not in night
528-
530+
529531
IsNight --> NightModeContext: PRE-DAWN or EVENING-NIGHT window?
530532
NightModeContext --> SummerPreSunrise: PRE-DAWN window + summer discharge enabled?
531533
NightModeContext --> PreCheapRateExport: EVENING-NIGHT window + hours until cheap rate?
532-
534+
533535
SummerPreSunrise --> CheckSOC_PreSunrise: Check current SOC
534536
CheckSOC_PreSunrise --> SwitchToSelfPowered: SOC above minimum?
535537
CheckSOC_PreSunrise --> UseNightMode: SOC too low
536538
SwitchToSelfPowered --> ApplyMode_Night
537539
UseNightMode --> ApplyMode_Night
538-
540+
539541
PreCheapRateExport --> PlanExport: Calculate export duration
540542
PlanExport --> StartTimedExport: Conditions met?
541543
PlanExport --> ApplyMode_Night: No export planned
542544
StartTimedExport --> ApplyMode_Night
543545
ApplyMode_Night --> SleepWithinTolerance
544-
546+
545547
DaytimeEvaluation --> OrderPeriods: Get today's periods in order
546548
OrderPeriods --> EvaluatePeriod: For each period (Morn/Aftn/Eve)
547-
548-
EvaluatePeriod --> PrePeriodWindow: Within MAX_PRE_PERIOD_WINDOW of period start?
549-
550-
PrePeriodWindow --> CheckHeadroom: PRE-PERIOD stage check
549+
550+
EvaluatePeriod --> MidPeriodHighSOC: Period started & high_soc_export_set=False?
551+
MidPeriodHighSOC --> StartHighSOCExport: SOC > 55% threshold?
552+
StartHighSOCExport --> MarkHighSOCSet: Start bounded export (floor=45%), set high_soc_export_set=True
553+
MarkHighSOCSet --> SkipRestOfPeriods
554+
MidPeriodHighSOC --> PrePeriodWindow: SOC below threshold or flag already set
555+
556+
PrePeriodWindow --> CheckHeadroom: Within MAX_PRE_PERIOD_WINDOW of period start?
557+
551558
CheckHeadroom --> CalcHeadroomDeficit: Current SOC vs headroom target
552-
553559
CalcHeadroomDeficit --> DeficitExceeded: Deficit > 0?
554-
DeficitExceeded --> TriggerPreExport: YES - start GRID_EXPORT for headroom
560+
DeficitExceeded --> TriggerPreExport: YES - start GRID_EXPORT for headroom (saves restore mode)
555561
TriggerPreExport --> TimedExportSet: Pre-export window set
556562
TimedExportSet --> SkipRestOfPeriods: Mark period as pre-set, continue
557563
DeficitExceeded --> WaitForPeriodStart: NO - headroom OK
558-
564+
559565
WaitForPeriodStart --> PeriodStartWindow: At or after period start time?
560-
566+
561567
PeriodStartWindow --> CheckClipping: PERIOD-START stage check
562568
CheckClipping --> EvaluateClipping: Live solar clipping risk detected?
563-
569+
564570
EvaluateClipping --> PromoteToGreen: Live solar near ceiling & SOC high?
565-
EvaluateToGreen --> ApplyPeriodMode: Use promoted status
571+
PromoteToGreen --> ApplyPeriodMode: Use promoted status
566572
EvaluateClipping --> ApplyPeriodMode: Use forecast status
567-
573+
568574
ApplyPeriodMode --> DecideMode: Apply decision_logic to status & SOC
569575
DecideMode --> ModeResult: [TOU|SELF_POWERED|GRID_EXPORT|AI]?
570-
576+
571577
ModeResult --> CheckClippingExport: GRID_EXPORT + clipping risk?
572-
CheckClippingExport --> StartClippingTimed: YES - bounded timed export
578+
CheckClippingExport --> StartClippingTimed: YES - bounded timed export (saves restore mode)
573579
CheckClippingExport --> MarkPeriodComplete: NO - regular mode
574580
StartClippingTimed --> MarkPeriodComplete
575-
581+
576582
MarkPeriodComplete --> SkipRestOfPeriods: Mark period as start-set
577-
583+
578584
SkipRestOfPeriods --> ArchiveTelemetry: Collect inverter snapshot
579585
ArchiveTelemetry --> SleepWithinTolerance: Sleep POLL_INTERVAL_SECONDS
580586
SleepWithinTolerance --> [*]
581587
```
582588

583589
**Key state flows:**
584590

585-
- **Timed Export Override**: Once active (from pre-export or clipping), all normal scheduler decisions are skipped until the export window expires or SOC floor is reached (early exit)
586-
- **Day Boundary**: Triggers forecast and sunrise/sunset refresh; resets per-period action flags (pre-set, start-set)
591+
- **Timed Export Override**: Once active (from pre-export, clipping, or high-SOC), all normal scheduler decisions are skipped until the export window expires or SOC floor is reached (early exit). The restore target mode is saved at export start, not at restore time.
592+
- **Auto-Extension**: If the export window expires but SOC is still above the floor, the restore timestamp is bumped forward rather than restoring — this avoids immediately re-triggering the same export on the next tick.
593+
- **Day Boundary**: Triggers forecast and sunrise/sunset refresh; resets per-period action flags (pre-set, start-set, clipping-export-set, high-soc-export-set)
587594
- **Night Branching**: If night mode is enabled, either handles summer pre-sunrise discharge (PRE-DAWN) or pre-cheap-rate export (EVENING-NIGHT)
588595
- **Daytime Evaluation**: For each period in order:
589-
1. **PRE-PERIOD**: Checks headroom deficit; if buffer shortfall detected, triggers bounded GRID_EXPORT
590-
2. **PERIOD-START**: Detects live clipping risk (solar near inverter ceiling) and promotes forecast status; applies final mode decision
596+
1. **MID-PERIOD HIGH-SOC**: If the period has started and SOC is above the trigger threshold (55%) and the one-shot flag is not yet set, starts a bounded GRID_EXPORT down to the 45% floor; fires at most once per period
597+
2. **PRE-PERIOD**: Checks headroom deficit; if buffer shortfall detected, triggers bounded GRID_EXPORT
598+
3. **PERIOD-START**: Detects live clipping risk (solar near inverter ceiling) and promotes forecast status; applies final mode decision
591599
- **Live Clipping Promotion**: If Amber forecast but live solar is near ceiling and SOC is high, runtime system promotes it to Green for export decisions
592600
- **Mode Application**: Switches inverter to decided mode via Sigen API and records telemetry
593-
- **Restore**: Timed export windows track pre-export mode and automatically restore when complete
601+
- **Restore**: Timed export windows automatically restore to the mode that was active when the export started
594602

595603
### Official OpenAPI integration (`integrations/sigen_official.py`)
596604

@@ -664,10 +672,10 @@ The headroom target varies by forecast status:
664672
| Amber | `BATTERY_KWH × 0.25` (e.g. 6 kWh) | `AMBER_HEADROOM_TARGET_KWH` (derived from `AMBER_HEADROOM_FRACTION`) |
665673
| Red | — (no headroom export) | — |
666674

667-
The Green target is derived from hardware surplus capacity multiplied by a 3-hour reserve window:
675+
The Green target is a fixed fraction of the battery, ensuring enough free space to absorb a full day's solar without clipping:
668676

669677
$$
670-
\text{headroom target (Green)} = (\text{solar PV kW} - \text{inverter kW}) \times 3.0 = (8.9 - 5.5) \times 3.0 = 10.2 \text{ kWh}
678+
\text{headroom target (Green)} = \text{BATTERY\_KWH} \times 0.5 = 24 \times 0.5 = 12.0 \text{ kWh}
671679
$$
672680

673681
The Amber target is a reduced fraction of the battery, reflecting that Amber days carry real but moderate solar risk — enough to warrant partial headroom, but not the full Green reserve. Setting `AMBER_HEADROOM_FRACTION = 0.0` disables Amber headroom entirely.
@@ -679,7 +687,7 @@ $$
679687

680688
then the system selects `GRID_EXPORT` to create battery space ahead of the solar period.
681689

682-
**Why this model is superior:** The old fraction-based model (0.25 × forecast) was fundamentally broken because it relied on ESB synthetic forecast categories (500W "Green" label) rather than real solar predictions. This led to ultra-small targets (0.375 kWh at SOC=100%) and essentially no early lead time. The physics model instead grounds the target in actual hardware constraints: *how much surplus power can the inverter not distribute?* This is deterministic, testable, and produces realistic lead times (~2 hours at SOC=100% for Green periods).
690+
**Why this model is superior:** The old fraction-based model (0.25 × forecast) was fundamentally broken because it relied on ESB synthetic forecast categories (500W "Green" label) rather than real solar predictions. This led to ultra-small targets (0.375 kWh at SOC=100%) and essentially no early lead time. The fixed battery-capacity model instead grounds the target in the battery size itself: *what fraction of the battery must be free to absorb expected solar on a good day?* This is deterministic, testable, and produces realistic lead times (~2 hours at SOC=100% for Green periods).
683691

684692
### Day and peak tariff influence
685693

0 commit comments

Comments
 (0)