A simulated HVAC control system using BACnet/IP, designed as a companion to MITRE Caldera for OT for red/blue-team exercises involving cyber–physical systems.
Created by University of Hawaii at Manoa Students for Capstone Project: Elijah Saloma and Jake Dickinson
In collaboration with MITRE Caldera for OT tools ([email protected]).
HVACSim provides a realistic, software-only BACnet simulation of a server-room HVAC controller. The system exposes writable BACnet objects (setpoint, fans, emergency stop) and simulates:
- Temperature dynamics
- Chiller load and PI feedback loop
- Sensor noise and an actuator lag
- An HMI with sliders, trend charts, and an emergency-stop; the HMI is intended to easily observe overrides by the Caldera for OT client.
This allows cybersecurity practitioners to emulate attacks against building HVAC systems without requiring physical industrial hardware. When paired with the Caldera BACnet plugin, HVACSim becomes an OT testbed for reconnaissance, manipulation, and response.
-
Python 3.10+
-
matplotlib(for the HMI) -
bacpypes(BACnet/IP stack) -
Should run on Linux, macOS, or Windows
-
Caldera with its BACnet plugin
Linux users: Install system packages for matplotlib GUI support:
# Ubuntu/Debian
sudo apt-get install python3-tk
# Fedora/RHEL
sudo dnf install python3-tkinter- Clone the repository:
git clone https://github.com/mitre/hvac-sim.git- Install Python dependencies:
pip install -r requirements.txt- You can have a variety of different INI files specifying different port numbers or other BACnet communications parameters. Visit the BACpypes documentation to learn more about this INI file.
Example provided in repo root (BACpypes.ini):
[BACpypes]
objectName: HVACSim
objectIdentifier: 101
maxApduLengthAccepted: 1024
segmentationSupported: segmentedBoth
vendorIdentifier: 15
address: 127.0.0.1
The simulator exposes the following BACnet objects:
| Type | Object Name | Description |
|---|---|---|
| AO0 | temperature_setpoint_c |
Desired room temperature in °C (writable) |
| AO1 | intake_fan_speed_percent |
Intake fan command (0–100%) |
| AO2 | exhaust_fan_speed_percent |
Exhaust fan command (0–100%) |
| BO0 | emergency_stop |
Safety kill switch for chiller/fans |
| AI0 | current_temperature_c |
Measured room temperature (°C) |
| AI1 | chiller_speed_percent |
PI-controlled chiller load (%) |
python3 hvac_sim.py --ini ./BACpypes.iniLaunching the script does three things:
- Starts the BACnet/IP device
- Spawns the HVAC control loop thread
- Opens the interactive HMI dashboard
If the program fails to start, verify Python, dependencies, and the .ini file. Any exceptions should be printed in the console.
To "attack" the device, one can access property values (i.e., read) and modify them as desired (i.e., write). Below is an example of the commands to set in the adversary profile in Caldera (or through CLI):
The ReadProperty service is used by a BACnet client to request the value of one property from one BACnet object.
./bacrp <device-instance> <object-type> <object-instance> <property> <index>
./bacrp 101 analog-input 0 presentValue -1
The WriteProperty service is used by a BACnet client to write a value to a specific property of a BACnet object.
./bacwp <device-instance> <object-type> <object-instance> <property> <priority> <index> <tag> <value>
./bacwp 101 analog-output 0 presentValue 8 -1 real 18.0
./bacwp 101 analog-output 1 presentValue 8 -1 real 75.0
./bacwp 101 binary-output 0 presentValue 8 -1 boolean true
Disclaimer: HVACSim models core thermal dynamics, but its primary purpose is to support Caldera/BACnet testing rather than to serve as a fully accurate physical HVAC model.
The simulator continuously computes room temperature using:
- Ambient heat entering from the outside
- Internal server load (internal heat source)
- Airflow-based cooling
- Chiller-based cooling
- Temperature sensor noise
- Actuator lag
The temperature differential follows:
dT_dt = ((ambient + internal_load) - room_temp) / room_time_constant \
- (cooling_from_fans + cooling_from_chiller)
The intake and exhaust sliders (or Caldera writes) produce an airflow percentage:
airflow = (intake + exhaust) / 2
Cooling power is proportional to airflow:
cooling_airflow = (airflow / 100) * AIRFLOW_MAX_COOL
The chiller is controlled by a Proportional-Integral (PI) loop:
error = current_temp - setpoint
integral = integral + error * tick
chiller_target = KP * error + KI * integral
The chiller actuator then moves toward the target following:
chiller_speed = chiller_speed + (chiller_target - chiller_speed) * CHILLER_LAG
Random noise is added to emulate real-world imperfectness:
chiller_speed = chiller_speed + Uniform(-NOISE_CHILLER, NOISE_CHILLER)
When emergency stop is triggered:
- Airflow is forced to 0%
- Chiller target is forced to 0%
When launched, HVACSim displays an HMI containing:
Main Temperature Graph
- Real-time plot of current temperature (°F)
- Setpoint shown as a dashed line
Mini Trend Charts
- Chiller load (%)
- Intake airflow (%)
- Exhaust airflow (%)
Interactive Controls
- Setpoint slider (°F)
- Intake fan slider (%)
- Exhaust fan slider (%)
- Emergency-stop toggle button
Closing the window shuts down the control loop and BACnet stack.
If using Caldera with its BACnet plugin:
-
Start the HVACSim process
-
Start Caldera
-
Create an operation
- Create an agent
- Create an adversary profile including any BACnet features desired
- Create an operation that selects the constructed adversary profile
- Start operation
-
Use abilities such as:
- ReadProperty → Check temperature or chiller load
- WriteProperty → Change setpoint
- WriteProperty → Force fans to 100%
- WriteProperty → Trigger Emergency Stop
This allows for simulation of:
- Safety bypass attempts
- Setpoint manipulation attacks
- Disruptive fan/chiller control
- Reconnaissance of BACnet points
-
Ensure the
.inifile has a valid BACnet device ID and IP address -
If BACnet clients cannot discover HVACSim, verify:
- No firewall blocks UDP/47808
- Correct network interface is used
-
Use
--debug bacpypes.udpfor verbose network logs
This project is licensed under the Apache-2.0 License. See the LICENSE file for details.
© 2026 THE MITRE CORPORATION. ALL RIGHTS RESERVED. APPROVED FOR PUBLIC RELEASE. DISTRIBUTION UNLIMITED PR_26-0182