Skip to main content

A Python toolkit for programmatically generating Tridium Niagara .bog files for HVAC controls.

Project description

pybog: A Python Toolkit for Niagara BOG & DIST Files

bog_builder is a Python package for constructing Niagara Baja Object Graphs .bog files programmatically. The goal is for AI to assist human controls engineers in rapidly prototyping complex HVAC sequencing within wire sheet logic. If the software engineering community can prototype quickly, why shouldn’t the controls engineering community be able to do the same?

Leave Temp Snip

Python Project Setup

I use WSL (Windows Subsystem for Linux) but it make work just fine on ordinary Windows or Mac. Generating bogs can be done easily without setting up Python enivornments as shown further below via "ChatGPT Agent" mode and The Bog Maker 4000 website. Both examples are demo'd on YouTube.

pip install pybog

The project may get frequent updates so try:

pip install pybog --upgrade

Contribute to pybog via developing a local Python package

pip install .

To uninstall bog_builer if developing:

pip uninstall bog_builder

Make sure tests pass:

pytest

Make Git PR and if it is a mega make over beyond submitting examples Python files give me a heads up prior please.

Running Example Scripts with WSL

Each example script can be executed directly in WSL to generate a .bog file and drop it straight into your Niagara Workbench JENEsys directory. All example Python files are also compiled into a text file and used for LLM context.

  1. Run a specific example from project root directory Pass the Niagara Workbench path as the output directory (-o argument):

    python examples/bool_latch_play_ground.py -o /mnt/c/Users/ben/Niagara4.11/JENEsys
    

    This will create:

    /mnt/c/Users/ben/Niagara4.11/JENEsys/bool_latch_play_ground.bog
    
  2. Open Workbench Now you can import or open the generated .bog file inside your Niagara Workbench environment under the JENEsys station.


Tip: If you don’t want to type -o every time, you can edit each example script and change the default in its argparse:

parser.add_argument(
    "-o",
    "--output_dir",
    default="/mnt/c/Users/ben/Niagara4.11/JENEsys",
    help="Output directory for the .bog file."
)

Then you can just run:

python examples/bool_latch_play_ground.py

and it will always drop files directly into your Workbench directory for easy fast testing.


Bog Builder Python API Example

This is a code snip from the examples\subtract_simple.py file with optional start_sub_folder folder structures.

builder = BogFolderBuilder("SubtractionLogic")

# --- Inputs ---
builder.add_numeric_writable(name="Input_A", default_value=100.0)
builder.add_numeric_writable(name="Input_B", default_value=40.0)

# --- Output ---
builder.add_numeric_writable(name="Difference")

builder.start_sub_folder("CalculationLogic")
builder.add_component(comp_type="kitControl:Subtract", name="Subtract")

builder.end_sub_folder()

builder.add_link("Input_A", "out", "Subtract", "inA")
builder.add_link("Input_B", "out", "Subtract", "inB")
builder.add_link("Subtract", "out", "Difference", "in16")

bog_filename = f"{script_filename}.bog"
output_path = os.path.join(args.output_dir, bog_filename)
os.makedirs(args.output_dir, exist_ok=True)
builder.save(output_path)
print(f"\nSuccessfully created Niagara .bog file at: {output_path}")

When run, it will create a .bog file that can be directly imported into Workbench. Behind the scenes, pybog automatically arranges the grid layout to keep it neat and human-readable. Placing logic inside subfolders is optional, but it’s a great way to keep your bog files organized and clean. And yes—AI can handle all of this for you, too 😉.

python examples/subtract_simple.py -o /mnt/c/Users/ben/Niagara4.11/JENEsys

subtract image

Write Your Own .bog File in XML from scratch

The Python script operates by creating the entire XML structure of the Niagara .bog file as a single, multi-line text string. This string contains all the necessary tags to define each component, its properties, and the links between them. Finally, the script writes this complete XML string directly into a new file, which Niagara can then open and display as a standard wiresheet.

