A building HVAC simulation system that models realistic equipment behavior and exposes data via BACnet/IP. This enables testing of building automation systems, energy management software, and BACnet client applications without requiring physical equipment.
The easiest way to run the simulation is with the hvac-sim CLI, which handles container setup automatically:
# Run simple VAV simulation
./hvac-sim
# Run with a Brick schema file
./hvac-sim bldg36.ttl
# List available Brick schema files
./hvac-sim --listThe CLI auto-detects Podman or Docker, builds the image if needed, and sets up all the correct volume mappings and environment variables.
Security defaults:
- Host networking requires
--allow-host-network. - Campus BBMD host ports and foreign-device registration require
--expose-campus-bacnet. -e/--envonly acceptsBACNET_ADDRESS,BACNET_IP,BACNET_SUBNET,BACNET_PORT,BACNET_DEVICE_ID, andBACNET_NETWORK_NUMBER.- Custom script execution requires both
--custom-scriptand--allow-custom-script.
./hvac-sim [OPTIONS] [BRICK_FILE]
Options:
-l, --list List available Brick schema files
-d, --detach Run in background
--device-id ID BACnet device instance ID (default: 599)
-p, --port PORT Host port for BACnet UDP (default: 47808)
--network MODE Container network mode (e.g., 'host')
--allow-host-network Explicitly allow direct host-network exposure
--build Force rebuild the container image
--stop Stop the running simulation
--logs Show logs from the container
-e KEY=VALUE Set additional BACnet runtime variables
--custom-script PATH Run a mounted custom Python script
--allow-custom-script Explicitly allow arbitrary custom script execution
Campus simulation:
--campus [TTL_FILE] Run multi-container campus simulation
--campus-scenario S Select a built-in campus scenario
--campus-stop Stop the running campus simulation
--campus-logs Show logs from the campus simulation# Run with a specific Brick file
./hvac-sim bldg36.ttl
# Run with custom device ID and port
./hvac-sim --device-id 1000 --port 47809 bldg36.ttl
# Run in background and view logs
./hvac-sim -d bldg36.ttl
./hvac-sim --logs
# Use host networking for BACnet discovery
./hvac-sim --network host --allow-host-network bldg36.ttl
# Run a simple device that advertises BACnet network 100
./hvac-sim -e BACNET_NETWORK_NUMBER=100 --device-id 100
# Stop the running simulation
./hvac-sim --stop
# Run multi-container campus simulation
./hvac-sim --campus examples/multi_building_campus.ttl
# Run the built-in multi-network campus scenario
./hvac-sim --campus --campus-scenario multi-network
# Run the built-in collision scenario
./hvac-sim --campus --campus-scenario multi-network-collisions
# Run a custom script with explicit acknowledgement
./hvac-sim --custom-script examples/example_simulation.py --allow-custom-scriptThe simulator supports multi-building campus simulations with BBMDs (BACnet Broadcast Management Devices) connecting buildings across a WAN:
Building 1 (Networks 1000-1999) Building 2 (Networks 2000-2999)
[BBMD-1] <---- WAN ----> [BBMD-2]
| |
[Router] [Router]
| |
[VLANs] [VLANs]
Each building gets a dedicated range of 1000 network numbers:
| Building | Network Range | Central Plant | AHU Networks |
|---|---|---|---|
| Building 1 | 1000-1999 | 1001 | 1100, 1200, ... |
| Building 2 | 2000-2999 | 2001 | 2100, 2200, ... |
| Building N | N×1000-(N+1)×1000-1 | N×1000+1 | N×1000+100, ... |
| File | Buildings | AHUs | VAVs | Description |
|---|---|---|---|---|
multi_building_campus.ttl |
2 | 2 | 7 | Small demo with 2 buildings |
multi_building_campus_collisions.ttl |
2 | 2 | 2 | Checked-in collision scenario with duplicate bacnet:deviceId 100 across buildings |
large_campus.ttl |
6 | 12 | 108 | Full campus simulation |
Campus mode runs each building in its own container with real IP subnets and external ace-acl-bbmd instances for cross-building BACnet routing:
# Generate compose file, build images, and start the campus
./hvac-sim --campus examples/multi_building_campus.ttl
# Use the default built-in campus example
./hvac-sim --campus
# Use the built-in multi-network scenario (explicit BACnet network numbers)
./hvac-sim --campus --campus-scenario multi-network
# Use the built-in multi-network collision scenario
./hvac-sim --campus --campus-scenario multi-network-collisions
# View logs / stop
./hvac-sim --campus-logs
./hvac-sim --campus-stopCampus mode requires the ace-acl-bbmd project as a sibling directory. The build patches its Dockerfile to use the uv base image and adds --network=host to work around Podman's default build isolation.
Built-in campus scenarios:
default: usesexamples/multi_building_campus.ttlmulti-network: uses the standard two-building example and assigns explicit BACnet network numbers100,200, ...multi-network-collisions: usesexamples/multi_building_campus_collisions.ttland assigns explicit BACnet network numbers100,200, ...
In the scenario-driven campus modes, BACNET_NETWORK_NUMBER is applied to each building container's external BACnet/IP router port so downstream tests can target stable non-zero network numbers.
For downstream tests that need a multi-homed edge process instead of BBMD-routed reachability, use docker-compose.multihomed.yml. This topology keeps the BACnet networks isolated:
sim1only joinsbuilding1(10.11.0.0/24) and advertisesBACNET_NETWORK_NUMBER=100sim2only joinsbuilding2(10.12.0.0/24) and advertisesBACNET_NETWORK_NUMBER=200- both sims intentionally use
BACNET_DEVICE_ID=100to exercise duplicate device IDs on different networks - no BBMDs, no
campus-router, and no published BACnet host ports
Bring it up with:
podman compose -f docker-compose.multihomed.yml up --buildYour downstream edge container should attach to both building1 and building2 in its own compose file. No other service in this template bridges the two networks.
Brick campus examples can now carry explicit BACnet device instance numbers with the ASHRAE BACnet namespace. When present, those values override the simulator's usual auto-assigned per-building sequence for the annotated equipment.
@prefix bacnet: <http://data.ashrae.org/bacnet/2020#> .
campus:Building1_AHU01 a brick:Air_Handler_Unit ;
bacnet:deviceId 100 .This is how examples/multi_building_campus_collisions.ttl creates the duplicate-device_id scenario across separate buildings.
The large_campus.ttl file simulates a university/corporate campus:
| Building | Description | Floors | AHUs | VAVs | Plant |
|---|---|---|---|---|---|
| Building A | Administration | 3 | 2 | 16 | - |
| Building B | Engineering | 4 | 3 | 30 | - |
| Building C | Student Center | 2 | 2 | 20 | - |
| Building D | Library | 3 | 2 | 18 | - |
| Building E | Research Lab | 3 | 3 | 24 | - |
| Central Plant | Shared Infrastructure | - | - | - | 2 Chillers, 2 Boilers, 2 Cooling Towers |
Create a Brick schema with multiple brick:Building instances and use brick:isPartOf to associate equipment:
@prefix brick: <https://brickschema.org/schema/Brick#> .
@prefix ex: <http://example.com/building#> .
ex:Building1 a brick:Building .
ex:Building2 a brick:Building .
ex:AHU1 a brick:AHU ;
brick:isPartOf ex:Building1 .
ex:AHU2 a brick:AHU ;
brick:isPartOf ex:Building2 .See examples/multi_building_campus.ttl for a basic example or examples/large_campus.ttl for a complete campus.
For more control, you can run the container directly:
# Build the container image
podman build -t hvac-simulator .
# Run simple simulation
podman run --rm -it -p 47808:47808/udp hvac-simulator
# Run with Brick schema
podman run --rm -it -p 47808:47808/udp \
-v ./data/brick_schemas:/app/brick_schemas:ro \
-e SIMULATION_MODE=brick \
-e BRICK_TTL_FILE=/app/brick_schemas/bldg36.ttl \
hvac-simulator| Variable | Default | Description |
|---|---|---|
BACNET_IP |
auto-detected | BACnet device IP address |
BACNET_SUBNET |
16 | Subnet mask bits (e.g., 16 = /16) |
BACNET_PORT |
47808 | BACnet UDP port |
BACNET_DEVICE_ID |
599 | BACnet device instance ID |
BACNET_NETWORK_NUMBER |
- | Optional BACnet network number for the device's network-port object |
SIMULATION_MODE |
simple | Simulation mode: simple, brick, or custom |
BRICK_TTL_FILE |
- | Path to Brick TTL file (for brick mode) |
CUSTOM_SCRIPT |
- | Path to custom Python script (for custom mode; use --custom-script) |
ALLOW_CUSTOM_SCRIPT |
false | Required to run CUSTOM_SCRIPT in custom mode |
BUILDING_NAME |
- | Building to simulate from a multi-building TTL (campus mode) |
For development, you can run directly with uv:
# Install dependencies
uv sync
# Run tests
uv run pytest
# Run a simple example
uv run python examples/simple_vav.py
# Run the full building example
uv run python examples/complete_building.py
# Run the main BACnet simulation
uv run python src/main.py- VAV Box: Variable Air Volume terminal unit with optional reheat capability
- Modulates airflow in cooling mode
- Controls reheat valve in heating mode
- Maintains minimum airflow in deadband mode
- Models thermal behavior with occupancy and solar heat gain
- Air Handling Unit (AHU): Central unit supplying conditioned air to VAV boxes
- Controls supply air temperature
- Coordinates multiple VAV boxes
- Optional supply air temperature reset
- Supports chilled water and DX cooling
-
Chiller: Produces chilled water for cooling coils
- Water-cooled and air-cooled configurations
- COP modeling based on load and conditions
- Integrates with cooling tower
-
Cooling Tower: Evaporative heat rejection for water-cooled chillers
- Variable-speed fan control
- Approach temperature modeling
- Multi-cell support
-
Boiler: Produces hot water for heating coils
- Gas-fired and electric configurations
- Condensing boiler efficiency modeling
- Realistic cycling behavior
- Building: Top-level container for HVAC equipment
- Manages multiple AHUs and zones
- Tracks weather conditions
- Calculates solar position
- Energy reporting
src/
├── core/ # Core infrastructure
│ ├── constants.py # Physics constants
│ ├── config.py # Configuration system
│ └── logging.py # Structured logging
├── physics/ # Physics calculations
│ ├── thermal.py # Heat transfer
│ └── fluid.py # Fluid dynamics
├── controls/ # Control systems
│ └── pid.py # PID controller
├── equipment/ # Equipment base classes
│ └── base.py # Abstract hierarchy
├── bacnet/ # BACnet integration
│ ├── device.py # Device management
│ ├── mixin.py # Application mixin
│ ├── points.py # Point creation & unit mapping
│ ├── router_metrics.py # Router diagnostic instrumentation
│ ├── bbmd.py # BBMD management
│ └── errors.py # Error injection
├── brick/ # Brick schema parsing
│ ├── parser.py # TTL file parser
│ └── campus.py # Multi-building structures
├── vav_box.py # VAV terminal unit
├── ahu.py # Air handling unit
├── chiller.py # Chiller
├── boiler.py # Boiler
├── cooling_tower.py # Cooling tower
├── building.py # Building container
└── bacnet_network.py # Network management
examples/
├── simple_vav.py # Basic VAV example
├── complete_building.py # Full building simulation
├── multi_building_campus.ttl # 2-building demo (7 VAVs)
├── multi_building_campus_collisions.ttl # Duplicate device-id campus example
├── large_campus.ttl # Full campus (6 buildings, 108 VAVs)
├── test_campus.sh # End-to-end campus compose test helper
└── ... # Additional examples
docker-compose.multihomed.yml # Direct multi-network harness without BBMDs
tests/
├── test_*.py # Unit tests
├── test_multi_building.py # Multi-building tests
├── test_bbmd_errors.py # BBMD and error tests
├── integration/ # Integration tests
└── performance/ # Performance benchmarks
Equipment can be configured via YAML files or dataclasses:
# config.yaml
simulation:
time_step_minutes: 1
speed_multiplier: 60
bacnet:
port: 47808
device_id_base: 1000
defaults:
vav:
min_airflow: 100
max_airflow: 1000
zone_temp_setpoint: 72from src.core.config import VAVConfig, ThermalZoneConfig
from src.vav_box import VAVBox
config = VAVConfig(
name="VAV-101",
min_airflow=100,
max_airflow=800,
thermal_zone=ThermalZoneConfig(
zone_area=400,
window_orientation="east"
)
)
vav = VAVBox.from_config(config)The simulation exposes all equipment as BACnet devices:
- Each equipment instance gets a unique BACnet device ID
- Process variables are mapped to BACnet objects:
- Temperatures → Analog Value (AV) with degrees-fahrenheit
- Setpoints → Analog Value (AV) with degrees-fahrenheit
- Airflows → Analog Value (AV) with cubic-feet-per-minute
- Valve/damper positions → Analog Value (AV) with percent
- Status → Binary Value (BV)
- Modes → Multi-State Value (MSV) with stateText
- All numeric points carry proper BACnet engineering units (no-units for dimensionless values like COP and ratios)
All simulated BACnet objects support COV subscriptions:
- Analog values use
COVIncrementCriteria— notifications fire only when the change exceedscovIncrement(default 0.1), filtering out noise from minor simulation fluctuations - Binary values use
GenericCriteria— notifications fire on any state change - Multi-state values use
GenericCriteria— notifications fire on any state transition
Clients subscribe via the standard BACnet SubscribeCOV service. The initial notification on subscribe delivers the current value, and subsequent notifications arrive as the simulation updates equipment state (once per simulation tick).
Each building's IP-to-VLAN router device exposes operational metrics as BACnet analog values:
| Point | Object ID | Description |
|---|---|---|
packets_routed |
analog-value,100 | Total NPDUs routed between networks |
packets_from_ip |
analog-value,101 | NPDUs received from BACnet/IP |
packets_to_ip |
analog-value,102 | NPDUs sent to BACnet/IP |
who_is_requests |
analog-value,103 | Who-Is requests processed |
i_am_responses |
analog-value,104 | I-Am responses sent |
read_property_requests |
analog-value,105 | ReadProperty requests handled |
write_property_requests |
analog-value,106 | WriteProperty requests handled |
cov_notifications |
analog-value,107 | COV notifications forwarded |
rejected_packets |
analog-value,108 | Dropped/rejected packets |
uptime_seconds |
analog-value,109 | Router uptime in seconds |
connected_networks |
analog-value,110 | Number of connected VLANs |
These counters are COV-subscribable (covIncrement=1.0) and update each simulation tick.
Use any BACnet client to discover the simulated equipment:
- BAC0 Python library
- YABE (Yet Another BACnet Explorer)
- Proprietary BMS systems with BACnet/IP
# Run all tests
uv run pytest
# Run with verbose output
uv run pytest -v
# Run specific test category
uv run pytest tests/integration/
uv run pytest tests/performance/
# Run with coverage
uv run pytest --cov=src- Architecture Guide - System design and physics models
- Examples README - Example script documentation
- Realistic PID control for damper and valve modulation
- Dynamic thermal modeling with solar gains and occupancy
- Multiple cooling system types (chilled water, DX)
- Central plant equipment with performance curves
- Standardized process variable interface
- Automatic BACnet point generation with proper engineering units
- Optional explicit BACnet network numbers on simple-mode devices and campus router IP ports
- COV (Change of Value) notifications on all simulated points
- Router diagnostic counters (packets routed, request counts, uptime)
- Configuration via YAML or Python dataclasses
- Comprehensive test suite with performance benchmarks
- Multi-container campus mode with real IP subnets and ace-acl-bbmd
- Built-in campus scenarios for default, multi-network, and collision-focused test harnesses
- Checked-in Brick collision example with explicit
bacnet:deviceIdannotations - Hardened container defaults for host networking, campus exposure, and custom script execution
- Brick schema support for equipment topology
- Python 3.12+
- Dependencies managed via
uv(seepyproject.toml) - Podman or Docker for containerized deployment
See LICENSE file for details.