Python EMS (EnergyPlus Python Plugins)
SkillMonitoring & opsAdd custom EMS control or reporting logic (EnergyPlus Python Plugins) — setpoint resets, schedule overrides, supervisory control, custom metrics. Use when no packaged HVAC/schedule tool covers the control logic the user wants.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Python EMS (EnergyPlus Python Plugins) skill
What this skill tells your AI
The instructions your AI receives, as published by natlabrockies/openstudio-mcp in .claude/skills/python-ems/SKILL.md and read by ahel’s review.
Custom control/reporting logic that runs inside EnergyPlus each timestep. A plugin is a Python class attached to the model; it reads sensors (output variables), sets actuators (schedule values, setpoints, flows), and publishes custom output variables you can query like any other result.
Always prefer the named templates in create_python_plugin — they generate
correct, validated plugin code. Write template="custom" code only when no
template fits.
Workflow
- Model loaded (any create/load tool). The model must have a file path — save first if needed.
- Discover what you can control:
Returns (component_type, control_type, actuator_key) triples from a hidden sizing-only run (a few seconds). Names come back UPPERCASE — matching inside plugins is case-insensitive. Requires design days in the model.list_ems_actuators(component_type="Schedule") - Create the plugin from a template:
create_python_plugin(name="Night Setback", template="schedule_override", schedule_name="Htg Setpoint Sched", default_value=15.6, rules=[{"days": "weekdays", "start_hour": 5, "end_hour": 19, "value": 21.0}]) save_osm_model()— the script lives in the model's companionfiles/dir and travels with it.run_simulation(...), then verify the plugin loaded:extract_simulation_errors(run_id=<run_id>)should show no severe messages; the .err file logsPythonPlugin: Class X imported.- Read the plugin's outputs:
query_timeseries(run_id=<run_id>, variable_name="PythonPlugin:OutputVariable", key_value="Night Setback Applied Value")
Worked examples
Average building temperature (custom metric, no actuation):
create_python_plugin(name="Avg Building Temp", template="zone_metric_aggregate",
variable_name="Zone Mean Air Temperature",
aggregation="volume_weighted_average",
output_variable_name="Averaged Building Temperature", units="C")
Defaults to all zones; pass zone_names to restrict.
Schedule/setpoint override by time-of-day rules:
create_python_plugin(name="Night Setback", template="schedule_override",
schedule_name="Htg Setpoint Sched", default_value=15.6,
rules=[{"days": "weekdays", "start_hour": 5, "end_hour": 19, "value": 21.0},
{"days": "saturday", "start_hour": 6, "end_hour": 12, "value": 18.0}])
First matching rule wins, else default_value. Works on ScheduleConstant, ScheduleRuleset (actuated as Schedule:Year), and ScheduleCompact schedules.
Outdoor-air setpoint reset (overrides setpoint managers):
create_python_plugin(name="SAT Reset", template="node_setpoint_reset",
air_loop_name="Packaged Rooftop Air Conditioner",
oat_low=-20, oat_high=10,
setpoint_at_oat_low=15.0, setpoint_at_oat_high=12.8)
Pass node_name directly, or air_loop_name to use its supply outlet node.
Custom plugin (escape hatch): provide full source. Contract:
from pyenergyplus.plugin import EnergyPlusPlugin
class MyControl(EnergyPlusPlugin):
def on_begin_timestep_before_predictor(self, state) -> int:
if not self.api.exchange.api_data_fully_ready(state):
return 0
# get_*_handle returns -1 when not found — check it, issue_severe, return 1
# return 0 = success; nonzero aborts the simulation
return 0
Pass code=..., class_name="MyControl", and global_variables=[...] for any
globals used via get_global_handle/set_global_value. Use list_ems_actuators
first for valid actuator triples.
Rules of the plugin runtime
- Look up handles lazily, only after
self.api.exchange.api_data_fully_ready(state)is True; cache them. - Handle lookups return -1 (never raise). On -1:
self.api.runtime.issue_severe(state, msg); return 1. - Callback return 0 = success; nonzero is FATAL to the whole simulation.
- Globals must be declared (create_python_plugin does this) before
get_global_handlefinds them. - A sensed variable needs an
Output:Variablerequest to have a handle — the templates add it. self.api.exchange.hour(state)is 0-23;self.api.exchange.day_of_week(state)is 1=Sunday..7=Saturday.- Plugins run inside EnergyPlus (embedded Python 3.12, stdlib by default).
For third-party packages (numpy etc.):
install_plugin_packages(["numpy"])first — wheels-only, quota-capped, wired in automatically.pkg==versionpins allowed; URLs/paths are not. - To revise a plugin:
edit_python_plugin(name=..., code=...)(validated; the class name must stay unchanged).delete_objecton the instance removes it. trend_timesteps=Non create logs the plugin global as a PythonPlugin:TrendVariable forget_trend_average/min/maxhistory access.
Common actuator triples
| Component type | Control type | Key |
|---|---|---|
| Schedule:Constant / Schedule:Year / Schedule:Compact | Schedule Value | schedule name |
| System Node Setpoint | Temperature Setpoint | node name |
| Zone Temperature Control | Heating (or Cooling) Setpoint | zone name |
| Weather Data | Outdoor Dry Bulb | Environment |
Confirm with list_ems_actuators — availability is model-dependent.
Signals
- GitHub stars
- 33
- Forks
- 8
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
python-ems- Source
- github.com/natlabrockies/openstudio-mcp