diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba698c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode/launch.json diff --git a/README.md b/README.md index 26247eb..a285b38 100644 --- a/README.md +++ b/README.md @@ -1 +1,48 @@ -# hue_exporter \ No newline at end of file +# hue_exporter + +This exporter exports some variables from Philips Hue Bridge +(https://www.philips-hue.com) +to prometheus. + +## Building + + go get github.com/aexel90/hue_exporter/ + cd $GOPATH/src/github.com/aexel90/hue_exporter + go install + +## Running + +How to create a user for your bridge is described here: https://developers.meethue.com/develop/get-started-2/ + +Usage: + + $GOPATH/bin/hue_exporter -h + + Usage of ./hue_exporter: + -hue-url string + The URL of the bridge + -listen-address string + The address to listen on for HTTP requests. (default "127.0.0.1:9773") + -test + test configured metrics + -username string + The username token having bridge access + +## Example execution + +### Running within prometheus: + + $GOPATH/bin/hue_exporter -hue_url 192.168.xxx.xxx -username ZlEH24zabK2jTpJ... + + # HELP hue_light_status status of lights registered at hue bridge + # TYPE hue_light_status gauge + hue_light_status{manufacturer_name="...",model_id="...",name="...",state_alert="...",state_bri="...",state_ct="...",state_on="...",state_reachable="...",state_saturation="...",sw_version="...",type="...",unique_id="..."} 1 + hue_light_status{manufacturer_name="...",model_id="...",name="...",state_alert="...",state_bri="...",state_ct="...",state_on="...",state_reachable="...",state_saturation="...",sw_version="...",type="...",unique_id="..."} 0 + ... + +### Test exporter: + + $GOPATH/bin/hue_exporter -hue_url 192.168.xxx.xxx -username ZlEH24zabK2jTpJ... -test + +## Grafana Dashboard + diff --git a/collector/collector.go b/collector/collector.go new file mode 100644 index 0000000..12129f2 --- /dev/null +++ b/collector/collector.go @@ -0,0 +1,182 @@ +package collector + +import ( + "fmt" + "strings" + + "github.com/prometheus/client_golang/prometheus" + + "github.com/aexel90/hue_exporter/hue" + "github.com/aexel90/hue_exporter/metric" +) + +// Collector instance +type Collector struct { + exporter *hue.Exporter + metrics []*metric.Metric +} + +// NewHueCollector initialization +func NewHueCollector(URL string, username string) (*Collector, error) { + + hueExporter := hue.Exporter{ + BaseURL: URL, + Username: username, + } + + return &Collector{&hueExporter, nil}, nil +} + +// Describe for prometheus +func (collector *Collector) Describe(ch chan<- *prometheus.Desc) { + + collector.metrics = collector.exporter.InitMetrics() + collector.initDescAndType() +} + +// Collect for prometheus +func (collector *Collector) Collect(ch chan<- prometheus.Metric) { + + err := collector.collect() + if err != nil { + fmt.Println("Error: ", err) + } + + for _, m := range collector.metrics { + for _, promResult := range m.PromResult { + ch <- prometheus.MustNewConstMetric(promResult.PromDesc, promResult.PromValueType, promResult.Value, promResult.LabelValues...) + } + } +} + +//Test collector metrics +func (collector *Collector) Test() { + + collector.metrics = collector.exporter.InitMetrics() + collector.initDescAndType() + + err := collector.collect() + if err != nil { + fmt.Println("Error: ", err) + } + + err = collector.printResult() +} + +func (collector *Collector) printResult() error { + + for _, m := range collector.metrics { + fmt.Printf("Metric: %v\n", m.FqName) + fmt.Printf(" - Exporter Result:\n") + + for i, result := range m.MetricResult { + fmt.Printf(" - Exporter Result %v:\n", i) + for key, value := range result { + fmt.Printf(" - %s=\"%v\"\n", key, value) + } + } + + for _, promResult := range m.PromResult { + + fmt.Printf(" - prom desc: %v\n", promResult.PromDesc) + fmt.Printf(" - prom metric type: %v\n", promResult.PromValueType) + fmt.Printf(" - prom metric value: %v\n", uint64(promResult.Value)) + fmt.Printf(" - prom label values: %v\n", promResult.LabelValues) + } + } + + return nil +} + +func (collector *Collector) collect() (err error) { + + err = collector.exporter.Collect(collector.metrics) + if err != nil { + return err + } + + err = collector.getResult() + if err != nil { + return err + } + return nil +} + +func (collector *Collector) getResult() (err error) { + + for _, m := range collector.metrics { + m.PromResult = nil + for _, metricResult := range m.MetricResult { + + labelValues, err := getLabelValues(m.Labels, metricResult) + if err != nil { + return err + } + resultValue, err := getResultValue(m.ResultKey, metricResult) + if err != nil { + return err + } + + result := metric.PrometheusResult{PromDesc: m.PromDesc, PromValueType: m.PromType, Value: resultValue, LabelValues: labelValues} + m.PromResult = append(m.PromResult, &result) + } + } + return nil +} + +func (collector *Collector) initDescAndType() { + + for _, metric := range collector.metrics { + + var help string + + switch metric.HueType { + case hue.TypeLight: + metric.FqName = "hue_light_status" + help = "status of lights registered at hue bridge" + metric.PromType = prometheus.GaugeValue + } + + labels := []string{} + for _, label := range metric.Labels { + labels = append(labels, strings.ToLower(label)) + } + + metric.PromDesc = prometheus.NewDesc(metric.FqName, help, labels, nil) + } +} + +func getResultValue(resultKey string, result map[string]interface{}) (float64, error) { + + value := result[resultKey] + var floatValue float64 + + switch tval := value.(type) { + case float64: + floatValue = tval + case int: + floatValue = float64(tval) + case uint64: + floatValue = float64(tval) + case bool: + if tval { + floatValue = 1 + } else { + floatValue = 0 + } + default: + return 0, fmt.Errorf("[getResultValue] %v in %v - unknown type: %T", resultKey, result, value) + } + return floatValue, nil +} + +func getLabelValues(labelNames []string, result map[string]interface{}) ([]string, error) { + + labelValues := []string{} + for _, labelname := range labelNames { + labelValue := fmt.Sprintf("%v", result[labelname]) + labelValue = strings.ToLower(labelValue) + labelValues = append(labelValues, labelValue) + } + return labelValues, nil +} diff --git a/hue/hue.go b/hue/hue.go new file mode 100644 index 0000000..4d2196e --- /dev/null +++ b/hue/hue.go @@ -0,0 +1,127 @@ +package hue + +import ( + "fmt" + "log" + + "github.com/aexel90/hue_exporter/metric" + hue "github.com/collinux/gohue" +) + +// Exporter data +type Exporter struct { + BaseURL string + Username string +} + +const ( + TypeLight = "light" + TypeOther = "???" + + LightLabelName = "Name" + LightLabelType = "Type" + LightLabelModelID = "Model_ID" + LightLabelManufacturerName = "Manufacturer_Name" + LightLabelSWVersion = "SW_Version" + LightLabelUniqueID = "Unique_ID" + LightLabelStateOn = "State_On" + LightLabelStateAlert = "State_Alert" + LightLabelStateBri = "State_Bri" + LightLabelStateCT = "State_CT" + LightLabelStateReachable = "State_Reachable" + LightLabelStateSaturation = "State_Saturation" +) + +// InitMetrics func +func (exporter *Exporter) InitMetrics() (metrics []*metric.Metric) { + + metrics = append(metrics, &metric.Metric{ + HueType: TypeLight, + Labels: []string{LightLabelName, LightLabelType, LightLabelModelID, LightLabelManufacturerName, LightLabelSWVersion, LightLabelUniqueID, LightLabelStateOn, LightLabelStateAlert, LightLabelStateBri, LightLabelStateCT, LightLabelStateReachable, LightLabelStateSaturation}, + ResultKey: LightLabelStateOn}) + + return metrics +} + +// Collect metrics +func (exporter *Exporter) Collect(metrics []*metric.Metric) (err error) { + + bridge := newBridge(exporter.BaseURL) + + err = bridge.Login(exporter.Username) + if err != nil { + return fmt.Errorf("[error login] '%v'", err) + } + + for _, metric := range metrics { + + var err error + + switch metric.HueType { + case TypeLight: + err = collectLights(bridge, metric) + } + + if err != nil { + return err + } + } + + return nil +} + +func collectLights(bridge *hue.Bridge, metric *metric.Metric) (err error) { + + metric.MetricResult = nil + + lights, err := bridge.GetAllLights() + if err != nil { + return fmt.Errorf("[error GetAllLights()] '%v'", err) + } + + for _, light := range lights { + + result := make(map[string]interface{}) + for _, label := range metric.Labels { + + switch label { + case LightLabelName: + result[LightLabelName] = light.Name + case LightLabelType: + result[LightLabelType] = light.Type + case LightLabelModelID: + result[LightLabelModelID] = light.ModelID + case LightLabelManufacturerName: + result[LightLabelManufacturerName] = light.ManufacturerName + case LightLabelSWVersion: + result[LightLabelSWVersion] = light.SWVersion + case LightLabelUniqueID: + result[LightLabelUniqueID] = light.UniqueID + case LightLabelStateOn: + result[LightLabelStateOn] = light.State.On + case LightLabelStateAlert: + result[LightLabelStateAlert] = light.State.Alert + case LightLabelStateBri: + result[LightLabelStateBri] = light.State.Bri + case LightLabelStateCT: + result[LightLabelStateCT] = light.State.CT + case LightLabelStateReachable: + result[LightLabelStateReachable] = light.State.Reachable + case LightLabelStateSaturation: + result[LightLabelStateSaturation] = light.State.Saturation + } + } + + metric.MetricResult = append(metric.MetricResult, result) + } + + return nil +} + +func newBridge(ipAddr string) *hue.Bridge { + bridge, err := hue.NewBridge(ipAddr) + if err != nil { + log.Fatalf("Error connecting to Hue bridge at '%v': '%v'\n", ipAddr, err) + } + return bridge +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..c167919 --- /dev/null +++ b/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "log" + "net/http" + + "github.com/namsral/flag" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + + "github.com/aexel90/hue_exporter/collector" +) + +var ( + flagBridgeURL = flag.String("hue-url", "", "The URL of the bridge") + flagUsername = flag.String("username", "", "The username token having bridge access") + flagAddress = flag.String("listen-address", "127.0.0.1:9773", "The address to listen on for HTTP requests.") + + flagTest = flag.Bool("test", false, "test configured metrics") +) + +func main() { + + flag.Parse() + + hueCollector, err := collector.NewHueCollector(*flagBridgeURL, *flagUsername) + if err != nil { + fmt.Println(err) + return + } + + // test mode + if *flagTest { + hueCollector.Test() + return + } + + prometheus.MustRegister(hueCollector) + + http.Handle("/metrics", promhttp.Handler()) + fmt.Printf("metrics available at http://%s/metrics\n", *flagAddress) + log.Fatal(http.ListenAndServe(*flagAddress, nil)) +} diff --git a/metric/metric.go b/metric/metric.go new file mode 100644 index 0000000..dba7229 --- /dev/null +++ b/metric/metric.go @@ -0,0 +1,40 @@ +package metric + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +type PrometheusResult struct { + PromDesc *prometheus.Desc + PromValueType prometheus.ValueType + Value float64 + LabelValues []string +} + +// Metric struct +type Metric struct { + // PromDesc PromDesc `json:"promDesc"` + // PromType string `json:"promType"` + // ResultKey string `json:"resultKey"` + // OkValue string `json:"okValue"` + // ResultPath string `json:"resultPath"` + // Page string `json:"page"` + // Service string `json:"service"` + // Action string `json:"action"` + // ActionArgument *ActionArg `json:"actionArgument"` + + // Desc *prometheus.Desc + + // Value float64 + // labelValues []string + + HueType string + Labels []string + MetricResult []map[string]interface{} //filled during collect + ResultKey string + FqName string + + PromType prometheus.ValueType + PromDesc *prometheus.Desc + PromResult []*PrometheusResult +}