Plugin System

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.

Quick Start

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 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 --help

How Plugins Work

Plugins are executable programs named shelly-<name> that:

  1. Discovered automatically from ~/.config/shelly/plugins/ and $PATH
  2. Invoked transparently - shelly myext runs shelly-myext
  3. Receive context via environment variables (devices, config, theme)
  4. Output handled - stdout/stderr passed through to user
1
2
User runs:     shelly myext --flag arg
CLI executes:  ~/.config/shelly/plugins/shelly-myext --flag arg

Plugin Commands

CommandAliasesDescription
shelly plugin listls, lList installed plugins
shelly plugin install <source>addInstall from file, URL, or GitHub
shelly plugin remove <name>rm, uninstallRemove a plugin
shelly plugin upgrade [name]updateUpgrade plugin(s)
shelly plugin create <name>new, initCreate plugin scaffold
shelly plugin exec <name>runExecute plugin explicitly

Environment Variables

Plugins receive these environment variables:

VariableDescriptionExample
SHELLY_CONFIG_PATHPath to config file~/.config/shelly/config.yaml
SHELLY_DEVICES_JSONJSON of registered devices{"kitchen": {"ip": "192.168.1.100"}}
SHELLY_OUTPUT_FORMATCurrent output formattable, json, yaml
SHELLY_NO_COLORColor disabled1 if disabled
SHELLY_VERBOSEVerbose mode1 if enabled
SHELLY_QUIETQuiet mode1 if enabled
SHELLY_API_MODEAPI modelocal, cloud, auto
SHELLY_THEMECurrent theme namedracula

Installation Sources

Local File

1
2
shelly plugin install ./shelly-myext
shelly plugin install /path/to/shelly-myext

GitHub Repository

1
2
shelly plugin install gh:user/shelly-myext
shelly plugin install github:user/shelly-myext

Downloads the latest release binary for your platform (linux/darwin, amd64/arm64).

HTTP URL

1
shelly plugin install https://example.com/shelly-myext

Creating Plugins

Using the Scaffold Command

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 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

Plugin Requirements

  1. Naming: Must be named shelly-<name> (e.g., shelly-notify)
  2. Executable: Must have executable permissions
  3. Version flag: Should support --version for version detection
  4. Help flag: Should support --help for usage info

Bash Plugin Template

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#!/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 "$@"

Go Plugin Template

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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)
    }
}

Python Plugin Template

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#!/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()

Included Example: shelly-notify

The repository includes a complete example plugin at examples/plugins/shelly-notify/. This plugin sends desktop notifications for Shelly device events.

Features

  • Send custom notifications
  • Check device status and notify
  • Monitor device online/offline state
  • Report power consumption
  • Cross-platform (Linux notify-send, macOS osascript)

Usage

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 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 test

See examples/plugins/shelly-notify/README.md for full documentation.

Plugin Discovery

Plugins are discovered in this order (first match wins):

  1. ~/.config/shelly/plugins/ - User plugin directory
  2. Custom paths from config (plugins.path in config.yaml)
  3. $PATH directories - System-wide plugins
1
2
3
4
5
# config.yaml
plugins:
  path:
    - /opt/shelly-plugins
    - ~/custom-plugins

Best Practices

1. Handle Missing Environment

1
2
# Don't assume SHELLY_DEVICES_JSON exists
devices="${SHELLY_DEVICES_JSON:-{}}"

2. Respect Output Format

1
2
3
4
5
if [[ "$SHELLY_OUTPUT_FORMAT" == "json" ]]; then
    echo '{"status": "ok"}'
else
    echo "Status: OK"
fi

3. Respect Color Settings

1
2
3
4
5
6
7
8
if [[ -z "$SHELLY_NO_COLOR" ]]; then
    GREEN='\033[0;32m'
    NC='\033[0m'
else
    GREEN=''
    NC=''
fi
echo -e "${GREEN}Success${NC}"

4. Exit Codes

CodeMeaning
0Success
1General error
2Invalid arguments

5. Version Output

Keep version output simple for parsing:

1
shelly-myext version 0.1.0

Publishing Plugins

GitHub Releases

  1. Create a GitHub repository named shelly-<name>
  2. Build binaries for each platform
  3. Create a release with assets:
    • shelly-myext-linux-amd64.tar.gz
    • shelly-myext-linux-arm64.tar.gz
    • shelly-myext-darwin-amd64.tar.gz
    • shelly-myext-darwin-arm64.tar.gz

Users can then install with:

