Skip to content

Commit 2010dd1

Browse files
committed
Some linting stuff and add frequency
1 parent 35b5630 commit 2010dd1

File tree

5 files changed

+100
-33
lines changed

5 files changed

+100
-33
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Then follow the same steps that are using to setup most integrations:
4848
| Name | ID | Description |
4949
| ---- | -- | ----------- |
5050
| National Grid Current Sell Price (optional) | sensor.national_grid_current_sell_price | Current balancing price of Grid |
51+
| National Grid Current Grid Frequency | sensor.national_grid_current_grid_frequency | Current Grid Frequency (every 5 minutes) |
5152
| National Grid Today Wind Peak | sensor.national_grid_today_wind_peak | Estimated peak wind production of Grid today |
5253
| National Grid Today Wind Peak Time | sensor.national_grid_today_wind_peak_time | Estimated time of peak wind production of Grid today |
5354
| National Grid Tomorrow Wind Peak | sensor.national_grid_tomorrow_wind_peak | Estimated peak wind production of Grid tomorrow |

custom_components/national_grid/__init__.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,12 @@
22

33
import logging
44

5-
from dateutil import tz
6-
7-
from .coordinators.national_grid import (
8-
NationalGridCoordinator,
9-
)
105
from homeassistant.config_entries import ConfigEntry
116
from homeassistant.const import Platform
127
from homeassistant.core import HomeAssistant
13-
from homeassistant.helpers.typing import ConfigType
148

15-
from .const import API_KEY, DATA_CLIENT, DOMAIN, API_KEY_PROVIDED
9+
from .const import API_KEY_PROVIDED, DATA_CLIENT, DOMAIN
10+
from .coordinators.national_grid import NationalGridCoordinator
1611

1712
PLATFORMS = [Platform.SENSOR]
1813
_LOGGER = logging.getLogger(__name__)

custom_components/national_grid/coordinators/national_grid.py

Lines changed: 84 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,73 @@
1+
from collections import OrderedDict
12
import csv
3+
from datetime import datetime, timedelta
24
import io
35
import json
46
import logging
5-
from collections import OrderedDict
6-
from datetime import datetime, timedelta
7-
from typing import Any, TypedDict
7+
from typing import Any
88

9-
import requests
10-
import xmltodict
119
from _collections_abc import Mapping
1210
from dateutil import tz
11+
import requests
12+
import xmltodict
1313

1414
from homeassistant.config_entries import ConfigEntry
1515
from homeassistant.core import HomeAssistant
1616
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
1717
from homeassistant.util import dt as dt_util
1818

19-
from ..const import API_KEY, DOMAIN, API_KEY_PROVIDED
19+
from ..const import API_KEY, API_KEY_PROVIDED, DOMAIN
2020
from ..errors import InvalidAuthError, UnexpectedDataError, UnexpectedStatusCode
2121
from ..models import (
22+
DFSRequirementItem,
23+
DFSRequirements,
2224
NationalGridData,
25+
NationalGridDemandDayAheadForecast,
26+
NationalGridDemandDayAheadForecastItem,
27+
NationalGridDemandForecast,
28+
NationalGridDemandForecastItem,
2329
NationalGridGeneration,
2430
NationalGridSolarForecast,
2531
NationalGridSolarForecastItem,
2632
NationalGridWindData,
2733
NationalGridWindForecast,
28-
NationalGridWindForecastLongTerm,
2934
NationalGridWindForecastItem,
30-
NationalGridDemandForecastItem,
31-
NationalGridDemandForecast,
32-
NationalGridDemandDayAheadForecast,
33-
NationalGridDemandDayAheadForecastItem,
34-
DFSRequirements,
35-
DFSRequirementItem,
35+
NationalGridWindForecastLongTerm,
3636
)
3737

3838
_LOGGER = logging.getLogger(__name__)
3939

4040

4141
class NationalGridCoordinator(DataUpdateCoordinator[NationalGridData]):
42+
"""National Grid Data Coordinator."""
43+
4244
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
43-
"""Initialize"""
45+
"""Initialize."""
4446
super().__init__(
4547
hass, _LOGGER, name=DOMAIN, update_interval=timedelta(minutes=5)
4648
)
4749
self._entry = entry
4850

