1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View file

@ -0,0 +1,183 @@
# HueBridge Input Plugin
This plugin gathers status from [Hue Bridge][hue] devices using the
[CLIP API][hue_api] interface of the devices.
⭐ Telegraf v1.34.0
🏷️ iot
💻 all
[hue]: https://www.philips-hue.com/
[hue_api]: https://developers.meethue.com/develop/hue-api-v2/
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
In addition to the plugin-specific configuration settings, plugins support
additional global and plugin configuration settings. These settings are used to
modify metrics, tags, and field or create aliases and configure ordering, etc.
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
## Configuration
```toml @sample.conf
# Gather smart home status from Hue Bridge
[[inputs.huebridge]]
## URL of bridges to query in the form <scheme>://<bridge id>:<user name>@<address>/
## See documentation for available schemes.
bridges = [ "address://<bridge id>:<user name>@<bridge hostname or address>/" ]
## Manual device to room assignments to apply during status evaluation.
## E.g. for motion sensors which are reported without a room assignment.
# room_assignments = { "Motion sensor 1" = "Living room", "Motion sensor 2" = "Corridor" }
## Timeout for gathering information
# timeout = "10s"
## Optional TLS Config
# tls_ca = "/etc/telegraf/ca.pem"
# tls_cert = "/etc/telegraf/cert.pem"
# tls_key = "/etc/telegraf/key.pem"
# tls_key_pwd = "secret"
## Use TLS but skip chain & host verification
# insecure_skip_verify = false
```
### Extended bridge access options
The Hue bridges to query can be defined by URLs of the following form:
```text
<locator scheme>://<bridge id>:<user name>@<locator dependent address>/
```
where the `bridge id` is the unique bridge id as returned in
```bash
curl -k https://<address>/api/config/0
```
and the `user name` is the secret user name returned during application
authentication.
To create a new user name issue the following command
after pressing the bridge's link button:
```bash
curl -k -X POST http://<bridge address>/api \
-H 'Content-Type: application/json' \
-d '{"devicetype":"huebridge-telegraf-plugin"}'
```
The `scheme` can have one of the following values and will also determine the
structure of the `address` part.
#### `address` scheme
Addresses a local bridge with `address` being the DNS name or IP address of the
bridge, e.g.
```text
address://0123456789ABCDEF:sFlEGnMAFXO6RtZV17aViNUB95G2uXWw64texDzD@mybridge/
```
#### `cloud` scheme
With this scheme the plugin discovers a bridge via its cloud registration.
The `address` part defines the discovery endpoint to use.
If not specified otherwise,
the [standard discovery endpoint][discovery_url] is used, e.g.
```text
cloud://0123456789ABCDEF:sFlEGnMAFXO6RtZV17aViNUB95G2uXWw64texDzD@/
```
[discovery_url]: https://discovery.meethue.com/
#### `mdns` scheme
This scheme uses mDNS to discover the bridge. Leave the `address` part unset
for this scheme like
```text
mdns://0123456789ABCDEF:sFlEGnMAFXO6RtZV17aViNUB95G2uXWw64texDzD@/
```
#### `remote` scheme
This scheme accesses the bridge via the Cloud Remote API. The `address` part
defines the cloud API endpoint defaulting to the
[standard API endpoint][cloud_api_endpoint].
```text
remote://0123456789ABCDEF:sFlEGnMAFXO6RtZV17aViNUB95G2uXWw64texDzD@/
```
In order to use this method a Hue Developer Account is required, a Remote App
must be registered and the corresponding Authorization flow must be completed.
See the [Cloud2Cloud Getting Started documentation][cloud_getting_started]
for full details.
Additionally, the `remote_client_id`, `remote_client_secret`, and
`remote_callback_url` parameters must be set in the plugin configuration
exactly as used during the App registration.
Furthermore the `remote_token_dir` parameter must point to the directory
containing the persisted token.
[cloud_api_endpoint]: https://api.meethue.com
[cloud_getting_started]: https://developers.meethue.com/develop/hue-api-v2/cloud2cloud-getting-started/
## Metrics
- `huebridge_light`
- tags
- `bridge_id` - The bridge id (this metrics has been queried from)
- `room` - The name of the room
- `device` - The name of the device
- fields
- `on` (int) - 0: light is off 1: light is on
- `huebridge_temperature`
- tags
- `bridge_id` - The bridge id (this metrics has been queried from)
- `room` - The name of the room
- `device` - The name of the device
- `enabled` - The current status of sensor (active: true|false)
- fields
- `temperature` (float) - The current temperatue (in °Celsius)
- `huebridge_light_level`
- tags
- `bridge_id` - The bridge id (this metrics has been queried from)
- `room` - The name of the room
- `device` - The name of the device
- `enabled` - The current status of sensor (active: true|false)
- fields
- `light_level` (int) - The current light level (in human friendly scale 10.000*log10(lux)+1)
- `light_level_lux` (float) - The current light level (in lux)
- `huebridge_motion_sensor`
- tags
- `bridge_id` - The bridge id (this metrics has been queried from)
- `room` - The name of the room
- `device` - The name of the device
- `enabled` - The current status of sensor (active: true|false)
- fields
- `motion` (int) - 0: no motion detected 1: motion detected
- `huebridge_device_power`
- tags
- `bridge_id` - The bridge id (this metrics has been queried from)
- `room` - The name of the room
- `device` - The name of the device
- fields
- `battery_level` (int) - Power source status (normal, low, critical)
- `battery_state` (string) - Battery charge level (in %)
## Example Output
```text
huebridge_light,huebridge_bridge_id=0123456789ABCDEF,huebridge_room=Name#15,huebridge_device=Name#3 on=0 1734880329
huebridge_temperature,huebridge_room=Name#15,huebridge_device=Name#7,huebridge_device_enabled=true,huebridge_bridge_id=0123456789ABCDEF temperature=17.63 1734880329
huebridge_light_level,huebridge_bridge_id=0123456789ABCDEF,huebridge_room=Name#15,huebridge_device=Name#7,huebridge_device_enabled=true light_level=18948,light_level_lux=78.46934003526889 1734880329
huebridge_motion_sensor,huebridge_bridge_id=0123456789ABCDEF,huebridge_room=Name#15,huebridge_device=Name#7,huebridge_device_enabled=true motion=0 1734880329
huebridge_device_power,huebridge_bridge_id=0123456789ABCDEF,huebridge_room=Name#15,huebridge_device=Name#7 battery_level=100,battery_state=normal 1734880329
```

