@@ -119,6 +119,10 @@ class SolarAnalysisData:
119119 )
120120
121121
122+ class NoDataAvailableError (Exception ):
123+ """Raised when there is no available data."""
124+
125+
122126# pylint: disable=too-many-statements, too-many-branches, too-many-locals
123127async def run_workflow (user_config_changes : dict [str , Any ]) -> SolarAnalysisData :
124128 """Run the Solar Maintenance App workflow.
@@ -142,6 +146,7 @@ async def run_workflow(user_config_changes: dict[str, Any]) -> SolarAnalysisData
142146 `stat_profile_view_col_to_plot`) are hardcoded.
143147 - If the timezone of the data does not match the timezone in the
144148 configuration.
149+ NoDataAvailableError: If no reporting data is available.
145150 """
146151 config , all_client_site_info = _load_and_validate_config (user_config_changes )
147152
@@ -193,12 +198,19 @@ async def run_workflow(user_config_changes: dict[str, Any]) -> SolarAnalysisData
193198 )
194199 reporting_data_higher_fs = await retrieve_data (reporting_config )
195200
196- weather_data = transform_weather_data (
197- data = weather_data ,
198- weather_feature_names_mapping = config .weather_feature_names_mapping ,
199- time_zone = config .time_zone ,
200- verbose = config .verbose ,
201- )
201+ if reporting_data .empty and reporting_data_higher_fs .empty :
202+ raise NoDataAvailableError ("No reporting data available. Cannot proceed." )
203+
204+ if not weather_data .empty :
205+ weather_data = transform_weather_data (
206+ data = weather_data ,
207+ weather_feature_names_mapping = config .weather_feature_names_mapping ,
208+ time_zone = config .time_zone ,
209+ verbose = config .verbose ,
210+ )
211+ lat_lon_pairs = _create_lat_lon_pairs (
212+ weather_data ["latitude" ].unique (), weather_data ["longitude" ].unique ()
213+ )
202214
203215 reporting_data = transform_reporting_data (
204216 data = reporting_data ,
@@ -224,10 +236,6 @@ async def run_workflow(user_config_changes: dict[str, Any]) -> SolarAnalysisData
224236 lambda x : abs (x ) if np .issubdtype (type (x ), np .number ) else x
225237 )
226238
227- lat_lon_pairs = _create_lat_lon_pairs (
228- weather_data ["latitude" ].unique (), weather_data ["longitude" ].unique ()
229- )
230-
231239 # display the results for each microgrid separately
232240 production_legend_label = tm .translate ("production" )
233241 patch_label = tm .translate ("current value" )
@@ -285,6 +293,10 @@ async def run_workflow(user_config_changes: dict[str, Any]) -> SolarAnalysisData
285293 reporting_data_higher_fs , col_text , verbose = config .verbose
286294 )
287295 normalisation_factor = 1
296+ if data .empty :
297+ reason = NoDataAvailableError (f"No data available for microgrid ID { mid } ." )
298+ print (f"{ type (reason ).__name__ } : { reason } Skipping..." )
299+ continue
288300 timezone = str (pd .to_datetime (data .index ).tzinfo )
289301 if timezone != config .time_zone .key :
290302 raise ValueError ("Timezone mismatch." )
@@ -302,25 +314,31 @@ async def run_workflow(user_config_changes: dict[str, Any]) -> SolarAnalysisData
302314 [k for k in config .baseline_models if k != "weather-based-forecast" ],
303315 )
304316 if "weather-based-forecast" in config .baseline_models :
305- closest_grid_point = _find_closest_grid_point (
306- all_client_site_info [mid ]["latitude" ],
307- all_client_site_info [mid ]["longitude" ],
308- lat_lon_pairs ,
309- )
310- prediction_models .update (
311- prepare_prediction_models (
312- weather_data [
313- (weather_data ["latitude" ] == closest_grid_point [0 ])
314- & (weather_data ["longitude" ] == closest_grid_point [1 ])
315- & (
316- weather_data ["validity_ts" ]
317- <= config .end_timestamp + datetime .timedelta (hours = 1 )
318- )
319- ],
320- model_specs ,
321- ["weather-based-forecast" ],
317+ if weather_data .empty :
318+ reason = NoDataAvailableError ("No weather data available." )
319+ print (
320+ f"{ type (reason ).__name__ } : { reason } Skipping weather-based-forecast model."
321+ )
322+ else :
323+ closest_grid_point = _find_closest_grid_point (
324+ all_client_site_info [mid ]["latitude" ],
325+ all_client_site_info [mid ]["longitude" ],
326+ lat_lon_pairs ,
327+ )
328+ prediction_models .update (
329+ prepare_prediction_models (
330+ weather_data [
331+ (weather_data ["latitude" ] == closest_grid_point [0 ])
332+ & (weather_data ["longitude" ] == closest_grid_point [1 ])
333+ & (
334+ weather_data ["validity_ts" ]
335+ <= config .end_timestamp + datetime .timedelta (hours = 1 )
336+ )
337+ ],
338+ model_specs ,
339+ ["weather-based-forecast" ],
340+ )
322341 )
323- )
324342 # NOTE: the below is a hack until PV lib simulation is properly set up
325343 # (i.e. needs user input for the PV system parameters)
326344 if "simulation" in prediction_models :
@@ -397,8 +415,12 @@ async def run_workflow(user_config_changes: dict[str, Any]) -> SolarAnalysisData
397415 rolling_view_average = RollingPreparer (rolling_view_average_config ).prepare (
398416 data [[long_term_view_col_to_plot ]]
399417 )
400- rolling_view_real_time = RollingPreparer (rolling_view_real_time_config ).prepare (
401- data_higher_fs [real_time_view_col_to_plot ] / normalisation_factor
418+ rolling_view_real_time = (
419+ pd .DataFrame ()
420+ if data_higher_fs .empty
421+ else RollingPreparer (rolling_view_real_time_config ).prepare (
422+ data_higher_fs [real_time_view_col_to_plot ] / normalisation_factor
423+ )
402424 )
403425 daily_production_view = DailyPreparer (daily_plot_config ).prepare (data )
404426 statistical_view = ProfilePreparer (statistical_plot_config ).prepare (data )
@@ -481,54 +503,67 @@ async def run_workflow(user_config_changes: dict[str, Any]) -> SolarAnalysisData
481503 fig = figures_and_axes ["fig_real_time" ]["figure" ],
482504 ax = figures_and_axes ["fig_real_time" ]["axes" ][0 ],
483505 )
484- if plot_settings ["show_annotation" ]:
485- if len (real_time_view_col_to_plot ) == 1 :
486- for col in real_time_view_col_to_plot :
487- recent_y = rolling_view_real_time [str (col )].iloc [- 2 ]
488- _annotate_last_point (
489- figures_and_axes ["fig_real_time" ]["axes" ][0 ], recent_y
490- )
491- patch = Patch (
492- color = plot_settings ["patch_colour" ], label = patch_label
493- )
494- figures_and_axes ["fig_real_time" ]["axes" ][0 ].set_ylabel (real_time_view_ylabel )
495-
496- if plot_settings ["legend_update_on" ] == "figure" :
497- _legend_kwargs_copy = plot_settings ["legend_kwargs" ].copy ()
498- # divide legend labels into groups of 2 if needed
499- _legend_kwargs_copy ["ncol" ] = max (
500- _legend_kwargs_copy ["ncol" ],
501- (
502- len (
503- figures_and_axes ["fig_real_time" ]["axes" ][
504- 0
505- ].get_legend_handles_labels ()[1 ]
506- )
507- + 1
508- )
509- // 2 ,
510- )
506+ if data_higher_fs .empty :
507+ reason = NoDataAvailableError ("No data available for real-time view." )
508+ print (f"{ reason } Skipping this plot." )
509+ print (f"{ type (reason ).__name__ } : { reason } . Skipping this plot." )
510+ # the plotter automatically hides the axis when data is empty
511+ # but we need to deal with the figure itself
512+ # we can safely clear the figure like this because it only plots rolling_view_real_time
513+ figures_and_axes ["fig_real_time" ]["figure" ].clf ()
511514 else :
512- _legend_kwargs_copy = plot_settings ["legend_kwargs" ]
513- plot_manager .update_legend (
514- fig_id = "fig_real_time" ,
515- axs = [figures_and_axes ["fig_real_time" ]["axes" ][0 ]],
516- on = plot_settings ["legend_update_on" ],
517- modifications = {
518- "additional_items" : (
519- [(patch , patch_label )] if plot_settings ["show_annotation" ] else None
520- ),
521- "replace_label" : {
522- str (col ): (
523- tm .translate ("component_{value}" , value = col )
524- if config .split_real_time_view_per_inverter
525- else production_legend_label
515+ if plot_settings ["show_annotation" ]:
516+ if len (real_time_view_col_to_plot ) == 1 :
517+ for col in real_time_view_col_to_plot :
518+ recent_y = rolling_view_real_time [str (col )].iloc [- 2 ]
519+ _annotate_last_point (
520+ figures_and_axes ["fig_real_time" ]["axes" ][0 ], recent_y
521+ )
522+ patch = Patch (
523+ color = plot_settings ["patch_colour" ], label = patch_label
524+ )
525+ figures_and_axes ["fig_real_time" ]["axes" ][0 ].set_ylabel (
526+ real_time_view_ylabel
527+ )
528+
529+ if plot_settings ["legend_update_on" ] == "figure" :
530+ _legend_kwargs_copy = plot_settings ["legend_kwargs" ].copy ()
531+ # divide legend labels into groups of 2 if needed
532+ _legend_kwargs_copy ["ncol" ] = max (
533+ _legend_kwargs_copy ["ncol" ],
534+ (
535+ len (
536+ figures_and_axes ["fig_real_time" ]["axes" ][
537+ 0
538+ ].get_legend_handles_labels ()[1 ]
539+ )
540+ + 1
526541 )
527- for col in real_time_view_col_to_plot
542+ // 2 ,
543+ )
544+ else :
545+ _legend_kwargs_copy = plot_settings ["legend_kwargs" ]
546+ plot_manager .update_legend (
547+ fig_id = "fig_real_time" ,
548+ axs = [figures_and_axes ["fig_real_time" ]["axes" ][0 ]],
549+ on = plot_settings ["legend_update_on" ],
550+ modifications = {
551+ "additional_items" : (
552+ [(patch , patch_label )]
553+ if plot_settings ["show_annotation" ]
554+ else None
555+ ),
556+ "replace_label" : {
557+ str (col ): (
558+ tm .translate ("component_{value}" , value = col )
559+ if config .split_real_time_view_per_inverter
560+ else production_legend_label
561+ )
562+ for col in real_time_view_col_to_plot
563+ },
528564 },
529- },
530- ** _legend_kwargs_copy ,
531- )
565+ ** _legend_kwargs_copy ,
566+ )
532567 # ------------------- #
533568
534569 # --- plot the statistical production profile --- #
@@ -798,7 +833,9 @@ async def run_workflow(user_config_changes: dict[str, Any]) -> SolarAnalysisData
798833 for fig in figures_and_axes .keys ():
799834 plot_manager .adjust_axes_spacing (fig_id = fig , pixels = 100.0 )
800835
801- output .real_time_view [mid ] = rolling_view_real_time
836+ output .real_time_view [mid ] = (
837+ pd .DataFrame () if data_higher_fs .empty else rolling_view_real_time
838+ )
802839 output .rolling_view_short_term [mid ] = rolling_view_short_term
803840 output .rolling_view_long_term [mid ] = rolling_view_long_term
804841 output .rolling_view_average [mid ] = rolling_view_average
0 commit comments