Skip to content

Commit

Permalink
Add input plugin for KNX home automation bus (influxdata#7048)
Browse files Browse the repository at this point in the history
  • Loading branch information
DocLambda authored Mar 18, 2021
1 parent 1746f96 commit 1eb47e2
Show file tree
Hide file tree
Showing 9 changed files with 432 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ For documentation on the latest development code see the [documentation index][d
* [kernel](./plugins/inputs/kernel)
* [kernel_vmstat](./plugins/inputs/kernel_vmstat)
* [kibana](./plugins/inputs/kibana)
* [knx_listener](./plugins/inputs/knx_listener)
* [kubernetes](./plugins/inputs/kubernetes)
* [kube_inventory](./plugins/inputs/kube_inventory)
* [lanz](./plugins/inputs/lanz)
Expand Down
1 change: 1 addition & 0 deletions docs/LICENSE_OF_DEPENDENCIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ following works:
- github.com/tidwall/match [MIT License](https://github.com/tidwall/match/blob/master/LICENSE)
- github.com/tidwall/pretty [MIT License](https://github.com/tidwall/pretty/blob/master/LICENSE)
- github.com/tinylib/msgp [MIT License](https://github.com/tinylib/msgp/blob/master/LICENSE)
- github.com/vapourismo/knx-go [MIT License](https://github.com/vapourismo/knx-go/blob/master/LICENSE)
- github.com/vishvananda/netlink [Apache License 2.0](https://github.com/vishvananda/netlink/blob/master/LICENSE)
- github.com/vishvananda/netns [Apache License 2.0](https://github.com/vishvananda/netns/blob/master/LICENSE)
- github.com/vjeantet/grok [Apache License 2.0](https://github.com/vjeantet/grok/blob/master/LICENSE)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ require (
github.com/tbrandon/mbserver v0.0.0-20170611213546-993e1772cc62
github.com/tidwall/gjson v1.6.0
github.com/tinylib/msgp v1.1.5
github.com/vapourismo/knx-go v0.0.0-20201122213738-75fe09ace330
github.com/vishvananda/netlink v0.0.0-20171020171820-b2de5d10e38e // indirect
github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc // indirect
github.com/vjeantet/grok v1.0.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,8 @@ github.com/tinylib/msgp v1.1.5 h1:2gXmtWueD2HefZHQe1QOy9HVzmFrLOVvsXwXBQ0ayy0=
github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg=
github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/vapourismo/knx-go v0.0.0-20201122213738-75fe09ace330 h1:iBlTJosRsR70amr0zsmSPvaKNH8K/p3YlX/5SdPmSl8=
github.com/vapourismo/knx-go v0.0.0-20201122213738-75fe09ace330/go.mod h1:7+aWBsUJCo9OQRCgTypRmIQW9KKKcPMjtrdnYIBsS70=
github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec/go.mod h1:owBmyHYMLkxyrugmfwE/DLJyW8Ro9mkphwuVErQ0iUw=
github.com/vishvananda/netlink v0.0.0-20171020171820-b2de5d10e38e h1:f1yevOHP+Suqk0rVc13fIkzcLULJbyQcXDba2klljD0=
github.com/vishvananda/netlink v0.0.0-20171020171820-b2de5d10e38e/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
Expand Down
1 change: 1 addition & 0 deletions plugins/inputs/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ import (
_ "github.com/influxdata/telegraf/plugins/inputs/kernel_vmstat"
_ "github.com/influxdata/telegraf/plugins/inputs/kibana"
_ "github.com/influxdata/telegraf/plugins/inputs/kinesis_consumer"
_ "github.com/influxdata/telegraf/plugins/inputs/knx_listener"
_ "github.com/influxdata/telegraf/plugins/inputs/kube_inventory"
_ "github.com/influxdata/telegraf/plugins/inputs/kubernetes"
_ "github.com/influxdata/telegraf/plugins/inputs/lanz"
Expand Down
66 changes: 66 additions & 0 deletions plugins/inputs/knx_listener/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# KNX input plugin

The KNX input plugin that listens for messages on the KNX home-automation bus.
This plugin connects to the KNX bus via a KNX-IP interface.
Information about supported KNX message datapoint types can be found at the
underlying "knx-go" project site (https://github.com/vapourismo/knx-go).

### Configuration

This is a sample config for the plugin.

```toml
# Listener capable of handling KNX bus messages provided through a KNX-IP Interface.
[[inputs.KNXListener]]
## Type of KNX-IP interface.
## Can be either "tunnel" or "router".
# service_type = "tunnel"

## Address of the KNX-IP interface.
service_address = "localhost:3671"

## Measurement definition(s)
# [[inputs.KNXListener.measurement]]
# ## Name of the measurement
# name = "temperature"
# ## Datapoint-Type (DPT) of the KNX messages
# dpt = "9.001"
# ## List of Group-Addresses (GAs) assigned to the measurement
# addresses = ["5/5/1"]

# [[inputs.KNXListener.measurement]]
# name = "illumination"
# dpt = "9.004"
# addresses = ["5/5/3"]
```

#### Measurement configurations

Each measurement contains only one datapoint-type (DPT) and assigns a list of
addresses to this measurement. You can, for example group all temperature sensor
messages within a "temperature" measurement. However, you are free to split
messages of one datapoint-type to multiple measurements.

**NOTE: You should not assign a group-address (GA) to multiple measurements!**

### Metrics

Received KNX data is stored in the named measurement as configured above using
the "value" field. Additional to the value, there are the following tags added
to the datapoint:
- "groupaddress": KNX group-address corresponding to the value
- "unit": unit of the value
- "source": KNX physical address sending the value

To find out about the datatype of the datapoint please check your KNX project,
the KNX-specification or the "knx-go" project for the corresponding DPT.

### Example Output

This section shows example output in Line Protocol format.

```
illumination,groupaddress=5/5/4,host=Hugin,source=1.1.12,unit=lux value=17.889999389648438 1582132674999013274
temperature,groupaddress=5/5/1,host=Hugin,source=1.1.8,unit=°C value=17.799999237060547 1582132663427587361
windowopen,groupaddress=1/0/1,host=Hugin,source=1.1.3 value=true 1582132630425581320
```
28 changes: 28 additions & 0 deletions plugins/inputs/knx_listener/knx_dummy_interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package knx_listener

import (
"github.com/vapourismo/knx-go/knx"
)

type KNXDummyInterface struct {
inbound chan knx.GroupEvent
}

func NewDummyInterface() (di KNXDummyInterface, err error) {
di, err = KNXDummyInterface{}, nil
di.inbound = make(chan knx.GroupEvent)

return di, err
}

func (di *KNXDummyInterface) Send(event knx.GroupEvent) {
di.inbound <- event
}

func (di *KNXDummyInterface) Inbound() <-chan knx.GroupEvent {
return di.inbound
}

func (di *KNXDummyInterface) Close() {
close(di.inbound)
}
197 changes: 197 additions & 0 deletions plugins/inputs/knx_listener/knx_listener.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package knx_listener

import (
"fmt"
"reflect"
"sync"

"github.com/vapourismo/knx-go/knx"
"github.com/vapourismo/knx-go/knx/dpt"

"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/plugins/inputs"
)

type KNXInterface interface {
Inbound() <-chan knx.GroupEvent
Close()
}

type addressTarget struct {
measurement string
datapoint dpt.DatapointValue
}

type Measurement struct {
Name string
Dpt string
Addresses []string
}

type KNXListener struct {
ServiceType string `toml:"service_type"`
ServiceAddress string `toml:"service_address"`
Measurements []Measurement `toml:"measurement"`
Log telegraf.Logger `toml:"-"`

client KNXInterface
gaTargetMap map[string]addressTarget
gaLogbook map[string]bool

acc telegraf.Accumulator
wg sync.WaitGroup
}

func (kl *KNXListener) Description() string {
return "Listener capable of handling KNX bus messages provided through a KNX-IP Interface."
}

func (kl *KNXListener) SampleConfig() string {
return `
## Type of KNX-IP interface.
## Can be either "tunnel" or "router".
# service_type = "tunnel"
## Address of the KNX-IP interface.
service_address = "localhost:3671"
## Measurement definition(s)
# [[inputs.KNXListener.measurement]]
# ## Name of the measurement
# name = "temperature"
# ## Datapoint-Type (DPT) of the KNX messages
# dpt = "9.001"
# ## List of Group-Addresses (GAs) assigned to the measurement
# addresses = ["5/5/1"]
# [[inputs.KNXListener.measurement]]
# name = "illumination"
# dpt = "9.004"
# addresses = ["5/5/3"]
`
}

func (kl *KNXListener) Gather(_ telegraf.Accumulator) error {
return nil
}

func (kl *KNXListener) Start(acc telegraf.Accumulator) error {
// Store the accumulator for later use
kl.acc = acc

// Setup a logbook to track unknown GAs to avoid log-spamming
kl.gaLogbook = make(map[string]bool)

// Construct the mapping of Group-addresses (GAs) to DPTs and the name
// of the measurement
kl.gaTargetMap = make(map[string]addressTarget)
for _, m := range kl.Measurements {
kl.Log.Debugf("Group-address mapping for measurement %q:", m.Name)
for _, ga := range m.Addresses {
kl.Log.Debugf(" %s --> %s", ga, m.Dpt)
if _, ok := kl.gaTargetMap[ga]; ok {
return fmt.Errorf("duplicate specification of address %q", ga)
}
d, ok := dpt.Produce(m.Dpt)
if !ok {
return fmt.Errorf("cannot create datapoint-type %q for address %q", m.Dpt, ga)
}
kl.gaTargetMap[ga] = addressTarget{m.Name, d}
}
}

// Connect to the KNX-IP interface
kl.Log.Infof("Trying to connect to %q at %q", kl.ServiceType, kl.ServiceAddress)
switch kl.ServiceType {
case "tunnel":
c, err := knx.NewGroupTunnel(kl.ServiceAddress, knx.DefaultTunnelConfig)
if err != nil {
return err
}
kl.client = &c
case "router":
c, err := knx.NewGroupRouter(kl.ServiceAddress, knx.DefaultRouterConfig)
if err != nil {
return err
}
kl.client = &c
case "dummy":
c, err := NewDummyInterface()
if err != nil {
return err
}
kl.client = &c
default:
return fmt.Errorf("invalid interface type: %s", kl.ServiceAddress)
}
kl.Log.Infof("Connected!")

// Listen to the KNX bus
kl.wg.Add(1)
go func() {
kl.wg.Done()
kl.listen()
}()

return nil
}

func (kl *KNXListener) Stop() {
if kl.client != nil {
kl.client.Close()
kl.wg.Wait()
}
}

func (kl *KNXListener) listen() {
for msg := range kl.client.Inbound() {
// Match GA to DataPointType and measurement name
ga := msg.Destination.String()
target, ok := kl.gaTargetMap[ga]
if !ok && !kl.gaLogbook[ga] {
kl.Log.Infof("Ignoring message %+v for unknown GA %q", msg, ga)
kl.gaLogbook[ga] = true
continue
}

// Extract the value from the data-frame
err := target.datapoint.Unpack(msg.Data)
if err != nil {
kl.Log.Errorf("Unpacking data failed: %v", err)
continue
}
kl.Log.Debugf("Matched GA %q to measurement %q with value %v", ga, target.measurement, target.datapoint)

// Convert the DatapointValue interface back to its basic type again
// as otherwise telegraf will not push out the metrics and eat it
// silently.
var value interface{}
vi := reflect.Indirect(reflect.ValueOf(target.datapoint))
switch vi.Kind() {
case reflect.Bool:
value = vi.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
value = vi.Int()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
value = vi.Uint()
case reflect.Float32, reflect.Float64:
value = vi.Float()
default:
kl.Log.Errorf("Type conversion %v failed for address %q", vi.Kind(), ga)
continue
}

// Compose the actual data to be pushed out
fields := map[string]interface{}{"value": value}
tags := map[string]string{
"groupaddress": ga,
"unit": target.datapoint.(dpt.DatapointMeta).Unit(),
"source": msg.Source.String(),
}
kl.acc.AddFields(target.measurement, fields, tags)
}
}

func init() {
inputs.Add("KNXListener", func() telegraf.Input { return &KNXListener{ServiceType: "tunnel"} })
}
Loading

0 comments on commit 1eb47e2

Please sign in to comment.