diff --git a/README.md b/README.md index d7d2a3599..0d64cab73 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 d58002089..e3264ef8b 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 000000000..512accc76 --- /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://:80" + + ## Required credentials to access the API (http://) + username = "" + 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 000000000..d532c454e --- /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://:80" + + ## Required credentials to access the API (http://) + username = "" + 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 000000000..111f4c872 --- /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) +}