View file

@ -0,0 +1,424 @@
package huebridge
import (
"crypto/tls"
"fmt"
"maps"
"math"
"net/http"
"net/url"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/tdrn-org/go-hue"
"github.com/influxdata/telegraf"
)
type bridge struct {
url *url.URL
configRoomAssignments map[string]string
remoteCfg *remoteClientConfig
tlsCfg *tls.Config
timeout time.Duration
log telegraf.Logger
resolvedClient hue.BridgeClient
resourceTree map[string]string
deviceNames map[string]string
roomAssignments map[string]string
}
func (b *bridge) String() string {
return b.url.Redacted()
}
func (b *bridge) process(acc telegraf.Accumulator) error {
if b.resolvedClient == nil {
if err := b.resolve(); err != nil {
return err
}
}
b.log.Tracef("Processing bridge %s", b)
if err := b.fetchMetadata(); err != nil {
// Discard previously resolved client and re-resolve on next process call
b.resolvedClient = nil
return err
}
acc.AddError(b.processLights(acc))
acc.AddError(b.processTemperatures(acc))
acc.AddError(b.processLightLevels(acc))
acc.AddError(b.processMotionSensors(acc))
acc.AddError(b.processDevicePowers(acc))
return nil
}
func (b *bridge) processLights(acc telegraf.Accumulator) error {
getLightsResponse, err := b.resolvedClient.GetLights()
if err != nil {
return fmt.Errorf("failed to access bridge lights on %s: %w", b, err)
}
if getLightsResponse.HTTPResponse.StatusCode != http.StatusOK {
return fmt.Errorf("failed to fetch bridge lights from %s: %s", b, getLightsResponse.HTTPResponse.Status)
}
responseData := getLightsResponse.JSON200.Data
if responseData != nil {
for _, light := range *responseData {
tags := make(map[string]string)
tags["bridge_id"] = b.resolvedClient.Bridge().BridgeId
tags["room"] = b.resolveResourceRoom(*light.Id, *light.Metadata.Name)
tags["device"] = *light.Metadata.Name
fields := make(map[string]interface{})
if *light.On.On {
fields["on"] = 1
} else {
fields["on"] = 0
}
acc.AddGauge("huebridge_light", fields, tags)
}
}
return nil
}
func (b *bridge) processTemperatures(acc telegraf.Accumulator) error {
getTemperaturesResponse, err := b.resolvedClient.GetTemperatures()
if err != nil {
return fmt.Errorf("failed to access bridge temperatures on %s: %w", b, err)
}
if getTemperaturesResponse.HTTPResponse.StatusCode != http.StatusOK {
return fmt.Errorf("failed to fetch bridge temperatures from %s: %s", b, getTemperaturesResponse.HTTPResponse.Status)
}
responseData := getTemperaturesResponse.JSON200.Data
if responseData != nil {
for _, temperature := range *responseData {
temperatureName := b.resolveDeviceName(*temperature.Id)
tags := make(map[string]string)
tags["bridge_id"] = b.resolvedClient.Bridge().BridgeId
tags["room"] = b.resolveResourceRoom(*temperature.Id, temperatureName)
tags["device"] = temperatureName
tags["enabled"] = strconv.FormatBool(*temperature.Enabled)
fields := make(map[string]interface{})
fields["temperature"] = *temperature.Temperature.TemperatureReport.Temperature
acc.AddGauge("huebridge_temperature", fields, tags)
}
}
return nil
}
func (b *bridge) processLightLevels(acc telegraf.Accumulator) error {
getLightLevelsResponse, err := b.resolvedClient.GetLightLevels()
if err != nil {
return fmt.Errorf("failed to access bridge lights levels on %s: %w", b, err)
}
if getLightLevelsResponse.HTTPResponse.StatusCode != http.StatusOK {
return fmt.Errorf("failed to fetch bridge light levels from %s: %s", b, getLightLevelsResponse.HTTPResponse.Status)
}
responseData := getLightLevelsResponse.JSON200.Data
if responseData != nil {
for _, lightLevel := range *responseData {
lightLevelName := b.resolveDeviceName(*lightLevel.Id)
tags := make(map[string]string)
tags["bridge_id"] = b.resolvedClient.Bridge().BridgeId
tags["room"] = b.resolveResourceRoom(*lightLevel.Id, lightLevelName)
tags["device"] = lightLevelName
tags["enabled"] = strconv.FormatBool(*lightLevel.Enabled)
fields := make(map[string]interface{})
fields["light_level"] = *lightLevel.Light.LightLevelReport.LightLevel
fields["light_level_lux"] = math.Pow(10.0, (float64(*lightLevel.Light.LightLevelReport.LightLevel)-1.0)/10000.0)
acc.AddGauge("huebridge_light_level", fields, tags)
}
}
return nil
}
func (b *bridge) processMotionSensors(acc telegraf.Accumulator) error {
getMotionSensorsResponse, err := b.resolvedClient.GetMotionSensors()
if err != nil {
return fmt.Errorf("failed to access bridge motion sensors on %s: %w", b, err)
}
if getMotionSensorsResponse.HTTPResponse.StatusCode != http.StatusOK {
return fmt.Errorf("failed to fetch bridge motion sensors from %s: %s", b, getMotionSensorsResponse.HTTPResponse.Status)
}
responseData := getMotionSensorsResponse.JSON200.Data
if responseData != nil {
for _, motionSensor := range *responseData {
motionSensorName := b.resolveDeviceName(*motionSensor.Id)
tags := make(map[string]string)
tags["bridge_id"] = b.resolvedClient.Bridge().BridgeId
tags["room"] = b.resolveResourceRoom(*motionSensor.Id, motionSensorName)
tags["device"] = motionSensorName
tags["enabled"] = strconv.FormatBool(*motionSensor.Enabled)
fields := make(map[string]interface{})
if *motionSensor.Motion.MotionReport.Motion {
fields["motion"] = 1
} else {
fields["motion"] = 0
}
acc.AddGauge("huebridge_motion_sensor", fields, tags)
}
}
return nil
}
func (b *bridge) processDevicePowers(acc telegraf.Accumulator) error {
getDevicePowersResponse, err := b.resolvedClient.GetDevicePowers()
if err != nil {
return fmt.Errorf("failed to access bridge device powers on %s: %w", b, err)
}
if getDevicePowersResponse.HTTPResponse.StatusCode != http.StatusOK {
return fmt.Errorf("failed to fetch bridge device powers from %s: %s", b, getDevicePowersResponse.HTTPResponse.Status)
}
responseData := getDevicePowersResponse.JSON200.Data
if responseData != nil {
for _, devicePower := range *responseData {
if devicePower.PowerState.BatteryLevel == nil && devicePower.PowerState.BatteryState == nil {
continue
}
devicePowerName := b.resolveDeviceName(*devicePower.Id)
tags := make(map[string]string)
tags["bridge_id"] = b.resolvedClient.Bridge().BridgeId
tags["room"] = b.resolveResourceRoom(*devicePower.Id, devicePowerName)
tags["device"] = devicePowerName
fields := make(map[string]interface{})
fields["battery_level"] = *devicePower.PowerState.BatteryLevel
fields["battery_state"] = *devicePower.PowerState.BatteryState
acc.AddGauge("huebridge_device_power", fields, tags)
}
}
return nil
}
func (b *bridge) resolve() error {
if b.resolvedClient != nil {
return nil
}
switch b.url.Scheme {
case "address":
return b.resolveViaAddress()
case "cloud":
return b.resolveViaCloud()
case "mdns":
return b.resolveViaMDNS()
case "remote":
return b.resolveViaRemote()
}
return fmt.Errorf("unrecognized bridge URL %s", b)
}
func (b *bridge) resolveViaAddress() error {
locator, err := hue.NewAddressBridgeLocator(b.url.Host)
if err != nil {
return err
}
return b.resolveLocalBridge(locator)
}
func (b *bridge) resolveViaCloud() error {
locator := hue.NewCloudBridgeLocator()
if b.url.Host != "" {
u, err := url.Parse(fmt.Sprintf("https://%s/", b.url.Host))
if err != nil {
return err
}
locator.DiscoveryEndpointUrl = u.JoinPath(b.url.Path)
}
locator.TlsConfig = b.tlsCfg
return b.resolveLocalBridge(locator)
}
func (b *bridge) resolveViaMDNS() error {
locator := hue.NewMDNSBridgeLocator()
return b.resolveLocalBridge(locator)
}
func (b *bridge) resolveLocalBridge(locator hue.BridgeLocator) error {
hueBridge, err := locator.Lookup(b.url.User.Username(), b.timeout)
if err != nil {
return err
}
urlPassword, _ := b.url.User.Password()
bridgeClient, err := hueBridge.NewClient(hue.NewLocalBridgeAuthenticator(urlPassword), b.timeout)
if err != nil {
return err
}
b.resolvedClient = bridgeClient
return nil
}
func (b *bridge) resolveViaRemote() error {
var redirectURL *url.URL
if b.remoteCfg.RemoteCallbackURL != "" {
u, err := url.Parse(b.remoteCfg.RemoteCallbackURL)
if err != nil {
return err
}
redirectURL = u
}
tokenFile := filepath.Join(
b.remoteCfg.RemoteTokenDir,
b.remoteCfg.RemoteClientID,
strings.ToUpper(b.url.User.Username())+".json",
)
locator, err := hue.NewRemoteBridgeLocator(
b.remoteCfg.RemoteClientID,
b.remoteCfg.RemoteClientSecret,
redirectURL,
tokenFile,
)
if err != nil {
return err
}
if b.url.Host != "" {
u, err := url.Parse(fmt.Sprintf("https://%s/", b.url.Host))
if err != nil {
return err
}
locator.EndpointUrl = u.JoinPath(b.url.Path)
}
locator.TlsConfig = b.tlsCfg
return b.resolveRemoteBridge(locator)
}
func (b *bridge) resolveRemoteBridge(locator *hue.RemoteBridgeLocator) error {
hueBridge, err := locator.Lookup(b.url.User.Username(), b.timeout)
if err != nil {
return err
}
urlPassword, _ := b.url.User.Password()
bridgeClient, err := hueBridge.NewClient(hue.NewRemoteBridgeAuthenticator(locator, urlPassword), b.timeout)
if err != nil {
return err
}
b.resolvedClient = bridgeClient
return nil
}
func (b *bridge) fetchMetadata() error {
err := b.fetchResourceTree()
if err != nil {
return err
}
err = b.fetchDeviceNames()
if err != nil {
return err
}
return b.fetchRoomAssignments()
}
func (b *bridge) fetchResourceTree() error {
getResourcesResponse, err := b.resolvedClient.GetResources()
if err != nil {
return fmt.Errorf("failed to access bridge resources on %s: %w", b, err)
}
if getResourcesResponse.HTTPResponse.StatusCode != http.StatusOK {
return fmt.Errorf("failed to fetch bridge resources from %s: %s", b, getResourcesResponse.HTTPResponse.Status)
}
responseData := getResourcesResponse.JSON200.Data
if responseData == nil {
b.resourceTree = make(map[string]string)
return nil
}
b.resourceTree = make(map[string]string, len(*responseData))
for _, resource := range *responseData {
if resource.Owner != nil {
b.resourceTree[*resource.Id] = *resource.Owner.Rid
}
}
return nil
}
func (b *bridge) fetchDeviceNames() error {
getDevicesResponse, err := b.resolvedClient.GetDevices()
if err != nil {
return fmt.Errorf("failed to access bridge devices on %s: %w", b, err)
}
if getDevicesResponse.HTTPResponse.StatusCode != http.StatusOK {
return fmt.Errorf("failed to fetch bridge devices from %s: %s", b, getDevicesResponse.HTTPResponse.Status)
}
responseData := getDevicesResponse.JSON200.Data
if responseData == nil {
b.deviceNames = make(map[string]string)
return nil
}
b.deviceNames = make(map[string]string, len(*responseData))
for _, device := range *responseData {
b.deviceNames[*device.Id] = *device.Metadata.Name
}
return nil
}
func (b *bridge) fetchRoomAssignments() error {
getRoomsResponse, err := b.resolvedClient.GetRooms()
if err != nil {
return fmt.Errorf("failed to access bridge rooms on %s: %w", b, err)
}
if getRoomsResponse.HTTPResponse.StatusCode != http.StatusOK {
return fmt.Errorf("failed to fetch bridge rooms from %s: %s", b, getRoomsResponse.HTTPResponse.Status)
}
responseData := getRoomsResponse.JSON200.Data
if responseData == nil {
b.roomAssignments = maps.Clone(b.configRoomAssignments)
return nil
}
b.roomAssignments = make(map[string]string, len(*responseData))
for _, roomGet := range *responseData {
for _, children := range *roomGet.Children {
b.roomAssignments[*children.Rid] = *roomGet.Metadata.Name
}
}
maps.Copy(b.roomAssignments, b.configRoomAssignments)
return nil
}
func (b *bridge) resolveResourceRoom(resourceID, resourceName string) string {
roomName := b.roomAssignments[resourceName]
if roomName != "" {
return roomName
}
// If resource does not have a room assigned directly, iterate upwards via
// its owners until we find a room or there is no more owner. The latter
// may happen (e.g. for Motion Sensors) resulting in room name
// "<unassigned>".
currentResourceID := resourceID
for {
// Try next owner
currentResourceID = b.resourceTree[currentResourceID]
if currentResourceID == "" {
// No owner left but no room found
break
}
roomName = b.roomAssignments[currentResourceID]
if roomName != "" {
// Room name found, done
return roomName
}
}
return "<unassigned>"
}
func (b *bridge) resolveDeviceName(resourceID string) string {
deviceName := b.deviceNames[resourceID]
if deviceName != "" {
return deviceName
}
// If resource does not have a device name assigned directly, iterate
// upwards via its owners until we find a room or there is no more
// owner. The latter may happen resulting in device name "<undefined>".
currentResourceID := resourceID
for {
// Try next owner
currentResourceID = b.resourceTree[currentResourceID]
if currentResourceID == "" {
// No owner left but no device found
break
}
deviceName = b.deviceNames[currentResourceID]
if deviceName != "" {
// Device name found, done
return deviceName
}
}
return "<undefined>"
}

