Add Fibaro input plugin (#2741)
This commit is contained in:
parent
387bae9b9f
commit
93e2381f42
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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