Inital commit

This commit is contained in:
Stephen Newey
2018-11-10 15:28:41 +00:00
commit c197367916
11 changed files with 1416 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.idea

69
README.rst Normal file
View File

@@ -0,0 +1,69 @@
Octograph
---------
Python tool for downloading energy consumption data from the
`Octopus Energy API`_ and loading it into `InfluxDB`_.
In the process, additional metrics will be generated and stored for unit rates
and costs as configured by the user. Suitable for two-rate electricity tariffs
like `Octopus Energy Go`_. Single rate gas readings are also retrieved and
stored.
The secondary unit rate is specified with start and end times, and a timezone
which is useful for the Go tariff where the discount rate changes with
daylight savings time.
Included is an example `Grafana`_ dashboard to visualise the captured data.
An example Docker Compose file is included for easily running InfluxDB and
Grafana.
.. image:: grafana-dashboard.png
:width: 800
Installation
============
Tested on macOS with Docker for Mac and Python 3.6. A Python virtualenv is
recommended.
Install the Python requirements with ``pip``
.. code:: bash
pip install -r app/requirements.txt
Usage
=====
Create a configuration file ``octograph.ini`` customised with your Octopus
API key, meter details and energy rate information. This file should be in the
working directory where you run the ``octopus_to_influxdb.py`` command, or
can be passed as an argument.
.. code:: bash
python app/octopus_to_influxdb.py --help
By default, energy data for the previous day will be collected. Optional from
and to ranges may be specified to retrieve larger datasets. It is anticipated
that the script will be run daily by a cron job.
.. code:: bash
docker-compose up -d # start InfluxDB and Grafana in Docker
python app/octopus_to_infuxdb.py --from-date=2018-10-20
open http://localhost:3000
The default login credentials for Grafana are admin/admin, and you will be
prompted to set a new password on first login. You should then proceed to add
InfluxDB as a datasource with URL ``http://influxdb:8086`` and database
``energy`` if using the Docker version provided. The dashboard provided can
then be imported to review the data.
.. _Octopus Energy API: https://developer.octopus.energy/docs/api/
.. _Octopus Energy Go: https://octopus.energy/go/
.. _InfluxDB: https://www.influxdata.com/time-series-platform/influxdb/
.. _Grafana: https://grafana.com

1
app/.dockerignore Normal file
View File

@@ -0,0 +1 @@
octograph.ini

1
app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
octograph.ini

12
app/Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM python:3.6
LABEL maintainer="Stephen Newey <github@s-n.me>"
WORKDIR /usr/src/app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY octopus_to_influxdb.py ./
ENTRYPOINT [ "python", "./octopus_to_influxdb.py" ]

25
app/example-octograph.ini Normal file
View File

@@ -0,0 +1,25 @@
[influxdb]
host = localhost
port = 8086
user = user
password = password
database = energy
[octopus]
api_key = sk_live_1234
[electricity]
mpan = 12345
serial_number = 12A3456
standing_charge = 25.00
unit_rate_high = 14.12
unit_rate_low = 5.00
unit_rate_low_start = 00:30
unit_rate_low_end = 04:30
unit_rate_low_zone = Europe/London
[gas]
mpan = 12345
serial_number = 12A3456
standing_charge = 16.80
unit_rate = 3.03

187
app/octopus_to_influxdb.py Normal file
View File

