Initila commit
Build Docker image on push / docker (push) Successful in 22s

This commit is contained in:
2026-08-18 00:46:59 +02:00
commit 9db7aa2560
28 changed files with 2823 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
# Documentation
This directory contains the detailed Shelly Exporter documentation.
## User guides
- [Configuration reference](configuration.md) describes every YAML field, product identifiers, authentication, and multi-device examples.
- [Metrics reference](metrics.md) lists metric types, labels, units, optional behavior, and energy counter semantics.
- [Operations and troubleshooting](operations.md) covers deployment, Prometheus setup, example alerts and queries, security, and common failures.
## Developer guide
- [Architecture and adding products](architecture.md) explains request flow, package responsibilities, Gen1 schema extension, metric conventions, and testing.
## Protocol scope
The current implementation targets the Shelly Gen1 HTTP API:
```text
GET <device-url>/status
Authorization: Basic ... # only when credentials are configured
Accept: application/json
```
Newer Shelly generations use a different RPC API and authentication model. They require a separate protocol client in addition to new response structures and collectors.
+189
View File
@@ -0,0 +1,189 @@
# Architecture and adding products
## Design goals
The exporter separates transport, product decoding, Prometheus collection, configuration, and HTTP serving so additional Shelly models do not require changes throughout the application.
The Gen1 implementation uses a superset response structure with optional fields. This matches the Shelly API, where `/status` contains common fields plus product- and mode-specific blocks.
## Request flow
```mermaid
flowchart LR
P[Prometheus] -->|GET /metrics| H[HTTP server]
H --> R[Prometheus registry]
R --> G[Collector group]
G -->|concurrent| C1[Device collector A]
G -->|concurrent| C2[Device collector B]
C1 --> S1[Status client]
C2 --> S2[Status client]
S1 -->|GET /status| D1[Shelly device A]
S2 -->|GET /status| D2[Shelly device B]
```
There is no background polling loop or value cache. Each `/metrics` request produces one status request per configured device.
## Package responsibilities
| Package/file | Responsibility |
| --- | --- |
| [`main.go`](../main.go) | Parses `-config`, installs signal handling, and starts the application. |
| [`configuration`](../configuration/configuration.go) | Strict YAML loading, defaults, and product-independent validation. |
| [`application`](../application/application.go) | Creates clients and collectors, owns the Prometheus registry, and serves HTTP endpoints. |
| [`shelly`](../shelly/client.go) | Gen1 HTTP `/status` client, timeout, Basic Authentication, status checking, and JSON decoding. |
| [`collector/factory.go`](../collector/factory.go) | Normalizes product identifiers and maps them to collector constructors. |
| [`collector/group.go`](../collector/group.go) | Presents all device collectors as one registered collector and runs them concurrently. |
| [`collector/base.go`](../collector/base.go) | Shared `shelly_up`, request duration, and request error metrics. |
| [`collector/gen1_status.go`](../collector/gen1_status.go) | Optional superset of known Gen1 `/status` response blocks. |
| [`collector/gen1_collector.go`](../collector/gen1_collector.go) | Descriptor definitions and conversion from Gen1 status data to Prometheus metrics. |
## Startup lifecycle
1. `configuration.Load` reads and strictly parses YAML.
2. Product-independent validation checks the listener, timeout, unique names, and URLs.
3. `application.New` creates one HTTP status client per configured device.
4. `collector.NewDeviceCollector` normalizes the configured product and resolves its factory.
5. All device collectors are wrapped in `collector.Group` and registered once in a private Prometheus registry.
6. `/metrics` and `/-/healthy` handlers are installed.
7. `Application.Run` opens the listener and serves until cancellation or a server error.
A private registry prevents application metrics from being mixed with process-global collectors registered by dependencies.
## Collection behavior
`collector.Group.Collect` starts one goroutine per configured device and waits for all of them. Each Gen1 collector then:
1. calls `StatusClient.GetStatus`;
2. emits scrape health, duration, and cumulative error count;
3. stops for that device if HTTP or JSON decoding failed;
4. emits common status metrics;
5. processes relays, inputs, meters, EM meters, rollers, lights, thermostats, and sensors;
6. skips every absent optional field.
The base error counter uses an atomic integer because Prometheus can execute overlapping collections. Device-specific values are const metrics built from the latest response and do not retain mutable state.
## Why status fields use pointers
In JSON, a missing field and a present zero value have different meanings. For example:
```json
{}
```
means that a capability may not exist, while:
```json
{"power": 0}
```
means that the device supports power measurement and currently reports zero watts.
Numeric and boolean status fields therefore use pointers in `gen1Status`. Collection helpers emit a metric only for a non-nil pointer. The custom `numberOrBool` decoder handles the Gen1 `overpower` field because different product families represent it as either a boolean state or a numeric threshold.
## Metric conventions
- Use the `shelly_` namespace.
- Use Prometheus base-unit suffixes such as `_seconds`, `_bytes`, `_watts`, and `_volts`.
- Use `_total` only for counters.
- All product metrics include `device`; indexed response arrays additionally include `index`.
- Boolean values are gauges with `0` or `1`.
- Text is exposed through labels on an `_info` gauge with value `1`.
- Reuse an existing metric when a new product has the same semantic value.
- Avoid labels containing frequently changing or unbounded data.
- Preserve native cumulative device values instead of accumulating scrape deltas in exporter memory.
### Energy counter rules
Gen1 uses two different cumulative representations:
- `meters[].total` is watt-minutes and is divided by 60 before being exported as `shelly_meter_energy_watt_hours_total`.
- `emeters[].total` and `total_returned` are already Wh and are exported unchanged.
Both are Prometheus counters. Reboots or explicit device resets can decrease them; Prometheus counter functions handle the reset.
## Extending the Gen1 schema
Use this path when a product already supports `GET /status` but returns a field the generic collector does not understand.
### 1. Capture an official response fixture
Use the official Shelly documentation and, when available, a real device response. Remove passwords and private network identifiers before committing fixtures.
### 2. Add optional response fields
Extend [`gen1_status.go`](../collector/gen1_status.go). Use pointers for scalar fields:
```go
type gen1Status struct {
// Existing fields...
Voltage *float64 `json:"voltage"`
}
```
For a nested or repeated block, introduce a dedicated type rather than an anonymous map. A typed structure catches incompatible API changes during JSON decoding.
### 3. Register descriptors
Add each metric once in `registerDescriptors`:
```go
c.add("supply_voltage_volts", "Device supply voltage in volts.")
```
`add` automatically prefixes the name with `shelly_` and prepends the `device` label. Pass only additional label names.
### 4. Emit only present values
Use the existing helpers:
```go
c.optionalGauge(channel, "supply_voltage_volts", status.Voltage)
c.optionalBool(channel, "feature_enabled", status.FeatureEnabled)
```
Use `counter` only for a cumulative, monotonically increasing source value:
```go
if status.Events != nil && *status.Events >= 0 {
c.counter(channel, "events_total", *status.Events)
}
```
### 5. Register the product identifier
If the new model is known to use this schema, append its normalized identifier to `gen1Products`. Unlisted compatible devices can already use `product: gen1` without registration.
### 6. Add fixture-based tests
Add a representative JSON fixture in the collector tests and gather through `prometheus.NewPedanticRegistry`. Test:
- expected metric values and labels;
- metric type, particularly counters;
- unit conversions;
- valid zero values;
- absence of a metric when the source field is absent;
- inconsistent JSON forms if the device family has firmware variations.
Run:
```sh
gofmt -w collector
go test ./...
go vet ./...
go build ./...
```
## Adding a different protocol generation
Do not model a Gen2/Gen3 RPC response as a Gen1 `/status` response. The current `shelly.StatusClient` always appends `/status` and implements Basic Authentication, so supporting an RPC generation requires a transport extension as well as a collector.
A clean implementation should:
1. define a product-independent request/client interface or a protocol client factory;
2. choose the protocol client from product metadata during application construction;
3. implement the RPC endpoint and its authentication scheme separately;
4. add a collector with generation-appropriate response structures;
5. register product identifiers through the existing collector factory;
6. retain the shared collector group and scrape health metrics.
This keeps generation-specific endpoint paths and authentication out of the application HTTP server.
+131
View File
@@ -0,0 +1,131 @@
# Configuration reference
Shelly Exporter reads one YAML file at startup. The default path is `config.yaml`; use the command-line flag to select another file:
```sh
./shelly-exporter -config /etc/shelly-exporter/config.yaml
```
Configuration is parsed strictly. Unknown fields, invalid value types, and duplicate YAML keys accepted by neither the parser nor validation cause startup to fail instead of being silently ignored.
## Complete example
```yaml
host: 0.0.0.0
port: 9090
logLevel: 4
shelly:
timeoutSeconds: 10
devices:
- name: office-plug
product: plug_s
url: http://192.168.0.30
username: admin
password: secret
- name: main-meter
product: shelly_3em
url: http://192.168.0.31
username: admin
password: another-secret
- name: garage-relay
product: shelly_1
url: http://192.168.0.32
username: ""
password: ""
```
## Top-level fields
| Field | Required | Default | Description |
| --- | --- | --- | --- |
| `host` | yes | none | Literal IPv4 or IPv6 address on which the exporter listens. Hostnames such as `localhost` are not accepted. Use `0.0.0.0` to listen on all IPv4 interfaces. |
| `port` | yes | none | TCP listening port from 1 to 65535. |
| `logLevel` | no | `0` | Numeric Logrus severity described below. |
| `shelly` | yes | none | Shelly client and device configuration. |
### Log levels
| Value | Level |
| ---: | --- |
| `0` | Panic |
| `1` | Fatal |
| `2` | Error |
| `3` | Warn |
| `4` | Info |
| `5` | Debug |
| `6` | Trace |
`4` is a practical production default. Debug and trace logging can be useful during device communication troubleshooting.
## Shelly fields
| Field | Required | Default | Description |
| --- | --- | --- | --- |
| `shelly.timeoutSeconds` | no | `10` | Complete HTTP request timeout for each device. A value of `0` also selects the default; negative values are invalid. |
| `shelly.devices` | yes | none | Non-empty list of devices. Devices are contacted concurrently during a Prometheus scrape. |
The timeout applies independently to each device. Since devices are scraped concurrently, one unavailable device delays the scrape by at most its own timeout rather than the sum of every device timeout.
## Device fields
| Field | Required | Description |
| --- | --- | --- |
| `name` | yes | Unique stable device name. It becomes the `device` label on all metrics. Names are compared case-sensitively. |
| `product` | yes | Registered product identifier or `gen1`. Product normalization is described below. |
| `url` | yes | Absolute `http://` or `https://` base URL. The exporter appends `/status`. Queries and fragments are rejected. |
| `username` | no | HTTP Basic Authentication username. |
| `password` | no | HTTP Basic Authentication password. |
Credentials are never used as metric labels. Keep the configuration file readable only by the exporter account because it contains plaintext passwords.
The exporter sends an `Authorization: Basic` header when either `username` or `password` is non-empty. In normal configurations, provide both values or leave both empty.
### URL handling
| Configured URL | Requested status URL |
| --- | --- |
| `http://192.168.0.30` | `http://192.168.0.30/status` |
| `http://192.168.0.30/` | `http://192.168.0.30/status` |
| `https://shelly.example/device` | `https://shelly.example/device/status` |
HTTPS uses the operating system trust store and normal certificate verification. There is no option to disable TLS verification.
## Product identifiers
| Family | Identifiers |
| --- | --- |
| Generic Gen1 | `gen1` |
| Plugs | `plug`, `plug_s` |
| Relays and inputs | `shelly_1`, `shelly_1pm`, `shelly_1l`, `shelly_2`, `shelly_2_5`, `shelly_4pro`, `shelly_uni`, `shelly_i3`, `shelly_button1` |
| Energy meters | `shelly_em`, `shelly_3em` |
| Lights and dimmers | `shelly_bulb`, `shelly_bulb_rgbw`, `shelly_duo`, `shelly_vintage`, `shelly_rgbw2`, `shelly_dimmer` |
| Sensors and heating | `shelly_ht`, `shelly_flood`, `shelly_smoke`, `shelly_door_window`, `shelly_motion`, `shelly_sense`, `shelly_gas`, `shelly_trv` |
Identifiers are converted to lowercase, and each punctuation or whitespace run becomes an underscore. Examples:
| Input | Normalized identifier |
| --- | --- |
| `plug-s` | `plug_s` |
| `Shelly 2.5` | `shelly_2_5` |
| `SHELLY 3EM` | `shelly_3em` |
Use `gen1` for an unlisted product that returns a compatible Gen1 `/status` JSON response. The collector detects known optional response blocks and emits only corresponding metrics. A successful HTTP request does not guarantee that every model-specific field is understood; inspect `/metrics` and compare it with the raw device response.
## Validation failures
The exporter refuses to start when:
- `host` is not a literal IP address;
- `port` is zero or outside the YAML target type range;
- `logLevel` is outside `0..6`;
- the timeout is negative;
- the device list is empty;
- a device name is empty or duplicated;
- a product is empty or not registered;
- a URL is not absolute HTTP/HTTPS or contains a query or fragment;
- an unknown YAML field is present.
Configuration is loaded only during startup. Restart the process after editing the file.
+188
View File
@@ -0,0 +1,188 @@
# Metrics reference
Shelly Exporter exposes OpenMetrics-compatible Prometheus data on `/metrics`. A collection starts when Prometheus requests that endpoint; the exporter does not poll devices in the background.
## Conventions
- Every series has a stable `device` label taken from configuration.
- Request health metrics also have a normalized `product` label.
- Channel-based values use a zero-based `index` label matching the position in the Shelly JSON array.
- Information metrics have a constant value of `1` and place textual state in labels.
- Boolean gauges use `1` for true or active and `0` for false or inactive.
- A metric is omitted when its source field is absent. Missing metrics do not imply a zero value.
- Device credentials and URLs are not exported as labels.
## Exporter and device health
These metrics exist for every configured device, including failed scrapes.
| Metric | Type | Labels | Description |
| --- | --- | --- | --- |
| `shelly_up` | gauge | `device`, `product` | `1` when the latest `/status` request and JSON decoding succeeded; otherwise `0`. |
| `shelly_scrape_duration_seconds` | gauge | `device`, `product` | Wall-clock duration of the latest device request. |
| `shelly_scrape_errors_total` | counter | `device`, `product` | Failed requests since this exporter process started. |
On a failed request only these three metric families are emitted for the affected device. A failure from one device does not prevent concurrent devices from being collected.
## Common Gen1 status
| Metric | Type | Additional labels | Description |
| --- | --- | --- | --- |
| `shelly_device_info` | gauge | `product`, `mac`, `firmware` | Configured product and device identity. Value is always `1`. |
| `shelly_wifi_info` | gauge | `ssid`, `ip` | Connected Wi-Fi network identity. Value is always `1`. |
| `shelly_wifi_connected` | gauge | none | Wi-Fi station connection state. |
| `shelly_wifi_rssi_dbm` | gauge | none | Received Wi-Fi signal strength in dBm. |
| `shelly_cloud_enabled` | gauge | none | Shelly Cloud enabled state. |
| `shelly_cloud_connected` | gauge | none | Shelly Cloud connection state. |
| `shelly_mqtt_connected` | gauge | none | MQTT connection state. |
| `shelly_update_available` | gauge | none | Firmware update availability. |
| `shelly_update_info` | gauge | `status`, `current_version`, `new_version` | Firmware update state. Value is always `1`. |
| `shelly_status_serial` | gauge | none | Device status sequence number. |
| `shelly_ram_size_bytes` | gauge | none | Total device RAM. |
| `shelly_ram_free_bytes` | gauge | none | Available device RAM. |
| `shelly_filesystem_size_bytes` | gauge | none | Total filesystem size. |
| `shelly_filesystem_free_bytes` | gauge | none | Free filesystem space. |
| `shelly_filesystem_mounted` | gauge | none | Data filesystem mount state, reported by products such as 3EM. |
| `shelly_uptime_seconds` | gauge | none | Device uptime. |
## Relays and inputs
| Metric | Type | Additional labels | Description |
| --- | --- | --- | --- |
| `shelly_relay_on` | gauge | `index` | Relay output state. |
| `shelly_relay_has_timer` | gauge | `index` | Whether a relay timer is active. |
| `shelly_relay_timer_duration_seconds` | gauge | `index` | Configured duration of the relay timer. |
| `shelly_relay_timer_remaining_seconds` | gauge | `index` | Remaining relay timer duration. |
| `shelly_relay_overpower` | gauge | `index` | Relay overpower protection state. |
| `shelly_relay_valid` | gauge | `index` | Relay status validity when provided by the device. |
| `shelly_input_on` | gauge | `index` | Logical input state. |
| `shelly_input_event_info` | gauge | `index`, `event`, `last_sequence` | Latest button/input event. Value is always `1`. |
| `shelly_input_event_count_total` | counter | `index` | Input events since device restart. A reboot is interpreted by Prometheus as a counter reset. |
## Standard power meters
The `meters` block is used by Plug, Plug S, PM relays, dimmers, and several light products.
| Metric | Type | Additional labels | Description |
| --- | --- | --- | --- |
| `shelly_meter_power_watts` | gauge | `index` | Current active power. |
| `shelly_meter_valid` | gauge | `index` | Meter reading validity. |
| `shelly_meter_timestamp_seconds` | gauge | `index` | Unix timestamp of the latest counter reading. |
| `shelly_meter_overpower` | gauge | `index` | Boolean overpower state on products that return a boolean. |
| `shelly_meter_overpower_threshold_watts` | gauge | `index` | Overpower threshold on products that return a numeric value. |
| `shelly_meter_recent_energy_watt_minutes` | gauge | `index`, `minute` | Device-provided recent per-minute energy slots. `minute` is the zero-based array position. |
| `shelly_meter_energy_watt_hours_total` | counter | `index` | Cumulative consumed energy converted from watt-minutes to watt-hours. |
Shelly reports `meters[].total` in watt-minutes. The exporter divides the value by 60:
```text
shelly_meter_energy_watt_hours_total = meters[index].total / 60
```
For example, a device total of `4188430` watt-minutes is exported as approximately `69807.1667` Wh.
## EM and 3EM power meters
The `emeters` block uses watt-hours directly and can include returned energy.
| Metric | Type | Additional labels | Description |
| --- | --- | --- | --- |
| `shelly_emeter_power_watts` | gauge | `index` | Current active power. |
| `shelly_emeter_reactive_power_var` | gauge | `index` | Current reactive power. |
| `shelly_emeter_power_factor` | gauge | `index` | Power factor, primarily on 3EM. |
| `shelly_emeter_current_amperes` | gauge | `index` | Measured current, primarily on 3EM. |
| `shelly_emeter_voltage_volts` | gauge | `index` | RMS voltage. |
| `shelly_emeter_valid` | gauge | `index` | Energy meter validity. |
| `shelly_emeter_energy_watt_hours_total` | counter | `index` | Cumulative consumed energy in Wh. No conversion is applied. |
| `shelly_emeter_returned_energy_watt_hours_total` | counter | `index` | Cumulative energy returned to the grid in Wh. |
| `shelly_total_power_watts` | gauge | none | Sum of power over all channels when supplied by the device. |
The device can reset persisted totals through its API. Prometheus correctly treats a decrease as a counter reset when functions such as `rate()` or `increase()` are used.
## Rollers
| Metric | Type | Additional labels | Description |
| --- | --- | --- | --- |
| `shelly_roller_info` | gauge | `index`, `state`, `stop_reason`, `last_direction` | Roller textual state. Value is always `1`. |
| `shelly_roller_power_watts` | gauge | `index` | Current motor power. |
| `shelly_roller_valid` | gauge | `index` | Power meter validity. |
| `shelly_roller_safety_switch` | gauge | `index` | Safety input state. |
| `shelly_roller_overtemperature` | gauge | `index` | Roller overtemperature state. |
| `shelly_roller_position_percent` | gauge | `index` | Current position in percent. A device may use a negative value for an invalid or uncalibrated position. |
| `shelly_roller_calibrating` | gauge | `index` | Calibration in progress. |
| `shelly_roller_positioning` | gauge | `index` | Position control availability. |
## Lights and dimmers
| Metric | Type | Additional labels | Description |
| --- | --- | --- | --- |
| `shelly_light_info` | gauge | `index`, `mode` | Light operating mode. Value is always `1`. |
| `shelly_light_on` | gauge | `index` | Light output state. |
| `shelly_light_has_timer` | gauge | `index` | Light timer state. |
| `shelly_light_timer_remaining_seconds` | gauge | `index` | Remaining light timer duration. |
| `shelly_light_brightness_percent` | gauge | `index` | Brightness in percent. |
| `shelly_light_red` | gauge | `index` | Red channel, normally `0..255`. |
| `shelly_light_green` | gauge | `index` | Green channel, normally `0..255`. |
| `shelly_light_blue` | gauge | `index` | Blue channel, normally `0..255`. |
| `shelly_light_white` | gauge | `index` | White channel, normally `0..255`. |
| `shelly_light_gain_percent` | gauge | `index` | Color gain in percent. |
| `shelly_light_color_temperature_kelvin` | gauge | `index` | White color temperature. |
| `shelly_light_effect` | gauge | `index` | Selected numeric effect. |
## Environmental and safety sensors
| Metric | Type | Additional labels | Description |
| --- | --- | --- | --- |
| `shelly_temperature_celsius` | gauge | none | Internal or primary sensor temperature in °C. |
| `shelly_temperature_valid` | gauge | none | Temperature reading validity. |
| `shelly_temperature_status_info` | gauge | `status` | Textual temperature state such as `Normal`. Value is always `1`. |
| `shelly_overtemperature` | gauge | none | Device overtemperature protection state. |
| `shelly_humidity_percent` | gauge | none | Relative humidity. |
| `shelly_humidity_valid` | gauge | none | Humidity reading validity. |
| `shelly_battery_percent` | gauge | none | Estimated battery capacity. |
| `shelly_battery_voltage_volts` | gauge | none | Measured battery voltage. |
| `shelly_charger_connected` | gauge | none | External charger state. |
| `shelly_illuminance_lux` | gauge | none | Measured illuminance. |
| `shelly_illuminance_valid` | gauge | none | Illuminance reading validity. |
| `shelly_illuminance_info` | gauge | `illumination` | Classified illumination such as `dark`, `twilight`, or `bright`. |
| `shelly_sensor_valid` | gauge | none | Primary product sensor validity. |
| `shelly_sensor_error` | gauge | none | Product-specific sensor error code. |
| `shelly_connect_retries` | gauge | none | Wi-Fi retries during the current wake cycle. |
| `shelly_motion` | gauge | none | Motion detection state. |
| `shelly_motion_active` | gauge | none | Motion detection enabled/active state. |
| `shelly_motion_timestamp_seconds` | gauge | none | Motion reading Unix timestamp. |
| `shelly_vibration` | gauge | none | Motion sensor vibration/tamper state. |
| `shelly_smoke` | gauge | none | Smoke detection state. |
| `shelly_flood` | gauge | none | Flood detection state. |
| `shelly_rain_sensor_mode` | gauge | none | Flood sensor rain mode. |
| `shelly_door_window_info` | gauge | `state` | Door/window state, normally `open` or `close`. Value is always `1`. |
| `shelly_tilt_degrees` | gauge | none | Door/window tilt. |
| `shelly_vibration_value` | gauge | none | Raw door/window vibration state; `-1` can mean disabled. |
| `shelly_vibration_time_seconds` | gauge | none | Door/window vibration validity duration. |
| `shelly_adc_voltage_volts` | gauge | `index` | Shelly Uni ADC voltage. |
| `shelly_external_temperature_celsius` | gauge | `index`, `hardware_id` | External addon temperature. |
| `shelly_external_humidity_percent` | gauge | `index`, `hardware_id` | External addon humidity. |
## Shelly TRV
| Metric | Type | Additional labels | Description |
| --- | --- | --- | --- |
| `shelly_thermostat_valve_position_percent` | gauge | `index` | Valve position. A negative value can mean uncalibrated. |
| `shelly_thermostat_target_enabled` | gauge | `index` | Automatic target control state. |
| `shelly_thermostat_target_temperature_celsius` | gauge | `index` | Target temperature when reported in °C. |
| `shelly_thermostat_temperature_celsius` | gauge | `index` | Measured thermostat temperature. |
| `shelly_thermostat_temperature_valid` | gauge | `index` | Thermostat temperature validity. |
| `shelly_thermostat_schedule_enabled` | gauge | `index` | Schedule state. |
| `shelly_thermostat_schedule_profile` | gauge | `index` | Selected schedule profile. |
| `shelly_thermostat_boost_minutes` | gauge | `index` | Boost duration in minutes. |
| `shelly_thermostat_window_open` | gauge | `index` | Open-window state. |
| `shelly_thermostat_calibrated` | gauge | none | Valve calibration state. |
## Shelly Gas
| Metric | Type | Additional labels | Description |
| --- | --- | --- | --- |
| `shelly_gas_concentration_ppm` | gauge | none | Combustible gas concentration in ppm. |
| `shelly_gas_concentration_valid` | gauge | none | Concentration reading validity. |
| `shelly_gas_sensor_info` | gauge | `sensor_state`, `self_test_state`, `alarm_state` | Sensor operating and alarm state. Value is always `1`. |
| `shelly_gas_valve_info` | gauge | `index`, `state` | Valve addon state. Value is always `1`. |
+209
View File
@@ -0,0 +1,209 @@
# Operations and troubleshooting
## HTTP endpoints
| Path | Purpose | Device access |
| --- | --- | --- |
| `/metrics` | Prometheus/OpenMetrics output. Each request triggers collection from all configured devices. | yes |
| `/-/healthy` | Exporter process health; returns HTTP 200 and `OK`. | no |
The health endpoint proves that the HTTP process can respond. It deliberately does not contact Shelly devices. Use `shelly_up` to monitor individual device availability.
## Native deployment
```sh
go build -trimpath -o shelly-exporter .
./shelly-exporter -config config.yaml
```
The process handles `SIGINT` and `SIGTERM`, stops accepting requests, and gives the HTTP server up to 10 seconds to shut down.
Example systemd unit:
```ini
[Unit]
Description=Shelly Prometheus Exporter
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=shelly-exporter
Group=shelly-exporter
ExecStart=/usr/local/bin/shelly-exporter -config /etc/shelly-exporter/config.yaml
Restart=on-failure
RestartSec=5s
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadOnlyPaths=/etc/shelly-exporter/config.yaml
[Install]
WantedBy=multi-user.target
```
Ensure the service account can read the configuration and reach every device over the network.
## Docker deployment
Build the image:
```sh
docker build --rm -t shelly-exporter:latest .
```
Run it with a read-only configuration mount:
```sh
docker run --rm -d \
--name shelly-exporter \
-p 9090:9090 \
-v "$PWD/config.yaml:/opt/shelly-exporter/config.yaml:ro" \
shelly-exporter:latest
```
The image runs as an unprivileged user and includes a health check against `/-/healthy`. The configuration bundled in the image is only an example; mount the actual configuration for production.
## Prometheus setup
```yaml
scrape_configs:
- job_name: shelly
scrape_interval: 30s
scrape_timeout: 15s
static_configs:
- targets:
- shelly-exporter:9090
```
Keep the Prometheus `scrape_timeout` greater than `shelly.timeoutSeconds` plus a small allowance for metric encoding and network latency. Devices are queried concurrently, so the worst normal collection duration is close to the slowest device request rather than the sum of all requests.
The exporter contacts every device for every Prometheus scrape. Very short scrape intervals add unnecessary work to low-power hardware. A 1560 second interval is a reasonable starting point for powered devices.
### Battery-powered sensors
Shelly Gen1 H&T, Flood, Door/Window, Button1, and similar battery devices sleep to conserve power. They may be unavailable during most scheduled scrapes, causing expected `shelly_up == 0` results. Consider MQTT or event callbacks when continuous collection from these products is required.
## Useful PromQL
Current power per standard meter:
```promql
shelly_meter_power_watts
```
Consumed energy over 24 hours in kWh:
```promql
increase(shelly_meter_energy_watt_hours_total[24h]) / 1000
```
EM/3EM imported and exported energy over 24 hours in kWh:
```promql
increase(shelly_emeter_energy_watt_hours_total[24h]) / 1000
```
```promql
increase(shelly_emeter_returned_energy_watt_hours_total[24h]) / 1000
```
Devices currently unavailable:
```promql
shelly_up == 0
```
Free RAM percentage where both fields are reported:
```promql
100 * shelly_ram_free_bytes / shelly_ram_size_bytes
```
## Example alert rules
Adjust durations and exclude sleeping products according to the installation.
```yaml
groups:
- name: shelly
rules:
- alert: ShellyDeviceDown
expr: shelly_up == 0
for: 5m
labels:
severity: warning
annotations:
summary: "Shelly {{ $labels.device }} is unreachable"
- alert: ShellyDeviceOvertemperature
expr: shelly_overtemperature == 1
for: 1m
labels:
severity: critical
annotations:
summary: "Shelly {{ $labels.device }} reports overtemperature"
- alert: ShellyWeakWiFi
expr: shelly_wifi_rssi_dbm < -80
for: 10m
labels:
severity: warning
annotations:
summary: "Shelly {{ $labels.device }} has weak Wi-Fi signal"
```
## Security
- Store `config.yaml` outside source control and restrict it to the exporter account, for example with mode `0600` on Linux.
- Prefer a trusted management network or HTTPS because Basic Authentication does not encrypt credentials over plain HTTP.
- The exporter endpoints themselves do not require authentication. Restrict port 9090 with firewall, reverse proxy, or network policy when necessary.
- Device passwords are not metric labels, but request failures can include the target URL in logs. Do not put credentials in URL user-info; use `username` and `password` fields.
- HTTPS certificate verification is enabled and cannot be bypassed by configuration.
## Troubleshooting
### Exporter does not start
Read the fatal error first. Configuration loading is strict, and common causes are:
- an unknown or misspelled YAML key;
- a hostname in `host` instead of a literal IP address;
- an empty device list;
- duplicate device names;
- an unsupported product identifier;
- a URL missing `http://` or `https://`.
Increase `logLevel` to `5` or `6` only after configuration validation succeeds.
### `shelly_up` is zero
Test the same request from the exporter host or container network:
```sh
curl -v -u admin:password http://192.168.0.30/status
```
Check routing, firewall rules, device power, credentials, and the configured timeout. HTTP errors and JSON decoding failures increment `shelly_scrape_errors_total`.
### HTTP 401 Unauthorized
Confirm that the configured username and password match the device. The exporter uses HTTP Basic Authentication. A device configured for a different authentication scheme will not work with the current client.
### TLS certificate error
The device certificate must be trusted by the exporter operating system or container. Use a certificate issued by a trusted internal CA and add that CA to the runtime trust store.
### Device is up but an expected metric is absent
Metrics are intentionally conditional. Capture the raw `/status` JSON and verify that the corresponding field is present. Firmware versions and device modes can change response blocks—for example, Shelly 2.5 reports either relays or rollers depending on its operating mode.
For an unlisted Gen1 device, try `product: gen1`. If the field exists but is not modeled, follow the [extension guide](architecture.md#extending-the-gen1-schema).
### Scrapes time out
- Increase `shelly.timeoutSeconds` and Prometheus `scrape_timeout` together.
- Verify Wi-Fi RSSI and packet loss.
- Avoid overly short scrape intervals.
- Check whether a configured battery device is normally asleep.