diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index c58b21f..17ac612 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.5", + "version": "2.0.0", "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 3ad870f..d7b589f 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "indigo", - "version": "1.9.5", + "version": "2.0.0", "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/README.md b/README.md index 1230b21..34f706b 100644 --- a/README.md +++ b/README.md @@ -33,19 +33,19 @@ Skills activate automatically when working on relevant files: ## Documentation -- `docs/plugin-dev/` — Plugin development guides, concepts, API reference -- `docs/api/` — WebSocket and HTTP API integration docs +- `reference/canonical/` — **Indigo docs vendored verbatim** (generated by `tools/refresh_canonical.py`, pinned to 2025.2); the source of truth for all reference facts. Start at `reference/canonical/INDEX.md`. +- `docs/plugin-dev/` — Workspace on-ramps & patterns (quick-start, lifecycle, testing, troubleshooting) - `docs/control-pages/` — Control page schema, images, layouts, export -- `reference/` — SDK reference documents and migration guide +- `reference/` — SDK reference + legacy Python 2→3 porting guide - `sdk-examples/` — 16 complete SDK example plugins - `snippets/` — Plugin templates - `examples/` — Control page .textClipping examples -- `tools/` — Utility scripts (e.g., create_clipping.py) +- `tools/` — Utility scripts (`refresh_canonical.py`, `create_clipping.py`) ## Related - [Indigo](https://www.indigodomo.com) — Home automation platform for macOS -- [Plugin Developer's Guide](https://wiki.indigodomo.com/doku.php?id=indigo_2025.1_documentation:plugin_guide) +- [Plugin Development (official docs)](https://docs.indigodomo.com/2025.2/plugin-dev/) - [Developer Forum](https://forums.indigodomo.com/viewforum.php?f=18) ## License diff --git a/commands/api.md b/commands/api.md index d74b1fe..c1dafdc 100644 --- a/commands/api.md +++ b/commands/api.md @@ -16,53 +16,59 @@ Expert assistant for building applications that integrate with Indigo home autom ## CRITICAL: Context Optimization Strategy -This command provides focused API documentation (~50KB). Load selectively based on query type. - -### All Files (Load by Topic) - -| File | Size | Use For | -|------|------|---------| -| `docs/api/README.md` | 5KB | Navigation and quick lookup | -| `docs/api/overview.md` | 7KB | WebSocket vs HTTP decision | -| `docs/api/authentication.md` | 7KB | API key setup and security | -| `docs/api/websocket-api.md` | 10KB | WebSocket API reference | -| `docs/api/http-api.md` | 11KB | HTTP/REST API reference | -| `docs/api/device-commands.md` | 12KB | Complete command reference | - -**Total**: ~52KB across 6 files - load selectively. +Reference docs are vendored verbatim from Indigo's published docs under `reference/canonical/` +(generated by `tools/refresh_canonical.py`, pinned to Indigo 2025.2). Load only the page a query +needs — never the whole tree. Start from `reference/canonical/INDEX.md` if unsure. All +`reference/canonical/…` paths are relative to `${CLAUDE_PLUGIN_ROOT}` (this plugin), not the user's project. + +### Canonical API Files (Load by Topic) + +| File | Use For | +|------|---------| +| `reference/canonical/api.md` | Integration-APIs overview, WebSocket-vs-HTTP choice | +| `reference/canonical/api/http.md` | HTTP endpoints, authentication, curl/Python examples | +| `reference/canonical/api/messages.md` | Message format — commands, events, error responses | +| `reference/canonical/api/websocket.md` | WebSocket feeds, `refresh`, add/patch/delete handling | +| `reference/canonical/api/webhooks.md` | Receiving webhooks from external services | +| `reference/canonical/api/rest-migration.md` | Converting legacy REST API calls to the HTTP API | ## Query Routing Guide ### General Integration Questions **"Build iOS/web app" / "Get started with Indigo API"** -1. Read `docs/api/overview.md` (WebSocket vs HTTP decision) -2. Read `docs/api/authentication.md` (API key setup) +1. Read `reference/canonical/api.md` (WebSocket vs HTTP decision) +2. Read `reference/canonical/api/http.md` → Authentication section (API key setup) **"Should I use WebSocket or HTTP?"** -1. Read `docs/api/overview.md` +1. Read `reference/canonical/api.md` ### WebSocket API Questions **"Monitor devices in real-time" / "WebSocket API" / "Live updates"** -1. Read `docs/api/websocket-api.md` -2. If auth needed: Read `docs/api/authentication.md` +1. Read `reference/canonical/api/websocket.md` +2. For command/event message shapes: Read `reference/canonical/api/messages.md` ### HTTP API Questions -**"Control via REST API" / "HTTP endpoints" / "curl examples"** -1. Read `docs/api/http-api.md` -2. If auth needed: Read `docs/api/authentication.md` +**"Control via HTTP" / "REST" / "HTTP endpoints" / "curl examples"** +1. Read `reference/canonical/api/http.md` +2. For the command message shapes: Read `reference/canonical/api/messages.md` ### Command Reference Questions **"What commands can I send?" / "Device commands" / "Control devices"** -1. Read `docs/api/device-commands.md` +1. Read `reference/canonical/api/messages.md` (device/variable/action/thermostat command messages) + +### Webhook Questions + +**"Receive a webhook" / "trigger from external service" / "doorbell/location webhook"** +1. Read `reference/canonical/api/webhooks.md` ### Authentication Questions **"Setup API keys" / "Authentication" / "Security"** -1. Read `docs/api/authentication.md` +1. Read `reference/canonical/api/http.md` → Authentication section ### Device Capability Catalog @@ -88,28 +94,27 @@ If the user has the `indigo-device-catalog` installed (check for `../indigo-devi **User**: "I want to build an iOS app that shows my Indigo devices in real-time" -1. Read `docs/api/overview.md` - Explain WebSocket is best for real-time -2. Read `docs/api/authentication.md` - Show API key setup -3. Read `docs/api/websocket-api.md` - Provide connection example -4. Read `docs/api/device-commands.md` - Show control commands +1. Read `reference/canonical/api.md` - Explain WebSocket is best for real-time +2. Read `reference/canonical/api/http.md` → Authentication - Show API key setup +3. Read `reference/canonical/api/websocket.md` - Provide feed connection + `refresh` example +4. Read `reference/canonical/api/messages.md` - Show control command messages ### Building a Web Dashboard **User**: "Create a web dashboard to control my lights" -1. Read `docs/api/overview.md` - Suggest HTTP for initial load, WebSocket for updates -2. Read `docs/api/authentication.md` - API key setup -3. Read `docs/api/http-api.md` - Show GET /devices endpoint -4. Read `docs/api/websocket-api.md` - Real-time updates -5. Read `docs/api/device-commands.md` - Control commands +1. Read `reference/canonical/api.md` - Suggest HTTP for initial load, WebSocket for updates +2. Read `reference/canonical/api/http.md` → Authentication - API key setup +3. Read `reference/canonical/api/http.md` - Show `GET /v2/api/indigo.devices` +4. Read `reference/canonical/api/websocket.md` - Real-time updates +5. Read `reference/canonical/api/messages.md` - Control command messages ### Simple Automation Script **User**: "I want to turn off all lights via curl script" -1. Read `docs/api/http-api.md` - Show curl examples -2. Read `docs/api/authentication.md` - Header authentication -3. Read `docs/api/device-commands.md` - turnOff command +1. Read `reference/canonical/api/http.md` - Show curl examples + `POST /v2/api/command` +2. Read `reference/canonical/api/messages.md` - `indigo.device.turnOff` message ## Key Concepts @@ -125,43 +130,46 @@ If the user has the `indigo-device-catalog` installed (check for `../indigo-devi ### Command Messages Across Transports -The APIs share most command messages (documented in `device-commands.md`), though some commands may be specific to one transport: -- WebSocket sends commands via persistent connection -- HTTP sends commands via POST to `/v2/api/command` +Both transports share the same command envelope (documented in `reference/canonical/api/messages.md`): +`{"message": "indigo..", "objectId": , "parameters": {...}}`. There is no +`PUT`/per-object write endpoint and no `?detail` flag — GET returns full objects. +- WebSocket sends commands over a persistent feed connection +- HTTP sends commands via `POST /v2/api/command` ### Authentication -Both APIs use **API Keys** or **Local Secrets**: -- API Keys: Created in Indigo Account portal, per-app, revocable -- Local Secrets: Shared secret in Indigo preferences, local network only +Both APIs accept **HTTP Digest, API Keys, or local secrets**: +- **API Keys** (recommended): managed in Indigo Account → **Authorizations**, per-app, revocable. Sent as `Authorization: Bearer ` or `?api-key=`. Can control devices but not modify the database. +- **Local secrets**: a special kind of API key that doesn't route through the reflector (LAN/same-machine); used identically as a Bearer token. +- **HTTP Basic** and the **old REST API** are deprecated. ## Common Use Cases ### iOS/Android App - WebSocket API for real-time device status - Send commands via WebSocket -- Files: `websocket-api.md`, `authentication.md`, `device-commands.md` +- Files: `api/websocket.md`, `api/http.md` (auth), `api/messages.md` ### Web Dashboard - HTTP for initial page load - WebSocket for live updates -- Files: `http-api.md`, `websocket-api.md`, `device-commands.md` +- Files: `api/http.md`, `api/websocket.md`, `api/messages.md` ### Automation Script - HTTP API with curl/requests - Simple polling or command execution -- Files: `http-api.md`, `device-commands.md` +- Files: `api/http.md`, `api/messages.md` ### Third-Party Integration -- Webhooks receive HTTP POST +- Receive external events via Indigo webhooks - WebSocket for monitoring Indigo events -- Files: `http-api.md`, `websocket-api.md` +- Files: `api/webhooks.md`, `api/http.md`, `api/websocket.md` ## External Resources -- [Official API Documentation](https://wiki.indigodomo.com/doku.php?id=indigo_2025.1_documentation:api) +- [Integration APIs (official docs)](https://docs.indigodomo.com/2025.2/api/) - [Indigo Forum](https://forums.indigodomo.com/) -- [Indigo Account Portal](https://www.indigodomo.com/account/) - Create API keys +- [Indigo Account → Authorizations](https://www.indigodomo.com/account/authorizations) - Create/revoke API keys ## Related Commands diff --git a/commands/dev.md b/commands/dev.md index 5319202..66d4a10 100644 --- a/commands/dev.md +++ b/commands/dev.md @@ -10,109 +10,95 @@ description: Indigo plugin development expert — SDK docs, examples, lifecycle, ## Description -Expert assistant for Indigo home automation plugin development. Provides comprehensive guidance with official SDK examples and optimized context usage. +Expert assistant for Indigo home automation plugin development. Reference facts come from +**canonical docs vendored verbatim** under `reference/canonical/**` (Indigo 2025.2, generated by +`tools/refresh_canonical.py`). A thin layer of workspace on-ramps and patterns lives under +`docs/plugin-dev/`. SDK example plugins live under `sdk-examples/`. ## CRITICAL: Context Optimization Strategy -**DO NOT load all files.** This plugin contains 1.3MB of SDK examples. Use Read tool selectively. +**DO NOT load all files.** `sdk-examples/` alone is ~1.3MB. Use Read selectively, and prefer a +single canonical page (each is small and topic-scoped) over broad loads. When unsure which page, +start from `reference/canonical/INDEX.md` — the routing manifest for every canonical page. All +paths below are relative to `${CLAUDE_PLUGIN_ROOT}` (this plugin), not the user's project. -### Safe to Load (Small Files) +### Workspace on-ramps & patterns (small, hand-maintained) -| File | Size | Use For | -|------|------|---------| -| `docs/plugin-dev/quick-start.md` | 9KB | Getting started | -| `docs/plugin-dev/concepts/plugin-lifecycle.md` | 12KB | Lifecycle methods | -| `docs/plugin-dev/concepts/devices.md` | 7KB | Device design (Devices.xml, ConfigUI) | -| `docs/plugin-dev/concepts/plugin-preferences.md` | 4KB | Plugin preferences (pluginPrefs) | -| `docs/plugin-dev/concepts/events.md` | 5KB | Custom trigger events (Events.xml) | -| `docs/plugin-dev/api/indigo-object-model.md` | 3KB | API overview and quick reference | -| `docs/plugin-dev/examples/sdk-examples-guide.md` | 8KB | Example catalog | -| `docs/plugin-dev/troubleshooting/common-issues.md` | 11KB | Troubleshooting | -| `docs/plugin-dev/patterns/api-patterns.md` | 5KB | Common API patterns | -| `docs/plugin-dev/patterns/open-source-contributing.md` | 3KB | Contributing to IndigoDomotics open source | -| `docs/plugin-dev/patterns/testing.md` | 4KB | Testing — pytest mocks (Pattern A) and TestingBase live integration (Pattern B) | +| File | Use For | +|------|---------| +| `docs/plugin-dev/quick-start.md` | Getting started (opinionated on-ramp) | +| `docs/plugin-dev/concepts/plugin-lifecycle.md` | Lifecycle narrative + `super()` rules | +| `docs/plugin-dev/concepts/{devices,actions,events,plugin-preferences}.md` | **Field notes** — undocumented gotchas (state-ID naming, live-list cache trap, `uiPath` crash, cross-plugin actions, custom-event traps, `_`-prefix rule) | +| `docs/plugin-dev/patterns/api-patterns.md` | State updates, `replaceOnServer` patterns | +| `docs/plugin-dev/patterns/testing.md` | pytest mocks (Pattern A) + TestingBase (Pattern B) | +| `docs/plugin-dev/patterns/open-source-contributing.md` | Contributing to IndigoDomotics open source | +| `docs/plugin-dev/troubleshooting/common-issues.md` | Workspace-specific troubleshooting | +| `docs/plugin-dev/examples/sdk-examples-guide.md` | Catalog of the 16 SDK examples | -### Modular IOM Reference (Load by Topic) +### Canonical reference (load by topic) -The Indigo Object Model is split into focused files (~4KB each): +Plugin authoring — `reference/canonical/plugin-dev/`: | Topic | File | |-------|------| -| Architecture | `docs/plugin-dev/api/iom/architecture.md` | -| Command namespaces | `docs/plugin-dev/api/iom/command-namespaces.md` | -| Device classes | `docs/plugin-dev/api/iom/devices.md` | -| Trigger classes | `docs/plugin-dev/api/iom/triggers.md` | -| Filters | `docs/plugin-dev/api/iom/filters.md` | -| Subscriptions | `docs/plugin-dev/api/iom/subscriptions.md` | -| Constants | `docs/plugin-dev/api/iom/constants.md` | -| indigo.Dict/List | `docs/plugin-dev/api/iom/containers.md` | -| Utilities | `docs/plugin-dev/api/iom/utilities.md` | +| Developer's guide (bundle, Info.plist) | `guide.md` | +| Dev environment (IDE, **symlink workflow**, **debuggers**) | `reference/dev-environment.md` | +| plugin.py lifecycle methods | `reference/plugin-py/general-methods.md` | +| Device callbacks (start/stop/config/action) | `reference/plugin-py/device-methods.md` | +| Trigger callbacks | `reference/plugin-py/trigger-methods.md` | +| Variable callbacks | `reference/plugin-py/variable-methods.md` | +| Helper methods (`substitute*`, serial, browser) | `reference/plugin-py/helper-methods.md` | +| HTTP request handling | `reference/plugin-py/http-requests.md` | +| Logging | `reference/plugin-py/logging.md` | +| Devices.xml (types, states, `subType`) | `reference/xml/devices.md` | +| Actions.xml | `reference/xml/actions.md` | +| Events.xml | `reference/xml/events.md` | +| MenuItems.xml | `reference/xml/menuitems.md` | +| PluginConfig.xml | `reference/xml/pluginconfig.md` | +| ConfigUI fields | `reference/xml/configui.md` (+ `reference/xml/configui/` subpages) | +| ConfigUI validation (+ `ValidationError`) | `reference/xml/configui/validation.md` | +| Building-a-plugin tutorial | `tutorials/building.md` | + +Indigo Object Model (scripting) — `reference/canonical/scripting/`: + +| Topic | File | +|-------|------| +| IOM concepts (hierarchy, base classes) | `iom-concepts.md` | +| Device base class (props & methods) | `reference/devices/base-class.md` | +| Device dictionary representation | `reference/devices/dictionary.md` | +| Device subclasses | `reference/device-subclasses/{dimmer,relay,sensor,thermostat,sprinkler,speedcontrol,multiio}.md` | +| Triggers / Schedules / Action Groups | `reference/{triggers,schedules,action-groups}.md` | +| Variables | `reference/variables.md` | +| Server properties & commands | `reference/server-commands.md` | +| Folders / Insteon / X10 / Utils | `reference/{folders,insteon-commands,x10-commands,utils}.md` | +| Bundling Python packages | `guides/python-packages.md` | ### NEVER Load All at Once -- `sdk-examples/` - 16 complete plugins (1.3MB total) -- All `docs/plugin-dev/api/iom/` files together +- `sdk-examples/` — 16 complete plugins (~1.3MB). Read the guide, then one example. +- The whole `reference/canonical/` tree — load individual pages. ## Query Routing Guide -### Concepts vs API vs Patterns - -These are complementary - load based on question type: - -| User Asks About | Load | +| User asks about | Read | |-----------------|------| -| Device design, Devices.xml, ConfigUI | `docs/plugin-dev/concepts/devices.md` | -| Device properties, methods | `docs/plugin-dev/api/iom/devices.md` | -| State updates, replaceOnServer | `docs/plugin-dev/patterns/api-patterns.md` | -| Plugin lifecycle | `docs/plugin-dev/concepts/plugin-lifecycle.md` | -| Plugin preferences, pluginPrefs | `docs/plugin-dev/concepts/plugin-preferences.md` | -| Custom events, Events.xml | `docs/plugin-dev/concepts/events.md` | -| Filters/iteration | `docs/plugin-dev/api/iom/filters.md` | -| Subscriptions | `docs/plugin-dev/api/iom/subscriptions.md` | -| Schedule/action group commands | `docs/plugin-dev/api/iom/command-namespaces.md` | -| Open source contributing | `docs/plugin-dev/patterns/open-source-contributing.md` | -| Testing, pytest, mocking, TestingBase | `docs/plugin-dev/patterns/testing.md` | - -### Specific Routing - -**"Create a plugin" / "Getting started"** -1. Read `docs/plugin-dev/quick-start.md` -2. Read `snippets/plugin-base-template.py` -3. If specific device type: Read `docs/plugin-dev/examples/sdk-examples-guide.md` -4. Read ONLY that specific SDK example if needed - -**"Debug error" / "Plugin not working"** -1. Read `docs/plugin-dev/troubleshooting/common-issues.md` -2. If lifecycle issue: Read `docs/plugin-dev/concepts/plugin-lifecycle.md` - -**"How do I save plugin settings?"** -1. Read `docs/plugin-dev/concepts/plugin-preferences.md` - -**"How do I create trigger events?"** -1. Read `docs/plugin-dev/concepts/events.md` - -**"How do I update device state?"** -1. Read `docs/plugin-dev/patterns/api-patterns.md` - -**"How do I execute/manage schedules or action groups?"** -1. Read `docs/plugin-dev/api/iom/command-namespaces.md` -2. Read `docs/plugin-dev/patterns/api-patterns.md` - -**"How do I contribute to open source?" / "Add plugin to IndigoDomotics"** -1. Read `docs/plugin-dev/patterns/open-source-contributing.md` - -**"How do I test my plugin?" / "Write tests" / "TestingBase" / "pytest"** -1. Read `docs/plugin-dev/patterns/testing.md` - -**"What device properties exist?"** -1. Read `docs/plugin-dev/api/iom/devices.md` - -**"How does replaceOnServer work?"** -1. Read `docs/plugin-dev/api/iom/architecture.md` - -**"Show me an example"** -1. Read `docs/plugin-dev/examples/sdk-examples-guide.md` -2. Read ONLY that specific example's code +| "Create a plugin" / getting started | `docs/plugin-dev/quick-start.md`, then `snippets/plugin-base-template.py` | +| Plugin lifecycle / `super()` footguns | `docs/plugin-dev/concepts/plugin-lifecycle.md` + `reference/canonical/plugin-dev/reference/plugin-py/device-methods.md` | +| Device design, Devices.xml, `subType` | `reference/canonical/plugin-dev/reference/xml/devices.md` | +| Device properties/methods (runtime) | `reference/canonical/scripting/reference/devices/base-class.md` + the relevant subclass | +| State updates / `replaceOnServer` | `docs/plugin-dev/patterns/api-patterns.md` | +| ConfigUI fields / dynamic lists / validation | `reference/canonical/plugin-dev/reference/xml/configui.md` (+ `configui/` subpages) | +| Actions / `actionControlDevice` | `reference/canonical/plugin-dev/reference/xml/actions.md` + `.../plugin-py/device-methods.md` | +| Menu items | `reference/canonical/plugin-dev/reference/xml/menuitems.md` | +| Custom events / triggers | `reference/canonical/plugin-dev/reference/xml/events.md` + `.../plugin-py/trigger-methods.md` | +| Plugin preferences / PluginConfig.xml | `reference/canonical/plugin-dev/reference/xml/pluginconfig.md` | +| HTTP responder / IWS endpoints | `reference/canonical/plugin-dev/reference/plugin-py/http-requests.md` | +| Bundling pip packages / requirements.txt | `reference/canonical/scripting/guides/python-packages.md` + `docs/plugin-dev/troubleshooting/common-issues.md` | +| Dev environment / debuggers / symlinking | `reference/canonical/plugin-dev/reference/dev-environment.md` | +| Schedules / action groups / variables (scripting) | `reference/canonical/scripting/reference/*.md` | +| Testing / pytest / TestingBase | `docs/plugin-dev/patterns/testing.md` | +| Contributing to open source | `docs/plugin-dev/patterns/open-source-contributing.md` | +| "Show me an example" | `docs/plugin-dev/examples/sdk-examples-guide.md`, then that one example | ## Workflow: Creating Plugins @@ -120,80 +106,32 @@ These are complementary - load based on question type: 1. Read `docs/plugin-dev/quick-start.md` 2. Read `docs/plugin-dev/examples/sdk-examples-guide.md` -3. Read specific thermostat example -4. Generate custom code -5. Don't load all 16 examples - -## Documentation Structure - -``` -docs/plugin-dev/ -├── quick-start.md # Getting started (9KB) -├── concepts/ -│ ├── plugin-lifecycle.md # Lifecycle methods (12KB) -│ ├── devices.md # Device design (7KB) -│ ├── plugin-preferences.md # Plugin prefs (4KB) -│ └── events.md # Custom events (5KB) -├── api/ -│ ├── indigo-object-model.md # Overview (3KB) -│ └── iom/ # Modular reference (~40KB total) -│ ├── architecture.md # Core concepts (5KB) -│ ├── command-namespaces.md # Commands (5KB) -│ ├── devices.md # Device classes (5KB) -│ ├── triggers.md # Trigger classes (4KB) -│ ├── filters.md # Iteration (4KB) -│ ├── subscriptions.md # Callbacks (6KB) -│ ├── constants.md # Icons/enums (4KB) -│ ├── containers.md # Dict/List (3KB) -│ └── utilities.md # Helpers (4KB) -├── patterns/ -│ ├── api-patterns.md # Common patterns (5KB) -│ ├── open-source-contributing.md # IndigoDomotics contributing guide (3KB) -│ └── testing.md # pytest mocks + TestingBase (4KB) -├── examples/ -│ └── sdk-examples-guide.md # Example catalog (8KB) -└── troubleshooting/ - └── common-issues.md # Debugging (11KB) - -sdk-examples/ # 16 plugins (1.3MB) - Load individually -snippets/ -└── plugin-base-template.py # Clean template -``` +3. Read the specific thermostat SDK example (only that one) +4. For runtime API detail: `reference/canonical/scripting/reference/device-subclasses/thermostat.md` +5. Generate custom code — don't load all 16 examples ## SDK Examples: 16 Complete Plugins **Device Types**: Custom, Relay/Dimmer, Thermostat, Sensor, Speed Control, Sprinkler, Energy Meter - **Integration**: HTTP Responder, Action API, Broadcaster/Subscriber, Variable Subscriber, Database Traverse -**Finding examples**: Read `docs/plugin-dev/examples/sdk-examples-guide.md` first, then load specific example. +Read `docs/plugin-dev/examples/sdk-examples-guide.md` first, then load a specific example. ## Best Practices -### Code Quality -- Always call `super()` in `__init__()` (but NOT in startup/shutdown) -- Use `self.sleep()` NOT `time.sleep()` in concurrent threads -- Validate all user input -- Log appropriately: `self.logger.debug/info/error/exception()` -- Handle exceptions gracefully -- Close connections in `shutdown()` - -### Security -- Never expose API keys in logs -- Validate all external input - -## Response Guidelines - -1. **Reference file paths**: Link to specific docs with markdown -2. **Show code examples**: Include working code snippets -3. **Explain why**: Don't just show code, explain the pattern -4. **Point to examples**: "This is like Example Device - Thermostat" +- Always call `super().__init__()` in `__init__` — but NOT in `startup`/`shutdown`. +- `deviceUpdated`/`deviceCreated`/`deviceDeleted` (and trigger equivalents): if overridden, call the base or re-implement start/stop — the base does real work. +- `secure="true"` only masks a field in the UI; it is **not** stored securely — never use it for secrets. +- Use `self.sleep()` NOT `time.sleep()` in concurrent threads; handle `self.StopThread`. +- Log via `self.logger.debug/info/warning/error/exception()`; never log secrets. +- Bundle deps via `requirements.txt` (preferred) — see canonical `scripting/guides/python-packages.md` for manual `-t` bundling and venv-symlink alternatives. ## External Resources -- Official Plugin Guide: https://wiki.indigodomo.com/doku.php?id=indigo_2025.1_documentation:plugin_guide -- Object Model Reference: https://wiki.indigodomo.com/doku.php?id=indigo_2025.1_documentation:object_model_reference -- Developer Forum: https://forums.indigodomo.com/viewforum.php?f=18 +- [Plugin Development (official docs)](https://docs.indigodomo.com/2025.2/plugin-dev/) +- [plugin.py Method Reference](https://docs.indigodomo.com/2025.2/plugin-dev/reference/plugin-py/) +- [Scripting Indigo / IOM](https://docs.indigodomo.com/2025.2/scripting/) +- [Developer Forum](https://forums.indigodomo.com/viewforum.php?f=18) ## Related Commands diff --git a/commands/html-pages.md b/commands/html-pages.md index 3f2ffdf..c336388 100644 --- a/commands/html-pages.md +++ b/commands/html-pages.md @@ -49,7 +49,7 @@ Produce a single self-contained HTML file. Refer to `references/indigo-api-js.md - Debounce slider inputs (300ms) - Handle errors gracefully -**Critical**: All device commands go to `POST /v2/api/command` — consult `/indigo:api` docs (`docs/api/device-commands.md`) for the command reference. Do not guess command formats. +**Critical**: All device commands go to `POST /v2/api/command` — consult `/indigo:api` docs (`reference/canonical/api/messages.md`) for the command reference. Do not guess command formats. ### Phase 4: DEPLOY diff --git a/docs/api/README.md b/docs/api/README.md deleted file mode 100644 index 2ffddcb..0000000 --- a/docs/api/README.md +++ /dev/null @@ -1,110 +0,0 @@ -# Integration APIs - -External integration with Indigo via WebSocket and HTTP APIs for building custom applications, iOS/Android apps, web dashboards, and third-party service integrations. - -## Files in This Section - -| File | Purpose | Size | When to Load | -|------|---------|------|--------------| -| [overview.md](overview.md) | API comparison and decision guide | ~3KB | When choosing WebSocket vs HTTP | -| [authentication.md](authentication.md) | API keys, OAuth, security setup | ~4KB | When setting up authentication | -| [websocket-api.md](websocket-api.md) | WebSocket API complete reference | ~6KB | For real-time monitoring questions | -| [http-api.md](http-api.md) | HTTP/REST API complete reference | ~5KB | For REST API questions | -| [device-commands.md](device-commands.md) | All device command messages | ~6KB | For command reference | - -## Quick Task Lookup - -| I want to... | Read this file | -|--------------|----------------| -| Build an iOS/web app for Indigo | [overview.md](overview.md) | -| Decide between WebSocket and HTTP | [overview.md](overview.md) | -| Set up API authentication | [authentication.md](authentication.md) | -| Secure my API connection | [authentication.md](authentication.md) | -| Monitor devices in real-time | [websocket-api.md](websocket-api.md) | -| Subscribe to device changes | [websocket-api.md](websocket-api.md) | -| Control a device from REST API | [http-api.md](http-api.md) | -| Get all devices via HTTP | [http-api.md](http-api.md) | -| Turn on a light via API | [device-commands.md](device-commands.md) | -| Set dimmer brightness | [device-commands.md](device-commands.md) | -| Send any device command | [device-commands.md](device-commands.md) | - -## Overview - -Indigo provides two primary integration APIs: - -### WebSocket API -**Best for:** Real-time monitoring, mobile apps, live dashboards - -- Persistent bidirectional connection -- Real-time device/variable/action group updates -- 5 specialized feeds (devices, variables, action groups, control pages, logs) -- Automatic push notifications when state changes -- Efficient for continuous monitoring - -→ See [websocket-api.md](websocket-api.md) - -### HTTP API -**Best for:** Simple automation scripts, periodic polling, RESTful integrations - -- Stateless request/response model -- Standard GET/POST endpoints -- Easy to use with curl, Python requests, etc. -- Perfect for one-off commands or periodic checks -- Lower overhead for occasional access - -→ See [http-api.md](http-api.md) - -## Getting Started - -1. **Choose your API**: Read [overview.md](overview.md) to decide WebSocket vs HTTP -2. **Set up authentication**: Follow [authentication.md](authentication.md) to get API keys -3. **Connect and test**: Use examples in [websocket-api.md](websocket-api.md) or [http-api.md](http-api.md) -4. **Send commands**: Reference [device-commands.md](device-commands.md) for all available commands - -## Common Use Cases - -### iOS/Android App -- Use WebSocket API for real-time device monitoring -- Subscribe to device feed for instant updates -- Send commands via WebSocket for immediate control -→ [websocket-api.md](websocket-api.md) + [device-commands.md](device-commands.md) - -### Web Dashboard -- WebSocket for live status updates -- HTTP for initial page load -- Display real-time device states -→ [websocket-api.md](websocket-api.md) + [http-api.md](http-api.md) - -### Automation Script -- HTTP API for simple periodic checks -- curl or Python requests library -- Turn devices on/off based on conditions -→ [http-api.md](http-api.md) + [device-commands.md](device-commands.md) - -### Third-Party Integration -- HTTP webhooks for triggering Indigo actions -- WebSocket for monitoring Indigo events -- Bidirectional integration with external services -→ Both APIs - -## Related Documentation - -- **[Indigo Object Model](../plugin-dev/api/indigo-object-model.md)** - Understanding device/variable objects -- **[Plugin Development](../plugin-dev/concepts/)** - Building Indigo plugins (server-side) -- **[Examples](../plugin-dev/examples/)** - SDK examples for plugin development - -## For Claude (Context Optimization) - -**Total size**: ~26KB across 6 files - never load all files at once. - -**Routing rules**: -- General integration questions → Load [overview.md](overview.md) first -- WebSocket questions → Load [websocket-api.md](websocket-api.md) -- HTTP/REST questions → Load [http-api.md](http-api.md) -- Authentication/security → Load [authentication.md](authentication.md) -- Command reference → Load [device-commands.md](device-commands.md) -- "Should I use..." questions → Load [overview.md](overview.md) - -**Safe to always load**: This README (~2KB) - -**Cross-references**: Integration docs use same device/variable object structure as Indigo Object Model docs. If user asks about object properties, also load `../plugin-dev/api/iom/devices.md` or relevant IOM file. diff --git a/docs/api/authentication.md b/docs/api/authentication.md deleted file mode 100644 index 11982f6..0000000 --- a/docs/api/authentication.md +++ /dev/null @@ -1,327 +0,0 @@ -# Authentication - -Complete guide to authenticating with Indigo's WebSocket and HTTP APIs using API keys and local secrets. - -## Quick Reference - -| Method | Security | Use Case | Revocable | Setup Location | -|--------|----------|----------|-----------|----------------| -| **API Key** | High | Production, remote access | Yes | Indigo Account portal | -| **Local Secret** | Medium | Trusted local network only | No | Indigo preferences | -| **HTTP Basic** | Low | DEPRECATED - do not use | No | - | - -**Recommendation**: Use API Keys for all integrations. - -## Prerequisites - -1. **Enable Local Server**: Indigo → Preferences → "Start Local Server" -2. **Enable API Authentication**: Check "OAuth and API Key authentication" in server dialog -3. **Get Reflector ID** (for remote access): Note your `{REFLECTOR}.indigodomo.net` address - -## API Key Authentication (Recommended) - -### Creating an API Key - -1. Visit [Indigo Account Portal](https://www.indigodomo.com/account/) -2. Log in with your Indigo account -3. Navigate to "API Keys" section -4. Click "Create New API Key" -5. Name your key (e.g., "iOS App", "Home Dashboard") -6. Set permissions (device control, variable access, etc.) -7. **Save the key immediately** - it won't be shown again - -### Using API Keys - -**Header Authentication (Best Practice)**: - -```python -# Python -import requests - -headers = { - 'Authorization': f'Bearer {API_KEY}' -} -response = requests.get('https://REFLECTOR.indigodomo.net/v2/api/indigo.devices', headers=headers) -``` - -```javascript -// JavaScript -const headers = { - 'Authorization': `Bearer ${API_KEY}` -}; - -fetch('https://REFLECTOR.indigodomo.net/v2/api/indigo.devices', { headers }) - .then(response => response.json()) - .then(data => console.log(data)); -``` - -```bash -# curl -curl -H "Authorization: Bearer API_KEY" \ - https://REFLECTOR.indigodomo.net/v2/api/indigo.devices -``` - -**Query Parameter (Alternative)**: - -``` -https://REFLECTOR.indigodomo.net/v2/api/indigo.devices?api-key=YOUR_API_KEY -``` - -⚠️ **Warning**: Query parameters may appear in logs. Use header authentication when possible. - -### WebSocket with API Key - -```python -import websockets -import json - -async def connect(): - uri = f"wss://REFLECTOR.indigodomo.net/v2/api/ws/device-feed" - headers = { - 'Authorization': f'Bearer {API_KEY}' - } - - async with websockets.connect(uri, extra_headers=headers) as websocket: - # Connected - start using WebSocket - await websocket.send(json.dumps({ - "message": "refresh", - "objectType": "indigo.Device" - })) -``` - -```javascript -// JavaScript (browser or Node.js with ws library) -const WebSocket = require('ws'); - -const ws = new WebSocket('wss://REFLECTOR.indigodomo.net/v2/api/ws/device-feed', { - headers: { - 'Authorization': `Bearer ${API_KEY}` - } -}); - -ws.on('open', function() { - // Connected - ws.send(JSON.stringify({ - message: 'refresh', - objectType: 'indigo.Device' - })); -}); -``` - -### API Key Best Practices - -✅ **DO**: -- Use separate keys for different applications -- Name keys descriptively ("Production iOS App", "Dev Dashboard") -- Revoke compromised keys immediately -- Store keys securely (environment variables, keychain) -- Use header authentication over query parameters -- Rotate keys periodically for sensitive applications - -❌ **DON'T**: -- Share keys publicly (forums, GitHub, etc.) -- Hardcode keys in source code -- Use same key for dev and production -- Include keys in client-side JavaScript (use backend proxy) - -### Revoking API Keys - -1. Return to Indigo Account Portal -2. Find the compromised key -3. Click "Revoke" -4. Key is immediately invalidated across all connections - -## Local Secrets - -**Use only for trusted local network devices**. - -### Setup - -1. Indigo → Preferences → "Start Local Server" -2. Enter a shared secret in "Local Secret" field -3. Click "Save" - -### Using Local Secrets - -Same as API key authentication - use the local secret as the Bearer token: - -```python -headers = { - 'Authorization': f'Bearer {LOCAL_SECRET}' -} -``` - -### Local Secret Limitations - -- Cannot be revoked without changing for all clients -- No per-application tracking -- Less secure than individual API keys -- Only recommended for fully trusted local devices - -## Connection Security - -### Local Network Access - -**Unencrypted (ws:// or http://)**: -``` -ws://192.168.1.100:8176/v2/api/ws/device-feed -http://192.168.1.100:8176/v2/api/indigo.devices -``` - -✅ Pros: Fast, low latency -❌ Cons: **No encryption** - credentials and data visible on network - -**When to use**: Only on fully trusted networks (home network with WPA2/WPA3 encryption) - -**Local HTTPS/WSS (wss:// or https:// with self-signed certificate)**: -``` -wss://192.168.1.100:8176/v2/api/ws/device-feed -https://192.168.1.100:8176/v2/api/indigo.devices -``` - -✅ Pros: **TLS encryption** on local network, protects credentials in transit -⚠️ Cons: Uses **self-signed certificate** — clients must explicitly trust it (see below) - -**When to use**: When you want encryption on your local network without routing through the Reflector. Indigo's local server generates a self-signed TLS certificate automatically when HTTPS is enabled. - -### Handling Self-Signed Certificates (Local HTTPS) - -When connecting to Indigo's local HTTPS server, standard TLS validation will reject the self-signed certificate. Your client must explicitly trust it. - -**Python**: -```python -import requests - -# Disable certificate verification for local self-signed cert -response = requests.get( - 'https://192.168.1.100:8176/v2/api/indigo.devices', - headers={'Authorization': f'Bearer {API_KEY}'}, - verify=False -) -``` - -**Swift (iOS/macOS)**: -```swift -// Use a URLSessionDelegate that trusts self-signed certificates -class LocalTrustDelegate: NSObject, URLSessionDelegate { - static let shared = LocalTrustDelegate() - - func urlSession(_ session: URLSession, - didReceive challenge: URLAuthenticationChallenge, - completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, - let serverTrust = challenge.protectionSpace.serverTrust else { - completionHandler(.performDefaultHandling, nil) - return - } - completionHandler(.useCredential, URLCredential(trust: serverTrust)) - } -} - -// Use with URLSession -let session = URLSession(configuration: .ephemeral, - delegate: LocalTrustDelegate.shared, - delegateQueue: nil) -``` - -**curl**: -```bash -# -k flag skips certificate verification -curl -k -H "Authorization: Bearer API_KEY" \ - https://192.168.1.100:8176/v2/api/indigo.devices -``` - -⚠️ **Important**: Only bypass certificate verification for **local** connections to your own Indigo server. Always use proper certificate validation for remote/Reflector connections. - -### Remote Access (Indigo Reflector) - -**Encrypted (wss:// or https://)**: -``` -wss://REFLECTOR.indigodomo.net/v2/api/ws/device-feed -https://REFLECTOR.indigodomo.net/v2/api/indigo.devices -``` - -✅ Pros: **TLS encryption** with valid certificate, secure over internet -❌ Cons: Slightly higher latency - -**When to use**: All remote access, mobile apps, public internet - -### Security Recommendations - -1. **Use HTTPS/WSS** for all connections (remote and local where possible) -2. **Use API Keys** instead of local secrets -3. **Enable OAuth** in server preferences -4. **Firewall rules**: Only expose Indigo server if using Reflector -5. **Monitor access**: Check Indigo logs for unauthorized attempts -6. **Revoke unused keys**: Remove old API keys for retired applications -7. **Local HTTPS**: Enable for encrypted local connections; only trust self-signed certs for your own server - -## Troubleshooting - -### "Authentication Failed" Error - -**Causes**: -1. API key not enabled in server preferences -2. Invalid or revoked API key -3. Missing `Authorization` header -4. Typo in API key - -**Solutions**: -- Verify "OAuth and API Key authentication" is checked -- Regenerate API key in portal -- Check header format: `Authorization: Bearer {KEY}` (note the space) -- Test with curl to isolate issue - -### "Connection Refused" Error - -**Causes**: -1. Local server not started -2. Firewall blocking port 8176 -3. Incorrect IP address or Reflector ID - -**Solutions**: -- Verify "Start Local Server" is enabled in preferences -- Check firewall allows port 8176 (local) or 443 (Reflector) -- Confirm Reflector ID at https://www.indigodomo.com/account/ - -### WebSocket Connection Drops Immediately - -**Causes**: -1. Authentication failure -2. Server preference disables WebSocket -3. Network issue - -**Solutions**: -- Check WebSocket error message for authentication failure -- Verify API key has correct permissions -- Test HTTP endpoint first to isolate WebSocket-specific issues - -## Example: Secure Production Setup - -```python -# .env file (never commit to git) -INDIGO_API_KEY=your_production_api_key_here -INDIGO_REFLECTOR=your_reflector_id - -# Python app -import os -from dotenv import load_dotenv - -load_dotenv() - -API_KEY = os.getenv('INDIGO_API_KEY') -REFLECTOR = os.getenv('INDIGO_REFLECTOR') - -# Always use HTTPS for production -BASE_URL = f'https://{REFLECTOR}.indigodomo.net/v2/api' -HEADERS = {'Authorization': f'Bearer {API_KEY}'} - -# Make requests -response = requests.get(f'{BASE_URL}/indigo.devices', headers=HEADERS) -``` - -## Related Documentation - -- **[WebSocket API](websocket-api.md)** - Using authentication with WebSocket -- **[HTTP API](http-api.md)** - Using authentication with HTTP -- **[Overview](overview.md)** - Choosing the right API diff --git a/docs/api/device-commands.md b/docs/api/device-commands.md deleted file mode 100644 index ccf1ce0..0000000 --- a/docs/api/device-commands.md +++ /dev/null @@ -1,708 +0,0 @@ -# Device Commands - -Complete reference for all device commands available via WebSocket and HTTP APIs. - -> **Note on Command Availability:** The commands documented here are primarily tested with the WebSocket API and are generally available in both transports. However, some commands may be transport-specific. If a command doesn't work in your chosen transport, try the alternative or consult the official Indigo documentation. - -## Command Message Structure - -All commands use this JSON format: - -```json -{ - "message": "command.namespace.action", // Required - "objectId": 123456789, // Required - "parameters": { // Optional - "key": "value" - }, - "id": "tracking-id" // Optional -} -``` - -## Quick Command Reference - -| Device Type | Commands | Common Use Cases | -|-------------|----------|------------------| -| **All Devices** | statusRequest, toggle, turnOn, turnOff, enable | Basic control | -| **Dimmer** | setBrightness, brighten, dim | Light intensity | -| **Dimmer (Color)** | setColorLevels | RGB, white level, color temperature | -| **Speed Control** | setSpeedIndex, setSpeedLevel, increaseSpeed, decreaseSpeed | Fan speed | -| **Sensor** | setOnState | Manual sensor state | -| **I/O Device** | setBinaryOutput | Digital output control | -| **Sprinkler** | nextZone, previousZone, pause, resume | Zone management | -| **Lock** | lock, unlock | Security devices | -| **Variable** | updateValue | Variable management | -| **Action Group** | execute | Run actions | - -## Generic Device Commands (`indigo.device`) - -Available for: RelayDevice, DimmerDevice, SpeedControl, some SensorDevice types - -### statusRequest - -Request device to update its status from hardware. - -```json -{ - "message": "indigo.device.statusRequest", - "objectId": 123456789 -} -``` - -**Parameters**: None - -**Use Case**: Force device to poll current state from hardware - -### toggle - -Toggle device between on and off states. - -```json -{ - "message": "indigo.device.toggle", - "objectId": 123456789, - "parameters": { - "delay": 5, // Optional: delay in seconds before executing - "duration": 10 // Optional: auto turn off after N seconds - } -} -``` - -**Parameters**: -- `delay` (int, optional): Seconds to wait before toggling -- `duration` (int, optional): Seconds after which to automatically reverse state - -**Example - Toggle after 5 seconds**: -```json -{ - "message": "indigo.device.toggle", - "objectId": 123456789, - "parameters": {"delay": 5} -} -``` - -### turnOn - -Turn device on. - -```json -{ - "message": "indigo.device.turnOn", - "objectId": 123456789, - "parameters": { - "delay": 0, // Optional - "duration": 0 // Optional - } -} -``` - -**Parameters**: Same as toggle - -**Example - Turn on for 60 seconds then auto-off**: -```json -{ - "message": "indigo.device.turnOn", - "objectId": 123456789, - "parameters": {"duration": 60} -} -``` - -### turnOff - -Turn device off. - -```json -{ - "message": "indigo.device.turnOff", - "objectId": 123456789, - "parameters": { - "delay": 0 // Optional - } -} -``` - -**Parameters**: -- `delay` (int, optional): Seconds to wait before turning off - -### lock / unlock - -Lock or unlock security devices. - -```json -{ - "message": "indigo.device.lock", - "objectId": 123456789, - "parameters": { - "delay": 0 // Optional - } -} -``` - -```json -{ - "message": "indigo.device.unlock", - "objectId": 123456789, - "parameters": { - "delay": 0 // Optional - } -} -``` - -**Parameters**: -- `delay` (int, optional): Seconds to wait before executing - -### enable - -Enable or disable device. - -```json -{ - "message": "indigo.device.enable", - "objectId": 123456789, - "parameters": { - "value": true // true = enable, false = disable - } -} -``` - -**Parameters**: -- `value` (boolean, required): `true` to enable, `false` to disable - -**Use Case**: Temporarily disable device without deleting it - -## Dimmer Commands (`indigo.dimmer`) - -Available for: DimmerDevice - -### setBrightness - -Set dimmer to specific brightness level. - -```json -{ - "message": "indigo.dimmer.setBrightness", - "objectId": 123456789, - "parameters": { - "value": 50, // Required: 0-100 - "delay": 0 // Optional: delay in seconds - } -} -``` - -**Parameters**: -- `value` (int, required): Brightness level 0-100 (0 = off, 100 = full brightness) -- `delay` (int, optional): Seconds to wait before executing - -**Example - Dim to 25% brightness**: -```json -{ - "message": "indigo.dimmer.setBrightness", - "objectId": 123456789, - "parameters": {"value": 25} -} -``` - -### brighten - -Increase brightness by specified amount. - -```json -{ - "message": "indigo.dimmer.brighten", - "objectId": 123456789, - "parameters": { - "by": 10, // Optional: amount to increase (default: 10) - "delay": 0 // Optional: delay in seconds - } -} -``` - -**Parameters**: -- `by` (int, optional): Amount to increase brightness (default: 10) -- `delay` (int, optional): Seconds to wait before executing - -**Example - Brighten by 20%**: -```json -{ - "message": "indigo.dimmer.brighten", - "objectId": 123456789, - "parameters": {"by": 20} -} -``` - -### dim - -Decrease brightness by specified amount. - -```json -{ - "message": "indigo.dimmer.dim", - "objectId": 123456789, - "parameters": { - "by": 10, // Optional: amount to decrease (default: 10) - "delay": 0 // Optional: delay in seconds - } -} -``` - -**Parameters**: -- `by` (int, optional): Amount to decrease brightness (default: 10) -- `delay` (int, optional): Seconds to wait before executing - -## Color Commands (`indigo.dimmer`) - -Available for: DimmerDevice with color capabilities (`supportsRGB`, `supportsWhiteTemperature`, `supportsWhite`) - -### setColorLevels - -Set RGB color, white level, and/or color temperature in a single command. All color channel parameters are optional — include only the channels you want to change. - -```json -{ - "message": "indigo.dimmer.setColorLevels", - "objectId": 123456789, - "parameters": { - "redLevel": 100, - "greenLevel": 50, - "blueLevel": 0, - "whiteLevel": 0, - "whiteLevel2": 0, - "whiteTemperature": 3500 - } -} -``` - -**Parameters** (all optional — include only channels being set): -- `redLevel` (int): Red channel 0-100 -- `greenLevel` (int): Green channel 0-100 -- `blueLevel` (int): Blue channel 0-100 -- `whiteLevel` (int): White channel 0-100 (warm white on RGBW devices) -- `whiteLevel2` (int): Second white channel 0-100 (cool white on dual-white devices) -- `whiteTemperature` (int): Color temperature in Kelvin (typically 2000-6500) -- `delay` (int, optional): Seconds to wait before executing - -**Important**: This is a single combined command — there are no separate `setRedLevel`, `setGreenLevel`, etc. commands. Always use `setColorLevels` with the appropriate parameter keys. - -#### Example — Set RGB Color (Orange) - -```json -{ - "message": "indigo.dimmer.setColorLevels", - "objectId": 123456789, - "parameters": { - "redLevel": 100, - "greenLevel": 50, - "blueLevel": 0 - } -} -``` - -#### Example — Set Color Temperature (Warm White) - -```json -{ - "message": "indigo.dimmer.setColorLevels", - "objectId": 123456789, - "parameters": { - "whiteTemperature": 2700 - } -} -``` - -#### Example — Set White Level Only - -```json -{ - "message": "indigo.dimmer.setColorLevels", - "objectId": 123456789, - "parameters": { - "whiteLevel": 80 - } -} -``` - -### Color Device Properties - -Devices with color support expose these properties: - -| Property | Type | Range | Description | -|----------|------|-------|-------------| -| `supportsRGB` | bool | — | Device can set red/green/blue channels | -| `supportsWhiteTemperature` | bool | — | Device can set color temperature | -| `supportsWhite` | bool | — | Device has white channel(s) | -| `redLevel` | int | 0-100 | Current red channel level | -| `greenLevel` | int | 0-100 | Current green channel level | -| `blueLevel` | int | 0-100 | Current blue channel level | -| `whiteLevel` | int | 0-100 | Current white channel level | -| `whiteLevel2` | int | 0-100 | Second white channel (cool white) | -| `whiteTemperature` | int | Kelvin | Current color temperature | - -### Color Mode - -Devices may report a `colorMode` state (via `states["colorMode"]`) indicating whether they are currently in `"rgb"` or `"colorTemp"` mode. When sending RGB parameters the device switches to RGB mode; when sending `whiteTemperature` it switches to color temperature mode. - -## Speed Control Commands (`indigo.speedcontrol`) - -Available for: SpeedControl devices (fans, etc.) - -### setSpeedIndex - -Set fan to specific speed index (0, 1, 2, 3, etc.). - -```json -{ - "message": "indigo.speedcontrol.setSpeedIndex", - "objectId": 123456789, - "parameters": { - "value": 2 // Speed index (0 = off, 1-3 = speeds) - } -} -``` - -**Parameters**: -- `value` (int, required): Speed index (0 = off, device-specific max) - -### setSpeedLevel - -Set fan to specific speed percentage (0-100). - -```json -{ - "message": "indigo.speedcontrol.setSpeedLevel", - "objectId": 123456789, - "parameters": { - "value": 66 // Speed percentage 0-100 - } -} -``` - -**Parameters**: -- `value` (int, required): Speed level 0-100 - -### increaseSpeedIndex - -Increase speed by number of index levels. - -```json -{ - "message": "indigo.speedcontrol.increaseSpeedIndex", - "objectId": 123456789, - "parameters": { - "by": 1 // Optional: number of levels (default: 1) - } -} -``` - -**Parameters**: -- `by` (int, optional): Number of speed levels to increase (default: 1) - -### decreaseSpeedIndex - -Decrease speed by number of index levels. - -```json -{ - "message": "indigo.speedcontrol.decreaseSpeedIndex", - "objectId": 123456789, - "parameters": { - "by": 1 // Optional: number of levels (default: 1) - } -} -``` - -**Parameters**: -- `by` (int, optional): Number of speed levels to decrease (default: 1) - -## Sensor Commands (`indigo.sensor`) - -Available for: SensorDevice (limited set) - -### setOnState - -Manually set sensor on/off state. - -```json -{ - "message": "indigo.sensor.setOnState", - "objectId": 123456789, - "parameters": { - "value": true // true = on, false = off - } -} -``` - -**Parameters**: -- `value` (boolean, required): `true` for on, `false` for off - -**Use Case**: Virtual sensors or manual override - -## I/O Device Commands (`indigo.iodevice`) - -Available for: IODevice (digital outputs) - -### setBinaryOutput - -Set specific binary output on I/O device. - -```json -{ - "message": "indigo.iodevice.setBinaryOutput", - "objectId": 123456789, - "parameters": { - "index": 0, // Output index - "value": true // true = on, false = off - } -} -``` - -**Parameters**: -- `index` (int, required): Output index number -- `value` (boolean, required): `true` for on, `false` for off - -## Sprinkler Commands (`indigo.sprinkler`) - -Available for: SprinklerDevice - -### nextZone - -Advance to next sprinkler zone. - -```json -{ - "message": "indigo.sprinkler.nextZone", - "objectId": 123456789 -} -``` - -**Parameters**: None - -### previousZone - -Go back to previous sprinkler zone. - -```json -{ - "message": "indigo.sprinkler.previousZone", - "objectId": 123456789 -} -``` - -**Parameters**: None - -### pause - -Pause sprinkler schedule. - -```json -{ - "message": "indigo.sprinkler.pause", - "objectId": 123456789 -} -``` - -**Parameters**: None - -### resume - -Resume sprinkler schedule. - -```json -{ - "message": "indigo.sprinkler.resume", - "objectId": 123456789 -} -``` - -**Parameters**: None - -## Variable Commands (`indigo.variable`) - -### updateValue - -Update variable value. - -```json -{ - "message": "indigo.variable.updateValue", - "objectId": 345633244, - "parameters": { - "value": "new value" // Must be string - } -} -``` - -**Parameters**: -- `value` (string, required): New variable value (must be string, use `""` to clear) - -**Important**: Variable values are **always strings**. Numbers, booleans, etc. must be converted to strings. - -**Examples**: -```json -// Set to text -{"message": "indigo.variable.updateValue", "objectId": 123, "parameters": {"value": "home"}} - -// Set to number (as string) -{"message": "indigo.variable.updateValue", "objectId": 123, "parameters": {"value": "42"}} - -// Clear value -{"message": "indigo.variable.updateValue", "objectId": 123, "parameters": {"value": ""}} -``` - -## Action Group Commands (`indigo.actionGroup`) - -### execute - -Execute action group. - -```json -{ - "message": "indigo.actionGroup.execute", - "objectId": 94914463 -} -``` - -**Parameters**: None - -**Use Case**: Trigger scenes, run automation sequences - -## Data Type Reference - -### Python ↔ JSON Conversion - -| Python Type | JSON Type | Example | -|-------------|-----------|---------| -| `True` | `true` | `"value": true` | -| `False` | `false` | `"value": false` | -| `None` | `null` | `"value": null` | -| `int` | Number | `"value": 50` | -| `float` | Number | `"value": 3.14` | -| `str` | String | `"value": "text"` | -| `dict` | Object | `"parameters": {"key": "value"}` | -| `list` | Array | `"list": [1, 2, 3]` | - -**Important**: Python `True`/`False` must be lowercase `true`/`false` in JSON. - -## Common Patterns - -### Turn Light On at 50% Brightness - -```json -{ - "message": "indigo.dimmer.setBrightness", - "objectId": 123456789, - "parameters": {"value": 50} -} -``` - -### Toggle Light After 30 Second Delay - -```json -{ - "message": "indigo.device.toggle", - "objectId": 123456789, - "parameters": {"delay": 30} -} -``` - -### Turn Light On for 60 Seconds Then Auto-Off - -```json -{ - "message": "indigo.device.turnOn", - "objectId": 123456789, - "parameters": {"duration": 60} -} -``` - -### Set Light to Warm White (2700K) - -```json -{ - "message": "indigo.dimmer.setColorLevels", - "objectId": 123456789, - "parameters": {"whiteTemperature": 2700} -} -``` - -### Set Light to Blue - -```json -{ - "message": "indigo.dimmer.setColorLevels", - "objectId": 123456789, - "parameters": {"redLevel": 0, "greenLevel": 0, "blueLevel": 100} -} -``` - -### Set Fan to Medium Speed (66%) - -```json -{ - "message": "indigo.speedcontrol.setSpeedLevel", - "objectId": 123456789, - "parameters": {"value": 66} -} -``` - -### Update Variable with Sensor Reading - -```json -{ - "message": "indigo.variable.updateValue", - "objectId": 345633244, - "parameters": {"value": "72.5"} -} -``` - -## Error Handling - -### Invalid Command - -```json -{ - "error": "Unknown command: indigo.device.invalid", - "errorId": "unknown_command" -} -``` - -### Invalid Object ID - -```json -{ - "error": "Device not found", - "errorId": "device_not_found" -} -``` - -### Missing Required Parameter - -```json -{ - "error": "Missing required parameter: value", - "errorId": "missing_parameter" -} -``` - -### Invalid Parameter Value - -```json -{ - "error": "Brightness value must be 0-100", - "errorId": "invalid_parameter_value" -} -``` - -## Best Practices - -1. **Check Device Type**: Not all commands work on all devices -2. **Validate Parameters**: Ensure brightness/speed values are in valid range -3. **Use Delays Wisely**: Delays block other commands to same device -4. **Track Requests**: Use optional `id` field to match responses -5. **Handle Errors**: Always check for `error` field in response -6. **String Variables**: Remember variable values are always strings - -## Related Documentation - -- **[WebSocket API](websocket-api.md)** - Using commands via WebSocket -- **[HTTP API](http-api.md)** - Using commands via HTTP POST -- **[Indigo Object Model](../indigo-object-model.md)** - Device properties and structure diff --git a/docs/api/http-api.md b/docs/api/http-api.md deleted file mode 100644 index e8681fa..0000000 --- a/docs/api/http-api.md +++ /dev/null @@ -1,499 +0,0 @@ -# HTTP API - -Complete reference for Indigo's HTTP/REST API - stateless request/response model for device control, data retrieval, and command execution. - -## Quick Reference - -| Endpoint | Method | Purpose | Example | -|----------|--------|---------|---------| -| `/v2/api/indigo.devices` | GET | Get all devices | List all devices | -| `/v2/api/indigo.devices/{id}` | GET | Get single device | Get device details | -| `/v2/api/indigo.variables` | GET | Get all variables | List all variables | -| `/v2/api/indigo.variables/{id}` | GET | Get single variable | Get variable value | -| `/v2/api/indigo.actionGroups` | GET | Get all action groups | List action groups | -| `/v2/api/indigo.actionGroups/{id}` | GET | Get single action group | Get action group details | -| `/v2/api/command` | POST | Execute command | Control devices, update variables | - -**Base URLs**: -- Local: `http://192.168.1.100:8176` -- Remote (Reflector): `https://{REFLECTOR}.indigodomo.net` - -## Authentication - -All requests require authentication via `Authorization` header: - -```http -Authorization: Bearer YOUR_API_KEY -``` - -See [authentication.md](authentication.md) for complete setup guide. - -## Device Endpoints - -### Get All Devices - -**Request**: -```http -GET /v2/api/indigo.devices -Authorization: Bearer API_KEY -``` - -**Python Example**: -```python -import requests - -url = "https://REFLECTOR.indigodomo.net/v2/api/indigo.devices" -headers = {'Authorization': f'Bearer {API_KEY}'} - -response = requests.get(url, headers=headers) -devices = response.json() - -for device in devices: - print(f"{device['name']}: {device['onState']}") -``` - -**Response**: -```json -[ - { - "id": 123456789, - "name": "Living Room Light", - "class": "indigo.DimmerDevice", - "onState": true, - "brightness": 75, - "remoteDisplay": true, - ... - }, - ... -] -``` - -### Get Single Device - -**Request**: -```http -GET /v2/api/indigo.devices/123456789 -Authorization: Bearer API_KEY -``` - -**curl Example**: -```bash -curl -H "Authorization: Bearer API_KEY" \ - https://REFLECTOR.indigodomo.net/v2/api/indigo.devices/123456789 -``` - -**Response**: -```json -{ - "id": 123456789, - "name": "Living Room Light", - "class": "indigo.DimmerDevice", - "onState": true, - "brightness": 75, - "deviceTypeId": "dimmer", - "folderId": 0, - "remoteDisplay": true, - ... -} -``` - -### Control Device via Command Endpoint - -**Request**: -```http -POST /v2/api/command -Authorization: Bearer API_KEY -Content-Type: application/json - -{ - "message": "indigo.device.toggle", - "objectId": 123456789 -} -``` - -**Python Example**: -```python -import requests -import json - -url = "https://REFLECTOR.indigodomo.net/v2/api/command" -headers = { - 'Authorization': f'Bearer {API_KEY}', - 'Content-Type': 'application/json' -} -data = { - "message": "indigo.device.toggle", - "objectId": 123456789, - "id": "my-tracking-id" # Optional -} - -response = requests.post(url, headers=headers, data=json.dumps(data)) -print(response.json()) -``` - -**JavaScript/Fetch Example**: -```javascript -const url = 'https://REFLECTOR.indigodomo.net/v2/api/command'; -const headers = { - 'Authorization': `Bearer ${API_KEY}`, - 'Content-Type': 'application/json' -}; -const body = JSON.stringify({ - message: 'indigo.dimmer.setBrightness', - objectId: 123456789, - parameters: { - value: 50, - delay: 0 - } -}); - -fetch(url, { method: 'POST', headers, body }) - .then(response => response.json()) - .then(data => console.log(data)); -``` - -**curl Example**: -```bash -curl -X POST \ - -H "Authorization: Bearer API_KEY" \ - -H "Content-Type: application/json" \ - -d '{"message": "indigo.device.turnOn", "objectId": 123456789}' \ - https://REFLECTOR.indigodomo.net/v2/api/command -``` - -## Variable Endpoints - -### Get All Variables - -**Request**: -```http -GET /v2/api/indigo.variables -Authorization: Bearer API_KEY -``` - -**Response**: -```json -[ - { - "id": 345633244, - "name": "house_status", - "value": "home", - "readOnly": false, - "class": "indigo.Variable", - ... - }, - ... -] -``` - -### Get Single Variable - -**Request**: -```http -GET /v2/api/indigo.variables/345633244 -Authorization: Bearer API_KEY -``` - -### Update Variable Value - -**Request**: -```http -POST /v2/api/command -Authorization: Bearer API_KEY -Content-Type: application/json - -{ - "message": "indigo.variable.updateValue", - "objectId": 345633244, - "parameters": { - "value": "away" - } -} -``` - -**Important**: Variable values must be **strings**. Use empty string `""` to clear value. - -**Python Example**: -```python -import requests -import json - -url = "https://REFLECTOR.indigodomo.net/v2/api/command" -headers = { - 'Authorization': f'Bearer {API_KEY}', - 'Content-Type': 'application/json' -} -data = { - "message": "indigo.variable.updateValue", - "objectId": 345633244, - "parameters": {"value": "away"} -} - -response = requests.post(url, headers=headers, data=json.dumps(data)) -``` - -## Action Group Endpoints - -### Get All Action Groups - -**Request**: -```http -GET /v2/api/indigo.actionGroups -Authorization: Bearer API_KEY -``` - -**Response**: -```json -[ - { - "id": 94914463, - "name": "Movie Night", - "class": "indigo.ActionGroup", - "remoteDisplay": true, - ... - }, - ... -] -``` - -### Get Single Action Group - -**Request**: -```http -GET /v2/api/indigo.actionGroups/94914463 -Authorization: Bearer API_KEY -``` - -### Execute Action Group - -**Request**: -```http -POST /v2/api/command -Authorization: Bearer API_KEY -Content-Type: application/json - -{ - "message": "indigo.actionGroup.execute", - "objectId": 94914463 -} -``` - -**curl Example**: -```bash -curl -X POST \ - -H "Authorization: Bearer API_KEY" \ - -H "Content-Type": application/json" \ - -d '{"message": "indigo.actionGroup.execute", "objectId": 94914463}' \ - https://REFLECTOR.indigodomo.net/v2/api/command -``` - -## Command Endpoint Details - -### Request Format - -```json -{ - "message": "command.name", // Required: command to execute - "objectId": 123456789, // Required: target object ID - "parameters": { // Optional: command parameters - "key": "value" - }, - "id": "tracking-id" // Optional: for request tracking -} -``` - -### Response Format - -**Success**: -```json -{ - "success": true, - "id": "tracking-id" // if you provided one -} -``` - -**Error**: -```json -{ - "error": "Error description", - "errorId": "error-code", - "id": "tracking-id" -} -``` - -### Available Commands - -See [device-commands.md](device-commands.md) for complete command reference. - -**Common Commands**: -- `indigo.device.toggle` - Toggle device on/off -- `indigo.device.turnOn` - Turn device on -- `indigo.device.turnOff` - Turn device off -- `indigo.dimmer.setBrightness` - Set dimmer level -- `indigo.variable.updateValue` - Update variable value -- `indigo.actionGroup.execute` - Run action group - -## Data Types - -### JSON ↔ Python Conversion - -| Python | JSON | Example | -|--------|------|---------| -| `True` | `true` | `"onState": true` | -| `False` | `false` | `"onState": false` | -| `None` | `null` | `"value": null` | -| `dict` | Object | `{"key": "value"}` | -| `list` | Array | `[1, 2, 3]` | -| `str` | String | `"text"` | -| `int/float` | Number | `42` or `3.14` | - -## Error Handling - -### Common HTTP Status Codes - -| Code | Meaning | Cause | -|------|---------|-------| -| 200 | OK | Request successful | -| 400 | Bad Request | Invalid JSON or missing required fields | -| 401 | Unauthorized | Invalid or missing API key | -| 404 | Not Found | Object ID doesn't exist | -| 500 | Server Error | Internal Indigo error | - -### Error Response Example - -```json -{ - "error": "Device not found", - "errorId": "device_not_found", - "id": "your-tracking-id" -} -``` - -### Python Error Handling - -```python -try: - response = requests.post(url, headers=headers, data=json.dumps(command)) - response.raise_for_status() # Raises HTTPError for bad status - result = response.json() - - if "error" in result: - print(f"Command failed: {result['error']}") - else: - print("Command successful") - -except requests.exceptions.HTTPError as e: - print(f"HTTP error: {e}") -except requests.exceptions.ConnectionError as e: - print(f"Connection error: {e}") -except json.JSONDecodeError as e: - print(f"Invalid JSON response: {e}") -``` - -## Performance Considerations - -### Polling vs WebSocket - -**HTTP Polling**: -- Simple to implement -- Higher overhead (new connection per request) -- Polling delay for updates -- Good for: Periodic checks (every few minutes) - -**WebSocket**: -- Real-time updates -- Single persistent connection -- Lower overhead for frequent updates -- Good for: Continuous monitoring - -**Recommendation**: Use HTTP for occasional commands, WebSocket for monitoring. - -### Rate Limiting - -**Best Practices**: -- Don't poll faster than once per second -- Batch multiple commands when possible -- Use WebSocket for real-time monitoring -- Cache responses when appropriate - -### Request Optimization - -```python -# Good: Reuse session -import requests - -session = requests.Session() -session.headers.update({'Authorization': f'Bearer {API_KEY}'}) - -# Make multiple requests -devices = session.get(f'{BASE_URL}/indigo.devices').json() -variables = session.get(f'{BASE_URL}/indigo.variables').json() - -# Bad: Create new session each time -# requests.get(...) creates new connection every time -``` - -## Complete Examples - -### Python Script - -```python -import requests -import json - -API_KEY = "your_api_key" -REFLECTOR = "your_reflector" -BASE_URL = f"https://{REFLECTOR}.indigodomo.net/v2/api" - -# Setup session with auth -session = requests.Session() -session.headers.update({ - 'Authorization': f'Bearer {API_KEY}', - 'Content-Type': 'application/json' -}) - -# Get all devices -devices = session.get(f'{BASE_URL}/indigo.devices').json() -print(f"Found {len(devices)} devices") - -# Find specific device -light = next(d for d in devices if 'Light' in d['name']) -print(f"Device: {light['name']}, State: {light['onState']}") - -# Control device -command = { - "message": "indigo.dimmer.setBrightness", - "objectId": light['id'], - "parameters": {"value": 50} -} -response = session.post(f'{BASE_URL}/command', data=json.dumps(command)) -print(f"Command result: {response.json()}") -``` - -### Shell Script (curl) - -```bash -#!/bin/bash - -API_KEY="your_api_key" -REFLECTOR="your_reflector" -BASE_URL="https://$REFLECTOR.indigodomo.net/v2/api" - -# Get all devices -curl -H "Authorization: Bearer $API_KEY" \ - "$BASE_URL/indigo.devices" | jq '.' - -# Toggle device -curl -X POST \ - -H "Authorization: Bearer $API_KEY" \ - -H "Content-Type: application/json" \ - -d '{"message": "indigo.device.toggle", "objectId": 123456789}' \ - "$BASE_URL/command" -``` - -## Related Documentation - -- **[Device Commands](device-commands.md)** - Complete command reference -- **[WebSocket API](websocket-api.md)** - Real-time alternative -- **[Authentication](authentication.md)** - Setup and security -- **[Overview](overview.md)** - Choosing the right API diff --git a/docs/api/overview.md b/docs/api/overview.md deleted file mode 100644 index 5a87823..0000000 --- a/docs/api/overview.md +++ /dev/null @@ -1,192 +0,0 @@ -# Integration APIs Overview - -Indigo 2025.1 provides two complementary APIs for external integration: **WebSocket API** for real-time monitoring and **HTTP API** for request/response interactions. - -## Quick Comparison - -| Feature | WebSocket API | HTTP API | -|---------|--------------|----------| -| **Connection** | Persistent, bidirectional | Stateless, request/response | -| **Updates** | Real-time push notifications | Must poll for changes | -| **Latency** | Instant (push) | Polling delay | -| **Overhead** | Single connection | Per-request connection | -| **Complexity** | Moderate (connection management) | Simple (HTTP requests) | -| **Best for** | Mobile apps, dashboards, continuous monitoring | Scripts, periodic checks, webhooks | -| **Libraries** | websockets, socket.io | requests, curl, fetch() | -| **Authentication** | Initial handshake | Per-request | -| **Bandwidth** | Efficient (only sends changes) | Higher (full responses) | - -## When to Use Each API - -### Use WebSocket API When... - -✅ Building mobile apps (iOS/Android) that need live updates -✅ Creating web dashboards with real-time device status -✅ Monitoring continuous changes (temperature sensors, motion detectors) -✅ Need instant notifications when devices change state -✅ Building chat/voice assistant integrations -✅ Developing custom control interfaces - -**Example Use Cases**: -- iOS app showing live device status with instant updates -- Web dashboard displaying real-time energy monitoring -- Custom notification system for security sensors -- Voice assistant integration (Alexa, Google Home custom skills) - -### Use HTTP API When... - -✅ Writing simple automation scripts -✅ Periodic polling (every few minutes/hours) -✅ One-off device control commands -✅ Integration with services that only support HTTP webhooks -✅ Quick prototyping and testing -✅ Non-real-time data collection - -**Example Use Cases**: -- Cron job that checks device states hourly -- Webhook receiver that triggers Indigo actions -- Simple Python script to toggle lights -- Data export to external analytics platform - -## Architecture Overview - -### WebSocket API Architecture - -``` -┌─────────────┐ ┌──────────────┐ -│ Client │◄───WebSocket───────│ Indigo │ -│ App │ │ Server │ -└─────────────┘ └──────────────┘ - ▲ │ - │ 1. Connect │ - │ 2. Subscribe to feeds │ - │ 3. Receive push updates ◄────┘ - │ 4. Send commands ─────► - │ 5. Connection stays open │ -``` - -**5 Specialized Feeds:** -- Device feed - Monitor/control devices -- Variable feed - Track variable changes -- Action group feed - Execute action groups -- Control page feed - Page management -- Log feed - Real-time logging - -### HTTP API Architecture - -``` -┌─────────────┐ ┌──────────────┐ -│ Client │────GET Request────►│ Indigo │ -│ Script │◄───Response────────│ Server │ -└─────────────┘ └──────────────┘ - │ ▲ - │ 1. Send GET/POST │ - │ 2. Receive response │ - │ 3. Connection closes │ - └───────(repeat for next request)────┘ -``` - -**Endpoints:** -- `/v2/api/indigo.devices` - Get/control devices -- `/v2/api/indigo.variables` - Get/update variables -- `/v2/api/indigo.actionGroups` - Execute action groups -- `/v2/api/command` - Generic command execution - -## Getting Started Checklist - -### Prerequisites (Both APIs) -- [ ] Indigo 2025.1 or later installed -- [ ] "Start Local Server" enabled in Indigo preferences -- [ ] API authentication enabled (OAuth/API Keys) -- [ ] API key created in Indigo Account portal -- [ ] Network access to Indigo server (local or via Reflector) - -### For WebSocket Development -- [ ] WebSocket library installed (`pip install websockets` or `npm install websocket`) -- [ ] Understanding of async/event-driven programming -- [ ] Connection management strategy (reconnection on disconnect) -- [ ] Message parsing logic for add/patch/delete messages - -### For HTTP Development -- [ ] HTTP library available (requests, curl, fetch, etc.) -- [ ] Basic understanding of REST APIs -- [ ] JSON parsing capability -- [ ] (Optional) Polling interval if monitoring changes - -## Authentication - -Both APIs support the same authentication methods: - -**API Key Authentication (Recommended)**: -- Created in Indigo Account portal -- Include in `Authorization: Bearer {API_KEY}` header -- Can be revoked instantly if compromised -- Granular permission control - -**Local Secrets (Local Network Only)**: -- Pre-shared secret in Indigo preferences -- Only for trusted local network clients -- No per-user tracking - -→ See [authentication.md](authentication.md) for complete setup guide - -## Connection Security - -### Local Network -- `ws://` or `http://` - Unencrypted (use cautiously, only on trusted networks) -- `wss://` or `https://` - **Local HTTPS** with self-signed certificate (encrypted, recommended) -- Direct connection to Indigo server IP on port 8176 -- Fast, low latency - -Indigo's local server supports HTTPS with a self-signed TLS certificate. Clients must explicitly trust the self-signed cert (e.g., disable certificate verification for local connections only). This provides encryption without requiring the Reflector. - -### Remote Access (Indigo Reflector) -- `wss://` or `https://` - TLS encrypted with valid certificate (recommended) -- Connection via `{REFLECTOR}.indigodomo.net` -- Secure over internet -- Slightly higher latency - -→ See [authentication.md](authentication.md) for security best practices and self-signed certificate handling - -## Next Steps - -1. **Choose your API** based on use case above -2. **Set up authentication**: [authentication.md](authentication.md) -3. **Start developing**: - - WebSocket: [websocket-api.md](websocket-api.md) - - HTTP: [http-api.md](http-api.md) -4. **Command reference**: [device-commands.md](device-commands.md) - -## API Feature Parity - -While both APIs provide access to core Indigo functionality (devices, variables, action groups), there are some differences: - -### Known Differences: -- **Folders**: HTTP API includes folder objects; WebSocket handles folders differently -- **Command Availability**: Some commands may be specific to one transport -- **Data Structure**: Response formats are generally consistent but may vary in edge cases - -**Recommendation:** For critical functionality, test with your target transport to verify command availability. The official Indigo wiki may not fully document all differences. - -## Can I Use Both? - -**Yes!** Many applications combine both APIs: - -**Example: Web Dashboard** -- HTTP GET on page load to get initial device list -- WebSocket connection for real-time updates -- HTTP POST for commands (or use WebSocket) - -**Example: Monitoring with Alerts** -- WebSocket for real-time monitoring -- HTTP webhook to trigger external alerts - -**Best Practice**: Use WebSocket for monitoring, HTTP for occasional commands or as fallback. - -## Related Documentation - -- **[WebSocket API Reference](websocket-api.md)** - Complete WebSocket documentation -- **[HTTP API Reference](http-api.md)** - Complete HTTP documentation -- **[Authentication Guide](authentication.md)** - Setup and security -- **[Device Commands](device-commands.md)** - All available commands -- **[Indigo Object Model](../plugin-dev/api/indigo-object-model.md)** - Understanding device objects diff --git a/docs/api/websocket-api.md b/docs/api/websocket-api.md deleted file mode 100644 index 52e22ee..0000000 --- a/docs/api/websocket-api.md +++ /dev/null @@ -1,600 +0,0 @@ -# WebSocket API - -Complete reference for Indigo's WebSocket API - real-time bidirectional communication for monitoring devices, variables, action groups, and logs. - -## Quick Reference - -| Feed | Endpoint | Purpose | Read | Write | -|------|----------|---------|------|-------| -| Device | `/v2/api/ws/device-feed` | Monitor/control devices | ✓ | ✓ | -| Variable | `/v2/api/ws/variable-feed` | Track variables | ✓ | ✓ | -| Action Group | `/v2/api/ws/action-feed` | Execute action groups | ✓ | ✓ | -| Control Page | `/v2/api/ws/page-feed` | Page management | ✓ | ✗ | -| Log | `/v2/api/ws/log-feed` | Real-time logging | ✓ | ✓ | - -## Connection Lifecycle - -### Phase 1: Connect - -**Local Network**: -```python -import websockets -import json - -uri = "ws://192.168.1.100:8176/v2/api/ws/device-feed" -headers = {'Authorization': f'Bearer {API_KEY}'} - -async with websockets.connect(uri, extra_headers=headers) as ws: - # Connected -``` - -**Remote (Reflector)**: -```python -uri = f"wss://{REFLECTOR}.indigodomo.net/v2/api/ws/device-feed" -``` - -### Phase 2: Request Initial Data - -Send refresh message to get existing objects: - -```python -await ws.send(json.dumps({ - "message": "refresh", - "objectType": "indigo.Device" -})) - -# Receive response with all devices -response = json.loads(await ws.recv()) -# response["list"] contains array of all device objects -``` - -### Phase 3: Process Updates - -Listen for add/patch/delete messages: - -```python -async for message in ws: - data = json.loads(message) - - if data["message"] == "add": - # New device or device enabled for remote display - device = data["objectDict"] - - elif data["message"] == "patch": - # Device property changed - device_id = data["objectId"] - patch = data["patch"] # dictdiffer format - - elif data["message"] == "delete": - # Device removed or remote display disabled - device_id = data["objectId"] -``` - -### Phase 4: Send Commands - -```python -# Toggle device -await ws.send(json.dumps({ - "message": "indigo.device.toggle", - "objectId": 123456789, - "id": "optional-tracking-id" -})) -``` - -### Phase 5: Disconnect - -```python -await ws.close() -``` - -## Message Types (Server → Client) - -### Add Message - -Sent when: -- New object created in Indigo -- Existing object's `remoteDisplay` set to `true` - -```json -{ - "message": "add", - "objectType": "indigo.Device", - "objectDict": { - "id": 123456789, - "name": "Living Room Light", - "class": "indigo.DimmerDevice", - "brightness": 75, - "onState": true, - ... - } -} -``` - -### Patch Message - -Sent when object properties change. Uses [dictdiffer](https://github.com/inveniosoftware/dictdiffer) format: - -```json -{ - "message": "patch", - "objectType": "indigo.Device", - "objectId": 123456789, - "patch": [ - ["change", "brightness", [50, 75]], - ["change", "onState", [false, true]] - ] -} -``` - -**Patch Format**: `[operation, path, values]` -- `"change"` - Property modified: `[oldValue, newValue]` -- `"add"` - Property added: `[(key, value)]` -- `"remove"` - Property removed: `[(key, value)]` - -### Delete Message - -Sent when: -- Object deleted from Indigo -- Object's `remoteDisplay` set to `false` - -```json -{ - "message": "delete", - "objectType": "indigo.Device", - "objectId": 123456789 -} -``` - -### Refresh Response - -Server reply to refresh requests: - -```json -{ - "message": "refresh", - "objectType": "indigo.Device", - "list": [ - { "id": 123, "name": "Device 1", ... }, - { "id": 456, "name": "Device 2", ... } - ], - "id": "your-tracking-id" // if you sent one -} -``` - -**Single object refresh**: -```json -{ - "message": "refresh", - "objectType": "indigo.Device", - "objectDict": { "id": 123456789, ... } -} -``` - -## Device Feed - -### Refresh All Devices - -```json -{ - "message": "refresh", - "objectType": "indigo.Device" -} -``` - -### Refresh Single Device - -```json -{ - "message": "refresh", - "objectType": "indigo.Device", - "objectId": 123456789 -} -``` - -### Send Device Commands - -See [device-commands.md](device-commands.md) for complete command reference. - -```json -{ - "message": "indigo.device.toggle", - "objectId": 123456789 -} -``` - -```json -{ - "message": "indigo.dimmer.setBrightness", - "objectId": 123456789, - "parameters": { - "value": 50, - "delay": 0 - } -} -``` - -## Variable Feed - -### Variable Object Structure - -```json -{ - "class": "indigo.Variable", - "id": 345633244, - "name": "house_status", - "value": "home", - "readOnly": false, - "description": "", - "folderId": 0, - "remoteDisplay": true, - "globalProps": {}, - "pluginProps": {}, - "sharedProps": {} -} -``` - -### Update Variable Value - -```json -{ - "message": "indigo.variable.updateValue", - "objectId": 345633244, - "parameters": { - "value": "away" - } -} -``` - -**Important**: Parameter values must be **strings**. Empty string `""` clears the value. - -### Refresh Variables - -```json -{ - "message": "refresh", - "objectType": "indigo.Variable" -} -``` - -## Action Group Feed - -### Action Group Object - -```json -{ - "class": "indigo.ActionGroup", - "id": 94914463, - "name": "Movie Night", - "description": "", - "folderId": 532526508, - "remoteDisplay": true, - "globalProps": {}, - "pluginProps": {}, - "sharedProps": {} -} -``` - -### Execute Action Group - -```json -{ - "message": "indigo.actionGroup.execute", - "objectId": 94914463 -} -``` - -### Refresh Action Groups - -```json -{ - "message": "refresh", - "objectType": "indigo.ActionGroup" -} -``` - -## Control Page Feed - -**Read-only feed** - used for managing a list of available control pages. No command messages can be sent; its purpose is to use incoming messages to manage a list of available control pages which the user can select to open. - -**Connection URLs**: -``` -ws://192.168.1.100:8176/v2/api/ws/page-feed -wss://{REFLECTOR}.indigodomo.net/v2/api/ws/page-feed -``` - -### Rendering Control Pages in a Browser/WebView - -To display a control page's rendered content, load the following URL in a browser or WKWebView: - -``` -http://192.168.1.100:8176/web/controlpage.html?id={pageID} -https://{REFLECTOR}.indigodomo.net/web/controlpage.html?id={pageID} -``` - -For example, a page with ID `963336187` would be: -``` -https://{REFLECTOR}.indigodomo.net/web/controlpage.html?id=963336187 -``` - -Authentication is required via `Authorization: Bearer {API_KEY}` header. In a WKWebView, inject the header using a `URLRequest`: - -```swift -var request = URLRequest(url: url) -request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") -webView.load(request) -``` - -### Control Page Object Structure - -```json -{ - "class": "indigo.ControlPage", - "backgroundImage": "", - "description": "", - "folderId": 0, - "globalProps": {}, - "hideTabBar": true, - "id": 963336187, - "name": "Weather Images", - "pluginProps": {}, - "remoteDisplay": true, - "sharedProps": {} -} -``` - -### Refresh Control Pages - -```json -{ - "message": "refresh", - "objectType": "indigo.ControlPage" -} -``` - -### Add Control Page Message - -Received when a new control page is added to the Indigo Server after the connection is opened, or when a control page's `remoteDisplay` property is changed from `false` to `true`. - -```json -{ - "message": "add", - "objectType": "indigo.ControlPage", - "objectDict": { ... } -} -``` - -### Update Control Page Message (Patch) - -Received when a control page has been updated on the Indigo server. Does not allow users to update control pages via the WebSocket API. - -```json -{ - "message": "patch", - "objectType": "indigo.ControlPage", - "objectId": 963336187, - "patch": [["change", "name", ["Old Name", "New Name"]]] -} -``` - -Patch objects use `dictdiffer` format. See the Object Patches section above for details on applying patches. - -### Delete Control Page Message - -Received when a control page is deleted from the Indigo server, or when a control page's `remoteDisplay` property is set from `true` to `false`. - -```json -{ - "message": "delete", - "objectType": "indigo.ControlPage", - "objectId": 123456789 -} -``` - -## Log Feed - -**Special behavior**: -- Upon connection, receives last **25 chronological messages** -- Then receives real-time log updates -- Only accepts `"add"` messages (read-only for logs) - -### Log Entry Message (received) - -Each log entry arrives as an `"add"` message with the log data in `objectDict`: - -```json -{ - "message": "add", - "objectDict": { - "message": "sent \"Living Room Pendant\" set brightness to 30", - "timeStamp": "2026-02-23T20:47:10.617000", - "typeStr": "Z-Wave", - "typeVal": 8 - } -} -``` - -**objectDict fields** (all camelCase): - -| Field | Type | Description | -|-------|------|-------------| -| `message` | String | The log message text | -| `timeStamp` | String | Timestamp in `yyyy-MM-dd'T'HH:mm:ss.SSSSSS` format (local server time, no timezone indicator). **Note the capital S** — this is camelCase `timeStamp`, not `timestamp`. | -| `typeStr` | String | Source/plugin name (e.g. `"Z-Wave"`, `"Web Server Warning"`, `"Heatmiser-Neo Error"`) | -| `typeVal` | Int | Log level numeric value (1 = error, 3 = warning, 8 = info) | - -> **Important**: The WebSocket log feed uses **camelCase** keys (`timeStamp`, `typeStr`, `typeVal`), which differ from both the Python API's PascalCase (`TimeStamp`, `TypeStr`, `TypeVal`) and all-lowercase conventions. The `typeStr` suffix indicates severity: names ending in `"Error"` are errors, `"Warning"` are warnings, `"Debug"` are debug messages. - -### Send Log Message - -```json -{ - "message": "indigo.server.log", - "messageText": "Custom log entry from external app" -} -``` - -## Object Folders - -### Refresh Folder List - -```json -{ - "message": "refresh", - "objectType": "indigo.Device.Folder" -} -``` - -Response: -```json -{ - "message": "refresh", - "objectType": "indigo.Device.Folder", - "list": [ - {"id": 123, "name": "Living Room", "remoteDisplay": true}, - {"id": 456, "name": "Bedroom", "remoteDisplay": true} - ] -} -``` - -**Other folder types**: -- `"indigo.Variable.Folder"` -- `"indigo.ActionGroup.Folder"` - -## Error Handling - -### Generic Error Response - -```json -{ - "error": "Error description", - "errorId": "error-code" -} -``` - -### Connection Failures - -**Causes**: -- Authentication failure -- Server not reachable -- Network timeout - -**Best Practice**: Implement reconnection logic with exponential backoff - -```python -import asyncio - -async def connect_with_retry(uri, headers, max_retries=5): - retry_delay = 1 - for attempt in range(max_retries): - try: - ws = await websockets.connect(uri, extra_headers=headers) - return ws - except Exception as e: - if attempt < max_retries - 1: - await asyncio.sleep(retry_delay) - retry_delay *= 2 # Exponential backoff - else: - raise -``` - -## Complete Example (Python) - -```python -import asyncio -import websockets -import json - -API_KEY = "your_api_key" -REFLECTOR = "your_reflector" - -async def monitor_devices(): - uri = f"wss://{REFLECTOR}.indigodomo.net/v2/api/ws/device-feed" - headers = {'Authorization': f'Bearer {API_KEY}'} - - async with websockets.connect(uri, extra_headers=headers) as ws: - # Request all devices - await ws.send(json.dumps({ - "message": "refresh", - "objectType": "indigo.Device" - })) - - # Process messages - async for message in ws: - data = json.loads(message) - - if data["message"] == "refresh": - devices = data.get("list", []) - print(f"Received {len(devices)} devices") - - elif data["message"] == "add": - device = data["objectDict"] - print(f"Device added: {device['name']}") - - elif data["message"] == "patch": - print(f"Device {data['objectId']} updated") - for change in data["patch"]: - op, prop, values = change - if op == "change": - print(f" {prop}: {values[0]} → {values[1]}") - - elif data["message"] == "delete": - print(f"Device {data['objectId']} removed") - -asyncio.run(monitor_devices()) -``` - -## Complete Example (JavaScript/Node.js) - -```javascript -const WebSocket = require('ws'); - -const API_KEY = 'your_api_key'; -const REFLECTOR = 'your_reflector'; - -const ws = new WebSocket(`wss://${REFLECTOR}.indigodomo.net/v2/api/ws/device-feed`, { - headers: { 'Authorization': `Bearer ${API_KEY}` } -}); - -ws.on('open', function() { - // Request all devices - ws.send(JSON.stringify({ - message: 'refresh', - objectType: 'indigo.Device' - })); -}); - -ws.on('message', function(data) { - const msg = JSON.parse(data); - - if (msg.message === 'refresh') { - console.log(`Received ${msg.list.length} devices`); - } else if (msg.message === 'add') { - console.log(`Device added: ${msg.objectDict.name}`); - } else if (msg.message === 'patch') { - console.log(`Device ${msg.objectId} updated`); - } else if (msg.message === 'delete') { - console.log(`Device ${msg.objectId} removed`); - } -}); - -ws.on('error', function(error) { - console.error('WebSocket error:', error); -}); -``` - -## Best Practices - -1. **Connection Management**: Implement automatic reconnection with exponential backoff -2. **Message Tracking**: Use optional `id` field to track request/response pairs -3. **Patch Handling**: Use dictdiffer library to apply patches efficiently -4. **Error Logging**: Log all errors with context for debugging -5. **Resource Cleanup**: Always close WebSocket connections properly -6. **Rate Limiting**: Don't send commands faster than devices can process -7. **Object Caching**: Maintain local copy of objects, apply patches incrementally - -## Related Documentation - -- **[Device Commands](device-commands.md)** - All available device commands -- **[HTTP API](http-api.md)** - Alternative REST API -- **[Authentication](authentication.md)** - Setup and security -- **[Overview](overview.md)** - When to use WebSocket vs HTTP diff --git a/docs/plans/2026-07-09-canonical-docs-audit.md b/docs/plans/2026-07-09-canonical-docs-audit.md new file mode 100644 index 0000000..d8f817a --- /dev/null +++ b/docs/plans/2026-07-09-canonical-docs-audit.md @@ -0,0 +1,78 @@ +# indigo-claude-plugin — Docs Audit vs. Indigo 2025.2 canonical (`llms-full.txt`) + +Date: 2026-07-09. Canonical source: https://docs.indigodomo.com/llms-full.txt (20,624 lines). +Method: 4 parallel agents compared our ~11.3k lines of bundled docs against the rewritten canonical. +**Context: Indigo rewrote the docs completely but did NOT change functionality.** So every "inaccuracy" +below is a *pre-existing* error in our docs the clearer canonical surfaced — not a behaviour change. + +--- + +## TIER 1 — HIGH: actively wrong, produces broken code. Fix regardless of refactor scope. + +### API skill (`skills/api/SKILL.md` — the always-loaded file, currently ~100% wrong on command shape) +1. `PUT /v2/api/indigo.devices/` — **does not exist.** Only write path is `POST /v2/api/command`. (canon 52-64; PUT is the *deprecated* old-REST form, canon 1850) +2. Command envelope wrong in every example: `{"name":"device.turnOn","parameters":{"id":123}}` → real is `{"message":"indigo.device.turnOn","objectId":123,"parameters":{...}}`. Three errors: `message` not `name`; namespaced `indigo.device.*` names; top-level `objectId` not `parameters.id`. (canon 243-248, 1036-1085). Note our deeper files http-api.md/device-commands.md get this right — SKILL.md contradicts them. +3. WebSocket wrong: bare `/v2/api/ws` isn't a feed (use `/v2/api/ws/device-feed` etc., 7 feeds); subscribe `{"name":"device","id":..}` should be `{"message":"refresh","objectType":"indigo.Device","objectId":..}`. (canon 2403-2411, 2770-2778) +4. `?detail=true` — **fabricated.** GET always returns full objects. (no such param in canon) +5. Invented `errorId` codes (`device_not_found`, `unknown_command`…) in http-api.md/device-commands.md → real error model is `error` + `validationErrors` dict + passthrough `id`. (canon 1704-1754) +6. Thermostat command family **entirely missing** from device-commands.md (setHeat/CoolSetpoint, setHvacMode, setFanMode + `indigo.kHvacMode.*`/`indigo.kFanMode.*`). (canon 1284-1405) + +### Plugin authoring +7. **super() story is backwards.** We tell authors `super().deviceStartComm/StopComm()` is "required/recommended" (SKILL.md:183, quick-start.md:183, plugin-lifecycle.md:263/296/656) — canonical treats these as override hooks (no super needed). The methods that DO need the base call — `deviceUpdated`/`triggerUpdated`/`deviceCreated`/`deviceDeleted` — are where we under-emphasize it. (canon 4090-4124, 5484-5518, 7274-7278) +8. `validateDeviceConfigUi` success-return documented wrong in `devices.md:230-254` (docstring says 3-tuple always). Real: success = `True` | `(True, valuesDict)` | `(True, valuesDict, msgDict)`; failure = `(False, valuesDict, errorDict)`. Our own configui.md:347 gets it right — devices.md contradicts it. (canon 6867-6973) +9. MenuItem ConfigUI callback wrong: `menu-items.md:75` takes `(values_dict, menu_item_id)` and returns `None` → must be `(valuesDict, typeId)` returning `True` or `(False, valuesDict, errorsDict)`. Returning None is a real bug. (canon 6155-6172) +10. **`secure="true"` "stored encrypted" is FALSE** (configui.md:20,:169 + echoes). Canonical (6827): "not stored securely — solely masks the value in the UI." Security-misleading. (canon 6827) + +### IOM / scripting reference +11. `indigo.actionGroup.enable()` — **doesn't exist** (command-namespaces.md:163, :387). Action groups have no enabled state. (canon 10963-11076) +12. `DeviceStateChangeTrigger` missing the two props you MUST set: `stateChangeType`, `stateSelectorIndex` (triggers.md:40) — a plugin following our doc can't build the trigger. (canon 13005-13009, 13096-13102) +13. `InsteonCommandReceivedTrigger.address` wrong (triggers.md:72) → real props `deviceId`, `command`, `commandSourceType`, `buttonOrGroup`. (canon 13161-13166) +14. `indigo.server.error()` — **doesn't exist** (command-namespaces.md:249) → `log(..., isError=True)`. Our api-patterns.md already does it right. (canon 12618-12639) + +--- + +## TIER 2 — MED: stale/misleading, should fix. + +**Version currency (pervasive — a global sweep):** +- Platform stated as 2023.2 / 2025.1 / Python 3.10 / API 3.0 → should be **2025.2 / Python 3.13 / API 3.8**. Files: README.md:133, reference/README.md:40-42, troubleshooting/README.md:12, SDK-CLAUDE.md:40, common-issues.md:14-15, examples/README.md:88. (Python 3 = API **3.4** @ 2023.2, not 3.0.) Our own common-issues.md:348 already has 3.13 right — inconsistent internally. +- **Stale external links** in `commands/dev.md:194-195` and `commands/api.md:162`: point at old `wiki.indigodomo.com/doku.php?id=indigo_2025.1_documentation:*` → moved to `docs.indigodomo.com/2025.2/*`. + +**API:** +- Auth model omits HTTP Digest; reframe local secrets as "a special kind of API key" (not a separate prefs secret); portal path is account **Authorizations** not "API Keys". (canon 8, 69, 2434) +- `enable` device param is optional, we say required (device-commands.md:169). Sprinkler `run`/`stop`/`setActiveZone` missing. Plugin messaging (`plugin.restart`, `plugin.executeAction`) missing. schedule-feed/trigger-feed missing from WS feed table. Typo: http-api.md:285 stray quote. + +**Authoring:** +- `body_params` is form-POST-only; JSON POST authors get empty dict — must document `request_body`/`headers` (http-responder.md). (canon 4967-4979) +- Device `subType` + subtype enums (`kDeviceSubType` Lock/Presence/GarageController…) + `allowUserCreation` missing from devices.md — **directly relevant to Domio lock/presence/garage work**. (canon 5827-5869) +- Custom-device state ValueTypes: add `List`, canonical set is Boolean/Number/List/String/Separator (devices.md:82). (canon 5924) + +**IOM:** +- `getPluginList()` returns enabled plugin *objects*, not IDs (command-namespaces.md:292). (canon 12542) +- `displayStateValue` → `displayStateValRaw`/`displayStateValUi` (devices.md:47). (canon 15115) +- Sprinkler `zoneDurations` → `zoneMaxDurations` (devices.md:132). `speedLabels` likely nonexistent (verify). Folder `getId` on `.folders` not `.folder` (architecture.md:147). + +--- + +## TIER 3 — GAPS: net-new coverage worth adding. + +- **Webhooks** — entirely absent, a first-class integration surface (Webhook trigger type, `/webhook/?api-key=`, JSON/FORM/GET, event_data). (canon 2098-2397) +- **REST→HTTP migration** guidance — absent. (canon 1794-2096) +- **`indigo.utils.ValidationError`** — the modern validation idiom, recurs in BOTH authoring (ConfigUI/HTTP) and IOM. We still hand-roll error dicts everywhere. (canon 4981-5007, 6975-7006, 13565-13615) +- **`substitute*()`, `getDeviceStateList()`/`getDeviceDisplayStateId()`** — common authoring APIs, missing. (canon 4203-4281, 4811-4925) +- **Dev environment**: symlink workflow (canonical's *recommended* approach; we tell users to `cp`), debuggers (pdb/PuDB/PyCharm — we say "can't use debuggers"), Python-conflicts page. (canon 3634-3785, 20564-20624) +- **Device base-class props**: energy* group, errorState, sharedProps, supportsOnState. Subclass gaps: ledStates, MultiIO *Count, thermostat supports*/humidifier, sprinkler paused*. (canon 15106-15141, 14138-15055) +- **Constants**: kAllDeviceSel, kInsteonCmd, kStateChange/kStateSelector, kDeviceSourceType, extra kStateImageSel icons. (canon 13017-13199, 15157-15479) +- Plugin management: status-dot legend, uninstall procedure, Plugin Store submission, upgrade path. (canon 17562-17689, 19747-19844) + +--- + +## KEEP AS-IS (ahead of canonical — no change) +`patterns/testing.md`, `patterns/README.md`, `workflows/README.md`, `patterns/open-source-contributing.md`, `examples/sdk-examples-guide.md`, `events.md` (event-data section). + +## REFRAME +`reference/Python3-Migration-Guide.md` — still correct & live-linked from the SDK, but audience shrank to legacy Py2→3 porters. Demote from "Essential reading" (reference/README.md:22, README.md:134) to "legacy porting reference." + +--- + +## STRATEGIC ROOT CAUSE +Indigo now publishes `llms-full.txt` — a single, always-current, LLM-optimised canonical. We ship ~11k lines of hand-derived docs that (a) drift, (b) fabricate specifics (items 1-6, 10-14 are invented, not just stale), (c) have gaps. Maintaining a parallel derivative is the drift trap. But we can't just point Claude at a 1.5MB file (context blow-up), and our real value-add is curation the canonical lacks: workspace conventions, testing patterns, CI workflows, SDK examples, query-routing. → the answer is a **hybrid**, not "delete ours" or "keep grinding derivatives." diff --git a/docs/plans/2026-07-09-canonical-hybrid-restructure.md b/docs/plans/2026-07-09-canonical-hybrid-restructure.md new file mode 100644 index 0000000..9a6c0f4 --- /dev/null +++ b/docs/plans/2026-07-09-canonical-hybrid-restructure.md @@ -0,0 +1,115 @@ +# Design: Canonical-docs hybrid restructure — indigo-claude-plugin + +Status: PROPOSED (2026-07-09). Supersedes the "hand-maintain derivative docs" model. +Companion: `indigo-docs-audit.md` (the gap analysis this responds to). + +## Problem +We hand-maintain ~11.3k lines of derivative Indigo docs. The audit found they (a) drift +(version numbers, moved links), (b) **fabricate** specifics that never existed (wrong API +command envelope, `?detail=true`, `PUT` verb, `errorId` codes, `indigo.actionGroup.enable()`, +`indigo.server.error()`, `InsteonCommandReceivedTrigger.address`), and (c) have real gaps +(webhooks, ValidationError, subType enums, dev-env). Indigo now publishes a canonical +`llms-full.txt` + `llms.txt` index — always current, version-pegged, LLM-optimised. + +## Principles +1. **Canonical is the single source of truth for reference facts.** We never hand-write API + surface (methods, endpoints, XML attrs, constants) again — we vendor it verbatim. +2. **Local + incremental discovery.** Split the monolith into small per-page files a skill + loads on demand (progressive disclosure) — never a 167KB webpage or 1.5MB monolith in context. +3. **Our docs shrink to what canonical lacks:** routing, workspace conventions/opinion, testing + patterns, CI workflows, SDK examples, corrected quick-reference. Curation, not duplication. +4. **Drift is caught mechanically,** not by memory — a refresh script + optional CI check. + +## What Indigo actually publishes (verified 2026-07-09) +- `https://docs.indigodomo.com/llms.txt` — 25KB index: `[Title](url): description` per page. +- `https://docs.indigodomo.com/llms-full.txt` — 1.5MB, sections delimited `--- Title (URL) ---`. +- Per-page `.md` → 404. Per-page HTML → 167KB each (rejected: too heavy, not machine-clean). +- URLs are version-pegged: `.../2025.2/...`. + +## Target architecture + +``` +indigo-claude-plugin/ +├── reference/ +│ └── canonical/ # GENERATED — never hand-edit +│ ├── VERSION # indigo version + source URLs + fetch date + sha256(llms-full) +│ ├── INDEX.md # routing manifest built from llms.txt (title · desc · local path) +│ ├── api/ +│ │ ├── http.md messages.md websocket.md webhooks.md rest-migration.md index.md +│ ├── plugin-dev/ +│ │ ├── guide.md sdk-examples.md +│ │ └── reference/ +│ │ ├── dev-environment.md +│ │ ├── plugin-py/*.md # device-methods, general-methods, helper-methods, +│ │ │ # http-requests, logging, message-flow, trigger-methods, +│ │ │ # variable-methods, additional-topics, properties +│ │ └── xml/*.md + xml/configui/*.md +│ └── scripting/ +│ └── reference/*.md + device-subclasses/*.md + devices/*.md +├── tools/ +│ └── refresh_canonical.py # NEW — fetch, split, index, version, --check drift mode +├── skills/ # THIN: opinion + corrected quick-ref + routing INTO canonical +├── commands/ # THIN: same, plus fixed external links +└── docs/ # KEEP only genuinely-additive hand docs (below) +``` + +Path mirrors the URL after the version segment, so `.../2025.2/api/http/` → `canonical/api/http.md`. + +## The refresh tool (`tools/refresh_canonical.py`, stdlib only) +- `python3 tools/refresh_canonical.py` → + 1. GET `llms.txt` + `llms-full.txt`. + 2. Derive version from the URL path segment (`2025.2`). + 3. Split `llms-full.txt` on `^--- (?P.+) \((?P<url>http\S+)\) ---$`; each section → a file + at the URL-derived path under `reference/canonical/`. + 4. Prepend a 2-line "GENERATED from <url> — do not edit" header to each file. + 5. Write `INDEX.md` from `llms.txt` (title, description, → local path). + 6. Write `VERSION` (version, source URLs, UTC fetch date, sha256 of llms-full.txt). +- `--check` → re-fetch, re-split to a temp dir, diff against committed `reference/canonical/`; + exit 1 on drift (for an optional scheduled CI job / release step). Prints changed pages. +- No third-party deps (urllib + re + hashlib + pathlib). Runs anywhere. + +## Migration map (from the audit) + +DELETE (pure duplication / fabrication — replaced by canonical): +- `docs/api/{http-api,websocket-api,device-commands,authentication,overview,README}.md` +- `docs/plugin-dev/api/iom/*.md`, `docs/plugin-dev/api/indigo-object-model.md`, `docs/plugin-dev/api/README.md` +- Fabrication-heavy concepts we can't trust: fold `concepts/{devices,actions,events,menu-items,configui,plugin-preferences,http-responder}.md` + content into thin routers OR delete where they only duplicate canonical. + +KEEP AS-IS (audit: ahead of canonical): +- `docs/plugin-dev/patterns/{testing,open-source-contributing,README}.md` +- `docs/plugin-dev/workflows/*` · `docs/plugin-dev/examples/sdk-examples-guide.md` + +KEEP + REWRITE THIN (opinionated on-ramp, corrected, routes into canonical): +- `docs/plugin-dev/quick-start.md` — the "how we build plugins here" on-ramp (fix versions/super()). +- `docs/plugin-dev/concepts/plugin-lifecycle.md` — keep the narrative, fix the super() story, point + method-level detail at `canonical/plugin-dev/reference/plugin-py/*`. +- `docs/plugin-dev/troubleshooting/common-issues.md` — workspace-specific gotchas (keep), version-fix, + route env/debugger/python-conflicts detail into canonical. + +REFRAME: +- `reference/Python3-Migration-Guide.md` → "legacy Py2→3 porting reference" (demote, don't delete). + +REWRITE (routing + corrected quick-ref → canonical): +- `skills/api/SKILL.md`, `skills/dev/SKILL.md`, `commands/api.md`, `commands/dev.md`. + Fix every Tier-1 inline error; replace `docs/...` routing tables with `reference/canonical/...`; + fix stale wiki links → `docs.indigodomo.com/2025.2/*`. + +## Correctness fixes fold in for free +Everything fabricated lived in the DELETE set → gone by construction. The only hand-fixes left are +the inline examples in the 4 rewritten routing files (Tier-1 items 1-3, 7, 10) + version sweep (Tier-2). + +## Phasing (each a reviewable PR; version-bump per repo rule) +- **P1 — Vendor + tool.** Add `refresh_canonical.py`, generate `reference/canonical/**`, INDEX, VERSION. + Pure addition, nothing wired yet. (Also lets us eyeball the split before deleting anything.) +- **P2 — Rewrite the 4 routing files** (api/dev SKILL + commands) to point at canonical + fix Tier-1 + inline errors + stale links. This alone kills the highest-traffic wrong content. +- **P3 — Prune + thin** the DELETE / REWRITE-THIN doc sets; reframe the migration guide; version sweep. +- **P4 — (optional)** CI: scheduled `refresh_canonical.py --check` → open an issue/PR on drift. + +## Open choices for Simon +- A. concepts/ narrative docs: **thin-and-keep** (pedagogical on-ramp) vs **delete** (lean, pure routing)? + Default proposed: thin-and-keep lifecycle + quick-start; delete the rest. +- B. Pin to `2025.2` explicitly, or make the tool track "latest published"? Default: pin, bump on Indigo release. +- C. P4 drift-CI now or later? Default: later (P1-P3 first). +``` diff --git a/docs/plugin-dev/README.md b/docs/plugin-dev/README.md index 065d65e..03b4c97 100644 --- a/docs/plugin-dev/README.md +++ b/docs/plugin-dev/README.md @@ -1,134 +1,49 @@ # Indigo Plugin Development Documentation -Optimized documentation for building Indigo plugins. +Guidance for building Indigo plugins. **Reference facts are vendored verbatim from Indigo's +published docs** under `../../reference/canonical/**` (generated by `../../tools/refresh_canonical.py`, +pinned to Indigo 2025.2). The files here are workspace **on-ramps and patterns** — the opinionated +"how we build plugins" layer that canonical doesn't provide. ## Start Here -### New to Indigo Plugin Development? +- **[Quick Start](quick-start.md)** — create your first plugin (on-ramp) +- **[Plugin Lifecycle](concepts/plugin-lifecycle.md)** — lifecycle narrative + the `super()` rules -**[Quick Start Guide](quick-start.md)** - Create your first plugin in minutes +## What lives where -## Documentation Structure +| Need | Go to | +|------|-------| +| Getting started / scaffolding | `quick-start.md` | +| Lifecycle, `super()` footguns | `concepts/plugin-lifecycle.md` | +| State updates, `replaceOnServer` patterns | `patterns/api-patterns.md` | +| Testing (pytest mocks, TestingBase) | `patterns/testing.md` | +| Contributing to IndigoDomotics open source | `patterns/open-source-contributing.md` | +| Troubleshooting (workspace-specific) | `troubleshooting/common-issues.md` | +| SDK example catalog | `examples/sdk-examples-guide.md` | +| CI workflow templates | `workflows/` | +| **Any reference fact** (methods, XML, IOM, constants) | `../../reference/canonical/INDEX.md` | -| Section | Purpose | -|---------|---------| -| [Concepts](concepts/) | Plugin lifecycle, devices, events, preferences | -| [API Reference](api/) | Indigo Object Model reference | -| [Patterns](patterns/) | Implementation patterns and best practices | -| [Examples](examples/) | SDK example plugin catalog | -| [Troubleshooting](troubleshooting/) | Common issues and solutions | +## Canonical reference quick map -## Quick Navigation by Use Case +- Plugin authoring: `../../reference/canonical/plugin-dev/reference/` (plugin-py methods, xml/, configui/) +- Dev environment (symlink workflow, debuggers): `../../reference/canonical/plugin-dev/reference/dev-environment.md` +- Indigo Object Model (scripting): `../../reference/canonical/scripting/` +- Integration APIs (client-side): `../../reference/canonical/api/` — but for client apps prefer `/indigo:api` -### "How do I create a device?" +## For Claude (context optimization) -1. **Start**: [Concepts → Devices](concepts/devices.md) - Device types, Devices.xml, ConfigUI -2. **API details**: [API → IOM → Devices](api/iom/devices.md) - Device class properties/methods -3. **Example**: [SDK Examples Guide](examples/sdk-examples-guide.md) - -### "What device properties/methods exist?" - -→ [API → IOM → Devices](api/iom/devices.md) - -### "How do I handle device lifecycle?" - -→ [Concepts → Plugin Lifecycle](concepts/plugin-lifecycle.md#device-lifecycle-callbacks) - -### "How do I update device state?" - -→ [Patterns → API Patterns](patterns/api-patterns.md#device-state-updates) - -### "How do I save plugin preferences?" - -→ [Concepts → Plugin Preferences](concepts/plugin-preferences.md) - -### "How do I create custom trigger events?" - -→ [Concepts → Custom Events](concepts/events.md) - -### "How do I iterate/filter devices?" - -→ [API → IOM → Filters](api/iom/filters.md) - -### "How does replaceOnServer work?" - -→ [API → IOM → Architecture](api/iom/architecture.md) - -### "How do I subscribe to changes?" - -→ [API → IOM → Subscriptions](api/iom/subscriptions.md) - -### "What constants/icons are available?" - -→ [API → IOM → Constants](api/iom/constants.md) - -## By Device Type - -| Device | Example | -|--------|---------| -| Custom device | [Example Device - Custom](examples/sdk-examples-guide.md#example-device---custom) | -| Lights/switches | [Example Device - Relay/Dimmer](examples/sdk-examples-guide.md#example-device---relay-and-dimmer) | -| Thermostat | [Example Device - Thermostat](examples/sdk-examples-guide.md#example-device---thermostat) | -| Sensors | [Example Device - Sensor](examples/sdk-examples-guide.md#example-device---sensor) | -| Web API | [Example HTTP Responder](examples/sdk-examples-guide.md#example-http-responder) | - -## For Claude (Context Optimization) - -### File Loading Strategy - -**Concepts vs API**: These are complementary, not duplicates: -- `concepts/devices.md` → Design (Devices.xml, ConfigUI, validation) -- `api/iom/devices.md` → Reference (class properties, methods) - -**Load based on question type**: - -| User Question | Load | -|---------------|------| -| "How do I create a device?" | `concepts/devices.md` | -| "What properties does a device have?" | `api/iom/devices.md` | -| "How do I update state?" | `patterns/api-patterns.md` | -| "How do I save settings?" | `concepts/plugin-preferences.md` | -| "How do I create trigger events?" | `concepts/events.md` | -| "Show me an example" | Specific example from `sdk-examples/` | - -### Modular IOM Files - -The IOM documentation is split into focused files (~4KB each): - -| Topic | File | -|-------|------| -| Core architecture | `api/iom/architecture.md` | -| Device classes | `api/iom/devices.md` | -| Trigger classes | `api/iom/triggers.md` | -| Iteration filters | `api/iom/filters.md` | -| Change subscriptions | `api/iom/subscriptions.md` | -| Constants/icons | `api/iom/constants.md` | -| indigo.Dict/List | `api/iom/containers.md` | -| Utility functions | `api/iom/utilities.md` | - -### Concept Files - -| Topic | File | -|-------|------| -| Plugin lifecycle | `concepts/plugin-lifecycle.md` | -| Device development | `concepts/devices.md` | -| Plugin preferences | `concepts/plugin-preferences.md` | -| Custom events | `concepts/events.md` | - -Load only the specific topic needed. - -### Never Load All at Once - -- `sdk-examples/` - 16 complete plugins (1.3MB) - Load individually -- Don't load all IOM files together - load by topic +- Prefer a single canonical page (small, topic-scoped) over broad loads; start from `INDEX.md`. +- Never load `sdk-examples/` wholesale (~1.3MB) — read the guide, then one example. +- Never load the whole `reference/canonical/` tree — load by topic. ## External Resources -- [Official Plugin Guide](https://wiki.indigodomo.com/doku.php?id=indigo_2025.1_documentation:plugin_guide) -- [Object Model Reference](https://wiki.indigodomo.com/doku.php?id=indigo_2025.1_documentation:object_model_reference) +- [Plugin Development (official docs)](https://docs.indigodomo.com/2025.2/plugin-dev/) +- [Scripting Indigo / IOM](https://docs.indigodomo.com/2025.2/scripting/) - [Developer Forum](https://forums.indigodomo.com/viewforum.php?f=18) -## Version Information +## Version -- **Current**: Indigo 2025.1+ (Python 3.10+) -- **Migration**: See [`../../reference/Python3-Migration-Guide.md`](../../reference/Python3-Migration-Guide.md) +- **Target**: Indigo 2025.2 (Python 3.13, ServerApiVersion 3.8) +- **Legacy porting**: [`../../reference/Python3-Migration-Guide.md`](../../reference/Python3-Migration-Guide.md) (Python 2→3, only if porting an old plugin) diff --git a/docs/plugin-dev/api/README.md b/docs/plugin-dev/api/README.md deleted file mode 100644 index aaf4d1d..0000000 --- a/docs/plugin-dev/api/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# API Reference - -Reference documentation for the Indigo Plugin API. - -## Indigo Object Model (IOM) - -The IOM provides Python access to all Indigo objects. Start with the [overview](indigo-object-model.md), then dive into specific topics: - -| Topic | Description | -|-------|-------------| -| [Overview & Quick Reference](indigo-object-model.md) | Index and common patterns | -| [Architecture](iom/architecture.md) | Client-server model, replaceOnServer pattern | -| [Command Namespaces](iom/command-namespaces.md) | indigo.dimmer.*, indigo.relay.*, etc. | -| [Device Classes](iom/devices.md) | DimmerDevice, SensorDevice, ThermostatDevice, etc. | -| [Trigger Classes](iom/triggers.md) | Trigger types and plugin events | -| [Containers](iom/containers.md) | indigo.Dict and indigo.List behavior | -| [Filters](iom/filters.md) | Device/trigger iteration filters | -| [Subscriptions](iom/subscriptions.md) | Change callbacks and low-level events | -| [Constants](iom/constants.md) | Icons, actions, protocols, modes | -| [Utilities](iom/utilities.md) | Helper functions and classes | - -## Quick Task Lookup - -| Task | Documentation | -|------|---------------| -| Turn on/off devices | [Command Namespaces](iom/command-namespaces.md) | -| Update device states | [Device Classes](iom/devices.md#device-methods) | -| Iterate specific device types | [Filters](iom/filters.md#device-filters) | -| Subscribe to device changes | [Subscriptions](iom/subscriptions.md) | -| Modify object properties | [Architecture](iom/architecture.md#replaceOnServer-pattern) | -| Handle nested indigo.Dict | [Containers](iom/containers.md#critical-copy-semantics) | -| Serve static files | [Utilities](iom/utilities.md#return_static_file) | -| State icon reference | [Constants](iom/constants.md#state-image-icons) | - -## Related Documentation - -| Topic | Location | -|-------|----------| -| Plugin structure and lifecycle | [Concepts](../concepts/) | -| Device configuration UI | [Concepts → Devices](../concepts/devices.md) | -| Implementation patterns | [Patterns](../patterns/) | -| Working examples | [Examples](../examples/) | -| SDK example plugins | [sdk-examples/](../../sdk-examples/) | - -## External References - -- [Official Object Model Reference](https://wiki.indigodomo.com/doku.php?id=indigo_2025.1_documentation:object_model_reference) -- [Plugin Developer's Guide](https://wiki.indigodomo.com/doku.php?id=indigo_2025.1_documentation:plugin_guide) diff --git a/docs/plugin-dev/api/indigo-object-model.md b/docs/plugin-dev/api/indigo-object-model.md deleted file mode 100644 index 23ab203..0000000 --- a/docs/plugin-dev/api/indigo-object-model.md +++ /dev/null @@ -1,103 +0,0 @@ -# Indigo Object Model (IOM) Reference - -The IOM provides Python access to Indigo objects. See [official documentation](https://wiki.indigodomo.com/doku.php?id=indigo_2025.1_documentation:object_model_reference). - -## Detailed Documentation - -| Topic | File | -|-------|------| -| Architecture & Core Concepts | [iom/architecture.md](iom/architecture.md) | -| Command Namespaces | [iom/command-namespaces.md](iom/command-namespaces.md) | -| Device Classes | [iom/devices.md](iom/devices.md) | -| Trigger Classes | [iom/triggers.md](iom/triggers.md) | -| indigo.Dict & indigo.List | [iom/containers.md](iom/containers.md) | -| Iteration Filters | [iom/filters.md](iom/filters.md) | -| Subscriptions & Callbacks | [iom/subscriptions.md](iom/subscriptions.md) | -| Constants | [iom/constants.md](iom/constants.md) | -| Utility Functions | [iom/utilities.md](iom/utilities.md) | - -## Quick Reference - -### Access Objects - -```python -dev = indigo.devices[123456] # By ID -dev = indigo.devices["Device Name"] # By name -var = indigo.variables["VarName"] -trigger = indigo.triggers[trigger_id] -ag = indigo.actionGroups[ag_id] -``` - -### Iterate Objects - -```python -for dev in indigo.devices: # All devices -for dev in indigo.devices.iter("self"): # Plugin's devices -for dev in indigo.devices.iter("indigo.dimmer"): # All dimmers -``` - -### Modify Objects - -```python -# Get copy, modify, push back -dev = indigo.devices[123456] -dev.name = "New Name" -dev.replaceOnServer() - -# Update state -dev.updateStateOnServer('myState', value=42) - -# Update plugin props -dev.replacePluginPropsOnServer(new_props) -``` - -### Send Commands - -```python -indigo.dimmer.turnOn(dev) -indigo.dimmer.setBrightness(dev, value=75) -indigo.relay.toggle(dev) -indigo.thermostat.setHeatSetpoint(dev, value=68) -indigo.variable.updateValue(var, value="new") -indigo.actionGroup.execute(ag) -``` - -### Server Commands - -```python -indigo.server.log("message") -indigo.server.speak("Hello") -time = indigo.server.getTime() -``` - -### Subscribe to Changes - -```python -# In startup() -indigo.devices.subscribeToChanges() - -# Callback -def deviceUpdated(self, origDev, newDev): - indigo.PluginBase.deviceUpdated(self, origDev, newDev) - # Handle change -``` - -## Key Concepts - -1. **Objects are copies** - When you get an object, you get a copy, not the real object -2. **Use command namespaces** - `indigo.dimmer.turnOn(dev)` not `dev.turnOn()` -3. **replaceOnServer()** - Push local changes to server -4. **Store IDs, not names** - Names can change, IDs are permanent - -## Common Tasks - -| Task | Solution | -|------|----------| -| Turn on a device | `indigo.dimmer.turnOn(dev)` | -| Update device state | `dev.updateStateOnServer('key', value=val)` | -| Create variable | `indigo.variable.create("Name", value="val")` | -| Execute action group | `indigo.actionGroup.execute(ag_id)` | -| Get device name efficiently | `indigo.devices.getName(dev_id)` | -| Log message | `indigo.server.log("message")` | -| Convert to dict | `dict(dev)` | -| Serialize to JSON | `json.dumps(dict(dev), cls=indigo.utils.IndigoJSONEncoder)` | diff --git a/docs/plugin-dev/api/iom/architecture.md b/docs/plugin-dev/api/iom/architecture.md deleted file mode 100644 index e07f2f0..0000000 --- a/docs/plugin-dev/api/iom/architecture.md +++ /dev/null @@ -1,272 +0,0 @@ -# IOM Architecture and Core Concepts - -## Client-Server Architecture - -The Indigo Object Model runs in a **separate process** from IndigoServer. This design prevents scripts/plugins from crashing the server. - -**Key implication**: When you get an object, you receive a **copy**, not the actual object. - -## Object Manipulation Rules - -### For All Scripts and Plugins - -| Task | Method | -|------|--------| -| Create/duplicate/delete objects | Use command namespace (e.g., `indigo.device.create()`) | -| Send commands (turnOn, etc.) | Use command namespace with object reference | -| Modify object definition | Get copy → modify → `myObject.replaceOnServer()` | -| Refresh local copy | Call `myObject.refreshFromServer()` | - -### For Plugin Developers Only - -| Task | Method | -|------|--------| -| Update device plugin props | `myDevice.replacePluginPropsOnServer(newPropsDict)` | -| Update device state | `myDevice.updateStateOnServer(key="keyName", value="Value")` | - -## Object IDs - -Every top-level object has a **globally unique integer ID**: -- Action groups, devices, folders, schedules, triggers, variables -- Commands accept ID, object reference, or name -- **Always store IDs, never names** (names can change) - -```python -# All equivalent - but ID is preferred -indigo.device.turnOn(123456) # Best - ID -indigo.device.turnOn(dev) # Good - object reference -indigo.device.turnOn("Living Room") # Avoid - name can change -``` - -**Tip**: Control-click on any object in Indigo's Main Window to copy its ID. - -## replaceOnServer Pattern - -The standard pattern for modifying object definitions: - -```python -# Get a copy -dev = indigo.devices[123456] - -# Modify locally -dev.name = "New Name" -dev.description = "Updated description" - -# Push changes to server -dev.replaceOnServer() -``` - -## refreshFromServer Pattern - -Update your local copy to match the server: - -```python -dev = indigo.devices[123456] -# ... time passes, device may have changed ... -dev.refreshFromServer() -# dev now has current server values -``` - -## Plugin Props (pluginProps) - -Every Indigo object supports plugin-specific metadata via `pluginProps`. - -### Access Rules - -| Context | Access | -|---------|--------| -| Plugin's own objects | Read/Write | -| Scripts | Read-only | -| Other plugins | Read-only (via globalProps) | - -### Supported Value Types - -- Numbers (int, float) -- Booleans -- Strings -- `indigo.Dict()` -- `indigo.List()` - -### Reading pluginProps - -```python -dev = indigo.devices[123456] # Use ID, not name - -# Direct access -address = dev.pluginProps["address"] - -# Safe access with default -interval = dev.pluginProps.get("pollingInterval", 60) -``` - -### Writing pluginProps - -```python -dev = indigo.devices[123456] - -# Get current props -newProps = dev.pluginProps - -# Modify -newProps["onCycles"] = 5 -newProps["lastUpdate"] = str(datetime.now()) -newProps["customData"] = {"key": "value"} - -# Push to server -dev.replacePluginPropsOnServer(newProps) -``` - -### Incrementing Values - -```python -dev = indigo.devices[123456] -newProps = dev.pluginProps -newProps["onCycles"] = newProps.get("onCycles", 0) + 1 -dev.replacePluginPropsOnServer(newProps) -``` - -## Folder Operations - -All collections have a `.folders` attribute: - -```python -# Iterate folders -for folder in indigo.devices.folders: - self.logger.info(f"Folder: {folder.name} (id={folder.id})") - -# Folder properties -folder.id # Unique ID (int) -folder.name # Folder name (str) -folder.remoteDisplay # Show in remote UI (bool) - -# Folder commands (via collection namespace) -indigo.devices.folder.create("My Folder") -indigo.devices.folder.delete(folder_id) -indigo.devices.folder.delete(folder_id, deleteAllChildren=True) -indigo.devices.folder.duplicate(folder_id, duplicateName="Copy") -indigo.devices.folder.getId("My Folder") # Returns folder ID -indigo.devices.folder.displayInRemoteUI(folder_id, value=True) - -# Check device's folder -dev = indigo.devices[123456] -dev.folderId # 0 = no folder -``` - -Available for: `indigo.devices.folder`, `indigo.triggers.folder`, `indigo.schedules.folder`, `indigo.actionGroups.folder`, `indigo.variables.folder`. - -### globalProps (Cross-Plugin Access) - -Plugins have read-only access to other plugins' props: - -```python -dev = indigo.devices[123456] - -# Access another plugin's data (read-only) -other_plugin_data = dev.globalProps.get("com.other.plugin", {}) -some_value = other_plugin_data.get("someKey") -``` - -## Built-in Collection Objects - -| Object | Description | -|--------|-------------| -| `indigo.devices` | All devices | -| `indigo.triggers` | All triggers | -| `indigo.schedules` | All schedules | -| `indigo.actionGroups` | All action groups | -| `indigo.variables` | All variables | - -These collections are **read-only** - use command namespaces to modify. - -### Folders Attribute - -Each collection has a `folders` attribute: - -```python -# Access device folders -for folder in indigo.devices.folders: - print(folder.name) - -# Access variable folders -for folder in indigo.variables.folders: - print(folder.name) -``` - -### getName() Convenience Method - -Efficiently get an object's name without fetching the entire object: - -```python -# More efficient than indigo.devices[123].name -name = indigo.devices.getName(123456) - -# Works for folders too -folder_name = indigo.variables.folders.getName(folder_id) -``` - -### iterkeys() Method - -Iterate over just the IDs (more efficient when you only need IDs): - -```python -for dev_id in indigo.devices.iterkeys(): - # process just the ID without loading the full object - pass -``` - -## self vs indigo.activePlugin - -In plugin.py methods, `self` refers to the Plugin instance: - -```python -class Plugin(indigo.PluginBase): - def startup(self): - self.logger.info("Starting") # self is the Plugin -``` - -Outside plugin methods, use `indigo.activePlugin`: - -```python -# In a script or embedded Python -indigo.activePlugin.logger.info("Message") -``` - -## Common Exceptions - -| Exception | Cause | -|-----------|-------| -| `ArgumentError` | Missing or incorrect parameter type | -| `IndexError` | Index out of range | -| `KeyError` | Dictionary key not found | -| `PluginNotFoundError` | Target plugin is disabled or missing | -| `TypeError` | Incorrect runtime type | -| `ValueError` | Illegal or disallowed value | - -## Python Introspection - -Use standard Python introspection to explore objects: - -```python -dev = indigo.devices[123456] - -# Get class type -dev.__class__ # <class 'indigo.DimmerDevice'> - -# List all attributes and methods -dir(dev) - -# Check for attribute -hasattr(dev, 'brightness') # True for dimmers - -# Get plugin device type -dev.deviceTypeId # 'myDeviceType' -``` - -## Object to Dictionary Conversion - -Convert any Indigo object to a Python dict for serialization: - -```python -dev = indigo.devices[123456] -dev_dict = dict(dev) # Standard Python dict with all properties -``` diff --git a/docs/plugin-dev/api/iom/command-namespaces.md b/docs/plugin-dev/api/iom/command-namespaces.md deleted file mode 100644 index a730dfd..0000000 --- a/docs/plugin-dev/api/iom/command-namespaces.md +++ /dev/null @@ -1,390 +0,0 @@ -# Command Namespaces Reference - -Commands are organized into namespaces matching their target class type. - -## Device Commands - -| Class | Namespace | Description | -|-------|-----------|-------------| -| `indigo.Device` | `indigo.device.*` | All device subclasses | -| `indigo.DimmerDevice` | `indigo.dimmer.*` | Dimmer devices | -| `indigo.RelayDevice` | `indigo.relay.*` | Relay, lock, 2-state devices | -| `indigo.SensorDevice` | `indigo.sensor.*` | Sensor devices | -| `indigo.SpeedControlDevice` | `indigo.speedcontrol.*` | Speed control/motor devices | -| `indigo.SprinklerDevice` | `indigo.sprinkler.*` | Sprinkler devices | -| `indigo.ThermostatDevice` | `indigo.thermostat.*` | Thermostat devices | -| `indigo.MultiIODevice` | `indigo.iodevice.*` | Input/output devices | - -### Common Device Commands - -```python -# Create device (factory method for all types) -indigo.device.create(protocol, deviceTypeId=..., props=...) - -# Duplicate device -indigo.device.duplicate(dev_or_id, duplicateName="Copy") - -# Delete device -indigo.device.delete(dev_or_id) - -# Move to folder -indigo.device.moveToFolder(dev_or_id, folder_id) - -# Enable/disable -indigo.device.enable(dev_or_id, value=True) - -# Display in remote UI -indigo.device.displayInRemoteUI(dev_or_id, value=True) -``` - -### Dimmer Commands - -```python -indigo.dimmer.turnOn(dev) -indigo.dimmer.turnOff(dev) -indigo.dimmer.toggle(dev) -indigo.dimmer.setBrightness(dev, value=75) -indigo.dimmer.brightenBy(dev, value=10) -indigo.dimmer.dimBy(dev, value=10) -indigo.dimmer.statusRequest(dev) -``` - -### Relay Commands - -```python -indigo.relay.turnOn(dev) -indigo.relay.turnOff(dev) -indigo.relay.toggle(dev) -indigo.relay.statusRequest(dev) -``` - -### Sensor Commands - -```python -indigo.sensor.turnOn(dev) # For virtual sensors -indigo.sensor.turnOff(dev) -indigo.sensor.statusRequest(dev) -``` - -### Thermostat Commands - -```python -indigo.thermostat.setHeatSetpoint(dev, value=68) -indigo.thermostat.setCoolSetpoint(dev, value=76) -indigo.thermostat.setHvacMode(dev, value=indigo.kHvacMode.Auto) -indigo.thermostat.setFanMode(dev, value=indigo.kFanMode.Auto) -indigo.thermostat.statusRequest(dev) -``` - -### Sprinkler Commands - -```python -indigo.sprinkler.setActiveZone(dev, index=1) -indigo.sprinkler.run(dev) -indigo.sprinkler.stop(dev) -indigo.sprinkler.pause(dev) -indigo.sprinkler.resume(dev) -indigo.sprinkler.previousZone(dev) -indigo.sprinkler.nextZone(dev) -``` - -### Speed Control Commands - -```python -indigo.speedcontrol.turnOn(dev) -indigo.speedcontrol.turnOff(dev) -indigo.speedcontrol.toggle(dev) -indigo.speedcontrol.setSpeedLevel(dev, value=50) -indigo.speedcontrol.setSpeedIndex(dev, value=2) -indigo.speedcontrol.increaseSpeedIndex(dev) -indigo.speedcontrol.decreaseSpeedIndex(dev) -indigo.speedcontrol.statusRequest(dev) -``` - -## Trigger Commands - -| Class | Namespace | -|-------|-----------| -| `indigo.Trigger` | `indigo.trigger.*` | -| `indigo.DeviceStateChangeTrigger` | `indigo.devStateChange.*` | -| `indigo.VariableValueChangeTrigger` | `indigo.varValueChange.*` | -| `indigo.EmailReceivedTrigger` | `indigo.emailRcvd.*` | -| `indigo.InsteonCommandReceivedTrigger` | `indigo.insteonCmdRcvd.*` | -| `indigo.X10CommandReceivedTrigger` | `indigo.x10CmdRcvd.*` | -| `indigo.ServerStartupTrigger` | `indigo.serverStartup.*` | -| `indigo.PowerFailureTrigger` | `indigo.powerFail.*` | -| `indigo.InterfaceFailureTrigger` | `indigo.interfaceFail.*` | -| `indigo.InterfaceInitializedTrigger` | `indigo.interfaceInit.*` | -| `indigo.PluginEventTrigger` | `indigo.pluginEvent.*` | - -```python -# Common trigger commands -indigo.trigger.enable(trigger_or_id, value=True) -indigo.trigger.execute(trigger_or_id) -indigo.trigger.delete(trigger_or_id) -indigo.trigger.moveToFolder(trigger_or_id, folder_id) -``` - -## Variable Commands - -| Class | Namespace | -|-------|-----------| -| `indigo.Variable` | `indigo.variable.*` | - -```python -# Create variable -indigo.variable.create("VarName", value="initial", folder=folder_id) - -# Update value -indigo.variable.updateValue(var_or_id, value="new value") - -# Delete -indigo.variable.delete(var_or_id) - -# Move to folder -indigo.variable.moveToFolder(var_or_id, folder_id) -``` - -## Action Group Commands - -| Class | Namespace | -|-------|-----------| -| `indigo.ActionGroup` | `indigo.actionGroup.*` | - -```python -# Execute action group -indigo.actionGroup.execute(ag_or_id) - -# Execute with event data (indigo.Dict passed to actions) -# Indigo auto-adds "source" key ("server", "python", "api-http", or "api-websocket") -indigo.actionGroup.execute(ag_or_id, event_data=some_dict) - -# Enable/disable -indigo.actionGroup.enable(ag_or_id, value=True) - -# Delete -indigo.actionGroup.delete(ag_or_id) - -# Duplicate (returns new action group instance) -indigo.actionGroup.duplicate(ag_or_id, duplicateName="Copy of AG") - -# Move to folder -indigo.actionGroup.moveToFolder(ag_or_id, value=folder_id) - -# Display in remote UI -indigo.actionGroup.displayInRemoteUI(ag_or_id, value=True) - -# Get dependencies (returns indigo.Dict of dependent objects) -indigo.actionGroup.getDependencies(ag_or_id) -``` - -## Schedule Commands - -| Class | Namespace | -|-------|-----------| -| `indigo.Schedule` | `indigo.schedule.*` | - -```python -# Enable/disable (with optional delay and duration in seconds) -indigo.schedule.enable(sched_or_id, value=True, delay=0, duration=0) - -# Delete -indigo.schedule.delete(sched_or_id) - -# Duplicate (returns new schedule instance) -indigo.schedule.duplicate(sched_or_id, duplicateName="Copy of Schedule") - -# Execute immediately -# ignoreConditions: bypass any schedule conditions -# schedule_data: indigo.Dict with optional metadata (Indigo auto-adds "source" and "timestamp") -indigo.schedule.execute(sched_or_id, ignoreConditions=False, schedule_data=None) - -# Move to folder -indigo.schedule.moveToFolder(sched_or_id, value=folder_id) - -# Get dependencies (returns indigo.Dict of dependent objects) -indigo.schedule.getDependencies(sched_or_id) - -# Remove any pending delayed actions -indigo.schedule.removeDelayedActions(sched_or_id) -``` - -## Protocol-Specific Commands - -### INSTEON - -```python -indigo.insteon.sendRawInsteon(address, cmd, cmd2=0, waitUntilAck=True) -indigo.insteon.sendRawExtended(address, cmd, cmd2, data) -indigo.insteon.subscribeToIncoming() # Low-level monitoring -indigo.insteon.subscribeToOutgoing() -``` - -### X10 - -```python -indigo.x10.sendRawX10(address, cmd) -indigo.x10.subscribeToIncoming() -indigo.x10.subscribeToOutgoing() -``` - -## Server Commands - -### Properties - -```python -indigo.server.apiVersion # API version string, e.g., "3.6" -indigo.server.version # Indigo version string, e.g., "2025.1" -indigo.server.address # Server address -indigo.server.portNum # Server port number -indigo.server.connectionGood # True if connected to server -indigo.server.licenseStatus # kLicenseStatus enum value -``` - -### Logging - -```python -indigo.server.log("message") -indigo.server.log("message", type="My Plugin", level=logging.INFO) -indigo.server.error("error message") -indigo.server.getEventLogList(lineCount=100) -indigo.server.getEventLogList(returnAsList=True, lineCount=50, showTimeStamp=True) -``` - -### Communication - -```python -indigo.server.speak("text to speak", waitUntilDone=False) -indigo.server.sendEmailTo("user@example.com", subject="Alert", body="Motion detected") -``` - -### Time & Location - -```python -indigo.server.getTime() # Current server datetime -indigo.server.calculateSunrise() # Today's sunrise -indigo.server.calculateSunset() # Today's sunset -indigo.server.calculateSunrise(date_obj) # Sunrise for specific date -indigo.server.calculateSunset(date_obj) # Sunset for specific date -indigo.server.getLatitudeAndLongitude() # Returns (latitude, longitude) tuple -``` - -### File System & Database - -```python -indigo.server.getDbName() # Database name -indigo.server.getDbFilePath() # Full path to database file -indigo.server.getInstallFolderPath() # Indigo installation folder -``` - -### Hardware & Network - -```python -indigo.server.getSerialPorts(filter="indigo.ignoreBluetooth") -indigo.server.getReflectorURL() # Remote access URL -indigo.server.getWebServerURL() # Local web server URL -``` - -### Plugin Management - -```python -indigo.server.getPlugin(pluginId) # Returns plugin object (see below) -indigo.server.getPluginList() # List of all installed plugin IDs -indigo.server.savePluginPrefs() # Force save current plugin prefs -indigo.server.restartPlugin(message="Restarting", isError=False) -indigo.server.stopPlugin(message="Stopping", isError=False) -``` - -### Broadcasting - -```python -indigo.server.broadcastToSubscribers(messageName="myEvent") -indigo.server.subscribeToLogBroadcasts() # Receive log entries as broadcasts -``` - -### Maintenance - -```python -indigo.server.removeAllDelayedActions() # Remove all pending delayed actions -indigo.server.waitUntilIdle() # Block until server is idle -indigo.server.getDeprecatedElems(includeWarnings=False) # Deprecated object scan -``` - -## Plugin Object Access - -Access other plugins (or self) via `indigo.server.getPlugin()`: - -```python -plugin = indigo.server.getPlugin("com.other.plugin") -``` - -### Plugin Properties - -```python -plugin.pluginId # Bundle identifier -plugin.pluginVersion # Version string -plugin.pluginDisplayName # Display name -plugin.pluginFolderPath # Path to plugin bundle -plugin.pluginServerApiVersion # API version -plugin.pluginSupportURL # Support URL - -# Status -plugin.isEnabled() # Is plugin enabled? -plugin.isInstalled() # Is plugin installed? -plugin.isRunning() # Is plugin running? - -# Plugin Store info -plugin.storeIconURL # Store icon URL -plugin.storeName # Store display name -plugin.storePluginURL # Store page URL -plugin.storeSummary # Store description -plugin.includedWithServer # Bundled with Indigo? - -# Update info -plugin.compatibleUpdateAvailable # Compatible update available? -plugin.incompatibleUpdateAvailable # Incompatible update available? -plugin.latestCompatibleVers # Latest compatible version string -plugin.latestVers # Latest version string (any) -plugin.latestCompatibleDownloadURL # Download URL -plugin.latestCompatibleDownloadCount # Download count -plugin.latestCompatibleReleaseDate # Release date -plugin.latestCompatibleSummaryDesc # Release summary -plugin.latestCompatibleWhatsNewDesc # What's new text -plugin.latestRequiresIndigoVers # Required Indigo version -plugin.latestReleaseDate # Latest release date -``` - -### Executing Actions on Other Plugins - -```python -plugin = indigo.server.getPlugin("com.other.plugin") - -# Execute a plugin action (defined in its Actions.xml) -result = plugin.executeAction("actionId", deviceId=12345, - props={"key": "value"}, - waitUntilDone=True) -# result can be: None, bool, int, float, str, indigo.Dict, indigo.List -``` - -### Restarting Plugins - -```python -plugin = indigo.server.getPlugin("com.other.plugin") -plugin.restart(waitUntilDone=True) -plugin.restartAndDebug(waitUntilDone=True) # Restart with debug logging -``` - -## Common Command Patterns - -All namespaces support these methods for their object type: - -| Method | Description | -|--------|-------------| -| `create()` | Create new object | -| `duplicate()` | Duplicate existing object (returns new instance) | -| `delete()` | Delete object | -| `enable()` | Enable/disable object | -| `execute()` | Execute object (action groups, schedules) | -| `moveToFolder()` | Move to folder | -| `getDependencies()` | Get dependent Indigo objects (returns `indigo.Dict`) | -| `removeDelayedActions()` | Remove pending delayed actions (schedules) | diff --git a/docs/plugin-dev/api/iom/constants.md b/docs/plugin-dev/api/iom/constants.md deleted file mode 100644 index cc8bad4..0000000 --- a/docs/plugin-dev/api/iom/constants.md +++ /dev/null @@ -1,417 +0,0 @@ -# Constants Reference - -## State Image Icons - -Used with `dev.updateStateImageOnServer()`: - -```python -dev.updateStateImageOnServer(indigo.kStateImageSel.SensorOn) -``` - -| Constant | Use Case | -|----------|----------| -| `indigo.kStateImageSel.Auto` | Automatic selection | -| `indigo.kStateImageSel.NoImage` | No icon | -| `indigo.kStateImageSel.Error` | Error state | -| `indigo.kStateImageSel.Custom` | Custom icon | - -### Dimmer/Relay Icons - -| Constant | -|----------| -| `indigo.kStateImageSel.DimmerOff` | -| `indigo.kStateImageSel.DimmerOn` | -| `indigo.kStateImageSel.PowerOff` | -| `indigo.kStateImageSel.PowerOn` | - -### Sensor Icons - -| Constant | -|----------| -| `indigo.kStateImageSel.SensorOff` | -| `indigo.kStateImageSel.SensorOn` | -| `indigo.kStateImageSel.SensorTripped` | -| `indigo.kStateImageSel.MotionSensor` | -| `indigo.kStateImageSel.MotionSensorTripped` | -| `indigo.kStateImageSel.DoorSensorClosed` | -| `indigo.kStateImageSel.DoorSensorOpened` | -| `indigo.kStateImageSel.WindowSensorClosed` | -| `indigo.kStateImageSel.WindowSensorOpened` | -| `indigo.kStateImageSel.LightSensor` | -| `indigo.kStateImageSel.LightSensorOn` | -| `indigo.kStateImageSel.TemperatureSensor` | -| `indigo.kStateImageSel.TemperatureSensorOn` | -| `indigo.kStateImageSel.HumiditySensor` | -| `indigo.kStateImageSel.HumiditySensorOn` | -| `indigo.kStateImageSel.WindSpeedSensor` | -| `indigo.kStateImageSel.WindDirectionSensor` | - -### HVAC Icons - -| Constant | -|----------| -| `indigo.kStateImageSel.HvacOff` | -| `indigo.kStateImageSel.HvacHeatMode` | -| `indigo.kStateImageSel.HvacCoolMode` | -| `indigo.kStateImageSel.HvacAutoMode` | -| `indigo.kStateImageSel.HvacFanMode` | -| `indigo.kStateImageSel.HvacHeating` | -| `indigo.kStateImageSel.HvacCooling` | - -### Fan Icons - -| Constant | -|----------| -| `indigo.kStateImageSel.FanOff` | -| `indigo.kStateImageSel.FanLow` | -| `indigo.kStateImageSel.FanMedium` | -| `indigo.kStateImageSel.FanHigh` | - -### Media Icons - -| Constant | -|----------| -| `indigo.kStateImageSel.AvPaused` | -| `indigo.kStateImageSel.AvPlaying` | -| `indigo.kStateImageSel.AvStopped` | - -### Lock Icons - -| Constant | -|----------| -| `indigo.kStateImageSel.Locked` | -| `indigo.kStateImageSel.Unlocked` | - -### Sprinkler Icons - -| Constant | -|----------| -| `indigo.kStateImageSel.SprinklerOff` | -| `indigo.kStateImageSel.SprinklerOn` | - -### Timer Icons - -| Constant | -|----------| -| `indigo.kStateImageSel.TimerOff` | -| `indigo.kStateImageSel.TimerOn` | - -### Energy Icons - -| Constant | -|----------| -| `indigo.kStateImageSel.EnergyMeterOff` | -| `indigo.kStateImageSel.EnergyMeterOn` | - -### Battery Icons - -| Constant | -|----------| -| `indigo.kStateImageSel.BatteryCharger` | -| `indigo.kStateImageSel.BatteryChargerOn` | -| `indigo.kStateImageSel.BatteryLevelLow` | -| `indigo.kStateImageSel.BatteryLevel25` | -| `indigo.kStateImageSel.BatteryLevel50` | -| `indigo.kStateImageSel.BatteryLevel75` | -| `indigo.kStateImageSel.BatteryLevelHigh` | - -## Device Action Constants - -### Relay/Dimmer Actions (`indigo.kDeviceAction`) - -```python -indigo.kDeviceAction.TurnOn -indigo.kDeviceAction.TurnOff -indigo.kDeviceAction.Toggle -indigo.kDeviceAction.SetBrightness # action.actionValue = 0-100 -indigo.kDeviceAction.BrightenBy # action.actionValue = relative amount -indigo.kDeviceAction.DimBy # action.actionValue = relative amount -indigo.kDeviceAction.SetColorLevels # action.actionValue = dict with RGB/white keys -indigo.kDeviceAction.Lock # Lock sub-type devices -indigo.kDeviceAction.Unlock # Lock sub-type devices -indigo.kDeviceAction.AllLightsOff -indigo.kDeviceAction.AllLightsOn -indigo.kDeviceAction.AllOff -indigo.kDeviceAction.RequestStatus -``` - -### Thermostat Actions (`indigo.kThermostatAction`) - -```python -# Setpoint control -indigo.kThermostatAction.SetHeatSetpoint # action.actionValue = temperature -indigo.kThermostatAction.SetCoolSetpoint # action.actionValue = temperature -indigo.kThermostatAction.IncreaseHeatSetpoint # action.actionValue = delta -indigo.kThermostatAction.DecreaseHeatSetpoint # action.actionValue = delta -indigo.kThermostatAction.IncreaseCoolSetpoint # action.actionValue = delta -indigo.kThermostatAction.DecreaseCoolSetpoint # action.actionValue = delta - -# Mode control -indigo.kThermostatAction.SetHvacMode # action.actionMode = kHvacMode value -indigo.kThermostatAction.SetFanMode # action.actionMode = kFanMode value - -# Status requests -indigo.kThermostatAction.RequestStatusAll -indigo.kThermostatAction.RequestMode -indigo.kThermostatAction.RequestEquipmentState -indigo.kThermostatAction.RequestTemperatures -indigo.kThermostatAction.RequestHumidities -indigo.kThermostatAction.RequestDeadbands -indigo.kThermostatAction.RequestSetpoints -``` - -### Sensor Actions (`indigo.kSensorAction`) - -```python -indigo.kSensorAction.TurnOn -indigo.kSensorAction.TurnOff -indigo.kSensorAction.Toggle -indigo.kSensorAction.RequestStatus -``` - -### Sprinkler Actions (`indigo.kSprinklerAction`) - -```python -indigo.kSprinklerAction.ZoneOn # action.zoneIndex = 1-based zone -indigo.kSprinklerAction.AllZonesOff -indigo.kSprinklerAction.RunNewSchedule # action.zoneDurations = list -indigo.kSprinklerAction.RunPreviousSchedule -indigo.kSprinklerAction.PauseSchedule -indigo.kSprinklerAction.ResumeSchedule -indigo.kSprinklerAction.StopSchedule -indigo.kSprinklerAction.PreviousZone -indigo.kSprinklerAction.NextZone -``` - -### Speed Control Actions (`indigo.kSpeedControlAction`) - -```python -indigo.kSpeedControlAction.TurnOn -indigo.kSpeedControlAction.TurnOff -indigo.kSpeedControlAction.Toggle -indigo.kSpeedControlAction.SetSpeedLevel # action.actionValue = 0-100 -indigo.kSpeedControlAction.SetSpeedIndex # action.actionValue = 0-N index -indigo.kSpeedControlAction.IncreaseSpeedIndex # action.actionValue = delta -indigo.kSpeedControlAction.DecreaseSpeedIndex # action.actionValue = delta -indigo.kSpeedControlAction.RequestStatus -``` - -### Universal Actions (`indigo.kUniversalAction`) - -Handled by `actionControlUniversal()` for all device types: - -```python -indigo.kUniversalAction.Beep -indigo.kUniversalAction.EnergyUpdate -indigo.kUniversalAction.EnergyReset -indigo.kUniversalAction.RequestStatus -``` - -### I/O Device Actions (`indigo.kIOAction`) - -```python -indigo.kIOAction.TurnOn -indigo.kIOAction.TurnOff -indigo.kIOAction.Toggle -indigo.kIOAction.SetBinaryOutput # action.index, action.actionValue -indigo.kIOAction.RequestStatus -indigo.kIOAction.AllOff -indigo.kIOAction.EnergyUpdate -indigo.kIOAction.EnergyReset -``` - -### Variable Actions (`indigo.kVariableAction`) - -```python -indigo.kVariableAction.SetValue -indigo.kVariableAction.IncrementValue -indigo.kVariableAction.DecrementValue -``` - -## Device Sub-Types - -Sub-types affect how devices appear in Indigo's UI (icons and categorization). Set in `Devices.xml` or via `pluginProps`. - -### Setting Sub-Types in Devices.xml - -```xml -<Device type="sensor" subType="kSensorDeviceSubType.Temperature" id="myTempSensor"> - <Name>Temperature Sensor</Name> -</Device> - -<!-- With custom UI display name --> -<Device type="sensor" subType="kSensorDeviceSubType.DoorWindow,ui=Magnetic Reed Sensor" id="myDoorSensor"> - <Name>Door Sensor</Name> -</Device> -``` - -### General Device Sub-Types (`kDeviceSubType`) - -For `type="custom"` devices: - -| Value | Description | -|-------|-------------| -| `Amplifier` | Audio amplifier | -| `Automobile` | Vehicle | -| `Camera` | Security/IP camera | -| `Keypad` | Keypad controller | -| `Mobile` | Mobile device | -| `Remote` | Remote control | -| `Robot` | Robot/automation | -| `Security` | Security device | -| `Speaker` | Speaker/audio output | -| `Streaming` | Streaming device | -| `Television` | TV/display | -| `Weather` | Weather station | -| `Other` | Other device type | - -### Dimmer Sub-Types (`kDimmerDeviceSubType`) - -| Value | Description | -|-------|-------------| -| `Blind` | Window blind/shade | -| `Bulb` | Light bulb | -| `ColorBulb` | Color light bulb | -| `ColorDimmer` | Color-capable dimmer | -| `Dimmer` | Standard dimmer | -| `Fan` | Dimmable fan | -| `InLine` | In-line dimmer module | -| `Outlet` | Dimmable outlet | -| `Plugin` | Plugin-defined dimmer | -| `Value` | Value-based dimmer | - -### Relay Sub-Types (`kRelayDeviceSubType`) - -| Value | Description | -|-------|-------------| -| `DoorBell` | Doorbell | -| `DoorController` | Door controller | -| `GarageController` | Garage door controller | -| `InLine` | In-line relay module | -| `Lock` | Lock device | -| `Outlet` | Switched outlet | -| `Plugin` | Plugin-defined relay | -| `Siren` | Siren/alarm | -| `Switch` | Wall switch | - -### Sensor Sub-Types (`kSensorDeviceSubType`) - -| Value | Description | -|-------|-------------| -| `Analog` | Analog sensor | -| `Binary` | Binary/digital sensor | -| `CO` | Carbon monoxide | -| `DoorWindow` | Door/window sensor | -| `GasLeak` | Gas leak detector | -| `GlassBreak` | Glass break sensor | -| `Humidity` | Humidity sensor | -| `Illuminance` | Light level sensor | -| `Motion` | Motion sensor | -| `Presence` | Presence/occupancy | -| `Pressure` | Pressure sensor | -| `Smoke` | Smoke detector | -| `Tamper` | Tamper sensor | -| `Temperature` | Temperature sensor | -| `UV` | UV index sensor | -| `Vibration` | Vibration sensor | -| `Voltage` | Voltage sensor | -| `WaterLeak` | Water leak sensor | -| `Zone` | Security zone | - -### Setting Sub-Types via pluginProps - -Some sub-type behaviors are set programmatically in `deviceStartComm()`: - -```python -def deviceStartComm(self, dev): - props = dev.pluginProps - - # Enable lock UI for relay devices - props["IsLockSubType"] = True - - # Enable color controls for dimmer devices - props["SupportsColor"] = True - props["SupportsRGB"] = True - props["SupportsWhite"] = True - props["SupportsTwoWhiteLevels"] = False - props["SupportsWhiteTemperature"] = True - props["WhiteTemperatureMin"] = 2000 # Kelvin - props["WhiteTemperatureMax"] = 6500 - - dev.replacePluginPropsOnServer(props) -``` - -## Protocol Constants - -```python -indigo.kProtocol.Plugin # Plugin-defined device -indigo.kProtocol.Insteon # INSTEON protocol -indigo.kProtocol.X10 # X10 protocol -indigo.kProtocol.ZWave # Z-Wave protocol -``` - -## HVAC Mode Constants - -```python -indigo.kHvacMode.Off -indigo.kHvacMode.Heat -indigo.kHvacMode.Cool -indigo.kHvacMode.HeatCool # Auto mode -indigo.kHvacMode.ProgramHeat -indigo.kHvacMode.ProgramCool -indigo.kHvacMode.ProgramHeatCool -``` - -## Fan Mode Constants - -```python -indigo.kFanMode.Auto -indigo.kFanMode.AlwaysOn -``` - -## State Value Types - -Used in `Devices.xml` state definitions: - -| Type | Description | -|------|-------------| -| `Integer` | Whole numbers | -| `Number` | Floating point | -| `String` | Text values | -| `Boolean` | True/False (sub-types: `TrueFalse`, `OnOff`, `YesNo`, `OneZero`) | -| `Separator` | UI separator (no value) | - -Boolean sub-types control display labels: - -```xml -<State id="isOnline"> - <ValueType boolType="TrueFalse">Boolean</ValueType> - <TriggerLabel>Online Status</TriggerLabel> -</State> -``` - -## UI Value Types (ConfigUI) - -| Type | Description | -|------|-------------| -| `textfield` | Single-line text input | -| `textarea` | Multi-line text input | -| `checkbox` | Boolean checkbox | -| `menu` | Dropdown menu | -| `list` | Multi-select list | -| `button` | Action button | -| `label` | Static text | -| `separator` | Visual separator | -| `serialport` | Serial port selector (auto-expanding) | -| `colorPicker` | Color picker (returns RGB string) | - -See [ConfigUI Reference](../../concepts/configui.md) for full field attribute documentation. - -## License Status Constants - -```python -indigo.kLicenseStatus.ActiveTrial -indigo.kLicenseStatus.ActiveSubscription -indigo.kLicenseStatus.ExpiredSubscription -indigo.kLicenseStatus.Unknown -``` diff --git a/docs/plugin-dev/api/iom/containers.md b/docs/plugin-dev/api/iom/containers.md deleted file mode 100644 index 9ee8aaf..0000000 --- a/docs/plugin-dev/api/iom/containers.md +++ /dev/null @@ -1,162 +0,0 @@ -# indigo.Dict and indigo.List - -Indigo provides special container classes that integrate with the Indigo database and preference system. - -## Key Differences from Python Containers - -| Behavior | `indigo.Dict`/`indigo.List` | Python `dict`/`list` | -|----------|----------------------------|----------------------| -| Value retrieval | Returns **copy** | Returns **reference** | -| Nested modification | Must reassign parent | Modifies in place | -| Database integration | Native | Requires conversion | -| Key restrictions | ASCII only, no spaces | Any hashable | - -## indigo.Dict - -### Basic Usage - -```python -d = indigo.Dict() -d['key'] = "value" -d.key = "value" # Dot notation also works -value = d['key'] -value = d.key -value = d.get('key', 'default') - -if 'key' in d: - pass -``` - -### Key Restrictions - -Keys must: -- Contain only letters, numbers, and ASCII characters -- NOT contain spaces -- NOT start with a number or punctuation -- NOT start with "xml", "XML", or "Xml" - -### Value Type Restrictions - -Values must be: `bool`, `float`, `int`, `string`, `list`, or `dict` - -Nested containers must recursively contain only compatible values. - -## indigo.List - -```python -c = indigo.List() -c.append(4) -c.append("string") -c.append(True) - -for item in c: - print(item) -``` - -## Critical: Copy Semantics - -**Values are always retrieved as copies, not references.** - -### The Gotcha - -```python -c = indigo.List() -c.append(4) -c.append(5) - -a = indigo.Dict() -a['c'] = c # COPY of c is inserted - -# This does NOT modify a['c']: -c.append(6) # Only modifies local c -print(a['c']) # Still [4, 5] - -# This also does NOT work: -a['c'].append(6) # Gets copy, appends to copy, copy discarded -print(a['c']) # Still [4, 5] -``` - -### The Solution: Reassign - -```python -# Must reassign to parent container: -c.append(6) -a['c'] = c # Now a['c'] has [4, 5, 6] -``` - -## Efficient Nested Access - -Avoid creating temporary copies with helper methods: - -### setitem_in_item() - -```python -# Slow - creates temporary copy of a['c']: -temp = a['c'] -temp[4] = "value" -a['c'] = temp - -# Fast - no temporary copy: -a.setitem_in_item('c', 4, "value") -``` - -### getitem_in_item() - -```python -# Slow - creates temporary copy: -value = a['c'][4] - -# Fast - no temporary copy: -value = a.getitem_in_item('c', 4) -``` - -## Converting to Native Python - -### to_dict() / to_list() - -Recursively converts nested structures: - -```python -python_dict = my_indigo_dict.to_dict() -python_list = my_indigo_list.to_list() -``` - -### Native Conversion (Indigo 2021.2+) - -```python -python_dict = dict(my_indigo_dict) -python_list = list(my_indigo_list) -``` - -## Common Use Cases - -### Plugin Properties - -```python -# Read plugin props (returns indigo.Dict) -props = dev.pluginProps - -# Modify and save -props['setting'] = "new value" -dev.replacePluginPropsOnServer(props) -``` - -### Device States - -```python -# Access states (returns indigo.Dict) -states = dev.states -current_value = states['myState'] -``` - -### Config UI Values - -```python -def validateDeviceConfigUi(self, valuesDict, typeId, devId): - # valuesDict is an indigo.Dict - if not valuesDict.get('requiredField'): - errorsDict = indigo.Dict() - errorsDict['requiredField'] = "This field is required" - return (False, valuesDict, errorsDict) - return (True, valuesDict) -``` diff --git a/docs/plugin-dev/api/iom/devices.md b/docs/plugin-dev/api/iom/devices.md deleted file mode 100644 index c78be01..0000000 --- a/docs/plugin-dev/api/iom/devices.md +++ /dev/null @@ -1,224 +0,0 @@ -# Device Classes Reference - -## Device Class Hierarchy - -``` -indigo.Device (base) -├── indigo.DimmerDevice -├── indigo.RelayDevice -├── indigo.SensorDevice -├── indigo.ThermostatDevice -├── indigo.SprinklerDevice -├── indigo.SpeedControlDevice -└── indigo.MultiIODevice -``` - -## Common Device Properties - -All device types share these properties: - -```python -dev.id # Unique ID (int) -dev.name # Device name (str) -dev.enabled # Is enabled (bool) -dev.configured # Is configured (bool) -dev.description # Description (str) -dev.model # Model name (str) -dev.address # Device address (str) -dev.version # Firmware version (str) -dev.subModel # Sub-model identifier (str) -dev.protocol # Protocol (indigo.kProtocol.*) -dev.deviceTypeId # Plugin device type ID (str) -dev.pluginId # Plugin bundle ID (str) -dev.folderId # Parent folder ID (int) -dev.lastChanged # Last state change (datetime) -dev.batteryLevel # Battery level if applicable (int or None) -dev.buttonGroupCount # Number of button groups (int) - -# Plugin properties -dev.pluginProps # indigo.Dict of user config -dev.globalProps # indigo.Dict of global properties -dev.ownerProps # Read-only plugin properties - -# States -dev.states # indigo.Dict of all states -dev.displayStateId # Primary display state ID -dev.displayStateValue # Primary display state value -dev.displayStateImageSel # Current state icon - -# Remote UI -dev.remoteDisplay # Text for remote display - -# Capabilities -dev.supportsStatusRequest # Can request status -dev.supportsAllOff # Supports all-off -dev.supportsAllLightsOnOff # Supports all-lights commands -``` - -## DimmerDevice - -Dimmable lighting devices. - -```python -dev.onState # Is on (bool) -dev.brightness # Brightness 0-100 (int) -dev.supportsRGB # Supports color (bool) -dev.supportsWhite # Supports white level (bool) -dev.supportsWhiteTemperature # Supports color temp (bool) -``` - -### Color Properties (if supported) - -```python -dev.redLevel # Red 0-100 -dev.greenLevel # Green 0-100 -dev.blueLevel # Blue 0-100 -dev.whiteLevel # White 0-100 -dev.whiteTemperature # Color temp in Kelvin -``` - -## RelayDevice - -On/off devices (switches, locks, outlets). - -```python -dev.onState # Is on (bool) -``` - -## SensorDevice - -Sensors (motion, door/window, temperature). - -```python -dev.onState # Sensor triggered state (bool) -dev.sensorValue # Numeric sensor value (float) - -# For binary sensors -dev.onOffState # "on" or "off" string -``` - -## ThermostatDevice - -HVAC thermostats. - -```python -dev.hvacMode # Current mode (indigo.kHvacMode.*) -dev.fanMode # Fan mode (indigo.kFanMode.*) -dev.coolSetpoint # Cooling setpoint (float) -dev.heatSetpoint # Heating setpoint (float) -dev.coolIsOn # Cooling active (bool) -dev.heatIsOn # Heating active (bool) -dev.fanIsOn # Fan active (bool) - -# Temperature sensors -dev.temperatureSensorCount # Number of sensors -dev.temperatures # List of temperatures - -# Humidity sensors -dev.humiditySensorCount # Number of humidity sensors -dev.humidities # List of humidity values -``` - -## SprinklerDevice - -Irrigation controllers. - -```python -dev.zoneCount # Number of zones -dev.activeZone # Currently active zone (0 = none) -dev.zoneNames # List of zone names -dev.zoneEnableList # List of enabled zones -dev.zoneDurations # List of zone durations -dev.zoneScheduledDurations # Scheduled durations -``` - -## SpeedControlDevice - -Variable speed devices (fans, motors). - -```python -dev.onState # Is on (bool) -dev.speedLevel # Speed 0-100 (int) -dev.speedIndex # Discrete speed index (int) -dev.speedIndexCount # Number of speed levels -dev.speedLabels # List of speed labels -``` - -## MultiIODevice - -Input/output devices. - -```python -dev.binaryInputs # List of binary input states -dev.binaryOutputs # List of binary output states -dev.analogInputs # List of analog input values -dev.sensorInputs # List of sensor values -``` - -## Device Methods - -### State Updates - -```python -# Update single state -dev.updateStateOnServer('stateName', value=new_value) -dev.updateStateOnServer('stateName', value=new_value, uiValue="Display Text") - -# Update multiple states (more efficient) -dev.updateStatesOnServer([ - {'key': 'state1', 'value': value1}, - {'key': 'state2', 'value': value2, 'uiValue': 'Display'} -]) -``` - -### State Image - -```python -dev.updateStateImageOnServer(indigo.kStateImageSel.SensorOn) -``` - -### Error State - -```python -dev.setErrorStateOnServer("Connection failed") -dev.setErrorStateOnServer(None) # Clear error -``` - -### Refresh Device Definition - -```python -# Call if Devices.xml changed dynamically -dev.stateListOrDisplayStateIdChanged() -``` - -### Server Synchronization - -```python -# Get fresh copy from server -dev.refreshFromServer() - -# Push local changes to server -dev.replaceOnServer() - -# Update plugin props -dev.replacePluginPropsOnServer(new_props_dict) -``` - -## HVAC Mode Constants - -```python -indigo.kHvacMode.Off -indigo.kHvacMode.Heat -indigo.kHvacMode.Cool -indigo.kHvacMode.HeatCool # Auto -indigo.kHvacMode.ProgramHeat -indigo.kHvacMode.ProgramCool -indigo.kHvacMode.ProgramHeatCool -``` - -## Fan Mode Constants - -```python -indigo.kFanMode.Auto -indigo.kFanMode.AlwaysOn -``` diff --git a/docs/plugin-dev/api/iom/filters.md b/docs/plugin-dev/api/iom/filters.md deleted file mode 100644 index 7bf658a..0000000 --- a/docs/plugin-dev/api/iom/filters.md +++ /dev/null @@ -1,149 +0,0 @@ -# Iteration Filters Reference - -Use filters with `.iter()` to efficiently iterate subsets of objects. - -## Device Filters - -```python -for dev in indigo.devices.iter("filter"): - pass -``` - -### Protocol Filters - -| Filter | Description | -|--------|-------------| -| `indigo.insteon` | INSTEON devices | -| `indigo.zwave` | Z-Wave devices | -| `indigo.x10` | X10 devices | - -### Device Type Filters - -| Filter | Description | -|--------|-------------| -| `indigo.dimmer` | Dimmer devices | -| `indigo.relay` | Relay/switch/lock devices | -| `indigo.sensor` | Sensor devices | -| `indigo.thermostat` | Thermostat devices | -| `indigo.sprinkler` | Sprinkler devices | -| `indigo.speedcontrol` | Speed control devices | -| `indigo.iodevice` | Input/output devices | - -### Capability Filters - -| Filter | Description | -|--------|-------------| -| `indigo.responder` | Devices whose state can be changed | -| `indigo.controller` | Devices that can send commands | - -### Plugin Filters - -| Filter | Description | -|--------|-------------| -| `self` | Devices defined by calling plugin | -| `self.myDeviceType` | Specific device type from calling plugin | -| `com.company.plugin` | All devices from specific plugin | -| `com.company.plugin.deviceType` | Specific device type from plugin | - -### Property Filters - -| Filter | Description | -|--------|-------------| -| `props.SupportsOnState` | Devices supporting ON state | - -### Combining Filters - -Combine filters with commas: - -```python -# INSTEON dimmers only -for dev in indigo.devices.iter("indigo.dimmer, indigo.insteon"): - print(dev.name) - -# Z-Wave sensors only -for dev in indigo.devices.iter("indigo.sensor, indigo.zwave"): - print(dev.name) -``` - -## Trigger Filters - -```python -for trigger in indigo.triggers.iter("filter"): - pass -``` - -| Filter | Description | -|--------|-------------| -| `indigo.devStateChange` | Device state change triggers | -| `indigo.varValueChange` | Variable value change triggers | -| `indigo.emailRcvd` | Email received triggers | -| `indigo.insteonCmdRcvd` | INSTEON command received | -| `indigo.x10CmdRcvd` | X10 command received | -| `indigo.serverStartup` | Server startup triggers | -| `indigo.powerFail` | Power failure triggers | -| `indigo.interfaceFail` | Interface failure triggers | -| `indigo.interfaceInit` | Interface initialized triggers | -| `indigo.pluginEvent` | Plugin-defined event triggers | -| `self` | Triggers from calling plugin | -| `self.myTriggerType` | Specific trigger type from plugin | -| `com.company.plugin.triggerType` | Trigger type from other plugin | - -**Note**: Unlike devices, only a single trigger filter can be used. - -## Variable Filters - -```python -for var in indigo.variables.iter("filter"): - pass -``` - -| Filter | Description | -|--------|-------------| -| `indigo.readWrite` | Read/write variables only | - -## Examples - -### All Plugin Devices - -```python -for dev in indigo.devices.iter("self"): - self.logger.info(f"My device: {dev.name}") -``` - -### Specific Plugin Device Type - -```python -for dev in indigo.devices.iter("self.myThermostat"): - self.logger.info(f"My thermostat: {dev.name}") -``` - -### All Dimmers at Full Brightness - -```python -for dev in indigo.devices.iter("indigo.dimmer"): - if dev.brightness == 100: - self.logger.info(f"Full bright: {dev.name}") -``` - -### Device State Change Triggers - -```python -for trigger in indigo.triggers.iter("indigo.devStateChange"): - self.logger.info(f"Monitoring device {trigger.deviceId}") -``` - -### Iterate IDs Only (More Efficient) - -```python -# When you only need IDs, not full objects -for dev_id in indigo.devices.iterkeys(): - # Process without loading full device - pass -``` - -## Iteration Behavior - -When iteration begins, the list is fixed: -- Items added during iteration are NOT included -- Items deleted during iteration are gracefully skipped -- No exceptions are thrown for mid-iteration changes diff --git a/docs/plugin-dev/api/iom/subscriptions.md b/docs/plugin-dev/api/iom/subscriptions.md deleted file mode 100644 index 4391530..0000000 --- a/docs/plugin-dev/api/iom/subscriptions.md +++ /dev/null @@ -1,276 +0,0 @@ -# Subscriptions and Event Callbacks - -Subscribe to object changes to receive real-time notifications. - -## Object Change Subscriptions - -### Available Subscriptions - -| Collection | Method | -|------------|--------| -| `indigo.devices` | `subscribeToChanges()` | -| `indigo.variables` | `subscribeToChanges()` | -| `indigo.triggers` | `subscribeToChanges()` | -| `indigo.schedules` | `subscribeToChanges()` | -| `indigo.actionGroups` | `subscribeToChanges()` | -| `indigo.controlPages` | `subscribeToChanges()` | - -### Subscribing - -Call in `__init__()` or `startup()`: - -```python -def __init__(self, pluginId, pluginDisplayName, pluginVersion, pluginPrefs): - super().__init__(pluginId, pluginDisplayName, pluginVersion, pluginPrefs) - - indigo.devices.subscribeToChanges() - indigo.variables.subscribeToChanges() - indigo.triggers.subscribeToChanges() - indigo.actionGroups.subscribeToChanges() -``` - -**Note**: Use sparingly - subscriptions generate significant traffic between IndigoServer and your plugin. - -## Device Callbacks - -```python -def deviceCreated(self, dev): - """Called when any device is created.""" - self.logger.debug(f"{dev.name} created") - -def deviceUpdated(self, origDev, newDev): - """Called when device state or properties change.""" - # Always call super first - indigo.PluginBase.deviceUpdated(self, origDev, newDev) - - # Check what changed - if origDev.onState != newDev.onState: - self.logger.info(f"{newDev.name} turned {'on' if newDev.onState else 'off'}") - -def deviceDeleted(self, dev): - """Called when any device is deleted.""" - self.logger.debug(f"{dev.name} deleted") -``` - -### Finding What Changed - -```python -def deviceUpdated(self, origDev, newDev): - indigo.PluginBase.deviceUpdated(self, origDev, newDev) - - # Convert to dicts for comparison - orig_dict = dict(origDev) - new_dict = dict(newDev) - - # Find changed attributes - diff = {k: new_dict[k] for k in orig_dict - if k in new_dict and orig_dict[k] != new_dict[k]} - - if diff: - self.logger.debug(f"Changed: {diff}") -``` - -## Variable Callbacks - -```python -def variableCreated(self, var): - """Called when any variable is created.""" - self.logger.debug(f"Variable created: {var.name}") - -def variableUpdated(self, origVar, newVar): - """Called when variable value changes.""" - indigo.PluginBase.variableUpdated(self, origVar, newVar) - - if origVar.value != newVar.value: - self.logger.info(f"{newVar.name}: {origVar.value} -> {newVar.value}") - -def variableDeleted(self, var): - """Called when any variable is deleted.""" - self.logger.debug(f"Variable deleted: {var.name}") -``` - -## Trigger Callbacks - -```python -def triggerCreated(self, trigger): - self.logger.debug(f"Trigger created: {trigger.name}") - -def triggerUpdated(self, origTrigger, newTrigger): - indigo.PluginBase.triggerUpdated(self, origTrigger, newTrigger) - -def triggerDeleted(self, trigger): - self.logger.debug(f"Trigger deleted: {trigger.name}") -``` - -## Schedule Callbacks - -```python -def scheduleCreated(self, schedule): - self.logger.debug(f"Schedule created: {schedule.name}") - -def scheduleUpdated(self, origSchedule, newSchedule): - indigo.PluginBase.scheduleUpdated(self, origSchedule, newSchedule) - -def scheduleDeleted(self, schedule): - self.logger.debug(f"Schedule deleted: {schedule.name}") -``` - -## Action Group Callbacks - -```python -def actionGroupCreated(self, actionGroup): - self.logger.debug(f"Action group created: {actionGroup.name}") - -def actionGroupUpdated(self, origActionGroup, newActionGroup): - indigo.PluginBase.actionGroupUpdated(self, origActionGroup, newActionGroup) - -def actionGroupDeleted(self, actionGroup): - self.logger.debug(f"Action group deleted: {actionGroup.name}") -``` - -## Control Page Callbacks - -```python -def controlPageCreated(self, controlPage): - self.logger.debug(f"Control page created: {controlPage.name}") - -def controlPageUpdated(self, origControlPage, newControlPage): - indigo.PluginBase.controlPageUpdated(self, origControlPage, newControlPage) - -def controlPageDeleted(self, controlPage): - self.logger.debug(f"Control page deleted: {controlPage.name}") -``` - -## Low-Level Protocol Subscriptions - -Monitor raw INSTEON or X10 commands (regardless of effect on device state). - -### INSTEON - -```python -def startup(self): - indigo.insteon.subscribeToIncoming() - indigo.insteon.subscribeToOutgoing() - -def insteonCommandReceived(self, cmd): - """Called for incoming INSTEON commands.""" - self.logger.debug(f"INSTEON received: {cmd}") - -def insteonCommandSent(self, cmd): - """Called for outgoing INSTEON commands.""" - self.logger.debug(f"INSTEON sent: {cmd}") -``` - -### X10 - -```python -def startup(self): - indigo.x10.subscribeToIncoming() - indigo.x10.subscribeToOutgoing() - -def x10CommandReceived(self, cmd): - """Called for incoming X10 commands.""" - self.logger.debug(f"X10 received: {cmd}") - - if cmd.cmdType == "sec": # Security command - if cmd.secCodeId == 6: - if cmd.secFunc == "sensor alert (max delay)": - self.logger.info("SENSOR OPEN") - elif cmd.secFunc == "sensor normal (max delay)": - self.logger.info("SENSOR CLOSED") - -def x10CommandSent(self, cmd): - """Called for outgoing X10 commands.""" - self.logger.debug(f"X10 sent: {cmd}") -``` - -## Best Practices - -### Use Sparingly - -Subscriptions create significant traffic. Good use cases: - -- Logging plugins (SQL Logger) -- Scene management (track device states) -- Integration bridges (sync with external systems) -- Fan controllers (monitor related devices) - -### Filter in Callbacks - -Check if the change is relevant before processing: - -```python -def deviceUpdated(self, origDev, newDev): - indigo.PluginBase.deviceUpdated(self, origDev, newDev) - - # Only process our plugin's devices - if newDev.pluginId != self.pluginId: - return - - # Only process if state actually changed - if origDev.states == newDev.states: - return - - # Now do work - self.processDeviceChange(newDev) -``` - -### Subscription Scope - -Note that `subscribeToChanges()` reports **actual changes only**: - -- Light turns ON → notification -- Light commanded ON when already ON → no notification (no change) - -### Plugin Device Subscriptions - -For your own plugin's devices, prefer the device lifecycle methods: - -```python -def deviceStartComm(self, dev): - """Called when your device starts.""" - pass - -def deviceStopComm(self, dev): - """Called when your device stops.""" - pass -``` - -## Example: Multi-Device Sync - -Sync multiple fans to act as one: - -```python -def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - indigo.devices.subscribeToChanges() - self.syncing = False # Prevent loops - -def deviceUpdated(self, origDev, newDev): - indigo.PluginBase.deviceUpdated(self, origDev, newDev) - - # Avoid recursive updates - if self.syncing: - return - - # Only handle our grouped fans - if newDev.pluginId != self.pluginId: - return - if newDev.deviceTypeId != "groupedFan": - return - - # Check if speed changed - if origDev.speedLevel == newDev.speedLevel: - return - - # Sync all fans in group - self.syncing = True - try: - group_id = newDev.pluginProps.get("groupId") - for dev in indigo.devices.iter("self.groupedFan"): - if dev.id != newDev.id: - if dev.pluginProps.get("groupId") == group_id: - indigo.speedcontrol.setSpeedLevel(dev, value=newDev.speedLevel) - finally: - self.syncing = False -``` diff --git a/docs/plugin-dev/api/iom/triggers.md b/docs/plugin-dev/api/iom/triggers.md deleted file mode 100644 index 5fa23f8..0000000 --- a/docs/plugin-dev/api/iom/triggers.md +++ /dev/null @@ -1,173 +0,0 @@ -# Trigger Classes Reference - -## Trigger Class Hierarchy - -``` -indigo.Trigger (base) -├── indigo.DeviceStateChangeTrigger -├── indigo.VariableValueChangeTrigger -├── indigo.EmailReceivedTrigger -├── indigo.InsteonCommandReceivedTrigger -├── indigo.X10CommandReceivedTrigger -├── indigo.ServerStartupTrigger -├── indigo.PowerFailureTrigger -├── indigo.InterfaceFailureTrigger -├── indigo.InterfaceInitializedTrigger -└── indigo.PluginEventTrigger -``` - -## Common Trigger Properties - -All trigger types share these properties: - -```python -trigger.id # Unique ID (int) -trigger.name # Trigger name (str) -trigger.enabled # Is enabled (bool) -trigger.folderId # Parent folder ID -trigger.pluginId # Plugin ID if plugin-defined -trigger.pluginTypeId # Plugin's event type ID -``` - -## Trigger Types - -### DeviceStateChangeTrigger - -Fires when a device state changes. - -```python -trigger = indigo.triggers[trigger_id] -trigger.deviceId # Monitored device ID -trigger.stateSelector # Which state to monitor -trigger.stateValue # Value to match (if applicable) -``` - -### VariableValueChangeTrigger - -Fires when a variable value changes. - -```python -trigger.variableId # Monitored variable ID -trigger.variableValue # Value to match (if applicable) -``` - -### PluginEventTrigger - -Fires when a plugin raises an event. - -```python -trigger.pluginId # Source plugin ID -trigger.pluginTypeId # Event type from Events.xml -trigger.pluginProps # Configuration properties -``` - -### EmailReceivedTrigger - -Fires when email is received matching criteria. - -### InsteonCommandReceivedTrigger - -Fires when an INSTEON command is received. - -```python -trigger.address # INSTEON address to monitor -trigger.command # Command type to match -``` - -### X10CommandReceivedTrigger - -Fires when an X10 command is received. - -### ServerStartupTrigger - -Fires when Indigo server starts. - -### PowerFailureTrigger - -Fires on power failure detection. - -### InterfaceFailureTrigger - -Fires when a hardware interface fails. - -### InterfaceInitializedTrigger - -Fires when a hardware interface initializes. - -## Plugin-Defined Triggers - -Plugins define custom trigger types in `Events.xml`: - -```xml -<Events> - <Event id="motionDetected"> - <Name>Motion Detected</Name> - <ConfigUI> - <Field id="zone" type="menu"> - <Label>Zone:</Label> - </Field> - </ConfigUI> - </Event> -</Events> -``` - -### Firing Plugin Events - -```python -# In plugin code, fire the event -indigo.trigger.execute(trigger) - -# Or trigger all matching plugin events -for trigger in indigo.triggers.iter("self.motionDetected"): - if trigger.pluginProps.get("zone") == detected_zone: - indigo.trigger.execute(trigger) -``` - -### triggerStartProcessing / triggerStopProcessing - -```python -def triggerStartProcessing(self, trigger): - """Called when trigger is enabled.""" - self.logger.debug(f"Trigger started: {trigger.name}") - -def triggerStopProcessing(self, trigger): - """Called when trigger is disabled.""" - self.logger.debug(f"Trigger stopped: {trigger.name}") -``` - -## Iterating Triggers - -```python -# All triggers -for trigger in indigo.triggers: - print(trigger.name) - -# By type -for trigger in indigo.triggers.iter("indigo.devStateChange"): - print(f"Device trigger: {trigger.name}") - -# Plugin's own triggers -for trigger in indigo.triggers.iter("self"): - print(f"My trigger: {trigger.name}") - -# Specific plugin trigger type -for trigger in indigo.triggers.iter("self.motionDetected"): - print(f"Motion trigger: {trigger.name}") -``` - -## Subscribing to Trigger Changes - -```python -# In startup() -indigo.triggers.subscribeToChanges() - -# Implement callbacks -def triggerCreated(self, trigger): - pass - -def triggerUpdated(self, origTrigger, newTrigger): - pass - -def triggerDeleted(self, trigger): - pass -``` diff --git a/docs/plugin-dev/api/iom/utilities.md b/docs/plugin-dev/api/iom/utilities.md deleted file mode 100644 index 57fb5ec..0000000 --- a/docs/plugin-dev/api/iom/utilities.md +++ /dev/null @@ -1,175 +0,0 @@ -# Utility Classes and Functions - -The `indigo.utils` module provides helper classes and functions. - -## Classes - -### FileNotFoundError - -Exception for file operations: - -```python -try: - # file operation - pass -except indigo.utils.FileNotFoundError: - self.logger.error("File not found") -``` - -### IndigoJSONEncoder - -JSON encoder that handles Python date/time objects: - -```python -import json - -dev = indigo.devices[123456] -dev_dict = dict(dev) - -# Properly encodes datetime objects -json_str = json.dumps(dev_dict, indent=4, cls=indigo.utils.IndigoJSONEncoder) -``` - -## Functions - -### return_static_file() - -Returns a file for the Indigo Web Server to serve: - -```python -indigo.utils.return_static_file("relative/path/to/file.html") - -indigo.utils.return_static_file( - "/absolute/path/to/file.json", - status=200, - path_is_relative=False, - content_type="application/json" -) -``` - -**Parameters:** - -| Parameter | Required | Type | Description | -|-----------|----------|------|-------------| -| `file_path` | Yes | str | Path to file | -| `status` | No | int | HTTP status code (default: 200) | -| `path_is_relative` | No | bool | Relative to plugin (default: True) | -| `content_type` | No | str | MIME type (auto-detected if omitted) | - -**Returns:** `indigo.Dict` for IWS to process: - -```python -{ - "status": 200, - "headers": {"Content-Type": "text/html"}, - "file_path": "/full/path/to/file.html" -} -``` - -**Raises:** `FileNotFoundError` or `TypeError` - -### validate_email_address() - -Validates email address format: - -```python -if indigo.utils.validate_email_address("user@example.com"): - # Valid format - pass -``` - -**Note:** Validates format only, not whether the address exists. - -### str_to_bool() - -Converts boolean-like strings to actual booleans: - -```python -indigo.utils.str_to_bool("yes") # True -indigo.utils.str_to_bool("no") # False -indigo.utils.str_to_bool("on") # True -indigo.utils.str_to_bool("off") # False -indigo.utils.str_to_bool("open") # True -indigo.utils.str_to_bool("closed") # False -``` - -**Recognized mappings:** - -| True | False | -|------|-------| -| y | n | -| yes | no | -| t | f | -| true | false | -| on | off | -| 1 | 0 | -| open | closed | -| locked | unlocked | - -**Raises:** `ValueError` if string cannot be converted. - -### reverse_bool_str_value() - -Returns the opposite boolean string: - -```python -indigo.utils.reverse_bool_str_value("open") # "closed" -indigo.utils.reverse_bool_str_value("yes") # "no" -indigo.utils.reverse_bool_str_value("locked") # "unlocked" -``` - -**Raises:** `ValueError` if string not recognized. - -## Common Use Cases - -### Serving Static Files from HTTP Responder - -```python -def handleWebRequest(self, path, params): - if path == "/status": - return indigo.utils.return_static_file("static/status.html") - elif path == "/data.json": - return indigo.utils.return_static_file("static/data.json") - else: - return indigo.utils.return_static_file( - "static/404.html", - status=404 - ) -``` - -### Validating Email in Config UI - -```python -def validatePrefsConfigUi(self, valuesDict): - errorsDict = indigo.Dict() - - email = valuesDict.get("notificationEmail", "") - if email and not indigo.utils.validate_email_address(email): - errorsDict["notificationEmail"] = "Invalid email format" - - if errorsDict: - return (False, valuesDict, errorsDict) - return (True, valuesDict) -``` - -### Converting User Input - -```python -def processUserSetting(self, value_str): - try: - enabled = indigo.utils.str_to_bool(value_str) - return enabled - except ValueError: - self.logger.warning(f"Unrecognized value: {value_str}") - return False -``` - -### Serializing Device State - -```python -import json - -def getDeviceStateAsJson(self, dev): - dev_dict = dict(dev) - return json.dumps(dev_dict, cls=indigo.utils.IndigoJSONEncoder) -``` diff --git a/docs/plugin-dev/concepts/README.md b/docs/plugin-dev/concepts/README.md deleted file mode 100644 index ee3963b..0000000 --- a/docs/plugin-dev/concepts/README.md +++ /dev/null @@ -1,80 +0,0 @@ -# Core Concepts - -Essential architectural concepts for Indigo plugin development. - -## Available Documentation - -### [Plugin Lifecycle](plugin-lifecycle.md) -Complete guide to plugin initialization, startup, execution, and shutdown. - -**Topics covered**: -- Lifecycle methods: `__init__()`, `startup()`, `runConcurrentThread()`, `shutdown()` -- Device lifecycle callbacks -- Sleep/wake handling -- Common patterns and mistakes -- Resource management best practices - -**Start here if**: You're creating a new plugin or debugging lifecycle issues - -### [Device Development](devices.md) -Comprehensive guide to creating and managing plugin devices. - -**Topics covered**: -- Device types (relay, dimmer, sensor, thermostat, custom, etc.) -- Device XML definition -- State management and updates -- Configuration UIs -- Validation callbacks -- Dynamic lists - -**Start here if**: You need to create devices or manage device states - -## Coming Soon - -Documentation planned for this section: - -- **State Management** - Deep dive into device state patterns and performance -- **Concurrent Threading** - Advanced background task patterns and thread safety -- **Event System** - Subscribing to and handling Indigo events -- **Action System** - Creating and handling custom actions -- **Variable System** - Working with Indigo variables -- **Trigger System** - Creating custom trigger types -- **Plugin Communication** - Broadcasting and subscribing between plugins - -## Quick Reference - -### When to use each concept: - -| Task | Read This | -|------|-----------| -| Creating first plugin | [Plugin Lifecycle](plugin-lifecycle.md) | -| Plugin won't start | [Plugin Lifecycle](plugin-lifecycle.md) → Debugging | -| Plugin won't stop | [Plugin Lifecycle](plugin-lifecycle.md) → Common Mistakes | -| Creating devices | [Device Development](devices.md) | -| Updating device states | [Device Development](devices.md) → Updating Device States | -| Validating config | [Device Development](devices.md) → Configuration Validation | -| Polling APIs | [Plugin Lifecycle](plugin-lifecycle.md) → runConcurrentThread() | - -## Related Documentation - -- **[Quick Start](../quick-start.md)** - Get a plugin running in minutes -- **[API Reference](../api/)** - Detailed API documentation -- **[Patterns](../patterns/)** - Reusable implementation patterns -- **[Examples](../examples/)** - Complete working examples - -## External Resources - -- [Official Plugin Developer's Guide](https://www.indigodomo.com/docs/plugin_guide) -- [Indigo Object Model Reference](https://www.indigodomo.com/docs/object_model_reference) -- [Indigo Developer Forum](https://forums.indigodomo.com/viewforum.php?f=18) - -## Contributing - -Want to add documentation to this section? Open a PR or issue on the [GitHub repo](https://github.com/simons-plugins/indigo-claude-plugin). - -**Valuable topics include**: -- State update performance patterns -- Thread safety with concurrent access -- Communication between plugins -- Working with server scripts -- Memory management best practices diff --git a/docs/plugin-dev/concepts/actions.md b/docs/plugin-dev/concepts/actions.md index 086e92c..12cd4c6 100644 --- a/docs/plugin-dev/concepts/actions.md +++ b/docs/plugin-dev/concepts/actions.md @@ -1,336 +1,10 @@ -# Actions & Action Handling +# Actions & Action Handling — field notes -**Official Documentation**: https://wiki.indigodomo.com/doku.php?id=indigo_2025.1_documentation:plugin_guide#actions - -## Actions.xml - -Define custom plugin actions in `Actions.xml`. Users configure and trigger these from Indigo's action groups, triggers, and schedules. - -### Basic Structure - -```xml -<?xml version="1.0"?> -<Actions> - <!-- Optional: global help URL for all actions --> - <SupportURL>https://my-plugin-docs.example.com/actions</SupportURL> - - <Action id="resetHardware" deviceFilter="self"> - <Name>Reset Hardware</Name> - <CallbackMethod>reset_hardware</CallbackMethod> - </Action> -</Actions> -``` - -### Action with ConfigUI - -```xml -<Action id="setBacklightBrightness" deviceFilter="self"> - <Name>Set Backlight Brightness</Name> - <CallbackMethod>set_backlight_brightness</CallbackMethod> - <ConfigUI> - <Field id="brightness" type="textfield" defaultValue="100"> - <Label>Brightness (0-100):</Label> - </Field> - </ConfigUI> -</Action> -``` - -To pre-populate fields with computed or live values when the dialog opens (e.g. seed `brightness` from the device's current state), override `getActionConfigUiValues(self, plugin_props, type_id, dev_id)` — see [`plugin-lifecycle.md`](plugin-lifecycle.md#configui-pre-population-callbacks). - -### Action Targeting All Devices - -```xml -<Action id="getDeviceInfo" deviceFilter="indigo.devices"> - <Name>Get Device Info</Name> - <CallbackMethod>get_device_info</CallbackMethod> - <ConfigUI> - <Field id="format" type="menu" defaultValue="json"> - <Label>Output Format:</Label> - <List> - <Option value="json">JSON</Option> - <Option value="yaml">YAML</Option> - </List> - </Field> - </ConfigUI> -</Action> -``` - -### Action Attributes - -| Attribute | Description | -|-----------|-------------| -| `id` | Unique action identifier (required) | -| `deviceFilter` | Which devices appear in the device picker | - -**deviceFilter values**: -- `"self"` — only this plugin's devices -- `"indigo.devices"` — all Indigo devices -- Omit for actions not tied to a device - -### Custom Plugin Action Callback - -```python -def set_backlight_brightness(self, plugin_action, dev): - """Handle custom plugin action.""" - try: - brightness = int(plugin_action.props.get("brightness", 100)) - except ValueError: - self.logger.error(f"Invalid brightness value for \"{dev.name}\"") - return - - # Command hardware here - send_success = True - - if send_success: - self.logger.info(f"sent \"{dev.name}\" set backlight to {brightness}") - dev.updateStateOnServer("backlightBrightness", brightness) - else: - self.logger.error(f"send \"{dev.name}\" set backlight to {brightness} failed") -``` - -Note: The parameter is `plugin_action` (a `PluginAction` object), not `action`. Access user-configured values via `plugin_action.props`. - -### Returning Results to Scripting Callers - -Actions called via `indigo.server.getPlugin(pluginId).executeAction()` can return values: - -```python -def get_device_info(self, action, dev=None, caller_waiting_for_result=None): - props = dict(action.props) - reply = indigo.Dict() - reply["name"] = dev.name - reply["status"] = "online" - return reply # Returned to the calling script -``` - -Return types: `None`, `bool`, `int`, `float`, `str`, `indigo.Dict`, `indigo.List`. - -## Standard Action Handling Callbacks - -When a plugin defines device types (relay, dimmer, thermostat, etc.), it **must** implement the corresponding `actionControl*()` callback to handle standard Indigo actions. - -### Callback Summary - -| Device Type | Callback | Action Property | Enum | -|-------------|----------|-----------------|------| -| relay | `actionControlDevice(self, action, dev)` | `action.deviceAction` | `indigo.kDeviceAction` | -| dimmer | `actionControlDevice(self, action, dev)` | `action.deviceAction` | `indigo.kDeviceAction` | -| sensor | `actionControlSensor(self, action, dev)` | `action.sensorAction` | `indigo.kSensorAction` | -| thermostat | `actionControlThermostat(self, action, dev)` | `action.thermostatAction` | `indigo.kThermostatAction` | -| sprinkler | `actionControlSprinkler(self, action, dev)` | `action.sprinklerAction` | `indigo.kSprinklerAction` | -| speedcontrol | `actionControlSpeedControl(self, action, dev)` | `action.speedControlAction` | `indigo.kSpeedControlAction` | -| all types | `actionControlUniversal(self, action, dev)` | `action.deviceAction` | `indigo.kUniversalAction` | - -### Relay / Dimmer Actions - -```python -def actionControlDevice(self, action, dev): - if action.deviceAction == indigo.kDeviceAction.TurnOn: - # Command hardware to turn on - send_success = True - if send_success: - self.logger.info(f"sent \"{dev.name}\" on") - dev.updateStateOnServer("onOffState", True) - else: - self.logger.error(f"send \"{dev.name}\" on failed") - - elif action.deviceAction == indigo.kDeviceAction.TurnOff: - send_success = True - if send_success: - self.logger.info(f"sent \"{dev.name}\" off") - dev.updateStateOnServer("onOffState", False) - - elif action.deviceAction == indigo.kDeviceAction.Toggle: - new_on_state = not dev.onState - send_success = True - if send_success: - self.logger.info(f"sent \"{dev.name}\" toggle") - dev.updateStateOnServer("onOffState", new_on_state) - - elif action.deviceAction == indigo.kDeviceAction.SetBrightness: - new_brightness = action.actionValue # 0-100 - send_success = True - if send_success: - self.logger.info(f"sent \"{dev.name}\" set brightness to {new_brightness}") - dev.updateStateOnServer("brightnessLevel", new_brightness) - - elif action.deviceAction == indigo.kDeviceAction.BrightenBy: - new_brightness = min(dev.brightness + action.actionValue, 100) - send_success = True - if send_success: - dev.updateStateOnServer("brightnessLevel", new_brightness) - - elif action.deviceAction == indigo.kDeviceAction.DimBy: - new_brightness = max(dev.brightness - action.actionValue, 0) - send_success = True - if send_success: - dev.updateStateOnServer("brightnessLevel", new_brightness) - - elif action.deviceAction == indigo.kDeviceAction.SetColorLevels: - # action.actionValue is a dict with keys: - # redLevel, greenLevel, blueLevel, whiteLevel, whiteLevel2, whiteTemperature - color_dict = action.actionValue - # Command hardware with color values - - elif action.deviceAction == indigo.kDeviceAction.Lock: - # For lock sub-type devices - send_success = True - if send_success: - dev.updateStateOnServer("onOffState", True) - - elif action.deviceAction == indigo.kDeviceAction.Unlock: - send_success = True - if send_success: - dev.updateStateOnServer("onOffState", False) -``` - -### Thermostat Actions - -```python -def actionControlThermostat(self, action, dev): - if action.thermostatAction == indigo.kThermostatAction.SetHvacMode: - hvac_mode = action.actionMode - # Command hardware, then update state: - dev.updateStateOnServer("hvacOperationMode", hvac_mode) - - elif action.thermostatAction == indigo.kThermostatAction.SetFanMode: - fan_mode = action.actionMode - dev.updateStateOnServer("hvacFanMode", fan_mode) - - elif action.thermostatAction == indigo.kThermostatAction.SetHeatSetpoint: - new_setpoint = action.actionValue - dev.updateStateOnServer("setpointHeat", new_setpoint, - uiValue=f"{new_setpoint:.1f} °F") - - elif action.thermostatAction == indigo.kThermostatAction.SetCoolSetpoint: - new_setpoint = action.actionValue - dev.updateStateOnServer("setpointCool", new_setpoint, - uiValue=f"{new_setpoint:.1f} °F") - - elif action.thermostatAction == indigo.kThermostatAction.IncreaseHeatSetpoint: - new_setpoint = dev.heatSetpoint + action.actionValue - dev.updateStateOnServer("setpointHeat", new_setpoint, - uiValue=f"{new_setpoint:.1f} °F") - - elif action.thermostatAction == indigo.kThermostatAction.DecreaseHeatSetpoint: - new_setpoint = dev.heatSetpoint - action.actionValue - dev.updateStateOnServer("setpointHeat", new_setpoint, - uiValue=f"{new_setpoint:.1f} °F") - - elif action.thermostatAction == indigo.kThermostatAction.IncreaseCoolSetpoint: - new_setpoint = dev.coolSetpoint + action.actionValue - dev.updateStateOnServer("setpointCool", new_setpoint, - uiValue=f"{new_setpoint:.1f} °F") - - elif action.thermostatAction == indigo.kThermostatAction.DecreaseCoolSetpoint: - new_setpoint = dev.coolSetpoint - action.actionValue - dev.updateStateOnServer("setpointCool", new_setpoint, - uiValue=f"{new_setpoint:.1f} °F") - - elif action.thermostatAction in ( - indigo.kThermostatAction.RequestStatusAll, - indigo.kThermostatAction.RequestMode, - indigo.kThermostatAction.RequestEquipmentState, - indigo.kThermostatAction.RequestTemperatures, - indigo.kThermostatAction.RequestHumidities, - indigo.kThermostatAction.RequestDeadbands, - indigo.kThermostatAction.RequestSetpoints, - ): - # Query hardware for current status - self.logger.info(f"sent \"{dev.name}\" status request") -``` - -### Sensor Actions - -Sensors are typically read-only — reject write actions: - -```python -def actionControlSensor(self, action, dev): - self.logger.info(f"ignored \"{dev.name}\" {action.sensorAction} request (sensor is read-only)") -``` - -### Sprinkler Actions - -```python -def actionControlSprinkler(self, action, dev): - if action.sprinklerAction == indigo.kSprinklerAction.ZoneOn: - zone = action.zoneIndex # 1-based zone index - # Turn on zone - - elif action.sprinklerAction == indigo.kSprinklerAction.AllZonesOff: - # Turn off all zones - - elif action.sprinklerAction == indigo.kSprinklerAction.RunNewSchedule: - durations = action.zoneDurations # List of durations per zone - # Start schedule run - - elif action.sprinklerAction == indigo.kSprinklerAction.RunPreviousSchedule: - # Re-run previous schedule - - elif action.sprinklerAction == indigo.kSprinklerAction.PauseSchedule: - # Pause current schedule - - elif action.sprinklerAction == indigo.kSprinklerAction.ResumeSchedule: - # Resume paused schedule - - elif action.sprinklerAction == indigo.kSprinklerAction.StopSchedule: - # Stop schedule - - elif action.sprinklerAction == indigo.kSprinklerAction.PreviousZone: - # Move to previous zone - - elif action.sprinklerAction == indigo.kSprinklerAction.NextZone: - # Move to next zone -``` - -### Speed Control Actions - -```python -def actionControlSpeedControl(self, action, dev): - if action.speedControlAction == indigo.kSpeedControlAction.TurnOn: - dev.updateStateOnServer("onOffState", True) - - elif action.speedControlAction == indigo.kSpeedControlAction.TurnOff: - dev.updateStateOnServer("onOffState", False) - - elif action.speedControlAction == indigo.kSpeedControlAction.Toggle: - dev.updateStateOnServer("onOffState", not dev.onState) - - elif action.speedControlAction == indigo.kSpeedControlAction.SetSpeedIndex: - # 0=off, 1=low, 2=medium, 3=high - index = action.actionValue - dev.updateStateOnServer("speedIndex", index) - - elif action.speedControlAction == indigo.kSpeedControlAction.SetSpeedLevel: - # 0-100 absolute level - level = action.actionValue - dev.updateStateOnServer("speedLevel", level) - - elif action.speedControlAction == indigo.kSpeedControlAction.IncreaseSpeedIndex: - new_index = min(dev.speedIndex + action.actionValue, dev.speedIndexCount - 1) - dev.updateStateOnServer("speedIndex", new_index) - - elif action.speedControlAction == indigo.kSpeedControlAction.DecreaseSpeedIndex: - new_index = max(dev.speedIndex - action.actionValue, 0) - dev.updateStateOnServer("speedIndex", new_index) -``` - -### Universal Actions (all device types) - -```python -def actionControlUniversal(self, action, dev): - if action.deviceAction == indigo.kUniversalAction.Beep: - self.logger.info(f"sent \"{dev.name}\" beep request") - - elif action.deviceAction == indigo.kUniversalAction.EnergyUpdate: - self.logger.info(f"sent \"{dev.name}\" energy update request") - - elif action.deviceAction == indigo.kUniversalAction.EnergyReset: - self.logger.info(f"sent \"{dev.name}\" energy reset request") - - elif action.deviceAction == indigo.kUniversalAction.RequestStatus: - self.logger.info(f"sent \"{dev.name}\" status request") -``` +Undocumented Indigo action behaviour learned in the field — **not** covered by the canonical +reference. For `Actions.xml`, action ConfigUI, the standard device-action callbacks +(relay/dimmer/thermostat/sensor/sprinkler/speed/universal), and validation, use the canonical docs +(routed from `/indigo:dev`): `reference/canonical/plugin-dev/reference/xml/actions.md` and +`reference/canonical/plugin-dev/reference/plugin-py/device-methods.md`. ## `uiPath` attribute — PascalCase, no spaces @@ -380,28 +54,8 @@ If Indigo exposes a direct server API for the same operation (e.g. `getPlugin(...).executeAction(...)` — fewer moving parts and no prop- serialization layer to misbehave. -## Action Validation - -```python -def validateActionConfigUi(self, values_dict, type_id, device_id): - errors = indigo.Dict() - - brightness = values_dict.get("brightness", "") - try: - val = int(brightness) - if val < 0 or val > 100: - errors["brightness"] = "Brightness must be between 0 and 100" - except ValueError: - errors["brightness"] = "Must be a number" - - if len(errors) > 0: - return (False, values_dict, errors) - return (True, values_dict) -``` - ## See Also -- [ConfigUI Reference](configui.md) — Field types, attributes, dynamic lists -- [Constants Reference](../api/iom/constants.md) — Action enum values -- [Device Development](devices.md) — Device types and state management -- [SDK Examples Guide](../examples/sdk-examples-guide.md) — Working action implementations +- Actions.xml + action ConfigUI: `reference/canonical/plugin-dev/reference/xml/actions.md` +- `actionControlDevice` and device-action callbacks: `reference/canonical/plugin-dev/reference/plugin-py/device-methods.md` +- Device field notes: [devices.md](devices.md) diff --git a/docs/plugin-dev/concepts/configui.md b/docs/plugin-dev/concepts/configui.md deleted file mode 100644 index 74e0674..0000000 --- a/docs/plugin-dev/concepts/configui.md +++ /dev/null @@ -1,405 +0,0 @@ -# ConfigUI Reference - -**Official Documentation**: https://wiki.indigodomo.com/doku.php?id=indigo_2025.1_documentation:plugin_guide#configui_xml_reference - -ConfigUI elements define the user interface for plugin configuration, device settings, action parameters, event settings, and menu item dialogs. They appear in `PluginConfig.xml`, `Devices.xml`, `Actions.xml`, `Events.xml`, and `MenuItems.xml`. - -## Field Types - -### textfield - -Single-line text input. - -```xml -<Field id="ipAddress" type="textfield" defaultValue="192.168.1.100"> - <Label>IP Address:</Label> -</Field> - -<!-- Secure field (masked input, stored encrypted) --> -<Field id="apiKey" type="textfield" defaultValue="" secure="true"> - <Label>API Key:</Label> -</Field> - -<!-- Read-only field --> -<Field id="status" type="textfield" readonly="yes"> - <Label>Status:</Label> -</Field> -``` - -### textarea - -Multi-line text input. - -```xml -<Field id="notes" type="textarea" defaultValue=""> - <Label>Notes:</Label> -</Field> -``` - -### checkbox - -Boolean toggle with optional description text below. - -```xml -<Field id="enableLogging" type="checkbox" defaultValue="false"> - <Label>Enable Detailed Logging:</Label> - <Description>Logs all API requests and responses to the Indigo event log.</Description> -</Field> -``` - -### menu - -Dropdown/popup menu — static options or dynamic from plugin method. - -```xml -<!-- Static options --> -<Field id="protocol" type="menu" defaultValue="https"> - <Label>Protocol:</Label> - <List> - <Option value="http">HTTP</Option> - <Option value="https">HTTPS</Option> - </List> -</Field> - -<!-- Dynamic options from plugin method --> -<Field id="targetDevice" type="menu"> - <Label>Device:</Label> - <List class="self" method="get_device_list" dynamicReload="true"/> -</Field> -``` - -### list - -Multi-select list control. Use `rows` attribute to set visible rows (minimum 4). - -```xml -<Field id="memberDeviceList" type="list"> - <Label>Devices in group:</Label> - <List class="self" method="member_devices" dynamicReload="true"/> -</Field> -``` - -### button - -Clickable button that triggers a plugin callback method. - -```xml -<Field id="testConnection" type="button"> - <Label/> - <Title>Test Connection - test_connection - -``` - -### label - -Static text for display. Supports font styling. - -```xml - - - - - - - - - -``` - -**Label attributes**: - -| Attribute | Values | Description | -|-----------|--------|-------------| -| `fontColor` | `black`, `darkgray`, `red`, `orange`, `green`, `blue` | Text color | -| `fontSize` | `regular`, `small`, `mini` | Text size | -| `alignText` | `left`, `center`, `right` | Text alignment | -| `alignWithControl` | `true` / `false` | Align with control column (default aligns with labels) | - -### separator - -Visual separator line. Self-terminating. - -```xml - -``` - -### serialport - -Auto-expanding serial port selector. Generates fields for local ports, network sockets, and RFC-2217 connections. - -```xml - - - -``` - -Validate with the built-in helper: - -```python -def validateDeviceConfigUi(self, values_dict, type_id, dev_id): - errors_dict = indigo.Dict() - self.validateSerialPortUi(values_dict, errors_dict, "serialPort") - if len(errors_dict) > 0: - return (False, values_dict, errors_dict) - return (True, values_dict) -``` - -### colorPicker - -Color selector that returns a space-delimited RGB string (e.g., `"255 128 0"`). - -```xml - - - -``` - -## Field Attributes - -### Common Attributes - -| Attribute | Type | Description | -|-----------|------|-------------| -| `id` | String | Unique field identifier (required) | -| `type` | String | Field type (required) | -| `defaultValue` | String | Default value | -| `hidden` | `"true"` / `"yes"` | Hide field from UI (value still accessible in `valuesDict`) | -| `tooltip` | String | Hover tooltip text | -| `secure` | `"true"` | Mask input and store encrypted (for passwords, API keys) | -| `readonly` | `"yes"` | Prevent user editing | - -### Conditional Visibility - -Control whether a field is visible based on another field's value: - -```xml - - - - - - - - - -``` - -| Attribute | Description | -|-----------|-------------| -| `visibleBindingId` | Field ID that controls this field's visibility | -| `visibleBindingValue` | Value of the binding field that makes this field visible | -| `alwaysUseInDialogHeightCalc` | `"true"` — prevents dialog resizing when field toggles visibility | - -### Conditional Enabled State - -Control whether a field is enabled/disabled based on a checkbox: - -```xml - - - - - - - - -``` - -| Attribute | Description | -|-----------|-------------| -| `enabledBindingId` | Checkbox field ID that controls this field's enabled state | -| `enabledBindingNegate` | `"true"` — negate the binding (enabled when checkbox is unchecked) | - -### Combined Bindings - -Attributes can be combined for complex conditional logic: - -```xml - - - -``` - -This field is: -- Only **visible** when `supportsTwoWhiteLevels` is `"false"` -- Only **enabled** when `supportsWhite` is checked -- Always counted in dialog height calculation - -## Dynamic Lists - -### Built-in List Classes - -Use the `class` attribute on `` to populate from Indigo's database: - -| Class | Description | -|-------|-------------| -| `indigo.devices` | All devices | -| `indigo.variables` | All variables | -| `indigo.triggers` | All triggers | -| `indigo.schedules` | All schedules | -| `indigo.actionGroups` | All action groups | -| `indigo.controlPages` | All control pages | -| `indigo.serialPorts` | Available serial ports | -| `self` | This plugin's devices only | -| `self.devTypeId` | This plugin's devices of a specific type | -| `com.somePlugin` | Another plugin's devices | -| `com.somePlugin.devTypeId` | Another plugin's devices of a specific type | - -### Device Filters - -Combine with the `filter` attribute to narrow device selection: - -| Filter | Description | -|--------|-------------| -| `indigo.relay` | Relay devices | -| `indigo.dimmer` | Dimmer devices | -| `indigo.sensor` | Sensor devices | -| `indigo.thermostat` | Thermostat devices | -| `indigo.sprinkler` | Sprinkler devices | -| `indigo.speedcontrol` | Speed control devices | -| `indigo.iodevice` | Multi I/O devices | -| `indigo.insteon` | INSTEON protocol devices | -| `indigo.zwave` | Z-Wave protocol devices | -| `indigo.x10` | X10 protocol devices | -| `indigo.responder` | Responder devices | -| `indigo.controller` | Controller devices | - -```xml - - - - -``` - -### Trigger Filters - -| Filter | Description | -|--------|-------------| -| `indigo.devStateChange` | Device state change triggers | -| `indigo.varValueChange` | Variable value change triggers | -| `indigo.insteonCmdRcvd` | INSTEON command received triggers | -| `indigo.x10CmdRcvd` | X10 command received triggers | -| `indigo.serverStartup` | Server startup triggers | -| `indigo.powerFailure` | Power failure triggers | -| `indigo.interfaceFail` | Interface failure triggers | -| `indigo.interfaceInit` | Interface initialized triggers | -| `indigo.emailRcvd` | Email received triggers | - -### Variable Filters - -| Filter | Description | -|--------|-------------| -| `indigo.readWrite` | Read/write variables only (excludes read-only) | - -### Custom Dynamic Lists - -Plugin method populating a menu or list: - -```xml - - - - -``` - -```python -def get_device_list(self, filter="", values_dict=None, type_id="", target_id=0): - """Return list of (value, label) tuples for menu/list population.""" - device_list = [] - for dev in indigo.devices.iter("self"): - device_list.append((str(dev.id), dev.name)) - return device_list -``` - -**Parameters**: -- `filter` — filter string from `` element (optional) -- `values_dict` — current dialog values -- `type_id` — device/action/event type ID -- `target_id` — device/action/event ID (0 for new) - -**Return**: List of `(value_string, display_string)` tuples. - -## Pre-population Callbacks - -To seed fields with computed values when a dialog opens (today's date, the latest sensor reading, a freshly minted token), override the matching `get*ConfigUiValues` callback. Static `defaultValue` only fires once and dynamic lists do not auto-select their first item — the callback is the supported way to set values that may change between opens. See [`plugin-lifecycle.md`](plugin-lifecycle.md#configui-pre-population-callbacks) for the full reference table covering all five callbacks (`getPrefsConfigUiValues`, `getDeviceConfigUiValues`, `getEventConfigUiValues`, `getMenuActionConfigUiValues`, `getActionConfigUiValues`). - -## Validation - -### Validation Callbacks - -| Context | Method | -|---------|--------| -| Plugin config | `validatePrefsConfigUi(self, values_dict)` | -| Device config | `validateDeviceConfigUi(self, values_dict, type_id, dev_id)` | -| Action config | `validateActionConfigUi(self, values_dict, type_id, device_id)` | -| Event config | `validateEventConfigUi(self, values_dict, type_id, event_id)` | -| Device factory | `validateDeviceFactoryUi(self, values_dict, dev_id_list)` | - -### Return Values - -```python -# Success (3 patterns, all accepted) -return True -return (True, values_dict) -return (True, values_dict, indigo.Dict()) - -# Failure — show errors on specific fields -errors = indigo.Dict() -errors["fieldId"] = "This field is required" -return (False, values_dict, errors) - -# Failure — show alert dialog -errors = indigo.Dict() -errors["showAlertText"] = "Unable to connect. Check IP address and try again." -return (False, values_dict, errors) -``` - -The special key `"showAlertText"` in the errors dict displays a modal alert dialog instead of highlighting a field. - -### Dialog Close Callbacks - -Called after validation succeeds or user cancels: - -| Context | Method | -|---------|--------| -| Plugin config | `closedPrefsConfigUi(self, values_dict, user_cancelled)` | -| Device config | `closedDeviceConfigUi(self, values_dict, user_cancelled, type_id, dev_id)` | -| Action config | `closedActionConfigUi(self, values_dict, user_cancelled, type_id, action_id)` | -| Event config | `closedEventConfigUi(self, values_dict, user_cancelled, type_id, event_id)` | -| Device factory | `closedDeviceFactoryUi(self, values_dict, user_cancelled, dev_id_list)` | - -## SupportURL - -Add a help button to any ConfigUI dialog: - -```xml - - https://my-plugin-docs.example.com/setup - - -``` - -Can also be set at the top level of `Actions.xml`: - -```xml - - https://my-plugin-docs.example.com/actions - -``` - -Falls back to the `SupportURL` in `Info.plist` if not specified. - -## See Also - -- [Device Development](devices.md) — Device-specific ConfigUI in Devices.xml -- [Actions](actions.md) — Action-specific ConfigUI in Actions.xml -- [Menu Items](menu-items.md) — Menu-specific ConfigUI in MenuItems.xml -- [Plugin Preferences](plugin-preferences.md) — PluginConfig.xml -- [Constants Reference](../api/iom/constants.md) — Enum values diff --git a/docs/plugin-dev/concepts/devices.md b/docs/plugin-dev/concepts/devices.md index 17dfd67..16c525e 100644 --- a/docs/plugin-dev/concepts/devices.md +++ b/docs/plugin-dev/concepts/devices.md @@ -1,299 +1,10 @@ -# Device Development +# Device Development — field notes -**Official Documentation**: https://wiki.indigodomo.com/doku.php?id=indigo_2025.1_documentation:plugin_guide#devices - -## Device Types - -Plugins can create devices of various types defined in `Devices.xml`: - -### Native Device Types - -These inherit states and actions from Indigo: - -| Type | Actions | Use Case | Example | -|------|---------|----------|---------| -| `relay` | ON/OFF/TOGGLE/STATUS | Binary switches, outlets | Smart plugs | -| `dimmer` | ON/OFF/DIM/BRIGHTEN/SET BRIGHTNESS | Dimmable lights | LED bulbs | -| `speedcontrol` | ON/OFF/SET SPEED LEVEL/INCREASE/DECREASE | Fan control | Ceiling fans | -| `sensor` | ON/OFF/STATUS (read-only) | Sensors, monitors | Motion sensors | -| `thermostat` | Full HVAC control | Climate control | Smart thermostats | -| `sprinkler` | Zone control | Irrigation | Sprinkler systems | - -### Custom Device Type - -Complete control over states and actions: - -```xml - - My Custom Device - - - - - - - -``` - -## Device Definition (Devices.xml) - -### Basic Structure - -```xml - - - - Display Name - - - - - - - primaryStateId - - -``` - -### State Definitions - -States store device data and can trigger events: - -```xml - - - Number - Temperature - Temp - - - Boolean - Device Online Status - Online - - - String - Status Message - Status - - -``` - -**Value Types**: `Integer`, `Number`, `String`, `Boolean`, `Separator` - -### Configuration UI (ConfigUI) - -```xml - - - - - - - - - - - - - - - - - - -``` - -**Field Types**: `textfield`, `textarea`, `checkbox`, `menu`, `list`, `button`, `label`, `separator` - -To pre-populate fields when the device dialog opens (e.g. seed a fresh API token, load the current sensor reading), override `getDeviceConfigUiValues(self, plugin_props, type_id, dev_id)` — see [`plugin-lifecycle.md`](plugin-lifecycle.md#configui-pre-population-callbacks). - -## Device Factory Pattern - -A Device Factory creates and manages a group of child devices from a single dialog. Use this when a hub or controller discovers multiple sub-devices (e.g., a bridge with relays, dimmers, and sensors). - -### Devices.xml Structure - -Instead of (or in addition to) individual `` elements, define a ``: - -```xml - - - Define Device Group... - Close - - - - - - - - - - Add Relay - _add_relay - - - Remove All Devices - _remove_all_devices - - - - - - - Relay - - - - Dimmer - - - -``` - -### Plugin Callbacks - -All factory methods receive `dev_id_list` — the list of device IDs currently in the group: - -```python -def getDeviceFactoryUiValues(self, dev_id_list): - """Prime initial values for the factory dialog.""" - values_dict = indigo.Dict() - error_msg_dict = indigo.Dict() - return (values_dict, error_msg_dict) - -def validateDeviceFactoryUi(self, values_dict, dev_id_list): - """Validate factory dialog before closing.""" - errors_dict = indigo.Dict() - return (True, values_dict, errors_dict) - -def closedDeviceFactoryUi(self, values_dict, user_cancelled, dev_id_list): - """Called after factory dialog closes.""" - pass -``` - -### Adding and Removing Devices - -Button callbacks create/delete devices using `indigo.device.create()` and `indigo.device.delete()`: - -```python -def _add_relay(self, values_dict, dev_id_list): - newdev = indigo.device.create(indigo.kProtocol.Plugin, deviceTypeId="myRelayType") - newdev.model = "My Hub" # Display in UI - newdev.subType = "Relay" # Tab label in UI - newdev.replaceOnServer() - return values_dict - -def _remove_all_devices(self, values_dict, dev_id_list): - for dev_id in dev_id_list: - try: - indigo.device.delete(dev_id) - except: - pass # Root element cannot be deleted - return values_dict -``` - -### Populating the Device Group List - -```python -def _get_device_group_list(self, filter, values_dict, dev_id_list): - """Return list of (id, name) tuples for the group list UI.""" - menu_items = [] - for dev_id in dev_id_list: - if dev_id in indigo.devices: - menu_items.append((dev_id, indigo.devices[dev_id].name)) - else: - menu_items.append((dev_id, "- device not found -")) - return menu_items -``` - -### Important Notes - -- Device groups should only contain devices defined by the plugin (not X10/INSTEON/Z-Wave) -- Set `dev.model` and `dev.subType` after creation, then call `dev.replaceOnServer()` -- The `dev_id_list` auto-updates after `indigo.device.create()` / `indigo.device.delete()` -- See the **Example Device - Factory** SDK example for a complete working implementation - -## Device Lifecycle - -Device lifecycle callbacks are documented in [Plugin Lifecycle → Device Callbacks](plugin-lifecycle.md#device-lifecycle-callbacks). - -Key callbacks: -- `deviceStartComm(dev)` - Initialize device communication -- `deviceStopComm(dev)` - Clean up device resources -- `deviceUpdated(origDev, newDev)` - Handle configuration changes - -## Configuration Validation - -```python -def validateDeviceConfigUi(self, values_dict, type_id, dev_id): - """ - Validate device configuration before saving - - :return: (is_valid, values_dict, errors_dict) - """ - errors_dict = indigo.Dict() - - # Validate address - address = values_dict.get('address', '').strip() - if not address: - errors_dict['address'] = "Address is required" - - # Validate polling interval - try: - interval = int(values_dict.get('pollingInterval', 60)) - if interval < 10: - errors_dict['pollingInterval'] = "Minimum interval is 10 seconds" - except ValueError: - errors_dict['pollingInterval'] = "Must be a number" - - if len(errors_dict) > 0: - return (False, values_dict, errors_dict) - - return (True, values_dict) -``` - -## Dynamic Lists - -Populate configuration fields dynamically: - -```python -def get_device_list(self, filter="", values_dict=None, type_id="", target_id=0): - """Return list of available devices for dropdown""" - device_list = [] - for dev in indigo.devices: - if dev.id != target_id: # Don't include self - device_list.append((dev.id, dev.name)) - return device_list -``` - -```xml - - - - -``` - -## Device Properties - -For complete device class reference including all properties and methods, see [API → IOM → Devices](../api/iom/devices.md). - -Common access patterns: - -```python -# Plugin-defined properties (from ConfigUI) -address = dev.pluginProps.get('address', '') - -# Device states -temp = dev.states['temperature'] - -# Built-in properties -dev.id # Unique device ID -dev.name # Device name -dev.deviceTypeId # Type ID from Devices.xml -dev.enabled # Is device enabled? -``` +Undocumented Indigo device behaviour learned in the field — **not** covered by the canonical +reference. For device types, `Devices.xml`, states, ConfigUI, the device factory, and the runtime +device API, use the canonical docs (routed from `/indigo:dev`): +`reference/canonical/plugin-dev/reference/xml/devices.md` and +`reference/canonical/scripting/reference/devices/base-class.md`. ## State ID naming rules (undocumented but strict) @@ -435,33 +146,16 @@ def deviceUpdated(self, origDev, newDev): # ...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 -- Validate all user input -- Use dynamic lists for device/variable selection +## Quick rules -### Performance -- Batch state updates with `updateStatesOnServer()` -- Update states only when values change -- Use `indigo.devices.iter("self")` to iterate only your plugin's devices +- State IDs: camelCase ASCII only — no underscores, no non-ASCII letters. +- Don't reuse reserved names like `batteryLevel` (use `battery`, `Integer` type). +- Don't append to the live list from `getDeviceStateList()` — copy it first. +- Device `pluginProps` keys via `replacePluginPropsOnServer` cannot start with `_` (Plugin `pluginPrefs` can). +- If you `subscribeToChanges()`, add the `pluginId` self-loop guard at the top of `deviceUpdated()`. ## See Also -- [Device Classes Reference](../api/iom/devices.md) - Device properties and methods -- [Plugin Lifecycle](plugin-lifecycle.md) - Lifecycle callbacks -- [Constants Reference](../api/iom/constants.md) - State icons +- Device types, states, ConfigUI: `reference/canonical/plugin-dev/reference/xml/devices.md` +- Runtime device API: `reference/canonical/scripting/reference/devices/base-class.md` +- Lifecycle callbacks & `super()` rules: [plugin-lifecycle.md](plugin-lifecycle.md) diff --git a/docs/plugin-dev/concepts/events.md b/docs/plugin-dev/concepts/events.md index b346b8a..0b188fa 100644 --- a/docs/plugin-dev/concepts/events.md +++ b/docs/plugin-dev/concepts/events.md @@ -1,82 +1,10 @@ -# Custom Plugin Events (Events.xml) +# Custom Plugin Events — field notes -Define custom trigger events that users can respond to in Indigo. - -## Overview - -Events.xml defines plugin-specific events that appear in Indigo's trigger system alongside built-in events like Power Failure or Email Received. - -Use cases: -- Update notifications -- Battery low alerts -- Button press events -- Connection status changes -- Custom sensor events - -## Events.xml Structure - -```xml - - - https://example.com/plugin/events.html - - - Plugin Update Available - - - - - - - - - Battery Low - - - - - - - - - - - - Connection Lost - - -``` - -## Event Elements - -| Element | Description | -|---------|-------------| -| `` | Unique identifier for the event | -| `` | Display name in trigger UI | -| `` | Optional configuration fields | -| `` | Help link for the event | - -## Firing Events - -Trigger events from your plugin code: - -```python -def _check_for_updates(self): - if self._update_available(): - # Fire event for all matching triggers - for trigger in indigo.triggers.iter("self.updateAvailable"): - if trigger.enabled: - indigo.trigger.execute(trigger) - -def _check_battery(self, dev, level): - for trigger in indigo.triggers.iter("self.batteryLow"): - if not trigger.enabled: - continue - - threshold = int(trigger.pluginProps.get("threshold", 20)) - if level < threshold: - indigo.trigger.execute(trigger) -``` +Undocumented Indigo event behaviour learned in the field — **not** covered by the canonical +reference. For `Events.xml`, the event ConfigUI, the `triggerStartProcessing` / +`triggerStopProcessing` lifecycle, and event data, use the canonical docs (routed from +`/indigo:dev`): `reference/canonical/plugin-dev/reference/xml/events.md` and +`reference/canonical/plugin-dev/reference/plugin-py/trigger-methods.md`. ## Common mistake — methods that don't exist @@ -118,202 +46,7 @@ The `AttributeError` is easy to miss because it's typically caught by a broad event silently never fires until someone notices the trigger was never configured. Always log trigger-execute failures at ERROR. -## Event Callback Methods - -### triggerStartProcessing - -Called when a trigger using your event is enabled: - -```python -def triggerStartProcessing(self, trigger): - """Called when trigger is enabled.""" - self.logger.debug(f"Trigger started: {trigger.name}") - - # Track active triggers - self.active_triggers[trigger.id] = trigger.pluginTypeId -``` - -### triggerStopProcessing - -Called when a trigger is disabled: - -```python -def triggerStopProcessing(self, trigger): - """Called when trigger is disabled.""" - self.logger.debug(f"Trigger stopped: {trigger.name}") - - # Remove from tracking - if trigger.id in self.active_triggers: - del self.active_triggers[trigger.id] -``` - -### validateEventConfigUi - -Validate event configuration: - -```python -def validateEventConfigUi(self, valuesDict, typeId, triggerId): - """Validate event configuration.""" - errorsDict = indigo.Dict() - - if typeId == "batteryLow": - try: - threshold = int(valuesDict.get("threshold", 20)) - if threshold < 1 or threshold > 100: - errorsDict["threshold"] = "Must be between 1 and 100" - except ValueError: - errorsDict["threshold"] = "Must be a number" - - if errorsDict: - return (False, valuesDict, errorsDict) - - return (True, valuesDict) -``` - -### getEventConfigUiValues - -Provide initial/default values: - -```python -def getEventConfigUiValues(self, pluginProps, typeId, triggerId): - """Return initial values for event config UI.""" - valuesDict = pluginProps - - if typeId == "batteryLow": - valuesDict.setdefault("threshold", "20") - - return valuesDict -``` - -This is one of five `get*ConfigUiValues` callbacks — see [`plugin-lifecycle.md`](plugin-lifecycle.md#configui-pre-population-callbacks) for the full reference (device, action, menu item, and plugin prefs dialogs). - -### closedEventConfigUi - -Called after event config is saved: - -```python -def closedEventConfigUi(self, valuesDict, userCancelled, typeId, triggerId): - """Called after event config dialog closes.""" - if userCancelled: - return - - self.logger.debug(f"Event config saved: {typeId}") -``` - -## Complete Example - -### Events.xml - -```xml - - - - Motion Detected - - - - - - - - - - - - - - - - -``` - -### plugin.py - -```python -def getZoneList(self, filter="", valuesDict=None, typeId="", targetId=0): - """Return list of zones for event config.""" - zones = [] - for dev in indigo.devices.iter("self.motionSensor"): - zone = dev.pluginProps.get("zone", "unknown") - zones.append((zone, f"Zone: {zone}")) - return zones - -def triggerStartProcessing(self, trigger): - self.logger.debug(f"Motion trigger started: {trigger.name}") - self.motion_triggers.append(trigger.id) - -def triggerStopProcessing(self, trigger): - self.logger.debug(f"Motion trigger stopped: {trigger.name}") - if trigger.id in self.motion_triggers: - self.motion_triggers.remove(trigger.id) - -def _on_motion_detected(self, zone): - """Called when motion is detected.""" - for trigger_id in self.motion_triggers: - try: - trigger = indigo.triggers[trigger_id] - if not trigger.enabled: - continue - - # Check if zone matches - trigger_zone = trigger.pluginProps.get("zone", "") - if trigger_zone and trigger_zone != zone: - continue - - # Fire the trigger - indigo.trigger.execute(trigger) - - except KeyError: - # Trigger was deleted - self.motion_triggers.remove(trigger_id) -``` - -## Event Data Dictionary - -When executing action groups or triggers, you can pass an `event_data` dictionary. Indigo automatically adds a `source` key indicating the origin. - -### Standard Event Data Keys - -| Key | Type | Description | -|-----|------|-------------| -| `event-indigo-id` | int | ID of the Indigo object that fired the event | -| `event-type` | str | Type identifier for the event | -| `source` | str | Auto-added by Indigo: `"server"`, `"python"`, `"api-http"`, or `"api-websocket"` | -| `timestamp` | float | Epoch timestamp of the event | - -### Webhook Event Data Keys - -When events originate from HTTP webhooks, additional keys are present: - -| Key | Type | Description | -|-----|------|-------------| -| `http-method` | str | HTTP method (`GET`, `POST`, etc.) | -| `request-url` | str | The request URL path | -| `status-code` | int | HTTP response status code | -| `webhook-id` | str | Identifier of the webhook | -| `data` | dict | Parsed request body data | - -### Passing Custom Event Data - -```python -event_data = indigo.Dict() -event_data["scene"] = "evening" -event_data["triggered_by"] = "schedule" -indigo.actionGroup.execute(ag.id, event_data=event_data) -# Indigo auto-adds "source" key to event_data -``` - -## Best Practices - -- Use descriptive event IDs and names -- Provide sensible defaults in ConfigUI -- Validate all configuration input -- Track active triggers to avoid unnecessary iteration -- Handle deleted triggers gracefully -- Use separators to group related events - ## See Also -- [Trigger Classes Reference](../api/iom/triggers.md) - Trigger object properties -- [Subscriptions](../api/iom/subscriptions.md) - Monitor trigger changes -- [Plugin Lifecycle](plugin-lifecycle.md) - When callbacks are called +- Events.xml + event ConfigUI: `reference/canonical/plugin-dev/reference/xml/events.md` +- Trigger callbacks (`triggerStartProcessing`/`triggerStopProcessing`): `reference/canonical/plugin-dev/reference/plugin-py/trigger-methods.md` diff --git a/docs/plugin-dev/concepts/http-responder.md b/docs/plugin-dev/concepts/http-responder.md deleted file mode 100644 index a464296..0000000 --- a/docs/plugin-dev/concepts/http-responder.md +++ /dev/null @@ -1,187 +0,0 @@ -# HTTP Responder - -**Official Documentation**: https://wiki.indigodomo.com/doku.php?id=indigo_2025.1_documentation:plugin_guide#http_request_processing - -Plugins can serve web content through Indigo's built-in web server (IWS). Define HTTP handler actions in `Actions.xml` and implement handler methods in `plugin.py`. - -## URL Pattern - -``` -http://localhost:8176/message/{CFBundleIdentifier}/{action_id}/{path...}?{query_args} -``` - -Example: `http://localhost:8176/message/com.my.plugin/api/devices/123.json` - -## Actions.xml Setup - -Define an HTTP action (typically hidden from the action UI): - -```xml - - - API Endpoint - api - - -``` - -## Handler Method - -```python -def api(self, action, dev=None, caller_waiting_for_result=None): - props_dict = dict(action.props) - - # Request data - file_path = props_dict.get("file_path", []) # URL path segments (list) - query_args = props_dict.get("url_query_args", {}) # Query parameters (dict) - method = props_dict.get("incoming_request_method", "GET") # HTTP method - body_params = props_dict.get("body_params", {}) # POST body parameters (dict) - - # Build response - reply = indigo.Dict() - reply["status"] = 200 - reply["content"] = json.dumps({"message": "Hello"}) - reply["headers"] = indigo.Dict() - reply["headers"]["Content-Type"] = "application/json" - return reply -``` - -### Request Properties (from action.props) - -| Key | Type | Description | -|-----|------|-------------| -| `file_path` | list | URL path segments after the action ID | -| `url_query_args` | dict | Query string parameters | -| `incoming_request_method` | str | HTTP method (`"GET"`, `"POST"`, etc.) | -| `body_params` | dict | POST/PUT body parameters | - -### Reply Object - -| Key | Type | Description | -|-----|------|-------------| -| `status` | int | HTTP status code (200, 400, 404, 500) | -| `content` | str | Response body | -| `headers` | indigo.Dict | Response headers (must include `Content-Type`) | - -## Content Types - -```python -# JSON (use JSONDateEncoder to serialize Indigo objects and datetimes) -reply["headers"]["Content-Type"] = "application/json" -reply["content"] = json.dumps(data, cls=indigo.utils.JSONDateEncoder) - -# HTML -reply["headers"]["Content-Type"] = "text/html" -reply["content"] = "Hello" - -# XML -reply["headers"]["Content-Type"] = "application/xml" -reply["content"] = "ok" -``` - -## Jinja2 Templates - -Use Jinja2 for HTML templating. Templates live in `Contents/Resources/templates/`. Note the loader path is relative to `Contents/Server Plugin/` (the plugin's working directory), so `"../Resources/templates"` resolves to `Contents/Resources/templates/`: - -```python -import jinja2 - -def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.templates = jinja2.Environment( - loader=jinja2.FileSystemLoader("../Resources/templates") - ) - -def config(self, action, dev=None, caller_waiting_for_result=None): - template = self.templates.get_template("config.html") - context = {"plugin": self, "devices": indigo.devices} - - reply = indigo.Dict() - reply["status"] = 200 - reply["content"] = template.render(context) - reply["headers"] = indigo.Dict() - reply["headers"]["Content-Type"] = "text/html" - return reply -``` - -## Static File Serving - -### Auto-Served Content - -Files in `Contents/Resources/` are automatically served by IWS: - -``` -Contents/Resources/ -├── static/ -│ ├── css/style.css -│ ├── html/help.html -│ └── js/app.js -└── templates/ - └── config.html -``` - -Access via: `http://localhost:8176/{CFBundleIdentifier}/static/css/style.css` - -### return_static_file() Utility - -Serve files programmatically from a handler: - -```python -reply = indigo.utils.return_static_file( - f"{self.pluginFolderPath}/Contents/Resources/static/html/help.html", - status=200, - path_is_relative=False -) -return reply -``` - -Parameters: -- **file_path** — Path to the file -- **status** — HTTP status code (default 200) -- **path_is_relative** — `True` for relative paths, `False` for absolute -- **content_type** — Override content type (auto-detected if omitted) - -## Error Handling - -```python -def api(self, action, dev=None, caller_waiting_for_result=None): - props_dict = dict(action.props) - file_path = props_dict.get("file_path", []) - - reply = indigo.Dict() - reply["headers"] = indigo.Dict() - - # 404 - Not Found - if not file_path: - reply["status"] = 404 - reply["content"] = json.dumps({"error": "Not found"}) - reply["headers"]["Content-Type"] = "application/json" - return reply - - # 400 - Bad Request - query_args = props_dict.get("url_query_args", {}) - if "required_param" not in query_args: - reply["status"] = 400 - reply["content"] = json.dumps({"error": "Missing required parameter"}) - reply["headers"]["Content-Type"] = "application/json" - return reply - - # 500 - Internal Server Error - try: - result = self._process_request(file_path) - reply["status"] = 200 - reply["content"] = json.dumps(result) - reply["headers"]["Content-Type"] = "application/json" - except Exception as exc: - self.logger.exception("Request processing error") - reply["status"] = 500 - reply["content"] = json.dumps({"error": str(exc)}) - reply["headers"]["Content-Type"] = "application/json" - - return reply -``` - -## See Also - -- [SDK Examples Guide](../examples/sdk-examples-guide.md) — Example HTTP Responder -- [Utility Functions](../api/iom/utilities.md) — `return_static_file()` diff --git a/docs/plugin-dev/concepts/menu-items.md b/docs/plugin-dev/concepts/menu-items.md deleted file mode 100644 index cf70e99..0000000 --- a/docs/plugin-dev/concepts/menu-items.md +++ /dev/null @@ -1,120 +0,0 @@ -# Menu Items - -**Official Documentation**: https://wiki.indigodomo.com/doku.php?id=indigo_2025.1_documentation:plugin_guide#menuitems - -Define plugin menu items in `MenuItems.xml`. These appear in the Indigo menu under `Plugins > [Your Plugin Name]`. - -## Basic Structure - -```xml - - - - Toggle Debug Logging - toggle_debug - - -``` - -## Menu Item Types - -### Callback Menu Item - -Triggers a plugin method when clicked: - -```xml - - Reload Configuration - reload_config - -``` - -```python -def reload_config(self): - self.logger.info("Reloading configuration...") - # Reload logic here -``` - -### URL Menu Item - -Opens a URL in the user's browser: - -```xml - - Advanced Configuration Page... - /message/com.my.plugin/config - -``` - -The URL can be: -- A relative path served by the plugin's HTTP responder -- A full `https://` URL to external documentation - -### Menu Item with ConfigUI Dialog - -Opens a dialog window before executing the callback: - -```xml - - Send Custom Command... - send_custom_command - - https://my-plugin-docs.example.com/commands - - - - - - - - - -``` - -```python -def send_custom_command(self, values_dict, menu_item_id): - command = values_dict.get("command", "") - device_id = values_dict.get("targetDevice", "") - self.logger.info(f"Sending command: {command}") -``` - -**Pre-populating fields when the dialog opens.** Override `getMenuActionConfigUiValues(self, menu_id)` to seed fields with computed values (today's date, the latest reading, etc.) before the dialog is shown. Static `defaultValue` only fires once and dynamic lists do not auto-select their first item — see [`plugin-lifecycle.md`](plugin-lifecycle.md#configui-pre-population-callbacks) for the full reference. - -## Common Patterns - -### Debug Toggle - -```xml - - Toggle Debug Logging - toggle_debug - -``` - -```python -def toggle_debug(self): - if self.debug: - self.debug = False - self.logger.info("Debug logging disabled") - else: - self.debug = True - self.logger.info("Debug logging enabled") -``` - -### Print Connection Info - -```xml - - Print Connection Information - print_connection_info - -``` - -### Separator - -Not supported in MenuItems.xml — all items appear in a flat list. - -## See Also - -- [ConfigUI Reference](configui.md) — Field types and attributes for menu dialogs -- [Plugin Lifecycle](plugin-lifecycle.md) — Plugin startup and initialization diff --git a/docs/plugin-dev/concepts/plugin-lifecycle.md b/docs/plugin-dev/concepts/plugin-lifecycle.md index af3d130..d09930f 100644 --- a/docs/plugin-dev/concepts/plugin-lifecycle.md +++ b/docs/plugin-dev/concepts/plugin-lifecycle.md @@ -1,6 +1,6 @@ # Plugin Lifecycle -**Official Documentation**: [Plugin Guide - Lifecycle](https://wiki.indigodomo.com/doku.php?id=indigo_2025.1_documentation:plugin_guide#plugin_lifecycle) +**Canonical reference**: `reference/canonical/plugin-dev/reference/plugin-py/general-methods.md` (+ `device-methods.md`) — or the [official docs](https://docs.indigodomo.com/2025.2/plugin-dev/reference/plugin-py/). Understanding the plugin lifecycle is essential for proper plugin development, resource management, and avoiding memory leaks or zombie processes. @@ -247,7 +247,14 @@ def wakeUp(self): ## Device Lifecycle Callbacks -These callbacks are called for each device as it starts, stops, or changes: +These callbacks are called for each device as it starts, stops, or changes. + +> **The `super()` contract (common footgun).** The base implementations differ by method: +> - `deviceStartComm` / `deviceStopComm` are **override hooks** — the base versions are effectively no-ops, so calling `super()` is optional (harmless, not required). +> - `deviceCreated` / `deviceDeleted` — the base **starts/stops comm** for the device. If you override these, call `super()` (or call `deviceStartComm`/`deviceStopComm` yourself), or you silently lose the default start/stop. +> - `deviceUpdated` — the base drives the comm-property-change machinery (stop→start on a relevant config change). If you override it, you **must** call `super()`, or config changes won't restart the device. +> +> See canonical `reference/canonical/plugin-dev/reference/plugin-py/device-methods.md`. ### `deviceStartComm(dev)` @@ -314,6 +321,7 @@ def deviceStopComm(self, dev): ```python def deviceCreated(self, dev): + super().deviceCreated(dev) # base starts comm — keep it, or call deviceStartComm yourself self.logger.debug(f"Device created: {dev.name}") # Perform one-time setup if needed ``` @@ -346,6 +354,7 @@ def deviceUpdated(self, origDev, newDev): ```python def deviceDeleted(self, dev): + super().deviceDeleted(dev) # base stops comm — keep it, or call deviceStopComm yourself self.logger.debug(f"Device deleted: {dev.name}") # Clean up any persistent data or external resources self._cleanup_device_data(dev.id) @@ -558,7 +567,7 @@ def shutdown(self): self.logger.info("Shutting down") ``` -**Remember**: Only call `super()` in `__init__()` (required) and device callbacks like `deviceStartComm()` (recommended) +**Remember**: `super().__init__()` is required in `__init__()`. Do NOT call super in `startup`/`shutdown`/`runConcurrentThread`. For device callbacks, call super where the base does real work — `deviceUpdated` (required), `deviceCreated`/`deviceDeleted` (keeps default start/stop) — while `deviceStartComm`/`deviceStopComm` are override hooks where super is optional. ### ❌ Using `time.sleep()` in Concurrent Thread @@ -653,7 +662,7 @@ Indigo handles plugin failures differently depending on where they occur: - [ ] All instance variables initialized in `__init__()` - [ ] No Indigo database access in `__init__()` - [ ] `super().__init__()` called in `__init__()` (required) -- [ ] `super()` called in device callbacks like `deviceStartComm()` (recommended) +- [ ] `super()` called in `deviceUpdated` (required) and `deviceCreated`/`deviceDeleted` (keeps default start/stop); optional in `deviceStartComm`/`deviceStopComm` - [ ] `super()` NOT called in `startup()` or `shutdown()` - [ ] Connections opened in `startup()` - [ ] Event subscriptions done in `startup()` @@ -672,6 +681,6 @@ Indigo handles plugin failures differently depending on where they occur: ## Official References -- [Plugin Developer's Guide - Lifecycle](https://wiki.indigodomo.com/doku.php?id=indigo_2025.1_documentation:plugin_guide#plugin_lifecycle) -- [Plugin Developer's Guide - Concurrent Thread](https://wiki.indigodomo.com/doku.php?id=indigo_2025.1_documentation:plugin_guide#concurrent_thread) -- [Object Model Reference](https://wiki.indigodomo.com/doku.php?id=indigo_2025.1_documentation:object_model_reference) +- [plugin.py General Methods (lifecycle)](https://docs.indigodomo.com/2025.2/plugin-dev/reference/plugin-py/general-methods/) — or vendored `reference/canonical/plugin-dev/reference/plugin-py/general-methods.md` +- [Device Methods (start/stop comm)](https://docs.indigodomo.com/2025.2/plugin-dev/reference/plugin-py/device-methods/) +- [Scripting / IOM Reference](https://docs.indigodomo.com/2025.2/scripting/) diff --git a/docs/plugin-dev/concepts/plugin-preferences.md b/docs/plugin-dev/concepts/plugin-preferences.md index f6672d3..8db543a 100644 --- a/docs/plugin-dev/concepts/plugin-preferences.md +++ b/docs/plugin-dev/concepts/plugin-preferences.md @@ -1,70 +1,13 @@ -# Plugin Preferences +# Plugin Preferences — field notes -Plugin preferences store persistent configuration that survives plugin restarts. - -## Overview - -- Preferences are automatically saved to disk -- Available via `self.pluginPrefs` dictionary -- Fields in `PluginConfig.xml` map to `pluginPrefs` automatically -- Can store: numbers, booleans, strings, `indigo.Dict()`, `indigo.List()` - -## Reading Preferences - -```python -# Direct access (raises KeyError if missing) -api_key = self.pluginPrefs["apiKey"] - -# Safe access with default -debug = self.pluginPrefs.get("showDebugInfo", False) -interval = self.pluginPrefs.get("pollingInterval", 60) -``` - -## Writing Preferences - -```python -# Set/update a preference -self.pluginPrefs["lastUpdate"] = str(datetime.now()) -self.pluginPrefs["retryCount"] = 5 -self.pluginPrefs["cachedData"] = {"key": "value"} - -# Preferences auto-save, but force immediate save: -indigo.server.savePluginPrefs() -``` - -## PluginConfig.xml Integration - -Fields defined in `PluginConfig.xml` automatically map to `pluginPrefs`: - -```xml - - - - - - - - - - - - -``` - -Access in plugin: - -```python -def startup(self): - api_key = self.pluginPrefs.get("apiKey", "") - interval = int(self.pluginPrefs.get("pollingInterval", 60)) - debug = self.pluginPrefs.get("showDebugInfo", False) -``` - -To seed fields with computed values when the prefs dialog opens, override `getPrefsConfigUiValues(self)` — see [`plugin-lifecycle.md`](plugin-lifecycle.md#configui-pre-population-callbacks). +Undocumented Indigo preference behaviour learned in the field — **not** covered by the canonical +reference. For `PluginConfig.xml`, reading/writing `pluginPrefs`, validation, and the change +callbacks, use the canonical docs (routed from `/indigo:dev`): +`reference/canonical/plugin-dev/reference/xml/pluginconfig.md`. ## Hidden Preferences -Store values not shown in the config UI: +Store values not shown in the config UI by convention-prefixing the key with `_`: ```python def startup(self): @@ -99,114 +42,9 @@ def _after_sync(self): > dev.replacePluginPropsOnServer(new_props) > ``` -## Validating Preferences - -```python -def validatePrefsConfigUi(self, valuesDict): - """Validate plugin preferences before saving.""" - errorsDict = indigo.Dict() - - # Validate API key - api_key = valuesDict.get("apiKey", "").strip() - if not api_key: - errorsDict["apiKey"] = "API key is required" - - # Validate polling interval - try: - interval = int(valuesDict.get("pollingInterval", 60)) - if interval < 10: - errorsDict["pollingInterval"] = "Minimum is 10 seconds" - except ValueError: - errorsDict["pollingInterval"] = "Must be a number" - - if errorsDict: - return (False, valuesDict, errorsDict) - - return (True, valuesDict) -``` - -## Preference Change Callback - -React when preferences are saved: - -```python -def closedPrefsConfigUi(self, valuesDict, userCancelled): - """Called after preferences dialog closes.""" - if userCancelled: - return - - # Apply new settings - self.debug = valuesDict.get("showDebugInfo", False) - - # Reinitialize if API key changed - if valuesDict.get("apiKey") != self.api_key: - self.api_key = valuesDict.get("apiKey") - self._reinitialize_api_client() -``` - -## Common Patterns - -### Caching API Data - -```python -def _fetch_data(self): - """Fetch data with caching.""" - cached = self.pluginPrefs.get("_cachedData") - cache_time = self.pluginPrefs.get("_cacheTime") - - # Check cache validity (1 hour) - if cached and cache_time: - if datetime.now() - datetime.fromisoformat(cache_time) < timedelta(hours=1): - return cached - - # Fetch fresh data - data = self.api_client.get_data() - - # Update cache - self.pluginPrefs["_cachedData"] = data - self.pluginPrefs["_cacheTime"] = datetime.now().isoformat() - - return data -``` - -### Tracking Statistics - -```python -def _on_successful_action(self): - """Track usage statistics in preferences.""" - self.pluginPrefs["_successCount"] = self.pluginPrefs.get("_successCount", 0) + 1 - self.pluginPrefs["_lastSuccess"] = str(datetime.now()) - -def _on_failed_action(self, error): - """Track errors in preferences.""" - self.pluginPrefs["_errorCount"] = self.pluginPrefs.get("_errorCount", 0) + 1 - self.pluginPrefs["_lastError"] = str(error) -``` - -### Migration Between Versions - -```python -def startup(self): - # Check preference version - pref_version = self.pluginPrefs.get("_prefVersion", 1) - - if pref_version < 2: - # Migrate old format to new - if "oldKey" in self.pluginPrefs: - self.pluginPrefs["newKey"] = self.pluginPrefs["oldKey"] - del self.pluginPrefs["oldKey"] - self.pluginPrefs["_prefVersion"] = 2 -``` - -## 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. -- Validate all user input in `validatePrefsConfigUi()` -- React to changes in `closedPrefsConfigUi()` -- Don't store sensitive data like passwords in plain text +See [devices.md](devices.md) for the device-level `pluginProps`/state-ID validation rules. ## See Also -- [Plugin Lifecycle](plugin-lifecycle.md) - When to access preferences -- [Device Development](devices.md) - Device-specific configuration (pluginProps) +- PluginConfig.xml, reading/writing prefs, validation: `reference/canonical/plugin-dev/reference/xml/pluginconfig.md` +- Device field notes: [devices.md](devices.md) diff --git a/docs/plugin-dev/concepts/scripting-shell.md b/docs/plugin-dev/concepts/scripting-shell.md deleted file mode 100644 index f1910f6..0000000 --- a/docs/plugin-dev/concepts/scripting-shell.md +++ /dev/null @@ -1,60 +0,0 @@ -# Scripting Shell & CLI - -Indigo provides an interactive Python scripting shell and a command-line interface for running scripts outside of plugins. - -## Interactive Scripting Shell (IPH) - -Open via **Plugins > Open Scripting Shell** in the Indigo application. - -The shell provides a Python REPL with full access to the Indigo Object Model: - -```python ->>> indigo.devices["Hallway Light"].onState -True ->>> indigo.device.turnOff("Hallway Light") ->>> indigo.variables["myVar"].value -'42' ->>> indigo.variable.updateValue("myVar", value="100") -``` - -Useful for: -- Testing API calls before adding them to a plugin -- Quick device control and state inspection -- Debugging device properties and states -- Exploring the Indigo Object Model interactively - -## Command-Line Interface - -> Path examples below use `Indigo 2025.2`. Substitute your installed version (`2025.1`, `2023.2`, etc.) — see the [version reference table](../quick-start.md#version-reference). - -### Run a Script File - -```bash -/Library/Application\ Support/Perceptive\ Automation/Indigo\ 2025.2/IndigoPluginHost.app/Contents/MacOS/IndigoPluginHost -x /path/to/script.py -``` - -### Run Inline Code - -```bash -/Library/Application\ Support/Perceptive\ Automation/Indigo\ 2025.2/IndigoPluginHost.app/Contents/MacOS/IndigoPluginHost -e 'indigo.device.turnOn("Hallway Light")' -``` - -### SSH Remote Access - -You can run Indigo scripts remotely via SSH: - -```bash -ssh user@indigo-mac '/Library/Application\ Support/Perceptive\ Automation/Indigo\ 2025.2/IndigoPluginHost.app/Contents/MacOS/IndigoPluginHost -e "indigo.server.log(\"Hello from SSH\")"' -``` - -## Notes - -- The scripting shell and CLI share the same Python environment as plugins -- Scripts run via CLI have full access to the Indigo Object Model -- The CLI is useful for cron jobs, automation scripts, and CI/CD integration -- All logging from CLI scripts appears in the Indigo Event Log - -## See Also - -- [Plugin Lifecycle](plugin-lifecycle.md) - How plugins run within Indigo -- [Indigo Object Model](../api/indigo-object-model.md) - Available API diff --git a/docs/plugin-dev/patterns/api-patterns.md b/docs/plugin-dev/patterns/api-patterns.md index 4aee101..9c5d097 100644 --- a/docs/plugin-dev/patterns/api-patterns.md +++ b/docs/plugin-dev/patterns/api-patterns.md @@ -2,7 +2,7 @@ Common patterns for working with the Indigo Object Model. -For core concepts like client-server architecture, object modification, and `replaceOnServer()` patterns, see [IOM Architecture](../api/iom/architecture.md). +For core concepts like client-server architecture, object modification, and `replaceOnServer()` patterns, see the canonical [IOM Concepts](../../../reference/canonical/scripting/iom-concepts.md). ## Device State Updates @@ -376,6 +376,6 @@ def poll(self): ## See Also -- [IOM Architecture](../api/iom/architecture.md) - Core concepts, replaceOnServer, pluginProps -- [Device Classes](../api/iom/devices.md) - Device properties and methods -- [Filters](../api/iom/filters.md) - Iteration patterns +- [IOM Concepts](../../../reference/canonical/scripting/iom-concepts.md) - Core concepts, replaceOnServer, pluginProps +- [Device Base Class](../../../reference/canonical/scripting/reference/devices/base-class.md) - Device properties and methods +- [Devices collection](../../../reference/canonical/scripting/reference/devices.md) - Iteration patterns (`indigo.devices.iter`) diff --git a/docs/plugin-dev/patterns/testing.md b/docs/plugin-dev/patterns/testing.md index d6e8b20..abec15b 100644 --- a/docs/plugin-dev/patterns/testing.md +++ b/docs/plugin-dev/patterns/testing.md @@ -173,7 +173,7 @@ class TestPluginLoaded(APIBase): self.assertIn("enabled=True", result, f"indigo-host returned: {result!r}") ``` -The plugin object's API (`isInstalled()`, `isEnabled()`, `isRunning()`, plus several properties) is in `/indigo:dev` → `docs/plugin-dev/api/iom/command-namespaces.md` under "Plugin Object Access". That's the SDK side; this section is about how to *invoke* it from a TestingBase test. +The plugin object's API (`isInstalled()`, `isEnabled()`, `isRunning()`, plus several properties) is in `/indigo:dev` → `reference/canonical/scripting/reference/server-commands.md` (plugin object access). That's the SDK side; this section is about how to *invoke* it from a TestingBase test. --- @@ -219,4 +219,4 @@ The right `URL_PREFIX` is whatever protocol your specific server is configured t - TestingBase upstream `example_test_xml_files.py`: canonical `ValidateXmlFile` patterns - Plugin HTTP API (consumed by Pattern B): see `/indigo:api` - Plugin lifecycle (what you'd typically test): see `concepts/plugin-lifecycle.md` -- Plugin Object Access (the IOM surface for plugin queries): `docs/plugin-dev/api/iom/command-namespaces.md` — "Plugin Object Access" section +- Plugin Object Access (the IOM surface for plugin queries): `reference/canonical/scripting/reference/server-commands.md` diff --git a/docs/plugin-dev/quick-start.md b/docs/plugin-dev/quick-start.md index a1aed82..d337c6f 100644 --- a/docs/plugin-dev/quick-start.md +++ b/docs/plugin-dev/quick-start.md @@ -32,7 +32,7 @@ Start with a working example as your template: cd ~/Documents # Copy the Custom Device example -cp -r "/Library/Application Support/Perceptive Automation/Indigo 2023.2/IndigoSDK/Example Device - Custom.indigoPlugin" MyFirstPlugin.indigoPlugin +cp -r "/Library/Application Support/Perceptive Automation/Indigo 2025.2/IndigoSDK/Example Device - Custom.indigoPlugin" MyFirstPlugin.indigoPlugin # Edit the plugin cd MyFirstPlugin.indigoPlugin/Contents @@ -83,11 +83,15 @@ cd HelloWorld.indigoPlugin/Contents CFBundleVersion 1.0.0 ServerApiVersion - 3.0 + 3.4 ``` +> `ServerApiVersion` is the **minimum** server API your plugin needs. Use **`3.4`** for a modern +> plugin: 3.4 (Indigo 2023.2) is the first version with Python 3 **and** with `requirements.txt` +> auto-install — declaring `3.0` predates both and would silently disable dependency auto-install. + ### 3. Create Server Plugin/plugin.py ```python @@ -112,7 +116,7 @@ class Plugin(indigo.PluginBase): ```bash # Copy to Indigo's plugin folder -cp -r HelloWorld.indigoPlugin "/Library/Application Support/Perceptive Automation/Indigo 2023.2/Plugins/" +cp -r HelloWorld.indigoPlugin "/Library/Application Support/Perceptive Automation/Indigo 2025.2/Plugins/" ``` ### 5. Enable in Indigo @@ -154,12 +158,12 @@ MyPlugin.indigoPlugin/ Before enabling your plugin, verify: - [ ] Unique `CFBundleIdentifier` in Info.plist (no conflicts with other plugins) -- [ ] `ServerApiVersion` is `3.0` or higher (for Python 3 support) +- [ ] `ServerApiVersion` is `3.4` or higher (Python 3 + requirements.txt auto-install; 3.4 = Indigo 2023.2) - [ ] `Plugin` class inherits from `indigo.PluginBase` - [ ] `__init__` method calls `super().__init__()` - [ ] `startup()` and `shutdown()` methods are defined - [ ] Plugin bundle ends with `.indigoPlugin` -- [ ] Plugin is in correct folder: `/Library/Application Support/Perceptive Automation/Indigo 2023.2/Plugins/` +- [ ] Plugin is in correct folder: `/Library/Application Support/Perceptive Automation/Indigo 2025.2/Plugins/` ## Common Beginner Mistakes @@ -171,7 +175,7 @@ def startup(self): super().startup() # AttributeError! self.logger.info("Starting") -# RIGHT - Only call super() in __init__ and device callbacks +# RIGHT - super() is required in __init__; never in startup/shutdown def __init__(self, pluginId, pluginDisplayName, pluginVersion, pluginPrefs): super().__init__(pluginId, pluginDisplayName, pluginVersion, pluginPrefs) # REQUIRED! @@ -180,7 +184,8 @@ def startup(self): self.logger.info("Starting") def deviceStartComm(self, dev): - super().deviceStartComm(dev) # Good practice for device callbacks + # deviceStartComm is an override hook — super() is optional here (base is a no-op). + # But deviceUpdated/deviceCreated/deviceDeleted DO need super() — the base does real work. # Your code here ``` @@ -280,7 +285,7 @@ Place `.py` files here to make them importable by any plugin. After adding or up **Solutions**: - Check `CFBundleIdentifier` is unique (doesn't match another plugin) -- Verify `ServerApiVersion` is `3.0` or higher +- Verify `ServerApiVersion` is `3.4` or higher - Open Event Log for Python errors - Check file permissions: `chmod -R 755 MyPlugin.indigoPlugin` @@ -290,7 +295,7 @@ Place `.py` files here to make them importable by any plugin. After adding or up **Solutions**: - Verify bundle name ends with `.indigoPlugin` -- Check bundle is in correct location: `/Library/Application Support/Perceptive Automation/Indigo 2023.2/Plugins/` +- Check bundle is in correct location: `/Library/Application Support/Perceptive Automation/Indigo 2025.2/Plugins/` - Restart Indigo Server Helper - Check Info.plist is valid XML (no syntax errors) @@ -311,10 +316,10 @@ Place `.py` files here to make them importable by any plugin. After adding or up |--------|--------|----------------------------------------------------|-------------| | 2025.2 | 3.13.9 | `.../Perceptive Automation/Indigo 2025.2/Plugins/` | 3.8 | | 2025.1 | 3.11.6 | `.../Perceptive Automation/Indigo 2025.1/Plugins/` | 3.7 | -| 2023.2 | 3.10+ | `.../Perceptive Automation/Indigo 2023.2/Plugins/` | (see docs) | +| 2023.2 | 3.10+ | `.../Perceptive Automation/Indigo 2025.2/Plugins/` | (see docs) | | 2022.x | 2.7 | Legacy, end-of-life | n/a | -`ServerApiVersion` in `Info.plist` declares the **minimum** API your plugin requires. `3.0` continues to work on all 2023.2+ versions — there is no need to bump it just because you're on a newer Indigo release. The path examples elsewhere in this guide use 2023.2; substitute the row above that matches your installation. +`ServerApiVersion` in `Info.plist` declares the **minimum** API your plugin requires. Use **`3.4`** for a Python-3 plugin (first version with Python 3 and `requirements.txt` auto-install, Indigo 2023.2); `3.0`/`3.1` predate Python 3. There's no need to bump beyond the minimum you actually use just because you're on a newer release. Path examples in this guide use 2025.2; substitute the row above that matches your installation. **Upgrading to 2025.2?** Python 3.13 has several library breaking changes — see [`troubleshooting/common-issues.md`](troubleshooting/common-issues.md#upgrading-to-indigo-20252--python-313). @@ -326,12 +331,12 @@ Now that you have a working plugin: 1. **Understand Plugin Architecture** - Read [`concepts/plugin-lifecycle.md`](concepts/plugin-lifecycle.md) - - Learn about device types in [`concepts/devices.md`](concepts/devices.md) + - Device types & Devices.xml: [`reference/canonical/plugin-dev/reference/xml/devices.md`](../../reference/canonical/plugin-dev/reference/xml/devices.md) 2. **Add Functionality** - - Create devices: See [`concepts/devices.md`](concepts/devices.md) - - Add custom events: See [`concepts/events.md`](concepts/events.md) - - Plugin preferences: See [`concepts/plugin-preferences.md`](concepts/plugin-preferences.md) + - Create devices: [`reference/canonical/plugin-dev/reference/xml/devices.md`](../../reference/canonical/plugin-dev/reference/xml/devices.md) + - Add custom events: [`reference/canonical/plugin-dev/reference/xml/events.md`](../../reference/canonical/plugin-dev/reference/xml/events.md) + - Plugin preferences: [`reference/canonical/plugin-dev/reference/xml/pluginconfig.md`](../../reference/canonical/plugin-dev/reference/xml/pluginconfig.md) 3. **Study Examples** - Browse [`sdk-examples/README.md`](../../sdk-examples/README.md) for 16 complete examples @@ -343,8 +348,8 @@ Now that you have a working plugin: ## External Resources -- [Official Plugin Developer's Guide](https://wiki.indigodomo.com/doku.php?id=indigo_2025.1_documentation:plugin_guide) -- [Indigo Object Model Reference](https://wiki.indigodomo.com/doku.php?id=indigo_2025.1_documentation:object_model_reference) +- [Plugin Development (official docs)](https://docs.indigodomo.com/2025.2/plugin-dev/) +- [Scripting Indigo / IOM (official docs)](https://docs.indigodomo.com/2025.2/scripting/) - [Indigo Developer Forum](https://forums.indigodomo.com/viewforum.php?f=18) - [GitHub: Indigo Skill Repository](https://github.com/simons-plugins/indigo-claude-skill) diff --git a/docs/plugin-dev/troubleshooting/common-issues.md b/docs/plugin-dev/troubleshooting/common-issues.md index c1c77fa..7e9bf49 100644 --- a/docs/plugin-dev/troubleshooting/common-issues.md +++ b/docs/plugin-dev/troubleshooting/common-issues.md @@ -10,9 +10,9 @@ 1. **Wrong Location** ```bash - # Plugins should be in one of these locations: - ~/Library/Application Support/Perceptive Automation/Indigo 2023.2/Plugins/ - ~/Library/Application Support/Perceptive Automation/Indigo 2023.2/Plugins (Disabled)/ + # Plugins should be in one of these locations (substitute your Indigo version): + ~/Library/Application Support/Perceptive Automation/Indigo 2025.2/Plugins/ + ~/Library/Application Support/Perceptive Automation/Indigo 2025.2/Plugins (Disabled)/ ``` 2. **Bundle Extension Wrong** @@ -44,8 +44,8 @@ ``` ModuleNotFoundError: No module named 'requests' ``` - - Add a `requirements.txt` in `Contents/Server Plugin/` — Indigo auto-installs on load - - Don't manually bundle packages or rely on system Python packages + - Add a `requirements.txt` in `Contents/Server Plugin/` — Indigo auto-installs on load (needs `ServerApiVersion` ≥ 3.4) + - Preferred over relying on system Python packages (which vanish on Python upgrades). Manual bundling into `Contents/Packages/` via `pip3 install -t` is also supported — see canonical `reference/canonical/scripting/guides/python-packages.md` - See [Python Dependencies](#python-dependency-issues) below 3. **API Version Mismatch** @@ -313,7 +313,7 @@ The marker filename is version-keyed: | 2025.2 | `3.13-pip-install-log-success.txt` | | 2025.1 / older | `pip-install-log-success.txt` | -Never `pip install` into the system Python — those packages disappear on any Python upgrade. `requirements.txt` is the only supported path on every Indigo version. +Never `pip install` into the system Python — those packages disappear on any Python upgrade. `requirements.txt` is the **preferred** path (auto-install requires `ServerApiVersion` ≥ 3.4); manual bundling into `Contents/Packages/` with `pip3 install -t` is the supported fallback for older API levels or vendored builds. See canonical `reference/canonical/scripting/guides/python-packages.md`. ### Force Reinstall diff --git a/reference/README.md b/reference/README.md index 6b593c9..b2d4ecf 100644 --- a/reference/README.md +++ b/reference/README.md @@ -19,9 +19,11 @@ Quick reference guide for Claude Code when working with the Indigo SDK. Contains - Common patterns ### [Python3-Migration-Guide.md](Python3-Migration-Guide.md) -**Essential reading for upgrading plugins from Python 2 to Python 3.** +**Legacy porting reference — only needed if you're updating an old Python 2 plugin.** -Complete guide for updating plugins to API version 3.0: +Indigo has shipped Python 3 since 2023.2 (API 3.4), so new plugins never need this. It remains +accurate and is still linked from the live SDK, kept here for the few porting a legacy plugin. +Covers updating a Python 2 plugin to Python 3: - Python 2 vs Python 3 syntax changes - Import statement updates - String/Unicode handling @@ -37,16 +39,16 @@ Complete guide for updating plugins to API version 3.0: ## SDK Version -These materials are from **Indigo SDK 2025.1** which supports: -- Indigo 2023.2+ (Python 3.10+) -- ServerApiVersion 3.0 +Current target: **Indigo 2025.2** — Python 3.13, ServerApiVersion 3.8. Python 3 support began at +Indigo 2023.2 (API 3.4). For any live reference fact, prefer the vendored canonical docs under +[`../reference/canonical/`](canonical/) (see `canonical/VERSION`). ## Related Resources +- **[Canonical docs (vendored)](canonical/INDEX.md)** - Indigo 2025.2 reference, source of truth - **[SDK Examples](../sdk-examples/)** - 16 complete example plugins -- **[SDK Documentation](.)** - Organized SDK docs -- **[Official Plugin Guide](https://www.indigodomo.com/docs/plugin_guide)** - Online reference -- **[Object Model Reference](https://www.indigodomo.com/docs/object_model_reference)** - API docs +- **[Plugin Development (official docs)](https://docs.indigodomo.com/2025.2/plugin-dev/)** +- **[Scripting / IOM (official docs)](https://docs.indigodomo.com/2025.2/scripting/)** ## Using These References diff --git a/reference/canonical/INDEX.md b/reference/canonical/INDEX.md new file mode 100644 index 0000000..6dc3b3e --- /dev/null +++ b/reference/canonical/INDEX.md @@ -0,0 +1,90 @@ +# Canonical documentation index + +GENERATED by tools/refresh_canonical.py from https://docs.indigodomo.com/llms.txt. +Each entry points at a local vendored file — load only the page you need. + + +## Integration APIs + +- **Integration APIs** — Overview of the Integration APIs: choosing between the HTTP API and the WebSocket API to integrate external systems with Indigo. → `reference/canonical/api.md` +- **HTTP API** — HTTP API reference for querying and commanding Indigo objects via authenticated HTTP requests using JSON. → `reference/canonical/api/http.md` +- **API Messages** — JSON message format reference for WebSocket and HTTP API commands, events, plugin messaging, and error responses. → `reference/canonical/api/messages.md` +- **Migrating from the REST API** — Examples showing how to convert legacy Indigo REST API calls to the new HTTP API format. → `reference/canonical/api/rest-migration.md` +- **Webhooks** — How to configure Indigo to send and receive webhooks for integration with external services and automation platforms. → `reference/canonical/api/webhooks.md` +- **WebSocket API** — WebSocket API reference: persistent bidirectional connections for receiving live Indigo object updates and sending commands. → `reference/canonical/api/websocket.md` + +## Plugin Development + +- **Plugin Development** — Developing Indigo plugins: the Developer's Guide, tutorials, the plugin.py and XML references, and the SDK example plugins. → `reference/canonical/plugin-dev.md` +- **Developer's Guide** — Introduction to the Indigo Plugin Developer's Guide: plugin bundle structure, Info.plist, folder layout, and the server plugin architecture. → `reference/canonical/plugin-dev/guide.md` +- **SDK Example Plugins** — The example plugins that ship with the Indigo SDK: custom devices, energy meters, relays, sensors, sprinklers, Insteon/X10 listeners, database traversal, and more — with full XML and Python source. → `reference/canonical/plugin-dev/sdk-examples.md` +- **Setting Up a Development Environment** — Tools and setup for developing Indigo plugins: choosing an IDE or editor, organizing your project, virtual environments and version control, symlinking a plugin for development, debugging with pdb/PuDB/PyCharm, and packaging tips. → `reference/canonical/plugin-dev/reference/dev-environment.md` +- **plugin.py Method Reference** — Reference for plugin.py: PluginBase lifecycle methods, device/action/event callbacks, logging, HTTP request handling, and message flow. → `reference/canonical/plugin-dev/reference/plugin-py.md` +- **Additional Topics** — Additional plugin topics: the preferences file, bundling 3rd-party Python libraries, and setting up a development environment. → `reference/canonical/plugin-dev/reference/plugin-py/additional-topics.md` +- **Device Methods** — Device-related plugin callbacks: device start/stop comm, config UI, and device action handling. → `reference/canonical/plugin-dev/reference/plugin-py/device-methods.md` +- **General Plugin Methods** — General PluginBase lifecycle methods: startup, shutdown, runConcurrentThread, prefs, and update callbacks. → `reference/canonical/plugin-dev/reference/plugin-py/general-methods.md` +- **Helper Methods** — Helper methods available to plugins for common tasks. → `reference/canonical/plugin-dev/reference/plugin-py/helper-methods.md` +- **Processing HTTP Requests** — Handling HTTP requests inside a plugin: request, reply, and error handling. → `reference/canonical/plugin-dev/reference/plugin-py/http-requests.md` +- **Logging** — Plugin logging: the indigo_log_handler, file handler, log levels, and logging from submodules and embedded scripts. → `reference/canonical/plugin-dev/reference/plugin-py/logging.md` +- **Event and Message Flow** — How events and messages flow between the Indigo Server and a plugin. → `reference/canonical/plugin-dev/reference/plugin-py/message-flow.md` +- **Properties** — PluginBase instance properties available to your plugin code. → `reference/canonical/plugin-dev/reference/plugin-py/properties.md` +- **Trigger Methods** — Trigger-related plugin callbacks: trigger start/stop processing and event handling. → `reference/canonical/plugin-dev/reference/plugin-py/trigger-methods.md` +- **Variable Methods** — Variable-related plugin callbacks. → `reference/canonical/plugin-dev/reference/plugin-py/variable-methods.md` +- **Plugin XML Reference** — Reference for Indigo plugin XML configuration files: ConfigUI fields, PluginConfig.xml, Devices.xml, Events.xml, Actions.xml, and MenuItems.xml. → `reference/canonical/plugin-dev/reference/xml.md` +- **Actions.xml** — Reference for Actions.xml: declaring custom plugin actions and their ConfigUI. → `reference/canonical/plugin-dev/reference/xml/actions.md` +- **Devices.xml** — Reference for Devices.xml: declaring device types, their ConfigUI, states, and display. → `reference/canonical/plugin-dev/reference/xml/devices.md` +- **Events.xml** — Reference for Events.xml: declaring custom plugin trigger/event types. → `reference/canonical/plugin-dev/reference/xml/events.md` +- **MenuItems.xml** — Reference for MenuItems.xml: adding items to the plugin menu. → `reference/canonical/plugin-dev/reference/xml/menuitems.md` +- **PluginConfig.xml** — Reference for PluginConfig.xml: the plugin-wide configuration dialog. → `reference/canonical/plugin-dev/reference/xml/pluginconfig.md` +- **SupportURL Elements** — Reference for SupportURL elements: linking plugin UI to documentation. → `reference/canonical/plugin-dev/reference/xml/supporturl.md` +- **Configuration Dialogs** — Reference for ConfigUI dialog field types: buttons, checkboxes, lists, popups, text fields, validation, and dynamic lists. → `reference/canonical/plugin-dev/reference/xml/configui.md` +- **Button** — ConfigUI Button field reference. → `reference/canonical/plugin-dev/reference/xml/configui/button.md` +- **Checkbox** — ConfigUI Checkbox field reference. → `reference/canonical/plugin-dev/reference/xml/configui/checkbox.md` +- **Color Picker** — ConfigUI Color Picker field reference. → `reference/canonical/plugin-dev/reference/xml/configui/color-picker.md` +- **Dialog Close Notification** — ConfigUI dialog close notification callback. → `reference/canonical/plugin-dev/reference/xml/configui/dialog-close.md` +- **Dynamic Lists & Filters** — ConfigUI dynamic lists and filters: populating menus and lists at runtime. → `reference/canonical/plugin-dev/reference/xml/configui/dynamic-lists.md` +- **Label** — ConfigUI Label field reference. → `reference/canonical/plugin-dev/reference/xml/configui/label.md` +- **List** — ConfigUI List field reference. → `reference/canonical/plugin-dev/reference/xml/configui/list.md` +- **Popup Menu** — ConfigUI Popup Menu field reference. → `reference/canonical/plugin-dev/reference/xml/configui/popup-menu.md` +- **Separator** — ConfigUI Separator field reference. → `reference/canonical/plugin-dev/reference/xml/configui/separator.md` +- **Serial Port** — ConfigUI Serial Port field reference. → `reference/canonical/plugin-dev/reference/xml/configui/serial-port.md` +- **Text Field** — ConfigUI Text Field reference. → `reference/canonical/plugin-dev/reference/xml/configui/text-field.md` +- **Validation Methods** — ConfigUI validation callbacks: validateDeviceConfigUi, validateEventConfigUi, validateActionConfigUi, validatePrefsConfigUi, and serial-port validation. → `reference/canonical/plugin-dev/reference/xml/configui/validation.md` +- **Building a Plugin** — Tutorial for building an Indigo plugin: adding device types, actions, and event handlers to a working plugin project. → `reference/canonical/plugin-dev/tutorials/building.md` + +## Bundled Plugins + + +## Scripting + +- **Scripting Indigo** — Scripting Indigo with Python: the Script Editor, embedded and external scripts, the Indigo Object Model (IOM), and the complete IOM reference. → `reference/canonical/scripting.md` +- **IOM Concepts** — Overview of the Indigo Object Model (IOM): the Python API namespaces for devices, variables, triggers, action groups, schedules, and server commands. → `reference/canonical/scripting/iom-concepts.md` +- **Scripting Tutorial** — Tutorial for writing Indigo scripts: using the IOM from the Script Editor, accessing devices, variables, and sending commands. → `reference/canonical/scripting/tutorial.md` +- **Python Packages** — List of Python packages bundled with Indigo 2025.2 and guidance on installing additional packages for use in plugins and scripts. → `reference/canonical/scripting/guides/python-packages.md` +- **Action Groups** — Reference for the ActionGroup class: properties and the execute command for running action groups from scripts. → `reference/canonical/scripting/reference/action-groups.md` +- **Actions** — Complete reference for the Actions class: properties, methods, and commands for all built-in action types. → `reference/canonical/scripting/reference/actions.md` +- **Event Data Path Specifiers** — Reference for path specifier strings used to extract specific fields from event data objects in triggers and action scripts. → `reference/canonical/scripting/reference/event-data-paths.md` +- **Folders** — Reference for the Folder class: properties and methods for working with Indigo organizational folders in scripts. → `reference/canonical/scripting/reference/folders.md` +- **Insteon Commands** — Reference for the indigo.insteon.* command namespace: send raw Insteon commands, manage links, and control the interface. → `reference/canonical/scripting/reference/insteon-commands.md` +- **Schedules** — Reference for the Schedule class: properties and the execute command for time-based automations in scripts. → `reference/canonical/scripting/reference/schedules.md` +- **Server Properties & Commands** — Reference for indigo.server.* properties and commands: logging, speaking, sending email, launching URLs, and managing the server. → `reference/canonical/scripting/reference/server-commands.md` +- **Triggers** — Reference for the Trigger class: properties, conditions, and commands for event-driven automation objects in scripts. → `reference/canonical/scripting/reference/triggers.md` +- **Utility Classes & Functions** — Reference for the indigo.utils module: ValidationError, the JSON encoder, return_static_file, email/boolean helpers, is_int, and the indigo.Dict/List conversion methods. → `reference/canonical/scripting/reference/utils.md` +- **Variables** — Reference for the Variable class: properties and commands for reading and writing Indigo variables in scripts. → `reference/canonical/scripting/reference/variables.md` +- **X10 Commands** — Reference for the indigo.x10.* command namespace: send raw X10 commands from scripts. → `reference/canonical/scripting/reference/x10-commands.md` +- **Device Subclasses** — Reference for Indigo device subclasses: DimmerDevice, RelayDevice, SensorDevice, SpeedControlDevice, SprinklerDevice, ThermostatDevice, and MultiIODevice. → `reference/canonical/scripting/reference/device-subclasses.md` +- **DimmerDevice** — IOM reference for DimmerDevice: properties, states, and the indigo.dimmer.* commands. → `reference/canonical/scripting/reference/device-subclasses/dimmer.md` +- **MultiIODevice** — IOM reference for MultiIODevice: analog/binary/sensor inputs and outputs. → `reference/canonical/scripting/reference/device-subclasses/multiio.md` +- **RelayDevice** — IOM reference for RelayDevice: properties, states, and commands. → `reference/canonical/scripting/reference/device-subclasses/relay.md` +- **SensorDevice** — IOM reference for SensorDevice: properties, states, and commands. → `reference/canonical/scripting/reference/device-subclasses/sensor.md` +- **SpeedControlDevice** — IOM reference for SpeedControlDevice: properties, states, and commands. → `reference/canonical/scripting/reference/device-subclasses/speedcontrol.md` +- **SprinklerDevice** — IOM reference for SprinklerDevice: properties, states, and commands. → `reference/canonical/scripting/reference/device-subclasses/sprinkler.md` +- **ThermostatDevice** — IOM reference for ThermostatDevice: properties, states, and commands. → `reference/canonical/scripting/reference/device-subclasses/thermostat.md` +- **Devices** — Reference for the Indigo Device base class: properties, methods, and the dictionary representation used in scripting. → `reference/canonical/scripting/reference/devices.md` +- **Device Base Class** — IOM reference for the Device base class: properties, plugin properties, custom states, and the indigo.device.* command namespace. → `reference/canonical/scripting/reference/devices/base-class.md` +- **Dictionary Representation** — How an Indigo device is represented as a Python dictionary, and the keys it contains. → `reference/canonical/scripting/reference/devices/dictionary.md` + +## User Guide + + +## Documentation diff --git a/reference/canonical/VERSION b/reference/canonical/VERSION new file mode 100644 index 0000000..4f24465 --- /dev/null +++ b/reference/canonical/VERSION @@ -0,0 +1,7 @@ +indigo_version: 2025.2 +source_index: https://docs.indigodomo.com/llms.txt +source_full: https://docs.indigodomo.com/llms-full.txt +fetched_utc: 2026-07-09T22:05:28Z +sha256_full: 3d79e398f7093ce9199b055c5be56de60441192e07e0495ba31b0f4c86ace5bd +pages: 68 +generated_by: tools/refresh_canonical.py diff --git a/reference/canonical/api.md b/reference/canonical/api.md new file mode 100644 index 0000000..8f616c7 --- /dev/null +++ b/reference/canonical/api.md @@ -0,0 +1,47 @@ + + +# Integration APIs +This article is about the two APIs in IWS that developers can use to integrate Indigo with external services: + +- [WebSocket API](websocket.md) – (which the new Indigo Touch Web UI uses), and +- [HTTP API](http.md) – shares as much of the messaging construction with the WebSocket interface as is practical. + +Both of these APIs are authenticated with HTTP Digest, API Keys, and [local secrets](../user/remote-access/web-server.md#authentication) (either as a query string or preferably an Authorization header) depending on how the user configures it in the Start Local Server dialog. + +!!! warning "Warning" + - **HTTP Basic authentication** has been deprecated due to its insecure nature. + - The old REST API has been deprecated in favor of the [HTTP API](http.md). + +## Versioning +A quick note on versioning: all APIs will be versioned under the following scheme: + +- `/v2/` - this is the top level version number and will change as necessary + +### Python vs JavaScript +In these APIs, we’re using [JSON](https://www.json.org/) (JavaScript Object Notation) as the message format for communicating between the WebSocket and HTTP APIs and IWS. In JavaScript, an “object” definition looks (almost) exactly like a Python dictionary (and vice versa). So we may refer to an object or dictionary (dict): for the purposes of this document, they refer to the same JSON construct. For example, we may call this an object or a dict: + +```json +{ + "key1": "value 1", + "key2": 2 +} +``` + +We expect there will be both Python and JavaScript users integrating our APIs, so we wanted to explicitly call this out. As a primarily Python organization, you may notice a bias towards “dict”. + +Python developers will notice the use of `null` in the message descriptions. This corresponds to the Python `None` object. Also of note are the booleans `true` and `false`, which are capitalized in Python but not in JSON. Here’s a handy cheat sheet: + +| Python | JSON Equivalent | +|--------|-----------------| +| True | true | +| False | false | +| float | Number | +| int | Number | +| None | null | +| dict | Object | +| list | Array | +| tuple | Array | +| str | String | + + +If you are new to JSON, you may want to use the [JSON Validator website](https://jsonlint.com) to validate that the JSON message you are sending is valid JSON. diff --git a/reference/canonical/api/http.md b/reference/canonical/api/http.md new file mode 100644 index 0000000..4769bc8 --- /dev/null +++ b/reference/canonical/api/http.md @@ -0,0 +1,775 @@ + + +# HTTP API + +!!! abstract "In this guide" + This API is meant for use with standard HTTP as the communication mechanism. HTTP **GET** requests to get Indigo object + instances in JSON format, and **POST** requests to send commands to the Indigo Server. + +## HTTP API Endpoints +The following is a summary of endpoints (URLs that you will need to use the API) that are available (detail on each is further down): + +- `/v2/api/indigo.devices` - endpoint to get a list of devices +- `/v2/api/indigo.devices/123456789` - endpoint to get a specific device instance (See [Device Objects](messages.md#device-objects) below) +- `/v2/api/indigo.variables` - endpoint to get a list of variables +- `/v2/api/indigo.variables/123456789` - endpoint to specific variable object (See [Variable Objects](messages.md#variable-objects) below) +- `/v2/api/indigo.actionGroups` - endpoint to get a list of action groups +- `/v2/api/indigo.actionGroups/123456789` - endpoint to get a specific action group (See [Action Group Objects](messages.md#action-group-objects) below) +- `/v2/api/command` - endpoint to send Indigo a command (device control, variable update, action execution). + +You’ll receive full JSON objects which represent either a list of all instances of the object type requested (for example, a list of all action groups) or an individual Indigo object instance (a single device, variable, etc.) Each individual object will be different based on the object type, class and its definition (a custom device, for example). See the [Indigo Object Model](../scripting/iom-concepts.md) docs for details about each object type. + +## Authentication +HTTP API requests must be authenticated using an **API Key**. You can manage API Keys in the [Authorizations section of your Indigo Account](https://www.indigodomo.com/account/authorizations). Using keys instead of your Indigo Server username/password has several advantages: you can, at any time, revoke an API key, and it will immediately cause anything using it to fail. This will not affect anything else using your username/password or another API key, so your server protections against intrusions are much more granular. Also, if someone does manage to get your API Key, they can control devices, but they cannot modify your database (add/delete devices, etc.) - that is reserved for Indigo clients using the username/password. + +The best way to use an API Key is to include it in an **Authorization** header on your HTTP request. All the examples below show this approach. If you are using a system which does not allow you to set headers for your HTTP request, you can include the API Key as a query argument with the URL: + +`https://YOUR-REFLECTOR-NAME.indigodomo.net/v2/api/indigo.devices/123456789?api-key=YOUR-API-KEY` + +When using HTTPS, such as when you are using your Indigo Reflector, then your API Key (in both instances) is protected by the TLS security used by the HTTPS protocol. You may use the API locally (or thorough your own router port forwarding), but those connections will be HTTP and **will not be secure**. It is highly recommended that you always use your Indigo Reflector because it provides a very simple and **secure** solution for accessing your system. + +!!! warning + Don't share your API keys with anyone who is not authorized to use them — especially in posts to the user forums. + +## Getting Device Objects +The HTTP API includes a couple of methods for getting device instances as JSON objects. + +### Getting All Device Objects +[Device Objects](messages.md#device-objects) are JSON representations of an Indigo device instance. This JSON object will contain all information about the object, which you can use in your solutions. By using the endpoint without specifying a specific device id, you can get the complete list of all devices in your Indigo database: + +`/v2/api/indigo.devices` + +Here are some examples in different languages/technologies that illustrate how to get all devices. + +#### Pure Python 3 (no additional libraries) + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. + +```python +# This is a pure python example - no additional libraries needed +from urllib.request import Request, urlopen +import json + +REFLECTORNAME = "YOUR-REFLECTOR-NAME" +APIKEY = "YOUR-API-KEY" + +req = Request(f"https://{REFLECTORNAME}.indigodomo.net/v2/api/indigo.devices") +req.add_header('Authorization', f"Bearer {APIKEY}") +with urlopen(req) as request: + device_list = json.load(request) + print(device_list) +``` + +#### JavaScript run from nodejs (no additional libraries) + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. + +```javascript +// Things that are specific to your environment +const REFLECTORNAME = "YOUR-REFLECTOR-NAME" +const APIKEY = "YOUR-API-KEY" + +// Get the http module, and tell it that you're using HTTPS +const http = require("https") + +// These are options that you'll pass to the get call +const options = { + hostname: `${REFLECTORNAME}.indigodomo.net`, + path: `/v2/api/indigo.devices`, + headers: { + Authorization: `Bearer ${APIKEY}` + } +} + +// Get the device JSON from Indigo, parse it into an object, and log it to the console +http.get(options, (response) => { + let result = "" + response.on("data", chunk => { + result += chunk; + }) + response.on("end", () => { + const deviceList = JSON.parse(result); + console.log(deviceList); + }) +}) +``` + +#### Using curl from the command line + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. + +```bash +curl -H "Authorization: Bearer YOUR-API-KEY" https://YOUR-REFLECTOR-NAME.indigodomo.net/v2/api/indigo.devices +``` + +### Getting a Single Device Object +Here are a few examples in different languages that illustrate how to get device objects. + +#### Pure Python 3 (no additional libraries) { #getting-a-single-device-object-pure-python-3-no-additional-libraries } + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. +- Replace `*123456789*` with a valid Indigo device ID. + +```python +# This is a pure python example - no additional libraries needed +from urllib.request import Request, urlopen +import json + +REFLECTORNAME = "YOUR-REFLECTOR-NAME" +APIKEY = "YOUR-API-KEY" +DEVICEID = 123456789 + +req = Request(f"https://{REFLECTORNAME}.indigodomo.net/v2/api/indigo.devices/{DEVICEID}") +req.add_header('Authorization', f"Bearer {APIKEY}") +with urlopen(req) as request: + device_instance = json.load(request) + print(device_instance) +``` + +#### JavaScript run from nodejs (no additional libraries) { #getting-a-single-device-object-javascript-run-from-nodejs-no-additional-libraries } + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. +- Replace `*123456789*` with a valid Indigo device ID. + +```javascript +// Things that are specific to your environment +const REFLECTORNAME = "YOUR-REFLECTOR-NAME" +const APIKEY = "YOUR-API-KEY" +const DEVICEID = 123456789 + +// Get the http module, and tell it that you're using HTTPS +const http = require("https") + +// These are options that you'll pass to the get call +const options = { + hostname: `${REFLECTORNAME}.indigodomo.net`, + path: `/v2/api/indigo.devices/${DEVICEID}`, + headers: { + Authorization: `Bearer ${APIKEY}` + } +} + +// Get the device JSON from Indigo, parse it into an object, and log it to the console +http.get(options, (response) => { + let result = "" + response.on("data", chunk => { + result += chunk; + }) + response.on("end", () => { + const deviceInstance = JSON.parse(result); + console.log(deviceInstance); + }) +}) +``` + +#### Using curl from the command line { #getting-a-single-device-object-using-curl-from-the-command-line } + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. + +```bash +curl -H "Authorization: Bearer YOUR-API-KEY" https://YOUR-REFLECTOR-NAME.indigodomo.net/v2/api/indigo.devices/123456789 +``` + +### Controlling an Indigo Device +You can control Indigo devices by sending commands through the API that instruct Indigo how to control the device. Indigo devices come in a variety of types, and each type has its own command set. See the [Device Command Messages](messages.md#device-command-messages) below for a description of all messages. For now, here are some simple examples to toggle devices: + +#### Pure Python 3 (no additional libraries) { #controlling-an-indigo-device-pure-python-3-no-additional-libraries } + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. +- Replace `*123456789*` with a valid Indigo device ID. + +```python +# This is a pure python example - no additional libraries needed +from urllib.request import Request, urlopen +import json + +REFLECTORNAME = "YOUR-REFLECTOR-NAME" +APIKEY = "YOUR-API-KEY" +DEVICEID = 123456789 + +# The message to send to the Indigo Server +message = json.dumps({ + "id": "optional-custom-user-message", + "message": "indigo.device.toggle", + "objectId": DEVICEID +}).encode("utf8") + +req = Request(f"https://{REFLECTORNAME}.indigodomo.net/v2/api/command", data=message) +req.add_header('Authorization', f"Bearer {APIKEY}") +with urlopen(req) as request: + reply = json.load(request) + print(reply) +``` + +#### JavaScript toggle from nodejs (no additional libraries) + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. +- Replace `*123456789*` with a valid Indigo device ID. + +```javascript +// Things that are specific to your environment +const REFLECTORNAME = "YOUR-REFLECTOR-NAME" +const APIKEY = "YOUR-API-KEY" +const DEVICEID = 123456789 + +// Get the http module, and tell it that you're using HTTPS +const http = require("https") + +// The message to send to the Indigo Server +const message = JSON.stringify({ + "id": "optional-custom-user-message", + "message": "indigo.device.toggle", + "objectId": DEVICEID +}) + +// These are options that you'll pass to the get call +const options = { + hostname: `${REFLECTORNAME}.indigodomo.net`, + path: "/v2/api/command", + method: "POST", + headers: { + Authorization: `Bearer ${APIKEY}`, + "Content-Length": message.length + } +} + +// Get the device JSON from Indigo, parse it into an object, and log it to the console +const req = http.request(options, (response) => { + let result = "" + response.on("data", chunk => { + result += chunk; + }) + response.on("end", () => { + const deviceInstance = JSON.parse(result); + console.log(deviceInstance); + }) +}) +req.write(message) +req.end() +``` + +#### Using curl from the command line { #controlling-an-indigo-device-using-curl-from-the-command-line } + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. +- Replace `*123456789*` with a valid Indigo device ID. + +```bash +curl -X POST -H "Authorization: Bearer YOUR-API-KEY" -d '{"message": "indigo.device.toggle", "objectId": 123456789}' https://YOUR-REFLECTOR-NAME.indigodomo.net/v2/api/command +``` + +Using these examples, you can now construct any command listed below for any device type. + +### Controlling an Indigo Device with Parameters +As noted above, Indigo devices come in a variety of types, and each type has its own command set. As a part of this command set, some devices require specific parameters in order for Indigo to be able to execute them (some devices accept optional parameters, and others do not require any parameters at all). See the [Device Command Messages](messages.md#device-command-messages) below for a description of all messages. The following examples use the `indigo.dimmer.setBrightness` command and demonstrate how to include parameters. + +#### Pure Python 3 (no additional libraries) { #controlling-an-indigo-device-with-parameters-pure-python-3-no-additional-libraries } + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. +- Replace `*123456789*` with a valid Indigo device ID. + +```python +# This is a pure python example - no additional libraries needed +from urllib.request import Request, urlopen +import json + +REFLECTORNAME = "YOUR-REFLECTOR-NAME" +APIKEY = "YOUR-API-KEY" +DEVICEID = 123456789 + +# The message to send to the Indigo Server +message = json.dumps({ + "id": "optional-custom-user-message", + "message": "indigo.dimmer.setBrightness", + "objectId": DEVICEID, + "parameters": { + "value": 50, + "delay": 10 + } +}).encode("utf8") + +req = Request(f"https://{REFLECTORNAME}.indigodomo.net/v2/api/command", data=message) +req.add_header('Authorization', f"Bearer {APIKEY}") +with urlopen(req) as request: + reply = json.load(request) + print(reply) +``` + +#### JavaScript run from nodejs (no additional libraries) { #controlling-an-indigo-device-with-parameters-javascript-run-from-nodejs-no-additional-libraries } + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. +- Replace `*123456789*` with a valid Indigo device ID. + +```javascript +// Things that are specific to your environment +const REFLECTORNAME = "YOUR-REFLECTOR-NAME" +const APIKEY = "YOUR-API-KEY" +const DEVICEID = 123456789 + +// Get the http module, and tell it that you're using HTTPS +const http = require("https") + +// The message to send to the Indigo Server +const message = JSON.stringify({ + "id": "optional-custom-user-message", + "message": "indigo.dimmer.setBrightness", + "objectId": DEVICEID, + "parameters": { + "value": 50, + "delay": 10 + } +}) + +// These are options that you'll pass to the get call +const options = { + hostname: `${REFLECTORNAME}.indigodomo.net`, + path: "/v2/api/command", + method: "POST", + headers: { + Authorization: `Bearer ${APIKEY}`, + "Content-Length": message.length + } +} + +// Get the device JSON from Indigo, parse it into an object, and log it to the console +const req = http.request(options, (response) => { + let result = "" + response.on("data", chunk => { + result += chunk; + }) + response.on("end", () => { + const deviceInstance = JSON.parse(result); + console.log(deviceInstance); + }) +}) +req.write(message) +req.end() +``` + +#### Using curl from the command line { #controlling-an-indigo-device-with-parameters-using-curl-from-the-command-line } + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. +- Replace `*123456789*` with a valid Indigo device ID. + +```bash +curl -X POST -H "Authorization: Bearer YOUR-API-KEY" -d '{"message": "indigo.dimmer.setBrightness", "objectId": 123456789, "parameters": {"value": 50, "delay": 10}}' https://YOUR-REFLECTOR-NAME.indigodomo.net/v2/api/command +``` + +Using these examples, you can now construct any command listed below for any device type. + +## Getting Variable Objects +### Getting All Variable Objects +[Variable Objects](messages.md#variable-objects) are JSON representations of an Indigo variable instance. This JSON object will contain all information about the object, which you can use in your solutions. By using the endpoint without specifying a specific variable id, you can get the complete list of all variables in your Indigo database: + +`/v2/api/indigo.variables` + +Here are some examples in different languages/technologies that illustrate how to get all variables. + +#### Pure Python 3 (no additional libraries) { #getting-all-variable-objects-pure-python-3-no-additional-libraries } + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. + +```python +# This is a pure python example - no additional libraries needed +from urllib.request import Request, urlopen +import json + +REFLECTORNAME = "YOUR-REFLECTOR-NAME" +APIKEY = "YOUR-API-KEY" + +req = Request(f"https://{REFLECTORNAME}.indigodomo.net/v2/api/indigo.variables") +req.add_header('Authorization', f"Bearer {APIKEY}") +with urlopen(req) as request: + variable_list = json.load(request) + print(variable_list) +``` + +#### JavaScript run from nodejs (no additional libraries) { #getting-all-variable-objects-javascript-run-from-nodejs-no-additional-libraries } + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. + +```javascript +// Things that are specific to your environment +const REFLECTORNAME = "YOUR-REFLECTOR-NAME" +const APIKEY = "YOUR-API-KEY" + +// Get the http module, and tell it that you're using HTTPS +const http = require("https") + +// These are options that you'll pass to the get call +const options = { + hostname: `${REFLECTORNAME}.indigodomo.net`, + path: `/v2/api/indigo.variables`, + headers: { + Authorization: `Bearer ${APIKEY}` + } +} + +// Get the variable JSON from Indigo, parse it into an object, and log it to the console +http.get(options, (response) => { + let result = "" + response.on("data", chunk => { + result += chunk; + }) + response.on("end", () => { + const VariableList = JSON.parse(result); + console.log(VariableList); + }) +}) +``` + +#### Using curl from the command line { #getting-all-variable-objects-using-curl-from-the-command-line } + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. + +```bash +curl -H "Authorization: Bearer YOUR-API-KEY" https://YOUR-REFLECTOR-NAME.indigodomo.net/v2/api/indigo.variables +``` + +### Getting a Single Variable Object +Here are a few examples in different languages that illustrate how to get variable objects. + +#### Pure Python 3 (no additional libraries) { #getting-a-single-variable-object-pure-python-3-no-additional-libraries } + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. +- Replace `*123456789*` with a valid Indigo variable ID. + +```python +# This is a pure python example - no additional libraries needed +from urllib.request import Request, urlopen +import json + +REFLECTORNAME = "YOUR-REFLECTOR-NAME" +APIKEY = "YOUR-API-KEY" +VARID = 123456789 # Indigo variable object id + +req = Request(f"https://{REFLECTORNAME}.indigodomo.net/v2/api/indigo.variables/{VARID}") +req.add_header('Authorization', f"Bearer {APIKEY}") +with urlopen(req) as request: + var_instance = json.load(request) + print(var_instance) +``` + +#### JavaScript run from nodejs (no additional libraries) { #getting-a-single-variable-object-javascript-run-from-nodejs-no-additional-libraries } + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. +- Replace `*123456789*` with a valid Indigo variable ID. + +```javascript +// Things that are specific to your environment +const REFLECTORNAME = "YOUR-REFLECTOR-NAME" +const APIKEY = "YOUR-API-KEY" +const VARID = 123456789 + +// Get the http module, and tell it that you're using HTTPS +const http = require("https") + +// These are options that you'll pass to the get call +const options = { + hostname: `${REFLECTORNAME}.indigodomo.net`, + path: `/v2/api/indigo.variables/${VARID}`, + headers: { + Authorization: `Bearer ${APIKEY}` + } +} + +// Get the variable object JSON from Indigo, parse it into an object, and log it to the console +http.get(options, (response) => { + let result = "" + response.on("data", chunk => { + result += chunk; + }) + response.on("end", () => { + const varInstance = JSON.parse(result); + console.log(varInstance); + }) +}) +``` + +#### Using curl from the command line { #getting-a-single-variable-object-using-curl-from-the-command-line } + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. +- Replace `*123456789*` with a valid Indigo variable ID. + +```bash +curl -H "Authorization: Bearer YOUR-API-KEY" https://YOUR-REFLECTOR-NAME.indigodomo.net/v2/api/indigo.variables/123456789 +``` + +### Updating a Variable's Value +You can interact with Indigo variables by sending commands through the API that instruct Indigo what to do. There is only one type of Indigo variable, and the variable type has its own command set. See the [Variable Command Messages](messages.md#variable-command-messages) below for a description of all messages. For now, here is an example of how to set the value of a variable: + +#### Pure Python 3 (no additional libraries) { #updating-a-variables-value-pure-python-3-no-additional-libraries } + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. +- Replace `*123456789*` with a valid Indigo variable ID. + +```python +# This is a pure python example - no additional libraries needed +from urllib.request import Request, urlopen +import json + +REFLECTORNAME = "YOUR-REFLECTOR-NAME" +APIKEY = "YOUR-API-KEY" +VARIABLEID = 123456789 + +# The message to send to the Indigo Server +message = json.dumps({ + "id": "optional-custom-user-message", + "message": "indigo.variable.updateValue", + "objectId": VARIABLEID, + "parameters": { + "value": "Some string value" + } +}).encode("utf8") + +req = Request(f"https://{REFLECTORNAME}.indigodomo.net/v2/api/command", data=message) +req.add_header('Authorization', f"Bearer {APIKEY}") +with urlopen(req) as request: + reply = json.load(request) + print(reply) +``` + +#### JavaScript run from nodejs (no additional libraries) { #updating-a-variables-value-javascript-run-from-nodejs-no-additional-libraries } + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. +- Replace `*123456789*` with a valid Indigo variable ID. + +```javascript +// Things that are specific to your environment +const REFLECTORNAME = "YOUR-REFLECTOR-NAME" +const APIKEY = "YOUR-API-KEY" +const VARIABLEID = 123456789 + +// Get the http module, and tell it that you're using HTTPS +const http = require("https") + +// The message to send to the Indigo Server +const message = JSON.stringify({ + "id": "optional-custom-user-message", + "message": "indigo.variable.updateValue", + "objectId": VARIABLEID, + "parameters": { + "value": "Some string value" + } +}) + +// These are options that you'll pass to the get call +const options = { + hostname: `${REFLECTORNAME}.indigodomo.net`, + path: "/v2/api/command", + method: "POST", + headers: { + Authorization: `Bearer ${APIKEY}`, + "Content-Length": message.length + } +} + +// Get the variable JSON from Indigo, parse it into an object, and log it to the console +const req = http.request(options, (response) => { + let result = "" + response.on("data", chunk => { + result += chunk; + }) + response.on("end", () => { + const VariableInstance = JSON.parse(result); + console.log(VariableInstance); + }) +}) +req.write(message) +req.end() +``` + +#### Using curl from the command line { #updating-a-variables-value-using-curl-from-the-command-line } + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. +- Replace `*123456789*` with a valid Indigo variable ID. + +```bash +curl -X POST -H "Authorization: Bearer YOUR-API-KEY" -d '{"message": "indigo.variable.updateValue", "objectId": 123456789, "parameters": {"value": "Some string value"}}' https://YOUR-REFLECTOR-NAME.indigodomo.net/v2/api/command +``` + +Using these examples, you can now construct any command listed below for any variable type. + +## Getting Action Group Objects +### Getting All Action Group Objects +[Action Group Objects](messages.md#action-group-objects) are JSON representations of an Indigo action group instance. This JSON object will contain all information about the object, which you can use in your solutions. By using the endpoint without specifying a specific action group id, you can get the complete list of all action groups in your Indigo database: + +`/v2/api/indigo.actionGroups` + +Here are some examples in different languages/technologies that illustrate how to get all action groups. + +#### Pure Python 3 (no additional libraries) { #getting-all-action-group-objects-pure-python-3-no-additional-libraries } + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. + +```python +# This is a pure python example - no additional libraries needed +from urllib.request import Request, urlopen +import json + +REFLECTORNAME = "YOUR-REFLECTOR-NAME" +APIKEY = "YOUR-API-KEY" + +req = Request(f"https://{REFLECTORNAME}.indigodomo.net/v2/api/indigo.actionGroups") +req.add_header('Authorization', f"Bearer {APIKEY}") +with urlopen(req) as request: + action_group_list = json.load(request) + print(action_group_list) +``` + +#### JavaScript run from nodejs (no additional libraries) { #getting-all-action-group-objects-javascript-run-from-nodejs-no-additional-libraries } + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. + +```javascript +// Things that are specific to your environment +const REFLECTORNAME = "YOUR-REFLECTOR-NAME" +const APIKEY = "YOUR-API-KEY" + +// Get the http module, and tell it that you're using HTTPS +const http = require("https") + +// These are options that you'll pass to the get call +const options = { + hostname: `${REFLECTORNAME}.indigodomo.net`, + path: `/v2/api/indigo.actionGroups`, + headers: { + Authorization: `Bearer ${APIKEY}` + } +} + +// Get the action group JSON from Indigo, parse it into an object, and log it to the console +http.get(options, (response) => { + let result = "" + response.on("data", chunk => { + result += chunk; + }) + response.on("end", () => { + const actionGroupList = JSON.parse(result); + console.log(actionGroupList); + }) +}) +``` + +#### Using curl from the command line { #getting-all-action-group-objects-using-curl-from-the-command-line } + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. + +```bash +curl -H "Authorization: Bearer YOUR-API-KEY" https://YOUR-REFLECTOR-NAME.indigodomo.net/v2/api/indigo.actionGroups +``` + +### Executing an Action Group +You can control Indigo action groups by sending commands through the API that instruct Indigo how to execute the action group. There is only one type of Indigo action group, and the action group type has its own command set. See the [Action Group Command Messages](messages.md#action-group-command-messages) below for a description of all messages. For now, here is a simple example of how to execute an action group: + +#### Pure Python 3 (no additional libraries) { #executing-an-action-group-pure-python-3-no-additional-libraries } + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. +- Replace `*123456789*` with a valid Indigo action group ID. + +```python +# This is a pure python example - no additional libraries needed +from urllib.request import Request, urlopen +import json + +REFLECTORNAME = "YOUR-REFLECTOR-NAME" +APIKEY = "YOUR-API-KEY" +ACTIONGROUPID = 123456789 + +# The message to send to the Indigo Server +message = json.dumps({ + "id": "optional-custom-user-message", + "message": "indigo.actionGroup.execute", + "objectId": ACTIONGROUPID +}).encode("utf8") + +req = Request(f"https://{REFLECTORNAME}.indigodomo.net/v2/api/command", data=message) +req.add_header('Authorization', f"Bearer {APIKEY}") +with urlopen(req) as request: + reply = json.load(request) + print(reply) +``` + +#### JavaScript run from nodejs (no additional libraries) { #executing-an-action-group-javascript-run-from-nodejs-no-additional-libraries } + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. +- Replace `*123456789*` with a valid Indigo action group ID. + +```javascript +// Things that are specific to your environment +const REFLECTORNAME = "YOUR-REFLECTOR-NAME" +const APIKEY = "YOUR-API-KEY" +const ACTIONGROUPID = 123456789 + +// Get the http module, and tell it that you're using HTTPS +const http = require("https") + +// The message to send to the Indigo Server +const message = JSON.stringify({ + "id": "optional-custom-user-message", + "message": "indigo.actionGroup.execute", + "objectId": ACTIONGROUPID +}) + +// These are options that you'll pass to the get call +const options = { + hostname: `${REFLECTORNAME}.indigodomo.net`, + path: "/v2/api/command", + method: "POST", + headers: { + Authorization: `Bearer ${APIKEY}`, + "Content-Length": message.length + } +} + +// Get the action group JSON from Indigo, parse it into an object, and log it to the console +const req = http.request(options, (response) => { + let result = "" + response.on("data", chunk => { + result += chunk; + }) + response.on("end", () => { + const actionGroupInstance = JSON.parse(result); + console.log(actionGroupInstance); + }) +}) +req.write(message) +req.end() +``` + +#### Using curl from the command line { #executing-an-action-group-using-curl-from-the-command-line } + +- Replace `*YOUR-API-KEY*` with a valid key from your Indigo account. +- Replace `*YOUR-REFLECTOR-NAME*` with the reflector name for your Indigo server. +- Replace `*123456789*` with a valid Indigo action group ID. + +```bash +curl -X POST -H "Authorization: Bearer YOUR-API-KEY" -d '{"message": "indigo.actionGroup.execute", "objectId": 123456789}' https://YOUR-REFLECTOR-NAME.indigodomo.net/v2/api/command +``` + +Using these examples, you can now construct any command listed below for any action group type. diff --git a/reference/canonical/api/messages.md b/reference/canonical/api/messages.md new file mode 100644 index 0000000..12cdf6b --- /dev/null +++ b/reference/canonical/api/messages.md @@ -0,0 +1,971 @@ + + +# API Messages +As mentioned earlier, both the WebSocket and HTTP APIs use JSON as the message format. We have exposed JSON versions of main Indigo objects: devices, variables, action groups, and control pages. We are also exposing command messages, which parallel the IOM command namespaces (where appropriate) for each of these object types (i.e. [indigo.device](../scripting/reference/devices/base-class.md#commands-indigodevice), [indigo.variable](../scripting/reference/variables.md#commands-indigovariable), etc.), which will allow you to command devices, set variable values, execute action groups, etc. + +There are also two other message types: event log messages, which represent each message that flow into the Event Log window, and error messages, which is a standardized way to communicate error conditions with each API. + +In this section, we will give examples of each message type and describe their use. + +If you are new to JSON, you might want to use the [JSON Validator website](https://jsonlint.com) to validate that your JSON messages are valid JSON. + +## Device Messaging +This section describes the messages that you'll use when implementing either the WebSocket or HTTP APIs. They fall into two categories: device objects and device commands. + +### Device Objects +You’ll receive full device JSON objects which represent an Indigo device instance. Each device will be slightly different based on the device class and the definition (if a custom device). See the [Indigo Object Model](../scripting/iom-concepts.md) docs for device details. + +#### Example device object +This is an example of an Indigo device as a JSON object. Of course, to accommodate the wide variety of supported device types, each one will be somewhat different. This one represents an Insteon Dimmer. + +```json +{ + "class": "indigo.DimmerDevice", + "address": "3B.04.7A", + "batteryLevel": null, + "blueLevel": null, + "brightness": 0, + "buttonConfiguredCount": 0, + "buttonGroupCount": 1, + "configured": true, + "defaultBrightness": 100, + "description": "- sample device -", + "deviceTypeId": "", + "displayStateId": "brightnessLevel", + "displayStateImageSel": "indigo.kStateImageSel.DimmerOff", + "displayStateValRaw": 0, + "displayStateValUi": "0", + "enabled": true, + "energyAccumBaseTime": null, + "energyAccumTimeDelta": null, + "energyAccumTotal": null, + "energyCurLevel": null, + "errorState": "", + "folderId": 1552926800, + "folder": { + "class": "indigo.Folder", + "id": 1552926800, + "name": "Insteon", + "remoteDisplay": true + }, + "globalProps": {}, + "greenLevel": null, + "id": 1508839119, + "lastChanged": "2023-02-01T11:39:58", + "lastSuccessfulComm": "2023-02-01T11:39:58", + "ledStates": [], + "model": "LampLinc (dual-band)", + "name": "Insteon Dimmer", + "onBrightensToDefaultToggle": true, + "onBrightensToLast": false, + "onState": false, + "ownerProps": {}, + "pluginId": "", + "pluginProps": {}, + "protocol": "indigo.kProtocol.Insteon", + "redLevel": null, + "remoteDisplay": false, + "sharedProps": {}, + "states": { + "brightnessLevel": 0, + "onOffState": false + }, + "subModel": "Plug-In", + "subType": "Plug-In", + "supportsAllLightsOnOff": true, + "supportsAllOff": true, + "supportsColor": false, + "supportsOnState": true, + "supportsRGB": false, + "supportsRGBandWhiteSimultaneously": false, + "supportsStatusRequest": true, + "supportsTwoWhiteLevels": false, + "supportsTwoWhiteLevelsSimultaneously": false, + "supportsWhite": false, + "supportsWhiteTemperature": false, + "version": 67, + "whiteLevel": null, + "whiteLevel2": null, + "whiteTemperature": null +} +``` + +Message that contain device objects generate those objects by first converting the device to a python dictionary and then converting the python dictionary to JSON. See the [Generating a Dictionary for a device](../scripting/reference/devices/dictionary.md#generating-a-dictionary-for-a-device) section of the IOM Reference for details. + +!!! note + The `folder` element is only available in the HTTP API. Folders are handled differently in the WebSocket API. + +#### Device Command Messages +One thing you will notice as you are looking through these examples, is that they closely mirror the Python-based [IOM commands for controlling devices](../scripting/iom-concepts.md). This was intentional to make learning one API a stepping stone to another. The HTTP API messages and the WebSocket API messages are identical, and are very clearly a JSON-rendered version of the associated IOM command. + + The `id` key is optional, but may contain a user generated ID that will be logged and returned to help match up with message requests. Custom `id` values should be strings. + +#### indigo.device +[indigo.device](../scripting/reference/devices/base-class.md#commands-indigodevice) command messages can be used for several Indigo device types - [indigo.RelayDevice](../scripting/reference/device-subclasses/relay.md#relaydevice), [indigo.DimmerDevice](../scripting/reference/device-subclasses/dimmer.md#dimmerdevice), [indigo.SpeedControl](../scripting/reference/device-subclasses/speedcontrol.md#speedcontroldevice) (fans), and some [indigo.SensorDevice](../scripting/reference/device-subclasses/sensor.md#sensordevice) instances. + +**status request** `[indigo.device.statusRequest(123456789)](../scripting/reference/devices/base-class.md#status-request)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.device.statusRequest", + "objectId": 123456789 +} +``` + +**Note:** This message will work on any Indigo device type, though the device that it targets may not respond to status request messages in which case it will do nothing. + +This won't necessarily cause a device update message - if the device didn't have any changes after the status request, there will be no updates to the device in the server, so no update message will be sent out the websocket. + +**toggle** `[indigo.device.toggle(123456789, delay=5, duration=10)](../scripting/reference/devices/base-class.md#toggle)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.device.toggle", + "objectId": 123456789, + "parameters": { + "delay": 5, + "duration": 10 + } +} +``` + +`objectId` is the id of the device. The `parameters` dictionary is optional. + +**turn off** `[indigo.device.turnOff(123456789, delay=5, duration=10)](../scripting/reference/devices/base-class.md#turn-off)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.device.turnOff", + "objectId": 123456789, + "parameters": { + "delay": 5, + "duration": 10 + } +} +``` + +`objectId` is the id of the device. The `parameters` dictionary is optional. + +**turn on** `[indigo.device.turnOn(123456789, delay=5, duration=10)](../scripting/reference/devices/base-class.md#turn-on)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.device.turnOn", + "objectId": 123456789, + "parameters": { + "delay": 5, + "duration": 10 + } +} +``` + +`objectId` is the id of the device. The `parameters` dictionary is optional. + +**lock** `[indigo.device.lock(123456789, delay=5, duration=10)](../scripting/reference/devices/base-class.md#lock)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.device.lock", + "objectId": 123456789, + "parameters": { + "delay": 5, + "duration": 10 + } +} +``` + +`objectId` is the id of the device. The `parameters` dictionary is optional. + +**unlock** `[indigo.device.unlock(123456789, delay=5, duration=10)](../scripting/reference/devices/base-class.md#unlock)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.device.unlock", + "objectId": 123456789, + "parameters": { + "delay": 5, + "duration": 10 + } +} +``` + +`objectId` is the id of the device. The `parameters` dictionary is optional. + +**enable/disable** `[indigo.device.enable(123456789, value=True)](../scripting/reference/devices/base-class.md#enable-disable)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.device.enable", + "objectId": 123456789, + "parameters": { + "value": True, + } +} +``` + +`objectId` is the id of the device. The `value` parameter is optional (`True` to enable, `False` to disable). + +#### indigo.dimmer +[indigo.dimmer](../scripting/reference/devices/base-class.md#commands-indigodevice) command messages can be used for [indigo.DimmerDevice](../scripting/reference/device-subclasses/dimmer.md#dimmerdevice) instances. + +**brighten** `[indigo.dimmer.brighten(123456789, delay=5, duration=10)](../scripting/reference/device-subclasses/dimmer.md#brighten)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.dimmer.brighten", + "objectId": 123456789, + "parameters": { + "by": 5, + "delay": 10 + } +} +``` + +`objectId` is the id of the device. The `parameters` dictionary is optional. + +**dim** `[indigo.dimmer.dim(123456789, delay=5, duration=10)](../scripting/reference/device-subclasses/dimmer.md#dim)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.dimmer.dim", + "objectId": 123456789, + "parameters": { + "by": 5, + "delay": 10 + } +} +``` + +`objectId` is the id of the device. The `parameters` dictionary is optional. + +**set brightness** `[indigo.dimmer.setBrightness(123456789, value=50, delay=5)](../scripting/reference/device-subclasses/dimmer.md#set-brightness)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.dimmer.setBrightness", + "objectId": 123456789, + "parameters": { + "value": 50, + "delay": 10 + } +} +``` + +`objectId` is the id of the device. The `value` parameter is required and the `delay` parameter is optional. + +#### indigo.iodevice +[indigo.iodevice](../scripting/reference/devices/base-class.md#commands-indigodevice) command messages can be used for [indigo.MultiIODevice](../scripting/reference/device-subclasses/dimmer.md#dimmerdevice) instances. + +**set binary output** `[indigo.iodevice.setBinaryOutput(123456789, index=2, value=True)](../scripting/reference/device-subclasses/multiio.md#set-binary-output)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.iodevice.setBinaryOutput", + "objectId": 123456789, + "parameters": { + "index": 5, + "value": true + } +} +``` + +`objectId` is the id of the device. The `index` and `value` parameters are required. + +#### indigo.sensor +[indigo.sensor](../scripting/reference/device-subclasses/sensor.md#commands-indigosensor) command messages can be used for [indigo.sensorDevice](../scripting/reference/device-subclasses/sensor.md#sensordevice) instances. + +**set on state** `[indigo.sensor.setOnState(123456789, value=True)](../scripting/reference/device-subclasses/sensor.md#set-on-state)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.sensor.setOnState", + "objectId": 123456789, + "parameters": { + "value": true + } +} +``` + +`objectId` is the id of the device. The `value` parameter is required. + +#### indigo.speedcontrol +[indigo.speedcontrol](../scripting/reference/device-subclasses/speedcontrol.md#commands-indigospeedcontrol) command messages can be used for [indigo.speedcontrol](../scripting/reference/device-subclasses/speedcontrol.md#speedcontroldevice) instances. + +**decrease speed index** `[indigo.speedcontrol.decreaseSpeedIndex(123456789, by=2, delay=5)](../scripting/reference/device-subclasses/speedcontrol.md#decrease-speed-index)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.speedcontrol.decreaseSpeedIndex", + "objectId": 123456789, + "parameters": { + "by": 2, + "delay": 5 + } +} +``` + +`objectId` is the id of the device. The `by` and `delay` parameters are optional. + +**increase speed index** `[indigo.speedcontrol.increaseSpeedIndex(123456789, by=2, delay=5)](../scripting/reference/device-subclasses/speedcontrol.md#increase-speed-index)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.speedcontrol.increaseSpeedIndex", + "objectId": 123456789, + "parameters": { + "by": 2, + "delay": 5 + } +} +``` + +`objectId` is the id of the device. The `by` and `delay` parameters are optional. + +**set speed index** `[indigo.speedcontrol.setSpeedIndex(123456789, value=2, delay=5)](../scripting/reference/device-subclasses/speedcontrol.md#set-speed-index)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.speedcontrol.setSpeedIndex", + "objectId": 123456789, + "parameters": { + "value": 2, + "delay": 5 + } +} +``` + +`objectId` is the id of the device. The `value` parameter is required and the `delay` parameter is optional. + +**set speed level** `[indigo.speedcontrol.setSpeedLevel(123456789, value=50, delay=5)](../scripting/reference/device-subclasses/speedcontrol.md#set-speed-level)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.speedcontrol.setSpeedLevel", + "objectId": 123456789, + "parameters": { + "value": 50, + "delay": 5 + } +} +``` + +`objectId` is the id of the device. The `value` parameter is required and the `delay` parameter is optional. + +#### indigo.sprinkler +[indigo.sprinkler](../scripting/reference/device-subclasses/sprinkler.md#commands-indigosprinkler) command messages can be used for [indigo.sprinkler](../scripting/reference/device-subclasses/sprinkler.md#sprinklerdevice) instances. + +**next zone** `[indigo.sprinkler.nextZone(123456789)](../scripting/reference/device-subclasses/sprinkler.md#next-zone)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.sprinkler.nextZone", + "objectId": 123456789 +} +``` + +`objectId` is the id of the device. The `next zone` command does not have any additional parameters. + +**pause schedule** `[indigo.sprinkler.pause(123456789)](../scripting/reference/device-subclasses/sprinkler.md#pause-schedule)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.sprinkler.pause", + "objectId": 123456789 +} +``` + +`objectId` is the id of the device. The `next zone` command does not have any additional parameters. + +**previous zone** `[indigo.sprinkler.previousZone(123456789)](../scripting/reference/device-subclasses/sprinkler.md#previous-zone)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.sprinkler.previousZone", + "objectId": 123456789 +} +``` + +`objectId` is the id of the device. The `previous zone` command does not have any additional parameters. + +**resume schedule** `[indigo.sprinkler.resume(123456789)](../scripting/reference/device-subclasses/sprinkler.md#resume-schedule)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.sprinkler.resume", + "objectId": 123456789 +} +``` + +`objectId` is the id of the device. The `resume schedule` command does not have any additional parameters. + +**run schedule** `[indigo.sprinkler.run(123456789, schedule=[10, 15, 8, 0, 0, 0, 0, 0])](../scripting/reference/device-subclasses/sprinkler.md#run-schedule)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.sprinkler.run", + "objectId": 123456789, + "parameters": { + "schedule": [10, 15, 8, 0, 0, 0, 0, 0] + } +} +``` + +`objectId` is the id of the device. The `schedule` parameter is required. + +**stop schedule** `[indigo.sprinkler.stop(123456789)](../scripting/reference/device-subclasses/sprinkler.md#stop-schedule)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.sprinkler.stop", + "objectId": 123456789 +} +``` + +`objectId` is the id of the device. The `resume schedule` command does not have any additional parameters. + +**set active zone** `[indigo.sprinkler.setActiveZone(123456789, index=2)](../scripting/reference/device-subclasses/sprinkler.md#set-active-zone)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.sprinkler.setActiveZone", + "objectId": 123456789, + "parameters": { + "index": 2 + } +} +``` + +`objectId` is the id of the device. The `index` parameter is required. + +#### indigo.thermostat +[indigo.thermostatdevice](../scripting/reference/device-subclasses/thermostat.md#commands-indigothermostat) command messages can be used for [indigo.thermostat](../scripting/reference/device-subclasses/thermostat.md#thermostatdevice) instances. + +**decrease cool setpoint** `[indigo.thermostat.decreaseCoolSetpoint(123456789, delta=2)](../scripting/reference/device-subclasses/thermostat.md#decrease-cool-setpoint)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.thermostat.decreaseCoolSetpoint", + "objectId": 123456789, + "parameters": { + "delta": 2 + } +} +``` + +`objectId` is the id of the device. The `value` parameter is optional. + +**decrease heat setpoint** `[indigo.thermostat.decreaseHeatSetpoint(123456789, delta=2)](../scripting/reference/device-subclasses/thermostat.md#decrease-heat-setpoint)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.thermostat.decreaseHeatSetpoint", + "objectId": 123456789, + "parameters": { + "delta": 2 + } +} +``` + +`objectId` is the id of the device. The `value` parameter is optional. + +**increase cool setpoint** `[indigo.thermostat.increaseCoolSetpoint(123456789, delta=2)](../scripting/reference/device-subclasses/thermostat.md#increase-cool-setpoint)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.thermostat.increaseCoolSetpoint", + "objectId": 123456789, + "parameters": { + "delta": 2 + } +} +``` + +`objectId` is the id of the device. The `value` parameter is optional. + +**increase heat setpoint** `[indigo.thermostat.increaseHeatSetpoint(123456789, delta=2)](../scripting/reference/device-subclasses/thermostat.md#increase-heat-setpoint)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.thermostat.increaseHeatSetpoint", + "objectId": 123456789, + "parameters": { + "delta": 2 + } +} +``` + +`objectId` is the id of the device. The `value` parameter is optional. + +**set cool setpoint** `[indigo.thermostat.setCoolSetpoint(123456789, value=76)](../scripting/reference/device-subclasses/thermostat.md#set-cool-setpoint)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.thermostat.setCoolSetpoint", + "objectId": 123456789, + "parameters": { + "value": 76 + } +} +``` + +`objectId` is the id of the device. The `value` parameters is required. + +**set fan mode** `[indigo.thermostat.setFanMode(123456789, value=indigo.kFanMode.AlwaysOn)](../scripting/reference/device-subclasses/thermostat.md#set-fan-mode)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.thermostat.setFanMode", + "objectId": 123456789, + "parameters": { + "value": "indigo.kFanMode.AlwaysOn" + } +} +``` + +`objectId` is the id of the device. The `value` parameter is required. + +**set heat setpoint** `[indigo.thermostat.setHeatSetpoint(123456789, value=76)](../scripting/reference/device-subclasses/thermostat.md#set-heat-setpoint)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.thermostat.setHeatSetpoint", + "objectId": 123456789, + "parameters": { + "value": 76 + } +} +``` + +`objectId` is the id of the device. The `value` parameter is required. + +**set hvac mode** `[indigo.thermostat.setHvacMode(123456789, value=indigo.kHvacMode.HeatCool)](../scripting/reference/device-subclasses/thermostat.md#set-hvac-mode)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.thermostat.setHvacMode", + "objectId": 123456789, + "parameters": { + "value": "indigo.kHvacMode.HeatCool" + } +} +``` + +`objectId` is the id of the device. The `value` parameter is required. + +### Variable Messaging +This section describes the messages that you'll use when implementing either the WebSocket or HTTP APIs when dealing with Indigo devices. They fall into two categories: device objects and device command messages. + +#### Variable Objects +You’ll receive full variable JSON objects which represent an Indigo variable instance. See the [Indigo Object Model](../scripting/iom-concepts.md) docs for device details. + +##### Example variable object +```json +{ + "class": "indigo.Variable", + "description": "", + "folderId": 0, + "folder": {}, + "globalProps": { + "com.indigodomo.indigoserver": {} + }, + "id": 345633244, + "name": "house_status", + "pluginProps": {}, + "readOnly": false, + "remoteDisplay": true, + "sharedProps": {}, + "value": "home" +} +``` + +Messages that contain variable objects generate those objects by first converting the variable to a python dictionary and then converting the python dictionary to JSON. See the [Generating a Dictionary for a variable](../scripting/reference/variables.md) section of the IOM Reference for details. + +!!! note + The `folder` element is only available in the HTTP API. Folders are handled differently in the WebSocket API. + +#### Variable Command Messages +The only action that can currently be performed on a variable is to update the value. The `id` key is optional, but may contain a user generated ID that will be logged and returned to help match up with message requests. Custom `id` values should be strings. + +**updateValue** `[indigo.variable.updateValue(123456789, value="Some string value")](../scripting/reference/variables.md#update-value)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.variable.updateValue", + "objectId": 123456789, + "parameters": { + "value": "Some string value" + } +} +``` + +`objectId` is the id of the variable. The `value` parameter is required and must be a string. Pass an empty string ("") to clear the variable value. + +### Action Group Messaging +#### Action Group Objects +You’ll receive full variable JSON objects which represent an Indigo action group instance. See the [Indigo Object Model](../scripting/iom-concepts.md) docs for action group details. + +##### Example action group object +```json +{ + "class": "indigo.ActionGroup", + "description": "", + "folderId": 532526508, + "folder": { + "class": "indigo.Folder", + "id": 532526508, + "name": "Mood Scenes", + "remoteDisplay": true + }, + "globalProps": { + "com.indigodomo.indigoserver": { + "speakDelayTime": "5", + "speakTextVariable": "speech_string" + } + }, + "id": 94914463, + "name": "Movie Night", + "pluginProps": {}, + "remoteDisplay": true, + "sharedProps": { + "speakDelayTime": "5", + "speakTextVariable": "speech_string" + } +} +``` + +!!! note + The `folder` element is only available in the HTTP API. Folders are handled differently in the WebSocket API. + +#### Action Group Command Messages +There is only a single action group command, and that's to execute it. The `id` key is optional, but may contain a user generated ID that will be logged and returned to help match up with message requests. Custom `id` values should be strings. + +** execute ** `[indigo.actionGroup.execute(123456789)](../scripting/reference/action-groups.md#execute)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.actionGroup.execute", + "objectId": 123456789 +} +``` + +`objectId` is the id of the action group. + +### Schedule Messaging +#### Schedule Objects +You’ll receive full variable JSON objects which represent an Indigo schedule instance. See the [Indigo Object Model](../scripting/iom-concepts.md) docs for schedule details. + +```json +{ + "class": "indigo.Schedule", + "absoluteDate": null, + "absoluteDateTime": null, + "absoluteTime": null, + "autoDelete": false, + "configured": true, + "dateType": 0, + "description": "", + "enabled": false, + "folderId": 0, + "globalProps": {}, + "id": 12345678, + "name": "My Schedule Name", + "nextExecution": null, + "pluginProps": {}, + "randomizeBy": 0, + "remoteDisplay": true, + "sharedProps": {}, + "sunDelta": 0, + "suppressLogging": false, + "timeType": 3 +} +``` + +#### Schedule Command Messages +These are the available schedule commands. The `id` key is optional, but may contain a user generated ID that will be logged and returned to help match up with message requests. Custom `id` values should be strings. + +** execute ** `[indigo.schedule.execute(123456789)](../scripting/reference/schedules.md#execute)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.schedule.execute", + "objectId": 12345678, +} +``` + +`objectId` is the id of the schedule. + +** enable ** `[indigo.schedule.enable(123456789)](../scripting/reference/schedules.md#enable)` +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.schedule.enable", + "objectId": 12345678, + "parameters": {"value": false} +} +``` + +### Trigger Messaging +#### Trigger Objects +You’ll receive full variable JSON objects which represent an Indigo trigger instance. See the [Indigo Object Model](../scripting/iom-concepts.md) docs for trigger details. + +```json +{ + "class": "indigo.Trigger", + "configured": true, + "description": "A trigger description.", + "enabled": true, + "folderId": 12345678, + "globalProps": { + "com.indigodomo.indigoserver": {}, + "com.indigodomo.webserver": { + "httpMethod": "GET", + "postProcessing": "JSON", + "webhookId": "8aa4b5140c5c473ea170465239143839" + }, + "emptyDict": {} + }, + "id": 12345678, + "name": "My Trigger Name", + "pluginProps": {}, + "remoteDisplay": true, + "sharedProps": {}, + "suppressLogging": false +} +``` + +#### Trigger Command Messages +These are the available trigger commands. The `id` key is optional, but may contain a user generated ID that will be logged and returned to help match up with message requests. Custom `id` values should be strings. + +** execute ** `[indigo.trigger.execute(123456789)](../scripting/reference/triggers.md#execute)` + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.trigger.execute", + "objectId": 12345678, +} +``` + +`objectId` is the id of the trigger. + +** enable ** `[indigo.trigger.enable(123456789)](../scripting/reference/triggers.md#enable)` +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.trigger.enable", + "objectId": 12345678, + "parameters": {"value": false} +} +``` + +### Log Messages +We convert the event Indigo dictionary into a python dictionary in the `event_log_line_received` plugin method. + +#### Example Log Message +```json +{ + "message": "Stopping plugin \"Web Server 2025.1.0\" (pid 1020)", + "timeStamp": "2022-12-01T12:03:27.759000", + "typeStr": "Application", + "typeVal": 0 + "objectType": "indigo.LogEvent" +} +``` + +Note that `typeVal` will be one of the following values: + +```python +EVENT_TYPES = { + Application = 0, + Error = 1, + Error_Client = 2, + Warning = 3, + Warning_Client = 4, + Debug_Server = 5, + Debug_Client = 6, + Debug_Plugin = 7, + Custom = 8, +} +``` + +You can use the values above to help determine any kind of decoration you want to use when displaying or otherwise interpreting the log event. + +#### Log Command Message +In some situations, you might want to send a log message to the server and have that message appear in the Indigo Event Log. This is done using the *`indigo.server.log`* command namespace and the *`command`* API endpoint. Here is full python example with a log message payload: + +```python +import requests + +REFLECTOR = "MY_REFLECTOR_NAME" +API_KEY = "MY_API_KEY" +url = f"https://{REFLECTOR}.indigodomo.net/v2/api/command" +headers = {'Authorization': f'Bearer {API_KEY}'} + +message = { +"id": "optional-user-generated-id", +"messageText": "Some important log message goes here.", +"message": "indigo.server.log", +} + +response = requests.post(url, headers=headers, json=message) + +if response.status_code == 200: + device_list = response.json() + print(device_list) +else: + print(f"Failed to fetch data. Status code: {response.status_code} - {response.text}") +``` + +## Plugin Messaging +There are two plugin-based command messages in the HTTP API: `*plugin.restart*` and `*plugin.executeAction*`. + +### Plugin Command Messages +**restart plugin ** + +When the `*plugin.restart*` command message is sent to the HTTP API, Indigo will restart the target plugin **only if it is installed and enabled.** +```python +{ + "id": "some-optional-message-text", + "message": "plugin.restart", + "pluginId": "com.some.indigo.plugin" # the plugin's bundle identifier +} +``` + +** Execute Plugin Action ** + +When the `*plugin.executeAction*` command message is sent to the HTTP API, Indigo will attempt to fire the plugin action **as long as the plugin is installed and enabled.** Many plugin actions require additional properties to complete (such as plugin Action configuration settings) and those are included as `*props*`. The `*props*` payload must be valid JSON. +```python +{ + "id": "some-optional-message-text", + "message": "plugin.executeAction", + "pluginId": "com.some.indigo.plugin", # the plugin's bundle identifier + "actionId": "some_plugin_action", # the ID of the plugin action found in the plugin's Actions.xml file [string] + "deviceId": 12345678, # the device ID targeted by the plugin action (not all actions will require a device ID). [int] + "props": {"prop1": "foo", "prop2": "bar"}, # required if the plugin action requires props to fire the action [valid JSON] + "waitUntilDone": True # optional [True/False] +} +``` + +## Error Messaging +Error messages will be returned to API clients in the event that something didn’t go as planned. The structure of messages returned will depend on what went wrong and how the message was sent. **Note that folder messages are added to the feed by the server; there are no folder endpoints to manage folders from a client at this time.** + +A generic example message is provided in JSON format: + +### Generic Error Message +```json +{ + "error": "Some error description", + "id": "the id sent from the client in the message, or null if there wasn't one", + "validationErrors": { + "field1": "some error that occurred in the message with the key field1" + } +} +``` + +where the value of the JSON name (or key) `validationErrors` is a dictionary with a field name and a description of the error in that field that came from the client. For instance, if you pass a float `value` to the `indigo.variable.updateValue` message: + +#### Example API Call +```json +{ + "id": "a-random-id-for-this-message", + "message": "indigo.variable.updateValue", + "objectId": 123456789, + "parameters": { + "value": 1234.56 + } +} +``` + +You will receive the following error response: + +##### Resulting Error Message +```json +{ + "validationErrors": { + "value": "variable values must be strings" + }, + "error": "invalid command payload received, id: my-set-var-command", + "id": "a-random-id-for-this-message" +} +``` + +The `error` key is the indicator that the response is some kind of error. `validationErrors` is a dict which contains all the validation errors for the message, in this case the `value` that was passed in was not a string (it was the float `1234.56`). The possible keys in the error message reply `validationErrors` could be: + +1. `message`, +1. `objectId`, +1. `parameters`, and +1. `value` + +based on the `indigo.variable.updateValue` message format (we do **no validation** on the `id` value passed in, we pass it through, and if your message doesn’t contain one then the value will be `null`). Only the keys from your message that have errors will be returned. So in the example error above, only the `value` key had a validation error (because we passed in a float) so that was the only key returned. + +`id` is the ID you (may have) passed in when you sent the command message. + +#### Invalid JSON +One other type of error that you may receive would be if you POST a string (or something else, like XML, etc.) that’s not JSON. This will result in the following message return: + +##### Example Invalid JSON Message +```json +{ + "request_body": "this is not valid JSON", + "error": "invalid JSON" +} +``` + +We will return the entire request body since it isn’t valid JSON, and we don’t know what else to do with it. + +#### Web Server Warnings +Occasionally, you might see a warning from the Web Server written to the Indigo Event Log that might look something like this: + +*`Web Server Warning HTTP 400 error for request /v2/api/command/ from 123.45.678.90`* + +The codes are standard HTTP response codes and may help you to determine what happened. The most common codes you might encounter are: + +| Code | Condition | Description | +|------|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 200 | OK or Success | Indicates that the request has succeeded. | +| 400 | Bad Request | Indicates that the server cannot or will not process the request due to something that is perceived to be a client error (for example, malformed request syntax, invalid request message framing, or deceptive request routing) | +| 401 | Unauthorized | Indicates that the client request has not been completed because it lacks valid authentication credentials for the requested resource. | +| 404 | Not Found | Indicates that the server cannot find the requested resource. | +| 500 | Internal Server Error | Indicates that the server encountered an unexpected condition that prevented it from fulfilling the request. | + + +You might see another code from time to time, and a complete list of codes and their meanings can be found on the [Mozilla web docs](https://developer.mozilla.org/en-US/docs/Web/HTTP) site under "HTTP response status codes". + +#### Other Errors and Warnings +From time to time, you might see other errors and warnings as you use the APIs. These messages can appear in the Indigo Events Log, the Web Server plugin log, and in your client's console. Sometimes, these messages can be somewhat generic (it's not possible to catch every conceivable error) so it can be helpful to know where to look for answers. + +A likely place where errors can creep into your API scripting involves the payloads that are sent to and from the Indigo Web Server. For example, JSON payloads may be converted to XML behind the scenes, so bear in mind the XML conventions found in the [Plugin Developer's Guide](../plugin-dev/guide.md#indigo-plugin-xml-conventions). diff --git a/reference/canonical/api/rest-migration.md b/reference/canonical/api/rest-migration.md new file mode 100644 index 0000000..d530dd8 --- /dev/null +++ b/reference/canonical/api/rest-migration.md @@ -0,0 +1,304 @@ + + +# REST API Conversion Examples + +!!! abstract "In this guide" + This page provides side-by-side examples for converting legacy REST API calls to the newer HTTP API format. + Familiarity with [HTTP API authentication](http.md#authentication) and [JSON message formats](messages.md) is + recommended before working through the examples. + +If you're using the old REST API (which has been deprecated), here are some examples of how you might convert your REST usages to the HTTP API. + +The first thing you'll want to understand is how to use [authentication with the HTTP API](http.md#authentication). The examples below use both headers and query args for authentication. You can use whichever works for you. You should replace `YOUR-API-KEY` with [your actual API Key](https://www.indigodomo.com/account/authorizations). + +Second, all replies to the API will be [JSON messages](messages.md). + +In the following examples, we'll be using a mix of authentication headers and the API Key as a query arg. You can use either. We mark the examples **REST** (old REST API) and **HTTP** (newer HTTP API). + +## Device Access { .ref-head-no-code } + +### Getting Devices { .ref-head-no-code } + +#### get device list { .ca } + +**REST** +```bash +http://username:password@127.0.0.1:8176/devices.json +``` + +**HTTP** +```bash +http://127.0.0.1:8176/v2/api/indigo.devices?api-key=YOUR-API-KEY +``` + +This will return a JSON list of [Device Objects](messages.md#device-objects). + +#### get single device { .ca } + +**REST** +```bash +http://username:password@127.0.0.1:8176/devices/office-lamp.json +``` + +**HTTP** +```bash +http://127.0.0.1:8176/v2/api/indigo.devices/123456789?api-key=YOUR-API-KEY +``` + +Insert the ID of the `office-lamp` device rather than the name. Note you can quickly copy the device ID to the clipboard by right-clicking on the device in Indigo app's main window and choosing the `Copy ID` context menu. This will return a single [Device Object](messages.md#device-objects). + +### Device Commands { .ref-head-no-code } + +Sending commands to devices requires that you POST a JSON message to the `/v2/api/command` URL. + +#### set brightness { .ca } + +**REST** +```bash +curl -X PUT -u user:password --digest -d brightness=27 http://127.0.0.1:8176/devices/office-lamp +``` + +**HTTP** +```bash +curl -X POST -H "Authorization: Bearer YOUR-API-KEY" -d '{"message":"indigo.device.setBrightness","objectId":123456789,"parameters":{"value":27}}' http://127.0.0.1:8176/v2/api/command +``` + +Insert the ID of the `office-lamp` device as the objectId. + +#### turn on/turn off { .ca } + +**REST** +```bash +curl -X PUT -u user:password --digest -d isOn=1 http://127.0.0.1:8176/devices/office-lamp +curl -X PUT -u user:password --digest -d isOn=0 http://127.0.0.1:8176/devices/office-lamp +``` + +**HTTP** +```bash +curl -X POST -H "Authorization: Bearer YOUR-API-KEY" -d '{"message":"indigo.device.turnOn","objectId":123456789}' http://127.0.0.1:8176/v2/api/command +curl -X POST -H "Authorization: Bearer YOUR-API-KEY" -d '{"message":"indigo.device.turnOff","objectId":123456789}' http://127.0.0.1:8176/v2/api/command +``` + +Insert the ID of the `office-lamp` device as the objectId. + +#### toggle { .ca } + +**REST** +```bash +curl -X PUT -u user:password --digest -d toggle=1 http://127.0.0.1:8176/devices/office-lamp +``` + +**HTTP** +```bash +curl -X POST -H "Authorization: Bearer YOUR-API-KEY" -d '{"message":"indigo.device.toggle","objectId":123456789}' http://127.0.0.1:8176/v2/api/command +``` + +Insert the ID of the `office-lamp` device as the objectId. + +#### change speed index (fan) { .ca } + +These examples will set device `office-ceiling-fan` to 0 (off), then set to 3 (high), then decrease back to 0 (off). + +**REST** +```bash +curl -X PUT -u user:password --digest -d speedIndex=0 http://127.0.0.1:8176/devices/office-ceiling-fan +curl -X PUT -u user:password --digest -d speedIndex=3 http://127.0.0.1:8176/devices/office-ceiling-fan +curl -X PUT -u user:password --digest -d speedIndex=dn http://127.0.0.1:8176/devices/office-ceiling-fan +curl -X PUT -u user:password --digest -d speedIndex=dn http://127.0.0.1:8176/devices/office-ceiling-fan +curl -X PUT -u user:password --digest -d speedIndex=dn http://127.0.0.1:8176/devices/office-ceiling-fan +``` + +**HTTP** +```bash +curl -X POST -H "Authorization: Bearer YOUR-API-KEY" -d '{"message":"indigo.speedcontrol.setSpeedIndex","objectId":123456789,"parameters":{"value":0}}' http://127.0.0.1:8176/v2/api/command +curl -X POST -H "Authorization: Bearer YOUR-API-KEY" -d '{"message":"indigo.speedcontrol.setSpeedIndex","objectId":123456789,"parameters":{"value":3}}' http://127.0.0.1:8176/v2/api/command +curl -X POST -H "Authorization: Bearer YOUR-API-KEY" -d '{"message":"indigo.speedcontrol.decreaseSpeedIndex","objectId":123456789}' http://127.0.0.1:8176/v2/api/command +curl -X POST -H "Authorization: Bearer YOUR-API-KEY" -d '{"message":"indigo.speedcontrol.decreaseSpeedIndex","objectId":123456789}' http://127.0.0.1:8176/v2/api/command +curl -X POST -H "Authorization: Bearer YOUR-API-KEY" -d '{"message":"indigo.speedcontrol.decreaseSpeedIndex","objectId":123456789}' http://127.0.0.1:8176/v2/api/command +``` + +Insert the ID of the `office-ceiling-fan` device as the objectId. + +#### change sprinkler zones { .ca } + +These examples will change device `irrmaster-pro` active sprinkler zone to 3 and all off. + +**REST** +```bash +curl -X PUT -u user:password --digest -d activeZone=3 http://127.0.0.1:8176/devices/irrmaster-pro +curl -X PUT -u user:password --digest -d activeZone=0 http://127.0.0.1:8176/devices/irrmaster-pro +``` + +**HTTP** +```bash +curl -X POST -H "Authorization: Bearer YOUR-API-KEY" -d '{"message":"indigo.sprinkler.setActiveZone","objectId":123456789,"parameters":{"index":3}}' http://127.0.0.1:8176/v2/api/command +curl -X POST -H "Authorization: Bearer YOUR-API-KEY" -d '{"message":"indigo.sprinkler.setActiveZone","objectId":123456789,"parameters":{"index":0}}' http://127.0.0.1:8176/v2/api/command +``` + +Insert the ID of the `irrmaster-pro` device as the objectId. + +#### set thermostat setpoints { .ca } + +These examples will set device `thermostat`'s heat and cool setpoints + +**REST** +```bash +curl -X PUT -u user:password --digest -d setpointCool=76 http://127.0.0.1:8176/devices/thermostat +curl -X PUT -u user:password --digest -d setpointHeat=70 http://127.0.0.1:8176/devices/thermostat +``` + +**Increase Heat Setpoint** + +```bash +curl -X PUT -u user:password --digest -d setpointHeat=up http://127.0.0.1:8176/devices/thermostat +``` + +**HTTP** +```bash +curl -X POST -H "Authorization: Bearer YOUR-API-KEY" -d '{"message":"indigo.thermostat.setCoolSetpoint","objectId":123456789,"parameters":{"value":76}}' http://127.0.0.1:8176/v2/api/command +curl -X POST -H "Authorization: Bearer YOUR-API-KEY" -d '{"message":"indigo.thermostat.setHeatSetpoint","objectId":123456789,"parameters":{"value":70}}' http://127.0.0.1:8176/v2/api/command +``` + +**Increase Heat Setpoint** + +```bash +curl -X POST -H "Authorization: Bearer YOUR-API-KEY" -d '{"message":"indigo.thermostat.increaseHeatSetpoint","objectId":123456789}' http://127.0.0.1:8176/v2/api/command +``` + +Insert the ID of the `thermostat` device as the objectId. + +#### set thermostat mode { .ca } + +These examples will set device `thermostat`'s mode to "cool on" and "auto on" + +**REST** +```bash +curl -X PUT -u user:password --digest -d hvacCurrentMode="cool on" http://127.0.0.1:8176/devices/thermostat +curl -X PUT -u user:password --digest -d hvacCurrentMode="auto on" http://127.0.0.1:8176/devices/thermostat +``` + +**HTTP** +```bash +curl -X POST -H "Authorization: Bearer YOUR-API-KEY" -d '{"message":"indigo.thermostat.setHvacMode","objectId":123456789,"parameters":{"value":"indigo.kHvacMode.Cool"}}' http://127.0.0.1:8176/v2/api/command +curl -X POST -H "Authorization: Bearer YOUR-API-KEY" -d '{"message":"indigo.thermostat.setHvacMode","objectId":123456789,"parameters":{"value":"indigo.kHvacMode.HeatCool"}}' http://127.0.0.1:8176/v2/api/command +``` + +Insert the ID of the `thermostat` device as the objectId. + +#### set thermostat fan mode { .ca } + +These examples will set device `thermostat`'s fan mode to "always on" and "auto on" + +**REST** +```bash +curl -X PUT -u user:password --digest -d hvacFanMode="always on" http://127.0.0.1:8176/devices/thermostat +curl -X PUT -u user:password --digest -d hvacFanMode="auto on" http://127.0.0.1:8176/devices/thermostat +``` + +**HTTP** +```bash +curl -X POST -H "Authorization: Bearer YOUR-API-KEY" -d '{"message":"indigo.thermostat.setFanMode","objectId":123456789,"parameters":{"value":"indigo.kFanMode.AlwaysOn"}}' http://127.0.0.1:8176/v2/api/command +curl -X POST -H "Authorization: Bearer YOUR-API-KEY" -d '{"message":"indigo.thermostat.setFanMode","objectId":123456789,"parameters":{"value":"indigo.kFanMode.Auto"}}' http://127.0.0.1:8176/v2/api/command +``` + +Insert the ID of the `thermostat` device as the objectId. + +## Variable Access { .ref-head-no-code } + +### Getting Variables { .ref-head-no-code } + +#### get variable list { .ca } + +**REST** +```bash +http://username:password@127.0.0.1:8176/variables.json +``` + +**HTTP** +```bash +http://127.0.0.1:8176/v2/api/indigo.variables?api-key=YOUR-API-KEY +``` + +This will return a JSON list of [Variable Objects](messages.md#variable-objects). + +#### get single variable { .ca } + +**REST** +```bash +http://username:password@127.0.0.1:8176/variables/sprinklerDurationMultiplier.json +``` + +**HTTP** +```bash +http://127.0.0.1:8176/v2/api/indigo.variables/123456789?api-key=YOUR-API-KEY +``` + +Insert the ID of the `sprinklerDurationMultiplier` variable rather than the name. This will return a single [Variable Object](messages.md#variable-objects). + +### Variable Commands { .ref-head-no-code } + +Sending commands to the server requires that you POST a JSON message to the `/v2/api/command` URL. + +#### update variable value { .ca } + +**REST** +```bash +curl -X PUT -u user:password --digest -d value=1.23 http://127.0.0.1:8176/variables/sprinklerDurationMultiplier +``` + +**HTTP** +```bash +curl -X POST -H "Authorization: Bearer YOUR-API-KEY" -d '{"message":"indigo.variable.updateValue","objectId":123456789,"parameters":{"value":"1.23"}}' http://127.0.0.1:8176/v2/api/command +``` + +Insert the ID of the `sprinklerDurationMultiplier` variable as the objectId. **Note**: variable values are always strings to make sure to enclose the value in quotes. + +## Action Group Access { .ref-head-no-code } + +### Getting Action Groups { .ref-head-no-code } + +#### get action group list { .ca } + +**REST** +```bash +http://username:password@127.0.0.1:8176/actions.json +``` + +**HTTP** +```bash +http://127.0.0.1:8176/v2/api/indigo.actionGroups?api-key=YOUR-API-KEY +``` + +This will return a JSON list of [Action Group Objects](messages.md#action-group-objects). + +#### get single action group { .ca } + +**REST** +```bash +http://username:password@127.0.0.1:8176/actions/party%20scene.json +``` + +**HTTP** +```bash +http://127.0.0.1:8176/v2/api/indigo.actionGroups/123456789?api-key=YOUR-API-KEY +``` + +Insert the ID of the `party scene` action group rather than the name. This will return a single [Action Group Object](messages.md#action-group-objects). + +### Action Group Commands { .ref-head-no-code } + +Sending commands to the server requires that you POST a JSON message to the `/v2/api/command` URL. + +#### execute action group { .ca } + +**REST** +```bash +curl -X EXECUTE -u user:password --digest -d value=1.23 http://127.0.0.1:8176/actions/party%20scene +``` + +**HTTP** +```bash +curl -X POST -H "Authorization: Bearer YOUR-API-KEY" -d '{"message":"indigo.actionGroup.execute","objectId":123456789}' http://127.0.0.1:8176/v2/api/command +``` + +Insert the ID of the `party scene` action group as the objectId. diff --git a/reference/canonical/api/webhooks.md b/reference/canonical/api/webhooks.md new file mode 100644 index 0000000..802dc01 --- /dev/null +++ b/reference/canonical/api/webhooks.md @@ -0,0 +1,301 @@ + + +# Webhooks + +!!! abstract "In this guide" + How to configure Indigo to receive webhooks from external services using the Webhook trigger type. Covers the three webhook data formats (JSON POST, FORM POST, GET), the URL pattern for calling a webhook, API key authentication, and how received data is passed through to action scripts via the event data mechanism. + +Another feature that we started (before going down the event data path) is to make it easier for users to capture webhooks. As you may know, these are quite popular with internet services, be it doorbells, network equipment, location apps, etc. These services can sometimes be used with the HTTP API, but there are limitations to that which cause pain. + +Without the event data mechanism, webhooks might still be somewhat useful, but to make them really useful we needed to pass the data through to actions. That was the final motivation to create the mechanism discussed above. + +The new **Webhook** trigger event type (under the **Web Server** trigger section) allows the user to specify an ID (we prepopulate it with a random UUID but the user can override that if they like) and how the data will come in. There are 3 options for receiving webhook events: + +1. JSON POST - this type of webhook will accept a POST and interpret the payload as JSON. The `event_data` dict will contain two keys: `http-post-content` which will be `"JSON"` and `data` which will be the full JSON payload as a dict or list, whatever the caller passes in. +1. FORM POST - this type of webhook will accept a POST with optional form data, which will be converted into a dict of name value pairs and passed through as `data`. The `http-post-content` key will be `"FORM"` in this case. +1. GET - this type of webhook will accept a GET and will pass through any query arguments as the `data` element. + +Calling a webhook is a very straight-forward URL pattern: + +```text +https://myreflector.indigodomo.net/webhook/IDFROMCONFIG +``` + +As with the other APIs, the caller can either send a `Authorization` header of `Bearer APIKEYHERE` or can pass it as a query arg to the url: + +```text +https://myreflector.indigodomo.net/webhook/IDFROMCONFIG?api-key=APIKEYHERE +``` + +We believe this covers the vast majority of webhook options that are in use at the moment (let us know if you have an example of one which wouldn't work correctly and we'll look into it). + +## Broadcast +Webhooks will also use the broadcast function to send out the entire `event_data` dict to any plugins that subscribe to events from the web server: + +```python +indigo.server.broadcastToSubscribers("webhook-received", message) +``` + +Subscribers can: + +```python +iws_id = "com.indigodomo.webserver" +indigo.server.subscribeToBroadcast(iws_id, "webhook-received", "webhook_received_method") +``` + +## Locative Webhook Examples +The [Locative geolocation app](https://www.locative.app) provides location notifications on iOS. It has very good support for webhook events and therefore is a great example for our webhook implementation. + +Here's a rough summary of how locative works: users can define a geofence (or beacon) as a location. Here's an example of two locations we'll use in this example: + +![](../images/locative-main.jpeg){ width=300 } + +We've defined (completely made up) two geofences, one that represents a home and another representing a work location. Whenever a location is entered or exited, Locative can send webhook requests that Indigo can catch and act upon: + +![](../images/locative-home.jpeg){ width=300 } + +Here's what each of those fields represent: + +- LOCATION ID - this is an ID that will be sent to the webhooks endpoint whenever the webhooks are sent so if so desired the user can use a single endpoint to handle all locations. We describe this approach in the advanced below. +- WEBHOOK ON ARRIVAL - this is the URL that will be used when the geofence is entered. You also specify the request type (Indigo handles both POST and GET requests). +- WEBHOOK ON DEPARTURE - this is the URL that will be used when the geofence is exited. You also specify the request type (Indigo handles both POST and GET requests). +- WEBHOOK AUTHENTICATION - you will be using an API key specified on the URL line, so this setting should always be Disabled. +- CUSTOM REQUEST-DESIGN - Locative allows you to specify which data will be sent to the webhook. We'll describe below two different payloads (one JSON and the other FORM POST) that we defined for this though you can create your own. + +### Payloads +As mentioned above, we define two payloads. The first payload uses JSON format while the second is a FORM POST which will also double as GET query string parameters. First, we define the "JSON" payload like this: + +![](../images/locative-json-payload.jpeg){ width=300 } + +The Content Type is set to application/json and for completeness we're including all the fields that Locative supports (as of this writing). The resulting JSON looks something like this: + +```json +{ + "device": "A-UUID-THAT-REPRESENTS-YOUR-IOS-DEVICE", + "device_model": "iPhone15,2", + "device_type": "iOS", + "id": "home", + "latitude": 29.12345, + "longitude": -96.12345, + "timestamp": 1759348496.39196, + "trigger": "enter" +} +``` + +The other payload is "FORM POST", which is similar but instead of JSON we specify it as //`application/x-www-form-urlencoded; charset=utf-8`//: + +![](../images/locative-form-get-payload.jpeg){ width=300 } + +We also specify all the available fields here like with the JSON payload. One more setting in the Locative app (select the Settings tab at the bottom) - you should turn on the Send Payload as URL-Query when using GET-Method setting so that if you select GET you will get all the values as a query string which Indigo will also make available. + +### Update a Variable When Entering / Leaving Home +For our first example, we will create a simple JSON webhook that updates a variable when entering or leaving home. In other words, we're going to configure a webhook for a geofence that we will configure as "home-json". This webhook will catch the webhook enter and exit messages from Locative and update a variable named home. The value will come directly from the JSON payload described earlier. + +#### Indigo configuration steps +On the Indigo side, we're going to create a variable called home and then add a webhook trigger that will catch the webhook from Locative and change the variable value to the value received in the trigger field. Here are the steps: + +1. Create a variable and name it "home" (or whatever you want) +1. Configure a new Webhook Trigger action (select Web Server Event from Type and Webhook from Event). + +![](../images/locative-home-json-trigger.jpeg){ width=500 } + +The Webhook ID field specifies the name of the webhook that you'll use on your URL. The Method is POST and the processing is JSON. Next, switch to the Actions tab and select *`Variable Actions->Insert Event Data into Variable`* from the Type popup, which will bring up the config dialog for that action: + +![](../images/locative-home-json-trigger-actions.jpeg){ width=500 } + +Select the Variable you created in step 1 above, then enter *`data.trigger`* in the Path specifier field. A brief reminder: this is what the event_data contains from a webhook trigger event: + +```text +event_data = { + 'data': { + 'device': 'YOUR-DEVICES-UUID', + 'device_model': 'iPhone15,2', + 'device_type': 'iOS', + 'id': 'home', + 'latitude': 30.25458515413769, + 'longitude': -97.76605401300928, + 'timestamp': 1759348496.39196, + 'trigger': 'enter' + }, + 'event-indigo-id': 264490832, + 'event-plugin-event-id': 'simpleWebhook', + 'event-plugin-id': 'com.indigodomo.webserver', + 'event-plugin-name': 'Web Server', + 'event-type': 'PluginEventTrigger', + 'http-method': 'POST', + 'http-post-content': 'JSON', + 'request-url': 'http://localhost:8176/webhook/home-json', + 'source': 'python', + 'status-code': 200, + 'timestamp': '2025-10-01T14:54:56', + 'webhook-id': 'home-json' +} +``` + +Note that the JSON data from Locative is stored in the *`data`* part of the event data dictionary, and the trigger event is in the *`trigger`* field, so we specify that as *`data.trigger`*. + +#### Locative configuration steps +Create a new Geofence that represents your home (or whatever). Here's how the webhooks are configured in Locative: + +![](../images/locative-home.jpeg){ width=300 } + +You set the URL in both arrival and departure to this: + +```text +https://REFLECTORNAMEHERE.indigodomo.net/webhook/home-json?api-key=APIKEYHERE +``` + +Change to your reflector name and add an API key at the end. Note the webhook ID that you set in the trigger definition is used as the last part of the path specifier (`*home-json*` in this case). Then select POST for both, then JSON (or whatever you named your definition). + +#### Test your setup +We recommend enabling debug logging for the webserver (Indigo XXXX.Y->Advanced Web Server Settings... then click on the Debug Logging option). This will show you the traffic between Locative and Indigo. Then in the Locative app's Locations list, long press on your home location and select *`Trigger "Enter" Event`* and watch your Event Log window in Indigo. + +You should see something like this: + +```text +Web Server Debug caught webhook with ID: 'home-json' + Web Server Debug received webhook 'home-json-locative' with JSON data: {'longitude': XXXXX, 'device_type': 'iOS', 'id': 'home', 'latitude': XXXXXX, 'timestamp': 1759522339.223068, 'device': 'YOUR-DEVICES-UUID', 'device_model': 'iPhone15,2', 'trigger': 'enter'} + Web Server Debug webhook 'home-json-locative' message sent to subscribers: +{'webhook-id': 'home-json', 'status-code': 200, 'http-method': 'POST', 'request-url': 'http://localhost:8176/webhook/home-json', 'http-post-content': 'JSON', 'data': {'longitude': XXXXXX, 'device_type': 'iOS', 'id': 'home', 'latitude': XXXXXX, 'timestamp': 1759522339.223068, 'device': 'YOUR-DEVICES-UUID', 'device_model': 'iPhone15,2', 'trigger': 'enter'}} +``` + +Hopefully you'll see this and you'll see the value of the *`home`* Indigo variable set to *`enter`*. You can test the exit as well to make sure that your variable is getting updated correctly. You can disable debug logging once you've confirmed it's working correctly. + +If you see an error about the webhook being undefined, or you see a Not Found (404) error, you may need to restart Indigo. + +#### Summary +In summary, we created a geofence in Locative that represents your home and configured it to send a JSON webhook to Indigo, where we catch the webhook and update a variable with the *`trigger`* value, either *`enter`* or *`exit`* which represents your iOS device arriving home or leaving. + +### Update a Variable When Entering / Leaving Work +For our second example, simple FORM POST and GET webhooks that update a variable when entering or leaving work + +Next, we are going to create a similar solution for work, but instead of using JSON and a single webhook to catch both enter and exit calls, we'll create two separate webhooks. One will catch a FORM POST, and the other will catch a GET. Under most circumstances, you wouldn't need to do this, but for the purposes of this example we wanted to show +you how to do both of these. + +Using the first example as a starting point, we'll first create the Indigo elements. + +1. Create a "work" variable. +1. Create a webhook trigger we'll call *`work-form-post`* which will have a webhook ID of *`work-form`* and which uses *`POST`* and *`HTTP`* Form for method and processing respectively. For the action we will configure another insert event data action for your new *`work`* variable, but this time you'll use the following path specifier: *`data.trigger[0]`*. Now, you're probably wondering why the *`[0]`* is appended to the end: this is because FORM POST (as well as GET query) args are always a list because you can have multiple duplicate field names with different values in those methods. But we know that Locative only sends a single value so we just get the one at the 0 index (the first one, Python lists are 0-based, so the first element is 0, second is 1, etc.) +1. Just like above we'll create a webhook trigger we'll call *`work-get`* which will have a webhook ID of *`work-form`* and which uses *`GET`* for the method. Configure an identical action as above to insert the value into the variable you created in step 1. + +Next, in Locative, create a location just as before (except for your work). For this one we'll have two different URLs and methods defined for the Arrival and Departure webhooks. + +For Arrival, you'll want to set it to POST and use the following URL (with appropriate substitutions): + +```text +https://REFLECTORNAMEHERE.indigodomo.net/webhook/work-form?api-key=APIKEYHERE +``` + +For Departure, select GET and use the following URL (with appropriate substitutions): + +```text +https://REFLECTORNAMEHERE.indigodomo.net/webhook/work-get?api-key=APIKEYHERE +``` + +Select your FORM POST custom design for the custom request field as illustrated above. + +Test and debug your solution to make sure that your work location is updating correctly when entered/exited. + +#### Summary { #update-a-variable-when-entering-leaving-work-summary } +For the work location, we used two different webhooks (a GET and a FORM POST) to catch data from Locative. Both webhooks update the same variable with exit/enter just like the first example. We artificially created these scenarios to illustrate how GET and FORM POST works in case you have a data provider that uses those methods. + +### Advanced Scripting Example +As you can see from the two examples above, Indigo is quite flexible when it comes to configuring webhooks and using the data provided by them. But there are other ways to customize your solution to fit your needs. Let's say what you really want Indigo to know is when you (for this example, we'll call you Joe) are at *`home`*, at *`work`*, or some other place (which we'll just call *`away`*). For this example, we're just going to have a single webhook which will catch all the webhooks and have a script that uses the data from the webhook to determine where you are. We'll then change the value of a variable to *`home`*, *`work`*, or *`away`* + +First, let's create the Indigo webhook. Here's the trigger definition: + +![](../images/locative-joe-trigger.jpeg){ width=500 } + +It's a straight-forward JSON POST webhook named *`joe-location`*. The action is where the real work is. Select the Server Actions->Script and File Actions->Execute Script, with the following script: + +```python +# Get the location ID and the triggering event from the +# locative webhook data +location_id = event_data["data"]["id"] +location_trigger = event_data["data"]["trigger"] +# The default value will be "away" +joe_location = "away" +if location_id == "joe-home": + if location_trigger == "enter": + # The location was home and enter, so joe is + # home + joe_location = "home" +else: + # We only have 2 locations that execute this webhook + # so it must be the work location + if location_trigger == "enter": + # The location was joe-work and enter, so joe is + # at work + joe_location = "work" +# Update the variable with one of ["home", "work", "away"] +# Substitute the ID of the variable that will hold your location +indigo.variable.updateValue(IDOFVARIABLE, value=joe_location) +``` + +In Locative, create two different locations, one with an id of "joe-home" and another with an id of "joe-work". For each, configure all the webhooks as POST JSON using the following url: + +```text +https://REFLECTORNAMEHERE.indigodomo.net/webhook/joe-location?api-key=APIKEYHERE +``` + +All webhooks for both locations will go to the same trigger in Indigo and the script will process it based on the data present in the webhook JSON. Give it a try and debug as necessary. + +#### Summary { #substitute-the-id-of-the-variable-that-will-hold-your-location-summary } +We created a single Indigo webhook that catches a JSON payload and in an embedded script it uses data from the payloads to determine where Joe is. We also created two locations in Locative that represent home and work, and configured the webhooks for both locations to hit our single webhook trigger and pass through their data. + +## Synology NAS Webhook Example +Synology DSM supports webhook events which work great with Indigo's Webhook Events. This document describes how to set up a Synology DSM webhook event (sender) and Indigo webhook event (receiver). It assumes a certain level of familiarity with both Synology DSM Notifications, Indigo Webhook events and Indigo authentication. There are many configurations that are possible and this article describes a very basic example. + +### Setting Up Indigo Webhooks + +1. In Indigo, create a new Trigger. +1. Under Type, select Web Server Event. +1. Under Event, select Webhook. +1. Under the Configure Webhook dialog, copy the Webhook ID and paste it somewhere safe. If you want, you can change the ID to something else -- it must be unique. +1. Under Webhook Method, ensure it's set to POST. +1. Under Processing, ensure it's set to JSON. +1. Set up the Action for your Indigo Trigger (send an email, write to the event log, etc.) +1. Save your Trigger. + +![Webhook Trigger](../images/synology_nas_webhook_trigger.png){ width=600 } + +![Webhook Trigger Config](../images/synology_nas_webhook_config_post.png){ width=600 } + +![Webhook Action](../images/synology_nas_webhook_action.png){ width=600 } + +### Setting Up Synology DSM Webhooks + +1. Log into the Synology NAS administrative dashboard. +1. Open the Control Panel app. +1. Select Notification. +1. Select Webhooks +1. Click the "Add" button to add a new webhook. +1. Choose a Notification Rule, enter a Provider Name, and Subject +1. Enter the configured webhook URL which should have a form similar to this: `https://MY_REFLECTOR_NAME.indigodomo.net/webhook/MY_INDIGO_WEBHOOK_ID?api-key=MY_INDIGO_API_KEYwhere you replace `MY_REFLECTOR_NAME`, `MY_INDIGO_WEBHOOK_ID`, and `MY_INDIGO_API_KEY with values from your Indigo setup. Use the webhook ID you got from the Indigo Webhook Trigger. For the API key, you can also use a local secret. +1. Click Next (or switch to the HTTP Request tab). +1. Adjust the request as needed -- can leave at defaults for your first webhook. +1. Save your webhook. + +![Webhook Provider](../images/synology_nas_webhook_provider.png){ width=600 } + +![Webhook Request](../images/synology_nas_webhook_request.png){ width=600 } + +### Test Your Webhook + +1. Select your new Synology web hook if it's not already selected. +1. Press the "Send Test Message" button. +1. Confirm the webhook worked (depends on the Action options you chose above.) + +An example event log success message: + +```bash +Trigger Synology Webhook Event +Email+ sending email 'Synology Webhook Event' to 'me@me.com' using Email+ SMTP Server +``` + +And the notification! + +![Webhook Email](../images/synology_nas_webhook_email.png){ width=600 } + +### Customize Your Webhook Event +Now that you've confirmed that your Indigo Webhook event is working properly, you can customize it to make it even more useful. Because we've selected a POST event, the Synology DSM webhook will pass DSM event data to Indigo. You can use that payload to take different actions. When an Indigo POST web hook is triggered, it includes the payload that was POSTed. You can access this payload with a Python script. The payload is sent as JSON, so you can access the DSM event information by working with the `event_data` payload that's sent to the trigger. See the [Webhooks Page](webhooks.md) for more information. diff --git a/reference/canonical/api/websocket.md b/reference/canonical/api/websocket.md new file mode 100644 index 0000000..590ea19 --- /dev/null +++ b/reference/canonical/api/websocket.md @@ -0,0 +1,745 @@ + + +# WebSocket API +The Indigo WebSocket API is a persistent connection, meaning that your app/integration will open the connection and keep it open until your app/integration quits. If you are looking for a transactional API, try our [HTTP API](http.md). WebSockets are bidirectional TCP connections, which clients can read from and write to – much like you can a socket or serial connection. This initial version of this API is meant to support functionality like that provided by the Indigo Touch and Domotics Pat clients and does not provide all the necessary bits to support a full configuration clients like the Mac Client. In other words, there are things that the Indigo client can do that are not supported in the WebSockets API at this time. + +There are 7 WebSocket feeds that are available: + +1. `/v2/api/ws/device-feed` - WebSocket to do all communication about devices +1. `/v2/api/ws/variable-feed` - WebSocket to do all communication about variables +1. `/v2/api/ws/action-feed` - WebSocket to do all communication about action groups +1. `/v2/api/ws/schedule-feed` - WebSocket to do all communication about schedules +1. `/v2/api/ws/trigger-feed` - WebSocket to do all communication about triggers +1. `/v2/api/ws/page-feed` - WebSocket to do all communication about control pages +1. `/v2/api/ws/log-feed` - WebSocket to do all communication about logs + +Each feed (except log feed) sends the following server messages: + +- **add** (when a new Indigo object is added) +- **patch** (when an Indigo object changes - you would apply the patch to an existing object) +- **delete** (when the Indigo object is deleted) +- **refresh** (when you request a fresh copy of a device or the entire list of devices) + +## WebSocket Lifecycle +The flow of how you will interact with any of the WebSockets above will generally be: + +1. Setup: + - Open the websocket connection, and + - Send a refresh message to obtain all of the existing instances of that Indigo type in your database. +1. Routine operation (asynchronously processing for the lifetime of your app/integration): + - Read incoming messages from the server (add/patch/delete/refresh) and handle those as appropriate, and + - Write messages to the server to instruct the server to do something (commands like statusRequest, turnOn, or refresh). +1. Close the connection when your app/integration quits. + +The **log-feed** is different in that it will only send **add** messages with the appropriate log event object defined below. + +## Authentication +WebSocket API requests must be authenticated using an **API Key**. You can manage API Keys in the [Authorizations section of your Indigo Account](https://www.indigodomo.com/account/authorizations). Alternatively, you can create "[local secrets](../user/remote-access/web-server.md#authentication)" -- a special kind of API key that doesn't pass through the Indigo reflector -- or a combination of both types of keys. Using keys instead of your Indigo Server username/password has several advantages: you can, at any time, revoke an API key, and it will immediately cause anything using it to fail. This will not affect anything else using a username/password or another API key, so your server protections against intrusions are much more granular. Also, if someone does manage to get your API Key, they can control devices, but they cannot modify your database (add/delete devices, etc.) - that is reserved for Indigo clients using the username/password. + +The best way to use an API Key is to include it in an **Authorization** header on your HTTP request. If you are using a system which does not allow you to set headers for your HTTP request, you can include the API Key as a query argument with the URL: + +`*wss://YOUR-REFLECTOR-NAME.indigodomo.net/v2/api/ws/device-feed?api-key=YOUR-API-KEY*` + +Note the protocol: `wss`. WebSockets actually begin their life as HTTP/HTTPS connections, which the WebSocket client then requests the server to upgrade to a WebSocket. In this respect, you can think of WSS as HTTPS (protected) and WS as HTTP (unprotected). When using WSS, such as when you are using your Indigo Reflector, then your API Key (in both instances) is protected by the TLS security used by the HTTPS protocol. You may use the API locally (or thorough your own router port forwarding), but those connections will be WS and **will not be secure**. It is highly recommended that you always use your Indigo Reflector because it provides a very simple and **secure** solution for accessing your system. + +!!! warning + In order to use API Key authentication, you MUST have enabled *`Enable OAuth and API Key authentication`* in the Indigo ["Start Local Server"](../user/getting-started/installation.md#starting-indigo-server) dialog box. Don't share your API keys with anyone who is not authorized to use them — especially in posts to the Indigo user forums. Note that disabling this feature disables both API Keys and secrets. + +## Examples +The WebSocket and HTTP APIs are designed to be familiar to those that have been using the legacy RESTful API. However, anyone with knowledge of Python, JavaScript or similar languages should be able to pick up the structure of the new APIs very quickly. To help those making the transition as well as those learning to use API calls for the first time, we've laid out several examples to show how API calls are made, as well as all the current API hooks available. + +Here's a simple Python script to open the device-feed and print out any messages it receives (you'll need to `pip3 install websockets` before running it if it isn't already installed - Indigo installs it on the Indigo Server Mac so you won't need to install there). + +### Python Receiver Example +This example opens a websocket connection and prints all messages received to the console. It runs until you stop it. +```python +import asyncio +import websockets +import json + +API_KEY = 'YOUR-API-KEY' + +async def receiver(): + try: + headers = {"Authorization": f"Bearer {API_KEY}"} + # Note the update to Python 3.13 changed the `websockets.connect()` attribute `additional_headers` to + `additional_headers`. + async with websockets.connect("ws://localhost:8176/v2/api/ws/device-feed", additional_headers=headers) as websocket: + while True: + message = await websocket.recv() + print(json.dumps(json.loads(message), indent=2)) + except Exception as exc: + print(f"Exception:\n{exc}") + +asyncio.run(receiver()) +``` + +### Python Receiver/Sender Example +This example opens a websocket connection and shows how messages can be received and sent from the same websocket connection. It runs until the loop counters run out. + +```python +import asyncio +import json +from websockets import connect + +API_KEY = 'YOUR_API_KEY' # YOUR API KEY HERE +DEVICE_ID = 12345678 # YOUR DEVICE ID HERE +HEADERS = {"Authorization": f"Bearer {API_KEY}"} +URI = "ws://127.0.0.1:8176/v2/api/ws/device-feed" # wss:// if using the reflector + +async def receiver(ws): + try: + print("Starting receiver task") + # Ask for a refresh of device with ID 12345678 + refresh_message = { + "id": "initial-device-refresh", + "message": "refresh", + "objectType": "indigo.Device", + "objectId": DEVICE_ID + } + await ws.send(json.dumps(refresh_message)) + counter = 0 + device = {} + + while counter < 20: + # just keep looping waiting for a message to come + log_string = "ignoring message" + message_json = await ws.recv() + message = json.loads(message_json) + message_type = message['message'] + + if message_type == "refresh": + # This is the response to the refresh message we sent above. It gets us a full copy of the device. + device = message["objectDict"] + log_string = f"receiver: device refresh message: '{device.get('name', 'unknown')}'" + + elif message_type == "patch": + # There's been a change, so confirm it's the device we want then log the change + if message["objectId"] == device["id"]: + log_string = f"'receiver: device patch message: {device.get('name', 'unknown')}' update: \n{message_json}" + + print(f"receiver: loop count: {counter}") + print(f"receiver: {log_string}") + counter += 1 + + except Exception as exc: + print(f"Exception:\n{exc}") + +async def sender(ws, count): + try: + print("Starting sender task") + message = { + "id": "initial-device-refresh", + "message": "refresh", + "objectType": "indigo.Device", + "objectId": DEVICE_ID + } + + for count in range(10): + print(f"sender: loop count: {count}") + msg = json.dumps(message) + print(f"sender: sending message: {msg}") + await ws.send(json.dumps(msg)) + await asyncio.sleep(5) + + except Exception as exc: + print(f"Exception:\n{exc}") + +async def main(): + try: + # Note the update to Python 3.13 changed the `websockets.connect()` attribute `additional_headers` to + `additional_headers`. + async with connect(URI, additional_headers=HEADERS) as websocket: + await asyncio.gather(receiver(websocket), sender(websocket, 10)) + + except Exception as exc: + print(f"Exception:\n{exc}") + +asyncio.run(main()) +``` + +### JavaScript Example +Here's a simple Node.js JavaScript to open the device-feed and print out any messages it receives (you'll need to `npm install websocket` before running it). + +```javascript +const APIKEY = "YOUR-API-KEY"; + +const W3CWebSocket = require("websocket").w3cwebsocket; + +console.log("Creating websocket"); + +const client = new W3CWebSocket( + `ws://localhost:8176/v2/api/ws/device-feed?api-key=${APIKEY}` +); +client.onmessage = function (message) { + console.log(message.data); +}; +client.onerror = function (err) { + console.log(`Error: ${JSON.stringify(err)}`); +}; +``` + +## Device Feed +This is the WebSocket you'll use for all device-related communication. You will receive the following messages from the Indigo server during the lifetime of the WebSocket. + +Here are the URLs you will use to connect to this feed. + +```bash +ws://localhost:8176/v2/api/ws/device-feed +wss://YOUR-REFLECTOR-NAME.indigodomo.net/v2/api/ws/device-feed +``` + + +### Device messages from the server +The following are examples of all the device messages that you will receive from the server. + +#### add device message +```json +{ + "message": "add", + "objectType": "indigo.Device", + "objectDict": { + "class": "indigo.DimmerDevice", + "address": "3B.04.7A", + "batteryLevel": null, + "blueLevel": null, + "brightness": 100, + "buttonConfiguredCount": 0, + "buttonGroupCount": 1, + "configured": true, + "defaultBrightness": 100, + "description": "- sample device -", + "deviceTypeId": "", + "displayStateId": "brightnessLevel", + "displayStateImageSel": "indigo.kStateImageSel.DimmerOn", + "displayStateValRaw": 100, + "displayStateValUi": "100", + "enabled": true, + "energyAccumBaseTime": null, + "energyAccumTimeDelta": null, + "energyAccumTotal": null, + "energyCurLevel": null, + "errorState": "", + "folderId": 1552926800, + "globalProps": { + "com.indigodomo.indigoserver": {}, + "emptyDict": {} + }, + "greenLevel": null, + "id": 1508839119, + "lastChanged": "2023-02-16T15:43:53", + "lastSuccessfulComm": "2023-02-16T15:43:53", + "ledStates": [], + "model": "LampLinc (dual-band)", + "name": "Insteon Dimmer", + "onBrightensToDefaultToggle": true, + "onBrightensToLast": false, + "onState": true, + "ownerProps": {}, + "pluginId": "", + "pluginProps": {}, + "protocol": "indigo.kProtocol.Insteon", + "redLevel": null, + "remoteDisplay": false, + "sharedProps": {}, + "states": { + "brightnessLevel": 100, + "onOffState": true + }, + "subModel": "Plug-In", + "subType": "Plug-In", + "supportsAllLightsOnOff": true, + "supportsAllOff": true, + "supportsColor": false, + "supportsOnState": true, + "supportsRGB": false, + "supportsRGBandWhiteSimultaneously": false, + "supportsStatusRequest": true, + "supportsTwoWhiteLevels": false, + "supportsTwoWhiteLevelsSimultaneously": false, + "supportsWhite": false, + "supportsWhiteTemperature": false, + "version": 67, + "whiteLevel": null, + "whiteLevel2": null, + "whiteTemperature": null + } +} +``` + +When a new device is added to the Indigo Server after you've opened the connection, you will receive this message. It contains a [device object](messages.md#device-objects) that you will want to add to your device list (since you'll want to patch it as it changes over time - see the next section). You'll also receive this message when a device's remote display property is changed from False to True. + +#### update device message +```json +{ + "message": "patch", + "objectType": "indigo.Device", + "objectId": 1508839119, + "patch": [ + [ + "change", + "brightness", + [100, 0] + ], + [ + "change", + "displayStateImageSel", + ["indigo.kStateImageSel.DimmerOn", "indigo.kStateImageSel.DimmerOff"] + ], + [ + "change", + "displayStateValRaw", + [100, 0] + ], + [ + "change", + "displayStateValUi", + ["100", "0"] + ], + [ + "change", + "lastChanged", + ["2023-02-17T16:29:56", "2023-02-17T16:30:54"] + ], + [ + "change", + "lastSuccessfulComm", + ["2023-02-17T16:29:56", "2023-02-17T16:30:54"] + ], + [ + "change", + "onState", + [true, false] + ], + [ + "change", + "states.brightnessLevel", + [100, 0] + ], + [ + "change", + "states.onOffState", + [true, false] + ] + ] +} +``` + +Patch objects are created via the [dictdiffer python module](https://dictdiffer.readthedocs.io/en/latest/), by comparing the device dictionary (`dict(some_device)`) for the old device with the one for the new dictionary as they are received in the `device_updated()` plugin method call. We've implemented a JavaScript library, [dictdiffer-js](https://github.com/IndigoDomotics/dictdiffer-js), to patch JavaScript objects given the patch object created by the dictdiffer Python library. You can use it in your projects if you like. + +#### device refresh messages +When you send a command that asks to refresh the entire list of devices, you'll receive the following message from the server: + +```json +{ + "id": "optional-custom-user-message", + "message": "refresh", + "objectType": "indigo.Device", + "list": [] +} +``` + +And if you requested just a single device refresh, you will receive the following message from the server: + +```json +{ + "id": "optional-custom-user-message", + "message": "refresh", + "objectType": "indigo.Device", + "objectDict": {} +} +``` + +!!! note + That the first message includes a list of objectDict elements, and the second includes a single objectDict element. + +#### delete device message +```json +{ + "message": "delete", + "objectType": "indigo.Device", + "objectId": 123456789 +} +``` + +This is the simplest of the messages, as it just contains the Indigo ID of the device to delete from your collection. You'll receive this message when a device is deleted from the Indigo server and when a device's remote display property is set from True to False. + +### Device messages to the server +You have a variety of messages you can send to the server. + +#### device refresh requests +To refresh either the full device list or a single device from the server, send the following message. + +```json +{ + "id": "optional-custom-user-message", + "message": "refresh", + // Specify the object type + "objectType": "indigo.Device", + "objectId": 123456789 +} +``` + +If you want the entire device list, simply omit the `objectId` key and the server will return the full list. The server will respond with the appropriate [device refresh message](#device-refresh-messages) shown above. + +#### device command messages +To command devices to do something, you will be using the [Device Command Messages](#device-command-messages) described below. For instance, to toggle a lamp device, you would send the following message: + +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.device.toggle", + "objectId": 123456789, +} +``` +You will receive a "patch" message as a result of this command. + +## Variable Feed +This is the WebSocket you'll use for all variable-related communication. You will receive the following messages from the Indigo server during the lifetime of the WebSocket. + +Here are the URLs you will use to connect to this feed. + +```bash +ws://localhost:8176/v2/api/ws/variable-feed +wss://YOUR-REFLECTOR-NAME.indigodomo.net/v2/api/ws/variable-feed +``` + +### Variable messages from the server +The following is an example of the variable message that you will receive from the server. + +#### Example variable object (dictionary in Python) +```json +{ + "class": "indigo.Variable", + "description": "", + "folderId": 0, + "globalProps": { + "com.indigodomo.indigoserver": {} + }, + "id": 345633244, + "name": "house_status", + "pluginProps": {}, + "readOnly": false, + "remoteDisplay": true, + "sharedProps": {}, + "value": "home" +} +``` + +Here are some examples of the server messages that clients will receive on the variable feed. + +#### add variable message +```json +{ + "message": "add", + "objectType": "indigo.Variable", + "objectDict": {} // Variable object as outlined above +} +``` +When a new variable is added to the Indigo Server after you've opened the connection, you will receive this message. It contains a [variable object](messages.md#variable-objects) that you will want to add to your variable list (since you'll want to patch it as it changes over time - see the next section). You'll also receive this message when a variable's remote display property is changed from False to True. + +#### update variable message +```json +{ + "message": "patch", // we use a patch rather than send the entire updated device + "objectType": "indigo.Variable", + "patch": {} // A patch object - see the Object Patches below for details +} +``` + +Variable patch objects are created via the [dictdiffer python module](https://dictdiffer.readthedocs.io/en/latest/) by comparing the variable dictionary (`dict(some_variable)`) for the old variable with the one for the new dictionary as they are received in the `variable_updated()` Plugin method call. + +#### delete variable message +```json +{ + "message": "delete", + "objectType": "indigo.Variable", + "objectId": 123456789 +} +``` +This is the simplest of the messages, as it just contains the Indigo ID of the variable to delete from your collection. You'll receive this message when a variable is deleted from the Indigo server and when a variable's remote display property is set from True to False. + +### Variable messages to the server +There are a few messages you can send to the server. + +#### variable refresh messages +```json +{ + "id": "optional-custom-user-message", + "message": "refresh", + "objectType": "indigo.Variable", + "objectDict": {} // Variable object as outlined above +} +``` + +#### Example refresh all variables message +```json +{ + "id": "optional-custom-user-message", + "message": "refresh", + "objectType": "indigo.Variable", + "list": [] // a list of Variable objects as outlined above +} +``` + +### variable command messages +The `updateValue` command is currently the only command message you can send to the variable feed. It closely mirrors the Python-based [IOM command for updating variables](../scripting/iom-concepts.md). This was completely intentional to make learning one API a stepping stone to another. The HTTP API messages and the Websocket API messages are identical, and are very clearly a JSON-rendered version of the associated IOM command. + +#### updateValue +```json +{ + // Note, values passed in the parameter dictionary must be strings. You can + // pass in an empty string ("") to clear the variable value. + "id": "optional-custom-user-message", + "message": "indigo.variable.updateValue", + "objectId": 123456789, // the variable id to update + "parameters": { + "value": "Some string value" + } +} +``` + +## Action Group Feed +This is the WebSocket you'll use for all action group-related communication. You will receive the following messages from the Indigo server during the lifetime of the WebSocket. + +Here are the URLs you will use to connect to this feed. + +```bash +ws://localhost:8176/v2/api/ws/action-feed +wss://YOUR-REFLECTOR-NAME.indigodomo.net/v2/api/ws/action-feed +``` + + +### Action Group messages from the server +The following are examples of all the action group messages that you will receive from the server. + +#### Example action group object (dictionary in Python) +```json +{ + "class": "indigo.ActionGroup", + "description": "", + "folderId": 532526508, + "globalProps": { + "com.indigodomo.indigoserver": { + "speakDelayTime": "5", + "speakTextVariable": "speech_string" + } + }, + "id": 94914463, + "name": "Movie Night", + "pluginProps": {}, + "remoteDisplay": true, + "sharedProps": { + "speakDelayTime": "5", + "speakTextVariable": "speech_string" + } +} +``` + +Here are some examples of the server messages that clients will receive on the action feed. + +#### add action group message +```json +{ + "message": "add", + "objectType": "indigo.ActionGroup", + "objectDict": {} // ActionGroup object as outlined above +} +``` +When a new action group is added to the Indigo Server after you've opened the connection, you will receive this message. It contains an [action group object](messages.md#action-group-objects) object that you will want to add to your action group list (since you'll want to patch it as it changes over time - see the next section). You'll also receive this message when an action group's remote display property is changed from False to True. + +#### update action group message +The update action group message is received when an action group has been updated on the Indigo server. It doesn't allow users to update action groups via the WebSocket API. + +```json +{ + "message": "patch", // we use a patch rather than send the entire updated device + "objectType": "indigo.ActionGroup", + "patch": {} // A patch object - see the Object Patches below for details +} +``` + +Action group patch objects are created via the [dictdiffer python module](https://dictdiffer.readthedocs.io/en/latest/), by comparing the action dictionary (`dict(some_action_group)`) for the old action with the one for the new dictionary as they are received in the `action_group_updated()` Plugin method call. + +#### delete action group message +```json +{ + "message": "delete", + "objectType": "indigo.ActionGroup", + "objectId": 123456789 +} +``` +This is the simplest of the messages, as it just contains the Indigo ID of the action group to delete from your collection. You'll receive this message when an action group is deleted from the Indigo server and when an action group's remote display property is set from True to False. + +### Action Group messages to the server +The following are examples of the action group messages that you can send to the server. + +#### action group refresh messages +```json +{ + "id": "optional-custom-user-message", + "message": "refresh", + "objectType": "indigo.ActionGroup", + "objectDict": {} // ActionGroup object as outlined above +} +``` + +#### Example refresh all action groups message +```json +{ + "id": "optional-custom-user-message", + "message": "refresh", + "objectType": "indigo.ActionGroup", + "list": [] // a list of ActionGroup objects as outlined above +} +``` + +### action group command messages +The `execute` command is currently the only command message you can send to the action group feed. It closely mirrors the Python-based [IOM command for executing action groups](../scripting/iom-concepts.md). This was completely intentional to make learning one API a stepping stone to another. The HTTP API messages and the Websocket API messages are identical, and are very clearly a JSON-rendered version of the associated IOM command. + +#### Execute +```json +{ + "id": "optional-custom-user-message", + "message": "indigo.actionGroup.execute", + "objectId": 123456789 // the action group id to execute +} +``` + +## Control Page Feed +This is the WebSocket you'll use for all control page-related communication. You will receive the following messages from the Indigo server during the lifetime of the WebSocket. The control page feed has no command messages you can send to the server; rather, its purpose is to use incoming messages to manage a list of the available control pages which (presumably) the user would select to open that page. + +Here are the URLs you will use to connect to this feed. + +```bash +ws://localhost:8176/v2/api/ws/page-feed +wss://YOUR-REFLECTOR-NAME.indigodomo.net/v2/api/ws/page-feed +``` + +### Control Page messages from the server +The following are examples of the control page messages that you will receive from the server. + +#### Example variable object (dictionary in Python) { #control-page-messages-from-the-server-example-variable-object-dictionary-in-python } +```json +{ + "class": "indigo.ControlPage", + "backgroundImage": "", + "description": "", + "folderId": 0, + "globalProps": {}, + "hideTabBar": true, + "id": 963336187, + "name": "Weather Images", + "pluginProps": {}, + "remoteDisplay": true, + "sharedProps": {} +} +``` + +Here are some examples of the server messages that clients will receive on the control page feed. + +#### add control page message +```json +{ + "message": "add", + "objectType": "indigo.ControlPage", + "objectDict": {} // ControlPage object as outlined above +} +``` +When a new control page is added to the Indigo Server after you've opened the connection, you will receive this message. It contains a control page that you will want to add to your control page list (since you'll want to patch it as it changes over time - see the next section). You'll also receive this message when a control page's remote display property is changed from False to True. + +#### update control page message +The update control page message is received when a control page has been updated on the Indigo server. It doesn't allow users to update control pages via the WebSocket API. + +```json +{ + "message": "patch", // we use a patch rather than send the entire updated control page + "objectType": "indigo.ControlPage", + "patch": {} // A patch object - see the Object Patches below for details +} +``` + +Page patch objects are created via the [dictdiffer python module](https://dictdiffer.readthedocs.io/en/latest/), by comparing the device dictionary (`dict(some_page)`) for the old page with the one for the new dictionary as they are received in the `control_page_updated()` Plugin method call. + +#### delete control page message +```json +{ + "message": "delete", + "objectType": "indigo.ControlPage", + "objectId": 123456789 +} +``` +This is the simplest of the messages, as it just contains the Indigo ID of the control page to delete from your collection. You'll receive this message when a control page is deleted from the Indigo server and when a control page's remote display property is set from True to False. + +## Indigo Object Folders +Each object type above may have folders. Since those folders are specific to the type, you will use the same feed (i.e. device-feed, etc.) to get all the available folders for that type. + +This is an example of a folder object. It is the same for any folder in any feed - children is the generic name for the indigo objects contained in the folder (device, variable, etc.) + +```python +{ + "id": 617272302, + "class": "indigo.Folder", + "name": "My Device Folder", + "remoteDisplay": true +} +``` + +### refresh folder server message +To get the full folder list from the server, send the following message. + +```python +{ + "message": "refresh", + "id": "optional-custom-user-message", + "objectType": "indigo.Device.Folder", +} +``` +You will then receive the following message with all the folders for that Indigo object type. + +```python +{ + "message": "refresh", + "id": "optional-custom-user-message", + "objectType": "indigo.Device.Folder", // or indigo.Variable.Folder, etc + "list": [] // A list of folder objects defined above +} +``` + +As of this release, this is the only way to get the current state of folders (there are no add/update/delete messages). If you think you need to refresh your folder list, then send the refresh message. Since folders are generally static, there really isn't much need to continually get the folder list. + +## Log Feed +Use this feed to catch all log messages as they happen in the Indigo Server. When you first open the log-feed WebSocket, you will receive the last 25 log messages from the server ***in chronological order***. After that, the messages come through the socket as they are generated (chronological order). See the [Log Messages](messages.md#log-messages) section below for a description of a log message object. + +Here are the URLs you will use to connect to this feed: + +```text +ws://localhost:8176/v2/api/ws/log-feed +wss://YOUR-REFLECTOR-NAME.indigodomo.net/v2/api/ws/log-feed +``` + +The only message you will receive on the log feed will be an add message for every new log entry: + +```json +{ + "message": "add", + "objectType": "indigo.LogEvent", + "objectDict": { + "message": "Stopping plugin \"Web Server 2025.1.0\" (pid 1020)", + "timeStamp": "2022-12-01T12:03:27.759000", + "typeStr": "Application", + "typeVal": 0 + "objectType": "indigo.LogEvent" + } +} +``` + +You can also send messages to the log feed from your websocket client with the following payload: + +```json +{ + "id": "optional-custom-user-message", + "messageText": "My log message.", // required + "message": "indigo.server.log", // required +} +``` diff --git a/reference/canonical/plugin-dev.md b/reference/canonical/plugin-dev.md new file mode 100644 index 0000000..c6ce618 --- /dev/null +++ b/reference/canonical/plugin-dev.md @@ -0,0 +1,22 @@ + + +# Plugin Development + +Plugins integrate new devices, triggers, actions, and services natively into Indigo — distributed as a single `.indigoPlugin` bundle users can double-click to install. Plugins are written in Python against the same [Indigo Object Model](../scripting/iom-concepts.md) used for scripting, plus a declarative XML layer for configuration UI. + +## Where to start + +Read the [Plugin Developer's Guide](guide.md) first — bundle structure, `Info.plist`, and how the Indigo Plugin Host runs your code. Then grab the [Indigo SDK](https://github.com/IndigoDomotics/IndigoSDK/releases) and explore the [example plugins](sdk-examples.md); modifying an example that's close to your goal is the fastest path to a working plugin. The [Building a Plugin tutorial](tutorials/building.md) walks through adding device types, actions, and event handlers. + +If you haven't scripted Indigo before, skim the [Scripting Tutorial](../scripting/tutorial.md) first — plugin callbacks are ordinary IOM Python. + +## Reference + +- [plugin.py Method Reference](reference/plugin-py/index.md) — `PluginBase` lifecycle methods and every callback hook. +- [Plugin XML Reference](reference/xml/index.md) — `PluginConfig.xml`, `Devices.xml`, `Events.xml`, `Actions.xml`, `MenuItems.xml`, and ConfigUI fields. +- [IOM Reference](../scripting/index.md#iom-reference) — the object model shared with scripting. +- [Python Packages](../scripting/guides/python-packages.md) — what's bundled and how to vendor dependencies. + +## Distributing your plugin + +Submit finished plugins to the [Indigo Plugin Store](https://www.indigodomo.com/pluginstore/) from [your Indigo account](https://www.indigodomo.com/account/plugins). diff --git a/reference/canonical/plugin-dev/guide.md b/reference/canonical/plugin-dev/guide.md new file mode 100644 index 0000000..1f9a2ef --- /dev/null +++ b/reference/canonical/plugin-dev/guide.md @@ -0,0 +1,384 @@ + + +# Indigo Plugin Developer's Guide v2.0 { #indigo-plugin-developer-s-guide-v10 } + +!!! abstract "In this guide" + Introduction to the Indigo plugin bundle format: the required `Info.plist` keys, folder structure (`Server Plugin`, `Resources`, `Packages`, `Menu Items`), and how the Indigo Plugin Host (IPH) sandboxes and manages each plugin process. Start here before reading the XML Reference or the implementation reference for `plugin.py`. + +## Indigo Plugins and APIs +Indigo has a long history of extensibility - AppleScript Attachment scripts were available from the start (and deprecated in Indigo 7.4). Later, the Indigo Web Server (IWS) was added along with the IWS plugin and the ability to add custom images. + +With Indigo 5.0, we added a new server plugin API that allows 3rd party developers to more natively add devices, triggers, and actions to Indigo. This server API allows users and 3rd party vendors to implement their own functionality in Python with full access to all the objects and events that Indigo understands (referred to as the [Indigo Object Model](../scripting/iom-concepts.md), or **IOM**). Someone with sufficient skills can implement support for any kind of device and have them integrated into the Indigo UI as first-class citizens. + +To deliver this additional functionality, we created the Indigo plugin bundle. Let’s start by first looking at the Application Support folder structure which the Indigo installer creates. + +A note about version numbers: We increment the API version number when the API is revised. The major number (X.0) is incremented when we do something that will break backwards compatibility. The minor number (1.X) is incremented when we add new features. See the API version chart to see which API versions were released in which Indigo version. If any of the API tables in the documentation don't have a version number you can assume that the feature is available in API version 1.0 and later. + +[Back to Top](#indigo-plugin-developer-s-guide-v10) + +## Indigo Support Folder Structure +Indigo's folder structure looks like this: + +![Folder Structure Image](../images/folder_structure.png) + +in this location: + +`/Library/Application Support/Perceptive Automation/Indigo [VERSION])` +] +You'll notice these two folders in particular: *`Plugins`* and `*Plugins (Disabled)*`. These two folders are where the plugin bundles are stored (see the next section for details). Plugins that are enabled from the UI are located in the *`Plugins`* folder and when a user disables a plugin it’s moved to the `*Plugins (Disabled)*` folder. + +[Back to Top](#indigo-plugin-developer-s-guide-v10) + +## The Indigo Plugin Bundle +We created a macOS Finder bundle type, the Indigo plugin bundle (*`.indigoPlugin`*), which has a very specific structure to encapsulate everything that a plugin needs to perform its functions: + +![Bundle Layout Image](../images/bundle_layout.png) + +The first thing you’ll notice is that this is actually a real Finder bundle - so it appears to be a single file called *`Example.indigoPlugin`*. It’s moved around and treated as a single file, and all the user has to do to install your solution is to double-click it in the Finder and Indigo will install and enable it for you. + +Creating a bundle is really easy: just create a folder in the Finder and end the name with *`.indigoPlugin`*. The Finder will prompt you about adding the extension *`.indigoPlugin`*. Click “Add”, and now your folder appears as a file. To get to the contents, right click on it and select `Show Package Contents` and it will open a separate window that is, in fact, just a new Finder window just like any other. In this window, you can create the folder structure above to have the elements that your plugin will need. Let’s go through each folder/file and discuss what it does. + +First, though, you can see that there is only one top-level folder in the bundle - *`Contents`*. All other files/folders are inside that folder. This is the standard macOS bundle construction, so we decided to follow the pattern. + +So, why did we go to the trouble of using the bundle format when there's just a file and a couple of folders? Because in future versions, we're going to add capabilities to the plugin bundle. + +### The Info.plist File +There’s only one file that’s directly inside the *`Contents`* folder, and it’s required (read very important). The *`Info.plist`* file is a standard XML property list file that contains several important key/value pairs. The keys in this file will help Indigo understand what functionality your plugin provides, what it’s name and version number are, etc. + +Editing a plist file isn’t difficult since it's just a text XML file that looks like this: + +```xml + + + + + PluginVersion + 1.2.3 + ServerApiVersion + 2.0 + CFBundleDisplayName + Rachio Sprinklers + CFBundleIdentifier + com.yourorgidentifier.yourpluginidentifier + CFBundleVersion + 1.0.1 + CFBundleURLTypes + + + CFBundleURLName + https://somehost.com/path/to/help/ + + + + +``` + +Here is what they keys are for: + +- *`PluginVersion`* (Plugin version) - this is the version number for your plugin - it’s shown to the user in the UI and will help you when supporting your plugin users. This key is required and should only contain numerical characters and periods (0-9 and .). For example, "1.0.5.2" is valid, but "1.0.5b2" is not. This is very important as it will help Indigo determine what do to when a user double-clicks a plugin to install it. If the version number is a higher version, Indigo will notify the user that the plugin will be installed and enabled. If the version number is lower than an already-installed version, Indigo will prompt the user to confirm that they want to downgrade the plugin. The version number is also used in version checking, and may eventually be used for automatic updates. +- *`ServerApiVersion`* (Server API version) - this value refers to the minimum server API version your plugin requires. In other words, if your plugin requires server API version 3.0, users must be running Indigo 2022.1.0 or later (the first Indigo version that supports server API version 3.0). Otherwise, Indigo will not allow the plugin to be installed. In rare circumstances, a new server API version may deprecate prior functions, so it's best to review the [API Version Chart](https://www.indigodomo.com/indigo/api_version_chart.html) as new API versions are released. +- *`CFBundleDisplayName`* (Bundle display name) - this is a standard macOS key, and its value represents the name of your plugin. It’s used in a bunch of places in the UI, so make sure that it appropriately identifies your plugin. This key is required. +- *`CFBundleName`* (Bundle name) - this is another standard macOS key, and it's a name that should be less than 16 characters long and be suitable for displaying in menu items with various strings appended (i.e. "ShortName Device Controls"). If it's not provided we'll use the bundle display name instead. +- *`CFBundleIdentifier`* (Bundle identifier) - this is another standard macOS key, and it represents a unique string that represents your plugin. This is used for namespacing where necessary in the code, so it is critical that it is unique. The standard reverse DNS naming scheme is what should be used, although if you aren’t a company you’ll need to figure something out (maybe your blog, etc.). You should limit your bundle id to standard alphanumerics as special/extended characters may cause problems. You should **not** use the `*com.yourorgidentifier.**` namespace. This key is required. +- *`CFBundleVersion`* (Bundle version) - another standard macOS key, and it represents the layout of the bundle. This is controlled by us. This key is required. +- *`CFBundleURLTypes`* (URL types) - you must specify one URL that represents a web page where your user can get support. Your plugin will have a menu item called "About [PLUGIN NAME]" - when the user selects this menu item, the default browser will open to this URL. Note - this can be a link to your plugin's GitHub repo wiki if there is one, or could be a forum topic in the “User Contributions” section of our user forums if you don’t have any other place to host the support page. This key is required. + +We want the user experience to be very similar for plugins, at least until it comes to configuration and use of the plugin, so you should be careful to get the Info.plist correct. + +### Menu Items Folder +You can drop Python scripts into this folder, and they will show up in your plugin's sub-menu on the new "Plugins" menu. When the user selects the menu item, the script is executed. This is a really simple way of giving your plugin some visible UI. + +### Resources Folder +This folder is meant to contain any assets (like images, templates, etc.) that your plugin might need. +If there is an `icon.png` file in here, it will be displayed in the [Plugin Store](https://www.indigodomo.com/pluginstore/) once you submit the plugin to the [Indigo Plugin Store](https://www.indigodomo.com/pluginstore/). Note that the code that loads [config UI XML templates](https://www.indigodomo.com/indigo/api_release_notes/1.4/) assumes that the `Server Plugin` folder is the root, so those template files should always be in the `Server Plugin` tree (i.e., `Server Plugin/Templates/my_template.xml`). + +If this folder contains any of the following sub-folders, that content will be made available directly from the Indigo Web Server: + +| Folder | Authentication | Description | +|----------|--------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `images` | IWS Configured | Any images in this folder or any sub-folders will be delivered if the user is authenticated via whatever authentication methods are enabled for IWS (digest, basic, api key). | +| `public` | None (open to all) | Any files in this folder or any sub-folders will be delivered to anyone without any authentication. | +| `static` | IWS Configured | Any files in this folder or any sub-folders will be delivered if the user is authenticated via whatever authentication methods are enabled for IWS (digest, basic, api key). | +| `videos` | IWS Configured | Any files in this folder or any sub-folders will be delivered if the user is authenticated via whatever authentication methods are enabled for IWS (digest, basic, api key). | + + +The URL for those files will be constructed using the plugin's id followed by the path. Here are some examples: + +- https://yourreflector.indigodomo.net/com.your.pluginid/static/html/something.html +- http://localhost:PORT/com.your.pluginid/images/an_image.png +- http://YourServer.local:PORT/com.your.pluginid/public/open_to_all.txt + +### Packages Folder +This folder is optional, but it's useful when your users will need to install additional Python packages in order to use your plugin. Any library installs should be directed to the *`../Contents/Packages/`* folder. Note that this may be required in future versions of Indigo. See the [Python Packages and Indigo](../scripting/guides/python-packages.md) page for more information. + +## Indigo Server Plugins +The most comprehensive way to extend Indigo is by implementing a Server Plugin. This mechanism allows you to add native components such as device types, events, actions, and menu items. Because we didn’t want to make the server plugin mechanism dependent on any single OS architecture, we decided to implement them in Python and have the description and user interface for the plugins’ components described in HTML and XML files respectively. We chose Python for several reasons: + +- it’s object-oriented nature fits well with the IndigoServer’s representations of various objects +- it is easily interfaced with C++ - which is what the server is written in +- it’s cross-platform so there’s a lot of documentation and expertise out there (many hardware makers supply a Python interface to their hardware) +- it’s easy to learn (really - we promise) + +Likewise, we chose XML because it’s very readable and universally understood and supported. We will not discuss XML in general in this document, but we believe that even those developers that aren’t familiar with XML will be able to quickly grasp the concepts since HTML is structurally similar. If you find understanding XML challenging, there are plenty of resources both on the web and in print that can help you get up to speed. + +[Back to Server Plugins](#indigo-server-plugins) + +### Building a Server Plugin vs Scripting IOM +The IOM is used for two similar purposes: scripting Indigo and building Server Plugins. They aren’t mutually exclusive, but they serve different needs. For instance, you may just want to write an embedded Python script action that just does some specific things vs building a full Server Plugin. Likewise, you may be interested in building a Server Plugin that doesn’t actually create any new device types, but simply adds events, actions, and menus to Indigo. + +[Back to Server Plugins](#indigo-server-plugins) + +### Indigo Plugin Host +Before we get to the specifics, let’s describe the process by which your plugin will get executed. Each Server Plugin will be launched in a special application called the Indigo Plugin Host (IPH). Each plugin will have its own instance of an IPH as well, so one plugin isn’t likely to bring down another or the IndigoServer. + +The IPH communicates with the IndigoServer through the XML interface the IndigoServer provides. But, fear not, the IPH hides all this complexity from you. It creates and manages C++ objects (and bridges them to native Python objects) that represent the Indigo Object Model (IOM), deals with communication with the IndigoServer, and makes sure that the IOM is kept in sync with the IndigoServer. + +More specifically, the IOM is presented to your plugin as a python module called *`indigo`*, that all plugins automatically import. Every object that represents an Indigo object and every command that you use to communicate with Indigo is done through the *`indigo`* module. For example, to write something to the Indigo Log, you would do this: + +`indigo.server.log(“Write this to the event log”)` + +To have the server speak a text message using your Mac's built-in speech synthesizer: + +`indigo.server.speak("Message to speak")` + +We describe the IOM in detail in the [IOM Reference Guide](../scripting/iom-concepts.md). + +The IndigoServer will manage the IPH for your plugin - starting it at IndigoServer startup or when the user enables your plugin, shutting it down at IndigoServer shutdown time or when the user disables your plugin. You never need to worry about process management or what happens when your plugin fails. IndigoServer will attempt to restart a failed plugin and warn the user when it can’t. + +### Plugin Failure Handling +For robustness and performance, plugins are executed inside their own process sandbox by a special Indigo application wrapper called the Indigo Plugin Host (see above). Runtime errors or crashes that occur within a plugin are handled differently based on when and where they occur: + +- **Plugin Fatal**: Errors that occur because of invalid XML or a failure inside the plugin initialization code (**init** method) are considered fatal. They will log an error and cause the Indigo Server to temporarily suspend the plugin -- the plugin will remain enabled but won't be running. Once the errors are corrected, the plugin can be restarted by its Reload menu option. Because the plugin is still in the enabled state, it will also be restarted on the next Indigo Server restart. + +- **Plugin Auto-Restarted**: If a plugin successfully initializes but then experiences a runtime crash (or is terminated via a kill signal not originating from an Indigo Server plugin disable request), then the Indigo Server will log an error and automatically restart the plugin after several seconds. This is only a crash fail-safe -- if your plugin has crashes then please fix the underlying problem (or forward the information to us if you think the problem is in the Indigo Plugin Host). If a plugin crashes multiple times over a short period of time, then the Indigo Server may slow or suppress its plugin auto-restart functionality requiring the plugin to be manually restarted via the Reload menu item. + +- **Concurrent Thread Auto-Restarted**: If a plugin defines runConcurrentThread() and an uncaught python exception is thrown within that method, then the Indigo Plugin Host will automatically (after several seconds) create a new thread and again call runConcurrentThread(). An error will also be logged with a call stack trace. In general, plugins should catch and handle common errors (hardware communication problems, out-of-bounds parameters, etc.) themselves inside runConcurrentThread(). The thread restart functionality implemented by the plugin Indigo Plugin Host is a fail-safe in case an error isn't handled, and should only be relied on for unexpected errors (if at all). + +- **Error Logged**: If an uncaught python exception is thrown out of any callback method (deviceStartComm, deviceStopComm, validatePrefsConfigUi, validateDeviceConfigUi, actionControlDimmerRelay, etc.) then an error will be logged with a call stack trace. In general, plugins should catch and handle common errors (hardware communication problems, out-of-bounds parameters, etc.) themselves. Errors logged for uncaught exceptions should be used by developers to implement their own (and more user-friendly) error handling. + +### Server Plugin Folder +The structure of the *`Server Plugin`* directory in the plugin bundle is something like this: + +![Server Plugin Folder Image](../images/serverpluginfolder.png) + +Each of the XML files describes the components that your plugin provides. You must also have at least the *`plugin.py`* file which is the entry point into the Python code that executes your plugin. Beyond that, you may create any other structure you like inside the folder. We’ll go over each file in detail, but first we should discuss the general characteristics of the XML files. + +**Note**: you can’t edit generic XML documents like these with the Property List Editor application - it will only edit correctly formatted property lists (which are XML, but specially formatted). You’ll need to use a generic text editor such as TextMate, TextWrangler, BBEdit or Xcode. + +### Indigo Plugin XML Conventions +The Indigo plugin XML is formulated with a few rules that will help you navigate it’s constituent parts. Elements, such as *`Field`*, *`Action`*, *`Device`*, etc., will always start with a capital letter. Attributes, such as *`id`*, *`type`*, *`defaultValue`*, etc., will always start with a lowerCase letter. Both elements and attributes will be camel case - that is, aside from the initial character described above, each new word will be capitalized. + +!!! important + Most of the major elements will have an *`id`* of some kind. It's extremely important that you follow these rules when creating the *`id`*. + +*`id`*'s: + +- can contain letters, numbers, and other ASCII characters, +- cannot start with a number or punctuation character, +- cannot start with the letters xml (XML, Xml, etc.), and +- cannot contain spaces. + +## Adding Your Plugins to the Plugin Store + +This section outlines the rules for submitting a plugin to the [Indigo Plugin Store](https://www.indigodomo.com/pluginstore/). While it may seem like a lot, it's mostly common sense. You can start the process from the [Plugin Contributions section of your Indigo Account](https://www.indigodomo.com/account/plugins). + +### Creating Your Developer Account +If you haven't already, you'll need to go to the [Plugin Contributions](https://www.indigodomo.com/account/plugins) page in your Indigo Account and add your developer information: + +![Plugin Contributions Image](../images/plugin_contributions.png) + +Please read through the entire top section of that page as it contains important information. + +**Warning**: please **do not use** `com.indigodomo` or `com.perceptiveautomation` as your developer ID (or embed those in your developer ID). Those are our internal IDs and should not be used. + +### Two Ways to Add a Plugin +First and foremost: please *do not* attempt to add a plugin that you don't "own." If a developer of a plugin has abandoned it and you would like to take it over, please let us know and we'll take it from there (we've done this in the past and it's usually not a problem). Once we get permission we'll let you know and we can coordinate from there. If the developer is still active, let them know that you'd like to see it in the Plugin Store. + +There are two ways to add a plugin: + +![Add Plugin Image](../images/add_plugin_1.png){ width=400 } + +The first and recommended option is via a GitHub Repository. You'll manage your plugin almost completely through GitHub. You'll create releases there, manage the various descriptions, etc. You will have the option to override some of that information in the plugin administration UI that we provide, but you'll likely find that managing everything through GitHub will be a better and more consistent experience. + +The other option is to directly manage everything through our UI. With this approach you'll create your own plugin releases and upload each new release via our plugin administration UI. You'll need to manage all descriptions through our UI. If you don't want to learn how to use GitHub then this is the option for you. + +### General Rules +First, some general rules that you need to follow regardless of which approach you take: + +- Version numbers in the `Info.plist` file must be of the form X.Y.Z (except the ServerApiVersion, which is X.Y). There are no beta/pre-release signifiers allowed. +- No two releases can have the same version number (X.Y.Z). +- The Plugin ID (CFBundleIdentifier) can't change once a plugin has been added. +- The Plugin ID **must begin with your developer ID** specified above. This **should not** begin with `com.indigodomo` or `com.perceptiveautomation`. So, if you specified `com.johnsmith` as your developer ID, your plugin IDs would look like `com.johnsmith.myfirstplugin`. +- If the `Contents/Resources/icon.png` file exists in your Plugin bundle it will be used as the icon in the store (the ![Plugin Image](../images/plugin_128x128.png){ width=24 } icon will be used if the file doesn't exist). See [Icons and Branding](#icons-and-branding) below for more details. +- Plugins and releases can only be deleted by Indigo Domotics staff. Email us with details if you need a release removed. + +### GitHub Specifics +If you want to use GitHub (and there are many advantages to doing so), there are a few things to consider. We have a few (but not many) requirements in how your repo is constructed and how you do releases. There are also some optional things that you'll want to consider to make the experience for users even better. + +If you're new to GitHub but want to try it out, we've written [a simple How-To](https://wiki.indigodomo.com/doku.php?id=developer:github_setup_for_plugins) with lots of screen captures describing one way to set up your repository. + +#### GitHub Repo Layout +This is the required GitHub Repo layout: + +![GitHub Repo Layout Image](../images/github_repo_layout.png){ width=600 } + +At the top level of the repo should be your plugin folder and an optional `README` file. You may have other files in there as well, like the `LICENSE` file in the rachio-indigo repo above. The plugin is there for obvious reasons — it's the path into the source for your plugin. The `README` file's contents will automatically be used as the plugin's description and shown on the About tab on your Plugin's detail page: + +![Rachio Plugin Store Detail Image](../images/rachio-pluginstore-detail.png){ width=600 } + +Markdown in the `README.md` file will be rendered correctly in the Plugin Store though you can also use a plain text file (`README.txt`). + +#### GitHub Releases +When you are ready to publicly release a version of the plugin (either initially or a follow-up release), you should add a release in GitHub: + +![GitHub Releases Image](../images/github-releases.png){ width=600 } + +We **do not** look at the "master branch" (or "tips") of the repo. Only published releases (not pre-releases) will be added to the Plugin Store's release list (see [Adding a GitHub Release](#adding-a-github-release) below for details). The Plugin Store currently doesn't support listing betas (pre-releases) but you can still create them and point your beta testers to them on GitHub (one of the advantages to using GitHub). + +##### Release Requirements +There are a few requirements for GitHub releases: + +- The GitHub release tag # must match PluginVersion in the `Info.plist` file, otherwise the release will be rejected when added to the Plugin Store. **Note**: we will be looking in the source code zipball from the release for the Info.plist, **not** the attached plugin. If you're getting the mismatch error, that's where you'll need to look to fix any mismatching version numbers. +- The `README.md` or `README.txt` file must be at the top level of the archive for it to automatically be used. This is to avoid issues if your plugin includes other source projects that may also have their own README files. +- Although optional, we strongly encourage you to add a zipped version of just the plugin folder (not the entire repo) to each release. Make sure that `indigoPlugin` is in the name and that it's a zip file (i.e. `MyPlugin.indigoPlugin.zip`) and make sure that there aren't any other zip files attached to the release. If you don't include an attached zipped plugin, the user will download a zipped copy of the repo itself, including the plugin, the README, etc. (basically everything in the repo). This may make it confusing for users to find and install the plugin. + +### Adding a Plugin +As we mentioned at the top, there are two ways to add a new plugin to the Plugin Store: by specifying a GitHub repo or by uploading an existing plugin: + +![Add Plugin Image](../images/add_plugin_1.png){ width=400 } + +!!! note + There is no automated way to convert between a GitHub-based plugin and a directly managed plugin, so please carefully consider which approach you want to use before initially adding the plugin (we recommend GitHub!). We can manually convert between the two, but it takes some time and effort. + +#### Adding a Plugin from GitHub +To add a new plugin from a GitHub repo, just enter the GitHub user and repo names. As an example, for our Rachio Sprinkler repo, which is located here: + +`https://github.com/IndigoDomotics/rachio-indigo` + +We entered **IndigoDomotics** for the user and **rachio-indigo** as the repo. Note your repo must have at least one published release to be added to the Plugin Store. + +Click the **Next** button and you'll see the following page with the appropriate data pre-populated: + +![Add Plugin From GitHub Image](../images/add_plugin_from_github.png){ width=600 } + +This form has a variety of fields (some read-only) that fall into two groups. The first group are fields about the plugin itself (not any specific release of the plugin): + +1. **Plugin ID** — the ID from the `Info.plist` file (read-only). +2. **Name** — the name from the `Info.plist` file (read-only). +3. **Summary\*** — a very brief (200 character) summary of your plugin. It is pre-populated with the description for your GitHub repo but you may edit it as you want. It will never be overwritten from GitHub. +4. **Description\*** — the `README` file's contents from the repo will be inserted here. It may contain Markdown and you can edit it to make any changes you like. You will have the option of updating it from GitHub when you add new releases. +5. **Category** — the major category of your plugin. If you can't find one that seems suitable, then select **Miscellaneous**. This can be changed later. +6. **Help URL\*** — the help URL pre-populated from the `Info.plist`. You may override it and it will never be overwritten automatically. +7. **Documentation URL** — a separate and optional URL that can point to more comprehensive documentation. This is a good place to put the URL to the GitHub wiki for this repo (see the [Rachio Plugin's wiki page](https://github.com/IndigoDomotics/rachio-indigo/wiki) for an example). +8. **Plugin Icon** — if your plugin's bundle has an icon (`Contents/Resources/icon.png` file) then it will automatically be used if you leave this field blank. Alternately, you can add a PNG file here. +9. **GitHub User and Repo** — a read-only copy of what you entered on the first screen. + +And the next section is specific information about the release that you're adding: + +1. **Release Version** — automatically retrieved from the releases section of GitHub. If you have more than one release in the repo, this will be the most recent (only repos that are not pre-release or draft are used). **Note**: the release version number in the GitHub release source zip's Info.plist **must match** the tag for the GitHub release. Otherwise, the release will not be added. This sanity check will help ensure that you don't end up with version mismatches and the associated problems that will result (failed update checking, failed upgrades, etc). +2. **Release Title** — also retrieved from GitHub, it's the short release title. +3. **What's New** — again, retrieved from GitHub, this is the full description of the release. +4. **Requirements** — an optional separate field that may contain Markdown. The intention of this field is to outline any specific requirements or steps needed for this release. GitHub doesn't have anywhere (other than the release description) to put this kind of information. You may, of course, just add it to the description (and therefore the What's New section) and leave this field blank. We won't show it if it's blank. +5. **Date Released** — the release date as specified in the GitHub release info. You can change it here if you like, using the YYYY-MM-DD format. Note if you change it to some date in the future it won't show up in the Plugin Store until that date. +6. **Server API** and **Bundle Versions** — retrieved from the `Info.plist` and just shown here for completeness. The Server API version is particularly important in that it determines what's shown as the minimum Indigo version required to use the plugin. + +Required fields are marked with an asterisk (\*). The majority of GitHub repos will have all the information needed for the required fields so it's only likely that you'll have to edit the Category field as a minimum (and if your plugin is an A/V or IR plugin you won't even have to do that!). Once you have reviewed all the form fields hit **Add Plugin**. + +You'll notice that on the right side of the screen we've provided a quick cheatsheet for Markdown syntax. This will be helpful when editing any fields that allow Markdown. + +#### Adding a Directly Managed Plugin +To add a new plugin from an existing zipped plugin, just click the Choose File button and select the file. Note that Safari will automatically zip plugin folders before uploading (but your browser of choice may not). + +Click the **Next** button and you'll see the following page (with the appropriate data pre-populated): + +![Add Plugin From File Image](../images/add_plugin_from_file.png){ width=600 } + +This form has a variety of fields (some read-only) that fall into two groups. The first group are fields about the plugin itself (not any specific release of the plugin): + +1. **Plugin ID** — the ID from the `Info.plist` file (read-only). +2. **Name** — the name from the `Info.plist` file (read-only). +3. **Summary\*** — a very brief (200 character) summary of your plugin. +4. **Description\*** — a full description of what your plugin does. The field may contain Markdown. +5. **Category** — the major category of your plugin. If you can't find one that seems suitable, then select **Miscellaneous**. +6. **Help URL\*** — the help URL pre-populated from the `Info.plist`. You may override it and it will never be overwritten automatically. Commonly developers use this URL to point to a specific post on [their own sub-forum](https://forums.indigodomo.com/viewtopic.php?f=121&t=7526). +7. **Documentation URL** — a separate and optional URL that can point to more comprehensive documentation. So if you have a forum post that contains the documentation for the plugin for instance (and if it's different than the Help URL) then this is the place to put it. +8. **Plugin Icon** — if your plugin's bundle has an icon (at `Contents/Resources/icon.png`) then it will automatically be used if you leave this field blank. Alternately, you can add a PNG file here that will be used in the plugin's display in the Plugin Store. +9. **GitHub User and Repo** — disabled since this plugin will be managed directly through our pages. + +And the next section is specific information about the release that you're adding: + +1. **Release Version** — automatically retrieved from the `Info.plist` file. +2. **Release Title\*** — a short summary (100 characters max) of this release of the plugin. +3. **What's New\*** — a full description of the release. This field may contain Markdown. +4. **Requirements** — an optional separate field that may contain Markdown. The intention of this field is to outline any specific requirements or steps needed for this release. You may, of course, just add it to the What's New section and leave this field blank. We won't show it if it's blank. +5. **Date Released** — the release date defaulting to today. You can change it here if you like, using the YYYY-MM-DD format. Note, if you change it to some date in the future it won't show up in the Plugin Store until that date. +6. **Server API** and **Bundle Versions** — retrieved from the `Info.plist` and just shown here for completeness. The Server API version is particularly important in that it determines what's shown as the minimum Indigo version required to use the plugin. + +Required fields are marked with an asterisk (\*). Once you've filled out the form hit **Add Plugin**. + +You'll notice that on the right side of the screen we've provided a quick cheatsheet for Markdown syntax. This will be helpful when editing any fields that allow Markdown. + +### Editing a Plugin +To edit a plugin, just go to your Indigo Account's [Plugin Contributions](https://www.indigodomo.com/account/plugins) page, find the release in the list and click the **Edit** button. You'll be able to change all editable fields there. + +### Adding a Release +When you have an update to a plugin, you just need to add a release to it. Go to the [Plugin Contributions](https://www.indigodomo.com/account/plugins) section of your Indigo Account, find the plugin, and click either the **Edit/Add Release** button for GitHub plugins or the **Add Release** button for directly managed plugins. + +#### Adding a GitHub Release +Adding a GitHub release couldn't be easier. Once you've clicked the **Edit/Add Release** button, you'll see the plugin edit form where you can edit the plugin and release information. At the bottom of the form, you'll see the following checkboxes: + +![GitHub Checkboxes Image](../images/github_checkboxes.png) + +The first checkbox is automatically checked and therefore when you Save the form it will add any new releases. The next checkbox will update the main plugin description with the `README` file from the most recent release, and the last checkbox will update the icon from the most recent release. Super simple! + +**Note**: the release version number in the GitHub release source zip's Info.plist **must match** the tag for the GitHub release. Otherwise, the release will not be added. This sanity check will help ensure that you don't end up with version mismatches and the associated problems that will result (failed update checking, failed upgrades, etc). + +#### Adding a Release for a Directly Managed Plugin +Adding a new release this way isn't really very hard either, there are just a few fields you need to fill out: + +![Add Plugin Release File Image](../images/add_plugin_release_file.png){ width=600 } + +1. **Plugin ZIP File** — click the **Choose File** button and select the plugin folder. Note that Safari will automatically zip plugin folders before uploading (but your browser of choice may not). +2. **If there is an icon in the bundle, use it for the plugin's icon in the Plugin Store** — if checked then the icon in the bundle will replace the icon currently being used. +3. **Release Title\*** — a short summary (100 characters max) of this release of the plugin. +4. **What's New\*** — a full release description. This field may contain Markdown. +5. **Requirements** — an optional separate field that may contain Markdown. The intention of this field is to outline any specific requirements or steps needed for this release. You may, of course, just add it to the What's New section and leave this field blank. We won't show it if it's blank. +6. **Date Released** — the release date defaulting to today. You can change it here if you like, using the YYYY-MM-DD format. Note, if you change it to some date in the future it won't show up in the Plugin Store until that date. + +Required fields are marked with an asterisk (\*). Once you've filled out the form hit **Add Release**. + +You'll notice that on the right side of the screen we've provided a quick cheatsheet for Markdown syntax. This will be helpful when editing any fields that allow Markdown. + +### Editing a Release +To edit a release, just go to your plugin's detail page, switch to the Releases tab, expand the release you want to edit and click the **Edit** button. You'll be able to change all editable fields there. + +### Icons and Branding +The Plugin Store shows icons for each plugin. If you don't include one in the plugin bundle (`Contents/Resources/icon.png`) and you don't add one explicitly to the release (see [Adding a Release](#adding-a-release) for details) then the default Indigo Plugin icon ![Plugin Image](../images/plugin_128x128.png){ width=24 } will be shown. + +To make it easy for users browsing the Plugin Store to identify something they're looking for, it's quite helpful to include an icon that represents what your plugin does. For instance, if you are integrating a specific vendor's products (like the Rachio Sprinkler example above) then you'll probably want to use their icon. Here are some tips to help find an appropriate icon: + +- Many companies provide media links, including icons, that can be used for promotional purposes. +- Some do it as part of the developer API documentation (often under "marketing" or some such). +- You can often find logos on their social media accounts in the photograph sections. + +We believe that the vast majority of companies won't mind you using their logos to promote their products as long as it's clear that you have no direct affiliation to the company. If there is an issue it's easy for us to remove. + +Icon details: 256 × 256 is the optimal size. Images **must** be 128px high or they aren't going to look good (somewhat wider might work on some screen sizes). The icons must be `.png` files and the file name should always be `icon.png`. Note the macOS Preview app can be used to convert other image formats to `.png`. + +### Linking to Indigo Docs +If your plugin's documentation needs to link to Indigo's documentation, use the version-agnostic redirect URL so that your links always resolve to the current release of the docs: + +`https://www.indigodomo.com/docs/` + +For example, `https://www.indigodomo.com/docs/overview#sprinkler_controls` maps to the most recent documentation for the sprinkler controls. If you need to pin a link to a specific release, you can link directly to a versioned page at `https://docs.indigodomo.com//…` instead. + +### A Few Final Thoughts +That's pretty much all there is to managing a plugin in the Plugin Store. Here are a few final random thoughts and important reminders: + +- Putting your plugin on GitHub will encourage others to help you maintain the plugin. Wouldn't it be great if someone with a problem could actually fix it and submit a patch to you? +- We highly recommend that you attach a zipped plugin (don't forget to delete the `.pyc` files) in your GitHub releases. When users click the Download Release button in the Plugin Store, it will download just the zipped plugin (not the entire repo). It'll be much clearer to users what they need to do next. +- The GitHub wiki page is the best way to provide documentation (see the [Rachio](https://github.com/IndigoDomotics/rachio-indigo/wiki) and [Alexa-Hue Bridge](https://github.com/IndigoDomotics/alexa-hue-bridge/wiki) examples). The advantage to using the repo wiki is that others can help you maintain the documentation. +- The most common way to provide support for a plugin (see *Help URL* above) is via our online forum. You can [request your own sub-forum](https://forums.indigodomo.com/viewtopic.php?f=121&t=7526) and create topics (or children sub-forums) for each plugin. +- You may not delete plugins or releases — if you need to for some reason just let us know and we'll handle it. +- If you want to give someone else edit access (only edit, not add privileges) to your plugins, let us know and we can do that. +- If you need to change a plugin from Directly Managed to GitHub or vice versa, let us know. It's a manually intensive process so it may take us a few days to get everything converted but we will do it for you. diff --git a/reference/canonical/plugin-dev/reference/dev-environment.md b/reference/canonical/plugin-dev/reference/dev-environment.md new file mode 100644 index 0000000..cb9ff42 --- /dev/null +++ b/reference/canonical/plugin-dev/reference/dev-environment.md @@ -0,0 +1,300 @@ + + +# Setting Up a Development Environment + +If you're interested in developing an Indigo plugin to share with other users or just for yourself, the way you approach development can make a big difference. Whether you choose to write your Indigo plugin without any specialized tools or expensive software packages, or instead choose one of the open source applications -- or even a plain text editor -- there are ways to make the process easier and more streamlined. This page describes many authoring packages and steps you can take to make your effort more successful. + +## Integrated Development Environments + +Using an Integrated Development Environment (IDE) can make writing Indigo plugins much easier. IDEs are programming environments that can help you reference Python functions, make recommendations on syntax, and even color code and highlight sections of code to make things easier for you. There are way too many Python IDEs to list them all here, but the more popular ones include (in alphabetical order): + +- [Atom](https://atom.io) - Atom is a free and open-source text and source code editor for macOS, Linux, and Microsoft Windows with support for plug-ins written in JavaScript, and embedded Git Control. It was developed by GitHub. **NOTE:** Atom and all projects under the Atom organization were officially sunset on December 15, 2022. +- [BBEdit](https://www.barebones.com) - BBEdit is a proprietary text editor made by Bare Bones Software, originally developed for Macintosh System Software 6, and currently supporting macOS. The free version of BBEdit works very well for writing Indigo plugins, and the paid version includes some IDE-type integration as well. +- [PyCharm](https://www.jetbrains.com/pycharm/) - PyCharm is an excellent full-featured commercial Python development environment made by JetBrains. There are several licenses available for PyCharm from paid to free -- the license you need depends on what you use the program for. **NOTE:** certain features (including the ability to debug Indigo plugins from within the IDE) require a paid PyCharm Professional Edition license. +- [Spyder](https://www.spyder-ide.org) - Spyder is an open-source cross-platform integrated development environment for scientific programming in the Python language. While it is more geared towards scientific programming, it works for non-scientific applications, too. +- [Sublime Text](https://www.sublimetext.com) - Sublime Text is a shareware cross-platform source code editor. It natively supports many programming languages and markup languages. Users can expand its functionality with plugins, typically community-built and maintained under free-software licenses. +- [VSCode](https://code.visualstudio.com) - VSCode is a free source code editor made by Microsoft that runs on Mac, Linux and Windows. VSCode is an extremely popular tool used for Python development. + +## Other Editors + +You can use any of several great text editors to write plugin code, but one aspect is crucial -- they must be able to save **plain text files**. + +- **TextEdit** - the editor that ships with macOS. If you choose to use Apple's TextEdit app, when you save your code to file, you MUST select **Make Plain Text** from the **Format** menu, and when you save, be sure that the **Plain Text Encoding** is set to `Unicode (UTF-8)`. +- [vim](https://www.vim.org) - Vim is a highly configurable text editor built to make creating and changing any kind of text very efficient. It is included as "vi" with most UNIX systems and with Apple macOS. + +## Setting Up Your Development Environment + +Choosing what tools to use to develop your plugin is only one of the considerations you'll need to address. You'll also need to decide how you're going to configure your environment. + +- **Project organization considerations** -- will you be developing on the same machine where your Indigo server lives? Where will your project files be located? On your machine? Online? A common approach is to have a separate folder structure just for development -- with subfolders dedicated to each project. You should also consider whether you'll benefit from a common location within your project space that can be used for segments of code that you'll use across multiple plugin projects. +- **Virtual environments** -- some developers choose to build their projects using [virtual environments](https://docs.python.org/3/tutorial/venv.html). This allows for each project to reside in a separate environment where changes can be made to one environment while leaving the others unchanged. For example, installing different versions of Python libraries depending on the project's needs. +- **Backups and version control** -- You'll want to save your work along the way. Backups are easy. Make them. Often. But you'll also want to be in a position to be able to revert back to prior versions in case you decide to undo some changes you've made. GitHub is a popular choice, but there are other options out there and you can always roll your own. +- **Plugin versioning** -- You'll need to come up with a way to assign version numbers to your plugin. By incrementing the version number with each release, Indigo will be better able to identify updates and more easily install them -- version numbers are required if you release your plugin through the Indigo Plugin Store. There are many ways to do versioning, but it's common to use a two- or three-number [semantic versioning system](https://en.wikipedia.org/wiki/Software_versioning#Semantic_versioning) where: + - Major version - often incremented with substantial changes to the software. + - Minor version - often incremented with new features or enhancements. + - Patch - often incremented with bug fixes and minor code refinements. +- **Collaboration** -- will you be working on the plugin by yourself or will you be coordinating with other developers? If you're working in a group environment, you'll need to establish a method (again, GitHub is a popular choice) but also protocols for how your team's work will be combined. +- **Symlinks** -- Once you're ready to test your code, you'll need to install your plugin on your Indigo server in order for it to run and access the Indigo Object Model Framework (IOM). You could choose to install your plugin like you would with any other plugin, but there is a better way -- using symbolic links. A link allows your "original" plugin code to remain outside the Indigo file system but in a place where the Indigo server will still see it. Creating symlinks for your development plugins using the Terminal app is easy: + 1. Open two Finder windows, one pointing to the folder that contains your plugin code and the second pointing to the Indigo file tree. + 2. First, type the following command in Terminal: `ln -s` + 3. Drag your plugin file and drop it on the Terminal window. + 4. Drag the Indigo Plugins (Disabled) folder and drop it on the Terminal window, so it looks something like this: + `ln -s /Users/User/My Development Environment/my_plugin.indigoPlugin /Library/Application\ Support/Perceptive\ Automation/Indigo\ 2022.1/Plugins\ \(Disabled\)` + 5. Switch to Terminal and hit return. If things went according to plan, you should see your plugin in the Plugins (Disabled) folder with a small arrow in the lower left corner (indicating that it's a linked file). + 6. Restart the Indigo server and you should see your plugin in the Plugins Menu. With the file successfully linked, you can safely Enable and Disable the plugin in Indigo and the operating system will update the symbolic link accordingly. You can then safely make changes to your code, save, and reload the plugin in Indigo. +- **Hosting** -- If you choose to share your plugin with the world, you'll need a place to host the plugin files so that others can download it. + - **Indigo Plugin Store** -- The recommended way to share plugins with other Indigo users is via the Indigo Plugin Store. By using the store, you get a central place where users can search for plugins to solve various scenarios, you get built-in version notifications to users via the Mac Client, etc. Check out the [Plugin Store Submission Guidelines](../guide.md#adding-your-plugins-to-the-plugin-store). + - **GitHub** -- Many developers manage their plugin code on GitHub, and in fact you can configure your Plugin Store entry to pull information and releases from GitHub (which is the recommended way of adding plugins to the store). GitHub is a very powerful tool used for distribution, version tracking, collaboration and other things. A GitHub account is free with a few limitations that most users won't encounter. +- **Providing Support** -- If you choose to share your plugin with others, it's inevitable that someone will have a question, an issue, or want to contribute code to your effort. There are a variety of ways to do this, but there are several common approaches that developers currently use: + - **Indigo Forums** - many developers have dedicated forums for their plugins on the Indigo forums site. If you think your plugin might qualify for its own forum, please [contact Support](mailto:support@indigodomo.com). + - **GitHub Wikis** - many developers write documentation for their plugins, and one popular way to make that available to others is by creating a dedicated wiki page on GitHub. + - **Plugin Issues** - Some developers prefer users report problems via the Indigo forums, while others prefer users to file issues on GitHub. + +## Debugging + +As a quick summary, Indigo plugins and scripts must run in a special process called the Indigo Plugin Host. That process bridges Indigo native objects (C++ objects) to Python. One side effect of this requirement is that debugging a plugin has some special challenges. For instance, you can't directly debug plugins from most of the IDEs listed above. + +However, we have facilitated the use of several Python debuggers together with Indigo: + +- [pdb](https://docs.python.org/3/library/pdb.html) (command line tool), +- [PuDB](https://pypi.python.org/pypi/pudb/) (command line tool), and +- [PyCharm](https://www.jetbrains.com/pycharm/) (professional version). + +Each has its pros and cons, but all are a significant improvement over `self.logger.debug()...` + +In the Plugins Preferences tab, there is a Development section: + +![Indigo Plugin Preference for Debugging Image](../../images/ss88.png) + +(Bet you didn't even remember that tab was there). Because we need to start plugins in a special way for debugging to work, we need to show some special debugging menus. Check the checkbox to see those menus in each plugin's submenu. Next, each debugger needs to be started in a specific way, so select the debugger that you want to use. + +By enabling debugging menus, each plugin will have some additional menu items on their submenu: + +- Enable/Reload in Debugger +- Enable/Reload in Interactive Shell ([discussed below](#plugin-specific-interactive-shell)) + +Selecting the first will enable or restart the plugin with the plumbing enabled for the debugger you selected. We'll talk about the specifics for each next. + +### pdb + +[pdb](https://docs.python.org/3/library/pdb.html) is a command line debugger that's built-in to Python. We'll let you read the docs to find commands and features. What you do need to know is when you select Enable/Reload in Debugger, Indigo will restart your plugin and open a terminal window running pdb: + +![PDB Image](../../images/ss89.png) + +To add breakpoints to your code, you just add `indigo.debugger()` method calls wherever you want plugin execution to pause in the debugger. Trying to interactively add breakpoints from pdb or PuDB is hit-or-miss because of the threaded way in which Indigo plugins run. The most reliable way to force a breakpoint is by manually adding the `indigo.debugger()` call to the python source and restarting the plugin. Also note `indigo.debugger()` calls are ignored (NOPs) when the plugin is not launched in debugger mode, so don't lose sleep over leaving an `indigo.debugger()` call in a shipping plugin. + +### PuDB + +[PuDB](http://heather.cs.ucdavis.edu/~matloff/pudb.html) is a more graphical debugger (though it's still character based), much like the old [Borland Turbo Debugger](https://en.wikipedia.org/wiki/Borland_Turbo_Debugger) from many years ago: + +![PuDB Image](../../images/ss90.png) + +As with pdb, you add breakpoints to your code by adding `indigo.debugger()` method calls wherever you want plugin execution to pause in the debugger. + +### PyCharm + +Finally, we were able to use PyCharm's **Python Debug Server** feature to enable plugin debugging (formerly referred to as "remote debugging"). It requires a bit more setup, but if you want to use a fantastic modern IDE, this is the choice for you. **Note**: because we're using the Debug Server feature, you can only use the paid professional version of PyCharm as the community edition doesn't support it. + +#### Configure Local Debugging + +The most straight-forward debug configuration is to use PyCharm to debug your plugin running in Indigo on the same Mac. With your plugin's project open in PyCharm, create a run configuration of type **Python Debug Server** (formerly **Remote Python Debug**): + +![Python Debug Server Image](../../images/python_debug_server.png) + +There are three important config parameters in this dialog (you can name the configuration anything you want). The first two tell how to connect: specify localhost in the **Local host name** box and 5678 in the **Port** field. The next field you need to adjust is the Path mappings field. + +Recall that the recommended way of developing Indigo plugins is to put your plugin in a central location (not inside the Indigo folders), then make a symbolic link to it in the Plugins directory. We do this because the Indigo server moves a plugin between two different folders when enabling/disabling. An IDE/editor will get confused when this happens, so by putting the actual code in a place that never moves and allowing the Indigo Server to move a symbolic link around, you get around this issue. + +Because of this, you need to tell PyCharm where the actual path to the source is when the plugin is enabled and being debugged. Click the ellipsis button at the end of the **Path mappings** field to add a mapping. On the **Local path** side, you want to specify the actual path to your plugin's source (i.e. `/Users/you/path/to/myplugin.indigoPlugin`). On the **Remote path** side, you want to specify the path to your plugin's symlink when the plugin is enabled (i.e. `/Library/Application Support/Perceptive Automation/Indigo 2022.1/Plugins/myplugin.indigoPlugin`). **TIP:** to get the path to a file in the Finder, right-click on the file to show the contextual menu, then press the Option key. The Copy item will change to Copy as Pathname which is exactly what you want. + +One other option is the **Suspend after connect** checkbox: if you have that checked, then when your plugin restarts with the debugger, it will pause execution in the `__init__()` method. We don't recommend doing this since you can add breakpoints anywhere you want in the code, including in the `__init__()` method. + +**Note:** the PyCharm debug configuration dialog says that you need to install `pydevd` and add two lines of code to connect to the debug server, however, the IPH (Indigo Plugin Host) takes care of this for you so **_there's no need to complete those steps._** + +That's it for setup! To debug, just run the debug configuration and then restart your plugin in the debugger. Unlike when using pdb and PuDB, you don't need to add explicit breakpoints to the source code using `indigo.debugger()` -- rather, just interactively add breakpoints in PyCharm: + +![Breakpoints in PyCharm Image](../../images/ss92.png) + +You can step through code, inspect, etc., just like you are debugging any other Python project. We hope that you'll find a great debugging solution for your needs in one of these options. We've added a couple of new API methods on the plugin objects that are part of this change: + +- `plugin.isRunning()` -- will return true if the plugin is enabled, initialized, and running, and +- `plugin.restartAndDebug()` -- a parallel to the `restart()` method except that it starts the plugin running in the selected debugger. + +##### PyCharm Plugin Restart Tool + +In PyCharm, you can add "External Tools" like linters and custom tools as you build out your development environment. This script is a convenience tool which allows you to restart an Indigo plugin directly from PyCharm. For example, you might make changes to your plugin code and need to restart the plugin to continue debugging. This approach allows you to do this all through the PyCharm UI. The Python code you need for the external tool is: + +```python +#! /usr/bin/python3 + +import os +import sys +import argparse +import plistlib + +def searchFile(fileName, path): + return None + +def main(argv): + parser = argparse.ArgumentParser( + description="Restart a plugin given its full name or ID" + ) + parser.add_argument( + "project_directory", + help="the project directory in which to search", + type=str + ) + + args = parser.parse_args() + + info_plist = None + found = False + for root, dirs, files in os.walk(args.project_directory, topdown=False): + if not found: + for name in files: + if name == "Info.plist": + info_plist = os.path.join(root, name) + found = True + break + if info_plist: + with open(info_plist, "rb") as f: + plist_dict = plistlib.load(f, fmt=plistlib.FMT_XML) + os.system(f"/usr/local/indigo/indigo-restart-plugin -d -n {plist_dict['CFBundleIdentifier']}") + else: + print("Info.plist not found") + +if __name__ == "__main__": + main(sys.argv[1:]) +``` + +And you would set up the external tool with these settings: + +![Remote Plugin Restart Image](../../images/remote_plugin_restart.png) + +Now, when you use that External Tool, it will reload your plugin with the debugger enabled. You can even configure your debug run configuration to run this as a **Before launch** tool (see the configuration image above). So every time you run the debug configuration, it will also restart the plugin. + +#### Configure Remote Debugging (Experimental) + +The above local debugging configuration presumes that the Indigo Server and the plugin you are debugging are running on the same machine on which you are running PyCharm. It's also possible (and experimental at this point) to debug a plugin running on an Indigo Server on a separate Mac on the network from the one on which you are running PyCharm. We're listing this approach as experimental for the time being as it hasn't been fully tested; however, the implementation shows promise. + +The setup isn't radically different than the local configuration outlined above: it's the same "Python Debug Server" configuration, EXCEPT that you specify an IP address. In the Debug configuration in PyCharm you specify the IP address of the Mac _PyCharm_ is running on (local won't work, it has to be the IP address). In the plugin's init method, make the very first line of the init method: + +```python +def __init__(self, pluginId, pluginDisplayName, pluginVersion, pluginPrefs): + indigo.DEBUG_SERVER_IP = "192.168.1.24" # IP address of the Mac running PyCharm + super(Plugin, self).__init__(pluginId, pluginDisplayName, pluginVersion, pluginPrefs) + self.debug = self.pluginPrefs.get("showDebugInfo", False) + # Etc... +``` + +Note that it **must** happen before you call the `super` method. You can use the same port number (_5678_ by default) or you can specify a custom port as well by setting `indigo.DEBUG_SERVER_PORT` along with the IP. Another note: you must remove any custom IP addresses/ports that you specify in the plugin before distributing your plugin to avoid any erroneous error messages during startup on users' Indigo Servers. + +The next requirement is that the **EXACT** same version of PyCharm has to be installed in the remote Mac's `/Applications/` directory. It doesn't have to be licensed (and you don't have to launch it). It needs to be there and match exactly because Indigo looks for the debug module inside the PyCharm application bundle, and it has to be the exact same module on both sides of the connection. You might be tempted to try to install it from pip3, and while it looks like the same pydevd-pycharm version exists on pypi (the pip repository) that PyCharm is using, we've found that it just doesn't work consistently when installed from pip. It's best to put PyCharm on the remote Indigo Server Mac (and make sure PyCharm is the same version on both Macs). + +### Plugin Specific Interactive Shell + +Another great debugging tool is the ability to open a scripting shell that's specific to your plugin's context. This shell is like the more general shell you get when you select the **Plugins->Open Scripting Shell** menu item, except that because we launch it as part of your plugin's startup, it has access to everything in your plugin. You can call methods that your plugin implements, inspect your plugin's objects, etc. + +### Detecting if Your Plugin is in Debug Mode + +It may be useful for your plugin to be able to detect whether it has been loaded in Debug Mode (Plugin Menu > My Plugin > Enable/Reload in Debug Mode) and what debugger it's loaded in. This can reduce the kinds of "one off" debug logging levels that might be needed in your plugin. To do this, call this command: + +```python +indigo.host.debugMode() +``` + +Possible values are currently these integers: + +```text +kPluginDebugMode_none = 0, +kPluginDebugMode_debugPdb = 100, +kPluginDebugMode_debugPudb, +kPluginDebugMode_debugPyCharm, +kPluginDebugMode_debugShell = 200 +``` + +## Tips, Tricks and Best Practices + +### External Requirements + +Python has a huge collection of libraries/modules available on [pypi.org](https://pypi.org) and installable with the `pip3` command. Your plugin can use those libraries, and by specifying which of those libraries in a `requirements.txt` file, Indigo will automatically install those when your plugin first starts. Check out the [Python Packages for Plugin Developers](../../scripting/guides/python-packages.md) section of the developer docs (**starting with the 2023.2 release**) for more details. + +One option for setting up your development environment would be to create a virtual environment into which you install/manage the libraries that your plugin will need. This is best done when you first start working on a plugin or when you make significant changes (additional libraries, etc.) There's lots of information available online regarding setting up and working with virtual environments -- it may be best to start at [the source](https://docs.python.org/3.11/library/venv.html). + +Once you create a virtual environment (let's say you named it `venv`), this is what its directory structure will look like: + +```text +- venv + - bin + - lib + - python3.XX + - site-packages + - pyvenv.cfg +``` + +The `site-packages` directory is where any pip-installed modules (in that venv) will be installed. You can make a symbolic link from that directory as your `Contents/Packages` directory using a command like this: + +```text +ln -s /path/to/venv/lib/python3.XX/site-packages /path/to/YourPlugin.indigoPlugin/Contents/Packages +``` + +and then you can do this: + +```text +touch /path/to/YourPlugin.indigoPlugin/Contents/Packages/pip-install-log-success.txt +``` + +This will keep the plugin host process from trying to install any requirements.txt file that you have in the `Server Plugin` folder. + +Then, you just manage the packages in the `venv` as necessary until you are satisfied that you have everything working. Once you are ready to release your plugin, then you can generate the requirements.txt file (make sure the terminal window you're using has the `venv` activated): + +```text +pip3 freeze > "/path/to/YourPlugin.indigoPlugin/Contents/Server Plugin/requirements.txt" +``` + +And this will write out the packages you have installed in your `venv` that the host process will install for the user. The last step would be to remove (or just move out of the way) the `Packages` directory, and create a release for your plugin using whatever method you currently use. + +### Indigo Utilities + +Indigo ships with several command line utilities -- several of which can help you with your development efforts. The tools can be found in the `/usr/local/indigo` folder. From the command prompt, type: + +- `indigo-restart-plugin` - to restart a plugin. Pass the plugin ID as an argument: `indigo-restart-plugin com.myOrganization.my_plugin`. For security reasons, you can not start a plugin that was not previously running and you can not stop a running plugin. Only `Restart` is allowable and only one plugin can be restarted at a time. +- `indigo-clean-and-zip-plugin` - to package a plugin file for distribution (more below). +- `indigo-start` - to start the Indigo server. This utility will only work if the "Auto start Indigo Server on user login" option is checked within the Indigo Server preferences pane. +- `indigo-host` - to start an interactive shell session with the Indigo server (the same as if started from the Plugins menu in the Indigo UI). +- `indigo-stop` - to send a stop signal to the Indigo server. + +**_The indigo-clean-and-zip-plugin utility is especially useful for developers_**. You can use this utility to prepare your `indigoPlugin` file for distribution. The utility will inspect the plugin package and remove unnecessary files (such as `.pyc` files) and zip the plugin package to the same location as the plugin file. **_Using the clean and zip tool before publishing your plugin is highly recommended._** + +### Linters + +There are several utilities available to improve your code; one type of utility is often referred to as a "Linter". Linters inspect your code for things that you might want to address including: + +- Syntax problems, +- Duplicated code segments, +- typos, and +- other potential issues. + +Even if your code is running the way you expect, a linter may still suggest potential improvements. + +### How to Tell if a Plugin was Started in Debug Mode + +If you want to know whether your plugin was started in debug mode, you can call `indigo.host.debugMode`. The method will return an int, so you'll need to cast it to a bool. If it's `True` it's in debug mode, otherwise `False`. + +```python +debug_mode = indigo.host.debugMode +debug_mode_bool = bool(debug_mode) +``` + +### No Module Named 'indigo' + +Your preferred IDE may provide some automated syntax checking, highlighting potential errors in your code. Typically, plugins will often make references to the Indigo base class `indigo.*` and your syntax checker might alert you that it can't find the indigo module. This is to be expected because you can't import Indigo into a plugin like you can with standard Python modules. However, you can minimize the number of syntax error alerts by "tricking" your IDE by including the following with your other import statements: + +```python +try: + import indigo +except ImportError: + pass +``` + +You will likely still get a syntax error warning on the `import indigo` line, but this is preferable to many, many syntax warnings throughout your code base. diff --git a/reference/canonical/plugin-dev/reference/plugin-py.md b/reference/canonical/plugin-dev/reference/plugin-py.md new file mode 100644 index 0000000..625f40f --- /dev/null +++ b/reference/canonical/plugin-dev/reference/plugin-py.md @@ -0,0 +1,29 @@ + + +# Plugin Method Reference + +!!! abstract "In this guide" + This page is a complete reference for `plugin.py` — the main Python class that every Indigo plugin must implement — + covering lifecycle methods, device and action callbacks, logging, and HTTP request handling. + +## plugin.py +Once you have your UI all described, it’s time to write some code. Your `plugin.py` file is just like any other Python file - it will start with any `import` statements to include various libraries, and it can define global variables. The most important part of the `plugin.py` file is the definition of your plugin’s main class: + +`class Plugin(indigo.PluginBase):` + +This is the class that will define all the entry points into your plugin from the host process and the object bridge between your Python objects and the host process’s C++ objects. Your class must inherit from the `indigo.PluginBase` class. A quick note here - all bridge objects and communication with the IndigoServer will be done through the `indigo` module. Because it’s so important, we automatically import it for you so you don’t need an import statement. + +There are a few methods that the host process will call at various times during your plugins lifecycle, some required and others are optional. + +!!! note "Subscribing to Object Changes" + Some of these methods may require you to [subscribe to object changes](../../../scripting/iom-concepts.md#subscribing-to-object-change-events) - specifically, if they're objects that your plugin didn't directly create (devices of other types) or other object types (triggers, schedules, variables). Often, those subscriptions should be made in the `startup()` method. + +## In This Section + +The `Plugin` class (subclass of `indigo.PluginBase`) implements your plugin through a set of callbacks: + +- **[General Plugin Methods](general-methods.md)** — lifecycle: startup, shutdown, concurrent thread, prefs. +- **[Device Methods](device-methods.md)** · **[Trigger Methods](trigger-methods.md)** · **[Variable Methods](variable-methods.md)** — per-object-type callbacks. +- **[Helper Methods](helper-methods.md)** · **[Properties](properties.md)**. +- **[Logging](logging.md)** · **[Processing HTTP Requests](http-requests.md)** · **[Event and Message Flow](message-flow.md)**. +- **[Additional Topics](additional-topics.md)** — preferences file, 3rd-party libraries, dev environment. diff --git a/reference/canonical/plugin-dev/reference/plugin-py/additional-topics.md b/reference/canonical/plugin-dev/reference/plugin-py/additional-topics.md new file mode 100644 index 0000000..e715f7b --- /dev/null +++ b/reference/canonical/plugin-dev/reference/plugin-py/additional-topics.md @@ -0,0 +1,17 @@ + + +# Additional Topics + +## Plugin Preferences File +The plugin's preferences are stored in its preferences file. Plugin prefs are cached but are flushed periodically to the actual preference file. It will also automatically flush when the plugin exits. + + +## 3rd Party Python Libraries +Indigo {{ version }} includes a variety of popular 3rd party Python libraries which are described on the [Python Packages](../../../scripting/guides/python-packages.md) page. Any changes to the libraries installed will be detailed there. + +!!! warning + You should not make changes to the packages that Indigo installs. If you need a different package version, you should include it within your plugin package distribution. + + +## Setting Up a Development Environment +Many plugin developers choose to write their code in an IDE (Integrated Development Environment) such as [PyCharm](https://www.jetbrains.com/pycharm/) or [pdb](https://docs.python.org/3/library/pdb.html). There are several tips that will make using IDEs to develop Indigo Plugins more effective on the [Setting Up a Development Environment](../dev-environment.md) page. diff --git a/reference/canonical/plugin-dev/reference/plugin-py/device-methods.md b/reference/canonical/plugin-dev/reference/plugin-py/device-methods.md new file mode 100644 index 0000000..24ae05e --- /dev/null +++ b/reference/canonical/plugin-dev/reference/plugin-py/device-methods.md @@ -0,0 +1,338 @@ + + +# Device Specific Methods + +## deviceCreated() { .ref-head data-toc-label="deviceCreated" } + +This method will get called whenever a new device defined by your plugin is created. In many circumstances you won't need to implement this method since the default behavior — which is to call the `deviceStartComm()` method if the device belongs to your plugin and is enabled — is what you want anyway (see `deviceStartComm()` above for details). However, if you need to know when a device is created but before your plugin is asked to start communicating with it, this method provides that hook. If you implement this method you'll need to call `deviceStartComm()` yourself or duplicate the functionality here. + +You can also have this method called for devices that don't belong to your plugin. If you want to know when all devices are created (and updated/deleted), call `indigo.devices.subscribeToChanges()` to have the IndigoServer send all device creation/update/deletion notifications. As with other change subscriptions, this should be used very sparingly since it's a lot of overhead both for your plugin and, more importantly, for the IndigoServer. + +**Method** + +| Method Name | Required | +|-------------------------------------------------------|----------| +| `deviceCreated(self, dev)` | No | + +**Parameters** + +| Parameter | Description | +|----------------------------------|--------------------------------------------------------------------| +| `dev` | an `indigo.Device` object representing the device that was created | + +**Return Value:** + +| Type | Description | +|------|--------------------------------------| +| None | This method does not return a value. | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +def deviceCreated(self, dev): + # Perform any tasks necessary to make sure the new device is fully configured, and optionally, perform an initial refresh. +``` + +## deviceDeleted() { .ref-head data-toc-label="deviceDeleted" } + +Complementary to the `deviceCreated()` method described above, but signals device deletes. The default implementation just checks to see if the device belongs to your plugin and -- if so -- calls the `deviceStopComm()` method. If you implement this method you'll need to call `deviceStopComm()` yourself or duplicate the functionality here. + +**Method** + +| Method Name | Required | +|-------------------------------------------------------|----------| +| `deviceDeleted(self, dev)` | No | + +**Parameters** + +| Parameter | Description | +|----------------------------------|--------------------------------------------------------------------| +| `dev` | an `indigo.Device` object representing the device that was deleted | + +**Return Value:** + +| Type | Description | +|------|--------------------------------------| +| None | This method does not return a value. | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +def deviceDeleted(self, dev): + # Perform any clean up tasks after a plugin device is deleted +``` + +## deviceStartComm() { .ref-head data-toc-label="deviceStartComm" } + +If your plugin defines devices, this is likely the place where you'll want to do the work of starting your device up. For instance, let's say that you have a device somewhere out on the network - the easiest way to "start" your device is to implement this method. You would open the network address:port (that's defined in `dev.pluginProps`), get it's current state(s) and tell the IndigoServer to set those states (using the `dev.updateStateOnServer()` method). + +**Method** + +| Method Name | Required | +|---------------------------------------------------------|----------| +| `deviceStartComm(self, dev)` | No | + +**Parameters** + +| Parameter | Description | +|----------------------------------|---------------------------------------------------| +| `dev` | an `indigo.Device` object representing the device | + +**Return Value:** + +| Type | Description | +|------|--------------------------------------| +| None | This method does not return a value. | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +def deviceStartComm(self, dev): + # Perform any clean up tasks after communication with a plugin device is (re)established. +``` + +## deviceStopComm() { .ref-head data-toc-label="deviceStopComm" } + +This is the complementary method to `deviceStartComm()` - it gets called when the device should no longer be active/enabled. For instance, when the user disables or deletes a device, this method gets called. + +**Method** + +| Method Name | Required | +|--------------------------------------------------------|----------| +| `deviceStopComm(self, dev)` | No | + +**Parameters** + +| Parameter | Description | +|----------------------------------|---------------------------------------------------| +| `dev` | an `indigo.Device` object representing the device | + +**Return Value:** + +| Type | Description | +|------|--------------------------------------| +| None | This method does not return a value. | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +def deviceStopComm(self, dev): + # Perform any clean up tasks after communication with a plugin device is disabled. +``` + +## deviceUpdated() { .ref-head data-toc-label="deviceUpdated" } + +Complementary to the `deviceCreated()` method described above, but signals device updates. You'll get a copy of the old device object as well as the new device object. The default implementation of this method will do a few things for you: if either the old or new device are devices defined by you, and if the device type changed OR the communication-related properties have changed (as defined by the `didDeviceCommPropertyChange()` method - see above for details) then `deviceStopComm()` and `deviceStartComm()` methods will be called as necessary (stop only if the device changed to a type that isn't your device, start only if the device changed to a type that belongs to you, or both if the props/type changed and they both belong to you). + +**Method** + +| Method Name | Required | +|-------------------------------------------------------------------|----------| +| `deviceUpdated(self, origDev, newDev)` | No | + +**Parameters** + +| Parameter | Description | +|--------------------------------------|---------------------------------------------------------------------| +| `origDev` | an `indigo.Device` object representing the device before the change | +| `newDev` | an `indigo.Device` object representing the device after the change | + +**Return Value:** + +| Type | Description | +|------|--------------------------------------| +| None | This method does not return a value. | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +def deviceUpdated(self, origDev, newDev): + # You are responsible for isolating the te difference(s) between the old and new device objects as needed. +``` + +## didDeviceCommPropertyChange() { .ref-head data-toc-label="didDeviceCommPropertyChange" } + +This method gets called by the default implementation of `deviceUpdated()` to determine if any of the properties needed for device communication (or any other change requires a device to be stopped and restarted). The default implementation checks for any changes to properties. You can implement your own to provide more granular results. For instance, if your device requires 4 parameters, but only 2 of those parameters requires that you restart the device, then you can check to see if either of those changed. If they didn't then you can just return False and your device won't be restarted (via `deviceStopComm()`/`deviceStartComm()` calls). + +**Method** + +| Method Name | Required | +|---------------------------------------------------------------------------------|----------| +| `didDeviceCommPropertyChange(self, origDev, newDev)` | No | + +**Parameters** + +| Parameter | Description | +|--------------------------------------|---------------------------------------------------------------------| +| `origDev` | an `indigo.Device` object representing the device before the change | +| `newDev` | an `indigo.Device` object representing the device after the change | + +**Return Value:** + +| Type | Description | +|------|----------------------------------------------------------------------------| +| bool | True if communication-relevant device properties changed; False otherwise. | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +def didDeviceCommPropertyChange(self, origDev, newDev): + # You are responsible for isolating the te difference(s) between the old and new device objects as needed. +``` + +## getDeviceConfigUiValues() { .ref-head data-toc-label="getDeviceConfigUiValues" } + +This method will get called whenever a Device configuration is opened. Indigo will look for this method and, if it exists, will pre-populate the configuration dialog with the information created/modified in the method. This method is particularly helpful when you want a Device's configuration to be different from the default (set in the Device configuration XML file). A simple example is provided below. + +**Method** + +| Method Name | Required | +|----------------------------------------------------------------------------------------|----------| +| `getDeviceConfigUiValues(self, pluginProps, typeId, devId)` | No | + +**Parameters** + +| Parameter | Description | +|------------------------------------------|--------------------------------------------------------| +| `pluginProps` | a dictionary of the device's current plugin properties | +| `typeId` | the device type ID string as defined in Devices.xml | +| `devId` | the integer ID of the device being configured | + +**Return Value:** + +| Type | Description | +|-------|-------------------------------------------------------------------------| +| tuple | A `(valuesDict, errorMsgDict)` tuple to pre-populate the config dialog. | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +def getDeviceConfigUiValues(self, pluginProps, typeId, devId): + valuesDict = pluginProps + errorMsgDict = indigo.Dict() + if not valuesDict.get("someProp"): + valuesDict["someProp"] = "default value" + return valuesDict, errorMsgDict +``` + +## getDeviceDisplayStateId() { .ref-head data-toc-label="getDeviceDisplayStateId" } + +If your plugin defines custom devices, this method will be called by the server to determine which device state ID to display in the device list UI state column. The default implementation just returns the `` element in your Devices.xml file. You can, however, implement the method the plugin needs to dynamically determine the which state ID to display. + +**Method** + +| Method Name | Required | +|-----------------------------------------------------------------|----------| +| `getDeviceDisplayStateId(self, dev)` | No | + +**Parameters** + +| Parameter | Description | +|----------------------------------|---------------------------------------------------| +| `dev` | an `indigo.Device` object representing the device | + +**Return Value:** + +| Type | Description | +|------|-------------------------------------------------------------| +| str | The state ID to display in the device list UI state column. | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +def getDeviceDisplayStateId(self, dev): + return dev.states['some_state_id'] +``` + +## getDeviceStateList() { .ref-head data-toc-label="getDeviceStateList" } + +If your plugin defines custom devices, this method will be called by the server when it tries to build the state list for your device. The default implementation just returns the `` element (reformatted as an `indigo.List()` that's available to your plugin via `devicesTypeDict["yourCustomTypeIdHere"]`) in your Devices.xml file. You can, however, implement the method yourself to return a custom set of states. For instance, you may want to allow the user to create custom labels for the various inputs on your device rather than use generic "Input 1", "Input 2", etc., labels. Check out the EasyDAQ plugin which uses this approach. + +Most plugins will not need to subclass `get_device_state_list()` because -- by default -- it returns the `` list as defined in Devices.xml. So only subclass this method if you dynamically need to change the device states list provided based on specific device instance data (not just device types). + +**Method** + +| Method Name | Required | +|------------------------------------------------------------|----------| +| `getDeviceStateList(self, dev)` | No | + +**Parameters** + +| Parameter | Description | +|----------------------------------|---------------------------------------------------| +| `dev` | an `indigo.Device` object representing the device | + +**Return Value:** + +| Type | Description | +|-------------|--------------------------------------------| +| indigo.List | The list of device states for this device. | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +def getDeviceStateList(self, dev): + type_id = dev.deviceTypeId + default_states_list = self.devicesTypeDict[type_id]['States'] + new_states_list = indigo.List() + + for state in default_states_list: + # Make your changes + new_states_list.append(state) + + return new_states_list +``` diff --git a/reference/canonical/plugin-dev/reference/plugin-py/general-methods.md b/reference/canonical/plugin-dev/reference/plugin-py/general-methods.md new file mode 100644 index 0000000..446228a --- /dev/null +++ b/reference/canonical/plugin-dev/reference/plugin-py/general-methods.md @@ -0,0 +1,308 @@ + + +# General Plugin Methods + +## \_\_init\_\_() { .ref-head data-toc-label="\_\_init\_\_" } + +This is where the class is initialized. Here you can initialize class-wide variables, initialize and configure custom logging and so on. + +You have the opportunity to use or alter the prefs before passing them on, but most plugins simply forward them to the base class. You'll most likely use the `startup()` method, described below, to do your global plugin initialization. + +**Method Signature** + +```python +def __init__(self, + pluginId: str, + pluginDisplayName: str, + pluginVersion: str, + pluginPrefs: indigo.Dict) -> None: +``` + +**Parameters** + +| Parameter | Description | +|------------------------------------------------|------------------------------------------------------------------| +| `pluginId` | the bundle identifier of the plugin (e.g. `com.example.myplugin`) | +| `pluginDisplayName` | the human-readable name of the plugin | +| `pluginVersion` | the version string of the plugin | +| `pluginPrefs` | a dictionary of preferences that the server read from disk | + +**Return Values** + +| Type | Description | +|------|--------------------------------------| +| None | This method does not return a value. | + +**Exceptions Raised** + +| Type | Description | +|------|-----------------------------------------------------------------------------------------------------------| +| None | This method should not raise any exceptions. If it does, then that would stop plugin startup immediately. | + +**Command Syntax Examples** + +```python +def __init__(self, pluginId, pluginDisplayName, pluginVersion, pluginPrefs): + super().__init__(pluginId, pluginDisplayName, pluginVersion, pluginPrefs) +``` + + +## \_\_del\_\_() { .ref-head data-toc-label="\_\_del\_\_" } + +This is the destructor for the class. You will almost certainly never need to override this method, but if you do, you'll need call the super class's method when you have finished your code. + +**Method Signature** + +```python +def __del__(self) -> None: +``` +**Parameters** + +| Parameter | Description | +|-----------|---------------------------------------------| +| None | This method does not accept any parameters. | + +**Return Values** + +| Type | Description | +|------|--------------------------------------| +| None | This method does not return a value. | + +**Exceptions Raised** + +| Type | Description | +|------|--------------------------------------------------------------------------------------------------| +| None | This method should not raise any exceptions. If it does, the plugin will shut down ungracefully. | + +**Command Syntax Examples** + +```python +def __del__(self): + super().__del__(self) +``` + +## startup() { .ref-head data-toc-label="startup" } + +This method will get called after your plugin has been initialized. This is really the place where you want to make sure that everything your plugin needs to do gets set up correctly. It's passed no parameters. If you're storing a config parameter that's not editable by the user, this is a good place to make sure it's there and set to the right value. This is not, however, where you want to initialize devices and triggers that your plugin may provide - those are handled after this method completes (see the methods below). + +**Method Signature** + +```python +def startup(self) -> None | bool | str: +``` + +**Parameters** + +| Parameter | Description | +|-----------|---------------------------------------------| +| None | This method does not accept any parameters. | + +**Return Values** + +| Type | Description | +|--------------|--------------------------------------------------------| +| None or True | Plugin starts up normally. | +| False | Plugin stops with a default message. | +| str | Plugin stops with the string used as the stop message. | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +def startup(self): + indigo.server.info(u"Startup called") + # if your plugin needs to connect to a single service and remain connected + # this is a good place to do it + self.connection = start_some_connection() + +``` + +## runConcurrentThread() { .ref-head data-toc-label="runConcurrentThread" } + +This method is called in a newly created thread after the `startup()` method finishes executing. It's expected that it should run a loop continuously until asked to shut down. + +You must call `self.sleep()` with the number of seconds to delay between loops. `self.sleep()` will raise an `self.StopThread` exception when you should end `runConcurrentThread`. You don't have to catch that exception if you don't need to do any cleanup before returning - it will just throw out to the next level. Note: shutdown will be called after `runConcurrentThread` finishes processing so you can do your cleanup there. + +**Method** + +| Method Name | Required | +|--------------------------------------------------------|----------| +| `runConcurrentThread(self)` | No | + +**Parameters** + +| Parameter | Description | +|-----------|---------------------------------------------| +| None | This method does not accept any parameters. | + +**Return Value:** + +| Type | Description | +|------|--------------------------------------| +| None | This method does not return a value. | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +def runConcurrentThread(self): + try: + while True: + # Do your stuff here + self.sleep(60) # in seconds + except self.StopThread: + # do any cleanup here + pass +``` + +## stopConcurrentThread() { .ref-head data-toc-label="stopConcurrentThread" } + +This method will get called when the IndigoServer wants your plugin to stop any threads that it may have created. The default implementation (below) will set the stopThread instance [variable](../../../user/glossary.md) which causes the `self.sleep()` method to throw the exception that you handle in runConcurrentThread above. In most circumstances, your plugin won't need to implement this method. + +**Method** + +| Method Name | Required | +|---------------------------------------------------------|----------| +| `stopConcurrentThread(self)` | No | + +**Parameters** + +| Parameter | Description | +|-----------|---------------------------------------------| +| None | This method does not accept any parameters. | + +**Return Value:** + +| Type | Description | +|------|--------------------------------------| +| None | This method does not return a value. | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +def stopConcurrentThread(self): + self.stopThread = True +``` + +## prepareToSleep() { .ref-head data-toc-label="prepareToSleep" } + +The default implementation of this method will call `deviceStopComm()` for each device instance and `triggerStopProcessing()` for each [trigger](../../../user/glossary.md) instance provided by your plugin. You can of course override them to do anything you like. + +**Method** + +| Method Name | Required | +|---------------------------------------------------|----------| +| `prepareToSleep(self)` | No | + +**Parameters** + +| Parameter | Description | +|-----------|---------------------------------------------| +| None | This method does not accept any parameters. | + +**Return Value:** + +| Type | Description | +|------|--------------------------------------| +| None | This method does not return a value. | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +def prepareToSleep(self): + # Perform any tasks that should be done before the server machine sleeps +``` +## wakeUp() { .ref-head data-toc-label="wakeUp" } + +The default implementation of this method will call `deviceStartComm()` for each device instance and `triggerStartProcessing()` for each [trigger](../../../user/glossary.md) instance provided by your plugin. You can of course override them to do anything you like. + +**Method** + +| Method Name | Required | +|-------------------------------------------|----------| +| `wakeUp(self)` | No | + +**Parameters** + +| Parameter | Description | +|-----------|---------------------------------------------| +| None | This method does not accept any parameters. | + +**Return Value:** + +| Type | Description | +|------|--------------------------------------| +| None | This method does not return a value. | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +def wakeUp(self): + # Perform any tasks that should be done immediately upon waking the server +``` + +## shutdown() { .ref-head data-toc-label="shutdown" } + +This method will get called when the IndigoServer wants your plugin to exit. If you define a global shutdown [variable](../../../user/glossary.md), this is the place to set it. Other things you might do in this method: if your plugin uses a single interface to talk to multiple devices, this is the place where you would want to shut down that interface (close the serial port or network connection, etc.) Each device and [trigger](../../../user/glossary.md) will already have had a chance to shut down by the time this method is called (see the methods below). + +!!! note + `shutdown()` will be called after runConcurrentThread (discussed above) so any cleanup here will be performed after any changes that might result from a loop in runConcurrentThread. + +**Method** + +| Method Name | Required | +|---------------------------------------------|----------| +| `shutdown(self)` | No | + +**Parameters** + +| Parameter | Description | +|-----------|---------------------------------------------| +| None | This method does not accept any parameters. | + +**Return Value:** + +| Type | Description | +|------|--------------------------------------| +| None | This method does not return a value. | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +def shutdown(self): + # do any cleanup necessary before exiting +``` diff --git a/reference/canonical/plugin-dev/reference/plugin-py/helper-methods.md b/reference/canonical/plugin-dev/reference/plugin-py/helper-methods.md new file mode 100644 index 0000000..d35535f --- /dev/null +++ b/reference/canonical/plugin-dev/reference/plugin-py/helper-methods.md @@ -0,0 +1,336 @@ + + +# Helper Methods + +## applicationWithBundleIdentifier() { .ref-head data-toc-label="applicationWithBundleIdentifier" } + +What's returned is a scripting bridge SBApplication instance. See the [Scripting Bridge documentation](http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/ScriptingBridgeConcepts/Introduction/Introduction.html) for more information. + +**Method** + +| Method Name | +|------------------------------------------------------------------------------| +| `applicationWithBundleIdentifier(self, bundleID)` | + +**Return Value:** + +| Type | Description | +|---------------|------------------------------------------| +| SBApplication | A Scripting Bridge application instance. | + +**Parameters** + +| Parameter | Description | +|---------------------------------------|----------------------------------------------------------------------------------------------| +| `bundleID` | the bundle identifier for the app (usually a fully qualified string like `com.apple.iTunes`) | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +app = self.applicationWithBundleIdentifier("com.apple.iTunes") +if app: + app.playpause() +``` + +## browserOpen() { .ref-head data-toc-label="browserOpen" } + +This method will open the specified URL in the default browser. Note it does so on the server machine and not on any remotely connected clients. + +**Method** + +| Method Name | +|-----------------------------------------------------| +| `browserOpen(self, url)` | + +**Return Value:** + +| Type | Description | +|------|--------------------------------------| +| None | This method does not return a value. | + +**Parameters** + +| Parameter | Description | +|----------------------------------|--------------------------------| +| `url` | the URL to open in the browser | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +self.browserOpen("https://www.indigodomo.com") +``` + +## debugLog() { .ref-head data-toc-label="debugLog" } + +!!! warning "Deprecated" + (See Logging below) If, at any point in your plugin, you set `self.debug = True`, then any time debugLog is called the string will get inserted into Indigo's event log. If `self.debug = False` (the default) any call to debugLog does nothing. + +**Method** + +| Method Name | +|--------------------------------------------------| +| `debugLog(self, msg)` | + +**Return Value:** + +| Type | Description | +|------|--------------------------------------| +| None | This method does not return a value. | + +**Parameters** + +| Parameter | Description | +|----------------------------------|-----------------------------------------| +| `msg` | the string to insert into the event log | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +self.debugLog("This is a debug message") # deprecated; use self.logger.debug() instead +``` + +## errorLog() { .ref-head data-toc-label="errorLog" } +!!! warning "Deprecated" + See Logging below If you want an error to show up in the event log (in red text), use this log method rather than `indigo.server.log()`. + + +**Method** + +| Method Name | +|--------------------------------------------------| +| `errorLog(self, msg)` | + +**Return Value:** + +| Type | Description | +|------|--------------------------------------| +| None | This method does not return a value. | + +**Parameters** + +| Parameter | Description | +|----------------------------------|-----------------------------------------| +| `msg` | the string to insert into the event log | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +self.errorLog("An error occurred") # deprecated; use self.logger.error() instead +``` + +## openSerial() { .ref-head data-toc-label="openSerial" } + +This method is identical to creating a new [pySerial Serial object](http://pyserial.sourceforge.net/pyserial_api.html#classes) except that it never throws an exception. If the serial connection cannot be opened then None is returned and an error will be automatically logged to the Indigo Server event log. + +**Method** + +| Method Name | +|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `openSerial(self, ownerName, portUrl, baudrate, bytesize, parity, stopbits, timeout, xonxoff, rtscts, writeTimeout, dsrdtr, interCharTimeout)` | + +**Return Value:** + +| Type | Description | +|---------------|------------------------------------------------------------------| +| serial.Serial | The opened serial port object, or None if the connection failed. | + +**Parameters** + +| Parameter | Description | +|---------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------| +| `ownerName` | the name of the device or plugin that owns this serial port (used for error logging); must be ASCII text with no Unicode characters | +| `all other args` | passed directly to [pySerial's Serial constructor](http://pyserial.sourceforge.net/pyserial_api.html#classes) | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +self.serial_port = self.openSerial( + dev.name, dev.pluginProps["portUrl"], + baudrate=9600, bytesize=8, parity="N", + stopbits=1, timeout=1.0, xonxoff=False, + rtscts=False, writeTimeout=1.0, dsrdtr=False, + interCharTimeout=None +) +if self.serial_port is None: + self.logger.error(f"Unable to open serial port for \"{dev.name}\"") +``` + +## sleep() { .ref-head data-toc-label="sleep" } + +This method should be called from within your plugin's `runConcurrentThread()` defined method, if it is defined. It will automatically raise the `StopThread` exception when the Indigo Server is trying to shut down or restart the plugin. See `runConcurrentThread` documentation above for more details. + +**Method** + +| Method Name | +|---------------------------------------------------| +| `sleep(self, seconds)` | + +**Return Value:** + +| Type | Description | +|------|--------------------------------------| +| None | This method does not return a value. | + +**Parameters** + +| Parameter | Description | +|--------------------------------------|-------------------------------------| +| `seconds` | the sleep duration as a real number | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +self.sleep(60) # sleep for 60 seconds; raises StopThread on plugin shutdown +``` + +## substituteVariable() { .ref-head data-toc-label="substituteVariable" } + +This method will allow any string with the following markup to have a variable value substituted: `%%v:VARID%%` where VARID is the unique variable ID as found in the UI. It's recommended that you call this method twice: first during validation to check syntax and confirm the variable exists, and again at action execution time to perform the substitution. Errors will show up in the event log if the variable doesn't exist (or if there's a formatting problem) at action execution time. + +**Method** + +| Method Name | +|-------------------------------------------------------------------------------------| +| `substituteVariable(self, inString, validateOnly=False)` | + +**Return Value:** + +| Type | Description | +|-------------|-------------------------------------------------------------------------| +| str | The substituted string (when `validateOnly` is False). | +| (bool, str) | A `(isValid, errorString)` tuple (when `validateOnly` is True). | + +**Parameters** + +| Parameter | Description | +|-------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `inString` | the string which contains a valid variable ID | +| `validateOnly` | if False (default), returns the substituted string; if True, returns a `(bool, errStr)` tuple indicating whether the syntax is valid and the variable exists | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +# Validate during UI validation, then substitute at execution time +is_valid, err_msg = self.substituteVariable(action.props["varString"], validateOnly=True) +if is_valid: + result = self.substituteVariable(action.props["varString"]) +``` + +## substituteDeviceState() { .ref-head data-toc-label="substituteDeviceState" } + +This method will allow any string with the following markup to have a device state value substituted: `%%d:DEVICEID:STATEKEY%%` where DEVICEID is the unique device ID and STATEKEY is the identifier for the state. It's recommended that you call this method twice: first during validation, and again at action execution time. Errors will show up in the event log if the device doesn't exist (or if there's a formatting problem) at action execution time. + +**Method** + +| Method Name | +|----------------------------------------------------------------------------------------| +| `substituteDeviceState(self, inString, validateOnly=False)` | + +**Return Value:** + +| Type | Description | +|-------------|-------------------------------------------------------------------------| +| str | The substituted string (when `validateOnly` is False). | +| (bool, str) | A `(isValid, errorString)` tuple (when `validateOnly` is True). | + +**Parameters** + +| Parameter | Description | +|-------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `inString` | the string which contains a valid device ID and state key | +| `validateOnly` | if False (default), returns the substituted string; if True, returns a `(bool, errStr)` tuple indicating whether the syntax is valid and the device exists | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +# Validate during UI validation, then substitute at execution time +is_valid, err_msg = self.substituteDeviceState(action.props["devString"], validateOnly=True) +if is_valid: + result = self.substituteDeviceState(action.props["devString"]) +``` + +## substitute() { .ref-head data-toc-label="substitute" } + +Validation works the same and should be called when your dialog validates user input. This method calls `substituteVariable()` first followed by `substituteDeviceState()`. The ordering was carefully chosen such that the variable substitution could, in fact, add more device markup to the string before the device substitution happens. So the user can even more dynamically generate content by inserting device markup into a variable value. However, only device markup will be honored in variable values - we don't recursively call variable markup on variable values. + +**Method** + +| Method Name | +|-----------------------------------------------------------------------------| +| `substitute(self, inString, validateOnly=False)` | + +**Return Value:** + +| Type | Description | +|-------------|-------------------------------------------------------------------------| +| str | The substituted string (when `validateOnly` is False). | +| (bool, str) | A `(isValid, errorString)` tuple (when `validateOnly` is True). | + +**Parameters** + +| Parameter | Description | +|-------------------------------------------------------------------------------------------------------------------------------|-------------| +| `inString` and `validateOnly` as described in `substituteVariable()` and `substituteDeviceState()` | | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +# Validate during UI validation, then substitute at execution time +is_valid, err_msg = self.substitute(action.props["inputString"], validateOnly=True) +if is_valid: + result = self.substitute(action.props["inputString"]) +``` diff --git a/reference/canonical/plugin-dev/reference/plugin-py/http-requests.md b/reference/canonical/plugin-dev/reference/plugin-py/http-requests.md new file mode 100644 index 0000000..ca9e040 --- /dev/null +++ b/reference/canonical/plugin-dev/reference/plugin-py/http-requests.md @@ -0,0 +1,138 @@ + + +# Processing HTTP requests in your plugin + +Your plugin can process arbitrary HTTP GET or POST requests that are sent to a specific Indigo Web Server (IWS) URL: + +`https://myreflector.indigodomo.net/message/PLUGINID/actionId/` + +Substitute the ID of your plugin (as specified in its Info.plist) and the ID of the action that will handle the request (as specified in the Actions.xml). You can also use the direct IP address instead of your reflector. + +The HTTP request must authenticate if the IWS server has authentication enabled: + +- The recommended approach is to use an API key: the request must contain an "Authorization" HTTP header, the value of which is "Bearer API_KEY_HERE", where you substitute an API key that is generated from the [Authorizations page](https://www.indigodomo.com/account/authorizations) in the user's Indigo Account. This is also how OAuth is used to authenticate when using an external service like Alexa or Google Home. +- If for some reason you can't include the header, you may pass the API key as an additional GET argument on the URL (`?other=args&api-key=KEYHERE`) + +## Request +As a reminder, here's how you specify an action in Actions.xml that is used only via an API (from another plugin such as the IWS plugin): + +**Command Syntax Examples** + +```xml + + some message + handle_some_action + +``` + +And here's how an action method is defined in your plugin: + +```python +def handle_some_action(self, action, dev=None, callerWaitingForResult=None): + some_value = action.props["somekey"] + return some_value +``` + +Given these examples, here's the URL that would get directed to that action: + +`https://myreflector.indigodomo.net/message/com.your.pluginId/handle_message/` + +Calls via this method will always pass in `callerWaitingForResult=True`. You will want to make sure that your plugin returns as quickly as possible - any long-running processes should be put into another thread with some sort of asynchronous message back to the caller if necessary. + +IWS will insert several things into the `action.props` dictionary: + +| **Key** | **Value** | +|------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `incoming_request_method` | this will be either **POST** or **GET**. | +| `headers` | this is a dictionary of the headers in the request. | +| `body_params` | if this key exists, it will be a dictionary containing any form **POST** name/value pairs. It won't exist if the request was a **GET** or a **POST** *with a body*. | +| `url_query_args` | if there were any query args on the URL line, they will be in this dictionary if it exists. | +| `request_body` | this will be the contents of the body of the HTTP request. It won't exist if the request was a **GET** or a **POST** *without any body*. | +| `file_path` | this will be a list of path parts from the URL after the action ID. So for the url: `http://host/message/pluginid/actionid/path/to/some/file.txt` the value will be an `indigo.List` with the following items: `["path", "to", "some", "file.txt"]` | + +!!! note + The dicts mentioned above will all be *indigo.Dict* objects, not standard Python dicts. + +### Validating the request payload + +When you handle a JSON (or form) message, you'll typically want to validate the incoming fields +before acting on them. The [`indigo.utils.ValidationError`](../../../scripting/reference/utils.md#validationerror) +exception is handy here: accumulate per-field errors, raise once, and return a readable error +message with an appropriate HTTP status. The same validation helper can be shared with your +[Config UI validation methods](../xml/configui/validation.md#using-validationerror). + +```python +import json + +def handle_some_action(self, action, dev=None, callerWaitingForResult=None): + try: + payload = json.loads(action.props.get("request_body", "{}")) + errors = indigo.utils.ValidationError("Invalid request payload") + if "name" not in payload: + errors.add_error("name", "the 'name' field is required") + if "value" in payload and not indigo.utils.is_int(payload["value"]): + errors.add_error("value", "'value' must be a number") + errors.raise_if_errors() # raises only if an error was added + except (ValueError, indigo.utils.ValidationError) as exc: + # ValueError covers malformed JSON from json.loads() + return {"status": 400, "content": str(exc)} + + # ...payload is valid, do the work and return a reply... + return json.dumps({"result": "ok"}) +``` + +## Reply +What your plugin should return in the simplest case is a JSON string which will just be passed back in the HTTP reply. Your plugin can also return a more complex dictionary containing the following keys: + +| **Key** | **Value** | +|--------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `status` | this is an integer representing a valid [HTTP return code](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes). If it's not included, a 200 will be returned. | +| `headers` | a dictionary of HTTP headers that will be added to the reply. Most useful will be the 'Content-Type' header if you want to return something else besides JSON (which is the default). | +| `content` | this is the actual string returned in the HTTP reply. Defaults to an empty string. | + + +This will allow you to return just about anything - HTML, XML, plain text, etc. IWS will do **no postprocessing** of the content string, so you must ensure that you are returning the properly formatted information that the requester is expecting. + +You can also pass back an `indigo.Dict` instance that will instruct IWS to stream a file from the filesystem back to the requester. This is the structure: + +**Command Syntax Examples** + +```text +{ + "status": 310, # Internal status code indicating that IWS should stream back the specified file. + "file_path": path, # this is a full path string + "headers": indigo.Dict({"Content-Type": content_type} # you should add a Content-Type header +) +``` + +We've provided [a utility method](../../../scripting/reference/utils.md#return-static-file) that will validate that the file specified exists and then will pass back the appropriate `indigo.Dict` instance. + +## Errors +IWS will return the following HTTP error responses if an error occurs trying to process a request: + +| **Status** | **Meaning** | +|----------------------------------|----------------------------------------------------------------------------------| +| `401` | if the OAuth token is invalid. | +| `405` | if any method other than **POST** or **GET** is attempted. | +| `500` | any unexpected/uncaught exception. | +| `501` | if the plugin isn't installed or doesn't define the action specified in the URL. | +| `503` | if the plugin is installed but is disabled. | + +For `50x` errors, a JSON dictionary will be returned describing the issue: + +| **Key** | **Value** | +|------------------------------------------|----------------------------------------------------------------------------------------------------------| +| `error` | One of: `plugin_disabled`, `invalid_plugin`, `invalid_action`, `unknown_error` | +| `description` | A textual description of the error | +| `exception` | Only returned when an `unknown_error` is returned. It will be the stack trace of the uncaught exception. | + +If your handler returns any kind of error (4xx or 5xx) and includes a message, that message will be returned to the caller as the body of the HTTP reply. This will allow you to return a custom page (404 for instance) that will more appropriately reflect the error. + +### Usage Guidance +This API is meant primarily for small(ish) text message handling, like JSON/XML messaging APIs or small HTML files. There are a few things that you will want to avoid: + +- Long-running actions - if your action should be relatively quick to respond: 15-20 seconds is the max guidance +- Large files - large files will slow the total turnaround time, which you need to minimize (see above) +- Binary data - the API isn't designed for binary data + +For long-running actions, one pattern would be to reply immediately with some kind of acknowledgement of the incoming message, then handle the processing asynchronously. As this approach would complete the HTTP request/response loop, if your caller needs some kind of status after processing you would need to handle that yourself. diff --git a/reference/canonical/plugin-dev/reference/plugin-py/logging.md b/reference/canonical/plugin-dev/reference/plugin-py/logging.md new file mode 100644 index 0000000..8308f8d --- /dev/null +++ b/reference/canonical/plugin-dev/reference/plugin-py/logging.md @@ -0,0 +1,171 @@ + + +# Logging + +In previous versions of the API, the plugin base class defined three methods: `debugLog()`, `errorLog()`, and `exceptionLog()`. These were deprecated in favor of the standard [Python Logging Module](https://docs.python.org/2/library/logging.html) (don't worry, the previous APIs will continue to work). + +The plugin base now has a couple of new attributes related to logging: + +| Attribute | Description | +|--------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `logger` | An instance of the [standard Python Logging class](https://docs.python.org/2/library/logging.html#logger-objects). You should use this instance to log messages. See the examples below for details. | +| `indigo_log_handler` | An instance of a special [Python Handler class](https://docs.python.org/2/library/logging.html#handler-objects) that emits messages to the Indigo Event Log window and the Indigo Server's log file. | +| `plugin_file_handler` | An instance of a Python [TimedRotatingFileHandler](https://docs.python.org/2/library/logging.handlers.html#timedrotatingfilehandler) which writes you own log files in the /Logs/ directory. A subdirectory will be created using your plugin's ID as the name. Inside that directory will be daily log files (with 20 days of backups) of all log messages from your plugin. See below for examples of the default format of that file. | + +The [Python logging module](https://docs.python.org/3/library/logging.html) is extremely powerful and flexible, and we recommend reading through [the docs](https://docs.python.org/2/library/logging.html) for a good understanding of how it works and how you can add great logging to your plugin. We'll describe the minimum here that you need to know to do basic logging to the Indigo Event Log window and to your plugin's log file. The logger module defines [5 levels of logging](https://docs.python.org/2/library/logging.html#logging-levels) shown below. Instances of the [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object can be set to log messages at any of those 5 levels, and the level logged can be changed at any time. By default, the `self.logger` instance is set to `logging.DEBUG` (we'll see why a bit further down). You can change this if you want using the `setLevel()` method. We've named the `self.logger` instance "Plugin", because it's the name of the class from which your plugin begins. We'll see why this is important later in the examples. + +You can very easily write log messages at any level using the following convenience methods defined in the [Logger](https://docs.python.org/2/library/logging.html#logger-objects) class: + +**Command Syntax Examples** + +```python +self.logger.debug(u"Debug log message") +self.logger.info(u"Info log message") +self.logger.warn(u"Warning log message") +self.logger.error(u"Error log message") +self.logger.critical(u"Critical log message") +``` + +The `debugLog()`, `errorLog()`, and `exceptionLog()` methods are now just wrappers around the corresponding method above. There is another convenience method defined in the logging module: `self.logger.exception("Error log message with exception appended")`. If you call this method from within an `except` block in your plugin, it will automatically create a `logging.ERROR` level message and append the stack trace to whatever message you supply. + +A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) instance can have any number of [Handler](https://docs.python.org/2/library/logging.html#handler-objects) objects associated with it. These handler objects are what actually do the heavy lifting in terms of where log messages go and how those messages are formatted. The plugin base class provides two of them: `self.indigo_log_handler` and `self.plugin_file_handler`, both of which are automatically added to `self.logger`. Therefore, calling any of the 5 methods above (`self.logger.debug`, `self.logger.info`, etc.) will automatically route those messages to both the Indigo Server and the plugin file handler. + +## self.indigo_log_handler +The `self.indigo_log_handler` is an instance of a custom [Handler](https://docs.python.org/3.10/library/logging.html#handler-objects) object that will write (or emit in Python Handler speak) your log messages into the Indigo Event Log. The message type (the left part in the Event Log) is modified so that it reflects the level of the log message (except for the `info` level). For convenience, the various log levels are also represented in color. Here's an example of each level: + +![Embedded Script Logging Image](../../../images/embedded_script_logging.png) + +## self.plugin_file_handler +The `self.plugin_file_handler` is an instance of a [TimedRotatingFileHandler](https://docs.python.org/2/library/logging.handlers.html#timedrotatingfilehandler). This handler is used to write log files with some automatic file management: it can rotate the log files (so they don't get too big), and can be configured to keep some number of backups. By default, we've configured it to rotate the log files at midnight each night and to keep 20 backups. You can, of course, change those settings on the handler. + +[Handler](https://docs.python.org/2/library/logging.html#handler-objects) objects have a [Formatter](https://docs.python.org/2/library/logging.html#formatter-objects) object set, which is how the log line is formatted. By default, we format the log lines with the date/time stamp, the level (e.g. DEBUG), `[the logger name ("Plugin" by default)].[method name]:`, message. Each element is separated by a tab. So, for example, the lines from the methods above will result in these lines in your log file: + +**Command Syntax Examples** + +```text +2016-02-10 15:27:18.194 DEBUG Plugin.runConcurrentThread: Debug logging +2016-02-10 15:27:18.194 INFO Plugin.runConcurrentThread: Info logging +2016-02-10 15:27:18.195 WARNING Plugin.runConcurrentThread: Warning logging +2016-02-10 15:27:18.195 ERROR Plugin.runConcurrentThread: Error logging +2016-02-10 15:27:18.195 CRITICAL Plugin.runConcurrentThread: Critical logging +``` + +So, date time, level, `Plugin.method` (in this case we're writing from the runConcurrentThread method): message. However, if that's not what you want, you can create your own [Formatter](https://docs.python.org/2/library/logging.html#formatter-objects) instance and set the handler to use that instead. + +## Log Levels +Earlier, we mentioned that we set the level of the logger to `logger.DEBUG`. Does this mean that all debug or better messages are automatically sent to both the file and the Indigo Event Log? Actually, no. This is because you can also specify at the handler what level to actually log. We default the `plugin_file_handler` to `logger.DEBUG` but we default the `indigo_log_handler` to `logger.INFO`. The reasoning is that you want the file to have all debugging information, but you are likely to need less logging to the Event Log by default. Again, because you have access to those handlers, you can use their `setLevel()` methods to change them as well, and if you've implemented user selectable debug levels, this should fall right in line with what you're expecting. + +In fact, we've done a little trickery for you so that the old `self.debug` attribute will continue to behave as you might expect. If you have `self.debug` set to `True`, we set the `indigo_log_handler` to `logger.DEBUG`. And if it's `False`, we set it to `logger.INFO`. We also continue to maintain the `self.debug` attribute so your legacy code will continue to work, though that is deprecated as well. + +## Logging from Another Class or Submodule +Logging from another class or submodule is relatively straight-forward. You can either get the logger for the plugin: + +**Command Syntax Examples** + +```python +plugin_logger = logging.getLogger("Plugin") +``` +For example, +```python +class MyClass(object): + + def __init__(self): + self.logger = logging.getLogger("Plugin") + self.logger.debug("MyClass Object") +``` + +and use it directly, or you can pass your logger into the methods of the submodule. You could also pass in the event log handler defined for you by the plugin base (`self.indigo_log_handler`), and then attach that to a custom logger in your module. + +### Custom IndigoLogHandler +If you use the instance of `self.indigo_log_handler`, the message emitted to the Event Log window will have a type that is the name of the plugin. + +If you want log lines with titles other than the plugin name (like a name specific to the submodule), you can instantiate an instance of the `IndigoLogHandler` class (which is what `self.indigo_log_handler` is) instead: + +**Command Syntax Examples** + +```python +custom_logger = logging.getLogger("MyModule") +custom_handler = self.IndigoLogHandler("MyModule", logging.DEBUG) +custom_logger.addHandler(custom_handler) +``` + +And anything logged to your `plugin_logger` will be reflected in the Event Log: + +```text +MyModule Debug Some event log debug message here +``` + +### exc_info +With the logging message `self.logger.critical("Something bad happened.")` you will see the following in the log: + +**Command Syntax Examples** + +```text +My Plugin Error Something bad happened. +My Plugin Error plugin runConcurrentThread function returned or failed (will attempt again in 10 seconds) +``` +which doesn't provide any detail on what actually went wrong. Fortunately, the logging method provides a way to pass more information about the error to the logger with `exc_info`; such as `self.logger.critical("Something bad happened.", exc_info=True)` which yields: + +```text +My Plugin Error Something bad happened. +Traceback (most recent call last): + File "plugin.py", line 123, in runConcurrentThread + x = 1 / 0 +ZeroDivisionError: division by zero + My Plugin Error plugin runConcurrentThread function returned or failed (will attempt again in 10 seconds) +``` + +### Full Example +While there are many different ways to implement logging, here is a "full" example all in one place. + +**Command Syntax Examples** + +```python +import logging + +def __init__(self): + # Get the current logging level from pluginPrefs + self.debugLevel = int(self.pluginPrefs.get('showDebugLevel', "30")) + + # Set preferred log format specifier + log_format = '%(asctime)s.%(msecs)03d\t%(levelname)-10s\t%(name)s.%(funcName)-28s %(message)s' + self.plugin_file_handler.setFormatter( + logging.Formatter(fmt=log_format, datefmt='%Y-%m-%d %H:%M:%S') + ) + self.indigo_log_handler.setLevel(self.debugLevel) + +def runConcurrentThread(self): + self.logger.debug("Starting concurrent thread.") + + try: + x = 1 / 0 + except ZeroDivisionError: + self.logger.critical("Something bad happened.", exc_info=True) +``` +Which yields: +```text +My Plugin Error Something bad happened. +Traceback (most recent call last): + File "plugin.py", line 123, in runConcurrentThread + x = 1 / 0 +ZeroDivisionError: division by zero +``` + +## Logging from Linked and Embedded Scripts +Logging from linked and embedded scripts is very straight-forward. You can simply set the level you want by accessing the `logging.*` level you want: + +**Command Syntax Examples** + +```text +import logging + +indigo.server.log("debug message", level=logging.DEBUG) +indigo.server.log("info message", level=logging.INFO) +indigo.server.log("warning message", level=logging.WARNING) +indigo.server.log("error message", level=logging.ERROR) +indigo.server.log("critical message", level=logging.CRITICAL) +``` + +and then logging messages will appear with the colors above. Result: + +![Embedded Script Logging Image](../../../images/embedded_script_logging.png) diff --git a/reference/canonical/plugin-dev/reference/plugin-py/message-flow.md b/reference/canonical/plugin-dev/reference/plugin-py/message-flow.md new file mode 100644 index 0000000..ca0cd68 --- /dev/null +++ b/reference/canonical/plugin-dev/reference/plugin-py/message-flow.md @@ -0,0 +1,51 @@ + + +# Event and Message Flow +Under some circumstances, the Indigo Server will send callbacks to your plugin based on events that take place. For example, if your plugin devices expose features like Turn On, Turn Off or Status Request, Indigo will send a callback to your plugin so you can take actions on these events. There are many different callbacks that can occur which are documented extensively in the [SDK](https://github.com/IndigoDomotics/IndigoSDK/releases/tag/v2025.1). + +Here is a simple example showing how to handle these callbacks (consult the [SDK](https://github.com/IndigoDomotics/IndigoSDK/releases) for a detailed example that relates to your device's class). + +**Command Syntax Examples** + +```python +def actionControlDevice(self, action, dev): + ###### TURN ON ###### + if action.deviceAction == indigo.kDeviceAction.TurnOn: + # Command hardware module (dev) to turn ON here: + send_success = True # Set to False if it failed. + + if send_success: + # If success then log that the command was successfully sent. + self.logger.info(f"sent \"{dev.name}\" on") + + # And then tell the Indigo Server to update the state. + dev.updateStateOnServer("onOffState", True) + else: + # Else log failure but do NOT update state on Indigo Server. + self.logger.error(f"send \"{dev.name}\" on failed") + + ###### TURN OFF ###### + elif action.deviceAction == indigo.kDeviceAction.TurnOff: + # Command hardware module (dev) to turn OFF here: + send_success = True # Set to False if it failed. + + if send_success: + # If success then log that the command was successfully sent. + self.logger.info(f"sent \"{dev.name}\" off") + + # And then tell the Indigo Server to update the state: + dev.updateStateOnServer("onOffState", False) + else: + # Else log failure but do NOT update state on Indigo Server. + self.logger.error(f"send \"{dev.name}\" off failed") + + ###### STATUS REQUEST ###### + elif action.deviceAction == indigo.kUniversalAction.RequestStatus: + # Report on the status of the device + self.update_device_status() # Take your action(s) to update the device's status. + self.logger.info(f"\"{dev.name}\" updated.") +``` + +Note that you may see camel case examples of these callbacks like `actionControlDevice()` or snake case `action_control_device()`. As a convenience, Indigo supports both naming styles; however, camel case may be deprecated in the future. + +[//]: # (FIXME Add discussion about plugin event and message flow for several types of plugins: with devices, events, actions - and plugins that use runConcurrentThread and plugins that start up threads, etc., so that it'll be easier to envision how to approach thinking about a plugin.) diff --git a/reference/canonical/plugin-dev/reference/plugin-py/properties.md b/reference/canonical/plugin-dev/reference/plugin-py/properties.md new file mode 100644 index 0000000..1c0dc83 --- /dev/null +++ b/reference/canonical/plugin-dev/reference/plugin-py/properties.md @@ -0,0 +1,10 @@ + + +# Properties + +The base plugin provides some properties that are specific to a plugin instance. + +| Property | Value Type | Notes | +|-----------------------------------------------|------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `pluginFolderPath` | string | The return value is the full path to the plugin. This is useful if you need to construct a full path to a file somewhere in the plugin's hierarchy, perhaps to have IWS stream the file back. | +| `pluginSupportURL` | string | The return value is URL that's specified in the plugin's Info.plist. | diff --git a/reference/canonical/plugin-dev/reference/plugin-py/trigger-methods.md b/reference/canonical/plugin-dev/reference/plugin-py/trigger-methods.md new file mode 100644 index 0000000..d4aa85e --- /dev/null +++ b/reference/canonical/plugin-dev/reference/plugin-py/trigger-methods.md @@ -0,0 +1,223 @@ + + +# Trigger Specific Methods + +## didTriggerProcessingPropertyChange() { .ref-head data-toc-label="didTriggerProcessingPropertyChange" } + +Much like it's device counterpart above (`didDeviceCommPropertyChange()`), this method gets called by the default implementation of `triggerUpdated()` to determine if any of the properties needed for recognizing an event have changed. The default implementation checks for any changes to any properties. + +**Method** + +| Method Name | Required | +|------------------------------------------------------------------------------------------------|----------| +| `didTriggerProcessingPropertyChange(self, origTrigger, newTrigger)` | No | + +**Parameters** + +| Parameter | Description | +|------------------------------------------|-----------------------------------------------------------------------| +| `origTrigger` | an `indigo.Trigger` object representing the trigger before the change | +| `newTrigger` | an `indigo.Trigger` object representing the trigger after the change | + +**Return Value:** + +| Type | Description | +|------|--------------------------------------------------------------------------| +| bool | True if processing-relevant trigger properties changed; False otherwise. | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +def didTriggerProcessingPropertyChange(self, origTrigger, newTrigger): + some_property_changed = (origTrigger['some_prop'] == newTrigger['some_prop']) + if some_property_changed: + # Implement any actions required if your target property changed. + return True + else: + return False +``` + +## triggerCreated() { .ref-head data-toc-label="triggerCreated" } + +This method will get called whenever a new trigger defined by your plugin is created. In many circumstances, you won't need to implement this method since the default behavior (which is to call the `triggerStartProcessing()` method if it's your trigger, and it's enabled) is what you want anyway (see the `triggerStartProcessing()` method above for details). However, if for some reason you need to know when a trigger is created, but before your plugin is asked to start watching for the appropriate conditions, this method can provide that hook. If you implement this method, you'll need to either call `triggerStartProcessing()` or duplicate the functionality here. + +You can also have this method called for triggers that don't belong to your plugin. If, for instance, you want to know when all triggers are created (and updated/deleted), you can call the `indigo.triggers.subscribeToChanges()` method to have the IndigoServer send all trigger creation/update/deletion notifications. As with other change subscriptions, this should be used very sparingly since it's a lot of overhead both for your plugin and, more importantly, for the IndigoServer. + +**Method** + +| Method Name | Required | +|------------------------------------------------------------|----------| +| `triggerCreated(self, trigger)` | No | + +**Parameters** + +| Parameter | Description | +|--------------------------------------|----------------------------------------------------------------------| +| `trigger` | an `indigo.Trigger` object representing the trigger that was created | + +**Return Value:** + +| Type | Description | +|------|--------------------------------------| +| None | This method does not return a value. | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +def triggerCreated(self, trigger): + self.triggerStartProcessing(trigger) +``` + +## triggerDeleted() { .ref-head data-toc-label="triggerDeleted" } + +Complementary to the `triggerCreated()` method described above, but signals trigger deletes. The default implementation just checks to see if the trigger belongs to your plugin and if so calls the `triggerStopProcessing()` method. If you implement this method you'll need to call `triggerStopProcessing()` yourself or duplicate the functionality here. + +**Method** + +| Method Name | Required | +|------------------------------------------------------------|----------| +| `triggerDeleted(self, trigger)` | No | + +**Parameters** + +| Parameter | Description | +|--------------------------------------|----------------------------------------------------------------------| +| `trigger` | an `indigo.Trigger` object representing the trigger that was deleted | + +**Return Value:** + +| Type | Description | +|------|--------------------------------------| +| None | This method does not return a value. | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +def triggerDeleted(self, trigger): + self.triggerStopProcessing(trigger) +``` + +## triggerStartProcessing() { .ref-head data-toc-label="triggerStartProcessing" } + +If your plugin defines events, this is likely the place where you'll want to do the work to start watching for those events to occur. For instance, let's say that you have an event for a plugin update, then you'll want to periodically check your site to see if there's a new version available. This is where you'd start that process. When conditions are met in your plugin for a trigger to be executed, you would call indigo.trigger.execute(triggerReference) to tell the Server to execute the trigger (and it's conditions). + +**Method** + +| Method Name | Required | +|--------------------------------------------------------------------|----------| +| `triggerStartProcessing(self, trigger)` | No | + +**Parameters** + +| Parameter | Description | +|--------------------------------------|-----------------------------------------------------| +| `trigger` | an `indigo.Trigger` object representing the trigger | + +**Return Value:** + +| Type | Description | +|------|--------------------------------------| +| None | This method does not return a value. | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +def triggerStartProcessing(self, trigger): + self.active_triggers[trigger.id] = trigger +``` + +## triggerStopProcessing() { .ref-head data-toc-label="triggerStopProcessing" } + +This is the complementary method to `triggerStartProcessing()` - it gets called when the event should no longer be active/enabled. For instance, when the user disables or deletes a trigger, this method gets called. + +**Method** + +| Method Name | Required | +|-------------------------------------------------------------------|----------| +| `triggerStopProcessing(self, trigger)` | No | + +**Parameters** + +| Parameter | Description | +|--------------------------------------|-----------------------------------------------------| +| `trigger` | an `indigo.Trigger` object representing the trigger | + +**Return Value:** + +| Type | Description | +|------|--------------------------------------| +| None | This method does not return a value. | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +def triggerStopProcessing(self, trigger): + if trigger.id in self.active_triggers: + del self.active_triggers[trigger.id] +``` + +## triggerUpdated() { .ref-head data-toc-label="triggerUpdated" } + +Complementary to the `triggerCreated()` method described above, but signals trigger updates. You'll get a copy of the old trigger object as well as the new trigger object. The default implementation of this method will do a few things for you: if either the old or new trigger are triggers defined by you, and if the trigger type changed OR the communication-related properties have changed (as defined by the `didTriggerProcessingPropertyChange()` method - see above for details) then `triggerStopProcessing()` and `triggerStartProcessing()` methods will be called as necessary. + +**Method** + +| Method Name | Required | +|----------------------------------------------------------------------------|----------| +| `triggerUpdated(self, origTrigger, newTrigger)` | No | + +**Parameters** + +| Parameter | Description | +|------------------------------------------|-----------------------------------------------------------------------| +| `origTrigger` | an `indigo.Trigger` object representing the trigger before the change | +| `newTrigger` | an `indigo.Trigger` object representing the trigger after the change | + +**Return Value:** + +| Type | Description | +|------|--------------------------------------| +| None | This method does not return a value. | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +def triggerUpdated(self, origTrigger, newTrigger): + indigo.PluginBase.triggerUpdated(self, origTrigger, newTrigger) +``` diff --git a/reference/canonical/plugin-dev/reference/plugin-py/variable-methods.md b/reference/canonical/plugin-dev/reference/plugin-py/variable-methods.md new file mode 100644 index 0000000..07d26b2 --- /dev/null +++ b/reference/canonical/plugin-dev/reference/plugin-py/variable-methods.md @@ -0,0 +1,113 @@ + + +# Variable Specific Methods { .ref-head-no-code } + +## variableCreated() { .ref-head data-toc-label="variableCreated" } + +This method will get called whenever a new variable is created. You can call the `indigo.variables.subscribeToChanges()` method to have the IndigoServer send all variable creation/update/deletion notifications. As with other change subscriptions, this should be used very sparingly since it's a lot of overhead both for your plugin and, more importantly, for the IndigoServer. + +**Method** + +| Method | Required | +|---------------------------------------------------------|----------| +| `variableCreated(self, var)` | No | + +**Parameters** + +| Parameter | Description | +|----------------------------------|------------------------------------------------------------------------| +| `var` | an `indigo.Variable` object representing the variable that was created | + +**Return Value:** + +| Type | Description | +|------|--------------------------------------| +| None | This method does not return a value. | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +def variableCreated(self, var): + pass # respond to the newly-created variable as needed +``` + +## variableDeleted() { .ref-head data-toc-label="variableDeleted" } + +Complementary to the `variableCreated()` method described above, but signals variable deletes. + +**Method** + +| Method Name | Required | +|---------------------------------------------------------|----------| +| `variableDeleted(self, var)` | No | + +**Parameters** + +| Parameter | Description | +|----------------------------------|------------------------------------------------------------------------| +| `var` | an `indigo.Variable` object representing the variable that was deleted | + +**Return Value:** + +| Type | Description | +|------|--------------------------------------| +| None | This method does not return a value. | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +def variableDeleted(self, var): + pass # respond to the deleted variable as needed +``` + +## variableUpdated() { .ref-head data-toc-label="variableUpdated" } + +Complementary to the `variableCreated()` method described above, but signals variable updates. You'll get a copy of the old variable object as well as the new variable object. + +**Method** + +| Method Name | Required | +|---------------------------------------------------------------------|----------| +| `variableUpdated(self, origVar, newVar)` | No | + +**Parameters** + +| Parameter | Description | +|--------------------------------------|-------------------------------------------------------------------------| +| `origVar` | an `indigo.Variable` object representing the variable before the change | +| `newVar` | an `indigo.Variable` object representing the variable after the change | + +**Return Value:** + +| Type | Description | +|------|--------------------------------------| +| None | This method does not return a value. | + +**Exceptions Raised** + +| Type | Description | +|--------------|-------------| +| | | + +**Command Syntax Examples** + +```python +def variableUpdated(self, origVar, newVar): + pass # respond to the updated variable as needed +``` + +That’s all the methods that will be called automatically by the host process. You may, of course, define many more methods. Some that you will probably want to define: methods to be called when a button is clicked in a `` dialog and methods called by `` and ``. + +You can also define your own classes, either in `plugin.py` or more likely in separate files. We believe the plugin host process offers you a great deal of flexibility in how you construct your Python code. diff --git a/reference/canonical/plugin-dev/reference/xml.md b/reference/canonical/plugin-dev/reference/xml.md new file mode 100644 index 0000000..05fbb49 --- /dev/null +++ b/reference/canonical/plugin-dev/reference/xml.md @@ -0,0 +1,16 @@ + + +# Plugin XML Reference + +!!! abstract "In this guide" + This page is a complete reference for the XML files used to define Indigo plugin configuration dialogs, devices, + events, actions, and menu items, including all supported field types and attributes. + +## Plugin XML Files + +An Indigo plugin is configured through a set of XML files plus the ConfigUI markup they share: + +- **[ConfigUI Fields](configui/index.md)** — the dialog field types used throughout the XML files. +- **[PluginConfig.xml](pluginconfig.md)** — plugin-wide configuration. +- **[Devices.xml](devices.md)** · **[Events.xml](events.md)** · **[Actions.xml](actions.md)** — declaring device types, events, and actions. +- **[MenuItems.xml](menuitems.md)** · **[SupportURL Elements](supporturl.md)**. diff --git a/reference/canonical/plugin-dev/reference/xml/actions.md b/reference/canonical/plugin-dev/reference/xml/actions.md new file mode 100644 index 0000000..5f18e8a --- /dev/null +++ b/reference/canonical/plugin-dev/reference/xml/actions.md @@ -0,0 +1,140 @@ + + +# Actions.xml { .ref-head-no-code #actions-xml } + +Your plugin will also very likely define some actions that a user can take. This is where you define those. Here’s a simple example: + +```xml + + + http://www.yourdomain.com/plugin/pluginActions.html + + Reset Interface + resetInterface + + +``` + +As with ``, your `` elements can define a `` element as well - the actions dialog now has a help button on it and if one of your actions is selected clicking on the help button will take your user to the specified URL. If you don’t specify one then the default help page for all actions will show. + +You can specify an Action item field to be a label within a `ConfigUI` in your action list when you want to include some text -- for example, to explain what users should enter into a text field. Label tags require a unique `id` and the type should be set to `label`. Labels do not require any other elements. + +```xml + + + +``` + +You can specify an Action item to be a separator in your action list so that when they're displayed in the UI there is a visual separation. Simply insert an Action defined like this between two other Action elements: While you don't have to include any elements, the id still must be unique. + +`` + +For a full description of all the different XML conventions that Indigo uses, [see this page](../../guide.md#indigo-plugin-xml-conventions). + +**Attributes** + +When defining a full plugin action, a few elements are required. Here’s how you construct your `` elements in `Actions.xml`: + +| Attribute | Type | Required | Notes | | | +|---------------------------------------------|-----------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--|--| +| `id` | Attribute | Yes | This is a unique id for the event in this `Actions.xml` file. | | | +| `deviceFilter` | Attribute | No | If present, a popup list of devices that match the specified [device filter](configui/dynamic-lists.md#config-dynamic-list) will be shown in the UI. Many actions may not need any configuration beyond a device selection so we've enabled you to specify that a device must be selected and passed to the action's `CallbackMethod`. This will avoid the necessity to create a `ConfigUI` element with just a device popup. | | +| `uiPath` | Attribute | No | [Added in API v1.4](https://www.indigodomo.com/indigo/api_release_notes/1.4/). You can specify where in the menu hierarchy your action will be placed. By default, Indigo will create a submenu at the bottom of the Actions list and put your actions there. If you specify "DeviceActions", Indigo will create a submenu on the Device Actions menu and put your actions there. If you specify "NotificationActions", Indigo will insert your action n the Notification Actions menu without a submenu (be sure to name your action appropriately so that it's clear what it does). [Added in API v2.0](https://forums.indigodomo.com/viewtopic.php?p=126067#p126067): use "hidden" to hide the action in the UI. Useful for actions that are only intended to be used from scripts/plugins (an API of sorts). | +| `Name` | Element | Yes* | This is the text that’s shown in the actions dialog that represents this action. `Name` is not required for labels and separators. | | | +| `CallbackMethod` | Element | Yes* | This is the name of the method that implements the action in your code. `CallbackMethod` is not required for labels and separators. | | | +| `ConfigUI` | Element | No | If your action requires any configuration (as most will), you can specify a `` element that’s defined exactly as above. | | | + +When the Action is fired in Indigo, the callback method will be called and an Indigo dictionary will be passed with information about how to handle the call. In the example below, the `action` payload will contain the necessary things to implement the call. Notice the call to `action.props.get()` in the Python callback below, which will pull the appropriate value from the specified key (`message`, `type`, etc.) using the standard Python dictionary `get()` method. + +Here is a sample action dict sent to a plugin: +```text +configured : True +delayAmount : 900 <-- Read only; currently not used +description : redraw one chart <-- Taken from the Actions.xml element. +props : com.foo.indigoplugin.my_plugin : (dict) + config_prop_1 : 123 (integer) + config_prop_2 : true (bool) + config_prop_3 : "baz" (string) +replaceExisting : True <-- Read only; currently not used +textToSpeak : +``` + +Here is the code that implements the Write to Log action defined in the Action Collection plugin: + +```python +def writeToLog(self, action): + # call for variable substitution on the message field they entered + theMessage = self.substitute(action.props.get("message", "")) + # set the type for the message if they configured one + theType = action.props.get("type") + # debugging - show the message if debugging is enabled + self.debugLog(u"Write to log: " + theMessage) + # if they entered a type, log the message with it, otherwise log the message without a type + if theType: + indigo.server.log(theMessage,type=theType) + else: + indigo.server.log(theMessage) +``` + +Notice the call to `self.substitute()` - this method is defined in the [plugin base class](../plugin-py/index.md). If your user inserts `%%v:12345%%` into their string where `12345` is the ID of a variable, the call will return a string with all variable occurrences substituted. If your user inserts `%%d:12345:someStateId%%` into their string where `12345` is the ID of a device and `someStateId` is a valid state identifier, the call will return a string with all device state occurrences substituted. See the [substitutions docs](../../../user/automation/substitutions.md) for more information. + +Your plugin actions can return any Python "primitive" value to users when they call your action +with the `executeAction` method (a return value is not required). The return value can +be any one of the following object types: + +- None `None`, +- booleans `bool`, +- integers `int()`, +- floats `float()`, +- strings `str()`, +- Indigo Dicts `indigo.Dict()`, +- Indigo Lists `indigo.List()`. + +Your plugin action may also receive an optional `callerWaitingForResult` parameter which is a request for your plugin to block until it can complete its tasks (and also return a response). + +```python +def actionSomeXmlDefineActionCallback(self, action, dev, callerWaitingForResult, event_data=None): + # do your typical action stuff here + # note: event_data contains a dictionary of information about what caused the event to fire + return some_value +``` + +The action can block until it has the result of the action (which it can then just return directly), but note that all callbacks must be executed in the same plugin host thread. This means that other callbacks would become blocked, including those to get configuration UI XML and values. This turns into a potential usability problem in the Indigo client, as the plugin configuration, actions, trigger, etc., UI will become unresponsive and start to timeout. + +Instead, if the plugin cannot immediately return a result for the action (because it has to communicate with hardware, for example), then it can acquire a completion handler callback function that can be executed later (asynchronously) in another thread. + +The pattern would then be something like: + +```python +def actionSomeXmlDefineActionCallback(self, action, dev, callerWaitingForResult): + completeHandler = None + if callerWaitingForResult: # only acquire the completion handler if caller is wanting a result + completeHandler = indigo.acquireCallbackCompleteHandler() + + self.pluginsActionQueue.put((action, dev, completeHandler)) + # Note self.pluginsActionQueue is not implemented by the base plugin and is just an example + # of how one might pass the action and completeHandler to another thread using a queue. + +def anotherThreadQueueHandlerFunc(self) + try: + (action, dev, completeHandler) = self.pluginsActionQueue.get(True, 60) + + # Process the requested action here. This can block and take a long + # time without impacting UI usability because this method would be + # called from another thread. + + # Upon completion, you can return the result to the original action caller + # via: + if completeHandler is not None: + completeHandler.returnResult("success performing action") + except Exception as exc: + # If there was an exception performing the action (hardware not available, etc.) + # then the exception can be returned to the original action caller via: + if completeHandler is not None: + completeHandler.returnException(exc) +``` + +So, rather than return a value from the actual callback, you request a callback completion handler from the server. You then can queue up data, including the handler object, so that some other async thread can perform whatever communication, calculation, etc., is needed and use the completion handler to return the result. As shown in the example above, you can also return an exception which will be thrown in the requesting process if necessary. + +Note: To increase the value of your plugin, you should ensure that you test and document the necessary +information so that Python scripters can [script your plugin's actions](../../../scripting/tutorial.md#scripting-indigo-plugins). You should include information on the parameters your action will expect to receive and what your action will return (if anything) when the action is called. Here is an example that shows how a scripter can tell [the Timers plugin to restart a timer](../../../plugins/timersandpesters.md#restart-timer). You can provide that information in a relatively straight-forward way in your plugin's documentation as we've done with our plugins (check the [Airfoil Pro](../../../plugins/airfoilpro.md) and [Timers and Pesters](../../../plugins/timersandpesters.md#scripting-support) docs for examples). You shouldn't really have to do much - but you should test each action. Sometimes you can make assumptions about the data that you get from your action's ConfigUI that you may want to change - for instance you may expect data when it would be advantageous to not include it (optional data) from a scripters perspective. diff --git a/reference/canonical/plugin-dev/reference/xml/configui.md b/reference/canonical/plugin-dev/reference/xml/configui.md new file mode 100644 index 0000000..ce2ce4a --- /dev/null +++ b/reference/canonical/plugin-dev/reference/xml/configui.md @@ -0,0 +1,66 @@ + + +# Configuration Dialogs { .ref-head-no-code #plugin-config } +The Indigo plugin XML contains some structures that are used in the various component XML files. `Devices`, `Events`, `Actions`, and the `PluginConfig` each may describe some type of user interface, so we needed a simple description language that described the user interface elements and layout. So, for the first 3, we created an XML element called a `ConfigUI` (we’ll discuss the `PluginConfig` a bit later). Here’s an example for a device: + +```xml + + http://www.yourdomain.com/plugin/ApplianceModule.html + + + + + + Find Node ID + validPythonMethodName + + + + + + + + (Not Recommended) + + + + + +``` + +The ConfigUI definition will result in the following dialog being presented to the user: + +![Configuration UI Rendering Image](../../../../images/configuirendering.png) + +When each component type (device, event, action) needs a configuration user interface, and most will need some kind of configuration, it will have a ConfigUI element that describes the UI field elements along with a URL. When the user clicks on help button in the lower left corner (as shown above) their browser will be opened to the URL provided. If no URL is specified then the user will be directed to the main URL specified in the `Info.plist` file. + +The rest of the elements in the ConfigUI represent fields that are shown in the dialog. We’ll go through each field type, starting with a screenshot of how it’s rendered, then the XML, and finally the details for each. The order in which you specify the fields is the order that they will show up in the dialog, and the `id` attribute is required for every field and must be unique within the dialog - it’s used to establish enabled and visible bindings between fields and more importantly it’s the key used in the dictionary to get the values when you get messages from the dialog (discussed more later). The `id` must be alphanumeric (and may contain underscores) and must begin with an alphabetic character. + +You'll have the opportunity to validate the fields before they're saved (as well as know when the user cancels out of a dialog). How that's done is discussed at the end of this section. + + +## ConfigUI Field Types + +Each control you can place in a plugin configuration dialog: + +- [Button](button.md) · [Checkbox](checkbox.md) · [Color Picker](color-picker.md) · [Label](label.md) · [List](list.md) · [Popup Menu](popup-menu.md) +- [Separator](separator.md) · [Serial Port](serial-port.md) · [Text Field](text-field.md) +- [Dynamic Lists and Filters](dynamic-lists.md) — runtime-populated menus and lists. +- [Validation Methods](validation.md) — validating dialog input. +- [Dialog Close Notification](dialog-close.md) — reacting to dialog close. diff --git a/reference/canonical/plugin-dev/reference/xml/configui/button.md b/reference/canonical/plugin-dev/reference/xml/configui/button.md new file mode 100644 index 0000000..2bbdb5f --- /dev/null +++ b/reference/canonical/plugin-dev/reference/xml/configui/button.md @@ -0,0 +1,73 @@ + + +# Button { .ref-head-no-code #config-button } + +Button fields allow your user to communicate with your plugin during configuration. The following table describes the attributes that are available and the XML elements that are required for button fields. + +![Configuration UI Button Image](../../../../images/configui_button.png) + +**Attributes** + +| Attribute | Required | API Version | Notes | +|----------------------------------------------------------|----------|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `alwaysUseInDialogHeightCalc` | No | 1.0 | For dialogs that contain hidden controls (see `visibleBindingId` and `visibleBindingValue` below), setting this attribute to `true` will force Indigo to take hidden controls into account when initially sizing the dialog box. This is useful when hidden controls become visible based on user input. | +| `enabledBindingId` | No | 1.0 (1.9) | If you want to conditionally enable/disable this field based on the value of a checkbox, set this value to the id of that field.

API v1.9: You can also bind to a list field and this field will only be enabled if something is selected in the list. | +| `enabledBindingNegate` | No | 1.9 | Set this to "true" to negate the binding. So if the target binding field's value is true then the binding will be false. | +| `id` | Yes | 1.0 | This is a unique identifier for this Field within the context of this `ConfigUI` element. | +| `type` | Yes | 1.0 | This must be `button`. | +| `visibleBindingId` | No | 1.0 | If you want to conditionally show/hide this field based on the value of another field, set this value to the `id` of another field. If you set this, you must also include a `visibleBindingValue` | +| `visibleBindingValue` | No | 1.0 | If you specify a `visibleBindingId`, you must specify the value(s) here. For instance, if you are binding to a checkbox, setting this to `false` will mean the menu field is visible only when the checkbox is unchecked, and a `true` means the opposite. To make the field dependent on another list or menu, set this value to a comma separated list of option values (`item1, item2`). You can even bind it to the value in a text field, but that’s probably of limited use. **Note**: the string comparison is a contains - so if any option contains the specified string it will match. This offers some extra flexibility in defining dependency groups. | +| Element | | | | +| `CallbackMethod` | Yes | 1.0 | The `CallbackMethod` is the plugin method that you want to execute when the button is pressed. Several elements will be passed to the method when it's called: the dialog's configuration values (`valuesDict`), the type of device (`typeId`), and the device ID (`devId`). The 'CallbackMethod` field type is read-only, which means that you can't have an error message attached to it from a validation method or a button method (see below for details). | +| `title` | Yes | 1.0 | The `title` is the text that will appear on the button itself. | + +```xml + + + Visible Button Title + ConfigButtonPressed + +``` + +As an example, if your plugin needs to start listening for a specific network broadcast packet that a device sends when it’s in a special discover mode, then you probably only want to listen for that packet when the user actually makes the device start to broadcast. So, as in the example device dialog above, you instruct the user to press a button on the device, then have them click the button. This would give your plugin an opportunity to do something while the configuration dialog was up and return the results to the dialog. + +The process flow is: + +1. The user clicks the button. +1. A dictionary containing all the values of the fields in the dialog are sent to the method that you identify in the `` element. +1. Your plugin can then do whatever it needs to, and it returns a dictionary back to the dialog containing any field changes. +1. The dialog will update the values (and enable/visible bindings appropriately) for any changed fields. + +Each button's method signature will match the parameters that are used in the [validation methods](validation.md#validation-methods) described below. For instance, if you specify a method called "beginPairing" in a button field of the `` XML for a device like this: + +```xml + + Music Server + + + + Begin Pairing + beginPairing + + + + +``` + +The method for that event in your `plugin.py` file would look something like this: + +```python +def beginPairing(self, valuesDict, typeId, devId): + # do whatever you need to here + # typeId is the device type specified in the Devices.xml + # devId is the device ID - 0 if it's a new device + return valuesDict +``` + +Note that the parameters match those specified below for the `validateDeviceConfigUi` method. + +[Back to Configuration Dialogs](index.md) diff --git a/reference/canonical/plugin-dev/reference/xml/configui/checkbox.md b/reference/canonical/plugin-dev/reference/xml/configui/checkbox.md new file mode 100644 index 0000000..7198bec --- /dev/null +++ b/reference/canonical/plugin-dev/reference/xml/configui/checkbox.md @@ -0,0 +1,53 @@ + + +# Checkbox { .ref-head-no-code #config-checkbox } + +Checkbox fields allow for binary selections (yes/no, true/false, etc.) The following table describes the attributes that are available for checkbox fields. + +![Configuration UI Checkbox Image](../../../../images/configui_checkbox.png) + +**Attributes** + +| Attribute | Required | API Version | Notes | +|----------------------------------------------------------|----------|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `alwaysUseInDialogHeightCalc` | No | 1.0 | For dialogs that contain hidden controls (see `visibleBindingId` and `visibleBindingValue` below), setting this attribute to `true` will force Indigo to take hidden controls into account when initially sizing the dialog box. This is useful when hidden controls become visible based on user input. | +| `defaultValue` | No | 1.0 | You can enter a default value here. The value must be either `true` or `false`. | +| `enabledBindingId` | No | 1.0 (1.9) | If you want to conditionally enable/disable this field based on the value of a checkbox, set this value to the id of that field.

API v1.9: You can also bind to a list field and this field will only be enabled if something is selected in the list. | +| `enabledBindingNegate` | No | 1.9 | Set this to "true" to negate the binding. So if the target binding field’s value is true then the binding will be false. | +| `hidden` | No | 1.0 | If this attribute is set to `true` then the field will never be displayed regardless of what other options you have set for it. It’s useful if you need to contain some kind of state variables for the dialog that are controlled by button presses rather than controlled directly by the user. | +| `id` | Yes | 1.0 | This is a unique identifier for this Field within the context of this `ConfigUI` element. | +| `readonly` | No | 1.0 | This attribute will make the field readonly - useful to show the user data that changes in some other way rather than being manipulated directly by the user. | +| `tooltip` | No | 1.0 | The tooltip will be shown when you hover over the actual list control. | +| `type` | Yes | 1.0 | This must be "checkbox". | +| `visibleBindingId` | No | 1.0 | If you want to conditionally show/hide this field based on the value of another field, set this value to the `id` of another field. If you set this, you must also include a `visibleBindingValue` | +| `visibleBindingValue` | No | 1.0 | If you specify a `visibleBindingId`, you must specify the value(s) here. For instance, if you are binding to a checkbox, setting this to `false` will mean the menu field is visible only when the checkbox is unchecked, and a `true` means the opposite. To make the field dependent on another list or menu, set this value to a comma separated list of option values (`item1, item2`). You can even bind it to the value in a text field, but that’s probably of limited use. **Note**: the string comparison is a contains - so if any option contains the specified string it will match. This offers some extra flexibility in defining dependency groups. | + +```xml + + + What’s on the right side of the checkbox + +``` + +Checkboxes contain the standard required Label element. They also contain an optional element, `Description`, that’s used to deliver extra information on the right side of the checkbox. Many checkboxes may not need this extra information. + +Like [button fields](button.md), you can specify a `CallbackMethod` element in your field definition: + +```xml + + + checkboxChanged + +``` + +This method will be called every time the user clicks the checkbox. The method in your plugin.py file should look something like this: + +```python +def checkboxChanged(self, valuesDict, typeId, devId): + # do whatever you need to here + # typeId is the device type specified in the Devices.xml + # devId is the device ID - 0 if it's a new device + return valuesDict +``` + +[Back to Configuration Dialogs](index.md) diff --git a/reference/canonical/plugin-dev/reference/xml/configui/color-picker.md b/reference/canonical/plugin-dev/reference/xml/configui/color-picker.md new file mode 100644 index 0000000..8f5da7c --- /dev/null +++ b/reference/canonical/plugin-dev/reference/xml/configui/color-picker.md @@ -0,0 +1,40 @@ + + +# Color Picker { .ref-head-no-code #config-color-picker } + +Color Picker fields allow your user to choose a color value. For example, you might provide a method for a user to select a color for an RGB LED device. When the user clicks the color button, Indigo will open the standard macOS color selector dialog. + +![Color Picker](../../../../images/color_picker.png){ width=200 } + +**Attributes** + +| Attribute | Required | API Version | Notes | +|----------------------------------------------------------|----------|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `alwaysUseInDialogHeightCalc` | No | 1.0 | For dialogs that contain hidden controls (see `visibleBindingId` and `visibleBindingValue` below), setting this attribute to `true` will force Indigo to take hidden controls into account when initially sizing the dialog box. This is useful when hidden controls become visible based on user input. | +| `enabledBindingId` | No | 1.0 (1.9) | If you want to conditionally enable/disable this field based on the value of a checkbox, set this value to the id of that field.

API v1.9: You can also bind to a list field and this field will only be enabled if something is selected in the list. | +| `enabledBindingNegate` | No | 1.9 | Set this to "true" to negate the binding. So if the target binding field’s value is true then the binding will be false. | +| `id` | Yes | 1.0 | This is a unique identifier for this Field within the context of this `ConfigUI` element. | +| `type` | Yes | 2.0 | This must be `colorpicker`. | +| `visibleBindingId` | No | 1.0 | If you want to conditionally show/hide this field based on the value of another field, set this value to the `id` of another field. If you set this, you must also include a `visibleBindingValue` | +| `visibleBindingValue` | No | 1.0 | If you specify a `visibleBindingId`, you must specify the value(s) here. For instance, if you are binding to a checkbox, setting this to `false` will mean the menu field is visible only when the checkbox is unchecked, and a `true` means the opposite. To make the field dependent on another list or menu, set this value to a comma separated list of option values (`item1, item2`). You can even bind it to the value in a text field, but that’s probably of limited use. **Note**: the string comparison is a contains - so if any option contains the specified string it will match. This offers some extra flexibility in defining dependency groups. | + +```xml + + + ColorPickerPressed + +``` + +![Color Selector](../../../../images/color_selector.png){ width=200 } + +After the user selects a color, the color’s value will be passed back to your plugin as a `values_dict` value when the user executes/closes the configuration dialog. Note that the value provided is a standard RGB value as a space-delimited string. You are responsible for converting that value into the format you need like `#8000FF`. + +```text +UiValuesDict : (dict) + chosenColor : 80 00 FF (string) + instructions : (string) +``` + +Aside from the standard `Label` element, the colorPicker field also contains one other required element--the `CallbackMethod` element--which contains the name of a Python method in your code that’s called when the user executes your configuration dialog. + +[Back to Configuration Dialogs](index.md) diff --git a/reference/canonical/plugin-dev/reference/xml/configui/dialog-close.md b/reference/canonical/plugin-dev/reference/xml/configui/dialog-close.md new file mode 100644 index 0000000..04fc90c --- /dev/null +++ b/reference/canonical/plugin-dev/reference/xml/configui/dialog-close.md @@ -0,0 +1,19 @@ + + +# Dialog Close Notification { .ref-head-no-code #dialog-close-notification } + +So you know that when the user clicks the "Save" button in the dialog, your validation method will get called (if it exists). But, what if your dialog starts some separate thread (perhaps as a result of a button click), then the user clicks the "Cancel" button? You still need to know to stop the thread that's doing something, right? There is another, final method that's called at the very end of the dialog's lifecycle. It's very similar to the validation method, but with an additional parameter that tells you whether the user canceled the dialog. Here are the various closed notification methods: + +**Dialog Types** + +| Dialog Type | Method Signature | +|--------------|------------------------------------------------------------------------------------------------------| +| device | `closedDeviceConfigUi(self, valuesDict, userCancelled, typeId, devId)` | +| event | `closedEventConfigUi(self, valuesDict, userCancelled, typeId, eventId)` | +| action | `closedActionConfigUi(self, valuesDict, userCancelled, typeId, actionId)` | +| PluginConfig | `closedPrefsConfigUi(self, valuesDict, userCancelled)` | + +The parameters are exactly the same as in the [validation methods](validation.md#validation-methods) with the addition of `userCancelled` - which is a boolean. We're sure there are other reasons why one would need to know when the dialog closes so we wanted to make sure that the complete lifecycle of the dialog was exposed to you. + +!!! note + The parameters in this instance are all read-only. If you need to modify the valuesDict, for instance, you need to do it in the validate UI (which is fine since a cancel should never change anything). diff --git a/reference/canonical/plugin-dev/reference/xml/configui/dynamic-lists.md b/reference/canonical/plugin-dev/reference/xml/configui/dynamic-lists.md new file mode 100644 index 0000000..e0fa058 --- /dev/null +++ b/reference/canonical/plugin-dev/reference/xml/configui/dynamic-lists.md @@ -0,0 +1,126 @@ + + +# Dynamic Lists and Filters { .ref-head-no-code #config-dynamic-list } + +You may create dynamic menus and lists that are created at runtime. Indigo will supply some specific ones for you or your plugin may be called each time the UI is presented that will allow you to return anything you like. + +To specify a dynamic list, you add some attributes to the List element and skip adding Option elements. To request any of the built-in lists, you specify a class attribute and (optionally) a filter attribute. For example, this field: + +```xml + + + + +``` + +would request a list of all Insteon dimmer devices. The list would be constructed with the device name as the menu or list item viewable by the user, and the value would be the device ID. + +**Attributes** + +| Attributes | | | +|-------------------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Attribute | Required | Notes | +| `class` | Yes | A reference to a class value as described below. The class attribute must be set to one or more of the built-in classes (listed below), or `self`. | +| `filter` | No | A filter attribute is not required, but if one is included, it must be set to a valid filter (see below) or empty `filter=""`. | +| `method` | No | A reference to a `plugin.py` method that returns the list elements as described below. If not included, `class` must refer to one of the built-in methods (`class="self"` by itself will not work). | + +**Device Filters** + +The `indigo.devices` class specifies that the list will contain Indigo devices. If you don’t specify a filter, all devices defined by the user in Indigo will be returned. You can supply up to two filters if and only if one is an interface filter (see the first three in the list below). The filters are ANDed together, so one interface filter and one device type filter are the only combinations that will result in a non-empty list. + +| Class: `indigo.devices` | | +|-------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| +| Optional Filters | Description | +| `indigo.zwave` | include Z-Wave devices - this is an interface filter that can be used with other filters | +| `indigo.insteon` | include Insteon devices - this is an interface filter that can be used with other filters | +| `indigo.x10` | include X10 devices - this is an interface filter that can be used with other filters | +| `indigo.responder` | include devices whose state can be changed | +| `indigo.controller` | include devices that can send commands | +| `indigo.relay` | relay devices | +| `indigo.dimmer` | dimmer devices | +| `indigo.sprinkler` | sprinklers | +| `indigo.thermostat` | thermostats | +| `indigo.iodevice` | input/output devices | +| `indigo.sensor` | all sensor type devices: motion sensors, TriggerLinc, SynchroLinc (sensor devices that have a virtual state in Indigo) | +| `self` | all device types defined by the calling plugin | +| `self.devTypeId` | all devices of type deviceTypeId, where deviceTypeId is one of the device types specified by the calling plugin | +| `com.somePlugin` | all device types defined by some other plugin | +| `com.somePlugin.devTypeId` | all devices of type deviceTypeId, where deviceTypeId is one of the device types specified in some other plugin | + +The `indigo.triggers` class specifies that the list will contain Indigo triggers. Only a single filter from the list below will return a useful list. + +**Trigger Filters** + +| Class: `indigo.triggers` | | +|----------------------------------------------------|----------------------------------------------------------------------------------| +| Optional Filters | Description | +| `indigo.insteonCmdRcvd` | Insteon command received triggers | +| `indigo.x10CmdRcvd` | x10 command received triggers | +| `indigo.devStateChange` | device state changed triggers | +| `indigo.varValueChange` | variable changed triggers | +| `indigo.serverStartup` | startup triggers | +| `indigo.powerFailure` | power failure triggers | +| `indigo.interfaceFail` | interface failure triggers - can be used with or without a specified protocol | +| `indigo.interfaceInit` | interface connection triggers - can be used with or without a specified protocol | +| `indigo.emailRcvd` | email received triggers | + +Using the `indigo.schedules` class will result in a list of all schedules, as will `indigo.actionGroups` and `indigo.controlPages` for their respective object types. None of these classes support any kind of filter. Lastly, `indigo.variables` will get you a list of variables. If you include `filter="indigo.readWrite"`, you’ll get only variables for which you can change the value. Currently, there’s only one read-only variable (isDaylight) defined by the system, but it may be possible to create them in future versions of the API. + +Another special built-in list is serial ports - use `indigo.serialPorts` to get a list of available serial ports, and what's returned to your plugin is the full path to the plugin (e.g. "/dev/tty*"). This is suitable for using with PySerial, which we include with Indigo. See the "serialport" field type below for an even better way of collecting serial communication information. + +You may also include custom dynamic lists that are constructed on-the-fly by your plugin. If you defined your list like this: + +```xml + + + + +``` + +Then your plugin will have the method specified called with the filter. For the above example, you must define a method like this: + +```python +def myListGenerator(self, filter="", valuesDict=None, typeId="", targetId=0): + # From the example above, filter = "stuff" + # You can pass anything you want in the filter for any purpose + # Create a list where each entry is a list - the first item is + # the value attribute and last is the display string that will + # show up in the control. All parameters are read-only. + myArray = [("option1", "First Option"),("option2","Second Option")] + return myArray +``` + +!!! note + Both return tuple values should be strings. Specifically, items with `None` as the value (first item) will be skipped. Additionally, the first tuple value (option1 and option2 in the above example) must be a string that does not contain comma or semicolon characters. + +The `valuesDict` parameter will contain the valuesDict for the object being edited - if it's a device config UI then it'll be the valuesDict from that device (just like what's passed in to [validation methods](validation.md#validation-methods)), etc. **Note**: if it's a new object that hasn't been saved yet, valuesDict may be None or empty so test accordingly. + +The `typeId` parameter will contain the type - for instance, if it's an event, it will be the event type id. The `targetId` is the ID of the object being edited. It will be 0 if it's a new object that hasn't been saved yet. + +The field would then be created as if you had specified it statically like this: + +```xml + + + + + + + +``` + +This mechanism allows you to create any number of lists: devices dynamically discovered through some discovery protocol, calendars available, email addresses, iTunes playlists, etc. + +One further option to dynamic lists is the ability to have them reload whenever the config dialog returns from a button or menu callback. So, for instance, if you have a button that somehow changes the contents of the dynamic list, you can specify the list as a one that's dynamically reloaded: + +```xml + + + + +``` + +!!! note + This will cause an extra round-trip (client->Indigo server->plugin->Indigo server->client) so you should only use this when the list can change while the dialog is actually running. + +[Back to Configuration Dialogs](index.md) diff --git a/reference/canonical/plugin-dev/reference/xml/configui/label.md b/reference/canonical/plugin-dev/reference/xml/configui/label.md new file mode 100644 index 0000000..6bb9693 --- /dev/null +++ b/reference/canonical/plugin-dev/reference/xml/configui/label.md @@ -0,0 +1,33 @@ + + +# Label { .ref-head-no-code #config-label } + +Label fields are used to present text that span the width of the dialog. As you may have noticed, each control has an element called `Label` (except for `Separator`, discussed next). When the dialog is displayed, each of those Label elements is right aligned and the actual control is left aligned directly to the right of the label. This field gives you a way to communicate a much longer chunk of text - like instructions, etc. It will actually span the entire width of the dialog and will wrap as necessary. The following table describes the attributes that are available for label fields. + +![Configuration UI Label Image](../../../../images/configui_label.png) + +**Attributes** + +| Attribute | Required | API Version | Notes | +|----------------------------------------------------------|----------|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `alignText` | No | 1.4 | This will control how the label text is justified. Valid options are: "left" (default), "center", and "right". | +| `alignWithControl` | No | 1.4 | If set to "true", this will align the left margin of the label with the left margin for the actual controls (not their labels). Anything other than "true" will be considered "false" (which is also the default if not specified). This is very useful in conjunction with specifying font characteristics described below when you want to add some help specific to a control - just insert the label right below the control field and it will align with the actual control above. | +| `alwaysUseInDialogHeightCalc` | No | 1.0 | For dialogs that contain hidden controls (see `visibleBindingId` and `visibleBindingValue` below), setting this attribute to `true` will force Indigo to take hidden controls into account when initially sizing the dialog box. This is useful when hidden controls become visible based on user input. | +| `enabledBindingId` | No | 1.0 (1.9) | If you want to conditionally enable/disable this field based on the value of a checkbox, set this value to the id of that field.

API v1.9: You can also bind to a list field and this field will only be enabled if something is selected in the list. | +| `enabledBindingNegate` | No | 1.9 | Set this to "true" to negate the binding. So if the target binding field’s value is true then the binding will be false. | +| `fontColor` | No | 1.4 | Use this attribute to control the color of the label text. Valid options are: "black (default), "darkgray", "red", "orange", "green", and "blue". | +| `fontSize` | No | 1.4 | Use this attribute to specify the size of the font. Valid options are: "regular" (default, same size as all other text in the dialog), "small, which is a point or two smaller, and "mini which is yet another point size or two smaller. | +| `id` | Yes | 1.0 | This is a unique identifier for this Field within the context of this `ConfigUI` element. | +| `type` | Yes | 1.0 | This must be `label`. | +| `visibleBindingId` | No | 1.0 | If you want to conditionally show/hide this field based on the value of another field, set this value to the `id` of another field. If you set this, you must also include a `visibleBindingValue` | +| `visibleBindingValue` | No | 1.0 | If you specify a `visibleBindingId`, you must specify the value(s) here. For instance, if you are binding to a checkbox, setting this to `false` will mean the menu field is visible only when the checkbox is unchecked, and a `true` means the opposite. To make the field dependent on another list or menu, set this value to a comma separated list of option values (`item1, item2`). You can even bind it to the value in a text field, but that’s probably of limited use. **Note**: the string comparison is a contains - so if any option contains the specified string it will match. This offers some extra flexibility in defining dependency groups. | +| Element | | | | +| Label | Yes | 1.0 | Labels have a single element, `Label`, that holds the text to be displayed. **Note**: in a label field type, the `Label` element is required. This field type is read-only, which means that you can't have an error message attached to it from a validation method or a button method (see below for details). | + +```xml + + + +``` + +[Back to Configuration Dialogs](index.md) diff --git a/reference/canonical/plugin-dev/reference/xml/configui/list.md b/reference/canonical/plugin-dev/reference/xml/configui/list.md new file mode 100644 index 0000000..3da2eab --- /dev/null +++ b/reference/canonical/plugin-dev/reference/xml/configui/list.md @@ -0,0 +1,42 @@ + + +# List { .ref-head-no-code #config-list } + +List fields allow for the selection of 0-N items with no implicit hierarchy. The following table describes the attributes that are available for list fields. + +![Configuration UI List Image](../../../../images/configui_list.png) + +**Attributes** + +| Attribute | Required | API Version | Notes | +|----------------------------------------------------------|----------|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `alwaysUseInDialogHeightCalc` | No | 1.0 | For dialogs that contain hidden controls (see `visibleBindingId` and `visibleBindingValue` below), setting this attribute to `true` will force Indigo to take hidden controls into account when initially sizing the dialog box. This is useful when hidden controls become visible based on user input. | +| `defaultValue` | No | 1.0 | You can enter a default value here. The value must be a comma separated list of values, each value must be listed as an option in the `List` element. | +| `enabledBindingId` | No | 1.0 (1.9) | If you want to conditionally enable/disable this field based on the value of a checkbox, set this value to the id of that field.

API v1.9: You can also bind to a list field and this field will only be enabled if something is selected in the list. | +| `enabledBindingNegate` | No | 1.9 | Set this to "true" to negate the binding. So if the target binding field’s value is true then the binding will be false. | +| `hidden` | No | 1.0 | If this attribute is set to `true` then the field will never be displayed regardless of what other options you have set for it. It’s useful if you need to contain some kind of state variables for the dialog that are controlled by button presses rather than controlled directly by the user. | +| `id` | Yes | 1.0 | This is a unique identifier for this `Field` within the context of this `ConfigUI` element. It must be alphanumeric (and may contain underscores) and must begin with an alphabetic character. | +| `readonly` | No | 1.0 | This attribute will make the field readonly - useful to show the user data that changes in some other way rather than being manipulated directly by the user. | +| `rows` | No | 1.4 | Use this attribute to specify the number of rows that will show in the list. The minimum (and default) is 4. | +| `tooltip` | No | 1.0 | The tooltip will be shown when you hover over the actual list control. | +| `type` | Yes | 1.0 | This must be `list`. | +| `visibleBindingId` | No | 1.0 | If you want to conditionally show/hide this field based on the value of another field, set this value to the `id` of another field. If you set this, you must also include a `visibleBindingValue` | +| `visibleBindingValue` | No | 1.0 | If you specify a `visibleBindingId`, you must specify the value(s) here. For instance, if you are binding to a checkbox, setting this to `false` will mean the menu field is visible only when the checkbox is unchecked, and a `true` means the opposite. To make the field dependent on another list or menu, set this value to a comma separated list of `Option` values (`item1, item2`). You can even bind it to the value in a text field, but that’s probably of limited use. **Note**: the string comparison is a contains - so if any option contains the specified string it will match. This offers some extra flexibility in defining dependency groups. | + +```xml + + + + + + + + + +``` + +Static list fields are constructed almost identically to static popup menu fields - with a `Label` element and a `List` element which contains multiple `Option` elements. Each `Option` element must have a `value` attribute and that attribute is what will be passed back to your plugin when the dialog is validated; it may not contain comma or semicolon characters. The text inside the `Option` element is what is displayed for each line in the user interface. + +As with `menu` fields, you may also specify a list dynamically at runtime - that is, when the UI is created, the client will call into your plugin to get the list items. This will allow you to dynamically adjust the list items every time the UI comes up. You can also specify some built-in dynamic items that the IndigoServer will provide automatically for you. See the [Dynamic Lists](dynamic-lists.md) section below for details. + +[Back to Configuration Dialogs](index.md) diff --git a/reference/canonical/plugin-dev/reference/xml/configui/popup-menu.md b/reference/canonical/plugin-dev/reference/xml/configui/popup-menu.md new file mode 100644 index 0000000..85b204a --- /dev/null +++ b/reference/canonical/plugin-dev/reference/xml/configui/popup-menu.md @@ -0,0 +1,66 @@ + + +# Popup Menu { .ref-head-no-code #config-pop-up-menu } + +Popup menu fields are used to select a single fixed value from a list with no inherent hierarchy. The following table describes the attributes that are available for menu fields. + +![Configuration UI Menu Image](../../../../images/configui_menu.png) + +**Attributes** + +| Attribute | Required | API Version | Notes | +|----------------------------------------------------------|----------|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `alwaysUseInDialogHeightCalc` | No | 1.0 | For dialogs that contain hidden controls (see `visibleBindingId` and `visibleBindingValue` below), setting this attribute to `true` will force Indigo to take hidden controls into account when initially sizing the dialog box. This is useful when hidden controls become visible based on user input. | +| `defaultValue` | No | 1.0 (1.9) | You can enter a default value here. The value must be one of the values from the List element described below.

API v1.9: If you specify a default value of "" (empty string) and there is no matching option, then the first menu item will be selected when the dialog is first run. This way you can avoid the user seeing "- no selection -" at the bottom of the menu. | +| `enabledBindingId` | No | 1.0 (1.9) | If you want to conditionally enable/disable this field based on the value of a checkbox, set this value to the id of that field.

API v1.9: You can also bind to a list field and this field will only be enabled if something is selected in the list. | +| `enabledBindingNegate` | No | 1.9 | Set this to "true" to negate the binding. So if the target binding field’s value is true then the binding will be false. | +| `hidden` | No | 1.0 | If this attribute is set to "true" then the field will never be displayed regardless of what other options you have set for it. It’s useful if you need to contain some kind of state variables for the dialog that are controlled by button presses rather than controlled directly by the user. | +| `id` | Yes | 1.0 | This is a unique identifier for this Field within the context of this ConfigUI element. It must be alphanumeric (and may contain underscores) and must begin with an alphabetic character. | +| `readonly` | No | 1.0 | This attribute will make the field readonly - useful to show the user data that changes in some other way rather than being manipulated directly by the user. | +| `tooltip` | No | 1.0 | Unfortunately, enabled menus won’t show tooltips (Cocoa limitation), so your tooltip should probably tell the user why it’s disabled (since that’s the only time it’ll show) if the field is ever disabled. | +| `type` | Yes | 1.0 | This must be "menu". | +| `visibleBindingId` | No | 1.0 | If you want to conditionally show/hide this field based on the value of another field, set this value to the id of another field. If you set this, you must also include a visibleBindingValue | +| `visibleBindingValue` | No | 1.0 | If you specify a visibleBindingId, you must specify the value(s) here. For instance, if you are binding to a checkbox, setting this to `false` will mean the menu field is visible only when the checkbox is unchecked, and a `true` means the opposite. To make the field dependent on a list or another menu, set this value to a comma separated list of option values ("item1, item2"). You can even bind it to the value in a text field, but that’s probably of limited use. **Note**: the string comparison is a contains - so if any option contains the specified string it will match. This offers some extra flexibility in defining dependency groups. | + +```xml + + + + + + + + +``` + +Static popup menu fields contain two elements. The first is the same `Label` element that every field has (except `Separator` which we’ll talk about later). The other is a `List` element which defines the menu items in your popup menu field. The `List` element contains multiple `Option` elements, each of which have a required `value` attribute. The `value` attribute will be passed back to your plugin when the dialog is validated; it may not contain comma characters. The text inside the `Option` element is what is displayed for each menu item in the user interface. + +Like [button fields](button.md), you can specify a `CallbackMethod` element in your field definition: + +```xml + + + + + + + + menuChanged + +``` + +This method will be called every time the user changes the menu. The method in your plugin.py file should look something like this: + +```python +def menuChanged(self, valuesDict, typeId, devId): + # do whatever you need to here + # typeId is the device type specified in the Devices.xml + # devId is the device ID - 0 if it's a new device + return valuesDict +``` + +Depending on which object type (plugin config, device, trigger, action, etc.) the dialog is being called for. This will allow you to adjust other fields in the valuesDict based on what's selected in the menu field. For instance, if you've selected a device type, you may decide to alter some hidden fields so that other fields are shown/and hidden. Used in combination with [Dynamic Lists](dynamic-lists.md), you may also repopulate other lists with more appropriate values. + +You may also specify a menu dynamically at runtime - that is, when the UI is shown, the client will call into your plugin to get the menu items. This will allow you to dynamically adjust the menu items every time the UI comes up. You can also specify some built-in dynamic items that the IndigoServer will provide automatically for you. See the [Dynamic Lists](dynamic-lists.md) section below for details. + +[Back to Configuration Dialogs](index.md) diff --git a/reference/canonical/plugin-dev/reference/xml/configui/separator.md b/reference/canonical/plugin-dev/reference/xml/configui/separator.md new file mode 100644 index 0000000..1e786f5 --- /dev/null +++ b/reference/canonical/plugin-dev/reference/xml/configui/separator.md @@ -0,0 +1,30 @@ + + +# Separator { .ref-head-no-code #config-separator } + +Separators work very similarly to labels, except that they just draw a nice embossed line. You can use it to separate sections of your config dialogs. Used in conjunction with the visible attributes, you can make whole sections appear and disappear based on other data in the dialog. The following table describes the attributes that are available for separator fields. + +![Configuration UI Separator Image](../../../../images/configui_separator.png) + +**Attributes** + +| Attribute | Required | API Version | Notes | +|----------------------------------------------------------|----------|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `alwaysUseInDialogHeightCalc` | No | 1.0 | For dialogs that contain hidden controls (see `visibleBindingId` and `visibleBindingValue` below), setting this attribute to `true` will force Indigo to take hidden controls into account when initially sizing the dialog box. This is useful when hidden controls become visible based on user input. | +| `enabledBindingId` | No | 1.0 (1.9) | If you want to conditionally enable/disable this field based on the value of a checkbox, set this value to the id of that field.

API v1.9: You can also bind to a list field and this field will only be enabled if something is selected in the list. | +| `enabledBindingNegate` | No | 1.9 | Set this to "true" to negate the binding. So if the target binding field’s value is true then the binding will be false. | +| `id` | Yes | 1.0 | This is a unique identifier for this Field within the context of this `ConfigUI` element. | +| `type` | Yes | 1.0 | This must be `separator`. | +| `visibleBindingId` | No | 1.0 | If you want to conditionally show/hide this field based on the value of another field, set this value to the `id` of another field. If you set this, you must also include a `visibleBindingValue` | +| `visibleBindingValue` | No | 1.0 | If you specify a `visibleBindingId`, you must specify the value(s) here. For instance, if you are binding to a checkbox, setting this to `false` will mean the menu field is visible only when the checkbox is unchecked, and a `true` means the opposite. To make the field dependent on another list or menu, set this value to a comma separated list of option values (`item1, item2`). You can even bind it to the value in a text field, but that’s probably of limited use. **Note**: the string comparison is a contains - so if any option contains the specified string it will match. This offers some extra flexibility in defining dependency groups. | + +```xml + +``` + +Separators are the only field type that contain no `Label` element. In fact, they don’t contain any elements at all, which is why the Field element that represents it is self terminating (notice the forward slash just before the closing bracket for the opening `Field` tag). This field type is read-only, which means that you can't have an error message attached to it from a validation method or a button method (see below for details). + +[Back to Configuration Dialogs](index.md) diff --git a/reference/canonical/plugin-dev/reference/xml/configui/serial-port.md b/reference/canonical/plugin-dev/reference/xml/configui/serial-port.md new file mode 100644 index 0000000..c8cb60c --- /dev/null +++ b/reference/canonical/plugin-dev/reference/xml/configui/serial-port.md @@ -0,0 +1,17 @@ + + +# Serial Port { .ref-head-no-code #config-serial-port } + +PySerial, the Python-based serial communication library that we ship with Indigo, supports not only doing traditional serial communications via physical serial connections, but also by doing serial communications over the network using serial over sockets or RFC 2217 (a standard for serial communication over the network). If the device you're connecting to supports one of these methods of connection then you can use the `serialport` field type. This allows you to specify a single field type that, when rendered in the UI, will expand to multiple fields that will allow the user to select `Local (physical)`, `Network Socket`, or `Network RFC-2217`. Then, based on which option they select, we'll also present the other controls needed to completely select the connection method: a serial port list for the first or the hostname/ip address and port for the last two. Make sure you leave "socket://" or "rfc2217://" as the beginning of the address field for the last two. + +![Configuration UI Serial Port Local Image](../../../../images/configui_serialport_local.png) + +![Configuration Serial Port Socket Image](../../../../images/configui_serialport_socket.png) + +![Configuration Serial Port RFC2217 Image](../../../../images/configui_serialport_rfc2217.png) + +`` + +This field type generates multiple controls in your dialog automatically. We've also included a helper method, `validateSerialPortUi(valuesDict, errorsDict, u"devicePortFieldId")` that will help you validate that the serialport control was used properly by the user. See the [Validating Serialport Fields](validation.md#validation-serialport-config) section for more information. + +[Back to Configuration Dialogs](index.md) diff --git a/reference/canonical/plugin-dev/reference/xml/configui/text-field.md b/reference/canonical/plugin-dev/reference/xml/configui/text-field.md new file mode 100644 index 0000000..2c37f88 --- /dev/null +++ b/reference/canonical/plugin-dev/reference/xml/configui/text-field.md @@ -0,0 +1,34 @@ + + +# Text Field { .ref-head-no-code #config-text-field } + +A text field is used to collect text input from the user. The following table describes the attributes that are available for text fields. + +![Configuration UI Textfield Image](../../../../images/configui_textfield.png) + +**Attributes** + +| Attribute | Required | API Version | Notes | +|----------------------------------------------------------|----------|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `alwaysUseInDialogHeightCalc` | No | 1.0 | For dialogs that contain hidden controls (see `visibleBindingId` and `visibleBindingValue` below), setting this attribute to `true` will force Indigo to take hidden controls into account when initially sizing the dialog box. This is useful when hidden controls become visible based on user input. | +| `defaultValue` | No | 1.0 | You can enter a default value here. | +| `enabledBindingId` | No | 1.0 (1.9) | If you want to conditionally enable/disable this field based on the value of a checkbox, set this value to the id of that field.

API v1.9: You can also bind to a list field and this field will only be enabled if something is selected in the list. | +| `enabledBindingNegate` | No | 1.9 | Set this to "true" to negate the binding. So if the target binding field's value is true then the binding will be false. | +| `hidden` | No | 1.0 | If this attribute is set to `true` then the field will never be displayed regardless of what other options you have set for it. It’s useful if you need to contain some kind of state variables for the dialog that are controlled by button presses rather than controlled directly by the user. | +| `id` | Yes | 1.0 | This is a unique identifier for this Field within the context of this ConfigUI element. It must be alphanumeric (and may contain underscores) and must begin with an alphabetic character. | +| `readonly` | No | 1.0 | This attribute will make the field readonly - useful to show the user data that changes in some other way rather than being manipulated directly by the user - in the example above clicking the `Find Node ID` button will populate the ID field. | +| `secure` | No | 1.4 | If you specify true for this value then when the user types into the field the actual characters won't show but rather will be replaced with the bullet (•) character. The values typed into these fields **are not** stored securely - this will solely mask the value in the field from viewing in the UI. | +| `tooltip` | No | 1.0 | Unfortunately, disabled text fields won’t show tooltips (Cocoa limitation), so your tooltip should probably tell the user why it’s disabled (since that’s the only time it’ll show) if the field is ever disabled. | +| `type` | Yes | 1.0 | This must be `textfield`. | +| `visibleBindingId` | No | 1.0 | If you want to conditionally show/hide this field based on the value of another field, set this value to the id of another field. If you set this, you must also include a `visibleBindingValue` | +| `visibleBindingValue` | No | 1.0 | If you specify a `visibleBindingId`, you must specify the value(s) here. For instance, if you are binding to a checkbox, setting this to `false` will mean the text field is visible only when the checkbox is unchecked, and a `true` means the opposite. To make the field dependent on a list or menu, set this value to a comma separated list of option values (`item1, item2`). You can even bind it to the value in another text field, but that’s probably of limited use. **Note**: the string comparison is a contains - so if any option contains the specified string it will match. This offers some extra flexibility in defining dependency groups. | + +```xml + + + +``` + +A text field contains only one element: Label. This label is shown to the left of the field and is optional, although there are probably very few times when you won’t want a label. Labels are available on every Field type except for the separator. + +[Back to Configuration Dialogs](index.md) diff --git a/reference/canonical/plugin-dev/reference/xml/configui/validation.md b/reference/canonical/plugin-dev/reference/xml/configui/validation.md new file mode 100644 index 0000000..7c52a12 --- /dev/null +++ b/reference/canonical/plugin-dev/reference/xml/configui/validation.md @@ -0,0 +1,189 @@ + + +# Validation Methods { .ref-head-no-code #validation-methods } + +When the user clicks on the save button, a validation method is automatically called with all the field/value pairs from your dialog. This table shows which method names are called based on the calling dialog type: + +--- + +## validateDeviceConfigUi() { #validation-device-config-ui } + +Validate the configuration settings of a device. + +**Method** + +`validateDeviceConfigUi(self, valuesDict, typeId, devId)` +`validate_device_config_ui(self, valuesDict, typeId, devId)` + +**Parameters** + +| Parameter | Type | Signature | +|-------------------------------------|---------------------------------------|------------------------------------------------------------------------------| +| `valuesDict` | `indigo.Dict` | the dictionary of values currently specified in the dialog | +| `typeId` | `str` | device type specified in the `type` attribute | +| `deviceId` | `int` | the unique device ID for the device being edited (or 0 of it's a new device) | + +**Return** + +Returns one or more `indigo.Dict` objects (`valuesDict`, `errorMsgDict`) + +--- + +## validateEventConfigUi() { #validation-event-config-ui } + +Validate the configuration of an event. + +**Method** + +`validateEventConfigUi(self, valuesDict, typeId, eventId)` +`validate_event_config_ui(self, valuesDict, typeId, eventId)` + +**Parameters** + +| Parameter | Type | Signature | +|--------------------------------------|---------------------------------------|---------------------------------------------------------------------------| +| `valuesDict` | `indigo.Dict` | the dictionary of values currently specified in the dialog | +| `typeId` | `str` | event type specified in the `type` attribute | +| `eventId` | `int` | the unique event ID for the event being edited (or 0 if it's a new event) | + +**Return** + +Returns one or more `indigo.Dict` objects (`valuesDict`, `errorMsgDict`) + +--- + +## validateActionConfigUi() { #validation-action-config-ui } + +Validate the configuration of an action. + +**Method** + +`validateActionConfigUi(self, valuesDict, typeId, deviceId)` +`validate_action_config_ui(self, valuesDict, typeId, deviceId)` + +**Parameters** + +| Parameter | Type | Signature | +|--------------------------------------|---------------------------------------|--------------------------------------------------------------------------------------------------------------------| +| `valuesDict` | `indigo.Dict` | the dictionary of values currently specified in the dialog | +| `typeId` | `str` | action type specified in the `type` attribute | +| `deviceId` | `int` | the unique device ID for the device the user selected for the action if you specify a `deviceFilter` | + +**Return** + +Returns one or more `indigo.Dict` objects (`valuesDict`, `errorMsgDict`) + +--- + +## validatePrefsConfigUi() { #validation-prefs-config-ui } + +Validate a plugin's configuration settings. + +**Method** + +`validatePrefsConfigUi(self, valuesDict)` +`validate_prefs_config_ui(self, valuesDict)` + +**Parameters** + +| Parameter | Type | Signature | +|--------------------------------------|---------------------------------------|--------------------------------------------------------------------------------| +| `valuesDict` | `indigo.Dict` | the dictionary of values currently specified in the dialog | + +**Return** + +Returns one or more `indigo.Dict` objects (`valuesDict`, `errorMsgDict`) + +--- + +Before the dialog is dismissed, this method will be called with a dictionary containing all the fields in the dialog. In your validation method, you’d do whatever validation is necessary. If everything validates correctly, then just return `True`: + +```python +def validateEventConfigUi(self, valuesDict, typeId, eventId): + # Do your validation logic here + return True +``` + +If you need to adjust the values, return the valuesDict with changes: + +```python +def validateEventConfigUi(self, valuesDict, typeId, eventId): + # Do your validation logic here + valuesDict["someKey"] = someNewValue + return (True, valuesDict) +``` + +If you have errors that the user must correct, then you’ll return 3 things: + +1. `False`, to indicate that validation failed +1. The values dictionary (with or without any changes) +1. An error dictionary. The keys will be the fieldId and the value will be the error string. When the dialog receives this return, it will turn each field with an error red and add a tooltip to the label part of the field so the user can mouse over the label to see what’s wrong. API v1.4+: You can also add a special dictionary entry to your error message dictionary that will cause a sheet to drop down with the specified text. This will help the user identify what went wrong in the validation method. Just add a string with the key "showAlertText" to your error dictionary before return from your dialog. + +```python +def validateEventConfigUi(self, valuesDict, typeId, eventId): + # Do your validation logic here + errorDict = indigo.Dict() + errorDict["someKey"] = "The value of this field must be from 1 to 10" + errorDict["showAlertText"] = "Some very descriptive message to your user that will help them solve the validation problem." + valuesDict["someOtherKey"] = someNewValue + return (False, valuesDict, errorDict) +``` + +If you don't define these methods, then the default behavior is to have the validation calls always return `True`. + +## Using ValidationError { .ref-head-no-code #using-validationerror } + +Building the error dictionary by hand works well for a field or two, but it gets tedious when you +have several fields to check. The [`indigo.utils.ValidationError`](../../../../scripting/reference/utils.md#validationerror) +exception is purpose-built for this: accumulate field errors as you validate, then convert its +`error_dict` into the dictionary the validation method returns. Because a `ValidationError` is +iterable, `dict()` turns it directly into the field/message pairs the dialog expects, and `str()` +produces a readable summary suitable for the `showAlertText` sheet. + +```python +def validateDeviceConfigUi(self, valuesDict, typeId, devId): + errors = indigo.utils.ValidationError("Device configuration is invalid") + + if not valuesDict.get("address"): + errors.add_error("address", "You must enter an address.") + if not indigo.utils.is_int(valuesDict.get("pollInterval", "")): + errors.add_error("pollInterval", "Poll interval must be a whole number.") + + if errors.error_dict: + # Turn the accumulated errors into the (False, valuesDict, errorsDict) the dialog expects. + errorsDict = indigo.Dict(dict(errors)) + errorsDict["showAlertText"] = str(errors) + return (False, valuesDict, errorsDict) + + return (True, valuesDict) +``` + +`ValidationError` can also be raised from deeper validation helpers (for example a function that +validates a JSON payload) and caught here, so the same validation logic can be shared between your +Config UI methods and your [HTTP request handlers](../../plugin-py/http-requests.md). See the +[ValidationError reference](../../../../scripting/reference/utils.md#validationerror) for its full +constructor, attributes, and methods. + +## Validating Serial Port Fields { .ref-head-no-code #validation-serialport-config } + +Because serialport fields are a bit special (they generate multiple visible fields), we've provided a helper method that you can use to make sure the user used the control correctly: + +```python +def validateDeviceConfigUi(self, valuesDict, typeId, devId): + errorsDict = indigo.Dict() + self.validateSerialPortUi(valuesDict, errorsDict, u"devicePortFieldId") + + # Put other config UI validation here -- add errors to errorDict. + + if len(errorsDict) > 0: + # Some UI fields are not valid, return corrected fields and error messages (client + # will not let the dialog window close). + return (False, valuesDict, errorsDict) + + # User choices look good, so return True (client will then close the dialog window). + return (True, valuesDict) +``` + +Notice that at the top of the validation method after we define the errorsDict, we call `self.validateSerialPortUi()`. This method will look for the fields that make up the specified field ("devicePortFieldId" in this case). It will make sure that a valid serial port is selected if it's a physical connection or that the address field is formatted properly if it's one of the network connections. If not, it will automatically add the appropriate errors to the errorsDict and will attempt to correct any URL prefix errors ("socket:%%*%%" or "rfc2217:%%*%%"). You can then continue to validate the rest of the fields in your dialog and use the length check on errorsDict to see if there were any errors discovered. If so, return False along with both dicts. If it's empty, just return true with the valuesDict. + +[Back to Configuration Dialogs](index.md) diff --git a/reference/canonical/plugin-dev/reference/xml/devices.md b/reference/canonical/plugin-dev/reference/xml/devices.md new file mode 100644 index 0000000..4a10608 --- /dev/null +++ b/reference/canonical/plugin-dev/reference/xml/devices.md @@ -0,0 +1,276 @@ + + +# Devices.xml { .ref-head-no-code #device-xml } + +One of the main components that a server plugin can define is a device. In Indigo, devices are the primary objects that users deal with. Lights, thermostats, sprinklers, I/O devices are all examples of device types. One of the biggest requests we get is to support the [name your favorite device]. Obviously, we can’t keep up, so the server plugin architecture was designed to allow you to add device support to Indigo. + +The root element in the Devices.xml file is Devices. Contained in the Devices element are an unlimited number of Device elements that define the device types that your plugin will define. Each Device will contain a ConfigUI element to collect the specific configuration information for a device (note that you don’t need to include name, description, or typeId from the user - Indigo will collect those for you). Here are the specific attributes and elements that can be defined in a Device element: + +## Device Types { .ref-head #devices-xml-device-types } + +| Name | Type | Required | Notes | +|------------------------------------------------|-----------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `type` | Attribute | Yes | This must be one of: `dimmer`, `relay`, `sensor`, `speedcontrol`, `thermostat`, or `custom`. We’ll discuss each below. | +| `id` | Attribute | Yes | This is your plugins unique identifier for this device type. It must be unique in this plugin. | +| `subType` | Attribute | No | This must be one of: `kDimmerDeviceSubType`, `kRelayDeviceSubType`, `kSensorDeviceSubType`. We’ll discuss each below. There are also several device `SubType` examples in the [SDK](https://github.com/IndigoDomotics/IndigoSDK/releases/tag/v2025.1). | +| `allowUserCreation` | Attribute | No | If set, the value must either be `true` or `false` (defaults to `true`). This attribute is discussed in more detail below. | +| `Name` | Element | Yes | This is the name of the device type that users will see when selecting the type in the Devices dialog. | +| `ConfigUI` | Element | No | This is the custom UI for configuring the device. It’s optional, but in reality we can’t envision a device that needs no configuration. If the optional `` element is not used, the "Edit Device Settings…" button will be displayed, but disabled. You'll also need to manually set `dev.configured = True` and then `dev.replaceOnServer()` for the change to take effect. | +| `States` | Element | No | This element is used to describe the possible states for the device. For custom devices it is all available states. For subclassed devices it will describe any additional states for the device. See the description below for more details. | + +```xml + + + Example Temperature Sensor Module + + + + + + + + Integer + Temperature + Temperature + + + +``` + +## Device Subtypes { .ref-head #devices-xml-device-subtypes } + +| Category | SubType | Attribute(s) | Notes | +|----------|---------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------| +| Device | `kDeviceSubType` | Amplifier, Automobile, Camera, Keypad, Mobile, Remote, Robot, Security, Speaker, Streaming, Television, Weather, Other | For example, `indigo.kDeviceSubType.Amplifier` | +| Dimmer | `kDimmerDeviceSubType` | Blind, Bulb, ColorBulb, ColorDimmer, Dimmer, Fan, InLine, Outlet, Plugin, Value | For example, `indigo.kDimmerDeviceSubType.Dimmer` | +| Relay | `kRelayDeviceSubType` | DoorBell, DoorController, GarageController, InLine, Lock, Outlet, Plugin, Siren, Switch | For example, `indigo.kRelayDeviceSubType.Switch` | +| Sensor | `kSensorDeviceSubType` | Analog, Binary, CO, DoorWindow, GasLeak, GlassBreak, Humidity, Illuminance, Motion, Presence, Pressure, Smoke, Tamper, Temperature, UV, Vibration, Voltage, WaterLeak, Zone | For example, `indigo.kSensorDeviceSubType.Temperature` | + +!!! note + There are no device subtypes for other built-in devices at this time. + +In `Devices.xml` + +```xml + + + + Example Adjustable Sensor Module +... +``` + +![Device Subtype Example Image](../../../images/device_sub_type_example.png) + +In Python: + +```python +newdev = indigo.device.create(indigo.kRelayDeviceSubType.Plugin, deviceTypeId="Device1") +newdev.model = "Device 1 Model" +newdev.subModel = "Sub Model 1" +newdev.name = u"Device 1" +``` + +To get information on the device group: +```python +indigo.device.getGroupList(dev.id) +``` + +When the `allowUserCreation` attribute is present and set to `false`, Indigo will not display the device model type when the user elects to create a new device (the "Model" dropdown will not contain the device type). This is especially useful for complex devices that have multiple subtypes that are set programmatically (but not created using the [Device Factory](#devices-xml-device-factory) approach). Your plugin may not support individual subtypes as stand-alone devices and, therefore, you wouldn't want users to be able to create them. The `allowUserCreation` attribute was introduced with Indigo version `2022.1.2` and API `3.1`. + +```xml + +``` + +Check out the [SDK](https://github.com/IndigoDomotics/IndigoSDK/releases/tag/v2025.1) for example plugins of each type of device listed above. The examples show config dialogs, required method stubs, etc., to implement the respective device types. + +!!! important + Plugin developers should account for each devices' callbacks when implementing built-in device types -- even if you don't use all the callbacks within your plugin. For example, if your plugin implements the `Speed Control` device type, your plugin should include handlers for all the Speed Control callbacks -- such as `turnOn`, `turnOff`, or `toggle`. This is very important because these callbacks will still be exposed in the Indigo Client UI for things like Actions and Triggers. It's best to give users an indication that the callback won't do anything in your plugin by writing a warning to the Event log. Consult the [SDK](https://github.com/IndigoDomotics/IndigoSDK/releases/tag/v2025.1) for more information on each device types' callbacks and examples of how to handle them. + +## Custom Device Type { .ref-head #devices-xml-custom-device} + +The `custom` device type is special because it’s completely custom. Therefore, you must specify everything about the device - it’s states and their types (`Number`, `String`, `List` (enumeration), `Boolean`). So, here’s a simple example of the `` element of a custom device, in this case a thermostat: + +```xml + + + Number + Temperature + Temperature + + + Number + Heat Setpoint + Heat Setpoint + + + Number + Cool Setpoint + Cool Setpoint + + + + + + + + + + + Operation Mode Changed + Mode Changed to + Current Mode + Mode is + + +``` + +Each State listed in the States element will be available in various parts of the UI, including the "Device State Changed" Event dialog. + +**Attributes** + +The State element has several attributes and elements that should be defined: + +| Attribute | Type | Required | Notes | +|-----------------------------------------------------|-----------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | Attribute | Yes | This is a unique identifier for this State within the context of this Device element. IDs must follow the XML standard in terms of construction with one addition: they **may not** include periods ('.') - periods are reserved for internal use only. | +| `readonly` | Attribute | No | If you want this state to be read only, set this attribute to "YES". For instance, as in this example, the temperature the thermostat is sensing isn’t user settable. (Currently unused but it may be used in the future) | +| `ValueType` | Element | Yes | This is the type of the state, which must be one of the following: `Boolean`, `Number`, `List` (enumeration constructed just like the `` element of a `` as described above), `String`, and `Separator` (used only to visually group your states in the various popups). The `` element may also have an optional attribute if the value is *`Boolean`//: `boolType` can be: `"TrueFalse"` (default), `"OnOff"`, `"YesNo"`, and `"OneZero"`. Example: *`Boolean`// | +| `TriggerLabel` | Element | Yes | When selecting "Device State Changed" type trigger events, you can select your device in the resulting list. A popup below the device will list all of your States using this text. | +| `TriggerLabelPrefix` | Element | No | When defining an enumeration, trigger change label is usually going to need to be a bit different than just the trigger label ("Mode Changed to Heat" vs "Operation Mode Changed" - the former is to test a specific change, the latter will trigger on any change). To accomplish this, you can specify a prefix that’s prepended to the actual state value (with a space in between). See the example above. | +| `ControlPageLabel` | Element | Yes | In the Control Page Editor, when selecting "Device State" as the display type, then select your device, the next popup will show all your states using this text. | +| `ControlPageLabelPrefix` | Element | No | When defining an enumeration, the control page label is usually going to need to be a bit different ("Mode is Heat" vs "Current Mode" - the former will show whether a the state is true, the latter shows the state value). You can specify a prefix that’s prepended to the actual state value (with a space in between). See the example above. | + +The ValueType element is particularly important - it controls what types of controls are presented to the user in the UI: an integer tells it to show greater than, less than, equal to, not equal to, etc. and a way to enter a number (in the case of a trigger). + +See the [States](../../../scripting/reference/devices/base-class.md#about-custom-device-states) section of the Devices Class page for details on how to set states once the IndigoServer understands the structure of your device's states. + +## Device Factory { .ref-head #devices-xml-device-factory } + +![Device Factory Edit Device Group Image](../../../images/screenshot_2023-02-22_at_1.07.12_pm.png) + +One way to create "multifunction" devices is to use Indigo's Device Factory method. Device Factory devices are defined by using a special `` node in `Devices.xml`. A basic Device Factory device definition would look something like this: + +```xml + + Device Factory Plugin Device Group + Create + + + + + + + + + + +``` + +Note that when the `` node is present, devices that include the `subType` attribute will not be presented to the user as individual device types when they elect to create a new plugin device (it's not possible to have a Device Factory implementation and individual plugin device definitions together). You define sub-devices like any other device type, with the addition of the `subType` attribute: + +```xml + + Plugin Device Group + Create + + + + + + + + + + + + + + Example Dimmer Module + + + + + + + + + + String + State + State + + + + + + Example Adjustable Sensor Module + + + String + State + State + + + + + + Example Lock Module + + + String + State + State + + + +``` + +Once the device definitions have been established, you can use your plugin code to create the device group. This function can be called from the `validate_device_factory_ui()`or `closed_device_factory_ui()` methods. + +```python +def create_the_device_group(self, my_name): + """ + Convenience method for device creation. This could also be done in closed_device_factory_ui() for example. Note + that some device props are read only even when you create them from scratch (i.e., dev.version) and some will + be ignored if you try to set them (i.e., dev.description, dev.errorState). + + Note that `protocol`, `name` and `deviceTypeId` are all required with the `indigo.device.create()` method call. + """ + self.debugLog("create_the_device_group called") + + # Create the first device in the group. Note that the ''deviceTypeId'' value matches the ''id'' we used in our ''Devices.xml'' definition. + new_dev = indigo.device.create(protocol=indigo.kProtocol.Plugin, name=my_name, deviceTypeId="my_dimmer_device") + new_dev.model = "Grouped Device" + new_dev.subModel = "Dimmer" + new_dev.name = f"{my_name} Dimmer" + new_dev.replaceOnServer() + + # Add the group my_name setting to the props of device 1 for later use. + new_props = new_dev.pluginProps + new_props['name'] = my_name + new_dev.replacePluginPropsOnServer(new_props) + + # Create the second device in the group. You can also add a state value here if desired. + new_dev = indigo.device.create(protocol=indigo.kProtocol.Plugin, name=my_name, deviceTypeId="my_sensor_device") + new_dev.model = "Grouped Device" + new_dev.subModel = "Temperature" + new_dev.name = f"{my_name} Temperature" + new_dev.replaceOnServer() + new_dev.updateStateOnServer('state', value="Some value.") + + # You can also create devices that don't have corresponding device parameters established in Devices.xml. + new_dev = indigo.device.create(protocol=indigo.kProtocol.Plugin, name=my_name, deviceTypeId="my_lock_device") + new_dev.model = "Grouped Device" + new_dev.subModel = "Lock" + new_dev.name = f"{my_name} Lock" + new_dev.replaceOnServer() +``` + +!!! important + You are responsible for catching all the mandatory methods for the devices you create. The Device Factory method does not create them for you. + +For another option to create Device Factory devices, check out the example in the [Indigo SDK](https://github.com/IndigoDomotics/IndigoSDK). diff --git a/reference/canonical/plugin-dev/reference/xml/events.md b/reference/canonical/plugin-dev/reference/xml/events.md new file mode 100644 index 0000000..13e0966 --- /dev/null +++ b/reference/canonical/plugin-dev/reference/xml/events.md @@ -0,0 +1,31 @@ + + +# Events.xml { .ref-head-no-code #events-xml } + +The XML in this file describes all events that your plugin will generate for use in Indigo. Your users will use these in the Trigger Events dialog just like any of the built-in Indigo events (like Power Failure, Email Received, etc.) Device State Changed events are handled by the `` defined in the `` elements described above, but your plugin can offer other types of events, including update notifications, battery low notifications, button press notifications, etc. + +Here’s a very small `Events.xml` file that just defines a plugin update event: + +```xml + + + http://www.yourdomain.com/plugin/pluginEvents.html + + Plugin Update Available + + +``` + +As you can see, your `` elements can define a `` element as well - the trigger events dialog now has a help button on it and if one of your events is selected clicking on the help button will take your user to the specified URL. If you don’t specify one then the default help page for all trigger events will show. You can specify an Event to be a separator in your event list so that when they're displayed in the UI there is a visual separation. Simply insert an Event defined like this between two other Event elements: + +`` + +**Attributes** + +While you don't have to include any elements, the id still must be unique. Here’s how to construct your `` elements: + +| Attribute | Type | Required | Notes | +|---------------------------------------|-----------|----------|-------------------------------------------------------------------------------------------------------------------| +| `id` | Attribute | Yes | This is a unique id for the event in this `Events.xml` file. | +| `Name` | Element | Yes | This is the text that’s shown in the trigger event dialog that represents this event. | +| `ConfigUI` | Element | No | If your event requires any configuration, you can specify a `` element that’s defined exactly as above. | diff --git a/reference/canonical/plugin-dev/reference/xml/menuitems.md b/reference/canonical/plugin-dev/reference/xml/menuitems.md new file mode 100644 index 0000000..74d460c --- /dev/null +++ b/reference/canonical/plugin-dev/reference/xml/menuitems.md @@ -0,0 +1,98 @@ + + +# MenuItems.xml { .ref-head-no-code #menuitems-xml } + +Your plugin may also define menu items, which will be shown at the bottom of your plugin’s sub-menu on the `Plugins` menu - they will be the last thing in the menu unless you also include scripts in the `Menu Items` folder of your plugin’s bundle. If there are scripts there, they will be last, and a separator will be placed between the menu items defined in this XML file and any script files. + +Here’s a sample MenuItems.xml file: + +```xml + + + + Reset Interface + resetInterface + + + Check for Updates + checkForUpdates + + + Turn Off Light... + turnOffLight + Turn Off + + + + + + + + + + Execute Some Action + + + +``` + +It’s pretty straight-forward. + +**Attributes** + +The `MenuItems` element will contain multiple `` elements. Here’s how to construct a ``: + +| Attribute | Type | Required | API Version | Notes | | +|---------------------------------------------|-----------|----------|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--| +| `id` | Attribute | Yes | 1.0 | This is a unique id for the menu item in this file. | | +| `Name` | Element | Yes | 1.0 | This is the text that’s shown in your plugin’s sub-menu. | | +| `CallbackMethod` | Element | No | 1.0 (1.1) | This is the name of the method defined in your plugin that will be called when your user selects this menu item.

API v1.1: If you specify a for the menu, then this property is optional. If it's present, then the the method will be called with arguments much like a validation (see example below). If it's not present (in which case a ConfigUI must be present), then the dialog will contain only a single "Close" button. In that case the functionality in the dialog will be completely left up to buttons contained within the dialog. | | +| `ConfigUI` | Element | No | 1.1 (1.2) | If you want a menu to open an associated dialog, you can include a standard ConfigUI element (see [Configuration Dialogs](configui/index.md#plugin-config) above for details).

API v1.2: You may also specify an actionId attribute on this element with the id of one of your actions and that action UI will be executed rather than having to specify the UI itself. If it requires a device ID that will be added at the top of the dialog. Ex: `` | +| `ButtonTitle` | Element | No | 1.1 | If you have a ConfigUI element and a CallbackMethod, you can add this element to specify the title used on the button that executes the dialog. By default it's "Execute". | | + +Here's an example of a menu item method definition in your plugin.py file for the "Check for Updates" menu item above: + +```python +def checkForUpdates(self): + indigo.server.log(u"checkForUpdates called") + # Do the actual work here to see if there are any updates +``` + +As of API v1.1 you can have your menu item open a dialog which will allow you to gather input from a user before executing the action. Just add a `` element to your MenuItem definition and define that dialog just like any other. There are a couple of differences between this dialog and other dialogs. First, the `CallbackMethod` for menu items will be used rather than a validation method. It will, however, operate in much the same way. If you specify a `CallbackMethod` then it will get called with the valuesDict and the menu item's ID. You can return `True`, which will cause the dialog to close, or return false with an error dictionary which will mark the invalid fields just like the validation method would. You also have the option of adding a `ButtonTitle` element - which will allow you to change the name of the button (it defaults to `Execute`). + +If you don't specify a `CallbackMethod` then the dialog will not have `Execute` and `Cancel` buttons but rather a single `Close` button. Why? We wanted to allow you the flexibility of making a dialog perform multiple actions if you like using button field types within the dialog itself. When doing that it would be awkward to have an `Execute` button that didn't really do anything. + +Whenever a user executes a menu item that contains a configuration dialog, it’s running as if it’s the first time the menu item is run (default values will always be used if present). Any values the user enters will be discarded after the menu item action is run. In other words, the next time the dialog is opened, default values will be used even if a user changed that value the last time the menu item was run. If a field doesn't specify a default value, the field will be empty each time the dialog is opened. + +Here's an example of a menu item method definition in your plugin.py file for the "Turn Off Light" menu item above: + +```python +def turnOffLight(self, valuesDict, typeId): + indigo.server.log(f"Turning off light: {valuesDict["targetDevice"]}") + # perform the action here. If there are errors in any of the fields then + # you can return (False, valuesDict, errorsDict) just like a validation + # method and the dialog won't close and will show the errors. If you + # return True then the dialog will close when the method completes. + errorsDict = indigo.Dict() + return (True, valuesDict, errorsDict) +``` + +**Get Menu Action Config UI Values** + +As noted above, a menu item config dialog will open as if it's being opened for the first time. However, if you would like values entered into the dialog to be persistent, you can load those values using the built-in `get_menu_action_config_ui_values` callback. Your plugin is responsible for storing and retrieving the values for the dialog yourself. How you store those values is up to you (you could save them to a file or store them in a hidden plugin configuration field, for example). Then, you can load them into the config dialog using `get_menu_action_config_ui_values` which will be called automatically if it exists: + +`get_menu_action_config_ui_values()` + +```python +def get_menu_action_config_ui_values(self, menu_id): + menu_items = indigo.Dict() + if menu_id == "my_menu_id": + menu_items['foobar'] = "my stored value" + return menu_items +``` + +This method will be called whenever a menu item with a config dialog is called, so be sure to apply values using the proper menu id. + +**Custom HTML Menu Item Dialogs** + +You can also implement your own custom menu item form in HTML if you prefer. Rather than adding `` and `` definitions, you simply specify a `` element. The URL specified can either be a fully specified URL (`protocol://host/path`) or it may be a relative URL (`/some/relative/path`). If it's the latter, then Indigo will attempt to guess the [best base URL](../../../scripting/reference/server-commands.md#get-web-server-url). You would then handle those form requests using [the built-in request handling mechanism discussed below](../plugin-py/http-requests.md#processing-http-requests-in-your-plugin). See the **Example HTTP Responder** plugin in the [SDK](https://github.com/IndigoDomotics/IndigoSDK/releases/tag/v2025.1) for an example. diff --git a/reference/canonical/plugin-dev/reference/xml/pluginconfig.md b/reference/canonical/plugin-dev/reference/xml/pluginconfig.md new file mode 100644 index 0000000..264a7ee --- /dev/null +++ b/reference/canonical/plugin-dev/reference/xml/pluginconfig.md @@ -0,0 +1,31 @@ + + +# PluginConfig.xml { .ref-head-no-code #plugin-config } + +We started the discussion of the Indigo XML by describing the `ConfigUI` element that’s present for `Devices`, `Events`, and `Actions`. But, how do you configure your plugin itself? For instance, let’s say your plugin requires a connection to some hardware interface, say something akin to the Insteon PowerLinc interface. How do you specify which serial port that interface is connected to? + +The simple answer is that, just as there is a configuration interface for an Insteon hardware adaptor, your plugin has a configuration interface as well. The `PluginConfig.xml` file describes the UI to configure your plugin. The root element in that file is `PluginConfig`, and it contains exactly the same elements that the `ConfigUI` element described above does. Here’s an example: + +```xml + + + http://www.yourdomain.com/plugin/config.html + + [SNIP - lots of definitions] + + +``` + +The IPH will handle retrieving saved preferences and passing them to your plugin as well as saving changed preferences to disk. Your preferences will be stored in a file in this directory: + +`/Library/Application Support/Perceptive Automation/Indigo {{ version }}/Preferences/Plugins/` + +and it will be named by using your plugin’s ID (as defined by the `CFBundleIdentifier` in the `Info.plist` file). So, using the example `Info.plist` at the beginning of this doc, the pref file would be named: + +`com.yourdomain.plugin.indiPref` + +We write each plugin’s preferences into individual files so that it will be easier for you to help your users troubleshoot problems by deleting the prefs and starting over. We also write them into the standard `Preferences` folder so that when you upgrade your plugin -- or we upgrade Indigo -- the preferences won’t get lost. + +## Custom HTML Config Dialogs { #custom-html-config-dialogs } + +You can also implement your own custom configuration in HTML if you prefer. Rather than adding lots of `` definitions, you simply specify a `` element. The URL specified can either be a fully specified URL (protocol://host/path) or it may be a relative URL (/some/relative/path). If it's the latter, then Indigo will attempt to guess the [best base URL](../../../scripting/reference/server-commands.md#get-web-server-url). You would then handle those form requests using [the built-in request handling mechanism discussed below](../plugin-py/http-requests.md#processing-http-requests-in-your-plugin). See the **Example HTTP Responder** plugin in the [SDK](https://github.com/IndigoDomotics/IndigoSDK/releases/tag/v2025.1) for an example. diff --git a/reference/canonical/plugin-dev/reference/xml/supporturl.md b/reference/canonical/plugin-dev/reference/xml/supporturl.md new file mode 100644 index 0000000..8e8539a --- /dev/null +++ b/reference/canonical/plugin-dev/reference/xml/supporturl.md @@ -0,0 +1,19 @@ + + +# SupportURL Elements { .ref-head-no-code #support-url-elements} + +Anywhere you can specify a `` element, the value can be either a full URL or a relative URL. If it's relative, then Indigo will attempt to guess the [best base URL](../../../scripting/reference/server-commands.md#get-web-server-url). You can use this to supply [static HTML](../../guide.md#resources-folder) files or dynamic help provided through the [HTTP processing API discussed below](../plugin-py/http-requests.md#processing-http-requests-in-your-plugin). + +**Full URL** +```xml + + https://www.somesite.com + +``` + +**Relative URL** +```xml + + /some/relative/path + +``` diff --git a/reference/canonical/plugin-dev/sdk-examples.md b/reference/canonical/plugin-dev/sdk-examples.md new file mode 100644 index 0000000..3c15ce1 --- /dev/null +++ b/reference/canonical/plugin-dev/sdk-examples.md @@ -0,0 +1,49 @@ + + +# SDK Example Plugins + +The [Indigo SDK](https://github.com/IndigoDomotics/IndigoSDK/releases) ships a set of fully working example plugins with complete XML and Python source. They're the recommended starting point for new plugins — find the example closest to what you're building and modify it. Several are also installed with Indigo itself (look in the Plugins menu under the disabled plugins list). + +## Custom Device Plugin { #example-custom-device } + +Illustrates how a plugin can create custom Indigo devices which, like native devices, have states, triggers, actions, and UI. + +## Relay/Dimmer Device Plugin { #example-relay-dimmer } + +Creates Indigo devices which inherit basic relay and dimmer states, triggers, actions, and UI — and shows how to add additional device states and actions on top. + +## Sensor Device Plugin { #example-sensor } + +Creates devices which inherit basic sensor states, triggers, actions, and UI, plus plugin-defined states and actions. + +## Energy Meter Device Plugin { #example-energy-meter } + +Creates devices which inherit energy meter states (watts, kWh), triggers, actions, and UI. + +## Speed Control Device Plugin { #example-speed-control } + +Creates speed-control devices (for example, ceiling fans) with inherited speed states, triggers, actions, and UI. + +## Sprinkler Device Plugin { #example-sprinkler } + +Creates sprinkler controller devices with inherited zone-control states, triggers, actions, and UI. + +## Insteon/X10 Listener Plugin { #example-insteon-x10-listener } + +Shows how a plugin can subscribe to receive callbacks whenever an Insteon or X10 command is received or sent by Indigo. + +## Database Traverse Plugin { #example-db-traverse } + +Shows how to traverse the [Indigo Object Model](../scripting/iom-concepts.md) to enumerate all devices, triggers, schedules, and other objects. + +## Twisted Telnet Server Plugin { #example-twisted-telnet } + +Shows how a plugin can use the Python [Twisted event framework](https://twistedmatrix.com). The example runs a small telnet server that can flash devices on/off; once enabled, connect from Terminal with: + +```bash +telnet 127.0.0.1 9176 +``` + +## More examples in the SDK + +The SDK repository also includes examples not documented here — device factories, thermostat devices, action APIs, an HTTP responder, broadcaster/subscriber patterns, variable-change and Z-Wave listeners — plus the *Updating to API version 3.0 (Python 3)* migration guide. Browse the [SDK on GitHub](https://github.com/IndigoDomotics/IndigoSDK) for the full set. diff --git a/reference/canonical/plugin-dev/tutorials/building.md b/reference/canonical/plugin-dev/tutorials/building.md new file mode 100644 index 0000000..a194f01 --- /dev/null +++ b/reference/canonical/plugin-dev/tutorials/building.md @@ -0,0 +1,273 @@ + + +# Building a Plugin + +!!! abstract "In this guide" + Code-level reference for building and extending Indigo server plugins: reading and writing preferences, handling device configuration callbacks, implementing concurrent threads, and using the Indigo SDK example plugins as starting points. Complements the Plugin Developer's Guide with runnable, copy-pasteable examples. + +Note most of the code examples below are calling methods on the object *`self`*. In this context, *`self`* is meant to be the Plugin instance as defined inside the *`plugin.py`* file. To execute the sample code outside of Plugin instance methods, use the *`indigo.activePlugin`* object instead. + +*Important:* For simplicity, some of the samples below specify objects based on name (*`indigo.devices["office desk lamp"]`*). However, the preferred lookup mechanism is to use the object's numeric ID (*`indigo.devices[12345678]`*), which can be retrieved by control-clicking on the object name in Indigo's Main Window. By using the numeric ID you ensure the object will be found even if its name is changed. + +## Indigo Plugin SDK Source Code Examples +The Indigo Plugin SDK ([available here](https://github.com/IndigoDomotics/IndigoSDK/releases)) includes short, example plugins with full XML and python source. They are a great place to start when developing new plugins. + +Included in the SDK are examples that create plugin based relay, dimmer, thermostat, sensor, speed control, sprinkler, energy meter and custom devices. Also included is an example showing basic Indigo database traversal, how to catch low-level X10/Insteon messages, and how to create an Indigo telnet server using the python twisted framework. The following table lists the Indigo Plugin SDK examples that were available at the time Indigo {{ version }} shipped. + +| SDK Plugin task | Plugin that illustrates an approach | +| --- | --- | +| Broadcast Plugin Information to Listeners | Example Custom Broadcaster | +| Subscribe to Plugin Information Broadcast | Example Custom Subscriber | +| Walk Through the Indigo Database | Example Database Traverse | +| Non-native Device Type | Example Device - Custom | +| Energy Meter Device Type | Example Device - Energy Meter | +| Device Factory Device Type | Example Device - Factory | +| Switch / Relay / Dimmer Device Type | Example Device - Relay and Dimmer | +| Sensor Device Type (Water, Motion, Light, Humidity) | Example Device - Sensor | +| Speed Control Device Type (Fan) | Example Device - Speed Control | +| Sprinkler Device Type | Example Device - Sprinkler | +| Thermostat Device Type | Example Device - Thermostat | +| Interact with the Indigo Server Using HTTP Client / Server | Example HTTP Responder | +| Listen for Insteon and/or X-10 Traffic | Example Insteon:X10 Listener | +| Listen for Indigo Variable Change Notifications | Example Variable Change Subscriber | +| Listen for Z-Wave Traffic | Example Z-Wave Listener | + + +We'll be adding additional example plugins in the future. + +## Other Useful Plugin Source Code Examples +Additionally, below is a table of common plugin tasks that are used in either built-in or freely available plugins that implement those tasks in some form or another (from simplest to most complex): + +| Plugin task | Plugin that illustrates an approach | | +| --- | --- | --- | +| Parsing JSON, XML from an IP source | NOAA Weather, [WeatherSnoop](https://www.indigodomo.com/pluginstore/10/) | +| Integrating with native Mac Apps | Airfoil | | +| Sending RS232 (serial port & network serial port) Commands | EasyDAQ | | +| Reading RS232 (serial & network serial port) Input | EasyDAQ | | +| Interacting with an IMAP mail server | Email+ | | +| Interacting with an HTTP Web server | Example HTTP Responder | | +| Creating custom devices with states | Simple: NOAA Weather, [WeatherSnoop](https://www.indigodomo.com/pluginstore/10/) - Complex: EasyDAQ | +| Creating custom actions | [GhostXML](https://www.indigodomo.com/pluginstore/38/) | +| Creating custom events | Airfoil | | + + +Each of these plugins is installed by default with Indigo - in the `/Library/Application Support/Perceptive Automation/Indigo [VERSION]/Plugins (Disabled)/` folder, or available as an Open Source plugin from the [Indigo Plugin Store](https://www.indigodomo.com/pluginstore/). To see the various XML and python source files, just right-click on it in the `Finder` and select `Show Package Contents`. + +The [SDK example](https://github.com/IndigoDomotics/IndigoSDK/releases) plugins, the plugins included with Indigo and Open Source plugins above are great places to see working examples of plugins and their source code. + +## How to Read and Write Plugin Preferences + +- The per plugin preferences (pref) file is automatically managed (created, loaded, updated). +- Pref values can be numbers, boolean, strings, indigo.Dict() or indigo.List(). +- Key values defined in `PluginConfig.xml` are automatically mapped into the plugin's prefs space which is available via: + +`self.pluginPrefs["somePrefKey"]` + +- Or, if you're not sure the key exists: + +`self.pluginPrefs.get("somePrefKey", "default value if key doesn't exist")` + +- The latter will return the second parameter if they key doesn't exist in the dictionary - it's your responsibility to add it to the prefs dict if you want it to be stored permanently. +- A plugin can also insert other values into its pluginPrefs space (not just values shown in the plugin's config UI). This is a great way to maintain values that are not directly visible or available to the end user. +- To read a preference value access its key: + +```python +someVal = self.pluginPrefs["somePrefKey"] +indigo.server.log("value is " + str(someVal)) +``` + +- To create a new or update an existing preference value assign it a new value: + +`self.pluginPrefs["somePrefKey"] = 1234` + +- The Indigo server will save the plugin prefs automatically, but you can also cause them to be saved to the server immediately using: + +```python +indigo.server.savePluginPrefs +``` + +## How to Add Plugin Metadata to Devices, Trigger & Scheduled Events, Variables, etc. +FIXME (useful, but very rough notes below) + +- Most Indigo database objects support the addition of plugin specific metadata. +- Every plugin has its own name space accessed via the object instance *`.pluginProps`*. +- The pluginProps dictionary supports numbers, boolean, strings, indigo.Dict() or indigo.List(). + +### Add new plugin metadata to the Device "den fixture" +```python +dev = indigo.devices["den fixture"] # device ID preferred +newProps = dev.pluginProps +newProps["onCycles"] = 5 +newProps["moreData1"] = "abc" +newProps["moreData2"] = True +newProps["moreData3"] = 123.45 +dev.replacePluginPropsOnServer(newProps) +``` + +#### Read plugin specific property `onCycles` from the Device "den fixture" +```python +dev = indigo.devices["den fixture"] # device ID preferred +onCycles = dev.pluginProps["onCycles"] +indigo.server.log("onCycles is " + str(onCycles)) +``` + +#### Increment the plugin specific property onCycles by 1 for the Device "den fixture" +```python +dev = indigo.devices["den fixture"] # device ID preferred +newProps = dev.pluginProps +newProps["onCycles"] += 1 +dev.replacePluginPropsOnServer(newProps) + +dev = indigo.devices["den fixture"] # device ID preferred +onCycles = dev.pluginProps["onCycles"] +indigo.server.log("onCycles is now " + str(onCycles)) +``` + +- Plugins have read-only access to other plugin metadata via *`.globalProps`*. +- Plugins have read/write access to their own metadata space. + +## How to Create a Custom Plugin Device +FIXME (useful, but very rough notes below) + +- Plugin Device state and properties are defined in Devices.xml. +- Properties define the user configurable options for a device instance, and are specified in the *``* XML node. Every field *`id`* is automatically mapped into the device instance *`.pluginProps`* metadata dictionary (described above) as a unique key. +- States are specified in the Devices.xml *``* XML node, and are used to define the transient state information for a device (ex: on/off setting, brightness, temperature, etc.). +- States defined in Devices.xml are automatically shown in the Trigger Event `*Device State Changed*` options when that plugin device type is selected, and are automatically shown in the Control Page editor when a control is created inspecting that plugin device. +- States are read-only for everyone except the plugin that defines the device's states. +- Plugins should update a device state after it has sent commands to hardware, or somehow received new state information from hardware. Example that increments the plugin defined state `*heatSetPoint*` by 1: + +```python +dev = indigo.devices["Custom Plugin Thermostat"] # device ID preferred +dev.updateStateOnServer("heatSetPoint", dev.states["heatSetPoint"] + 1) +``` + +- Plugins should subclass `*deviceStartComm*` and `*deviceStopComm*` to start/stop any hardware communication (normally via a new per-device thread): + +```python +def deviceStartComm(self, dev): + self.easydaq.startCommThread(dev) + +def deviceStopComm(self, dev): + self.easydaq.stopCommThread(dev) +``` + +- Calls to `*deviceStartComm*` and `*deviceStopComm*` are automatically managed by the Indigo Server and Indigo Plugin Host. When a plugin first connects all enabled device instances owned by the plugin will receive `*deviceStartComm*` calls. Likewise, `*deviceStartComm*` is called when a new plugin device is created or duplicated. `*deviceStopComm*` is called whenever a plugin is disabled, deleted, or when the plugin is shutting down. Therefore, these two functions should be the primary bottlenecks for starting/stopping device hardware or network connections. + +## How to Create a Custom Plugin Trigger Event +The XML in this file describes all events that your plugin will generate for use in Indigo. Your users will use these in the Trigger Events dialog just like any of the built-in Indigo events (like Power Failure, Email Received, etc.) Your plugin can offer other types of events, including update notifications, battery low notifications, button press notifications, and so on. + +Here’s a very small `Events.xml` file that defines a plugin update event, with a small sample configuration dialog: + +```xml + + + http://www.yourdomain.com/plugin/pluginEvents.html + + Plugin Update Available + + https://www.yourdomain.com/plugin/someOtherEvent.html + + + + + + +``` + +As you can see, your `` elements can define a `` element as well -- and separate support elements for the event's configuration dialog (the trigger events dialog now has a help button on it and if one of your events is selected clicking on the help button will take your user to the specified URL). You can specify an Event to be a separator in your event list so that when they're displayed in the UI there is a visual separation. Simply insert an Event defined like this between two other Event elements: + +```xml + +``` + +There are several Event-related and Trigger-related methods that you can add to your `plugin.py` file--some of which are required in order to have the Event do something. These methods include: + +| Element | Description | +| --- | --- | +| self.closedEventConfigUi(self, values_dict, user_cancelled, type_id, event_id) | Called after user clicks the Save button within the event configuration dialog (after 'validateEventConfigUi()' method has finished successfully). Used to finalize any Event configuration steps. | +| self.getEventConfigUiValues(self, plugin_props, type_id, event_id) | Called when an Event configuration window is first opened. | +| self.getEventConfigUiXml(self, type_id, event_id) | Called when an Event configuration window is first opened. The method is used to provide a valid XML payload in place of the `Events.xml` file. | +| validateEventConfigUi(self, values_dict, type_id, event_id) | Called when a user clicks the Save button within the event configuration dialog. Used to provide configuration input validation to ensure the settings are within your parameters. | + + +The power of Events + +## How to Monitor Changes to Various Indigo Objects +In some instances, a plugin may need to know about changes to an Indigo object that it doesn't directly control. For example, you might have a [plugin that groups together a number of fans](https://www.indigodomo.com/pluginstore/89/) and treats the multiple fans as one single fan. To do this successfully, it would be necessary to monitor changes to a single fan (say a local call to turn the fans on) and propagate that call to the other fan objects. + +Indigo provides a method to do this by allowing the plugin to "subscribe" to changes for all objects in a particular class. + +### Subscribe to Indigo Object Changes +To monitor changes to different classes of Indigo objects, you can include a call to the appropriate `subscribeToChanges` method. Calling any of these methods tells Indigo to send a notification to the plugin of any changes to all objects of that type -- not only plugin's own objects -- so use these methods sparingly. + +| Element | +| --- | +| indigo.devices.subscribeToChanges() | +| indigo.variables.subscribeToChanges() | +| indigo.triggers.subscribeToChanges() | +| indigo.schedules.subscribeToChanges() | +| indigo.actionGroups.subscribeToChanges() | +| indigo.controlPages.subscribeToChanges() | + + +```python +def __init__(self, plugin_id, plugin_display_name, plugin_version, plugin_prefs): + super().__init__(plugin_id, plugin_display_name, plugin_version, plugin_prefs) + + indigo.devices.subscribeToChanges() + indigo.variables.subscribeToChanges() + indigo.triggers.subscribeToChanges() + indigo.actionGroups.subscribeToChanges() +``` + +These methods only invoke the notification process, they don't do anything with the notifications they subscribe to. In order to react to any notifications, you'll need to use the methods below. + +#### Monitor Device State Changes from a Plugin +| Element | +| --- | +| self.deviceCreated(self, dev) | +| self.deviceDeleted(self, dev) | +| self.deviceUpdated(self, orig_dev, new_dev) | + + +```python +def deviceCreated(self, dev): + # Receives a copy of the device instance + self.logger.debug(f"{dev.name} created." +``` +```python +def deviceDeleted(self, dev): + # Receives a copy of the device instance + self.logger.info(f"{dev.name} deleted." +``` +```python +def deviceUpdated(self, orig_dev, new_dev): + self.logger.debug("===== deviceUpdated =====") + + # Call the parent implementation of deviceUpdated() base class + indigo.PluginBase.deviceUpdated(self, orig_dev, new_dev) + + # Convert the payload objects from indigo.Dict() objects to Python dict() objects. + orig_dict = {} + for (k, v) in orig_dev: + orig_dict[k] = v + + new_dict = {} + for (k, v) in new_dev: + new_dict[k] = v + + # Create a dictionary that contains only those properties and attributes that have changed. + diff = {k: new_dict[k] for k in orig_dict if k in new_dict and orig_dict[k] != new_dict[k]} + + self.logger.debug(f"Attributes changed: {diff}") +``` + +#### Monitor Changes to Variables +| Element | +| --- | +| self.variableCreated(self, var) | +| self.variableDeleted(self, var) | +| self.variableUpdated(self, orig_var, new_var) | + + +The variable change monitoring methods operate like the methods for devices above. Note, unlike Indigo devices, you'll need to monkey patch the `var` object in order to iterate over it. diff --git a/reference/canonical/scripting.md b/reference/canonical/scripting.md new file mode 100644 index 0000000..8a6541b --- /dev/null +++ b/reference/canonical/scripting.md @@ -0,0 +1,24 @@ + + +# Scripting Indigo + +Everything in Indigo you can do from the user interface — and a good deal you can't — can be done from Python. Scripts run in the **Script Editor** (Plugins → Open Scripting Shell), embedded inside triggers, schedules, and action groups, or as external script files. No plugin development required. + +## Where to start + +If you're new to scripting Indigo, work through the [Scripting Tutorial](tutorial.md) — it builds up from one-line device commands to scripting third-party plugins. Then read [IOM Concepts](iom-concepts.md) to understand how the Indigo Object Model represents your devices, triggers, schedules, action groups, and variables in Python. + +## Guides + +- [Python Packages](guides/python-packages.md) — what ships with Indigo's bundled Python and how to install additional packages. +- [Python Version Conflicts](../user/troubleshooting/python-conflicts.md) — if scripts behave differently inside and outside Indigo. + +## IOM Reference + +The complete reference for every IOM class and command namespace: [Actions](reference/actions.md), [Action Groups](reference/action-groups.md), [Devices](reference/devices/index.md), [Device Subclasses](reference/device-subclasses/index.md), [Folders](reference/folders.md), [Schedules](reference/schedules.md), [Triggers](reference/triggers.md), [Variables](reference/variables.md), plus [Server Properties & Commands](reference/server-commands.md), [Insteon Commands](reference/insteon-commands.md), [X10 Commands](reference/x10-commands.md), and [Event Data Path Specifiers](reference/event-data-paths.md). + +These pages document the *scripting* (Python) view of Indigo's objects. For what these objects mean and how to use them from the UI, see the [Concept Overview](../user/concepts/index.md) in the User Guide. + +## Related + +Building a full plugin instead? The [Plugin Development](../plugin-dev/index.md) section builds on everything here. Integrating an external system over HTTP or WebSockets? See [Integration APIs](../api/index.md). diff --git a/reference/canonical/scripting/guides/python-packages.md b/reference/canonical/scripting/guides/python-packages.md new file mode 100644 index 0000000..892377a --- /dev/null +++ b/reference/canonical/scripting/guides/python-packages.md @@ -0,0 +1,292 @@ + + +# Python Packages and Indigo + +!!! abstract "In this guide" + How Python 3.11 is bundled with Indigo {{ version }}, which packages come pre-installed, how to install additional packages using `pip3`, and where plugin packages should be installed to avoid conflicts. Also covers the risks of installing other Python versions on the same Mac. + +This page describes how **Indigo {{ version }}.0** works with Python, specifically with respect to packages/libraries. + +Indigo {{ version }} ships with [API version 3.8](https://www.indigodomo.com/indigo/api_release_notes/3.8/) (check the [Api Version Chart](https://www.indigodomo.com/indigo/api_version_chart.html) to see what version of the API is available in which Indigo versions). Python 3 support was introduced in Indigo 2023.2 (API 3.4), and Python 3 is automatically installed as part of the Indigo installer. For Indigo {{ version }}, we are including **Python 3.13**. + +**All embedded and external Python scripts are run in Python 3** - so you will need to update your scripts if they aren't working (we can't automatically tell if they will work or not unfortunately). We believe that simple scripts will most likely work without change, but if not, you can walk through [the document](https://github.com/IndigoDomotics/IndigoSDK/blob/main/Updating%20to%20API%20version%203.0%20(Python%203).md) mentioned above and perhaps spot the changes you need. You can also post your scripts on the [Help Converting to Python 3](https://forums.indigodomo.com/viewforum.php?f=364) forum to get help with any conversion issues. + +The Indigo {{ version }} installer includes the Python installer (including Intel and Apple Silicon support) from [https://www.python.org](https://www.python.org) - the official Mac installers for the Python language. As a result, any installs of other versions of Python from that website may cause unexpected results in Indigo and potentially cause it to break. We've taken all precautions we can try to avoid conflicts, but there is only so much we can do. So please use caution when installing other Python versions to your server machine. If you have any questions about other Python versions, [post your questions on our forums](https://forums.indigodomo.com/viewforum.php?f=106). + +## Installing Additional Python Packages with Indigo {{ version }} +As noted above, Indigo {{ version }} ships with Python 3.11 and which contains several modules in addition to Python's standard core modules. The specific modules installed with each version of Python are listed below. However, in some instances, you may need to manually install one or more additional Python packages to support a third-party plugin or your own custom scripts. The manner in which this is done, depends on the Indigo API version that the plugin supports. + +Use `pip3 install [package]` commands to install packages: + +```text +Last login: Tue jan 1 12:34:56 on console +user@my mac ~ % pip3 install tensorflow +... +``` + +To see which packages are installed, use the `list` command: + +```text +Last login: Tue jan 1 12:34:56 on ttys000 +user@my mac ~ % pip3 list +Package Version +------------------ --------- +astroid 2.9.3 +certifi 2021.10.8 +charset-normalizer 2.0.11 +cycler 0.11.0 +fonttools 4.29.1 +... +user@my mac ~ % +``` + +!!! note "Note" + Some Python packages -- and specifically those that are included in IPH3 as [described below](#python-packages-installed-by-indigo) -- will not appear in the `pip3 list` output. + +To see more detailed information about an individual package, use the `show` command. + +```text +Last login: Sun Mar 27 10:20:04 on ttys000 +user@my mac ~ % pip3 show requests +Name: requests +Version: 2.27.1 +Summary: Python HTTP for Humans. +Home-page: https://requests.readthedocs.io +Author: Kenneth Reitz +Author-email: me@kennethreitz.org +License: Apache 2.0 +Location: /Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages +Requires: charset-normalizer, certifi, idna, urllib3 +Required-by: +user@my mac ~ % +``` + +Particularly helpful in the `show` output is the dependencies information -- both what the package requires and what requires the package. Note that the dependencies information may not include information about packages that are included with the IPH. + +!!! warning + Installing additional Python versions on the Indigo {{ version }} server machine is ****NOT**** recommended (see above). Installing additional Python modules should only be done when necessary. + +### Python Packages for Plugin Developers +Because the packages included with Indigo should not be modified (to ensure a smoothly running server), plugin developers must include any other desired version within the plugin package. Plugin developers should note that some Python packages will not integrate universally across all installations as they rely on code compiled for specific hardware configurations (for example, Intel vs. Apple silicon). The IPH processes use the following [Module Search Paths](https://docs.python.org/3/tutorial/modules.html#the-module-search-path) (Python interpreters use `PYTHONPATH` to describe the order of directories to search for installed packages, the first matching package will be used): + +The `PYTHONPATH` for IPH3 is: + +1. `Contents/Server Plugin` (inside the plugin bundle) +1. `Contents/Packages` (inside the plugin bundle) +1. IndigoPluginHost3 (inside the app itself) +1. `/Library/Application Support/Perceptive Automation/Python3-includes` +1. `/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages` + +Packages are installed in several ways: + +1. You may add modules/packages **that you develop yourself** to the `Python#-includes` folders above, and those will be shared across all Indigo Plugins and Scripts. +1. Other packages are installed using a `pip3` process. **Warning**: Indigo will preinstall several packages ([listed below](#python-packages-installed-by-indigo)) at install time. You should avoid modifying those unless absolutely necessary as other 3rd party plugins may rely on them. + +If you are going to instruct users to install packages that support your plugin using *`pip`*, you should probably do one of the following: + +#### Using a "requirements.txt" File +If your plugin requires libraries that are not part of the basic Python installation or packages installed by Indigo, the preferred method is to include a list of those modules so Indigo can install them automatically when your plugin is installed. This automatic process is available in Server API version 3.4 and later (prior API versions will not invoke this process). This is handled by the following process: + +If a plugin contains the plain text file: *`Contents/Server Plugin/requirements.txt`*, we'll run *`pip`* to install the listed modules into *`Contents/Packages`* (an example is shown below). + +1. When a plugin starts up, it'll look for the *`Contents/Server Plugin/requirements.txt`* file. If it's not there, we'll continue with plugin startup. If it **is** there, we'll proceed to the next step. +1. Indigo will check to see if this file exists: *`Contents/Packages/pip-install-log-success.txt`*. If it does exist (meaning that we've already successfully installed requirements before), we will continue plugin startup. If it doesn't exist, we'll proceed to the next step. +1. We run *`pip3`* with the requirements file and target the *`Contents/Package`* directory (all libraries and dependencies will be installed there). If it fails, we output an error and log the install attempt to the Event Log window and stop the plugin. If it installs correctly, we write the install log results to the *`Contents/Packages/pip-install-log-success.txt`* file so that the next time it starts it won't do it again (see previous step above). + +If processing the *`requirements.txt`* file has failed (say, if the network is down so that pip can't get to pypi.org to download), it will continue to try when the user reloads the plugin, so transient issues will eventually resolve themselves. If the error is caused by a problem with the *`requirements.txt`* file or with a specific requirement, the developer will need to address in a new version. + +Here is a sample *`requirements.txt`* file. Note that the following includes references to specific versions of the packages to be installed. We strongly recommend this approach to minimize the possibility that a newer version could be installed which may cause problems with your plugin. +```text +zeroconf==0.130.0 +websocket-client==1.7.0 +``` + +**Best Practices:** + +If you're using GitHub, be sure that the contents of the `*../Packages*` folder is included in your `*.gitignore*` file. This will ensure the `*Contents/Packages/pip-install-log-success.txt*` and any other content is not included as a part of your package. When testing your plugin, you should include any needed binaries in this folder so when you publish an update, the binaries will not be included. + +**Troubleshooting:** + +If you find that packages are not being installed when a user installs/updates your plugin, one cause might be that you have a *`Contents/Packages/pip-install-log-success.txt`* file left over in your plugin bundle. If it's in the bundle, Indigo will assume the packages are already installed. + +If you're working on transitioning from included binaries to using a `*requirements.txt*` file, be sure to remove the deprecated binaries from your package. If they're present, Indigo may favor those over the libraries you're trying to install automatically. + +##### Manual Installation by User +**Using pip:** + +If you're going to instruct users to install needed packages individually, they should use a command similar to: +`pip3 install package -t /Path/To/Plugin/Contents/Packages/` +or, if a specific version is required, use: + +```text +pip3 install package==1.2.3 -t /Path/To/Plugin/Contents/Packages/ +``` + +which will install the libraries to the specified folder that exists in your plugin bundle. It is strongly recommended that you use the *`Packages`* folder as this may be required in future versions of Indigo. + +**Logging Import Errors** + +If your plugin requires one or more libraries that your users will install manually, you should provide some helpful messages to guide them through the process. One such way is to monitor import failures and notify users that they will need to install some additional libraries in order to use your plugin, such as: + +```python +import_errors = [] +try: + import zeroconf +except ImportError: + import_errors.append("zeroconf") +``` + +```python +def startup(self): + self.logger.debug("startup called") + + if len(import_errors): + msg = f"Required Python libraries missing. Run the following command(s) in a Terminal window to install them, then reload the plugin.\n\n" + for i in import_errors: + msg += f'pip3 install {i} -t "{self.pluginFolderPath}/Contents/Packages/"\n' + self.logger.error(msg) + return "Plugin startup canceled due to missing Python libraries." +``` + +#### Access to IPH3 Libraries +There are some important implications that result from having some libraries included in the IPH3. Namely, those packages will be available to plugins, Indigo scripts (embedded and linked), the Indigo python shell (from the menu item or by doing `indigo-host` in a terminal window), but IPH3 libraries will ****NOT**** be available if you start the python interpreter directly (by doing python2 or python3 in a terminal window). Refer to this chart to determine accessibility: + +| Method | Access to
IPH3 Libraries | +| --- | --- | +| plugins | yes | +| embedded scripts | yes | +| linked scripts | yes | +| Indigo python shell using Indigo menu | yes | +| Indigo python shell using Terminal `indigo-host` | yes | +| Mac Terminal using `python3` command | no | + + +#### Python Packages for Scripters +All Python scripts (either embedded or linked) are executed using the IPH3 process using Python 3. Therefore, the server will be able to locate any packages your scripts use in the same way as described above: + +1. IndigoPluginHost3 +1. /Library/Application Support/Perceptive Automation/Python3-includes +1. /Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages + +If you need a module/package for your plugin, you can install it via `pip3` as shown above, which will install it into the Python `site-packages` folder. We recommend (but don't require) the following folders be used for custom scripts and modules: + +- **/Library/Application Support/Perceptive Automation/Scripts** -- any linked scripts you use for Indigo Schedules and Actions using the "Execute Script > Script and File Actions" command should be stored in this folder. +- **/Library/Application Support/Perceptive Automation/Python3-includes** -- any modules you have developed (a module is a file that contains classes, functions, constants, etc.) that you want to share across multiple scripts, should be stored in the `/Library/Application Support/Perceptive Automation/Python3-includes` folder. (Indigo versions prior to 2022 should use the `/Library/Application Support/Perceptive Automation/Python2-includes` folder). + +!!! warning "Warning 1" + As mentioned above, Indigo will preinstall several packages ([listed below](#python-packages-installed-by-indigo)) at install time. You should avoid modifying those unless absolutely necessary as other 3rd party plugins may rely on them. + +!!! warning "Warning 2" + You should use caution when naming your custom scripts to ensure they don't supplant other Python packages. For example, if you install the **holidays** Python package using `pip3 install holidays`, it will be installed in the **site-packages** folder. If you also have a custom script in the `Python3-includes` folder named **holidays.py**, the IPH will import your script instead of the **site-packages** version (because the Python3-includes folder is searched before the site-packages folder). + +#### If Something Goes Wrong +To provide users with a measure of safety regarding the Python packages that Indigo installs, the Indigo installer also includes the capacity to repair the Python installation if something should become damaged. For example, if a user were to accidentally upgrade one of the Python packages -- and as a result cause the Indigo server to stop functioning properly -- re-running the Indigo installer may be able to bring things back under control. There's [a few things you can try](../../user/troubleshooting/python-conflicts.md) should you run into conflicts between Python versions. + +Re-running the Indigo installer on an existing server shouldn't affect user-installed libraries (existing user-installed libraries should be unaffected). + +#### Moving Indigo to a New Machine +Depending on the method used, any Python modules installed via **pip3** may need to be re-installed on the new machine. For more information, consult [Moving an Indigo Installation to Another Mac](../../user/maintenance/moving.md). + +#### Python 3.11.6 Packages Installed by Indigo { #python-packages-installed-by-indigo } +Indigo {{ version }} ships with support for the following Python 3 packages in addition to the packages installed with the standard Python framework. Generally, if earlier versions of the listed packages are installed at the location Indigo uses, they will be upgraded to the version numbers below. The *`>=`* symbol means the version installed by Indigo may be newer than the version listed, but will not be older. *`pyserial`* is also installed but is installed differently than the other packages listed above and should be unmodifiable. Libraries marked with * are dependencies and may get newer versions on later installs. **You should not update any of the libraries listed as they (and their dependencies may rely on a specific version.)** + +| PIP installed by Indigo
PIP Modifiable (but not recommended) | | +|-----------------------------------------------------------------|--------------| +| Package | Version | +| * aiofiles | >=24.1.0 | +| * anyio | >=4.8.0 | +| certifi | ==2023.11.17 | +| * cffi | >=1.17.1 | +| charset_normalizer | ==3.3.2 | +| * contourpy | >=1.3.1 | +| cryptography | ==41.0.7 | +| * cycler | >=0.12.1 | +| dictdiffer | ==0.9.0 | +| * fonttools | >=4.56.0 | +| future | ==0.18.3 | +| * h11 | >=0.14.0 | +| * html5tagger | >=1.3.0 | +| * httpcore | >=1.0.7 | +| * httptools | >=0.6.4 | +| httpx | ==0.25.2 | +| idna | ==3.6 | +| janus | ==1.0.0 | +| * jedi | >=0.19.2 | +| Jinja2 | ==3.1.2 | +| * kiwisolver | >=1.4.8 | +| MarkupSafe | ==2.1.3 | +| matplotlib | ==3.8.2 | +| * multidict | >=6.1.0 | +| * numpy | >=1.26.4 | +| oauthlib | ==3.2.2 | +| * packaging | >=24.2 | +| * parso | >=0.8.4 | +| Pillow | ==11.1.0 | +| pudb | ==2023.1 | +| py-applescript | ==1.0.3 | +| pyaes | ==1.6.1 | +| * pycparser | >=2.22 | +| * Pygments | >=2.19.1 | +| pyobjc | ==10.0 | +| pyparsing | ==3.1.1 | +| python-box | ==7.3.2 | +| python-dateutil | ==2.8.2 | +| requests | ==2.31.0 | +| requests-oauthlib | ==1.3.1 | +| sanic | ==23.6.0 | +| * sanic-routing | >=23.12.0 | +| sanic-session | ==0.8.0 | +| scipy | ==1.11.4 | +| six | ==1.16.0 | +| * sniffio | >=1.3.1 | +| * tracerite | >=1.1.1 | +| * typing_extensions | >=4.12.2 | +| * ujson | >=5.10.0 | +| urllib3 | ==2.1.0 | +| * urwid | >=2.6.16 | +| * urwid-readline | >=0.15.1 | +| * uvloop | >=0.21.0 | +| * wcwidth | >= 0.2.13 | +| websockets | ==14.1 | +| xmljson | ==0.2.1 | +| | | +| pyserial | 3.5 | + + +## Special Packages +This section contains notes and examples about some library features that may be of particular help to developers. + +### Python Box +Depending on your Indigo version, we may have installed the [Python-Box library](https://github.com/cdgriffith/Box). This library allows the user to use JavaScript style dot notation to access parts of a collection. There are good descriptions of all the functionality on the library's page link above. What we were most interested in is a `box` instance that has the `box_dots` option set to true, like this: + +```text +my_box = box.Box(some_dict, box_dots=True) +``` + +This allows developers to specify a string using JavaScript style dot notation to get to parts of the `box` object: + +```text +some_element = my_box["keyhere.[0].anotherkey"] +``` + +Here are some concrete examples: + +```python +import box + +# Given this dictionary +my_dict = { + "a-list":[1, 2, {"dict-in-list":"fun stuff"}], + "a-dict":{"first":"first element", "second":"second element"} +} + +my_box = box.Box(my_dict, box_dots=True) # create a box object of my_dict + +dict_in_list = my_box["a-list.[2].dict-in-list"] # equals "fun stuff" + +int_from_list = my_box["a-list.[0]"] # equals 1 + +a_dict = my_box["a-dict"] # equals {'first': 'first element', 'second': 'second element'} +``` + +This is useful in a variety of ways, but we use it extensively in the [Event Data Passing](../../user/automation/event-data.md) and [Webhook](../../api/webhooks.md) features. You can determine whether you have the *`box`* library installed by simply trying to import it into a scripting shell or embedded script. diff --git a/reference/canonical/scripting/iom-concepts.md b/reference/canonical/scripting/iom-concepts.md new file mode 100644 index 0000000..60a5bb9 --- /dev/null +++ b/reference/canonical/scripting/iom-concepts.md @@ -0,0 +1,503 @@ + + +# Indigo Object Model Reference + +!!! abstract "In this guide" + This guide is meant to provide specific reference information about the Indigo Object Model (IOM) as used by scripters + writing embedded scripts as well as developers writing Server plugins. Scripters can be Developers and vice versa - we + just wanted to define the different potential uses for IOM. + +## About this Reference Guide + +The IOM is divided up based on the major object types: [Devices](reference/devices/index.md), [Triggers](reference/triggers.md), [Schedules](reference/schedules.md) *(not yet implemented)*, [Action Groups](reference/action-groups.md) *(not yet fully implemented)*, [Variables](reference/variables.md), and [Folders](reference/folders.md). There is also a utility base class (and subclasses), [Action](reference/actions.md) *(not yet complete)*, that's used with the major object types that support actions. + +For ease of discussion, any Python program that uses the object model will be referred to as a “script” throughout this document. From a style perspective, anything that’s in `code text` is generally considered to be Python code and represents the exact code needed. You'll also see code blocks like this: + +```text + +# some code here + +# that does something + +``` + +Some conventions of the Python API: all values are represented in camelCase, where words aren’t separated but each starts with an uppercase letter. The first character of all class names are uppercase (`DimmerDevice`) and the first character of class properties are lowercase letter (`folderId`). Constant enumerations start with a lowercase “k” (`kEnumerationName`), and each enumerated value begins with an uppercase letter (`kEnumerationName.EnumeratedValue`). + +!!! warning "A note about version numbers" + As we've revised the API and added features, we've incremented the API version number. Up until Indigo 5 v5.1.1, the API remained 1.0 and the documents were just updated with new features (which could lead to incompatible features if using an older release of Indigo). As of Indigo 5 v 5.1.2, we've begun incrementing the API minor version number (1.X) whenever we add new features. The major number (X.0) will only be incremented when we do something that will break backwards compatibility or when there are major changes. See the [API version chart](https://www.indigodomo.com/indigo/api_version_chart.html) to see which API versions were released in which Indigo version. + +If any of the API tables in the documentation don't have a version number you can assume that the feature is available in API version 1.0 and later. + +Definitions of terms used throughout this manual are listed below: + +| Term | Meaning | +| --- | --- | +| Action Group | An action (or collection of actions) that can be used by multiple Schedules, Triggers, and Action Groups. This allows you to change the collection in a single place rather than editing multiple Schedules, Triggers, etc., that require identical actions to be executed. | +| Developer | A person who writes Server Plugins. | +| Direct Parameter | The first parameter to a command, although it may be optional. Any further parameters to a command must have a name specified. | +| Event | Some external plugin defined event (outside Indigo) that is used to instruct the IndigoServer to execute a Trigger. | +| Protocol | Any built-in home automation technology that Indigo supports (Insteon, X10, etc.). This does not include plugins that might implement their own protocol. | +| Schedule | An action (or collection of actions) that will be executed based on some temporal settings - dates and times. When the appropriate time and date is met, IndigoServer evaluates any conditions specified in the schedule in order to decide if the actions associated with the event should execute. In previous versions of Indigo, these were referred to as Time/Date Actions. | +| Script | Any section of code using the IOM regardless of language being used. | +| Scripter | A person who writes embedded scripts. | +| Trigger | An action (or collection of actions) that will be executed based on some input event - a device state change, an event from a plugin, an email received, etc. When the event occurs, IndigoServer evaluates any conditions specified in the trigger in order to decide if the actions associated with the event should execute. | + +## Indigo Object Model (IOM) Overview + +### Introduction + +The Indigo Object Model (IOM) is a collection of objects and methods that allow scripts to interact with objects in Indigo. + +From an architectural perspective, the most fundamental design point that needs to be understood is that the host process that executes python scripts is a separate process from the IndigoServer process - which is where the objects actually reside. We do this so that a single script or plugin can't bring down the entire IndigoServer. + +So, when you get an object, it’s actually **a copy of the object**, not the real object. We could have implemented a distributed object model with object locking, etc., but that would have added significant complexity. Instead, we decided to create some simple rules that you need to follow in order to use the IOM correctly: + +For Scripters and Developers: + +- To create, duplicate, delete, and send commands to an object (i.e. turnOn, nextZone, displayInRemoteUI, moveToFolder, etc.), use the appropriate command namespace and a reference to the object rather than altering the object directly (you can see the list of objects and their command name spaces in the table below) +- To modify an object's definition (name, description, etc.), get a copy of it, make the necessary changes directly to the object, then call `myObject.replaceOnServer()` +- To update a copy of an object that you previously obtained, call the object's `myObject.refreshFromServer()` method. + +For Developers: + +- To update a plugin's props on an object on the server, call `myDevice.replacePluginPropsOnServer(newPropsDict)` rather than try to update them on the local device +- To change a device's state on the server, use `myDevice.updateStateOnServer(key="keyName", value="Value")` + +A couple of notes to help you understand these rules. First, the IOM allows [Server Plugins](../plugin-dev/guide.md#indigo-server-plugins) to attach arbitrary attributes to any object. We call these plugin properties, or just props. They are read-write for a Server Plugin, but are readonly for all Python scripts. + +Second, as discussed in the [Plugin Developer's Guide](../plugin-dev/guide.md), a [Server Plugin](../plugin-dev/guide.md#indigo-server-plugins) may define multiple device types. All devices have some state (or states): on/off, paused/playing/stopped, heating/cooling/off, etc. These states are used in Triggers and shown on Control Pages. Your Server Plugin will need to be able to tell the server whenever a state changes for one of its devices. That's what the last rule above is referring to. See the [Devices](reference/devices/index.md) description for more information and examples. + +We’ve divided the commands into name spaces such that if the command applies to a specific class type, it’s put into a namespace that matches the type. Here is a chart that maps the classes to their associated namespace: + +| Class | Command namespace | Notes | +| --- | --- | --- | +| **[Devices](reference/devices/index.md)** | | | +| [Device](reference/devices/base-class.md#device-base-class) (indigo.Device) | [indigo.device.*](reference/devices/base-class.md#commands-indigodevice) | for commands and properties that apply to all device subclasses | +| [DimmerDevice](reference/device-subclasses/dimmer.md#dimmerdevice) (indigo.DimmerDevice) | [indigo.dimmer.*](reference/device-subclasses/dimmer.md#commands-indigodimmer) | manipulate a dimmer | +| [InputOutputDevice](reference/device-subclasses/multiio.md#multiiodevice) (indigo.MultiIODevice) | [indigo.iodevice.*](reference/device-subclasses/multiio.md#commands-indigoiodevice) | manipulate an I/O device | +| [SensorDevice](reference/device-subclasses/sensor.md#sensordevice) (indigo.SensorDevice) | [indigo.sensor.*](reference/device-subclasses/sensor.md#commands-indigosensor) | manipulate a sensor (motion, etc) | +| [RelayDevice](reference/device-subclasses/relay.md#relaydevice) (indigo.RelayDevice) | [indigo.relay.*](reference/device-subclasses/relay.md#commands-indigorelay) | manipulate a relay, lock, or other 2 state device | +| [SpeedControlDevice](reference/device-subclasses/speedcontrol.md#speedcontroldevice) (indigo.SpeedControlDevice) | [indigo.speedcontrol.*](reference/device-subclasses/speedcontrol.md#commands-indigospeedcontrol) | manipulate a speed control/motor device | +| [SprinklerDevice](reference/device-subclasses/sprinkler.md#sprinklerdevice) (indigo.SprinklerDevice) | [indigo.sprinkler.*](reference/device-subclasses/sprinkler.md#commands-indigosprinkler) | manipulate a sprinkler device | +| [ThermostatDevice](reference/device-subclasses/thermostat.md#thermostatdevice) (indigo.ThermostatDevice) | [indigo.thermostat.*](reference/device-subclasses/thermostat.md#commands-indigothermostat) | manipulate a thermostat | +| **Schedule** | | | +| `Schedule` (indigo.Schedule) | indigo.schedule.* | manipulate a scheduled event FIXME not yet implemented | +| **[Triggers](reference/triggers.md)** | | | +| [Trigger](reference/triggers.md#trigger) (indigo.Trigger) | [indigo.trigger.*](reference/triggers.md#commands-indigotrigger) | commands and properties that apply to all trigger subclasses | +| [DeviceStateChangeTrigger](reference/triggers.md#devicestatechangetrigger) (indigo.DeviceStateChangeTrigger) | [indigo.devStateChange.*](reference/triggers.md#commands-indigodevstatechange) | manipulate a device state change trigger | +| [EmailReceivedTrigger](reference/triggers.md#emailreceivedtrigger) (indigo.EmailReceivedTrigger) | [indigo.emailRcvd.*](reference/triggers.md#commands-indigoemailrcvd) | manipulate an email received trigger | +| [Trigger Class](reference/triggers.md#insteoncommandreceivedtrigger) (indigo.InsteonCommandReceivedTrigger) | [indigo.insteonCmdRcvd.*](reference/triggers.md#commands-indigoinsteoncmdrcvd) | manipulate an Insteon command received trigger | +| [InterfaceFailureTrigger](reference/triggers.md#interfacefailuretrigger) (indigo.InterfaceFailureTrigger) | [indigo.interfaceFail.*](reference/triggers.md#commands-indigointerfacefail) | manipulate an interface failure trigger | +| [InterfaceInitializedTrigger](reference/triggers.md#interfaceinitializedtrigger) (indigo.InterfaceInitializedTrigger) | [indigo.interfaceInit.*](reference/triggers.md#commands-indigointerfaceinit) | manipulate an interface initialized trigger | +| [PluginEventTrigger](reference/triggers.md#plugineventtrigger) (indigo.PluginEventTrigger) | [indigo.pluginEvent.*](reference/triggers.md#commands-indigopluginevent) | manipulate a trigger defined by a plugin event | +| [PowerFailureTrigger](reference/triggers.md#powerfailuretrigger) (indigo.PowerFailureTrigger) | [indigo.powerFail.*](reference/triggers.md#commands-indigopowerfailure) | manipulate a power failure trigger | +| [ServerStartupTrigger](reference/triggers.md#serverstartuptrigger) (indigo.ServerStartupTrigger) | [indigo.serverStartup.*](reference/triggers.md#commands-indigoserverstartup) | manipulate a server startup trigger | +| [X10CommandReceivedTrigger](reference/triggers.md#x10commandreceivedtrigger) (indigo.X10CommandReceivedTrigger) | [indigo.x10CmdRcvd.*](reference/triggers.md#commands-indigox10cmdrcvd) | manipulate an X10 command received trigger | +| [VariableValueChangeTrigger](reference/triggers.md#variablevaluechangetrigger) (indigo.VariableValueChangeTrigger) | [indigo.varValueChange.*](reference/triggers.md#commands-indigovarvaluechange) | manipulate a variable changed trigger | +| **[Action Groups](reference/action-groups.md)** | | | +| `ActionGroup` (indigo.ActionGroup) | indigo.actionGroup.* | commands for action groups | +| **[Variables](reference/variables.md)** | | | +| [Variable](reference/variables.md) (indigo.Variable) | [indigo.variable.*](reference/variables.md#commands-indigovariable) | manipulate a variable | +| **Protocol Specific Commands** | | | +| [Insteon Specific Commands](reference/insteon-commands.md) | indigo.insteon.* | commands specific to the Insteon protocol | +| [X10 Specific Commands](reference/x10-commands.md) | indigo.x10.* | commands specific to the X10 protocol | +| **General Commands** | | | +| [Commonly used commands and server properties](reference/server-commands.md) | indigo.server.* | for commands and properties that are more general in nature | + +The next question you probably have is “How do I create a new object in the IndigoServer?”. The simple answer is that most class namespaces have a `create()` method. They work a bit differently based on the class type (for instance, `indigo.device.create()` is used as a factory method to create all types of devices), but it's always named the same. There are also `duplicate()` and `delete()` methods for each. See the individual class pages for details. + +This will keep the IOM simple and understandable and it avoids significant complexity. In this section we’ll describe the classes exposed to your script so that you can use them for whatever your script needs: checking the value of a variable, the state of a device, the definition of an event or action, etc. + +There are a few classes that are special. One is `Action` and it’s subclasses (collectively referred to as "actions"). Why are these classes different? Because outside the trigger, schedule, action group, or control page that they’re associated with they aren’t individually addressable in the IndigoServer: for instance, there is no way to identify an action without first identifying the trigger that it’s associated with. + +So, how do you interact with actions? Directly, as you would any other object. You can create one (which creates it locally), edit it, etc. Once you have the object set up correctly, you can add it to the appropriate local copy of an object then call the object's `replaceOnServer()` method. If you have a local copy of an object and you want to ensure that it's in sync with what's on the server then call the object's `refreshFromServer()` method. Don’t worry if this seems abstract: the examples section for each class page has examples of how to manipulate actions. + +Another very important concept that we’re introducing along with the IOM is the concept of an identifier, or `id`. Every top-level object in Indigo (action group, device, folder, schedule, trigger, variable) now has a globally unique integer ID associated with it. + +All commands in the IOM accept the `id` of an object or the object reference itself (along with the name). You should **never** store the object’s name. The `id` is visible in the various lists throughout the UI. You can also select any object in the GUI and right click it to get its contextual menu where you'll find a `Copy ID (123)` item that shows the ID and allows you to copy it to the clipboard. Server plugins that define config UIs that use the built-in lists will get the `id`(s) of the object(s) selected in the lists rather than the name. + +Now, if you're being very observant, you'll notice something missing: Control Pages. It is our intention to support Control Pages in the IOM, but unfortunately it just didn't make it into v1. Look for it in an upcoming IOM revision. + +### IOM Class Hierarchy + +![IOM Object Hierarchy Image](../images/iom_object_hierarchy.png) + +## IOM Data Types and Object manipulation + +### Indigo specific data types + +Indigo exposes a couple of special classes in Python: `indigo.Dict()` and `indigo.List()`. These are very similar to their Python counterparts (`dict` and `list`). The big difference is that when you're dealing with dictionaries and lists that come from the IndigoServer and that go to the IndigoServer, you'll want to use these rather than the built-in Python types because they are handled natively by Indigo and can automatically be saved to the database and preference files. + +The behavior of `indigo.Dict` and `indigo.List` is similar but not identical to the native python containers. Specifically, the Indigo containers: + +- must contain values that are of types: **bool**, **float**, **int**, **string**, **list** or **dict**. Any list/dict containers must recursively contain only compatible values. +- do **not** support value access via slicing (we might add this eventually). +- always retrieve values (ex: myprops["key"] or mylist[2]) as a copy and **not reference** of the object. +- keys can only contain letters, numbers, and other ASCII characters. +- keys cannot contain spaces. +- keys cannot start with a number or punctuation character. +- keys cannot start with the letters XML, xml, or Xml. + +The behavior that items are always retrieved as values (not references) can lead to some unexpected results when compared to python dicts and lists, especially when multiple levels of container nesting is being used. What this means is that you can set items and append to items, but the items returned from the get iterators are always new copies of the values and not references. For example, consider: + +```python +c = indigo.List() +c.append(4) +c.append(5) +c.append(True) +c.append(False) + +a = indigo.Dict() +a['a'] = "the letter a" +a['b'] = False +a['c'] = c # a COPY of list C above is inserted (not a reference) +print(str(a)) + +# Because a['c'] has a COPY of list C this will NOT append to instance a: + +c.append(6) +print(str(a)) # no change from previous output! + +# And because a['c'] returns a COPY of its value this will also NOT append to instance a: + +a['c'].append(6) +print(str(a)) # no change from previous output! +``` + +But all is not lost. You can just re-assign the nested object to the root container: + +```text + +# Instead you must re-assign a copy to the container. Since we already + +# appended c with 6 above, we just need to reassign it now: + +a['c'] = c +print(str(a)) # now has a COPY of the updated object, c +``` + +If you just need to override an existing value in a container (and not append to it), then a more efficient solution is to use our helper method `setitem_in_item`. It avoids creating temporary copies of values. + +```text +a.setitem_in_item('c', 4, "the number six") +print(str(a)) +``` + +Likewise, it is most efficient to use `getitem_in_item` when accessing nested items since it avoids temporary copies of containers. For example, this creates a temporary copy of the c object before accessing index 4: + +`a['c'][4] # works, but is slow because a temporary copy of a['c'] is created` + +Where this returns the same result but is more efficient: + +`a.getitem_in_item('c', 4) # same result, but fast` + +#### Converting to native Python collections + +You can convert an `indigo.Dict` to a python `dict` and an `indigo.List` to a python `list`, by calling the appropriate methods on those instances. Note, this will recursively convert nested objects (which attempting a manual cast will not): + +```python +python_dict = my_indigo_dict.to_dict() # recursively convert an indigo.Dict instance to a python dict instance +python_list = my_indigo_list.to_list() # recursively convert an indigo.List instance to a python list instance +``` + +Beginning with Indigo 2021.2, you can use a native Python conversion: +```text +python_dict = dict(my_indigo_dict) +python_list = list(my_indigo_list) +``` + +### Built-in objects + +The IOM supplies the following built-in objects: + +| Object | Description | +| --- | --- | +| indigo.devices | All devices in the database | +| indigo.triggers | All triggers in the database | +| indigo.schedules | All schedules in the database | +| indigo.actionGroups | All action groups in the database | +| indigo.variables | All variables in the database | + +These objects are very similar to an indigo.Dict - but have some special abilities and constraints: + +1. They are read-only - you can't modify them directly but rather use other methods to change them +1. They contain a "folders" attribute that has all the folders defined for that object type +1. They define a couple of convenience methods that are specialized for their use + +Let's elaborate on each of these a bit. First, they're read-only in that you can't do something like this: + +`indigo.devices[123] = someOtherDevice` + +To change a device, in most cases you get a copy of the device, make the necessary changes, then `replaceOnServer()`. You'll find specific information about each of those in the appropriate section linked below in the [Classes and Commands](#classes-and-commands) section. + +Each of the objects above corresponds to one of the high-level objects in Indigo. In the UI, you know that you can create folders for each of those object types. To access the folders available for each type, you can reference the "folders" attribute: i.e. `indigo.devices.folders`. This is also a read-only `indigo.Dict` of [folder](reference/folders.md) objects. As with the other top-level classes, you manipulate folders within specific namespaces, described on the [folder class](reference/folders.md) page. + +Finally, these objects define some convenience methods as well as supporting most of the standard Python [dict](http://docs.python.org/library/stdtypes.html#typesmapping) object methods. + +The first added method, `getName(elemId),` is a shortcut to get the string name of the item. Normally you would need to get a copy of the object (which would pull the entire object from the IndigoServer) then get the name, but this method is more efficient since it only gets the name from the IndigoServer rather than the whole object. So, an easy way to get a variable folder's name (maybe for logging) would be to do this: `indigo.variables.folders.getName(1234)`. Make sure you use the correct object (`indigo.variables`) so that we'll know where to search for the folder. + +#### Subscribing to Object Change Events + +The other additional method is `subscribeToChanges()`. This method doesn't return anything - rather, it tells the IndigoServer that your plugin wants notification of all changes to the specific object type. Use this method sparingly as it causes a significant amount of traffic between IndigoServer and your plugin. What kind of plugins would use this? One good example is a logging plugin - one that's logging activity within Indigo (like our [SQL Logger](../plugins/sql_logger.md)). Another example is a plugin that's doing some kind of scene management - you would probably want to issue a `indigo.devices.subscribeToChanges()` at plugin start so that you'll always be notified when any device changes - you can then determine if your scene "state" has changed and act accordingly. Note that subscribeToChanges() only reports actual changes to devices - so for instance when a light turns ON you'll get a notification. If the light is commanded to turn ON again, you won't get the notification because the device state didn't change. + +##### Object Events + +| Objects that support subscribeTochanges() | +| --- | +| indigo.actionGroups.subscribeToChanges() | +| indigo.devices.subscribeToChanges() | +| indigo.triggers.subscribeToChanges() | +| indigo.variables.subscribeToChanges() | + +For example, +```python +def deviceUpdated(self, origDev, newDev): + # call the base's implementation first just to make sure all the right things happen elsewhere + indigo.PluginBase.deviceUpdated(self, origDev, newDev) + # do your stuff here - make the network connection if necessary, write the data, etc. +``` + +##### Lower-level Events + +Use the lower-level `subscribeToIncoming()` and `subscribeToOutgoing()` methods in the `indigo.insteon` and `indigo.x10` command spaces to see commands regardless of their effect on device state. These commands are exclusive to `Insteon` and `x10` device classes. + +| Low Level Commands (Insteon and x10 Only) | +| --- | +| indigo.insteon.subscribeToIncoming() | +| indigo.insteon.subscribeToOutgoing() | +| indigo.x10.subscribeToIncoming() | +| indigo.x10.subscribeToOutgoing() | + +For example, +```python +######################################## + def x10CommandReceived(self, cmd): + self.logger.debug(f"x10CommandReceived: \n{str(cmd)}") + + if cmd.cmdType == "sec": # or "x10" for power line commands + if cmd.secCodeId == 6: + if cmd.secFunc == "sensor alert (max delay)": + self.logger.info("SENSOR OPEN") + elif cmd.secFunc == "sensor normal (max delay)": + self.logger.info("SENSOR CLOSED") + + def x10CommandSent(self, cmd): + self.logger.debug(f"x10CommandSent: \n{str(cmd)}") +``` + +#### Iterating Object Lists + +For each of the built-in objects, we've also provided some handy iterators that in some cases will allow you to filter. For instance, if you would like to iterate over the list of all devices in Indigo, here's what you would do in Python: + +```python +for dev in indigo.devices: + # do stuff with dev here +``` + +A note on iteration: when the above loop begins, the list of devices is fixed. So if mid-loop an element is added then it will NOT be iterated (unless the entire loop runs again later). +Likewise, if an element is deleted midway through, then the iterator gracefully handles it and just ignores that deleted item (it skips to the next item automatically). +So if items are added/removed during an iteration it never throws an exception -- it handles it gracefully but you are not guaranteed to get the new ones. + +You can also iterate with just the ID of the object: + +```python +for devId in indigo.devices.iterkey(): + # all device id's (rather than the whole device object) +``` + +For some of the built-in objects, you can filter the results: + +```python +for dev in indigo.devices.iter("indigo.dimmer"): + # all devices will be dimmer devices +``` + +You can further restrict some filters - for instance, you can specify the device type and the protocol: + +```python +for dev in indigo.devices.iter("indigo.dimmer, indigo.insteon"): + # all devices will be Insteon dimmer devices +``` + +Or you can iterate just devices defined by custom plugin types: + +```python +for dev in indigo.devices.iter("self"): + # each dev will be one that matches one of our custom plugin devices +``` + +Here's a list of all device filters: + +| Filter | Description | +| --- | --- | +| indigo.zwave | include Z-Wave devices | +| indigo.insteon | include Insteon devices | +| indigo.x10 | include X10 devices | +| com.mycompany.myplugin | include all devices defined by a plugin | +| indigo.responder | include devices whose state can be changed | +| indigo.controller | include devices that can send commands | +| indigo.iodevice | input/output devices | +| indigo.dimmer | dimmer devices | +| indigo.relay | relay, lock, or other 2 state devices | +| indigo.sensor | sensor devices (motion, temperature, etc.) | +| indigo.speedcontrol | multi-speed controlled device (fans, motors, etc.) | +| indigo.sprinkler | sprinklers | +| indigo.thermostat | thermostats | +| self | include devices defined by the calling plugin | +| self.myDeviceType | include myDeviceType's devices defined by the calling plugin | +| com.company.plugin.xyzDeviceType | include xyzDeviceType's defined by another plugin | +| props.SupportsOnState | return only devices that support an ON state property | + +Triggers also have filters: + +| Filter | Description | +| --- | --- | +| indigo.insteonCmdRcvd | insteon command received triggers | +| indigo.x10CmdRcvd | x10 command received triggers | +| indigo.devStateChange | device state changed triggers | +| indigo.varValueChange | variable changed triggers | +| indigo.serverStartup | startup triggers | +| indigo.powerFail | power failure triggers | +| indigo.interfaceFail | interface failure triggers - can be used with or without a specified protocol | +| indigo.interfaceInit | interface connection triggers - can be used with or without a specified protocol | +| indigo.emailRcvd | email received triggers | +| indigo.pluginEvent | plugin defined triggers | +| self | include triggers defined by the calling plugin | +| self.myTriggerType | include myTriggerType's triggers defined by the calling plugin | +| com.company.plugin.xyzTriggerType | include all xyzTriggerType's defined by another plugin | + +So, to iterate over a list of device state change triggers, you would: + +```python +for trigger in indigo.triggers.iter("indigo.devStateChange"): + # each trigger will be a device state change trigger +``` + +Or to iterate over all triggers defined by our plugin types: + +```python +for trigger in indigo.triggers.iter("self"): + # each trigger will be one that matches one of our custom plugin triggers +``` + +Unlike devices, however, you may only use a single trigger filter - anything else will result in an empty list. + +There is a special filter for variables as well. To iterate over the variable list, you would probably guess this: + +```python +for var in indigo.variables: + # all variables +``` + +And you'd be correct. You can also just iterate through the writable variables: + +```python +for varName in indigo.variables.iter("indigo.readWrite"): + # you'll only get readwrite variables +``` + +### Common Python Exceptions + +All IOM Commands can throw exceptions if there is a missing or incorrect parameter, or if there is a runtime problem that prevents the command or request from completing successfully. Common exceptions that may be raised include: + +| Possible Exceptions | | +| --- | --- | +| `ArgumentError` | a required parameter is missing, or is not the correct type | +| `IndexError` | an index parameter value is out-of-range | +| `KeyError` | a key parameter (to a dictionary) was not found in the dictionary | +| `PluginNotFoundError` | a plugin that you're trying to talk to (on a `create()` perhaps) isn't available (disabled or gone) | +| `TypeError` | a parameter had the incorrect runtime type | +| `ValueError` | a parameter has a value that is illegal or not allowed | + +Other exceptions, such as `IOError`, `MemoryError`, `OverflowError`, etc., are also possible although less likely to occur. + +## Classes and Commands + +Indigo provides many classes and commands to work on those classes. You can use standard Python introspection methods to find out information about those classes (these examples are done using the [Scripting Shell](tutorial.md)): + +```python + +# first, let's get an indigo.DimmerDevice like a LampLinc + +>>> dev = indigo.devices[238621905] +>>> dev.__class__ + +>>> hasattr(dev,'onState') +True +>>> dir(dev) +['__class__', '__delattr__', '__dict__', '__doc__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__', '__weakref__', 'address', 'brightness', 'buttonGroupCount', 'description', 'deviceTypeId', 'enabled', 'folderId', 'globalProps', 'id', 'lastChanged', 'model', 'name', 'onState', 'pluginId', 'pluginProps', 'protocol', 'refreshFromServer', 'remoteDisplay', 'replaceOnServer', 'replacePluginPropsOnServer', 'states', 'supportsAllLightsOnOff', 'supportsAllOff', 'supportsStatusRequest', 'updateStateOnServer', 'version'] + +# Next, an indigo.ThermostatDevice + +>>> thermo = indigo.devices[171495708] +>>> thermo.__class__ + +>>> dir(thermo) +['__class__', '__delattr__', '__dict__', '__doc__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__', '__weakref__', 'address', 'buttonGroupCount', 'coolIsOn', 'coolSetpoint', 'description', 'deviceTypeId', 'enabled', 'fanIsOn', 'fanMode', 'folderId', 'globalProps', 'heatIsOn', 'heatSetpoint', 'humidities', 'humiditySensorCount', 'hvacMode', 'id', 'lastChanged', 'model', 'name', 'pluginId', 'pluginProps', 'protocol', 'refreshFromServer', 'remoteDisplay', 'replaceOnServer', 'replacePluginPropsOnServer', 'states', 'supportsAllLightsOnOff', 'supportsAllOff', 'supportsStatusRequest', 'temperatureSensorCount', 'temperatures', 'updateStateOnServer', 'version'] + +# Here's a plugin defined device + +>>> tunes = indigo.devices["Whole House iTunes"] + +# notice that it only shows the Device base class + +>>> tunes.__class__ + +>>> dir(tunes) +['__class__', '__delattr__', '__dict__', '__doc__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__', '__weakref__', 'address', 'buttonGroupCount', 'description', 'deviceTypeId', 'enabled', 'folderId', 'globalProps', 'id', 'lastChanged', 'model', 'name', 'pluginId', 'pluginProps', 'protocol', 'refreshFromServer', 'remoteDisplay', 'replaceOnServer', 'replacePluginPropsOnServer', 'states', 'supportsAllLightsOnOff', 'supportsAllOff', 'supportsStatusRequest', 'updateStateOnServer', 'version'] + +# You can use the deviceTypeId attribute to get the plugin's defined type + +>>> tunes.deviceTypeId +u'mediaserver' +``` + +So, from a discovery perspective, the `dir(obj)` method will list all properties and methods for an object. The `hasattr(obj, attr)` will test an object to see if it has a specific attribute. The `obj.__class__` attribute will show you what class the object is. As you explore IOM objects, these methods will help you figure out how to use IOM objects effectively. + +## Object Dictionaries + +An important note about Indigo objects. When you print an object instance to the Events log, you will get all the attributes and properties for that object: +```python +>>> dev = indigo.devices[123] +>>> indigo.server.log(f"{dev}") +address : +batteryLevel : None +buttonGroupCount : 0 +configured : True +description : +... + +>>> indigo.server.log(f"{type(dev)}") +<'indigo.Device'> +``` +These data are stored as Indigo class instances. In order to send a complete description of the Indigo instance to another process or system (serializing it into JSON, for example), you can convert the Indigo instance to a traditional Python dictionary: +```python +>>> dev = indigo.devices[123] +>>> dev_dict = dict(dev) +>>> indigo.server.log(f"{dev_dict}") +{'class': 'indigo.Device', 'address': '', 'batteryLevel': None, 'buttonGroupCount': 0, 'configured': True, 'description': '', ...} + +>>> indigo.server.log(f"{type(dev_dict)}") + +``` +Converted objects include all elements of the class instance. All the object types listed below support conversion to Python dictionaries. + +Here are the major groupings of classes defined in the IOM (the command namespace for each is described with the classes): + +- [Device](reference/devices/index.md) classes +- [Trigger](reference/triggers.md) classes +- [Schedule](reference/schedules.md) class FIXME not yet implemented +- [Action](reference/actions.md) classes +- [Action Group](reference/action-groups.md) class +- [Variable](reference/variables.md) class +- [Folder](reference/folders.md) class + +Here are the command namespaces that are independent (to some extent) from the classes they effect: + +- [Server Commands](reference/server-commands.md) + +## Utility Classes and Functions + +The `indigo.utils` module includes several classes and functions that aren't tied to a specific Indigo object but are helpful when writing scripts and building plugins — a JSON encoder, the `ValidationError` exception, a static-file helper for plugin HTTP replies, email/boolean helpers, the `is_int()` test, and the `indigo.Dict`/`indigo.List` conversion methods. + +See the [Utility Classes & Functions](reference/utils.md) reference page for the full list. diff --git a/reference/canonical/scripting/reference/action-groups.md b/reference/canonical/scripting/reference/action-groups.md new file mode 100644 index 0000000..94bcf07 --- /dev/null +++ b/reference/canonical/scripting/reference/action-groups.md @@ -0,0 +1,130 @@ + + +# Action Groups + +The following properties and commands are available with the Action Group object class. + +## Class Properties { .ref-head-no-code } + +| Property | Type | Writable | Description | +|--------------------------------------------|------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `description` | string | Yes | description of the action group | +| `folderId` | integer | No | unique ID of the folder this action group is in | +| `id` | integer | No | a unique id of the action group, assigned on creation by IndigoServer | +| `name` | string | Yes | the unique name of the action group - no two action groups can have the same name | +| `remoteDisplay` | boolean | No | `True` if this action group is shown in remote clients, `False` otherwise | +| `sharedProps` | dictionary | No | **[API v2.3](https://www.indigodomo.com/indigo/api_release_notes/2.3/)** : an `indigo.Dict()` representing the name/value pairs that are shared by all plugins. This is the property dictionary that you can edit via the Global Properties plugin, and your plugin may manage properties in this dictionary as well to add metadata to devices that your plugin can use for other purposes. Use `ag.replaceSharedPropsOnServer()` to update them (as with pluginProps, you should get copy first, update the copy, then set them back to that copy so you don't accidentally remove some other plugin's props). | + +## Commands (indigo.actionGroup.*) { .ref-head-no-code } + +The commands in this section are common to all action groups regardless of type. + +### Delete { .ref-head-no-code } + +Delete the specified action group. + +**Command Syntax Examples** + +```python +indigo.actionGroup.delete(123) +``` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------|----------|---------|----------------------------------------------| +| direct parameter | Yes | integer | id or instance of the action group to delete | + +### Display In Remote UI { .ref-head-no-code } + +This command will show or hide the action group in remote clients. + +**Command Syntax Examples** + +```python +indigo.actionGroup.displayInRemoteUI(123, value=True) +indigo.actionGroup.displayInRemoteUI(123, value=False) +``` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------|----------|---------|-------------------------------------------------------| +| direct parameter | No | integer | id or instance of the action group | +| `value` | Yes | boolean | `True` to show the action group or `False` to hide it | + +### Duplicate { .ref-head-no-code } + +Duplicate the specified action group. This method returns a copy of the new action group. + +**Command Syntax Examples** + +```python +indigo.actionGroup.duplicate(123, duplicateName="New Name") +``` + +**Parameters** + +| Parameter | Required | Type | Description | +|--------------------------------------------|----------|---------|-------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the action group to duplicate | +| `duplicateName` | No | string | name for the new action group trigger | + +### Execute { .ref-head-no-code } + +Execute the specified action group. + +**Command Syntax Examples** + +```python +indigo.actionGroup.execute(123, event_data=some_dict) +``` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------|----------|---------|----------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the action group to execute | +| event_data | No | dict | a dictionary instance that will get passed to all actions in the action group for processing | + +A note on `event_data` - Indigo will automatically add a `source` key to your dictionary to represent where the action execution came from: + +- "server" if it's something generated from the server itself (schedule execution, built-in trigger, etc.) +- "python" if it's something that comes through IPH that doesn't already have a source attached (scripts, plugins) +- "api-http" if it came from the HTTP API and there wasn't already an included "source" +- "api-websocket" if it came from the websocket API and there wasn't already an included "source" + +However, if you include a `source` key in your `event_data`, we will not overwrite it, we'll just pass through whatever your value is. + +### Get Dependencies { .ref-head-no-code } + +Use this command to retrieve all the Indigo objects dependent on the action group. The command will return an `indigo.Dict` containing all the dependencies. + +**Command Syntax Examples** + +```python +indigo.actionGroup.getDependencies(123) +``` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------|----------|---------|------------------------------------| +| direct parameter | Yes | integer | id or instance of the action group | + +### Move To Folder { .ref-head-no-code } + +Use this command to move the action group to a different folder. You can get a list of folder id’s by using indigo.actionGroups.folders, which will return a dictionary. The key to the dictionary is the ID, the value is the folder name. + +**Command Syntax Examples** + +```python +indigo.actionGroup.moveToFolder(123, value=987) +``` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------|----------|---------|----------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the action group | +| `value` | Yes | integer | id or instance of the folder to move the action group to | diff --git a/reference/canonical/scripting/reference/actions.md b/reference/canonical/scripting/reference/actions.md new file mode 100644 index 0000000..aa25323 --- /dev/null +++ b/reference/canonical/scripting/reference/actions.md @@ -0,0 +1,344 @@ + + +# Actions + +In the IOM, all actions are derived from a common Action base class. This base contains all the shared components of actions. Unlike the other major classes, however, there isn't a command namespace for Actions. Why is that, you say? Because Actions don't exist outside the object that contains them (Triggers, Schedules, Action Groups, etc.). So, when manipulating actions, you always work directly on the object, then assign that object to one of the container objects. Don't worry, we'll walk you through it. + +## Action Base Class {.ref-head-no-code } + +The Action class is a base class that provides the common functionality to its subclasses (to follow). You may specify a new `Action` class in your list of actions for a trigger, schedule, or action group, but the action type will be `None` - that is, it will delay and speak any text set in the object, but other than that it will do nothing. Most of the time you'll be using one of the subclasses so that you get the specific functionality you're looking for. + +**Class Properties** + +| Property | Type | Description | +| --- | --- | --- | +| `delayAmount` | integer | number of seconds to delay before executing the actions (0 for none) | +| `replaceExisting` | boolean | if true then any existing delayed action is replaced by this one | +| `textToSpeak` | string | this is the text to speak when the action is executed | + +### Complementary Delays { #complementary-delays .ref-head-no-code } + +For some action types, the user (and you) may specify that the complementary action be taken after some number of seconds. The table below specifies what complementary actions are available with the various action types: + +| Complementary Actions | | +| --- | --- | +| Action | Complement | +| `DeviceAction`
`kDeviceAction.TurnOff`
`kDeviceAction.TurnOn`
`kDeviceAction.Toggle`
`kDeviceAction.Lock`
`kDeviceAction.Unlock`
| `DeviceAction`
`kDeviceAction.TurnOn`
`kDeviceAction.TurnOff`
`kDeviceAction.Toggle`
`kDeviceAction.Unlock`
`kDeviceAction.Lock`
| +| `DisableScheduleAction` | `EnableScheduleAction` | +| `DisableTriggerAction` | `EnableTriggerAction` | +| `EnableScheduleAction` | `DisableScheduleAction` | +| `EnableTriggerAction` | `DisableTriggerAction` | + +## DeviceAction (API v2.0+ only) {.ref-head-no-code } + +API v2.0+ only: This class represents an action to control dimmers (lights), relay (appliances), and locks. Previously this class was named DimmerRelayAction -- it was renamed in API v2.0. + +**Class Properties** + +| Property | Type | Description | +| --- | --- | --- | +| `complementDelay` | integer | the number of seconds to delay before issuing the complementary action (0 for no action) - see [Complementary Delays](#complementary-delays) for details | +| `deviceId` | integer | the id of the device | +| `deviceAction` | [kDeviceAction](#device-action-enumeration) | this is the command to send to the device | +| `actionValue` | integer or dict | if `deviceAction` is in [Brighten, Dim, SetBrightness] then this is an integer value | + +### Device Action Enumeration { #device-action-enumeration .ref-head-no-code } + +| indigo.kDeviceAction | | +| --- | --- | +| Value | Description | +| `AllLightsOff` | turn off all dimmer (light) devices | +| `AllLightsOn` | turn on all dimmer (light) devices | +| `AllOff` | turn off all dimmer (light) and relay (appliance) devices | +| `BrightenBy` | brighten a dimmer (light) device by the amount specified in the `actionValue` integer property | +| `DimBy` | dim a dimmer (light) device by the amount specified in the `actionValue` integer property | +| `SetBrightness` | set a dimmer (light) device to the brightness specified in the `actionValue` integer property | +| `SetColorLevels` | set the color (RGB) and white levels to the values specified in the `actionValue` dict property | +| `Toggle` | toggle the on/off state of a dimmer (light) or relay (appliance) device | +| `TurnOff` | turn off a dimmer (light) or relay (appliance) device | +| `TurnOn` | turn on a dimmer (light) or relay (appliance) device | +| `Lock` | lock a deadbolt or door device | +| `Unlock` | unlock a deadbolt or door device | + +## DisableScheduleAction {.ref-head-no-code } + +This class represents an action to disable a schedule. + +**Class Properties** + +| Property | Type | Description | +| --- | --- | --- | +| `complementDelay` | integer | the number of seconds to delay before issuing the complementary action (0 for no action), in this case an EnableScheduleAction - see [Complementary Delays](#complementary-delays) for details | +| `scheduleId` | integer | the id of the schedule to disable | + +## DisableTriggerAction {.ref-head-no-code } + +This class represents an action to disable a trigger. + +**Class Properties** + +| Property | Type | Description | +| --- | --- | --- | +| `complementDelay` | integer | the number of seconds to delay before issuing the complementary action (0 for no action), in this case an EnableTriggerAction - see [Complementary Delays](#complementary-delays) for details | +| `triggerId` | integer | the id of the trigger to disable | + +## EmailAction {.ref-head-no-code } + +This class represents an action to send an email. + +**Class Properties** + +| Property | Type | Description | +| --- | --- | --- | +| `emailBody` | string | the body of the email to send | +| `emailSubject` | string | the subject of the email to send | +| `emailTo` | string | the (semicolon separated) list of email addresses | + +## EnableScheduleAction {.ref-head-no-code } + +This class represents an action to enable a schedule. + +**Class Properties** + +| Property | Type | Description | +| --- | --- | --- | +| `complementDelay` | integer | the number of seconds to delay before issuing the complementary action (0 for no action), in this case an `DisableScheduleAction` - see [Complementary Delays](#complementary-delays) for details | +| `scheduleId` | integer | the id of the schedule to enable | + +## EnableTriggerEventAction {.ref-head-no-code } + +This class represents an action to enable a trigger event. + +**Class Properties** + +| Property | Type | Description | +| --- | --- | --- | +| `complementDelay` | integer | the number of seconds to delay before issuing the complementary action (0 for no action), in this case an DisableTriggerAction - see [Complementary Delays](#complementary-delays) for details | +| `eventId` | integer | the id of the event to enable | + +## ExecuteGroupAction {.ref-head-no-code } + +This class represents an action to execute an action group. + +**Class Properties** + +| Property | Type | Description | +| --- | --- | --- | +| `groupId` | integer | the id of the action group to execute | + +## ExecuteScriptAction {.ref-head-no-code } + +This class represents an action to execute a script. + +**Class Properties** + +| Property | Type | Description | +| --- | --- | --- | +| `scriptCode` | string | this is the source code of the script to execute | + +## Get Dependencies {.ref-head-no-code } + +Return an indigo.Dict with all the dependencies on this action group. + +**Command Syntax Examples** + +```python +indigo.actionGroup.getDependencies(123) +``` + +**Parameters** + +| Parameter | Required | Type | Description | +| --- | --- | --- | --- | +| direct parameter | Yes | integer | id or instance of the action group to get the dependencies for. | + +The dictionary will look something like this: + +```python +>>> print(indigo.actionGroup.getDependencies(91776575)) +Data : (dict) + actionGroups : (list) + controlPages : (list) + devices : (list) + schedules : (list) + Data : (dict) + ID : 552463741 (integer) + Name : Between condition test (string) + Data : (dict) + ID : 296710860 (integer) + Name : Greater than condition test (string) + triggers : (list) + variables : (list) +``` + +So, the dictionary will have 6 top-level keys: "actionGroups", "controlPages", "devices", "schedules", "triggers", and "variables". Each one of those keys will return a list object. Inside that list object will be multiple dicts, one for each dependency (or an empty list if there are none). Each dependency dictionary has two keys: "ID" which is the unique id and "Name" which is the name of the object. + +## InputOutputAction {.ref-head-no-code } + +This class represents an action to control an input/output module. + +**Class Properties** + +| Property | Type | Description | +|---------------------------------------|-----------------------------------------------|---------------------------------------------------------------------------------------------| +| `deviceId` | integer | this is the ID of the I/O device | +| `action` | [kIOAction](#input-output-action-enumeration) | this I/O action to execute | +| `index` | integer | if `action` is `TurnOffOutput`, `TurnOnOutput`, the index of the input or output to control | + +### Input/Output Action Enumeration { #input-output-action-enumeration .ref-head-no-code } + +| indigo.kIOAction | | +|---------------------------------------------------------|-------------------------------------------------| +| Value | Description | +| `TurnOffOutput` | turn off the output specified by index | +| `TurnOffAllOutputs` | turn off all outputs | +| `TurnOnOutput` | turn on the output specified by index | +| `RequestStatusAll` | request status of all hardware input/outputs | +| `RequestAnalogInputValues` | request all analog input values from the device | +| `RequestBinaryInputsStatus` | request all binary input statuses | +| `RequestBinaryOutputsStatus` | request all binary output statuses | +| `RequestSensorInputValues` | request all sensor input values | + +## ModifyVariableAction {.ref-head-no-code } + +This class represents an action to modify an Indigo variable. + +**Class Properties** + +| Property | Type | Description | +|---------------------------------------------|-------------------------------------------------|----------------------------------------| +| `variableId` | integer | the id of the variable | +| `variableAction` | [kVariableAction](#variable-action-enumeration) | the type of variable action to execute | +| `variableValue` | string | the new variable value | + +### Variable Action Enumeration { #variable-action-enumeration .ref-head-no-code } + +| indigo.kVariableAction | | +|---------------------------------------------|--------------------------------------------------| +| Value | Description | +| `DecrementValue` | decrement the variable value by 1 | +| `IncrementValue` | increment the variable value by 1 | +| `SetValue` | set the variable to the `variableValue` property | + +## PluginAction {.ref-head-no-code } + +A plugin action is defined by a plugin, and is similar in definition to a CustomPluginDevice. + +**Class Properties** + +| Property | Type | Description | +|-------------------------------------------|------------|-------------------------------------------------------------------------------------------------| +| `deviceId` | integer | the id of the device | +| `pluginId` | string | the unique ID of the plugin, specified in the Info.plist for the plugin (or it’s documentation) | +| `pluginTypeId` | string | the id specified in the Actions.xml (or it’s documentation) | +| `props` | dictionary | an indigo.Dict() defining this action's parameters | + +## SendInsteonGroupCommandAction {.ref-head-no-code } + +This class represents an action to send an Insteon group command. + +**Class Properties** + +| Property | Type | Description | +|--------------------------------------|---------------------------------------------------------------|-----------------------------------------------| +| `command` | [kInstnGroupCommand](#insteon-send-group-command-enumeration) | this is the group command to send | +| `group` | integer | this is the group number to send `command` to | + +### Insteon Send Group Command Enumeration { #insteon-send-group-command-enumeration .ref-head-no-code } + +| indigo.kIOAction | | +|-----------------------------------------|-----------------------------------------------------------------| +| Value | Description | +| `InstantOff` | send the instant (fast) off command to group ignoring ramp rate | +| `InstantOn` | send the instant (fast) on command to group ignoring ramp rate | +| `Off` | send the off command to group | +| `On` | send the on command to group | + +## SprinklerAction {.ref-head-no-code } + +This class represents an action to control a sprinkler module. + +**Class Properties** + +| Property | Type | Description | +|----------------------------------------------|---------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------| +| `deviceId` | integer | the id of the sprinkler device | +| `multiplierVarId` | integer | API v1.20+ only: optional elem ID for the variable multiplier (None if no multiplier is specified) | +| `sprinklerAction` | [kSprinklerAction](#sprinkler-action-enumeration) | this sprinkler action to execute | +| `zoneDurations` | list of float | list of floats that represent the durations in minutes for each zone to schedule - used when `sprinklerAction` is `RunNewSchedule` | +| `zoneIndex` | integer | the zone to turn on as a 1-based index -- used when `sprinklerAction` is `ZoneOn` | + +### Sprinkler Action Enumeration { #sprinkler-action-enumeration .ref-head-no-code } + +| indigo.kSprinklerAction | | +|--------------------------------------------------|------------------------------------------| +| Value | Description | +| `RunNewSchedule` | run a new sprinkler schedule | +| `RunPreviousSchedule` | run the last executed sprinkler schedule | +| `PauseSchedule` | pause the current sprinkler schedule | +| `ResumeSchedule` | resume the current sprinkler schedule | +| `StopSchedule` | stop the current sprinkler schedule | +| `PreviousZone` | set sprinkler to the previous zone | +| `NextZone` | set sprinkler to the next zone | +| `ZoneOn` | turn on a single zone | +| `AllZonesOff` | turn off all zones | +| `RequestStatusAll` | request status of all valves | + +## ResetInterfacesAction {.ref-head-no-code } + +This class represents an action to reset the built-in interfaces. There are no extra properties necessary for this action. + +## ThermostatAction {.ref-head-no-code } + +This class represents an action to control a thermostat. + +**Class Properties** + +| Property | Type | Description | +|-----------------------------------------------|----------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------| +| `deviceId` | integer | this is the ID of the thermostat | +| `thermostatAction` | [kThermostatAction](#thermostat-action-enumeration) | this thermostat action to execute | +| `actionMode` | [kFanMode](device-subclasses/thermostat.md#fan-mode-enumeration)
OR
[kHvacMode](device-subclasses/thermostat.md#hvac-mode-enumeration) | if action is SetFanMode, then a kFanMode enumeration
if action is SetHvacMode, then a kHvacMode enumeration | +| `actionValue` | float | if action is `Decrease` or `Increase`, the amount to increase/decrease the setpoints
if action is `Set`, the temperature to set the setpoint to | + +### Thermostat Action Enumeration { #thermostat-action-enumeration .ref-head-no-code } + +| indigo.kThermostatAction | | +|----------------------------------------------------|------------------------------------------------------------------------| +| Value | Description | +| `DecreaseCoolSetpoint` | decrease setpoint by value | +| `DecreaseHeatSetpoint` | decrease setpoint by value | +| `IncreaseCoolSetpoint` | increase setpoint by value | +| `IncreaseHeatSetpoint` | increase setpoint by value | +| `SetCoolSetpoint` | set the setpoint to value | +| `SetFanMode` | set the fan mode to mode | +| `SetHeatSetpoint` | set the setpoint to value | +| `SetHvacMode` | set the hvac mode to mode | +| `RequestStatusAll` | request all current values from the thermostat | +| `RequestMode` | request the current mode of the thermostat | +| `RequestEquipmentState` | request the current operational state of the compressor, furnace, etc. | +| `RequestTemperatures` | request all current temperatures from the thermostat | +| `RequestHumidities` | request all current humidities from the thermostat | +| `RequestDeadbands` | request the current deadband ranges from the thermostat | +| `RequestSetpoints` | request all current setpoints from the thermostat | + +## UniversalAction (API v2.0+ only) {.ref-head-no-code } + +API v2.0+ only: This class represents a universal action that can be used with devices of different classes. Previously this class was named GeneralDeviceAction -- it was renamed in API v2.0. + +**Class Properties** + +| Property | Type | Description | +|-------------------------------------------|--------------------------------------------------------|-------------------------------------------| +| `deviceId` | integer | the id of the device | +| `deviceAction` | [kUniversalAction](#general-device-action-enumeration) | this is the command to send to the device | + +### General Device Action Enumeration { #general-device-action-enumeration .ref-head-no-code } + +| indigo.kUniversalAction | | +|--------------------------------------------|------------------------------------------------------------------------| +| Value | Description | +| `Beep` | request that the device perform an audible beep or buzz | +| `EnergyUpdate` | request that the energy meter send its most recent meter data | +| `EnergyReset` | request that the energy meter reset its accumulative energy usage data | +| `RequestStatus` | send a device a status request for a complete update | diff --git a/reference/canonical/scripting/reference/device-subclasses.md b/reference/canonical/scripting/reference/device-subclasses.md new file mode 100644 index 0000000..208ba2b --- /dev/null +++ b/reference/canonical/scripting/reference/device-subclasses.md @@ -0,0 +1,10 @@ + + +# Device Subclasses + +## Device Subclasses { #device-subclasses-device-subclasses } + +Each built-in device type extends the [Device base class](../devices/index.md) with its own properties, states, and command namespace: + +- [DimmerDevice](dimmer.md) · [RelayDevice](relay.md) · [SensorDevice](sensor.md) +- [SpeedControlDevice](speedcontrol.md) · [SprinklerDevice](sprinkler.md) · [ThermostatDevice](thermostat.md) · [MultiIODevice](multiio.md) diff --git a/reference/canonical/scripting/reference/device-subclasses/dimmer.md b/reference/canonical/scripting/reference/device-subclasses/dimmer.md new file mode 100644 index 0000000..74b63fe --- /dev/null +++ b/reference/canonical/scripting/reference/device-subclasses/dimmer.md @@ -0,0 +1,237 @@ + + +# DimmerDevice { .ref-head-no-code } + +Dimmer devices are dimmable light modules. They can be turned on and off and their brightness may be set. Your script may manipulate any dimmer device. You may specify that devices defined by your plugin are of this type. The standard dimmer UI in the various clients will be presented to users attempting to control your device. + +## Class Properties { .ref-head-no-code } + +| Property | Type | Writable | Description | +|-----------------------------------------|---------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `brightness` | integer | No | an integer from 0-100 indicating the brightness of the device - plugin developers may decide if their device may be on but have a brightness of 0 (non-standard) - set using commands below | +| `ledStates` | list | No | this is a list of booleans that represent the state of LEDs on the device. So, to check to see if LED 3 is on you'd check dev.ledStates[2] (Python arrays are 0-based so the first element is element 0). Currently, only KeypadLinc devices use this array - however, future devices may use it as well so you should probably check the length first to make sure that the LED you're looking for is actually there. Use the len(dev.ledStates) method to see how many entries there are before you access a specific index. | +| `onState` | boolean | No | indicates whether the device is on - shortcut for `dev.states['onOffState']` | + +## Device States { .ref-head-no-code } + +These are the states provided by this device type and accessible through the `dev.states` dictionary. They are read-only, but if you're a plugin developer you can use the `updateStateOnServer()` class method to update the value for devices owned by your plugin. + +| State ID | Type | Property Name | Notes | +|----------------------------------------------|---------|---------------|---------------------------------------------------------------------------------------------------| +| `brightnessLevel` | integer | `brightness` | brightness of the device - value is in the range of 0 to 100. Can be accessed by `dev.brightness` | +| `onOffState` | boolean | `onState` | indicates whether the device is on - can be accessed by `dev.onState`. | + +## Commands (indigo.dimmer.*) { .ref-head-no-code } + +### All Lights Off { .ref-head-no-code } + +Turns off all lights for all protocols unless a direct parameter is specified. The direct parameter, if specified, will determine which lights will be turned off. Lights are defined as all dimmable devices. This command doesn’t work for plugin defined devices regardless of type. + +**Command Syntax Examples** + + +`indigo.dimmer.allLightsOff()`
+`indigo.dimmer.allLightsOff(indigo.kAllDeviceSel.HouseCodeA)`
+`indigo.dimmer.allLightsOff(indigo.kAllDeviceSel.Insteon)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| direct parameter | No | [kAllDeviceSel](../devices/base-class.md#all-device-selector-enumeration) | enumerated value to indicate which devices to turn off, all if no parameter is passed - see the [kAllDeviceSel](../devices/base-class.md#all-device-selector-enumeration) enumeration for a full description | + +### All Lights On { .ref-head-no-code } + +Turns on all lights for all protocols unless a direct parameter is specified. The direct parameter, if specified, will determine which lights will be turned on. Lights are defined as all dimmable devices. This command doesn’t work for plugin defined devices regardless of type. + +**Command Syntax Examples** + + +`indigo.dimmer.allLightsOn()`
+`indigo.dimmer.allLightsOn(indigo.kAllDeviceSel.HouseCodeA)`
+`indigo.dimmer.allLightsOn(indigo.kAllDeviceSel.Insteon)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| direct parameter | No | [kAllDeviceSel](../devices/base-class.md#all-device-selector-enumeration) | enumerated value to indicate which lights to turn on, all if no parameter is passed - see the [kAllDeviceSel](../devices/base-class.md#all-device-selector-enumeration) enumeration for a full description | + +### Brighten { .ref-head-no-code } + +Changes the brightness of the specified light relative to the current brightness. + +**Command Syntax Examples** + + +`indigo.dimmer.brighten(123)`
+`indigo.dimmer.brighten('Office Lamp', by=50, delay=4)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `by` | No | integer | relative amount to brighten by where brightness is 0-100 | +| `delay` | No | integer | number of seconds to delay before executing the brighten command | + +### Dim { .ref-head-no-code } + +Dim the brightness of the specified light by some amount relative to the current brightness. + +**Command Syntax Examples** + + +`indigo.dimmer.dim(123)`
+`indigo.dimmer.dim('Office Lamp', by=50, delay=4)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|-------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `by` | No | integer | relative amount to dim by where dimness is 0-100 | +| `delay` | No | integer | number of seconds to delay before executing the dim command | + +### Set Brightness { .ref-head-no-code } + +Changes the brightness of the specified light to a specific value. + +**Command Syntax Examples** + + +`indigo.dimmer.setBrightness(123, value=75)`
+`indigo.dimmer.setBrightness(123, value=75, delay=360)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|-------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `value` | Yes | integer | absolute value to set the brightness to on a scale of 0-100 | +| `delay` | No | integer | number of seconds to delay before executing the dim command | + +### Set LED State { .ref-head-no-code } + +Turns on/off the specified LED. Useful for KeypadLinc (and similar) devices. + +!!! note + You can't use this method to control the LED of the button(s) that control the direct load - use Turn ON/Turn OFF for those. + +**Command Syntax Examples** + + +`indigo.dimmer.setLedState(123, index=0, value=True)`
+`indigo.dimmer.setLedState(123, index=0, value=True, suppressLogging=True)`
+`indigo.dimmer.setLedState(123, index=0, value=True, updateStatesOnly=True)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|-----------------------------------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `index` | Yes | integer | the index of the button. Recall that Python-based arrays are 0-based, so the index is also 0-based. That is, the first object in an array is object 0. | +| `value` | Yes | boolean | True to turn on the LED, False to turn it off | +| `suppressLogging` | No | boolean | True to suppress logging (defaults to False) | +| `updateStatesOnly` | No | boolean | True to only update Indigo's internal state - no command will be sent to the KPL (defaults to False) | + +### Set Color Levels { .ref-head-no-code } + +Sets the color levels for the dimmer device. The levels are the same as brightness, so 0 to 100, except for **whiteTemperature** which ranges from 1200-15000. Real number precision is allowed and the actions UI will display precision up to the hundredths digit, allowing for relatively good precision if a plugin needs to convert between RGB and HSB. + +Some RGBW devices (Aeotec bulb) support 2 different white channels with unique white temperatures: warm and cool. In those cases both **whiteLevel** and **whiteLevel2** can be used. Other white color devices allow for a specific temperature value to be specified (in Kelvin). For those devices you would use both the **whiteLevel** parameter in combination with the **whiteTemperature** parameter. Note for specifying white color temperature devices should support either the 2 white level technique, or the single white level + white temperature technique. That is, if the **whiteLevel2** parameter is used then whiteTemperature will be ignored. + +**Command Syntax Examples** + +```python +indigo.dimmer.setColorLevels(device, redLevel, greenLevel, blueLevel, whiteLevel, + whiteLevel2, whiteTemperature, delay, suppressLogging, + updateStatesOnly +) +``` + +**Parameters** + +| Parameter | Required | Type | Description | +|-----------------------------------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `redLevel` | No | integer | 0 - 100 | +| `greenLevel` | No | integer | 0 - 100 | +| `blueLevel` | No | integer | 0 - 100 | +| `whiteLevel` | No | integer | 0 - 100 | +| `whiteLevel2` | No | integer | 0 - 100 | +| `whiteTemperature` | No | integer | 1200 - 15000 | +| `delay` | No | integer | Defaults to 0. | +| `suppressLogging` | No | boolean | Defaults to `False` | +| `updateStatesOnly` | No | boolean | is an argument that exists on several device instance APIs that has Indigo update its device state/UI but does not have the corresponding action sent out to physical module. The use cases for this are pretty few, but sometimes you just want the internal state to update and can skip the actual command to the module. Defaults to `False` | + +A note about setting color levels: RGB values are expressed as values from 0 to 255, however, Indigo stores those values from 0 to 100. This requires the RGB values to be converted which is very easy. + +```python +# RGB value / 255 * 100 +redLevel = 128 +newRedLevel = redLevel / 255 * 100 +``` + +### Set On State { .ref-head-no-code } + +Use the Turn On, Turn Off, and Toggle methods to turn on/off a dimmer device. + +**Examples** + +Here are examples of device properties: + +```python +# Getting a device +myDevice = indigo.devices[123] +``` + +```python +# Set a device’s brightness to 75 if it’s currently +# less than that, but only if it’s turned on +myDevice = indigo.devices[123] +if ((myDevice.brightness < 75) and + (myDevice.onState)): + indigo.dimmer.setBrightness(myDevice, 75) +``` + +```python +# Brighten a light by 25% after 10 minutes +indigo.dimmer.brighten(123, by=25, delay=600) +``` + +```python +# Logging a message if it’s showing in Indigo Touch +myDevice = indigo.devices[123] +if (myDevice.remoteDisplay): + indigo.server.log("device is showing in Indigo Touch") +``` + +```python +# Getting the folder ID that the device is in +myDevice = indigo.devices[123] +myDevice.folderId + +# OR + +indigo.devices[123].folderId # see comments below +``` + +You might look at the second example above, and wonder why we didn’t do something like this: + +```python +# Set a device’s brightness to 75 if it’s currently +# less than that, but only if it’s turned on +myDevice = indigo.devices[123] +if ((indigo.devices[123].brightness < 75) and + (indigo.devices[123].onState)): + indigo.dimmer.setBrightness(indigo.devices[123], 75) +``` + +That code works correctly, but is very inefficient. The Python to C++ bridge will cause a copy of the object to be made every time `indigo.devices[123]` is used since the devices list is a C++ object, but the object returned from it using the id subscript is bridged to a Python object, and all bridged objects are copies. So, the code directly above will create 3 copies of the device object - very inefficient. The code in the example above gets a single copy of the object and uses that in all the rest of the code. A good rule of thumb is to get an explicit copy of an object if you need to use it more than once. diff --git a/reference/canonical/scripting/reference/device-subclasses/multiio.md b/reference/canonical/scripting/reference/device-subclasses/multiio.md new file mode 100644 index 0000000..740d508 --- /dev/null +++ b/reference/canonical/scripting/reference/device-subclasses/multiio.md @@ -0,0 +1,66 @@ + + +# MultiIODevice { .ref-head-no-code } + +I/O devices have a wide variety of capabilities that we’ve tried to boil down to some specifics. The I/O devices that Indigo supports generally have some combination of three types of inputs: analog, binary, and sensor. They may also support some number of binary outputs. + +## Class Properties { .ref-head-no-code } + +All outputs are modified using commands below. + +| Property | Type | Writable | Description | +|------------------------------------------------|-----------------|----------|----------------------------------------------------------------------------------------------------------------------------------------| +| `analogInputs` | list of integer | No | a list of the current analog input values, one per input, in a python list - can be accessed individually using the states below | +| `analogInputCount` | integer | No | number of analog inputs this device supports | +| `binaryInputs` | list of boolean | No | a list of the current binary input values, one per input - can be accessed individually using the states below | +| `binaryInputCount` | integer | No | number of binary inputs this device supports | +| `binaryOutputs` | list of boolean | No | a list of the current binary output values, on per output (max 12 total outputs) - can be accessed individually using the states below | +| `binaryOutputCount` | integer | No | number of binary outputs this device supports | +| `sensorInputCount` | integer | No | number of sensor inputs this device supports | +| `sensorInputs` | list of integer | No | a list of the current sensor input values, one per input - can be accessed individually using the states below | + +## Device States { .ref-head-no-code } + +These are the states provided by this device type and accessible through the `dev.states` dictionary. They are read-only. + +| State ID | Type | Property Name | Notes | +|-----------------------------------------------|---------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| +| `analogInput#` | integer | N/A | value of input number represented by the # sign (input 1 would be `analogInput1`) - there will only be `dev.analogInputCount` inputs available | +| `analogInputsAll` | string | N/A | a comma separated list of all analog input values. Can be accessed as a Python list of integers by using `dev.analogInputs`. | +| `binaryInput#` | boolean | N/A | value of input number represented by the # sign (input 1 would be `binaryInput1`) - there will only be `dev.binaryInputCount` inputs available | +| `binaryInputsAll` | string | N/A | a comma separated list of all binary input values. Can be accessed as a Python list of booleans by using `dev.binaryInputs`. | +| `binaryOutput#` | boolean | N/A | value of output number represented by the # sign (output 1 would be `binaryOutput1`) - there will only be `dev.binaryOutputCount` outputs available | +| `binaryOutputsAll` | string | N/A | a comma separated list of all binary output values. Can be accessed as a Python list of booleans by using `dev.binaryOutputs`. | +| `sensorInput#` | integer | N/A | value of input number represented by the # sign (input 1 would be `sensorInput1`) - there will only be `dev.sensorInputCount` inputs available | +| `sensorInputsAll` | string | N/A | a comma separated list of all binary input values. Can be accessed as a Python list of booleans by using `dev.sensorInputs`. | + +## Commands (indigo.iodevice.*) { .ref-head-no-code } + +### Set Binary Output { .ref-head-no-code } + +Set the state of the specified binary output. + +**Command Syntax Examples** + +`indigo.iodevice.setBinaryOutput(123, index=2, value=True)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|------------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `index` | Yes | integer | 0-based index of the output to change - should be less than the `binaryOutputCount` property of the device | +| `value` | Yes | boolean | True to turn the output on, False to turn it off | + +**Examples** + +```python + +# If binary input 3 is true, set binary output 1 + +# to false (python arrays are 0-based) + +myIODevice = indigo.devices[123] +if not myIODevice.binaryInputs[2]: + indigo.iodevice.setBinaryOutput(myIODevice, index=1, value=False) +``` diff --git a/reference/canonical/scripting/reference/device-subclasses/relay.md b/reference/canonical/scripting/reference/device-subclasses/relay.md new file mode 100644 index 0000000..144294c --- /dev/null +++ b/reference/canonical/scripting/reference/device-subclasses/relay.md @@ -0,0 +1,58 @@ + + +# RelayDevice { .ref-head-no-code } + +Relay devices are very simple devices, often times called appliance modules. They can be turned on and off only. Your script may manipulate any relay device. Your plugin may specify devices that are of this type and the standard relay UI in the various clients will be presented to users when controlling it. + +## Class Properties { .ref-head-no-code } + +| Property | Type | Writable | Description | +|----------------------------------------|---------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `ledStates` | list | No | this is a list of booleans that represent the state of LEDs on the device. So, to check to see if LED 3 is on you'd check dev.ledStates[2] (Python arrays are 0-based so the first element is element 0). Currently, only KeypadLinc devices use this array - however, future devices may use it as well so you should probably check the length first to make sure that the LED you're looking for is actually there. Use the len(dev.ledStates) method to see how many entries there are before you access a specific index. | +| `onState` | boolean | No | indicates whether the device is on - shortcut to `dev.states['onOffState']` | + +## Device States { .ref-head-no-code } + +These are the states provided by this device type and accessible through the `dev.states` dictionary. They are read-only, but if you're a plugin developer you can use the `updateStateOnServer()` class method to update the value for devices owned by your plugin. + +| State ID | Type | Property Name | Notes | +|-----------------------------------------|---------|---------------|-------------------------------------------------------------------------------| +| `onOffState` | boolean | `onState` | indicates whether the device is on - use `dev.onState` property as a shortcut | + +## Commands (indigo.relay.*) { .ref-head-no-code } + +Use the [Turn On](../devices/base-class.md#turn-on), [Turn Off](../devices/base-class.md#turn-off), and [Toggle](../devices/base-class.md#toggle) methods in the indigo.device.* namespace to turn on/off a relay device. + +### Set LED State { .ref-head-no-code } + +Turns on/off the specified LED. Useful for KeypadLinc (and similar) devices. + +!!! note + You can't use this method to control the LED of the button(s) that control the direct load - use Turn ON/Turn OFF for those. + +**Command Syntax Examples** + + +`indigo.relay.setLedState(123, index=0, value=True)`
+`indigo.relay.setLedState(123, index=0, value=True, suppressLogging=True)`
+`indigo.relay.setLedState(123, index=0, value=True, updateStatesOnly=True)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|-----------------------------------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `index` | Yes | integer | the index of the button. Recall that Python-based arrays are 0-based, so the index is also 0-based. That is, the first object in an array is object 0. | +| `value` | Yes | boolean | True to turn on the LED, False to turn it off | +| `suppressLogging` | No | boolean | True to suppress logging (defaults to False) | +| `updateStatesOnly` | No | boolean | True to only update Indigo's internal state - no command will be sent to the KPL (defaults to False) | + +**Examples** + +```python + +# Toggle the device + +indigo.device.toggle(123) +``` diff --git a/reference/canonical/scripting/reference/device-subclasses/sensor.md b/reference/canonical/scripting/reference/device-subclasses/sensor.md new file mode 100644 index 0000000..05d03ae --- /dev/null +++ b/reference/canonical/scripting/reference/device-subclasses/sensor.md @@ -0,0 +1,41 @@ + + +# SensorDevice { .ref-head-no-code } + +Some sensor devices, like motion sensors, are treated differently in Indigo. They don’t generally maintain state - so there’s no concept of a sensor being on/off: in the case of a motion sensor, detecting motion and not detecting motion. + +They generally send a command of some type when they detect some condition and send another command when they stop detecting it. However, dealing with them in Indigo as if they maintain state is much more useful in many cases. So, Indigo will attempt to maintain a virtual state for each motion sensor if configured that way. Currently, the following devices fall under this category: + +- X10 Motion Sensors +- the Wireless Insteon Motion / Occupancy Sensor (2420M) from SmartLabs +- the TriggerLinc from SmartLabs +- the SynchroLinc from SmartLabs + +## Class Properties { .ref-head-no-code } + +| Property | Type | Writable | [Min API](https://www.indigodomo.com/indigo/api_version_chart.html) | Description | +|-----------------------------------------------------|------------------|----------|---------------------------------------|-----------------------------------------------------------------------------------------------------------| +| `allowOnStateChange` | boolean | No | 1.6 | True if UI controls should be shown or enabled to change the onState | +| `allowSensorValueChange` | boolean | No | 1.6 | True if UI controls should be shown or enabled to change the sensorValue | +| `onState` | boolean | No | 1.0 | indicates that the device is currently in a triggered or ON state (None if sensor doesn't support ON/OFF) | +| `sensorValue` | integer or float | No | 1.6 | the numerical value of a sensor, such as temperature (None if sensor doesn't support sensor values) | + +## Commands (indigo.sensor.*) { .ref-head-no-code } + +### Set On State { .ref-head-no-code } + +Set the sensor onState property. Normally this is unnecessary since Indigo will maintain it for you so this method is provided mainly for testing and error recovery. + +**Command Syntax Examples** + +`indigo.sensor.setOnState(123, value=True)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|----------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| value | Yes | boolean | True to set Indigo’s state to on, False to set it to off | + +**Examples**
+None diff --git a/reference/canonical/scripting/reference/device-subclasses/speedcontrol.md b/reference/canonical/scripting/reference/device-subclasses/speedcontrol.md new file mode 100644 index 0000000..2e2e751 --- /dev/null +++ b/reference/canonical/scripting/reference/device-subclasses/speedcontrol.md @@ -0,0 +1,105 @@ + + +# SpeedControlDevice { .ref-head-no-code } + +Speed control devices are generally controllers for motors of some type. The first implementation was for the Insteon FanLinc, which is a ceiling fan motor controller. This device type provides two different methods of setting the speed of the motor: by level, which ranges from 0 (off) to 100 (full on), and by index. With a device like a FanLinc, for instance, you can't set an arbitrary speed - you can only set it to some number of fixed speeds and this is what index is for. The FanLinc will in fact respond to setting the level, but we normalize the level to the appropriate speed. Other speed control devices may not choose to do so. + +## Class Properties { .ref-head-no-code } + +| Property | Type | Writable | Description | +|----------------------------------------------|---------|----------|----------------------------------------------------------------------------------------------------------------------------| +| `onState` | boolean | No | indicates whether the device is on - shortcut to `dev.states['onOffState']` | +| `speedIndex` | integer | No | indicates the current speed index for devices that support some fixed # of speeds - shortcut to `dev.states['speedIndex']` | +| `speedIndexCount` | integer | No | indicates the number of indexes available for this device (defaults to 4) | +| `speedLevel` | integer | No | indicates the level the device is set to (0-100) - shortcut to `dev.states['speedLevel']` | + +## Device States { .ref-head-no-code } + +These are the states provided by this device type and accessible through the `dev.states` dictionary. They are read-only, but if you're a plugin developer you can use the `updateStateOnServer()` class method to update the value for devices owned by your plugin. + +| State ID | Type | Property Name | Notes | +|--------------------------------------------|---------|---------------|---------------------------------------------------------------------------------------------------------------------------------| +| `onOffState` | boolean | `onState` | indicates whether the device is on - use `dev.onState` property as a shortcut | +| `speedIndex` | boolean | `speedIndex` | indicates the current speed index for devices that support some fixed # of speeds - use `dev.speedIndex` property as a shortcut | +| `speedIndex.ui` | string | n/a | a more user friendly name for the current index (e.g. high, medium, low, off) | +| `speedLevel` | integer | `speedLevel` | indicates the level the device is set to (0-100) - use `dev.speedLevel` property as a shortcut | + +## Commands (indigo.speedcontrol.*) { .ref-head-no-code } + +### Decrease Speed Index { .ref-head-no-code } + +Decreases the speed index. + +**Command Syntax Examples** + + +`indigo.speedcontrol.decreaseSpeedIndex(123)`
+`indigo.speedcontrol.decreaseSpeedIndex(123, by=2)`
+`indigo.speedcontrol.decreaseSpeedIndex(123, delay=10)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|-----------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `by` | No | integer | the number of index positions to decrease by. Defaults to 1 if not specified. | +| `delay` | No | integer | delays the command by the specified number of seconds. Defaults to no delay if not specified. | + +### Increase Speed Index { .ref-head-no-code } + +Increases the speed index. + +**Command Syntax Examples** + + +`indigo.speedcontrol.increaseSpeedIndex(123)`
+`indigo.speedcontrol.increaseSpeedIndex(123, by=2)`
+`indigo.speedcontrol.increaseSpeedIndex(123, delay=10)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|-----------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `by` | No | integer | the number of index positions to increase by. Defaults to 1 if not specified. | +| `delay` | No | integer | delays the command by the specified number of seconds. Defaults to no delay if not specified. | + +### Set Speed Index { .ref-head-no-code } + +Sets the speed index to the specified value. + +**Command Syntax Examples** + + +`indigo.speedcontrol.setSpeedIndex(123, value=2)`
+`indigo.speedcontrol.increaseSpeedIndex(123, value=2, delay=10)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|-----------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `value` | Yes | integer | index position to set the index to. | +| `delay` | No | integer | delays the command by the specified number of seconds. Defaults to no delay if not specified. | + +### Set Speed Level { .ref-head-no-code } + +Sets the speed index to the specified value. + +**Command Syntax Examples** + + +`indigo.speedcontrol.setSpeedLevel(123, value=50)`
+`indigo.speedcontrol.setSpeedLevel(123, value=75, delay=10)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|-----------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `value` | Yes | integer | level of the motor control, from 0-100. | +| `delay` | No | integer | delays the command by the specified number of seconds. Defaults to no delay if not specified. | diff --git a/reference/canonical/scripting/reference/device-subclasses/sprinkler.md b/reference/canonical/scripting/reference/device-subclasses/sprinkler.md new file mode 100644 index 0000000..f6d2768 --- /dev/null +++ b/reference/canonical/scripting/reference/device-subclasses/sprinkler.md @@ -0,0 +1,140 @@ + + +# SprinklerDevice { .ref-head-no-code } + +Sprinkler devices generally have some number of sprinkler zones. + +## Class Properties { .ref-head-no-code } + +| Property | Type | Writable | [Min API](https://www.indigodomo.com/indigo/api_version_chart.html) | Description | +|------------------------------------------------------------------|-----------------|----------|---------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `activeZone` | integer | No | 1.0 | the 1-based index of the active zone (None=all zones off, 1=zone 1, 2=zone 2, ...). Note this property has undergone changes in name (previously called activeZoneIndex) as well as semantics (previously the index being 0-based). | +| `zoneCount` | integer | No | 1.0 | the number of zones available for this sprinkler | +| `zoneEnableList` | list of boolean | No | 1.13 | list of booleans, starting at zone 1 through zone [`zoneCount`], that specify if a given zone is enabled (has a maximum zone duration > 0). | +| `zoneNames` | list of string | No | 1.0 | list of zone names, starting at zone 1 through zone [`zoneCount`]. You must include `zoneCount` strings in the list. | +| `zoneMaxDurations` | list of floats | No | 1.0 | list of zone durations in minutes, starting at zone 1 through zone [`zoneCount`]. You must include `zoneCount` integers in the list. | +| `zoneScheduledDurations` | list of floats | No | 1.0 | list of currently active zone durations in minutes if a schedule is running (empty list if no schedule is running), starts at zone 1 through zone [`zoneCount`] | +| `pausedScheduleZone` | integer | No | 1.16 | the 1-based index of the paused sprinkler zone index (None=schedule not paused, 1=zone 1 paused, 2=zone 2 paused, ...). | +| `pausedScheduleRemainingZoneDuration` | float | No | 1.16 | the paused sprinkler zone duration in minutes (None if schedule not paused) | + +## Device States { .ref-head-no-code } + +These are the states provided by this device type and accessible through the `dev.states` dictionary. They are read-only. + +| State ID | Type | Property Name | Notes | +|--------------------------------------------|---------|---------------|---------------------------------------------------------------------------------------------------------| +| `activeZone` | integer | `activeZone` | the number of the active zone, 0 if off (1=zone 1, 2=zone 2, etc) | +| `activeZone.ui` | string | N/A | the active zone as a human readable string | +| `zone#` | boolean | N/A | replace the # with the zone number (up to `dev.zoneCount`) to return whether the zone is running or not | + +## Commands (indigo.sprinkler.*) { .ref-head-no-code } + +### Next Zone { .ref-head-no-code } + +Set the sprinkler to the next zone, and turn off if it’s the last defined zone. + +**Command Syntax Examples** + +`indigo.sprinkler.nextZone(123)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|------------------------------| +| direct parameter | Yes | integer | id or instance of the device | + +### Pause Schedule { .ref-head-no-code } + +Pause the current sprinkler schedule but keep it active so it can be resumed. + +**Command Syntax Examples** + +`indigo.sprinkler.pause(123)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|------------------------------| +| direct parameter | Yes | integer | id or instance of the device | + +### Previous Zone { .ref-head-no-code } + +Set the sprinkler to the previous zone, and turn off if it’s the first defined zone. + +**Command Syntax Examples** + +`indigo.sprinkler.previousZone(123)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|------------------------------| +| direct parameter | Yes | integer | id or instance of the device | + +### Resume Schedule { .ref-head-no-code } + +Resume the current sprinkler schedule. + +**Command Syntax Examples** + +`indigo.sprinkler.resume(123)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|------------------------------| +| direct parameter | Yes | integer | id or instance of the device | + +### Run Schedule { .ref-head-no-code } + +Run a sprinkler schedule. + +**Command Syntax Examples** + +`indigo.sprinkler.run(123, schedule=[10,15,8, 0, 0, 0, 0, 0])` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `schedule` | Yes | list of reals | list of reals representing the number of minutes to run for each zone - the list must have [`zoneCount`] elements, with 0 for any zone that shouldn’t run | + +### Stop Schedule { .ref-head-no-code } + +Stop the current sprinkler schedule and clear it. + +**Command Syntax Examples** + +`indigo.sprinkler.stop(123)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|------------------------------| +| direct parameter | Yes | integer | id or instance of the device | + +### Set Active Zone { .ref-head-no-code } + +API v1.12+ only: Turn on a specific zone. + +**Command Syntax Examples** + +`indigo.sprinkler.setActiveZone(123, index=2)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|--------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `index` | Yes | integer | 1-based index of the zone to turn on | + +**Examples** + +```python + +# run schedule + +indigo.sprinkler.run(123, + schedule=[10,15,8, 0, 0, 0, 0, 0]) +``` diff --git a/reference/canonical/scripting/reference/device-subclasses/thermostat.md b/reference/canonical/scripting/reference/device-subclasses/thermostat.md new file mode 100644 index 0000000..b79e9f0 --- /dev/null +++ b/reference/canonical/scripting/reference/device-subclasses/thermostat.md @@ -0,0 +1,261 @@ + + +# ThermostatDevice { .ref-head-no-code } + +Thermostats have a wide variety of capabilities that we’ve tried to boil down to some specifics. They can have multiple temperature and humidity sensors and--depending on the device capabilities and region--may have a fan mode, an HVAC mode (heating, cooling, etc.) and associated setpoints. **Note:** some thermostats don’t support getting the equipment state values: `coolIsOn`, `fanIsOn`, `heatIsOn`, `dehumidifierIsOn`, and `humidifierIsOn`. For those thermostats those properties will always be False. + +## Class Properties { .ref-head-no-code } + +| Property | Type | Writable | [Min API](https://www.indigodomo.com/indigo/api_version_chart.html) | Description | +|-----------------------------------------------------|-------------------------------------|----------|---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `coolIsOn` | boolean | No | 1.0 | is the cooling system (compressor) currently running - shortcut for `dev.states['hvacCoolerIsOn']`. This property is always present and will be `False` if the corresponding state `dev.states['hvacCoolerIsOn']` is not present. | +| `coolSetpoint` | float | No | 1.0 | current cool setpoint value - shortcut for `dev.states['setpointCool']` | +| `dehumidifierIsOn` | boolean | No | 1.7 | is the dehumidifier currently turned ON - shortcut for `dev.states['hvacDehumidifierIsOn']` | +| `fanMode` | [kFanMode](#fan-mode-enumeration) | No | 1.0 | the operating mode for the fan attached to the thermostat - shortcut for `dev.states['hvacFanMode']` | +| `fanIsOn` | boolean | No | 1.0 | is the fan currently running - shortcut for `dev.states['hvacFanIsOn']` | +| `heatIsOn` | boolean | No | 1.0 | is the heater currently running - shortcut for `dev.states['hvacHeaterIsOn']`. This property is always present and will be `False` if the corresponding state `dev.states['hvacHeaterIsOn']` is not present. | +| `heatSetpoint` | float | No | 1.0 | current heat setpoint value - shortcut for `dev.states['setpointHeat']` | +| `humidities` | list of float | No | 1.0 | a list of floating point values representing the current values of all humidity sensors connected to the thermostat | +| `humiditySensorCount` | integer | No | 1.0 | number of humidity sensors this thermostat supports | +| `humidifierIsOn` | boolean | No | 1.7 | is the humidifier currently turned ON - shortcut for `dev.states['hvacHumidifierIsOn']` | +| `hvacMode` | [kHvacMode](#hvac-mode-enumeration) | No | 1.0 | the operating mode for the HVAC system attached to the thermostat - shortcut for `dev.states['hvacOperationMode']` | +| `temperatureSensorCount` | integer | No | 1.0 | number of temperature sensors this thermostat supports | +| `temperatures` | list of float | No | 1.0 | a list of floating point values representing the current values of all temperature sensors connected to the thermostat | + +To check whether the thermostat device actually supports the `coolIsOn` and `heatIsOn` properties, one can check: `support_heat_and_cool = dev.pluginProps.get("ShowCoolHeatEquipmentStateUI", False)` + +### Plugin Capabilities { .ref-head-no-code } + +These pluginProps can be updated by a plugin to describe the capabilities for a particular thermostat instance. Some of these are useful to provide a higher-level abstraction for accessing/changing thermostat properties or states. + +| Property | Type | Writeable | Description | +|-----------------------------------------------------------|---------|-----------|------------------------------| +| `NumTemperatureInputs` | Integer | Yes | should range between 1 and 3 | +| `NumHumidityInputs` | Integer | Yes | should range between 0 and 3 | +| `SupportsHeatSetpoint` | Boolean | Yes | True or False | +| `SupportsCoolSetpoint` | Boolean | Yes | True or False | +| `SupportsHvacOperationMode` | Boolean | Yes | True or False | +| `SupportsHvacFanMode` | Boolean | Yes | True or False | +| `ShowCoolHeatEquipmentStateUI` | Boolean | Yes | True or False | + +Some of these are reflected as attributes in the device instance as well: + +| Attribute | Read-Only | +|------------------------------------------------------------|-----------| +| `dev.hvacMode` | Yes | +| `dev.fanMode` | Yes | +| `dev.coolSetpoint` | Yes | +| `dev.heatSetpoint` | Yes | +| `dev.temperatureSensorCount` | Yes | +| `dev.temperatures` | Yes | +| `dev.humiditySensorCount` | Yes | +| `dev.humidities` | Yes | +| `dev.coolIsOn` | Yes | +| `dev.heatIsOn` | Yes | +| `dev.fanIsOn` | Yes | +| `dev.dehumidifierIsOn` | Yes | +| `dev.humidifierIsOn` | Yes | +| `dev.supportsHvacFanMode` | Yes | +| `dev.supportsHvacOperationMode` | Yes | +| `dev.supportsCoolSetpoint` | Yes | +| `dev.supportsHeatSetpoint` | Yes | + +More information on how to use these Thermostat properties and attributes can be found in the `Example Device - Thermostat.indigoPlugin` in the [Indigo Plugin SDK](https://github.com/IndigoDomotics/IndigoSDK). + +#### Fan Mode Enumeration { #fan-mode-enumeration .ref-head-no-code } + +| `indigo.kFanMode` | | +|----------------------------------------------|------------------------------------------------------------------------------------| +| Value | Description | +| `AlwaysOn` | signal the fan that it should be running continuously | +| `Auto` | signal the fan that it should only run when the HVAC system needs it to be running | + +#### HVAC Mode Enumeration { #hvac-mode-enumeration .ref-head-no-code } + +| `indigo.kHvacMode` | | +|-----------------------------------------------|----------------------------------------------------------------------------------------------------------------------| +| Value | Description | +| `Cool` | the hvac system is only reacting to cool setpoints | +| `HeatCool` | the hvac system is reacting to both cool and heat setpoints | +| `Heat` | the hvac system is only reacting to and heat setpoints | +| `Off` | the hvac system is turned off | +| `ProgramHeatCool` | the hvac system is executing it’s built-in automatic program, which usually responds to both heat and cool setpoints | +| `ProgramCool` | the hvac system is executing it’s built-in cooling program | +| `ProgramHeat` | the hvac system is executing it’s built-in heating program | + +## Device States { .ref-head-no-code } + +These are the states provided by this device type and accessible through the `dev.states` dictionary. They are read-only, but if you're a plugin developer you can use the `updateStateOnServer()` class method to update most of these states for devices owned by your plugin (exceptions are noted below). + +| State ID | Type | Property Name | Notes | +|-------------------------------------------------------------|-------------------------------------|-----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `humidityInput#` | float | N/A | replace the # with the humidity sensor # (up to `humiditySensorCount`) to directly access the humidity value for that input | +| `humidityInputsAll` | string | N/A | a comma separated list of all humidity values | +| `hvacCoolerIsOn` | boolean | `coolIsOn` | `True` if the cooling system currently running | +| `hvacDehumidifierIsOn` | boolean | `dehumidifierIsOn` | `True` if the dehumidifier is currently running | +| `hvacFanIsOn` | boolean | `fanIsOn` | `True` if the fan currently running | +| `hvacFanMode` | [kFanMode](#fan-mode-enumeration) | `fanMode` | operating mode for the fan attached to the thermostat | +| `hvacFanIsAlwaysOn` | boolean | N/A | `True` if the fan mode is set to `AlwaysOn`. Note: this state can't be directly updated but rather will be updated automatically when you update `hvacFanMode`. | +| `hvacFanIsAuto` | boolean | N/A | `True` if the fan mode is set to `Auto`. Note: this state can't be directly updated but rather will be updated automatically when you update `hvacFanMode`. | +| `hvacHeaterIsOn` | boolean | `heatIsOn` | `True` if the heating system currently running | +| `hvacHumidifierIsOn` | boolean | `humidifierIsOn` | `True` if the humidifier is currently running | +| `hvacOperationMode` | [kHvacMode](#hvac-mode-enumeration) | `hvacMode` | operating mode for the HVAC system attached to the thermostat | +| `hvacOperationModeIsAuto` | boolean | N/A | `True` if the HVAC is set to `HeatCool`. Note: this state can't be directly updated but rather will be updated automatically when you update `hvacOperationMode`. | +| `hvacOperationModeIsCool` | boolean | N/A | `True` if the HVAC is set to `Cool`. Note: this state can't be directly updated but rather will be updated automatically when you update `hvacOperationMode`. | +| `hvacOperationModeIsHeat` | boolean | N/A | `True` if the HVAC is set to `Heat`. Note: this state can't be directly updated but rather will be updated automatically when you update `hvacOperationMode`. | +| `hvacOperationModeIsOff` | boolean | N/A | `True` if the HVAC is set to `Off`. Note: this state can't be directly updated but rather will be updated automatically when you update `hvacOperationMode`. | +| `hvacOperationModeIsProgramAuto` | boolean | N/A | `True` if the HVAC is set to `ProgramHeatCool`. Note: this state can't be directly updated but rather will be updated automatically when you update `hvacOperationMode`. | +| `hvacOperationModeIsProgramCool` | boolean | N/A | `True` if the HVAC is set to `ProgramCool`. Note: this state can't be directly updated but rather will be updated automatically when you update `hvacOperationMode`. | +| `hvacOperationModeIsProgramHeat` | boolean | N/A | `True` if the HVAC is set to `ProgramHeat`. Note: this state can't be directly updated but rather will be updated automatically when you update `hvacOperationMode`. | +| `setpointCool` | float | `coolSetpoint` | the cool setpoint | +| `setpointHeat` | float | `heatSetpoint` | the heat setpoint | +| `temperatureInput#` | float | N/A | replace the # with the temperature sensor # (up to `temperatureSensorCount`) to directly access the humidity value for that input | +| `temperatureInputsAll` | string | N/A | a comma separated list of all temperature values | + +## Commands (indigo.thermostat.*) { .ref-head-no-code } + +### Decrease Cool Setpoint { .ref-head-no-code } + +Decrease the cool setpoint by a delta value. + +**Command Syntax Examples** + + +`indigo.thermostat.decreaseCoolSetpoint(123)`
+`indigo.thermostat.decreaseCoolSetpoint(123, delta=5)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|----------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `delta` | No | float | Number of degrees to decrease the cool setpoint. If unspecified, the change will be equal to one degree. | + +### Decrease Heat Setpoint { .ref-head-no-code } + +Decrease the heat setpoint by a delta value. + +**Command Syntax Examples** + + +`indigo.thermostat.decreaseHeatSetpoint(123)`
+`indigo.thermostat.decreaseHeatSetpoint(123, delta=5)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `delta` | No | float | Number of degrees to decrease the heat setpoint. If unspecified, the change will be equal to one degree. | + +### Increase Cool Setpoint { .ref-head-no-code } + +Increase the cool setpoint by a delta value. + +**Command Syntax Examples** + + +`indigo.thermostat.increaseCoolSetpoint(123)`
+`indigo.thermostat.increaseCoolSetpoint(123, delta=5)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `delta` | No | float | Number of degrees to increase the cool setpoint. If unspecified, the change will be equal to one degree. | + +### Increase Heat Setpoint { .ref-head-no-code } + +Increase the heat setpoint by a delta value. + +**Command Syntax Examples** + + +`indigo.thermostat.increaseHeatSetpoint(123)`
+`indigo.thermostat.increaseHeatSetpoint(123, delta=5)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|----------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `delta` | No | float | Number of degrees to increase the heat setpoint. If unspecified, the change will be equal to one degree. | + +### Set Cool Setpoint { .ref-head-no-code } + +Set the cool setpoint to an absolute temperature. + +**Command Syntax Examples** + +`indigo.thermostat.setCoolSetpoint(123, value=78)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `value` | Yes | integer | the absolute temperature of the setpoint | + +### Set Fan Mode { .ref-head-no-code } + +Adjust the fan mode. + +**Command Syntax Examples** + +`indigo.thermostat.setFanMode(123, value=indigo.kFanMode.AlwaysOn)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|-----------------------------------|--------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `value` | Yes | [kFanMode](#fan-mode-enumeration) | the operating mode for the fan | + +### Set Heat Setpoint { .ref-head-no-code } + +Set the heat setpoint to an absolute temperature. + +**Command Syntax Examples** + +`indigo.thermostat.setHeatSetpoint(123, value=78)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `value` | Yes | integer | the absolute temperature of the setpoint | + +### Set HVAC Mode { .ref-head-no-code } + +Adjust the HVAC mode. + +**Command Syntax Examples** + +`indigo.thermostat.setHvacMode(123, value=indigo.kHvacMode.HeatCool)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|-------------------------------------|------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `value` | Yes | [kHvacMode](#hvac-mode-enumeration) | HVAC mode identifier | + +**Examples** + +```python +# increase the cool setpoint by 5 degrees +indigo.thermostat.decreaseCoolSetpoint(123, delta=5) + +# set the thermostat mode to auto +indigo.thermostat.setHvacMode(123, + value=indigo.kHvacMode.HeatCool) + +# set the heat setpoint to 78 +indigo.thermostat.setHeatSetpoint(123, value=78) +``` diff --git a/reference/canonical/scripting/reference/devices.md b/reference/canonical/scripting/reference/devices.md new file mode 100644 index 0000000..2c5058c --- /dev/null +++ b/reference/canonical/scripting/reference/devices.md @@ -0,0 +1,37 @@ + + +# Devices + +In the IOM, all devices are derived from a common `Device` base class. This base class provides for all the common properties of a device (including devices defined by Server plugins) and each subclass also inherits all the commands in the device command namespace (`indigo.device.`) - there are a few exceptions that will be noted below. Check out the examples for each section to see how you use each device type. + +Like other high-level objects in Indigo, there are rules for modifying devices. For Scripters and Plugin Developers: + +- To create, duplicate, delete, and send commands to a device, use the appropriate command namespace as defined below +- To modify a device's definition get a copy of the device, make the necessary changes, then call `myDevice.replaceOnServer()` + +For Plugin Developers: + +- To update a plugin's props on a device on the server, call `myDevice.replacePluginPropsOnServer(newPropsDict)` rather than try to update them on the local device +- To change a device's state on the server, use `myDevice.updateStateOnServer(key='keyName', value='Value')` +- To change multiple device states on the server at one time, use `myDevice.updateStatesOnServer(update_list)` passing a dictionary that looks like this: + +**example** + +```python +dev = indigo.devices[12345678] +key_value_list = [ + {'key':'someKey1', 'value':True}, + {'key':'someKey2', 'value':456}, + {'key':'someKey3', 'value':789.123, 'uiValue':"789.12 lbs", 'decimalPlaces':2} +] +dev.updateStatesOnServer(key_value_list) +``` + +!!! note + All API descriptions apply to all API versions unless otherwise specified. + +## In This Section + +- **[Device Base Class](base-class.md)** — properties, plugin properties, custom states, and the `indigo.device.*` commands shared by every device. +- **[Device Dictionary Representation](dictionary.md)** — the dict form of a device. +- **[Device Subclasses](../device-subclasses/index.md)** — type-specific properties, states, and commands (dimmer, relay, sensor, speed control, sprinkler, thermostat, multi-IO). diff --git a/reference/canonical/scripting/reference/devices/base-class.md b/reference/canonical/scripting/reference/devices/base-class.md new file mode 100644 index 0000000..c6fa697 --- /dev/null +++ b/reference/canonical/scripting/reference/devices/base-class.md @@ -0,0 +1,819 @@ + + +# Device Base Class { .ref-head-no-code } + +The `Device` class is generally used as a base class - your script will use objects that are instances of one of its subclasses - we'll discuss each of the subclasses later in this section. First, a quick refresher on a fundamental aspect of devices: some devices can only be controlled (i.e. old-style ApplianceLincs, most X10 devices, etc.), called responders in Indigo terminology, other devices are only controllers (i.e. RemoteLincs, PalmPads, etc.), and some devices are both responders and controllers (i.e. KeypadLincs). + +This terminology was really invented for Insteon devices, but we think it applies to other technologies as well but perhaps in a slightly different way. In any event, controller devices can support a number of buttons and/or a number of groups from which commands are sent, all of which Indigo can use to trigger actions. From a technical and practical standpoint, they are in fact the same thing so a single number, indexed based on how the device supports them, is how this property should be used. + +The base class supports getting the number of buttons or groups as a single property (buttonGroupCount) for the device. Use that count as a guide to what you can pass in the various trigger classes. For any device that doesn’t support local physical buttons or sending group commands and/or status commands, the number returned should be 0. + +If you’re writing a plugin that defines devices, they will automatically inherit all the following properties. + +**Class Properties** + +| Property | Type | Writable | [Min API](https://www.indigodomo.com/indigo/api_version_chart.html) | Description | +|-----------------------------------------------------|--------------------------------------------------|----------|-------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `address` | string | No* | 1.0 | the address for the device. *Plugin developers can change this value for their plugin's devices. | +| `batteryLevel` | integer | No | 1.0 | battery level for the device - `None` if the device doesn't support battery operation - shortcut for `dev.states['batteryLevel']` | +| `buttonGroupCount` | integer | No | 1.0 | the number of groups (or buttons in the case of the RemoteLinc and ControLinc) that the controller supports - currently used only for Insteon devices - 0 for other devices | +| `configured` | boolean | No | 1.0 | true if the device has been fully configured - if it's a plugin device, use this to make sure that the device's config dialog has been run at least once. | +| `description` | string | Yes | 1.0 | a description of the device as specified by the user | +| `deviceTypeId` | string | No | 1.0 | the typeId specified in the Devices.xml (or it’s documentation) - only used for plugin defined devices | +| `displayStateId` | string | No | 1.15 | main display stateId key which can be used as a key into states[] property below | +| `displayStateValRaw` | boolean integer string or none | No | 1.15 | raw value of main display state (ex: 72.0) | +| `displayStateValUi` | string | No | 1.15 | UI value (string) of main display state (ex: '72.0°F' ) | +| `displayStateImageSel` | *[kStateImageSel](#state-image-sel-enumeration)* | No | 1.18 | an enumeration specifying which state image icon is shown in Indigo Touch and Indigo client UI - see state image sel enumeration below for possible values | +| `enabled` | boolean | No | 1.0 | is the device enabled - set using the `indigo.device.enable()` method | +| `energyCurLevel` | float | No | 1.11 | current Wattage energy being used by the device (None if not supported) | +| `energyAccumTotal` | float | No | 1.11 | current accumulated energy used since last base time specified in `energyAccumBaseTime` (None if not supported) | +| `energyAccumBaseTime` | datetime | No | 1.11 | the base time from which to calculate the energy total (`energyAccumTotal`) (None if not supported) | +| `energyAccumTimeDelta` | integer | No | 1.11 | the time delta in seconds since the last base time (None if not supported) | +| `errorState` | string | No | 1.0 | the string that represents the current error for the device, empty string if there is no error | +| `folderId` | integer | No | 1.0 | the unique ID of the folder this device is in (0 if it's not in a folder) - use `moveToFolder()` method to change | +| `globalProps` | dictionary | No | 1.0 | an `indigo.Dict()` representing all name/value pairs associated with this device - each plugin will have its own dictionary (`globalProps[pluginId]`) - see [About Plugin Properties](base-class.md#about-plugin-properties) below for details | +| `id` | integer | No | 1.0 | id or instance of the device, assigned on creation by IndigoServer | +| `lastChanged` | datetime | No | 1.0 | the last date/time that the device was changed - populated by IndigoServer | +| `model` | string | No | 1.0 | the model name of the device - defined either by Indigo based on type or by the plugin's device definition | +| `name` | string | Yes | 1.0 | the unique name of the device - no two devices can have the same name | +| `ownerProps` | dictionary | No | 1.20 | an `indigo.Dict()` representing the name/value pairs defined by the plugin that created the device - this is a shortcut into the owner plugin's globalProps data | +| `pluginId` | string | No | 1.0 | if protocol is `Plugin`, the string ID for the plugin | +| `pluginProps` | dictionary | No | 1.0 | an `indigo.Dict()` representing the name/value pairs defined by your plugin for the device - plugin developers should publish this information if you want other plugins/scripts to create devices of this type - see [About Plugin Properties](base-class.md#about-plugin-properties) below for details - use `replacePluginPropsOnServer()` method to change | +| `protocol` | *[kProtocol](#protocol-enumeration)* | No | All | an enumeration specifying the kProtocol of the device - see protocol enumeration below for possible values | +| `remoteDisplay` | boolean | Yes | 1.0 | should this device be displayed in remote clients (IWS, Indigo Touch, etc) - may also be set with `indigo.device.displayInRemoteUI()` | +| `sharedProps` | dictionary | No | **[2.3](https://www.indigodomo.com/indigo/api_release_notes/2.3/)** | an `indigo.Dict()` representing the name/value pairs that are shared by all plugins. This is the property dictionary that you can edit via the Global Properties plugin, and your plugin may manage properties in this dictionary as well to add metadata to devices that your plugin can use for other purposes. Use `dev.replaceSharedPropsOnServer()` to update them (as with pluginProps, you should get copy first, update the copy, then set them back to that copy so you don't accidentally remove some other plugin's props). | +| `states` | dictionary | No | 1.0 | returns an indigo.Dict() of device states - the key is the state id and the value is the value. Note that enumerated states will have not only the state, but also each option for the state. So, for instance, if I had a state called *status* and it had 3 options (*online*, *offline*, *error*), then you'd not only have *status* as a key, but also *status.online*, *status.offline*, and *status.error* as keys in the dictionary. This is so that you can test each state enumeration independently in trigger actions (e.g. *status.online* is true). | +| `supportsAllLightsOnOff` | boolean | No | All | indicates that this device should react to all lights On and all lights Off commands - always False for plugin defined devices | +| `supportsAllOff` | boolean | No | 1.0 | indicates that this device should react to all Off commands - always False for plugin defined devices | +| `supportsOnState` | boolean | No | **[2.2](https://www.indigodomo.com/indigo/api_release_notes/2.2/)** | indicates that the device has an on state | +| `supportsStatusRequest` | boolean | No | 1.0 | indicates if the device supports querying the status - always False for plugin defined devices | +| `version` | boolean | No* | 1.0 | indicates the device's version as appropriate. *Plugin developers can change this value for their plugin's devices. | + +## Protocol Enumeration { #protocol-enumeration .ref-head-no-code } + +| indigo.kProtocol | | +|--------------------------------------|------------------------------------------------------| +| Value | Description | +| `Insteon` | identifies the device as an Insteon device | +| `X10` | identifies the device as an X10 device | +| `ZWave` | identifies the device as an Z-Wave device | +| `Plugin` | identifies the device as a being defined by a plugin | + +## State Image Sel Enumeration { #state-image-sel-enumeration .ref-head-no-code } +!!! note + API v1.18+ only + +| indigo.kStateImageSel | | +|--------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------| +| Value | Description | +| `Auto` | specifies Indigo Server to pick a device image icon that best represents this device class and/or state value (default for all devices) | +| `AvPaused` | overrides to show a A/V paused icon | +| `AvPlaying` | overrides to show a A/V playing icon | +| `AvStopped` | overrides to show a A/V stopped icon | +| `Closed` | overrides to show a generic sensor off icon (grey circle) | +| `DehumidifierOff` | overrides to show a dehumidifier turned off icon | +| `DehumidifierOn` | overrides to show a dehumidifier turned on icon | +| `DimmerOff` | overrides to show a dimmer or bulb off icon | +| `DimmerOn` | overrides to show a dimmer or bulb on icon | +| `DoorSensorClosed` | overrides to show a door sensor closed icon (grey circle) | +| `DoorSensorOpened` | overrides to show a door sensor opened icon (green circle) | +| `EnergyMeterOff` | overrides to show an energy meter off icon | +| `EnergyMeterOn` | overrides to show an energy meter on icon | +| `FanHigh` | overrides to show a fan on (high) icon | +| `FanLow` | overrides to show a fan on (low) icon | +| `FanMedium` | overrides to show a fan on (medium) icon | +| `FanOff` | overrides to show a fan off icon | +| `HumidifierOff` | overrides to show a humidifier turned off icon | +| `HumidifierOn` | overrides to show a humidifier turned on icon | +| `HumiditySensor` | overrides to show a humidity sensor icon | +| `HumiditySensorOn` | overrides to show a humidity sensor on icon | +| `HvacAutoMode` | overrides to show a thermostat in auto mode icon | +| `HvacCooling` | overrides to show a thermostat that is cooling icon | +| `HvacCoolMode` | overrides to show a thermostat in cool mode icon | +| `HvacFanOn` | overrides to show a thermostat with fan blower on only icon | +| `HvacHeating` | overrides to show a thermostat that is heating icon | +| `HvacHeatMode` | overrides to show a thermostat in heat mode icon | +| `HvacOff` | overrides to show a thermostat off icon | +| `LightSensor` | overrides to show a light meter off icon | +| `LightSensorOn` | overrides to show a light meter on icon | +| `Locked` | overrides to show a green lock icon | +| `MotionSensor` | overrides to show a motion sensor icon | +| `MotionSensorTripped` | overrides to show a motion sensor tripped/activated icon | +| `NoImage` | overrides to show no device image icon (was `None` in previous API versions) | +| `Opened` | overrides to show a generic sensor off icon (green circle) | +| `PowerOff` | overrides to show a power off icon | +| `PowerOn` | overrides to show a power on icon | +| `SensorOff` | overrides to show a generic sensor off icon (gray circle) | +| `SensorOn` | overrides to show a generic sensor on icon (green circle) | +| `SensorTripped` | overrides to show a generic sensor tripped icon (red circle) | +| `SprinklerOff` | overrides to show a sprinkler off icon | +| `SprinklerOn` | overrides to show a sprinkler off icon | +| `TemperatureSensor` | overrides to show a temperature sensor icon | +| `TemperatureSensorOn` | overrides to show a temperature sensor on icon | +| `TimerOff` | overrides to show a timer off icon | +| `TimerOn` | overrides to show a timer on icon | +| `Unlocked` | overrides to show a red lock icon | +| `WindowSensorClosed` | overrides to show a window sensor closed icon (grey circle) | +| `WindowSensorOpened` | overrides to show a window sensor opened icon (green circle) | + +!!! note + The following image selectors are available but do not yet have function-specific icons in Indigo Touch and Indigo client UI. Developers are encouraged to use them for automatic future compatibility when the icons are added. + +| indigo.kStateImageSel | | +|-----------------------------------------------------------|---------------------------------------------------------------------------------------------| +| Value | Description | +| `BatteryCharger` | overrides to show a battery charger icon | +| `BatteryChargerOn` | overrides to show a battery charger on icon | +| `BatteryLevel` | overrides to show a battery level icon | +| `BatteryLevel25` | overrides to show a battery level (25%) icon | +| `BatteryLevel50` | overrides to show a battery level (50%) icon | +| `BatteryLevel75` | overrides to show a battery level (75%) icon | +| `BatteryLevelHigh` | overrides to show a battery level (full) icon | +| `BatteryLevelLow` | overrides to show a battery level (low) icon | +| `Custom` | overrides to show a plugin defined custom image icon (not yet implemented; shows `NoImage`) | +| `Error` | overrides to show an error device image icon | +| `WindDirectionSensor` | overrides to show a wind direction sensor icon | +| `WindDirectionSensorEast` | overrides to show a wind direction sensor (E) icon | +| `WindDirectionSensorNorth` | overrides to show a wind direction sensor (N) icon | +| `WindDirectionSensorNorthEast` | overrides to show a wind direction sensor (NE) icon | +| `WindDirectionSensorNorthWest` | overrides to show a wind direction sensor (NW) icon | +| `WindDirectionSensorSouth` | overrides to show a wind direction sensor (S) icon | +| `WindDirectionSensorSouthEast` | overrides to show a wind direction sensor (SE) icon | +| `WindDirectionSensorSouthWest` | overrides to show a wind direction sensor (SW) icon | +| `WindDirectionSensorWest` | overrides to show a wind direction sensor (W) icon | +| `WindSpeedSensor` | overrides to show a wind speed sensor icon | +| `WindSpeedSensorHigh` | overrides to show a wind speed sensor (high) icon | +| `WindSpeedSensorLow` | overrides to show a wind speed sensor (low) icon | +| `WindSpeedSensorMedium` | overrides to show a wind speed sensor (medium) icon | + +## Device Base Class Instance Methods { .ref-head-no-code } + +The following instance methods can be called on device objects. Most are restricted and can only be called from a plugin on a device that the plugin owns. See the Restricted column below for those that are restricted in this way. + +| Method | Restricted | [Min API](https://www.indigodomo.com/indigo/api_version_chart.html) | Description | +|-------------------------------------------------------------------------------|------------|---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `replaceOnServer()` | No | 1.0 | Because you can't directly modify a device's properties on the server, you have to get a local instance of the device and modify the writable properties as necessary. You then call this method and the device will be updated on the server and changes will be sent out to all connected clients. A few examples appear below this table. | +| `replacePluginPropsOnServer(newPropsdict)` | Yes | 1.0 | If you need to make a change to your plugin's property dictionary that's stored as part of the device (see [About Plugin Properties](base-class.md#about-plugin-properties) below) you just use this method to replace the entire dict with a new one. A typical usage will be to get the property dictionary from the device, make changes to the dict, then use this method to store the new dict. A few examples appear below this table. | +| `stateListOrDisplayStateIdChanged()` | Yes | 1.0 | Plugins can subclass the method `getDeviceStateList()` to provide dynamic state list definition information. The default implementation provides a static solution by retrieving the device state list definition from the Devices.xml file. Likewise, the method `getDeviceDisplayStateId()` can be used to dynamically determine which device state should be displayed in the State column of the main device table UI. The problem is that the Indigo Server only calls `getDeviceStateList()` and `getDeviceDisplayStateId()` at very specific times, like when a plugin device dialog is dismissed. So, call `stateListOrDisplayStateIdChanged()` on the device instance you need refreshed at any time and Indigo will then automatically call your plugin's `getDeviceStateList()` and `getDeviceDisplayStateId()` methods (or use the base implementation of looking up the list from Devices.xml) and update the Indigo Server (and all clients). This is particularly useful for plugin updates that need to add new device states to existing device instances created by older versions. In this case, the plugin will need to update the device instances by calling `stateListOrDisplayStateIdChanged()`. A likely place to do this type of instance level upgrading is inside your plugin's `deviceStartComm()` method. | +| `setErrorStateOnServer('error string')` | Yes | 1.0 | The supplied string will show in the state column and turn it red. Passing `None` will clear it. | +| `updateStateOnServer(key, value, clearErrorState)` | Yes | 1.0 | Use this method to update the value of one of your device's states on the server. The server will propagate the change out to any connected clients and fire any triggers that are defined on that state. Pass "true" (default) or "false" on the clearErrorState parameter (not required) to have the error state of the device (set with `setErrorStateOnServer` above) cleared. | +| `updateStateImageOnServer(stateImageSel)` | Yes | 1.18 | Use this method to override which [device state image icon](#state-image-sel-enumeration) is shown for this device on Indigo Touch and the Indigo client UI. The default behavior is for Indigo Server to automatically determine which icon should be shown based on the device class and state value. Only call this method if overriding the default behavior is needed. | + +**Command Syntax Examples** + +Certain device properties are writeable for all Indigo devices, including a device's name, description, enabled/disabled state and remote display. You can't do this directly because device instances are read only. You must first get a copy of the device, make changes to the copy, and then send the copy back to the server. For example, + +```python +# Updating a device's description (the Indigo UI Notes field value.) +dev = indigo.devices[123456789] +dev.description = "My new description." +dev.replaceOnServer() + +dev = indigo.devices[987654321] +dev.name = "My New Name" +dev.replaceOnServer() +``` + +Properties like `Comm Enabled` and `Remote Display` can also be updated using a different approach. +```python +# Set Enabled/Disabled state +indigo.device.enable(12345678, True) +# Set Remote Display Flag +indigo.device.displayInRemoteUI(987654321, False) +``` + +**Command Syntax Examples** + +Some device base class properties need to be updated differently than the examples above because they can only be updated by the plugin that owns them. Properties like `address` and `version` can be updated by their own plugins. For example, + +```python +# Updating a plugin device's properties +dev = indigo.devices[641471711] +new_props = dev.pluginProps +new_props['address'] = "abc" +new_props['version'] = "123" # Indigo UI Firmware field +dev.replacePluginPropsOnServer(new_props) +``` + +Note that you can't update these in the same way as `name` and `description`. Instead, you change them as `pluginProps` and Indigo migrates these values to the base class props for you. + +## About Plugin Properties { .ref-head-no-code } + +Devices have properties - some are class properties, defined by the class itself. One of the biggest requests we've gotten in the past is some way to add arbitrary properties to a device - so that you could store your own data with the device in the database. And with plugin defined devices, we needed a place to store the properties that you need to operate the device. That's what the `pluginProps` and `globalProps` represent - the additional properties that are not defined by the class. `globalProps` is a dictionary of every additional property defined for the device - each plugin has its own dictionary of props in here which are readable by anyone. `pluginProps` is a shortcut to get to your plugin's props and are only writable by your plugin once the plugin has been created - a script can create a device supplied by your plugin along with the necessary properties, which are passed in on the `create()` method. You should publish the properties necessary to make your device work so that scripters can create your devices. + +We mentioned before that devices were read-only, and that's true, and that you'd need to use commands in a different command name space. That's ***mostly*** true. Here's one exception to that rule: to change a device's pluginProps (it must be "owned" by your plugin - that is, the pluginId must be set to your id), you use a method that's in the device's class: replacePluginPropsOnServer(). Here's an example: + +```python +dev=indigo.devices[123] +localPropsCopy = dev.pluginProps +localPropsCopy['pollInterval'] = 10 +dev.replacePluginPropsOnServer(localPropsCopy) +``` + +You would use this technique if you wanted to just change some of the properties that are already defined. Because this method replaces ALL the properties for your plugin in the device, you can just set them all in one call: + +```python +dev=indigo.devices[123] +dev.replacePluginPropsOnServer({'pollInterval':10,'checkForUpdates':True}) +``` + +Note, though, that if you have a `` defined for the device, those properties are also stored here - so in order to make sure your device works correctly you must include those properties as well. If you need to update several properties in your props dict, you can use the `update()` method: + +```python +dev=indigo.devices[123] +localPropsCopy = dev.pluginProps +localPropsCopy.update({'pollInterval':10, 'checkForUpdates':True}) +dev.replacePluginPropsOnServer(localPropsCopy) +``` + +The `update()` method will change the properties specified, and add the property if it doesn't exist. Now, you might be wondering - why do the extra `localPropsCopy = dev.pluginProps` rather than just modify the props in place: + +```python +dev=indigo.devices[123] +dev.pluginProps.update({'pollInterval':10, 'checkForUpdates':True}) +dev.replacePluginPropsOnServer(dev.pluginProps) +``` + +Because the dev object is read-only - when you reference `dev.pluginProps`, it returns a copy rather than returning a reference to the read-only object. So, in effect, you'd be modifying a copy. But, because you aren't saving a reference to that copy, it goes away since the next time you reference `dev.pluginProps` another copy is made. + +If you need to just dump all the properties for a device, you can just: + +```python +dev=indigo.devices[123] +dev.replacePluginPropsOnServer(None) +``` + +That will completely remove your properties from the device. + +## About Custom Device States { .ref-head-no-code } + +If your plugin defines custom devices, they will also need to define a collection of custom states. For instance, let's look at the states defined in a custom device's Devices.xml: + +```xml + + + + + + + + + + + Player Status Changed + Player Status is + Current Player Status + Player Status is + + + Separator + + + String + Current Playlist Name + Current Playlist Name + + + String + Current Album + Current Album + + + String + Current Artist + Current Artist + + + String + Current Track + Current Track + + + Integer + Current Volume + Current Volume + + + Boolean + Shuffling + Shuffling + + +playStatus +``` + +You'll recall from the [Custom Device Type](../../../plugin-dev/reference/xml/devices.md#devices-xml-custom-device) section of the developers guide, these define the states that are used in various places in the UI and by other objects (triggers, control pages, etc.) So, the question is now that the server understands the structure of your devices' states, how do you change them? + +It's actually pretty simple. When your plugin detects a change in one of the states, you just call the `updateStateOnServer('id', value='value')` method. Here are some examples for setting the state based on the above state definitions: + +```python +# assume that someMusicServer represents a device with the above states +# to update the volume state +someMusicServer.updateStateOnServer('volume', value=50) +# to update the track name +someMusicServer.updateStateOnServer('track', value='Cluster One') +# to update the album name +someMusicServer.updateStateOnServer('album', value='The Division Bell') +# to update the artist name +someMusicServer.updateStateOnServer('artist', value='Pink Floyd') +# to update the playStatus +someMusicServer.updateStateOnServer('playStatus', value='playing') +# to update the playStatus +someMusicServer.updateStateOnServer('shuffle', value=True) +``` + +!!! note + For states that have a `` of `Number`, you pass an integer or float; for states that are `Boolean`, you pass Python `True` or `False`; all others pass a string. + + +It's just that simple. This will cause any triggers on the server that are set on your device's states to be fired. It will update any visible control pages. It will show the state that's defined in the `` element in the Mac device table's `State` column. + +The `updateStateOnServer` method has 3 optional parameters: `decimalPlaces` (integer), `triggerEvents` (boolean), and `uiValue` (string, API v1.6+ only). + +When updating floating point state values use the `decimalPlaces` parameter to specify the number of fractional digits to store and display. For example: + +`someThermmostateDevice.updateStateOnServer('mainTemp', value=76.1234, decimalPlaces=2)` + +instructs the Indigo Server to store and display the value as 76.12. + +The `triggerEvents` parameter can be set to False (defaults to True) to have the Indigo Server update the state but ignore any Device State Changed triggers that should be processed as a result of the state change. + +And the optional `uiValue` parameter is used to set UI only display string of the value which is not used in triggers or conditional logic. This is useful for adding units, percent signs, etc.: + +`dev.updateStateOnServer('sensorValue', 72.3, uiValue=u'72.3 °F')` + +## Commands (indigo.device.*) { .ref-head-no-code } + +### All Off { .ref-head-no-code } + +Turns off all devices for all protocols unless a direct parameter is specified. The direct parameter, if specified, will determine which devices will be turned off. In the context of this command, devices are defined as all dimmable (light) and relay (appliance) devices and does not include other device types that may have an on/off state. This command doesn’t work for plugin defined devices regardless of type. + +**Command Syntax Examples** + + +`indigo.device.allOff()`
+`indigo.device.allOff(indigo.kAllDeviceSel.HouseCodeA)`
+`indigo.device.allOff(indigo.kAllDeviceSel.Insteon)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|---------------------------------------------|----------|-----------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | *[kAllDeviceSel](#all-device-selector-enumeration)* | enumerated value to indicate which devices to turn off, all if no parameter is passed - see the `kAllDeviceSel` enumeration for a full description | + +#### All Device Selector Enumeration { #all-device-selector-enumeration .ref-head-no-code } + +| `indigo.kAllDeviceSel` | | +|---------------------------------------------------|-------------------------------------------------| +| Enumerated Type | Description | +| `Insteon` | specify all Insteon devices that support ON/OFF | +| `X10` | specify all X10 devices that support ON/OFF | +| `ZWave` | specify all Z-Wave devices that support ON/OFF | +| `HouseCodeA` | specify all X10 devices in house code A | +| `HouseCodeB` | specify all X10 devices in house code B | +| `HouseCodeC` | specify all X10 devices in house code C | +| `HouseCodeD` | specify all X10 devices in house code D | +| `HouseCodeE` | specify all X10 devices in house code E | +| `HouseCodeF` | specify all X10 devices in house code F | +| `HouseCodeG` | specify all X10 devices in house code G | +| `HouseCodeH` | specify all X10 devices in house code H | +| `HouseCodeI` | specify all X10 devices in house code I | +| `HouseCodeJ` | specify all X10 devices in house code J | +| `HouseCodeK` | specify all X10 devices in house code K | +| `HouseCodeL` | specify all X10 devices in house code L | +| `HouseCodeM` | specify all X10 devices in house code M | +| `HouseCodeN` | specify all X10 devices in house code N | +| `HouseCodeO` | specify all X10 devices in house code O | +| `HouseCodeP` | specify all X10 devices in house code P | + +### Beep { .ref-head-no-code } + +!!! note + API v1.11+ only + +Requests that the device make an audible beep or buzz. Only supported by some hardware. + +**Command Syntax Examples** + +`indigo.device.beep(123)` + +**Parameters** + +| Parameter | Required | Type | Description | +|---------------------------------------------|----------|---------|------------------------------| +| direct parameter | Yes | integer | id or instance of the device | + +### Create { .ref-head-no-code } + +Create a device. You can create devices that are defined by your plugin, in other plugins, and X10 devices. You can't currently create devices that use the Insteon or Z-Wave protocol because of the complex synchronization needed during definition. Use this method to create ALL device types - it will return a device of the correct class to you based on the arguments. It can be considered the "device" factory method. + +This method returns a **copy** of the newly created device. + +**Command Syntax Examples** + +```python +indigo.device.create(protocol=indigo.kProtocol.Plugin, + address='F8', + name='Device Name Here', + description='Description Here', + pluginId='com.mycompany.pluginId', + deviceTypeId='myDeviceTypeId', + props={'propA':'value', 'propB':'value'}, + folder=1234) +``` + +**Parameters** + +| Parameter | Required | Type | Description | +|-------------------------------------------|----------|--------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `address` | No | string | the address of the X10 device - plugins must set an `address` property in their property dictionary | +| `description` | No | string | the description of the device | +| `deviceTypeId` | Yes | string | the id of the device type – defined by the plugin or one of the defined X10 devices. | +| `folder` | No | integer | id or instance of the folder in which to put the newly created device | +| `name` | Yes | string | the name of the device | +| `pluginId` | No | string | the plugin ID - defaults to your plugin's id if in a [Server Plugin](../../../plugin-dev/guide.md#indigo-server-plugins) | +| `props` | No | dictionary | this is the properties for the device - they will be inserted in to the pluginId's property space as supplied above. If you are creating a device of a type defined in a different plugin, it's that plugin's id and properties. | +| `protocol` | Yes | *[kProtocol](#protocol-enumeration)* | the protocol for the device (`indigo.kProtocol.Plugin` or `indigo.kProtocol.X10`) | + +### Delete { .ref-head-no-code } + +Delete the specified device regardless of its type. + +**Command Syntax Examples** + +`indigo.device.delete(123)` + +**Parameters** + +| Parameter | Required | Type | Description | +|---------------------------------------------|----------|---------|----------------------------------------| +| direct parameter | Yes | integer | id or instance of the device to delete | + +### Duplicate { .ref-head-no-code } + +Duplicate the specified device regardless of the type. This method returns a copy of the new device. + +**Command Syntax Examples** + +`indigo.device.duplicate(123, duplicateName='New Name')` + +**Parameters** + +| Parameter | Required | Type | Description | +|---------------------------------------------|----------|---------|-------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device to duplicate | +| `duplicateName` | No | string | name for the newly duplicated device | + +### Enable/Disable { .ca #enable-disable } + +Enable/Disable the specified device regardless of the type. + +**Command Syntax Examples** + +```python +indigo.device.enable(123, value=True) # enable +indigo.device.enable(123, value=False) # disable +``` + +**Parameters** + +| Parameter | Required | Type | Description | +|---------------------------------------------|----------|---------|------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device to enable/disable | +| `value` | No | boolean | `True` to enable, `False` to disable | + +### Get Dependencies { .ref-head-no-code } + +Return an indigo.Dict with all the dependencies on this device. + +**Command Syntax Examples** + +`indigo.device.getDependencies(123)` + +**Parameters** + +| Parameter | Required | Type | Description | +|---------------------------------------------|----------|---------|-----------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device to get the dependencies for. | + +The dictionary will look something like this: + +```python +>>> print( )indigo.device.getDependencies(91776575)) +Data : (dict) + actionGroups : (list) + Data : (dict) + ID : 1280166770 (integer) + Name : Set var to device state (string) + controlPages : (list) + devices : (list) + schedules : (list) + triggers : (list) + Data : (dict) + ID : 106487666 (integer) + Name : Thermostat condition test (string) + variables : (list) +``` + +So, the dictionary will have 6 top-level keys: "actionGroups", "controlPages", "devices", "schedules", "triggers", and "variables". Each one of those keys will return a list object. Inside that list object will be multiple dicts, one for each dependency (or an empty list if there are none). Each dependency dictionary has two keys: "ID" which is the unique id and "Name" which is the name of the object. + +### Get Group List { .ref-head-no-code } + +!!! note + API v1.14+ only + +Return an indigo.List with all device IDs in a device group. + +**Command Syntax Examples** + +Returns an indigo.List of all devices grouped with dev + +`indigo.device.getGroupList(123)` + +**Parameters** + +| Parameter | Required | Type | Description | +|---------------------------------------------|----------|---------|--------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of any device that belongs to a device group. | + +getGroupList() is useful to get the main/root device of a device group. Some properties, such as batteryLevel, only exist on the main/root device. In this example we log the batteryLevel for a module given any devices that belong to its group: + +```python +groupList = indigo.device.getGroupList(devIdOrInstance) +rootDevice = indigo.devices[groupList[0]] +indigo.server.log('battery level is: ' + str(rootDevice.batteryLevel)) +``` + +See also `indigo.device.groupWithDevice()` and `indigo.device.ungroupDevice()`. + +### Group With Device { .ref-head-no-code } + +To group two or more devices together, use the `indigo.device.groupWithDevice()` command. The parameters are the Indigo Device object IDs of the devices to be grouped. **Note if you have the device dialog UI open, it will not dynamically update, and you shouldn’t call either method if the device factory UI is open.** + +**Command Syntax Examples** + +`indigo.device.groupWithDevice(dev_1, dev_2)` + +**Parameters** + +| Parameter | Required | Type | Description | +|-----------------------------------------------------|----------|---------|-----------------------------------------------------------------------| +| direct parameter (dev_1) | Yes | integer | id, name or instance of a device that will belong to the group. | +| direct parameter (dev_2) | Yes | integer | id, name or instance of another device that will belong to the group. | + +For example, if you want to group devices 123 and 456, you would use `indigo.device.groupWithDevice(123, 456)`. There is no message printed to the events log if the devices grouped together successfully. If you want to add device 789 to the group, you would use `indigo.device.groupWithDevice(456, 789)`. This is a great way to bring together different devices that have a common thread, but bear in mind that it's best not to try to group too many devices together. See also `indigo.device.ungroupDevice()` and `indigo.device.getGroupList()`. + +### Move To Folder { .ref-head-no-code } + +Use this command to move the device to a different folder. + +**Command Syntax Examples** + +`indigo.device.moveToFolder(123, value=987)` + +**Parameters** + +| Parameter | Required | Type | Description | +|---------------------------------------------|----------|---------|----------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `value` | Yes | integer | id or instance of the folder to move the device to | + +### Ping Device { .ref-head-no-code } + +!!! note + API v1.16+ only + +Sends the Z-Wave or Insteon module a ping command and measures the round trip ACK time. Returns a dict containing the `Success` and `TimeDelta` (milliseconds) result. + +**Command Syntax Examples** + +```python +result = indigo.device.ping(123, suppressLogging=True) +if result["Success"]: + indigo.server.log("%.3f seconds ping for %s" % (result["TimeDelta"]/1000.0, dev.name)) +else: + indigo.server.log("ping failed for %s" % dev.name, isError=True) +``` + +**Parameters** + +| Parameter | Required | Type | Description | +|----------------------------------------------|----------|---------|---------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `suppressLogging` | No | boolean | `True` to keep the request from being logged into the event log window (default is `False`) | + +### Remove Delayed Actions { .ref-head-no-code } + +This command will remove delayed actions for the specified device. + +**Command Syntax Examples** + +`indigo.device.removeDelayedActions(123)` + +**Parameters** + +| Parameter | Required | Type | Description | +|---------------------------------------------|----------|---------|------------------------------| +| direct parameter | No | integer | id or instance of the device | + +### Reset Accumulated Energy Total { .ref-head-no-code } + +!!! note + API v1.11+ only + +Resets the `energyAccumTotal` and `energyAccumTimeDelta` values and changes the `energyAccumBaseTime` to the server's current datetime. + +**Command Syntax Examples** + +`indigo.device.resetEnergyAccumTotal(123)` + +**Parameters** + +| Parameter | Required | Type | Description | +|---------------------------------------------|----------|---------|------------------------------| +| direct parameter | Yes | integer | id or instance of the device | + +### Set Remote Display { .ref-head-no-code } + +Use this command to set the remote display flag for the folder. + +**Command Syntax Examples** + +`indigo.device.displayInRemoteUI(123, value=True)` + +**Parameters** + +| Parameter | Required | Type | Description | +|---------------------------------------------|----------|---------|--------------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `value` | Yes | boolean | True to display the device on remote user interfaces or False to hide it | + +### Status Request { .ref-head-no-code } + +This tells IndigoServer to send a status request command to the specified device and refresh its status. + +**Command Syntax Examples** + + +`indigo.device.statusRequest(123)`
+`indigo.device.statusRequest(123, suppressLogging=True)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|----------------------------------------------|----------|---------|---------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer | the id of the device | +| `suppressLogging` | No | boolean | `True` to keep the request from being logged into the event log window (default is `False`) | + +### Toggle { .ref-head-no-code } + +This tells IndigoServer to toggle a device from on to off or vice versa depending on its current state. This command only works for device types that can be turned on and off. + +**Command Syntax Examples** + + +`indigo.device.toggle(123)`
+`indigo.device.toggle(123, delay=10, duration=300)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|---------------------------------------------|----------|---------|-------------------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `delay` | No | integer | number of seconds to delay before toggling the device | +| `duration` | No | integer | number of seconds delay before the device toggles back to it’s original state | + +### Turn Off { .ref-head-no-code } + +This tells IndigoServer to turn off a device. This command only works for device types that can be turned on and off. + +**Command Syntax Examples** + + +`indigo.device.turnOff(123)`
+`indigo.device.turnOff(123, delay=10, duration=300)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|---------------------------------------------|----------|---------|----------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `delay` | No | integer | number of seconds to delay before turning off the device | +| `duration` | No | integer | number of seconds delay before the device turns back on | + +### Turn On { .ref-head-no-code } + +This tells IndigoServer to turn on a device. This command only works for device types that can be turned on and off. + +**Command Syntax Examples** + + +`indigo.device.turnOn(123)`
+`indigo.device.turnOn(123, delay=10, duration=300)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|---------------------------------------------|----------|---------|----------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `delay` | No | integer | number of seconds to delay before turning on the device | +| `duration` | No | integer | number of seconds delay before the device turns back off | + +### Ungroup With Device { .ref-head-no-code } + +If you want to remove a device from a group, use the `indigo.device.ungroupDevice()` command. Use this command with the ID of the device you want removed from the group.**Note if you have the device dialog UI open, it will not dynamically update, and you shouldn’t call either method if the device factory UI is open.** + +**Command Syntax Examples** + +`indigo.device.ungroupDevice(dev)` + +**Parameters** + +| Parameter | Required | Type | Description | +|---------------------------------------------|----------|---------|------------------------------------------------------------------| +| direct parameter | Yes | integer | id, name or instance of the device to be removed from the group. | + +If successful, nothing will be printed to the events log. See also `indigo.device.groupWithDevice()` and `indigo.device.getGroupList()`. + +### Unlock { .ref-head-no-code } + +!!! note + API v2.0+ only + +This tells IndigoServer to unlock a device. This command only works for relay device types that have `pluginProps["IsLockSubType"]` set to True. + +**Command Syntax Examples** + + +`indigo.device.unlock(123)`
+`indigo.device.unlock(123, delay=10, duration=300)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|---------------------------------------------|----------|---------|---------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `delay` | No | integer | number of seconds to delay before unlocking the device | +| `duration` | No | integer | number of seconds delay before the device automatically locks | + +### Lock { .ref-head-no-code } + +!!! note + API v2.0+ only + +This tells IndigoServer to lock a device. This command only works for relay device types that have the property `pluginProps["IsLockSubType"]` set to True. + +**Command Syntax Examples** + + +`indigo.device.lock(123)`
+`indigo.device.lock(123, delay=10, duration=300)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|---------------------------------------------|----------|---------|-----------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the device | +| `delay` | No | integer | number of seconds to delay before locking the device | +| `duration` | No | integer | number of seconds delay before the device automatically unlocks | + +**Command Syntax Examples** + +```python +# Creating a device +myDevice = indigo.device.create(protocol=indigo.kProtocol.X10, + name="Office Lamp", + description="X10 Lamp module", + address="F7", + deviceTypeId="LampLinc Plus Plug-In Dimmer") + +# Getting a copy of a device +myDevice = indigo.devices[123] + +# Logging a message if it’s an X10 device +if myDevice.protocol == indigo.kProtocol.X10: + indigo.server.log("device is an X10 device") + +# Logging a message if it’s showing in Indigo Touch +if myDevice.remoteDisplay: + indigo.server.log("device is showing in Indigo Touch") + +# Setting the folder ID that the device is in +indigo.device.setFolder(myDevice, 987) + +# Turning off all devices (dimmer and relay) +indigo.device.allOff() + +# Turning off all devices in X10 house code A +indigo.device.allOff(indigo.kAllDeviceSel.HouseCodeA) + +# Turning off all Insteon devices +indigo.device.allOff(indigo.kAllDeviceSel.Insteon) +``` diff --git a/reference/canonical/scripting/reference/devices/dictionary.md b/reference/canonical/scripting/reference/devices/dictionary.md new file mode 100644 index 0000000..182757a --- /dev/null +++ b/reference/canonical/scripting/reference/devices/dictionary.md @@ -0,0 +1,219 @@ + + +# Generating a Dictionary for a device { .ref-head-no-code } + +Sometimes, it's useful to get a python dictionary that has all properties and states of a device, perhaps for conversion to JSON to send to some other system. This can easily be done for any device regardless of type: + +```text +>>> my_device = indigo.devices[854505717] # "017 - Door/Window Sensor" +>>> python_dict = dict(my_device) +>>> print(json.dumps(python_dict, indent=4, cls=indigo.utils.IndigoJSONEncoder)) +{ + "allowOnStateChange": false, + "protocol": "ZWave", + "configured": true, + "states": { + "onOffState": true, + "batteryLevel.ui": "100%", + "batteryLevel": 100 + }, + "subType": "", + "globalProps": { + "com.perceptiveautomation.indigoplugin.zwave": { + "zwEncryptClassCmdMapStr": "- none -", + "zwConfigVals": {}, + "zwClassInstanceCountMap": {}, + "zwFeatureListStr": "routing, battery, beaming, waking", + "zwShowWakeIntervalUI": true, + "zwModelId": 0, + "zwClassIds": [ + 4, + 7, + 1 + ], + "zwModelName": "Notification Sensor", + "zwShowDumpDevToLog": false, + "zwEndpointClassMapStr": "- none -", + "zwLibType": 3, + "SupportsBatteryLevel": true, + "zwShowSubmitModelInfoUI": true, + "zwEncryptionStatusStr": "Not Supported", + "zwProtoVersMajor": 4, + "zwEndpointDevTypeMap": {}, + "userWakeInterval": 60, + "indigoObjVersion": 10, + "zwClassInstanceCountMapStr": "- none -", + "version": "5.01", + "zwClassCmdMap": { + "c134": 1, + "c90": 1, + "c133": 1, + "c132": 2, + "c89": 1, + "c94": 1, + "c128": 1, + "c122": 1, + "c115": 1, + "c114": 1, + "c113": 1, + "c32": 1 + }, + "SupportsOnState": true, + "zwClassCmdMapStr": "20v1 80v1 84v2 85v1 86v1 71v1 72v1 73v1 59v1 5Av1 7Av1 5Ev1", + "userEnergyPollingEnabled": false, + "zwShowMainUI": true, + "zwAppVersMinor": 1, + "userPollInterval": 0, + "zwDevSubIndex": 0, + "zwShowManualModifyConfigParmUI": true, + "zwManufactureName": "Unknown", + "zwEncryptClassCmdMap": {}, + "address": 17, + "zwEndpointClassMap": {}, + "zwConfigValsStr": "- none -", + "zwClassName": "Notification Sensor", + "zwEndpointDevTypeMapStr": "- none -", + "zwNodeNeighborsStr": "1, 15, 16", + "zwModelDefnVers": 0, + "zwManufactureId": 0, + "SupportsSensorValue": false, + "zwAssociationsMap": { + "g1": [ + 1 + ] + }, + "zwShowPollingUI": false, + "zwAssociationsMapStr": "1:[1]", + "zwShowEnergyPollingUI": false, + "zwWakeInterval": 60, + "userPollAfterActivity": true, + "zwClassCmdBase": 0, + "zwAppVersMajor": 5, + "zwProtoVersMinor": 5, + "zwNodeNeighbors": [ + 1, + 15, + 16 + ], + "userPollingEnabled": true + } + }, + "pluginProps": {}, + "lastSuccessfulComm": "2021-11-18T10:56:35", + "buttonGroupCount": 0, + "id": 854505717, + "supportsAllOff": false, + "errorState": "", + "remoteDisplay": true, + "energyAccumBaseTime": null, + "displayStateValUi": "on", + "subModel": "", + "allowSensorValueChange": false, + "version": "5.01", + "energyAccumTimeDelta": null, + "displayStateId": "onOffState", + "batteryLevel": 100, + "supportsStatusRequest": true, + "supportsSensorValue": false, + "ownerProps": { + "zwEncryptClassCmdMapStr": "- none -", + "zwConfigVals": {}, + "zwClassInstanceCountMap": {}, + "zwFeatureListStr": "routing, battery, beaming, waking", + "zwShowWakeIntervalUI": true, + "zwModelId": 0, + "zwClassIds": [ + 4, + 7, + 1 + ], + "zwModelName": "Notification Sensor", + "zwShowDumpDevToLog": false, + "zwEndpointClassMapStr": "- none -", + "zwLibType": 3, + "SupportsBatteryLevel": true, + "zwShowSubmitModelInfoUI": true, + "zwEncryptionStatusStr": "Not Supported", + "zwProtoVersMajor": 4, + "zwEndpointDevTypeMap": {}, + "userWakeInterval": 60, + "indigoObjVersion": 10, + "zwClassInstanceCountMapStr": "- none -", + "version": "5.01", + "zwClassCmdMap": { + "c134": 1, + "c90": 1, + "c133": 1, + "c132": 2, + "c89": 1, + "c94": 1, + "c128": 1, + "c122": 1, + "c115": 1, + "c114": 1, + "c113": 1, + "c32": 1 + }, + "SupportsOnState": true, + "zwClassCmdMapStr": "20v1 80v1 84v2 85v1 86v1 71v1 72v1 73v1 59v1 5Av1 7Av1 5Ev1", + "userEnergyPollingEnabled": false, + "zwShowMainUI": true, + "zwAppVersMinor": 1, + "userPollInterval": 0, + "zwDevSubIndex": 0, + "zwShowManualModifyConfigParmUI": true, + "zwManufactureName": "Unknown", + "zwEncryptClassCmdMap": {}, + "address": 17, + "zwEndpointClassMap": {}, + "zwConfigValsStr": "- none -", + "zwClassName": "Notification Sensor", + "zwEndpointDevTypeMapStr": "- none -", + "zwNodeNeighborsStr": "1, 15, 16", + "zwModelDefnVers": 0, + "zwManufactureId": 0, + "SupportsSensorValue": false, + "zwAssociationsMap": { + "g1": [ + 1 + ] + }, + "zwShowPollingUI": false, + "zwAssociationsMapStr": "1:[1]", + "zwShowEnergyPollingUI": false, + "zwWakeInterval": 60, + "userPollAfterActivity": true, + "zwClassCmdBase": 0, + "zwAppVersMajor": 5, + "zwProtoVersMinor": 5, + "zwNodeNeighbors": [ + 1, + 15, + 16 + ], + "userPollingEnabled": true + }, + "description": "", + "onState": true, + "energyAccumTotal": null, + "address": "17", + "sharedProps": {}, + "folderId": 1145028206, + "supportsOnState": true, + "sensorValue": null, + "energyCurLevel": null, + "name": "017 - Door/Window Sensor", + "lastChanged": "2021-11-18T10:56:35", + "enabled": true, + "pluginId": "com.perceptiveautomation.indigoplugin.zwave", + "deviceTypeId": "zwOnOffSensorType", + "supportsAllLightsOnOff": false, + "displayStateImageSel": "SensorOn", + "displayStateValRaw": true, + "model": "Notification Sensor" +} +``` + +This will be a python dictionary with all details about a device. One other feature to note here: by default, Python datetime objects are not serializable by the JSON module (odd oversight we believe). We have included a JSON encoder class that will solve this problem: [`indigo.utils.IndigoJSONEncoder`](../utils.md#indigojsonencoder). Just specify that as the cls parameter to a JSON dump call and any datetime class that's encountered during JSON encoding will be correctly converted to a string in the resulting JSON string: + + json.dumps(python_dict_with_datetime, cls=indigo.utils.JSONDateEncoder) diff --git a/reference/canonical/scripting/reference/event-data-paths.md b/reference/canonical/scripting/reference/event-data-paths.md new file mode 100644 index 0000000..6c6bb29 --- /dev/null +++ b/reference/canonical/scripting/reference/event-data-paths.md @@ -0,0 +1,338 @@ + + +# Events Data Path Specifiers +## Path Strings +Path strings use a specific syntax and require knowledge about the data you'll be working with. For example, given this Indigo event data: +```json +{ + "foo": 1234567890, + "bar": "Baz", + "data": ["Thing 1", "Thing 2"], # a list + "more_data": {'a': 1, 'b': 2}, # a dictionary + "timestamp": "2025-08-07T14:32:21", +} +``` + +You could use path strings like these to retrieve the associated data: + +| Path String | Result | +|--------------------|------------------------| +| *`bar`* | "Baz" | +| *`data`* | ["Thing 1", "Thing 2"] | +| *`more_data['b']`* | 2 | + + +If you want to go deeper into the payload, path strings can be chained together like this: + +```json +{ + "foo": 1234567890, + "bar": "Baz", + "data": [ + "Thing 1", + [ + "Thing A", + "Thing B", + "Thing C" + ] + ], + "more_data": { + "a": 1, + "b": 2, + "c": [ + 1, + 2, + 3, + 4, + 5 + ] + }, + "timestamp": "2025-08-07T14:32:21" +} +``` + +You can go deeper like this: + +| Path String | Result | +|--------------------|------------------------------------------------------------------------------------------------------------------------| +| *`data[0]`* | "Thing 1" (The index of the list element, the index starts at zero) | +| *`data[1][2]`* | "Thing C" # The second element (index 1) of "data" is a list and the third element (index 2) of that list is "Thing C" | +| *`more_data.a`* | 1 # The value of key "a" is 1 | +| *`more_data.c[3]`* | 4 # The value of key "c" is a list and the fourth element (index 3) of that list is 4 | + + +You can chain these path strings as needed, such as *`some_json[3].a.foo[9]`*. NOTE: if a path string is not provided, Indigo will return the entire payload. Any path that includes a collection (like the lists and dicts above), Indigo will convert it to JSON and respond with that data. + +## Indigo-Supplied Event Data +We updated most of the built-in events (Triggers, Schedules, etc.) to pass through the event data that's specific to those events. All events, regardless of what they are, will contain the following values: + +```json +{ + "event-indigo-id": 127375748, + "event-type": "VariableValueChangeTrigger", + "source": "server", + "timestamp": "2025-08-07T14:13:54" +} +``` + +The `event-indigo-id` is the Indigo ID for the trigger, schedule, or action group. The `event-type` is the IOM event type. `source` is how the trigger was fired, and `timestamp` is an ISO formatted `datetime` of the event. + +Each specific trigger/schedule/action group may add additional data that will assist you later in the event processing chain. The following blocks provide samples of event data that Indigo supplies for various events. + +### Action Groups +```json +[ + { + "_comment": "This is what you get when you execute an action group from the http api", + "event-indigo-id": 1893263747, + "event-type": "ActionGroup", + "source": "api-http", + "timestamp": "2025-10-16T14:59:05", + "unique-id": "1dd66fc1-ce8c-43f7-996f-5b29278c1b90" + }, + { + "_comment": "When a webhook is called with a JSON dictionary (in data param)", + "data": { + "id": "ShortcutWithJsonInputTests.test_webhook_json_input_var_output", + "message": "something-happened", + "some-data": { + "some-key": [ + "some-value", + "some-other-value" + ], + "unique-id": "9d34e21b-9926-4023-ab94-c0cc52ecedd5" + } + }, + "event-indigo-id": 879507411, + "event-plugin-event-id": "simpleWebhook", + "event-plugin-id": "com.indigodomo.webserver", + "event-plugin-name": "Web Server", + "event-type": "PluginEventTrigger", + "http-method": "POST", + "http-post-content": "JSON", + "request-url": "https://localhost:8176/webhook/shortcut-json-input-var-output", + "source": "python", + "status-code": 200, + "timestamp": "2025-10-16T14:58:55", + "webhook-id": "shortcut-json-input-var-output" + } +] +``` + +### Control Page Clicks +```json +{ + "client-ip": "127.0.0.1", + "client-is-private": true, + "control-id": 0, + "event-indigo-id": 120091806, + "event-type": "ControlPage", + "timestamp": "2025-10-16T16:10:54" +} +``` + +### Device State Change Trigger +If a device state has any change. + +```python +{ + "_comment": "When the server executes a has any change device state change trigger" + "change-type": "any change", + "change-type-iom": "indigo.kStateChange.Changes", + "device-id": 1167180255, + "event-indigo-id": 625370601, + "event-type": "DeviceStateChangeTrigger", + "new-value": "on", + "old-value": "off", + "source": "server", + "state-key": "onOffState", + "timestamp": "2025-10-16T17:07:23" +} +``` + +### Insteon Command Received +```json +[ + { + "_comment": "when any insteon command is received", + "event-indigo-id": 1086420814, + "event-type": "InsteonCommandReceivedTrigger", + "insteon-cmd-rcvd": { + "Address": 1652724, + "AddressStr": "19.37.F4", + "CommandDetails": 0, + "CommandName": "on", + "CommandVal": 0, + "GroupBroadcastNum": 1, + "IsGroupBroadcast": true, + "SendCleanups": false + }, + "source": "server", + "timestamp": "2025-09-24T15:00:12" + }, + { + "_comment": "when a double-tap off command is received" + "event-indigo-id": 1398699236, + "event-type": "InsteonCommandReceivedTrigger", + "insteon-cmd-rcvd": { + "Address": 1652724, + "AddressStr": "19.37.F4", + "CommandDetails": 0, + "CommandName": "off (instant)", + "CommandVal": 0, + "GroupBroadcastNum": 1, + "IsGroupBroadcast": true, + "SendCleanups": false + }, + "source": "server", + "timestamp": "2025-08-12T17:39:49" + } +] +``` + +### Server Executed Schedules +When the server executes a schedule during normal operation. + +```json +{ + "event-indigo-id": 485722026, + "event-type": "Schedule", + "source": "server", + "timestamp": "2025-10-16T13:55:02" +} +``` + +### Variable Changes +```python +[ + { + "change-type": "becomes equal", + "change-type-iom": "indigo.kVarChange.BecomesEqual", + "event-indigo-id": 1533121690, + "event-type": "VariableValueChangeTrigger", + "new-val": "var_becomes_equal_to", + "old-val": "", + "source": "server", + "test-val": "var_becomes_equal_to", + "timestamp": "2025-10-16T13:56:23", + "var-id": 54247914 + }, + { + "change-type": "becomes false", + "change-type-iom": "indigo.kVarChange.BecomesFalse", + "event-indigo-id": 1202935007, + "event-type": "VariableValueChangeTrigger", + "new-val": "false", + "old-val": "true", + "source": "server", + "timestamp": "2025-10-16T13:56:24", + "var-id": 24284935 + }, + { + "change-type": "becomes greater than", + "change-type-iom": "indigo.kVarChange.BecomesGreaterThan", + "event-indigo-id": 1952491014, + "event-type": "VariableValueChangeTrigger", + "new-val": "11", + "old-val": "9", + "source": "server", + "test-val": "10", + "timestamp": "2025-10-16T13:56:25", + "var-id": 738010762 + }, + { + "change-type": "becomes less than", + "change-type-iom": "indigo.kVarChange.BecomesLessThan", + "event-indigo-id": 1574402918, + "event-type": "VariableValueChangeTrigger", + "new-val": "9", + "old-val": "11", + "source": "server", + "test-val": "10", + "timestamp": "2025-10-16T13:56:26", + "var-id": 1177127882 + }, + { + "change-type": "becomes not equal", + "change-type-iom": "indigo.kVarChange.BecomesNotEqual", + "event-indigo-id": 986473566, + "event-type": "VariableValueChangeTrigger", + "new-val": "", + "old-val": "var_becomes_not_equal_to", + "source": "server", + "test-val": "var_becomes_not_equal_to", + "timestamp": "2025-10-16T13:56:28", + "var-id": 127994353 + }, + { + "change-type": "becomes true", + "change-type-iom": "indigo.kVarChange.BecomesTrue", + "event-indigo-id": 184109916, + "event-type": "VariableValueChangeTrigger", + "new-val": "true", + "old-val": "false", + "source": "server", + "timestamp": "2025-10-16T13:56:29", + "var-id": 836393156 + }, + { + "change-type": "any change", + "change-type-iom": "indigo.kVarChange.Changes", + "event-indigo-id": 127375748, + "event-type": "VariableValueChangeTrigger", + "new-val": "2025-10-16T13:56:30.146312", + "old-val": "", + "source": "server", + "timestamp": "2025-10-16T13:56:30", + "var-id": 908899214 + } +] +``` + +### Z-Wave +```python +[ + { + "_comment": "This is an example of the event_data from a Z-Wave command received, all event types except match raw (see below)", + "event-indigo-id": 886317539, + "event-plugin-event-id": "zwaveCommand", + "event-plugin-id": "com.perceptiveautomation.indigoplugin.zwave", + "event-plugin-name": "Z-Wave", + "event-type": "PluginEventTrigger", + "timestamp": "2025-07-25T15:37:14", + "zwavecmd-device-id": 1191650674, + "zwavecmd-node-id": 2, + "zwavecmd-scene-id": 255 + }, + { + "_comment": "This is an example of the event_data from a Z-Wave command received, Match Raw Packet", + "event-indigo-id": 572444380, + "event-plugin-event-id": "zwaveCommand", + "event-plugin-id": "com.perceptiveautomation.indigoplugin.zwave", + "event-plugin-name": "Z-Wave", + "event-type": "PluginEventTrigger", + "timestamp": "2025-07-25T16:44:40", + "zwavecmd-node-id": 2, + "zwavecmd-packet": [ + 1, + 9, + 0, + 4, + 0, + 2, + 3, + 38, + 3, + 58, + 242 + ], + "zwavecmd-packet-str": "01 09 00 04 00 02 03 26 03 3A F2" + }, + "If you want to get the match string from the trigger, then you can do this:", + " trigger_id = event_data['event-indigo-id'] # get the trigger id", + " firing_trigger = indigo.triggers[trigger_id] # get the trigger instance", + " zwave_plugin_id = event_data['event-plugin-id'] # get the z-wave plugin id", + " zwave_props = firing_trigger.globalProps[zwave_plugin_id] # get the properties", + " match_string = zwave_props['matchBytes'] # get the match string from properties" +] +``` diff --git a/reference/canonical/scripting/reference/folders.md b/reference/canonical/scripting/reference/folders.md new file mode 100644 index 0000000..54deeca --- /dev/null +++ b/reference/canonical/scripting/reference/folders.md @@ -0,0 +1,148 @@ + + +# Folders + +The folder class represents a folder in the various Indigo user interfaces (Mac client, Indigo Touch, web, etc.). + +**Class Properties** + +| Property | Type | Description | +|--------------------------------------------|---------|--------------------------------------------------------------------------------------------------------------| +| `id` | integer | the unique id of the folder, assigned on creation by IndigoServer | +| `name` | string | the name of the folder - no two folders in the same namespace (i.e. `indigo.devices`) can have the same name | +| `remoteDisplay` | boolean | should this folder be displayed in remote clients (IWS, Indigo Touch, etc) | + +## Commands (indigo.*.folder.*) { .ref-head-no-code } + +The commands to manipulate folders are within the object lists defined in the IOM Overview page (`indigo.devices.folder.*`, `indigo.variables.folder.*`, etc.) + +### Create { .ref-head-no-code } + +Create a folder. This method returns a **copy** of the newly created folder. + +**Command Syntax Examples** + +`indigo.variables.folder.create("Folder Name Here")` + +**Parameters** + +| Parameter | Required | Type | Description | +|-----------------------------------|----------|--------|------------------------| +| `name` | Yes | string | the name of the folder | + +### Delete { .ref-head-no-code } + +Delete the specified folder. + +**Command Syntax Examples** + +`indigo.devices.folder.delete(123, deleteAllChildren=True)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------------|----------|---------|------------------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the folder to delete | +| `deleteAllChildren` | No | boolean | a boolean to specify whether all objects contained in the folder should be deleted as well - defaults to `False` | + +### Duplicate { .ref-head-no-code } + +Duplicate the specified folder. This method returns a copy of the new folder. + +**Command Syntax Examples** + +`indigo.controlPages.folder.duplicate(123, duplicateName="New Name")` + +**Parameters** + +| Parameter | Required | Type | Description | +|--------------------------------------------|----------|---------|-------------------------------------------| +| direct parameter | Yes | integer | id or instance of the folder to duplicate | +| `duplicateName` | No | string | name for the newly duplicated folder | + +### Get ID { .ref-head-no-code } + +Returns the ID of the named folder under the specified object type (device, trigger, etc.) + +**Command Syntax Examples** + +`indigo.device.folders.getId("Some Folder Name")` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|--------|---------------------------------------------------| +| direct parameter | Yes | string | name of any folder for the specified object type. | + +### Set Remote Display { .ref-head-no-code } + +Use this command to set the remote display flag for the folder. + +**Command Syntax Examples** + + +`indigo.devices.folder.displayInRemoteUI(123, value=True)`
+`indigo.schedules.folder.displayInRemoteUI(123, value=False) +`
+ +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|--------------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the folder | +| `value` | Yes | boolean | True to display the folder on remote user interfaces or False to hide it | + +## Examples + +```python +# create a new variable folder +newFolder = indigo.variables.folder.create("My New Variable Folder") + +# test to see if a folder exists by Name +if "My New Variable Folder" in indigo.variables.folders: + # should execute this because we just created it + indigo.server.log("folder named 'My New Variable Folder' exists") + +# test to see if a folder exists by ID +if newFolder.id in indigo.variables.folders: + # should execute this because we just created it + indigo.server.log("folder id " + newFolder.id + " exists") + +# set the remote display flag on the folder immediately +indigo.variables.folder.displayInRemoteUI(newFolder, value=False) + +# a ValueError exception with the text "NameNotUniqueError" is thrown if you try to +# create a folder with a name that already exists +try: + indigo.variables.folder.create("My New Variable Folder") +except ValueError as e: + if str(e) == "NameNotUniqueError": + # should execute this because it's a dup name + indigo.server.log("folder named 'My New Variable Folder' already exists") + else: + indigo.server.log("Some other error") + +# NOTE - at this point, newFolder.remoteDisplay is still true (default for new folders) +# because we're still working with a copy. Refresh it to get it updated: +newFolder.refreshFromServer() + +# change the name of a folder +newFolder.name="My Variable Folder" +newFolder.replaceOnServer() + +# duplicate the folder +indigo.variables.folder.duplicate(newFolder, duplicateName="My Duplicate Folder") + +# delete a folder +indigo.variables.folder.delete(newFolder) + +#test to see if a folder doesn't exist using name +if "My New Variable Folder" not in indigo.variables.folders: + # should execute this because we just deleted it + indigo.server.log("folder named 'My New Variable Folder' does not exist on the server") + +# test to see if a folder doesn't exist using ID (was deleted perhaps) +if newFolder.id not in indigo.variables.folders: + # should execute this because we just deleted it + indigo.server.log("folder id " + newFolder.id + " does not exist on the server") +``` diff --git a/reference/canonical/scripting/reference/insteon-commands.md b/reference/canonical/scripting/reference/insteon-commands.md new file mode 100644 index 0000000..b2b8a13 --- /dev/null +++ b/reference/canonical/scripting/reference/insteon-commands.md @@ -0,0 +1,311 @@ + + +# Insteon Commands (indigo.insteon.*) + +Commands that are specific to Insteon devices. + +## Send Scene Decrease { .ref-head-no-code } + +This command will send an Insteon Scene Decrease (dim on dimmable loads) command to the specified PowerLinc scene. The value will decrease by 3% for each call. + +**Command Syntax Examples** + + +`indigo.insteon.sendSceneDecrease(11)`
+`indigo.insteon.sendSceneDecrease(11, repeatCount=5)`
+`indigo.insteon.sendSceneDecrease(11, suppressLogging=True)`
+`indigo.insteon.sendSceneDecrease(11, updateStatesOnly=True)`
+`indigo.insteon.sendSceneDecrease("working in office scene")` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|-----------------------------------------------|----------|-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer or string | either the scene number or the scene name - we encourage numbers since they won't change and the scene names can. | +| `repeatCount` | No | integer | a value from 1-32 for the number of times to repeat the decrease (default is 1) | +| `suppressLogging` | No | boolean | True if entries in the event log should be suppressed (default is False) | +| `updateStatesOnly` | No | boolean | use if you only want Indigo's internal device state representation to be updated - no actual Insteon command will be sent on RF or the power line (default is False) | + +## Send Scene Increase { .ref-head-no-code } + +This command will send an Insteon Scene Increase (brighten on dimmable loads) command to the specified PowerLinc scene. The value will increase by 3% for each call. + +**Command Syntax Examples** + + +`indigo.insteon.sendSceneIncrease(11)`
+`indigo.insteon.sendSceneIncrease(11, repeatCount=5)`
+`indigo.insteon.sendSceneIncrease(11, suppressLogging=True)`
+`indigo.insteon.sendSceneDecrease(11, updateStatesOnly=True)`
+`indigo.insteon.sendSceneIncrease("working in office scene")` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|-----------------------------------------------|----------|-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer or string | either the scene number or the scene name - we encourage numbers since they won't change and the scene names can. | +| `repeatCount` | No | integer | a value from 1-32 for the number of times to repeat the increase (default is 1) | +| `suppressLogging` | No | boolean | True if entries in the event log should be suppressed (default is False) | +| `updateStatesOnly` | No | boolean | use if you only want Indigo's internal device state representation to be updated - no actual Insteon command will be sent on RF or the power line (default is False) | + +## Send Scene ON { .ref-head-no-code } + +This command will send an Insteon Scene ON command using the specified PowerLinc scene. + +**Command Syntax Examples** + + +`indigo.insteon.sendSceneOn(11)`
+`indigo.insteon.sendSceneOn(11)`
+`indigo.insteon.sendSceneOn(11, sendCleanUps=False)`
+`indigo.insteon.sendSceneOn(11, suppressLogging=True)`
+`indigo.insteon.sendSceneOn(11, updateStatesOnly=True)`
+`indigo.insteon.sendSceneOn("working in office scene")`
+`indigo.insteon.sendSceneDecrease(11, updateStatesOnly=True)`
+`indigo.insteon.sendSceneOn("working in office scene", sendCleanUps=False)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|-----------------------------------------------|----------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer or string | either the scene number or the scene name - we encourage numbers since they won't change and the scene names can. | +| `sendCleanUps` | No | boolean | True if cleanup messages should be sent to each device in the scene after the scene command - this will have the PowerLinc send each device a message to make sure it received the command. You might want to stop cleanup messages from being sent because it involves adding a lot of Insteon traffic and might affect performance (particularly if a group has a lot of responders). But, of course, disabling it may reduce reliability of some modules receiving the scene command so it's usually best to ignore this setting. (default is True) | +| `suppressLogging` | No | boolean | True if entries in the event log should be suppressed (default is False) | +| `updateStatesOnly` | No | boolean | use if you only want Indigo's internal device state representation to be updated - no actual Insteon command will be sent on RF or the power line (default is False) | + +## Send Scene OFF { .ref-head-no-code } + +This command will send an Insteon Scene OFF command using the specified PowerLinc scene. + +**Command Syntax Examples** + + +`indigo.insteon.sendSceneOff(11)`
+`indigo.insteon.sendSceneOff(11, sendCleanUps=False)`
+`indigo.insteon.sendSceneOff(11, suppressLogging=True)`
+`indigo.insteon.sendSceneOff(11, updateStatesOnly=True)`
+`indigo.insteon.sendSceneOff("working in office scene")`
+`indigo.insteon.sendSceneOff("working in office scene", sendCleanUps=False)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|-----------------------------------------------|----------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer or string | either the scene number or the scene name - we encourage numbers since they won't change and the scene names can. | +| `sendCleanUps` | No | boolean | True if cleanup messages should be sent to each device in the scene after the scene command - this will have the PowerLinc send each device a message to make sure it received the command. You might want to stop cleanup messages from being sent because it involves adding a lot of Insteon traffic and might affect performance (particularly if a group has a lot of responders). But, of course, disabling it may reduce reliability of some modules receiving the scene command so it's usually best to ignore this setting. (default is True) | +| `suppressLogging` | No | boolean | True if entries in the event log should be suppressed (default is False) | +| `updateStatesOnly` | No | boolean | use if you only want Indigo's internal device state representation to be updated - no actual Insteon command will be sent on RF or the power line (default is False) | + +## Send Scene Fast ON { .ref-head-no-code } + +This command will send an Insteon Scene Fast ON command using the specified PowerLinc scene. + +**Command Syntax Examples** + + +`indigo.insteon.sendSceneFastOn(11)`
+`indigo.insteon.sendSceneFastOn(11, sendCleanUps=False)`
+`indigo.insteon.sendSceneFastOn(11, suppressLogging=True)`
+`indigo.insteon.sendSceneFastOn(11, updateStatesOnly=True)`
+`indigo.insteon.sendSceneFastOn("working in office scene")`
+`indigo.insteon.sendSceneFastOn("working in office scene", sendCleanUps=False)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|-----------------------------------------------|----------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer or string | either the scene number or the scene name - we encourage numbers since they won't change and the scene names can. | +| `sendCleanUps` | No | boolean | True if cleanup messages should be sent to each device in the scene after the scene command - this will have the PowerLinc send each device a message to make sure it received the command. You might want to stop cleanup messages from being sent because it involves adding a lot of Insteon traffic and might affect performance (particularly if a group has a lot of responders). But, of course, disabling it may reduce reliability of some modules receiving the scene command so it's usually best to ignore this setting. (default is True) | +| `suppressLogging` | No | boolean | True if entries in the event log should be suppressed (default is False) | +| `updateStatesOnly` | No | boolean | use if you only want Indigo's internal device state representation to be updated - no actual Insteon command will be sent on RF or the power line (default is False) | + +## Send Scene Fast OFF { .ref-head-no-code } + +This command will send an Insteon Scene Fast OFF command using the specified PowerLinc scene. + +**Command Syntax Examples** + + +`indigo.insteon.sendSceneFastOff(11)`
+`indigo.insteon.sendSceneFastOff(11, sendCleanUps=False)`
+`indigo.insteon.sendSceneFastOff(11, suppressLogging=True)`
+`indigo.insteon.sendSceneFastOff(11, updateStatesOnly=True)`
+`indigo.insteon.sendSceneFastOff("working in office scene")`
+`indigo.insteon.sendSceneFastOff("working in office scene", sendCleanUps=False)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|-----------------------------------------------|----------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer or string | either the scene number or the scene name - we encourage numbers since they won't change and the scene names can. | +| `sendCleanUps` | No | boolean | True if cleanup messages should be sent to each device in the scene after the scene command - this will have the PowerLinc send each device a message to make sure it received the command. You might want to stop cleanup messages from being sent because it involves adding a lot of Insteon traffic and might affect performance (particularly if a group has a lot of responders). But, of course, disabling it may reduce reliability of some modules receiving the scene command so it's usually best to ignore this setting. (default is True) | +| `suppressLogging` | No | boolean | True if entries in the event log should be suppressed (default is False) | +| `updateStatesOnly` | No | boolean | use if you only want Indigo's internal device state representation to be updated - no actual Insteon command will be sent on RF or the power line (default is False) | + +## Send Scene Start Change { .ref-head-no-code } + +This command will tell the specified PowerLinc scene to begin increasing/decreasing in value. It will continue to ramp each device in the scene until either the corresponding [Send Scene Stop Change](#send-scene-stop-change) command is called or until the devices are at 100% for increase ramping or 0% for decrease ramping. + +**Command Syntax Examples** + + +`indigo.insteon.sendSceneIncrease(11, increase=True)`
+`indigo.insteon.sendSceneIncrease(11, suppressLogging=True)`
+`indigo.insteon.sendSceneIncrease("working in office scene")` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|----------------------------------------------|----------|-------------------|-------------------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer or string | either the scene number or the scene name - we encourage numbers since they won't change and the scene names can. | +| `increase` | Yes | boolean | True if you want to ramp up, False if you want to ramp down | +| `suppressLogging` | No | boolean | True if entries in the event log should be suppressed (default is False) | + +## Send Scene Stop Change { .ref-head-no-code } + +This command will tell the specified PowerLinc scene to stop any ramping activity started by a [Send Scene Start Change](#send-scene-start-change) command. + +**Command Syntax Examples** + + +`indigo.insteon.sendSceneIncrease(11)`
+`indigo.insteon.sendSceneIncrease(11, suppressLogging=True)`
+`indigo.insteon.sendSceneIncrease("working in office scene")` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|----------------------------------------------|----------|-------------------|-------------------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer or string | either the scene number or the scene name - we encourage numbers since they won't change and the scene names can. | +| `suppressLogging` | No | boolean | True if entries in the event log should be suppressed (default is False) | + +## Send Status Request { .ref-head-no-code } + +This command will send an Insteon Status Request command to the specified address. + +**Command Syntax Examples** + +```python +reply = indigo.insteon.sendStatusRequest("0A.B9.DC") +indigo.server.log("reply success: %d, ack value: %02X" % (reply.cmdSuccess, reply.ackValue)) +``` + +**Parameters** + +| Parameter | Required | Type | Description | +|----------------------------------------------|----------|---------|--------------------------------------------------------------------------| +| direct parameter | Yes | string | the target Insteon address as a hexadecimal string. | +| `waitUntilAck` | No | boolean | true if the caller wants to wait for the result. (default is True) | +| `suppressLogging` | No | boolean | True if entries in the event log should be suppressed (default is False) | + +## Send Raw { .ref-head-no-code } + +This command will send a raw Insteon standard command. + +**Command Syntax Examples** + +```python +reply = indigo.insteon.sendRaw("0A.B9.DC", [0x10, 0x00]) +indigo.server.log("reply success: %d, ack value: %02X" % (reply.cmdSuccess, reply.ackValue)) +``` + +**Parameters** + +| Parameter | Required | Type | Description | +|---------------------------------------------------|----------|---------|--------------------------------------------------------------------------------------------| +| direct parameter | Yes | string | the target Insteon address as a hexadecimal string. | +| `cmdBytes` | Yes | list | a list of 2 integer bytes that represent the command to be sent. | +| `waitUntilAck` | No | boolean | True if the caller wants to wait for the result. (default is True) | +| `waitForStandardReply` | No | boolean | True if the caller wants to wait for a follow-up direct standard reply. (default is False) | +| `waitForExtendedReply` | No | boolean | True if the caller wants to wait for a follow-up direct extended reply. (default is False) | +| `suppressLogging` | No | boolean | True if entries in the event log should be suppressed (default is False) | + +## Send Raw Extended { .ref-head-no-code } + +This command will send a raw Insteon extended command (command payload can be between 2 and 16 bytes). + +**Command Syntax Examples** + +```python +# Get KeypadLinc info (LED states, brightness, etc.) +reply = indigo.insteon.sendRawExtended("11.7B.2E", [0x2E, 0x00], waitForExtendedReply=True) +if reply.cmdSuccess: + indigo.server.log(" backlight brightness: %d" % (reply.replyBytes[10],)) + indigo.server.log("button toggle mode bitmap: 0x%02X" % (reply.replyBytes[11],)) + indigo.server.log(" button states bitmap: 0x%02X" % (reply.replyBytes[12],)) +``` + +```python +# Change the Keypad LED brightness from dim to bright +setKeypadLedBrightness = [ + 0x2E, 0x00, + 0x00, # unused + 0x07, # change LED backlight brightness + 0x11, # brightness between 0x11 and 0x7F + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +] +for brightness in [0x07, 0x44, 0x66, 0x7F]: + setKeypadLedBrightness[4] = brightness + indigo.insteon.sendRawExtended("11.7B.2E", setKeypadLedBrightness) +``` + +**Parameters** + +| Parameter | Required | Type | Description | +|---------------------------------------------------|----------|---------|----------------------------------------------------------------------------------------------------| +| direct parameter | Yes | string | the target Insteon address as a hexadecimal string. | +| `cmdBytes` | Yes | list | a list of 2 to 16 integer bytes that represent the command to be sent. | +| `calcCrc` | No | boolean | automatically calculate 8-bit CRC byte for message (used by i2CS firmware). (default is True) | +| `calc16bitCrc` | No | boolean | automatically calculate 16-bit CRC byte for message (used by specific modules). (default is False) | +| `waitUntilAck` | No | boolean | True if the caller wants to wait for the result. (default is True) | +| `waitForStandardReply` | No | boolean | True if the caller wants to wait for a follow-up direct standard reply. (default is False) | +| `waitForExtendedReply` | No | boolean | True if the caller wants to wait for a follow-up direct extended reply. (default is False) | +| `suppressLogging` | No | boolean | True if entries in the event log should be suppressed (default is False) | + +## Send PowerLinc SET Button Press Message { .ref-head-no-code } + +This will command the PowerLinc to send its SET Button Press Message, which can be useful in some linking scenarios. + +**Command Syntax Examples** + +`reply = indigo.insteon.sendPowerLincSetButtonPress()` + +**Parameters** + +| Parameter | Required | Type | Description | +|----------------------------------------------|----------|---------|--------------------------------------------------------------------------| +| `suppressLogging` | No | boolean | True if entries in the event log should be suppressed (default is False) | + +## Subscribe to Events { .ref-head-no-code } + +Use the lower-level `subscribeToIncoming()` and `subscribeToOutgoing()` methods in the `indigo.insteon` command space to see commands regardless of their effect on device state. + +**Command Syntax Examples** + + +`indigo.insteon.subscribeToIncoming()`
+`indigo.insteon.subscribeToOutgoing()` +
+ +For example, +```python +def startup(self): + self.logger.debug("startup called -- subscribing to all Insteon commands") + indigo.insteon.subscribeToIncoming() + indigo.insteon.subscribeToOutgoing() + + ######################################## + def insteonCommandReceived(self, cmd): + self.logger.debug(f"insteonCommandReceived: \n{str(cmd)}") + + def insteonCommandSent(self, cmd): + self.logger.debug(f"insteonCommandSent: \n{str(cmd)}") +``` diff --git a/reference/canonical/scripting/reference/schedules.md b/reference/canonical/scripting/reference/schedules.md new file mode 100644 index 0000000..55f0717 --- /dev/null +++ b/reference/canonical/scripting/reference/schedules.md @@ -0,0 +1,171 @@ + + +# Schedules + +In the IOM, all schedules are derived from a common Schedule base class. This base contains all the shared components of schedules. + +## Schedule Base Class { .ref-head-no-code } + +All schedules will inherit properties from the Schedule base class. + +Like other high-level objects in Indigo, there are rules for modifying schedules. For Scripters and Plugin Developers: + +1. To duplicate, delete, and send commands to a schedule, use the command namespace as described below +1. To modify an object's definition, get a copy of the schedule, make the necessary changes, then call `mySchedule.replaceSharedPropsOnServer(newPropsDict)` (see below). + +### Class Properties { .ref-head-no-code } + +Under construction + +| Property | Type | Writable | Description | +|-----------------------------------------------|-------------------------------------------------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `absoluteDate` | `datetime.datetime` / None | Yes | The absolute date of the next schedule execution with 00:00:00 as the base time. | +| `absoluteDateTime` | `datetime.datetime` / None | Yes | The absolute date and time of the next schedule execution. | +| `absoluteTime` | `datetime.datetime` / None | Yes | The absolute time of the next schedule execution with 2000-01-01 as the base date. | +| `autoDelete` | boolean | Yes | true if Indigo should automatically delete this schedule after the next execution, otherwise false. | +| `configured` | boolean | Yes | true if the schedule has been fully configured, otherwise false. | +| `dateType` | `indigo.kDateType` / None | | Describes the "type" of date/time options for the schedule. [Absolute / EveryDay / DaysOfWeek / DaysOfMonth] | +| `description` | string | Yes | description of the schedule. | +| `enabled` | boolean | Yes | true if the schedule is enabled, otherwise false (Indigo will not execute the schedule if false). | +| `folderId` | integer | No | unique ID of the folder this schedule is in. | +| `globalProps` | dictionary | No | an `indigo.Dict()` that will contain the props for the schedule. It's generally easier to use the shortcut `sharedProps` below. | +| `id` | integer | No | a unique id of the schedule, assigned on creation by IndigoServer. | +| `name` | string | Yes | the unique name of the schedule - no two schedules can have the same name. | +| `nextExecution` | `datetime.datetime` / None | No | The date and time of the schedule's next execution. | +| `pluginProps` | dictionary | No | pluginProps will return an empty dict because plugins cannot currently create custom schedules. | +| `randomizeBy` | integer | Yes | the number of minutes (plus or minus) Indigo should use to randomize the execution of the schedule. | +| `remoteDisplay` | boolean | Yes | true if remote clients should display the schedule, otherwise false (does not affect the Indigo client UI). | +| `sharedProps` | dictionary | No | an `indigo.Dict()` containing the name/value pairs that are shared by all plugins. This is the property dictionary that you can edit via the Global Properties plugin, and your plugin may manage properties in this dictionary as well to add metadata to devices that your plugin can use for other purposes. Use `sched.replaceSharedPropsOnServer()` to update them (as with pluginProps, you should get copy first, update the copy, then set them back to that copy so you don't accidentally remove some other plugin's props). | +| `sunDelta` | integer | Yes | The number of seconds before (negative) or after (positive) sunrise or sunset when the schedule should be executed (zero if no offset). | +| `suppressLogging` | boolean | Yes | true if Indigo should skip logging the schedule's execution in the event log, otherwise false. | +| `timeType` | `indigo.kTimeType` | Yes | Absolute / Sunrise / Sunset / Countdown depending on the date and time options selected. | +| `upload` | boolean | Yes | true if IndigoServer should attempt to upload this schedule to the interface. | + +### Commands (indigo.schedule.*) { .ref-head-no-code } + +#### Delete { .ref-head-no-code } + +Delete the specified schedule. + +**Command Syntax Examples** + +`indigo.schedule.delete(123)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------|----------|---------|------------------------------------------| +| direct parameter | Yes | integer | id or instance of the schedule to delete | + +#### Duplicate { .ref-head-no-code } + +Duplicate the specified schedule regardless of the type. This method returns a copy of the new schedule. + +**Command Syntax Examples** + +`indigo.schedule.duplicate(123, duplicateName="my duplicate name")` + +**Parameters** + +| Parameter | Required | Type | Description | +|--------------------------------------------|----------|---------|---------------------------------------------| +| direct parameter | Yes | integer | id or instance of the schedule to duplicate | +| `duplicateName` | No | string | name for the newly duplicated schedule | + +#### Enable { .ref-head-no-code } + +Enables or disables the specified schedule. + +**Command Syntax Examples** + +`indigo.schedule.enable(123, value=True, delay=0, duration=0)` + +**Parameters** + +| Parameter | Required | Type | Description | +|---------------------------------------|----------|---------|---------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the schedule to enable/disable | +| `value` | Yes | boolean | set to True to enable the schedule, False to disable the control | +| `delay` | No | integer | the number of seconds to wait before executing the command | +| `duration` | No | integer | the number of seconds to wait before reverting the executed command | + +#### Execute { .ref-head-no-code } + +Execute the specified schedule. + +**Command Syntax Examples** + +`indigo.schedule.execute(123, ignoreConditions=False, schedule_data=None)` + +**Parameters** + +| Parameter | Required | Type | Description | +|-----------------------------------------------|----------|---------|------------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the schedule to execute | +| `ignoreConditions` | No | boolean | True will execute the schedule regardless of the conditions set within the schedule, False (the default) will honor them | +| `schedule_data` | No | object | an `indigo.Dict` to be passed to the schedule before it is executed | + +A note on `schedule_data` - Indigo will automatically add a `source` key to your dictionary to represent where the action execution came from: + +- "server" if it's something generated from the server itself (schedule execution, built-in trigger, etc.) +- "python" if it's something that comes through IPH that doesn't already have a source attached (scripts, plugins) +- "api-http" if it came from the HTTP API and there wasn't already an included "source" +- "api-websocket" if it came from the websocket API and there wasn't already an included "source" + +However, if you include a `source` key in your `schedule_data`, we will not overwrite it, we'll just pass through whatever your value is. + +For example, if you + +```python +my_dict = indigo.Dict() +my_dict["foo"] = "bar" +indigo.schedule.execute(324976872, schedule_data=my_dict) +``` + +The schedule you executed will receive something like this: +```json +{"event-indigo-id": 324976872, "event-type": "Schedule", "foo": "bar", "source": "python", "timestamp": "1970-01-01T09:09:40"} +``` + +#### Get Dependencies { .ref-head-no-code } + +Get the dependencies of the specified schedule. Returns an `indigo.Dict` object that contains the schedule's dependencies. Will return an empty `indigo.Dict` object if the schedule has no dependencies. + +**Command Syntax Examples** + +`indigo.schedule.getDependencies(123)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------|----------|---------|--------------------------------| +| direct parameter | Yes | integer | id or instance of the schedule | + +#### Move to Folder { .ref-head-no-code } + +Move the specified schedule to the designated folder. + +**Command Syntax Examples** + +`indigo.schedule.moveToFolder(123, value=987)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------|----------|---------|------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the schedule | +| `value` | Yes | integer | id or instance of the folder to move the schedule to | + +#### Remove Delayed Actions { .ref-head-no-code } + +Remove any outstanding delayed actions from the specified schedule. + +**Command Syntax Examples** + +`indigo.schedule.removeDelayedActions(123)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------|----------|---------|--------------------------------| +| direct parameter | Yes | integer | id or instance of the schedule | diff --git a/reference/canonical/scripting/reference/server-commands.md b/reference/canonical/scripting/reference/server-commands.md new file mode 100644 index 0000000..9719ae9 --- /dev/null +++ b/reference/canonical/scripting/reference/server-commands.md @@ -0,0 +1,340 @@ + + +# Server Properties and Commands (indigo.server.*) + +!!! abstract "In this guide" + These are properties and commands that aren't associated with any specific object type and are inside the indigo.server.* command namespace. + +## Properties For Connected Indigo Server { .ref-head-no-code } + +| Property | Type | Writable | Description | +|---------------------------------------------|---------|----------|-------------------------------------------------------------------------------------------------------------------------------| +| `address` | string | No | the IP address of the currently connected Indigo Server | +| `apiVersion` | string | No | [API v1.7](https://www.indigodomo.com/indigo/api_release_notes/1.7/): the currently connected Indigo Server plugin API version as a string (ex: "1.7") | +| `connectionGood` | boolean | No | true if the connection to the Indigo Server is currently good | +| `licenseStatus` | string | No | [API v2.5](https://www.indigodomo.com/indigo/api_release_notes/2.5/): returns one of the values specified in the `indigo.kLicenseStatus` enumeration below | +| `portNum` | integer | No | the port number of the currently connected Indigo Server | +| `version` | string | No | the currently connected Indigo Server version string | + +## License Status Enumeration { .ref-head-no-code } + +| `indigo.kLicenseStatus` | | +|----------------------------------------------------|----------------------------------------------------------------------------------| +| Value | Description | +| `ActiveTrial` | the license is a trial | +| `ActiveSubscription` | license has an active Indigo Up-to-Date subscription (access to a reflector) | +| `ExpiredSubscription` | license has an expired Indigo Up-to-Date subscription (no access to a reflector) | +| `Unknown` | license is in an unknown state | + +## Broadcast to Subscribers { .ref-head-no-code } + +This command will broadcast message to other plugins that have subscribed to the specified name message. + +**Command Syntax Examples** + +`indigo.server.broadcastToSubscribers(messageName)` + +## Calculate Sunrise { .ref-head-no-code } + +This command will return a datetime object that represents the sunrise for the specified day (or the next sunrise if no date is passed in). + +**Command Syntax Examples** + + +`indigo.server.calculateSunrise()`
+`indigo.server.calculateSunrise(myDateObject)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|--------------------------------------------|------------------------------------------------------------------------------------------------------------| +| direct parameter | No | `datetime.date` | a `datetime.date` object representing the day to calculate the sunrise time for | + +## Calculate Sunset { .ref-head-no-code } + +This command will return a datetime object that represents the sunset for the specified day (or the next sunset if no date is passed in). + +**Command Syntax Examples** + + +`indigo.server.calculateSunset()`
+`indigo.server.calculateSunset(myDateObject)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|--------------------------------------------|------------------------------------------------------------------------------------------------------------| +| direct parameter | No | `datetime.date` | a `datetime.date` object representing the day to calculate the sunrise time for | + +## Event Log List { .ref-head-no-code } + +This command will return a string that contains the latest log entries. Each line is terminated with a line feed character. + +**Command Syntax Examples** + + +`indigo.server.getEventLogList()`
+`indigo.server.getEventLogList(lineCount=5)`
+`indigo.server.getEventLogList(showTimeStamp=False)`
+`indigo.server.getEventLogList(lineCount=5, showTimeStamp=False)`
+`indigo.server.getEventLogList(returnAsList=True, lineCount=5)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|---------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| returnAsList | No | boolean | if true a list of dicts is returned containing individual log entry attributes; if false a string containing a textual description of the log lines is returned - the default is false | +| lineCount | No | integer | the number of lines to return from the event log starting from newest and going backwards in time - the default is 1500 | +| showTimeStamp | No | boolean | indicate whether every line should have its timestamp prepended to the log entry - the default is true | + +## Get Database File Name { .ref-head-no-code } + +Returns the name of the current database name (without the file extension). It takes no parameters. + +**Command Syntax Examples** + +`name=indigo.server.getDbName()` + +## Get Database File Path { .ref-head-no-code } + +Returns the POSIX path to the current database file (includes the file name with extension). It takes no parameters. + +**Command Syntax Examples** + +`name=indigo.server.getDbFilePath()` + +## Get Deprecated Elements { .ref-head-no-code } + +Returns the server's list of elements that have attributes or properties that are now deprecated. + +**Command Syntax Examples** + +`name=indigo.server. getDeprecatedElems(includeWarnings=[True/False]` + +## Get Install Folder Path { .ref-head-no-code } + +Returns the POSIX path to the current Indigo installation path. Useful if you want to manipulate files (like graphics and scripts) that are in the Indigo installation path. It takes no parameters. + +**Command Syntax Examples** + +`name=indigo.server.getInstallFolderPath()` + +## Get Latitude and Longitude { .ref-head-no-code } + +Returns a list of floating point numbers where the first float is the latitude and the second (and last) is the longitude. It takes no parameters. + +**Command Syntax Examples** + + +`latLong = indigo.server.getLatitudeAndLongitude()`
+`lat = latLong[0]`
+`long = latLong[1]` +
+ +## Get Plugin { .ref-head-no-code } + +Returns a plugin object given the plugin id. + +**Command Syntax Examples** + +`myPlugin=indigo.server.getPlugin("com.company.pluginId")` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|--------|----------------------------------| +| direct parameter | Yes | string | the id of the plugin to retrieve | + +See [scripting plugins](../tutorial.md#scripting-indigo-plugins) for details and examples of using this method. + +## Get Plugin List { .ref-head-no-code } + +**[API v2.4](https://www.indigodomo.com/indigo/api_release_notes/2.4/)**: Returns a list of all enabled plugin object instances. + +**Command Syntax Examples** + +`enabled_plugin_list=indigo.server.getPluginList()` + +## Get Reflector URL { .ref-head-no-code } + +**[API v2.5](https://www.indigodomo.com/indigo/api_release_notes/2.5/)**: Returns a string with the URL to the active reflector. Returns None if there is no reflector or if remote access is disabled. + +**Command Syntax Examples** + + +`indigo.server.getReflectorURL()`
+`>>> "https://myreflector.indigodomo.net/"` +
+ +## Get Serial Ports { .ref-head-no-code } + +Returns a dictionary representing all serial ports on the server machine. The key is the full path specification for the port (for use by PySerial) and the value is just the name of the port (for display purposes). + +**Command Syntax Examples** + + +`indigo.server.getSerialPorts()`
+`indigo.server.getSerialPorts(filter="indigo.ignoreBluetooth")` +
+ +```python +ports = indigo.server.getSerialPorts(filter="indigo.ignoreBluetooth") +# iterate through the full paths +for posixPath in ports: + print(posixPath) +# iterate through just the port name itself +for uiName in ports.itervalues(): + print(uiName) +# iterate through both +for posixPath, uiName in ports.iteritems(): + print(posixPath) + print(uiName) +``` + +**Parameters** + +| Parameter | Required | Type | Description | +|-------------------------------------|----------|--------|-----------------------------------------------------------------------------------------------------------------------------------| +| `filter` | No | string | currently there’s only one valid filter: "indigo.ignoreBluetooth" which will remove the "Bluetooth-PDA-Sync" option from the list | + +## Get Time { .ref-head-no-code } + +Returns a datetime object representing the server's current time. + +**Command Syntax Examples** + +`indigo.server.getTime()` + +**Parameters**
+None + +## Get Web Server URL { .ref-head-no-code } + +Returns a URL string that best represents the URL to the active Indigo Web Server. This is the order of which URL will be returned: + +1. Reflector (`https://reflector.indigodomo.net`) if a reflector is configured. +1. Bonjour name (`http://MacName.local:PORT`) if it can be determined. +1. Localhost (`http://localhost:PORT`) if all else fails. + +Note, there is no trailing slash. `PORT` is the port number of the server (for example, 8176). + +**Command Syntax Examples** + +`indigo.server.getWebServerURL()` + +**Parameters**
+None + +## Log { .ref-head-no-code } + +This tells IndigoServer to write a log entry with the specified text. The type in the log will be the name of the plugin. The examples below that refer to the logging package assume that you've done this somewhere before: `import logging` + +**Command Syntax Examples** + + +`indigo.server.log("Info Text to log")`
+`indigo.server.log("Info Text to log", type="myType")`
+`indigo.server.log("Warning Text to log", level=logging.WARNING)`
+`indigo.server.log("Error Text to log1", level=logging.ERROR)`
+`indigo.server.log("Error Text to log2", isError=True)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | string | this is the text that’s written to the log | +| `type` | No | string | a string representing the type - if it’s not included and is run from a Server Plugin, the name of the plugin will automatically be used | +| `level` | No | integer | **[API v2.4](https://www.indigodomo.com/indigo/api_release_notes/2.4/)**: the python logging level which determines both the type shown and the text color used. (ex: using `level=logging.WARNING` will show orange text) | +| `isError` | No | boolean | if `True`, it will show up in red in the event log - default is `False` - if no `type` is included, the name of the plugin will automatically be used with " Error" appended | + +## Remove All Delayed Actions { .ref-head-no-code } + +This command will remove all delayed actions currently scheduled. It doesn’t take any parameters. + +**Command Syntax Examples** + +`indigo.server.removeAllDelayedActions()` + +## Restart Plugin { .ref-head-no-code } + +This command will tell the server to restart our plugin process. The message is printed to the event log, and if isError is true then it's logged as an error. This command can only be called from a plugin, and it refers to the plugin itself (not other plugins). + +**Command Syntax Examples** + +`indigo.server.restartPlugin("Restarting now for some reason", isError=True)` + +## Save Plugin Preferences { .ref-head-no-code } + +The Indigo server will save changes to plugin preferences automatically, and this command will cause the server to save plugin preferences immediately. It doesn’t take any parameters. + +**Command Syntax Examples** + +`indigo.server.savePluginPrefs()` + +## Send Email { .ref-head-no-code } + +This tells IndigoServer to send an email using the SMTP settings configured in the preferences "Email" tab. + +**Command Syntax Examples** + + +`indigo.server.sendEmailTo("my.address@example.com")`
+`indigo.server.sendEmailTo("my.address@example.com",`
    `subject="Subject of email", body="Body of email")` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|--------|-------------------------------------------------| +| direct parameter | Yes | string | a semicolon separated string of email addresses | +| `subject` | No | string | the subject of the email | +| `body` | No | string | the body of the email | + +## Speak { .ref-head-no-code } + +Speak a text string using the built-in speech synthesizer. + +**Command Syntax Examples** + +`indigo.server.speak("text to speak", waitUntilDone=True)` + +**Parameters** + +| Parameter | Required | Type | Description | +|--------------------------------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | string | the string to speak | +| `waitUntilDone` | No | boolean | should the method call block until speaking is complete or should it just return immediately (queue up the speech) | + +## Stop Plugin { .ref-head-no-code } + +Tell the server to shut down our plugin process. Plugin will remain enabled but be in a stopped state. This command can only be called from a plugin, and it refers to the plugin itself (not all plugins). + +**Command Syntax Examples** + +`indigo.server.stopPlugin("Stopping now for some reason", isError=True)` + +## Subscribe To Log Broadcasts { .ref-head-no-code } + +Subscribes to all server event log broadcasts. + +**Command Syntax Examples** + +`indigo.server.subscribeToLogBroadcasts()` + +**Parameters**
+None + +## Wait Until Idle { .ref-head-no-code } + +Wait (block) until server has completed event processing and command sending. + +**Command Syntax Examples** + +`indigo.server.waitUntilIdle()` + +**Parameters**
+None diff --git a/reference/canonical/scripting/reference/triggers.md b/reference/canonical/scripting/reference/triggers.md new file mode 100644 index 0000000..5f814f4 --- /dev/null +++ b/reference/canonical/scripting/reference/triggers.md @@ -0,0 +1,808 @@ + + +# Triggers + +In the IOM, all triggers are derived from a common Trigger base class. This base contains all the shared components of triggers. + +## Trigger Base Class { #trigger .ref-head-no-code } + +All triggers will inherit properties from the Trigger base class - including plugin defined events. Note: plugin defined events will always return False for the upload property. + +Like other high-level objects in Indigo, there are rules for modifying triggers. For Scripters and Plugin Developers: + +1. To create, duplicate, delete, and send commands to a trigger, use the command namespace as described below +1. To modify an object's definition get a copy of the trigger, make the necessary changes, then call `myTrigger.replaceOnServer(newPropsDict)` + +For Plugin Developers: + +1. To update a plugin's props on a trigger, call `myTrigger.replacePluginPropsOnServer(newPropsDict)` rather than try to update them on the local trigger + +Unlike [Devices](devices/index.md), you can't call `create()` in the trigger base class command namespace (`indigo.trigger.*`). Rather, each subclass has its own `create()` method that takes the appropriate arguments for that trigger type. + +### Firing Plugin Defined Triggers { .ref-head-no-code } + +If you are a plugin developer and your plugin defines events, The process for executing your plugin's events is this: + +1. User creates a trigger of type plugin and selects one of your plugin events - configures it (if necessary) and saves. +1. Indigo Server sends the new trigger object to your plugin via the various Trigger Specific Methods in `plugin.py`. The easiest one (parallel to the various device events) is `triggerStartProcessing(self, trigger)`. This method is also called when the server first starts up your plugin - it will pass all triggers defined to your plugin, one at a time, through this method. +1. Your trigger catches the trigger passed in that method and stores it so that it can watch for the conditions that define the event. +1. When the conditions are met that would cause that trigger to fire (plugin implementation specific), you tell the server to [execute the trigger](#execute) using the `indigo.trigger.execute(triggerRef)` method. Note that the execute method will allow you to optionally bypass any conditions - you should NOT do this unless you're providing some kind of override/bypass. Users will clearly want conditions to be taken into account under normal circumstances (that's the default behavior). + +You will also need to implement `triggerStopProcessing(self,trigger)` so that you can remove disabled/deleted triggers from your watch list. So, the Server isn't really involved at all with the trigger firing (except to execute the trigger when you tell it to) - it only notifies your plugin of events it's supposed to be watching for and your plugin does the rest. + +### Class Properties { #properties .ref-head-no-code } + +| Property | Type | Writable | Description | +|----------------------------------------------|------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `description` | string | Yes | description of the trigger | +| `enabled` | boolean | Yes | true if this trigger is enabled | +| `folderId` | integer | No | unique ID of the folder this trigger is in | +| `globalProps` | dictionary | No | an `indigo.Dict()` representing all name/value pairs associated with this trigger - each plugin will have its own dictionary (`globalProps[pluginId]`) - see [About Plugin Properties](#about-plugin-properties) below for details | +| `id` | integer | No | a unique id of the trigger, assigned on creation by IndigoServer | +| `name` | string | Yes | the unique name of the trigger - no two triggers can have the same name | +| `pluginProps` | dictionary | No | an `indigo.Dict()` representing the name/value pairs defined by your plugin for the trigger - plugin developers should publish this information if you want other plugins/scripts to create triggers of this type - see [About Plugin Properties](#about-plugin-properties) below for details | +| `sharedProps` | dictionary | No | **[API v2.3](https://www.indigodomo.com/indigo/api_release_notes/2.3/)** : an `indigo.Dict()` representing the name/value pairs that are shared by all plugins. This is the property dictionary that you can edit via the Global Properties plugin, and your plugin may manage properties in this dictionary as well to add metadata to devices that your plugin can use for other purposes. Use `trigger.replaceSharedPropsOnServer()` to update them (as with pluginProps, you should get copy first, update the copy, then set them back to that copy to avoid accidentally removing some other plugin's props). | +| `suppressLogging` | boolean | Yes | true if execution of this trigger will not be logged into the event log | +| `upload` | boolean | Yes | true if IndigoServer should attempt to upload this trigger to the interface - will always be false for plugin triggers | + +### About Plugin Properties { .ref-head-no-code } + +Triggers have properties - some are class properties, defined by the class itself. One of the biggest requests we've gotten in the past is some way to add arbitrary properties to an object - so that you could store your own data with the object in the database. And with plugin defined triggers, we needed a place to store the properties that you need to operate the trigger. That's what the `pluginProps` and `globalProps` represent - the additional properties that are not defined by the class. `globalProps` is a dictionary of every additional property defined for the trigger - each plugin has its own dictionary of props in here which are readable by anyone. `pluginProps` is a shortcut to get to your plugin's props and are only writable by your plugin. + +We mentioned before that triggers were read-only, and that's true, and that you'd need to use commands in a different command name space. That's ***mostly*** true. Here's another exception to that rule: to change a trigger's pluginProps (it must be "owned" by your plugin - that is, the pluginId must be set to your id), you use a method that's in the trigger's class: replacePluginPropsOnServer(). Here's an example: + +```python +trigger=indigo.triggers[123] +localPropsCopy = trigger.pluginProps +localPropsCopy["pollInterval"] = 10 +trigger.replacePluginPropsOnServer(localPropsCopy) +``` + +You would use this technique if you wanted to just change some of the properties that are already defined. Because this method replaces **all** the properties for your plugin in the trigger, you can just set them all in one call: + +```python +trigger=indigo.triggers[123] +trigger.replacePluginPropsOnServer({"prop1":10,"prop2":True}) +``` + +Note, though, that if you have a `` defined for the event, those properties are also stored here - so in order to make sure your trigger works correctly you must include those properties as well. If you need to update several properties in your props dict, you can use the `update()` method: + +```python +trigger=indigo.triggers[123] +localPropsCopy = trigger.pluginProps +localPropsCopy.update({"prop1":10,"prop2":True}) +trigger.replacePluginPropsOnServer(localPropsCopy) +``` + +The `update()` method will change the properties specified, and add the property if it doesn't exist. Now, you might be wondering - why do the extra `localPropsCopy = trigger.pluginProps` rather than just modify the props in place: + +```python +trigger=indigo.triggers[123] +trigger.pluginProps.update({"prop1":10,"prop2":True}) +trigger.replacePluginPropsOnServer(trigger.pluginProps) +``` + +Because the trigger object is read-only - when you reference `trigger.pluginProps`, it returns a copy rather than returning a reference to the read-only object. So, in effect, you'd be modifying a copy. But, because you aren't saving a reference to that copy, it goes away since the next time you reference `trigger.pluginProps` another copy is made. + +If you need to just dump all the properties for a trigger, you can just: + +```python +trigger=indigo.triggers[123] +trigger.replacePluginPropsOnServer(None) +``` + +That will completely remove your properties from the trigger. + +### Commands (indigo.trigger.*) { .ref-head-no-code } + +The commands in this section are common to all triggers regardless of type. + +#### Delete { .ref-head-no-code } + +Delete the specified trigger regardless of its type. + +**Command Syntax Examples** + +`indigo.trigger.delete(123)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------|----------|---------|-----------------------------------------| +| direct parameter | Yes | integer | id or instance of the trigger to delete | + +#### Duplicate { .ref-head-no-code } + +Duplicate the specified trigger regardless of the type. This method returns a copy of the new trigger. + +**Command Syntax Examples** + +`indigo.trigger.duplicate(123, duplicateName="New Name")` + +**Parameters** + +| Parameter | Required | Type | Description | +|--------------------------------------------|----------|---------|--------------------------------------------| +| direct parameter | Yes | integer | id or instance of the trigger to duplicate | +| `duplicateName` | No | string | name for the newly duplicated trigger | + +#### Enable/Disable Trigger { #enable .ref-head-no-code } + +Disables or enables the trigger, optionally delaying for some period of time and optionally toggling back after the given period. + +**Command Syntax Examples** + + +`indigo.trigger.enable(123,value=False)`
+`indigo.trigger.enable(123,value=True)`
+`indigo.trigger.enable(123, value=True, duration=360, delay=60)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|---------------------------------------|----------|---------|-----------------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the trigger | +| `value` | Yes | boolean | True to enable, False to disable | +| `delay` | No | integer | number of seconds to delay before disabling or enabling the trigger | +| `duration` | No | integer | number of seconds before the trigger is switched back to its original state | + +#### Execute Trigger { #execute .ref-head-no-code } + +Tell the IndigoServer to execute the actions associated with the trigger. If your plugin implements events, this is the method you'd call when the conditions for your specific event are met. If you're calling it this way, make sure you include the `ignoreConditions=False` parameter (or don't include the parameter since False is the default) so that any conditions associated with the trigger (by the user in the UI) are evaluated. + +This method can also be called by scripters to immediately execute the actions associated with the trigger. + +**Command Syntax Examples** + +`indigo.trigger.execute(123, ignoreConditions=False)` + +**Parameters** + +| Parameter | Required | Type | Description | +|-----------------------------------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the trigger | +| `ignoreConditions` | No | boolean | will ignore any conditions associated with the trigger if True and will evaluate conditions if False (the default) | +| `trigger_data` | No | object | an `indigo.Dict` to be passed to the trigger before it is executed | + +A note on `trigger_data` - Indigo will automatically add a `source` key to your dictionary to represent where the action execution came from: + +- "server" if it's something generated from the server itself (schedule execution, built-in trigger, etc.) +- "python" if it's something that comes through IPH that doesn't already have a source attached (scripts, plugins) +- "api-http" if it came from the HTTP API and there wasn't already an included "source" +- "api-websocket" if it came from the websocket API and there wasn't already an included "source" + +However, if you include a `source` key in your `trigger_data`, we will not overwrite it, we'll just pass through whatever your value is. + +For example, if you + +```python +my_dict = indigo.Dict() +my_dict["foo"] = "bar" +indigo.trigger.execute(8974982742, trigger_data=my_dict) +``` + +The schedule you executed will receive something like this: +```json +{"event-indigo-id": 8974982742, "event-type": "Trigger", "foo": "bar", "source": "python", "timestamp": "1970-01-01T09:09:40"} +``` + +#### Get Dependencies { .ref-head-no-code } + +Return an indigo.Dict with all the dependencies on this trigger. + +**Command Syntax Examples** + +`indigo.trigger.getDependencies(123)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------|----------|---------|------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the trigger to get the dependencies for. | + +The dictionary will look something like this: + +```python +>>> print(indigo.trigger.getDependencies(91776575)) +Data : (dict) + actionGroups : (list) + controlPages : (list) + devices : (list) + schedules : (list) + Data : (dict) + ID : 552463741 (integer) + Name : Between condition test (string) + Data : (dict) + ID : 296710860 (integer) + Name : Greater than condition test (string) + triggers : (list) + variables : (list) +``` + +So, the dictionary will have 6 top-level keys: "actionGroups", "controlPages", "devices", "schedules", "triggers", and "variables". Each one of those keys will return a list object. Inside that list object will be multiple dicts, one for each dependency (or an empty list if there are none). Each dependency dictionary has two keys: "ID" which is the unique id and "Name" which is the name of the object. + +#### Move To Folder { .ref-head-no-code } + +Use this command to move the trigger to a different folder. You can get a list of folder id’s by using indigo.triggers.folders, which will return a dictionary. The key to the dictionary is the ID, the value is the folder name. + +**Command Syntax Examples** + +`indigo.trigger.moveToFolder(123, value=987)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------|----------|---------|-----------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the trigger | +| `value` | Yes | integer | id or instance of the folder to move the trigger to | + +#### Remove Delayed Actions { .ref-head-no-code } + +This command will remove delayed actions for the specified trigger. + +**Command Syntax Examples** + +`indigo.trigger.removeDelayedActions(123)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------|----------|---------|-------------------------------| +| direct parameter | No | integer | id or instance of the trigger | + +### Examples { .ref-head-no-code } + +While several of a trigger's properties are read-only (the trigger ID for example), other properties can be changed programmatically. These are noted as "writeable" above. For example, + +```python +# Change a trigger's name: +trigger = indigo.triggers[123] +trigger.name = "My new name." +trigger.replaceOnServer() + +# Change a trigger's description: +trigger = indigo.triggers[123] +trigger.description = "My new description." +trigger.replaceOnServer() +``` + +## DeviceStateChangeTrigger { .ref-head-no-code } + +The DeviceStateChangeTrigger class represents an event that is described by various changes to a device’s state. Built-in device types have fixed states, but plugins may define custom devices that define their own device states. Note: Controller device types can’t be used in device state change events since they have no state. + +### Class Properties { .ref-head-no-code } + +| Property | Type | Writable | Description | +|-------------------------------------------------|------------------------------------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------| +| `deviceId` | integer | Yes | the unique device id | +| `stateChangeType` | [kStateChange](#state-change-type-enumeration) | Yes | the type of state change | +| `stateSelector` | [kStateSelector](#state-selector-enumeration) | Yes | can use either a string or one of the enumerations listed in the state selector enumeration | +| `stateSelectorIndex` | integer | Yes | a 0-based integer value to specify which of multiple options to monitor - see the About State Selector section below for more information | +| `stateValue` | string | Yes | the value the current state should be compared against - will be converted to the right type by the server | + +These events are rather complex for a variety of reasons, but the primary is that device states can be of multiple types which require a different set of controls. Indigo solves the problem by showing the right control options given the type. If your plugin is defining its own states, you’ll have to specify each state and it’s associated type so that Indigo will know what kind of controls to display to the user. + +For instance, a temperature reported from a thermostat may be a floating point number. Possible events that you could trigger off of would be greater than, less than, equal, not equal, or has any change. However, becomes true or becomes false doesn’t make any sense. Conversely, an I/O device with binary inputs would really only need becomes true, becomes false, and has any change. The other options don’t make sense. To help you define the appropriate state changes, we’ve created the State Change Enumeration that lists all possible state changes. + +However, that’s only one side of the issue. The other important concept is the state selector. In the example above, I mentioned a thermostat temperature and a binary input. I implied a single value for each, but in reality thermostats may in fact have multiple temperature sensors and I/O devices usually have multiple inputs and outputs. So, how do we specify which one? + +#### State Change Type Enumeration { #state-change-type-enumeration .ref-head-no-code } + +| indigo.kStateChange | | | +|-------------------------------------------------|---------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Value | Description | Valid for kStateSelector’s | +| `BecomesEqual` | `stateSelector` becomes equal to `stateValue` | `ActiveZone`
`AnalogInput`
`BrightnessLevel`
`HumidityInput`
`SensorInput`
`SetpointCool`
`SetpointHeat`
`TemperatureInput` | +| `BecomesFalse` | `stateSelector` becomes `False` | `BinaryInput`
`BinaryOutput`
`HvacCoolerIsOn`
`HvacFanIsOn`
`HvacFanModeIsAlwaysOn`
`HvacFanModeIsAuto`
`HvacHeaterIsOn`
`HvacOperationModeIsAuto`
`HvacOperationModeIsCool`
`HvacOperationModeIsHeat`
`HvacOperationModeIsOff`
`HvacOperationModeIsProgramAuto`
`HvacOperationModeIsProgramCool`
`HvacOperationModeIsProgramHeat`
`OnOffState`
`Zone` | +| `BecomesGreaterThan` | `stateSelector` becomes greater than `stateValue` | `AnalogInput`
`BrightnessLevel`
`HumidityInput`
`SensorInput`
`SetpointCool`
`SetpointHeat`
`TemperatureInput` | +| `BecomesLessThan` | `stateSelector` becomes less than `stateValue` | `AnalogInput`
`BrightnessLevel`
`HumidityInput`
`SensorInput`
`SetpointCool`
`SetpointHeat`
`TemperatureInput` | +| `BecomesNotEqual` | `stateSelector` becomes not equal to `stateValue` | `ActiveZone`
`AnalogInput`
`BrightnessLevel`
`HumidityInput`
`SensorInput`
`SetpointCool`
`SetpointHeat`
`TemperatureInput` | +| `BecomesTrue` | `stateSelector` becomes True | `BinaryInput`
`BinaryOutput`
`HvacCoolerIsOn`
`HvacFanIsOn`
`HvacFanModeIsAlwaysOn`
`HvacFanModeIsAuto`
`HvacHeaterIsOn`
`HvacOperationModeIsAuto`
`HvacOperationModeIsCool`
`HvacOperationModeIsHeat`
`HvacOperationModeIsOff`
`HvacOperationModeIsProgramAuto`
`HvacOperationModeIsProgramCool`
`HvacOperationModeIsProgramHeat`
`OnOffState`
`Zone` | +| `Changes` | `stateSelector` has any change | `ActiveZone`
`AnalogInput`
`AnalogInputsAll`
`BinaryInput`
`BinaryInputsAll`
`BinaryOutput`
`BinaryOutputsAll`
`BrightnessLevel`
`HumidityInput`
`HumidityInputsAll`
`HvacCoolerIsOn`
`HvacFanIsOn`
`HvacFanMode`
`HvacFanModeIsAlwaysOn`
`HvacFanModeIsAuto`
`HvacHeaterIsOn`
`HvacOperationModeIsAuto`
`HvacOperationModeIsCool`
`HvacOperationModeIsHeat`
`HvacOperationModeIsOff`
`HvacOperationModeIsProgramAuto`
`HvacOperationModeIsProgramCool`
`HvacOperationModeIsProgramHeat`
`OnOffState`
`SensorInput`
`SensorInputsAll`
`SetpointCool`
`SetpointHeat`
`TemperatureInput`
`TemperatureInputsAll`
`Zone` | + +#### State Selector Enumeration { #state-selector-enumeration .ref-head-no-code } + +| indigo.kStateSelector | | +|-------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Value | Description | +| `ActiveZone` | monitor the sprinkler’s activeZone to become =, !=, or any change | +| `AnalogInput` | monitor (one of) the analog input(s) available on the device for =, !=, <, >, or any change - stateSelectorIndex is required to be in the range of available inputs | +| `AnalogInputsAll` | monitors all of the analog inputs available on the device for any change | +| `BinaryInput` | monitor (one of) the binary input(s) to become true, false, or any change - stateSelectorIndex is required to be in the range of available inputs | +| `BinaryInputsAll` | monitors all of the binary inputs available on the device for any change | +| `BinaryOutput` | monitor (one of) the binary output(s) to become true, false, or any change - stateSelectorIndex is required to be in the range of available outputs | +| `BinaryOutputsAll` | +| `BrightnessLevel` | monitor the brightness level of a device for =, !=, <, >, and any change | +| `HumidityInput` | monitor (one of) the humidity sensor(s) available on the device for =, !=, <, >, and any change - stateSelectorIndex is required to be in the range of available inputs | +| `HumidityInputsAll` | monitors all of the humidity sensors available on the device for any change | +| `HvacCoolerIsOn` | monitor the thermostat for any time the air conditioning turns on, off, or has any change (coolIsOn is the current compressor state) | +| `HvacFanIsOn` | monitor the thermostat for any time the fan turns on, off, or has any change (fanIsOn is the current fan state) | +| `HvacFanMode` | monitor the fanMode of the thermostat for any change | +| `HvacFanModeIsAlwaysOn` | monitor the fanMode of the thermostat for a change to/from kFanMode.AlwaysOn | +| `HvacFanModeIsAuto` | monitor the fanMode of the thermostat for a change to/from kFanMode.AutoOn | +| `HvacHeaterIsOn` | monitor the thermostat for any time the heater turns on, off, or has any change (heatIsOn is the current heater state) | +| `HvacOperationMode` | monitor the hvacMode of the thermostat for any change | +| `HvacOperationModeIsAuto` | monitor the hvacMode of the thermostat for a change to/from kHvacMode.HeatCoolOn | +| `HvacOperationModeIsCool` | monitor the hvacMode of the thermostat for a change to/from kHvacMode.CoolOn | +| `HvacOperationModeIsHeat` | monitor the hvacMode of the thermostat for a change to/from kHvacMode.HeatOn | +| `HvacOperationModeIsOff` | monitor the hvacMode of the thermostat for a change to/from kHvacMode.Off | +| `HvacOperationModeIsProgramAuto` | monitor the hvacMode of the thermostat for a change to/from kHvacMode.ProgramAuto | +| `HvacOperationModeIsProgramCool` | monitor the hvacMode of the thermostat for a change to/from kHvacMode.ProgramCool | +| `HvacOperationModeIsProgramHeat` | monitor the hvacMode of the thermostat for a change to/from kHvacMode.ProgramHeat | +| `KeypadButtonLed` | | +| `OnOffState` | monitor the device for a change to/from on/off | +| `SensorInput` | monitor (one of) the sensor input(s) available on the device for =, !=, <, >, or any change - stateSelectorIndex is required to be in the range of available inputs | +| `SensorInputsAll` | monitors all of the sensor inputs available on the device for any change | +| `SetpointCool` | monitor the cool setpoint of the thermostat for =, !=, <, >, or any change | +| `SetpointHeat` | monitor the heat setpoint of the thermostat for =, !=, <, >, or any change | +| `TemperatureInput` | monitor (one of) the temperature sensor(s) available on the device for =, !=, <, >, and any change - stateSelectorIndex is required to be in the range of available inputs | +| `TemperatureInputsAll` | monitors all of the temperature sensors available on the device for any change | +| `Zone` | monitor (one of) the binary output(s) to become true, false, or any change - stateSelectorIndex is required to be in the range of available outputs | + +### Commands (indigo.devStateChange.*) { .ref-head-no-code } + +#### Create { .ref-head-no-code } + +Create a DeviceStateChange trigger. This method returns a **copy** of the newly created trigger. + +**Command Syntax Examples** + +`indigo.devStateChange.create(name="Trigger Name Here", description="Description Here", folder=1234)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|------------------------------------------------------------------------| +| `description` | No | string | the description of the trigger | +| `name` | Yes | string | the name of the trigger | +| `folder` | No | integer | id or instance of the folder in which to put the newly created trigger | + +### Examples { #devicestatechangetrigger-examples .ref-head-no-code } + +So, let’s look at a concrete example - a thermostat. A thermostat has several states which can be monitored, a couple of which may be lists. Specifically, a thermostat may have multiple temperature sensors and multiple humidity sensors. + +Here is an example of creating a device state changed trigger that will execute when the first temperature of thermostat id 738 (named "Main Thermostat") goes over 80 degrees in Python: + + + +```python +theTrigger=indigo.devStateChange.create(name="Temp exceeds 80 degrees") +theTrigger.deviceId = 738 +theTrigger.stateChangeType = indigo.kStateChange.BecomesGreaterThan +theTrigger.stateSelector = indigo.kStateSelector.TemperatureInput +theTrigger.stateSelectorIndex = 1 +theTrigger.stateValue = 80 +theTrigger.replaceOnServer() +``` + +## EmailReceivedTrigger { .ref-head-no-code } + +The EmailReceivedTrigger object represents the email scanning feature in Indigo. You can match on any email received or based on the match fields - subject and/or from email address. + +### Class Properties { #emailreceivedtrigger-class-properties .ref-head-no-code } + +| Property | Writable | Type | Description | +|-------------------------------------------|-------------------------------------------|------|-----------------------------------------------------------------------------------------| +| `emailFilter` | [kEmailFilter](#email-filter-enumeration) | Yes | the type of email filter based on the `kEmailFilter` enumeration below | +| `emailFrom` | string | Yes | if `emailFilter` is `MatchEmailFields`, the value to match against the sender’s address | +| `emailSubject` | string | Yes | if `emailFilter` is `MatchEmailFields`, the value to match against the subject line | + +#### Email Filter Enumeration { #email-filter-enumeration .ref-head-no-code } + +| indigo.kEmailFilter | | +|-----------------------------------------------|--------------------------------------| +| Value | Description | +| `AnyEmail` | when any email is received | +| `MatchEmailFields` | when email subject/from fields match | + +### Commands (indigo.emailRcvd.*) { .ref-head-no-code } + +#### Create { #commands-indigoemailrcvd-create .ref-head-no-code } + +Create a EmailReceivedTrigger. This method returns a **copy** of the newly created trigger. + +**Command Syntax Examples** + +`indigo.emailRcvd.create(name="Trigger Name Here", description="Description Here", folder=1234)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|------------------------------------------------------------------------| +| `description` | No | string | the description of the trigger | +| `name` | Yes | string | the name of the trigger | +| `folder` | No | integer | id or instance of the folder in which to put the newly created trigger | + +**Examples** + + + +```python +theTrigger=indigo.emailRcvd.create(name="Emails from some@body.com") + +theTrigger.emailFilter = indigo.kEmailFilter.MatchEmailFields +theTrigger.emailFrom = "some@body.com" +theTrigger.replaceOnServer() +``` + +## InsteonCommandReceivedTrigger { .ref-head-no-code } + +The InsteonCommandReceivedTrigger object will match incoming Insteon command events. + +### Class Properties { #insteoncommandreceivedtrigger-class-properties .ref-head-no-code } + +| Property | Type | Writable | Description | +|------------------------------------------------|--------------------------------------------------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `deviceId` | integer | Yes | the unique device id - only used if `commandSourceType` is `DeviceId` | +| `command` | [kInsteonCmd](#insteon-command-enumeration) | Yes | the command to watch for | +| `commandSourceType` | [kDeviceSourceType](#trigger-device-source-type-enumeration) | Yes | the source type - for Insteon only `DeviceId` and `AnyDevice` apply - and `deviceId` above will only be used if set to `DeviceId` | +| `buttonOrGroup` | integer | Yes | the button or group number from which the command is received - see [About Button or Group Numbers](#about-button-or-group-numbers-in-insteon-events) below for details | + +#### About Button or Group Numbers in Insteon Events { #about-button-or-group-numbers-in-insteon-events .ref-head-no-code } +Various Insteon devices support features that are implemented via extra group identifiers. For instance, a KeypadLinc will broadcast each of the commands below from each button on it, and each KeypadLinc may have either 6 or 8 buttons depending on configuration. Other devices, like the Motion Sensor and the TriggerLinc will broadcast out group numbers based on other events (battery low, motion detected, dusk/dawn sensor, etc.). + +#### Insteon Command Enumeration { #insteon-command-enumeration .ref-head-no-code } + +| indigo.kInsteonCmd | | +|--------------------------------------------|--------------------------------------------------------------------| +| Value | Description | +| `AllBrighten` | when an all brighten command begins | +| `AllDim` | when an all dim command begins | +| `AllInstantOff` | when an all instant (fast) off is received | +| `AllInstantOn` | when an all instant (fast) on is received | +| `AllOff` | when an all off is received | +| `AllOn` | when an all on is received | +| `AnyCommand` | when any command is received | +| `Brighten` | when a brighten command begins | +| `Dim` | when a dim command begins | +| `InstantOff` | when an Instant (Fast) off is received in response to a double-tap | +| `InstantOn` | when an Instant (Fast) on is received in response to a double-tap | +| `Off` | when an off command is received | +| `On` | when an on command is received | +| `StatusChanged` | when a status change broadcast is received | + +#### Trigger Device Source Type Enumeration { #trigger-device-source-type-enumeration .ref-head-no-code } + +| indigo.kDeviceSourceType | | +|-----------------------------------------|------------------------------------------------------------------------------| +| Value | Description | +| `NoDevice` | when the source uses no device or address (only used for X10 RF A/V remotes) | +| `Device` | when the source is an existing device, the ID is specified | +| `Address` | when the source is a raw X10 address | +| `AnyAddress` | when the source is any X10 address or Insteon device | + +### Commands (indigo.insteonCmdRcvd.*) { .ref-head-no-code } + +#### Create { #commands-indigoinsteoncmdrcvd-create .ref-head-no-code } + +Create a InsteonCommandReceivedTrigger. This method returns a **copy** of the newly created trigger. + +**Command Syntax Examples** + +```python +indigo.insteonCmdRcvd.create(name="Trigger Name Here", + description="Description Here", + folder=1234) +``` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|------------------------------------------------------------------------| +| `description` | No | string | the description of the trigger | +| `name` | Yes | string | the name of the trigger | +| `folder` | No | integer | id or instance of the folder in which to put the newly created trigger | + +**Examples** + + + +```python +theTrigger= indigo.insteonCmdRcvd.create(name="KeypadLinc button 1 Any Change") +theTrigger.command = indigo.kInsteonCmd.AnyCommand +theTrigger.commandSourceType = indigo.kDeviceSourceType.Device +theTrigger.deviceId = 43829874 +theTrigger.buttonOrGroup = 1 +theTrigger.replaceOnServer() +``` + +## InterfaceFailureTrigger { .ref-head-no-code } + +The InterfaceFailureTrigger object represents the trigger that’s executed when an interface fails for some reason. + +### Class Properties { #interfacefailuretrigger-class-properties .ref-head-no-code } + +The InterfaceFailed trigger has no additional properties. + +### Commands (indigo.interfaceFail.*) { .ref-head-no-code } + +#### Create { #commands-indigointerfacefail-create .ref-head-no-code } + +Create a InterfaceFailureTrigger. This method returns a **copy** of the newly created trigger. + +**Command Syntax Examples** + +`indigo.interfaceFail.create(name="Trigger Name Here", description="Description Here", folder=1234)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|------------------------------------------------------------------------| +| `description` | No | string | the description of the trigger | +| `name` | Yes | string | the name of the trigger | +| `folder` | No | integer | id or instance of the folder in which to put the newly created trigger | + +**Examples** + +```python +theTrigger= indigo.interfaceFail.create(name="Any Interface Failed") +``` + +## InterfaceInitializedTrigger { .ref-head-no-code } + +The InterfaceInitializedTrigger object represents the trigger that’s executed when an interface initializes successfully. + +### Class Properties { #interfaceinitializedtrigger-class-properties .ref-head-no-code } + +InterfaceInitializedTrigger has no additional properties. + +### Commands (indigo.interfaceInit.*) { .ref-head-no-code } + +#### Create { #commands-indigointerfaceinit-create .ref-head-no-code } + +Create a InterfaceInitializedTrigger. This method returns a **copy** of the newly created trigger. + +**Command Syntax Examples** + +`indigo.interfaceInit.create(name="Trigger Name Here", description="Description Here", folder=1234)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|------------------------------------------------------------------------| +| `description` | No | string | the description of the trigger | +| `name` | Yes | string | the name of the trigger | +| `folder` | No | integer | id or instance of the folder in which to put the newly created trigger | + +**Examples** + +```python +theTrigger= indigo.interfaceInit.create(name="Any Interface Initialized") +``` + +## PluginEventTrigger { .ref-head-no-code } + +A plugin event is defined by a plugin. + +### Class Properties { #plugineventtrigger-class-properties .ref-head-no-code } + +| Property | Type | Description | +|-------------------------------------------|--------|-------------------------------------------------------------------------------------------------| +| `pluginId` | string | the unique ID of the plugin, specified in the Info.plist for the plugin (or it’s documentation) | +| `pluginTypeId` | string | the id specified in the Events.xml (or it’s documentation) | + +### Commands (indigo.pluginEvent.*) { .ref-head-no-code } + +#### Create { #commands-indigopluginevent-create .ref-head-no-code } + +Create a trigger. You can create triggers using this method of any type except for events that are defined by your plugin (that class, PluginEventTrigger, has its own create() method). This method returns a **copy** of the newly created trigger. Once a trigger of this type is created, the properties can change (via the [pluginProps in the Trigger base class](#properties)), but nothing else can be changed. + +**Command Syntax Examples** + +`indigo.pluginEvent.create(name="Trigger Name Here", description="Description Here", folder=1234), pluginId="com.mycompany.myplugin", pluginTypeId="myEvent", props={"propA":"value","propB":"value"}` + +**Parameters** + +| Parameter | Required | Type | Description | +|-------------------------------------------|----------|------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `description` | No | string | the description of the trigger | +| `name` | Yes | string | the name of the trigger | +| `folder` | No | integer | id or instance of the folder in which to put the newly created trigger | +| `pluginId` | No | string | the plugin id of the plugin that owns the device you're creating - if it's not present, it defaults to your plugin's id | +| `pluginTypeId` | Yes | string | this is the id of the as specified in the Events.xml or in the documentation for the plugin | +| `props` | No | dictionary | this is the properties for the trigger - they will be inserted in to the pluginId's property space as supplied above. If you are creating a trigger of a type defined in a different plugin, it's that plugin's id and properties. | + +**Examples** + +See the Command Syntax above for an example. See [Firing Plugin Defined Triggers](#firing-plugin-defined-triggers) above for details on how your plugin should watch and fire triggers based on events defined in your Events.xml. + +## PowerFailureTrigger { .ref-head-no-code } + +The PowerFailureTrigger object represents a trigger that’s executed when an interface loses power (if applicable). Note - not all interfaces can detect a power failure - usually only those interfaces that are plugged directly into an electrical outlet. + +### Class Properties { #powerfailuretrigger-class-properties .ref-head-no-code } + +PowerFailureTrigger has no additional properties. + +### Commands (indigo.powerFailure.*) { .ref-head-no-code } + +#### Create { #commands-indigopowerfailure-create .ref-head-no-code } + +Create a `PowerFailureTrigger`. This method returns a **copy** of the newly created trigger. + +**Command Syntax Examples** + +`indigo.powerFailure.create(name="Trigger Name Here", description="Description Here", folder=1234)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|------------------------------------------------------------------------| +| `description` | No | string | the description of the trigger | +| `name` | Yes | string | the name of the trigger | +| `folder` | No | integer | id or instance of the folder in which to put the newly created trigger | + +**Examples** + +```python +theTrigger= indigo.powerFailure.create(name="Any Interface Detected Power Failure") +``` + +## ServerStartupTrigger { .ref-head-no-code } + +The ServerStartupTrigger class represents a trigger that’s executed when the IndigoServer process starts up. This is a special event in that it adds no additional parameters beyond what it inherits from [Trigger](#trigger). + +### Commands (indigo.serverStartup.*) { .ref-head-no-code } + +#### Create { #commands-indigoserverstartup-create .ref-head-no-code } + +Create a ServerStartupTrigger trigger. This method returns a **copy** of the newly created trigger. + +**Command Syntax Examples** + +`indigo.serverStartup.create(name="Trigger Name Here", description="Description Here", folder=1234)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|------------------------------------------------------------------------| +| `description` | No | string | the description of the trigger | +| `name` | Yes | string | the name of the trigger | +| `folder` | No | integer | id or instance of the folder in which to put the newly created trigger | + +**Examples** + +```python +theTrigger= indigo.serverStartup.create(name="Startup") +``` + +## X10CommandReceivedTrigger { .ref-head-no-code } + +The X10CommandReceivedTrigger object will match incoming X10 command events. + +### Class Properties { #x10commandreceivedtrigger-class-properties .ref-head-no-code } + +| Property | Type | Description | +|------------------------------------------------|--------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------| +| `address` | string | if commandSourceType = Address, the full X10 address to listen for or just the house code if command is All* or AnyCommand | +| `avButton` | [kX10AvButton](#x10-a-v-button-enumeration) | if command = AvButtonPressed, the A/V button to monitor | +| `deviceId` | integer | if commandSourceType = Device, the unique device id | +| `command` | [kX10Cmd](#x10-command-enumeration) | the X10 command to watch for | +| `commandSourceType` | [kDeviceSourceType](#trigger-device-source-type-enumeration) | the type of source specified for this trigger | + +#### X10 Command Enumeration { #x10-command-enumeration .ref-head-no-code } + +| indigo.kX10Cmd | | +|------------------------------------------------|-----------------------------------------------| +| Value | Description | +| `AllOff` | when an all off is received | +| `AllLightsOff` | when an all lights off is received | +| `AllLightsOn` | when an all lights on is received | +| `AvButtonPressed` | when an A/V button press is received | +| `AnyCommand` | when any command is received | +| `Brighten` | when a brighten command begins | +| `Dim` | when a dim command begins | +| `ExtendedData` | when an extended data X10 command is received | +| `Off` | when an off command is received | +| `On` | when an on command is received | +| `PresetDim` | when a preset dim command is received | +| `StatusOffResponse` | when a status off response is received | +| `StatusOnResponse` | when a status on response is received | + +#### X10 A/V Button Enumeration { #x10-a-v-button-enumeration .ref-head-no-code } + +| indigo.kX10AvButton | | +|------------------------------------------|--------------| +| Value | Value | +| `0` | `Left` | +| `1` | `Menu` | +| `2` | `Mute` | +| `3` | `Pause` | +| `4` | `PC` | +| `5` | `Play` | +| `6` | `Power` | +| `7` | `Recall` | +| `8` | `Record` | +| `9` | `Return` | +| `AB` | `Rewind` | +| `ChannelDown` | `Right` | +| `ChannelUp` | `Stop` | +| `Display` | `Title` | +| `Down` | `Up` | +| `Enter` | `VolumeDown` | +| `Exit` | `VolumeUp` | +| `Forward` | + +### Commands (indigo.x10CmdRcvd.*) { .ref-head-no-code } + +#### Create { #commands-indigox10cmdrcvd-create .ref-head-no-code } + +Create an X10CommandReceivedTrigger. This method returns a **copy** of the newly created trigger. + +**Command Syntax Examples** + +`indigo.x10CmdRcvd.create(name="Trigger Name Here", description="Description Here", folder=1234)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|------------------------------------------------------------------------| +| `description` | No | string | the description of the trigger | +| `name` | Yes | string | the name of the trigger | +| `folder` | No | integer | id or instance of the folder in which to put the newly created trigger | + +**Examples** + +```python +myTrigger=indigo.x10CmdRcvd.create(name="Received any command from F7") +myTrigger.command = indigo.kX10Cmd.AnyCommand +myTrigger.commandSourceType = indigo.kDeviceSourceType.Address +myTrigger.address = "F7" +myTrigger.replaceOnServer() +``` + +## VariableValueChangeTrigger { .ref-head-no-code } + +The VariableValueChangeTrigger object will match variable changes. + +### Class Properties { #variablevaluechangetrigger-class-properties .ref-head-no-code } + +| Property | Type | Description | +|-------------------------------------------------|-------------------------------------------------|------------------------------------------------------------------------------------------| +| `variableChangeType` | [kVarChange](#variable-change-type-enumeration) | the type of variable change to monitor | +| `variableId` | integer | the unique variable id | +| `variableValue` | string | the value to compare against the variable’s value if `variableChangeType` is =, !=, >, < | + +#### Variable Change Type Enumeration { #variable-change-type-enumeration .ref-head-no-code } + +| indigo.kVarChange | | +|-------------------------------------------------|-------------------------------------| +| Value | Description | +| `BecomesEqual` | variable value becomes equal to | +| `BecomesFalse` | variable value becomes false | +| `BecomesGreaterThan` | variable value becomes greater than | +| `BecomesLessThan` | variable value becomes less than | +| `BecomesNotEqual` | variable value becomes not equal to | +| `BecomesTrue` | variable value becomes true | +| `Changes` | variable value has any change | + +### Commands (indigo.varValueChange.*) { .ref-head-no-code } + +#### Create { #commands-indigovarvaluechange-create .ref-head-no-code } + +Create an VariableChangeEvent. This method returns a **copy** of the newly created trigger. + +**Command Syntax Examples** + +`indigo.varValueChange.create(name="Trigger Name Here", description="Description Here", folder=1234)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------------|----------|---------|------------------------------------------------------------------------| +| `description` | No | string | the description of the trigger | +| `name` | Yes | string | the name of the trigger | +| `folder` | No | integer | id or instance of the folder in which to put the newly created trigger | + +**Examples** + + +Commands (indigo.devStateChange.*) +```python +theTrigger=indigo.varValueChange.create(name="Var Changed to True") + +theTrigger.variableChangeType = indigo.kVarChange.BecomesEqual +theTrigger.variableId = 9283749872 +theTrigger.variableValue = "True" +theTrigger.replaceOnServer() +``` diff --git a/reference/canonical/scripting/reference/utils.md b/reference/canonical/scripting/reference/utils.md new file mode 100644 index 0000000..d7d0ae2 --- /dev/null +++ b/reference/canonical/scripting/reference/utils.md @@ -0,0 +1,213 @@ + + +# Utility Classes & Functions + +The `indigo.utils` module collects several things that aren't directly tied to a specific +Indigo object but are helpful when writing scripts and building plugins. Everything below is +reached through the `indigo.utils.` prefix from any Indigo Python script or plugin (e.g. +`indigo.utils.ValidationError`). + +## Classes { #classes } + +| Name | Base | Description | +| --- | --- | --- | +| `IndigoJSONEncoder` | `json.JSONEncoder` | A JSON encoder that converts Python `date`/`datetime` objects (and `NaN`) so they can be serialized. Very useful when encoding a device dictionary into JSON: `json.dumps(dict(my_device), cls=indigo.utils.IndigoJSONEncoder)`. The original name `JSONDateEncoder` is still available as an alias. | +| `ValidationError` | `Exception` | An exception for reporting validation problems. It can carry a single summary message or a whole dictionary of field-specific errors. See [ValidationError](#validationerror) below. | + +### IndigoJSONEncoder { #indigojsonencoder } + +By default the Python `json` module can't serialize `datetime` objects. Pass this encoder as the +`cls` argument to a JSON dump call and any `date`/`datetime` encountered during encoding is +converted to its ISO string. Indigo constants (e.g. `indigo.kFanMode.Auto`) are encoded as their +full string representation so they can be reconstituted later. + +```python +import json +my_device = indigo.devices[123456] +print(json.dumps(dict(my_device), indent=4, cls=indigo.utils.IndigoJSONEncoder)) +``` + +### ValidationError { #validationerror } + +`ValidationError` is primarily used when validating fields for commands, messages, or +[Config UIs](../../plugin-dev/reference/xml/configui/validation.md), but it can carry any kind of +validation result. The simplest use is to raise it with a string and let the caller `str()` it. +For more involved cases it holds a dictionary of field names (or keys), each mapped to a single +error string or a list of error strings, so the caller can process individual errors — for +example to mark the offending fields when validating a Config UI. + +**Constructor** + +`indigo.utils.ValidationError(message, error_state_str=None, error_dict=None)` + +| Parameter | Required | Type | Description | +| --- | --- | --- | --- | +| `message` | Yes | string | A general message summarizing the entire validation. Useful for logging. | +| `error_state_str` | No | string | If the validation represents a device state issue, this string is used as the device's error state in the various UIs. Pass an empty string to clear any existing error. | +| `error_dict` | No | dict | A dictionary of field names mapped to error message(s) — each value may be a single string or a list of strings. | + +**Attributes** + +| Attribute | Type | Description | +| --- | --- | --- | +| `error_message` | string | The general summary message passed to the constructor. | +| `error_state_str` | string or None | The optional device error-state string. | +| `error_dict` | dict | The dictionary of field/key → error message(s). | + +**Methods** + +| Method | Description | +| --- | --- | +| `add_error(key, description)` | Add an error for `key` (e.g. a field name). `description` can be a single string or a list of strings. If the key already has an error, the new message is appended (the value becomes a list). | +| `remove_error(key)` | Remove and return all error message(s) for `key`. Note that this removes the entire key, including any multiple messages. | +| `raise_if_errors()` | Raise this exception if there is anything to report — i.e. if `error_dict` is non-empty or `error_state_str` is not `None`. Otherwise does nothing. | + +A `ValidationError` is also iterable: calling `dict(my_validation_error)` yields the contents of +`error_dict`. And `str(my_validation_error)` produces a human-readable summary that includes the +general message followed by the formatted error details. + +**Example** + +```python +# Accumulate field errors, then raise only if something failed. +errors = indigo.utils.ValidationError("Sensor configuration is invalid") +if not values.get("address"): + errors.add_error("address", "You must enter an address.") +if not indigo.utils.is_int(values.get("pollInterval", "")): + errors.add_error("pollInterval", "Poll interval must be a whole number.") + +errors.raise_if_errors() # raises only if at least one error was added +``` + +For a worked example of turning a `ValidationError` into the error dictionary a Config UI +validation method returns, see +[Validation Methods](../../plugin-dev/reference/xml/configui/validation.md#using-validationerror). + +## Functions + +### Return Static File { #return-static-file } + +Accepts a file path and an optional content type and returns the correctly structured +`indigo.Dict` that the Indigo Web Server (IWS) interprets as a directive to stream the specified +file back to the caller. This avoids returning a large amount of data through the plugin IPC +mechanism. + +**Command Syntax Examples** + +```python +indigo.utils.return_static_file("some/relative/path/to/file.txt") +indigo.utils.return_static_file("/some/path/to/file.json", status=400, path_is_relative=False, content_type="application/json") +``` + +**Parameters** + +| Parameter | Required | Type | Description | +| --- | --- | --- | --- | +| `file_path` | Yes | string or list | A string path to the file, or a list of path parts ending in the file name. | +| `status` | No | int | The HTTP status code to return. Defaults to `200`. | +| `path_is_relative` | No | boolean | `True` if the path is relative to the Indigo install folder, `False` for a complete file path. Defaults to `True`. | +| `content_type` | No | string | The MIME type for the `Content-Type` header. If omitted, an appropriate type is chosen from the file extension. | + +The returned `indigo.Dict` can be passed directly back to IWS from an HTTP processing call in your +plugin. It looks something like this: + +```json +{ + "status": 404, + "headers": { + "Content-Type": "text/html" + }, + "file_path": "/Library/Application Support/Perceptive Automation/Indigo {{ version }}/Plugins/Example HTTP Responder.indigoPlugin/Contents/Resources/static/html/static_404.html" +} +``` + +IWS uses this to create the HTTP reply that streams the file back to the caller. The function +raises a `FileNotFoundError` if the file doesn't exist, or a `TypeError` if `file_path` isn't a +list of path parts or a string. + +### Validate Email Address { #validate-email-address } + +Accepts an email address string and returns `True` if it is constructed correctly, `False` +otherwise. Note: it only checks that the address is *formatted* correctly — it does not verify +that the address exists on the destination system. + +**Command Syntax Examples** + +```python +indigo.utils.validate_email_address("valid_email@someserver.com") # True +indigo.utils.validate_email_address("invalid address") # False +``` + +**Parameters** + +| Parameter | Required | Type | Description | +| --- | --- | --- | --- | +| `address` | Yes | string | A string that represents an email address. | + +### Boolean Functions { #boolean-functions } + +Two functions help convert and use strings that represent boolean values but aren't literally +`True`/`False`. Both use the following map (and its reverse): + +```text +BOOL_MAP_TRUE = { + "y": "n", + "yes": "no", + "t": "f", + "true": "false", + "on": "off", + "1": "0", + "open": "closed", + "locked": "unlocked", +} +``` + +`str_to_bool(val)` converts the supplied string to a boolean. It returns `True` for true values +(`y`, `yes`, `t`, `true`, `on`, `1`, `open`, `locked`), `False` for the corresponding false +values, and raises a `ValueError` if the input can't be converted. A `bool` passed in is returned +unchanged. + +```python +indigo.utils.str_to_bool("closed") # False +indigo.utils.str_to_bool("on") # True +``` + +`reverse_bool_str_value(val)` returns the string representing the opposite boolean value using the +map above. It raises a `ValueError` if the input can't be found. + +```python +indigo.utils.reverse_bool_str_value("closed") # "open" +``` + +| Parameter | Required | Type | Description | +| --- | --- | --- | --- | +| `val` | Yes | string | A string that represents a boolean value as mapped above. | + +### Is Integer { #is-int } + +Accepts any value and returns `True` if it is an integer or can be cast to one, `False` otherwise. +Handy for validating user-entered Config UI fields, which arrive as strings. + +```python +indigo.utils.is_int("42") # True +indigo.utils.is_int("3.5") # False +indigo.utils.is_int("abc") # False +``` + +| Parameter | Required | Type | Description | +| --- | --- | --- | --- | +| `value` | Yes | any | Any Python object to test. | + +## Converting indigo.Dict and indigo.List { #conversion-methods } + +The `indigo.utils` module also attaches convenience methods to the `indigo.Dict` and `indigo.List` +classes that recursively convert them to their native Python counterparts: + +```python +python_dict = my_indigo_dict.to_dict() # recursively convert an indigo.Dict to a python dict +python_list = my_indigo_list.to_list() # recursively convert an indigo.List to a python list +``` + +These are the same conversions used when you call `dict()` on an Indigo object. For the full +picture of how a device is represented as a dictionary, see +[Dictionary Representation](devices/dictionary.md). diff --git a/reference/canonical/scripting/reference/variables.md b/reference/canonical/scripting/reference/variables.md new file mode 100644 index 0000000..b4a33fe --- /dev/null +++ b/reference/canonical/scripting/reference/variables.md @@ -0,0 +1,214 @@ + + +# Variables + +The variable class represents an Indigo variable. + +## Class Properties { .ref-head-no-code } + +| Property | Type | Description | +|--------------------------------------------|------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | integer | the unique id of the variable, assigned on creation by IndigoServer | +| `folderId` | integer | the unique id of the folder this variable is in (0 if it's not in a folder) - use `moveToFolder()` method to change | +| `name` | string | the name of the variable - no two variables can have the same name and the name cannot contain whitespace | +| `readOnly` | string | is the variable read only - currently only the `isDaylight` variable is read only | +| `remoteDisplay` | boolean | should this variable be displayed in remote clients (IWS, Indigo Touch, etc) | +| `sharedProps` | dictionary | **[API v2.3](https://www.indigodomo.com/indigo/api_release_notes/2.3/)** : an `indigo.Dict()` representing the name/value pairs that are shared by all plugins. This is the property dictionary that you can edit via the Global Properties plugin, and your plugin may manage properties in this dictionary as well to add metadata to devices that your plugin can use for other purposes. Use `var.replaceSharedPropsOnServer()` to update them (as with pluginProps, you should get copy first, update the copy, then set them back to that copy so you don't accidentally remove some other plugin's props). | +| `value` | string | the Unicode string value of the variable | + +## Class Method { .ref-head-no-code } + +The Variable class has a special method, `getValue(TYPE, default=VALUE)`, that you can call which will retrieve the variable value as the specified Python class. There are a couple of advantages to using this method. First, it won't throw an exception but will always return a value. Second, the Indigo server will do the conversion in the exact same way that it does type conversions when using variables in triggers and conditions. You can also optionally specify a default value if the value can't be successfully converted into the specified type. Here's a list of the valid types you can specify and what the default is if not specified: + +| Type Literal | Type Returned | Default | +|--------------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| bool | boolean | `True` will be returned if the value is one of these: "true", "on", "yes", and "1". `False` will be returned if the value is one of these: "false", "off", "no", and "0". If no default value is specified and the value can't be successfully converted, the method will return False. | +| int | integer | An integer object will be returned if the value is an integer number or a float/decimal. In the latter case, the number will be rounded to the nearest integer. If no default value is specified and the value can't be converted, the method will return 0 | +| float | float | A float object will be returned if the value is a float/decimal or an integer. If no default value is specified and the value can't be converted, the method will return 0.0 | + +Remember that accessing the `value` property of a variable object (`var.value`) will return a Unicode string object so you don't need a conversion for that. + +**Class Method Examples** + +```python +# Get a variable +var = indigo.variables[123456] + +# Getting the Unicode string value (no conversion call necessary, just access the property) +unicodeValue = var.value + +# Getting the boolean value +intValue = var.getValue(bool) # False if it can't be converted +intValue = var.getValue(int, default=True) # True if it can't be converted + +# Getting the integer value +intValue = var.getValue(int) # 0 if it can't be converted +intValue = var.getValue(int, default=10) # 10 if it can't be converted + +# Getting the float value +intValue = var.getValue(float) # 0 if it can't be converted +intValue = var.getValue(float, default=98.6) # 98.6 if it can't be converted + +# Getting the name of the variable +varName = var.name + +# Getting the id of the variable +varId = var.id +``` + +## Commands (indigo.variable.*) { .ref-head-no-code } + +### Create { .ref-head-no-code } + +Create a variable. This method returns a **copy** of the newly created variable. + +**Command Syntax Examples** + +`indigo.variable.create("VariableName", value="Var Value", folder=843920)` + +**Parameters** + +| Parameter | Required | Type | Description | +|-------------------------------------|----------|---------|---------------------------------------------------------------------------------------------------| +| direct parameter | Yes | string | the name of the variable | +| `value` | No | string | the value of the variable | +| `folder` | No | integer | id or instance of the folder in which to put the newly created device - defaults to 0 (no folder) | + +### Delete { .ref-head-no-code } + +Delete the specified variable. + +**Command Syntax Examples** + +`indigo.variable.delete(123)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------|----------|---------|------------------------------------------| +| direct parameter | Yes | integer | id or instance of the variable to delete | + +### Duplicate { .ref-head-no-code } + +Duplicate the specified variable. This method returns a copy of the new variable. + +**Command Syntax Examples** + +`indigo.variable.duplicate(123, duplicateName="NewName")` + +**Parameters** + +| Parameter | Required | Type | Description | +|--------------------------------------------|----------|---------|---------------------------------------------| +| direct parameter | Yes | integer | id or instance of the variable to duplicate | +| `duplicateName` | No | string | name for the newly duplicated variable | + +### Get Dependencies { .ref-head-no-code } + +Return an indigo.Dict with all the dependencies on this variable. + +**Command Syntax Examples** + +`indigo.variable.getDependencies(123)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------|----------|---------|-------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the variable to get the dependencies for. | + +The dictionary will look something like this: + +```python +>>> print(indigo.variable.getDependencies(91776575)) +Data : (dict) + actionGroups : (list) + controlPages : (list) + devices : (list) + schedules : (list) + Data : (dict) + ID : 552463741 (integer) + Name : Between condition test (string) + Data : (dict) + ID : 296710860 (integer) + Name : Greater than condition test (string) + triggers : (list) + variables : (list) +``` + +So, the dictionary will have 6 top-level keys: "actionGroups", "controlPages", "devices", "schedules", "triggers", and "variables". Each one of those keys will return a list object. Inside that list object will be multiple dicts, one for each dependency (or an empty list if there are none). Each dependency dictionary has two keys: "ID" which is the unique id and "Name" which is the name of the object. + +### Move To Folder { .ref-head-no-code } + +Use this command to move the variable to a different folder. + +**Command Syntax Examples** + +`indigo.variable.moveToFolder(123, value=987)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------|----------|---------|------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the variable | +| `value` | Yes | integer | id or instance of the folder to move the variable to | + +### Set Remote Display { .ref-head-no-code } + +Use this command to set the remote display flag for the folder. + +**Command Syntax Examples** + +`indigo.variable.displayInRemoteUI(123, value=True)` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------|----------|---------|----------------------------------------------------------------------------| +| direct parameter | Yes | integer | id or instance of the variable | +| `value` | Yes | boolean | True to display the variable on remote user interfaces or False to hide it | + +### Update Value { .ref-head-no-code } + +Use this command to set the value of a variable without having to get a local copy first. + +**Command Syntax Examples** + +`indigo.variable.updateValue(123, value="New Value")` + +**Parameters** + +| Parameter | Required | Type | Description | +|------------------------------------|----------|---------|--------------------------------| +| direct parameter | Yes | integer | id or instance of the variable | +| `value` | Yes | string | the new value for the variable | + +**Examples** + +```python +# Create a new variable +newVar = indigo.variable.create("fooName", "fooMonster") + +# Updating value via command space function: +indigo.variable.updateValue(newVar, "asleep789") +newVar.refreshFromServer() # refresh needed to update local's .value + +# changing name property +newVar.name = "goodName" +newVar.replaceOnServer() + +newVar.name = "bad name" # should throw because of space character + +# changing name and values properties: +newVar.name = "goodName2" +newVar.value = "searchingForWaldo" +newVar.replaceOnServer() + +indigo.variable.delete(newVar) + +# Getting a variable using its ID +someVar = indigo.variables[123] + +# Getting a variable using its name +someVar = indigo.variables["MyVarName"] +``` diff --git a/reference/canonical/scripting/reference/x10-commands.md b/reference/canonical/scripting/reference/x10-commands.md new file mode 100644 index 0000000..fa19b12 --- /dev/null +++ b/reference/canonical/scripting/reference/x10-commands.md @@ -0,0 +1,174 @@ + + +# X10 Commands (indigo.X10.*) + +Commands that are specific to X10 devices. + +## Send Address { .ref-head-no-code } + +This command will send an X10 address to the interface with NO function code. + +**Command Syntax Examples** + +`indigo.x10.sendAddress("A1")` + +**Parameters** + +| Parameter | Required | Type | Description | +|----------------------------------------------|----------|---------|------------------------------------------------------------------------------------------| +| direct parameter | Yes | string | X10 address | +| `suppressLogging` | No | boolean | a boolean indicating if entries in the event log should be suppressed (default is False) | + +## Send Brighten { .ref-head-no-code } + +This will send the Brighten command to an X10 address. + +**Command Syntax Examples** + +`indigo.x10.sendBrighten("A1", delta=15)` + +**Parameters** + +| Parameter | Required | Type | Description | +|-----------------------------------------------|----------|---------|-------------------------------------------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | string | X10 address | +| `delta` | Yes | integer | the amount to brighten by relative to the current brightness - valid values from 1 to 100 | +| `suppressLogging` | No | boolean | a boolean indicating if entries in the event log should be suppressed (default is False) | +| `updateStatesOnly` | No | boolean | use if you only want Indigo's internal device state representation to be updated - no actual X10 commands will be sent (default is False) | + +## Send Dim { .ref-head-no-code } + +This will send the Dim command to an X10 address. + +**Command Syntax Examples** + +`indigo.x10.sendDim("A1", delta=15)` + +**Parameters** + +| Parameter | Required | Type | Description | +|-----------------------------------------------|----------|---------|-------------------------------------------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | string | X10 address | +| `delta` | Yes | integer | the amount to dim by relative to the current brightness - valid values from 1 to 100 | +| `suppressLogging` | No | boolean | a boolean indicating if entries in the event log should be suppressed (default is False) | +| `updateStatesOnly` | No | boolean | use if you only want Indigo's internal device state representation to be updated - no actual X10 commands will be sent (default is False) | + +## Send Extended { .ref-head-no-code } + +This will send an Extended command to an X10 address. + +**Command Syntax Examples** + +`indigo.x10.sendExtended("A1", data=10, command=128)` + +**Parameters** + +| Parameter | Required | Type | Description | +|-----------------------------------------------|----------|---------|-------------------------------------------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | string | X10 address | +| `command` | Yes | integer | the command to send | +| `data` | Yes | integer | the data to send | +| `suppressLogging` | No | boolean | a boolean indicating if entries in the event log should be suppressed (default is False) | +| `updateStatesOnly` | No | boolean | use if you only want Indigo's internal device state representation to be updated - no actual X10 commands will be sent (default is False) | + +## Send Hail Request { .ref-head-no-code } + +This will send a Hail Request command to the interface. + +**Command Syntax Examples** + +`indigo.x10.sendHailRequest("A1")` + +**Parameters** + +| Parameter | Required | Type | Description | +|----------------------------------------------|----------|---------|------------------------------------------------------------------------------------------| +| direct parameter | Yes | string | X10 address | +| `suppressLogging` | No | boolean | a boolean indicating if entries in the event log should be suppressed (default is False) | + +## Send Hail Reply { .ref-head-no-code } + +This will send a Hail Reply command to the interface. + +**Command Syntax Examples** + +`indigo.x10.sendHailReply("A1")` + +**Parameters** + +| Parameter | Required | Type | Description | +|----------------------------------------------|----------|---------|------------------------------------------------------------------------------------------| +| direct parameter | Yes | string | X10 address | +| `suppressLogging` | No | boolean | a boolean indicating if entries in the event log should be suppressed (default is False) | + +## Send On { .ref-head-no-code } + +This will send an ON command to an X10 address. + +**Command Syntax Examples** + + +`indigo.x10.sendOn("A1")`
+`indigo.x10.sendOn("A1", suppressLogging=True)`
+`indigo.x10.sendOn("A1", updateStatesOnly=True)`
+`indigo.x10.sendOn("A1", suppressLogging=True, updateStatesOnly=True)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|-----------------------------------------------|----------|---------|-------------------------------------------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | string | X10 address | +| `suppressLogging` | No | boolean | a boolean indicating if entries in the event log should be suppressed (default is False) | +| `updateStatesOnly` | No | boolean | use if you only want Indigo's internal device state representation to be updated - no actual X10 commands will be sent (default is False) | + +## Send Off { .ref-head-no-code } + +This will send an OFF command to an X10 address. + +**Command Syntax Examples** + + +`indigo.x10.sendOff("A1")`
+`indigo.x10.sendOff("A1", suppressLogging=True)`
+`indigo.x10.sendOff("A1", updateStatesOnly=True)`
+`indigo.x10.sendOff("A1", suppressLogging=True, updateStatesOnly=True)` +
+ +**Parameters** + +| Parameter | Required | Type | Description | +|-----------------------------------------------|----------|---------|-------------------------------------------------------------------------------------------------------------------------------------------| +| direct parameter | Yes | string | X10 address | +| `suppressLogging` | No | boolean | a boolean indicating if entries in the event log should be suppressed (default is False) | +| `updateStatesOnly` | No | boolean | use if you only want Indigo's internal device state representation to be updated - no actual X10 commands will be sent (default is False) | + +## Send Status Response On { .ref-head-no-code } + +This will send a Status Response On command to the interface. + +**Command Syntax Examples** + +`indigo.x10.sendStatusResponseOn("A1")` + +**Parameters** + +| Parameter | Required | Type | Description | +|----------------------------------------------|----------|---------|------------------------------------------------------------------------------------------| +| direct parameter | Yes | string | X10 address | +| `suppressLogging` | No | boolean | a boolean indicating if entries in the event log should be suppressed (default is False) | + +## Send Status Response Off { .ref-head-no-code } + +This will send a Status Response Off command to the interface. + +**Command Syntax Examples** + +`indigo.x10.sendStatusResponseOff("A1")` + +**Parameters** + +| Parameter | Required | Type | Description | +|----------------------------------------------|----------|---------|------------------------------------------------------------------------------------------| +| direct parameter | Yes | string | X10 address | +| `suppressLogging` | No | boolean | a boolean indicating if entries in the event log should be suppressed (default is False) | diff --git a/reference/canonical/scripting/tutorial.md b/reference/canonical/scripting/tutorial.md new file mode 100644 index 0000000..435f032 --- /dev/null +++ b/reference/canonical/scripting/tutorial.md @@ -0,0 +1,593 @@ + + +# Plugin Scripting Tutorial + +!!! abstract "In this guide" + A hands-on introduction to scripting Indigo from the interactive Python shell: turning devices on/off, reading and writing variables, iterating the Indigo object model, and executing action groups. Requires Indigo running locally; basic Python familiarity is helpful. Open the shell via `Plugins->Open Scripting Shell`. + +## Talking to the Indigo Server +The [Indigo Plugin Host (IPH)](../plugin-dev/guide.md#indigo-plugin-host) provides a python scripting and plugin environment, which makes controlling the Indigo Server easy via the high-level python language (the official python website has [great python tutorials](http://docs.python.org/tutorial/)). Every plugin automatically runs inside its own Indigo Plugin Host process, but you can also start the IPH in an interactive mode to directly communicate with the Indigo Server. It is a great way to experiment with Indigo's python environment, which is available to both python scripts run by Indigo and the plugin architecture. Want the ability to just put some Python scripts onto a menu somewhere for easy access? [There's a simple way to do it.](http://www.indigodomo.com/pluginstore/150/) + +To launch the Terminal utility application (inside *`/Applications/Utilities/`*) in an Indigo shell mode choose the `Plugins->Open Scripting Shell` menu item. + +Note: you'll want to read through the [introduction section of the Indigo Object Model](iom-concepts.md#introduction), which gives you some fundamental information that you need to successfully script Indigo. + +## Example Code Snippets +Below are some sample scripts you can copy/paste directly into the IPH window (opened when following the directions above). Keep in mind, these are only examples, so be sure to read the full [Indigo Object Model (IOM) Reference](iom-concepts.md). + +Note: For simplicity, some of the samples below specify objects based on name (*`"office desk lamp"`*). However, the preferred lookup mechanism is to use the **object's ID** which can be retrieved by **control-clicking on the object's name** in Indigo's Main Window. By using the ID, you ensure the object will be found even if its name is changed. + +### Device Examples +#### Turn on the device "office desk lamp": +```python +indigo.device.turnOn(1234567890) # Preferred, where the number is the ID of "office desk lamp" + +# OR the less preferred way because it will break if you rename the device +indigo.device.turnOn("office desk lamp") +``` + +#### Duplicate the device "office desk lamp": +```python +indigo.device.duplicate(1234567890, duplicateName="office desk lamp2") # where the number is the ID of "office desk lamp" +``` + +#### In 4 seconds turn on the device "office desk lamp" for 2 seconds: +```python +indigo.device.turnOn(1234567890, duration=2, delay=4) # where the number is the ID of "office desk lamp" +``` + +#### Turn off all devices: +```python +indigo.device.allOff() +``` + +#### Count the number of device modules: +```python +indigo.devices.len() +``` + +#### Count the number of dimmable device modules: +```python +indigo.devices.len(filter="indigo.dimmer") +``` + +#### Count the number of devices defined by all of our plugin types: +```python +indigo.devices.len(filter="self") +``` + +#### Count the number of irBlaster type devices defined by our plugin: +```python +indigo.devices.len(filter="self.irBlaster") +``` + +#### Get the on state of a device if it has the onState property (uses Python's hasattr() introspection method): +```python +lamp = indigo.devices[1234567890] # where the number is the ID of "office desk lamp" +if hasattr(lamp, 'onState'): + isOn = lamp.onState +``` + +#### Get the class of a device (uses Python's **class** property): +```python +lamp = indigo.devices[1234567890] # where the number is the ID of "office desk lamp" +if lamp.__class__ == indigo.DimmerDevice: + theBrightness = lamp.brightness +``` + +#### Turn on a light only if it's been off for longer than 1 minute: +```python +from datetime import datetime +lamp = indigo.devices[91776575] # ID of "Hallway light" +timeDelta = datetime.now() - lamp.lastChanged +if not lamp.onState and timeDelta.seconds > 60: + indigo.device.turnOn(91776575) # ID of "Hallway light" +``` + +#### Access a custom device state +```python +# get the device +dev = indigo.devices[23989834] # Some custom device +# access the state through the states property, use the key +# that's displayed in the Custom States tile on the main window +# when you have a custom device selected +print(dev.states["someDeviceStateKey"]) + +# show all the device states: +print(dev.states) +``` + +### Variable Examples +#### Create a new Indigo variable named fooMonster, change its value multiple times, and delete it: +```python +newVar = indigo.variable.create("fooMonster", "default value") +indigo.variable.updateValue(newVar, "asleep") +indigo.variable.updateValue(newVar, "awake") +indigo.variable.delete(newVar) +``` + +#### Getting a variable object and using its value: +```python +myVar = indigo.variables[123] +if myVar.value == "true": + indigo.server.log("The variable had a value of 'true'") +``` + +#### Duplicating a variable: +```python +indigo.variable.duplicate("fooMonster", duplicateName="fooMonsterSister") +``` + +#### Using a variable value in an HTTP GET +```python +import requests +my_var = indigo.variables[1893500335] # always use variable ID rather than name +query_args = {"param1": my_var.value} +reply = requests.get("http://example.com/foo/bar", params=query_args) +``` + +#### Setting a variable to Python types +Since Indigo variable values are always strings, you have to convert anything that's not a string. It's safest to use the str() method: + +```python +my_ascii_string = "ASCII String" +indigo.variable.updateValue(1234567, value=my_ascii_string) + +my_unicode_string = "éçø" +indigo.variable.updateValue(1234567, value=my_unicode_string) + +my_integer = 1 +indigo.variable.updateValue(1234567, value=str(my_integer)) + +my_float = 1.0 +indigo.variable.updateValue(1234567, value=str(my_float)) + +my_list = ["one", 2, "three", 4] +indigo.variable.updateValue(1234567, value=str(my_list)) + +my_dictionary = {"first":1, "second":"two", "third":3, "fourth":"four"} +indigo.variable.updateValue(1234567, value=str(my_dictionary)) +``` + +Any Python object that can be converted to a string can then be inserted into an Indigo variable. Note: conversions of this type are often one way: you can't necessarily automatically recreate the Python object from the string created by the str() method. For primitive types like numbers it may work, but for complex Python types like lists and dictionaries it will not. If you want to use Indigo to store a string representation of some complex Python data types, you can use JSON to encode them into strings then later decode them back into their respective Python objects: + +```python +import json +my_list = ["one", 2, "three", 4] +my_dictionary = {"first":1, "second":"two", "third":3, "fourth":"four", "my_list": my_list} +indigo.variable.updateValue(1234567, value=json.dumps(my_dictionary)) +# string will be something like: '{"second": "two", "myList": ["one", 2, "three", 4], "fourth": "four", "third": 3, "first": 1}' +my_new_dictionary = json.loads(indigo.variables[1234567].value) +#my_new_dictionary is now the same as my_dictionary +my_new_dictionary["my_list"] +# results in: ['one', 2, 'three', 4] +``` + +Custom Python classes that you create can implement the *`**str**(self)`* method. Take this simple example: + +```python +class myCustomClass: + def __init__(self): + self.a=1 + self.b=2 + def __str__(self): + outputString = f"a:{self.a}, b:{self.b}" + return outputString +``` + +If you have an instance of that class, then you can create a string representation of the class to insert into an Indigo variable: + +```python +cl = myCustomClass() +indigo.variable.updateValue(1234567, value=str(cl)) +# the value of the variable in Indigo will look like this: 'a:1, b:2' not including the quotes +``` + +For further information on Python classes, [check out this great tutorial](http://www.diveintopython.net/object_oriented_framework/index.html). + +### Date and Time Examples +#### Get the current server time: +```python +indigo.server.getTime() +``` + +#### Calculate the sunset time in 1 week: +```python +import datetime +one_week = indigo.server.getTime().date() + datetime.timedelta(days=7) +indigo.server.calculateSunset(one_week) +``` + +### Action Group Examples +#### Execute Action Group 12345678: +Execute an action group: +```python +indigo.actionGroup.execute(12345678) +``` + +#### Execute Action Group 12345678 with extra event data: +Execute an action group, but pass through an arbitrary dictionary of data to actions that support using **event_data**: +```python +extra_data = {"a": 1, "b": ["c", 2, 3]} +indigo.actionGroup.execute(12345678, event_data=extra_data) +``` + +### Log Examples +#### Log to the Indigo Event Log window all the attributes/properties of the device "office desk lamp": +```python +lamp = indigo.devices["office desk lamp"] +indigo.server.log(f"{lamp.name}: \n{lamp}") +``` + +#### Log to the Indigo Event Log window using different levels of logging +```python +import logging +indigo.server.log("info log message which will show in black text", level=logging.INFO) +indigo.server.log("warning log message which will show in orange text", level=logging.WARNING) +indigo.server.log("error log message which will show in red text", level=logging.ERROR) + +# Equivalent of above server API calls that instead use the plugin instances default logger instance: +self.logger.info("info log message which will show in black text") +self.logger.warn("warning log message which will show in orange text") +self.logger.error("error log message which will show in red text") +``` + +#### Print the last 5 Event Log entries: +```python +logList = indigo.server.getEventLogList(lineCount=5) +print(logList) +``` + +### Folder Examples +#### Iterate over a list of all device folders +```python +for folder in indigo.devices.folders: + print(f"Folder id: {folder.id} name: {folder.name}, remoteDisplay: {folder.remoteDisplay}") +``` + +#### Create a trigger folder named "My Triggers" and, if it exists, just return the existing one +```python +try: + myFolder = indigo.triggers.folder.create("My Triggers") +except ValueError as e: + if e.message == "NameNotUniqueError": + # a folder with that name already exists so just get it + myFolder = indigo.triggers.folders["My Triggers"] + else: + # you'll probably want to do something else to make myFolder a valid folder + myFolder = None +``` + +#### Make a folder visible in remote clients (IWS, Indigo Touch, etc.) +```python +indigo.devices.folder.displayInRemoteUI(123, value=True) +``` + +#### Get the folder containing a device +```python +lamp = indigo.devices["office desk lamp"] +# An object that's not in a folder will have a folder id of 0, which isn't a valid folder +# so we need to make sure it's a valid folder ID first +if lamp.folderId != 0: + lampsFolder = indigo.devices.folders[lamp.folderId] +else: + lampsFolder = None +``` + +#### Event Data Examples +Webhook events pass data along the chain that can be easily accessed in embedded or linked scripts. The data is passed first to any ***conditional scripts***. The event data can have many forms, but certain data will be passed regardless of the source of the call. By default, every event dictionary will contain the following: + +```json +{ + "event-indigo-id": 1214985350, # the ID of the trigger, schedule, action group, etc + "event-type": "Trigger", # the event type - trigger, schedule, action group, etc. + "source": "server", # the source of the event (see description below) + "timestamp": "2025-08-07T14:32:21", # ISO formatted datetime string +} +``` + +A more robust event dictionary might look like this: + +```json +{ + "data": (dict) + "event-indigo-id": 1234567890 (integer) + "event-plugin-event-id": simpleWebhook (string) + "event-plugin-id": com.indigodomo.webserver (string) + "event-plugin-name": Web Server (string) + "event-type": PluginEventTrigger (string) + "http-method": GET (string) + "request-url": https://localhost:8176/webhook/8143484549824dbba1286a873daa3cea (string) + "source": python (string) + "status-code": 200 (integer) + "timestamp": 2025-10-21T21:20:50 (string) + "webhook-id": 8143484549824dbba1286a873daa3cea (string) +} +``` + +Regardless of where the data came from (trigger, schedule, etc.) the variable name will be *`event_data`*. The data can be accessed like this: + +```python +# If you did an indigo.actionGroup.execute(12345) the source would be `python`. +# You don't need to import `event data`, it will be made available automatically. +if event_data["source"] == "python": + indigo.server.log(f"{event_data['timestamp']}") # or whatever + ... +``` + +All event_data payloads will be formatted as *``*. + +### Miscellaneous Examples +#### Get a list of all serial ports, excluding any Bluetooth ports: +```python +indigo.server.getSerialPorts(filter="indigo.ignoreBluetooth") +``` + +#### Sending emails +```python +# Simple Example +indigo.server.sendEmailTo("emailaddress@company.com", subject="Subject Line Here", body="Some longish text for the body of the email") + +# Putting a variable's data into the subject and body +theVar = indigo.variables[928734897] +theSubject = f"The value of {theVar.name}" +theBody = f"The value of {theVar.name} is now {theVar.value}" +indigo.server.sendEmailTo("emailaddress@company.com", subject=theSubject, body=theBody) + +# Putting device data into the subject and body +theDevice = indigo.devices[980532604] +theSubject = f"Summary of {theDevice.name}" +theBody = f"onState is {theDevice.onState}\n lastChanged is {theDevice.lastChanged}" +indigo.server.sendEmailTo("emailaddress@company.com", subject=theSubject, body=theBody) +``` + +## Starting the host from an existing terminal window +If you already have a Terminal shell running, you can launch it directly. To start the IPH in interactive mode just execute the following inside the Terminal: + +`/usr/local/indigo/indigo-host` + +![Plugin Host Prompt Image](../images/pluginhost_prompt.png) + +As shown, the IPH will automatically connect to the IndigoServer running on the same Mac and will show the server's version information and the Indigo Plugin Host Python version. Additionally, you'll notice that Indigo Server logs the connection of the interactive shell plugin. Add /usr/local/indigo/ to the PATH in your shell (in ~/.bashrc) and you won't have to specify the full path. + +Next, let's tell the Indigo Server to log a message to the Event Log window (again, via the Terminal application): +`indigo.server.log("Hello world!")` + +![Plugin Host Hello World Example Image](../images/pluginhost_helloworld.png) + +## Connecting Remotely over SSH +If you have SSH configured so you can remotely connect to your Mac running the Indigo Server, then you can use SSH to start the IPH interactively anywhere using the syntax: + +`ssh username@indigo_mac_ipaddr -t /usr/local/indigo/indigo-host` + +## What Else Can it Do? +The IPH gives you full access to the [Indigo Object Model (IOM)](iom-concepts.md) providing access to create/edit/delete/control Indigo objects, as well as several Indigo [utility functions](reference/server-commands.md), [Insteon device commands](reference/insteon-commands.md), and [X10 device commands](reference/x10-commands.md). Access to all of this functionality is provided by the indigo module automatically loaded and included in any python scripts run by the IPH (including interactive IPH sessions like we are using here). + +## Executing Indigo Commands Directly +In addition to communicating interactively with Indigo via the shell, you can also send direct python commands to Indigo via the IPH. For example, to get the current brightness of the device "office lamplinc": + +`indigo-host -e 'return indigo.devices["office lamplinc"].brightness'` + +Or to toggle the device "office lamplinc" three times: + +```python +indigo-host -e ' +indigo.device.toggle("office lamplinc") +indigo.device.toggle("office lamplinc") +indigo.device.toggle("office lamplinc") +' +``` + +Note when your commands are executed the indigo module is already loaded and connected, and you can execute standard python code (loops, conditionals, etc.). + +!!! note + Each call creates a new IPH (indigo-host) process which must establish a connection to the Indigo Server. Although this is relatively fast, calling it multiple times a second is not recommended. + +## Executing Indigo Python Files +The IPH can also be used to execute Indigo python (.py) files, like this: + +`indigo-host -x '/SomeFolder/indigo_script.py'` + +!!! note + Each call creates a new IPH (indigo-host) process which must establish a connection to the Indigo Server. Although this is relatively fast, calling it multiple times a second is not recommended. + +## Executing AppleScript +Often times, you may find yourself wanting to execute an AppleScript from Python. You want to send some parameters to the AppleScript and you want to get results back in some format. There's a pretty straight-forward way to do this that [we've described in this article](https://www.indigodomo.com/indigo/applescript.html). + +This is a great pattern for calling AppleScripts from Python, specifically a great way to integrate other scriptable Mac app data with Indigo. + +## Shared Classes and Methods in Python Files (Python Modules) +You may install Python modules/libraries in a special location to make them available to Indigo scripts and plugins, but not to generic Python. This is particularly useful if the module/library includes references to the IOM (since it will only be loaded by processes that know about the indigo module). This location is: + +*/Library/Application Support/Perceptive Automation/Python3-includes* + +Any Python files in this directory that define methods and/or classes will be available to any Python script whenever the interpreter is loaded (note that it's the Library directory at the top level of your boot disk, not the one in your user folder). These are referred to as Python modules. + +So, you can create files of classes, functions, etc. you want to share between all Python scripts using these mechanisms. If you add/change something in that directory while the Indigo Server is running, you'll need to tell the server to reload - select the `Plugins->Reload Libraries and Attachments` menu item and Indigo will restart the Python interpreter (which will cause your module to be reloaded). + +Files added to the generic module location can also import the entire IOM - IF the script that's actually running is started by Indigo. Here's a simple script that you can use that will safely import the IOM and will show a specific error when used from a Python script that's not started by Indigo: + +```python +""" +indigo_attachments.py + +In this file you can insert any methods and classes that you define. +They will be shared by all Python scripts - you can even import the +IOM (as shown below) but if you do then you'll only be able to import +this script in Python processes started by Indigo. If you don't need +the IOM then skip the import, and it'll work in any Python script +no matter where it's run from. +""" + +try: + import indigo +except ImportError: + print("The indigo module can only be used by scripts started from within Indigo") + raise ImportError +from datetime import datetime + +def log(message, label=None): + # Create a log line with the date/timestamp prepended in MM/DD/YYYY HH:MM:SS format + log_line = f"{datetime.today().strftime('%x %X')} {message}" + # Write the log line with the label. If you didn't pass in a label + # then the default label will be used. + indigo.server.log(log_line, type=label) +``` + +Then, when you want to use any of the classes/methods in the file, just import the file and go: + +```python +import indigo_attachments +indigo_attachments.log("Here's a special sort of log message", label="My Custom Label") +``` + +What you'll see in the Event Log: + +`My Custom Label 01/15/16 10:20:39 Here's a special sort of log message` + +If you try to run this script in a normal python session, you'll see the print statement followed by the ImportError: + +```python +MyMac:testdir jay$ python3.10 testHandlerCall.py +The indigo module can only be used by scripts started from within Indigo +Traceback (most recent call last): + File "testHandlerCall.py", line 3, in + import indigo_attachments + File "/Users/USERNAME/Library/Python/3.10/site-packages/indigo_attachments.py", line 14, in + raise ImportError +ImportError +``` + +## Scripting Indigo Plugins +Indigo plugins are also scriptable. Because plugin-defined devices look almost identical to built-in devices, you can get (but not set) state information from them (see [device examples](#device-examples) above for a lot of examples). You can also get some of the other properties for a plugin. Most importantly you can execute plugin-defined actions, which is how you'd set their state values. + +The first step is to get an instance of the plugin: + +```python +iTunesId = "com.perceptiveautomation.indigoplugin.itunes" # supplied by the plugin's documentation +iTunesPlugin = indigo.server.getPlugin(iTunesId) +``` + +This will **always** return to you a plugin object, defined with the following properties: + +| Property | Type | Description | +|---------------------------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------| +| `compatibleUpdateAvailable` | bool | True is there is a compatible update to the plugin available from the store | +| `includedWithServer` | str | True if the plugin is included in the Indigo installer | +| `incompatibleUpdateAvailable` | bool | True is there is an update to the plugin available from the store but that is incompatible with the Indigo Server version currently running | +| `latestCompatibleDownloadCount` | int | The number of downloads from the plugin store of this specific version of the plugin (`None` if the plugin isn't in the plugin store) | +| `latestCompatibleDownloadURL` | str | The URL for the latest compatible release from the plugin store (`None` if the plugin isn't in the plugin store) | +| `latestCompatibleReleaseDate` | datetime | The date for the latest compatible release in the plugin store (`None` if the plugin isn't in the plugin store) | +| `latestCompatibleSummaryDesc` | str | The summary for the latest compatible release in the plugin store (`None` if the plugin isn't in the plugin store) | +| `latestCompatibleVers` | str | The version of the most recent compatible release (`None` if the plugin isn't in the plugin store) | +| `latestCompatibleWhatsNewDesc` | str | The "what's new" description of the most recent compatible release (`None` if the plugin isn't in the plugin store) | +| `latestReleaseDate` | datetime | The release date of the most recent release (`None` if the plugin isn't in the plugin store) | +| `latestRequiresIndigoVers` | str | The Indigo release that is the minimum requirement of the most recent release (`None` if the plugin isn't in the plugin store) | +| `latestVers` | str | The most recent release version (`None` if the plugin isn't in the plugin store) | +| `pluginDisplayName` | str | The name displayed to users in the clients | +| `pluginFolderPath` | str | The full path to the plugin file location (useful for creating a full path to files within the plugin bundle) | +| `pluginId` | str | The ID of the plugin (same as was passed to the getPlugin() method above) | +| `pluginServerApiVersion` | str | The max API version supported by the running Indigo Server | +| `pluginSupportURL` | str | The URL supplied by the plugin that points to its user documentation | +| `pluginVersion` | str | The version number specified by the plugin | +| `storeIconURL` | str | The URL to the plugin's icon in the plugin store (`None` if the plugin isn't in the plugin store) | +| `storeName` | str | The name of the plugin in the plugin store (`None` if the plugin isn't in the plugin store) | +| `storePluginURL` | str | The URL to the plugin in the plugin store (`None` if the plugin isn't in the plugin store) | +| `storeSummary` | str | The description of the plugin in the plugin store (`None` if the plugin isn't in the plugin store) | + + +There are also a couple of methods defined by this object. + +| Method | Description | +|-----------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `executeAction(actionId, deviceId, props, waitUntilDone)` | This is the method that actually executes the action. *`actionId`* is the unique action id. *`deviceId`* is the id of the device that the action on which the action will perform (if it requires a device). *`props`* is a dictionary of properties that the action needs to process correctly. See the plugin's documentation for a description of what you need to pass in to execute actions. *`waitUntilDone`* is a boolean that tells the plugin to block until it can return an optional payload object. | +| `isEnabled()` | This method will return *`True`* if the plugin is enabled (running), *`False`* otherwise | +| `isInstalled()` | This method will return *`True`* if the plugin is installed, *`False`* otherwise | +| `isRunning()` | This method will return *`True`* if the plugin is running, *`False`* otherwise | +| `restart(waitUntilDone=True)` | This method will restart the plugin if it's already running. Pass the optional parameter `*waitUntilDone=False*` if you don't want to block while the plugin restarts (waitUntilDone defaults to `*True*` if it's no parameters are passed). If you're a plugin developer and you want to restart the plugin from within itself you should pass that parameter. | +| `restartAndDebug(waitUntilDone=True)` | The same as above, but it will restart in the debugger the user has selected in the Preferences. | + + +Plugin actions may return a value (a Python "primitive") when you call the *`executeAction`* +method, but they are not required to. The return value can be any one of the following object types: + +- None *`None`*, +- booleans *`bool`*, +- integers *`int()`*, +- floats *`float()`*, +- strings *`str()`*, +- Indigo Dicts *`indigo.Dict()`*, +- Indigo Lists *`indigo.List()`*. + +Note that a plugin action is not required to return something. If nothing is affirmatively returned, the return value will be set to *`None`* by default. Check with the plugin author's documentation for the possible return value for a particular action. The *`waitUntilDone`* parameter is optional (it defaults to true) and tells the plugin that you want it to pause your script until it's able to complete all the actions required to complete the request. For example, if you want to wait for the plugin to query an API or communicate with a device, set *`waitUntilDone`* to *`True`*. + +Plugin developers should [test and document their plugin actions](../plugin-dev/reference/xml/actions.md#actions-xml) to make sure that the necessary information is available to scripters. Consult the plugin's documentation to see whether an action call returns a value, and what object type the plugin returns for the action you're calling (different actions can return different object types). + +### Examples +#### Restart a Plugin +There may be instances where you want to restart a plugin. This is how you do that from a script: + +```python +airfoil_id = "com.perceptiveautomation.indigoplugin.airfoilpro" +airfoil_plugin = indigo.server.getPlugin(airfoil_id) +if airfoil_plugin.isEnabled(): + airfoil_plugin.restart() +``` + +!!! note + For safety reasons, we do not provide the ability to enable a plugin that is disabled, or disable a plugin that is enabled. + +#### Wait Until Done +There may be instances where you want to wait for a plugin to complete its action before you move on. You do that from a script by using the *`waitUntilDone`* parameter: + +```python +plugin_id = "com.perceptiveautomation.indigoplugin.airfoilpro" + +# Get a plugin object given the plugin id: +airfoil_plugin = indigo.server.getPlugin(plugin_id) + +if airfoilPlugin.isEnabled(): + try: + result = airfoilPlugin.executeAction( + 'getSources', + deviceId=12345678 # ID of Airfoil device, + waitUntilDone=True + ) + except Exception as e: + print(f"Exception occurred: {e}") +``` + +*Note:* The AirFoil plugin doesn't receive props as a part of the call, so no *`props`* argument is provided. In this example, the AirFoil plugin will block until it's finished processing the action before continuing. The reply in this instance is a dictionary that contains information about the result of the action: + +```text +Data : (dict) + audioDevices : (list) + Item : (dict) + friendlyName : Apple USB audio device (string) + icon : [SNIP] + identifier : AppleUSBAudioEngine:Apple Inc.:Apple USB audio device:241000:2,1 (string) + Item : (dict) + friendlyName : Built-in Microphone (string) + icon : [SNIP] + identifier : AppleHDAEngineInput:1B,0,1,0:1 (string) + Item : (dict) + friendlyName : USB audio CODEC (string) + icon : [SNIP] + identifier : AppleUSBAudioEngine:Burr-Brown from TI:USB audio CODEC:400000:2,1 (string) + recentApplications : (list) + Item : (dict) + friendlyName : iTunes (string) + icon : [SNIP] + identifier : /Applications/iTunes.app (string) + Item : (dict) + friendlyName : iMovie (string) + icon : [SNIP] + identifier : /Applications/iMovie.app (string) + systemAudio : (list) + Item : (dict) + friendlyName : System Audio (string) + icon : [SNIP] + identifier : com.rogueamoeba.source.systemaudio +``` + +Check the documentation for each plugin for the necessary information and more examples. diff --git a/skills/api/SKILL.md b/skills/api/SKILL.md index a234ee0..3f16d76 100644 --- a/skills/api/SKILL.md +++ b/skills/api/SKILL.md @@ -29,64 +29,71 @@ Indigo exposes two transport APIs for remote control. Choose based on use case: | iOS/mobile apps | WebSocket | Real-time device updates, bidirectional | | Web dashboards | WebSocket | Live state without polling | | Scripts/automation | HTTP | Simple, stateless, one-shot commands | -| Third-party integration | HTTP | Standard REST, easy to integrate | +| Third-party integration | HTTP | Standard request/response, easy to integrate | | Hybrid apps | Both | HTTP for initial load, WebSocket for live updates | ## Authentication -Two methods available: +Both APIs authenticate with **HTTP Digest, API Keys, or local secrets** (canonical `api/http.md`, `api/websocket.md`). -- **API Keys** (recommended) — Works over network, configured in Indigo preferences - - Header: `Authorization: Bearer ` -- **Local Secrets** — Local network only, auto-generated per-reflector - - Only for same-machine or trusted LAN access - -## WebSocket Quick Reference - -``` -ws[s]://:/v2/api/ws -``` - -- **Subscribe**: Send `{"name": "device", "id": }` to receive state changes -- **Commands**: Send actions via WebSocket messages -- **Reconnection**: Implement exponential backoff with jitter +- **API Keys** (recommended) — managed in your Indigo Account → **Authorizations** (not "API Keys"), per-app and revocable. Send as `Authorization: Bearer `, or as an `?api-key=` query arg when headers aren't available. Keys can control devices but **cannot modify the database** (add/delete devices) — that stays with the username/password. +- **Local secrets** — a *special kind of API key* that does not route through the Indigo reflector; used identically as a Bearer token. Also managed under Authorizations. +- **HTTP Basic** and the **old REST API** are **deprecated** — do not use. ## HTTP Quick Reference +**GET** retrieves object instances; **POST to `/v2/api/command`** sends every command. There is **no `PUT`** and no per-object write endpoint. + ``` -GET /v2/api/indigo.devices # List all devices -GET /v2/api/indigo.devices/ # Get specific device -PUT /v2/api/indigo.devices/ # Update device -POST /v2/api/command # Execute command +GET /v2/api/indigo.devices # List all devices +GET /v2/api/indigo.devices/ # Get one device (full object; there is no ?detail flag) +GET /v2/api/indigo.variables[/] # Variables +GET /v2/api/indigo.actionGroups[/] +POST /v2/api/command # ALL commands (device control, variable update, action execute) ``` -All responses are JSON. Use `?detail=true` for full state information. +## Command Message Shape (both transports) -## Common Device Commands +Commands use `message` + `objectId` (+ optional `parameters`) — **not** `name`/`parameters.id`. Command names are namespaced (`indigo.device.*`, `indigo.dimmer.*`, `indigo.thermostat.*`). See `api/messages.md`. ```json -// Turn on -{"name": "device.turnOn", "parameters": {"id": 123456}} +{"message": "indigo.device.turnOn", "objectId": 123456789} +{"message": "indigo.device.toggle", "objectId": 123456789, "parameters": {"delay": 5, "duration": 10}} +{"message": "indigo.dimmer.setBrightness", "objectId": 123456789, "parameters": {"value": 75}} +{"message": "indigo.thermostat.setHeatSetpoint", "objectId": 123456789, "parameters": {"value": 72}} +``` + +## WebSocket Quick Reference -// Set brightness -{"name": "device.setBrightness", "parameters": {"id": 123456, "value": 75}} +Connect to a **specific feed**, not a bare `/v2/api/ws`. Seven feeds: +`device-feed`, `variable-feed`, `action-feed`, `schedule-feed`, `trigger-feed`, `page-feed`, `log-feed`. -// Set thermostat -{"name": "thermostat.setHeatSetpoint", "parameters": {"id": 123456, "value": 72}} ``` +ws[s]://:/v2/api/ws/device-feed?api-key= +``` + +- **Get current state**: send a `refresh` message (there is no "subscribe"): + ```json + {"id": "req-1", "message": "refresh", "objectType": "indigo.Device", "objectId": 123456789} + ``` + Omit `objectId` to receive the whole list. +- **Incoming**: read `add` / `patch` / `delete` / `refresh` messages and apply them. +- **Commands**: send the same `message`/`objectId` envelope as HTTP. +- **Reconnection**: exponential backoff with jitter. -## Reference Documentation +## Reference Documentation (canonical — load only what's needed) -For detailed guidance, read these files relative to `${CLAUDE_PLUGIN_ROOT}`: +Vendored verbatim from Indigo's published docs. Read relative to `${CLAUDE_PLUGIN_ROOT}`: | Topic | File | |-------|------| -| WebSocket vs HTTP overview | `docs/api/overview.md` | -| Authentication setup | `docs/api/authentication.md` | -| WebSocket API (full) | `docs/api/websocket-api.md` | -| HTTP REST API (full) | `docs/api/http-api.md` | -| Device command reference | `docs/api/device-commands.md` | -| API navigation guide | `docs/api/README.md` | +| Integration APIs overview / transport choice | `reference/canonical/api.md` | +| HTTP API (endpoints, auth, curl/Python) | `reference/canonical/api/http.md` | +| Message format (commands, events, errors) | `reference/canonical/api/messages.md` | +| WebSocket API (feeds, refresh, patches) | `reference/canonical/api/websocket.md` | +| Webhooks (receiving external events) | `reference/canonical/api/webhooks.md` | +| Migrating from the old REST API | `reference/canonical/api/rest-migration.md` | +| Full canonical index (all pages) | `reference/canonical/INDEX.md` | ## Full Documentation diff --git a/skills/dev/SKILL.md b/skills/dev/SKILL.md index f620c72..4b86676 100644 --- a/skills/dev/SKILL.md +++ b/skills/dev/SKILL.md @@ -106,39 +106,59 @@ dev.updateStatesOnServer(states) dev.replaceOnServer() ``` +## Lifecycle callbacks & `super()` (common footgun) + +- `__init__` — **do** call `super().__init__(...)`. `startup`/`shutdown`/`runConcurrentThread` — do **not** call super. +- `deviceStartComm`/`deviceStopComm` — these are override hooks; the base versions are effectively no-ops, so calling super is optional (not "required"). +- `deviceUpdated`/`triggerUpdated` and `deviceCreated`/`deviceDeleted`/`triggerCreated`/`triggerDeleted` — the base implementations do real work (they drive the stop/start-comm machinery). Overriding these **requires** calling the base (`indigo.PluginBase.deviceUpdated(self, orig, new)`) or re-implementing start/stop. See `reference/canonical/plugin-dev/reference/plugin-py/device-methods.md`. +- `secure="true"` on a text field only **masks the value in the UI — it is NOT stored securely.** Never rely on it for secrets. + ## Reference Documentation -For detailed guidance on specific topics, read these files relative to `${CLAUDE_PLUGIN_ROOT}`: +Reference facts come from `reference/canonical/**` (relative to `${CLAUDE_PLUGIN_ROOT}`) — vendored +verbatim from Indigo 2025.2 docs via `tools/refresh_canonical.py`. Load only the page needed; +`reference/canonical/INDEX.md` lists all. A few docs under `docs/plugin-dev/` are workspace +on-ramps/patterns kept alongside canonical. All paths below are relative to `${CLAUDE_PLUGIN_ROOT}`. + +> **Field notes — undocumented gotchas canonical does NOT cover.** Consult these before writing +> custom states, dynamic state lists, cross-plugin actions, `uiPath` menus, or custom events: +> `docs/plugin-dev/concepts/{devices,actions,events,plugin-preferences}.md`. They cover strict +> state-ID naming (`LowLevelBadParameterError`), reserved names (`batteryLevel` shadowing), the +> live-`getDeviceStateList` cache trap, `deviceUpdated` self-loop guard, `uiPath` PascalCase (crashes +> the client), cross-plugin `executeAction` prop-matching, and the `pluginPrefs` vs `pluginProps` +> `_`-prefix rule. | Topic | File | |-------|------| -| Plugin lifecycle (full) | `docs/plugin-dev/concepts/plugin-lifecycle.md` | -| Device design & Devices.xml | `docs/plugin-dev/concepts/devices.md` | -| ConfigUI reference (fields, attributes, bindings) | `docs/plugin-dev/concepts/configui.md` | -| Actions.xml & actionControl callbacks | `docs/plugin-dev/concepts/actions.md` | -| Menu items (MenuItems.xml) | `docs/plugin-dev/concepts/menu-items.md` | -| Custom events | `docs/plugin-dev/concepts/events.md` | -| HTTP Responder (web endpoints, IWS) | `docs/plugin-dev/concepts/http-responder.md` | -| Scripting shell & CLI | `docs/plugin-dev/concepts/scripting-shell.md` | -| Plugin preferences & PluginConfig.xml | `docs/plugin-dev/concepts/plugin-preferences.md` | +| **Field notes — undocumented device/state/action/event gotchas** | `docs/plugin-dev/concepts/{devices,actions,events,plugin-preferences}.md` | +| Plugin lifecycle (workspace on-ramp) | `docs/plugin-dev/concepts/plugin-lifecycle.md` | +| plugin.py lifecycle methods (reference) | `reference/canonical/plugin-dev/reference/plugin-py/general-methods.md` | +| Device start/stop/config/action callbacks | `reference/canonical/plugin-dev/reference/plugin-py/device-methods.md` | +| Devices.xml (device types, states, subType) | `reference/canonical/plugin-dev/reference/xml/devices.md` | +| ConfigUI fields & bindings | `reference/canonical/plugin-dev/reference/xml/configui.md` (+ `configui/` subpages) | +| ConfigUI validation methods (+ `ValidationError`) | `reference/canonical/plugin-dev/reference/xml/configui/validation.md` | +| Actions.xml | `reference/canonical/plugin-dev/reference/xml/actions.md` | +| MenuItems.xml (+ callback contract) | `reference/canonical/plugin-dev/reference/xml/menuitems.md` | +| Events.xml / custom triggers | `reference/canonical/plugin-dev/reference/xml/events.md` | +| PluginConfig.xml & preferences | `reference/canonical/plugin-dev/reference/xml/pluginconfig.md` | +| HTTP request handling (IWS) | `reference/canonical/plugin-dev/reference/plugin-py/http-requests.md` | +| Logging | `reference/canonical/plugin-dev/reference/plugin-py/logging.md` | +| Dev environment (symlink workflow, debuggers) | `reference/canonical/plugin-dev/reference/dev-environment.md` | | API patterns (state updates, replaceOnServer) | `docs/plugin-dev/patterns/api-patterns.md` | | Testing patterns (pytest mocks, TestingBase) | `docs/plugin-dev/patterns/testing.md` | | Troubleshooting | `docs/plugin-dev/troubleshooting/common-issues.md` | | SDK examples guide | `docs/plugin-dev/examples/sdk-examples-guide.md` | -| Indigo Object Model overview | `docs/plugin-dev/api/indigo-object-model.md` | -### IOM Reference (modular) +### Indigo Object Model (scripting reference — canonical) -For specific Indigo Object Model topics, read from `docs/plugin-dev/api/iom/`: +For the `indigo.*` object model, read from `reference/canonical/scripting/`: -- `architecture.md` — Object hierarchy and base classes -- `devices.md` — Device properties, methods, base types -- `command-namespaces.md` — `indigo.device`, `indigo.variable`, etc. -- `triggers.md` — Trigger types and configuration -- `subscriptions.md` — Change subscriptions -- `constants.md` — Enums and constant values -- `containers.md` — Lists, dictionaries, database access -- `utilities.md` — Logging, scheduling, server info +- `iom-concepts.md` — object hierarchy, base classes, copy semantics +- `reference/devices/base-class.md` — device base class properties & methods +- `reference/device-subclasses/` — dimmer, relay, sensor, thermostat, sprinkler, speedcontrol, multiio +- `reference/triggers.md` / `reference/schedules.md` / `reference/action-groups.md` / `reference/variables.md` +- `reference/server-commands.md` — server properties & commands +- `reference/folders.md`, `reference/insteon-commands.md`, `reference/x10-commands.md`, `reference/utils.md` ### SDK Examples diff --git a/skills/html-pages/SKILL.md b/skills/html-pages/SKILL.md index 1c3f59f..b575612 100644 --- a/skills/html-pages/SKILL.md +++ b/skills/html-pages/SKILL.md @@ -134,7 +134,7 @@ Produce a single HTML file. Follow this template structure: - Disable toggle controls briefly (500ms) after a command to prevent double-taps - Escape all device names with a text-node approach before rendering as HTML -**Critical**: All device commands use `POST /v2/api/command` — consult `/indigo:api` skill docs (`docs/api/device-commands.md`) for the full command reference. Do not guess command formats. +**Critical**: All device commands use `POST /v2/api/command` — consult `/indigo:api` skill docs (`reference/canonical/api/messages.md`) for the full command reference. Do not guess command formats. ### Phase 4: DEPLOY diff --git a/skills/html-pages/references/indigo-api-js.md b/skills/html-pages/references/indigo-api-js.md index 335f333..dc221cb 100644 --- a/skills/html-pages/references/indigo-api-js.md +++ b/skills/html-pages/references/indigo-api-js.md @@ -107,7 +107,7 @@ All commands are sent via `POST /v2/api/command` with JSON body: } ``` -Refer to `/indigo:api` skill documentation (`docs/api/device-commands.md`) for the full command reference. +Refer to `/indigo:api` skill documentation (`reference/canonical/api/messages.md`) for the full command reference. ## Capability Detection diff --git a/tools/refresh_canonical.py b/tools/refresh_canonical.py new file mode 100644 index 0000000..1d3890d --- /dev/null +++ b/tools/refresh_canonical.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +"""Refresh the vendored canonical Indigo documentation. + +Indigo publishes two machine-readable docs: + - https://docs.indigodomo.com/llms.txt (index: [Title](url): description per page) + - https://docs.indigodomo.com/llms-full.txt (full content, sections delimited by + `--- Title (URL) ---` markers) + +This script fetches the monolith, splits it on those markers into small per-page Markdown +files under reference/canonical/ (path mirrors the doc URL), builds INDEX.md from the index, +and stamps VERSION. Skills route into these files for progressive, load-only-what-you-need +reference — never a whole webpage or the 1.5MB monolith in context. + +The vendored tree is GENERATED. Never hand-edit a file under reference/canonical/; edit here +and re-run instead. + +Usage: + python3 tools/refresh_canonical.py # fetch + regenerate reference/canonical/ + python3 tools/refresh_canonical.py --check # regenerate to a temp dir, diff, exit 1 on drift + python3 tools/refresh_canonical.py --from-dir DIR # use local llms.txt / llms-full.txt (offline) + python3 tools/refresh_canonical.py --allow-version-change # accept a new Indigo major + +Pinned to a specific Indigo version (see VERSION). A published-version change is refused unless +--allow-version-change is passed, so a refresh never silently pulls a new major's content. +Stdlib only — no third-party dependencies. +""" + +from __future__ import annotations + +import argparse +import hashlib +import re +import sys +import tempfile +from datetime import datetime, timezone +from pathlib import Path +from urllib.parse import urlsplit +from urllib.request import Request, urlopen + +INDEX_URL = "https://docs.indigodomo.com/llms.txt" +FULL_URL = "https://docs.indigodomo.com/llms-full.txt" + +# Only these top-level doc trees are vendored — the surface the dev/api skills route into. +INCLUDE_PREFIXES = ("api", "plugin-dev", "scripting") + +SECTION_RE = re.compile(r"^--- (?P.+?) \((?P<url>https?://\S+?)\) ---$") +INDEX_LINK_RE = re.compile(r"^- \[(?P<title>[^\]]+)\]\((?P<url>https?://\S+?)\)(?::\s*(?P<desc>.*))?$") + +REPO_ROOT = Path(__file__).resolve().parent.parent +CANONICAL_DIR = REPO_ROOT / "reference" / "canonical" + +GEN_HEADER = "<!-- GENERATED from {url} by tools/refresh_canonical.py — DO NOT EDIT. Run the script to refresh. -->\n\n" + + +# The docs host rejects urllib's default User-Agent (403), so send a conventional one. +USER_AGENT = "Mozilla/5.0 (compatible; indigo-claude-plugin refresh_canonical)" + + +def fetch(url: str) -> str: + req = Request(url, headers={"User-Agent": USER_AGENT}) + with urlopen(req, timeout=60) as resp: # noqa: S310 (trusted vendor URL) + return resp.read().decode("utf-8") + + +def url_to_relpath(url: str) -> tuple[str, str] | None: + """Map a doc URL to (version, 'a/b/c.md'), or None if outside the include set. + + .../2025.2/api/http/ -> ('2025.2', 'api/http.md') + .../2025.2/api/ -> ('2025.2', 'api/index.md') + .../2025.2/plugin-dev/reference/xml/configui/button/ -> (..., '.../configui/button.md') + """ + parts = [p for p in urlsplit(url).path.split("/") if p] + if not parts: + return None + version, doc_parts = parts[0], parts[1:] + if not doc_parts or doc_parts[0] not in INCLUDE_PREFIXES: + return None + relpath = "/".join(doc_parts) + ".md" + return version, relpath + + +def split_full(full_text: str) -> tuple[str, dict[str, tuple[str, str]]]: + """Split llms-full.txt. Returns (version, {relpath: (url, body)}).""" + lines = full_text.splitlines(keepends=True) + sections: dict[str, tuple[str, str]] = {} + version = "" + cur_url: str | None = None + cur_relpath: str | None = None + buf: list[str] = [] + + def flush() -> None: + if cur_relpath and cur_url is not None: + sections[cur_relpath] = (cur_url, "".join(buf).strip() + "\n") + + for line in lines: + m = SECTION_RE.match(line.rstrip("\n")) + if m: + flush() + buf = [] + mapped = url_to_relpath(m.group("url")) + if mapped: + version = version or mapped[0] + cur_relpath, cur_url = mapped[1], m.group("url") + else: + cur_relpath, cur_url = None, None + elif cur_relpath: + buf.append(line) + flush() + return version, sections + + +def build_index(index_text: str) -> str: + """Rewrite llms.txt into a routing manifest pointing at local vendored paths.""" + out = ["# Canonical documentation index", + "", + "GENERATED by tools/refresh_canonical.py from " + INDEX_URL + ".", + "Each entry points at a local vendored file — load only the page you need.", + ""] + for line in index_text.splitlines(): + if line.startswith("## "): + out.append("") + out.append(line) + out.append("") + continue + m = INDEX_LINK_RE.match(line.strip()) + if not m: + continue + mapped = url_to_relpath(m.group("url")) + if not mapped: + continue + desc = (m.group("desc") or "").strip() + rel = mapped[1] + suffix = f" — {desc}" if desc else "" + out.append(f"- **{m.group('title')}**{suffix} → `reference/canonical/{rel}`") + return "\n".join(out).rstrip() + "\n" + + +def generate(out_dir: Path, index_text: str, full_text: str, expected_version: str | None, + allow_version_change: bool) -> str: + version, sections = split_full(full_text) + if not sections: + sys.exit("ERROR: no sections matched — the canonical format may have changed.") + if expected_version and version != expected_version and not allow_version_change: + sys.exit( + f"ERROR: published version is {version!r} but VERSION pins {expected_version!r}.\n" + f"Indigo shipped a new major. Re-run with --allow-version-change to accept it " + f"(and bump the plugin version deliberately)." + ) + + for relpath, (url, body) in sorted(sections.items()): + dest = out_dir / relpath + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_text(GEN_HEADER.format(url=url) + body, encoding="utf-8") + + (out_dir / "INDEX.md").write_text(build_index(index_text), encoding="utf-8") + + sha = hashlib.sha256(full_text.encode("utf-8")).hexdigest() + fetched = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + (out_dir / "VERSION").write_text( + f"indigo_version: {version}\n" + f"source_index: {INDEX_URL}\n" + f"source_full: {FULL_URL}\n" + f"fetched_utc: {fetched}\n" + f"sha256_full: {sha}\n" + f"pages: {len(sections)}\n" + f"generated_by: tools/refresh_canonical.py\n", + encoding="utf-8", + ) + return version + + +def read_pinned_version() -> str | None: + vf = CANONICAL_DIR / "VERSION" + if not vf.exists(): + return None + for line in vf.read_text(encoding="utf-8").splitlines(): + if line.startswith("indigo_version:"): + return line.split(":", 1)[1].strip() + return None + + +def load_sources(from_dir: str | None) -> tuple[str, str]: + if from_dir: + d = Path(from_dir) + return (d / "llms.txt").read_text(encoding="utf-8"), (d / "llms-full.txt").read_text(encoding="utf-8") + return fetch(INDEX_URL), fetch(FULL_URL) + + +def main() -> None: + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--check", action="store_true", help="regenerate to a temp dir and diff; exit 1 on drift") + ap.add_argument("--from-dir", metavar="DIR", help="read local llms.txt / llms-full.txt instead of fetching") + ap.add_argument("--allow-version-change", action="store_true", help="accept a new pinned Indigo version") + args = ap.parse_args() + + index_text, full_text = load_sources(args.from_dir) + pinned = read_pinned_version() + + if args.check: + with tempfile.TemporaryDirectory() as tmp: + tmp_dir = Path(tmp) + generate(tmp_dir, index_text, full_text, pinned, allow_version_change=True) + drift = [] + new_files = {p.relative_to(tmp_dir) for p in tmp_dir.rglob("*") if p.is_file()} + old_files = {p.relative_to(CANONICAL_DIR) for p in CANONICAL_DIR.rglob("*") if p.is_file()} \ + if CANONICAL_DIR.exists() else set() + for rel in sorted(new_files | old_files): + if rel.name == "VERSION": # fetched_utc always differs; compare content pages only + continue + a = (CANONICAL_DIR / rel).read_text(encoding="utf-8") if (CANONICAL_DIR / rel).exists() else None + b = (tmp_dir / rel).read_text(encoding="utf-8") if (tmp_dir / rel).exists() else None + if a != b: + status = "added" if a is None else "removed" if b is None else "changed" + drift.append(f" {status}: reference/canonical/{rel}") + if drift: + print("Canonical docs have drifted from the vendored copy:") + print("\n".join(drift)) + print("\nRun: python3 tools/refresh_canonical.py") + sys.exit(1) + print("No drift — vendored canonical matches the live docs.") + return + + version = generate(CANONICAL_DIR, index_text, full_text, pinned, args.allow_version_change) + print(f"Refreshed reference/canonical/ from Indigo {version} docs.") + + +if __name__ == "__main__": + main()