Python Packages and Indigo¶
In this guide
How Python 3.13 is bundled with Indigo 2026.1, 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, and the bundled macsemantic package for matching
natural-language phrases to devices and other objects.
This page describes how Indigo 2026.1.0 works with Python, specifically with respect to packages/libraries.
Indigo 2026.1 ships with API version 3.9 (check the API Version Chart 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 2026.1, 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 mentioned above and perhaps spot the changes you need. You can also post your scripts on the Help Converting to Python 3 forum to get help with any conversion issues.
The Indigo 2026.1 installer includes the Python installer (including Intel and Apple silicon support) from 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 to 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.
Installing Additional Python Packages with Indigo 2026.1¶
As noted above, Indigo 2026.1 ships with Python 3.13 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:
To see which packages are installed, use the list command:
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
Some Python packages -- and specifically those that are included in IPH3 as described
below -- will not appear in the pip3 list output.
To see more detailed information about an individual package, use the show command.
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: [email protected]
License: Apache 2.0
Location: /Library/Frameworks/Python.framework/Versions/3/lib/python3.13/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 2026.1 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 (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:
Contents/Server Plugin(inside the plugin bundle)Contents/Packages(inside the plugin bundle)- IndigoPluginHost3 (inside the app itself)
/Library/Application Support/Perceptive Automation/Python3-includes/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages
Packages are installed in several ways:
- You may add modules/packages that you develop yourself to the
Python#-includesfolders above, and those will be shared across all Indigo Plugins and Scripts. - Other packages are installed using a
pip3process. Warning: Indigo will preinstall several packages (listed below) 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 aren't 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).
- When a plugin starts up, it'll look for the
Contents/Server Plugin/requirements.txtfile. If it's not there, we'll continue with plugin startup. If it is there, we'll proceed to the next step. - 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. - We run
pip3with the requirements file and target theContents/Packagesdirectory (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 theContents/Packages/pip-install-log-success.txtfile 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.
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:
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:
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:
- IndigoPluginHost3
- /Library/Application Support/Perceptive Automation/Python3-includes
- /Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/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-includesfolder. (Indigo versions prior to 2022 should use the/Library/Application Support/Perceptive Automation/Python2-includesfolder).
Warning 1
As mentioned above, Indigo will preinstall several packages (listed below) at install time. You should avoid modifying those unless absolutely necessary as other 3rd party plugins may rely on them.
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 are a few things you can try 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.
Python 3.13.9 Packages Installed by Indigo¶
Indigo 2026.1 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 below 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 |
| macsemantic | ==1.0.0 |
| 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.
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:
This allows developers to specify a string using JavaScript style dot notation to get to parts of the box object:
Here are some concrete examples:
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 and Webhook features. You can determine whether
you have the box library installed by simply trying to import it into a scripting shell or embedded script.
macsemantic¶
Indigo 2026.1 installs macsemantic, the package behind the MCP find_*
tools. Give it a phrase and a list of named things, and it tells you which one the phrase means:
"I can't see what I'm chopping" finds Kitchen Lights, and "I'm heading to bed" finds your Bedtime
action group. Scripts and plugins can use it to let people refer to things the way they talk,
rather than by exact name.
It works in two steps:
- Word matching, on every Mac. A phrase that shares words with a name — "the porch light" and Porch Light — is found here, and when the match is clear-cut it is answered here too.
- Apple's on-device model, when the Mac has Apple Intelligence: Apple silicon, macOS 26 or later, with Apple Intelligence turned on. This is what matches by meaning, so phrases that share no words with the answer, like the two above, need it.
On an Intel Mac, on macOS before 26, or with Apple Intelligence off, the same code runs and only
the first step happens. Write your script or plugin so that it still makes sense when a phrase
isn't understood — resolver.fm_available tells you which kind of Mac you're on.
Tell It What the List Is¶
Every resolve() call needs a domain: what the candidates are. The model reads a list of
thermostats and lights differently from a list of saved scenes, so there's no default. For Indigo's
own objects, indigo.utils has one ready to pass:
| Constant | For a list of |
|---|---|
indigo.utils.SEMANTIC_DOMAIN_DEVICES |
devices |
indigo.utils.SEMANTIC_DOMAIN_ACTION_GROUPS |
action groups |
indigo.utils.SEMANTIC_DOMAIN_TRIGGERS |
triggers |
indigo.utils.SEMANTIC_DOMAIN_SCHEDULES |
schedules |
indigo.utils.SEMANTIC_DOMAIN_VARIABLES |
variables |
indigo.utils.SEMANTIC_DOMAIN_CONTROL_PAGES |
control pages |
indigo.utils.SEMANTIC_DOMAIN_FOLDERS |
folders, of any type |
indigo.utils.SEMANTIC_DOMAIN_AUTOMATIONS |
a mixed list of action groups, triggers and schedules |
The device, variable, trigger and automation constants hold the prompts the matching MCP find_*
tools were tuned with, so those lookups in your code behave the same way. For a list that isn't an Indigo object type,
write a
short description of what the items are, as you'd say it to a person — "a user's music
playlists", "rooms in the house". See the reference for
the details.
Finding a Device¶
from macsemantic import SemanticResolver
resolver = SemanticResolver(warmup=False)
devices = [
{"id": 101, "name": "Upstairs Thermostat"},
{"id": 102, "name": "Kitchen Lights"},
{"id": 103, "name": "Bedroom Fan"},
{"id": 104, "name": "Front Door Lock"},
{"id": 105, "name": "Garage Door"},
]
result = resolver.resolve("make it warmer upstairs", devices, domain=indigo.utils.SEMANTIC_DOMAIN_DEVICES)
if result["no_match"]:
indigo.server.log("No device matches that.")
else:
indigo.server.log(f"Best match: {result['results'][0]['id']}") # 101
With Apple Intelligence on, these resolve as shown:
| Phrase | Resolves to |
|---|---|
"make it warmer upstairs" |
Upstairs Thermostat |
"lock up the house" |
Front Door Lock |
"I can't see what I'm chopping" |
Kitchen Lights |
Each candidate is a dictionary with an id and a name. The id comes back in the result
unchanged, so use the Indigo object id and look the object up afterward.
Always check no_match first. When it's true, the phrase asked for something that isn't in
the list, and results holds only near misses — don't act on results[0]. You can't use a
match's score for this instead: the model always names some candidate, and names one
confidently even when you ask for "the dishwasher" in a house that has none.
Reading the Results¶
resolve() answers one question: which single item did the phrase mean? results[0] is that
answer. The rest of the list is runners-up, useful for a "did you mean…?" prompt, not additional
matches. Here is the full result for "the kitchen lights" against a house with two kitchen lights:
result = resolver.resolve("the kitchen lights", candidates, domain=indigo.utils.SEMANTIC_DOMAIN_DEVICES)
# {
# "tier": "fm",
# "no_match": False,
# "results": [
# {"id": 1, "score": 0.9, "source": "model"}, # Kitchen Ceiling Light <- the answer
# {"id": 2, "score": 0.54, "source": "model"}, # Kitchen Under-Cabinet LEDs <- the model's second choice
# {"id": 3, "score": 0.0, "source": "lexical"}, # Living Room Lamp <- filler, see below
# {"id": 4, "score": 0.0, "source": "lexical"}, # Bedroom Lamp
# {"id": 5, "score": 0.0, "source": "lexical"}, # Porch Light
# ],
# }
How to read it:
- The first entry is the answer. Its
sourcesays who chose it:"model", or"lexical"when word matching answered on its own. - The second entry, when the model had one, is its runner-up, also
"model". - Everything after that is filled in by word matching (
"lexical") so the list reachestop_n(5 unless you pass another value). A score of0.0there means "shares no words with the phrase", not "also a match". Don't treat these entries as results. - Use the order, not the scores. Scores always go down with the list, but they only rank entries within one result: they aren't a confidence, and can't be compared between calls.
- It is never a set. "All the lights" still returns one light at the top, not every light. To
act on a group of things, use Indigo itself: a folder, or a loop over
indigo.deviceswith a filter.
To act on the answer but offer the runners-up when there's doubt:
result = resolver.resolve(phrase, candidates, top_n=3, domain=indigo.utils.SEMANTIC_DOMAIN_DEVICES)
if result["no_match"]:
indigo.server.log(f"Nothing matches \"{phrase}\".")
else:
best, *others = result["results"]
indigo.server.log(f"Using {indigo.devices[best['id']].name}")
alternatives = [indigo.devices[m["id"]].name for m in others if m["score"] > 0]
if alternatives:
indigo.server.log(f"Other possibilities: {', '.join(alternatives)}")
score > 0 keeps the model's runner-up and any name that shares a word with the phrase, and drops
the zero-score filler.
Categories: Long Lists¶
The model never reads a long list whole. It is shown 16 candidates: the ones whose names share the most words with the phrase. That is normally right, but it fails when the answer shares no words with the phrase. Ask "make it warmer in here" of a house with 60 lights and one thermostat named "Ecobee", and the thermostat is never among the 16 the model sees, so the model picks a light.
category fixes that. It's an optional label on each candidate that says what kind of thing it
is. When the 16 are chosen, the resolver also adds the best candidate from every category that
would otherwise be missing, so the lone thermostat is shown to the model and wins. The model never
reads the label itself; it is only used to group the list, so any strings work as long as items of
the same kind share one.
It is not specific to devices. Some other groupings that work the same way:
| List | A good category |
|---|---|
| Devices | the device's class and subtype (below) |
| Files | the file type: "spreadsheet", "photo", "pdf" |
| Contacts | the relationship: "family", "work", "service" |
| Action groups or variables | the folder they're in |
For example, 40 invoice PDFs and one "Grocery List.txt", asked "what do I need to buy at the store": without categories the grocery list never reaches the model, so the answer is either no match or the wrong file; with a file-type category it is found.
Categories only matter for lists longer than 16. With 16 or fewer, every candidate is already shown to the model and categories change nothing. With more than 16 categories, not all of them fit.
With Your Own Devices¶
In a real script, build the candidates from indigo.devices, with a category for each device:
from macsemantic import SemanticResolver
def category(dev):
"""Return a label for what kind of device this is, for the resolver's `category` key.
Every device has a class, which type(dev).__name__ returns: "DimmerDevice",
"RelayDevice", "SensorDevice", "ThermostatDevice", "SprinklerDevice", and so on.
Many devices also have a subType naming what kind of thing they are within that
class, such as "Motion" or "Door/Window" for a sensor, or "Lock", "Siren" or
"Garage Controller" for a relay. The subType is often empty.
We combine the two ("SensorDevice:Motion") so that, say, a motion sensor and a door
sensor count as different kinds of device. Plain plugin devices ("Device" or
"PluginDevice") say nothing about what they do, so for those we use only the
subType, and an empty string when there isn't one, which means "no category".
"""
kind = type(dev).__name__
subtype = (getattr(dev, "subType", "") or "").strip()
if kind in ("Device", "PluginDevice"):
return subtype
return f"{kind}:{subtype}" if subtype else kind
resolver = SemanticResolver(warmup=False)
candidates = [{"id": dev.id, "name": dev.name, "category": category(dev)} for dev in indigo.devices]
result = resolver.resolve("the porch light", candidates, domain=indigo.utils.SEMANTIC_DOMAIN_DEVICES)
if result["no_match"]:
indigo.server.log("No device matches that.")
else:
dev = indigo.devices[result["results"][0]["id"]]
indigo.device.turnOn(dev)
This is the same grouping the MCP find_device tool uses.
Pass warmup=False in a script that resolves once and exits; the default starts loading the model
in the background, which only pays off when the same resolver is used again.
Finding Action Groups¶
The same call with a different list and domain:
from macsemantic import SemanticResolver
resolver = SemanticResolver(warmup=False)
candidates = [{"id": ag.id, "name": ag.name} for ag in indigo.actionGroups]
result = resolver.resolve("I'm heading to bed", candidates,
domain=indigo.utils.SEMANTIC_DOMAIN_ACTION_GROUPS)
if not result["no_match"]:
indigo.actionGroup.execute(result["results"][0]["id"])
For a house whose action groups are Good Morning, Movie Night, Away Mode, Bedtime and Dinner Party:
| Phrase | Resolves to |
|---|---|
"I'm heading to bed" |
Bedtime |
"we're leaving for vacation" |
Away Mode |
"time to watch a film" |
Movie Night |
"guests are coming over to eat" |
Dinner Party |
"water the lawn" |
no match |
Using It in a Plugin¶
Create one resolver when the plugin starts and reuse it for everything. Loading it once and
letting it warm up in the background makes each later lookup faster. The domain goes on each
resolve() call, so the same resolver looks up devices on one call and action groups on the next;
you never need one resolver per kind of thing:
import indigo
from macsemantic import SemanticResolver
class Plugin(indigo.PluginBase):
def __init__(self, plugin_id, plugin_display_name, plugin_version, plugin_prefs, **kwargs):
super().__init__(plugin_id, plugin_display_name, plugin_version, plugin_prefs, **kwargs)
self.resolver = SemanticResolver() # one for the whole plugin; warms up in the background
def startup(self):
if not self.resolver.available:
self.logger.error(f"Semantic resolver unavailable: {self.resolver.load_error}")
elif not self.resolver.fm_available:
self.logger.info("Apple Intelligence isn't available; matching names by words only.")
def turn_on_by_phrase(self, plugin_action, dev=None):
phrase = plugin_action.props.get("phrase", "")
candidates = [{"id": d.id, "name": d.name} for d in indigo.devices]
result = self.resolver.resolve(phrase, candidates,
domain=indigo.utils.SEMANTIC_DOMAIN_DEVICES)
if result["no_match"]:
self.logger.warning(f"No device matches \"{phrase}\".")
return
indigo.device.turnOn(result["results"][0]["id"])
def run_scene_by_phrase(self, plugin_action, dev=None):
phrase = plugin_action.props.get("phrase", "")
candidates = [{"id": ag.id, "name": ag.name} for ag in indigo.actionGroups]
result = self.resolver.resolve(phrase, candidates,
domain=indigo.utils.SEMANTIC_DOMAIN_ACTION_GROUPS)
if result["no_match"]:
self.logger.warning(f"No action group matches \"{phrase}\".")
return
indigo.actionGroup.execute(result["results"][0]["id"])
resolve() waits for the model, typically about half a second. That's fine in an action callback.
In asyncio code, run it in an executor so it doesn't hold up the event loop. run_in_executor
only passes positional arguments, so bind the domain with functools.partial:
from functools import partial
result = await loop.run_in_executor(
None, partial(self.resolver.resolve, phrase, candidates,
domain=indigo.utils.SEMANTIC_DOMAIN_DEVICES))
If the resolver can't load, available is false and resolve() raises ResolverUnavailableError,
so check available at startup, as above, rather than letting the first lookup fail.
Things to Know¶
- It won't always be right. The model matches most everyday phrases, but not all of them, and a wrong answer is repeatable — the same phrase against the same list gives the same result. For anything costly to get wrong, such as unlocking a door, confirm the match before acting.
- Macs differ. macOS 26 and macOS 27 ship different Apple models, so the same phrase can resolve differently on each. Without Apple Intelligence, only word matches are found.
- It never invents anything. Every id in a result is one you supplied.
See the macsemantic reference for every parameter, property and result field.