Skip to content

Add PeakShavingConfig validation and combined-mode fallback#336

Merged
MaStr merged 4 commits intomainfrom
claude/fix-issue-328-review-DyNaz
Apr 15, 2026
Merged

Add PeakShavingConfig validation and combined-mode fallback#336
MaStr merged 4 commits intomainfrom
claude/fix-issue-328-review-DyNaz

Conversation

@MaStr
Copy link
Copy Markdown
Owner

@MaStr MaStr commented Apr 15, 2026

Summary

Introduces a new PeakShavingConfig dataclass with comprehensive validation and a fallback mechanism for combined-mode peak shaving when price_limit is not configured. This improves configuration error reporting and enables graceful degradation instead of complete disabling of peak shaving.

Key Changes

  • New PeakShavingConfig dataclass in src/batcontrol/core.py:

    • Validates mode, allow_full_battery_after, and price_limit at construction time
    • Raises ValueError with clear config-key-based messages (e.g., peak_shaving.mode) instead of failing later in CalculationParameters
    • Includes a from_config() factory method for loading from configuration dictionaries
    • Emits a one-time warning at config load when combined mode is enabled without price_limit
  • Combined-mode fallback behavior:

    • When mode='combined' and price_limit=None, the peak shaving now falls back to time-only behaviour instead of disabling entirely
    • The time component does not require price_limit, so the counter-linear ramp continues to work
    • mode='price' with price_limit=None still disables peak shaving entirely (no fallback available)
  • Shared validation constant:

    • Extracted PEAK_SHAVING_VALID_MODES = ('time', 'price', 'combined') to logic_interface.py as a single source of truth
    • Both CalculationParameters and PeakShavingConfig now reference this constant
  • Updated peak shaving logic in src/batcontrol/logic/next.py:

    • Implements the fallback: when price_limit=None in combined mode, switches to time-only internally
    • Maintains debug-level logging to avoid per-cycle spam (user is warned once at config load)
    • Clarified comments and improved documentation
  • Comprehensive test coverage in tests/batcontrol/test_peak_shaving_config.py:

    • Tests for valid/invalid modes, hour boundaries, and price_limit types
    • Tests for from_config() factory method
    • Tests for the one-time warning (only fires when enabled + combined + no price_limit)
  • Documentation updates:

    • Updated config example and wiki to clarify that price_limit is optional for combined mode
    • Documented that mode and price_limit are not settable at runtime via MQTT

Notable Implementation Details

  • Validation happens in PeakShavingConfig.__post_init__(), ensuring errors are caught at config load time rather than during calculation
  • The warning for combined-mode fallback is intentionally one-time (at config load) to inform users without spamming logs during operation
  • The fallback mechanism preserves the time-based limiter's counter-linear ramp calculation, which self-corrects based on free capacity

https://claude.ai/code/session_017PthEyvtN4XrYFf2MS5RP8

claude added 3 commits April 15, 2026 06:11
- next.py: in 'combined' mode fall back to time-only when price_limit
  is None and log a warning, so the time component is no longer silently
  disabled. 'price' mode still skips when price_limit is missing.
- next.py: replace 'MODE 8' inverter-layer reference with a descriptive
  comment about limit_battery_charge_rate mode.
- core.py: add PeakShavingConfig.__post_init__ validation for mode,
  allow_full_battery_after, and price_limit. Invalid values now raise a
  ValueError referencing the peak_shaving.* config key instead of
  failing later in CalculationParameters.
- docs: document that 'mode' and 'price_limit' are not settable at
  runtime via MQTT (restart required) and describe the combined-mode
  fallback. Mirror the note in batcontrol_config_dummy.yaml comments.
- tests: fix the counter-linear formula comment in
  test_currently_in_cheap_slot_surplus_overflow; split the 'price_limit
  is None' case into combined (fallback) and price (skip) tests; add
  test_peak_shaving_config.py covering the new validation.
run_tests.sh uses `uv pip install -e .[test]` which writes a uv.lock
into the working tree. The file is not intended to be tracked (no
history of it in the repo), so ignore it alongside the other lock-file
entries.
…fallback warning)

- logic_interface.py: introduce module-level PEAK_SHAVING_VALID_MODES
  constant and reference it from CalculationParameters.__post_init__
  (removes the inline ('time','price','combined') duplicate).
- logic/__init__.py: re-export PEAK_SHAVING_VALID_MODES for consumers.
- core.py: PeakShavingConfig uses the shared constant instead of its
  own class-level VALID_MODES. Emit the combined+missing-price_limit
  fallback warning here (once at config load) so we do not spam the log
  every 3 minutes.
- next.py: demote the runtime fallback log to debug. The user-visible
  warning is already produced at startup by PeakShavingConfig.
- tests: add four cases around the startup warning (fires on
  enabled+combined+None, silent when disabled, when price_limit is set,
  and in time mode).
