wording, add perspective panel to dumb-inverter demo
This commit is contained in:
parent
785172104f
commit
fe0979083f
2 changed files with 51 additions and 4 deletions
|
|
@ -4,11 +4,13 @@ import panel
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from watt42_viewlib import attach_w42_state
|
from watt42_viewlib import attach_w42_state
|
||||||
|
|
||||||
panel.extension('echarts', 'ace', 'jsoneditor')
|
panel.extension('echarts', 'ace', 'jsoneditor', 'perspective')
|
||||||
|
|
||||||
SYSTEM_ID = os.environ.get("WATT42_SYSTEM_ID", "invalid-system-id")
|
SYSTEM_ID = os.environ.get("WATT42_SYSTEM_ID", "invalid-system-id")
|
||||||
API_TOKEN = os.environ.get("WATT42_API_TOKEN", "invalid-api-token")
|
API_TOKEN = os.environ.get("WATT42_API_TOKEN", "invalid-api-token")
|
||||||
|
|
@ -28,6 +30,7 @@ class LoadForecast(BaseModel):
|
||||||
|
|
||||||
class InverterForecast(BaseModel):
|
class InverterForecast(BaseModel):
|
||||||
levels: list[float] = []
|
levels: list[float] = []
|
||||||
|
grid_import_power: list[float] = []
|
||||||
|
|
||||||
class SystemState(BaseModel):
|
class SystemState(BaseModel):
|
||||||
pv_forecast: PvForecast = PvForecast()
|
pv_forecast: PvForecast = PvForecast()
|
||||||
|
|
@ -52,7 +55,7 @@ def chart1(state: SystemState) -> dict:
|
||||||
option = {
|
option = {
|
||||||
"title": {"text": f"PV and Load Forecast at {now.strftime('%Y-%m-%d %H:%M')}"},
|
"title": {"text": f"PV and Load Forecast at {now.strftime('%Y-%m-%d %H:%M')}"},
|
||||||
"tooltip": {"trigger": "axis"},
|
"tooltip": {"trigger": "axis"},
|
||||||
"legend": {"data": ["PV Forecast", "Load Forecast", "Inverter Level"]},
|
"legend": {"data": ["PV Forecast", "Load Forecast", "Inverter Level", "Grid import"]},
|
||||||
"xAxis": {"type": "category", "data": hours, "name": "Hours"},
|
"xAxis": {"type": "category", "data": hours, "name": "Hours"},
|
||||||
"yAxis": {"type": "value", "name": "Power (kW)"},
|
"yAxis": {"type": "value", "name": "Power (kW)"},
|
||||||
"series": [
|
"series": [
|
||||||
|
|
@ -70,6 +73,11 @@ def chart1(state: SystemState) -> dict:
|
||||||
"name": "Inverter Level",
|
"name": "Inverter Level",
|
||||||
"type": "line",
|
"type": "line",
|
||||||
"data": state.inverter.levels,
|
"data": state.inverter.levels,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Grid import",
|
||||||
|
"type": "line",
|
||||||
|
"data": state.inverter.grid_import_power,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
@ -77,6 +85,32 @@ def chart1(state: SystemState) -> dict:
|
||||||
|
|
||||||
chart1_rx = panel.rx(chart1)(panel.rx(lambda s: SystemState.model_validate(s) if s else SystemState())(w42_state))
|
chart1_rx = panel.rx(chart1)(panel.rx(lambda s: SystemState.model_validate(s) if s else SystemState())(w42_state))
|
||||||
|
|
||||||
|
def series_as_df(state: SystemState) -> pd.DataFrame:
|
||||||
|
""" Return a pandas DataFrame representation of the series data."""
|
||||||
|
data = {
|
||||||
|
"Time": [state.load_forecast.at + pd.Timedelta(minutes=15 * i) for i in range(len(state.load_forecast.slots))],
|
||||||
|
"PV Forecast": state.pv_forecast.slots,
|
||||||
|
"Load Forecast": state.load_forecast.slots,
|
||||||
|
"Inverter Level": state.inverter.levels,
|
||||||
|
}
|
||||||
|
df = pd.DataFrame(data)
|
||||||
|
return df
|
||||||
|
|
||||||
|
series_df_rx = panel.rx(series_as_df)(panel.rx(lambda s: SystemState.model_validate(s) if s else SystemState())(w42_state))
|
||||||
|
|
||||||
|
def summary_as_markdown(state: SystemState) -> str:
|
||||||
|
""" Return a markdown summary of the system state."""
|
||||||
|
md = f"""
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
- Total PV Forecast: {sum(state.pv_forecast.slots) / 4000.0:.2f} kWh
|
||||||
|
- Total Load Forecast: {sum(state.load_forecast.slots) / 4000.0:.2f} kWh
|
||||||
|
- Total grid import: {sum(state.inverter.grid_import_power) / 4000.0:.2f} kWh
|
||||||
|
"""
|
||||||
|
|
||||||
|
return md
|
||||||
|
|
||||||
|
state_summary_rx = panel.rx(summary_as_markdown)(panel.rx(lambda s: SystemState.model_validate(s) if s else SystemState())(w42_state))
|
||||||
|
|
||||||
sidebar_content = """
|
sidebar_content = """
|
||||||
Demonstrates how to visualize the Watt42 "dumb inverter simulation" system state.
|
Demonstrates how to visualize the Watt42 "dumb inverter simulation" system state.
|
||||||
|
|
@ -86,11 +120,24 @@ here](https://source.c3.uber5.com/watt42-public/watt42_viewlib/src/branch/main/R
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
grid_config = json.loads('{"version":"3.8.0","plugin":"Datagrid","plugin_config":{"columns":{},"edit_mode":"READ_ONLY","scroll_lock":false},"columns_config":{},"settings":true,"theme":"Pro Light","title":null,"group_by":["level rounded"],"split_by":[],"sort":[["level rounded","desc"]],"filter":[],"expressions":{"level rounded":"round(\\"Inverter Level\\" / 100) * 100\\t","one":"1"},"columns":["index","one","Time","PV Forecast","Load Forecast","Inverter Level","level rounded"],"aggregates":{}}')
|
||||||
|
|
||||||
_ = panel.template.FastListTemplate(
|
_ = panel.template.FastListTemplate(
|
||||||
title="Watt42: Dumb Inverter Visualization Example",
|
title="Watt42: Dumb Inverter Visualization Example",
|
||||||
sidebar=[panel.pane.Markdown(sidebar_content, sizing_mode='stretch_width')],
|
sidebar=[panel.pane.Markdown(sidebar_content, sizing_mode='stretch_width'), panel.pane.Markdown(state_summary_rx, sizing_mode='stretch_width')],
|
||||||
main=[
|
main=[
|
||||||
panel.pane.ECharts(chart1_rx, sizing_mode='stretch_width', height=400, theme='light'),
|
panel.pane.ECharts(chart1_rx, sizing_mode='stretch_width', height=400, theme='light'),
|
||||||
|
panel.pane.Perspective(series_df_rx, sizing_mode='stretch_width',
|
||||||
|
height=400, columns=grid_config['columns'],
|
||||||
|
title="Group by rounded battery level (experimental)",
|
||||||
|
expressions=grid_config['expressions'],
|
||||||
|
aggregates=grid_config['aggregates'],
|
||||||
|
group_by=grid_config['group_by'],
|
||||||
|
sort=grid_config['sort'],
|
||||||
|
filters=grid_config['filter'],
|
||||||
|
),
|
||||||
|
# warn that above pane may not work on all browsers
|
||||||
|
panel.pane.Markdown("**Note:** The above data grid uses the Perspective library which may not work on all browsers.", sizing_mode='stretch_width'),
|
||||||
state_pane,
|
state_pane,
|
||||||
w42_state,
|
w42_state,
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,7 @@ def summary(state: SystemState) -> str:
|
||||||
return f"## Summary\n\nGrid {'cost' if total_cost > 0 else 'income'} total: {abs(state.stats.grid_cost_total):.2f} ZAR"
|
return f"## Summary\n\nGrid {'cost' if total_cost > 0 else 'income'} total: {abs(state.stats.grid_cost_total):.2f} ZAR"
|
||||||
|
|
||||||
_ = panel.template.FastListTemplate(
|
_ = panel.template.FastListTemplate(
|
||||||
title="Sample W42 App",
|
title="Watt42: PV, batteries, dynamic tariffs",
|
||||||
sidebar=[panel.pane.Markdown("This is a sample sidebar.")],
|
sidebar=[panel.pane.Markdown("This is a sample sidebar.")],
|
||||||
main=[
|
main=[
|
||||||
# panel.pane.Markdown(state_as_text, sizing_mode='stretch_width'),
|
# panel.pane.Markdown(state_as_text, sizing_mode='stretch_width'),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue