Skip to content

Power Monitoring with Shelly Devices

Using smart plugs to monitor appliance power consumption is a common practice. Many of these devices come with built-in power metering, making them useful for gathering statistics. The question then becomes: what tools should you use? In this case, the hardware of choice is Shelly devices, as they offer a wide variety of options.

Why Shelly Devices?

I previously used the Eve Energy Gen1 smart plugs to control my appliances. However, after transitioning away from Apple Home as my primary home automation solution, I moved to a Zigbee2MQTT and Home Assistant setup. Since this setup runs on a Kubernetes environment, pairing Thread devices was no longer feasible.

As a result, I decided to try the Shelly Smart Plug S as a replacement. My initial tests were successful, which led me to explore more of Shelly’s ecosystem. Ultimately, I ended up integrating multiple Shelly devices into my home. These include:

  • Smart Plug S – Power monitoring and control of standard outlets
  • Plus 2PM – Electric cover control
  • 1 Mini – Vent control for different rooms

Shelly devices have proven to be a reliable and flexible solution for my smart home setup.

Choosing the Right Integration Method

Shelly devices offer a wide range of connectivity options, making them highly versatile for different smart home setups. They can communicate via:

  • Local Web Server – Each device runs its own web interface for direct control.
  • Bluetooth – Useful for initial setup and short-range control.
  • MQTT – Ideal for integrating with local automation platforms like Home Assistant.
  • KNX – For those using KNX-based smart home systems.
  • REST API – Allows direct access to device data over the local network.
  • Shelly Cloud – A cloud-based option for remote access and control.

While the Shelly Cloud is convenient, it’s not an option for me—I prefer to keep all communication within my local network whenever possible. This ensures better privacy, lower latency, and independence from external services.

Integrating with My Monitoring Stack

For monitoring, my current stack revolves around Mimir as the backend and Grafana for visualization. To integrate Shelly power monitoring into this setup, I needed a solution that could export Shelly’s API data in a format compatible with Prometheus, which Mimir uses for storage.

Given this requirement, I started searching for a well-maintained Prometheus exporter that could pull power metrics from Shelly devices via their local API. Unfortunately, I found that no existing solution met my needs—either the projects were outdated, lacked proper maintenance, or didn't fully support the local API.

Building My Own Exporter

Since no suitable solution exists, I’ve decided to build my own Prometheus exporter for Shelly devices. This will allow me to efficiently gather power consumption data and integrate it into my existing monitoring stack. Stay tuned for the development process!

Developing the Prometheus Exporter

To start building the exporter, I first consulted the Prometheus "Writing Exporters" guide to understand best practices. Additionally, I looked at other Prometheus exporters to see common patterns in structuring an exporter.

Setting Up the Project

I chose Go as the programming language due to its efficiency and strong ecosystem for Prometheus exporters. For build and release automation, I set up Goreleaser, which simplifies binary distribution.

Exploring the Shelly API

Shelly provides excellent API documentation (Shelly API Docs), which helped me understand how to fetch data. I started by experimenting with curl to see what the API returns.

The key endpoints I focused on:

  1. Shelly.GetConfig – Retrieves device configuration details.
  2. Shelly.GetStatus – Fetches real-time power monitoring data.

Example API request using curl:

curl http://192.168.1.100/rpc/Shelly.GetStatus
Example response:
{
    "ble": {},
    "cloud": {
        "connected": false
    },
    "eth": {
        "ip": "10.33.55.170"
    },
    "input:0": {
        "id": 0,
        "state": false
    },
    "input:1": {
        "id": 1,
        "state": false
    },
    "input:2": {
        "id": 2,
        "state": false
    },
    "input:3": {
        "id": 3,
        "state": false
    },
    "mqtt": {
        "connected": false
    },
    "switch:0": {
        "id": 0,
        "source": "timer",
        "output": true,
        "timer_started_at": 1626935739.79,
        "timer_duration": 60,
        "apower": 8.9,
        "voltage": 237.5,
        "aenergy": {
        "total": 6.532,
        "by_minute": [
            45.199,
            47.141,
            88.397
        ],
        "minute_ts": 1626935779
        },
        "temperature": {
        "tC": 23.5,
        "tF": 74.4
        }
    },
    "switch:1": {
        "id": 1,
        "source": "init",
        "output": false,
        "apower": 0,
        "voltage": 237.5,
        "aenergy": {
        "total": 0,
        "by_minute": [
            0,
            0,
            0
        ],
        "minute_ts": 1626935779
        },
        "temperature": {
        "tC": 23.5,
        "tF": 74.4
        }
    },
    "switch:2": {
        "id": 2,
        "source": "timer",
        "output": false,
        "timer_started_at": 1626935591.8,
        "timer_duration": 345,
        "apower": 0,
        "voltage": 237.5,
        "aenergy": {
        "total": 0.068,
        "by_minute": [
            0,
            0,
            0
        ],
        "minute_ts": 1626935779
        },
        "temperature": {
        "tC": 23.5,
        "tF": 74.4
        }
    },
    "switch:3": {
        "id": 3,
        "source": "init",
        "output": false,
        "apower": 0,
        "voltage": 237.5,
        "aenergy": {
        "total": 0,
        "by_minute": [
            0,
            0,
            0
        ],
        "minute_ts": 1626935779
        },
        "temperature": {
        "tC": 23.5,
        "tF": 74.4
        }
    },
    "sys": {
        "mac": "A8032ABE54DC",
        "restart_required": false,
        "time": "16:06",
        "unixtime": 1650035219,
        "last_sync_ts": 1650035219,
        "uptime": 11081,
        "ram_size": 254744,
        "ram_free": 151560,
        "fs_size": 458752,
        "fs_free": 180224,
        "cfg_rev": 26,
        "kvs_rev": 2725,
        "schedule_rev": 0,
        "webhook_rev": 0,
        "available_updates": {
        "stable": {
            "version": "0.10.1"
        }
        }
    },
    "wifi": {
        "sta_ip": null,
        "status": "disconnected",
        "ssid": null,
        "rssi": 0
    },
    "ws": {
        "connected": true
    }
}

