4444 ATTR_STAGE ,
4545 ATTR_START_TIME ,
4646 CONF_AREAS ,
47+ CONF_MIN_EVENT_DURATION ,
4748 DEFAULT_SCAN_INTERVAL ,
4849 DOMAIN ,
4950 MANUFACTURER ,
@@ -61,29 +62,29 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
6162 return True
6263
6364
64- async def async_setup_entry (hass : HomeAssistant , entry : ConfigEntry ) -> bool :
65+ async def async_setup_entry (hass : HomeAssistant , config_entry : ConfigEntry ) -> bool :
6566 """Set up LoadShedding as config entry."""
6667 if not hass .data .get (DOMAIN ):
6768 hass .data .setdefault (DOMAIN , {})
6869
6970 sepush : SePush = None
70- if api_key := entry .options .get (CONF_API_KEY ):
71+ if api_key := config_entry .options .get (CONF_API_KEY ):
7172 sepush : SePush = SePush (token = api_key )
7273 if not sepush :
7374 return False
7475
7576 stage_coordinator = LoadSheddingStageCoordinator (hass , sepush )
7677 stage_coordinator .update_interval = timedelta (
77- seconds = entry .options .get (CONF_SCAN_INTERVAL , DEFAULT_SCAN_INTERVAL )
78+ seconds = config_entry .options .get (CONF_SCAN_INTERVAL , DEFAULT_SCAN_INTERVAL )
7879 )
7980
8081 area_coordinator = LoadSheddingAreaCoordinator (
8182 hass , sepush , stage_coordinator = stage_coordinator
8283 )
8384 area_coordinator .update_interval = timedelta (
84- seconds = entry .options .get (CONF_SCAN_INTERVAL , DEFAULT_SCAN_INTERVAL )
85+ seconds = config_entry .options .get (CONF_SCAN_INTERVAL , DEFAULT_SCAN_INTERVAL )
8586 )
86- for conf in entry .options .get (CONF_AREAS , {}).values ():
87+ for conf in config_entry .options .get (CONF_AREAS , {}).values ():
8788 area = Area (
8889 id = conf .get (CONF_ID ),
8990 name = conf .get (CONF_NAME ),
@@ -95,18 +96,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
9596 quota_coordinator = LoadSheddingQuotaCoordinator (hass , sepush )
9697 quota_coordinator .update_interval = timedelta (seconds = QUOTA_UPDATE_INTERVAL )
9798
98- hass .data [DOMAIN ][entry .entry_id ] = {
99+ hass .data [DOMAIN ][config_entry .entry_id ] = {
99100 ATTR_STAGE : stage_coordinator ,
100101 ATTR_AREA : area_coordinator ,
101102 ATTR_QUOTA : quota_coordinator ,
102103 }
103104
104- entry .async_on_unload (entry .add_update_listener (update_listener ))
105+ config_entry .async_on_unload (config_entry .add_update_listener (update_listener ))
105106
106107 await stage_coordinator .async_config_entry_first_refresh ()
107108 await area_coordinator .async_config_entry_first_refresh ()
108109 await quota_coordinator .async_config_entry_first_refresh ()
109- await hass .config_entries .async_forward_entry_setups (entry , PLATFORMS )
110+ await hass .config_entries .async_forward_entry_setups (config_entry , PLATFORMS )
110111
111112 return True
112113
@@ -119,14 +120,14 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
119120 return unload_ok
120121
121122
122- async def async_reload_entry (hass : HomeAssistant , entry : ConfigEntry ):
123+ async def async_reload_entry (hass : HomeAssistant , config_entry : ConfigEntry ):
123124 """Reload config entry."""
124- await hass .config_entries .async_reload (entry .entry_id )
125+ await hass .config_entries .async_reload (config_entry .entry_id )
125126
126127
127- async def update_listener (hass : HomeAssistant , entry : ConfigEntry ) -> bool :
128+ async def update_listener (hass : HomeAssistant , config_entry : ConfigEntry ) -> bool :
128129 """Update listener."""
129- return await hass .config_entries .async_reload (entry .entry_id )
130+ return await hass .config_entries .async_reload (config_entry .entry_id )
130131
131132
132133async def async_migrate_entry (hass : HomeAssistant , config_entry : ConfigEntry ) -> bool :
@@ -288,17 +289,17 @@ async def _async_update_data(self) -> dict:
288289 return self .data
289290
290291 async def async_update_area (self ) -> dict :
291- """Retrieve schedule data."""
292- areas_stage_schedules : dict = {}
292+ """Retrieve area data."""
293+ area_id_data : dict = {}
293294
294295 for area in self .areas :
295- # Get forecast for area
296- events = []
297296 try :
298297 esp = await self .hass .async_add_executor_job (self .sepush .area , area .id )
299298 except SePushError as err :
300299 raise UpdateFailed (err ) from err
301300
301+ # Get events for area
302+ events = []
302303 for event in esp .get ("events" , {}):
303304 note = event .get ("note" )
304305 parts = str (note ).split (" " )
@@ -318,7 +319,6 @@ async def async_update_area(self) -> dict:
318319
319320 # Get schedule for area
320321 stage_schedule = {}
321- sast = timezone (timedelta (hours = + 2 ), "SAST" )
322322 for day in esp .get ("schedule" , {}).get ("days" , []):
323323 date = datetime .strptime (day .get ("date" ), "%Y-%m-%d" )
324324 stage_timeslots = day .get ("stages" , [])
@@ -328,30 +328,8 @@ async def async_update_area(self) -> dict:
328328 stage_schedule [stage ] = []
329329 for timeslot in timeslots :
330330 start_str , end_str = timeslot .strip ().split ("-" )
331- start = (
332- datetime .strptime (start_str , "%H:%M" )
333- .replace (
334- year = date .year ,
335- month = date .month ,
336- day = date .day ,
337- second = 0 ,
338- microsecond = 0 ,
339- tzinfo = sast ,
340- )
341- .astimezone (timezone .utc )
342- )
343- end = (
344- datetime .strptime (end_str , "%H:%M" )
345- .replace (
346- year = date .year ,
347- month = date .month ,
348- day = date .day ,
349- second = 0 ,
350- microsecond = 0 ,
351- tzinfo = sast ,
352- )
353- .astimezone (timezone .utc )
354- )
331+ start = utc_dt (date , datetime .strptime (start_str , "%H:%M" ))
332+ end = utc_dt (date , datetime .strptime (end_str , "%H:%M" ))
355333 if end < start :
356334 end = end + timedelta (days = 1 )
357335 stage_schedule [stage ].append (
@@ -362,12 +340,12 @@ async def async_update_area(self) -> dict:
362340 }
363341 )
364342
365- areas_stage_schedules [area .id ] = {
343+ area_id_data [area .id ] = {
366344 ATTR_EVENTS : events ,
367345 ATTR_SCHEDULE : stage_schedule ,
368346 }
369347
370- return areas_stage_schedules
348+ return area_id_data
371349
372350 async def async_area_forecast (self ) -> None :
373351 """Derive area forecast from planned stages and area schedule."""
@@ -379,8 +357,6 @@ async def async_area_forecast(self) -> None:
379357 eskom_stages = stages .get (eskom , {}).get (ATTR_PLANNED , [])
380358 cape_town_stages = stages .get (cape_town , {}).get (ATTR_PLANNED , [])
381359
382- now = datetime .now (timezone .utc )
383-
384360 for area_id , data in self .data .items ():
385361 stage_schedules = data .get (ATTR_SCHEDULE )
386362
@@ -402,9 +378,6 @@ async def async_area_forecast(self) -> None:
402378 start_time = timeslot .get (ATTR_START_TIME )
403379 end_time = timeslot .get (ATTR_END_TIME )
404380
405- # if end_time < now:
406- # continue
407-
408381 if start_time >= planned_end_time :
409382 continue
410383 if end_time <= planned_start_time :
@@ -425,6 +398,13 @@ async def async_area_forecast(self) -> None:
425398 if start_time == end_time :
426399 continue
427400
401+ # Minimum event duration
402+ min_event_dur = self .stage_coordinator .config_entry .options .get (
403+ CONF_MIN_EVENT_DURATION , 30
404+ ) # minutes
405+ if end_time - start_time < timedelta (minutes = min_event_dur ):
406+ continue
407+
428408 forecast .append (
429409 {
430410 ATTR_STAGE : planned_stage ,
@@ -436,6 +416,20 @@ async def async_area_forecast(self) -> None:
436416 data [ATTR_FORECAST ] = forecast
437417
438418
419+ def utc_dt (date : datetime , time : datetime ) -> datetime :
420+ """Given a date and time in SAST, this function returns a datetime object in UTC"""
421+ sast = timezone (timedelta (hours = + 2 ), "SAST" )
422+
423+ return time .replace (
424+ year = date .year ,
425+ month = date .month ,
426+ day = date .day ,
427+ second = 0 ,
428+ microsecond = 0 ,
429+ tzinfo = sast ,
430+ ).astimezone (timezone .utc )
431+
432+
439433class LoadSheddingQuotaCoordinator (DataUpdateCoordinator [dict [str , Any ]]):
440434 """Class to manage fetching LoadShedding Quota."""
441435
0 commit comments