View file

@ -0,0 +1,103 @@
//go:generate ../../../tools/readme_config_includer/generator
package huebridge
import (
_ "embed"
"errors"
"fmt"
"net/url"
"sync"
"time"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/plugins/common/tls"
"github.com/influxdata/telegraf/plugins/inputs"
)
//go:embed sample.conf
var sampleConfig string
type HueBridge struct {
BridgeUrls []string `toml:"bridges"`
RoomAssignments map[string]string `toml:"room_assignments"`
Timeout config.Duration `toml:"timeout"`
Log telegraf.Logger `toml:"-"`
remoteClientConfig
tls.ClientConfig
bridges []*bridge
}
type remoteClientConfig struct {
RemoteClientID string `toml:"remote_client_id"`
RemoteClientSecret string `toml:"remote_client_secret"`
RemoteCallbackURL string `toml:"remote_callback_url"`
RemoteTokenDir string `toml:"remote_token_dir"`
}
func (*HueBridge) SampleConfig() string {
return sampleConfig
}
func (h *HueBridge) Init() error {
tlsCfg, err := h.ClientConfig.TLSConfig()
if err != nil {
return fmt.Errorf("creating TLS configuration failed: %w", err)
}
h.bridges = make([]*bridge, 0, len(h.BridgeUrls))
for _, b := range h.BridgeUrls {
u, err := url.Parse(b)
if err != nil {
return fmt.Errorf("failed to parse bridge URL %s: %w", b, err)
}
switch u.Scheme {
case "address", "cloud", "mdns":
// Do nothing, those are valid
case "remote":
// Remote scheme also requires a configured rcc
if h.RemoteClientID == "" || h.RemoteClientSecret == "" || h.RemoteTokenDir == "" {
return errors.New("missing remote application credentials and/or token director not configured")
}
default:
return fmt.Errorf("unrecognized scheme %s in URL %s", u.Scheme, b)
}
// All schemes require a password in the URL
if _, set := u.User.Password(); !set {
return fmt.Errorf("missing password in URL %s", u)
}
h.bridges = append(h.bridges, &bridge{
url: u,
configRoomAssignments: h.RoomAssignments,
remoteCfg: &h.remoteClientConfig,
tlsCfg: tlsCfg,
timeout: time.Duration(h.Timeout),
log: h.Log,
})
}
return nil
}
func (h *HueBridge) Gather(acc telegraf.Accumulator) error {
var wg sync.WaitGroup
for _, bridge := range h.bridges {
wg.Add(1)
go func() {
defer wg.Done()
acc.AddError(bridge.process(acc))
}()
}
wg.Wait()
return nil
}
func init() {
inputs.Add("huebridge", func() telegraf.Input {
return &HueBridge{Timeout: config.Duration(10 * time.Second)}
})
}