Since my goal is to export power monitoring metrics, I only needed read-only endpoints like GetStatus—controlling the devices was not a priority.

Wrapping API Responses in Go Structs

To work with the API responses in Go, I created structs that mirror the Shelly API’s JSON format:

type ShellyGetStatusResponse struct {
    BLE   map[string]interface{}   `json:"ble"`
    Cloud struct{ Connected bool } `json:"cloud"`
    Eth   struct{ IP string }      `json:"eth"`
    MQTT  struct{ Connected bool } `json:"mqtt"`
    Sys   Sys                      `json:"sys"`
    Wifi  Wifi                     `json:"wifi"`
    WS    struct{ Connected bool } `json:"ws"`
}

Converting API Data into Prometheus Metrics

With the API data structured, I integrated Prometheus' Go client library to expose metrics. I wrote a collector that fetches data and converts it into Prometheus metrics.

Converting API responses into metrics
package ShellyGetStatus

import (
    "fmt"

    "github.com/prometheus/client_golang/prometheus"
    "github.com/supporterino/shelly_exporter/client"
)

type ShellyGetStatusMetrics struct {
    Uptime   *prometheus.GaugeVec
    RAM      *prometheus.GaugeVec
    FS       *prometheus.GaugeVec
    WIFIRSSI *prometheus.GaugeVec
}

var metrics *ShellyGetStatusMetrics

func RegisterShellyGetStatusMetrics() {
    metrics = &ShellyGetStatusMetrics{
        Uptime: prometheus.NewGaugeVec(prometheus.GaugeOpts{
            Namespace: "shelly",
            Subsystem: "system",
            Name:      "uptime",
            Help:      "System uptime in seconds",
        }, []string{"device_mac"}),
        RAM: prometheus.NewGaugeVec(prometheus.GaugeOpts{
            Namespace: "shelly",
            Subsystem: "system",
            Name:      "ram",
            Help:      "RAM sizes free and used in bytes",
        }, []string{"device_mac", "kind"}),
        FS: prometheus.NewGaugeVec(prometheus.GaugeOpts{
            Namespace: "shelly",
            Subsystem: "system",
            Name:      "fs",
            Help:      "FS sizes free and used in bytes",
        }, []string{"device_mac", "kind"}),
        WIFIRSSI: prometheus.NewGaugeVec(prometheus.GaugeOpts{
            Namespace: "shelly",
            Subsystem: "system",
            Name:      "wifi_rssi",
            Help:      "Wi-Fi RSSI signal strength in dBm",
        }, []string{"device_mac", "ssid", "sta_ip"}),
    }

    prometheus.MustRegister(
        metrics.Uptime,
        metrics.FS,
        metrics.RAM,
        metrics.WIFIRSSI,
    )
}

func UpdateShellyStatusMetrics(apiClient *client.APIClient) error {
    var config client.ShellyGetStatusResponse
    err := apiClient.FetchData("/rpc/Shelly.GetStatus", &config)
    if err != nil {
        return fmt.Errorf("error fetching config: %w", err)
    }

    metrics.UpdateMetrics(config)

    return nil
}

