ALLTERCO / shelly-script-examples

Shelly Scripts for Gen2 Shelly devices
Apache License 2.0
188 stars 55 forks source link

prometheus.js not working on Shelly PM Mini Gen3 #109

Closed gmoscalu closed 1 week ago

gmoscalu commented 1 month ago

Describe the bug Script gives an error not the desired output.

To Reproduce Steps to reproduce the behavior:

  1. added the script as it comes from the library to my device
  2. adjusted the const monitored_switches = ["pm1:0"]
  3. calling from POSTMAN: GET http://10.10.200.43/script/2/metrics
  4. result:

    Body: "error" Status: 500 Internal Server Error Logs: { "ts": 1721422542.866, "level": 2, "data": "shelly_ejs_rpc.cpp:41 Shelly.call Shelly.GetStatus {}", "fd": 1 }, { "ts": 1721422542.9, "level": 2, "data": "shos_rpc_inst.c:230 Shelly.GetStatus via loopback ", "fd": 1 }, { "ts": 1721422545.687, "level": 2, "data": "in function \"generateMetricsForSwitch\" called from generateMetricsForSwitch(switch_string_id)", "fd": 102, "component": "script:2" }, { "ts": 1721422545.688, "level": 2, "data": " ^", "fd": 102, "component": "script:2" }, { "ts": 1721422545.688, "level": 2, "data": "in function called from }).join(\"\")", "fd": 102, "component": "script:2" }, { "ts": 1721422545.689, "level": 2, "data": " ^", "fd": 102, "component": "script:2" }, { "ts": 1721422545.689, "level": 2, "data": "in function called from system", "fd": 102, "component": "script:2" }, { "ts": 1721422545.69, "level": 1, "data": "shelly_user_script.:388 error: Error in EjsCall", "fd": 1 }, { "ts": 1721422545.691, "level": 2, "data": "shelly_ejs_httpserv:348 0x3fcba8bc send() -> -9 error", "fd": 1 }, { "ts": 1721422545.708, "level": 2, "data": "shelly_ejs_httpserv:117 2 unregistered /script/2/metrics", "fd": 1 }, { "ts": 1721422545.73, "level": 2, "data": "shelly_notification:163 Status change of script:2: {\"id\":2,\"errors\":[\"error\"],\"running\":false}", "fd": 1 },

Expected behavior As "promised" by the script, i hoped to be able to scrape these devices (10 of them) directly from Prometheus, not going through the json_exporter tool...

Screenshots Postman: image

Console: image

Device and script details (please complete the following information):

Additional context Add any other context about the problem here.

raikel93 commented 1 month ago

Hey @gmoscalu ,

i got the same problem with the script. But i could fixed it by reading the manual: https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/PM1/

The constant is CASE-SENSITIVE so try this: const monitored_switches = ["PM1:0"]

And the Temperature is not a valid Metric of the PM mini. Here is my modified Script:

/*
 * This script exposes a "/status" endpoint that returns Prometheus metrics that can be scraped.
 * It will be reachable under "<ip>/script/<id>/metrics". Id will be 1 if this is your first script.
 *
 * Example Prometheus config:
 *
 * scrape_configs:
 *   - job_name: 'shelly'
 *     metrics_path: /script/1/metrics
 *     static_configs:
 *       - targets: ['<ip>']
 *
 * Replace <ip> with the ip address of the device and adjust the script id if needed.
 *
 * If you use Grafana to display metrics scraped, you can give a try to the example dashboard in prometheus-grafana-example-dashboard.json
 * available via the url https://raw.githubusercontent.com/ALLTERCO/shelly-script-examples/main/prometheus-grafana-example-dashboard.json
 *
 * Note: This script assumes you have one switch, but you can configure that in the Configuration section
 */
// ---------------------------------------------------------------------------------------------------------------------
// Configuration (you can change / adapt here)
// ---------------------------------------------------------------------------------------------------------------------

