wording, add perspective panel to dumb-inverter demo

This commit is contained in:
Chris Oloff 2026-02-02 16:34:24 +02:00
parent 785172104f
commit fe0979083f
2 changed files with 51 additions and 4 deletions

View file

@ -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,
], ],

View file

@ -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'),