feat(inputs.fibaro): Support HC3 device types (#13754)
This commit is contained in:
parent
5fb290fb46
commit
ca2295e1a4
|
|
@ -4,6 +4,9 @@ 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,
|
of hooked devices. Those values could be true (1) or false (0) for switches,
|
||||||
percentage for dimmers, temperature, etc.
|
percentage for dimmers, temperature, etc.
|
||||||
|
|
||||||
|
By default, this plugin supports HC2 devices. To support HC3 devices, please
|
||||||
|
use the device type config option.
|
||||||
|
|
||||||
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
|
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
|
||||||
|
|
||||||
In addition to the plugin-specific configuration settings, plugins support
|
In addition to the plugin-specific configuration settings, plugins support
|
||||||
|
|
@ -28,6 +31,11 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
|
||||||
|
|
||||||
## Amount of time allowed to complete the HTTP request
|
## Amount of time allowed to complete the HTTP request
|
||||||
# timeout = "5s"
|
# timeout = "5s"
|
||||||
|
|
||||||
|
## Fibaro Device Type
|
||||||
|
## By default, this plugin will attempt to read using the HC2 API. For HC3
|
||||||
|
## devices, set this to "HC3"
|
||||||
|
# device_type = "HC2"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Metrics
|
## Metrics
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,16 @@ package fibaro
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/influxdata/telegraf"
|
"github.com/influxdata/telegraf"
|
||||||
"github.com/influxdata/telegraf/config"
|
"github.com/influxdata/telegraf/config"
|
||||||
"github.com/influxdata/telegraf/plugins/inputs"
|
"github.com/influxdata/telegraf/plugins/inputs"
|
||||||
|
"github.com/influxdata/telegraf/plugins/inputs/fibaro/hc2"
|
||||||
|
"github.com/influxdata/telegraf/plugins/inputs/fibaro/hc3"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed sample.conf
|
//go:embed sample.conf
|
||||||
|
|
@ -21,66 +22,28 @@ const defaultTimeout = 5 * time.Second
|
||||||
|
|
||||||
// Fibaro contains connection information
|
// Fibaro contains connection information
|
||||||
type Fibaro struct {
|
type Fibaro struct {
|
||||||
URL string `toml:"url"`
|
URL string `toml:"url"`
|
||||||
|
Username string `toml:"username"`
|
||||||
// HTTP Basic Auth Credentials
|
Password string `toml:"password"`
|
||||||
Username string `toml:"username"`
|
Timeout config.Duration `toml:"timeout"`
|
||||||
Password string `toml:"password"`
|
DeviceType string `toml:"device_type"`
|
||||||
|
|
||||||
Timeout config.Duration `toml:"timeout"`
|
|
||||||
|
|
||||||
client *http.Client
|
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 {
|
|
||||||
BatteryLevel *string `json:"batteryLevel"`
|
|
||||||
Dead string `json:"dead"`
|
|
||||||
Energy *string `json:"energy"`
|
|
||||||
Power *string `json:"power"`
|
|
||||||
Value interface{} `json:"value"`
|
|
||||||
Value2 *string `json:"value2"`
|
|
||||||
} `json:"properties"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// getJSON connects, authenticates and reads JSON payload returned by Fibaro box
|
// getJSON connects, authenticates and reads JSON payload returned by Fibaro box
|
||||||
func (f *Fibaro) getJSON(path string, dataStruct interface{}) error {
|
func (f *Fibaro) getJSON(path string) ([]byte, error) {
|
||||||
var requestURL = f.URL + path
|
var requestURL = f.URL + path
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", requestURL, nil)
|
req, err := http.NewRequest("GET", requestURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req.SetBasicAuth(f.Username, f.Password)
|
req.SetBasicAuth(f.Username, f.Password)
|
||||||
resp, err := f.client.Do(req)
|
resp, err := f.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
|
@ -91,13 +54,24 @@ func (f *Fibaro) getJSON(path string, dataStruct interface{}) error {
|
||||||
http.StatusText(resp.StatusCode),
|
http.StatusText(resp.StatusCode),
|
||||||
http.StatusOK,
|
http.StatusOK,
|
||||||
http.StatusText(http.StatusOK))
|
http.StatusText(http.StatusOK))
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
dec := json.NewDecoder(resp.Body)
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
err = dec.Decode(&dataStruct)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, fmt.Errorf("unable to read response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bodyBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fibaro) Init() error {
|
||||||
|
switch f.DeviceType {
|
||||||
|
case "":
|
||||||
|
f.DeviceType = "HC2"
|
||||||
|
case "HC2", "HC3":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid option for device type")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -118,89 +92,24 @@ func (f *Fibaro) Gather(acc telegraf.Accumulator) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var tmpSections []Sections
|
sections, err := f.getJSON("/api/sections")
|
||||||
err := f.getJSON("/api/sections", &tmpSections)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
sections := map[uint16]string{}
|
rooms, err := f.getJSON("/api/rooms")
|
||||||
for _, v := range tmpSections {
|
|
||||||
sections[v.ID] = v.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
var tmpRooms []Rooms
|
|
||||||
err = f.getJSON("/api/rooms", &tmpRooms)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rooms := map[uint16]LinkRoomsSections{}
|
devices, err := f.getJSON("/api/devices")
|
||||||
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, device := range devices {
|
switch f.DeviceType {
|
||||||
// skip device in some cases
|
case "HC2":
|
||||||
if device.RoomID == 0 ||
|
return hc2.Parse(acc, sections, rooms, devices)
|
||||||
!device.Enabled ||
|
case "HC3":
|
||||||
device.Properties.Dead == "true" ||
|
return hc3.Parse(acc, sections, rooms, devices)
|
||||||
device.Type == "com.fibaro.zwaveDevice" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
tags := map[string]string{
|
|
||||||
"deviceId": strconv.FormatUint(uint64(device.ID), 10),
|
|
||||||
"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.BatteryLevel != nil {
|
|
||||||
if fValue, err := strconv.ParseFloat(*device.Properties.BatteryLevel, 64); err == nil {
|
|
||||||
fields["batteryLevel"] = fValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if device.Properties.Energy != nil {
|
|
||||||
if fValue, err := strconv.ParseFloat(*device.Properties.Energy, 64); err == nil {
|
|
||||||
fields["energy"] = fValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if device.Properties.Power != nil {
|
|
||||||
if fValue, err := strconv.ParseFloat(*device.Properties.Power, 64); err == nil {
|
|
||||||
fields["power"] = fValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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, 64); err == nil {
|
|
||||||
fields["value2"] = fValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
acc.AddFields("fibaro", fields, tags)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -4,131 +4,16 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/influxdata/telegraf"
|
||||||
"github.com/influxdata/telegraf/testutil"
|
"github.com/influxdata/telegraf/testutil"
|
||||||
"github.com/stretchr/testify/require"
|
"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": {
|
|
||||||
"batteryLevel": "100",
|
|
||||||
"dead": "false",
|
|
||||||
"value": "22.80"
|
|
||||||
},
|
|
||||||
"sortOrder": 4
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 5,
|
|
||||||
"name": "Device 5",
|
|
||||||
"roomID": 4,
|
|
||||||
"type": "com.fibaro.FGRM222",
|
|
||||||
"enabled": true,
|
|
||||||
"properties": {
|
|
||||||
"energy": "4.33",
|
|
||||||
"power": "0.7",
|
|
||||||
"dead": "false",
|
|
||||||
"value": "50",
|
|
||||||
"value2": "75"
|
|
||||||
},
|
|
||||||
"sortOrder": 5
|
|
||||||
}
|
|
||||||
]`
|
|
||||||
|
|
||||||
// TestUnauthorized validates that 401 (wrong credentials) is managed properly
|
// TestUnauthorized validates that 401 (wrong credentials) is managed properly
|
||||||
func TestUnauthorized(t *testing.T) {
|
func TestUnauthorized(t *testing.T) {
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -142,6 +27,7 @@ func TestUnauthorized(t *testing.T) {
|
||||||
Password: "pass",
|
Password: "pass",
|
||||||
client: &http.Client{},
|
client: &http.Client{},
|
||||||
}
|
}
|
||||||
|
require.NoError(t, a.Init())
|
||||||
|
|
||||||
var acc testutil.Accumulator
|
var acc testutil.Accumulator
|
||||||
err := acc.GatherError(a.Gather)
|
err := acc.GatherError(a.Gather)
|
||||||
|
|
@ -154,11 +40,17 @@ func TestJSONSuccess(t *testing.T) {
|
||||||
payload := ""
|
payload := ""
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
case "/api/sections":
|
case "/api/sections":
|
||||||
payload = sectionsJSON
|
content, err := os.ReadFile(path.Join("testdata", "sections.json"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
payload = string(content)
|
||||||
case "/api/rooms":
|
case "/api/rooms":
|
||||||
payload = roomsJSON
|
content, err := os.ReadFile(path.Join("testdata", "rooms.json"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
payload = string(content)
|
||||||
case "/api/devices":
|
case "/api/devices":
|
||||||
payload = devicesJSON
|
content, err := os.ReadFile(path.Join("testdata", "device_hc2.json"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
payload = string(content)
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
_, err := fmt.Fprintln(w, payload)
|
_, err := fmt.Fprintln(w, payload)
|
||||||
|
|
@ -172,36 +64,212 @@ func TestJSONSuccess(t *testing.T) {
|
||||||
Password: "pass",
|
Password: "pass",
|
||||||
client: &http.Client{},
|
client: &http.Client{},
|
||||||
}
|
}
|
||||||
|
require.NoError(t, a.Init())
|
||||||
|
|
||||||
var acc testutil.Accumulator
|
var acc testutil.Accumulator
|
||||||
err := acc.GatherError(a.Gather)
|
err := acc.GatherError(a.Gather)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Gather should add 5 metrics
|
|
||||||
require.Equal(t, uint64(5), acc.NMetrics())
|
require.Equal(t, uint64(5), acc.NMetrics())
|
||||||
|
|
||||||
// Ensure fields / values are correct - Device 1
|
expected := []telegraf.Metric{
|
||||||
tags := map[string]string{"deviceId": "1", "section": "Section 1", "room": "Room 1", "name": "Device 1", "type": "com.fibaro.binarySwitch"}
|
testutil.MustMetric(
|
||||||
fields := map[string]interface{}{"value": float64(0)}
|
"fibaro",
|
||||||
acc.AssertContainsTaggedFields(t, "fibaro", fields, tags)
|
map[string]string{
|
||||||
|
"deviceId": "1",
|
||||||
|
"section": "Section 1",
|
||||||
|
"room": "Room 1",
|
||||||
|
"name": "Device 1",
|
||||||
|
"type": "com.fibaro.binarySwitch",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"value": float64(0),
|
||||||
|
},
|
||||||
|
time.Unix(0, 0),
|
||||||
|
),
|
||||||
|
testutil.MustMetric(
|
||||||
|
"fibaro",
|
||||||
|
map[string]string{
|
||||||
|
"deviceId": "2",
|
||||||
|
"section": "Section 2",
|
||||||
|
"room": "Room 2",
|
||||||
|
"name": "Device 2",
|
||||||
|
"type": "com.fibaro.binarySwitch",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"value": float64(1),
|
||||||
|
},
|
||||||
|
time.Unix(0, 0),
|
||||||
|
),
|
||||||
|
testutil.MustMetric(
|
||||||
|
"fibaro",
|
||||||
|
map[string]string{
|
||||||
|
"deviceId": "3",
|
||||||
|
"section": "Section 3",
|
||||||
|
"room": "Room 3",
|
||||||
|
"name": "Device 3",
|
||||||
|
"type": "com.fibaro.multilevelSwitch",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"value": float64(67),
|
||||||
|
},
|
||||||
|
time.Unix(0, 0),
|
||||||
|
),
|
||||||
|
testutil.MustMetric(
|
||||||
|
"fibaro",
|
||||||
|
map[string]string{
|
||||||
|
"deviceId": "4",
|
||||||
|
"section": "Section 3",
|
||||||
|
"room": "Room 4",
|
||||||
|
"name": "Device 4",
|
||||||
|
"type": "com.fibaro.temperatureSensor",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"batteryLevel": float64(100),
|
||||||
|
"value": float64(22.8),
|
||||||
|
},
|
||||||
|
time.Unix(0, 0),
|
||||||
|
),
|
||||||
|
testutil.MustMetric(
|
||||||
|
"fibaro",
|
||||||
|
map[string]string{
|
||||||
|
"deviceId": "5",
|
||||||
|
"section": "Section 3",
|
||||||
|
"room": "Room 4",
|
||||||
|
"name": "Device 5",
|
||||||
|
"type": "com.fibaro.FGRM222",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"energy": float64(4.33),
|
||||||
|
"power": float64(0.7),
|
||||||
|
"value": float64(50),
|
||||||
|
"value2": float64(75),
|
||||||
|
},
|
||||||
|
time.Unix(0, 0),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure fields / values are correct - Device 2
|
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
|
||||||
tags = map[string]string{"deviceId": "2", "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)
|
func TestHC3JSON(t *testing.T) {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Ensure fields / values are correct - Device 3
|
payload := ""
|
||||||
tags = map[string]string{"deviceId": "3", "section": "Section 3", "room": "Room 3", "name": "Device 3", "type": "com.fibaro.multilevelSwitch"}
|
switch r.URL.Path {
|
||||||
fields = map[string]interface{}{"value": float64(67)}
|
case "/api/sections":
|
||||||
acc.AssertContainsTaggedFields(t, "fibaro", fields, tags)
|
content, err := os.ReadFile(path.Join("testdata", "sections.json"))
|
||||||
|
require.NoError(t, err)
|
||||||
// Ensure fields / values are correct - Device 4
|
payload = string(content)
|
||||||
tags = map[string]string{"deviceId": "4", "section": "Section 3", "room": "Room 4", "name": "Device 4", "type": "com.fibaro.temperatureSensor"}
|
case "/api/rooms":
|
||||||
fields = map[string]interface{}{"batteryLevel": float64(100), "value": float64(22.8)}
|
content, err := os.ReadFile(path.Join("testdata", "rooms.json"))
|
||||||
acc.AssertContainsTaggedFields(t, "fibaro", fields, tags)
|
require.NoError(t, err)
|
||||||
|
payload = string(content)
|
||||||
// Ensure fields / values are correct - Device 5
|
case "/api/devices":
|
||||||
tags = map[string]string{"deviceId": "5", "section": "Section 3", "room": "Room 4", "name": "Device 5", "type": "com.fibaro.FGRM222"}
|
content, err := os.ReadFile(path.Join("testdata", "device_hc3.json"))
|
||||||
fields = map[string]interface{}{"energy": float64(4.33), "power": float64(0.7), "value": float64(50), "value2": float64(75)}
|
require.NoError(t, err)
|
||||||
acc.AssertContainsTaggedFields(t, "fibaro", fields, tags)
|
payload = string(content)
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, err := fmt.Fprintln(w, payload)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
a := Fibaro{
|
||||||
|
URL: ts.URL,
|
||||||
|
Username: "user",
|
||||||
|
Password: "pass",
|
||||||
|
DeviceType: "HC3",
|
||||||
|
client: &http.Client{},
|
||||||
|
}
|
||||||
|
require.NoError(t, a.Init())
|
||||||
|
|
||||||
|
var acc testutil.Accumulator
|
||||||
|
err := acc.GatherError(a.Gather)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint64(5), acc.NMetrics())
|
||||||
|
|
||||||
|
expected := []telegraf.Metric{
|
||||||
|
testutil.MustMetric(
|
||||||
|
"fibaro",
|
||||||
|
map[string]string{
|
||||||
|
"deviceId": "1",
|
||||||
|
"section": "Section 1",
|
||||||
|
"room": "Room 1",
|
||||||
|
"name": "Device 1",
|
||||||
|
"type": "com.fibaro.binarySwitch",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"value": float64(0),
|
||||||
|
},
|
||||||
|
time.Unix(0, 0),
|
||||||
|
),
|
||||||
|
testutil.MustMetric(
|
||||||
|
"fibaro",
|
||||||
|
map[string]string{
|
||||||
|
"deviceId": "2",
|
||||||
|
"section": "Section 2",
|
||||||
|
"room": "Room 2",
|
||||||
|
"name": "Device 2",
|
||||||
|
"type": "com.fibaro.binarySwitch",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"value": float64(1),
|
||||||
|
},
|
||||||
|
time.Unix(0, 0),
|
||||||
|
),
|
||||||
|
testutil.MustMetric(
|
||||||
|
"fibaro",
|
||||||
|
map[string]string{
|
||||||
|
"deviceId": "3",
|
||||||
|
"section": "Section 3",
|
||||||
|
"room": "Room 3",
|
||||||
|
"name": "Device 3",
|
||||||
|
"type": "com.fibaro.multilevelSwitch",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"value": float64(67),
|
||||||
|
},
|
||||||
|
time.Unix(0, 0),
|
||||||
|
),
|
||||||
|
testutil.MustMetric(
|
||||||
|
"fibaro",
|
||||||
|
map[string]string{
|
||||||
|
"deviceId": "4",
|
||||||
|
"section": "Section 3",
|
||||||
|
"room": "Room 4",
|
||||||
|
"name": "Device 4",
|
||||||
|
"type": "com.fibaro.temperatureSensor",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"batteryLevel": float64(100),
|
||||||
|
"value": float64(22.8),
|
||||||
|
},
|
||||||
|
time.Unix(0, 0),
|
||||||
|
),
|
||||||
|
testutil.MustMetric(
|
||||||
|
"fibaro",
|
||||||
|
map[string]string{
|
||||||
|
"deviceId": "5",
|
||||||
|
"section": "Section 3",
|
||||||
|
"room": "Room 4",
|
||||||
|
"name": "Device 5",
|
||||||
|
"type": "com.fibaro.FGRM222",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"energy": float64(4.33),
|
||||||
|
"power": float64(0.7),
|
||||||
|
"value": float64(34),
|
||||||
|
},
|
||||||
|
time.Unix(0, 0),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidDeviceType(t *testing.T) {
|
||||||
|
a := Fibaro{
|
||||||
|
DeviceType: "foobar",
|
||||||
|
}
|
||||||
|
require.Error(t, a.Init())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
package hc2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/influxdata/telegraf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Parse(acc telegraf.Accumulator, sectionBytes []byte, roomBytes []byte, deviecsBytes []byte) error {
|
||||||
|
var tmpSections []Sections
|
||||||
|
if err := json.Unmarshal(sectionBytes, &tmpSections); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sections := map[uint16]string{}
|
||||||
|
for _, v := range tmpSections {
|
||||||
|
sections[v.ID] = v.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
var tmpRooms []Rooms
|
||||||
|
if err := json.Unmarshal(roomBytes, &tmpRooms); 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
|
||||||
|
if err := json.Unmarshal(deviecsBytes, &devices); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, device := range devices {
|
||||||
|
// skip device in some cases
|
||||||
|
if device.RoomID == 0 ||
|
||||||
|
!device.Enabled ||
|
||||||
|
device.Properties.Dead == "true" ||
|
||||||
|
device.Type == "com.fibaro.zwaveDevice" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tags := map[string]string{
|
||||||
|
"deviceId": strconv.FormatUint(uint64(device.ID), 10),
|
||||||
|
"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.BatteryLevel != nil {
|
||||||
|
if fValue, err := strconv.ParseFloat(*device.Properties.BatteryLevel, 64); err == nil {
|
||||||
|
fields["batteryLevel"] = fValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if device.Properties.Energy != nil {
|
||||||
|
if fValue, err := strconv.ParseFloat(*device.Properties.Energy, 64); err == nil {
|
||||||
|
fields["energy"] = fValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if device.Properties.Power != nil {
|
||||||
|
if fValue, err := strconv.ParseFloat(*device.Properties.Power, 64); err == nil {
|
||||||
|
fields["power"] = fValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 64); err == nil {
|
||||||
|
fields["value2"] = fValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
acc.AddFields("fibaro", fields, tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package hc2
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
BatteryLevel *string `json:"batteryLevel"`
|
||||||
|
Dead string `json:"dead"`
|
||||||
|
Energy *string `json:"energy"`
|
||||||
|
Power *string `json:"power"`
|
||||||
|
Value interface{} `json:"value"`
|
||||||
|
Value2 *string `json:"value2"`
|
||||||
|
} `json:"properties"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
package hc3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/influxdata/telegraf"
|
||||||
|
"github.com/influxdata/telegraf/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Parse(acc telegraf.Accumulator, sectionBytes []byte, roomBytes []byte, deviecsBytes []byte) error {
|
||||||
|
var tmpSections []Sections
|
||||||
|
if err := json.Unmarshal(sectionBytes, &tmpSections); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sections := make(map[uint16]string, len(tmpSections))
|
||||||
|
for _, v := range tmpSections {
|
||||||
|
sections[v.ID] = v.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
var tmpRooms []Rooms
|
||||||
|
if err := json.Unmarshal(roomBytes, &tmpRooms); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rooms := make(map[uint16]linkRoomsSections, len(tmpRooms))
|
||||||
|
for _, v := range tmpRooms {
|
||||||
|
rooms[v.ID] = linkRoomsSections{Name: v.Name, SectionID: v.SectionID}
|
||||||
|
}
|
||||||
|
|
||||||
|
var devices []Devices
|
||||||
|
if err := json.Unmarshal(deviecsBytes, &devices); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, device := range devices {
|
||||||
|
// skip device in some cases
|
||||||
|
if device.RoomID == 0 ||
|
||||||
|
!device.Enabled ||
|
||||||
|
device.Properties.Dead ||
|
||||||
|
device.Type == "com.fibaro.zwaveDevice" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tags := map[string]string{
|
||||||
|
"deviceId": strconv.FormatUint(uint64(device.ID), 10),
|
||||||
|
"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.BatteryLevel != nil {
|
||||||
|
fields["batteryLevel"] = *device.Properties.BatteryLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
if device.Properties.Energy != nil {
|
||||||
|
fields["energy"] = *device.Properties.Energy
|
||||||
|
}
|
||||||
|
|
||||||
|
if device.Properties.Power != nil {
|
||||||
|
fields["power"] = *device.Properties.Power
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value can be a JSON bool, string, or numeric value
|
||||||
|
if device.Properties.Value != nil {
|
||||||
|
v, err := internal.ToFloat64(device.Properties.Value)
|
||||||
|
if err != nil {
|
||||||
|
acc.AddError(fmt.Errorf("unable to convert value: %w", err))
|
||||||
|
} else {
|
||||||
|
fields["value"] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
acc.AddFields("fibaro", fields, tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package hc3
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
BatteryLevel *float64 `json:"batteryLevel"`
|
||||||
|
Dead bool `json:"dead"`
|
||||||
|
Energy *float64 `json:"energy"`
|
||||||
|
Power *float64 `json:"power"`
|
||||||
|
Value interface{} `json:"value"`
|
||||||
|
Value2 *string `json:"value2"`
|
||||||
|
} `json:"properties"`
|
||||||
|
}
|
||||||
|
|
@ -10,3 +10,8 @@
|
||||||
|
|
||||||
## Amount of time allowed to complete the HTTP request
|
## Amount of time allowed to complete the HTTP request
|
||||||
# timeout = "5s"
|
# timeout = "5s"
|
||||||
|
|
||||||
|
## Fibaro Device Type
|
||||||
|
## By default, this plugin will attempt to read using the HC2 API. For HC3
|
||||||
|
## devices, set this to "HC3"
|
||||||
|
# device_type = "HC2"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"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": {
|
||||||
|
"batteryLevel": "100",
|
||||||
|
"dead": "false",
|
||||||
|
"value": "22.80"
|
||||||
|
},
|
||||||
|
"sortOrder": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"name": "Device 5",
|
||||||
|
"roomID": 4,
|
||||||
|
"type": "com.fibaro.FGRM222",
|
||||||
|
"enabled": true,
|
||||||
|
"properties": {
|
||||||
|
"energy": "4.33",
|
||||||
|
"power": "0.7",
|
||||||
|
"dead": "false",
|
||||||
|
"value": "50",
|
||||||
|
"value2": "75"
|
||||||
|
},
|
||||||
|
"sortOrder": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"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": {
|
||||||
|
"batteryLevel": 100,
|
||||||
|
"dead": false,
|
||||||
|
"value": 22.80
|
||||||
|
},
|
||||||
|
"sortOrder": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"name": "Device 5",
|
||||||
|
"roomID": 4,
|
||||||
|
"type": "com.fibaro.FGRM222",
|
||||||
|
"enabled": true,
|
||||||
|
"properties": {
|
||||||
|
"energy": 4.33,
|
||||||
|
"power": 0.7,
|
||||||
|
"dead": false,
|
||||||
|
"value": 34
|
||||||
|
},
|
||||||
|
"sortOrder": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Section 1",
|
||||||
|
"sortOrder": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Section 2",
|
||||||
|
"sortOrder": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"name": "Section 3",
|
||||||
|
"sortOrder": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
Loading…
Reference in New Issue