1
+ from collections import OrderedDict
1
2
import csv
3
+ from datetime import datetime , timedelta
2
4
import io
3
5
import json
4
6
import logging
5
- from collections import OrderedDict
6
- from datetime import datetime , timedelta
7
- from typing import Any , TypedDict
7
+ from typing import Any
8
8
9
- import requests
10
- import xmltodict
11
9
from _collections_abc import Mapping
12
10
from dateutil import tz
11
+ import requests
12
+ import xmltodict
13
13
14
14
from homeassistant .config_entries import ConfigEntry
15
15
from homeassistant .core import HomeAssistant
16
16
from homeassistant .helpers .update_coordinator import DataUpdateCoordinator
17
17
from homeassistant .util import dt as dt_util
18
18
19
- from ..const import API_KEY , DOMAIN , API_KEY_PROVIDED
19
+ from ..const import API_KEY , API_KEY_PROVIDED , DOMAIN
20
20
from ..errors import InvalidAuthError , UnexpectedDataError , UnexpectedStatusCode
21
21
from ..models import (
22
+ DFSRequirementItem ,
23
+ DFSRequirements ,
22
24
NationalGridData ,
25
+ NationalGridDemandDayAheadForecast ,
26
+ NationalGridDemandDayAheadForecastItem ,
27
+ NationalGridDemandForecast ,
28
+ NationalGridDemandForecastItem ,
23
29
NationalGridGeneration ,
24
30
NationalGridSolarForecast ,
25
31
NationalGridSolarForecastItem ,
26
32
NationalGridWindData ,
27
33
NationalGridWindForecast ,
28
- NationalGridWindForecastLongTerm ,
29
34
NationalGridWindForecastItem ,
30
- NationalGridDemandForecastItem ,
31
- NationalGridDemandForecast ,
32
- NationalGridDemandDayAheadForecast ,
33
- NationalGridDemandDayAheadForecastItem ,
34
- DFSRequirements ,
35
- DFSRequirementItem ,
35
+ NationalGridWindForecastLongTerm ,
36
36
)
37
37
38
38
_LOGGER = logging .getLogger (__name__ )
39
39
40
40
41
41
class NationalGridCoordinator (DataUpdateCoordinator [NationalGridData ]):
42
+ """National Grid Data Coordinator."""
43
+
42
44
def __init__ (self , hass : HomeAssistant , entry : ConfigEntry ) -> None :
43
- """Initialize"""
45
+ """Initialize. """
44
46
super ().__init__ (
45
47
hass , _LOGGER , name = DOMAIN , update_interval = timedelta (minutes = 5 )
46
48
)
47
49
self ._entry = entry
48
50
49
51
@property
50
52
def entry_id (self ) -> str :
53
+ """Return entry id."""
51
54
return self ._entry .entry_id
52
55
53
56
async def _async_update_data (self ) -> NationalGridData :
54
57
try :
55
58
data = await self .hass .async_add_executor_job (
56
59
get_data , self .hass , self ._entry .data , self .data
57
60
)
58
- except :
59
- raise Exception () # pylint: disable=broad-exception-raised
61
+ except : # noqa: E722
62
+ raise Exception () # pylint: disable=broad-exception-raised # noqa: B904
60
63
61
64
return data
62
65
63
66
64
67
def get_data (
65
68
hass : HomeAssistant , config : Mapping [str , Any ], current_data : NationalGridData
66
69
) -> NationalGridData :
70
+ """Get data."""
67
71
api_key = config [API_KEY ]
68
72
69
73
today_utc = dt_util .utcnow ().strftime ("%Y-%m-%d" )
@@ -89,6 +93,10 @@ def get_data(
89
93
now_utc_full ,
90
94
)
91
95
96
+ current_grid_frequency = obtain_data_with_fallback (
97
+ current_data , "grid_frequency" , get_current_frequency , api_key , now_utc_full
98
+ )
99
+
92
100
wind_forecast = obtain_data_with_fallback (
93
101
current_data ,
94
102
"wind_forecast" ,
@@ -189,6 +197,7 @@ def get_data(
189
197
return NationalGridData (
190
198
sell_price = current_price ,
191
199
carbon_intensity = carbon_intensity ,
200
+ grid_frequency = current_grid_frequency ,
192
201
wind_data = wind_data ,
193
202
wind_forecast = wind_forecast ,
194
203
wind_forecast_earliest = wind_forecast_earliest ,
@@ -213,6 +222,7 @@ def get_data(
213
222
214
223
215
224
def get_data_if_exists (data , key : str ):
225
+ """Get data if exists."""
216
226
if data is None :
217
227
_LOGGER .error ("Previous data is None, returning None" )
218
228
return None
@@ -224,6 +234,7 @@ def get_data_if_exists(data, key: str):
224
234
225
235
226
236
def get_hourly_wind_forecast (now_utc : datetime ) -> NationalGridWindForecast :
237
+ """Get hourly wind forecast."""
227
238
# Need to calculate start. We want data from 8pm on current day to day+2 8pm... however, this is calculated every so often.
228
239
# This means that day + 2 isn't calculated until 03:30 GMT
229
240
@@ -283,6 +294,7 @@ def get_hourly_wind_forecast(now_utc: datetime) -> NationalGridWindForecast:
283
294
284
295
285
296
def get_hourly_wind_forecast_earliest (now_utc : datetime ) -> NationalGridWindForecast :
297
+ """Get hourly wind forecast."""
286
298
# Need to calculate start. We want data from 8pm on current day to day+2 8pm... however, this is calculated every so often.
287
299
# This means that day + 2 isn't calculated until 03:30 GMT
288
300
@@ -344,6 +356,7 @@ def get_hourly_wind_forecast_earliest(now_utc: datetime) -> NationalGridWindFore
344
356
def get_half_hourly_solar_forecast (
345
357
api_key : str , now : datetime
346
358
) -> NationalGridSolarForecast :
359
+ """Get half hourly solar forecast."""
347
360
nearest_30_minutes = now + (now .min .replace (tzinfo = now .tzinfo ) - now ) % timedelta (
348
361
minutes = 30
349
362
)
@@ -407,6 +420,7 @@ def get_half_hourly_solar_forecast(
407
420
408
421
409
422
def get_current_price (api_key : str , today_utc : str ) -> float :
423
+ """Get current grid price."""
410
424
url = (
411
425
"https://api.bmreports.com/BMRS/DERSYSDATA/v1?APIKey="
412
426
+ api_key
@@ -421,7 +435,26 @@ def get_current_price(api_key: str, today_utc: str) -> float:
421
435
return currentPrice
422
436
423
437
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
+
424
456
def get_wind_data (today : str , tomorrow : str ) -> NationalGridWindData :
457
+ """Get wind data."""
425
458
url = "https://data.elexon.co.uk/bmrs/api/v1/forecast/generation/wind/peak?format=json"
426
459
response = requests .get (url , timeout = 10 )
427
460
items = json .loads (response .content )["data" ]
@@ -451,6 +484,7 @@ def get_wind_data(today: str, tomorrow: str) -> NationalGridWindData:
451
484
def get_demand_day_ahead_forecast (
452
485
utc_now : datetime ,
453
486
) -> NationalGridDemandDayAheadForecast :
487
+ """Get demand day ahead forecast."""
454
488
utc_now_formatted = utc_now .strftime ("%Y-%m-%dT%H:%M:%SZ" )
455
489
two_days = (utc_now + timedelta (days = 2 )).strftime ("%Y-%m-%dT%H:%M:%SZ" )
456
490
@@ -500,6 +534,7 @@ def get_demand_day_ahead_forecast(
500
534
501
535
502
536
def get_national_grid_data (today_utc : str , now_utc : datetime ) -> dict [str , Any ]:
537
+ """Get national grid data."""
503
538
today_minutes = now_utc .hour * 60 + now_utc .minute
504
539
settlement_period = (today_minutes // 30 ) + 1
505
540
@@ -525,13 +560,21 @@ def get_national_grid_data(today_utc: str, now_utc: datetime) -> dict[str, Any]:
525
560
526
561
def get_long_term_wind_forecast_eso_data (
527
562
now : datetime ,
528
- ) -> (NationalGridWindForecastLongTerm , NationalGridWindForecastLongTerm ,):
563
+ ) -> (
564
+ NationalGridWindForecastLongTerm ,
565
+ NationalGridWindForecastLongTerm ,
566
+ ):
567
+ """Get long term wind forecast."""
529
568
url = "https://api.nationalgrideso.com/api/3/action/datastore_search?resource_id=93c3048e-1dab-4057-a2a9-417540583929&limit=32000"
530
569
response = requests .get (url , timeout = 20 )
531
570
532
571
if response .status_code != 200 :
533
572
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 )
535
578
)
536
579
537
580
data = json .loads (response .content )
@@ -608,12 +651,17 @@ def get_long_term_embedded_wind_and_solar_forecast(
608
651
NationalGridWindForecast ,
609
652
NationalGridWindForecast ,
610
653
):
654
+ """Get long term embedded wind and solar forecast."""
611
655
url = "https://api.nationalgrideso.com/api/3/action/datastore_search?resource_id=db6c038f-98af-4570-ab60-24d71ebd0ae5&limit=32000"
612
656
response = requests .get (url , timeout = 20 )
613
657
614
658
if response .status_code != 200 :
615
659
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 )
617
665
)
618
666
619
667
data = json .loads (response .content )
@@ -714,6 +762,7 @@ def get_long_term_embedded_wind_and_solar_forecast(
714
762
715
763
716
764
def get_dfs_requirements () -> DFSRequirements :
765
+ """Get DFS requirements."""
717
766
url = "https://api.nationalgrideso.com/api/3/action/datastore_search?resource_id=7914dd99-fe1c-41ba-9989-5784531c58bb&limit=15&sort=_id%20asc"
718
767
response = requests .get (url , timeout = 20 )
719
768
@@ -757,6 +806,7 @@ def get_dfs_requirements() -> DFSRequirements:
757
806
def get_demand_forecast (
758
807
now : datetime , day_ahead_forecast : NationalGridDemandDayAheadForecast
759
808
) -> (NationalGridDemandForecast , NationalGridDemandForecast ):
809
+ """Get demand forecast."""
760
810
url = "https://api.nationalgrideso.com/api/3/action/datastore_search?resource_id=7c0411cd-2714-4bb5-a408-adb065edf34d&limit=1000"
761
811
response = requests .get (url , timeout = 20 )
762
812
@@ -1104,6 +1154,7 @@ def get_demand(grid_generation: NationalGridGeneration):
1104
1154
1105
1155
# Just adds up all of the transfers from interconnectors and storage
1106
1156
def get_transfers (grid_generation : NationalGridGeneration ):
1157
+ """Get transfers."""
1107
1158
if grid_generation is None :
1108
1159
raise UnexpectedDataError ("grid generation None" )
1109
1160
@@ -1119,6 +1170,7 @@ def get_transfers(grid_generation: NationalGridGeneration):
1119
1170
1120
1171
1121
1172
def get_bmrs_data (url : str ) -> OrderedDict [str , Any ]:
1173
+ """Get BMRS data."""
1122
1174
response = requests .get (url , timeout = 10 )
1123
1175
data = xmltodict .parse (response .content )
1124
1176
@@ -1129,6 +1181,7 @@ def get_bmrs_data(url: str) -> OrderedDict[str, Any]:
1129
1181
1130
1182
1131
1183
def get_bmrs_data_items (url : str ) -> OrderedDict [str , Any ]:
1184
+ """Get BMRS data items."""
1132
1185
data = get_bmrs_data (url )
1133
1186
if data ["response" ]["responseMetadata" ]["httpCode" ] == "204" :
1134
1187
return []
@@ -1149,20 +1202,27 @@ def get_bmrs_data_items(url: str) -> OrderedDict[str, Any]:
1149
1202
1150
1203
1151
1204
def get_bmrs_data_latest (url : str ) -> OrderedDict [str , Any ]:
1205
+ """Get latest BMRS data."""
1206
+
1152
1207
items = get_bmrs_data_items (url )
1208
+
1209
+ if len (items ) == 0 :
1210
+ raise UnexpectedDataError (url )
1211
+
1153
1212
latestResponse = items [len (items ) - 1 ]
1154
1213
1155
1214
return latestResponse
1156
1215
1157
1216
1158
1217
def obtain_data_with_fallback (current_data , key , func , * args ):
1218
+ """Obtain data with fallback."""
1159
1219
try :
1160
1220
return func (* args )
1161
1221
except UnexpectedDataError as e :
1162
1222
argument_str = ""
1163
1223
if len (e .args ) != 0 :
1164
1224
argument_str = e .args [0 ]
1165
- _LOGGER .warning ("Data unexpected " + argument_str )
1225
+ _LOGGER .warning ("Data unexpected " + argument_str ) # noqa: G003
1166
1226
return get_data_if_exists (current_data , key )
1167
1227
except requests .exceptions .ReadTimeout as e :
1168
1228
_LOGGER .warning ("Read timeout error" )
@@ -1176,18 +1236,20 @@ def obtain_data_with_fallback(current_data, key, func, *args):
1176
1236
argument_str = ""
1177
1237
if len (e .args ) != 0 :
1178
1238
argument_str = e .args [0 ]
1179
- if type (argument_str ) is not str :
1239
+ if type (argument_str ) is not str : # noqa: E721
1180
1240
argument_str = str (argument_str )
1181
- _LOGGER .warning ("Unexpected status code " + argument_str )
1241
+ _LOGGER .warning ("Unexpected status code " + argument_str ) # noqa: G003
1182
1242
return get_data_if_exists (current_data , key )
1183
1243
except Exception as e : # pylint: disable=broad-except
1184
1244
_LOGGER .exception ("Failed to obtain data" )
1185
1245
return get_data_if_exists (current_data , key )
1186
1246
1187
1247
1188
1248
def percentage_calc (int_sum , int_total ):
1249
+ """Calculate percentage."""
1189
1250
return round (int_sum / int_total * 100 , 2 )
1190
1251
1191
1252
1192
1253
def hour_minute_check (date : datetime , hour : int , minute : int ) -> bool :
1254
+ """Check if hour and minute match."""
1193
1255
return date .hour == hour and date .minute == minute
0 commit comments