"""OPT-11 P1: each battery's mode and on/off switch, as limits and weights. Pure: no I/O, no clock. main.py reads the store, calls these, and files the result as a `mode` claim (arbiter.P_MODE) and as the charge weights. DECISIONS/OPT-11-battery-modes.md has the record; OPT-11-design section 1 has the modes. Two rules hold for every mode, and they are what keeps modes safe: 1. A mode range always holds 0 W. A mode may forbid a direction, never force one, so batteries never move power in opposite directions because of a mode (allocator.unit_interval() drops a range without 0 as a software fault). 2. A mode is a limit, not a weight. A weight of 0 does not keep a battery out: allocator._fill()'s last-resort pass still uses it. So "may not" is a range, and weights only set the order among batteries that may. P1 has no price runs and no forecast (P3, P4). So both modes give power to the house at any hour, and the only mode that closes a direction is Off. What P1 changes is the solar order: while the site charges, Leftover solar batteries weigh 0, so they get solar only once the Solar first batteries are full or at their power limit. Selling (the owner's 2026-09-27 answer) is a per-battery choice that arrives with the price runs in P3; `PackMode.sell` and the `pack_sell` store key are reserved for it and read by nothing yet. """ from dataclasses import dataclass from .store import unit_key SOLAR_FIRST = "solar_first" LEFTOVER_SOLAR = "leftover_solar" CHEAP_HOURS = "cheap_hours" # reserved for P3; not accepted in P1 # The modes a battery may be set to in this release, in the order the page # lists them. The first is the default when the store has no value. MODES = (SOLAR_FIRST, LEFTOVER_SOLAR) DEFAULT_MODE = SOLAR_FIRST # Store keys, per battery: `unit//` (store.unit_key()). USE_KEY = "pack_use" MODE_KEY = "pack_mode" SELL_KEY = "pack_sell" # reserved for P3 @dataclass(frozen=True) class PackMode: """One battery's choice: on or off, its mode, and (P3) selling.""" use: bool = True mode: str = DEFAULT_MODE sell: bool = False # reserved for P3, never read in P1 @property def key(self) -> str: """The page's word for this state: a mode, or `off`.""" return self.mode if self.use else "off" def read(data: dict, name: str) -> PackMode: """Battery `name`'s choice from the store's data. A missing or unknown value falls back to the default (on, Solar first), never to Off: a store written by a later version must not switch a battery off here.""" use = data.get(unit_key(name, USE_KEY), True) mode = data.get(unit_key(name, MODE_KEY), DEFAULT_MODE) return PackMode(use=use is not False, mode=mode if mode in MODES else DEFAULT_MODE, sell=data.get(unit_key(name, SELL_KEY)) is True) def mode_range(pm: PackMode, rating_w: float) -> tuple: """`(lo, hi)` in W for this battery's mode claim. Always holds 0 W. Off asks for 0 W both ways. In P1 both modes may charge and give power up to the battery's rating: Leftover solar holds back its power only in the planned expensive hours, and there are none before P3. """ if not pm.use: return 0.0, 0.0 r = abs(float(rating_w)) return -r, r def charge_weights(base, modes: dict, target_w: float): """The allocator's weights with the solar order applied (design 3.5). `base` is packshare.weights()' answer (None for "all 1.0"). While the site charges (`target_w < 0`), a Leftover solar battery weighs 0 and a Solar first battery keeps its weight, so the fill gives the Solar first batteries everything they can take before the last-resort pass reaches the others. Giving power keeps `base` (P3 adds the expensive-run order). When no battery that is on would keep a positive weight (all Leftover solar, or every Solar first one at 0), `base` is returned unchanged, so the order among equals stays OPT-09's rather than an equal split. """ if float(target_w) >= 0.0 or not modes: return base names = sorted(modes) w = {n: (1.0 if base is None else float(base.get(n, 1.0))) for n in names} out = {n: (0.0 if modes[n].mode == LEFTOVER_SOLAR else w[n]) for n in names} if not any(out[n] > 0.0 for n in names if modes[n].use): return base return out