Writing add-ons

The manifest

Every field of niles-integration.toml.

Interface 0.3, and 0.2 and 0.1, which Niles still runs. Niles 0.2.0 runs them and installs them from GitHub: the runtime, niles plugin check, niles plugin dev, Add-on from GitHub with its approval screen, updates, rollback, turning off and removing, and in the app the generated setup panel, the plugin's log, and its pages and widgets.

Every plugin has a niles-integration.toml at the root of its repo, attached to each release. Niles reads it before downloading anything else: it decides whether the plugin can be installed, what the household is asked to approve, and what the setup panel looks like.

A complete example is in the worked example.

Top-level fields

FieldRequiredWhat it is
idyes[a-z0-9_], 2–32 characters, starting with a letter. Never changes once published.
nameyesWhat the Add-ons page calls it: "Spot prices".
versionyesSemver, 0.1.0. The release tag must be the same, with or without a leading v.
descriptionyesOne line, at most 120 characters. See below.
authorsyesList of "Name <email>" or "Name".
homepagenoUsually the repo. Linked from the setup panel.
min_nilesyesThe oldest Niles version it runs on. Niles 0.2.0 is the first with plugins, so "0.2.0" or later; "0.3.0" or later for interface 0.3.
interfaceyesThe niles:integration version the component targets: "0.3", or "0.2" or "0.1" for a plugin built before accounts and webhooks, or before MQTT. It must match the WIT the component was built with.
groupnoWhere it sits on the Add-ons page. See Groups.
logonoPath of an SVG in the release, normally "logo.svg". See Logo.
memory_mbnoMemory cap, 64 by default, at most 256. JavaScript and Python plugins want 128.
keywordsif it has toolsWords that make its tools relevant to a request. See tools.

id

The id ends up everywhere, so choose it once:

  • device source x_<id>: devices are x_spotprice:system/electricity inside Niles;
  • tool names <id>_<tool>: spotprice_prices;
  • secrets plugins.<id>.<key> in Niles's secret store;
  • menu entries <id>.<entry> in the house's [menu] settings.

It must not be the id of anything built in (mqtt, elevenlabs, cerebras, sonos, spotify, lg_tv, unifi, tado, microsoft, google, nemlig, claude_code, chatgpt, linear, and any added later) or of another plugin already installed. Prefer the product's name, short: airgradient, easee, rejseplanen.

description

One line saying what the household gets, not what the API is. The built-in catalogue's blurbs are the model:

  • "Switch and dim your Zigbee lights and WLED strips."
  • "Set the heating in each room by voice."
  • "Turn the shopping list into a filled basket."

So: "Hear what power costs hour by hour, and let rules run things when it is cheapest." Not: "Integration with the Energi Data Service DayAheadPrices API."

Groups

The Add-ons page (Settings → Add-ons) has two tabs, Connected and Available, each grouped by what an add-on does for the house. group is one of the built-in group ids:

groupShown asFor
musicMusic & TVSpeakers, music, the television
presencePresence & climateWho is home, heating, air
calendarsCalendarsCalendars
shoppingShoppingGroceries and deliveries
assistantsAssistants & toolsAI assistants and work tools

A missing or unknown group puts the plugin in Custom, shown after the built-in groups. These ids are part of the interface: Niles may add groups later, and a plugin naming a group its Niles does not know yet lands in Custom rather than failing, so naming a newer group is safe.

There is no group for what Niles needs to work at all — the broker and the providers that hear and think. Those are the base system, set up under Settings → System rather than listed with the add-ons, and a plugin cannot put itself there: essentials, the group that used to hold them, now lands in Custom like any other id Niles does not offer.

Wherever it sits, a plugin's row carries a Custom badge, and its setup panel shows where it came from: your repo (linked), authors, version and install date. Add-on from GitHub is at the end of the Available tab under its own heading.

An SVG on a 24×24 view box, like the built-in marks: one or a few paths, a brand colour or currentColor (which follows the text colour, so it works in dark mode). Niles sanitises it at install and serves its own copy, so it accepts only svg, g, path, circle, ellipse, rect, line, polyline and polygon, with geometry, fill, stroke and transform attributes. No script, style, image, use, foreignObject, links or external references, and at most 32 KiB. Without a logo the card shows a generic icon.