xml_content = '''<bajaObjectGraph version="4.0" reversibleEncodingKeySource="none" FIPSEnabled="false" reversibleEncodingValidator="[null.1]=">
  <p t="b:UnrestrictedFolder" m="b=baja">
    <p n="MyAdderLogic" t="b:Folder">

      <!-- Input1: Settable point with default value -->
      <p n="Input1" t="control:NumericWritable" h="1" m="control=control">
        <p n="out" f="s" t="b:StatusNumeric">
          <p n="value" v="6.0"/>
          <p n="status" v="0;activeLevel=e:17@control:PriorityLevel"/>
        </p>
        <p n="fallback" t="b:StatusNumeric">
          <p n="value" v="6.0"/>
        </p>
        <a n="emergencyOverride" f="h"/>
        <a n="emergencyAuto" f="h"/>
        <a n="override" f="ho"/>
        <a n="auto" f="ho"/>
        <p n="wsAnnotation" t="b:WsAnnotation" v="10,10,8"/>
      </p>
      
      <!-- Input2: Settable point with default value -->
      <p n="Input2" t="control:NumericWritable" h="2" m="control=control">
        <p n="out" f="s" t="b:StatusNumeric">
          <p n="value" v="4.0"/>
          <p n="status" v="0;activeLevel=e:17@control:PriorityLevel"/>
        </p>
        <p n="fallback" t="b:StatusNumeric">
          <p n="value" v="4.0"/>
        </p>
        <a n="emergencyOverride" f="h"/>
        <a n="emergencyAuto" f="h"/>
        <a n="override" f="ho"/>
        <a n="auto" f="ho"/>
        <p n="wsAnnotation" t="b:WsAnnotation" v="10,20,8"/>
      </p>

      <!-- Add: Logic block with verbose links -->
      <p n="Add" t="kitControl:Add" h="3" m="kitControl=kitControl">
        <p n="wsAnnotation" t="b:WsAnnotation" v="20,15,8"/>
        <p n="Link" t="b:Link">
          <p n="sourceOrd" v="h:1"/>
          <p n="relationId" v="n:dataLink"/>
          <p n="sourceSlotName" v="out"/>
          <p n="targetSlotName" v="inA"/>
        </p>
        <p n="Link1" t="b:Link">
          <p n="sourceOrd" v="h:2"/>
          <p n="relationId" v="n:dataLink"/>
          <p n="sourceSlotName" v="out"/>
          <p n="targetSlotName" v="inB"/>
        </p>
      </p>
      
      <!-- Sum: Read-only point with Set action explicitly hidden -->
      <p n="Sum" t="control:NumericWritable" h="4" m="control=control">
        <p n="out" f="h"/>
        <a n="emergencyOverride" f="h"/>
        <a n="emergencyAuto" f="h"/>
        <a n="override" f="ho"/>
        <a n="auto" f="ho"/>
        <a n="set" f="ho"/>
        <p n="wsAnnotation" t="b:WsAnnotation" v="30,15,8"/>
        <p n="Link" t="b:Link">
          <p n="sourceOrd" v="h:3"/>
          <p n="relationId" v="n:dataLink"/>
          <p n="sourceSlotName" v="out"/>
          <p n="targetSlotName" v="in16"/>
        </p>
      </p>

    </p>
  </p>
</bajaObjectGraph>'''

with open("PyMadeAddr.bog", "w", encoding="utf-8") as f:
    f.write(xml_content)

How it Works

  • Each <p> tag represents a Niagara component or a slot within a component (like out or fallback). Each <a> tag represents an action on that component, like set or override.
  • The f attribute (flags) is critical for controlling behavior. f="s" makes a slot settable, while f="h" or f="ho" hides a slot or action, which is how we create read-only points.
  • To set a default value, the out and fallback slots must be fully defined as complex properties containing nested <p n="value".../> and <p n="status".../> tags.
  • h="1", h="2", etc., are unique handles that links use to reference their source and target components.
  • wsAnnotation controls the block's position on the wiresheet. The coordinates are calculated using our Hierarchical Data Flow strategy to ensure a clean, grid-based layout.
  • The Add block's links use these handles to reference the out slots from Input1 and Input2 and connect them to its inA and inB inputs.

Adder Logic Created with Python


🔧 Using ChatGPT Agent Mode to Build .bog Files

The workflow is entirely conversational: upload your project zip, describe the control sequence you need, and ChatGPT will do the rest. Be se sure to hit the plus sign to enable "Agent" mode in ChatGPT.

Agent mode snip

🚀 How It Works

  1. Upload the project zip In the chat interface, attach the pybog-develop.zip file (found in this repository). The agent will automatically extract the archive and inspect the code.

  2. Describe your control logic Tell ChatGPT what sequence of operations you want to implement. For example:

    “Create a central plant with a boiler and chiller. Enable heating when the outside air temperature is 50 °F or below, and cooling when it is 65 °F or above. Use variable speed pumps with a differential pressure setpoint of 20 PSI and include a 2 °F deadband for both heating and cooling.”

  3. ChatGPT builds and tests the script

    • The agent writes a Python script using the BogFolderBuilder API.
    • It runs the script in a sandboxed environment and inspects the results.
    • If it fails, the agent reads the traceback, fixes the code, and tries again.
    • This iterate-and-repair loop continues until a valid .bog file is produced.
  4. Download the result Once successful, ChatGPT presents a link to download the generated .bog file. You can import this file directly into Niagara Workbench for testing.


✅ Advantages

  • No API key required
  • No local Python setup
  • Faster prototyping directly within the conversation

📊 AI Agent

The following Mermaid diagram illustrates the high-level flow when using ChatGPT Agent Mode:

flowchart TD
  start([Start chat session]) --> upload[User uploads pybog zip]
  upload --> describe[User describes desired control logic]
  describe --> init[Agent extracts context files and builder]
  init --> iterate{{Is first attempt?}}

  iterate -- Yes --> gen[Agent generates Python script]
  iterate -- No  --> fix[Agent repairs script using previous code and traceback]

  gen --> write[Write script to sandbox]
  fix --> write

  write --> run[Execute script and build .bog]
  run --> success{Run ok and file created?}

  success -- Yes --> done[Present download link\nExit]
  success -- No  --> cap[Capture error/traceback]
  cap --> retry{Attempts < max allowed?}
  retry -- Yes --> incr[Update attempt count and context]
  incr --> iterate
  retry -- No --> fail[Report failure\nExit]

💡 Tips

  • Be specific when describing your control logic (setpoints, deadbands, number of pumps, etc.). The more detail you provide, the more accurate the generated .bog file will be.
  • Validate in Workbench: After downloading, import the .bog file into Niagara Workbench to review the wiresheet and adjust as needed.

With Agent Mode, you can rapidly prototype complex HVAC sequences without writing any code yourself. Just describe what you need, and let ChatGPT handle the heavy lifting.


Generate Context Text Files

The context directory contains documentation specifically formatted for use by the LLM agent. Running the generator will take all Python files in the examples directory and combine them into a set of LLM-friendly documentation files (see GoFast MCP docs for the format specification).

  • llms.txt — a lightweight sitemap listing each example file and its relative path.
  • llms-full.txt — a single, concatenated file with the complete source of every example, wrapped with clear delimiters (=== FILE: ... ===, === CODE START ===, === CODE END ===). ⚠️ Note: this file can be quite large and may exceed the context window of some LLMs. For this project the llms-full.txt can push upwords of 20,000 tokens.

Generate the docs with:

python src/bog_builder/generate_llm_docs.py --examples examples --output context

This ensures the agent has direct access to all available example scripts, either as a quick index (llms.txt) or full training context (llms-full.txt).


Traversing Baja Object Graphs

Niagara represents the contents of a station as a directed graph of objects and properties. When working with the raw XML stored inside .bog and .dist archives you are effectively traversing this graph.

The graph is not strictly hierarchical: components can have links and references to other components across folders, and cycles may exist in more complex projects.

Best Practices

  • Parse once, traverse many. Extract the file.xml contents into an xml.etree.ElementTree and hold onto the root element. Re-parsing repeatedly is expensive.
  • Use breadth-first or depth-first search with a visited set. Each component element has a unique handle (h attribute). Track visited handles to avoid infinite loops.
  • Follow both containment and link relationships. Components are nested via <p h=...> elements, but logical connections are represented with b:Link child elements.
  • Build a handle → name map. Handles (e.g. s="h:123") are common in link definitions. Build a dictionary so you can resolve these references.
  • Be mindful of palettes. The type attribute encodes the palette and block name (e.g. kitControl:Add). Grouping by palette helps narrow searches or generate statistics.

Analyzer Class

The Analyzer in bog_builder.analyzer encapsulates these patterns. It:

  • Parses a .bog or .dist archive and extracts a flat JSON structure of components, properties, and links.
  • Builds a handle map so you can resolve references by handle.
  • Provides helpers to count kitControl blocks and generate bar/pie charts.

Example Usage

Analyse a .dist file, export JSON, and produce charts:

python -m bog_builder.analyzer analyze "/path/to/file.dist" \
  -o "/path/to/output.json" \
  --plots "/path/to/outputdir"

This will:

  • Save the JSON analysis into output.json.

  • Generate two PNGs in the outputdir folder:

    • kitcontrol_counts_bar.png
    • kitcontrol_counts_pie.png

Comparator Class

The BogComparator in bog_builder.analyzer provides a powerful diffing tool for your Niagara files. It:

  • Compares two .bog or .dist archives to find the differences between them.
  • Identifies components that have been added, removed, or modified.
  • Highlights specific changes to component properties and links, including changes to link types and converters.

Example Usage

Compare two .bog files to generate a diff report directly in your terminal:

python -m bog_builder.analyzer compare /path/to/version_A.bog /path/to/version_B.bog

This will print a detailed report listing:

  • Components that were added (+) or removed (-).
  • Modified components, detailing the exact property and link changes.

Future Features (TODO)

Web Interface (Flask App)

A simple web application is planned to provide a graphical user interface for the comparator tool.

  • How it will work: Users will be able to upload two .bog or raw .xml files directly in their browser using an intuitive interface.
  • Output: The application will display a user-friendly, color-coded diff report, making it even easier to visualize changes than the terminal output.

Example Output

Bar Chart (counts by block type) kitControl Bar

Pie Chart (distribution of block usage) kitControl Pie

👉 With this, you now have both machine-readable JSON for reverse engineering and visual plots for quick insights into station complexity and palette usage.


🎥 Keep Up with Talk Shop With Ben on YouTube


Component Library (kitControl)

Reference logic building blocks from Niagara’s kitControl palette are documented in pdf/docKitControl.pdf.


License

MIT License — free for reuse with attribution. Any files generated here are provided strictly for research and educational purposes. All outputs are delivered “as-is,” with no guarantees of accuracy, safety, or fitness for any application. Neither the pybog project nor its creator accepts any responsibility or liability under any circumstances. By generating or using a .bog file produced by this project, you agree that you assume all risks and full responsibility for any outcomes—including, but not limited to, personal injury, loss of life, financial loss, equipment damage, or mechanical system failures. If you choose to use these files in any way, you do so entirely at your own risk.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pybog-0.1.3.tar.gz (48.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pybog-0.1.3-py3-none-any.whl (36.6 kB view details)

Uploaded Python 3

File details

Details for the file pybog-0.1.3.tar.gz.

File metadata

  • Download URL: pybog-0.1.3.tar.gz
  • Upload date:
  • Size: 48.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.3

File hashes

Hashes for pybog-0.1.3.tar.gz
Algorithm Hash digest
SHA256 b27210eb41d9bb8bb7342a7582b57a41d7b9949acf3b1471cc51eab72bb08949
MD5 94048d7641af4d720d7afffe5d979e32
BLAKE2b-256 b8c246d0068939c4e9388c98d83fee8b090c77b1b1387ea3edaede5101cfe00a

See more details on using hashes here.

File details

Details for the file pybog-0.1.3-py3-none-any.whl.

File metadata

  • Download URL: pybog-0.1.3-py3-none-any.whl
  • Upload date:
  • Size: 36.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.3

File hashes

Hashes for pybog-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 35b4e2d1aff473101f015b204e99e35f250ab5825728509a30d1676a755430da
MD5 7e6fbd92414f7d5603341394694036fd
BLAKE2b-256 d09192aa6016c449272e52caa4cd7cf0e93eca1c8c4060e164acc26661983725

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page