1
shelly plugin install gh:youruser/shelly-myext

GoReleaser Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# .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 }}"

Plugin Manifest System

Plugins are stored with metadata that enables automatic upgrades and source tracking.

Directory Structure

1
2
3
4
5
6
7
8
~/.config/shelly/plugins/
├── shelly-myext/
│   ├── shelly-myext          # Binary executable
│   └── manifest.json         # Metadata file
├── shelly-another/
│   ├── shelly-another
│   └── manifest.json
└── .migrated                  # Migration marker

Manifest Contents

Each plugin has a manifest.json tracking:

FieldDescription
schema_versionManifest format version
namePlugin name (without shelly- prefix)
versionSemantic version
installed_atInstallation timestamp
updated_atLast upgrade timestamp
source.typeInstallation source: github, url, local, unknown
source.urlSource URL (GitHub or HTTP)
source.refGit tag/commit for GitHub sources
binary.checksumSHA256 checksum for integrity
binary.platformPlatform (e.g., linux-amd64)

Example Manifest

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
{
  "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
  }
}

Upgrade Behavior by Source Type

SourceUpgrade Behavior
githubChecks GitHub releases, downloads newer version if available
urlRe-downloads from URL, replaces if checksum differs
localCannot auto-upgrade - reinstall manually
unknownMigrated plugin - reinstall to enable auto-upgrade

Migration from Legacy Format

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:

1
2
3
4
5
6
# Check which plugins need reinstallation
shelly plugin list

# Reinstall from GitHub to enable upgrades
shelly plugin remove myext
shelly plugin install gh:user/shelly-myext

Enhanced Plugin Capabilities

Plugins 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.

Capabilities

The capabilities field in the manifest declares what a plugin can do:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "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/"
    }
  }
}
FieldTypeDescription
device_detectionbooleanPlugin can detect devices during shelly discover
platformstringPlatform name (e.g., “tasmota”, “esphome”)
componentsarrayControllable component types: “switch”, “light”, “cover”, “sensor”, “energy”
firmware_updatesbooleanPlugin supports firmware update operations
hintsobjectHelpful messages for unsupported commands (key=command, value=hint)

Hooks

The hooks field defines executable entry points that shelly-cli calls:

1
2
3
4
5
6
7
8
9
{
  "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"
  }
}

Detect Hook

Called during shelly discover to probe if an address belongs to this platform.

Input:

1
./shelly-myext detect --address=192.168.1.100 [--auth-user=<user> --auth-pass=<pass>]

Output: JSON DeviceDetectionResult:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "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

Status Hook

Called to get device status.

Input:

1
./shelly-myext status --address=192.168.1.100 [--auth-user=<user> --auth-pass=<pass>]

Output: JSON DeviceStatusResult:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  "online": true,
  "components": {
    "switch:0": {"output": true}
  },
  "sensors": {
    "wifi_rssi": -52
  },
  "energy": {
    "power": 45.3,
    "voltage": 121.5,
    "current": 0.372,
    "total": 123.456
  }
}

Control Hook

Called to execute device control commands.

Input:

1
./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:

1
2
3
4
{
  "success": true,
  "state": "on"
}

Check Updates Hook

Called to check for firmware updates.

Input:

1
./shelly-myext check-updates --address=192.168.1.100 [--auth-user=<user> --auth-pass=<pass>]

Output: JSON FirmwareUpdateInfo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "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"
}

Apply Update Hook

Called to apply a firmware update.

Input:

1
./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:

1
2
3
4
5
{
  "success": true,
  "message": "Update initiated",
  "rebooting": true
}

Device Lifecycle

When a plugin declares device_detection: true, it participates in the discovery flow:

  1. Discovery: shelly discover scans the network
  2. Detection: For addresses that aren’t Shelly devices, each detection-capable plugin’s detect hook is called
  3. Registration: Detected devices are registered with platform: "<plugin-platform>"
  4. Command Routing: When running commands on plugin-managed devices, the CLI routes to the appropriate plugin hook

Environment Variables for Hooks

Hooks receive the standard plugin environment variables plus:

VariableDescription
SHELLY_PLUGIN_DIRDirectory where the plugin is installed
SHELLY_CLI_VERSIONCLI version for compatibility checks

Complete Manifest Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
  "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"
  }
}

Troubleshooting

Plugin not found

1
2
3
4
5
6
7
8
# 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

Plugin crashes on load

1
2
3
4
5
6
# 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

Environment variables not set

1
2
# Debug environment
shelly plugin exec myext env | grep SHELLY_