def get(self, id: int): """GET from /sensors/<id>""" return render_flexmeasures_template( "views/sensors.html", sensor_id=id, msg="", )
def render_user(user: Optional[User], msg: str = None): user_form = UserForm() user_form.process(obj=user) return render_flexmeasures_template("crud/user.html", user=user, user_form=user_form, msg=msg)
def account_view(): return render_flexmeasures_template( "admin/account.html", logged_in_user=current_user, roles=",".join([role.name for role in current_user.roles]), num_assets=len(get_assets()), )
def owned_by(self, owner_id: str): """/assets/owned_by/<user_id>""" get_assets_response = InternalApi().get( url_for("flexmeasures_api_v2_0.get_assets"), query={"owner_id": owner_id}) assets = [ process_internal_api_response(ad, make_obj=True) for ad in get_assets_response.json() ] return render_flexmeasures_template("crud/assets.html", assets=assets)
def handle_bad_request(e: BadRequest): return ( render_flexmeasures_template( "error.html", error_class=e.__class__.__name__, error_description="We encountered a bad request.", error_message=e.description, ), 400, )
def handle_500_error(e: InternalServerError): return ( render_flexmeasures_template( "error.html", error_class=e.__class__.__name__, error_description="We encountered an internal problem.", error_message=str(e), ), 500, )
def handle_not_found(e): return ( render_flexmeasures_template( "error.html", error_class="", # standard message already includes "404: NotFound" error_description="The page you are looking for cannot be found.", error_message=str(e), ), 404, )
def unauthenticated_handler(): """An unauthenticated handler which renders an HTML error page""" return ( render_flexmeasures_template( "error.html", error_class=auth_setup.UNAUTH_ERROR_CLASS, error_message=auth_setup.UNAUTH_MSG, ), auth_setup.UNAUTH_STATUS_CODE, )
def control_view(): """Control view. This page lists balancing opportunities for a selected time window. The user can place manual orders or choose to automate the ordering process. """ next24hours = [(time_utils.get_most_recent_hour() + timedelta(hours=i)).strftime("%I:00 %p") for i in range(1, 26)] return render_flexmeasures_template("views/control.html", next24hours=next24hours)
def unauthorized_handler(): """An unauthorized handler which renders an HTML error page""" return ( render_flexmeasures_template( "error.html", error_class=auth_error_handling.FORBIDDEN_ERROR_CLASS, error_message=auth_error_handling.FORBIDDEN_MSG, ), auth_error_handling.FORBIDDEN_STATUS_CODE, )
def logged_in_user_view(): """TODO: - Show account name & roles - Count their assets with a query, link to their (new) list """ return render_flexmeasures_template( "admin/logged_in_user.html", logged_in_user=current_user, roles=",".join([role.name for role in current_user.roles]), num_assets=len(get_assets()), )
def index(self, msg=""): """/assets""" get_assets_response = InternalApi().get( url_for("flexmeasures_api_v2_0.get_assets")) assets = [ process_internal_api_response(ad, make_obj=True) for ad in get_assets_response.json() ] return render_flexmeasures_template("crud/assets.html", assets=assets, message=msg)
def get(self, id: str): """GET from /assets/<id> where id can be 'new' (and thus the form for asset creation is shown)""" if id == "new": if not current_user.has_role("admin"): return unauthorized_handler(None, []) asset_form = with_options(NewAssetForm()) return render_flexmeasures_template( "crud/asset_new.html", asset_form=asset_form, msg="", map_center=get_center_location_of_assets(user=current_user), mapboxAccessToken=current_app.config.get( "MAPBOX_ACCESS_TOKEN", ""), ) get_asset_response = InternalApi().get( url_for("AssetAPI:fetch_one", id=id)) asset_dict = get_asset_response.json() asset_form = with_options(AssetForm()) asset = process_internal_api_response(asset_dict, int(id), make_obj=True) asset_form.process(data=process_internal_api_response(asset_dict)) latest_measurement_time_str, asset_plot_html = _get_latest_power_plot( asset) return render_flexmeasures_template( "crud/asset.html", asset=asset, asset_form=asset_form, msg="", latest_measurement_time_str=latest_measurement_time_str, asset_plot_html=asset_plot_html, mapboxAccessToken=current_app.config.get("MAPBOX_ACCESS_TOKEN", ""), )
def index(self, msg=""): """/assets""" get_assets_response = InternalApi().get( url_for("AssetAPI:index"), query={"account_id": current_user.account_id}) assets = [ process_internal_api_response(ad, make_obj=True) for ad in get_assets_response.json() ] return render_flexmeasures_template("crud/assets.html", account=current_user.account, assets=assets, message=msg)
def index(self): """/users""" include_inactive = request.args.get("include_inactive", "0") != "0" get_users_response = InternalApi().get( url_for("flexmeasures_api_v2_0.get_users", include_inactive=include_inactive)) users = [ process_internal_api_response(user, make_obj=True) for user in get_users_response.json() ] return render_flexmeasures_template("crud/users.html", users=users, include_inactive=include_inactive)
def handle_generic_http_exception(e: HTTPException): """This handles all known exception as fall-back""" error_code = 500 if hasattr(e, "code") and e.code is not None: error_code = e.code error_text = getattr(e, "description", str(e)) return ( render_flexmeasures_template( "error.html", error_class=e.__class__.__name__, error_description="We encountered an Http exception.", error_message=error_text, ), error_code, )
def new_dashboard_view(): """Dashboard view. This is the default landing page. It shows a map with the location of all of the assets in the user's account, as well as a breakdown of the asset types. Here, we are only interested in showing assets with power sensors. Admins get to see all assets. TODO: Assets for which the platform has identified upcoming balancing opportunities are highlighted. """ msg = "" if "clear-session" in request.values: clear_session() msg = "Your session was cleared." aggregate_groups = current_app.config.get("FLEXMEASURES_ASSET_TYPE_GROUPS", {}) asset_groups = get_asset_group_queries( custom_additional_groups=aggregate_groups) map_asset_groups = {} for asset_group_name, asset_group_query in asset_groups.items(): asset_group = AssetGroup(asset_group_name, asset_query=asset_group_query) if any([a.location for a in asset_group.assets]): map_asset_groups[asset_group_name] = asset_group # Pack CDN resources (from pandas_bokeh/base.py) bokeh_html_embedded = "" for css in CDN.css_files: bokeh_html_embedded += ( """<link href="%s" rel="stylesheet" type="text/css">\n""" % css) for js in CDN.js_files: bokeh_html_embedded += """<script src="%s"></script>\n""" % js return render_flexmeasures_template( "views/new_dashboard.html", message=msg, bokeh_html_embedded=bokeh_html_embedded, mapboxAccessToken=current_app.config.get("MAPBOX_ACCESS_TOKEN", ""), map_center=get_center_location_of_assets(user=current_user), asset_groups=map_asset_groups, aggregate_groups=aggregate_groups, )
def owned_by(self, account_id: str): """/assets/owned_by/<account_id>""" msg = "" get_assets_response = InternalApi().get( url_for("AssetAPI:index"), query={"account_id": account_id}, do_not_raise_for=[404], ) if get_assets_response.status_code == 404: assets = [] msg = f"Account {account_id} unknown." else: assets = [ process_internal_api_response(ad, make_obj=True) for ad in get_assets_response.json() ] return render_flexmeasures_template( "crud/assets.html", account=Account.query.get(account_id), assets=assets, msg=msg, )
def index(self): """/users""" include_inactive = request.args.get("include_inactive", "0") != "0" users = [] if current_user.has_role(ADMIN_ROLE) or current_user.has_role( ADMIN_READER_ROLE): accounts = Account.query.all() else: accounts = [current_user.account] for account in accounts: get_users_response = InternalApi().get( url_for( "UserAPI:index", account_id=account.id, include_inactive=include_inactive, )) users += [ process_internal_api_response(user, make_obj=True) for user in get_users_response.json() ] return render_flexmeasures_template("crud/users.html", users=users, include_inactive=include_inactive)
def dashboard_view(): """Dashboard view. This is the default landing page for the platform user. It shows a map with the location and status of all of the user's assets, as well as a breakdown of the asset types in the user's portfolio. Assets for which the platform has identified upcoming balancing opportunities are highlighted. """ msg = "" if "clear-session" in request.values: clear_session() msg = "Your session was cleared." aggregate_groups = ["renewables", "EVSE"] asset_groups = get_asset_group_queries( custom_additional_groups=aggregate_groups) map_asset_groups = {} for asset_group_name in asset_groups: asset_group = Resource(asset_group_name) map_asset_groups[asset_group_name] = asset_group # Pack CDN resources (from pandas_bokeh/base.py) bokeh_html_embedded = "" for css in CDN.css_files: bokeh_html_embedded += ( """<link href="%s" rel="stylesheet" type="text/css">\n""" % css) for js in CDN.js_files: bokeh_html_embedded += """<script src="%s"></script>\n""" % js return render_flexmeasures_template( "views/dashboard.html", message=msg, bokeh_html_embedded=bokeh_html_embedded, mapboxAccessToken=current_app.config.get("MAPBOX_ACCESS_TOKEN", ""), map_center=get_center_location(user=current_user), asset_groups=map_asset_groups, aggregate_groups=aggregate_groups, )
def post(self, id: str): """POST to /assets/<id>, where id can be 'create' (and thus a new asset is made from POST data) Most of the code deals with creating a user for the asset if no existing is chosen. """ asset: Asset = None error_msg = "" if id == "create": asset_form = with_options(NewAssetForm()) owner, owner_error = set_owner(asset_form) market, market_error = set_market(asset_form) if asset_form.asset_type_name.data == "none chosen": asset_form.asset_type_name.data = "" form_valid = asset_form.validate_on_submit() # Fill up the form with useful errors for the user if owner_error is not None: form_valid = False asset_form.owner_id.errors.append(owner_error) if market_error is not None: form_valid = False asset_form.market_id.errors.append(market_error) # Create new asset or return the form for new assets with a message if form_valid and owner is not None and market is not None: post_asset_response = InternalApi().post( url_for("flexmeasures_api_v2_0.post_assets"), args=asset_form.to_json(), do_not_raise_for=[400, 422], ) if post_asset_response.status_code in (200, 201): asset_dict = post_asset_response.json() asset = process_internal_api_response( asset_dict, int(asset_dict["id"]), make_obj=True) msg = "Creation was successful." else: current_app.logger.error( f"Internal asset API call unsuccessful [{post_asset_response.status_code}]: {post_asset_response.text}" ) asset_form.process_api_validation_errors( post_asset_response.json()) if "message" in post_asset_response.json(): error_msg = post_asset_response.json()["message"] if asset is None: msg = "Cannot create asset. " + error_msg return render_flexmeasures_template( "crud/asset_new.html", asset_form=asset_form, msg=msg, map_center=get_center_location(db, user=current_user), mapboxAccessToken=current_app.config.get( "MAPBOX_ACCESS_TOKEN", ""), ) else: asset_form = with_options(AssetForm()) if not asset_form.validate_on_submit(): return render_flexmeasures_template( "crud/asset_new.html", asset_form=asset_form, msg="Cannot edit asset.", map_center=get_center_location(db, user=current_user), mapboxAccessToken=current_app.config.get( "MAPBOX_ACCESS_TOKEN", ""), ) patch_asset_response = InternalApi().patch( url_for("flexmeasures_api_v2_0.patch_asset", id=id), args=asset_form.to_json(), do_not_raise_for=[400, 422], ) asset_dict = patch_asset_response.json() if patch_asset_response.status_code in (200, 201): asset = process_internal_api_response(asset_dict, int(id), make_obj=True) msg = "Editing was successful." else: current_app.logger.error( f"Internal asset API call unsuccessful [{patch_asset_response.status_code}]: {patch_asset_response.text}" ) asset_form.process_api_validation_errors( patch_asset_response.json()) asset = Asset.query.get(id) latest_measurement_time_str, asset_plot_html = get_latest_power_as_plot( asset) return render_flexmeasures_template( "crud/asset.html", asset=asset, asset_form=asset_form, msg=msg, latest_measurement_time_str=latest_measurement_time_str, asset_plot_html=asset_plot_html, mapboxAccessToken=current_app.config.get("MAPBOX_ACCESS_TOKEN", ""), )
def post(self, id: str): """POST to /assets/<id>, where id can be 'create' (and thus a new asset is made from POST data) Most of the code deals with creating a user for the asset if no existing is chosen. """ asset: GenericAsset = None error_msg = "" if id == "create": asset_form = with_options(NewAssetForm()) account, account_error = _set_account(asset_form) asset_type, asset_type_error = _set_asset_type(asset_form) form_valid = asset_form.validate_on_submit() # Fill up the form with useful errors for the user if account_error is not None: form_valid = False asset_form.account_id.errors.append(account_error) if asset_type_error is not None: form_valid = False asset_form.generic_asset_type_id.errors.append( asset_type_error) # Create new asset or return the form for new assets with a message if form_valid and asset_type is not None: post_asset_response = InternalApi().post( url_for("AssetAPI:post"), args=asset_form.to_json(), do_not_raise_for=[400, 422], ) if post_asset_response.status_code in (200, 201): asset_dict = post_asset_response.json() asset = process_internal_api_response( asset_dict, int(asset_dict["id"]), make_obj=True) msg = "Creation was successful." else: current_app.logger.error( f"Internal asset API call unsuccessful [{post_asset_response.status_code}]: {post_asset_response.text}" ) asset_form.process_api_validation_errors( post_asset_response.json()) if ("message" in post_asset_response.json() and "json" in post_asset_response.json()["message"]): error_msg = str( post_asset_response.json()["message"]["json"]) if asset is None: msg = "Cannot create asset. " + error_msg return render_flexmeasures_template( "crud/asset_new.html", asset_form=asset_form, msg=msg, map_center=get_center_location_of_assets( user=current_user), mapboxAccessToken=current_app.config.get( "MAPBOX_ACCESS_TOKEN", ""), ) else: asset_form = with_options(AssetForm()) if not asset_form.validate_on_submit(): asset = GenericAsset.query.get(id) latest_measurement_time_str, asset_plot_html = _get_latest_power_plot( asset) # Display the form data, but set some extra data which the page wants to show. asset_info = asset_form.to_json() asset_info["id"] = id asset_info["account_id"] = asset.account_id asset = process_internal_api_response(asset_info, int(id), make_obj=True) return render_flexmeasures_template( "crud/asset.html", asset_form=asset_form, asset=asset, msg="Cannot edit asset.", latest_measurement_time_str=latest_measurement_time_str, asset_plot_html=asset_plot_html, mapboxAccessToken=current_app.config.get( "MAPBOX_ACCESS_TOKEN", ""), ) patch_asset_response = InternalApi().patch( url_for("AssetAPI:patch", id=id), args=asset_form.to_json(), do_not_raise_for=[400, 422], ) asset_dict = patch_asset_response.json() if patch_asset_response.status_code in (200, 201): asset = process_internal_api_response(asset_dict, int(id), make_obj=True) msg = "Editing was successful." else: current_app.logger.error( f"Internal asset API call unsuccessful [{patch_asset_response.status_code}]: {patch_asset_response.text}" ) msg = "Cannot edit asset." asset_form.process_api_validation_errors( patch_asset_response.json()) asset = GenericAsset.query.get(id) latest_measurement_time_str, asset_plot_html = _get_latest_power_plot( asset) return render_flexmeasures_template( "crud/asset.html", asset=asset, asset_form=asset_form, msg=msg, latest_measurement_time_str=latest_measurement_time_str, asset_plot_html=asset_plot_html, mapboxAccessToken=current_app.config.get("MAPBOX_ACCESS_TOKEN", ""), )
def portfolio_view(): # noqa: C901 """Portfolio view. By default, this page shows live results (production, consumption and market data) from the user's portfolio. Time windows for which the platform has identified upcoming balancing opportunities are highlighted. The page can also be used to navigate historical results. """ set_time_range_for_session() start = session.get("start_time") end = session.get("end_time") resolution = session.get("resolution") # Get plot perspective perspectives = ["production", "consumption"] default_stack_side = "production" # todo: move to user config setting show_stacked = request.values.get("show_stacked", default_stack_side) perspectives.remove(show_stacked) show_summed: str = perspectives[0] plot_label = f"Stacked {show_stacked} vs aggregated {show_summed}" # Get structure and data assets: List[Asset] = get_assets(order_by_asset_attribute="display_name", order_direction="asc") represented_asset_types, markets, resource_dict = get_structure(assets) for resource_name, resource in resource_dict.items(): resource.load_sensor_data( [Power, Price], start=start, end=end, resolution=resolution, exclude_source_types=["scheduling script"], ) # The resource caches the results ( supply_resources_df_dict, demand_resources_df_dict, production_per_asset_type, consumption_per_asset_type, production_per_asset, consumption_per_asset, ) = get_power_data(resource_dict) price_bdf_dict, average_price_dict = get_price_data(resource_dict) # Pick a perspective for summing and for stacking sum_dict = (demand_resources_df_dict.values() if show_summed == "consumption" else supply_resources_df_dict.values()) power_sum_df = (pd.concat(sum_dict, axis=1).sum(axis=1).to_frame( name="event_value") if sum_dict else pd.DataFrame()) # Create summed plot power_sum_df = data_or_zeroes(power_sum_df, start, end, resolution) x_range = plotting.make_range( pd.date_range(start, end, freq=resolution, closed="left")) fig_profile = plotting.create_graph( power_sum_df, unit="MW", title=plot_label, x_range=x_range, x_label="Time (resolution of %s)" % time_utils.freq_label_to_human_readable_label(resolution), y_label="Power (in MW)", legend_location="top_right", legend_labels=(capitalize(show_summed), None, None), show_y_floats=True, non_negative_only=True, ) fig_profile.plot_height = 450 fig_profile.plot_width = 900 # Create stacked plot stack_dict = (rename_event_value_column_to_resource_name( supply_resources_df_dict).values() if show_summed == "consumption" else rename_event_value_column_to_resource_name( demand_resources_df_dict).values()) df_stacked_data = pd.concat(stack_dict, axis=1) if stack_dict else pd.DataFrame() df_stacked_data = data_or_zeroes(df_stacked_data, start, end, resolution) df_stacked_areas = stack_df(df_stacked_data) num_areas = df_stacked_areas.shape[1] if num_areas <= 2: colors = ["#99d594", "#dddd9d"] else: colors = palettes.brewer["Spectral"][num_areas] df_stacked_data = time_utils.tz_index_naively(df_stacked_data) x_points = np.hstack((df_stacked_data.index[::-1], df_stacked_data.index)) fig_profile.grid.minor_grid_line_color = "#eeeeee" for a, area in enumerate(df_stacked_areas): fig_profile.patch( x_points, df_stacked_areas[area].values, color=colors[a], alpha=0.8, line_color=None, legend=df_stacked_data.columns[a], level="underlay", ) portfolio_plots_script, portfolio_plots_divs = components(fig_profile) # Flexibility numbers and a mocked control action are mocked for demo mode at the moment flex_info = {} if current_app.config.get("FLEXMEASURES_MODE") == "demo": flex_info = mock_flex_info(assets, represented_asset_types) fig_actions = mock_flex_figure(x_range, power_sum_df.index, fig_profile.plot_width) mock_flex_action_in_main_figure(fig_profile) portfolio_plots_script, portfolio_plots_divs = components( (fig_profile, fig_actions)) return render_flexmeasures_template( "views/portfolio.html", assets=assets, average_prices=average_price_dict, asset_types=represented_asset_types, markets=markets, production_per_asset=production_per_asset, consumption_per_asset=consumption_per_asset, production_per_asset_type=production_per_asset_type, consumption_per_asset_type=consumption_per_asset_type, sum_production=sum(production_per_asset_type.values()), sum_consumption=sum(consumption_per_asset_type.values()), flex_info=flex_info, portfolio_plots_script=portfolio_plots_script, portfolio_plots_divs=portfolio_plots_divs, alt_stacking=show_summed, fm_mode=current_app.config.get("FLEXMEASURES_MODE"), )
def portfolio_view(): # noqa: C901 """Portfolio view. By default, this page shows live results (production, consumption and market data) from the user's portfolio. Time windows for which the platform has identified upcoming balancing opportunities are highlighted. The page can also be used to navigate historical results. """ set_time_range_for_session() start = session.get("start_time") end = session.get("end_time") resolution = session.get("resolution") # Get plot perspective perspectives = ["production", "consumption"] default_stack_side = "production" # todo: move to user config setting show_stacked = request.values.get("show_stacked", default_stack_side) perspectives.remove(show_stacked) show_summed: str = perspectives[0] plot_label = f"Stacked {show_stacked} vs aggregated {show_summed}" # Get structure and data assets: List[Asset] = get_assets( order_by_asset_attribute="display_name", order_direction="asc" ) represented_asset_types, markets, resource_dict = get_structure(assets) for resource_name, resource in resource_dict.items(): resource.load_sensor_data( [Power, Price], start=start, end=end, resolution=resolution, exclude_source_types=["scheduling script"], ) # The resource caches the results ( supply_resources_df_dict, demand_resources_df_dict, production_per_asset_type, consumption_per_asset_type, production_per_asset, consumption_per_asset, ) = get_power_data(resource_dict) price_bdf_dict, average_price_dict = get_price_data(resource_dict) # Pick a perspective for summing and for stacking sum_dict = ( demand_resources_df_dict.values() if show_summed == "consumption" else supply_resources_df_dict.values() ) power_sum_df = ( pd.concat(sum_dict, axis=1).sum(axis=1).to_frame(name="event_value") if sum_dict else pd.DataFrame() ) stack_dict = ( rename_event_value_column_to_resource_name(supply_resources_df_dict).values() if show_summed == "consumption" else rename_event_value_column_to_resource_name( demand_resources_df_dict ).values() ) df_stacked_data = pd.concat(stack_dict, axis=1) if stack_dict else pd.DataFrame() # Create summed plot power_sum_df = data_or_zeroes(power_sum_df, start, end, resolution) x_range = plotting.make_range( pd.date_range(start, end, freq=resolution, closed="left") ) fig_profile = plotting.create_graph( power_sum_df, unit="MW", title=plot_label, x_range=x_range, x_label="Time (resolution of %s)" % time_utils.freq_label_to_human_readable_label(resolution), y_label="Power (in MW)", legend_location="top_right", legend_labels=(capitalize(show_summed), None, None), show_y_floats=True, non_negative_only=True, ) fig_profile.plot_height = 450 fig_profile.plot_width = 900 # Create stacked plot df_stacked_data = data_or_zeroes(df_stacked_data, start, end, resolution) df_stacked_areas = stack_df(df_stacked_data) num_areas = df_stacked_areas.shape[1] if num_areas <= 2: colors = ["#99d594", "#dddd9d"] else: colors = palettes.brewer["Spectral"][num_areas] df_stacked_data = time_utils.tz_index_naively(df_stacked_data) x_points = np.hstack((df_stacked_data.index[::-1], df_stacked_data.index)) fig_profile.grid.minor_grid_line_color = "#eeeeee" for a, area in enumerate(df_stacked_areas): fig_profile.patch( x_points, df_stacked_areas[area].values, color=colors[a], alpha=0.8, line_color=None, legend=df_stacked_data.columns[a], level="underlay", ) # Flexibility numbers are mocked for now curtailment_per_asset = {a.name: 0 for a in assets} shifting_per_asset = {a.name: 0 for a in assets} profit_loss_flexibility_per_asset = {a.name: 0 for a in assets} curtailment_per_asset_type = {k: 0 for k in represented_asset_types.keys()} shifting_per_asset_type = {k: 0 for k in represented_asset_types.keys()} profit_loss_flexibility_per_asset_type = { k: 0 for k in represented_asset_types.keys() } shifting_per_asset["48_r"] = 1.1 profit_loss_flexibility_per_asset["48_r"] = 76000 shifting_per_asset_type["one-way EVSE"] = shifting_per_asset["48_r"] profit_loss_flexibility_per_asset_type[ "one-way EVSE" ] = profit_loss_flexibility_per_asset["48_r"] curtailment_per_asset["hw-onshore"] = 1.3 profit_loss_flexibility_per_asset["hw-onshore"] = 84000 curtailment_per_asset_type["wind turbines"] = curtailment_per_asset["hw-onshore"] profit_loss_flexibility_per_asset_type[ "wind turbines" ] = profit_loss_flexibility_per_asset["hw-onshore"] # Add referral to mocked control action this_hour = time_utils.get_most_recent_hour() next4am = [ dt for dt in [this_hour + timedelta(hours=i) for i in range(1, 25)] if dt.hour == 4 ][0] # TODO: show when user has (possible) actions in order book for a time slot if current_user.is_authenticated and ( current_user.has_role("admin") or "wind" in current_user.email or "charging" in current_user.email ): plotting.highlight( fig_profile, next4am, next4am + timedelta(hours=1), redirect_to="/control" ) # actions df_actions = pd.DataFrame(index=power_sum_df.index, columns=["event_value"]).fillna( 0 ) if next4am in df_actions.index: if current_user.is_authenticated: if current_user.has_role("admin"): df_actions.loc[next4am] = -2.4 # mock two actions elif "wind" in current_user.email: df_actions.loc[next4am] = -1.3 # mock one action elif "charging" in current_user.email: df_actions.loc[next4am] = -1.1 # mock one action next2am = [ dt for dt in [this_hour + timedelta(hours=i) for i in range(1, 25)] if dt.hour == 2 ][0] if next2am in df_actions.index: if next2am < next4am and ( current_user.is_authenticated and ( current_user.has_role("admin") or "wind" in current_user.email or "charging" in current_user.email ) ): # mock the shift "payback" (actually occurs earlier in our mock example) df_actions.loc[next2am] = 1.1 next9am = [ dt for dt in [this_hour + timedelta(hours=i) for i in range(1, 25)] if dt.hour == 9 ][0] if next9am in df_actions.index: # mock some other ordered actions that are not in an opportunity hour anymore df_actions.loc[next9am] = 3.5 fig_actions = plotting.create_graph( df_actions, unit="MW", title="Ordered balancing actions", x_range=x_range, y_label="Power (in MW)", ) fig_actions.plot_height = 150 fig_actions.plot_width = fig_profile.plot_width fig_actions.xaxis.visible = False if current_user.is_authenticated and ( current_user.has_role("admin") or "wind" in current_user.email or "charging" in current_user.email ): plotting.highlight( fig_actions, next4am, next4am + timedelta(hours=1), redirect_to="/control" ) portfolio_plots_script, portfolio_plots_divs = components( (fig_profile, fig_actions) ) next24hours = [ (time_utils.get_most_recent_hour() + timedelta(hours=i)).strftime("%I:00 %p") for i in range(1, 26) ] return render_flexmeasures_template( "views/portfolio.html", assets=assets, average_prices=average_price_dict, asset_types=represented_asset_types, markets=markets, production_per_asset=production_per_asset, consumption_per_asset=consumption_per_asset, curtailment_per_asset=curtailment_per_asset, shifting_per_asset=shifting_per_asset, profit_loss_flexibility_per_asset=profit_loss_flexibility_per_asset, production_per_asset_type=production_per_asset_type, consumption_per_asset_type=consumption_per_asset_type, curtailment_per_asset_type=curtailment_per_asset_type, shifting_per_asset_type=shifting_per_asset_type, profit_loss_flexibility_per_asset_type=profit_loss_flexibility_per_asset_type, sum_production=sum(production_per_asset_type.values()), sum_consumption=sum(consumption_per_asset_type.values()), sum_curtailment=sum(curtailment_per_asset_type.values()), sum_shifting=sum(shifting_per_asset_type.values()), sum_profit_loss_flexibility=sum( profit_loss_flexibility_per_asset_type.values() ), portfolio_plots_script=portfolio_plots_script, portfolio_plots_divs=portfolio_plots_divs, next24hours=next24hours, alt_stacking=show_summed, )
def analytics_view(): """Analytics view. Here, four plots (consumption/generation, weather, prices and a profit/loss calculation) and a table of metrics data are prepared. This view allows to select a resource name, from which a `models.Resource` object can be made. The resource name is kept in the session. Based on the resource, plots and table are labelled appropriately. """ set_time_range_for_session() markets = get_markets() assets = get_assets(order_by_asset_attribute="display_name", order_direction="asc") asset_groups = get_asset_group_queries( custom_additional_groups=["renewables", "EVSE", "each Charge Point"]) asset_group_names: List[str] = [ group for group in asset_groups if asset_groups[group].count() > 0 ] selected_resource = set_session_resource(assets, asset_group_names) selected_market = set_session_market(selected_resource) sensor_types = get_sensor_types(selected_resource) selected_sensor_type = set_session_sensor_type(sensor_types) session_asset_types = selected_resource.unique_asset_types set_individual_traces_for_session() view_shows_individual_traces = ( session["showing_individual_traces_for"] in ("power", "schedules") and selected_resource.is_eligible_for_comparing_individual_traces()) query_window, resolution = ensure_timing_vars_are_set((None, None), None) # This is useful information - we might want to adapt the sign of the data and labels. showing_pure_consumption_data = all( [a.is_pure_consumer for a in selected_resource.assets]) showing_pure_production_data = all( [a.is_pure_producer for a in selected_resource.assets]) # Only show production positive if all assets are producers show_consumption_as_positive = False if showing_pure_production_data else True data, metrics, weather_type, selected_weather_sensor = get_data_and_metrics( query_window, resolution, show_consumption_as_positive, session["showing_individual_traces_for"] if view_shows_individual_traces else "none", selected_resource, selected_market, selected_sensor_type, selected_resource.assets, ) # Set shared x range shared_x_range = Range1d(start=query_window[0], end=query_window[1]) shared_x_range2 = Range1d( start=query_window[0], end=query_window[1] ) # only needed if we draw two legends (if individual traces are on) # TODO: get rid of this hack, which we use because we mock the current year's data from 2015 data in demo mode # Our demo server uses 2015 data as if it's the current year's data. Here we mask future beliefs. if current_app.config.get("FLEXMEASURES_MODE", "") == "demo": most_recent_quarter = time_utils.get_most_recent_quarter() # Show only past data, pretending we're in the current year if not data["power"].empty: data["power"] = data["power"].loc[ data["power"].index.get_level_values( "event_start") < most_recent_quarter] if not data["prices"].empty: data["prices"] = data["prices"].loc[ data["prices"].index < most_recent_quarter + timedelta(hours=24)] # keep tomorrow's prices if not data["weather"].empty: data["weather"] = data["weather"].loc[ data["weather"].index < most_recent_quarter] if not data["rev_cost"].empty: data["rev_cost"] = data["rev_cost"].loc[ data["rev_cost"].index.get_level_values( "event_start") < most_recent_quarter] # Show forecasts only up to a limited horizon horizon_days = 10 # keep a 10 day forecast max_forecast_datetime = most_recent_quarter + timedelta( hours=horizon_days * 24) if not data["power_forecast"].empty: data["power_forecast"] = data["power_forecast"].loc[ data["power_forecast"].index < max_forecast_datetime] if not data["prices_forecast"].empty: data["prices_forecast"] = data["prices_forecast"].loc[ data["prices_forecast"].index < max_forecast_datetime] if not data["weather_forecast"].empty: data["weather_forecast"] = data["weather_forecast"].loc[ data["weather_forecast"].index < max_forecast_datetime] if not data["rev_cost_forecast"].empty: data["rev_cost_forecast"] = data["rev_cost_forecast"].loc[ data["rev_cost_forecast"].index < max_forecast_datetime] # Making figures tools = ["box_zoom", "reset", "save"] power_fig = make_power_figure( selected_resource.display_name, data["power"], data["power_forecast"], data["power_schedule"], show_consumption_as_positive, shared_x_range, tools=tools, ) rev_cost_fig = make_revenues_costs_figure( selected_resource.display_name, data["rev_cost"], data["rev_cost_forecast"], show_consumption_as_positive, shared_x_range, selected_market, tools=tools, ) # the bottom plots need a separate x axis if they get their own legend (Bokeh complains otherwise) # this means in that in that corner case zooming will not work across all foour plots prices_fig = make_prices_figure( data["prices"], data["prices_forecast"], shared_x_range2 if view_shows_individual_traces else shared_x_range, selected_market, tools=tools, ) weather_fig = make_weather_figure( selected_resource, data["weather"], data["weather_forecast"], shared_x_range2 if view_shows_individual_traces else shared_x_range, selected_weather_sensor, tools=tools, ) # Separate a single legend and remove the others. # In case of individual traces, we need two legends. top_legend_fig = separate_legend(power_fig, orientation="horizontal") top_legend_script, top_legend_div = components(top_legend_fig) rev_cost_fig.renderers.remove(rev_cost_fig.legend[0]) if view_shows_individual_traces: bottom_legend_fig = separate_legend(weather_fig, orientation="horizontal") prices_fig.renderers.remove(prices_fig.legend[0]) bottom_legend_script, bottom_legend_div = components(bottom_legend_fig) else: prices_fig.renderers.remove(prices_fig.legend[0]) weather_fig.renderers.remove(weather_fig.legend[0]) bottom_legend_fig = bottom_legend_script = bottom_legend_div = None analytics_plots_script, analytics_plots_divs = components( (power_fig, rev_cost_fig, prices_fig, weather_fig)) return render_flexmeasures_template( "views/analytics.html", top_legend_height=top_legend_fig.plot_height, top_legend_script=top_legend_script, top_legend_div=top_legend_div, bottom_legend_height=0 if bottom_legend_fig is None else bottom_legend_fig.plot_height, bottom_legend_script=bottom_legend_script, bottom_legend_div=bottom_legend_div, analytics_plots_divs=[ encode_utf8(div) for div in analytics_plots_divs ], analytics_plots_script=analytics_plots_script, metrics=metrics, markets=markets, sensor_types=sensor_types, assets=assets, asset_group_names=asset_group_names, selected_market=selected_market, selected_resource=selected_resource, selected_sensor_type=selected_sensor_type, selected_sensor=selected_weather_sensor, asset_types=session_asset_types, showing_pure_consumption_data=showing_pure_consumption_data, showing_pure_production_data=showing_pure_production_data, show_consumption_as_positive=show_consumption_as_positive, showing_individual_traces_for=session["showing_individual_traces_for"], offer_showing_individual_traces=selected_resource. is_eligible_for_comparing_individual_traces(), forecast_horizons=time_utils.forecast_horizons_for( session["resolution"]), active_forecast_horizon=session["forecast_horizon"], )