@@ -0,0 +1,187 @@
#!/usr/bin/env python
from configparser import ConfigParser
from urllib import parse
import click
import maya
import requests
from influxdb import InfluxDBClient
def retrieve_paginated_consumption(
api_key, url, from_date, to_date, page=None
):
args = {
'period_from': from_date,
'period_to': to_date,
}
if page:
args['page'] = page
response = requests.get(url, params=args, auth=(api_key, ''))
response.raise_for_status()
data = response.json()
results = data.get('results', [])
if data['next']:
url_query = parse.urlparse(data['next']).query
next_page = parse.parse_qs(url_query)['page'][0]
results += retrieve_paginated_consumption(
api_key, url, from_date, to_date, next_page
)
return results
def store_series(connection, series, metrics, rate_data):
def active_rate_field(measurement):
if series == 'gas':
return 'unit_rate'
elif not rate_data['unit_rate_low_zone']: # no low rate
return 'unit_rate_high'
low_start_str = rate_data['unit_rate_low_start']
low_end_str = rate_data['unit_rate_low_end']
low_zone = rate_data['unit_rate_low_zone']
measurement_at = maya.parse(measurement['interval_start'])
low_start = maya.when(
measurement_at.datetime().strftime(f'%Y-%m-%dT{low_start_str}'),
timezone=low_zone
)
low_end = maya.when(
measurement_at.datetime().strftime(f'%Y-%m-%dT{low_end_str}'),
timezone=low_zone
)
low_period = maya.MayaInterval(low_start, low_end)
return \
'unit_rate_low' if measurement_at in low_period \
else 'unit_rate_high'
def fields_for_measurement(measurement):
consumption = measurement['consumption']
rate = active_rate_field(measurement)
rate_cost = rate_data[rate]
cost = consumption * rate_cost
standing_charge = rate_data['standing_charge'] / 48 # 30 minute reads
return {
'consumption': consumption,
'cost': cost,
'total_cost': cost + standing_charge,
}
def tags_for_measurement(measurement):
period = maya.parse(measurement['interval_end'])
time = period.datetime().strftime('%H:%M')
return {
'active_rate': active_rate_field(measurement),
'time_of_day': time,
}
measurements = [
{
'measurement': series,
'tags': tags_for_measurement(measurement),
'time': measurement['interval_end'],
'fields': fields_for_measurement(measurement),
}
for measurement in metrics
]
connection.write_points(measurements)
@click.command()
@click.option(
'--config-file',
default="octograph.ini",
type=click.Path(exists=True, dir_okay=True, readable=True),
)
@click.option('--from-date', default='yesterday midnight', type=click.STRING)
@click.option('--to-date', default='today midnight', type=click.STRING)
def cmd(config_file, from_date, to_date):
config = ConfigParser()
config.read(config_file)
influx = InfluxDBClient(
host=config.get('influxdb', 'host', fallback="localhost"),
port=config.getint('influxdb', 'port', fallback=8086),
username=config.get('influxdb', 'user', fallback=""),
password=config.get('influxdb', 'password', fallback=""),
database=config.get('influxdb', 'database', fallback="energy"),
)
api_key = config.get('octopus', 'api_key')
if not api_key:
raise click.ClickException('No Octopus API key set')
e_mpan = config.get('electricity', 'mpan', fallback=None)
e_serial = config.get('electricity', 'serial_number', fallback=None)
if not e_mpan or not e_serial:
raise click.ClickException('No electricity meter identifiers')
e_url = 'https://api.octopus.energy/v1/electricity-meter-points/' \
f'{e_mpan}/meters/{e_serial}/consumption/'
g_mpan = config.get('gas', 'mpan', fallback=None)
g_serial = config.get('gas', 'serial_number', fallback=None)
if not g_mpan or not g_serial:
raise click.ClickException('No gas meter identifiers')
g_url = 'https://api.octopus.energy/v1/gas-meter-points/' \
f'{g_mpan}/meters/{g_serial}/consumption/'
rate_data = {
'electricity': {
'standing_charge': config.getfloat(
'electricity', 'standing_charge', fallback=0.0
),
'unit_rate_high': config.getfloat(
'electricity', 'unit_rate_high', fallback=0.0
),
'unit_rate_low': config.getfloat(
'electricity', 'unit_rate_low', fallback=0.0
),
'unit_rate_low_start': config.get(
'electricity', 'unit_rate_low_start', fallback="00:00"
),
'unit_rate_low_end': config.get(
'electricity', 'unit_rate_low_end', fallback="00:00"
),
'unit_rate_low_zone': config.get(
'electricity', 'unit_rate_low_zone', fallback=None
),
},
'gas': {
'standing_charge': config.getfloat(
'gas', 'standing_charge', fallback=0.0
),
'unit_rate': config.getfloat('gas', 'unit_rate', fallback=0.0),
}
}
from_iso = maya.when(from_date).iso8601()
to_iso = maya.when(to_date).iso8601()
click.echo(
f'Retrieving electricity data for {from_iso} until {to_iso}...',
nl=False
)
e_consumption = retrieve_paginated_consumption(
api_key, e_url, from_iso, to_iso
)
click.echo(f' {len(e_consumption)} readings.')
store_series(influx, 'electricity', e_consumption, rate_data['electricity'])
click.echo(
f'Retrieving gas data for {from_iso} until {to_iso}...',
nl=False
)
g_consumption = retrieve_paginated_consumption(
api_key, g_url, from_iso, to_iso
)
click.echo(f' {len(g_consumption)} readings.')
store_series(influx, 'gas', g_consumption, rate_data['gas'])
if __name__ == '__main__':
cmd()

4
app/requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
requests >= 2.20.0
influxdb == 5.2.0
Click == 7.0
maya == 0.5.0

14
docker-compose.yml Normal file
View File

@@ -0,0 +1,14 @@
version: '3'
services:
influxdb:
image: influxdb
environment:
- INFLUXDB_DB=energy
ports:
- "8086:8086"
grafana:
image: grafana/grafana
ports:
- "3000:3000"
depends_on:
- influxdb

1102
energy_dashboard.json Normal file

File diff suppressed because it is too large Load Diff

BIN
grafana-dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB