From 93e2381f422489da38776a3ebd948ff8c6df3b11 Mon Sep 17 00:00:00 2001
From: Pierrick Brossin <dynek@users.noreply.github.com>
Date: Wed, 11 Apr 2018 03:04:59 +0200
Subject: [PATCH] Add Fibaro input plugin (#2741)

---
 README.md                            |   1 +
 plugins/inputs/all/all.go            |   1 +
 plugins/inputs/fibaro/README.md      |  53 +++++++
 plugins/inputs/fibaro/fibaro.go      | 202 ++++++++++++++++++++++++++
 plugins/inputs/fibaro/fibaro_test.go | 204 +++++++++++++++++++++++++++
 5 files changed, 461 insertions(+)
 create mode 100644 plugins/inputs/fibaro/README.md
 create mode 100644 plugins/inputs/fibaro/fibaro.go
 create mode 100644 plugins/inputs/fibaro/fibaro_test.go

diff --git a/README.md b/README.md
index d7d2a359..0d64cab7 100644
--- a/README.md
+++ b/README.md
@@ -147,6 +147,7 @@ configuration options.
 * [elasticsearch](./plugins/inputs/elasticsearch)
 * [exec](./plugins/inputs/exec) (generic executable plugin, support JSON, influx, graphite and nagios)
 * [fail2ban](./plugins/inputs/fail2ban)
+* [fibaro](./plugins/inputs/fibaro)
 * [filestat](./plugins/inputs/filestat)
 * [fluentd](./plugins/inputs/fluentd)
 * [graylog](./plugins/inputs/graylog)
diff --git a/plugins/inputs/all/all.go b/plugins/inputs/all/all.go
index d5800208..e3264ef8 100644
--- a/plugins/inputs/all/all.go
+++ b/plugins/inputs/all/all.go
@@ -24,6 +24,7 @@ import (
 	_ "github.com/influxdata/telegraf/plugins/inputs/elasticsearch"
 	_ "github.com/influxdata/telegraf/plugins/inputs/exec"
 	_ "github.com/influxdata/telegraf/plugins/inputs/fail2ban"
+	_ "github.com/influxdata/telegraf/plugins/inputs/fibaro"
 	_ "github.com/influxdata/telegraf/plugins/inputs/filestat"
 	_ "github.com/influxdata/telegraf/plugins/inputs/fluentd"
 	_ "github.com/influxdata/telegraf/plugins/inputs/graylog"
diff --git a/plugins/inputs/fibaro/README.md b/plugins/inputs/fibaro/README.md
new file mode 100644
index 00000000..512accc7
--- /dev/null
+++ b/plugins/inputs/fibaro/README.md
@@ -0,0 +1,53 @@
+# Fibaro Input Plugin
+
+The Fibaro plugin makes HTTP calls to the Fibaro controller API to gather values of hooked devices.  
+Those values could be true (1) or false (0) for switches, percentage for dimmers, temperature, etc.
+
+
+### Configuration:
+
+```toml
+# Read devices value(s) from a Fibaro controller
+[[inputs.fibaro]]
+  ## Required Fibaro controller address/hostname.
+  ## Note: at the time of writing this plugin, Fibaro only implemented http - no https available
+  url = "http://<controller>:80"
+
+  ## Required credentials to access the API (http://<controller/api/<component>)
+  username = "<username>"
+  password = "<password>"
+
+  ## Amount of time allowed to complete the HTTP request
+  # timeout = "5s"
+```
+
+
+### Tags:
+
+	section: section's name
+	room: room's name
+	name: device's name
+	type: device's type
+
+
+### Fields:
+
+	value float
+	value2 float (when available from device)
+
+### Example Output:
+
+```
+fibaro,host=vm1,name=Escaliers,room=Dégagement,section=Pièces\ communes,type=com.fibaro.binarySwitch value=0 1523351010000000000
+fibaro,host=vm1,name=Porte\ fenêtre,room=Salon,section=Pièces\ communes,type=com.fibaro.FGRM222 value=99,value2=99 1523351010000000000
+fibaro,host=vm1,name=LED\ îlot\ central,room=Cuisine,section=Cuisine,type=com.fibaro.binarySwitch value=0 1523351010000000000
+fibaro,host=vm1,name=Détérioration,room=Entrée,section=Pièces\ communes,type=com.fibaro.heatDetector value=0 1523351010000000000
+fibaro,host=vm1,name=Température,room=Cave,section=Cave,type=com.fibaro.temperatureSensor value=17.87 1523351010000000000
+fibaro,host=vm1,name=Présence,room=Garde-manger,section=Cuisine,type=com.fibaro.FGMS001 value=1 1523351010000000000
+fibaro,host=vm1,name=Luminosité,room=Garde-manger,section=Cuisine,type=com.fibaro.lightSensor value=92 1523351010000000000
+fibaro,host=vm1,name=Etat,room=Garage,section=Extérieur,type=com.fibaro.doorSensor value=0 1523351010000000000
+fibaro,host=vm1,name=CO2\ (ppm),room=Salon,section=Pièces\ communes,type=com.fibaro.multilevelSensor value=880 1523351010000000000
+fibaro,host=vm1,name=Humidité\ (%),room=Salon,section=Pièces\ communes,type=com.fibaro.humiditySensor value=53 1523351010000000000
+fibaro,host=vm1,name=Pression\ (mb),room=Salon,section=Pièces\ communes,type=com.fibaro.multilevelSensor value=1006.9 1523351010000000000
+fibaro,host=vm1,name=Bruit\ (db),room=Salon,section=Pièces\ communes,type=com.fibaro.multilevelSensor value=58 1523351010000000000
+```
diff --git a/plugins/inputs/fibaro/fibaro.go b/plugins/inputs/fibaro/fibaro.go
new file mode 100644
index 00000000..d532c454
--- /dev/null
+++ b/plugins/inputs/fibaro/fibaro.go
@@ -0,0 +1,202 @@
+package fibaro
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"strconv"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+const sampleConfig = `
+  ## Required Fibaro controller address/hostname.
+  ## Note: at the time of writing this plugin, Fibaro only implemented http - no https available
+  url = "http://<controller>:80"
+
+  ## Required credentials to access the API (http://<controller/api/<component>)
+  username = "<username>"
+  password = "<password>"
+
+  ## Amount of time allowed to complete the HTTP request
+  # timeout = "5s"
+`
+
+const description = "Read devices value(s) from a Fibaro controller"
+
+// Fibaro contains connection information
+type Fibaro struct {
+	URL string
+
+	// HTTP Basic Auth Credentials
+	Username string
+	Password string
+
+	Timeout internal.Duration
+
+	client *http.Client
+}
+
+// LinkRoomsSections links rooms to sections
+type LinkRoomsSections struct {
+	Name      string
+	SectionID uint16
+}
+
+// Sections contains sections informations
+type Sections struct {
+	ID   uint16 `json:"id"`
+	Name string `json:"name"`
+}
+
+// Rooms contains rooms informations
+type Rooms struct {
+	ID        uint16 `json:"id"`
+	Name      string `json:"name"`
+	SectionID uint16 `json:"sectionID"`
+}
+
+// Devices contains devices informations
+type Devices struct {
+	ID         uint16 `json:"id"`
+	Name       string `json:"name"`
+	RoomID     uint16 `json:"roomID"`
+	Type       string `json:"type"`
+	Enabled    bool   `json:"enabled"`
+	Properties struct {
+		Dead   interface{} `json:"dead"`
+		Value  interface{} `json:"value"`
+		Value2 interface{} `json:"value2"`
+	} `json:"properties"`
+}
+
+// Description returns a string explaining the purpose of this plugin
+func (f *Fibaro) Description() string { return description }
+
+// SampleConfig returns text explaining how plugin should be configured
+func (f *Fibaro) SampleConfig() string { return sampleConfig }
+
+// getJSON connects, authenticates and reads JSON payload returned by Fibaro box
+func (f *Fibaro) getJSON(path string, dataStruct interface{}) error {
+	var requestURL = f.URL + path
+
+	req, err := http.NewRequest("GET", requestURL, nil)
+	if err != nil {
+		return err
+	}
+
+	req.SetBasicAuth(f.Username, f.Password)
+	resp, err := f.client.Do(req)
+	if err != nil {
+		return err
+	}
+
+	if resp.StatusCode != http.StatusOK {
+		err = fmt.Errorf("Response from url \"%s\" has status code %d (%s), expected %d (%s)",
+			requestURL,
+			resp.StatusCode,
+			http.StatusText(resp.StatusCode),
+			http.StatusOK,
+			http.StatusText(http.StatusOK))
+		return err
+	}
+
+	defer resp.Body.Close()
+
+	dec := json.NewDecoder(resp.Body)
+	err = dec.Decode(&dataStruct)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Gather fetches all required information to output metrics
+func (f *Fibaro) Gather(acc telegraf.Accumulator) error {
+
+	if f.client == nil {
+		f.client = &http.Client{
+			Transport: &http.Transport{
+				Proxy: http.ProxyFromEnvironment,
+			},
+			Timeout: f.Timeout.Duration,
+		}
+	}
+
+	var tmpSections []Sections
+	err := f.getJSON("/api/sections", &tmpSections)
+	if err != nil {
+		return err
+	}
+	sections := map[uint16]string{}
+	for _, v := range tmpSections {
+		sections[v.ID] = v.Name
+	}
+
+	var tmpRooms []Rooms
+	err = f.getJSON("/api/rooms", &tmpRooms)
+	if err != nil {
+		return err
+	}
+	rooms := map[uint16]LinkRoomsSections{}
+	for _, v := range tmpRooms {
+		rooms[v.ID] = LinkRoomsSections{Name: v.Name, SectionID: v.SectionID}
+	}
+
+	var devices []Devices
+	err = f.getJSON("/api/devices", &devices)
+	if err != nil {
+		return err
+	}
+
+	for _, device := range devices {
+		// skip device in some cases
+		if device.RoomID == 0 ||
+			device.Enabled == false ||
+			device.Properties.Dead == "true" ||
+			device.Type == "com.fibaro.zwaveDevice" {
+			continue
+		}
+
+		tags := map[string]string{
+			"section": sections[rooms[device.RoomID].SectionID],
+			"room":    rooms[device.RoomID].Name,
+			"name":    device.Name,
+			"type":    device.Type,
+		}
+		fields := make(map[string]interface{})
+
+		if device.Properties.Value != nil {
+			value := device.Properties.Value
+			switch value {
+			case "true":
+				value = "1"
+			case "false":
+				value = "0"
+			}
+
+			if fValue, err := strconv.ParseFloat(value.(string), 64); err == nil {
+				fields["value"] = fValue
+			}
+		}
+
+		if device.Properties.Value2 != nil {
+			if fValue, err := strconv.ParseFloat(device.Properties.Value2.(string), 64); err == nil {
+				fields["value2"] = fValue
+			}
+		}
+
+		acc.AddFields("fibaro", fields, tags)
+	}
+
+	return nil
+}
+
+func init() {
+	inputs.Add("fibaro", func() telegraf.Input {
+		return &Fibaro{}
+	})
+}
diff --git a/plugins/inputs/fibaro/fibaro_test.go b/plugins/inputs/fibaro/fibaro_test.go
new file mode 100644
index 00000000..111f4c87
--- /dev/null
+++ b/plugins/inputs/fibaro/fibaro_test.go
@@ -0,0 +1,204 @@
+package fibaro
+
+import (
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/influxdata/telegraf/testutil"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+const sectionsJSON = `
+    [
+        {
+            "id": 1,
+            "name": "Section 1",
+            "sortOrder": 1
+        },
+        {
+            "id": 2,
+            "name": "Section 2",
+            "sortOrder": 2
+        },
+        {
+            "id": 3,
+            "name": "Section 3",
+            "sortOrder": 3
+        }
+    ]`
+
+const roomsJSON = `
+    [
+        {
+            "id": 1,
+            "name": "Room 1",
+            "sectionID": 1,
+            "icon": "room_1",
+            "sortOrder": 1
+        },
+        {
+            "id": 2,
+            "name": "Room 2",
+            "sectionID": 2,
+            "icon": "room_2",
+            "sortOrder": 2
+        },
+        {
+            "id": 3,
+            "name": "Room 3",
+            "sectionID": 3,
+            "icon": "room_3",
+            "sortOrder": 3
+        },
+        {
+            "id": 4,
+            "name": "Room 4",
+            "sectionID": 3,
+            "icon": "room_4",
+            "sortOrder": 4
+        }
+    ]`
+
+const devicesJSON = `
+    [
+        {
+            "id": 1,
+            "name": "Device 1",
+            "roomID": 1,
+            "type": "com.fibaro.binarySwitch",
+            "enabled": true,
+            "properties": {
+                "dead": "false",
+                "value": "false"
+            },
+            "sortOrder": 1
+        },
+        {
+            "id": 2,
+            "name": "Device 2",
+            "roomID": 2,
+            "type": "com.fibaro.binarySwitch",
+            "enabled": true,
+            "properties": {
+                "dead": "false",
+                "value": "true"
+            },
+            "sortOrder": 2
+        },
+        {
+            "id": 3,
+            "name": "Device 3",
+            "roomID": 3,
+            "type": "com.fibaro.multilevelSwitch",
+            "enabled": true,
+            "properties": {
+                "dead": "false",
+                "value": "67"
+            },
+            "sortOrder": 3
+        },
+        {
+            "id": 4,
+            "name": "Device 4",
+            "roomID": 4,
+            "type": "com.fibaro.temperatureSensor",
+            "enabled": true,
+            "properties": {
+                "dead": "false",
+                "value": "22.80"
+            },
+            "sortOrder": 4
+        },
+        {
+            "id": 5,
+            "name": "Device 5",
+            "roomID": 4,
+            "type": "com.fibaro.FGRM222",
+            "enabled": true,
+            "properties": {
+                "dead": "false",
+                "value": "50",
+                "value2": "75"
+            },
+            "sortOrder": 5
+        }
+    ]`
+
+// TestUnauthorized validates that 401 (wrong credentials) is managed properly
+func TestUnauthorized(t *testing.T) {
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(http.StatusUnauthorized)
+	}))
+	defer ts.Close()
+
+	a := Fibaro{
+		URL:      ts.URL,
+		Username: "user",
+		Password: "pass",
+		client:   &http.Client{},
+	}
+
+	var acc testutil.Accumulator
+	err := acc.GatherError(a.Gather)
+	require.Error(t, err)
+}
+
+// TestJSONSuccess validates that module works OK with valid JSON payloads
+func TestJSONSuccess(t *testing.T) {
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		payload := ""
+		switch r.URL.Path {
+		case "/api/sections":
+			payload = sectionsJSON
+		case "/api/rooms":
+			payload = roomsJSON
+		case "/api/devices":
+			payload = devicesJSON
+		}
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintln(w, payload)
+	}))
+	defer ts.Close()
+
+	a := Fibaro{
+		URL:      ts.URL,
+		Username: "user",
+		Password: "pass",
+		client:   &http.Client{},
+	}
+
+	var acc testutil.Accumulator
+	err := acc.GatherError(a.Gather)
+	require.NoError(t, err)
+
+	// Gather should add 5 metrics
+	assert.Equal(t, uint64(5), acc.NMetrics())
+
+	// Ensure fields / values are correct - Device 1
+	tags := map[string]string{"section": "Section 1", "room": "Room 1", "name": "Device 1", "type": "com.fibaro.binarySwitch"}
+	fields := map[string]interface{}{"value": float64(0)}
+	acc.AssertContainsTaggedFields(t, "fibaro", fields, tags)
+
+	// Ensure fields / values are correct - Device 2
+	tags = map[string]string{"section": "Section 2", "room": "Room 2", "name": "Device 2", "type": "com.fibaro.binarySwitch"}
+	fields = map[string]interface{}{"value": float64(1)}
+	acc.AssertContainsTaggedFields(t, "fibaro", fields, tags)
+
+	// Ensure fields / values are correct - Device 3
+	tags = map[string]string{"section": "Section 3", "room": "Room 3", "name": "Device 3", "type": "com.fibaro.multilevelSwitch"}
+	fields = map[string]interface{}{"value": float64(67)}
+	acc.AssertContainsTaggedFields(t, "fibaro", fields, tags)
+
+	// Ensure fields / values are correct - Device 4
+	tags = map[string]string{"section": "Section 3", "room": "Room 4", "name": "Device 4", "type": "com.fibaro.temperatureSensor"}
+	fields = map[string]interface{}{"value": float64(22.8)}
+	acc.AssertContainsTaggedFields(t, "fibaro", fields, tags)
+
+	// Ensure fields / values are correct - Device 5
+	tags = map[string]string{"section": "Section 3", "room": "Room 4", "name": "Device 5", "type": "com.fibaro.FGRM222"}
+	fields = map[string]interface{}{"value": float64(50), "value2": float64(75)}
+	acc.AssertContainsTaggedFields(t, "fibaro", fields, tags)
+}
-- 
GitLab