Add or remove devices from a Hubitat app instance (e.g., Maker API)
Add or remove devices from an installed app's device input. Primary use case: managing Maker API's device list for automated testing.
A single POST to /installedapp/update/json with _action_update=Done saves the app configuration. The settings[{deviceInput}] field contains the comma-separated list of device IDs — include the new device for add, exclude it for remove. All other settings must be echoed back with their type metadata.
Read .hubitat.json from the project root. Parse the multi-hub config:
$ARGUMENTS starts with @hubname (e.g., @myhub add 42). If so, use that hub name and strip the @hubname from arguments before further parsing. Otherwise, use default_hub.hubs[hubname] to get hub_ip and maker_api.app_id.username and password (non-null), it has hub security enabled. Authenticate first:
curl -s -c /tmp/hubitat_cookies_{hubname} -X POST "http://{hub_ip}/login" \
-d "username={username}&password={password}"
Then add -b /tmp/hubitat_cookies_{hubname} to all subsequent curl commands for this hub.Parse $ARGUMENTS (after stripping any @hubname):
add {device_id} — add device to Maker API (uses maker_api.app_id)remove {device_id} — remove device from Maker APIadd {device_id} {installed_app_id} — add device to a specific app instanceremove {device_id} {installed_app_id} — remove device from a specific app instancelist [{installed_app_id}] — show devices currently in the appIf no installed app ID is given, default to maker_api.app_id from the config.
curl -s "http://{hub_ip}/installedapp/configure/json/{installedAppId}"
From the JSON response, extract:
app.version — config version (usually 1)app.label — the app's display labelconfigPage.name — page name (usually mainPage)configPage.sections[].input[] — all input definitions (each has name, type, multiple)configPage.sections[].body[] — body elements; find any with element: "label" (these are label inputs)settings — current values for all inputstype starts with capability. (e.g., pickedDevices with type capability.*)The settings value for a device input is either:
{"12": "Device Name", "14": "Other"} — keys are device IDsShow the app name, device input name, and a table of current device IDs and names. Then stop.
Use Python (python3) with urllib.request and urllib.parse.
Build form fields as a list of tuples (preserves order, allows empty-string keys):
| Field | Value |
|---|---|
_action_update | Done |
formAction | update |
id | installed app ID (string) |
version | app.version (string) |
appTypeId | empty string |
appTypeName | empty string |
currentPage | page name from config |
pageBreadcrumbs | %5B%5D |
For each body element with element: "label":
| Field | Value |
|---|---|
{name}.type | text |
{name} | value from app.label (NOT from settings) |
For each input from configPage.sections[].input[], in order:
| Field | Value |
|---|---|
{name}.type | the input's type |
{name}.multiple | true or false (lowercase string) |
Then, if the input is a bool type:
| Field | Value |
|---|---|
checkbox[{name}] | on |
Then, if this is the device input (type starts with capability.):
| Field | Value |
|---|---|
settings[{name}] | comma-separated device IDs (see below) |
deviceList | the input name |
(empty string "") | empty string |
Otherwise (non-device inputs):
| Field | Value |
|---|---|
settings[{name}] | current value (see conversion rules below) |
settings[{deviceInput}]| Field | Value |
|---|---|
referrer | http://{hub_ip}/installedapp/list |
url | http://{hub_ip}/installedapp/configure/{id}/{pageName} |
_cancellable | false |
When reading settings values for the form body:
"true", "false", "some text") → use as-is[] (the string [])body = urllib.parse.urlencode(fields)
req = urllib.request.Request(
f"http://{hub_ip}/installedapp/update/json",
data=body.encode(),
headers={"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}
)
resp = urllib.request.urlopen(req)
Expected success response: {"status":"success","location":"/installedapp/list"}
Fetch the config again and check settings.{deviceInputName} to confirm:
Summarize: