VRF / HVAC
OmniBus speaks the native control bus of five VRF/HVAC families through one adapter and one config panel. Pick the brand when you add the port; all of them share the same indoor-unit table, the same control surface, and the same outputs. The adapter reads indoor-unit state from the bus — power, mode, fan, setpoint, room temperature, and fault codes — and, in control mode, writes those fields back to a single indoor unit.
This is the same job a CoolMaster or other VRF-to-BMS gateway does: tap the bus once, expose every indoor unit over MQTT / REST (and Sparkplug / BACnet / Modbus object servers), and drive setpoints from a supervisor.
Supported brands
| Brand | Bus | Recommended serial | Status |
|---|---|---|---|
| Samsung VRF / DVM-S | NASA / S-Net | 9600 8E1 | Bus decode and setpoint write hardware-verified |
| Daikin VRV / VRF | DIII-NET | 9600 8E2 | Decode scaffold — confirm against a live capture |
| Mitsubishi VRF | CN105 | 2400 8E1 | Polled, one indoor unit per port |
| Mitsubishi City Multi (whole site) | AE-200 / AE-50 / EW-50 / G-50 controller | — (Ethernet, no serial port) | Polled via the centralized controller — every indoor-unit group, no per-unit hardware |
| LG Multi V | LGAP | 4800 8N1 | Decode scaffold — confirm against a live capture |
| Toshiba / Carrier | TCC-Link | 9600 8E1 | Decode scaffold — confirm against a live capture |
The panel shows the recommended serial settings for the brand you picked; set them on the port. Match the bus exactly — VRF buses are parity-sensitive.
The Samsung DVM-S NASA bus is 9600 8E1 — eight data bits, even parity, one stop bit. With parity set to none the framing never locks and you decode zero frames; even parity is mandatory, not optional.
Samsung DVM-S decode and setpoint writes are confirmed on live hardware — the bus parses cleanly and a setpoint change is obeyed by the physical unit. The other brands ship as decode scaffolds: run them in sniffer/dry-run against your bus and confirm the decoded values look right before relying on them, and before ever switching control to live.
Reading state
Two read modes, depending on the brand:
- Sniffer (Samsung, Daikin, LG, Toshiba) — fully passive. Every frame on the bus is decoded; OmniBus never transmits, so it is safe to leave running on a live bus indefinitely.
- Poll (Mitsubishi CN105) — OmniBus actively sends read requests to one indoor unit on a point-to-point connector. Use this only when OmniBus is the sole controller on that connector; it never sends control writes in this mode.
Mitsubishi whole-site option (no serial port)
If the site has a Mitsubishi centralized controller (AE-200, AE-50, EW-50, or the older G-50/GB-50), OmniBus can talk to it directly over Ethernet instead of wiring each indoor unit: Add port → Mitsubishi VRF, enter the controller's IP, and every indoor-unit group on the controller appears in the same panel — including group names, and a per-group remote lockout control (prohibit/permit the local wall remotes) that the serial paths don't have. This replaces third-party "M-NET to Modbus" gateway boxes. Reads are polled politely (15 s minimum interval — the controller tolerates only gentle polling); control follows the same dry-run-first safety as every VRF brand.
The centralized controller's XML interface has no authentication — anything on its LAN can command the HVAC plant. Keep the controller on a protected management network and let OmniBus (which does authenticate, encrypt, and audit) be the thing that fronts it.
Once the bus is decoding, the port card lists every discovered indoor unit with its last-known state:
| Column | Meaning |
|---|---|
| Unit | Bus address (e.g. 20.0.1) |
| Name | Friendly label — editable inline (see Friendly names) |
| Power | On / off |
| Mode | auto / cool / dry / fan / heat |
| Fan | auto / low / mid / high (brand-dependent) |
| Set | Heat/cool setpoint |
| Current | Room temperature |
| Err | Fault code, or — |
A °F / °C toggle in the Indoor units header picks the temperature display
unit for the Set and Current columns (and the column header reads
Set °F or Set °C to match). It defaults to °F and is remembered per
browser, so it survives reloads. This is a display-and-entry preference only —
the bus always carries °C. The /action/status REST read reports each unit in
°C (setpointC, roomTempC) and adds whole-degree °F mirror fields
(setpointF, roomTempF) for convenience. The MQTT <prefix>/idu/<unit>/state
payload and the Sparkplug / BACnet / Modbus object points stay °C-only — one
canonical unit per point.
Each unit's normalized state is also republished to
<prefix>/idu/<unit>/state over MQTT.
Controlling units
Switch Mode to Control to add a write path. The indoor-unit table becomes editable: each row's Power, Mode, Fan, and Set turn into inline controls. Changing one sends a write to that unit — there is no separate "send" form. Type the setpoint in whatever unit the °F / °C toggle is set to (whole degrees 60–86 in °F, half-degree steps 16–30 in °C); OmniBus converts to °C for the bus. Setpoint commits when you press Enter or leave the field, and only when the value actually changed.
A field you just set is held (highlighted) until the unit re-broadcasts the new value on the bus, so the cell does not snap back to the old reading during the poll interval between your write and the unit's echo.
You can also drive a unit from outside the UI:
- REST —
POST /action/setwith{ "unit": "20.0.1", "changes": { "power": true, "mode": "cool", "setpointC": 22.5, "fanSpeed": "high" } }. - MQTT — publish the same
{ "unit": …, "changes": … }object to<prefix>/set.
The setpoint is °C (setpointC). To specify it in Fahrenheit instead, send
setpointF (a whole-degree value) and OmniBus converts it to °C before building
the frame — send one or the other, not both, or the write is rejected. All other
fields (power, mode, fanSpeed) are unchanged.
In dry-run, both return the frame OmniBus would send (as hex) without transmitting.
On Samsung, the setpoint write is verified end-to-end; power, mode, and fan writes use the same frame structure but have not yet been confirmed against hardware. Keep control in dry-run and diff the generated frame against a captured command before switching those writes to live.
Control safety
Control mode adds a write path, but it starts in dry-run — every "send" builds the frame and logs/returns it without touching the bus. Confirm the decode in sniffer/poll mode first, diff a built frame against a real captured command, then flip Transmit safety to Live on the port card to actually drive equipment. Verify behavior on one unit before trusting the bus.
REST integration (BMS / SCADA)
Everything the panel does is available over the REST API, so a supervisor (GEM, Ignition, a custom BMS driver) can read indoor-unit state and write control without the UI. See REST API for auth; the VRF-specific contract is below.
:path is the URL-encoded device path, e.g. %2Fdev%2FttyUSB0.
| Action | Endpoint | Auth | Body |
|---|---|---|---|
| status | POST /api/ports/:path/rest/status | admin token | {} |
| stats | POST /api/ports/:path/rest/stats | admin token | {} |
| set | POST /api/ports/:path/rest/set | admin token | { "unit": "20.2.4", "changes": { … } } |
All per-port rest/* actions are POST and require an admin token — even the
read-only status/stats — so an integrator polls with an admin account.
status — read every indoor unit
Returns the adapter's current snapshot. units[].fields is sparse — only
what has been decoded so far appears, so never assume a field is present.
{
"brand": "samsung-vrf", "mode": "sniffer", "dryRun": true, "controller": "50.0.0",
"framesSeen": 1240, "framesDecoded": 980, "crcFail": 0, "parseErrors": 0,
"units": [
{
"unit": "20.2.4", "name": "Gym",
"firstSeen": 1700000000000, "lastSeen": 1700000004200,
"fields": {
"power": true, "mode": "cool", "modeReal": "cool",
"fanSpeed": "mid", "fanReal": 14,
"setpointC": 20, "roomTempC": 19.7, "setpointF": 68, "roomTempF": 68,
"swingV": false, "swingH": false, "thermoOn": false,
"humidity": 74, "capacity": 0, "errorCode": 0
}
}
]
}
Field meanings: power (bool), mode/modeReal (auto|cool|dry|fan|heat),
fanSpeed/fanReal (auto|low|mid|high|turbo; *Real is the unit's actual
running value and may be an unmapped raw number), setpointC/roomTempC (°C,
canonical), setpointF/roomTempF (whole-°F mirrors, read convenience only),
swingV/swingH/thermoOn (bool), humidity/capacity (int), errorCode
(0 = no fault).
There is no on-demand bus read: status returns the last state decoded from the
unit's own broadcasts. Poll this endpoint on an interval (2–5 s is plenty)
and use each unit's lastSeen (epoch ms) to detect stale / offline units. A unit
that hasn't broadcast in, say, a few minutes is likely off the bus. Writes, by
contrast, take effect immediately.
set — write one indoor unit
{ "unit": "20.2.4", "changes": { "power": true, "mode": "cool", "fanSpeed": "high", "setpointC": 22.5 } }
unit— the bus address ("20.2.4") or a configured friendly name ("Gym").changes— any ofpower(bool),mode,fanSpeed(enum strings above),setpointC(°C). To set in Fahrenheit sendsetpointF(whole °F) instead ofsetpointC— the server converts; sending both is rejected.
Response (dry-run echoes the frame it would send; live also queues it):
{ "ok": true, "dryRun": true, "unit": "20.2.4",
"changes": { "setpointF": 68 }, "packetNumber": 42,
"hex": "320012500000200204c0132a01420100c8...34" }
Errors: 409 if the port is not in control mode; 400 for a
non-writable field, a setpointC+setpointF pair, a non-numeric setpointF, or
a missing unit/changes.
TOKEN=$(curl -sk -X POST https://omnibus.local/api/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"…"}' | jq -r .token)
# read all units
curl -sk -X POST "https://omnibus.local/api/ports/%2Fdev%2FttyUSB0/rest/status" \
-H "Authorization: Bearer $TOKEN" -d '{}'
# set the gym to 70 °F (server converts to °C)
curl -sk -X POST "https://omnibus.local/api/ports/%2Fdev%2FttyUSB0/rest/set" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"unit":"20.2.4","changes":{"setpointF":70}}'
Friendly names
Each indoor unit is addressed by its raw bus ID (e.g. 20.0.1). To make a unit
easy to find on the floor, type a friendly label (e.g. Gym, Lobby FCU)
directly into its Name cell in the Indoor units table; the name commits
when you leave the field, and clearing it removes the label. The name also
drives the object/metric names a supervisor sees — a labelled unit surfaces as
lobby-fcu/setpoint instead of 20.0.1/setpoint over Sparkplug and the
BACnet / Modbus object servers.
Outputs
| Output | Behavior |
|---|---|
| MQTT | Publishes decoded indoor-unit state to <prefix>/idu/<unit>/state; subscribes to <prefix>/set for control writes (see Controlling units). |
| REST | /action/status (discovered units + state), /action/stats (frame counters + TX queue depth), /action/set (control-mode write). |
| Webhooks | Fan out decoded state and control events. |
| TCP | Not bridged to the bus — VRF control goes through REST/MQTT set, not raw TCP bytes. |
With Sparkplug or a BACnet / Modbus object server enabled, each recognized field of every indoor unit is exposed as a named point, using the friendly name from its Name cell where one is set.