From ca2295e1a4a8fa79edfe8130d034a67b254d3af3 Mon Sep 17 00:00:00 2001 From: Joshua Powers Date: Mon, 28 Aug 2023 14:47:08 -0600 Subject: [PATCH] feat(inputs.fibaro): Support HC3 device types (#13754) --- plugins/inputs/fibaro/README.md | 8 + plugins/inputs/fibaro/fibaro.go | 159 ++------ plugins/inputs/fibaro/fibaro_test.go | 362 +++++++++++------- plugins/inputs/fibaro/hc2/parser.go | 95 +++++ plugins/inputs/fibaro/hc2/types.go | 37 ++ plugins/inputs/fibaro/hc3/parser.go | 80 ++++ plugins/inputs/fibaro/hc3/types.go | 37 ++ plugins/inputs/fibaro/sample.conf | 5 + .../inputs/fibaro/testdata/device_hc2.json | 66 ++++ .../inputs/fibaro/testdata/device_hc3.json | 65 ++++ plugins/inputs/fibaro/testdata/rooms.json | 30 ++ plugins/inputs/fibaro/testdata/sections.json | 17 + 12 files changed, 689 insertions(+), 272 deletions(-) create mode 100644 plugins/inputs/fibaro/hc2/parser.go create mode 100644 plugins/inputs/fibaro/hc2/types.go create mode 100644 plugins/inputs/fibaro/hc3/parser.go create mode 100644 plugins/inputs/fibaro/hc3/types.go create mode 100644 plugins/inputs/fibaro/testdata/device_hc2.json create mode 100644 plugins/inputs/fibaro/testdata/device_hc3.json create mode 100644 plugins/inputs/fibaro/testdata/rooms.json create mode 100644 plugins/inputs/fibaro/testdata/sections.json diff --git a/plugins/inputs/fibaro/README.md b/plugins/inputs/fibaro/README.md index e9d33ad4a..1fc503938 100644 --- a/plugins/inputs/fibaro/README.md +++ b/plugins/inputs/fibaro/README.md @@ -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, 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 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 # 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 diff --git a/plugins/inputs/fibaro/fibaro.go b/plugins/inputs/fibaro/fibaro.go index ea85cccc4..db0dd3798 100644 --- a/plugins/inputs/fibaro/fibaro.go +++ b/plugins/inputs/fibaro/fibaro.go @@ -3,15 +3,16 @@ package fibaro import ( _ "embed" - "encoding/json" "fmt" + "io" "net/http" - "strconv" "time" "github.com/influxdata/telegraf" "github.com/influxdata/telegraf/config" "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 @@ -21,66 +22,28 @@ const defaultTimeout = 5 * time.Second // Fibaro contains connection information type Fibaro struct { - URL string `toml:"url"` - - // HTTP Basic Auth Credentials - Username string `toml:"username"` - Password string `toml:"password"` - - Timeout config.Duration `toml:"timeout"` + URL string `toml:"url"` + Username string `toml:"username"` + Password string `toml:"password"` + Timeout config.Duration `toml:"timeout"` + DeviceType string `toml:"device_type"` 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 -func (f *Fibaro) getJSON(path string, dataStruct interface{}) error { +func (f *Fibaro) getJSON(path string) ([]byte, error) { var requestURL = f.URL + path req, err := http.NewRequest("GET", requestURL, nil) if err != nil { - return err + return nil, err } req.SetBasicAuth(f.Username, f.Password) resp, err := f.client.Do(req) if err != nil { - return err + return nil, err } defer resp.Body.Close() @@ -91,13 +54,24 @@ func (f *Fibaro) getJSON(path string, dataStruct interface{}) error { http.StatusText(resp.StatusCode), http.StatusOK, http.StatusText(http.StatusOK)) - return err + return nil, err } - dec := json.NewDecoder(resp.Body) - err = dec.Decode(&dataStruct) + bodyBytes, err := io.ReadAll(resp.Body) 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 @@ -118,89 +92,24 @@ func (f *Fibaro) Gather(acc telegraf.Accumulator) error { } } - var tmpSections []Sections - err := f.getJSON("/api/sections", &tmpSections) + sections, err := f.getJSON("/api/sections") 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) + rooms, err := f.getJSON("/api/rooms") 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) + devices, err := f.getJSON("/api/devices") if 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) + switch f.DeviceType { + case "HC2": + return hc2.Parse(acc, sections, rooms, devices) + case "HC3": + return hc3.Parse(acc, sections, rooms, devices) } return nil diff --git a/plugins/inputs/fibaro/fibaro_test.go b/plugins/inputs/fibaro/fibaro_test.go index dac8bc6fd..9c65b9d49 100644 --- a/plugins/inputs/fibaro/fibaro_test.go +++ b/plugins/inputs/fibaro/fibaro_test.go @@ -4,131 +4,16 @@ import ( "fmt" "net/http" "net/http/httptest" + "os" + "path" "testing" + "time" + "github.com/influxdata/telegraf" "github.com/influxdata/telegraf/testutil" "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 func TestUnauthorized(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -142,6 +27,7 @@ func TestUnauthorized(t *testing.T) { Password: "pass", client: &http.Client{}, } + require.NoError(t, a.Init()) var acc testutil.Accumulator err := acc.GatherError(a.Gather) @@ -154,11 +40,17 @@ func TestJSONSuccess(t *testing.T) { payload := "" switch r.URL.Path { case "/api/sections": - payload = sectionsJSON + content, err := os.ReadFile(path.Join("testdata", "sections.json")) + require.NoError(t, err) + payload = string(content) case "/api/rooms": - payload = roomsJSON + content, err := os.ReadFile(path.Join("testdata", "rooms.json")) + require.NoError(t, err) + payload = string(content) 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) _, err := fmt.Fprintln(w, payload) @@ -172,36 +64,212 @@ func TestJSONSuccess(t *testing.T) { Password: "pass", client: &http.Client{}, } + require.NoError(t, a.Init()) var acc testutil.Accumulator err := acc.GatherError(a.Gather) require.NoError(t, err) - - // Gather should add 5 metrics require.Equal(t, uint64(5), acc.NMetrics()) - // Ensure fields / values are correct - Device 1 - tags := map[string]string{"deviceId": "1", "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) + 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(50), + "value2": float64(75), + }, + time.Unix(0, 0), + ), + } - // Ensure fields / values are correct - Device 2 - 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) - - // Ensure fields / values are correct - Device 3 - tags = map[string]string{"deviceId": "3", "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{"deviceId": "4", "section": "Section 3", "room": "Room 4", "name": "Device 4", "type": "com.fibaro.temperatureSensor"} - fields = map[string]interface{}{"batteryLevel": float64(100), "value": float64(22.8)} - acc.AssertContainsTaggedFields(t, "fibaro", fields, tags) - - // Ensure fields / values are correct - Device 5 - tags = map[string]string{"deviceId": "5", "section": "Section 3", "room": "Room 4", "name": "Device 5", "type": "com.fibaro.FGRM222"} - fields = map[string]interface{}{"energy": float64(4.33), "power": float64(0.7), "value": float64(50), "value2": float64(75)} - acc.AssertContainsTaggedFields(t, "fibaro", fields, tags) + testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime()) +} + +func TestHC3JSON(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + payload := "" + switch r.URL.Path { + case "/api/sections": + content, err := os.ReadFile(path.Join("testdata", "sections.json")) + require.NoError(t, err) + payload = string(content) + case "/api/rooms": + content, err := os.ReadFile(path.Join("testdata", "rooms.json")) + require.NoError(t, err) + payload = string(content) + case "/api/devices": + content, err := os.ReadFile(path.Join("testdata", "device_hc3.json")) + require.NoError(t, err) + 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()) } diff --git a/plugins/inputs/fibaro/hc2/parser.go b/plugins/inputs/fibaro/hc2/parser.go new file mode 100644 index 000000000..c409cd2f3 --- /dev/null +++ b/plugins/inputs/fibaro/hc2/parser.go @@ -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 +} diff --git a/plugins/inputs/fibaro/hc2/types.go b/plugins/inputs/fibaro/hc2/types.go new file mode 100644 index 000000000..4c3f2966a --- /dev/null +++ b/plugins/inputs/fibaro/hc2/types.go @@ -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"` +} diff --git a/plugins/inputs/fibaro/hc3/parser.go b/plugins/inputs/fibaro/hc3/parser.go new file mode 100644 index 000000000..6e1409f77 --- /dev/null +++ b/plugins/inputs/fibaro/hc3/parser.go @@ -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 +} diff --git a/plugins/inputs/fibaro/hc3/types.go b/plugins/inputs/fibaro/hc3/types.go new file mode 100644 index 000000000..01d510014 --- /dev/null +++ b/plugins/inputs/fibaro/hc3/types.go @@ -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"` +} diff --git a/plugins/inputs/fibaro/sample.conf b/plugins/inputs/fibaro/sample.conf index cc38d783f..9f3921a04 100644 --- a/plugins/inputs/fibaro/sample.conf +++ b/plugins/inputs/fibaro/sample.conf @@ -10,3 +10,8 @@ ## Amount of time allowed to complete the HTTP request # 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" diff --git a/plugins/inputs/fibaro/testdata/device_hc2.json b/plugins/inputs/fibaro/testdata/device_hc2.json new file mode 100644 index 000000000..9ba56ab54 --- /dev/null +++ b/plugins/inputs/fibaro/testdata/device_hc2.json @@ -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 + } +] diff --git a/plugins/inputs/fibaro/testdata/device_hc3.json b/plugins/inputs/fibaro/testdata/device_hc3.json new file mode 100644 index 000000000..3bee33b7a --- /dev/null +++ b/plugins/inputs/fibaro/testdata/device_hc3.json @@ -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 + } +] diff --git a/plugins/inputs/fibaro/testdata/rooms.json b/plugins/inputs/fibaro/testdata/rooms.json new file mode 100644 index 000000000..a131f4ef7 --- /dev/null +++ b/plugins/inputs/fibaro/testdata/rooms.json @@ -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 + } +] diff --git a/plugins/inputs/fibaro/testdata/sections.json b/plugins/inputs/fibaro/testdata/sections.json new file mode 100644 index 000000000..5a638a9b4 --- /dev/null +++ b/plugins/inputs/fibaro/testdata/sections.json @@ -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 + } +]