Technical reference

Developer and maintainer reference: directory layout, environment variables, unit tests, BACnet scrape, data model API, bootstrap, database schema, and LLM tagging workflow. For user-facing docs see the Documentation index.

Setup: python3 -m venv .venv && source .venv/bin/activate. Install: pip install -e ".[dev]". Tests: pytest open_fdd/tests/ -v. BACnet scrape: see Run BACnet scrape and Confirm BACnet is scraping below.


Directory structure

open-fdd/
├── open_fdd/
│   ├── engine/            # runner, checks, brick_resolver
│   ├── reports/           # fault_viz, docx, fault_report
│   ├── schema/            # FDD result/event (canonical)
│   ├── platform/          # FastAPI, DB, drivers, loop
│   │   ├── api/           # CRUD (sites, points, equipment), config, bacnet, data_model, download, analytics, run_fdd
│   │   ├── drivers/       # open_meteo, bacnet (RPC + data-model scrape), bacnet_validate
│   │   ├── bacnet_brick.py # BACnet object_type → BRICK class mapping
│   │   ├── config.py, database.py, data_model_ttl.py, graph_model.py, site_resolver.py
│   │   ├── loop.py, rules_loader.py
│   │   └── static/        # Config UI — index.html, app.js, styles.css (served at /app; see Developer guide)
│   └── tests/             # engine/, platform/, test_schema.py
├── stack/rules/           # Default FDD rule YAML (sensor_bounds, sensor_flatline); upload more via Faults UI
├── stack/                 # docker-compose, Dockerfiles, SQL, grafana, caddy
│   ├── sql/               # 001_init … 015_fault_state_and_audit (migrations; see Developer guide — Database schema)
│   ├── grafana/           # provisioning/datasources, optional dashboards
│   └── caddy/             # Caddy only: [`stack/caddy/Caddyfile`](../../stack/caddy/Caddyfile) (minimal rev proxy; hardening TBD — [Security](../security))
├── config/                # data_model.ttl (Brick + BACnet + platform config)
├── scripts/               # bootstrap.sh, fake_*_faults.py
├── tools/
│   ├── discover_bacnet.py # Optional: BACnet discovery → CSV (bacpypes3); not used by default stack scrape
│   ├── run_bacnet_scrape.py, run_weather_fetch.py, run_rule_loop.py, run_host_stats.py
│   ├── graph_and_crud_test.py # Full CRUD + RDF + SPARQL e2e (see SPARQL cookbook)
│   ├── bacnet_crud_smoke_test.py # Simple BACnet instance range + CRUD smoke test
│   ├── trigger_fdd_run.py
│   └── ...
└── examples/              # cloud_export, brick_resolver, run_all_rules_brick, etc.

Front-end and database: See Developer guide for Config UI development (no build step; edit platform/static/ and refresh) and the full database schema (migrations 001–015, tables, cascade deletes).


Environmental variables

All platform settings use the OFDD_ prefix (pydantic-settings; .env and env override). Set on the host (e.g. stack/.env or in docker-compose.yml); Docker passes them into each container.

After first bootstrap, platform config lives in the RDF graph (GET/PUT /config; config/data_model.ttl). The Required / infra vars below are always read from env. The Platform config (graph) vars are used to seed the graph at bootstrap (PUT /config) and as fallback for processes that don’t call GET /config (e.g. FDD loop container). Scrapers (BACnet, weather) that call GET /config get live config from the graph. See Configuration and SPARQL cookbook.

Required / infra (always from env)

