Skip to content

feat: tomorrow schedule, observer readiness and phase safety - #722

Open
tintinz wants to merge 4 commits into
traktore-org:developfrom
tintinz:feat/tomorrow-schedule-phase-guard
Open

feat: tomorrow schedule, observer readiness and phase safety#722
tintinz wants to merge 4 commits into
traktore-org:developfrom
tintinz:feat/tomorrow-schedule-phase-guard

Conversation

@tintinz

@tintinz tintinz commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • adds a local-calendar Today | Tomorrow schedule with separate tariff/solar/EV data and localized preliminary/final publication status
  • persists an exact 72-hour Observer Mode readiness countdown without enabling automatic actuation
  • hardens dual-phase-guard notification handling so invalid phase data remains fail-closed while recovery debounce avoids notification spam
  • fixes the Energy Sankey EV node by using calendar-day EV energy rather than the charge-deadline bucket
  • ships updated card bundle and translations for all 16 dashboard locales

Safety

  • Observer Mode remains read-only after the countdown completes
  • invalid, missing, negative or over-limit RMS phase data blocks immediately
  • notification debounce does not delay safety decisions
  • no automatic merge, installation or activation requested

Verification

  • Python suite split by filename group: 3,887 passed, 2 skipped
  • focused regression checks after review: 17 passed
  • dashboard card tests: 62 passed
  • Rollup production build: passed
  • JSON/YAML/Python/JavaScript static validation: passed
  • translation key parity and placeholders verified across 16 locales
  • : passed

Notes

A monolithic pytest invocation exceeded the local 10-minute command limit at 84%; the complete suite was therefore run in four exhaustive filename groups, all green.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9f2a362c-55ab-43f1-8dba-7f26e3e2a06c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@traktore-org traktore-org left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks Patrik — one gate before the full review, so this is a fast fail rather than the deep pass.

CI is red. Both required test jobs fail on the same single test:

  • test (3.12) and test (3.13)tests/test_666_ev_accumulator_keys.py::TestAccumulatorKeyRoundTrip666::test_the_probe_actually_integrates
  • AssertionError: expected 7 daily categories, found ['battery_charge', 'battery_discharge', 'calendar_ev', 'ev', 'grid_export', 'grid_import', 'home', 'solar']

Root cause (verified against the diff and against develop): this PR adds a new daily_calendar_ev field to EnergyTotals — the calendar-day EV energy for the Sankey node. The #666 guard's _categories() derives straight from the dataclass fields, so the count went 7 → 8, and the tripwire at tests/test_666_ev_accumulator_keys.py:92 is pinned at == 7. The PR doesn't touch that file, so the tripwire trips.