View file

@ -0,0 +1,150 @@
package huebridge
import (
"fmt"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/tdrn-org/go-hue/mock"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/plugins/common/tls"
"github.com/influxdata/telegraf/plugins/parsers/influx"
"github.com/influxdata/telegraf/testutil"
)
func TestConfig(t *testing.T) {
// Verify plugin can be loaded from config
conf := config.NewConfig()
require.NoError(t, conf.LoadConfig("testdata/conf/huebridge.conf"))
require.Len(t, conf.Inputs, 1)
h, ok := conf.Inputs[0].Input.(*HueBridge)
require.True(t, ok)
// Verify successful Init
require.NoError(t, h.Init())
// Verify everything is setup according to config file
require.Len(t, h.BridgeUrls, 4)
require.Equal(t, "client", h.RemoteClientID)
require.Equal(t, "secret", h.RemoteClientSecret)
require.Equal(t, "url", h.RemoteCallbackURL)
require.Equal(t, "dir", h.RemoteTokenDir)
require.Len(t, h.RoomAssignments, 2)
require.Equal(t, config.Duration(60*time.Second), h.Timeout)
require.Equal(t, "secret", h.TLSKeyPwd)
require.True(t, h.InsecureSkipVerify)
}
func TestInitSuccess(t *testing.T) {
// Create plugin instance with all types of URL schemes
h := &HueBridge{
BridgeUrls: []string{
"address://12345678:secret@localhost/",
"cloud://12345678:secret@localhost/discovery/",
"mdns://12345678:secret@/",
"remote://12345678:secret@localhost/",
},
remoteClientConfig: remoteClientConfig{
RemoteClientID: mock.MockClientId,
RemoteClientSecret: mock.MockClientSecret,
RemoteTokenDir: ".",
},
ClientConfig: tls.ClientConfig{
InsecureSkipVerify: true,
},
Timeout: config.Duration(10 * time.Second),
Log: &testutil.Logger{Name: "huebridge"},
}
// Verify successful Init
require.NoError(t, h.Init())
// Verify successful configuration of all bridge URLs
require.Len(t, h.bridges, len(h.BridgeUrls))
}
func TestInitIgnoreInvalidUrls(t *testing.T) {
tests := []struct {
addr string
expected string
}{
{
addr: "invalid://12345678:secret@invalid-scheme.net/",
expected: "unrecognized scheme",
},
{
addr: "address://12345678@missing-password.net/",
expected: "missing password in URL",
},
{
addr: "cloud://12345678@missing-password.net/",
expected: "missing password in URL",
},
{
addr: "mdns://12345678@missing-password.net/",
expected: "missing password in URL",
},
{
addr: "remote://12345678@missing-password.net/",
expected: "missing remote application credentials and/or token director not configured",
},
{
addr: "remote://12345678:secret@missing-remote-config.net/",
expected: "missing remote application credentials and/or token director not configured",
},
}
for _, tt := range tests {
t.Run(tt.addr, func(t *testing.T) {
// The following URLs are all invalid must all be ignored during Init
plugin := &HueBridge{
BridgeUrls: []string{tt.addr},
Timeout: config.Duration(10 * time.Second),
Log: &testutil.Logger{Name: "huebridge"},
}
// Verify successful Init
require.ErrorContains(t, plugin.Init(), tt.expected)
// Verify no bridge have been configured
require.Empty(t, plugin.bridges)
})
}
}
func TestGatherLocal(t *testing.T) {
// Load the expected metrics
parser := &influx.Parser{}
require.NoError(t, parser.Init())
fn := filepath.Join("testdata", "metrics", "huebridge.txt")
expected, err := testutil.ParseMetricsFromFile(fn, parser)
require.NoError(t, err)
for i := range expected {
expected[i].SetType(telegraf.Gauge)
}
// Start mock server and make plugin targing it
bridgeMock := mock.Start()
require.NotNil(t, bridgeMock)
defer bridgeMock.Shutdown()
// Setup the plugin
h := &HueBridge{
BridgeUrls: []string{
fmt.Sprintf("address://%s:%s@%s/", mock.MockBridgeId, mock.MockBridgeUsername, bridgeMock.Server().Host),
},
RoomAssignments: map[string]string{"Name#7": "Name#15"},
Timeout: config.Duration(10 * time.Second),
Log: &testutil.Logger{Name: "huebridge"},
}
require.NoError(t, h.Init())
// Verify successfull collection
var acc testutil.Accumulator
require.NoError(t, acc.GatherError(h.Gather))
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime(), testutil.SortMetrics())
}

