All, I use Ignition regularly at work but wanted to try a Hubitat - Maker integration. There’s a lot of disparate information on the various forums about this but it all seems a bit convoluted and sometimes old.
I have a working integration and wanted to share, so I asked ChatGPT to assist with a guide. Here’s what I came up with…feel free to give it a try and share your own improvements.
Hubitat ↔ Ignition (Maker API Only) — End-to-End Setup
Goal: Control Hubitat devices from Ignition Perspective and get push status updates into Ignition without MQTT or polling.
Works with: Hubitat Maker API (LAN) → Ignition 8.1.x WebDev + Perspective.
Table of Contents
0) Prerequisites
-
Hubitat hub on your LAN with the Maker API app installed.
-
Ignition 8.1.x with WebDev + Perspective modules.
-
Hubitat and Ignition can reach each other over the network (same subnet is easiest).
1) Hubitat → Maker API
-
In Apps → Maker API:
-
Select the devices you want to control/monitor.
-
Enable Allow Access via Local IP Address.
-
(Optional) Enable control of modes/HSM if you need it.
-
Click Create New Access Token (copy it).
-
-
Note these values (we’ll use placeholders below):
HUB_IP = <HUB_IP> # e.g. 192.168.1.238
APP_ID = <APP_ID> # e.g. 307
TOKEN = <TOKEN> # e.g. c7720...6dee
Quick browser checks (sub in your values):
-
All devices
http://<HUB_IP>/apps/api/<APP_ID>/devices?access_token=<TOKEN>
-
Turn a switch on/off
http://<HUB_IP>/apps/api/<APP_ID>/devices/<DEVICE_ID>/on?access_token=<TOKEN> http://<HUB_IP>/apps/api/<APP_ID>/devices/<DEVICE_ID>/off?access_token=<TOKEN>
-
Set a dimmer level
http://<HUB_IP>/apps/api/<APP_ID>/devices/<DEVICE_ID>/setLevel/55?access_token=<TOKEN>
2) Ignition → Project Library (hubitat.py
)
Create Project → Scripting → Project Library → New Script named hubitat.py
:
# hubitat.py (Project Library)
HUB_IP = "<HUB_IP>"
APP_ID = "<APP_ID>"
TOKEN = "<TOKEN>"
def _base():
return "http://%s/apps/api/%s" % (HUB_IP, APP_ID)
def send_command(device_id, command, arg=None):
"""
Examples:
send_command("1", "on")
send_command("1", "off")
send_command("2", "setLevel", "55")
"""
import system
path = command if arg is None else "%s/%s" % (command, arg)
url = "%s/devices/%s/%s?access_token=%s" % (_base(), str(device_id), path, TOKEN)
try:
resp = system.net.httpGet(url, connectTimeout=5000, readTimeout=7000)
return {"ok": True, "response": resp}
except Exception as ex:
return {"ok": False, "error": str(ex), "url": url}
This is what your Perspective buttons/sliders will call.
3) Ignition → WebDev endpoint (events_post
)
Why: Hubitat will POST each device event to Ignition. We decode and write tags under [default]Hubitat/<deviceId>/…
.
-
In Web Dev, create a mount called
hubitat
. -
Inside it, create a New Python Resource named
events_post
.-
Method: doPost
-
Enabled:
-
Paste this complete doPost
:
def doPost(request, session):
import system, re
SHARED_KEY = "<STRONG_SHARED_KEY>" # any strong random string; see step 4
PROVIDER = "default" # your writable tag provider
baseProv = u"[%s]" % PROVIDER
# --- auth ---
params = request.get("params", {}) or {}
k = params.get("k")
if isinstance(k, list): k = k[0] if k else ""
if SHARED_KEY and (k or "") != SHARED_KEY:
return {"status": 401, "type": "application/json",
"json": {"ok": False, "err": "Unauthorized"}}
# helpers
def _s(x):
try: return x if isinstance(x, basestring) else unicode(x)
except: return u""
def _first(p, key):
v = p.get(key)
return v[0] if isinstance(v, list) and v else v
def _lenient_decode(s):
if not isinstance(s, basestring):
return None
t = s.strip()
if not (t.startswith("{") and t.endswith("}")):
return None
t = re.sub(r"u'(.+?)'", r"'\1'", t)
t = re.sub(r'(?<!\\)\'', '"', t)
try:
return system.util.jsonDecode(t)
except:
return None
# parse body (strict → lenient) or fallback to params
raw_json = request.get("json"); raw_pd = request.get("postData"); raw_dt = request.get("data")
evt = None
for cand in (raw_json, raw_pd, raw_dt):
if evt is not None: break
if cand is None: continue
if isinstance(cand, dict):
evt = cand; break
s = _s(cand)
if s:
try:
evt = system.util.jsonDecode(s.strip())
except:
evt = _lenient_decode(s)
if evt is None:
if _first(params, "deviceId") and _first(params, "name") and _first(params, "value"):
evt = {"deviceId": _first(params, "deviceId"),
"name": _first(params, "name"),
"value": _first(params, "value"),
"displayName": _first(params, "displayName") or ""}
if not evt:
# debug breadcrumbs if Hubitat isn't sending JSON
dbg_folder = baseProv + "Hubitat/_debug"
system.tag.configure(baseProv, [{"name":"Hubitat","tagType":"Folder"}], "m")
system.tag.configure(baseProv+"Hubitat", [{"name":"_debug","tagType":"Folder"}], "m")
system.tag.configure(dbg_folder, [
{"name":"_lastParams","tagType":"AtomicTag","dataType":"String"},
{"name":"_lastData", "tagType":"AtomicTag","dataType":"String"},
{"name":"_lastPD", "tagType":"AtomicTag","dataType":"String"},
{"name":"_lastTime", "tagType":"AtomicTag","dataType":"String"},
], "m")
system.tag.writeBlocking(
[dbg_folder+"/_lastParams", dbg_folder+"/_lastData", dbg_folder+"/_lastPD", dbg_folder+"/_lastTime"],
[system.util.jsonEncode(params),
_s(raw_dt)[:1000] if raw_dt else u"",
_s(raw_pd)[:1000] if raw_pd else u"",
system.date.format(system.date.now(), "yyyy-MM-dd HH:mm:ss")]
)
return {"status": 200, "type": "application/json",
"json": {"ok": False, "err": "Missing JSON"}}
# unwrap common Hubitat shapes
if isinstance(evt, dict):
if isinstance(evt.get("content"), dict):
evt = evt["content"]
elif isinstance(evt.get("event"), dict):
evt = evt["event"]
elif isinstance(evt.get("events"), list) and evt["events"]:
evt = evt["events"][0]
deviceId = _s(evt.get("deviceId") or evt.get("id") or evt.get("device") or "unknown")
attr = _s(evt.get("name") or evt.get("attribute") or "unknown")
value = _s(evt.get("value") or evt.get("val") or "")
displayName = _s(evt.get("displayName") or evt.get("deviceName") or evt.get("label") or "")
# canonical tree by deviceId
hub_folder = baseProv + "Hubitat"
dev_folder = hub_folder + "/" + deviceId
system.tag.configure(baseProv, [{"name":"Hubitat","tagType":"Folder"}], "m")
system.tag.configure(hub_folder, [{"name":deviceId,"tagType":"Folder"}], "m")
system.tag.configure(dev_folder, [
{"name":"displayName", "tagType":"AtomicTag","dataType":"String"},
{"name":"_lastEventJson", "tagType":"AtomicTag","dataType":"String"},
{"name":"_lastUpdate", "tagType":"AtomicTag","dataType":"String"},
{"name":attr, "tagType":"AtomicTag","dataType":"String"},
], "m")
system.tag.writeBlocking(
[dev_folder+"/displayName",
dev_folder+"/_lastEventJson",
dev_folder+"/_lastUpdate",
dev_folder+"/"+attr],
[displayName,
system.util.jsonEncode(evt),
system.date.format(system.date.now(), "yyyy-MM-dd HH:mm:ss"),
value]
)
# optional: keep a simple byId index for UI use
idx_path = hub_folder + "/_deviceIndexJson"
system.tag.configure(hub_folder, [{"name":"_deviceIndexJson","tagType":"AtomicTag","dataType":"String"}], "m")
cur = system.tag.readBlocking([idx_path])[0].value
try:
idx = system.util.jsonDecode(cur) if (cur and isinstance(cur, basestring)) else {}
except:
idx = {}
if not isinstance(idx, dict):
idx = {}
byId = idx.get("byId") if isinstance(idx.get("byId"), dict) else {}
if byId.get(deviceId) != displayName:
byId[deviceId] = displayName
idx["byId"] = byId
system.tag.writeBlocking([idx_path], [system.util.jsonEncode(idx)])
return {"status": 200, "type": "application/json",
"json": {"ok": True, "deviceId": deviceId, "attr": attr, "value": value}}
4) Point Hubitat at Ignition (Maker API URL)
In Maker API, set “URL to POST device events to” to:
http://<IGNITION_IP>:8088/system/webdev/<PROJECT_NAME>/hubitat/events_post?k=<STRONG_SHARED_KEY>
Example:
http://192.168.1.54:8088/system/webdev/HomeAutomation/hubitat/events_post?k=p7s9Q-2K1-ABCD
Save/publish the Ignition project and ensure the WebDev resource is Enabled.
5) Perspective: minimal control + status
Switch ON/OFF buttons (device 1
)
Button → onActionPerformed:
import hubitat
hubitat.send_command("1", "on") # or "off"
Dimmer slider (device 2
)
Slider → props.min=0
, max=100
, step=1
-
Readback: bind
props.value
(Indirect Tag)
[default]Hubitat/2/level
→ transform:try: return int(value) except: return 0
-
Write (send once on release): onActionPerformed:
import hubitat val = int(self.props.value) hubitat.send_command("2", "setLevel", str(val))
Status labels
-
Switch state:
[default]Hubitat/1/switch
→ transform:s = (value or "").lower() return "ON" if s=="on" else "OFF" if s=="off" else (value or "unknown")
-
Last event timestamp:
[default]Hubitat/1/_lastUpdate
6) Quick test plan
-
Health check (browser):
http://<IGNITION_IP>:8088/system/webdev/<PROJECT_NAME>/hubitat/events_post
(GET will return 405; that’s fine. We only accept POST.) -
Send a synthetic POST (from Ignition Script Console):
import system url = "http://<IGNITION_IP>:8088/system/webdev/<PROJECT_NAME>/hubitat/events_post?k=<STRONG_SHARED_KEY>" body = '{"deviceId":"1","name":"switch","value":"on","displayName":"Living Room Ceiling Fan"}' print system.net.httpPost(url, "application/json", body)
You should see tags appear under
[default]Hubitat/1/...
. -
Flip a real device in Hubitat and watch the tags update in Ignition.
-
Press Perspective buttons/slider; the device should respond.
7) Security notes
-
Treat TOKEN and SHARED_KEY like passwords.
-
Use a strong shared key and keep it in the URL’s
?k=...
. -
Limit Maker API to the devices you actually need.
-
On your firewall, restrict Ignition’s port 8088 to the Hubitat IP if possible.
-
If you ever pasted a real TOKEN while testing, re-generate it in Maker API.
8) Troubleshooting
-
Unauthorized from WebDev:
?k=...
missing or wrong key. -
Missing JSON response: Hubitat isn’t hitting Ignition; double-check the URL/IP or Maker API isn’t configured to POST.
-
No tags: ensure
events_post
is doPost, Enabled, and your project is Saved/Published. -
Perspective error
currentValue
undefined: In Perspective, use onActionPerformed andself.props.value
(notcurrentValue
). -
Tag quality issues: confirm your provider name is
default
(or changePROVIDER
in the script). -
Device not updating: make sure it’s selected in Maker API.
Appendix A: DeviceCard (hides slider if level
missing)
Reusable view: components/DeviceCard
Param: deviceId
(String)
Layout: Flex (row; gap 8–12; align center)
Components & bindings:
- Name (Label →
props.text
)
[default]Hubitat/{view.params.deviceId}/displayName
Transform:
return value or ("Device " + str(self.view.params.deviceId))
- State text (Label →
props.text
)
[default]Hubitat/{view.params.deviceId}/switch
Transform:
s = (value or "").lower()
return "ON" if s=="on" else "OFF" if s=="off" else (value or "unknown")
- On/Off buttons (onActionPerformed):
# On
import hubitat
hubitat.send_command(self.view.params.deviceId, "on")
# Off
import hubitat
hubitat.send_command(self.view.params.deviceId, "off")
- Slider (min 0, max 100, step 1)
Readback (props.value):[default]Hubitat/{view.params.deviceId}/level
Transform:
try: return int(value)
except: return 0
Send (onActionPerformed):
import hubitat
val = int(self.props.value)
hubitat.send_command(self.view.params.deviceId, "setLevel", str(val))
Hide when device has no level
(bind props.visible
to same level tag, transform):
return value is not None
- Last update (Label →
props.text
)
[default]Hubitat/{view.params.deviceId}/_lastUpdate
Transform:
return "Last update: " + (value or "")
Usage: Drop an Embedded View and set:
{ "path": "components/DeviceCard", "params": { "deviceId": "2" } }
Duplicate for "3"
, "8"
, etc. The slider will auto-hide on devices without level
.
Credits / Thanks: guide assembled with help from the community.
Replace all placeholders (<HUB_IP>
, <APP_ID>
, <TOKEN>
, <PROJECT_NAME>
, <STRONG_SHARED_KEY>
) before use.