The website includes a Reports workspace for native ETAP Crystal PDFs, batch jobs and PDF preview. See reporting setup for the Windows worker and Cloud Run configuration.
Browse ETAP project models and study results in your browser — no ETAP license required.
ETAP Lens is a local web app for power systems engineers to explore ETAP project data (buses, cables, transformers, generators, loads, protective devices) and study results (short circuit, load flow) without needing ETAP itself installed. Point it at a project folder, pick a file, and get a searchable, filterable, exportable view of everything inside it.
Unofficial / independent project. ETAP Lens is not affiliated with, endorsed by, or associated with ETAP® or Operation Technology, Inc. "ETAP" is a trademark of its respective owner; it's referenced here only to describe compatibility with ETAP's file formats.
For Windows reporting with the original ETAP Crystal Reports templates, see the
Crystal Report Generator companion. It reads .SA1S/.SA2S/.UL1S
files, previews reports in SAP's native viewer, and exports PDFs individually or
in batches. The companion has separate Windows/.NET/Crystal runtime requirements.
An ETAP .oti file is not the project database — it's a small container holding a connection pointer (ODBC DSN/DBQ) plus user and permission records. The real engineering data lives in a SQL Server database file next to it:
<name>.MDF— the live database (preferred)<name>.BAK— a backup, used as a fallback if no.MDFis found
ETAP Lens figures out which file actually has the data, reads it (via a local, throwaway copy — your original files are never modified), and shows everything in a browsable web UI.
Study result files (.SA1S, .SA2S, .LF1S, .UL1S, .TU1S, .HA1S, .GRDS) are a separate case: they're already plain SQLite files (ETAP writes them that way), so they're read directly — no SQL Server involved, and loading is near-instant.
- Load a project model (
.oti/.mdf/.bak) or study results — short circuit (.SA1SANSI duty,.SA2Sfault currents by type), load flow (.LF1Sbalanced,.UL1Sunbalanced/3-phase), time-domain load flow (.TU1STDLF — typically a full 8760-hour year), harmonics (.HA1S— both frequency scan and harmonic load flow), and ground grid (.GRDS) - Harmonic plot curves folded in — a harmonic run keeps its curves in
.fspdb/.hfpdbfiles beside the.HA1S; open the.HA1Sfrom a project folder and the impedance-vs-frequency sweeps, spectra and waveforms come with it, each point tagged with the device it belongs to (they ride along on upload too, so a hosted instance gets them as well) - Annual AC loss report for TDLF results — energy losses split across transmission lines, cables, unit transformers and main power transformers; generation at the units; and net output at the point of interconnection, all derived on import and reconciled against ETAP's own system totals
- In-app folder browser — navigate real folders and drives from inside the page (no native OS dialog limitations), with quick-access shortcuts to Desktop/Documents/Downloads/Home, and a "Select This Folder" action to scan and pick from everything loadable in it
- Load an entire folder at once — paste or browse to a folder and get a pick-list of every
.oti/.mdf/.bak/study-result file in it - Curated category views tailored to what's loaded:
- Project model: Buses, Cables & Lines, Transformers, Generators & Sources, Loads & Motors, Protective Devices, Capacitors & Reactors, Meters
- Short-circuit study: Device Duty, Bus Duty (Interrupting/Momentary), Fault Currents by type, Clipping Current, Alerts
- Load flow study: Bus Voltage & Loading, Branch Loading/Losses, System Totals, Voltage Violations, Alerts
- Harmonic study: Frequency Scan, Plot Curves, Voltage Distortion, Bus/Branch Harmonic Spectrum, IEEE 519 Limits, Harmonic Sources & Filters, Alerts
- Ground grid study: resistance to remote earth, ground potential rise, mesh and step voltages calculated vs. tolerable
- Single Line (Bus Explorer) — pick a bus, see every cable, transformer, breaker/switch, load, and source connected to it (a tabular topology view)
- Every table view has: search (scoped to one column or all), sortable columns, pagination, a column-visibility panel (wide tables auto-hide audit-trail noise columns like
IID/Revision/Checker*), and a Sum/Average/Min/Max/Count summary row - Export any view to CSV or Excel (
.xlsx), respecting your current filter/search/column-visibility state - Violations Report — one click bundles every alert/violation table in a loaded study into a single formatted Excel workbook
- Unload — remove any loaded project from the cache individually, or clear everything at once
- All Tables (raw) — browse any underlying table directly, including per-revision history (
H1–H8) and rating (_R) tables not part of the curated categories
(Add a screenshot here — run the app, load a project, and drop an image at docs/screenshot.png.)
- Windows (this project relies on SQL Server LocalDB and the Windows registry for a couple of features — see Platform notes)
- Python 3.10+
- A local SQL Server / SQL Server LocalDB instance. ETAP itself normally installs one named
ETAPLocalDB19(matches theDSN=otilocaldb19seen in.oticonnection strings) — if that's present, no extra setup is needed. Study result files (.SA1S/.SA2S/.LF1S/.UL1S/.TU1S/.HA1S/.GRDS) don't need this at all, since they're already SQLite. sqlcmdand the "ODBC Driver 17 for SQL Server" (both ship with SQL Server / SSMS tooling)
git clone https://github.com/hmazin/ETAP_LENS.git
cd ETAP_LENS
pip install -r requirements.txt
python app.pyThen open http://127.0.0.1:5151. Click Browse Folders... to navigate to your ETAP project directory, or paste a path directly. The first load of an .mdf/.bak takes a minute or two (it copies the database, attaches it, and reads every table); study result files load in a couple of seconds. Results are cached in cache/ (git-ignored) and reload instantly unless the source file changes.
The app has two modes, set by ETAP_LENS_MODE. It defaults to local — the desktop case, where the server is your own machine, so browsing the filesystem and loading a file by absolute path are the whole point.
hosted is for running it on a server that serves other people. Those same features become an unauthenticated directory lister over the server's disk, so they're switched off and upload becomes the only way in:
| Variable | Default | Purpose |
|---|---|---|
ETAP_LENS_MODE |
local |
local or hosted |
ETAP_LENS_MAX_UPLOAD_MB |
300 |
Rejected with a JSON 413 above this |
ETAP_LENS_CORS_ORIGINS |
— | Comma-separated exact origins allowed to call the API |
ETAP_LENS_CORS_ORIGIN_REGEX |
— | For preview deployments with generated hostnames |
ETAP_LENS_LITE_CACHE |
on when hosted | Keep only the summary tables (see below) |
ETAP_LENS_GCS_BUCKET |
— | Object storage for uploads; local disk if unset |
ETAP_LENS_TURNSTILE_SITE_KEY / _SECRET_KEY |
— | Bot check on the upload-URL endpoint; off unless both are set |
ETAP_LENS_MAX_UPLOADS_PER_SESSION |
25 |
Per-session ceiling on loaded files |
ETAP_LENS_DERIVE_TIMEOUT |
300 |
Seconds before an import is aborted |
ETAP_LENS_CACHE_TTL_HOURS |
168 |
Age at which idle caches are swept |
In hosted mode /api/browse, /api/browse/quick, and path-based /api/load return 403, and the frontend hides the controls that use them (it reads /api/config at startup). Only study result files are supported — .MDF/.BAK need SQL Server LocalDB, which is Windows-only.
Hosted mode also requires an X-Session-Id header on every API call. The browser generates a 128-bit random id and keeps it in localStorage; everything cached is namespaced by it, so one visitor's uploads are invisible to another and two people uploading files with the same name don't collide. It's a header rather than a cookie because the frontend and API can be on different origins. Locally there's one user, so no session is required and the cache is shared — the desktop app behaves exactly as it always has.
Uploads are validated before anything opens them: extension allowlist, SQLite magic bytes (an extension is a claim, not evidence), and a schema-readable check. Imports run under a wall-clock deadline, because file size bounds how much data there can be but not how long working over it takes.
Deploying it for real: see docs/DEPLOYMENT.md.
Anchor
ETAP_LENS_CORS_ORIGIN_REGEXto your own hostnames. A pattern like.*\.vercel\.appwould let anybody's app call your API from a visitor's browser.
A time-domain study is mostly per-step detail — a year of hourly results across 54 branches is ~473,000 rows, while the derived summaries that answer almost every real question are ~27,000. With ETAP_LENS_LITE_CACHE on, the per-branch series is never built and ETAP's own per-step tables are dropped once the summaries are computed:
| Cached size | Tables | Rows | |
|---|---|---|---|
| Full | 271.9 MB | 46 | 1,025,231 |
| Lite | 3.7 MB | 40 | 26,591 |
Every summary table is byte-identical between the two — lite changes what you can drill into, never a number. What it costs is the "Branch Time Series" and "Raw Result Tables" views, which the UI labels as deliberately empty rather than leaving you to wonder.
It only applies to time-domain results; nothing else has per-step tables worth dropping. Off by default locally, on by default when hosted, where it's the difference between an expensive service and a cheap one — and means what sits at rest is aggregate results rather than a complete model.
Build and run the container:
docker build -t etap-lens . && docker run -p 8080:8080 -e ETAP_LENS_CORS_ORIGINS=https://your-frontend.example.com etap-lensThe frontend in web/ is plain static files with no build step, so it can be served by Flask (same origin, nothing to configure) or uploaded to any static host — in which case set window.ETAP_API_BASE in web/config.js to the backend's URL.
If you just want a quick dump without the browser UI:
python read_oti.py "path\to\etapmodel.oti" # dump the .OTI container itself
python dump_mdf.py "path\to\etapmodel.oti" --out model.sqlite # full project -> one portable .sqlite filedump_mdf.py accepts any of .oti/.mdf/.bak/.sa1s/.sa2s/.lf1s/.ul1s directly and auto-resolves .oti → .mdf/.bak the same way the web app does.
etap_reader/locate.py— given an.oti, parses itsODBCInfo/ConnectionStringstream to get the real database name (DBQ=), then looks for<name>.MDF/<name>.BAKnext to it. Study result extensions (etap_reader/study_result.py) are recognized directly.etap_reader/mdf_dump.py— for.mdf/.bak: copies the source file, attaches/restores it on a local SQL Server LocalDB instance, walksINFORMATION_SCHEMAto read every table's schema and rows, writes it all into a single SQLite file, then detaches and deletes the working copy.etap_reader/study_result.py— for study result files: copies the (already-SQLite) file directly via SQLite's own backup API, then strips ETAP's print-formatting spacer rows (###<<<BlankLine>>>###) before indexing. Much faster since there's no SQL Server step.etap_reader/project_cache.py— caches the resulting.sqliteper source file (keyed by path + size + mtime) so repeat loads are instant, and handles unloading.etap_reader/browse_fs.py/etap_reader/folder_scan.py— server-side filesystem browsing, since browsers don't expose real paths from native file pickers and a native "select folder" dialog doesn't show files while browsing.etap_reader/xlsx_export.py— formats CSV-shaped data into styled.xlsxworkbooks for export and the violations report.app.py— a Flask API over the cached SQLite file;web/is a plain-JS single-page frontend with no build step (no npm, no bundler — just open and edit). It contains no server-side templating, so Flask can serve it directly (the local case, same origin as the API) or it can be hosted separately withweb/config.jspointing at the backend's URL.
app.py Flask app / API routes
etap_reader/
locate.py .oti -> .mdf/.bak resolution, study file recognition
mdf_dump.py SQL Server LocalDB attach + dump to SQLite
study_result.py direct SQLite copy + cleanup for study result files
time_domain.py TDLF (.TU1S) derived summaries + annual AC loss report
ha_plots.py harmonic plot curves (.fspdb/.hfpdb) folded into their .HA1S
ground_grid.py .GRDS cleanup (binary geometry column, epoch RunDate)
categories.py curated table groupings per project/study type
project_cache.py caching, loading, unloading
folder_scan.py flat "what's loadable in this folder" scan
browse_fs.py in-app folder browser backend
xlsx_export.py Excel export formatting
oti_parser.py raw .OTI (OLE compound file) parser
web/index.html page shell (plain HTML - no templating)
web/app.js frontend logic (tables, search, export, browser modal)
web/style.css styling
web/config.js API base URL (empty = same origin)
read_oti.py, dump_mdf.py standalone CLI tools
python -m unittest discover -s tests -t .Standard library only — no pytest, no extra dependency. The fixtures build small synthetic ETAP files in a temp folder rather than shipping a client's model, which also lets them cover shapes the projects on hand don't contain (a bus whose name has an underscore in it, a plot file with no curves, a grid that was never run).
This was built and tested on Windows against ETAP 24. A few pieces are Windows-specific:
- SQL Server LocalDB (for
.mdf/.bakproject models) - The Windows registry lookup for Desktop/Documents/Downloads quick-access folders (handles OneDrive Known Folder Move redirection)
- Drive-letter enumeration (
C:\,D:\, ...) in the folder browser
Study result file support (.SA1S/.SA2S/.LF1S/.UL1S/.TU1S/.HA1S/.GRDS) has no Windows-specific dependencies and should work anywhere Python + Flask run. Cross-platform support for the rest is a good first contribution — see below.
ETAP doesn't publish documentation for its file formats, so everything this
tool knows was determined by directly inspecting real project files. That
investigation - what's in the .OTI vs .MDF vs .LDF, what each table
category means, and an in-progress reverse-engineering log for where the
one-line diagram's actual element positions/sizes are stored - is written
up in docs/FILE_FORMAT_NOTES.md. Worth
reading before touching etap_reader/.
- The "Single Line" view is a connectivity summary built from each element's
FromBus/ToBus/Bus/FromElement/ToElementfields, not a rendering of the actual one-line diagram graphics (bus positions, symbol placement, wire routing). A graphical SLD renderer would be a substantial follow-on project. - Only tested against ETAP 24 output. Table/column names may differ across ETAP versions — if something doesn't map correctly, please open an issue with the version you're on.
_Rrating sub-tables were empty in every project this was tested against; they're still browsable under "All Tables" if your project populates them.- A harmonic run's plot curves only arrive if the whole folder is opened (or picked, when hosted). Opening a lone
.HA1Sgets the results without the curves — the.fspdb/.hfpdbbeside it are where those live. The Plot Curves view says so rather than looking empty. .fsp/.hfp(the binary plot settings — axis ranges, which curves are ticked) are not decoded. The curve data they accompany is read in full, so this costs nothing but the ability to reproduce ETAP's exact plot styling..XL1Sis recognized as an ETAP file but not read: the only sample available was empty, and its schema didn't match its name. See FILE_FORMAT_NOTES.md.
Some directions that would add real value, roughly in order of impact vs. effort:
- Cross-link the model to its study results — click a bus in the Single Line Explorer and see its short-circuit duty / load flow results inline, pulled from whichever studies are also loaded
- Cross-platform support — swap the SQL Server LocalDB dependency for something portable (e.g.
mdbtools/a pure-Python MDF reader) so this runs on macOS/Linux - Graphical one-line diagram rendering
- Case comparison view — same bus/device across multiple loaded study cases side by side
- Charts — voltage profile, loading margin, duty-vs-rating visualizations instead of just tables
- Conditional formatting in table views — color cells by % of rating
- Support for more ETAP result file types (transient stability, arc flash, reliability, protective coordination)
Found a bug or have a feature idea? Open an issue. Pull requests welcome — see CONTRIBUTING.md.
MIT © 2026 Hooman Mazin
This is an independent, community-built tool for reading ETAP's file formats. It is not produced, reviewed, or endorsed by ETAP / Operation Technology, Inc. Use it at your own risk, and always cross-check critical engineering results against ETAP itself.