4951
@property
5052
def entry_id(self) -> str:
53+
"""Return entry id."""
5154
return self._entry.entry_id
5255

5356
async def _async_update_data(self) -> NationalGridData:
5457
try:
5558
data = await self.hass.async_add_executor_job(
5659
get_data, self.hass, self._entry.data, self.data
5760
)
58-
except:
59-
raise Exception() # pylint: disable=broad-exception-raised
61+
except: # noqa: E722
62+
raise Exception() # pylint: disable=broad-exception-raised # noqa: B904
6063

6164
return data
6265

6366

6467
def get_data(
6568
hass: HomeAssistant, config: Mapping[str, Any], current_data: NationalGridData
6669
) -> NationalGridData:
70+
"""Get data."""
6771
api_key = config[API_KEY]
6872

6973
today_utc = dt_util.utcnow().strftime("%Y-%m-%d")
@@ -89,6 +93,10 @@ def get_data(
8993
now_utc_full,
9094
)
9195

96+
current_grid_frequency = obtain_data_with_fallback(
97+
current_data, "grid_frequency", get_current_frequency, api_key, now_utc_full
98+
)
99+
92100
wind_forecast = obtain_data_with_fallback(
93101
current_data,
94102
"wind_forecast",
@@ -189,6 +197,7 @@ def get_data(
189197
return NationalGridData(
190198
sell_price=current_price,
191199
carbon_intensity=carbon_intensity,
200+
grid_frequency=current_grid_frequency,
192201
wind_data=wind_data,
193202
wind_forecast=wind_forecast,
194203
wind_forecast_earliest=wind_forecast_earliest,
@@ -213,6 +222,7 @@ def get_data(
213222

214223

215224
def get_data_if_exists(data, key: str):
225+
"""Get data if exists."""
216226
if data is None:
217227
_LOGGER.error("Previous data is None, returning None")
218228
return None
@@ -224,6 +234,7 @@ def get_data_if_exists(data, key: str):
224234

225235

226236
def get_hourly_wind_forecast(now_utc: datetime) -> NationalGridWindForecast:
237+
"""Get hourly wind forecast."""
227238
# Need to calculate start. We want data from 8pm on current day to day+2 8pm... however, this is calculated every so often.
228239
# This means that day + 2 isn't calculated until 03:30 GMT
229240

@@ -283,6 +294,7 @@ def get_hourly_wind_forecast(now_utc: datetime) -> NationalGridWindForecast:
283294

284295

285296
def get_hourly_wind_forecast_earliest(now_utc: datetime) -> NationalGridWindForecast:
297+
"""Get hourly wind forecast."""
286298
# Need to calculate start. We want data from 8pm on current day to day+2 8pm... however, this is calculated every so often.
287299
# This means that day + 2 isn't calculated until 03:30 GMT
288300

@@ -344,6 +356,7 @@ def get_hourly_wind_forecast_earliest(now_utc: datetime) -> NationalGridWindFore
344356
def get_half_hourly_solar_forecast(
345357
api_key: str, now: datetime
346358
) -> NationalGridSolarForecast:
359+
"""Get half hourly solar forecast."""
347360
nearest_30_minutes = now + (now.min.replace(tzinfo=now.tzinfo) - now) % timedelta(
348361
minutes=30
349362
)
@@ -407,6 +420,7 @@ def get_half_hourly_solar_forecast(
407420

408421

409422
def get_current_price(api_key: str, today_utc: str) -> float:
423+
"""Get current grid price."""
410424
url = (
411425
"https://api.bmreports.com/BMRS/DERSYSDATA/v1?APIKey="
412426
+ api_key
@@ -421,7 +435,26 @@ def get_current_price(api_key: str, today_utc: str) -> float:
421435
return currentPrice
422436

423437

438+
def get_current_frequency(api_key: str, now_utc: datetime) -> float:
439+
"""Get current grid frequency."""
440+
url = (
441+
"https://data.elexon.co.uk/bmrs/api/v1/system/frequency?format=json&from="
442+
+ (now_utc - timedelta(minutes=5)).strftime("%Y-%m-%dT%H:%M:%SZ")
443+
+ "&to="
444+
+ (now_utc + timedelta(minutes=1)).strftime("%Y-%m-%dT%H:%M:%SZ")
445+
)
446+
447+
response = requests.get(url, timeout=10)
448+
items = json.loads(response.content)["data"]
449+
450+
if len(items) == 0:
451+
raise UnexpectedDataError(url)
452+
453+
return float(items[len(items) - 1]["frequency"])
454+
455+
424456
def get_wind_data(today: str, tomorrow: str) -> NationalGridWindData:
457+
"""Get wind data."""
425458
url = "https://data.elexon.co.uk/bmrs/api/v1/forecast/generation/wind/peak?format=json"
426459
response = requests.get(url, timeout=10)
427460
items = json.loads(response.content)["data"]
@@ -451,6 +484,7 @@ def get_wind_data(today: str, tomorrow: str) -> NationalGridWindData:
451484
def get_demand_day_ahead_forecast(
452485
utc_now: datetime,
453486
) -> NationalGridDemandDayAheadForecast:
487+
"""Get demand day ahead forecast."""
454488
utc_now_formatted = utc_now.strftime("%Y-%m-%dT%H:%M:%SZ")
455489
two_days = (utc_now + timedelta(days=2)).strftime("%Y-%m-%dT%H:%M:%SZ")
456490

@@ -500,6 +534,7 @@ def get_demand_day_ahead_forecast(
500534

501535

502536
def get_national_grid_data(today_utc: str, now_utc: datetime) -> dict[str, Any]:
537+
"""Get national grid data."""
503538
today_minutes = now_utc.hour * 60 + now_utc.minute
504539
settlement_period = (today_minutes // 30) + 1
505540

@@ -525,13 +560,21 @@ def get_national_grid_data(today_utc: str, now_utc: datetime) -> dict[str, Any]:
525560

526561
def get_long_term_wind_forecast_eso_data(
527562
now: datetime,
528-
) -> (NationalGridWindForecastLongTerm, NationalGridWindForecastLongTerm,):
563+
) -> (
564+
NationalGridWindForecastLongTerm,
565+
NationalGridWindForecastLongTerm,
566+
):
567+
"""Get long term wind forecast."""
529568
url = "https://api.nationalgrideso.com/api/3/action/datastore_search?resource_id=93c3048e-1dab-4057-a2a9-417540583929&limit=32000"
530569
response = requests.get(url, timeout=20)
531570

532571
if response.status_code != 200:
533572
raise UnexpectedStatusCode(
534-
url + " - " + "get_long_term_wind_forecast_eso_data" + " - " + str(response.status_code)
573+
url
574+
+ " - "
575+
+ "get_long_term_wind_forecast_eso_data"
576+
+ " - "
577+
+ str(response.status_code)
535578
)
536579

537580
data = json.loads(response.content)
@@ -608,12 +651,17 @@ def get_long_term_embedded_wind_and_solar_forecast(
608651
NationalGridWindForecast,
609652
NationalGridWindForecast,
610653
):
654+
"""Get long term embedded wind and solar forecast."""
611655
url = "https://api.nationalgrideso.com/api/3/action/datastore_search?resource_id=db6c038f-98af-4570-ab60-24d71ebd0ae5&limit=32000"
612656
response = requests.get(url, timeout=20)
613657

614658
if response.status_code != 200:
615659
raise UnexpectedStatusCode(
616-
url + " - " + "get_long_term_embedded_wind_and_solar_forecast" + " - " + str(response.status_code)
660+
url
661+
+ " - "
662+
+ "get_long_term_embedded_wind_and_solar_forecast"
663+
+ " - "
664+
+ str(response.status_code)
617665
)
618666

619667
data = json.loads(response.content)
@@ -714,6 +762,7 @@ def get_long_term_embedded_wind_and_solar_forecast(
714762

715763

716764
def get_dfs_requirements() -> DFSRequirements:
765+
"""Get DFS requirements."""
717766
url = "https://api.nationalgrideso.com/api/3/action/datastore_search?resource_id=7914dd99-fe1c-41ba-9989-5784531c58bb&limit=15&sort=_id%20asc"
718767
response = requests.get(url, timeout=20)
719768

@@ -757,6 +806,7 @@ def get_dfs_requirements() -> DFSRequirements:
757806
def get_demand_forecast(
758807
now: datetime, day_ahead_forecast: NationalGridDemandDayAheadForecast
759808
) -> (NationalGridDemandForecast, NationalGridDemandForecast):
809+
"""Get demand forecast."""
760810
url = "https://api.nationalgrideso.com/api/3/action/datastore_search?resource_id=7c0411cd-2714-4bb5-a408-adb065edf34d&limit=1000"
761811
response = requests.get(url, timeout=20)
762812

@@ -1104,6 +1154,7 @@ def get_demand(grid_generation: NationalGridGeneration):
11041154

11051155
# Just adds up all of the transfers from interconnectors and storage
11061156
def get_transfers(grid_generation: NationalGridGeneration):
1157+
"""Get transfers."""
11071158
if grid_generation is None:
11081159
raise UnexpectedDataError("grid generation None")
11091160

@@ -1119,6 +1170,7 @@ def get_transfers(grid_generation: NationalGridGeneration):
11191170

11201171

11211172
def get_bmrs_data(url: str) -> OrderedDict[str, Any]:
1173+
"""Get BMRS data."""
11221174
response = requests.get(url, timeout=10)
11231175
data = xmltodict.parse(response.content)
11241176

@@ -1129,6 +1181,7 @@ def get_bmrs_data(url: str) -> OrderedDict[str, Any]:
11291181

11301182

11311183
def get_bmrs_data_items(url: str) -> OrderedDict[str, Any]:
1184+
"""Get BMRS data items."""
11321185
data = get_bmrs_data(url)
11331186
if data["response"]["responseMetadata"]["httpCode"] == "204":
11341187
return []
@@ -1149,20 +1202,27 @@ def get_bmrs_data_items(url: str) -> OrderedDict[str, Any]:
11491202

11501203

11511204
def get_bmrs_data_latest(url: str) -> OrderedDict[str, Any]:
1205+
"""Get latest BMRS data."""
1206+
11521207
items = get_bmrs_data_items(url)
1208+
1209+
if len(items) == 0:
1210+
raise UnexpectedDataError(url)
1211+
11531212
latestResponse = items[len(items) - 1]
11541213

11551214
return latestResponse
11561215

11571216

11581217
def obtain_data_with_fallback(current_data, key, func, *args):
1218+
"""Obtain data with fallback."""
11591219
try:
11601220
return func(*args)
11611221
except UnexpectedDataError as e:
11621222
argument_str = ""
11631223
if len(e.args) != 0:
11641224
argument_str = e.args[0]
1165-
_LOGGER.warning("Data unexpected " + argument_str)
1225+
_LOGGER.warning("Data unexpected " + argument_str) # noqa: G003
11661226
return get_data_if_exists(current_data, key)
11671227
except requests.exceptions.ReadTimeout as e:
11681228
_LOGGER.warning("Read timeout error")
@@ -1176,18 +1236,20 @@ def obtain_data_with_fallback(current_data, key, func, *args):
11761236
argument_str = ""
11771237
if len(e.args) != 0:
11781238
argument_str = e.args[0]
1179-
if type(argument_str) is not str:
1239+
if type(argument_str) is not str: # noqa: E721
11801240
argument_str = str(argument_str)
1181-
_LOGGER.warning("Unexpected status code " + argument_str)
1241+
_LOGGER.warning("Unexpected status code " + argument_str) # noqa: G003
11821242
return get_data_if_exists(current_data, key)
11831243
except Exception as e: # pylint: disable=broad-except
11841244
_LOGGER.exception("Failed to obtain data")
11851245
return get_data_if_exists(current_data, key)
11861246

11871247

11881248
def percentage_calc(int_sum, int_total):
1249+
"""Calculate percentage."""
11891250
return round(int_sum / int_total * 100, 2)
11901251

11911252

11921253
def hour_minute_check(date: datetime, hour: int, minute: int) -> bool:
1254+
"""Check if hour and minute match."""
11931255
return date.hour == hour and date.minute == minute

custom_components/national_grid/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ class NationalGridData(TypedDict):
9999
sell_price: float
100100
carbon_intensity: int
101101

102+
grid_frequency: float
103+
102104
wind_data: NationalGridWindData
103105
wind_forecast: NationalGridWindForecast
104106
wind_forecast_earliest: NationalGridWindForecast

0 commit comments

Comments
 (0)