// Prefix added to all metrics name
const metric_prefix = "shelly_"
// url of metrics. will be used for the last part of "<ip>/script/<id>/<url>"
//    where ip = your plug ip/hostname; id the script id or number (1 if this is the first); url the value below
const url = "metrics"

// List of internal switch to monitor using the shelly's id naming scheme in the form of switch:<id> (ex switch:0)
const monitored_switches = ["PM1:0"]

// ---------------------------------------------------------------------------------------------------------------------
// Prometheus exporter
// ---------------------------------------------------------------------------------------------------------------------

const TYPE_GAUGE = "gauge"
const TYPE_COUNTER = "counter"

var info = Shelly.getDeviceInfo();

function promLabel(label, value) {
  return [label, "=", '"', value, '"'].join("");
}

// Will be added to every metrics
var defaultLabels = [
  ["name", info.name],
  ["id", info.id],
  ["mac", info.mac],
  ["app", info.app]
]
  .map(function (data) {
    return promLabel(data[0], data[1]);
  });

/**
 *  Generate one metric output with all the shenanigans around it
 * @param name The name of the metrics (Will be prefixed by metric_prefix value)
 * @param type One of the TYPE_* of metrics. Usually Counter, Gauge, Histogram (not supported yes)
 * @param specificLabels Array of labels generated by promLabel() specific to this metric.
 * @param description An Human description of the metric
 * @param value The actual metric numeric value
 * @returns {string} The metric string to include in final response
 */
function printPrometheusMetric(name, type, specificLabels, description, value) {
  return [
    "# HELP ", metric_prefix, name, " ", description, "\n",
    "# HELP ", metric_prefix, name, " ", type, "\n",
    metric_prefix, name, "{", defaultLabels.join(","), specificLabels.length > 0 ? "," : "", specificLabels.join(","), "}", " ", value, "\n\n"
  ].join("");
}

/**
 * HTTP handler that will be called when the url will be accessed
 * @param request
 * @param response
 */
function httpServerHandler(request, response) {
  response.body = [
    generateMetricsForSystem(),
    monitored_switches.map(function (switch_string_id) {
      return generateMetricsForSwitch(switch_string_id)
    }).join("")
  ].join("")
  response.code = 200;
  response.send();
}

/**
 * Generate metrics for the system part
 * @returns {string}
 */
function generateMetricsForSystem() {
  const sys = Shelly.getComponentStatus("sys")
  return [
    printPrometheusMetric("uptime_seconds", TYPE_COUNTER, [], "power level in watts", sys.uptime),
    printPrometheusMetric("ram_size_bytes", TYPE_GAUGE, [], "Internal board RAM size in bytes", sys.ram_size),
    printPrometheusMetric("ram_free_bytes", TYPE_GAUGE, [], "Internal board free RAM size in bytes", sys.ram_free)
  ].join("")
}

/**
 * generate metrics for one switch with the name given as parameter
 * @returns {string}
 */
function generateMetricsForSwitch(string_id) {
  const pm = Shelly.getComponentStatus(string_id);
  return [
    printPrometheusMetric("pm_power_watts", TYPE_GAUGE, [promLabel("pm", pm.id)], "Instant power consumption in watts", pm.apower),
    printPrometheusMetric("pm_voltage_volts", TYPE_GAUGE, [promLabel("pm", pm.id)], "Instant voltage in volts", pm.voltage),
    printPrometheusMetric("pm_current_amperes", TYPE_GAUGE, [promLabel("pm", pm.id)], "Instant current in amperes", pm.current),
    printPrometheusMetric("pm_returned_energy_total", TYPE_COUNTER, [promLabel("pm", pm.id)], "Total returned energy consumed in Watt-hours", pm.ret_aenergy.total),
    printPrometheusMetric("pm_power_total", TYPE_COUNTER, [promLabel("pm", pm.id)], "accumulated energy consumed in watts hours", pm.aenergy.total)
  ].join("");
}

HTTPServer.registerEndpoint(url, httpServerHandler);
gmoscalu commented 1 week ago

Thanks a lot @raikel93 !

Your script seems to work very well!