From d650b8bff9ae899dcbbf0e64ac1c40d23249d63e Mon Sep 17 00:00:00 2001 From: CliveS via Highsteads Date: Sun, 10 May 2026 14:35:55 +0100 Subject: [PATCH 1/3] docs: cover undocumented Indigo state-ID and plugin-host rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five small additions to the plugin-dev concepts documenting rules I hit while building a Zigbee2MQTT bridge plugin (capture-all dynamic states). All come from real LowLevelBadParameterError or AttributeError failures with no upstream documentation. concepts/devices.md + State ID naming rules — must be camelCase ASCII, no underscores (despite XML allowing them), no non-ASCII letters. Includes a snake_case -> camelCase sanitiser and a strict validator. + Reserved state names — don't shadow native device properties; in particular `batteryLevel` silently routes writes to the native int property instead of Custom States. Use `battery` (Integer). + Dynamic state declaration — three subtle rules for plugins overriding getDeviceStateList(): 1. The parent's list is a LIVE reference; mutating it permanently corrupts subsequent reads (must shallow-copy). 2. dev.pluginProps keys via replacePluginPropsOnServer cannot start with underscore (distinct from self.pluginPrefs). 3. Roll back pluginProps on stateListOrDisplay failure to avoid a sticky bad-key state. + deviceUpdated self-loop guard — when a plugin uses subscribeToChanges() AND writes its own device states, the guard must check pluginId (not id) at the top of deviceUpdated. concepts/events.md + Common-mistake section warning that `indigo.server.fireEvent()` and `self.triggerEvent()` both look correct but raise AttributeError. Use the triggerStartProcessing / triggerStopProcessing lifecycle plus indigo.trigger.execute(trigger_object). concepts/actions.md + uiPath="..." attribute — must be PascalCase with no spaces; spaces cause NSInternalInconsistencyException in the Indigo client (confirmed 2026-04-30). + Calling other plugins' actions — Pushover (action ID is "send", not "sendPushover"; msg* prop names; priority is a string) and Email+ (use indigo.server.sendEmailTo() directly; executeAction "sendEmail" silently drops the props dict during cross-plugin serialization). concepts/plugin-preferences.md + Counter-warning that the existing "prefix hidden prefs with underscore" advice applies to self.pluginPrefs ONLY — device- level dev.pluginProps keys cannot start with `_` (writes via replacePluginPropsOnServer are XML-validated and reject them). troubleshooting/common-issues.md + LowLevelBadParameterError section linking back to the state-ID rules in concepts/devices.md, since the error message doesn't identify which key is bad. --- docs/plugin-dev/concepts/actions.md | 71 +++++++++ docs/plugin-dev/concepts/devices.md | 143 ++++++++++++++++++ docs/plugin-dev/concepts/events.md | 40 +++++ .../plugin-dev/concepts/plugin-preferences.md | 24 ++- .../troubleshooting/common-issues.md | 14 ++ 5 files changed, 291 insertions(+), 1 deletion(-) diff --git a/docs/plugin-dev/concepts/actions.md b/docs/plugin-dev/concepts/actions.md index b2056aa..36ddbaa 100644 --- a/docs/plugin-dev/concepts/actions.md +++ b/docs/plugin-dev/concepts/actions.md @@ -332,6 +332,77 @@ def actionControlUniversal(self, action, dev): self.logger.info(f"sent \"{dev.name}\" status request") ``` +## `uiPath` attribute — PascalCase, no spaces + +When you group plugin actions under a sub-menu in Indigo's UI via +`uiPath="..."` on `` or ``, the value MUST be PascalCase +with no spaces and no punctuation. Spaces cause an +`NSInternalInconsistencyException` crash in the Indigo client (confirmed +2026-04-30 during SigenEnergyManager development). + +```xml + + + + + + + + + + +``` + +`uiPath="hidden"` is the documented reserved value that hides the action +from the user-visible Action picker (e.g. for actions only invoked from +plugin code via `executeAction`). + +## Calling other plugins' actions — known prop-name pitfalls + +`indigo.server.getPlugin(...)` lets you call actions on installed plugins, +but the action ID and prop names must match exactly what the target plugin +defines in its Actions.xml. Two patterns trip people up because the +"obvious" action name is not what the plugin actually exposes: + +### Pushover (`io.thechad.indigoplugin.pushover`) + +```python +pushover = indigo.server.getPlugin("io.thechad.indigoplugin.pushover") +if pushover and pushover.isEnabled(): + pushover.executeAction("send", props={ # ← "send", NOT "sendPushover" + "msgTitle": "Subject", + "msgBody": "Message body", # required + "msgUser": PUSHOVER_USER_TOKEN, + "msgPriority": "0", # string: -2,-1,0,1,2 + "msgSound": "vibrate", # any Pushover sound name + }) +``` + +- Action ID is `"send"` (NOT `"sendPushover"` — that does not exist) +- Priority is a **string**, not an int +- Prop names are `msg*`-prefixed; bare `title`/`message` are ignored + +### Email+ (`com.indigodomo.email`) + +```python +# Correct — direct API call +indigo.server.sendEmailTo( + "recipient@example.com", + subject="Subject here", + body="Plain text or HTML body", +) + +# Wrong — props dict loses keys during cross-plugin serialization, +# emailMessage is silently dropped, email never sends +indigo.server.getPlugin("com.indigodomo.email").executeAction( + "sendEmail", + props={"emailAddress": "...", "emailSubject": "...", "emailMessage": "..."} +) +``` + +`indigo.server.sendEmailTo()` automatically uses the first configured +SMTP device — no need to look up the plugin or device ID. + ## Action Validation ```python diff --git a/docs/plugin-dev/concepts/devices.md b/docs/plugin-dev/concepts/devices.md index 522bef7..2f787fb 100644 --- a/docs/plugin-dev/concepts/devices.md +++ b/docs/plugin-dev/concepts/devices.md @@ -295,17 +295,160 @@ dev.deviceTypeId # Type ID from Devices.xml dev.enabled # Is device enabled? ``` +## State ID naming rules (undocumented but strict) + +Indigo's plugin host validates custom state IDs more strictly than XML or +Python identifiers permit. Violating any of these rules raises +`LowLevelBadParameterError -- illegal XML tag name character` from +`stateListOrDisplayStateIdChanged()` or `replacePluginPropsOnServer()`, +and the error message does **not** identify which key is bad. + +| Rule | OK | Not OK | +|---|---|---| +| Must start with an ASCII letter | `colorTemp`, `linkQuality` | `_internal`, `2state` | +| Body is ASCII letters and digits ONLY | `colorTempStartup`, `motionSensitivity` | `color_temp_startup`, `temp-c`, `state.foo` | +| Underscores are **forbidden** despite XML allowing them | `lastSeen` | `last_seen` | +| Non-ASCII letters are forbidden despite `str.isalnum()` accepting them | `notifie` | `notifié` | + +Convert MQTT/JSON-style snake_case to camelCase before declaring states: + +```python +def _sanitise_state_key(key): + """color_temp_startup -> colorTempStartup""" + parts = [] + cur = [] + for c in key: + if c.isascii() and c.isalnum(): + cur.append(c) + else: + if cur: parts.append("".join(cur)); cur = [] + if cur: parts.append("".join(cur)) + if not parts: return "" + sk = parts[0][0].lower() + parts[0][1:] + "".join(p[:1].upper() + p[1:] for p in parts[1:]) + if not sk[0].isalpha(): + sk = "z" + sk[:1].upper() + sk[1:] # force ASCII-letter start + return sk +``` + +Strict validator (use before every `updateStateOnServer`): + +```python +def _is_valid_state_id(key): + if not key or not key[0].isascii() or not key[0].isalpha(): + return False + return all(c.isascii() and c.isalnum() for c in key) +``` + +## Reserved state names — don't shadow native device properties + +Indigo has reserved property names on device objects (e.g. `device.batteryLevel`). +If a plugin declares a custom state with the same name, Indigo silently routes +`updateStateOnServer()` writes to the **native property** instead of Custom States. +The state never appears in the Custom States panel and no error is raised. + +Known reserved names to avoid as custom state IDs: + +- `batteryLevel` — use `battery` (with `Integer` type) instead + +The reservation hides bugs that look like "my plugin isn't writing the state" +when actually the write succeeded into the wrong slot. Use `Integer` rather +than `Number` for whole-number percentages so the Custom States panel renders +the value correctly. + +## Dynamic state declaration — three subtle rules + +When overriding `getDeviceStateList(dev)` to advertise states beyond what's in +Devices.xml (typical pattern: capture-all sensor plugins, MQTT/HA bridges): + +### 1. The parent's list is a LIVE reference, not a copy + +`indigo.PluginBase.getDeviceStateList(self, dev)` returns the parser's +**internal cache** for that device type — not a fresh list. Appending to it +permanently corrupts subsequent reads: every call accumulates more duplicates, +and eventually Indigo's XML serialiser blows up. The error looks like a +random "illegal XML tag name character" failure that gets worse over time. + +Always work on a shallow copy: + +```python +def getDeviceStateList(self, dev): + state_list = list(indigo.PluginBase.getDeviceStateList(self, dev) or []) + # ...append dynamic state dicts to state_list, not to the parent return value + return state_list +``` + +### 2. `dev.pluginProps` keys cannot start with underscore + +Indigo's XML serialiser rejects `dev.replacePluginPropsOnServer({"_seenKeys": ...})` +with `LowLevelBadParameterError`. This is **distinct** from `self.pluginPrefs` +(plugin-level prefs written via direct dict assignment) — those accept +underscore-prefixed keys fine. Only **device-level** pluginProps written via +`replacePluginPropsOnServer` are strict. + +```python +# Bad — replacePluginPropsOnServer fails +new_props["_dynamicKeys"] = ",".join(seen) +dev.replacePluginPropsOnServer(new_props) + +# Good +new_props["dynamicKeys"] = ",".join(seen) +dev.replacePluginPropsOnServer(new_props) +``` + +### 3. Roll back pluginProps on stateListOrDisplay failure + +When you persist a new state name in pluginProps and then call +`stateListOrDisplayStateIdChanged()`, the latter can fail (e.g. the new name +hits an undocumented validation rule). The pluginProps write has already +committed though — so on failure you should restore the prior value, otherwise +every subsequent message fails the same way: + +```python +seen_csv_before = dev.pluginProps.get("dynamicKeys", "") +try: + new_props = dict(dev.pluginProps) + new_props["dynamicKeys"] = ",".join(sorted(seen_after)) + dev.replacePluginPropsOnServer(new_props) + indigo.devices[dev.id].stateListOrDisplayStateIdChanged() +except Exception: + rollback = dict(dev.pluginProps) + rollback["dynamicKeys"] = seen_csv_before + dev.replacePluginPropsOnServer(rollback) + raise +``` + +## `deviceUpdated` self-loop guard + +If your plugin calls `indigo.devices.subscribeToChanges()` AND also writes +states on its own devices, every state write fires `deviceUpdated()` again — +infinite loop unless guarded. + +The guard MUST be at the very top of `deviceUpdated()` and check `pluginId`, +not `id`. A per-device id check is not sufficient if the plugin manages more +than one device — it doesn't prevent A→B→A→B cross-device loops. + +```python +def deviceUpdated(self, origDev, newDev): + super().deviceUpdated(origDev, newDev) + if newDev.pluginId == self.pluginId: + return # ignore our own device updates + # ...rest of the handler +``` + ## Best Practices ### State Design - Use descriptive state IDs: `temperatureSensor1` not `temp1` - Choose appropriate value types for your data - Set `UiDisplayStateId` to most important state +- camelCase ASCII only — no underscores, no non-ASCII letters +- Don't reuse reserved names like `batteryLevel` ### Device Communication - Initialize connections in `deviceStartComm()` - Clean up in `deviceStopComm()` - Handle device offline gracefully +- If you `subscribeToChanges()`, add the `pluginId` self-loop guard at the top of `deviceUpdated()` ### Configuration - Provide sensible defaults diff --git a/docs/plugin-dev/concepts/events.md b/docs/plugin-dev/concepts/events.md index e43f944..b346b8a 100644 --- a/docs/plugin-dev/concepts/events.md +++ b/docs/plugin-dev/concepts/events.md @@ -78,6 +78,46 @@ def _check_battery(self, dev, level): indigo.trigger.execute(trigger) ``` +## Common mistake — methods that don't exist + +Two patterns look like they should fire custom events but **do not** — +both raise `AttributeError`: + +```python +# ❌ WRONG — does not exist on indigo.server / ServerInfo +indigo.server.fireEvent("myEvent") + +# ❌ WRONG — NOT a built-in on PluginBase +# (ZwaveLockManager defines its own custom method with this name, +# which misleads anyone who copy-pastes from there) +self.triggerEvent("myEvent") +``` + +The correct pattern is the `triggerStartProcessing` / `triggerStopProcessing` +lifecycle plus `indigo.trigger.execute(trigger_object)`: + +```python +def __init__(self, ...): + self.event_triggers = {} # trigger.id -> trigger object + +def triggerStartProcessing(self, trigger): + self.event_triggers[trigger.id] = trigger + +def triggerStopProcessing(self, trigger): + self.event_triggers.pop(trigger.id, None) + +def fire_event(self, event_id): + """Iterate registered triggers and execute the matching ones.""" + for trigger in self.event_triggers.values(): + if trigger.pluginTypeId == event_id: + indigo.trigger.execute(trigger) +``` + +The `AttributeError` is easy to miss because it's typically caught by a broad +`except Exception` in the calling code and swallowed at debug level. A custom +event silently never fires until someone notices the trigger was never +configured. Always log trigger-execute failures at ERROR. + ## Event Callback Methods ### triggerStartProcessing diff --git a/docs/plugin-dev/concepts/plugin-preferences.md b/docs/plugin-dev/concepts/plugin-preferences.md index 672f049..8a37a13 100644 --- a/docs/plugin-dev/concepts/plugin-preferences.md +++ b/docs/plugin-dev/concepts/plugin-preferences.md @@ -77,6 +77,28 @@ def _after_sync(self): self.pluginPrefs["_syncCount"] = self.pluginPrefs.get("_syncCount", 0) + 1 ``` +> **⚠️ The `_` prefix convention applies to `self.pluginPrefs` only — NOT to +> device-level `dev.pluginProps`.** +> +> Plugin-level prefs (`self.pluginPrefs[...] = ...`) accept underscore-prefixed +> keys because they're written via direct dict mutation. Device-level +> `dev.pluginProps` written via `replacePluginPropsOnServer()` go through +> Indigo's XML serialiser which rejects keys starting with `_`: +> +> ```python +> # Fine — direct dict, no XML validation +> self.pluginPrefs["_lastSync"] = "..." +> +> # FAILS with LowLevelBadParameterError -- illegal XML tag name character +> new_props = dict(dev.pluginProps) +> new_props["_dynamicKeys"] = "..." +> dev.replacePluginPropsOnServer(new_props) +> +> # Right — same intent, valid name +> new_props["dynamicKeys"] = "..." +> dev.replacePluginPropsOnServer(new_props) +> ``` + ## Validating Preferences ```python @@ -179,7 +201,7 @@ def startup(self): ## Best Practices - Use `get()` with defaults for safe access -- Prefix hidden preferences with underscore (`_cacheTime`) +- Prefix hidden plugin-level preferences with underscore (`_cacheTime`) — but **never** prefix device-level `dev.pluginProps` keys with `_`; those go through Indigo's XML serialiser and a leading `_` raises `LowLevelBadParameterError`. See the warning above. - Validate all user input in `validatePrefsConfigUi()` - React to changes in `closedPrefsConfigUi()` - Don't store sensitive data like passwords in plain text diff --git a/docs/plugin-dev/troubleshooting/common-issues.md b/docs/plugin-dev/troubleshooting/common-issues.md index 06a49d1..c1c77fa 100644 --- a/docs/plugin-dev/troubleshooting/common-issues.md +++ b/docs/plugin-dev/troubleshooting/common-issues.md @@ -135,6 +135,20 @@ ## Device Issues +### `LowLevelBadParameterError -- illegal XML tag name character` + +This is Indigo's catch-all rejection from the XML serialiser when something +in your `getDeviceStateList()` output, `replacePluginPropsOnServer()` payload, +or `stateListOrDisplayStateIdChanged()` refresh contains a character it +considers invalid for an XML element name. The error message does **not** +identify which key is bad. Three undocumented rules cause it; see +[concepts/devices.md → State ID naming rules](../concepts/devices.md#state-id-naming-rules-undocumented-but-strict) +for the full diagnosis. Quick checklist: + +- State IDs must be camelCase ASCII, no underscores (`colorTempStartup`, NOT `color_temp_startup`) +- `dev.pluginProps` keys via `replacePluginPropsOnServer` cannot start with `_` +- Don't append to the LIVE list returned by `indigo.PluginBase.getDeviceStateList()` — make a `list(...)` copy first + ### Device Won't Create **Check**: From 9d03c7a7292f25bd63c7efc79b19302579a7c113 Mon Sep 17 00:00:00 2001 From: CliveS via Highsteads Date: Sun, 10 May 2026 14:44:48 +0100 Subject: [PATCH 2/3] =?UTF-8?q?docs:=20address=20CodeRabbit=20nitpicks=20?= =?UTF-8?q?=E2=80=94=20capitalise=20'Plugin'=20for=20Indigo=20references?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per repo style guideline: 'Plugin' (capitalized) when referring to Indigo plugins (the tools this project helps build). Normalised in the new sections of actions.md, devices.md, and plugin-preferences.md. --- docs/plugin-dev/concepts/actions.md | 16 ++++++++-------- docs/plugin-dev/concepts/devices.md | 14 +++++++------- docs/plugin-dev/concepts/plugin-preferences.md | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/plugin-dev/concepts/actions.md b/docs/plugin-dev/concepts/actions.md index 36ddbaa..84d1075 100644 --- a/docs/plugin-dev/concepts/actions.md +++ b/docs/plugin-dev/concepts/actions.md @@ -334,7 +334,7 @@ def actionControlUniversal(self, action, dev): ## `uiPath` attribute — PascalCase, no spaces -When you group plugin actions under a sub-menu in Indigo's UI via +When you group Plugin actions under a sub-menu in Indigo's UI via `uiPath="..."` on `` or ``, the value MUST be PascalCase with no spaces and no punctuation. Spaces cause an `NSInternalInconsistencyException` crash in the Indigo client (confirmed @@ -355,14 +355,14 @@ with no spaces and no punctuation. Spaces cause an `uiPath="hidden"` is the documented reserved value that hides the action from the user-visible Action picker (e.g. for actions only invoked from -plugin code via `executeAction`). +Plugin code via `executeAction`). -## Calling other plugins' actions — known prop-name pitfalls +## Calling other Plugins' actions — known prop-name pitfalls -`indigo.server.getPlugin(...)` lets you call actions on installed plugins, -but the action ID and prop names must match exactly what the target plugin +`indigo.server.getPlugin(...)` lets you call actions on installed Plugins, +but the action ID and prop names must match exactly what the target Plugin defines in its Actions.xml. Two patterns trip people up because the -"obvious" action name is not what the plugin actually exposes: +"obvious" action name is not what the Plugin actually exposes: ### Pushover (`io.thechad.indigoplugin.pushover`) @@ -392,7 +392,7 @@ indigo.server.sendEmailTo( body="Plain text or HTML body", ) -# Wrong — props dict loses keys during cross-plugin serialization, +# Wrong — props dict loses keys during cross-Plugin serialization, # emailMessage is silently dropped, email never sends indigo.server.getPlugin("com.indigodomo.email").executeAction( "sendEmail", @@ -401,7 +401,7 @@ indigo.server.getPlugin("com.indigodomo.email").executeAction( ``` `indigo.server.sendEmailTo()` automatically uses the first configured -SMTP device — no need to look up the plugin or device ID. +SMTP device — no need to look up the Plugin or device ID. ## Action Validation diff --git a/docs/plugin-dev/concepts/devices.md b/docs/plugin-dev/concepts/devices.md index 2f787fb..17dfd67 100644 --- a/docs/plugin-dev/concepts/devices.md +++ b/docs/plugin-dev/concepts/devices.md @@ -297,7 +297,7 @@ dev.enabled # Is device enabled? ## State ID naming rules (undocumented but strict) -Indigo's plugin host validates custom state IDs more strictly than XML or +Indigo's Plugin host validates custom state IDs more strictly than XML or Python identifiers permit. Violating any of these rules raises `LowLevelBadParameterError -- illegal XML tag name character` from `stateListOrDisplayStateIdChanged()` or `replacePluginPropsOnServer()`, @@ -342,7 +342,7 @@ def _is_valid_state_id(key): ## Reserved state names — don't shadow native device properties Indigo has reserved property names on device objects (e.g. `device.batteryLevel`). -If a plugin declares a custom state with the same name, Indigo silently routes +If a Plugin declares a custom state with the same name, Indigo silently routes `updateStateOnServer()` writes to the **native property** instead of Custom States. The state never appears in the Custom States panel and no error is raised. @@ -350,7 +350,7 @@ Known reserved names to avoid as custom state IDs: - `batteryLevel` — use `battery` (with `Integer` type) instead -The reservation hides bugs that look like "my plugin isn't writing the state" +The reservation hides bugs that look like "my Plugin isn't writing the state" when actually the write succeeded into the wrong slot. Use `Integer` rather than `Number` for whole-number percentages so the Custom States panel renders the value correctly. @@ -358,7 +358,7 @@ the value correctly. ## Dynamic state declaration — three subtle rules When overriding `getDeviceStateList(dev)` to advertise states beyond what's in -Devices.xml (typical pattern: capture-all sensor plugins, MQTT/HA bridges): +Devices.xml (typical pattern: capture-all sensor Plugins, MQTT/HA bridges): ### 1. The parent's list is a LIVE reference, not a copy @@ -381,7 +381,7 @@ def getDeviceStateList(self, dev): Indigo's XML serialiser rejects `dev.replacePluginPropsOnServer({"_seenKeys": ...})` with `LowLevelBadParameterError`. This is **distinct** from `self.pluginPrefs` -(plugin-level prefs written via direct dict assignment) — those accept +(Plugin-level prefs written via direct dict assignment) — those accept underscore-prefixed keys fine. Only **device-level** pluginProps written via `replacePluginPropsOnServer` are strict. @@ -419,12 +419,12 @@ except Exception: ## `deviceUpdated` self-loop guard -If your plugin calls `indigo.devices.subscribeToChanges()` AND also writes +If your Plugin calls `indigo.devices.subscribeToChanges()` AND also writes states on its own devices, every state write fires `deviceUpdated()` again — infinite loop unless guarded. The guard MUST be at the very top of `deviceUpdated()` and check `pluginId`, -not `id`. A per-device id check is not sufficient if the plugin manages more +not `id`. A per-device id check is not sufficient if the Plugin manages more than one device — it doesn't prevent A→B→A→B cross-device loops. ```python diff --git a/docs/plugin-dev/concepts/plugin-preferences.md b/docs/plugin-dev/concepts/plugin-preferences.md index 8a37a13..f6672d3 100644 --- a/docs/plugin-dev/concepts/plugin-preferences.md +++ b/docs/plugin-dev/concepts/plugin-preferences.md @@ -201,7 +201,7 @@ def startup(self): ## Best Practices - Use `get()` with defaults for safe access -- Prefix hidden plugin-level preferences with underscore (`_cacheTime`) — but **never** prefix device-level `dev.pluginProps` keys with `_`; those go through Indigo's XML serialiser and a leading `_` raises `LowLevelBadParameterError`. See the warning above. +- Prefix hidden Plugin-level preferences with underscore (`_cacheTime`) — but **never** prefix device-level `dev.pluginProps` keys with `_`; those go through Indigo's XML serialiser and a leading `_` raises `LowLevelBadParameterError`. See the warning above. - Validate all user input in `validatePrefsConfigUi()` - React to changes in `closedPrefsConfigUi()` - Don't store sensitive data like passwords in plain text From 7a737fc14efbfbfa7afffe9e2bcf1a6be3a82e7d Mon Sep 17 00:00:00 2001 From: CliveS via Highsteads Date: Sun, 10 May 2026 16:45:14 +0100 Subject: [PATCH 3/3] =?UTF-8?q?docs:=20address=20review=20=E2=80=94=20gene?= =?UTF-8?q?ralise=20cross-plugin=20action=20note,=20bump=201.9.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace the Pushover/Email+ specific examples with a generic "calling another plugin's actions" warning. Avoids committing this repo to tracking third-party plugins' action IDs and prop names as they evolve. The general shape (read the target's Actions.xml, prefer direct server APIs when one exists) is what's actually transferable. - Bump plugin.json and marketplace.json to 1.9.5 so version-check passes now that 1.9.4 has landed on main. Co-Authored-By: Claude Opus 4.7 --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- docs/plugin-dev/concepts/actions.md | 67 ++++++++++------------------- 3 files changed, 24 insertions(+), 47 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index ae8f407..c58b21f 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,7 +8,7 @@ "name": "indigo", "source": "./", "description": "Indigo home automation development toolkit \u2014 plugin development, API integration, and control page building", - "version": "1.9.4", + "version": "1.9.5", "repository": "https://github.com/simons-plugins/indigo-claude-plugin", "license": "MIT", "keywords": [ diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 260caca..3ad870f 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "indigo", - "version": "1.9.4", + "version": "1.9.5", "description": "Indigo home automation development toolkit \u2014 plugin development, API integration, and control page building", "repository": "https://github.com/simons-plugins/indigo-claude-plugin" } diff --git a/docs/plugin-dev/concepts/actions.md b/docs/plugin-dev/concepts/actions.md index 84d1075..086e92c 100644 --- a/docs/plugin-dev/concepts/actions.md +++ b/docs/plugin-dev/concepts/actions.md @@ -357,51 +357,28 @@ with no spaces and no punctuation. Spaces cause an from the user-visible Action picker (e.g. for actions only invoked from Plugin code via `executeAction`). -## Calling other Plugins' actions — known prop-name pitfalls - -`indigo.server.getPlugin(...)` lets you call actions on installed Plugins, -but the action ID and prop names must match exactly what the target Plugin -defines in its Actions.xml. Two patterns trip people up because the -"obvious" action name is not what the Plugin actually exposes: - -### Pushover (`io.thechad.indigoplugin.pushover`) - -```python -pushover = indigo.server.getPlugin("io.thechad.indigoplugin.pushover") -if pushover and pushover.isEnabled(): - pushover.executeAction("send", props={ # ← "send", NOT "sendPushover" - "msgTitle": "Subject", - "msgBody": "Message body", # required - "msgUser": PUSHOVER_USER_TOKEN, - "msgPriority": "0", # string: -2,-1,0,1,2 - "msgSound": "vibrate", # any Pushover sound name - }) -``` - -- Action ID is `"send"` (NOT `"sendPushover"` — that does not exist) -- Priority is a **string**, not an int -- Prop names are `msg*`-prefixed; bare `title`/`message` are ignored - -### Email+ (`com.indigodomo.email`) - -```python -# Correct — direct API call -indigo.server.sendEmailTo( - "recipient@example.com", - subject="Subject here", - body="Plain text or HTML body", -) - -# Wrong — props dict loses keys during cross-Plugin serialization, -# emailMessage is silently dropped, email never sends -indigo.server.getPlugin("com.indigodomo.email").executeAction( - "sendEmail", - props={"emailAddress": "...", "emailSubject": "...", "emailMessage": "..."} -) -``` - -`indigo.server.sendEmailTo()` automatically uses the first configured -SMTP device — no need to look up the Plugin or device ID. +## Calling another Plugin's actions + +`indigo.server.getPlugin(plugin_id).executeAction(action_id, props=...)` +lets you invoke actions exposed by other installed Plugins. Two things +must match exactly what the target Plugin declares in its `Actions.xml`: + +1. **The action ID** — the `id` attribute on the `` element, not + the user-facing menu name. +2. **The prop names** — the `id` of every `` inside the action's + ``. A typo or guessed name is silently dropped during + cross-Plugin serialization; the action runs with missing data and no + error is raised. + +Always read the target Plugin's `Actions.xml` to confirm both. Don't +infer the action ID from the menu label, and don't guess prop names +from what feels natural ("title", "message" etc.) — they are whatever +that Plugin's author chose. + +If Indigo exposes a direct server API for the same operation (e.g. +`indigo.server.sendEmailTo(...)`), prefer it over routing through +`getPlugin(...).executeAction(...)` — fewer moving parts and no prop- +serialization layer to misbehave. ## Action Validation