Beispiel #1
0
 def get(self, id: int):
     """GET from /sensors/<id>"""
     return render_flexmeasures_template(
         "views/sensors.html",
         sensor_id=id,
         msg="",
     )
Beispiel #2
0
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)
Beispiel #3
0
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()),
    )
Beispiel #4
0
 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)
Beispiel #5
0
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,
    )
Beispiel #6
0
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,
    )
Beispiel #7
0
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,
    )
Beispiel #8
0
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,
    )
Beispiel #9
0
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)
Beispiel #10
0
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,
    )
Beispiel #11
0
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()),
    )
Beispiel #12
0
 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)
Beispiel #13
0
    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",
                                                     ""),
        )
Beispiel #14
0
 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)
Beispiel #15
0
 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)
Beispiel #16
0
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,
    )
Beispiel #17
0
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,
    )
Beispiel #18
0
 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,
     )
Beispiel #19
0
 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)
Beispiel #20
0
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,
    )
Beispiel #21
0
    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",
                                                     ""),
        )
Beispiel #22
0
    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",
                                                     ""),
        )
Beispiel #23
0
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"),
    )
Beispiel #24
0
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,
    )
Beispiel #25
0
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"],
    )