That's the guard working as designed — it's a deliberate "a category changed, come confirm the round-trip still holds" tripwire (see its docstring), not a test to silence. To clear it:

  1. Bump the count assertion == 7== 8.
  2. Confirm calendar_ev genuinely round-trips (#666 / #668): if it's persisted, its daily/monthly/yearly keys must all be written and read under the same category and be registered in the shared persistence list. The parametrized test_daily_monthly_yearly_move_together[calendar_ev] case is present and passing in this same run (only the tripwire fails), so the read/write plumbing already looks correct — you mainly need the tripwire to acknowledge the new category. If calendar_ev is display-only (no monthly/yearly siblings, not persisted), a one-line note in the guard on why it's daily-only keeps the next reader honest.

One note on the description: it reports a green local run ("3,887 passed"), but CI is the gate here and both jobs are red on this one test — the four-way local filename split most likely didn't include the updated test_666 file. CI needs to go green before I run the full correctness + SEM-invariants review.

Re-request review once CI passes and I'll pick up the full adversarial pass — the change is otherwise well-scoped. Thanks again.

@traktore-org traktore-org left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks Patrik — the #666 tripwire from the earlier pass is cleanly resolved (the 7 → 8 bump with the "calendar_ev is intentionally daily-only" rationale is exactly right), CI is green, and the branch is rebased. I ran the full two-lens adversarial pass this time. Most of it holds up well; one item needs a fix before merge, plus one confirmed correctness bug and a few quality notes.

Held up under challenge

  • Observer countdown is genuinely advisory / fail-closed. Nothing in the tree reads observation_progress, ready_for_manual_activation, observation_remaining_seconds, or automatic_activation to arm control — all actuation gates still read the unchanged _observer_mode bool; automatic_activation is hardcoded False; missing/invalid/future start clamps to elapsed=0, ready=False. The legacy-install _persist_flag(True) on add is guarded by self._is_on, and _skip_options_reload prevents a reload loop. Matches the #702/#695/#698 spirit.
  • daily_calendar_ev is a correct, display-only surface of the pre-existing midnight_ev accumulator — no new persistence key (#668 intact), daily-only by design so no monthly/yearly siblings, last_reset resolves to midnight. The Sankey swap to sensor.sem_daily_calendar_ev_energy (×4, old sensor fully removed) is the right fix for the deadline-vs-calendar day-boundary class (#628/#703), no double-count / sign flip / negate.
  • No FLEET-READ / #589 regression — the new per-charger tomorrow-plan loop reuses already-computed per-charger locals; zero new power.ev_power reads or coord._ev_*/pcc writes.
  • The debounce never delays a genuine safety block — the recovery elif sits after if unsafe:, and the phase guard blocks independently of notifications. Translation parity is complete — all 8 new dashboard/translations.json keys present ×16 locales, {location}/{reason} placeholders preserved; daily_calendar_ev_energy name added to strings.json + all 16 translations/*.json.

Needs a change before merge

HIGH — coordinator/notifications.py, the incident_kind classifier in notify_phase_guard_transition — a mixed multi-phase reading masks a real over-limit as a sensor fault, suppressing the very escalation this PR is meant to protect.
stop_reason is a comma-joined list of every unsafe lane/phase (coordinator/dual_phase_guard.py:275",".join(reasons), e.g. "grid:l2:over_limit,inverter:l1:invalid_current"). The new classifier tests sensor-fault tokens first with any(token in reason ...), so any snapshot where one phase is genuinely over the limit and another phase's sensor is stale/invalid is classified sensor_fault. Consequences:

  • The over-limit escalation carve-out (unsafe and incident_kind == "over_limit" and _last != "over_limit") never fires, so a real over-current arriving during an open sensor-fault incident (same phase_guard_observer_warning/phase_guard_blocked state) is swallowed by the state == previous → return under the 300 s hold.
  • Even when it does notify, the message says "sensor data is invalid" for a genuine over-current.
    This is exactly the case your own comment forbids ("A real over-limit event must never be hidden behind a data-quality incident…"). On a 3-phase install, one phase over-limit while another phase's CT goes stale is an ordinary independent-failure combination. Fail-closed actuation is unaffected (this path is notification-only, the guard still blocks) — hence HIGH, not blocker — but a suppressed/mislabeled over-current alert is a real defect in the feature being added.
    Fix: let a real limit breach outrank a co-occurring data-quality token — check "over_limit" in reason before the sensor-fault tokens, or classify per-lane and treat "any lane over_limit" as over_limit. Drive the escalation guard off that.

HIGH (same root) — the two new tests pass vacuously w.r.t. this case. test_over_limit_escalates_immediately_during_sensor_fault_incident and test_invalid_sensor_incident_stays_open… use only single-token reasons, so they pass precisely because they avoid the mixed string that fails. Please add a case with reason="grid:l1:invalid_current,grid:l2:over_limit" asserting a second notification fires and the message names the over-limit.

For merge time (non-blocking, tick at your leisure)

  • MEDIUM — coordinator/energy_calculator.py _build_current_totals (~line 494) omits daily_calendar_ev. This is the gap/clock-backwards early-return path (called from lines 286/294). It re-reads daily_ev and every other field from accumulators but leaves daily_calendar_ev at the dataclass default 0.0. On a clock-backwards or large-gap cycle, daily_calendar_ev_energy publishes 0 → the Sankey EV node + its 3 links momentarily collapse to zero while daily_ev_energy stays correct, self-healing next clean cycle. Add energy.daily_calendar_ev = self._get_daily(MIDNIGHT_EV_CATEGORY, today) alongside the sibling reads.
  • MEDIUM — _PHASE_GUARD_SENSOR_FAULT_REASONS is incomplete and has a dead token. The guard also emits invalid_voltage, invalid_power, invalid_unit, not_configured, unsupported_topology, etc. (none listed → generic wording + no debounce), while no_sample is listed but never produced. Deriving the fault set from the actual guard tokens (or inverting: treat over_limit as the only non-fault) would make this robust — and folds neatly into the HIGH fix.
  • MEDIUM — Tomorrow tab can read "final" with zero price rows on 15-min providers. compose_tomorrow_plan filters tariff_upcoming, which is capped at 48 points (~12 h for 15-min providers like Tibber NL / ENTSO-E), so early in the day it never reaches tomorrow 00:00; the coordinator's schedule_tomorrow_status override can then flip status to "final" over an empty plan. Consider sourcing tomorrow's points from the provider day schedule rather than the 48-point cap.
  • MEDIUM — duplicate display name. daily_ev_energy and the new daily_calendar_ev_energy are both named "Today EV" and both enabled-by-default on every install (incl. EV-less ones). Give the new one a distinguishing name (e.g. "Today EV (calendar)") and/or entity_registry_enabled_default=False, since its stated purpose is the Sankey feed. Unique_ids are distinct, so this is display-only.
  • NIT — compose_tomorrow_plan drops all prices if now is tz-naive (naive day_start vs tz-aware ISO points → TypeError swallowed). Latent in prod (dt_util.now() is aware) but the helper is public/unit-tested with naive NOW. Normalize to tz-aware before comparison.
  • NIT — coordinator.py exception path sets tomorrow_plan/tomorrow_plan_status but not tomorrow_plan_date (reads as None downstream, no crash — set it for symmetry). The _fleet_tomorrow_plan is None fallback is also effectively dead (_plan_cfgs always has ≥1 entry) and would skip the status override if ever hit.
  • NIT — sem-schedule-card.js reads forecast_peak_time_tomorrow, which is not a real sensor (degrades to null; the tomorrow_plan solar_peak row is the real source). Remove the stale reference.

Really solid feature otherwise — the day-boundary reasoning, the advisory countdown design, and the persistence handling are all sound. Fix the mixed-reason classification (+ the mixed-reason test) and I'll re-run the pass. Thanks again, Patrik.

@traktore-org

Copy link
Copy Markdown
Owner

One more for the merge-time list, Patrik — found on a separate pass while checking this against the overnight-planner branch (which it merges cleanly with, no conflicts outside the generated bundle, and no shared state). This one is distinct from the "final over empty rows" MEDIUM already in the review: it's about the night-window endpoints, not the price points.

MEDIUM — compose_tomorrow_plan inherits today's night window, so Tomorrow loses its whole night story for most of the day.

The coordinator resolves the endpoints as a next occurrence (coordinator/coordinator.py:3773-3776get_offset_time(...), +1 day only when already past), then hands those same values to compose_tomorrow_plan, which anchors its inner call at now = day_start - 1µs (today_plan.py:391) and clips to tomorrow 00:00–24:00. The night row is gated on now < night_start < horizon (today_plan.py:228), and the EV night rows key off the same night_start.

So before tonight's window opens, night_start is today 21:04, which sorts before the tomorrow anchor and is dropped — along with everything that depends on it:

now=08:00  night_start passed in = 05.08 21:04  → rows: cheap_start@06T02:00, solar_peak@06T13:30
now=14:00  night_start passed in = 05.08 21:04  → rows: cheap_start@06T02:00, solar_peak@06T13:30
now=20:00  night_start passed in = 05.08 21:04  → rows: cheap_start@06T02:00, solar_peak@06T13:30
now=21:00  night_start passed in = 05.08 21:04  → rows: cheap_start@06T02:00, solar_peak@06T13:30
now=22:00  night_start passed in = 06.08 21:04  → rows: cheap_start, solar_peak, night_open@06T21:04,
                                                        ev_charge_start@06T21:04, ev_min_reached@06T23:01

(Reproduced against your head merged into develop, with a full 24 h of tomorrow price points supplied — status is final in every row above, so the 48-point cap isn't the limiter here. Night window 21:04–07:00, EV 8 kWh remaining at 4.1 kW.)

Two things fall out of that:

  1. night_open, ev_charge_start and ev_min_reached are absent from Tomorrow for ~21 hours a day — including the whole daytime stretch when someone would actually open a "Tomorrow" tab to decide whether to plug in. The EV night-charging story only appears after the window has already opened tonight, which is roughly when it stops being useful.
  2. The view isn't time-invariant. A fixed calendar-day forecast should render the same at 09:00 and at 22:00; here the row set depends on when you look, which will read as flapping.

Fix: re-anchor the endpoints onto the target day inside compose_tomorrow_plan rather than passing today's next-occurrence values through — shift night_start / night_end by whole days until each lands in [day_start, day_end). Worth a test pinning "same rows at 09:00 and at 22:00", since that invariant is the actual contract of a calendar-day view.

Unrelated note while I was in there: KIND_NIGHT_END is defined but never emitted by compose_today_plan — pre-existing on develop, not something this PR needs to touch, just flagging it since the tomorrow view would be the natural place for a "night ends" row.

@traktore-org traktore-org left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks Patrik — I re-ran the full two-lens pass against the new head (ed5b2762df, "fix tomorrow plan and phase guard regressions"). Almost everything from the last pass is cleanly resolved, and the two prior HIGHs are genuinely closed. One new issue rides in with the fix and needs a change before merge.

Held up under challenge (re-verified on this head)

  • Both prior HIGHs fixed. The classifier now reads incident_kind = "over_limit" if "over_limit" in reason else "sensor_fault" (coordinator/notifications.py) — a real limit breach in a comma-joined multi-phase reason (inverter:l1:invalid_current,grid:l2:over_limit) now outranks the co-occurring data-quality token. I enumerated every token dual_phase_guard.py/active_phase_guard.py can emit; the inversion is sound for all of them, and the empty/None-reason path can't carry a real over-current. The two tests are no longer vacuous — they use the mixed string and assert the message says "limit exceeded", not "sensor data".
  • control_authorized default flipped to False in the recovery elif is correctly fail-closed: enforcing snapshots always set the key, so a genuine armed-recovery still fires; a missing key can no longer close an open incident (test_missing_control_authorized_cannot_close_an_open_incident).
  • _build_current_totals gap/clock-backwards path now sets daily_calendar_ev from MIDNIGHT_EV_CATEGORY/today — the Sankey EV feed no longer collapses to 0 on a skipped cycle (test_gap_return_preserves_calendar_day_ev_total).
  • Tomorrow price sourcing now composes from all local-calendar-day points instead of the 48-point slice, and the "final" status override is gated on if not _tariff_dynamic: so a dynamic provider stays preliminary over an empty plan. is_dynamic is a real TariffData field (no AttributeError), _tariff_local_date/_tomorrow_price_date are consistently local. 15-min-day coverage is pinned by test_tomorrow_plan_keeps_late_blocks_from_full_15_minute_day.
  • The three prior NITs are closed: exception path now sets tomorrow_plan_date; tz-naive price handling no longer drops aware points (test_tomorrow_plan_handles_naive_now_with_aware_prices); the stale forecast_peak_time_tomorrow card read is gone.
  • Safety invariants all still hold: observer countdown is inert (no production reader arms control, automatic_activation hard-False, fail-closed clamps), no FLEET-READ/#589 regression in the tomorrow-plan loop, no new persistence key (#668 intact), sign conventions clean, translation parity verified independently across strings.json + 16 locales + the 8 dashboard keys.

Needs a change before merge

HIGH — the Sankey EV node is bound to an entity that is now disabled by default, so the feature this PR headlines ("fix the Energy Sankey EV node") is blank on a fresh install.

The follow-up commit resolved the duplicate-"Today EV" display-name note by adding both the rename and entity_registry_enabled_default=False to the daily_calendar_ev_energy description (sensor.py). But dashboard/sem_dashboard_template.yaml points the Sankey at sensor.sem_daily_calendar_ev_energy in 4 places (the EV node plus the Sol>EV / Grid>EV / Bat>EV children) and drops every reference to the old sensor.sem_daily_ev_energy — which was enabled by default.

SEMSolarSensor never force-enables (its DISABLED_BY_DEFAULT override set is empty), and the dashboard generator does not enable entities it references. So on a default install the mirror entity is registry-disabled → has no state → custom:sankey-chart renders the EV node and its three inbound flows as empty/zero. Result: the EV column silently disappears from the default Energy dashboard even while the car charged overnight — strictly worse than before the PR, where daily_ev_energy was enabled and showed data.

Note tests/test_dashboard_generator.py::test_energy_flow_uses_calendar_day_ev_total only asserts the YAML text (4 references, old sensor absent), so it passes while pinning the broken wiring rather than catching it.

Fix (either): drop entity_registry_enabled_default=False from daily_calendar_ev_energy — a shipped card needs it, so it should be a normal enabled daily sensor, and the rename to "Today EV (calendar day)" already fully resolves the duplicate-name concern on its own; or keep the Sankey pointed at the enabled sensor.sem_daily_ev_energy. Don't ship a card bound to a disabled entity. A test asserting the referenced entity is enabled would keep the two in sync.

For merge time (non-blocking)

  • MEDIUM — still open from your own Aug-5 note: compose_tomorrow_plan inherits today's night window (night_start/night_end are passed as today's next-occurrence, un-re-anchored), so night_open / ev_charge_start / ev_min_reached drop out of Tomorrow for ~21 h/day and the view isn't time-invariant. This head doesn't touch it (the only today_plan.py change is the tz-naive handling). The passing test_tomorrow_plan_keeps_overnight_ev_window_inside_calendar_day hand-anchors night_start=TOMORROW 00:00, so it doesn't exercise the coordinator's today-anchored value. Fix: shift night_start/night_end onto day_start's date inside compose_tomorrow_plan.
  • LOW / confirm intent — the sensor-fault 300 s recovery hold is evaluated before the armed-recovery check, so after a data-quality incident the "restored & armed" notification can lag the actual re-arm by up to 5 min. This looks like your intended anti-flap hysteresis (incident stays open until readings are continuously valid) rather than a bug — just flagging it so it's a deliberate choice.

Really strong iteration — the classifier fix, the fail-closed default, and the tomorrow-price sourcing are all exactly right. Just un-disable the mirror sensor (or repoint the Sankey) so the EV node lights up out of the box, and I'll re-run the pass. Thanks again, Patrik.

@traktore-org

Copy link
Copy Markdown
Owner

Separate from the review above — a note on the shape of the PR rather than any single finding.

Credit first: the test coverage is thorough, translation parity is complete across all 16 locales with the placeholders intact, and you turned the #666 tripwire around fast. None of what follows is about the quality of the work.

One request: could you pull the Sankey calendar-day EV fix into its own PR?

That piece is ready to merge as-is, and it's the only part of this branch that is. It's also a real bug rather than a feature: the Sankey's EV node is bucketed on the EV charge deadline while every other node on that chart is calendar-day, so between midnight and the deadline the EV branch still carries last night's charge while every source it is drawn from has already reset. A conservation diagram that doesn't conserve, out by roughly an overnight charge, every night, on every install using the default 07:00 deadline. We've filed the underlying class as #723 — your fix is the right one and we'd like it in soon rather than waiting on the rest of this branch.

The files are self-contained:

  • coordinator/energy_calculator.py, coordinator/types.py, sensor.py
  • dashboard/sem_dashboard_template.yaml (the four entity swaps)
  • tests/test_666_ev_accumulator_keys.py, tests/test_628_home_balance.py, tests/test_sensor.py
  • the daily_calendar_ev_energy name in strings.json + translations/*.json

None of them touch dashboard/card/src/, so that PR needs no bundle rebuild.

One thing to settle inside it: daily_ev_energy and daily_calendar_ev_energy would both be called "Today EV". They are on different clocks and a user cannot tell which they are reading — please give the new one a distinguishing name, and consider entity_registry_enabled_default=False since its stated job is feeding the Sankey.

Everything else stays right here. The Tomorrow schedule, observer countdown and phase-guard work can keep sharing this PR — just fix the classifier HIGH from the review in place. We are not asking you to break the branch into four.

Two small things for whichever PR they land in:

  • Run the whole Python suite rather than filename groups — the four-way split described in your PR body is what let the test_666 tripwire reach CI.
  • Each PR wants an issue to reference (our rule is that every commit links to one). There isn't an issue for any of these yet — say the word and we'll file them for you, with the context from these reviews already written up, so you only have to point the PRs at them.

If splitting even one piece off a finished branch is more hassle than it's worth, tell us and we'll do the extraction ourselves with your authorship preserved on the commit — just say which you'd prefer.

@traktore-org

Copy link
Copy Markdown
Owner

Heads-up on direction before you iterate further on the Tomorrow-schedule half — the review items on the observer/phase work stand unchanged, and the Sankey split-out request from earlier is unaffected. This is about where the schedule surface itself is heading.

On the 1.8 planner branch, the "Tonight's Plan" card has become the Energy Plan — one card rendering the planner's ledger for the whole energy day (daylight + night in one timeline: demands, verdicts, blocks, price bands, night window). As part of 1.8 it will absorb the schedule/today-plan cards, so the Control tab has one timeline fed by one data source.

Your Today | Tomorrow idea is being adopted into that card — genuinely: the toggle becomes the horizon selector, and your preliminary/final publication chip is exactly the honest detail the Tomorrow view needs. What changes is the data path: Tomorrow renders as a preview of the next energy day's ledger (prices anchored to the target day, forecast over tomorrow's real sun times), which structurally removes the time-invariance issue from the review (the today-anchored night_open rows) rather than patching it.

Practically: the tomorrow-schedule half of this PR would land on a surface that's being replaced, so we'd rather not have you polish it further. Options, your pick: (a) we fold the concept in with your authorship credited on the toggle/status-chip work, or (b) you build the toggle on the new card with us once the branch is public — we'll share the mockup and the entity contract. The observer-countdown and phase-guard halves are unaffected and continue here.

traktore-org added a commit that referenced this pull request Aug 8, 2026
…one data path

tintinz's Today|Tomorrow idea adopted (PR #722, direction posted): the
Energy Plan sensor now carries attrs.tomorrow — forecast surplus windows
(sine over tomorrow's real sun times), cheap windows, the
preliminary/final price status, night_open and the stamp time — built by
day_ledger.tomorrow_preview from the SAME slot machinery and the SAME
tariff accessors as the plan itself (tariff_price_at/tariff_cheap_at
extracted pure so night slots, day slots and the preview cannot drift
apart). Anchored to tomorrow's date throughout — which is what makes it
time-invariant, the #722 review's open MEDIUM being a tomorrow view
composed from today-anchored values. No demands, no verdicts: that plan
honestly does not exist until it stamps.
traktore-org added a commit that referenced this pull request Aug 8, 2026
The Energy Plan card is the Control tab's schedule now. Today|Tomorrow
toggle (tintinz's #722 idea, adopted with the preliminary/final price
chip and its explaining tooltips); the Tomorrow view renders the books
preview from attrs.tomorrow — surplus and cheap bands on the same strip
language plus the honest 'plan stamps at ~06:07' line. The Today view
gains the night-window shading and the now marker the retired Schedule
Timeline used to draw — computed from the same sensor the pending face
already read. sem-schedule-card removed from the template (the element
stays in the bundle for custom dashboards); placement pin now also pins
the RETIREMENT — a schedule card creeping back is a second data path.
8 i18n keys × 16, localize regenerated, bundle rebuilt.
@traktore-org traktore-org added 1.8: planner Rides the #638 energy-planner line (feature branch → 1.8.0) status: soaking Fix deployed, real-hardware verification running labels Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

1.8: planner Rides the #638 energy-planner line (feature branch → 1.8.0) status: soaking Fix deployed, real-hardware verification running

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants