Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
e393c3af3f
commit
4978089aab
4963 changed files with 677545 additions and 0 deletions
183
plugins/inputs/huebridge/README.md
Normal file
183
plugins/inputs/huebridge/README.md
Normal 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
|
||||
```
|
424
plugins/inputs/huebridge/bridge.go
Normal file
424
plugins/inputs/huebridge/bridge.go
Normal 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>"
|
||||
}
|
103
plugins/inputs/huebridge/huebridge.go
Normal file
103
plugins/inputs/huebridge/huebridge.go
Normal 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)}
|
||||
})
|
||||
}
|
150
plugins/inputs/huebridge/huebridge_test.go
Normal file
150
plugins/inputs/huebridge/huebridge_test.go
Normal 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())
|
||||
}
|
20
plugins/inputs/huebridge/sample.conf
Normal file
20
plugins/inputs/huebridge/sample.conf
Normal 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
|
30
plugins/inputs/huebridge/testdata/conf/huebridge.conf
vendored
Normal file
30
plugins/inputs/huebridge/testdata/conf/huebridge.conf
vendored
Normal 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
|
14
plugins/inputs/huebridge/testdata/metrics/huebridge.txt
vendored
Normal file
14
plugins/inputs/huebridge/testdata/metrics/huebridge.txt
vendored
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue