diff --git a/EXTERNAL_PLUGINS.md b/EXTERNAL_PLUGINS.md index 5463ef9201c79..e306cc6ee3b22 100644 --- a/EXTERNAL_PLUGINS.md +++ b/EXTERNAL_PLUGINS.md @@ -30,7 +30,6 @@ Pull requests welcome. - [knot](https://github.com/x70b1/telegraf-knot) - Collect stats from Knot DNS. - [fritzbox](https://github.com/hdecarne-github/fritzbox-telegraf-plugin) - Gather statistics from [FRITZ!Box](https://avm.de/produkte/fritzbox/) router and repeater - [linux-psi-telegraf-plugin](https://github.com/gridscale/linux-psi-telegraf-plugin) - Gather pressure stall information ([PSI](https://facebookmicrosites.github.io/psi/)) from the Linux Kernel -- [huebridge](https://github.com/hdecarne-github/huebridge-telegraf-plugin) - Gather smart home statistics from [Hue Bridge](https://www.philips-hue.com/) devices - [nsdp](https://github.com/hdecarne-github/nsdp-telegraf-plugin) - Gather switch network statistics via [Netgear Switch Discovery Protocol](https://en.wikipedia.org/wiki/Netgear_Switch_Discovery_Protocol) - [hwinfo](https://github.com/zachstence/hwinfo-telegraf-plugin) - Gather Windows system hardware information from [HWiNFO](https://www.hwinfo.com/) - [libvirt](https://gitlab.com/warrenio/tools/telegraf-input-libvirt) - Gather libvirt domain stats, based on a historical Telegraf implementation [libvirt](https://libvirt.org/) diff --git a/docs/LICENSE_OF_DEPENDENCIES.md b/docs/LICENSE_OF_DEPENDENCIES.md index 24f5d6af3a5a2..79b1281c9bb5f 100644 --- a/docs/LICENSE_OF_DEPENDENCIES.md +++ b/docs/LICENSE_OF_DEPENDENCIES.md @@ -57,6 +57,7 @@ following works: - github.com/apache/arrow/go [Apache License 2.0](https://github.com/apache/arrow/blob/master/LICENSE.txt) - github.com/apache/iotdb-client-go [Apache License 2.0](https://github.com/apache/iotdb-client-go/blob/main/LICENSE) - github.com/apache/thrift [Apache License 2.0](https://github.com/apache/thrift/blob/master/LICENSE) +- github.com/apapsch/go-jsonmerge [MIT License](https://github.com/apapsch/go-jsonmerge/blob/master/LICENSE) - github.com/aristanetworks/glog [Apache License 2.0](https://github.com/aristanetworks/glog/blob/master/LICENSE) - github.com/aristanetworks/goarista [Apache License 2.0](https://github.com/aristanetworks/goarista/blob/master/COPYING) - github.com/armon/go-metrics [MIT License](https://github.com/armon/go-metrics/blob/master/LICENSE) @@ -96,6 +97,7 @@ following works: - github.com/blues/jsonata-go [MIT License](https://github.com/blues/jsonata-go/blob/main/LICENSE) - github.com/bmatcuk/doublestar [MIT License](https://github.com/bmatcuk/doublestar/blob/master/LICENSE) - github.com/boschrexroth/ctrlx-datalayer-golang [MIT License](https://github.com/boschrexroth/ctrlx-datalayer-golang/blob/main/LICENSE) +- github.com/brutella/dnssd [MIT License](https://github.com/brutella/dnssd/blob/master/LICENSE) - github.com/bufbuild/protocompile [Apache License 2.0](https://github.com/bufbuild/protocompile/blob/main/LICENSE) - github.com/caio/go-tdigest [MIT License](https://github.com/caio/go-tdigest/blob/master/LICENSE) - github.com/cenkalti/backoff [MIT License](https://github.com/cenkalti/backoff/blob/master/LICENSE) @@ -303,6 +305,7 @@ following works: - github.com/newrelic/newrelic-telemetry-sdk-go [Apache License 2.0](https://github.com/newrelic/newrelic-telemetry-sdk-go/blob/master/LICENSE.md) - github.com/nsqio/go-nsq [MIT License](https://github.com/nsqio/go-nsq/blob/master/LICENSE) - github.com/nwaples/tacplus [BSD 2-Clause "Simplified" License](https://github.com/nwaples/tacplus/blob/master/LICENSE) +- github.com/oapi-codegen/runtime [Apache License 2.0](https://github.com/oapi-codegen/runtime/blob/main/LICENSE) - github.com/olivere/elastic [MIT License](https://github.com/olivere/elastic/blob/release-branch.v7/LICENSE) - github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil [Apache License 2.0](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/LICENSE) - github.com/openconfig/gnmi [Apache License 2.0](https://github.com/openconfig/gnmi/blob/master/LICENSE) @@ -341,6 +344,7 @@ following works: - github.com/rivo/uniseg [MIT License](https://github.com/rivo/uniseg/blob/master/LICENSE.txt) - github.com/robbiet480/go.nut [MIT License](https://github.com/robbiet480/go.nut/blob/master/LICENSE) - github.com/robinson/gos7 [BSD 3-Clause "New" or "Revised" License](https://github.com/robinson/gos7/blob/master/LICENSE) +- github.com/rs/zerolog [MIT License](https://github.com/rs/zerolog/blob/master/LICENSE) - github.com/russross/blackfriday [BSD 2-Clause "Simplified" License](https://github.com/russross/blackfriday/blob/master/LICENSE.txt) - github.com/safchain/ethtool [Apache License 2.0](https://github.com/safchain/ethtool/blob/master/LICENSE) - github.com/samber/lo [MIT License](https://github.com/samber/lo/blob/master/LICENSE) @@ -365,6 +369,8 @@ following works: - github.com/stoewer/go-strcase [MIT License](https://github.com/stoewer/go-strcase/blob/master/LICENSE) - github.com/stretchr/objx [MIT License](https://github.com/stretchr/objx/blob/master/LICENSE) - github.com/stretchr/testify [MIT License](https://github.com/stretchr/testify/blob/master/LICENSE) +- github.com/tdrn-org/go-hue [MIT License](https://github.com/tdrn-org/go-log/blob/main/LICENSE) +- github.com/tdrn-org/go-log [Apache License 2.0](https://github.com/tdrn-org/go-hue/blob/main/LICENSE) - github.com/testcontainers/testcontainers-go [MIT License](https://github.com/testcontainers/testcontainers-go/blob/main/LICENSE) - github.com/thomasklein94/packer-plugin-libvirt [Mozilla Public License 2.0](https://github.com/thomasklein94/packer-plugin-libvirt/blob/main/LICENSE) - github.com/tidwall/gjson [MIT License](https://github.com/tidwall/gjson/blob/master/LICENSE) @@ -442,6 +448,7 @@ following works: - gopkg.in/gorethink/gorethink.v3 [Apache License 2.0](https://github.com/rethinkdb/rethinkdb-go/blob/v3.0.5/LICENSE) - gopkg.in/inf.v0 [BSD 3-Clause "New" or "Revised" License](https://github.com/go-inf/inf/blob/v0.9.1/LICENSE) - gopkg.in/ini.v1 [Apache License 2.0](https://github.com/go-ini/ini/blob/master/LICENSE) +- gopkg.in/natefinch/lumberjack.v2 [MIT License](https://github.com/natefinch/lumberjack/blob/v2.2.1/LICENSE) - gopkg.in/olivere/elastic.v5 [MIT License](https://github.com/olivere/elastic/blob/v5.0.76/LICENSE) - gopkg.in/tomb.v1 [BSD 3-Clause Clear License](https://github.com/go-tomb/tomb/blob/v1/LICENSE) - gopkg.in/tomb.v2 [BSD 3-Clause Clear License](https://github.com/go-tomb/tomb/blob/v2/LICENSE) diff --git a/go.mod b/go.mod index a4527713c6e48..48f7ef01ad594 100644 --- a/go.mod +++ b/go.mod @@ -189,6 +189,7 @@ require ( github.com/srebhan/protobufquery v1.0.1 github.com/stretchr/testify v1.10.0 github.com/tbrandon/mbserver v0.0.0-20170611213546-993e1772cc62 + github.com/tdrn-org/go-hue v0.2.0 github.com/testcontainers/testcontainers-go v0.34.0 github.com/testcontainers/testcontainers-go/modules/kafka v0.34.0 github.com/thomasklein94/packer-plugin-libvirt v0.5.0 @@ -278,6 +279,7 @@ require ( github.com/andybalholm/brotli v1.1.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/apache/arrow/go/v15 v15.0.2 // indirect + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/aristanetworks/glog v0.0.0-20191112221043-67e8567f59f3 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/awnumar/memcall v0.3.0 // indirect @@ -301,6 +303,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bitly/go-hostpool v0.1.0 // indirect github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect + github.com/brutella/dnssd v1.2.14 // indirect github.com/bufbuild/protocompile v0.10.0 // indirect github.com/caio/go-tdigest/v4 v4.0.1 // indirect github.com/cenkalti/backoff v2.2.1+incompatible // indirect @@ -405,7 +408,7 @@ require ( github.com/leodido/ragel-machinery v0.0.0-20190525184631-5f46317e436b // indirect github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a // indirect github.com/magiconair/properties v1.8.9 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-ieproxy v0.0.11 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -439,6 +442,7 @@ require ( github.com/nats-io/nuid v1.0.1 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncw/swift/v2 v2.0.3 // indirect + github.com/oapi-codegen/runtime v1.1.1 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.101.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect @@ -460,6 +464,7 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff // indirect github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/rs/zerolog v1.33.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/samber/lo v1.47.0 // indirect github.com/seancfoley/bintree v1.3.1 // indirect @@ -473,6 +478,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/tdrn-org/go-log v0.2.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/tinylru v1.2.1 // indirect @@ -522,6 +528,7 @@ require ( gopkg.in/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/sourcemap.v1 v1.0.5 // indirect gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 4809a23bd1f02..cb1c558d53be0 100644 --- a/go.sum +++ b/go.sum @@ -783,6 +783,7 @@ github.com/ProtonMail/gopenpgp/v2 v2.7.4 h1:Vz/8+HViFFnf2A6XX8JOvZMrA6F5puwNvvF2 github.com/ProtonMail/gopenpgp/v2 v2.7.4/go.mod h1:IhkNEDaxec6NyzSI0PlxapinnwPVIESk8/76da3Ct3g= github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/SAP/go-hdb v1.9.10 h1:Smi3w0y8G9DVxR4z+Tvow8AJNqQq1fdCCMwplyapvR4= github.com/SAP/go-hdb v1.9.10/go.mod h1:vxYDca44L2eRudZv5JAI6T+IygOfxb7vOCFh/Kj0pug= github.com/aalpar/deheap v0.0.0-20210914013432-0cc84d79dec3 h1:hhdWprfSpFbN7lz3W1gM40vOgvSh1WCSMxYD6gGB4Hs= @@ -846,6 +847,8 @@ github.com/apache/thrift v0.15.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2 github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE= github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/apex/log v1.6.0/go.mod h1:x7s+P9VtvFBXge9Vbn+8TrqKmuzmD35TTkeBHul8UtY= github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= @@ -977,6 +980,8 @@ github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwj github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/blues/jsonata-go v1.5.4 h1:XCsXaVVMrt4lcpKeJw6mNJHqQpWU751cnHdCFUq3xd8= github.com/blues/jsonata-go v1.5.4/go.mod h1:uns2jymDrnI7y+UFYCqsRTEiAH22GyHnNXrkupAVFWI= +github.com/bmatcuk/doublestar v1.1.1 h1:YroD6BJCZBYx06yYFEWvUuKVWQn3vLLQAVmDmvTSaiQ= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bmatcuk/doublestar/v3 v3.0.0 h1:TQtVPlDnAYwcrVNB2JiGuMc++H5qzWZd9PhkNo5WyHI= github.com/bmatcuk/doublestar/v3 v3.0.0/go.mod h1:6PcTVMw80pCY1RVuoqu3V++99uQB3vsSYKPTd8AWA0k= github.com/bmatcuk/doublestar/v4 v4.8.0 h1:DSXtrypQddoug1459viM9X9D3dp1Z7993fw36I2kNcQ= @@ -991,6 +996,8 @@ github.com/bradenaw/juniper v0.15.2 h1:0JdjBGEF2jP1pOxmlNIrPhAoQN7Ng5IMAY5D0PHMW github.com/bradenaw/juniper v0.15.2/go.mod h1:UX4FX57kVSaDp4TPqvSjkAAewmRFAfXf27BOs5z9dq8= github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8= github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og= +github.com/brutella/dnssd v1.2.14 h1:qLpTnRTm5peo2jA30hqMIbCuWn8x3sFg3e9o9ODOobw= +github.com/brutella/dnssd v1.2.14/go.mod h1:tG4GE8orv6+irE5rdsNgb6MJSxm6cyMUKdC5jmD22gk= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -1735,6 +1742,7 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolio/noiseconn v0.0.0-20231127013910-f6d9ecbf1de7 h1:JcltaO1HXM5S2KYOYcKgAV7slU0xPy1OcvrVgn98sRQ= github.com/jtolio/noiseconn v0.0.0-20231127013910-f6d9ecbf1de7/go.mod h1:MEkhEPFwP3yudWO0lj6vfYpLIB+3eIcuIW+e0AZzUQk= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= @@ -1823,8 +1831,8 @@ github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPK github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -1845,6 +1853,7 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= @@ -1989,6 +1998,8 @@ github.com/nwaples/tacplus v0.0.3/go.mod h1:y5ZA9N5V2JbmwO766S+ET9zuu5FtL1OtdfBC github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= +github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852/go.mod h1:eqOVx5Vwu4gd2mmMZvVZsgIqNSaW3xxRThUJ0k/TPk4= @@ -2199,8 +2210,11 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -2299,6 +2313,7 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/srebhan/cborquery v1.0.1 h1:cFG1falVzmlfyVI8tY6hYM7RQqLxFzt9STusdxHoy0U= github.com/srebhan/cborquery v1.0.1/go.mod h1:GgsaIoCW+qlqyU+cjSeOpaWhbiiMVkA0uU/H3+PWvjQ= github.com/srebhan/protobufquery v1.0.1 h1:V5NwX0GAQPPghWpoD9Pkm85j66CwISZ/zZW4grzayWs= @@ -2334,6 +2349,10 @@ github.com/t3rm1n4l/go-mega v0.0.0-20240219080617-d494b6a8ace7 h1:Jtcrb09q0AVWe3 github.com/t3rm1n4l/go-mega v0.0.0-20240219080617-d494b6a8ace7/go.mod h1:suDIky6yrK07NnaBadCB4sS0CqFOvUK91lH7CR+JlDA= github.com/tbrandon/mbserver v0.0.0-20170611213546-993e1772cc62 h1:Oj2e7Sae4XrOsk3ij21QjjEgAcVSeo9nkp0dI//cD2o= github.com/tbrandon/mbserver v0.0.0-20170611213546-993e1772cc62/go.mod h1:qUzPVlSj2UgxJkVbH0ZwuuiR46U8RBMDT5KLY78Ifpw= +github.com/tdrn-org/go-hue v0.2.0 h1:ySwTCt8YfDnsX0sMgOZiVE9mG0oPO8I5Y1gOsci9Lhk= +github.com/tdrn-org/go-hue v0.2.0/go.mod h1:OajO3RsIHOOCZyD/NwhbnMCLNspF74e8zAbRVVvA52g= +github.com/tdrn-org/go-log v0.2.0 h1:8+9apkGOEbn+J1PHtvLuUVIu9tRDQrYY9lzA1ds4AQI= +github.com/tdrn-org/go-log v0.2.0/go.mod h1:rThnKIZojFQ662PmJOwO8Ymmzi0dL+KrUQRQR90Na8Y= github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955uh98WQvzOm0dgAeLnf2O0Rz0LPoC5ze+0= github.com/testcontainers/testcontainers-go v0.34.0 h1:5fbgF0vIN5u+nD3IWabQwRybuB4GY8G2HHgCkbMzMHo= github.com/testcontainers/testcontainers-go v0.34.0/go.mod h1:6P/kMkQe8yqPHfPWNulFGdFHTD8HB2vLq/231xY2iPQ= @@ -2903,6 +2922,7 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -3361,6 +3381,8 @@ gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/olivere/elastic.v5 v5.0.86 h1:xFy6qRCGAmo5Wjx96srho9BitLhZl2fcnpuidPwduXM= gopkg.in/olivere/elastic.v5 v5.0.86/go.mod h1:M3WNlsF+WhYn7api4D87NIflwTV/c0iVs8cqfWhK+68= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= diff --git a/plugins/inputs/all/huebridge.go b/plugins/inputs/all/huebridge.go new file mode 100644 index 0000000000000..4bfd1499becd5 --- /dev/null +++ b/plugins/inputs/all/huebridge.go @@ -0,0 +1,5 @@ +//go:build !custom || inputs || inputs.huebridge + +package all + +import _ "github.com/influxdata/telegraf/plugins/inputs/huebridge" // register plugin diff --git a/plugins/inputs/huebridge/README.md b/plugins/inputs/huebridge/README.md new file mode 100644 index 0000000000000..86da0a2423148 --- /dev/null +++ b/plugins/inputs/huebridge/README.md @@ -0,0 +1,183 @@ +# HueBridge Input Plugin + +This input 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 + +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 ://:@
/ + ## See documentation for available schemes. + bridges = [ "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 + ://:@/ +``` + +where the `bridge id` is the unique bridge id as returned in + +```bash +curl -k https://
/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:///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 +``` diff --git a/plugins/inputs/huebridge/bridge.go b/plugins/inputs/huebridge/bridge.go new file mode 100644 index 0000000000000..36b2ce19be0c9 --- /dev/null +++ b/plugins/inputs/huebridge/bridge.go @@ -0,0 +1,323 @@ +package huebridge + +import ( + "fmt" + "math" + "net/http" + "net/url" + "path/filepath" + "slices" + "strconv" + "strings" + "time" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/config" + "github.com/influxdata/telegraf/plugins/common/tls" + "github.com/tdrn-org/go-hue" +) + +type bridge struct { + url *url.URL + roomAssignments map[string]string + rcc *RemoteClientConfig + tcc *tls.ClientConfig + timeout config.Duration + log telegraf.Logger + resolvedClient hue.BridgeClient +} + +func newBridge(rawUrl string, roomAssignments map[string]string, rcc *RemoteClientConfig, tcc *tls.ClientConfig, timeout config.Duration, log telegraf.Logger) (*bridge, error) { + parsedUrl, err := url.Parse(rawUrl) + if err != nil { + return nil, fmt.Errorf("failed to parse bridge URL %s: %w", rawUrl, err) + } + validSchemes := []string{"address", "cloud", "mdns", "remote"} + if slices.Index(validSchemes, parsedUrl.Scheme) < 0 { + return nil, fmt.Errorf("unrecognized scheme %s in URL %s", parsedUrl.Scheme, parsedUrl) + } + // All schemes require a password in the URL + _, passwordSet := parsedUrl.User.Password() + if !passwordSet { + return nil, fmt.Errorf("missing password in URL %s", parsedUrl) + } + // Remote scheme also requires a configured rcc + if parsedUrl.Scheme == "remote" { + if rcc.RemoteClientId == "" || rcc.RemoteClientSecret == "" || rcc.RemoteTokenDir == "" { + return nil, fmt.Errorf("missing remote application credentials and/or token director not configured") + } + } + return &bridge{ + url: parsedUrl, + roomAssignments: roomAssignments, + rcc: rcc, + tcc: tcc, + timeout: timeout, + log: log}, nil +} + +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) + metadata, err := fetchMetadata(b.resolvedClient, b.roomAssignments) + if err != nil { + // Discard previously resolved client and re-resolve on next process call + b.resolvedClient = nil + return err + } + acc.AddError(b.processLights(acc, metadata)) + acc.AddError(b.processTemperatures(acc, metadata)) + acc.AddError(b.processLightLevels(acc, metadata)) + acc.AddError(b.processMotionSensors(acc, metadata)) + acc.AddError(b.processDevicePowers(acc, metadata)) + return nil +} + +func (b *bridge) processLights(acc telegraf.Accumulator, metadata *bridgeMetadata) 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"] = metadata.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, metadata *bridgeMetadata) 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 := metadata.resolveDeviceName(*temperature.Id) + tags := make(map[string]string) + tags["bridge_id"] = b.resolvedClient.Bridge().BridgeId + tags["room"] = metadata.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, metadata *bridgeMetadata) 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 := metadata.resolveDeviceName(*lightLevel.Id) + tags := make(map[string]string) + tags["bridge_id"] = b.resolvedClient.Bridge().BridgeId + tags["room"] = metadata.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, metadata *bridgeMetadata) 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 := metadata.resolveDeviceName(*motionSensor.Id) + tags := make(map[string]string) + tags["bridge_id"] = b.resolvedClient.Bridge().BridgeId + tags["room"] = metadata.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, metadata *bridgeMetadata) 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 := metadata.resolveDeviceName(*devicePower.Id) + tags := make(map[string]string) + tags["bridge_id"] = b.resolvedClient.Bridge().BridgeId + tags["room"] = metadata.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 != "" { + discoveryEndpointUrl, err := url.Parse(fmt.Sprintf("https://%s/", b.url.Host)) + if err != nil { + return err + } + discoveryEndpointUrl = discoveryEndpointUrl.JoinPath(b.url.Path) + locator.DiscoveryEndpointUrl = discoveryEndpointUrl + } + tlsConfig, err := b.tcc.TLSConfig() + if err != nil { + return err + } + locator.TlsConfig = tlsConfig + 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(), time.Duration(b.timeout)) + if err != nil { + return err + } + urlPassword, _ := b.url.User.Password() + bridgeClient, err := hueBridge.NewClient(hue.NewLocalBridgeAuthenticator(urlPassword), time.Duration(b.timeout)) + if err != nil { + return err + } + b.resolvedClient = bridgeClient + return nil +} + +func (b *bridge) resolveViaRemote() error { + var redirectUrl *url.URL + if b.rcc.RemoteCallbackUrl != "" { + parsedRedirectUrl, err := url.Parse(b.rcc.RemoteCallbackUrl) + if err != nil { + return err + } + redirectUrl = parsedRedirectUrl + } + tokenFile := filepath.Join(b.rcc.RemoteTokenDir, b.rcc.RemoteClientId, strings.ToUpper(b.url.User.Username())+".json") + locator, err := hue.NewRemoteBridgeLocator(b.rcc.RemoteClientId, b.rcc.RemoteClientSecret, redirectUrl, tokenFile) + if err != nil { + return err + } + if b.url.Host != "" { + endpointUrl, err := url.Parse(fmt.Sprintf("https://%s/", b.url.Host)) + if err != nil { + return err + } + endpointUrl = endpointUrl.JoinPath(b.url.Path) + locator.EndpointUrl = endpointUrl + } + tlsConfig, err := b.tcc.TLSConfig() + if err != nil { + return err + } + locator.TlsConfig = tlsConfig.Clone() + return b.resolveRemoteBridge(locator) +} + +func (b *bridge) resolveRemoteBridge(locator *hue.RemoteBridgeLocator) error { + hueBridge, err := locator.Lookup(b.url.User.Username(), time.Duration(b.timeout)) + if err != nil { + return err + } + urlPassword, _ := b.url.User.Password() + bridgeClient, err := hueBridge.NewClient(hue.NewRemoteBridgeAuthenticator(locator, urlPassword), time.Duration(b.timeout)) + if err != nil { + return err + } + b.resolvedClient = bridgeClient + return nil +} diff --git a/plugins/inputs/huebridge/bridgemetadata.go b/plugins/inputs/huebridge/bridgemetadata.go new file mode 100644 index 0000000000000..728dd306ff164 --- /dev/null +++ b/plugins/inputs/huebridge/bridgemetadata.go @@ -0,0 +1,149 @@ +package huebridge + +import ( + "fmt" + "maps" + "net/http" + + "github.com/tdrn-org/go-hue" +) + +type bridgeMetadata struct { + resourceTree map[string]string + deviceNames map[string]string + roomAssignments map[string]string +} + +func fetchMetadata(bridgeClient hue.BridgeClient, manualRoomAsignments map[string]string) (*bridgeMetadata, error) { + resourceTree, err := fetchResourceTree(bridgeClient) + if err != nil { + return nil, err + } + deviceNames, err := fetchDeviceNames(bridgeClient) + if err != nil { + return nil, err + } + roomAssignments, err := fetchRoomAssignments(bridgeClient) + if err != nil { + return nil, err + } + maps.Copy(roomAssignments, manualRoomAsignments) + metadata := &bridgeMetadata{ + resourceTree: resourceTree, + deviceNames: deviceNames, + roomAssignments: roomAssignments, + } + return metadata, nil +} + +func fetchResourceTree(bridgeClient hue.BridgeClient) (map[string]string, error) { + getResourcesResponse, err := bridgeClient.GetResources() + if err != nil { + return nil, fmt.Errorf("failed to access bridge resources on %s: %w", bridgeClient.Url().Redacted(), err) + } + if getResourcesResponse.HTTPResponse.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to fetch bridge resources from %s: %s", bridgeClient.Url().Redacted(), getResourcesResponse.HTTPResponse.Status) + } + responseData := getResourcesResponse.JSON200.Data + if responseData == nil { + return make(map[string]string), nil + } + tree := make(map[string]string, len(*responseData)) + for _, resource := range *responseData { + if resource.Owner != nil { + tree[*resource.Id] = *resource.Owner.Rid + } + } + return tree, nil +} + +func fetchDeviceNames(bridgeClient hue.BridgeClient) (map[string]string, error) { + getDevicesResponse, err := bridgeClient.GetDevices() + if err != nil { + return nil, fmt.Errorf("failed to access bridge devices on %s: %w", bridgeClient.Url().Redacted(), err) + } + if getDevicesResponse.HTTPResponse.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to fetch bridge devices from %s: %s", bridgeClient.Url().Redacted(), getDevicesResponse.HTTPResponse.Status) + } + responseData := getDevicesResponse.JSON200.Data + if responseData == nil { + return make(map[string]string), nil + } + names := make(map[string]string, len(*responseData)) + for _, device := range *responseData { + names[*device.Id] = *device.Metadata.Name + } + return names, nil +} + +func fetchRoomAssignments(bridgeClient hue.BridgeClient) (map[string]string, error) { + getRoomsResponse, err := bridgeClient.GetRooms() + if err != nil { + return nil, fmt.Errorf("failed to access bridge rooms on %s: %w", bridgeClient.Url().Redacted(), err) + } + if getRoomsResponse.HTTPResponse.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to fetch bridge rooms from %s: %s", bridgeClient.Url().Redacted(), getRoomsResponse.HTTPResponse.Status) + } + responseData := getRoomsResponse.JSON200.Data + if responseData == nil { + return make(map[string]string), nil + } + assignments := make(map[string]string, len(*responseData)) + for _, roomGet := range *responseData { + for _, children := range *roomGet.Children { + assignments[*children.Rid] = *roomGet.Metadata.Name + } + } + return assignments, nil +} + +func (metadata *bridgeMetadata) resolveResourceRoom(resourceId string, resourceName string) string { + roomName := metadata.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 + // "". + currentResourceId := resourceId + for { + // Try next owner + currentResourceId = metadata.resourceTree[currentResourceId] + if currentResourceId == "" { + // No owner left but no room found + break + } + roomName = metadata.roomAssignments[currentResourceId] + if roomName != "" { + // Room name found, done + return roomName + } + } + return "" +} + +func (metadata *bridgeMetadata) resolveDeviceName(resourceId string) string { + deviceName := metadata.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 "". + currentResourceId := resourceId + for { + // Try next owner + currentResourceId = metadata.resourceTree[currentResourceId] + if currentResourceId == "" { + // No owner left but no device found + break + } + deviceName = metadata.deviceNames[currentResourceId] + if deviceName != "" { + // Device name found, done + return deviceName + } + } + return "" +} diff --git a/plugins/inputs/huebridge/huebridge.go b/plugins/inputs/huebridge/huebridge.go new file mode 100644 index 0000000000000..1a1be991f5ed3 --- /dev/null +++ b/plugins/inputs/huebridge/huebridge.go @@ -0,0 +1,70 @@ +//go:generate ../../../tools/readme_config_includer/generator +package huebridge + +import ( + _ "embed" + "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 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"` +} + +type HueBridge struct { + Bridges []string `toml:"bridges"` + RoomAssignments map[string]string `toml:"room_assignments"` + Timeout config.Duration `toml:"timeout"` + Log telegraf.Logger `toml:"-"` + RemoteClientConfig + tls.ClientConfig + + configuredBridges []*bridge +} + +func (*HueBridge) SampleConfig() string { + return sampleConfig +} + +func (h *HueBridge) Init() error { + h.configuredBridges = make([]*bridge, 0, len(h.Bridges)) + for _, bridgeUrl := range h.Bridges { + bridge, err := newBridge(bridgeUrl, h.RoomAssignments, &h.RemoteClientConfig, &h.ClientConfig, h.Timeout, h.Log) + if err != nil { + h.Log.Warnf("Failed to instantiate bridge for URL %s: %s", bridgeUrl, err) + continue + } + h.configuredBridges = append(h.configuredBridges, bridge) + } + return nil +} + +func (h *HueBridge) Gather(acc telegraf.Accumulator) error { + var waitComplete sync.WaitGroup + for _, bridge := range h.configuredBridges { + waitComplete.Add(1) + go func() { + defer waitComplete.Done() + acc.AddError(bridge.process(acc)) + }() + } + waitComplete.Wait() + return nil +} + +func init() { + inputs.Add("huebridge", func() telegraf.Input { + return &HueBridge{Timeout: config.Duration(10 * time.Second)} + }) +} diff --git a/plugins/inputs/huebridge/huebridge_test.go b/plugins/inputs/huebridge/huebridge_test.go new file mode 100644 index 0000000000000..8a913fadf6885 --- /dev/null +++ b/plugins/inputs/huebridge/huebridge_test.go @@ -0,0 +1,126 @@ +package huebridge + +import ( + "fmt" + "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.Bridges, 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{ + Bridges: []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.configuredBridges, len(h.Bridges)) +} + +func TestInitIgnoreInvalidUrls(t *testing.T) { + // The following URLs are all invalid must all be ignored during Init + h := &HueBridge{ + Bridges: []string{ + "invalid://12345678:secret@invalid-scheme.net/", + "address://12345678@missing-password.net/", + "cloud://12345678@missing-password.net/", + "mdns://12345678@missing-password.net/", + "remote://12345678@missing-password.net/", + "remote://12345678:secret@missing-remote-config.net/", + }, + Timeout: config.Duration(10 * time.Second), + Log: &testutil.Logger{Name: "huebridge"}, + } + + // Verify successful Init + require.NoError(t, h.Init()) + + // Verify no bridge have been configured + require.Len(t, h.configuredBridges, 0) +} + +func TestGatherLocal(t *testing.T) { + // Start mock server and make plugin targing it + bridgeMock := mock.Start() + require.NotNil(t, bridgeMock) + defer bridgeMock.Shutdown() + h := &HueBridge{ + Bridges: []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"}, + } + + // Verify successful Init + require.NoError(t, h.Init()) + + // Verify successfull Gather + acc := &testutil.Accumulator{} + require.NoError(t, acc.GatherError(h.Gather)) + + // Verify collected metrics are as expected + expectedMetrics := loadExpectedMetrics(t, "testdata/metrics/huebridge.txt", telegraf.Gauge) + testutil.RequireMetricsEqual(t, expectedMetrics, acc.GetTelegrafMetrics(), testutil.IgnoreTime(), testutil.SortMetrics()) +} + +func loadExpectedMetrics(t *testing.T, file string, vt telegraf.ValueType) []telegraf.Metric { + parser := &influx.Parser{} + require.NoError(t, parser.Init()) + expectedMetrics, err := testutil.ParseMetricsFromFile(file, parser) + require.NoError(t, err) + for index := range expectedMetrics { + expectedMetrics[index].SetType(vt) + } + return expectedMetrics +} diff --git a/plugins/inputs/huebridge/sample.conf b/plugins/inputs/huebridge/sample.conf new file mode 100644 index 0000000000000..1e799e84ec0a9 --- /dev/null +++ b/plugins/inputs/huebridge/sample.conf @@ -0,0 +1,20 @@ +# Gather smart home status from Hue Bridge +[[inputs.huebridge]] + ## URL of bridges to query in the form ://:@
/ + ## See documentation for available schemes. + bridges = [ "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 \ No newline at end of file diff --git a/plugins/inputs/huebridge/testdata/conf/huebridge.conf b/plugins/inputs/huebridge/testdata/conf/huebridge.conf new file mode 100644 index 0000000000000..398fa739cb532 --- /dev/null +++ b/plugins/inputs/huebridge/testdata/conf/huebridge.conf @@ -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 \ No newline at end of file diff --git a/plugins/inputs/huebridge/testdata/metrics/huebridge.txt b/plugins/inputs/huebridge/testdata/metrics/huebridge.txt new file mode 100644 index 0000000000000..1399190568534 --- /dev/null +++ b/plugins/inputs/huebridge/testdata/metrics/huebridge.txt @@ -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