Variable Default Description
OFDD_DB_DSN postgresql://postgres:postgres@localhost:5432/openfdd TimescaleDB connection. In Docker: postgresql://postgres:postgres@db:5432/openfdd.
OFDD_API_URL http://localhost:8000 Used by bootstrap and by scrapers to call GET /config.
OFDD_API_KEY Generated by bootstrap Optional. When set, all requests (except /, /health, /docs, /redoc, /openapi.json, /app) require Authorization: Bearer <OFDD_API_KEY>. Bootstrap generates a key and writes it to stack/.env unless you pass --no-auth. Use for API auth (e.g. frontend, scripts).
OFDD_BRICK_TTL_PATH config/data_model.ttl Unified TTL (Brick + BACnet + config); API load/save.
OFDD_APP_TITLE Open-FDD API API title.
OFDD_APP_VERSION 2.0.2 Fallback when package metadata missing.
OFDD_DEBUG false Debug mode.
OFDD_FDD_TRIGGER_FILE config/.run_fdd_now Touch to trigger FDD run and reset timer.
OFDD_GRAPH_SYNC_INTERVAL_MIN 5 Minutes between graph serialize to TTL (API).
OFDD_HOST_STATS_INTERVAL_SEC 60 host-stats container interval (seconds).
OFDD_DISK_MOUNT_PATHS / Comma-separated paths for disk usage → disk_metrics.
OFDD_RETENTION_DAYS 365 TimescaleDB retention (bootstrap / 007_retention).
OFDD_LOG_MAX_SIZE 100m Docker log max size per file.
OFDD_LOG_MAX_FILES 3 Docker log file count.

Platform config (RDF graph; env seeds bootstrap)

Used to build the PUT /config body at bootstrap; thereafter config is in the graph. Containers that don’t fetch GET /config (e.g. FDD loop) still read these from env.

