Add Fibaro input plugin (#2741)
This commit is contained in:
parent
da6a299a22
commit
2e0da23c7b
|
@ -147,6 +147,7 @@ configuration options.
|
||||||
* [elasticsearch](./plugins/inputs/elasticsearch)
|
* [elasticsearch](./plugins/inputs/elasticsearch)
|
||||||
* [exec](./plugins/inputs/exec) (generic executable plugin, support JSON, influx, graphite and nagios)
|
* [exec](./plugins/inputs/exec) (generic executable plugin, support JSON, influx, graphite and nagios)
|
||||||
* [fail2ban](./plugins/inputs/fail2ban)
|
* [fail2ban](./plugins/inputs/fail2ban)
|
||||||
|
* [fibaro](./plugins/inputs/fibaro)
|
||||||
* [filestat](./plugins/inputs/filestat)
|
* [filestat](./plugins/inputs/filestat)
|
||||||
* [fluentd](./plugins/inputs/fluentd)
|
* [fluentd](./plugins/inputs/fluentd)
|
||||||
* [graylog](./plugins/inputs/graylog)
|
* [graylog](./plugins/inputs/graylog)
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
_ "github.com/influxdata/telegraf/plugins/inputs/elasticsearch"
|
_ "github.com/influxdata/telegraf/plugins/inputs/elasticsearch"
|
||||||
_ "github.com/influxdata/telegraf/plugins/inputs/exec"
|
_ "github.com/influxdata/telegraf/plugins/inputs/exec"
|
||||||
_ "github.com/influxdata/telegraf/plugins/inputs/fail2ban"
|
_ "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/filestat"
|
||||||
_ "github.com/influxdata/telegraf/plugins/inputs/fluentd"
|
_ "github.com/influxdata/telegraf/plugins/inputs/fluentd"
|
||||||
_ "github.com/influxdata/telegraf/plugins/inputs/graylog"
|
_ "github.com/influxdata/telegraf/plugins/inputs/graylog"
|
||||||
|
|
|
@ -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
|
||||||
|
```
|
|
@ -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{}
|
||||||
|
})
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
Loading…
Reference in New Issue