Metadata-Version: 2.4
Name: gridhour
Version: 0.3.0
Summary: When British electricity is green and cheap: a 48 hour carbon intensity and Agile price timeline for the terminal
License-Expression: MIT
Project-URL: Homepage, https://github.com/777dimas/gridhour
Project-URL: Issues, https://github.com/777dimas/gridhour/issues
Project-URL: Security, https://github.com/777dimas/gridhour/security/policy
Project-URL: Changelog, https://github.com/777dimas/gridhour/blob/main/CHANGELOG.md
Keywords: carbon intensity,octopus agile,electricity,national grid,tui,terminal,tmux
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Utilities
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# gridhour

[![PyPI](https://img.shields.io/pypi/v/gridhour)](https://pypi.org/project/gridhour/)
[![Python](https://img.shields.io/pypi/pyversions/gridhour)](https://pypi.org/project/gridhour/)
[![CI](https://github.com/777dimas/gridhour/actions/workflows/ci.yml/badge.svg)](https://github.com/777dimas/gridhour/actions/workflows/ci.yml)
[![CodeQL](https://github.com/777dimas/gridhour/actions/workflows/codeql.yml/badge.svg)](https://github.com/777dimas/gridhour/actions/workflows/codeql.yml)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/777dimas/gridhour/badge)](https://scorecard.dev/viewer/?uri=github.com/777dimas/gridhour)

When is British electricity green and cheap? gridhour shows the next 48 hours of carbon
intensity for your postcode and Octopus Agile prices on one timeline in the terminal, then
picks the best time to start the washing, charge the car or kick off a batch job.

```console
$ gridhour --line
⚡ 142g · 14p · green in 2h
```

![gridhour demo: scrubbing the 48 hour timeline, jumping to the best window, switching ranking and themes](https://raw.githubusercontent.com/777dimas/gridhour/main/docs/demo.gif)

## What you see

![gridhour main view](https://raw.githubusercontent.com/777dimas/gridhour/main/docs/main.png)

* **The chart.** Carbon intensity (gCO₂/kWh) rises above the time axis, the Agile unit price
  hangs below it. Green is clean or cheap, red is dirty or dear, blue is a plunge price where
  Octopus pays you to use power. The hours already gone are dimmed.
* **Generation mix.** Wind, solar, gas, nuclear and the rest, as shares of your region's
  supply, on the same time axis. Handy for seeing *why* tomorrow afternoon is clean.
* **Best time to run.** One row per job. Each row is shaded by how good every start time is,
  and the bright block is the best window, with its average carbon and price next to it. The
  status line tells you how much that saves against starting right now.
* **A cursor** runs straight down through all of it. Move it with the arrow keys to read any
  half hour; `Enter` jumps to the selected job's best window.

Jobs are ranked by carbon, by price, or by both (each normalised over the 48 hours and
averaged). `w` switches between them.

### Deadlines and jobs that can run in pieces

A washing machine runs in one go and rarely has to finish by a set time. A car has to be charged
before you leave, and it doesn't care whether it charges in one block. A job can say both:

```
Washing      2h
EV charge    4h by 07:00 split
Battery      3h by 16:00 split
```

* **`by 07:00`** means the window has to finish by the next 07:00, UK time. If it is already
  past 07:00, that means tomorrow's. An amber `┤` marks the deadline on the job's row. If there
  isn't enough forecast before it, gridhour says so rather than suggesting a time after it.
* **`split`** lets gridhour pick the best half hours before the deadline in any order, so a dear
  half hour sitting between two cheap ones gets skipped. It only splits when the pieces really
  are better than one continuous block.

Type them in the add box (`a`), or select a job and press `b` for the deadline and `s` to
allow pieces.

| Scrubbed to tomorrow morning | Compact layout for a tmux split |
| --- | --- |
| ![scrubbing](https://raw.githubusercontent.com/777dimas/gridhour/main/docs/scrub.png) | ![compact](https://raw.githubusercontent.com/777dimas/gridhour/main/docs/compact.png) |

## Install

You need Python 3.11 or newer, a terminal with truecolor and a UTF-8 locale. Linux and macOS;
on Windows use WSL.

The simplest way is [pipx](https://pipx.pypa.io/), which installs the `gridhour` command for your
user in its own environment:

```sh
pipx install gridhour                                        # from PyPI
pipx install git+https://github.com/777dimas/gridhour        # or straight from GitHub
gridhour SW1A 1AA
```

Either line is enough. The second one is there for when PyPI is unreachable; it builds the latest
code on `main`, which can be ahead of the release.

No pipx yet? Get it from your system's package manager:

| System | Install pipx |
| --- | --- |
| Debian, Ubuntu | `sudo apt install pipx` |
| Fedora | `sudo dnf install pipx` |
| Rocky Linux, AlmaLinux | `sudo dnf install epel-release && sudo dnf install pipx` |
| RHEL | [enable EPEL](https://docs.fedoraproject.org/en-US/epel/getting-started/), then `sudo dnf install pipx` |
| openSUSE Tumbleweed | `sudo zypper install python313-pipx` |
| macOS | `brew install pipx` |

If your shell cannot find `gridhour` afterwards, run `pipx ensurepath` once and open a new
terminal.

**gridhour needs Python 3.11 or newer.** Some long-term-support systems ship an older default:
RHEL, Rocky and AlmaLinux 9 come with 3.9 (version 10 is fine), and openSUSE Leap 15 with 3.6.
There, install a newer Python next to the system one (`sudo dnf install python3.11` on the EL 9
family, `sudo zypper install python311` on Leap 15) and point pipx at it:

```sh
pipx install --python python3.11 gridhour
```

To try it without installing: `pipx run gridhour SW1A` or `uvx gridhour SW1A`.

The postcode you give is remembered, so after the first run plain `gridhour` is enough. Only
the first half (`SW1A`) is ever used or sent anywhere. Northern Ireland is not on the GB grid,
so BT postcodes have no forecast.

### Updating and removing

```sh
pipx upgrade gridhour
gridhour --reset && pipx uninstall gridhour    # --reset first deletes the saved settings and cache
```

`gridhour --version` shows what you have.

### Other ways

* **pip**, in a virtual environment: `pip install gridhour`.
* **From a clone**, nothing to build: `python3 -m gridhour SW1A`.
* **A specific release from GitHub**: `pipx install git+https://github.com/777dimas/gridhour@v0.3.0`.

Every release on PyPI carries signed build provenance; [SECURITY.md](https://github.com/777dimas/gridhour/blob/main/SECURITY.md#supply-chain)
shows how to check a file against it.

### Not on Octopus yet?

**Ad · referral link.** If you switch your home to Octopus Energy with
[my referral link](https://share.octopus.energy/cheeky-melon-215), we each get £50 account
credit (as of October 2026). gridhour works exactly the same whether you use it or not, and it
isn't affiliated with Octopus.

## Usage

```sh
gridhour                           # your saved postcode
gridhour M1 1AE                    # a new postcode (saved)
gridhour --region scotland         # a region instead, for this run only
gridhour --mode green              # rank by carbon only (also: cheap, both)
gridhour --no-prices               # skip Agile, for people on a flat tariff
gridhour --at 2026-10-04T18:00Z    # start with the cursor at a given time
gridhour --theme slate --12h
gridhour --reset                   # delete the saved postcode, jobs, settings and the cache
```

Keys inside the app (`?` lists them all):

| Key | Action |
| --- | --- |
| `←` `→` | move the cursor 30 minutes, Shift or `H` `L` for 2 hours |
| `Home` `End` `r` | start and end of the forecast, back to now |
| `↑` `↓` | select a job |
| `+` `-` | make the selected job 30 minutes longer or shorter |
| `a` `d` | add a job (`Dryer 1h30`, `EV 4h by 07:00 split`), delete the selected one |
| `b` `s` | selected job: finish by a time, allow it to run in pieces |
| `Enter` | jump to the selected job's best window |
| `w` | rank by carbon, price, or both |
| `p` | change postcode |
| `m` `c` `T` `t` | mix rows, compact layout, theme, 12/24h |
| `R` | refetch now |
| `q` | quit |

Settings and jobs live in `~/.config/gridhour/config.json`.

## Status bars and scripts

```console
$ gridhour --line
⚡ 142g · 14p · green in 2h

$ gridhour --line --best 3h
⚡ 142g · 14p · 3h best 13:00 (in 4h30)

$ gridhour --tmux                  # same, with tmux colour codes for status-right
$ gridhour --watch                 # same, updating in place
$ gridhour --json | jq '.jobs[] | {name, start: .best.from}'
```

"Green" means NESO's forecast grades the half hour as *low* or *very low* for your region.
If nothing in the next 48 hours makes that grade, the line says `greenest in 9h` instead.

`--json` gives the current half hour, the next green slot, the best window for every job (its
deadline and the pieces it runs in included, with
the carbon and price you would get starting now, for comparison) and the full 48 hour series.

Export the best job windows to an iCalendar file:

```sh
gridhour --ical > jobs.ics
```

Import `jobs.ics` into your calendar app. Each continuous part of a job's best
window becomes a separate event. Events include the best window's average carbon
intensity and price, with UTC timestamps and stable UIDs for repeat imports.

A tmux example:

```tmux
set -g status-right '#(gridhour --tmux) '
set -g status-interval 300
```

## Where the numbers come from

* **Carbon intensity** and the **generation mix**: the [Carbon Intensity API](https://carbonintensity.org.uk/)
  from the National Energy System Operator (NESO), regional forecast by postcode. Free, no key,
  CC BY 4.0.
* **Prices**: the public [Octopus Energy API](https://developer.octopus.energy/), the Agile
  import tariff for your region, including VAT. Octopus publishes the next day's prices at about
  4pm, so before that the price half of the chart stops around 11pm tonight and the "cheap"
  ranking only looks that far ahead.

These are forecasts. The carbon forecast for tomorrow afternoon can be off by a fair
margin; Agile prices, once published, are what you pay. gridhour is not affiliated with Octopus
Energy or NESO.

Responses are cached in `~/.cache/gridhour` for 30 minutes. If the network is down the app
keeps showing the cached data and says how old it is. `GRIDHOUR_OFFLINE=1` stops all network
access.

## Security and privacy

gridhour talks to those two APIs over HTTPS and nothing else (redirects are refused), sends only
the first half of your postcode and your region letter, and has no runtime dependencies.
Responses are checked field by field before they are used or cached, and text from them never
reaches your terminal or tmux with escape sequences or format codes in it. Details and how to
report a problem: [SECURITY.md](https://github.com/777dimas/gridhour/blob/main/SECURITY.md).

## Development

```sh
python3 -m venv .venv && . .venv/bin/activate
pip install --require-hashes -r requirements/dev.txt && pip install --no-deps -e .
pytest
```

The tests replay real API responses from `tests/fixtures/`; sockets are blocked while they run.
More in [CONTRIBUTING.md](https://github.com/777dimas/gridhour/blob/main/CONTRIBUTING.md).

| Module | What it does |
| --- | --- |
| `grid.py` | the two APIs, regions and postcodes, the cache |
| `plan.py` | scoring half hours, best windows, "green in 2h" |
| `compose.py` | turns the state into a frame |
| `canvas.py` | character grid with colours, rendered to ANSI |
| `safe.py` | cleaning outside text, private atomic file writes |
| `app.py`, `keys.py` | the terminal loop and every key |
| `output.py` | `--line`, `--tmux`, `--json`, `--ical`, `--watch` |
| `state.py`, `themes.py`, `cli.py` | settings, colours, arguments |

## Licence

MIT, see [LICENSE](https://github.com/777dimas/gridhour/blob/main/LICENSE).
