A multi-protocol discovery and audit tool for HVAC and building automation networks. Zero third-party dependencies — everything runs on the Python 3.10+ standard library.
Looking for a Siemens APOGEE P2 scanner or P2 Wireshark Decoder? See this project P2Scanner
Scans a network and tells you what building-automation gear lives on it. For each device found it tries to identify the exact model, pull readable points or registers, and surface the factory-default credentials that ship with that product so you can confirm they've been changed.
Works against:
- BACnet/IP — raw UDP
Who-Is/I-Am,ReadProperty, andReadPropertyMultiple. No BAC0 dependency, works on newer python versions. - BACnet MSTP — device enumeration behind BACnet routers via
Who-Is-Router-To-Networkand targetedWho-Isto remote DNETs. - Modbus TCP — port sweep, device identification (FC 43 / MEI 14), holding/input register reads, and coil reads.
- HVAC services — Niagara Fox, OPC UA, Siemens S7, EtherNet/IP CIP, KNXnet/IP, LonWorks/IP, MQTT, WebCTRL, Metasys, plus HTTP/HTTPS banner grabs.
- SNMP v1/v2c — raw UDP
sysDescrprobe, no pysnmp dependency.
Model identification combines signals from multiple protocols:
- Trane Tracer SC / SC+ / UC600 / UC400
- Siemens Desigo PXC automation stations, Desigo CC, TX-I/O modules
- Johnson Controls FEC / Metasys NAE
- Honeywell / Tridium Niagara
- Schneider EcoStruxure / SmartX
- Contemporary Controls BASRT-B routers
- Carrier i-Vu, Automated Logic WebCTRL, Distech, Delta, KMC, Reliable, Carel, Belimo, Daikin, Mitsubishi, LG, and more
The scanner has been verified against real hardware in live installations. These devices have known-good classification profiles:
- Trane Tracer SC+ — supervisory controller (5,000+ objects)
- Trane Symbio 400-500 — field controller (MSTP)
- Trane Tracer Ensemble (TES Workstation) — supervisor (handles RPM rejection via fallback to single ReadProperty)
- Siemens PXC Compact (EPXC V3.5.x firmware) — field panel
- Siemens PXC Modular (PXME V3.5.x firmware) — field panel (verified with 1,960-object program)
- Siemens DXR2.E10PL-1, .E12P-1, .E18-1 — room controllers
- Siemens Desigo CC / Insight — supervisors
Across these, the scanner has been tested with ~13,000 points read and ~30,000 paired BACnet exchanges at zero wire-level errors.
For hardware not on this list, the scanner uses a size-based heuristic that sets an appropriate object enumeration cap from the device's own reported object count. This is expected to work correctly for any BACnet-compliant device. If you scan a device that isn't on this list, please consider contributing a profile so future users can benefit from verified classification.
Known out-of-scope:
- Panels running proprietary firmware that doesn't include BACnet/IP. For example, Siemens APOGEE PXC panels running firmware revision 2.x speak Apogee P2 Ethernet rather than BACnet/IP. These panels may have IP addresses and be actively managed by Siemens Desigo CC, but they don't answer BACnet/IP. The same hardware upgraded to firmware revision 3.x or later would be scannable. If you're affected, check with your Siemens representative about a firmware upgrade path — or use commercial P2 gateways like PurpleSwift BACnetP2 for integration.
- Proprietary BMS protocols that are not BACnet or Modbus TCP (JCI N2, LonTalk, Honeywell C-Bus, various others).
git clone https://github.com/jamesccupps/HVAC-Network-Scanner.git
cd HVAC-Network-Scanner
pip install -e .Or run straight from the source tree without installing:
python -m hvac_scanner # launches the GUI
python -m hvac_scanner.cli --helpRequires Python 3.10 or newer. No extra packages.
For a Windows machine without Python — an engineering workstation you would
rather not install anything on — grab HVACNetworkScanner.exe from the
latest release.
One file, no installer, no runtime.
To build it yourself:
pip install -e ".[build]"
pyinstaller packaging/hvac_scanner.spec --noconfirm
# -> dist/HVACNetworkScanner.exePyInstaller is a build dependency only; the package itself stays dependency-free at runtime. The executable ships the GUI — the CLI remains available from a normal Python install.
The executable is not code-signed. On first run Windows SmartScreen will show "Windows protected your PC" and you will need More info → Run anyway. That warning means the binary has no purchased signing certificate, not that anything is wrong with it — but a network scanner is exactly the sort of program where you should not simply take that on trust.
Verify it before you run it. Each release publishes the SHA-256 of the executable next to the download; compare it against the file you got:
Get-FileHash .\HVACNetworkScanner.exe -Algorithm SHA256The binary is built by GitHub Actions from the tagged commit, not on a developer machine — the workflow is in .github/workflows/ci.yml and its build log is public, so you can see exactly what went into it. If you would rather not trust a binary at all, build it yourself with the two commands above; it is the same spec file CI uses.
python -m hvac_scannerEnter one or more CIDR networks (comma-separated), pick which protocols to scan, click SCAN. Devices populate into tabs:
- All Devices — cross-protocol table with identified model, vendor, web UI URL, default credentials, and description. Right-click for open-web-UI, copy-IP, copy-creds, ping, and a full details popup.
- BACnet Points — per-device object lists with present values and units.
- Modbus Registers — holding / input / coil reads.
- Services — discovered TCP service ports with banners and page titles.
- Raw JSON — the full scan result, ready to copy or export.
Click column headers to sort. IP addresses sort numerically by octet, not lexicographically.
Export to CSV or JSON with the EXPORT button.
The target field and CLI accept any mix of:
10.0.0.0/24 # CIDR
10.0.0.5 # single host
10.0.0.2-100 # last-octet range
10.0.0.2-10.0.0.12 # full-IP range
10.0.0.0/30, 10.0.1.5, 10.0.2.1-20 # mixed list
The broadcast address for BACnet Who-Is is computed automatically from
whatever you type — narrow CIDRs (e.g. /26), ranges, and single hosts
all auto-broadcast to the enclosing /24. The scan log shows the chosen
broadcast target so it's never magic. Power users can override via the
--broadcast CLI flag or ScanOptions.bacnet_broadcast.
New in v2. Runs headless — no display, no Tk. Intended for Task Scheduler, cron, and CI pipelines.
# Basic scan of a /24
python -m hvac_scanner.cli 192.168.1.0/24
# Multiple networks, export to JSON and CSV
python -m hvac_scanner.cli 10.0.0.0/24 10.0.1.0/24 \
--json scan.json --csv scan.csv
# BACnet only, with conservative rate limiting for small JACEs / UC400s
python -m hvac_scanner.cli 192.168.5.0/24 --bacnet-only --rate-limit 50
# Large-campus friendly: chunk Who-Is by 1000-instance ranges instead of
# one global broadcast. Avoids I-Am storms on big sites.
python -m hvac_scanner.cli 10.0.0.0/24 --whois-chunk 1000 --rate-limit 50
# Scan a narrow range of hosts — broadcast is auto-computed:
python -m hvac_scanner.cli 10.0.0.2-100
# Target specific devices only (no scan sweep at all):
python -m hvac_scanner.cli 10.0.0.19,10.0.0.21,10.0.0.192
# Power-user: override the auto-computed broadcast
python -m hvac_scanner.cli 10.0.0.0/24 --broadcast 255.255.255.255
# Quiet mode for scheduled runs
python -m hvac_scanner.cli 192.168.5.0/24 --json /var/log/bas-scan.json --quiet
# Generate a classification report — handy when you've scanned gear
# that fell back to the heuristic and you want to submit a profile:
python -m hvac_scanner.cli 10.0.0.0/24 --export-classification report.txtSee docs/CLI_USAGE.md for the full flag reference and a Windows Task Scheduler XML example.
Exit codes:
0— scan completed1— bad arguments2— interrupted (SIGINT)3— internal error
- TLS certificates are captured on HTTPS probes. The scan already established TLS to every 443/8443 and discarded the certificate. BAS controllers put the product line and often the panel name in the subject CN, and the expiry date is worth knowing on its own. Reported in the JSON, as a CSV column, and as a scan-log warning when a certificate is expired or inside 30 days. Self-signed is flagged, since that is the norm here and it means the CN is self-asserted.
- A Windows executable. One ~14 MB
HVACNetworkScanner.exe, no Python install required — see Standalone executable.
Earlier releases are in CHANGELOG.md, which covers every version back to v1.
This tool is intended for scanning networks you own or are authorized to audit. Running BACnet or Modbus sweeps against unfamiliar networks is at best rude and at worst unlawful in many jurisdictions. Building automation systems can also behave unpredictably when they see unexpected traffic — small field controllers have been known to lock up under probe load, and some equipment will fail-safe into unsafe mechanical states. Don't point it at anything you haven't been explicitly asked to assess.
The default-credentials database reflects the factory defaults published in each vendor's own documentation. It's here so the legitimate owner or operator of a system can quickly confirm whether defaults were ever changed, not as a remote-access toolkit.
hvac_scanner/
├── constants.py # Vendor DB, BACnet units, object types, HVAC ports
├── codec.py # Pure-function BACnet packet encode/decode
├── bacnet.py # UDP transport, socket reuse, RPM, deep-scan
├── modbus.py # Modbus TCP sweep + register reads
├── services.py # TCP port scan + protocol-specific probes
├── snmp.py # Raw UDP SNMP sysDescr probe
├── fingerprint.py # Cross-protocol model identification
├── device_profiles.py # Per-vendor enumeration caps + scan-depth presets
├── netrange.py # Target syntax: CIDR, ranges, host lists
├── engine.py # ScanEngine orchestrator + result/export
├── certs.py # Minimal X.509 parsing for HTTPS probes
├── diff.py # Baseline comparison (what changed since last scan)
├── cli.py # Headless command-line interface
├── gui.py # Tk GUI (thin wrapper over ScanEngine)
├── __main__.py # `python -m hvac_scanner` → GUI
└── __init__.py # Public API
tests/ # 502 tests
├── test_codec.py # BACnet packet encode/decode + parser regressions
├── test_bacnet_client.py # Socket / invoke-id filtering, RPM gap filling
├── test_modbus.py # Modbus framing, MBAP reassembly, device ID
├── test_snmp.py # SNMP BER encode/decode
├── test_rpm_batching.py # Array-index RPM enumeration
├── test_point_batching.py # Multi-object RPM point reads
├── test_bbmd.py # Foreign Device registration
├── test_certs.py # X.509 parsing, expiry, malformed input
├── test_diff.py # Baseline comparison
├── test_streaming.py # Live results during a scan
├── mock_device.py # UDP BACnet device for the tests
├── mock_bbmd.py # UDP BBMD for the tests
├── test_services_probes.py # CIP and S7 request wire formats
├── test_engine.py # Orchestration, sampling strategy, result shaping
├── test_device_profiles.py # Vendor caps, scan depth, --max-objects override
├── test_fingerprint.py # Model ID, vendor registry agreement, credentials
├── test_netrange.py # Target syntax, host limits, lazy iteration
├── test_target_filtering.py # I-Am filtering and deduplication
├── test_mstp_routing.py # DNET/DADR routing across BACnet routers
├── test_auto_broadcast.py # Broadcast address heuristic
├── test_property_per_type.py # Per-object-type property selection
├── test_validate_point_property.py # Per-property type validation
├── test_classification_report.py # Classification report export
├── test_gui_options.py # GUI controls reach ScanOptions (skips headless)
├── test_no_site_data.py # Guard: no site-identifying data in the tree
└── conftest.py
git clone https://github.com/jamesccupps/HVAC-Network-Scanner.git
cd HVAC-Network-Scanner
pip install -e ".[dev]"
pytestPull requests welcome. See CONTRIBUTING.md.
Open an issue for bugs and feature requests. For security reports or general questions, email jamesccupps@proton.me.
MIT — see LICENSE.
James Cupps — https://github.com/jamesccupps
