# IoT & Sensor Alerts

> Route IoT and sensor alerts to any channel: temperature thresholds, device offline detection, ESP32 and Arduino HTTP requests, and an MQTT bridge.

Source: https://docs.alphorn.dev/use-cases/iot-sensors/

IoT devices and sensors generate events that need different response times - a temperature spike in a server room is urgent, a humidity reading is informational. Alphorn routes each alert to the right channel based on severity.

## Temperature monitoring

```python
import os
import requests

WEBHOOK = "https://app.alphorn.dev/n/V1StGXR8_Z5jdHi6B-myT"
HEADERS = {"Authorization": "Bearer " + os.environ["ALPHORN_API_KEY"]}

def check_temperature(sensor_id, temp_celsius, location):
    if temp_celsius > 40:
        priority, status = 5, "CRITICAL"
    elif temp_celsius > 35:
        priority, status = 4, "WARNING"
    else:
        return  # Normal range, no notification

    requests.post(WEBHOOK, json={
        "title": f"Temperature {status}: {location}",
        "message": f"Sensor {sensor_id} reading: {temp_celsius}°C",
        "priority": priority,
        "tags": ["iot", "temperature", status.lower(), location],
    }, headers=HEADERS, timeout=10)
```

## Device offline detection

```python
def check_device_heartbeat(device_id, last_seen, threshold_minutes=10):
    from datetime import datetime, timedelta, timezone

    # last_seen must be timezone-aware, e.g. datetime(..., tzinfo=timezone.utc)
    if datetime.now(timezone.utc) - last_seen > timedelta(minutes=threshold_minutes):
        requests.post(WEBHOOK, json={
            "title": f"Device offline: {device_id}",
            "message": f"Last seen: {last_seen.isoformat()} ({threshold_minutes}+ minutes ago)",
            "priority": 4,
            "tags": ["iot", "offline", device_id],
        }, headers=HEADERS, timeout=10)
```

## ESP32 / Arduino (HTTP)

Lightweight devices can POST directly to Alphorn:

```cpp
#include <WiFi.h>
#include <HTTPClient.h>

#define ALPHORN_API_KEY "alp_..."

void sendAlert(float temperature) {
  HTTPClient http;
  http.begin("https://app.alphorn.dev/n/V1StGXR8_Z5jdHi6B-myT");
  http.addHeader("Content-Type", "application/json");
  http.addHeader("Authorization", "Bearer " + String(ALPHORN_API_KEY));

  String body = "{\"title\":\"Sensor alert\",\"message\":\"Temperature: "
    + String(temperature, 1) + "°C\",\"priority\":4,\"tags\":[\"iot\",\"sensor\"]}";

  http.POST(body);
  http.end();
}
```

Depending on your arduino-esp32 version, `http.begin()` with an `https://` URL and no CA certificate either doesn't verify the server or fails to connect. For production devices, pass the root CA: create a `WiFiClientSecure client;`, call `client.setCACert(rootCA);` with the PEM of the CA that signs `app.alphorn.dev`, and use `http.begin(client, url)`.

## MQTT to Alphorn bridge

If your devices use MQTT, write a small bridge that subscribes to topics and forwards to Alphorn:

```python
import paho.mqtt.client as mqtt
import requests
import json
import os

WEBHOOK = "https://app.alphorn.dev/n/V1StGXR8_Z5jdHi6B-myT"
HEADERS = {"Authorization": "Bearer " + os.environ["ALPHORN_API_KEY"]}

# paho-mqtt 2.x: the client needs an explicit callback API version
def on_connect(client, userdata, flags, reason_code, properties):
    # Subscribing here re-subscribes after every reconnect
    client.subscribe("sensors/#")

def on_message(client, userdata, msg):
    try:
        data = json.loads(msg.payload)
    except ValueError:
        return  # skip messages that aren't valid JSON
    if not isinstance(data, dict):
        return
    try:
        priority = int(data.get("priority", 3))  # Alphorn ignores non-numeric priorities
    except (TypeError, ValueError):
        priority = 3
    try:
        requests.post(WEBHOOK, json={
            "title": f"Sensor: {msg.topic}",
            "message": json.dumps(data, indent=2),
            "priority": priority,
            "tags": ["iot", "mqtt", msg.topic.replace("/", "-")],
        }, headers=HEADERS, timeout=10)
    except requests.RequestException as err:
        print("Forwarding to Alphorn failed:", err)  # keep the bridge running

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
client.on_connect = on_connect
client.on_message = on_message
client.connect("mqtt-broker.local")
client.loop_forever()
```

## Routing examples

| Channel | Filter | Purpose |
|---|---|---|
| PagerDuty | Tags has any of `critical` | Immediate response for critical thresholds |
| Slack (#facilities) | Tags has any of `temperature` | Temperature events for facilities team |
| Slack (#devices) | Tags has any of `offline` | Device outage tracking |
| Email | Tags has any of `iot` | Full sensor event log |