func (m *ShellyGetStatusMetrics) UpdateMetrics(status client.ShellyGetStatusResponse) {
    deviceMAC := status.Sys.MAC

    m.Uptime.WithLabelValues(deviceMAC).Set(float64(status.Sys.Uptime))
    m.RAM.WithLabelValues(deviceMAC, "free").Set(float64(status.Sys.RAMFree))
    m.RAM.WithLabelValues(deviceMAC, "max").Set(float64(status.Sys.RAMSize))
    m.FS.WithLabelValues(deviceMAC, "free").Set(float64(status.Sys.FSFree))
    m.FS.WithLabelValues(deviceMAC, "max").Set(float64(status.Sys.FSSize))
    m.WIFIRSSI.WithLabelValues(deviceMAC, *status.Wifi.SSID, *status.Wifi.StaIP).Set(float64(status.Wifi.RSSI))
}

Exposing Metrics via HTTP

Finally, I used Go’s built-in HTTP server along with Prometheus’ HTTP handler to serve the collected metrics:

package main

import (
    "fmt"
    "net/http"

    "github.com/prometheus/client_golang/prometheus/promhttp"
)

func main() {
    // Expose endpoints
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte(`<html>
             <head><title>Shelly Exporter</title></head>
             <body>
             <h1>Haproxy Exporter</h1>
             <p><a href=/metrics>Metrics</a></p>
             </body>
             </html>`))
    })
    http.Handle("/metrics", promhttp.Handler())

    logger.Info("Starting Prometheus exporter", slog.String("address", cfg.ListenAddress))
    if err := http.ListenAndServe(cfg.ListenAddress, nil); err != nil {
        logger.Error("Error starting HTTP server", slog.Any("error", err))
    }
}

Next Steps

With this basic structure in place, the next steps include:

  • Adding error handling and retries for API calls.
  • Supporting multiple devices dynamically.
  • Writing tests and refining the exporter for production use.

This initial prototype lays the foundation for a fully functional Prometheus exporter for Shelly devices, allowing seamless integration with Mimir and Grafana for real-time power monitoring.

Packaging and Deployment

To ensure that the Shelly Exporter is easy to install and use across different environments, I leverage Goreleaser to build and publish binaries for all major platforms, as well as a Docker image for containerized deployments. Additionally, I provide a Helm chart for seamless deployment on Kubernetes.

Multi-Platform Binary Releases

Using Goreleaser, I generate pre-built binaries for Linux, macOS, and Windows. This makes it easy for users to run the exporter on their preferred operating system without needing to compile it manually.

Example Goreleaser configuration snippet (.goreleaser.yaml):

builds:
  - main: ./cmd/exporter
    env:
      - CGO_ENABLED=0
    goos:
      - linux
      - windows
      - darwin
    mod_timestamp: "{{ .CommitTimestamp }}"

With this setup, each release automatically includes pre-built binaries, which can be downloaded from the GitHub Releases page.

Docker Image for Containerized Deployment

For those who prefer running the exporter in a containerized environment, I also publish a Docker image. The Dockerfile is straightforward:

FROM golang:1.23-alpine AS build

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .

RUN CGO_ENABLED=0 GOOS=linux go build -o shelly_exporter ./cmd/exporter

FROM scratch

COPY config.yaml /
COPY --from=build /app/shelly_exporter .

ENTRYPOINT ["/shelly_exporter", "-config", "config.yaml"]

The Docker image is available on GitHub ContainerRegistry, making it easy to pull and run:

docker run -v /path/to/config.yaml:/config.yaml -p 8080:8080 ghcr.io/supporterino/shelly_exporter

Deploying to Kubernetes with Helm

To integrate the exporter into Kubernetes environments, I provide a Helm chart that simplifies deployment. This allows users to install the exporter with a single command:

helm repo add supporterino https://supporterino.github.io/shelly_exporter
helm install my-shelly-exporter supporterino/shelly-exporter -f values.yaml

The Helm chart includes:

  • Deployment and Service definitions
  • ServiceMonitor to scrape the metrics
  • Values wrapping for the config file

Summary

With these packaging and deployment options, the Shelly Exporter can be easily used in a variety of environments, whether running locally, in a container, or within a Kubernetes cluster. This ensures flexibility and accessibility for different use cases.

The latest releases, including binaries, Docker images, and the Helm chart, are available at: 👉 GitHub Releases 🚀

Conclusion and Future Plans

That’s all for this blog post! 🚀 The Shelly Exporter is now up and running, providing seamless integration between Shelly devices and Prometheus-based monitoring stacks like Mimir and Grafana.

This is just the beginning—I plan to actively maintain and improve the exporter by adding support for more Shelly devices, enhancing metrics coverage, and optimizing performance over time. As new devices and firmware updates roll out, I’ll ensure compatibility and introduce additional features where needed.

Get Involved!

A full documentation is available on GitHub, including setup instructions, API details, and deployment guides:
👉 GitHub Repository

I will also be publishing pre-built Grafana dashboards, making it even easier to visualize Shelly device metrics right out of the box.

I’m always looking for feedback and contributions—whether it’s feature suggestions, bug reports, or code contributions. Feel free to open issues or pull requests on GitHub!

Thanks for reading, and happy monitoring! 📊🔌