View file

@ -0,0 +1,20 @@
# Gather smart home status from Hue Bridge
[[inputs.huebridge]]
## URL of bridges to query in the form <scheme>://<bridge id>:<user name>@<address>/
## See documentation for available schemes.
bridges = [ "address://<bridge id>:<user name>@<bridge hostname or address>/" ]
## Manual device to room assignments to apply during status evaluation.
## E.g. for motion sensors which are reported without a room assignment.
# room_assignments = { "Motion sensor 1" = "Living room", "Motion sensor 2" = "Corridor" }
## Timeout for gathering information
# timeout = "10s"
## Optional TLS Config
# tls_ca = "/etc/telegraf/ca.pem"
# tls_cert = "/etc/telegraf/cert.pem"
# tls_key = "/etc/telegraf/key.pem"
# tls_key_pwd = "secret"
## Use TLS but skip chain & host verification
# insecure_skip_verify = false

View file

@ -0,0 +1,30 @@
# Gather smart home status from Hue Bridge
[[inputs.huebridge]]
## The Hue bridges to query.
## See README file for all addressing options.
bridges = [
"address://0123456789ABCDEF:sFlEGnMAFXO6RtZV17aViNUB95G2uXWw64texDzD@mybridgenameorip/",
"cloud://0123456789ABCDEF:sFlEGnMAFXO6RtZV17aViNUB95G2uXWw64texDzD@discovery.meethue.com/",
"mdns://0123456789ABCDEF:sFlEGnMAFXO6RtZV17aViNUB95G2uXWw64texDzD@/",
"remote://0123456789ABCDEF:sFlEGnMAFXO6RtZV17aViNUB95G2uXWw64texDzD@api.meethue.com/",
]
remote_client_id = "client"
remote_client_secret = "secret"
remote_callback_url = "url"
remote_token_dir = "dir"
## Manual device to room assignments to apply during status evaluation.
## E.g. for motion sensors which are reported without a room assignment.
room_assignments = { "Device 1" = "Room A", "Device 2" = "Room B" }
## Timeout for gathering information
timeout = "1m"
## Optional TLS Config
# tls_ca = "/etc/telegraf/ca.pem"
# tls_cert = "/etc/telegraf/cert.pem"
# tls_key = "/etc/telegraf/key.pem"
tls_key_pwd = "secret"
## Use TLS but skip chain & host verification
insecure_skip_verify = true