Variable Default Description
OFDD_RULE_INTERVAL_HOURS 3 FDD run interval (hours).
OFDD_LOOKBACK_DAYS 3 Lookback window for timeseries.
OFDD_RULES_DIR stack/rules YAML rules directory (hot reload).
OFDD_BRICK_TTL_DIR config Brick TTL directory.
OFDD_BACNET_SERVER_URL diy-bacnet-server URL (e.g. http://localhost:8080).
OFDD_BACNET_SITE_ID default Site to tag when scraping.
OFDD_BACNET_GATEWAYS JSON array for central aggregator.
OFDD_BACNET_SCRAPE_ENABLED true Enable BACnet scraper.
OFDD_BACNET_SCRAPE_INTERVAL_MIN 5 Scrape interval (minutes).
OFDD_BACNET_USE_DATA_MODEL true Use data-model scrape (default). When false, run_bacnet_scrape.py may use a CSV path; the bundled bacnet-scraper container expects the data model.
OFDD_OPEN_METEO_* (see Configuration) enabled, interval_hours, latitude, longitude, timezone, days_back, site_id.
OFDD_GRAPH_SYNC_INTERVAL_MIN 5 Graph sync interval (also in graph).

Optional: OFDD_ENV_FILE (Configuration).


Unit tests

Tests live under open_fdd/tests/. Run: pytest open_fdd/tests/ -v. All use in-process mocks; no shared DB or live API. For end-to-end (real API, optional BACnet): python tools/graph_and_crud_test.py (see SPARQL cookbook).

  • engine/ — brick_resolver, runner, weather_rules
  • platform/ — bacnet_api, bacnet_brick, bacnet_driver, config, crud_api, data_model_api, data_model_ttl, download_api, graph_model, rules_loader, site_resolver
  • test_schema.py — FDD result/event to row

Run BACnet scrape

With DB and diy-bacnet-server reachable:

  • One shot: OFDD_BACNET_SERVER_URL=http://localhost:8080 python tools/run_bacnet_scrape.py --data-model
  • Loop: same with --loop (uses OFDD_BACNET_SCRAPE_INTERVAL_MIN).

Confirm scraping: Docker logs openfdd_bacnet_scraper; DB timeseries_readings; Grafana SQL cookbook; API GET /download/csv.


Data model API and discovery flow

GET /data-model/export — BACnet discovery + DB points (optional ?bacnet_only=true, ?site_id=...). Use for AI-assisted tagging; then PUT /data-model/import.

PUT /data-model/import — Points (required) and optional equipment (feeds/fed_by). Creates/updates points; backend rebuilds RDF and serializes TTL.

Flow: Discover (POST /bacnet/whois_range, POST /bacnet/point_discovery_to_graph) → Sites/equipment (CRUD) → GET /data-model/export → Tag (LLM or manual) → PUT /data-model/import → Scraping → GET /data-model/check, POST /data-model/sparql for integrity.

See Data modeling overview and SPARQL cookbook.


Data model sync

Live store: in-memory RDF graph (platform/graph_model.py). Brick triples from DB; BACnet from point_discovery. SPARQL and TTL read from this graph. Background thread serializes to config/data_model.ttl every OFDD_GRAPH_SYNC_INTERVAL_MIN; POST /data-model/serialize on demand.


Bootstrap and client updates

Safe for clients: ./scripts/bootstrap.sh --update --maintenance --verify does not wipe TimescaleDB or Grafana data (no volume prune). Migrations in stack/sql/ are idempotent. See Getting started.

Troubleshooting 500 (db host unresolved): Ensure full stack is up so API can resolve hostname db. Run ./scripts/bootstrap.sh or docker compose -f stack/docker-compose.yml up -d.


Database schema (TimescaleDB)

Schema is defined in stack/sql/ (migrations 001–015). Idempotent; bootstrap runs them in order. Cascade deletes: Site → equipment, points, timeseries; equipment → points; point → timeseries. Full migration list and table details: Developer guide — Database schema. See Danger zone.

Table Purpose
sites id, name, description, metadata, created_at
equipment id, site_id, name, equipment_type, feeds_equipment_id, fed_by_equipment_id
points id, site_id, equipment_id, external_id, brick_type, fdd_input, unit, bacnet_*, object_name, polling
timeseries_readings ts, site_id, point_id, value, job_id (hypertable; BACnet + weather + CSV ingest)
ingest_jobs id, site_id, name, format, point_columns, row_count (CSV ingest metadata)
fault_results ts, site_id, equipment_id, fault_id, flag_value (hypertable)
fault_events id, site_id, equipment_id, fault_id, start_ts, end_ts, duration_seconds, evidence
fault_state current active fault per (site_id, equipment_id, fault_id)
fault_definitions fault_id, name, description, severity, category, equipment_types, inputs, params, expression, source
fdd_run_log run_ts, status, sites_processed, faults_written (last FDD run for UI)
analytics_motor_runtime site_id, period_start, period_end, runtime_hours (data-model driven)
host_metrics ts, hostname, mem_, swap_, load_1/5/15 (hypertable)
container_metrics ts, container_name, cpu_pct, mem_, pids, net_, block_* (hypertable)
disk_metrics ts, hostname, mount_path, total_bytes, used_bytes, free_bytes (hypertable)
bacnet_write_audit point_id, value, source, ts, success, reason (write audit)

PyPI and this repo

Legacy PyPI: The package open-fdd on PyPI is an old, unsupported release. It contains FD (fault detection) equations only and no AFDD framework—no FastAPI platform, BACnet, or data model. That work evolved into the Expression Rule Cookbook and the full FastAPI AFDD platform in this repo. Do not use the PyPI version; use this repository instead.

Current platform: Open-FDD is developed and run from this repo (or from Docker images built from it). The stack installs open-fdd locally at build time (pip install -e ".[platform,brick]" from the copied repo); nothing in the project depends on the PyPI open-fdd package.

If you publish to PyPI again: Options are (a) publish the full current package (FastAPI API, rules engine, data model) as a new major version so pip install open-fdd gives the platform, or (b) publish a small integration-helpers library for third-party or cloud integrations. Until then, install from source: pip install -e ".[dev]" or use the Docker stack.


LLM tagging workflow

  1. Export — GET /data-model/export.
  2. Clean — Keep only points to tag and poll.
  3. Tag with LLM — Use prompt in AI-assisted tagging.
  4. Import — PUT /data-model/import with points and optional equipment. Set polling false on points that should not be scraped.

Prompt summary: Set site_id, external_id, brick_type, rule_input; optionally equipment_id, unit (e.g. degF, %, cfm, 0/1 — stored in DB and TTL; frontend uses it for Plots axis labels and grouping), and equipment feeds_equipment_id/fed_by_equipment_id. Output is the completed JSON for PUT /data-model/import.