luftdaten-prom/main.go

227 lines
5.5 KiB
Go
Raw Permalink Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

package main
import (
"flag"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/golang/glog"
"golang.org/x/net/html"
)
var (
flagLuftdaten string
flagScrapeInterval int
flagListen string
)
func init() {
flag.Set("logtostderr", "true")
}
type sensorData struct {
// Whether the data is fresh.
stale bool
// PM2.5 in ug/m3
pm25 float64
// PM10 in ug/m3
pm10 float64
// Temperature in degrees centigrade
temperature float64
// Humidity in percent
humidity float64
}
func getSensorData() (sensorData, error) {
data := sensorData{}
resp, err := http.Get(flagLuftdaten)
if err != nil {
return data, fmt.Errorf("when GETing data: %v", err)
}
root, err := html.Parse(resp.Body)
if err != nil {
return data, fmt.Errorf("when parsing HTML: %v", err)
}
// Find html.
htmlN := root.FirstChild
if htmlN == nil || htmlN.Type != html.ElementNode || htmlN.Data != "html" {
return data, fmt.Errorf("unexpected root node")
}
// Find body.
bodyN := htmlN.FirstChild.NextSibling
if bodyN == nil || bodyN.Type != html.ElementNode || bodyN.Data != "body" {
return data, fmt.Errorf("unexpected html[1] node")
}
// Find div class=content
contentN := bodyN.FirstChild.NextSibling
if contentN == nil || contentN.Type != html.ElementNode || contentN.Data != "div" || contentN.Attr[0].Key != "class" || contentN.Attr[0].Val != "content" {
return data, fmt.Errorf("unexpected body[1] node")
}
// Find table.
var tableN *html.Node = nil
for c := contentN.FirstChild; c != nil; c = c.NextSibling {
if c.Type == html.ElementNode && c.Data == "table" {
tableN = c
break
}
}
if tableN == nil {
return data, fmt.Errorf("could not find table")
}
// Find tbody.
tbodyN := tableN.FirstChild
if tbodyN == nil || tbodyN.Type != html.ElementNode || tbodyN.Data != "tbody" {
return data, fmt.Errorf("unexpected table[0] node")
}
got := make(map[string]bool)
// Iterate throught rows.
for row := tbodyN.FirstChild; row != nil; row = row.NextSibling {
td1 := row.FirstChild
if td1 == nil {
continue
}
td2 := td1.NextSibling
if td2 == nil {
continue
}
td3 := td2.NextSibling
if td3 == nil {
continue
}
if td1.Data != "td" || td2.Data != "td" || td3.Data != "td" {
continue
}
if td2.FirstChild.Data == "PM2.5" {
// This is not an ASCII space.
parts := strings.Split(td3.FirstChild.Data, " ")
if len(parts) != 2 {
continue
}
pm25, err := strconv.ParseFloat(parts[0], 64)
if err != nil {
continue
}
data.pm25 = pm25
got["PM2.5"] = true
}
if td2.FirstChild.Data == "PM10" {
// This is not an ASCII space.
parts := strings.Split(td3.FirstChild.Data, " ")
if len(parts) != 2 {
continue
}
pm10, err := strconv.ParseFloat(parts[0], 64)
if err != nil {
continue
}
data.pm10 = pm10
got["PM10"] = true
}
if td2.FirstChild.Data == "Temperatur" {
// This is not an ASCII space.
parts := strings.Split(td3.FirstChild.Data, " ")
if len(parts) != 2 {
continue
}
temp, err := strconv.ParseFloat(parts[0], 64)
if err != nil {
continue
}
data.temperature = temp
got["Temperature"] = true
}
if td2.FirstChild.Data == "rel. Luftfeuchte" {
// This is not an ASCII space.
parts := strings.Split(td3.FirstChild.Data, " ")
if len(parts) != 2 {
continue
}
hum, err := strconv.ParseFloat(parts[0], 64)
if err != nil {
continue
}
data.humidity = hum
got["Humidity"] = true
}
}
for _, want := range []string{"PM2.5", "PM10", "Temperature", "Humidity"} {
if !got[want] {
return data, fmt.Errorf("could not find %v in HTML", want)
}
}
return data, nil
}
func scraper(req chan int, resp chan sensorData) error {
data := sensorData{
stale: true,
}
feed := func() {
newData, err := getSensorData()
if err != nil {
glog.Error("Could not get sensor data: %v", err)
data.stale = true
} else {
data = newData
glog.Errorf("Got sensor data: %f/%f/%f/%f", data.pm25, data.pm10, data.temperature, data.humidity)
}
}
feed()
ticker := time.NewTicker(time.Duration(flagScrapeInterval) * time.Second)
for {
select {
case <-req:
resp <- data
case <-ticker.C:
feed()
}
}
}
func main() {
flag.StringVar(&flagLuftdaten, "luftdaten", "http://127.0.0.1:2140/values", "Luftdaten sensor endpoint")
flag.IntVar(&flagScrapeInterval, "scrape_interval", 60, "Scrape interval of Luftdaten, in seconds")
flag.StringVar(&flagListen, "listen", "0.0.0.0:2141", "Address to listen at for Prometheus requests")
flag.Parse()
glog.Info("Starting scraper...")
req := make(chan int, 1)
res := make(chan sensorData, 1)
go scraper(req, res)
http.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
req <- 0
data := <-res
if data.stale {
fmt.Fprintf(w, "# data is stale :(\n")
return
}
fmt.Fprintf(w, "# HELP pm25 The current PM2.5 concentration in micrograms per cubic meter\n")
fmt.Fprintf(w, "# TYPE pm25 gauge\n")
fmt.Fprintf(w, "pm25 %f\n", data.pm25)
fmt.Fprintf(w, "# HELP pm10 The current PM10 concentration in micrograms per cubic meter\n")
fmt.Fprintf(w, "# TYPE pm10 gauge\n")
fmt.Fprintf(w, "pm10 %f\n", data.pm10)
fmt.Fprintf(w, "# HELP temperature The current temperature in degrees centigrate\n")
fmt.Fprintf(w, "# TYPE temperature gauge\n")
fmt.Fprintf(w, "temperature %f\n", data.temperature)
fmt.Fprintf(w, "# HELP humidity The current relative humidity in percent\n")
fmt.Fprintf(w, "# TYPE humidity gauge\n")
fmt.Fprintf(w, "humidity %f\n", data.humidity)
})
http.ListenAndServe(flagListen, nil)
}