📄 OptiMiser Project File Format (OptiMiser Zeus JSON Specification)
Ryan Moore
Last Update vor einem Monat
This document explains the structure and interpretation of JSON files returned by the OptiMiser API. These files mirror the application's internal storage model and represent complete projects using a tree of objects.
Each project is serialized as a set of Rows (keyed objects) with a common structure and hierarchical relationship.
The files are flat lists of objects that must be restructured into a tree using the parent_ references.
A Row is a generic object representing a data entry "card" in the project. All rows share the following structure:
| Field | Type | Description |
|---|---|---|
| type_ | ||
| _id | string | A unique identifier (not a database ID) |
| parent_ | string | The _id of the parent Row (only omitted for the root BaseRow) |
| banner_ | string | Human-readable label for UI display |
| modified_ | string | Comma-separated list of modified fields since last sync |
| _rev | string | localstorage artifact (ignore) |
| syncdate_ | null/string | Timestamp of the last save (server-side only) |
| [custom props] | various | Dynamically defined properties for the row's type |
Property Conventions:
property/optimiserentry: 'yes' if set by OptiMiser programmatically
property/external: human-readable alias for internal values
property/caption, property/fullsize: image-related metadata
The root of the hierarchy, always has type_ = "base" and no parent_. For most types of Zeus projects this will appear as "idproject1" in the file.
| Field | Type | Description |
|---|---|---|
| id | number | Internal database ID |
| name | string | Project name |
| companyId | number | Associated company ID |
| programId | number | Associated program ID |
| stageId | number | Stage of project (deprecated projectStatus can be ignored) |
| projectformtype | string | Form variant |
| projectoptname | string | Optional friendly name |
| templateType | string/null | "user", "program", "company", or "sample" |
| modelCalculated | string/null | Timestamp when modeling was last computed |
| modelInvalidated | string/null | Reason for model invalidation |
| syncdate_ | string/null | Timestamp of last sync |
| zeusModelVersion_ | string/null | Version of the modeling engine at load time (not saved) |
When parsed by OptiMiser’s runtime, this object is extended with arrays for each row type, such as windowTypes, incentivess, etc. These are not present in the raw JSON but can be constructed by walking the tree of parent_ links from the base node.
🔁 Interpreting a Retrieved Project File
Locate the Base Row:
type_ === "base"
This serves as the root of the project tree.
Build a Tree:
For each Row look at the parent_ and attach as a child of the matching parent _id, being sure to keep groups of child type_ instances together (as there can be multiples of any child type_ for a given parent). See the example javascript code below, which creates arrays in the parent Row for each child type seen (e.g. combustionSafetys or measures), and fills those arrays with child Row(s)
Group Rows by Type (optional but recommended):
After tree construction, rows can be grouped by type_ for easier processing (e.g., all combustionSafety rows).
Ignore transient fields:
Fields like _rev and zeusModelVersion_ are runtime-only or managed by localstorage and do not affect project logic.
In this example:
The first item is the BaseRow.
The second is a combustionSafety row attached to the project, as its parent is the id of the project row
The third is a measure for the combustionSafety row, as its parent is the id of the combustionSafetyRow
The fourth is also a measure for the combustionSafety row, as its parent is the id of the combustionSafetyRow
When interpreted, these would be organized into a tree with the base record at the root, one combustion safety record attached to it, and two measures attached to the combustion safety record.
Sample javascript to build a tree
As the example above shows, you find measures/additional improvements by looking for records with the type_ === "measure". You can find out which record a measure is for by looking at its parent_ and finding the record with the matching _id. Measures include interesting properties including:
- measureName - the computer ID for the measure
- measureName/external - the 'human' name of the measure
- measureType - if there's a categorization for this measure, it would be given here
- measureFuelType - if there's a categorization for this measure's fuel type, it would be given here
- measureCost - the per-unit cost (to the consumer)
- measureLaborCost - the per-unit labor cost
- measureQty - the quantity
- measureTotalCost - the total cost (to the consumer). Usually just the cost+laborcost multiplied by the quantity, but that's not always the case, as some measures use a scaling factor (e.g. floor area for attic insulation)
How to know a measure is included
Look for "ImpPhase*" fields on the json blocks. When an ImpPhase* control is > 0, the corresponding measure is included in the package. Here is the list of ImpPhase controls:
Embedded Table Objects
To read one of these tables, first build a column map from `columns[index].name`, then apply that map to each row value. The `name` field is the best programmatic key, while `label` is intended for display. Other column metadata such as `type`, `width`, `hide`, or `CustomColHide` may help reproduce the OptiMiser UI, but consumers usually only need `name`, `label`, and the row values.
Values in table rows are often serialized as strings, even when the column type is `number` or `decimal`. Parsers should be prepared to handle formatted numbers such as `"12,855"`, blanks, `"-"`, `"NA"`, `"N/A"`, `"NaN"`, `"TRUE"`, and `"FALSE"`. Do not assume that every row is a complete data row; some tables include spacer rows or display-only rows. When available, the `ID` column is the most useful row identifier. The companion field ending in `/optimiserentry`, such as `ConsumptionObject/optimiserentry`, indicates that the table value was generated or maintained by OptiMiser.
Important table objects include:
- `ModelQAObject`: model quality assurance checks and warnings, including the check ID, severity icon, message, linked Zeus section, and noncompliance flag.
- `CalibrationResultsObject`: BPI-2400 or utility calibration results by fuel and end use, including bill usage, model usage, error, percent error, and recommended action.
- `ConsumptionObject`: annual energy consumption by fuel/source, base versus improved case, savings rows, end-use breakdowns, totals, and units such as Therms, kWh, or MBtu.
- `AnnualLoadsObject`: heating and cooling load components by envelope or internal gain category, with base and improved rows and total MBtu.
- `CostTableObject`: selected improvement package totals and measure economics, including savings, cost, incentives, net cost, payback, lifetime, SIR, and row/column hide flags.
- `CalibrationChartsObject`: chart-ready calibration values comparing bills and model output by fuel type.
- `UtilityAnalysisSummaryObject`: utility analysis messages shown to the user, such as missing bill warnings.
- `ElecBillModelObject` and `GasBillModelObject`: monthly utility bill data and regression/model prediction values.
F.A.Q.
Measures and Calculation Logic
- How do I accurately identify if a measure is included?
Do not rely on the anymeasures flag. Instead, check the ImpPhase[MeasureName] control for the specific section. If the value is greater than 0, the measure is included (e.g., ImpPhaseVentilation > 0 indicates ventilation is included).
- Why are some values different between the UI and the data file?
Data points like insulation may differ based on the building state. For example, insulation on crawl spaces will differ depending on whether the space is defined as "Conditioned" (insulation on wall/rim joist) or "Unconditioned" (insulation on the floor above).
- Why are some base sections (like idattic1) missing or renamed?
This occurs if a second instance of a card was created and the original was deleted. When a card is duplicated and the original is removed, the subsequent instances receive unique object IDs. Use the banner_ attribute (e.g., "Attic 1") to identify the card rather than relying on the object ID.
- How should I locate specific objects like HVAC 2 or Attic 2?
Always use the banner_ attribute to identify specific instances of repeated sections (e.g., look for "banner_": "HVAC 2").
Project File Creation
- Why are my utility bill dates and numbers not aligning after import?
Ensure all dates are provided in ascending order. Only the first period requires a full start date; end dates function as the start of the next period. Additionally, ensure date values are padded (e.g., "2026-05-14" instead of "2026-5-14").
- How do I clear out old data in a project?
If you have set invalid data and need to reset it to null, try sending a blank string in your API call.
- Why aren't my changes reflecting on the UI after a push?
Currently, you must reload the project after a successful push to see the changes. We are working on a system to automate this via the autosync system (websockets), but for now, a manual reload is required.
- Are there issues with specific API payload styles?
If you encounter issues during modify_project calls, try omitting the use_zeus_names parameter.
- What causes "No such variable" errors?
These errors often occur when sending numbered controls (e.g., ChkWindows1) when the system only recognizes the master variable without the number (e.g., ChkWindows). Avoid sending the numbered variants unless specifically required.
