Chapter 12: Cross-Platform Power Monitoring and Automation

The previous chapters covered optimization for individual device classes. This chapter builds a unified monitoring pipeline that collects power data from embedded devices, laptops, and servers into a single view — making it easy to track baselines, detect regressions, and verify that optimizations are holding.


12.1 Architecture Overview

The pipeline uses three tiers:

  1. Collectors: One per device class. Each publishes power metrics to an MQTT broker.
  2. Broker: A Mosquitto MQTT broker (lightweight, runs on a Raspberry Pi)
  3. Aggregator: A Python subscriber that stores data in SQLite and sends alerts

Topic namespace convention:

power/{location}/{device_name}/{metric}
# Examples:
power/lab/esp32-sensor-01/mW
power/office/laptop-thinkpad/package_W
power/rack/nuc-homeserver/rapl_W

12.2 MQTT Broker Setup

sudo apt install mosquitto mosquitto-clients
sudo systemctl enable --now mosquitto

# Test publish/subscribe
mosquitto_sub -h localhost -t "power/#" &
mosquitto_pub -h localhost -t "power/test/device/mW" -m "245.3"

12.3 Embedded Collector: INA219 + MQTT

Running on a Raspberry Pi connected to one or more INA219 sensors:

import smbus2, struct, time
import paho.mqtt.client as mqtt

BUS_NUM, ADDR, SHUNT_OHMS = 1, 0x40, 0.1
BROKER, DEVICE = "localhost", "esp32-sensor-01"
TOPIC = f"power/lab/{DEVICE}/mW"

def read_power_mw(bus):
    raw = bus.read_i2c_block_data(ADDR, 0x01, 2)
    shunt_mv = struct.unpack('>h', bytes(raw))[0] * 0.01
    raw = bus.read_i2c_block_data(ADDR, 0x02, 2)
    bus_v = ((struct.unpack('>h', bytes(raw))[0] >> 3) * 4) / 1000
    return bus_v * (shunt_mv / SHUNT_OHMS)

client = mqtt.Client()
client.connect(BROKER)
with smbus2.SMBus(BUS_NUM) as bus:
    while True:
        power_mw = read_power_mw(bus)
        client.publish(TOPIC, f"{power_mw:.2f}")
        time.sleep(10)

Full version with multiple INA219 addresses is in code/ina219_mqtt.py.


12.4 Laptop/Server Collector: RAPL + MQTT

import time, pathlib
import paho.mqtt.client as mqtt

BROKER = "192.168.1.100"   # your MQTT broker IP
DEVICE = "nuc-homeserver"
DOMAIN = pathlib.Path("/sys/class/powercap/intel-rapl/intel-rapl:0/energy_uj")
TOPIC = f"power/rack/{DEVICE}/rapl_W"

client = mqtt.Client()
client.connect(BROKER)

e0, t0 = int(DOMAIN.read_text()), time.monotonic()
while True:
    time.sleep(30)
    e1, t1 = int(DOMAIN.read_text()), time.monotonic()
    power_w = (e1 - e0) / 1e6 / (t1 - t0)
    client.publish(TOPIC, f"{power_w:.3f}")
    e0, t0 = e1, t1

Full version with DRAM and uncore subzones is in code/rapl_mqtt.py.


12.5 Central Aggregator with SQLite

The aggregator subscribes to all power topics and stores them with timestamps:

import sqlite3, time
import paho.mqtt.client as mqtt

DB = "power_log.db"
BROKER = "localhost"

def init_db():
    con = sqlite3.connect(DB)
    con.execute("""CREATE TABLE IF NOT EXISTS readings
                   (ts REAL, device TEXT, metric TEXT, value REAL)""")
    con.commit()
    return con

def on_message(client, con, msg):
    parts = msg.topic.split("/")   # power/location/device/metric
    if len(parts) == 4:
        _, _, device, metric = parts
        try:
            value = float(msg.payload)
            con.execute("INSERT INTO readings VALUES (?,?,?,?)",
                        (time.time(), device, metric, value))
            con.commit()
        except ValueError:
            pass

con = init_db()
client = mqtt.Client(userdata=con)
client.on_message = on_message
client.connect(BROKER)
client.subscribe("power/#")
client.loop_forever()

Full version with alerting and weekly report generation is in code/dashboard.py.


12.6 Alerting on Power Thresholds

The aggregator can compute a rolling average and alert when a device exceeds its baseline:

import smtplib
from email.mime.text import MIMEText

BASELINES = {
    "nuc-homeserver": 8.0,   # watts — alert if > 8 W at idle
    "esp32-sensor-01": 5.0,  # mW — alert if > 5 mW average
}

def check_and_alert(device, current_value, threshold):
    if current_value > threshold * 1.5:  # 50% above baseline
        msg = MIMEText(f"{device} drawing {current_value:.1f} (baseline {threshold})")
        msg["Subject"] = f"Power alert: {device}"
        msg["From"] = "monitor@home"
        msg["To"] = "you@example.com"
        with smtplib.SMTP("localhost") as s:
            s.send_message(msg)

Alternatively, use ntfy.sh for push notifications without an SMTP server — a single HTTP POST to your ntfy topic.


12.7 Accuracy and Calibration

Software measurements have systematic errors:

For a ground-truth baseline, measure wall power with a Kill-a-Watt or calibrated smart plug once, then use RAPL/INA219 for relative comparisons and trend monitoring.


← Chapter 11: Service and Workload Scheduling Table of Contents Chapter 13: Real-World Case Studies and Design Patterns →