# Hubitat - Maker Integration

**URL:** <https://forum.inductiveautomation.com/t/hubitat-maker-integration/110387>\
**Category:** Maker\
**Tags:** perspective\
**Created:** [September 4, 2025, 5:47pm UTC](https://forum.inductiveautomation.com/t/hubitat-maker-integration/110387 "2025-09-04T17:47:33Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![jkelleyus](https://avatars.discourse-cdn.com/v4/letter/j/3bc359/32.png) [@jkelleyus](https://forum.inductiveautomation.com/u/jkelleyus)\
**Post date:** [September 4, 2025, 5:47pm UTC](https://forum.inductiveautomation.com/t/hubitat-maker-integration/110387/1 "2025-09-04T17:47:33Z")

</div>

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](#0-prerequisites)

- [1) Hubitat → Maker API](#1-hubitat--maker-api)

- [2) Ignition → Project Library (`hubitat.py`)](#2-ignition--project-library-hubitatpy)

- [3) Ignition → WebDev endpoint (`events_post`)](#3-ignition--webdev-endpoint-events_post)

- [4) Point Hubitat at Ignition (Maker API URL)](#4-point-hubitat-at-ignition-maker-api-url)

- [5) Perspective: minimal control + status](#5-perspective-minimal-control--status)

- [6) Quick test plan](#6-quick-test-plan)

- [7) Security notes](#7-security-notes)

- [8) Troubleshooting](#8-troubleshooting)

- [Appendix A: DeviceCard (hides slider if `level` missing)](#appendix-a-devicecard-hides-slider-if-level-missing)

* * *

## 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

1. In **Apps → Maker API** :

2. Note these values (we’ll use placeholders below):

```python
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

- Turn a switch on/off

- Set a dimmer level

* * *

## 2) Ignition → Project Library (`hubitat.py`)

Create **Project → Scripting → Project Library → New Script** named **`hubitat.py`** :

```python
# 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>/…`.

1. In **Web Dev** , create a **mount** called `hubitat`.

2. Inside it, create a **New Python Resource** named **`events_post`**.

**Paste this complete `doPost`:**

```python
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:

```python
http://<IGNITION_IP>:8088/system/webdev/<PROJECT_NAME>/hubitat/events_post?k=<STRONG_SHARED_KEY>

```

Example:

```python
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** :

```python
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:

- **Write** (send once on release): **onActionPerformed** :

### Status labels

- Switch state: `[default]Hubitat/1/switch` → transform:

- Last event timestamp: `[default]Hubitat/1/_lastUpdate`

* * *

## 6) Quick test plan

1. **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.)

2. **Send a synthetic POST** (from Ignition Script Console):

3. **Flip a real device** in Hubitat and watch the tags update in Ignition.

4. **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** and `self.props.value` (not `currentValue`).

- **Tag quality issues** : confirm your provider name is `default` (or change `PROVIDER` 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:**

1. **Name** (Label → `props.text`)  
`[default]Hubitat/{view.params.deviceId}/displayName`  
Transform:

```python
return value or ("Device " + str(self.view.params.deviceId))

```

1. **State text** (Label → `props.text`)  
`[default]Hubitat/{view.params.deviceId}/switch`  
Transform:

```python
s = (value or "").lower()
return "ON" if s=="on" else "OFF" if s=="off" else (value or "unknown")

```

1. **On/Off buttons** (onActionPerformed):

```python
# On
import hubitat
hubitat.send_command(self.view.params.deviceId, "on")

```

```python
# Off
import hubitat
hubitat.send_command(self.view.params.deviceId, "off")

```

1. **Slider** (min 0, max 100, step 1)  
Readback (props.value): `[default]Hubitat/{view.params.deviceId}/level`  
Transform:

```python
try: return int(value)
except: return 0

```

Send (onActionPerformed):

```python
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):

```python
return value is not None

```

1. **Last update** (Label → `props.text`)  
`[default]Hubitat/{view.params.deviceId}/_lastUpdate`  
Transform:

```python
return "Last update: " + (value or "")

```

**Usage:** Drop an **Embedded View** and set:

```python
{ "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._
