| Both sides previous revision Previous revision Next revision | Previous revision |
| indigo_2023.2_documentation:plugin_guide [2024/06/25 01:27] – [Serial Port] davel17 | indigo_2023.2_documentation:plugin_guide [2025/04/14 20:10] (current) – external edit 127.0.0.1 |
|---|
| Indigo's folder structure looks like this: | Indigo's folder structure looks like this: |
| |
| {{folder_structure.png|}} | {{folder_structure.png?nolink|Folder Structure Image}} |
| |
| in this location: | in this location: |
| 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 it's functions: | 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 it's functions: |
| |
| {{bundle_layout.png|}} | {{bundle_layout.png?nolink|Bundle Layout Image}} |
| |
| 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. | 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. |
| Here is what they keys are for: | 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. | * //''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 [[https://wiki.indigodomo.com/doku.php?id=api_version_chart|API Version Chart]] as new API versions are released. | * //''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 [[https://wiki.indigodomo.com/doku.php?id=api_version_chart|API Version Chart]] 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. | * //''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. |
| * //''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. | * //''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. | * //''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. | * //''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. | 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. |
| ==== Resources Folder ==== | ==== Resources Folder ==== |
| |
| This folder is meant to contain any assets (like images, templates, etc.) that your plugin might need. | 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 [[https://www.indigodomo.com/pluginstore/|Plugin Store]] once you submit the plugin (see [[#Plugin Store]] below for more details). Note that the code that loads [[https://wiki.indigodomo.com/doku.php?id=api_release_notes:1.4#config_ui_templates|config UI XML templates]] 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 there is an ''**icon.png**'' file in here, it will be displayed in the [[https://www.indigodomo.com/pluginstore/|Plugin Store]] once you submit the plugin (see [[#Plugin Store]] below for more details). Note that the code that loads [[https://wiki.indigodomo.com/doku.php?id=api_release_notes:1.4#config_ui_templates|config UI XML templates]] 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''). |
| |
| The structure of the //''Server Plugin''// directory in the plugin bundle is something like this: | The structure of the //''Server Plugin''// directory in the plugin bundle is something like this: |
| |
| {{ServerPluginFolder.png}} | {{ServerPluginFolder.png?nolink|Server Plugin Folder Image}} |
| |
| 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. | 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. |
| <code><ConfigUI> | <code><ConfigUI> |
| <SupportURL>http://www.yourdomain.com/plugin/ApplianceModule.html</SupportURL> | <SupportURL>http://www.yourdomain.com/plugin/ApplianceModule.html</SupportURL> |
| <Field id="autoLabel" | <Field id="autoLabel" |
| type="label" | type="label" |
| visibleBindingId="manualEntry" | visibleBindingId="manualEntry" |
| visibleBindingValue="false"> | visibleBindingValue="false"> |
| <Label>First, put your device into linking mode.</Label> | <Label>First, put your device into linking mode.</Label> |
| </Field> | </Field> |
| <Field id="exampleButton" | <Field id="exampleButton" |
| type="button" | type="button" |
| tooltip="Click this button to start the automatic discovery process." | tooltip="Click this button to start the automatic discovery process." |
| visibleBindingId="manualEntry" | visibleBindingId="manualEntry" |
| visibleBindingValue="false"> | visibleBindingValue="false"> |
| <Label>Then click this button:</Label> | <Label>Then click this button:</Label> |
| <Action>validPythonMethodName</Action> | <Action>validPythonMethodName</Action> |
| </Field> | </Field> |
| <Field id="nodeID" | <Field id="nodeID" |
| type="textfield" | type="textfield" |
| readonly="YES" | readonly="YES" |
| visibleBindingId="manualEntry" | visibleBindingId="manualEntry" |
| visibleBindingValue="false"> | visibleBindingValue="false"> |
| <Label>Node ID:</Label> | <Label>Node ID:</Label> |
| </Field> | </Field> |
| <Field id="simpleSeparator1" type="separator"/> | <Field id="simpleSeparator1" type="separator"/> |
| <Field type="checkbox" | <Field type="checkbox" |
| id="manualEntry" | id="manualEntry" |
| defaultValue="false"> | defaultValue="false"> |
| <Label>Manually Enter Node ID:</Label> | <Label>Manually Enter Node ID:</Label> |
| <Description>(Not Recommended)</Description> | <Description>(Not Recommended)</Description> |
| </Field> | </Field> |
| <Field id="manualNodeID" | <Field id="manualNodeID" |
| type="textfield" | type="textfield" |
| visibleBindingId="manualEntry" | visibleBindingId="manualEntry" |
| visibleBindingValue="true"> | visibleBindingValue="true"> |
| <Label>Enter Node ID:</Label> | <Label>Enter Node ID:</Label> |
| The ConfigUI definition will result in the following dialog being presented to the user: | The ConfigUI definition will result in the following dialog being presented to the user: |
| |
| {{ConfigUIRendering.png}} | {{ConfigUIRendering.png?nolink|Configuration UI Rendering Image}} |
| |
| 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. | 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. |
| === Text Field === | === Text Field === |
| |
| {{ConfigUI_textfield.png}} | {{ConfigUI_textfield.png?nolink|Configuration UI Textfield Image}} |
| |
| <code><Field id="simpleTextField" type="textfield" enabledBindingId="checkboxSample" defaultValue="Default Value"> | <code><Field id="simpleTextField" type="textfield" enabledBindingId="checkboxSample" defaultValue="Default Value"> |
| === Popup Menu === | === Popup Menu === |
| |
| {{ConfigUI_menu.png}} | {{ConfigUI_menu.png?nolink|Configuration UI Menu Image}} |
| |
| <code><Field type="menu" id="simplePopUpButton" defaultValue="item2"> | <code><Field type="menu" id="simplePopUpButton" defaultValue="item2"> |
| |//''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.| | |//''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.| |
| |
| 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 is what is what 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. | 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 is what is what 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|button fields]], you can specify a //''CallbackMethod''// element in your field definition: | Like [[#button|button fields]], you can specify a //''CallbackMethod''// element in your field definition: |
| === List === | === List === |
| |
| {{ConfigUI_list.png}} | {{ConfigUI_list.png?nolink|COnfiguration UI List Image}} |
| |
| <code><Field type="list" id="listSample" defaultValue="item1,item3"> | <code><Field type="list" id="listSample" defaultValue="item1,item3"> |
| <Label>List:</Label> | <Label>List:</Label> |
| <List> | <List> |
| # You can pass anything you want in the filter for any purpose | # 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 | # Create a list where each entry is a list - the first item is |
| # the value attribute and last is the display string that will | # the value attribute and last is the display string that will |
| # show up in the control. All parameters are read-only. | # show up in the control. All parameters are read-only. |
| myArray = [("option1", "First Option"),("option2","Second Option")] | myArray = [("option1", "First Option"),("option2","Second Option")] |
| === Serial Port === | === Serial Port === |
| |
| {{:indigo_2023.2_documentation:configui_serialport_local.png}} | {{:indigo_2023.2_documentation:configui_serialport_local.png?nolink|Configuration UI Serial Port Local Image}} |
| |
| {{:indigo_2023.2_documentation:configui_serialport_socket.png}} | {{:indigo_2023.2_documentation:configui_serialport_socket.png?nolink|Configuration Serial Port Socket Image}} |
| |
| {{:indigo_2023.2_documentation:configui_serialport_rfc2217.png}} | {{:indigo_2023.2_documentation:configui_serialport_rfc2217.png?nolink|Configuration Serial Port RFC2217 Image}} |
| |
| <code><Field type="serialport" id="devicePortFieldId" /></code> | <code><Field type="serialport" id="devicePortFieldId" /></code> |
| === Checkbox === | === Checkbox === |
| |
| {{ConfigUI_checkbox.png}} | {{ConfigUI_checkbox.png?nolink|Configuration UI Checkbox Image}} |
| |
| <code><Field type="checkbox" id="checkboxSample" defaultValue="true"> | <code><Field type="checkbox" id="checkboxSample" defaultValue="true"> |
| <Label>Checkbox:</Label> | <Label>Checkbox:</Label> |
| <Description>What's on the right side of the checkbox</Description> | <Description>What's on the right side of the checkbox</Description> |
| === Label === | === Label === |
| |
| {{ConfigUI_label.png}} | {{ConfigUI_label.png?nolink|Configuration UI Label Image}} |
| |
| <code><Field id="exampleLabel" type="label"> | <code><Field id="exampleLabel" type="label"> |
| <Label>This is where you can put your label text. If need be it'll wrap. | <Label>This is where you can put your label text. If need be it'll wrap. |
| </Label> | </Label> |
| === Separator === | === Separator === |
| |
| {{ConfigUI_separator.png}} | {{ConfigUI_separator.png?nolink|Configuration UI Separator Image}} |
| |
| <code><Field id="simpleSeparator1" | <code><Field id="simpleSeparator1" |
| type="separator" | type="separator" |
| visibleBindingId="checkboxSample" | visibleBindingId="checkboxSample" |
| visibleBindingValue="1"/></code> | visibleBindingValue="1"/></code> |
| |
| === Button === | === Button === |
| |
| {{ConfigUI_button.png}} | {{ConfigUI_button.png?nolink|Configuration UI Button Image}} |
| |
| <code><Field id="exampleButton" | <code><Field id="exampleButton" |
| type="button" | type="button" |
| tooltip="Click this button to do something cool" | tooltip="Click this button to do something cool" |
| visibleBindingId="checkboxSample" | visibleBindingId="checkboxSample" |
| visibleBindingValue="1"> | visibleBindingValue="1"> |
| <Label>Visible button's label:</Label> | <Label>Visible button's label:</Label> |
| Aside from the standard //''Label''// element, the button field also contains two other required elements. The first is the //''Title''// element - this is the string that is displayed inside the button. The other required element is the //''CallbackMethod''// element, which contains the name of a Python method in your code that’s called when the user presses the button. 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). | Aside from the standard //''Label''// element, the button field also contains two other required elements. The first is the //''Title''// element - this is the string that is displayed inside the button. The other required element is the //''CallbackMethod''// element, which contains the name of a Python method in your code that’s called when the user presses the button. 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). |
| |
| 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. | 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 this: | The process flow is this: |
| return (True, valuesDict)</code> | return (True, valuesDict)</code> |
| |
| If you have errors that the user must correct, then you’ll return 3 things: | If you have errors that the user must correct, then you’ll return 3 things: |
| |
| - //''False''//, to indicate that validation failed | - //''False''//, to indicate that validation failed |
| |
| # Put other config UI validation here -- add errors to errorDict. | # Put other config UI validation here -- add errors to errorDict. |
| | |
| if len(errorsDict) > 0: | if len(errorsDict) > 0: |
| # Some UI fields are not valid, return corrected fields and error messages (client | # Some UI fields are not valid, return corrected fields and error messages (client |
| === 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 //''<Field>''// definitions, you simply specify a //''<URL>''// 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 later then Indigo will attempt to guess the [[indigo_2022.2_documentation:server_commands&#get_web_server_url|best base URL]]. You would then handle those form requests using [[#processing_http_requests_in_your_plugin|the built-in request handling mechanism discussed below]]. See the **Example HTTP Responder** plugin in the [[https://github.com/IndigoDomotics/IndigoSDK/releases/tag/v2022.2|SDK]] for an example. | You can also implement your own custom configuration in HTML if you prefer. Rather than adding lots of //''<Field>''// definitions, you simply specify a //''<URL>''// element. The URL specified can either be a fully specified URL <nowiki>(%%protocol://host/path%%)</nowiki> or it may be a relative URL (/some/relative/path). If it's the later then Indigo will attempt to guess the [[indigo_2022.2_documentation:server_commands&#get_web_server_url|best base URL]]. You would then handle those form requests using [[#processing_http_requests_in_your_plugin|the built-in request handling mechanism discussed below]]. See the **Example HTTP Responder** plugin in the [[https://github.com/IndigoDomotics/IndigoSDK/releases/tag/v2022.2|SDK]] for an example. |
| ==== Devices.xml ==== | ==== Devices.xml ==== |
| |
| |//''allowUserCreation''//| Attribute | No |If set, the value must either be //''true''// or //''false''// (defaults to //''true''//). This attribute is discussed in more detail below. | | |//''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.| | |//''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 //''<ConfigUi>''// 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.| | |//''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 //''<ConfigUi>''// 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.| | |//''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.| |
| |
| </code> | </code> |
| |
| {{ :indigo_2023.2_documentation:device_sub_type_example.png?nolink&600 |}} | {{ :indigo_2023.2_documentation:device_sub_type_example.png?nolink&600 |Device Subtype Example Image}} |
| |
| In Python: | In Python: |
| </States></code> | </States></code> |
| |
| Each State listed in the States element will be available in various parts of the UI, including the “Device State Changed” Event dialog. | Each State listed in the States element will be available in various parts of the UI, including the “Device State Changed” Event dialog. |
| |
| The State element has several attributes and elements that should be defined: | The State element has several attributes and elements that should be defined: |
| === Device Factory === | === Device Factory === |
| |
| {{:indigo_2023.2_documentation:screenshot_2023-02-22_at_1.07.12_pm.png?600|}} | {{:indigo_2023.2_documentation:screenshot_2023-02-22_at_1.07.12_pm.png?nolink&600|Device Factory Edit Device Group Image}} |
| |
| One way to create "multifunction" devices is to use Indigo's Device Factory method. Device Factory devices are defined by using a special ''%%<DeviceFactory>%%'' node in ''Devices.xml''. A basic Device Factory device definition would look something like this: | One way to create "multifunction" devices is to use Indigo's Device Factory method. Device Factory devices are defined by using a special ''%%<DeviceFactory>%%'' node in ''Devices.xml''. A basic Device Factory device definition would look something like this: |
| that some device props are read only even when you create them from scratch (i.e., dev.version) and some will | 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). | 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. | Note that `protocol`, `name` and `deviceTypeId` are all required with the `indigo.device.create()` method call. |
| """ | """ |
| </Actions></code> | </Actions></code> |
| |
| As with //''<Event>''//, your //''<Action>''// elements can define a //''<SupportURL>''// 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. | As with //''<Event>''//, your //''<Action>''// elements can define a //''<SupportURL>''// 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. | 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. |
| Notice the call to //''self.substitute()''// - this method is defined in the [[indigo_2023.2_documentation:plugin_guide#pluginpy|plugin base class]]. If your user inserts <code>%%v:12345%%</code> 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 <code>%%d:12345:someStateId%%</code> 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. | Notice the call to //''self.substitute()''// - this method is defined in the [[indigo_2023.2_documentation:plugin_guide#pluginpy|plugin base class]]. If your user inserts <code>%%v:12345%%</code> 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 <code>%%d:12345:someStateId%%</code> 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. |
| |
| Your plugin actions can return any Python "primitive" value to users when they call your action | 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 | with the //''executeAction''// method (a return value is not required). The return value can |
| be any one of the following object types: | be any one of the following object types: |
| |
| * None //''None''//, | * None //''None''//, |
| * booleans //''bool''//, | * booleans //''bool''//, |
| * integers //''int()''//, | * integers //''int()''//, |
| * floats //''float()''//, | * floats //''float()''//, |
| * strings //''str()''//, | * strings //''str()''//, |
| * Indigo Dicts //''indigo.Dict()''//, | * Indigo Dicts //''indigo.Dict()''//, |
| * Indigo Lists //''indigo.List()''//. | * 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). | 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). |
| try: | try: |
| (action, dev, completeHandler) = self.pluginsActionQueue.get(True, 60) | (action, dev, completeHandler) = self.pluginsActionQueue.get(True, 60) |
| | |
| # Process the requested action here. This can block and take a long | # Process the requested action here. This can block and take a long |
| # time without impacting UI usability because this method would be | # time without impacting UI usability because this method would be |
| 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. | 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. |
| |
| <color red>Note</color>: To increase the value of your plugin, you should ensure that you test and document the necessary | <color red>Note</color>: To increase the value of your plugin, you should ensure that you test and document the necessary |
| information so that Python scripters can [[indigo_2023.2_documentation:plugin_scripting_tutorial#scripting_indigo_plugins|script your plugin's actions]]. 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 [[plugins:timersandpesters#restart_timer|the Timers plugin to restart a 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 [[plugins:airfoil#scripting_support |Airfoil]] and [[plugins:timersandpesters#scripting_support|Timers and Pesters]] 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. | information so that Python scripters can [[indigo_2023.2_documentation:plugin_scripting_tutorial#scripting_indigo_plugins|script your plugin's actions]]. 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 [[plugins:timersandpesters#restart_timer|the Timers plugin to restart a 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 [[plugins:airfoil#scripting_support |Airfoil]] and [[plugins:timersandpesters#scripting_support|Timers and Pesters]] 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. |
| ==== MenuItems.xml ==== | ==== MenuItems.xml ==== |
| errorsDict = indigo.Dict() | errorsDict = indigo.Dict() |
| return (True, valuesDict, errorsDict)</code> | return (True, valuesDict, errorsDict)</code> |
| | |
| === get_menu_action_config_ui_values === | === 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: | 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: |
| === Custom HTML Menu Item Dialogs === | === Custom HTML Menu Item Dialogs === |
| |
| You can also implement your own custom menu item form in HTML if you prefer. Rather than adding //''<CallbackMethod>''// and //''<ConfigUI>''// definitions, you simply specify a //''<URL>''// 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 later then Indigo will attempt to guess the [[indigo_2023.2_documentation:server_commands&#get_web_server_url|best base URL]]. You would then handle those form requests using [[#processing_http_requests_in_your_plugin|the built-in request handling mechanism discussed below]]. See the **Example HTTP Responder** plugin in the [[https://github.com/IndigoDomotics/IndigoSDK/releases/tag/v2023.2|SDK]] for an example. | You can also implement your own custom menu item form in HTML if you prefer. Rather than adding //''<CallbackMethod>''// and //''<ConfigUI>''// definitions, you simply specify a //''<URL>''// element. The URL specified can either be a fully specified URL <nowiki>(%%protocol://host/path%%)</nowiki> or it may be a relative URL (/some/relative/path). If it's the later then Indigo will attempt to guess the [[indigo_2023.2_documentation:server_commands&#get_web_server_url|best base URL]]. You would then handle those form requests using [[#processing_http_requests_in_your_plugin|the built-in request handling mechanism discussed below]]. See the **Example HTTP Responder** plugin in the [[https://github.com/IndigoDomotics/IndigoSDK/releases/tag/v2023.2|SDK]] for an example. |
| |
| ==== SupportURL Elements ==== | ==== SupportURL Elements ==== |
| |
| ^ Method definition ^ Required ^ Notes ^ | ^ Method definition ^ Required ^ Notes ^ |
| |<code>__init__(self, | |<code>__init__(self, |
| pluginId, | pluginId, |
| pluginDisplayName, | pluginDisplayName, |
| pluginVersion, | pluginVersion, |
| pluginPrefs)</code>| Yes |This method gets a dictionary of preferences that the server read from disk. You have the opportunity here to use them or alter them if you like although you most likely will just pass them on to the plugin base class: \\ \\ <code>def __init__(self, pluginPrefs): | pluginPrefs)</code>| Yes |This method gets a dictionary of preferences that the server read from disk. You have the opportunity here to use them or alter them if you like although you most likely will just pass them on to the plugin base class: \\ \\ <code>def __init__(self, pluginPrefs): |
| indigo.PluginBase.__init__(self, pluginPrefs)</code> You'll most likely use the ''//startup//'' method, described below, to do your global plugin initialization.| | indigo.PluginBase.__init__(self, pluginPrefs)</code> You'll most likely use the ''//startup//'' method, described below, to do your global plugin initialization.| |
| indigo.PluginBase.__del__(self)</code>| | indigo.PluginBase.__del__(self)</code>| |
| |<code>startup(self)</code>| No |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). \\ \\ <code>def startup(self): | |<code>startup(self)</code>| No |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). \\ \\ <code>def startup(self): |
| indigo.server.log(u"Startup called")</code> Beginning with Indigo 2023.2, the //''startup''// method supports a return value:<html><br></html>- don't return anything (or return None explicitly) - your plugin will start up<html><br></html>- return True - your plugin will start up<html><br></html>- return False - your plugin stops with a default message <html><br></html>- return a string - your plugin stops with that string as the message<html><br></html>- return anything else - plugin stops with a default message| | indigo.server.log(u"Startup called")</code> Beginning with Indigo 2023.2, the //''startup''// method supports a return value: don't return anything (or return None explicitly) - your plugin will start up<html><br></html>- return True - your plugin will start up<html><br></html>- return False - your plugin stops with a default message <html><br></html>- return a string - your plugin stops with that string as the message<html><br></html>- return anything else - plugin stops with a default message| |
| |<code>shutdown(self)</code>| No |This method will get called when the IndigoServer wants your plugin to exit. If you define a global shutdown variable, 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 will already have had a chance to shutdown by the time this method is called (see the methods below). \\ \\ <code>def shutdown(self): | |<code>shutdown(self)</code>| No |This method will get called when the IndigoServer wants your plugin to exit. If you define a global shutdown variable, 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 will already have had a chance to shutdown by the time this method is called (see the methods below). \\ \\ <code>def shutdown(self): |
| # do any cleanup necessary before exiting</code> Note: //''shutdown''// will be called after //''runConcurrentThread''// (discussed next) so cleanup here will be after any changes that might result from a loop in //''runConcurrentThread''//.| | # do any cleanup necessary before exiting</code> Note: //''shutdown''// will be called after //''runConcurrentThread''// (discussed next) so cleanup here will be after any changes that might result from a loop in //''runConcurrentThread''//.| |
| ^ Device Specific Methods ^^^ | ^ Device Specific Methods ^^^ |
| ^ Method definition ^ Required ^ Notes ^ | ^ Method definition ^ Required ^ Notes ^ |
| |<code>getDeviceStateList(self, | |<code>getDeviceStateList(self, |
| dev)</code>| No |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 <States> 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 for an example.| | dev)</code>| No |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 <States> 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 for an example.| |
| |<code>getDeviceDisplayStateId(self, | |<code>getDeviceDisplayStateId(self, |
| dev)</code>| No |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 <UiDisplayStateId> element in your Devices.xml file. You can, however, implement the method the plugin needs to dynamically determine the which state ID to display.| | dev)</code>| No |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 <UiDisplayStateId> element in your Devices.xml file. You can, however, implement the method the plugin needs to dynamically determine the which state ID to display.| |
| |<code>deviceStartComm(self, | |<code>deviceStartComm(self, |
| dev)</code>| No |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 addrss: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).| | dev)</code>| No |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).| |
| |<code>deviceStopComm(self, | |<code>deviceStopComm(self, |
| dev)</code>| No |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.| | dev)</code>| No |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.| |
| |<code>didDeviceCommPropertyChange(self, | |<code>didDeviceCommPropertyChange(self, |
| origDev, | origDev, |
| newDev)</code>| No |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).| | newDev)</code>| No |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).| |
| |<code>deviceUpdated(self, | |<code>deviceUpdated(self, |
| origDev, | origDev, |
| newDev)</code>| No |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 both belong to you).| | newDev)</code>| No |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 both belong to you).| |
| |<code>deviceDeleted(self, | |<code>deviceDeleted(self, |
| dev)</code>| No |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.| | dev)</code>| No |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.| |
| ^ Trigger Specific Methods ^^^ | ^ Trigger Specific Methods ^^^ |
| ^ Method definition ^ Required ^ Notes ^ | ^ Method definition ^ Required ^ Notes ^ |
| |<code>triggerStartProcessing(self, | |<code>triggerStartProcessing(self, |
| trigger)</code>| No |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).| | trigger)</code>| No |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).| |
| |<code>triggerStopProcessing(self, | |<code>triggerStopProcessing(self, |
| trigger)</code>| No |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.| | trigger)</code>| No |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.| |
| |<code>didTriggerProcessingPropertyChange(self, | |<code>didTriggerProcessingPropertyChange(self, |
| origTrigger, | origTrigger, |
| newTrigger)</code>| No |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.| | newTrigger)</code>| No |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.| |
| |<code>triggerCreated(self, | |<code>triggerCreated(self, |
| trigger)</code>| No |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.| | trigger)</code>| No |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.| |
| |<code>triggerUpdated(self, | |<code>triggerUpdated(self, |
| origTrigger, | origTrigger, |
| newTrigger)</code>| No |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.| | newTrigger)</code>| No |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.| |
| |<code>triggerDeleted(self, | |<code>triggerDeleted(self, |
| trigger)</code>| No |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.| | trigger)</code>| No |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.| |
| ^ Schedule Specific Methods ^^^ | ^ Schedule Specific Methods ^^^ |
| |
| ^Method definition ^Parameters ^Return Value ^Notes ^ | ^Method definition ^Parameters ^Return Value ^Notes ^ |
| | <code>applicationWithBundleIdentifier(self, | | <code>applicationWithBundleIdentifier(self, |
| bundleID)</code> | ''//bundleID//'' - this is the bundle identifier for the app | SBApplication instance | Bundle id's are usually fully qualified strings - for iTunes it's com.apple.iTunes. What's returned is a scripting bridge SBApplication instance. See the [[http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/ScriptingBridgeConcepts/Introduction/Introduction.html | Scripting Bridge documentation]] for more information. | | bundleID)</code> | ''//bundleID//'' - this is the bundle identifier for the app | SBApplication instance | Bundle id's are usually fully qualified strings - for iTunes it's com.apple.iTunes. What's returned is a scripting bridge SBApplication instance. See the [[http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/ScriptingBridgeConcepts/Introduction/Introduction.html | Scripting Bridge documentation]] for more information. | |
| | <code>browserOpen(self, | | <code>browserOpen(self, |
| url)</code> | ''//url//'' - the url to open in the browser | None | This method will open the specified in the default browser. Note it does so on the server machine and not on any remotely connected clients. | | url)</code> | ''//url//'' - the url to open in the browser | None | This method will open the specified in the default browser. Note it does so on the server machine and not on any remotely connected clients. | |
| | <code>debugLog(self, | | <code>debugLog(self, |
| msg)</code> | ''//msg//'' - the string to insert into the event log | None | (<color red>DEPRECATED</color> - 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. | | msg)</code> | ''//msg//'' - the string to insert into the event log | None | (<color red>DEPRECATED</color> - 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. | |
| | <code>errorLog(self, | | <code>errorLog(self, |
| msg)</code> | ''//msg//'' - the string to insert into the event log | None | (<color red>DEPRECATED</color> - see Logging below) If you want an error to show up in the event log (in <color red>red text</color>), use this log method rather than //''indigo.server.log()''//. | | msg)</code> | ''//msg//'' - the string to insert into the event log | None | (<color red>DEPRECATED</color> - see Logging below) If you want an error to show up in the event log (in <color red>red text</color>), use this log method rather than //''indigo.server.log()''//. | |
| | <code>openSerial(self, | | <code>openSerial(self, |
| ownerName, | ownerName, |
| portUrl, baudrate, bytesize, | portUrl, baudrate, bytesize, |
| parity, stopbits, timeout, | parity, stopbits, timeout, |
| xonxoff, rtscts, writeTimeout, | xonxoff, rtscts, writeTimeout, |
| dsrdtr, interCharTimeout)</code> | ''//ownerName//'' - the name of the device or plugin that owns this serial port (used for error logging) - make sure that it's ASCII text with no unicode characters \\ ''//other args//'' - all other arguments are passed directly to the [[http://pyserial.sourceforge.net/pyserial_api.html#classes | pySerial's Serial contructor ]] | serial.Serial instance | This method is identical to creating a new [[http://pyserial.sourceforge.net/pyserial_api.html#classes | pySerial Serial object]] except that it never throws an exception. If the serial connection cannot be openned then None is returned and an error will be automatically logged to the Indigo Server event log. | | dsrdtr, interCharTimeout)</code> | ''//ownerName//'' - the name of the device or plugin that owns this serial port (used for error logging) - make sure that it's ASCII text with no unicode characters \\ ''//other args//'' - all other arguments are passed directly to the [[http://pyserial.sourceforge.net/pyserial_api.html#classes | pySerial's Serial contructor ]] | serial.Serial instance | This method is identical to creating a new [[http://pyserial.sourceforge.net/pyserial_api.html#classes | pySerial Serial object]] 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. | |
| | <code>sleep(self, | | <code>sleep(self, |
| seconds)</code> | ''//seconds//'' - the sleep duration as a real number | None | 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 shutdown or restart the plugin. See //''runConcurrentThread''// documentation above for more details. | | seconds)</code> | ''//seconds//'' - the sleep duration as a real number | None | 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 shutdown or restart the plugin. See //''runConcurrentThread''// documentation above for more details. | |
| | <code>substituteVariable(self, | | <code>substituteVariable(self, |
| inString, | inString, |
| validateOnly=False)</code> | ''//inString//'' - the string which contains valid variable ID \\ //''validateOnly''// - whether to return the actual string or a validation tuple where the first item is a boolean whether the formatting is valid and the variable ID exists and the second item is the error string to show (defaults to False) | //''String''// \\ if ''//validateOnly//'' is False \\ \\ //''(BOOL, errStr)''// \\ if ''//validateOnly//'' is True | This method will allow any string with the following markup to have a variable value substituted: <code>%%v:VARID%%</code>VARID is the unique variable ID as found in the UI in various places. It's recommended that you call this method twice: first, when the validation method for your action is called so that you can validate at that point if the syntax is correct and make sure the variable exists. The second time you call this method would be when you execute the action - call it before you actually use the string so that the substitutions will be made. 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. | | validateOnly=False)</code> | ''//inString//'' - the string which contains valid variable ID \\ //''validateOnly''// - whether to return the actual string or a validation tuple where the first item is a boolean whether the formatting is valid and the variable ID exists and the second item is the error string to show (defaults to False) | //''String''// \\ if ''//validateOnly//'' is False \\ \\ //''(BOOL, errStr)''// \\ if ''//validateOnly//'' is True | This method will allow any string with the following markup to have a variable value substituted: <code>%%v:VARID%%</code>VARID is the unique variable ID as found in the UI in various places. It's recommended that you call this method twice: first, when the validation method for your action is called so that you can validate at that point if the syntax is correct and make sure the variable exists. The second time you call this method would be when you execute the action - call it before you actually use the string so that the substitutions will be made. 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. | |
| | <code>substituteDeviceState(self, | | <code>substituteDeviceState(self, |
| inString, | inString, |
| validateOnly=False)</code> | ''//inString//'' - the string which contains valid device ID and state key \\ //''validateOnly''// - whether to return the actual string or a validation tuple where the first item is a boolean whether the formatting is valid and the variable ID exists and the second item is the error string to show (defaults to False) | //''String''// \\ if ''//validateOnly//'' is False \\ \\ //''(BOOL, errStr)''// \\ if ''//validateOnly//'' is True | This method will allow any string with the following markup to have a variable value substituted: <code>%%d:DEVICEID:STATEKEY%%</code>DEVICEID is the unique device ID as found in the UI in various places and the STATEKEY is the identifier for the state. It's recommended that you call this method twice: first, when the validation method for your action is called so that you can validate at that point if the syntax is correct and make sure the device exists. The second time you call this method would be when you execute the action - call it before you actually use the string so that the substitutions will be made. 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. | | validateOnly=False)</code> | ''//inString//'' - the string which contains valid device ID and state key \\ //''validateOnly''// - whether to return the actual string or a validation tuple where the first item is a boolean whether the formatting is valid and the variable ID exists and the second item is the error string to show (defaults to False) | //''String''// \\ if ''//validateOnly//'' is False \\ \\ //''(BOOL, errStr)''// \\ if ''//validateOnly//'' is True | This method will allow any string with the following markup to have a variable value substituted: <code>%%d:DEVICEID:STATEKEY%%</code>DEVICEID is the unique device ID as found in the UI in various places and the STATEKEY is the identifier for the state. It's recommended that you call this method twice: first, when the validation method for your action is called so that you can validate at that point if the syntax is correct and make sure the device exists. The second time you call this method would be when you execute the action - call it before you actually use the string so that the substitutions will be made. 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. | |
| | <code>substitute(self, | | <code>substitute(self, |
| inString, | inString, |
| validateOnly=False)</code> | ''//inString//'' and //''validateOnly''// as described in the previous two methods \\ //''substituteVariable()''//, //''substituteDeviceState()''// |See above | 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.| | validateOnly=False)</code> | ''//inString//'' and //''validateOnly''// as described in the previous two methods \\ //''substituteVariable()''//, //''substituteDeviceState()''// |See above | 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.| |
| |
| The ''self.indigo_log_handler'' is an instance of a custom [[https://docs.python.org/3.10/library/logging.html#handler-objects|Handler]] 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 (with the exception of the `info` level). For convenience, the various log levels are also represented in color. Here's an example of each level: | The ''self.indigo_log_handler'' is an instance of a custom [[https://docs.python.org/3.10/library/logging.html#handler-objects|Handler]] 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 (with the exception of the `info` level). For convenience, the various log levels are also represented in color. Here's an example of each level: |
| |
| {{indigo_2023.2_documentation:embedded_script_logging.png?nolink&600}} | {{indigo_2023.2_documentation:embedded_script_logging.png?nolink&600|Embedded Script Logging Image}} |
| |
| |
| </code> | </code> |
| |
| 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 [[https://docs.python.org/2/library/logging.html#formatter-objects|Formatter]] instance and set the handler to use that instead. | 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 [[https://docs.python.org/2/library/logging.html#formatter-objects|Formatter]] instance and set the handler to use that instead. |
| |
| === Log Levels === | === Log Levels === |
| == Custom IndigoLogHandler == | == 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 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: | 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: |
| # Get the current logging level from pluginPrefs | # Get the current logging level from pluginPrefs |
| self.debugLevel = int(self.pluginPrefs.get('showDebugLevel', "30")) | self.debugLevel = int(self.pluginPrefs.get('showDebugLevel', "30")) |
| | |
| # Set preferred log format specifier | # Set preferred log format specifier |
| log_format = '%(asctime)s.%(msecs)03d\t%(levelname)-10s\t%(name)s.%(funcName)-28s %(message)s' | log_format = '%(asctime)s.%(msecs)03d\t%(levelname)-10s\t%(name)s.%(funcName)-28s %(message)s' |
| def runConcurrentThread(self): | def runConcurrentThread(self): |
| self.logger.debug("Starting concurrent thread.") | self.logger.debug("Starting concurrent thread.") |
| | |
| try: | try: |
| x = 1 / 0 | x = 1 / 0 |
| and then logging messages will appear with the colors above. Result: | and then logging messages will appear with the colors above. Result: |
| |
| {{indigo_2023.2_documentation:embedded_script_logging.png?nolink&600}} | {{indigo_2023.2_documentation:embedded_script_logging.png?nolink&600|Embedded Script Logging Image}} |
| |
| |
| 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. | 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 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 [[https://www.indigodomo.com/account/authorizations|Authorizations page]] in the user's Indigo Account. This is also how OAuth is used to authenticate when using a external service like Alexa or Google Home. | * 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 [[https://www.indigodomo.com/account/authorizations|Authorizations page]] in the user's Indigo Account. This is also how OAuth is used to authenticate when using a external service like Alexa or Google Home. |
| |''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"]%%'' | | |''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"]%%'' | |
| |
| <color blue>**Note**</color>: the dicts mentioned above will all be //indigo.Dict// objects, not standard Python dicts. | <color blue>**Note**</color>: the dicts mentioned above will all be //indigo.Dict// objects, not standard Python dicts. |
| |
| ==== Reply ==== | ==== Reply ==== |
| ===== Setting Up a Development Environment ===== | ===== Setting Up a Development Environment ===== |
| Many plugin developers choose to write their code in an IDE (Integrated Development Environment) such as [[https://www.jetbrains.com/pycharm/|PyCharm]] or [[https://docs.python.org/3/library/pdb.html|pdb]]. There are several tips that will make using IDEs to develop Indigo Plugins more effective on the [[:setting_up_a_development_environment|Setting Up a Development Environment]] page. | Many plugin developers choose to write their code in an IDE (Integrated Development Environment) such as [[https://www.jetbrains.com/pycharm/|PyCharm]] or [[https://docs.python.org/3/library/pdb.html|pdb]]. There are several tips that will make using IDEs to develop Indigo Plugins more effective on the [[:setting_up_a_development_environment|Setting Up a Development Environment]] page. |
| | |