Plugins extend Shelly CLI functionality. They are standalone executables that integrate seamlessly with the CLI.
Terminology: "Plugin" is the primary term. "Extension" is an alias for compatibility.
# Create a new plugin scaffold
shelly plugin create myext --lang bash
# Test locally
./shelly-myext/shelly-myext --help
# Install
shelly plugin install ./shelly-myext/shelly-myext
# Run
shelly myext --helpPlugins are executable programs named shelly-<name> that:
- Discovered automatically from
~/.config/shelly/plugins/and$PATH - Invoked transparently -
shelly myextrunsshelly-myext - Receive context via environment variables (devices, config, theme)
- Output handled - stdout/stderr passed through to user
User runs: shelly myext --flag arg
CLI executes: ~/.config/shelly/plugins/shelly-myext --flag arg
| Command | Aliases | Description |
|---|---|---|
shelly plugin list |
ls, l |
List installed plugins |
shelly plugin install <source> |
add |
Install from file, URL, or GitHub |
shelly plugin remove <name> |
rm, uninstall |
Remove a plugin |
shelly plugin upgrade [name] |
update |
Upgrade plugin(s) |
shelly plugin create <name> |
new, init |
Create plugin scaffold |
shelly plugin exec <name> |
run |
Execute plugin explicitly |
Plugins receive these environment variables:
| Variable | Description | Example |
|---|---|---|
SHELLY_CONFIG_PATH |
Path to config file | ~/.config/shelly/config.yaml |
SHELLY_DEVICES_JSON |
JSON of registered devices | {"kitchen": {"ip": "192.168.1.100"}} |
SHELLY_OUTPUT_FORMAT |
Current output format | table, json, yaml |
SHELLY_NO_COLOR |
Color disabled | 1 if disabled |
SHELLY_VERBOSE |
Verbose mode | 1 if enabled |
SHELLY_QUIET |
Quiet mode | 1 if enabled |
SHELLY_API_MODE |
API mode | local, cloud, auto |
SHELLY_THEME |
Current theme name | dracula |
shelly plugin install ./shelly-myext
shelly plugin install /path/to/shelly-myextshelly plugin install gh:user/shelly-myext
shelly plugin install github:user/shelly-myextDownloads the latest release binary for your platform (linux/darwin, amd64/arm64).
shelly plugin install https://example.com/shelly-myext# Bash plugin (default)
shelly plugin create myext
# Go plugin
shelly plugin create myext --lang go
# Python plugin
shelly plugin create myext --lang python
# Custom output directory
shelly plugin create myext --output ~/projects- Naming: Must be named
shelly-<name>(e.g.,shelly-notify) - Executable: Must have executable permissions
- Version flag: Should support
--versionfor version detection - Help flag: Should support
--helpfor usage info
#!/usr/bin/env bash
# shelly-myext - Shelly CLI Plugin
set -euo pipefail
VERSION="0.1.0"
show_help() {
cat << EOF
shelly-myext - Description of what this plugin does
Usage: shelly myext [command] [options]
Commands:
help Show this help message
version Show version information
Options:
-h, --help Show help
-v, --version Show version
Environment:
SHELLY_DEVICES_JSON - JSON of registered devices
SHELLY_CONFIG_PATH - Path to config file
EOF
}
main() {
case "${1:-help}" in
-h|--help|help) show_help ;;
-v|--version|version) echo "shelly-myext version $VERSION" ;;
*)
echo "Unknown command: $1" >&2
exit 1
;;
esac
}
main "$@"package main
import (
"encoding/json"
"fmt"
"os"
)
const version = "0.1.0"
func main() {
if len(os.Args) < 2 {
showHelp()
return
}
switch os.Args[1] {
case "-h", "--help", "help":
showHelp()
case "-v", "--version", "version":
fmt.Printf("shelly-myext version %s\n", version)
case "devices":
listDevices()
default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", os.Args[1])
os.Exit(1)
}
}
func showHelp() {
fmt.Println(`shelly-myext - Description
Usage: shelly myext [command]
Commands:
help Show help
version Show version
devices List devices from environment`)
}
func listDevices() {
devicesJSON := os.Getenv("SHELLY_DEVICES_JSON")
if devicesJSON == "" {
fmt.Println("No devices (SHELLY_DEVICES_JSON not set)")
return
}
var devices map[string]any
if err := json.Unmarshal([]byte(devicesJSON), &devices); err != nil {
fmt.Fprintf(os.Stderr, "Failed to parse devices: %v\n", err)
os.Exit(1)
}
fmt.Printf("Found %d device(s):\n", len(devices))
for name := range devices {
fmt.Printf(" - %s\n", name)
}
}#!/usr/bin/env python3
"""shelly-myext - Shelly CLI Plugin"""
import json
import os
import sys
VERSION = "0.1.0"
def show_help():
print("""shelly-myext - Description
Usage: shelly myext [command]
Commands:
help Show help
version Show version
devices List devices""")
def list_devices():
devices_json = os.environ.get("SHELLY_DEVICES_JSON", "{}")
try:
devices = json.loads(devices_json)
except json.JSONDecodeError:
print("Failed to parse devices", file=sys.stderr)
sys.exit(1)
print(f"Found {len(devices)} device(s):")
for name in devices:
print(f" - {name}")
def main():
args = sys.argv[1:]
cmd = args[0] if args else "help"
if cmd in ("-h", "--help", "help"):
show_help()
elif cmd in ("-v", "--version", "version"):
print(f"shelly-myext version {VERSION}")
elif cmd == "devices":
list_devices()
else:
print(f"Unknown command: {cmd}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()The repository includes a complete example plugin at examples/plugins/shelly-notify/. This plugin sends desktop notifications for Shelly device events.
- Send custom notifications
- Check device status and notify
- Monitor device online/offline state
- Report power consumption
- Cross-platform (Linux notify-send, macOS osascript)
# Install the example plugin
shelly plugin install examples/plugins/shelly-notify/shelly-notify
# Send a custom notification
shelly notify send "Kitchen" "Light turned on"
# Check device status
shelly notify device kitchen
# Check if device is online
shelly notify online kitchen
# Get power consumption notification
shelly notify power kitchen
# Test notification system
shelly notify testSee examples/plugins/shelly-notify/README.md for full documentation.
Plugins are discovered in this order (first match wins):
~/.config/shelly/plugins/- User plugin directory- Custom paths from config (
plugins.pathin config.yaml) $PATHdirectories - System-wide plugins
# config.yaml
plugins:
path:
- /opt/shelly-plugins
- ~/custom-plugins# Don't assume SHELLY_DEVICES_JSON exists
devices="${SHELLY_DEVICES_JSON:-{}}"if [[ "$SHELLY_OUTPUT_FORMAT" == "json" ]]; then
echo '{"status": "ok"}'
else
echo "Status: OK"
fiif [[ -z "$SHELLY_NO_COLOR" ]]; then
GREEN='\033[0;32m'
NC='\033[0m'
else
GREEN=''
NC=''
fi
echo -e "${GREEN}Success${NC}"| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error |
| 2 | Invalid arguments |
Keep version output simple for parsing:
shelly-myext version 0.1.0
- Create a GitHub repository named
shelly-<name> - Build binaries for each platform
- Create a release with assets:
shelly-myext-linux-amd64.tar.gzshelly-myext-linux-arm64.tar.gzshelly-myext-darwin-amd64.tar.gzshelly-myext-darwin-arm64.tar.gz
Users can then install with:
shelly plugin install gh:youruser/shelly-myext# .goreleaser.yaml
project_name: shelly-myext
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- amd64
- arm64
archives:
- format: tar.gz
name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}"Plugins are stored with metadata that enables automatic upgrades and source tracking.
~/.config/shelly/plugins/
βββ shelly-myext/
β βββ shelly-myext # Binary executable
β βββ manifest.json # Metadata file
βββ shelly-another/
β βββ shelly-another
β βββ manifest.json
βββ .migrated # Migration marker
Each plugin has a manifest.json tracking:
| Field | Description |
|---|---|
schema_version |
Manifest format version |
name |
Plugin name (without shelly- prefix) |
version |
Semantic version |
installed_at |
Installation timestamp |
updated_at |
Last upgrade timestamp |
source.type |
Installation source: github, url, local, unknown |
source.url |
Source URL (GitHub or HTTP) |
source.ref |
Git tag/commit for GitHub sources |
binary.checksum |
SHA256 checksum for integrity |
binary.platform |
Platform (e.g., linux-amd64) |
{
"schema_version": "1",
"name": "notify",
"version": "1.2.0",
"installed_at": "2024-12-15T10:30:00Z",
"updated_at": "2024-12-15T10:30:00Z",
"source": {
"type": "github",
"url": "https://github.com/user/shelly-notify",
"ref": "v1.2.0",
"asset": "shelly-notify-linux-amd64.tar.gz"
},
"binary": {
"name": "shelly-notify",
"checksum": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"platform": "linux-amd64",
"size": 5242880
}
}| Source | Upgrade Behavior |
|---|---|
github |
Checks GitHub releases, downloads newer version if available |
url |
Re-downloads from URL, replaces if checksum differs |
local |
Cannot auto-upgrade - reinstall manually |
unknown |
Migrated plugin - reinstall to enable auto-upgrade |
Plugins installed before the manifest system are automatically migrated on first CLI run. Migrated plugins have source.type: "unknown" - reinstall them to enable auto-upgrade:
# Check which plugins need reinstallation
shelly plugin list
# Reinstall from GitHub to enable upgrades
shelly plugin remove myext
shelly plugin install gh:user/shelly-myextPlugins can declare capabilities and hooks to integrate deeply with shelly-cli. This enables features like device detection during discovery, device control via unified commands, and firmware updates.
The capabilities field in the manifest declares what a plugin can do:
{
"capabilities": {
"device_detection": true,
"platform": "tasmota",
"components": ["switch", "light", "sensor", "energy"],
"firmware_updates": true,
"hints": {
"scene": "Tasmota uses Rules for automation. See: https://tasmota.github.io/docs/Rules/",
"script": "Tasmota uses Berry scripting on ESP32. See: https://tasmota.github.io/docs/Berry/"
}
}
}| Field | Type | Description |
|---|---|---|
device_detection |
boolean | Plugin can detect devices during shelly discover |
platform |
string | Platform name (e.g., "tasmota", "esphome") |
components |
array | Controllable component types: "switch", "light", "cover", "sensor", "energy" |
firmware_updates |
boolean | Plugin supports firmware update operations |
hints |
object | Helpful messages for unsupported commands (key=command, value=hint) |
The hooks field defines executable entry points that shelly-cli calls:
{
"hooks": {
"detect": "./shelly-myext detect",
"status": "./shelly-myext status",
"control": "./shelly-myext control",
"check_updates": "./shelly-myext check-updates",
"apply_update": "./shelly-myext apply-update"
}
}Called during shelly discover to probe if an address belongs to this platform.
Input:
./shelly-myext detect --address=192.168.1.100 [--auth-user=<user> --auth-pass=<pass>]Output: JSON DeviceDetectionResult:
{
"detected": true,
"platform": "tasmota",
"device_id": "sonoff-basic-1234",
"device_name": "Garage Light",
"model": "Sonoff Basic R3",
"firmware": "14.3.0",
"mac": "AA:BB:CC:DD:EE:FF",
"components": [
{"type": "switch", "id": 0, "name": "Relay 1"}
]
}Exit codes: 0 = detected, 1 = not this platform
Called to get device status.
Input:
./shelly-myext status --address=192.168.1.100 [--auth-user=<user> --auth-pass=<pass>]Output: JSON DeviceStatusResult:
{
"online": true,
"components": {
"switch:0": {"output": true}
},
"sensors": {
"wifi_rssi": -52
},
"energy": {
"power": 45.3,
"voltage": 121.5,
"current": 0.372,
"total": 123.456
}
}Called to execute device control commands.
Input:
./shelly-myext control --address=192.168.1.100 --action=<on|off|toggle> --component=<switch|light|cover> --id=<n> [--auth-user=<user> --auth-pass=<pass>]Output: JSON ControlResult:
{
"success": true,
"state": "on"
}Called to check for firmware updates.
Input:
./shelly-myext check-updates --address=192.168.1.100 [--auth-user=<user> --auth-pass=<pass>]Output: JSON FirmwareUpdateInfo:
{
"current_version": "14.3.0",
"latest_stable": "15.2.0",
"latest_beta": "15.3.0b1",
"has_update": true,
"has_beta_update": true,
"ota_url_stable": "http://ota.tasmota.com/tasmota/release/tasmota.bin.gz",
"ota_url_beta": "http://ota.tasmota.com/tasmota/tasmota.bin.gz",
"chip_type": "ESP8266"
}Called to apply a firmware update.
Input:
./shelly-myext apply-update --address=192.168.1.100 --stage=<stable|beta> [--url=<custom_ota_url>] [--auth-user=<user> --auth-pass=<pass>]Output: JSON UpdateResult:
{
"success": true,
"message": "Update initiated",
"rebooting": true
}When a plugin declares device_detection: true, it participates in the discovery flow:
- Discovery:
shelly discoverscans the network - Detection: For addresses that aren't Shelly devices, each detection-capable plugin's
detecthook is called - Registration: Detected devices are registered with
platform: "<plugin-platform>" - Command Routing: When running commands on plugin-managed devices, the CLI routes to the appropriate plugin hook
Hooks receive the standard plugin environment variables plus:
| Variable | Description |
|---|---|
SHELLY_PLUGIN_DIR |
Directory where the plugin is installed |
SHELLY_CLI_VERSION |
CLI version for compatibility checks |
{
"schema_version": "1",
"name": "tasmota",
"version": "1.0.0",
"description": "Tasmota device support for shelly-cli",
"installed_at": "2024-12-25T10:30:00Z",
"source": {
"type": "github",
"url": "https://github.com/user/shelly-tasmota",
"ref": "v1.0.0"
},
"binary": {
"name": "shelly-tasmota",
"checksum": "sha256:abc123...",
"platform": "linux-amd64"
},
"minimum_shelly_version": "1.0.0",
"capabilities": {
"device_detection": true,
"platform": "tasmota",
"components": ["switch", "light", "sensor", "energy"],
"firmware_updates": true,
"hints": {
"scene": "Tasmota uses Rules for automation",
"script": "Tasmota uses Berry scripting on ESP32",
"schedule": "Tasmota uses Timers for scheduling"
}
},
"hooks": {
"detect": "./shelly-tasmota detect",
"status": "./shelly-tasmota status",
"control": "./shelly-tasmota control",
"check_updates": "./shelly-tasmota check-updates",
"apply_update": "./shelly-tasmota apply-update"
}
}# Check if plugin is installed
shelly plugin list
# Check plugin path
ls -la ~/.config/shelly/plugins/
# Verify executable permission
chmod +x ~/.config/shelly/plugins/shelly-myext# Run plugin directly to see errors
~/.config/shelly/plugins/shelly-myext --help
# Check for missing dependencies
ldd ~/.config/shelly/plugins/shelly-myext # Linux
otool -L ~/.config/shelly/plugins/shelly-myext # macOS# Debug environment
shelly plugin exec myext env | grep SHELLY_