master
q3k 2018-08-30 13:51:08 +01:00
commit 8502019852
1 changed files with 226 additions and 0 deletions

226
main.go Normal file
View File

@ -0,0 +1,226 @@
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)
}