{"id":35101405,"url":"https://github.com/bbartling/open-hvac-optimization","last_synced_at":"2026-05-23T07:01:42.316Z","repository":{"id":329431677,"uuid":"1117613511","full_name":"bbartling/open-hvac-optimization","owner":"bbartling","description":"Concept idea in WebAssembly for IoT edge for generic HVAC system optimization algorithms.","archived":false,"fork":false,"pushed_at":"2025-12-19T20:48:33.000Z","size":61,"stargazers_count":0,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"develop","last_synced_at":"2025-12-22T05:39:35.429Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bbartling.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-16T15:04:35.000Z","updated_at":"2025-12-19T20:48:36.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bbartling/open-hvac-optimization","commit_stats":null,"previous_names":["bbartling/open-hvac-optimization"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/bbartling/open-hvac-optimization","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bbartling%2Fopen-hvac-optimization","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bbartling%2Fopen-hvac-optimization/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bbartling%2Fopen-hvac-optimization/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bbartling%2Fopen-hvac-optimization/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bbartling","download_url":"https://codeload.github.com/bbartling/open-hvac-optimization/tar.gz/refs/heads/develop","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bbartling%2Fopen-hvac-optimization/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33386076,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-23T04:15:53.637Z","status":"ssl_error","status_checked_at":"2026-05-23T04:15:53.242Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-12-27T16:59:54.252Z","updated_at":"2026-05-23T07:01:42.309Z","avatar_url":"https://github.com/bbartling.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Open HVAC Optimization\n\nThis repository contains a modular implementation of the\n**ASHRAE Guideline 36** request and reset algorithms compiled to\nWebAssembly. The goal of this project is to provide a portable\nbuilding-automation core that can run on any Linux-based IoT edge\ndevice and be orchestrated from Python. The design mirrors the\nNiagara ProgramObjects used in the *open-hvac-optimization* project but\npackages each algorithm as a standalone WebAssembly module so it can\nbe versioned, tested and distributed like a `.bog` file.\n\nTwo levels of control are provided at the C layer, plus a combined\n“system” module:\n\n* **VAV zone request counter** (`c/vav/vav_algo.c`) — Implements the\n  GL-36 zone-level logic that determines cooling and pressure\n  requests for each VAV box based on damper position, airflow,\n  zone temperature and loop demand. Each call to `vav_update` can\n  process an arbitrary number of zones. Timers and hysteresis are\n  managed per zone internally.\n\n* **AHU Trim \u0026 Respond** (`c/ahu/ahu_algo.c`) — Implements simplified\n  duct static pressure and supply air temperature reset algorithms for\n  an air handling unit. The algorithms support startup delays,\n  adjustable update cadence, ignored request thresholds and bounded\n  trim/respond magnitudes. Outside air temperature inputs are\n  accepted and can be used to shape SAT reset behavior.\n\n* **Combined VAV + AHU system block** (`c/system/system_algo.c`) —\n  Wraps the VAV and AHU algorithms into a single WebAssembly module\n  that accepts full-system telemetry (all VAV zones, AHU fan status,\n  occupancy, last duct static and SAT setpoints, outside air\n  temperature and timestep). On each call it computes per-zone\n  cooling/pressure requests and returns updated AHU duct static\n  and SAT setpoints while internally tracking Guideline-36 style\n  startup delay and update cadence.\n\nPython wrappers (`python/vav_host.py`, `python/ahu_host.py` and\n`python/system_host.py`) use the\n[wasmtime](https://github.com/bytecodealliance/wasmtime) runtime to\nload and interact with the compiled `.wasm` files. They hide the\ncomplexity of WebAssembly memory management and provide idiomatic\nmethods such as `VAVAlgo.update()`, `AHUAlgo.update_pressure()`,\n`AHUAlgo.update_sat()` and `SystemAlgo.update()` for end-to-end\nVAV-plus-AHU simulation.\n\nThe `tests/` directory contains unit tests exercising the zone,\nAHU and combined system algorithms. The tests use a fixed timestep\n(e.g. one second) and accumulate minutes of elapsed time via loops to\nsatisfy the persistence timers specified in the Guideline. When\nEmscripten is available the tests call `c/build.sh` to compile the\nlatest C sources into `.wasm`.\n\n## Repository structure\n\n```text\nopen-hvac-optimization/\n├── LICENSE                  # SPDX: MIT\n├── c/                       # C sources and build script\n│   ├── build.sh             # builds vav, ahu, hvac, and system *.wasm modules\n│   ├── hvac_algo.c          # legacy combined VAV + AHU wrapper (optional)\n│   ├── hvac_algo.h          # C API for hvac_algo.wasm\n│   ├── vav/\n│   │   ├── vav_algo.c       # GL-36 VAV box request logic\n│   │   └── vav_algo.h       # C API\n│   ├── ahu/\n│   │   ├── ahu_algo.c       # Trim \u0026 Respond algorithms for AHU\n│   │   └── ahu_algo.h       # C API\n│   └── system/\n│       ├── system_algo.c    # Combined VAV + AHU system algorithm\n│       └── system_algo.h    # C API\n├── python/                  # Python wrappers using wasmtime\n│   ├── algo_host.py         # Legacy hvac_algo.wasm wrapper\n│   ├── vav_host.py          # VAV wrapper\n│   ├── ahu_host.py          # AHU wrapper\n│   └── system_host.py       # Combined system wrapper (VAV + AHU)\n├── tests/                   # Pytest unit tests\n│   ├── test_vav.py          # VAV algorithm tests\n│   ├── test_ahu.py          # AHU algorithm tests\n│   └── test_system_algo.py  # Combined system (VAV + AHU) tests\n└── README.md                # this file\n```\n\n\n## Prerequisites\n\n* **Emscripten** — You must install the Emscripten SDK to compile the\n  C sources to WebAssembly.  On Ubuntu you can follow the official\n  instructions to install `emsdk`【175348836414933†L81-L115】.  After installation run\n  `source emsdk_env.sh` to put `emcc` on your `PATH`.\n\n* **Python 3.8+** with `pip`.  The tests and wrappers depend on\n  `wasmtime`.  Install it into a virtual environment:\n\n  ```sh\n  python3 -m venv env\n  . env/bin/activate\n  pip install wasmtime pytest black\n  ```\n\n\u003cdetails\u003e\n\u003csummary\u003e💻 GL36 Code Base in C!\u003c/summary\u003e\n\n\u003e Combined VAV + AHU Guideline-36 logic\n\n```c\n/*\n * system_algo.c\n *\n * Implementation of the combined VAV + AHU Guideline-36 logic.\n *\n * This translation unit simply reuses the existing, battle-tested\n * VAV and AHU algorithms by including their C sources and wiring\n * them together into a single convenience API.\n */\n\n#include \u003cstddef.h\u003e\n#include \"../vav/vav_algo.h\"\n#include \"../ahu/ahu_algo.h\"\n#include \"system_algo.h\"\n\nvoid system_init(\n    int n_zones,\n    double p_sp0,\n    double p_spmin,\n    double p_spmax,\n    double p_startup_delay_sec,\n    double p_update_interval_sec,\n    double p_ignore_req,\n    double p_sp_trim,\n    double p_sp_respond,\n    double p_sp_respond_max,\n    double sat_sp0,\n    double sat_spmin,\n    double sat_spmax,\n    double sat_startup_delay_sec,\n    double sat_update_interval_sec,\n    double sat_ignore_req,\n    double sat_sp_trim,\n    double sat_sp_respond,\n    double sat_sp_respond_max\n)\n{\n    /* Initialise per-zone VAV state and both AHU loops. */\n    vav_init(n_zones);\n    ahu_init_pressure(\n        p_sp0, p_spmin, p_spmax,\n        p_startup_delay_sec, p_update_interval_sec,\n        p_ignore_req, p_sp_trim,\n        p_sp_respond, p_sp_respond_max\n    );\n    ahu_init_sat(\n        sat_sp0, sat_spmin, sat_spmax,\n        sat_startup_delay_sec, sat_update_interval_sec,\n        sat_ignore_req, sat_sp_trim,\n        sat_sp_respond, sat_sp_respond_max\n    );\n}\n\nvoid system_update(\n    const double* zoneTemp,\n    const double* zoneCoolingSpt,\n    const double* zoneDemand,\n    const double* vavFlow,\n    const double* vavFlowSpt,\n    const double* vavDamperCmd,\n    double dt_sec,\n    int is_imperial,\n    int n_zones,\n    int fanRun,\n    int occupied,\n    double current_pressure_sp,\n    double current_sat_sp,\n    double outside_air_temp,\n    double oat_min,\n    double oat_max,\n    int* coolRequests,\n    int* pressureRequests,\n    double* next_pressure_sp,\n    double* next_sat_sp\n)\n{\n    /* First, compute per-zone requests using the VAV logic.  We\n     * use variable-length arrays here for simplicity; the\n     * upper-level wrappers ensure that n_zones is modest.\n     */\n#if defined(__STDC_VERSION__) \u0026\u0026 __STDC_VERSION__ \u003e= 199901L\n    int local_cool[ n_zones ];\n    int local_press[ n_zones ];\n#else\n    /* Fallback for compilers without VLAs: require n_zones \u003e 0 and\n     * allocate a small worst-case.  For WebAssembly via Emscripten\n     * this branch should not be hit, but is kept for completeness.\n     */\n    int local_cool[128];\n    int local_press[128];\n    if (n_zones \u003e 128) {\n        n_zones = 128;\n    }\n#endif\n\n    vav_update(\n        zoneTemp,\n        zoneCoolingSpt,\n        zoneDemand,\n        vavFlow,\n        vavFlowSpt,\n        vavDamperCmd,\n        dt_sec,\n        is_imperial,\n        n_zones,\n        local_cool,\n        local_press\n    );\n\n    /* Aggregate requests across all zones. */\n    double total_pressure_req = 0.0;\n    double total_cool_req = 0.0;\n    for (int i = 0; i \u003c n_zones; ++i) {\n        pressureRequests[i] = local_press[i];\n        coolRequests[i]     = local_cool[i];\n        total_pressure_req += (double)local_press[i];\n        total_cool_req     += (double)local_cool[i];\n    }\n\n    /* Feed the aggregates into the AHU Trim \u0026 Respond loops to\n     * compute new duct static and SAT setpoints.\n     */\n    int fan_and_occ = (fanRun \u0026\u0026 occupied) ? 1 : 0;\n    double next_p = ahu_update_pressure(\n        fan_and_occ,\n        current_pressure_sp,\n        total_pressure_req,\n        dt_sec\n    );\n    double next_sat = ahu_update_sat(\n        fan_and_occ,\n        current_sat_sp,\n        total_cool_req,\n        outside_air_temp,\n        oat_min,\n        oat_max,\n        dt_sec\n    );\n\n    if (next_pressure_sp) {\n        *next_pressure_sp = next_p;\n    }\n    if (next_sat_sp) {\n        *next_sat_sp = next_sat;\n    }\n}\n```\n\n\n\u003e AHU Only T\u0026R\n\n```c\n/*\n * ahu_algo.c\n *\n * Implementation of simplified Trim \u0026 Respond algorithms for AHU duct\n * static pressure and supply air temperature setpoints.  These\n * algorithms are distilled from ASHRAE Guideline‑36 examples and the\n * Niagara reference code.  The goal is not to perfectly reproduce\n * every nuance of the Niagara implementation but to provide a\n * deterministic, portable core that can be compiled to WebAssembly\n * and exercised from Python test cases.  The algorithms support\n * startup delay, update cadence, ignored request thresholds and\n * bounded trim/respond adjustments.  Outside air temperature\n * influence is currently ignored for simplicity.\n */\n\n#include \"ahu_algo.h\"\n#include \u003cstdlib.h\u003e\n\n/* ------------------------------------------------------------------------- */\n/* Pressure Trim \u0026 Respond internal state */\n\ntypedef struct {\n  /* Configuration */\n  double sp0;\n  double spmin;\n  double spmax;\n  double startup_delay;\n  double update_interval;\n  double ignore_req;\n  double sp_trim;\n  double sp_respond;\n  double sp_respond_max;\n  /* Mutable state */\n  double pressure_sp;         /* last commanded setpoint */\n  double elapsed_startup;     /* seconds since fan turned on */\n  double elapsed_update;      /* seconds since last trim/respond action */\n  int fan_on_last;            /* 0/1 flag to detect edges */\n} PressureState;\n\nstatic PressureState pressureState = {0};\n\nvoid ahu_init_pressure(double sp0, double spmin, double spmax,\n                       double startup_delay_sec, double update_interval_sec,\n                       double ignore_req, double sp_trim,\n                       double sp_respond, double sp_respond_max) {\n  pressureState.sp0 = sp0;\n  pressureState.spmin = spmin;\n  pressureState.spmax = spmax;\n  pressureState.startup_delay = startup_delay_sec;\n  pressureState.update_interval = update_interval_sec;\n  pressureState.ignore_req = ignore_req;\n  pressureState.sp_trim = sp_trim;\n  pressureState.sp_respond = sp_respond;\n  pressureState.sp_respond_max = sp_respond_max;\n  /* Reset mutable state */\n  pressureState.pressure_sp = sp0;\n  pressureState.elapsed_startup = 0.0;\n  pressureState.elapsed_update = 0.0;\n  pressureState.fan_on_last = 0;\n}\n\nstatic double clamp_double(double v, double lo, double hi) {\n  if (v \u003c lo) return lo;\n  if (v \u003e hi) return hi;\n  return v;\n}\n\ndouble ahu_update_pressure(int fanRun, double current_sp,\n                           double total_requests, double dt_sec) {\n  PressureState* st = \u0026pressureState;\n  /* Fan off: reset to initial setpoint and clear timers */\n  if (!fanRun) {\n    st-\u003epressure_sp = st-\u003esp0;\n    st-\u003eelapsed_startup = 0.0;\n    st-\u003eelapsed_update  = 0.0;\n    st-\u003efan_on_last = 0;\n    return st-\u003epressure_sp;\n  }\n  /* Fan just turned on: edge detection */\n  if (st-\u003efan_on_last == 0) {\n    st-\u003efan_on_last = 1;\n    st-\u003epressure_sp = st-\u003esp0;\n    st-\u003eelapsed_startup = 0.0;\n    st-\u003eelapsed_update  = 0.0;\n    return st-\u003epressure_sp;\n  }\n  /* Accumulate startup delay */\n  st-\u003eelapsed_startup += dt_sec;\n  if (st-\u003eelapsed_startup \u003c st-\u003estartup_delay) {\n    /* Hold at sp0 during startup delay */\n    st-\u003epressure_sp = st-\u003esp0;\n    return st-\u003epressure_sp;\n  }\n  /* Accumulate update cadence */\n  st-\u003eelapsed_update += dt_sec;\n  if (st-\u003eelapsed_update \u003c st-\u003eupdate_interval) {\n    /* No change until the cadence is met */\n    return st-\u003epressure_sp;\n  }\n  /* Reset the cadence timer */\n  st-\u003eelapsed_update = 0.0;\n  /* Determine trim or respond action */\n  double new_sp;\n  if (total_requests \u003c= st-\u003eignore_req) {\n    /* Trim: reduce setpoint by a fixed amount */\n    new_sp = st-\u003epressure_sp + st-\u003esp_trim;\n  } else {\n    /* Respond: increase setpoint proportional to requests over the ignore threshold */\n    double respond = st-\u003esp_respond * (total_requests - st-\u003eignore_req);\n    /* Limit the magnitude of the respond increment */\n    if (st-\u003esp_respond \u003e 0) {\n      if (respond \u003e st-\u003esp_respond_max) respond = st-\u003esp_respond_max;\n    } else {\n      if (respond \u003c st-\u003esp_respond_max) respond = st-\u003esp_respond_max;\n    }\n    new_sp = st-\u003epressure_sp + respond;\n  }\n  /* Clamp to allowable range and update state */\n  st-\u003epressure_sp = clamp_double(new_sp, st-\u003espmin, st-\u003espmax);\n  return st-\u003epressure_sp;\n}\n\n/* ------------------------------------------------------------------------- */\n/* Supply Air Temperature Trim \u0026 Respond internal state */\n\ntypedef struct {\n  /* Configuration */\n  double sp0;\n  double spmin;\n  double spmax;\n  double startup_delay;\n  double update_interval;\n  double ignore_req;\n  double sp_trim;\n  double sp_respond;\n  double sp_respond_max;\n  /* Mutable state */\n  double sat_sp;\n  double elapsed_startup;\n  double elapsed_update;\n  int fan_on_last;\n} SatState;\n\nstatic SatState satState = {0};\n\nvoid ahu_init_sat(double sp0, double spmin, double spmax,\n                  double startup_delay_sec, double update_interval_sec,\n                  double ignore_req, double sp_trim,\n                  double sp_respond, double sp_respond_max) {\n  satState.sp0 = sp0;\n  satState.spmin = spmin;\n  satState.spmax = spmax;\n  satState.startup_delay = startup_delay_sec;\n  satState.update_interval = update_interval_sec;\n  satState.ignore_req = ignore_req;\n  satState.sp_trim = sp_trim;\n  satState.sp_respond = sp_respond;\n  satState.sp_respond_max = sp_respond_max;\n  satState.sat_sp = sp0;\n  satState.elapsed_startup = 0.0;\n  satState.elapsed_update = 0.0;\n  satState.fan_on_last = 0;\n}\n\ndouble ahu_update_sat(int fanRun, double current_sp,\n                      double total_requests, double outside_air_temp,\n                      double oat_min, double oat_max, double dt_sec) {\n  /* The outside air temperature parameters are accepted for API\n   * compatibility but currently unused. */\n  (void)outside_air_temp;\n  (void)oat_min;\n  (void)oat_max;\n  SatState* st = \u0026satState;\n  /* Fan off: reset to initial setpoint and clear timers */\n  if (!fanRun) {\n    st-\u003esat_sp = st-\u003esp0;\n    st-\u003eelapsed_startup = 0.0;\n    st-\u003eelapsed_update  = 0.0;\n    st-\u003efan_on_last = 0;\n    return st-\u003esat_sp;\n  }\n  /* Fan just turned on */\n  if (st-\u003efan_on_last == 0) {\n    st-\u003efan_on_last = 1;\n    st-\u003esat_sp = st-\u003esp0;\n    st-\u003eelapsed_startup = 0.0;\n    st-\u003eelapsed_update  = 0.0;\n    return st-\u003esat_sp;\n  }\n  /* Accumulate startup delay */\n  st-\u003eelapsed_startup += dt_sec;\n  if (st-\u003eelapsed_startup \u003c st-\u003estartup_delay) {\n    st-\u003esat_sp = st-\u003esp0;\n    return st-\u003esat_sp;\n  }\n  /* Accumulate update cadence */\n  st-\u003eelapsed_update += dt_sec;\n  if (st-\u003eelapsed_update \u003c st-\u003eupdate_interval) {\n    return st-\u003esat_sp;\n  }\n  /* Reset cadence timer */\n  st-\u003eelapsed_update = 0.0;\n  /* Trim or respond */\n  double new_sp;\n  if (total_requests \u003c= st-\u003eignore_req) {\n    /* Trim: raise setpoint (towards warmer) */\n    new_sp = st-\u003esat_sp + st-\u003esp_trim;\n  } else {\n    /* Respond: lower setpoint proportional to requests (cooler) */\n    double respond = st-\u003esp_respond * (total_requests - st-\u003eignore_req);\n    /* Limit the magnitude of respond */\n    if (st-\u003esp_respond \u003e 0) {\n      if (respond \u003e st-\u003esp_respond_max) respond = st-\u003esp_respond_max;\n    } else {\n      if (respond \u003c st-\u003esp_respond_max) respond = st-\u003esp_respond_max;\n    }\n    new_sp = st-\u003esat_sp + respond;\n  }\n  st-\u003esat_sp = clamp_double(new_sp, st-\u003espmin, st-\u003espmax);\n  return st-\u003esat_sp;\n}\n\n```\n\n\n\u003e VAV Box Only T\u0026R\n\n```c\n\n/*\n * vav_algo.c\n *\n * Implementation of ASHRAE Guideline‑36 zone level request logic for\n * multiple VAV boxes.  This module manages per‑zone timers and\n * hysteresis state required to determine cooling and pressure\n * requests for each VAV box.  The algorithm closely follows the\n * Niagara example provided in the project README but is expressed in\n * C so it can be compiled to WebAssembly using Emscripten.  See\n * vav_algo.h for the public interface.\n */\n\n#include \"vav_algo.h\"\n#include \u003cstdlib.h\u003e\n\n/* Global constants based on ASHRAE Guideline 36.  See the Java\n * implementation in the README for definitions.  These values are\n * intentionally constant so they can be inlined by the compiler.\n */\n\n/* Pressure timing / thresholds (seconds) */\nstatic const double PRESS_PERSIST_SEC      = 60.0; /* 1 minute persistence */\nstatic const double PRESS_RATIO_3REQ       = 0.50;\nstatic const double PRESS_DAMPER_3REQ_MIN  = 95.0;\nstatic const double PRESS_RATIO_2REQ       = 0.70;\nstatic const double PRESS_DAMPER_2REQ_MIN  = 95.0;\nstatic const double PRESS_DAMPER_1REQ_ON   = 95.0;\nstatic const double PRESS_DAMPER_1REQ_OFF  = 85.0;\n\n/* Temperature timing / thresholds */\nstatic const double TEMP_HIGH_DIFF_C  = 3.0;\nstatic const double TEMP_MED_DIFF_C   = 2.0;\nstatic const double TEMP_HIGH_DIFF_F  = 5.0;\nstatic const double TEMP_MED_DIFF_F   = 3.0;\nstatic const double TEMP_PERSIST_SEC  = 120.0; /* 2 minutes */\nstatic const double TEMP_SUPPRESS_SEC = 60.0;  /* 1 minute */\nstatic const double TEMP_LOOP_1REQ_ON  = 95.0;\nstatic const double TEMP_LOOP_1REQ_OFF = 85.0;\n\n/* Internal per‑zone state structure.  One instance of this struct\n * exists for each VAV box.  Timers accumulate elapsed time when\n * conditions are met.  lastPressureReq and lastTempReq store\n * hysteresis state to allow the 1‑request conditions to persist\n * between calls.\n */\ntypedef struct {\n  /* Pressure timers and state */\n  double pressHighTimerSec;\n  double pressMedTimerSec;\n  int    lastPressureReq;\n  double lastPressDamperPct;\n  double lastPressFlowRatio;\n  /* Temperature timers and state */\n  double tempHighTimerSec;\n  double tempMedTimerSec;\n  double tempSuppressTimerSec;\n  int    lastTempReq;\n  double lastTempDiff;\n  double lastTempLoopPct;\n} ZoneState;\n\n/* Global pointer to zone state array */\nstatic ZoneState* zoneStates = NULL;\nstatic int zoneCount = 0;\n\n/* Initialise the zone state array.  Any previously allocated state\n * will be freed.  See vav_algo.h for documentation.  */\nvoid vav_init(int n_zones) {\n  if (zoneStates) {\n    free(zoneStates);\n    zoneStates = NULL;\n    zoneCount = 0;\n  }\n  if (n_zones \u003e 0) {\n    zoneStates = (ZoneState*)calloc((size_t)n_zones, sizeof(ZoneState));\n    zoneCount = n_zones;\n    /* calloc zeroes all fields */\n  }\n}\n\n/* Helper to clamp an integer value between a minimum and maximum. */\nstatic inline int clamp_int(int v, int lo, int hi) {\n  if (v \u003c lo) return lo;\n  if (v \u003e hi) return hi;\n  return v;\n}\n\n/* Compute the pressure request for a single zone.  See the comments\n * above for threshold definitions.  The dt_sec parameter is the\n * elapsed time since the last call.  */\nstatic int compute_pressure(ZoneState* st, double damper, double flow, double flow_sp, double dt_sec) {\n  /* If flow setpoint \u003c= 0 treat as invalid and reset timers */\n  double ratio = 1.0;\n  if (flow_sp \u003e 0.0) {\n    ratio = flow / flow_sp;\n  } else {\n    st-\u003epressHighTimerSec = 0.0;\n    st-\u003epressMedTimerSec  = 0.0;\n    st-\u003elastPressDamperPct = damper;\n    st-\u003elastPressFlowRatio = 0.0;\n    st-\u003elastPressureReq = 0;\n    return 0;\n  }\n\n  st-\u003elastPressDamperPct = damper;\n  st-\u003elastPressFlowRatio = ratio;\n\n  /* 3 requests: ratio \u003c 0.50 and damper ≥ 95 for 1 minute */\n  int cond3 = (ratio \u003c PRESS_RATIO_3REQ) \u0026\u0026 (damper \u003e= PRESS_DAMPER_3REQ_MIN);\n  if (cond3) {\n    st-\u003epressHighTimerSec += dt_sec;\n  } else {\n    st-\u003epressHighTimerSec = 0.0;\n  }\n  if (st-\u003epressHighTimerSec \u003e= PRESS_PERSIST_SEC) {\n    st-\u003epressMedTimerSec = 0.0;\n    st-\u003elastPressureReq = 3;\n    return 3;\n  }\n\n  /* 2 requests: ratio \u003c 0.70 and damper ≥ 95 for 1 minute */\n  int cond2 = (ratio \u003c PRESS_RATIO_2REQ) \u0026\u0026 (damper \u003e= PRESS_DAMPER_2REQ_MIN);\n  if (cond2) {\n    st-\u003epressMedTimerSec += dt_sec;\n  } else {\n    st-\u003epressMedTimerSec = 0.0;\n  }\n  if (st-\u003epressMedTimerSec \u003e= PRESS_PERSIST_SEC) {\n    st-\u003elastPressureReq = 2;\n    return 2;\n  }\n\n  /* 1 request with hysteresis */\n  if (damper \u003e= PRESS_DAMPER_1REQ_ON) {\n    st-\u003elastPressureReq = 1;\n    return 1;\n  }\n  if (st-\u003elastPressureReq == 1 \u0026\u0026 damper \u003e= PRESS_DAMPER_1REQ_OFF) {\n    return 1;\n  }\n  st-\u003elastPressureReq = 0;\n  return 0;\n}\n\n/* Compute the cooling request for a single zone.  Differences in\n * temperature thresholds and persistence durations between Celsius and\n * Fahrenheit are handled via the is_imperial flag.  */\nstatic int compute_cooling(ZoneState* st, double zoneTemp, double zoneSp,\n                           double demand, int is_imperial, double dt_sec) {\n  /* Choose thresholds based on unit system */\n  double highDiff = is_imperial ? TEMP_HIGH_DIFF_F : TEMP_HIGH_DIFF_C;\n  double medDiff  = is_imperial ? TEMP_MED_DIFF_F  : TEMP_MED_DIFF_C;\n\n  double diff = zoneTemp - zoneSp; /* positive means too warm */\n  st-\u003elastTempDiff = diff;\n  st-\u003elastTempLoopPct = demand;\n\n  /* Advance suppression timer up to its max.  During suppression the\n   * zone should not accumulate deviation timers. */\n  if (st-\u003etempSuppressTimerSec \u003c TEMP_SUPPRESS_SEC) {\n    st-\u003etempSuppressTimerSec += dt_sec;\n    if (st-\u003etempSuppressTimerSec \u003e TEMP_SUPPRESS_SEC) {\n      st-\u003etempSuppressTimerSec = TEMP_SUPPRESS_SEC;\n    }\n  }\n\n  if (st-\u003etempSuppressTimerSec \u003e= TEMP_SUPPRESS_SEC) {\n    /* Accumulate temperature deviation timers */\n    if (diff \u003e= highDiff) {\n      st-\u003etempHighTimerSec += dt_sec;\n      st-\u003etempMedTimerSec  = 0.0;\n    } else if (diff \u003e= medDiff) {\n      st-\u003etempMedTimerSec  += dt_sec;\n      st-\u003etempHighTimerSec  = 0.0;\n    } else {\n      st-\u003etempHighTimerSec = 0.0;\n      st-\u003etempMedTimerSec  = 0.0;\n    }\n\n    if (st-\u003etempHighTimerSec \u003e= TEMP_PERSIST_SEC) {\n      st-\u003elastTempReq = 3;\n      return 3;\n    }\n    if (st-\u003etempMedTimerSec \u003e= TEMP_PERSIST_SEC) {\n      st-\u003elastTempReq = 2;\n      return 2;\n    }\n  } else {\n    /* During suppression period, do not accumulate deviation timers */\n    st-\u003etempHighTimerSec = 0.0;\n    st-\u003etempMedTimerSec  = 0.0;\n  }\n\n  /* 1 request: demand loop saturation with hysteresis */\n  if (demand \u003e= TEMP_LOOP_1REQ_ON) {\n    st-\u003elastTempReq = 1;\n    return 1;\n  }\n  if (st-\u003elastTempReq == 1 \u0026\u0026 demand \u003e= TEMP_LOOP_1REQ_OFF) {\n    return 1;\n  }\n  st-\u003elastTempReq = 0;\n  return 0;\n}\n\n/* Public API: compute requests for all zones.  See header for\n * documentation. */\nvoid vav_update(const double* zoneTemp,\n                const double* zoneCoolingSpt,\n                const double* zoneDemand,\n                const double* vavFlow,\n                const double* vavFlowSpt,\n                const double* vavDamperCmd,\n                double dt_sec,\n                int is_imperial,\n                int n_zones,\n                int* coolRequests,\n                int* pressureRequests) {\n  if (!zoneStates || n_zones \u003c= 0) return;\n  if (n_zones \u003e zoneCount) {\n    n_zones = zoneCount;\n  }\n  for (int i = 0; i \u003c n_zones; ++i) {\n    ZoneState* st = \u0026zoneStates[i];\n    double t  = zoneTemp[i];\n    double sp = zoneCoolingSpt[i];\n    double dem = zoneDemand[i];\n    double flow = vavFlow[i];\n    double flow_sp = vavFlowSpt[i];\n    double damper = vavDamperCmd[i];\n    /* compute pressure */\n    int pReq = compute_pressure(st, damper, flow, flow_sp, dt_sec);\n    /* compute cooling */\n    int cReq = compute_cooling(st, t, sp, dem, is_imperial, dt_sec);\n    /* clamp outputs */\n    pressureRequests[i] = clamp_int(pReq, 0, 3);\n    coolRequests[i]     = clamp_int(cReq, 0, 3);\n  }\n}\n```\n\n\u003c/details\u003e\n\n## Building the WebAssembly modules\n\nRun the build script from the `c/` directory:\n\n```sh\ncd c\n./build.sh\n```\n\nThis will produce two files:\n\n* `vav/vav_algo.wasm` — the zone request module\n* `ahu/ahu_algo.wasm` — the AHU reset module\n* `system/system_algo.wasm` — combined AHU and VAV reset module\n\nThese files are consumed by the Python wrappers.  If `emcc` is not on\nyour `PATH` the script will exit without building; in that case you\nshould place precompiled `.wasm` files in the respective directories.\n\n## Running the tests\n\nAfter building the wasm modules you can run the unit tests with\n`pytest`:\n\n```sh\npytest -q\n```\n\nThe VAV tests verify that zero inputs produce zero requests; that\nconsistent undersupply/overheating yields `3` requests after the\nappropriate persistence periods; and that invalid data (e.g. zero\nflow setpoint) resets the pressure request.  The AHU tests verify\ntrim and respond behaviour for both pressure and SAT loops, including\nclamping to minimum/maximum setpoints.\n\n## Example usage\n\nBelow is a minimal example demonstrating how to combine the VAV and\nAHU modules to implement a complete GL‑36 airside reset.  This\nexample assumes that the modules have been built with `c/build.sh`.\n\n```python\nfrom __future__ import annotations\n\nfrom pathlib import Path\n\nfrom system_host import SystemAlgo, SystemConfig\n\n\ndef main() -\u003e None:\n    base = Path(__file__).resolve().parents[1]\n    wasm_path = base / \"c\" / \"system\" / \"system_algo.wasm\"\n\n    cfg = SystemConfig(\n        n_zones=3,\n        # Pressure loop config\n        p_sp0=1.5, p_spmin=1.0, p_spmax=3.0,\n        p_startup_delay_sec=900.0,\n        p_update_interval_sec=300.0,\n        p_ignore_req=0.2,\n        p_sp_trim=-0.05,\n        p_sp_respond=0.10,\n        p_sp_respond_max=0.25,\n        # SAT loop config\n        sat_sp0=55.0, sat_spmin=50.0, sat_spmax=65.0,\n        sat_startup_delay_sec=900.0,\n        sat_update_interval_sec=300.0,\n        sat_ignore_req=0.2,\n        sat_sp_trim=0.5,\n        sat_sp_respond=-1.0,\n        sat_sp_respond_max=-2.0,\n    )\n\n    algo = SystemAlgo(str(wasm_path), cfg)\n\n    zoneTemp       = [75.0, 74.0, 73.0]\n    zoneCoolingSpt = [72.0, 72.0, 72.0]\n    zoneDemand     = [50.0, 80.0, 95.0]\n    vavFlow        = [800.0, 900.0, 950.0]\n    vavFlowSpt     = [1000.0, 1000.0, 1000.0]\n    vavDamperCmd   = [60.0, 80.0, 95.0]\n\n    duct_sp = 1.5\n    sat_sp  = 55.0\n    oat     = 65.0\n    dt      = 60.0\n\n    print(\"t_s  duct_sp  sat_sp  coolRequests  pressureRequests\")\n    print(\"---- -------- ------- -------------- ----------------\")\n\n    t = 0.0\n    for _ in range(10):\n        cool, press, duct_sp, sat_sp = algo.update(\n            zoneTemp,\n            zoneCoolingSpt,\n            zoneDemand,\n            vavFlow,\n            vavFlowSpt,\n            vavDamperCmd,\n            dt_sec=dt,\n            is_imperial=True,\n            fanRun=1,\n            occupied=1,\n            current_pressure_sp=duct_sp,\n            current_sat_sp=sat_sp,\n            outside_air_temp=oat,\n            oat_min=40.0,\n            oat_max=90.0,\n        )\n        print(f\"{t:4.0f} {duct_sp:8.3f} {sat_sp:7.2f}  {cool}  {press}\")\n        t += dt\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\nThe following output shows a simulated VAV system with an AHU Trim and Respond control loop running in WebAssembly while Python feeds in zone telemetry, occupancy state, and timing. Each interval represents a one-minute timestep where the combined algorithm evaluates VAV cooling and pressure requests and determines whether the AHU should adjust its duct static pressure and supply air temperature setpoints. In this example, the individual VAV boxes begin generating meaningful cooling requests early in the sequence, while the AHU setpoints intentionally remain steady at 1.50 in. wc and 55°F. This behavior demonstrates the embedded Guideline-36 style startup and cadence logic working as intended: even though demand exists, the AHU holds its SPs during the configured startup stabilization period, accumulating operating time until both fan and occupancy conditions are satisfied for long enough to justify a coordinated reset action. This confirms that the combined VAV + AHU WASM block is accurately aggregating VAV requests while respecting AHU startup timing before issuing pressure and temperature reset responses.\n\n\n```bash\n$ python3 python/smoke_test.py \nt_s  duct_sp  sat_sp  coolRequests  pressureRequests\n---- -------- ------- -------------- ----------------\n   0    1.500   55.00  (0, 0, 1)  (0, 0, 1)\n  60    1.500   55.00  (2, 0, 1)  (0, 0, 1)\n 120    1.500   55.00  (2, 0, 1)  (0, 0, 1)\n 180    1.500   55.00  (2, 0, 1)  (0, 0, 1)\n 240    1.500   55.00  (2, 0, 1)  (0, 0, 1)\n 300    1.500   55.00  (2, 0, 1)  (0, 0, 1)\n 360    1.500   55.00  (2, 0, 1)  (0, 0, 1)\n 420    1.500   55.00  (2, 0, 1)  (0, 0, 1)\n 480    1.500   55.00  (2, 0, 1)  (0, 0, 1)\n 540    1.500   55.00  (2, 0, 1)  (0, 0, 1)\n ```\n\n## Future directions\n\nThe current implementation focuses on the airside (VAV box and AHU\ntrim \u0026 respond) logic from Guideline 36.  Possible extensions include:\n\n* **Web App For Testing WASM algorithms** — Visualize algorithm testing for engineers.\n\n* **Central plant reset** — Implement chilled water and hot water\n  temperature resets based on aggregated AHU requests.\n* **Optimal start** — Convert the various ML models in the\n  `open‑hvac‑optimization` project to compiled kernels (.so or .wasm)\n  with fixed coefficients.\n* **Auto‑generated GitHub Releases** — Use GitHub Actions to build\n  the `.wasm` modules on each tag and attach them to a release,\n  similar to how Niagara shares `.bog` files.\n\nContributions are welcome!  See the tests and code for guidance on\nmaintaining API stability and deterministic behaviour.\n\n---\n\n## 📜 License\n\nEverything here is **MIT Licensed** — free, open source, and made for the BAS community.  \nUse it, remix it, or improve it — just share it forward so others can benefit too. 🥰🌍\n\n\n【MIT License】\n\nCopyright 2025 Ben Bartling\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbbartling%2Fopen-hvac-optimization","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbbartling%2Fopen-hvac-optimization","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbbartling%2Fopen-hvac-optimization/lists"}