Files
octograph/app/octopus_to_influxdb.py
Ben Roberts 73cf170e18 Handle unit conversion for SMETS2 gas meters
SMETS2 gas meter consumption is measured in m3 rather than kWh, so conversion
from one unit to the other is required.
Algorithm and description of terms is taken from:
https://www.theenergyshop.com/guides/how-to-convert-gas-units-to-kwh

This commit adds additional config file options to the `gas` section:
- `meter_type` (default `1` for backward compatibility)
  Enables unit conversion from m3 to kWh before reporting metrics to influxdb
- `volume_correction_factor` (default `1.02264`)
- `calorific_value` (default `40`)
2020-08-31 12:06:42 +01:00

231 lines
8.0 KiB
Python

#!/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_data(
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_data(
api_key, url, from_date, to_date, next_page
)
return results
def store_series(connection, series, metrics, rate_data):
agile_data = rate_data.get('agile_unit_rates', [])
agile_rates = {
point['valid_to']: point['value_inc_vat']
for point in agile_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(to_timezone=low_zone).strftime(
f'%Y-%m-%dT{low_start_str}'
),
timezone=low_zone
)
low_end = maya.when(
measurement_at.datetime(to_timezone=low_zone).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']
conversion_factor = rate_data.get('conversion_factor', None)
if conversion_factor:
consumption *= conversion_factor
rate = active_rate_field(measurement)
rate_cost = rate_data[rate]
cost = consumption * rate_cost
standing_charge = rate_data['standing_charge'] / 48 # 30 minute reads
fields = {
'consumption': consumption,
'cost': cost,
'total_cost': cost + standing_charge,
}
if agile_data:
agile_standing_charge = rate_data['agile_standing_charge'] / 48
agile_unit_rate = agile_rates.get(
maya.parse(measurement['interval_end']).iso8601(),
rate_data[rate] # cludge, use Go rate during DST changeover
)
agile_cost = agile_unit_rate * consumption
fields.update({
'agile_rate': agile_unit_rate,
'agile_cost': agile_cost,
'agile_total_cost': agile_cost + agile_standing_charge,
})
return fields
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/'
agile_url = config.get('electricity', 'agile_rate_url', fallback=None)
g_mpan = config.get('gas', 'mpan', fallback=None)
g_serial = config.get('gas', 'serial_number', fallback=None)
g_meter_type = config.get('gas', 'meter_type', fallback=1)
g_vcf = config.get('gas', 'volume_correction_factor', fallback=1.02264)
g_cv = config.get('gas', 'calorific_value', fallback=40)
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/'
timezone = config.get('electricity', 'unit_rate_low_zone', fallback=None)
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': timezone,
'agile_standing_charge': config.getfloat(
'electricity', 'agile_standing_charge', fallback=0.0
),
'agile_unit_rates': [],
},
'gas': {
'standing_charge': config.getfloat(
'gas', 'standing_charge', fallback=0.0
),
'unit_rate': config.getfloat('gas', 'unit_rate', fallback=0.0),
# SMETS1 meters report kWh, SMET2 report m^3 and need converting to kWh first
'conversion_factor': (g_vcf * g_cv) / 3.6 if int(g_meter_type) > 1 else None,
}
}
from_iso = maya.when(from_date, timezone=timezone).iso8601()
to_iso = maya.when(to_date, timezone=timezone).iso8601()
click.echo(
f'Retrieving electricity data for {from_iso} until {to_iso}...',
nl=False
)
e_consumption = retrieve_paginated_data(
api_key, e_url, from_iso, to_iso
)
click.echo(f' {len(e_consumption)} readings.')
click.echo(
f'Retrieving Agile rates for {from_iso} until {to_iso}...',
nl=False
)
rate_data['electricity']['agile_unit_rates'] = retrieve_paginated_data(
api_key, agile_url, from_iso, to_iso
)
click.echo(f' {len(rate_data["electricity"]["agile_unit_rates"])} rates.')
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_data(
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()