View file

@ -0,0 +1,14 @@
huebridge_light,bridge_id=0123456789ABCDEF,device=Name#3,room=Name#15 on=0i 1737181537879611000
huebridge_light,bridge_id=0123456789ABCDEF,device=Name#8,room=Name#14 on=0i 1737181537879628000
huebridge_light,bridge_id=0123456789ABCDEF,device=Name#12,room=Name#16 on=0i 1737181537879632000
huebridge_light,bridge_id=0123456789ABCDEF,device=Name#6,room=Name#13 on=0i 1737181537879634000
huebridge_light,bridge_id=0123456789ABCDEF,device=Name#1,room=Name#13 on=0i 1737181537879635000
huebridge_light,bridge_id=0123456789ABCDEF,device=Name#2,room=Name#13 on=0i 1737181537879637000
huebridge_light,bridge_id=0123456789ABCDEF,device=Name#5,room=Name#15 on=0i 1737181537879639000
huebridge_light,bridge_id=0123456789ABCDEF,device=Name#9,room=Name#13 on=0i 1737181537879640000
huebridge_light,bridge_id=0123456789ABCDEF,device=Name#11,room=Name#15 on=0i 1737181537879642000
huebridge_light,bridge_id=0123456789ABCDEF,device=Name#4,room=Name#14 on=0i 1737181537879646000
huebridge_temperature,bridge_id=0123456789ABCDEF,device=Name#7,enabled=true,room=Name#15 temperature=17.6299991607666 1737181537879828000
huebridge_light_level,bridge_id=0123456789ABCDEF,device=Name#7,enabled=true,room=Name#15 light_level=18948i,light_level_lux=78.46934003526889 1737181537880034000
huebridge_motion_sensor,bridge_id=0123456789ABCDEF,device=Name#7,enabled=true,room=Name#15 motion=0i 1737181537880213000
huebridge_device_power,bridge_id=0123456789ABCDEF,device=Name#7,room=Name#15 battery_level=100i 1737181537880360000