Skip to content

Power consumption

With this beautiful monitoring stack and all the effort I put on the system optimisation to make the homelab power efficient, it is a nice feature to monitor the power draw from the wall.

Measurment methods

To measure power consumption there is 2 main methods. The hardware one and the software extrapolation one. The hardware measure is more accurate but it cost more and add add work to retrieve the data.
The software method is cheaper but is not accurate and ask for a lot of tests combined with hardware to be sure that every component is taken into account in the measure (e.g. the motherboard can give a gross value but it did not include the psu)

For reasons of accuracy I will do the hardware method.

There is a lot of cheap and expensive powermeter with a wide range of feature. The key points for my project are that I don't want those data to end up on a proprietary cloud and I want something tinker-ready.

During my search I found Shelly. Shelly is a Bulgarian brand that produce a lot of range of smart things to automate your home (switches, plugs, sensors, etc.) with a heavy focus on :
- No cloud required: Control your Shelly devices locally without connecting them to an external cloud or server
- Highly compatible: Shelly devices are compatible with most home automation platforms, protocols and voice assistants

I choose the Shelly Plug S Gen3 for it versatility.

Collecting those data

Relying on the api

Once setup, I can access the smart plug's web UI directly on the IP http://192.168.1.45/#/scripts. the purpose of this webUI is configuring the smart plug. However Shelly has thought of everything by giving us access to an API to interact directly with the plug http://192.168.1.45/rpc/shelly.GetStatus.

Mainly because I'm lazy I tried to use alloy to retrieve the metrics directly using the API.

The most advanced step I went was retrieving a value but Alloy won't let me convert a float64 to a string :

config.alloy
// Defining Shelly API
remote.http "shelly_api" {
  url = "http://192.168.1.45/rpc/Switch.GetStatus?id=0"
  poll_frequency = "10s"
  poll_timeout = "5s"
}

// Scraping Shelly API
prometheus.scrape "default" {
  targets    = json_path(remote.http.shelly_api.content, ".aenergy.total")
  forward_to = [prometheus.remote_write.default.receiver]

  scrape_interval = "10s"
}
Resulting in :

Error: /etc/alloy/config.alloy:284:16: json_path(remote.http.shelly_api.content, ".aenergy.total")[0] target::ConvertFrom: conversion from 'float64' is not supported
283 | prometheus.scrape "default" {
284 |   targets    = json_path(remote.http.shelly_api.content, ".aenergy.total")
    |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
285 |   forward_to = [prometheus.remote_write.default.receiver]
interrupt received

    ts=2026-02-03T09:08:48.062393434Z
    level=error
    msg=failed to evaluate config
    controller_path=/
    controller_id=
    node=prometheus.scrape.default
    err=decoding configuration: /etc/alloy/config.alloy:284:16: json_path(remote.http.shelly_api.content, \".aenergy.total\")[0] target::ConvertFrom: conversion from 'float64' is not supported

Error: could not perform the initial load successfully

Setting up a script

Hopefully, Shelly make it possible to write script directly within the smart plug ! In the library I found a ready-to-use script that does exactly what I want. The problem is that the plug has a realy limited hardware and can't event run this little script. I had to optimize it the best I could.

/**
 * @title Prometheus HTTP Endpoint for a single switch
 * @description This script exposes a /status endpoint that returns Prometheus metrics.
 */

// Configuration
const metric_prefix = "shelly_";
const url = "metrics";
const monitored_switches = ["switch:0"];
const TYPE_GAUGE = "gauge";
const TYPE_COUNTER = "counter";

// Device info
const info = Shelly.getDeviceInfo();

// Helper function to format labels
function promLabel(label, value) {
  return label + '="' + value + '"';
}

// Default labels for all metrics
const defaultLabels = [
  promLabel("placement", "homelab"),
  promLabel("id", info.id)
];

// Generate a Prometheus metric string
function printPrometheusMetric(name, type, description, value) {
  let labels = defaultLabels.join(",");
  return (
    "# HELP " + metric_prefix + name + " " + description + "\n" +
    "# TYPE " + metric_prefix + name + " " + type + "\n" +
    metric_prefix + name + "{" + labels + "} " + value + "\n"
  );
}

// HTTP handler
function httpServerHandler(request, response) {
  response.body = generateMetricsForSystem();
  for (let i = 0; i < monitored_switches.length; i++) {
    response.body += generateMetricsForSwitch(monitored_switches[i]);
  }
  response.code = 200;
  response.headers = [['Content-Type', 'text/plain; version=0.0.4']];
  response.send();
}

// Generate system metrics
function generateMetricsForSystem() {
  const sys = Shelly.getComponentStatus("sys");
  let metrics = "";
  metrics += printPrometheusMetric("uptime_seconds", TYPE_COUNTER, "Uptime in seconds", sys.uptime) + "\n";
  metrics += printPrometheusMetric("ram_size_bytes", TYPE_GAUGE, "Internal board RAM size in bytes", sys.ram_size) + "\n";
  metrics += printPrometheusMetric("ram_free_bytes", TYPE_GAUGE, "Internal board free RAM size in bytes", sys.ram_free) + "\n";
  return metrics;
}

// Generate switch metrics
function generateMetricsForSwitch(string_id) {
  const sw = Shelly.getComponentStatus(string_id);
  let metrics = "";
  metrics += printPrometheusMetric("switch_power_watts", TYPE_GAUGE, "Instant power consumption in watts", sw.apower) + "\n";
  metrics += printPrometheusMetric("switch_voltage_volts", TYPE_GAUGE, "Instant voltage in volts", sw.voltage) + "\n";
  metrics += printPrometheusMetric("switch_current_amperes", TYPE_GAUGE, "Instant current in amperes", sw.current) + "\n";
  metrics += printPrometheusMetric("switch_temperature_celsius", TYPE_GAUGE, "Temperature of the plug in celsius", sw.temperature.tC) + "\n";
  metrics += printPrometheusMetric("switch_power_total", TYPE_COUNTER, "Accumulated energy consumed in watt-hours", sw.aenergy.total) + "\n";
  metrics += printPrometheusMetric("switch_output", TYPE_GAUGE, "Is switch (1) on or (0) off", sw.output ? 1 : 0) + "\n";
  return metrics;
}

// Register the HTTP endpoint
HTTPServer.registerEndpoint(url, httpServerHandler);

Once the metrics exposed, it's child's play to scrape it with Alloy :

config.alloy

Conclusion

This little challenge was quite funny and I think this will evolve someday because I plan to dive in the domotic world !