Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Hackathon entry: "Noonshift" — a CPO-side, deadline-based EV charging schedule
## Stack & Commands
- Python 3.12/3.13 · FastAPI · scipy 1.14 (HiGHS LP) · mobilityhouse/ocpp · asyncpg/Postgres (optional) · Docker Compose. Two Vite front-ends: `web/` (ops dashboard, React+JSX+Tailwind 3, :5173) and `wattwise/` (driver app, React+TS+Tailwind 4, :5174); `npm install && npm run dev` in each, both proxy `/api` and `/ws` to :8000. `npm run build` in each, then `docker compose up` serves them on :3000 / :3001.
- `python -m venv .venv && .venv/Scripts/pip install -r requirements-dev.txt`
- `python -m pytest` (63 tests, ~60 s) · `python scripts/prove.py` (slide 1) · `python -m noonshift.test_loop` · `docker compose up -d --build`
- `python -m pytest` (69 tests, ~90 s) · `python scripts/prove.py` (slide 1) · `python -m noonshift.test_loop` · `docker compose up -d --build`
- Data: `python -m noonshift.seed gen` (placeholders) · `python scripts/fetch_data.py signal|caiso|sessions --day ...` (real).

## Current State & Focus
Expand All @@ -25,9 +25,9 @@ Signals (WattTime MOER; CAISO fuel-mix fallback) + tariff table + sessions (dead

## File Map
- `noonshift/scheduler.py` — `solve(cars, site, signal, tariff, now)`, `solve_lp()` (+info), `impact()`, `impact_detail()`, `price(slack, *, r, saving_usd, kwh, alpha, urgent)` (§7b: R − α·S/E, ≤ R), `tier_of()`; per-car optional `floor_alpha`, `priority`; constants W_CARBON, ALPHA, FLOOR_EVERY, FIRST_HOUR_KWH (3.5), SPRINT_SLOTS, BUFFER_COST, M_SHORT, M_FLOOR, ASAP_EPS, E_MIN, MIN_KW. Slot 0 carries only its remaining minutes (`dur[0]`); floor checkpoints are times since arrival, pro rata in the slot containing the mark.
- `noonshift/api.py` — FastAPI app, state dict `S`, `resolve()`, `step()`, `session_impact()`, `meter_history()`, ladder `pick_mode()`, `/demo/*`; `POST /sessions/{id}/urgency {level: now|soon|priority, leave_at?}` (`/boost` = alias of `now`; `URGENCY_MIN` 15 min; `PRIORITY` = floor 0.9 / weight 2.0); `site_rate()` = `site.json: employee_rate_usd_per_kwh` (0 = free); `session_price()`.
- `noonshift/sim.py` — `Sim`, `Connector(id, p_max_kw, safe_kw)` (taper from 80% SoC; plug-in starts on `safe_kw`; a `set_limit` expires after `DYN_VALID_MIN`=15 min back to `safe_kw`), `load_sessions()`, `safe_share_kw(site)` = min(p_max, (feed − max building)/n) = 1.833 kW on site-1.
- `noonshift/models.py` — pydantic REST bodies + WS frames (contract with front-end). `SignalHour` added for `GET /grid/signal` (hourly MOER g/kWh + $/kWh; frames unchanged).
- `noonshift/api.py` — FastAPI app, state dict `S`, `resolve()`, `step()`, `session_impact()`, `meter_history()`, ladder `pick_mode()`, `/demo/*`; `POST /sessions/{id}/urgency {level: now|soon|priority, leave_at?}` (`/boost` = alias of `now`; `URGENCY_MIN` 15 min; `PRIORITY` = floor 0.9 / weight 2.0); `site_rate()` = `site.json: employee_rate_usd_per_kwh` (0 = free); `session_price()`; `car_kw(c)` = min(bay, `session.max_kw`); `need_estimate()` (bay history median → site median); `observe_caps()` (5 min under 80 % of limit → `max_kw` = metered × 1.05, event `cap_observed`); `move_by()` (waiting cars → slackest plugged cars get deadline = now + e_rem/p_max, one per waiting car); `site.json: connectors_asap` bays are always `boost`.
- `noonshift/sim.py` — `Sim`, `Connector(id, p_max_kw, safe_kw)` (taper from 80% SoC; plug-in starts on `safe_kw`; a `set_limit` expires after `DYN_VALID_MIN`=15 min back to `safe_kw`), `load_sessions()`, `safe_share_kw(site)` = min(p_max, (feed − max building)/n) = 1.833 kW on site-1. `Sim.waiting` (arrivals with no bay; plugged FIFO as bays free); a `done` car with `idle_since` ≥ `MOVE_AFTER_MIN`=10 is unplugged when someone waits (A5); `session.car_kw` = the car's physical limit (sim only; `max_kw` is the planner's belief).
- `noonshift/models.py` — pydantic REST bodies + WS frames (contract with front-end). `SessionIn` optional `vehicle{model,battery_kwh,max_kw}`, `soc_now`, `target_soc`; `SessionOut` +`kwh_needed`, `need_confidence`, `max_kw`, `urgency`; `ConnectorMeter` +`idle_min`, `need_confidence`, `p_max_kw`, `cap_observed`, `asap`, `move_by`, `urgency`; `MeterMsg.waiting`; `StatusOut` +`safe_share_kw`, `waiting`, `connectors_asap`; `EventMsg.name` +`urgency|done|cap_observed|move_by`. All additive.
- `web/src/context/GlobalStateContext.jsx` — ops app state: opens `/ws`, polls `/sites/site-1/impact|status`, derives the page shape (siteDetail/connectors Gantt on a 06–22 axis, sessions, alerts from events, chargersList); `triggerEvent()` maps demo buttons → `/demo/*`, prioritize → `/sessions/{id}/boost`.
- `.docs/ROADMAP.md` — plain-language plan (§0 status, §1 driver inputs, §3 emergency rules, §4 price flip, §5 evidence runs, §7 CI, §8 order). `.github/workflows/ci.yml` — backend job + web/wattwise matrix.
- `wattwise/src/api/noonshift.ts` — driver app client + hand-copied types; `connectWs()`, `onSimDay()` (ready-by is built on the **sim** day and must be > sim now).
Expand Down Expand Up @@ -79,6 +79,7 @@ Signals (WattTime MOER; CAISO fuel-mix fallback) + tariff table + sessions (dead
- 2026-09-12 — Session data = ACN 2019-04-09 re-dated onto the 2026 signal day; kwh_needed = delivered energy, stated departure = the driver's own input (early leavers kept).

## Changelog
2026-09-13 | WP3 cars, needs, queues | models.py, sim.py, api.py, data/site.json, tests/test_day.py, scripts/smoke.py | vehicle form → per-car cap + kWh from SoC; observation guard believes the meter; connectors_asap c41/c42; done event + idle_min; Sim.waiting + move-by tightening; a notified done car moves within 10 min when someone waits
2026-09-13 | WP2 fail-safe an inspector accepts: static share under everything | sim.py, api.py, scheduler.py, ocpp_gateway.py, models.py, data/site.json, tests/test_day.py | `full` rung, `apply_limits` fallback and the linprog fallback all return `safe_share_kw`; kill-the-loop test: 6 h with no step() never exceeds headroom and after 15 min never exceeds n × share; `StatusOut.safe_share_kw`
2026-09-13 | WP1 pricing + emergency + first-hour floor | scheduler.py, api.py, models.py, data/site.json, tests/, scripts/smoke.py, .docs/implementation-plan.md | price() = §7b shared savings (never above R); urgency endpoint; slot-0 remaining-duration + time-anchored checkpoints fixed a lost-energy bug that the 3.5 kWh promise exposed
2026-09-13 | Open-source docs pass: README rewrite (Mermaid, quick start, results, OCPP 1.6J badge), LICENSE/CONTRIBUTING/CODE_OF_CONDUCT/SECURITY/CHANGELOG, issue+PR templates, .docs/ARCHITECTURE.md (6 validated diagrams), .docs/README.md index; all human docs moved to .docs/; business.md (break points, §4b emergency scale at R, §7b shared-savings formula), metrics.md, solutions.md (real-world fixes → code; drivers under-state stay 29/36; prove.py plans with perfect foresight) | README.md, .docs/, LICENSE, CONTRIBUTING.md, SECURITY.md, CHANGELOG.md, .github/ | All Mermaid blocks validated with mermaid@11 + jsdom; ROADMAP emergency pricing reconciled to "R, no quota".
Expand Down
2 changes: 1 addition & 1 deletion data/site.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"id": "site-1", "feed_kw": 150, "block_kw": 100, "connectors": ["c01", "c02", "c03", "c04", "c05", "c06", "c07", "c08", "c09", "c10", "c11", "c12", "c13", "c14", "c15", "c16", "c17", "c18", "c19", "c20", "c21", "c22", "c23", "c24", "c25", "c26", "c27", "c28", "c29", "c30", "c31", "c32", "c33", "c34", "c35", "c36", "c37", "c38", "c39", "c40", "c41", "c42", "c43", "c44", "c45", "c46", "c47", "c48", "c49", "c50", "c51", "c52", "c53", "c54", "c55", "c56", "c57", "c58", "c59", "c60"], "p_max_kw": 7.0, "building_load_kw": [20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 21.7, 23.3, 25.0, 26.7, 28.3, 30.0, 31.7, 33.3, 35.0, 36.7, 38.3, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 38.3, 36.7, 35.0, 33.3, 31.7, 30.0, 28.3, 26.7, 25.0, 23.3, 21.7, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0], "employee_rate_usd_per_kwh": 0.25, "driver_share": 0.5, "noonshift_share": 0.2, "safe_share_kw": 1.833}
{"id": "site-1", "feed_kw": 150, "block_kw": 100, "connectors": ["c01", "c02", "c03", "c04", "c05", "c06", "c07", "c08", "c09", "c10", "c11", "c12", "c13", "c14", "c15", "c16", "c17", "c18", "c19", "c20", "c21", "c22", "c23", "c24", "c25", "c26", "c27", "c28", "c29", "c30", "c31", "c32", "c33", "c34", "c35", "c36", "c37", "c38", "c39", "c40", "c41", "c42", "c43", "c44", "c45", "c46", "c47", "c48", "c49", "c50", "c51", "c52", "c53", "c54", "c55", "c56", "c57", "c58", "c59", "c60"], "p_max_kw": 7.0, "building_load_kw": [20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 21.7, 23.3, 25.0, 26.7, 28.3, 30.0, 31.7, 33.3, 35.0, 36.7, 38.3, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 38.3, 36.7, 35.0, 33.3, 31.7, 30.0, 28.3, 26.7, 25.0, 23.3, 21.7, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0], "employee_rate_usd_per_kwh": 0.25, "driver_share": 0.5, "noonshift_share": 0.2, "safe_share_kw": 1.833, "connectors_asap": ["c41", "c42"]}
117 changes: 117 additions & 0 deletions docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,36 @@
}
],
"title": "Kwh Needed"
},
"vehicle": {
"anyOf": [
{
"$ref": "#/components/schemas/Vehicle"
},
{
"type": "null"
}
]
},
"soc_now": {
"anyOf": [
{
"type": "number",
"maximum": 1.0,
"minimum": 0.0
},
{
"type": "null"
}
],
"title": "Soc Now"
},
"target_soc": {
"type": "number",
"maximum": 1.0,
"exclusiveMinimum": 0.0,
"title": "Target Soc",
"default": 1.0
}
},
"type": "object",
Expand Down Expand Up @@ -1088,6 +1118,38 @@
}
],
"title": "Urgency"
},
"kwh_needed": {
"type": "number",
"title": "Kwh Needed",
"default": 0.0
},
"need_confidence": {
"anyOf": [
{
"type": "string",
"enum": [
"declared",
"history",
"site"
]
},
{
"type": "null"
}
],
"title": "Need Confidence"
},
"max_kw": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Max Kw"
}
},
"type": "object",
Expand Down Expand Up @@ -1189,6 +1251,19 @@
"type": "number",
"title": "Safe Share Kw",
"default": 0.0
},
"waiting": {
"type": "integer",
"title": "Waiting",
"default": 0
},
"connectors_asap": {
"items": {
"type": "string"
},
"type": "array",
"title": "Connectors Asap",
"default": []
}
},
"type": "object",
Expand Down Expand Up @@ -1261,6 +1336,48 @@
"type"
],
"title": "ValidationError"
},
"Vehicle": {
"properties": {
"model": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Model"
},
"battery_kwh": {
"anyOf": [
{
"type": "number",
"exclusiveMinimum": 0.0
},
{
"type": "null"
}
],
"title": "Battery Kwh"
},
"max_kw": {
"anyOf": [
{
"type": "number",
"exclusiveMinimum": 0.0
},
{
"type": "null"
}
],
"title": "Max Kw"
}
},
"type": "object",
"title": "Vehicle",
"description": "What the driver tells us about the car. All optional: the brain plans with the bay's limit when it knows nothing."
}
}
}
Expand Down
50 changes: 50 additions & 0 deletions docs/ws-frames.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,48 @@
],
"default": null,
"title": "Urgency"
},
"idle_min": {
"default": 0,
"title": "Idle Min",
"type": "integer"
},
"need_confidence": {
"anyOf": [
{
"enum": [
"declared",
"history",
"site"
],
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Need Confidence"
},
"p_max_kw": {
"default": 0.0,
"title": "P Max Kw",
"type": "number"
},
"cap_observed": {
"default": false,
"title": "Cap Observed",
"type": "boolean"
},
"asap": {
"default": false,
"title": "Asap",
"type": "boolean"
},
"move_by": {
"default": false,
"title": "Move By",
"type": "boolean"
}
},
"required": [
Expand Down Expand Up @@ -131,6 +173,9 @@
"deadline",
"boost",
"urgency",
"done",
"cap_observed",
"move_by",
"dr",
"demo",
"mode",
Expand Down Expand Up @@ -195,6 +240,11 @@
},
"title": "Connectors",
"type": "array"
},
"waiting": {
"default": 0,
"title": "Waiting",
"type": "integer"
}
},
"required": [
Expand Down
Loading
Loading