Copilot AI review requested due to automatic review settings April 15, 2026 06:39
@MaStr MaStr added this to the 0.8.0 milestone Apr 15, 2026
@MaStr MaStr added the enhancement New feature or request label Apr 15, 2026
@MaStr MaStr self-assigned this Apr 15, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds early validation for peak shaving configuration and updates peak shaving runtime behavior so mode='combined' gracefully falls back to time-only limiting when price_limit is not configured.

Changes:

  • Introduces PeakShavingConfig validation (with warning) and wires it into core config loading.
  • Extracts PEAK_SHAVING_VALID_MODES as a shared constant and updates peak shaving logic to support combined-mode fallback.
  • Expands tests and updates docs/config examples to reflect the new behavior and limitations.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/batcontrol/core.py Adds PeakShavingConfig validation + config loading + warning behavior.
src/batcontrol/logic/next.py Implements combined-mode fallback to time-only when price_limit is None.
src/batcontrol/logic/logic_interface.py Centralizes valid peak shaving modes in PEAK_SHAVING_VALID_MODES.
src/batcontrol/logic/__init__.py Re-exports PEAK_SHAVING_VALID_MODES.
tests/batcontrol/test_peak_shaving_config.py New unit tests for config validation and warning behavior.
tests/batcontrol/logic/test_peak_shaving.py Updates/extends logic tests for fallback and revised combined behavior.
docs/WIKI_peak_shaving.md Documents combined-mode optional price_limit and MQTT runtime limitations.
config/batcontrol_config_dummy.yaml Updates example comments to match new config semantics.
.gitignore Ignores uv.lock.

Comment thread src/batcontrol/core.py
Comment thread src/batcontrol/core.py
Comment thread src/batcontrol/logic/__init__.py Outdated
…ine length)

- core.py PeakShavingConfig.__post_init__: reject bool for both
  price_limit and allow_full_battery_after (bool is a subclass of int
  in Python; an accidental ``price_limit: true`` would otherwise pass
  validation and become 1.0). Also enforce int type for the hour value.
- core.py PeakShavingConfig.from_config: wrap float() coercion in
  try/except so unparseable strings, lists, dicts and similar raise a
  ValueError prefixed with peak_shaving.price_limit (matching the
  key-based error policy of the other validators). bool is passed
  through untouched so __post_init__ rejects it with the same prefix.
- logic/__init__.py: split the long re-export line across multiple
  lines (was ~175 chars after PEAK_SHAVING_VALID_MODES was added).
- tests: cover bool rejection for both numeric fields, string and
  list rejection from_config (with key-prefixed error), and the
  numeric-string ('0.05') happy-path coercion.

Triage: the remaining Copilot suggestion to also strict-type
``enabled`` (e.g. reject quoted ``"false"``) is intentionally not
implemented here -- YAML parses ``false`` to a bool natively, and
HomeAssistant-form-field string coercion is a separate convention
already used elsewhere in core.py (see time_resolution_minutes).
Copy link
Copy Markdown
Owner Author

MaStr commented Apr 15, 2026

Triage of Copilot review comments

Pushed 71dc926 — three of four sub-points fixed; one deferred with rationale.

# Reviewer comment Decision Action
1 from_config() calls float(price_limit_raw) — generic ValueError/TypeError for unparseable strings/lists/dicts, no peak_shaving.price_limit prefix FIX from_config now wraps the coercion in try/except and re-raises a ValueError prefixed with peak_shaving.price_limit. bool is short-circuited so it reaches __post_init__ and is rejected there with the same prefix.
2a Numeric validation accepts bool (subclass of int); peak_shaving.price_limit: true becomes 1.0 FIX __post_init__ now rejects bool for price_limit.
2b allow_full_battery_after not strictly type-checked (a bool would slip through as int) FIX __post_init__ now requires int and explicitly rejects bool for the hour value.
2c enabled: "false" (quoted string) silently truthy SKIP YAML parses false to a Python bool natively. HA-form-field string coercion is a separate convention already in use elsewhere in core.py (see time_resolution_minutes at core.py:120-124); doing it just for enabled here would be inconsistent and is out of scope for issue #328.
3 logic/__init__.py:2 import line ~175 chars after PEAK_SHAVING_VALID_MODES was added — would trip pylint line-length FIX Split across multiple lines with parentheses.

Tests: 7 new cases (bool rejection for both numeric fields, unparseable string from_config, list from_config, bool from_config, numeric-string '0.05' happy path) on top of the existing suite. Full run: 474 passed.

https://claude.ai/code/session_017PthEyvtN4XrYFf2MS5RP8


Generated by Claude Code

@MaStr MaStr merged commit d26920a into main Apr 15, 2026
13 checks passed
@MaStr MaStr deleted the claude/fix-issue-328-review-DyNaz branch April 15, 2026 11:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants