From 45696c8e775c3ffcbc3e9fa40b7f3dfad473d8e5 Mon Sep 17 00:00:00 2001 From: Guy Badman Date: Sun, 29 Mar 2020 17:08:33 +0100 Subject: [PATCH] Add ability to query the Agile Outgoing Tariff to find highest export price `import` and `export` should be set to True or False as required, `import = True` and `export = False` for the Agile Octopus tariff and `import = False` and `export = True` for the Agile Outgoing Octopus tariff. The defaults if these options are not found in the apps.yaml file are: `import = True` and `export = False` --- README.md | 16 +++++-- apps/octoblock/octoblock.py | 94 ++++++++++++++++++++++++++----------- 2 files changed, 80 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index a46923b..4631115 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,16 @@ # octoblock -Octoblock is an app which works under [AppDaemon](https://www.home-assistant.io/docs/ecosystem/appdaemon/) within [Home Assistant](https://www.home-assistant.io/) which finds the cheapest “n” hour block and works out the price of that block, for the Octopus Energy, Agile Octopus tariff. It creates and sets sensors for the cost and start time, for example, using the apps.yaml file below, the following entities are created and then updated: +Octoblock is an app which works under [AppDaemon](https://www.home-assistant.io/docs/ecosystem/appdaemon/) within [Home Assistant](https://www.home-assistant.io/) which finds the cheapest “n” hour block for import or the most expensive “n” hour block for export, and works out the price of that block, for the Octopus Energy, Agile Octopus / Agile Outgoing Octopus tariffs. It creates and sets sensors for the cost and start time, for example, using the apps.yaml file below, the following entities are created and then updated: ``` sensor.octopus_1hour_time -sensor.octopus_1hour_cost +sensor.octopus_1hour_price sensor.octopus_1_5hour_time -sensor.octopus_1_5hour_cost +sensor.octopus_1_5hour_price +``` + +Sensors for export will be created with naming such as: +``` +sensor.octopus_export_1hour_time +sensor.octopus_export_1hour_price ``` [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/custom-components/hacs) @@ -41,6 +47,8 @@ The module and class sections need to remain as above, other sections should be | hour | Yes | 1 | | start_period | Yes | today | | use_timezone | Yes | True | +| import | Yes | True | +| export | Yes | False | You can have multiple blocks with different time periods (`hour` setting) or starting points (`start_period` setting) as needed. It will work with whole hour or half hour blocks in the `hour` setting. @@ -64,6 +72,8 @@ Using `today` start_period this has only turned on once during the day `use_timezone` can be set to True or False, and defaults to False, it allows you to specify if the date/time should be displayed in UTC (False), or using Europe/London (True) as the timezone. For example, `2020-03-29T02:00:00Z` or `2020-03-29T03:00:00 BST` respectively. +`import` and `export` should be set to True or False as required, `import = True` and `export = False` for the Agile Octopus tariff and `import = False` and `export = True` for the Agile Outgoing Octopus tariff. + ### Home Assistant Automation The created start time sensors can then be used to trigger automations within Home Assistant. diff --git a/apps/octoblock/octoblock.py b/apps/octoblock/octoblock.py index 2820f45..96fa8fa 100644 --- a/apps/octoblock/octoblock.py +++ b/apps/octoblock/octoblock.py @@ -10,15 +10,25 @@ class OctoBlock(hass.Hass): def initialize(self): time = datetime.datetime.now() time = time + datetime.timedelta(seconds=5) - self.run_every(self.get_best_period_and_cost, time, 30 * 60) + self.run_every(self.period_and_cost_callback, time, 30 * 60) - def get_best_period_and_cost(self, kwargs): - hours = self.args.get('hour', 1) + def period_and_cost_callback(self, kwargs): + self.hours = self.args.get('hour', 1) region = self.args.get('region', 'H') start_period = self.args.get('start_period', 'now') start_period = str(start_period).lower() - use_timezone = self.args.get('use_timezone', False) + self.use_timezone = self.args.get('use_timezone', False) + self.incoming = self.args.get('import', True) + self.outgoing = self.args.get('export', False) + if self.outgoing: + # incoming defaults to True in case it hasnt been added to + # apps.yaml as it wasnt an option when originally released + # However if export is True, import must be False + self.incoming = False + if self.args.get('import') and self.args.get('export'): + self.log('import and export should not both be True in the ' + + 'same configuration block', level='ERROR') if start_period == 'today': d = datetime.date.today().isoformat() + 'T00:00:00' elif start_period == 'now': @@ -29,52 +39,82 @@ def get_best_period_and_cost(self, kwargs): ' defaulting to "now"') d = datetime.datetime.now().isoformat() - r = requests.get( - 'https://api.octopus.energy/v1/products/AGILE-18-02-21/' + - 'electricity-tariffs/E-1R-AGILE-18-02-21-' + - str(region).upper() + '/standard-unit-rates/?period_from=' + d) + self.get_period_and_cost(region, d) + + hours = str(self.hours).replace(".", "_") + if self.incoming: + self.set_state('sensor.octopus_' + hours + 'hour_time', + state=self.time, + attributes={'icon': 'mdi:clock-outline'}) + self.set_state('sensor.octopus_' + hours + 'hour_price', + state=round(self.price, 4), + attributes={'unit_of_measurement': 'p/kWh', + 'icon': 'mdi:flash'}) + elif self.outgoing: + self.set_state('sensor.octopus_export_' + hours + 'hour_time', + state=self.time, + attributes={'icon': 'mdi:clock-outline'}) + self.set_state('sensor.octopus_export_' + hours + 'hour_price', + state=round(self.price, 4), + attributes={'unit_of_measurement': 'p/kWh', + 'icon': 'mdi:flash-outline'}) + + def get_period_and_cost(self, region, timeperiod): + baseurl = 'https://api.octopus.energy/v1/products/' + if self.incoming: + r = requests.get( + baseurl + 'AGILE-18-02-21/electricity-tariffs/' + + 'E-1R-AGILE-18-02-21-' + str(region).upper() + + '/standard-unit-rates/?period_from=' + timeperiod) + elif self.outgoing: + r = requests.get( + baseurl + 'AGILE-OUTGOING-19-05-13/electricity-tariffs/' + + 'E-1R-AGILE-OUTGOING-19-05-13-' + str(region).upper() + + '/standard-unit-rates/?period_from=' + timeperiod) tariff = json.loads(r.text) tariffresults = tariff[u'results'] tariffresults.reverse() - blocks = float(hours) * 2 + blocks = float(self.hours) * 2 blocks = int(blocks) for period in tariffresults: curridx = tariffresults.index(period) tr_len = len(tariffresults) if curridx > tr_len-blocks: - period[str(hours) + '_hour_average'] = 99 + if self.incoming: + period[str(self.hours) + '_hour_average'] = 99 + elif self.outgoing: + period[str(self.hours) + '_hour_average'] = 0 continue cost = 0 for block in range(blocks): cost = cost + (tariffresults[curridx+block][u'value_inc_vat']) cost = cost / blocks - period[str(hours) + '_hour_average'] = cost + period[str(self.hours) + '_hour_average'] = cost - self.minprice = min( - period[str(hours) + '_hour_average'] for period in tariffresults) - self.log('Lowest average price for {} hour block'.format(str(hours)) + - ' is: {} p/kWh'.format(self.minprice)) + if self.incoming: + self.price = min( + period[str(self.hours) + '_hour_average'] + for period in tariffresults) + self.log('Lowest average price for {}'.format(str(self.hours)) + + ' hour block is: {} p/kWh'.format(self.price)) + elif self.outgoing: + self.price = max( + period[str(self.hours) + '_hour_average'] + for period in tariffresults) + self.log('Highest average price for {}'.format(str(self.hours)) + + ' hour block is: {} p/kWh'.format(self.price)) for period in tariffresults: - if period[str(hours) + '_hour_average'] == self.minprice: + if period[str(self.hours) + '_hour_average'] == self.price: self.time = period[u'valid_from'] - if use_timezone: + if self.use_timezone: fmt = '%Y-%m-%dT%H:%M:%S %Z' greenwich = pytz.timezone('Europe/London') date_time = dateutil.parser.parse(self.time) local_datetime = date_time.astimezone(greenwich) self.time = local_datetime.strftime(fmt) - self.log('Lowest priced {} hour period'.format(str(hours)) + + self.log('Best priced {} hour period'.format(str(self.hours)) + ' starts at: {}'.format(self.time)) - - hours = str(hours).replace(".", "_") - self.set_state('sensor.octopus_' + hours + 'hour_time', - state=self.time, - attributes={'icon': 'mdi:clock-outline'}) - self.set_state('sensor.octopus_' + hours + 'hour_price', - state=round(self.minprice, 4), - attributes={'unit_of_measurement': 'p/kWh', - 'icon': 'mdi:flash'})