[permissions]

What the plugin may reach. Everything not listed is refused at run time, and the household approves this list in plain words before install. Each entry is explained in permissions.

[permissions]
http = ["api.example.com", "*.example.net"]
local_network = ["setting:address"]
devices = true
tools = true
events = ["device_state", "device_action", "presence"]
notify = true            # or "urgent"
ui = true
mqtt = [                 # interface 0.2
    { name = "printer", host = "setting:address", port = 8883, tls = "trust-device" },
]
callback = true          # interface 0.3
webhook = true           # interface 0.3

All are optional; leave out what you do not use. mqtt needs interface = "0.2" or later; see MQTT for its fields. callback and webhook need interface = "0.3"; see accounts and webhooks.

Settings

Each [[settings]] table is one field on the generated setup panel.

FieldRequiredWhat it is
keyyes[a-z0-9_]+, what config.get and config.secret take.
labelyesThe field's label.
typeyesstring, number, boolean, secret, choice or room.
requirednofalse by default. Until every required setting is filled in, the plugin is "Not set up" and does not start.
helpnoOne or two sentences under the field: where to find the value.
defaultnoUsed until the user enters something. Not allowed for secret.
optionsfor choiceA list of { value, label }.
min, maxnoFor number.

The types:

  • string: one line of text. config.get returns text.
  • number: config.get returns number (an f64).
  • boolean: a switch. config.get returns boolean.
  • secret: a password field. The value goes into Niles's encrypted secret store as plugins.<id>.<key>, is never shown again (the panel says only whether it is set), and reaches the plugin only through config.secret. config.get returns none for it.
  • choice: a drop-down of options. config.get returns the chosen option's value as text.
  • room: a drop-down of the house's rooms. config.get returns the room id, living_room, as text. Use it for "which room is this device in".
[[settings]]
key = "address"
label = "Address"
type = "string"
required = true
help = "The device's IP address or host name, such as 192.168.1.50. Give it a fixed address in your router."

[[settings]]
key = "room"
label = "Room"
type = "room"
required = true

[[settings]]
key = "api_key"
label = "API key"
type = "secret"
required = true
help = "Under Account → API in the vendor's app."

[[settings]]
key = "interval"
label = "Check every (seconds)"
type = "number"
default = 60
min = 15
max = 3600

Saving the panel restarts the plugin: stop, a fresh instance, init. Read your settings in init and you never have to handle them changing.

Niles keeps the values in its configuration as [plugins.<id>], beside its own settings, and secrets in its encrypted secret store as plugins.<id>.<key>. The setup panel writes them there, as PATCH /config with {"plugins":{"spotprice":{"area":"DK2"}}} and PUT /secrets/plugins.spotprice.api_key; a script can do the same.

How each type is drawn: string and number as a text box, boolean as a switch, choice as a menu of its options, room as a menu of the house's rooms, and secret as a password box that says whether one is saved and never shows it. A required setting is marked, help is written under its field, and a default is shown beside it ("Default: West Denmark (DK1).") rather than filled in, so an emptied field goes back to the default. Until every required setting has a value the panel reads "Not set up: needs API key", and the plugin does not start.

[[ui.menu]] and [[ui.widgets]]

Pages and widgets the plugin adds to the app. Need ui = true. Described in pages and widgets.

[[ui.menu]]
id = "electricity"
label = "Electricity"
icon = "zap"
refresh_every = 300

[[ui.widgets]]
id = "price_now"
title = "Electricity now"
room = "kitchen"           # or "setting:<key>" for a room setting's room
refresh_every = 300

What Niles checks

At install and in niles plugin check: every required field is present and well-formed; version matches the release tag; interface is one this Niles runs and min_niles is not newer than it, and the component was built for that interface; mqtt brokers have unique names, a setting: that names a string setting, and trust-device or none only for home-network addresses; id is free; settings keys are unique and defaults match their types; keywords exist if the component returns tools; surface ids are unique and [a-z0-9_]+; icons are Lucide names; the logo passes sanitising.