gridhour 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gridhour-0.1.0/CHANGELOG.md +22 -0
- gridhour-0.1.0/LICENSE +21 -0
- gridhour-0.1.0/MANIFEST.in +4 -0
- gridhour-0.1.0/PKG-INFO +189 -0
- gridhour-0.1.0/README.md +163 -0
- gridhour-0.1.0/SECURITY.md +65 -0
- gridhour-0.1.0/gridhour/__init__.py +3 -0
- gridhour-0.1.0/gridhour/__main__.py +3 -0
- gridhour-0.1.0/gridhour/app.py +224 -0
- gridhour-0.1.0/gridhour/canvas.py +108 -0
- gridhour-0.1.0/gridhour/cli.py +126 -0
- gridhour-0.1.0/gridhour/compose.py +561 -0
- gridhour-0.1.0/gridhour/grid.py +417 -0
- gridhour-0.1.0/gridhour/keys.py +143 -0
- gridhour-0.1.0/gridhour/output.py +131 -0
- gridhour-0.1.0/gridhour/plan.py +184 -0
- gridhour-0.1.0/gridhour/safe.py +67 -0
- gridhour-0.1.0/gridhour/state.py +104 -0
- gridhour-0.1.0/gridhour/themes.py +111 -0
- gridhour-0.1.0/gridhour.egg-info/PKG-INFO +189 -0
- gridhour-0.1.0/gridhour.egg-info/SOURCES.txt +37 -0
- gridhour-0.1.0/gridhour.egg-info/dependency_links.txt +1 -0
- gridhour-0.1.0/gridhour.egg-info/entry_points.txt +2 -0
- gridhour-0.1.0/gridhour.egg-info/top_level.txt +1 -0
- gridhour-0.1.0/pyproject.toml +71 -0
- gridhour-0.1.0/setup.cfg +4 -0
- gridhour-0.1.0/tests/conftest.py +61 -0
- gridhour-0.1.0/tests/fixtures/agile_c.json +1 -0
- gridhour-0.1.0/tests/fixtures/carbon_sw1a.json +1 -0
- gridhour-0.1.0/tests/fixtures/products.json +1 -0
- gridhour-0.1.0/tests/test_app.py +67 -0
- gridhour-0.1.0/tests/test_canvas.py +58 -0
- gridhour-0.1.0/tests/test_cli.py +52 -0
- gridhour-0.1.0/tests/test_compose.py +89 -0
- gridhour-0.1.0/tests/test_grid.py +107 -0
- gridhour-0.1.0/tests/test_keys.py +105 -0
- gridhour-0.1.0/tests/test_output.py +48 -0
- gridhour-0.1.0/tests/test_plan.py +104 -0
- gridhour-0.1.0/tests/test_security.py +325 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes are listed here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
4
|
+
and the project uses [semantic versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-10-04
|
|
9
|
+
|
|
10
|
+
First release.
|
|
11
|
+
|
|
12
|
+
* 48 hour timeline of regional carbon intensity and Octopus Agile prices for a UK postcode.
|
|
13
|
+
* Best start time for each job (washing, dishwasher, EV charge, batch job, or your own), ranked by
|
|
14
|
+
carbon, price or both.
|
|
15
|
+
* Generation mix rows (wind, solar, gas, nuclear, ...) on the same time axis.
|
|
16
|
+
* `--line`, `--tmux`, `--watch` and `--json` for status bars and scripts.
|
|
17
|
+
* Six themes (carbon, daylight, slate, ember, mono, colorblind), 12/24 hour clock, compact layout
|
|
18
|
+
for small panes.
|
|
19
|
+
* Works offline from cache. Talks only to the two APIs over HTTPS.
|
|
20
|
+
|
|
21
|
+
[Unreleased]: https://github.com/777dimas/gridhour/compare/v0.1.0...HEAD
|
|
22
|
+
[0.1.0]: https://github.com/777dimas/gridhour/releases/tag/v0.1.0
|
gridhour-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 the gridhour authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
gridhour-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gridhour
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: When British electricity is green and cheap: a 48 hour carbon intensity and Agile price timeline for the terminal
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/777dimas/gridhour
|
|
7
|
+
Project-URL: Issues, https://github.com/777dimas/gridhour/issues
|
|
8
|
+
Project-URL: Security, https://github.com/777dimas/gridhour/security/policy
|
|
9
|
+
Project-URL: Changelog, https://github.com/777dimas/gridhour/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: carbon intensity,octopus agile,electricity,national grid,tui,terminal,tmux
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
13
|
+
Classifier: Operating System :: MacOS
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# gridhour
|
|
28
|
+
|
|
29
|
+
[](https://github.com/777dimas/gridhour/actions/workflows/ci.yml)
|
|
30
|
+
[](https://github.com/777dimas/gridhour/actions/workflows/codeql.yml)
|
|
31
|
+
[](https://scorecard.dev/viewer/?uri=github.com/777dimas/gridhour)
|
|
32
|
+
|
|
33
|
+
When is British electricity green and cheap? gridhour shows the next 48 hours of carbon
|
|
34
|
+
intensity for your postcode and Octopus Agile prices on one timeline in the terminal, then
|
|
35
|
+
picks the best time to start the washing, charge the car or kick off a batch job.
|
|
36
|
+
|
|
37
|
+
```console
|
|
38
|
+
$ gridhour --line
|
|
39
|
+
⚡ 142g · 14p · green in 2h
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+

|
|
43
|
+
|
|
44
|
+
## What you see
|
|
45
|
+
|
|
46
|
+
* **The chart.** Carbon intensity (gCO₂/kWh) rises above the time axis, the Agile unit price
|
|
47
|
+
hangs below it. Green is clean or cheap, red is dirty or dear, blue is a plunge price where
|
|
48
|
+
Octopus pays you to use power. The hours already gone are dimmed.
|
|
49
|
+
* **Generation mix.** Wind, solar, gas, nuclear and the rest, as shares of your region's
|
|
50
|
+
supply, on the same time axis. Handy for seeing *why* tomorrow afternoon is clean.
|
|
51
|
+
* **Best time to run.** One row per job. Each row is shaded by how good every start time is,
|
|
52
|
+
and the bright block is the best window, with its average carbon and price next to it. The
|
|
53
|
+
status line tells you how much that saves against starting right now.
|
|
54
|
+
* **A cursor** runs straight down through all of it. Move it with the arrow keys to read any
|
|
55
|
+
half hour; `Enter` jumps to the selected job's best window.
|
|
56
|
+
|
|
57
|
+
Jobs are ranked by carbon, by price, or by both (each normalised over the 48 hours and
|
|
58
|
+
averaged). `w` switches between them.
|
|
59
|
+
|
|
60
|
+
| Scrubbed to tomorrow morning | Compact layout for a tmux split |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
|  |  |
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
You need Python 3.11 or newer, a terminal with truecolor and a UTF-8 locale. Linux and macOS;
|
|
67
|
+
on Windows use WSL.
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
pipx install git+https://github.com/777dimas/gridhour
|
|
71
|
+
gridhour SW1A 1AA
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Once a release is on PyPI this becomes `pipx install gridhour`. From a clone there is nothing to
|
|
75
|
+
build: `python3 -m gridhour SW1A`.
|
|
76
|
+
|
|
77
|
+
The postcode you give is remembered, so after the first run plain `gridhour` is enough. Only
|
|
78
|
+
the first half (`SW1A`) is ever used or sent anywhere. Northern Ireland is not on the GB grid,
|
|
79
|
+
so BT postcodes have no forecast.
|
|
80
|
+
|
|
81
|
+
## Usage
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
gridhour # your saved postcode
|
|
85
|
+
gridhour M1 1AE # a new postcode (saved)
|
|
86
|
+
gridhour --region scotland # a region instead, for this run only
|
|
87
|
+
gridhour --mode green # rank by carbon only (also: cheap, both)
|
|
88
|
+
gridhour --no-prices # skip Agile, for people on a flat tariff
|
|
89
|
+
gridhour --at 2026-10-04T18:00Z # start with the cursor at a given time
|
|
90
|
+
gridhour --theme slate --12h
|
|
91
|
+
gridhour --reset # delete the saved postcode, jobs, settings and the cache
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Keys inside the app (`?` lists them all):
|
|
95
|
+
|
|
96
|
+
| Key | Action |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `←` `→` | move the cursor 30 minutes, Shift or `H` `L` for 2 hours |
|
|
99
|
+
| `Home` `End` `r` | start and end of the forecast, back to now |
|
|
100
|
+
| `↑` `↓` | select a job |
|
|
101
|
+
| `+` `-` | make the selected job 30 minutes longer or shorter |
|
|
102
|
+
| `a` `d` | add a job (`Dryer 1h30`), delete the selected one |
|
|
103
|
+
| `Enter` | jump to the selected job's best window |
|
|
104
|
+
| `w` | rank by carbon, price, or both |
|
|
105
|
+
| `p` | change postcode |
|
|
106
|
+
| `m` `c` `T` `t` | mix rows, compact layout, theme, 12/24h |
|
|
107
|
+
| `R` | refetch now |
|
|
108
|
+
| `q` | quit |
|
|
109
|
+
|
|
110
|
+
Settings and jobs live in `~/.config/gridhour/config.json`.
|
|
111
|
+
|
|
112
|
+
## Status bars and scripts
|
|
113
|
+
|
|
114
|
+
```console
|
|
115
|
+
$ gridhour --line
|
|
116
|
+
⚡ 142g · 14p · green in 2h
|
|
117
|
+
|
|
118
|
+
$ gridhour --line --best 3h
|
|
119
|
+
⚡ 142g · 14p · 3h best 13:00 (in 4h30)
|
|
120
|
+
|
|
121
|
+
$ gridhour --tmux # same, with tmux colour codes for status-right
|
|
122
|
+
$ gridhour --watch # same, updating in place
|
|
123
|
+
$ gridhour --json | jq '.jobs[] | {name, start: .best.from}'
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
"Green" means NESO's forecast grades the half hour as *low* or *very low* for your region.
|
|
127
|
+
If nothing in the next 48 hours makes that grade, the line says `greenest in 9h` instead.
|
|
128
|
+
|
|
129
|
+
`--json` gives the current half hour, the next green slot, the best window for every job (with
|
|
130
|
+
the carbon and price you would get starting now, for comparison) and the full 48 hour series.
|
|
131
|
+
|
|
132
|
+
A tmux example:
|
|
133
|
+
|
|
134
|
+
```tmux
|
|
135
|
+
set -g status-right '#(gridhour --tmux) '
|
|
136
|
+
set -g status-interval 300
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Where the numbers come from
|
|
140
|
+
|
|
141
|
+
* **Carbon intensity** and the **generation mix**: the [Carbon Intensity API](https://carbonintensity.org.uk/)
|
|
142
|
+
from the National Energy System Operator (NESO), regional forecast by postcode. Free, no key,
|
|
143
|
+
CC BY 4.0.
|
|
144
|
+
* **Prices**: the public [Octopus Energy API](https://developer.octopus.energy/), the Agile
|
|
145
|
+
import tariff for your region, including VAT. Octopus publishes the next day's prices at about
|
|
146
|
+
4pm, so before that the price half of the chart stops around 11pm tonight and the "cheap"
|
|
147
|
+
ranking only looks that far ahead.
|
|
148
|
+
|
|
149
|
+
These are forecasts. The carbon forecast for tomorrow afternoon can be off by a fair
|
|
150
|
+
margin; Agile prices, once published, are what you pay. gridhour is not affiliated with Octopus
|
|
151
|
+
Energy or NESO.
|
|
152
|
+
|
|
153
|
+
Responses are cached in `~/.cache/gridhour` for 30 minutes. If the network is down the app
|
|
154
|
+
keeps showing the cached data and says how old it is. `GRIDHOUR_OFFLINE=1` stops all network
|
|
155
|
+
access.
|
|
156
|
+
|
|
157
|
+
## Security and privacy
|
|
158
|
+
|
|
159
|
+
gridhour talks to those two APIs over HTTPS and nothing else (redirects are refused), sends only
|
|
160
|
+
the first half of your postcode and your region letter, and has no runtime dependencies.
|
|
161
|
+
Responses are checked field by field before they are used or cached, and text from them never
|
|
162
|
+
reaches your terminal or tmux with escape sequences or format codes in it. Details and how to
|
|
163
|
+
report a problem: [SECURITY.md](https://github.com/777dimas/gridhour/blob/main/SECURITY.md).
|
|
164
|
+
|
|
165
|
+
## Development
|
|
166
|
+
|
|
167
|
+
```sh
|
|
168
|
+
python3 -m venv .venv && . .venv/bin/activate
|
|
169
|
+
pip install --require-hashes -r requirements/dev.txt && pip install --no-deps -e .
|
|
170
|
+
pytest
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The tests replay real API responses from `tests/fixtures/`; sockets are blocked while they run.
|
|
174
|
+
More in [CONTRIBUTING.md](https://github.com/777dimas/gridhour/blob/main/CONTRIBUTING.md).
|
|
175
|
+
|
|
176
|
+
| Module | What it does |
|
|
177
|
+
| --- | --- |
|
|
178
|
+
| `grid.py` | the two APIs, regions and postcodes, the cache |
|
|
179
|
+
| `plan.py` | scoring half hours, best windows, "green in 2h" |
|
|
180
|
+
| `compose.py` | turns the state into a frame |
|
|
181
|
+
| `canvas.py` | character grid with colours, rendered to ANSI |
|
|
182
|
+
| `safe.py` | cleaning outside text, private atomic file writes |
|
|
183
|
+
| `app.py`, `keys.py` | the terminal loop and every key |
|
|
184
|
+
| `output.py` | `--line`, `--tmux`, `--json`, `--watch` |
|
|
185
|
+
| `state.py`, `themes.py`, `cli.py` | settings, colours, arguments |
|
|
186
|
+
|
|
187
|
+
## Licence
|
|
188
|
+
|
|
189
|
+
MIT, see [LICENSE](https://github.com/777dimas/gridhour/blob/main/LICENSE).
|
gridhour-0.1.0/README.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# gridhour
|
|
2
|
+
|
|
3
|
+
[](https://github.com/777dimas/gridhour/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/777dimas/gridhour/actions/workflows/codeql.yml)
|
|
5
|
+
[](https://scorecard.dev/viewer/?uri=github.com/777dimas/gridhour)
|
|
6
|
+
|
|
7
|
+
When is British electricity green and cheap? gridhour shows the next 48 hours of carbon
|
|
8
|
+
intensity for your postcode and Octopus Agile prices on one timeline in the terminal, then
|
|
9
|
+
picks the best time to start the washing, charge the car or kick off a batch job.
|
|
10
|
+
|
|
11
|
+
```console
|
|
12
|
+
$ gridhour --line
|
|
13
|
+
⚡ 142g · 14p · green in 2h
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
## What you see
|
|
19
|
+
|
|
20
|
+
* **The chart.** Carbon intensity (gCO₂/kWh) rises above the time axis, the Agile unit price
|
|
21
|
+
hangs below it. Green is clean or cheap, red is dirty or dear, blue is a plunge price where
|
|
22
|
+
Octopus pays you to use power. The hours already gone are dimmed.
|
|
23
|
+
* **Generation mix.** Wind, solar, gas, nuclear and the rest, as shares of your region's
|
|
24
|
+
supply, on the same time axis. Handy for seeing *why* tomorrow afternoon is clean.
|
|
25
|
+
* **Best time to run.** One row per job. Each row is shaded by how good every start time is,
|
|
26
|
+
and the bright block is the best window, with its average carbon and price next to it. The
|
|
27
|
+
status line tells you how much that saves against starting right now.
|
|
28
|
+
* **A cursor** runs straight down through all of it. Move it with the arrow keys to read any
|
|
29
|
+
half hour; `Enter` jumps to the selected job's best window.
|
|
30
|
+
|
|
31
|
+
Jobs are ranked by carbon, by price, or by both (each normalised over the 48 hours and
|
|
32
|
+
averaged). `w` switches between them.
|
|
33
|
+
|
|
34
|
+
| Scrubbed to tomorrow morning | Compact layout for a tmux split |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
|  |  |
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
You need Python 3.11 or newer, a terminal with truecolor and a UTF-8 locale. Linux and macOS;
|
|
41
|
+
on Windows use WSL.
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
pipx install git+https://github.com/777dimas/gridhour
|
|
45
|
+
gridhour SW1A 1AA
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Once a release is on PyPI this becomes `pipx install gridhour`. From a clone there is nothing to
|
|
49
|
+
build: `python3 -m gridhour SW1A`.
|
|
50
|
+
|
|
51
|
+
The postcode you give is remembered, so after the first run plain `gridhour` is enough. Only
|
|
52
|
+
the first half (`SW1A`) is ever used or sent anywhere. Northern Ireland is not on the GB grid,
|
|
53
|
+
so BT postcodes have no forecast.
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
gridhour # your saved postcode
|
|
59
|
+
gridhour M1 1AE # a new postcode (saved)
|
|
60
|
+
gridhour --region scotland # a region instead, for this run only
|
|
61
|
+
gridhour --mode green # rank by carbon only (also: cheap, both)
|
|
62
|
+
gridhour --no-prices # skip Agile, for people on a flat tariff
|
|
63
|
+
gridhour --at 2026-10-04T18:00Z # start with the cursor at a given time
|
|
64
|
+
gridhour --theme slate --12h
|
|
65
|
+
gridhour --reset # delete the saved postcode, jobs, settings and the cache
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Keys inside the app (`?` lists them all):
|
|
69
|
+
|
|
70
|
+
| Key | Action |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| `←` `→` | move the cursor 30 minutes, Shift or `H` `L` for 2 hours |
|
|
73
|
+
| `Home` `End` `r` | start and end of the forecast, back to now |
|
|
74
|
+
| `↑` `↓` | select a job |
|
|
75
|
+
| `+` `-` | make the selected job 30 minutes longer or shorter |
|
|
76
|
+
| `a` `d` | add a job (`Dryer 1h30`), delete the selected one |
|
|
77
|
+
| `Enter` | jump to the selected job's best window |
|
|
78
|
+
| `w` | rank by carbon, price, or both |
|
|
79
|
+
| `p` | change postcode |
|
|
80
|
+
| `m` `c` `T` `t` | mix rows, compact layout, theme, 12/24h |
|
|
81
|
+
| `R` | refetch now |
|
|
82
|
+
| `q` | quit |
|
|
83
|
+
|
|
84
|
+
Settings and jobs live in `~/.config/gridhour/config.json`.
|
|
85
|
+
|
|
86
|
+
## Status bars and scripts
|
|
87
|
+
|
|
88
|
+
```console
|
|
89
|
+
$ gridhour --line
|
|
90
|
+
⚡ 142g · 14p · green in 2h
|
|
91
|
+
|
|
92
|
+
$ gridhour --line --best 3h
|
|
93
|
+
⚡ 142g · 14p · 3h best 13:00 (in 4h30)
|
|
94
|
+
|
|
95
|
+
$ gridhour --tmux # same, with tmux colour codes for status-right
|
|
96
|
+
$ gridhour --watch # same, updating in place
|
|
97
|
+
$ gridhour --json | jq '.jobs[] | {name, start: .best.from}'
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
"Green" means NESO's forecast grades the half hour as *low* or *very low* for your region.
|
|
101
|
+
If nothing in the next 48 hours makes that grade, the line says `greenest in 9h` instead.
|
|
102
|
+
|
|
103
|
+
`--json` gives the current half hour, the next green slot, the best window for every job (with
|
|
104
|
+
the carbon and price you would get starting now, for comparison) and the full 48 hour series.
|
|
105
|
+
|
|
106
|
+
A tmux example:
|
|
107
|
+
|
|
108
|
+
```tmux
|
|
109
|
+
set -g status-right '#(gridhour --tmux) '
|
|
110
|
+
set -g status-interval 300
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Where the numbers come from
|
|
114
|
+
|
|
115
|
+
* **Carbon intensity** and the **generation mix**: the [Carbon Intensity API](https://carbonintensity.org.uk/)
|
|
116
|
+
from the National Energy System Operator (NESO), regional forecast by postcode. Free, no key,
|
|
117
|
+
CC BY 4.0.
|
|
118
|
+
* **Prices**: the public [Octopus Energy API](https://developer.octopus.energy/), the Agile
|
|
119
|
+
import tariff for your region, including VAT. Octopus publishes the next day's prices at about
|
|
120
|
+
4pm, so before that the price half of the chart stops around 11pm tonight and the "cheap"
|
|
121
|
+
ranking only looks that far ahead.
|
|
122
|
+
|
|
123
|
+
These are forecasts. The carbon forecast for tomorrow afternoon can be off by a fair
|
|
124
|
+
margin; Agile prices, once published, are what you pay. gridhour is not affiliated with Octopus
|
|
125
|
+
Energy or NESO.
|
|
126
|
+
|
|
127
|
+
Responses are cached in `~/.cache/gridhour` for 30 minutes. If the network is down the app
|
|
128
|
+
keeps showing the cached data and says how old it is. `GRIDHOUR_OFFLINE=1` stops all network
|
|
129
|
+
access.
|
|
130
|
+
|
|
131
|
+
## Security and privacy
|
|
132
|
+
|
|
133
|
+
gridhour talks to those two APIs over HTTPS and nothing else (redirects are refused), sends only
|
|
134
|
+
the first half of your postcode and your region letter, and has no runtime dependencies.
|
|
135
|
+
Responses are checked field by field before they are used or cached, and text from them never
|
|
136
|
+
reaches your terminal or tmux with escape sequences or format codes in it. Details and how to
|
|
137
|
+
report a problem: [SECURITY.md](https://github.com/777dimas/gridhour/blob/main/SECURITY.md).
|
|
138
|
+
|
|
139
|
+
## Development
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
python3 -m venv .venv && . .venv/bin/activate
|
|
143
|
+
pip install --require-hashes -r requirements/dev.txt && pip install --no-deps -e .
|
|
144
|
+
pytest
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The tests replay real API responses from `tests/fixtures/`; sockets are blocked while they run.
|
|
148
|
+
More in [CONTRIBUTING.md](https://github.com/777dimas/gridhour/blob/main/CONTRIBUTING.md).
|
|
149
|
+
|
|
150
|
+
| Module | What it does |
|
|
151
|
+
| --- | --- |
|
|
152
|
+
| `grid.py` | the two APIs, regions and postcodes, the cache |
|
|
153
|
+
| `plan.py` | scoring half hours, best windows, "green in 2h" |
|
|
154
|
+
| `compose.py` | turns the state into a frame |
|
|
155
|
+
| `canvas.py` | character grid with colours, rendered to ANSI |
|
|
156
|
+
| `safe.py` | cleaning outside text, private atomic file writes |
|
|
157
|
+
| `app.py`, `keys.py` | the terminal loop and every key |
|
|
158
|
+
| `output.py` | `--line`, `--tmux`, `--json`, `--watch` |
|
|
159
|
+
| `state.py`, `themes.py`, `cli.py` | settings, colours, arguments |
|
|
160
|
+
|
|
161
|
+
## Licence
|
|
162
|
+
|
|
163
|
+
MIT, see [LICENSE](https://github.com/777dimas/gridhour/blob/main/LICENSE).
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Reporting a problem
|
|
4
|
+
|
|
5
|
+
Please report security problems privately through
|
|
6
|
+
[GitHub's private vulnerability reporting](https://github.com/777dimas/gridhour/security/advisories/new),
|
|
7
|
+
not in a public issue. You should get a reply within 5 working days. Once a fix is out I credit the
|
|
8
|
+
reporter in the advisory and the changelog, unless you ask me not to.
|
|
9
|
+
|
|
10
|
+
## Supported versions
|
|
11
|
+
|
|
12
|
+
Only the latest release gets fixes.
|
|
13
|
+
|
|
14
|
+
## What gridhour does that matters for security
|
|
15
|
+
|
|
16
|
+
**Network.** HTTPS GET requests to two hosts and nothing else: `api.carbonintensity.org.uk` (with the
|
|
17
|
+
first half of your postcode, for example `SW1A`) and `api.octopus.energy` (with your region letter).
|
|
18
|
+
|
|
19
|
+
* `grid.http_json` refuses any other scheme or host, refuses redirects, and checks the final URL.
|
|
20
|
+
* A response may be 4 MB at most. Once connected, the whole body must arrive within 25 seconds,
|
|
21
|
+
however slowly it trickles in (connecting and DNS have their own 12 second timeout).
|
|
22
|
+
* No API keys, no accounts, no telemetry. `GRIDHOUR_OFFLINE=1` turns the network off.
|
|
23
|
+
|
|
24
|
+
**Untrusted input.** API responses, the cache and the config file are all treated as hostile.
|
|
25
|
+
|
|
26
|
+
* Every field is type- and range-checked before use: numbers must be finite and plausible
|
|
27
|
+
(booleans and strings are refused), fuel and index names must be known values, times must be
|
|
28
|
+
ISO 8601. A bad row costs that half hour, not the whole forecast.
|
|
29
|
+
* A response is checked before it is cached, so a broken one is never stored. A cache entry that
|
|
30
|
+
no longer passes the checks, or carries a timestamp from the future, is ignored.
|
|
31
|
+
* Text that is shown (the region name, job names) loses control characters, bidi and zero-width
|
|
32
|
+
characters, combining marks and double-width characters, so it can neither send escape
|
|
33
|
+
sequences to your terminal nor shift or reorder the screen. In `--tmux` output every `#` is
|
|
34
|
+
doubled, so no text can become a tmux format or a `#(command)`. `--json` output is pure ASCII.
|
|
35
|
+
* Error messages never repeat response content.
|
|
36
|
+
* The Agile product code from the API must match `AGILE-[A-Z0-9-]+` exactly, and the region letter
|
|
37
|
+
must be one of the fourteen known ones, before either goes into a URL.
|
|
38
|
+
* Malformed responses, config files and arguments produce an error message, not a crash. The
|
|
39
|
+
test suite in `tests/test_security.py` holds the cases.
|
|
40
|
+
|
|
41
|
+
**Files.** `~/.config/gridhour/config.json` (postcode outward code, jobs, theme) and
|
|
42
|
+
`~/.cache/gridhour/*.json` (checked API responses). Both directories are kept at `0700` (tightened
|
|
43
|
+
if they already exist with looser modes) and the files at `0600`. Writes go to a fresh temporary
|
|
44
|
+
file created with `O_EXCL` and are renamed into place, so a planted symlink is never followed.
|
|
45
|
+
`gridhour --reset` deletes the config and the cache.
|
|
46
|
+
|
|
47
|
+
**Dependencies.** None at runtime, only the Python standard library.
|
|
48
|
+
|
|
49
|
+
## Supply chain
|
|
50
|
+
|
|
51
|
+
* Every GitHub Action is pinned to a full commit SHA. Workflows run with read-only tokens unless a
|
|
52
|
+
job needs more, and never keep the checkout credentials.
|
|
53
|
+
* CI installs its tools from hash-locked files (`requirements/*.txt`, `pip --require-hashes`).
|
|
54
|
+
* CodeQL (Python and the workflows), OpenSSF Scorecard and dependency review run on the repository.
|
|
55
|
+
`zizmor --persona=pedantic` and `actionlint` are clean.
|
|
56
|
+
* A release runs only for a `vX.Y.Z` tag on a commit that is already on `main`, needs approval in
|
|
57
|
+
the `pypi` environment, and goes to PyPI through trusted publishing (OIDC, no stored token). The
|
|
58
|
+
jobs holding an OIDC token install nothing and can reach only an allowlist of hosts.
|
|
59
|
+
* Every release file carries signed build provenance. To check a file you downloaded:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
pip download gridhour==0.1.0 --no-deps -d .
|
|
63
|
+
gh attestation verify gridhour-0.1.0-py3-none-any.whl --repo 777dimas/gridhour \
|
|
64
|
+
--signer-workflow 777dimas/gridhour/.github/workflows/release.yml --source-ref refs/tags/v0.1.0
|
|
65
|
+
```
|