github.com/stampzilla/stampzilla-go@v2.0.0-rc9+incompatible/nodes/stampzilla-knx/tunnel.go (about)

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"math"
     6  	"sync"
     7  	"time"
     8  
     9  	"github.com/sirupsen/logrus"
    10  	"github.com/stampzilla/stampzilla-go/nodes/stampzilla-server/models/devices"
    11  	"github.com/stampzilla/stampzilla-go/pkg/node"
    12  	"github.com/vapourismo/knx-go/knx"
    13  	"github.com/vapourismo/knx-go/knx/cemi"
    14  	"github.com/vapourismo/knx-go/knx/dpt"
    15  )
    16  
    17  type tunnel struct {
    18  	Node      *node.Node
    19  	Address   string
    20  	Connected bool
    21  	Client    *knx.GroupTunnel
    22  
    23  	Groups map[string][]groupLink
    24  
    25  	OnConnect    func()
    26  	OnDisconnect func()
    27  
    28  	reconnect bool
    29  	wg        sync.WaitGroup
    30  	sync.RWMutex
    31  }
    32  
    33  type groupLink struct {
    34  	Name   string
    35  	Type   string
    36  	Device *devices.Device
    37  }
    38  
    39  func newTunnel(node *node.Node) *tunnel {
    40  	return &tunnel{
    41  		Node:      node,
    42  		Groups:    make(map[string][]groupLink),
    43  		reconnect: true,
    44  	}
    45  }
    46  
    47  func (tunnel *tunnel) SetAddress(address string) {
    48  	if address == tunnel.Address {
    49  		return
    50  	}
    51  
    52  	for {
    53  		err := tunnel.Connect(address)
    54  
    55  		if err == nil {
    56  			return
    57  		}
    58  
    59  		logrus.WithFields(logrus.Fields{
    60  			"address": address,
    61  			"error":   err,
    62  		}).Error("Failed to open KNX tunnel")
    63  
    64  		if !tunnel.reconnect {
    65  			return
    66  		}
    67  		logrus.Warn("Going to try again in 10s")
    68  		<-time.After(time.Second * 10)
    69  	}
    70  }
    71  
    72  func (tunnel *tunnel) Connect(address string) error {
    73  	// Connect to the gateway
    74  	client, err := knx.NewGroupTunnel(address, knx.DefaultTunnelConfig)
    75  	if err != nil {
    76  		return err
    77  	}
    78  
    79  	// Disconnect the previous tunnel
    80  	if tunnel.Client != nil {
    81  		tunnel.reconnect = false
    82  		tunnel.Client.Close()
    83  		tunnel.wg.Wait()
    84  		tunnel.reconnect = true
    85  	}
    86  
    87  	// Start using the new one
    88  	go func() {
    89  		tunnel.wg.Add(1)
    90  		tunnel.Address = address
    91  		tunnel.Client = &client
    92  		tunnel.onConnect()
    93  
    94  		defer func() {
    95  			tunnel.onDisconnect()
    96  			tunnel.Client = nil
    97  			client.Close()
    98  			tunnel.wg.Done()
    99  			logrus.Warn("Disconnect from KNX gateway done")
   100  
   101  			if tunnel.reconnect {
   102  				logrus.Warn("Going to try again in 10s")
   103  				<-time.After(time.Second * 10)
   104  				go tunnel.Connect(address)
   105  			}
   106  		}()
   107  
   108  		for msg := range client.Inbound() {
   109  			err := tunnel.decodeKNX(msg)
   110  			if err != nil {
   111  				logrus.WithFields(logrus.Fields{
   112  					"dest":    msg.Destination.String(),
   113  					"error":   err,
   114  					"message": fmt.Sprintf("%+v", msg),
   115  				}).Warn("Failed to handle message")
   116  			}
   117  		}
   118  	}()
   119  
   120  	return nil
   121  }
   122  
   123  func (tunnel *tunnel) onConnect() {
   124  	logrus.Info("Connected to KNX gateway")
   125  	tunnel.Connected = true
   126  	// Trigger a read on each group address that we monitor
   127  	tunnel.RLock()
   128  	for ga, _ := range tunnel.Groups {
   129  		tunnel.triggerRead(ga)
   130  	}
   131  	tunnel.RUnlock()
   132  	tunnel.OnConnect()
   133  }
   134  func (tunnel *tunnel) onDisconnect() {
   135  	logrus.Warn("Disconnected from KNX gateway")
   136  	tunnel.Connected = false
   137  	tunnel.OnDisconnect()
   138  }
   139  
   140  func (tunnel *tunnel) triggerRead(ga string) {
   141  	if !tunnel.Connected { // Dont try to send if we are not connected
   142  		return
   143  	}
   144  
   145  	addr, err := cemi.NewGroupAddrString(ga)
   146  	if err != nil {
   147  		logrus.WithFields(logrus.Fields{
   148  			"group_address": ga,
   149  			"error":         err,
   150  		}).Error("Failed to read group address")
   151  	}
   152  
   153  	err = tunnel.Client.Send(knx.GroupEvent{
   154  		Command:     knx.GroupRead,
   155  		Destination: addr,
   156  	})
   157  	if err != nil {
   158  		logrus.WithFields(logrus.Fields{
   159  			"group_address": ga,
   160  			"error":         err,
   161  		}).Error("Failed to send read request")
   162  	}
   163  	logrus.WithFields(logrus.Fields{
   164  		"group_address": ga,
   165  	}).Info("Sent read request")
   166  
   167  	<-time.After(time.Millisecond * 200)
   168  }
   169  
   170  func (tunnel *tunnel) decodeKNX(msg knx.GroupEvent) error {
   171  	tunnel.RLock()
   172  	defer tunnel.RUnlock()
   173  
   174  	tunnel.RLock()
   175  	links, ok := tunnel.Groups[msg.Destination.String()]
   176  	if !ok {
   177  		return fmt.Errorf("No link was found for: %s", msg.Destination.String())
   178  	}
   179  	tunnel.RUnlock()
   180  
   181  	for _, gl := range links {
   182  		logrus.WithFields(logrus.Fields{
   183  			"dest":     msg.Destination.String(),
   184  			"name":     gl.Name,
   185  			"deviceId": gl.Device.ID,
   186  		}).Trace("Found link")
   187  
   188  		var value interface{}
   189  		var err error
   190  		switch gl.Type {
   191  		case "bool":
   192  			value = new(dpt.DPT_1001)
   193  		case "temperature":
   194  			value = new(dpt.DPT_9001) //2 bytes floating point
   195  		case "lux":
   196  			value = new(dpt.DPT_9004) //2 bytes floating point
   197  		case "humidity":
   198  			value = new(dpt.DPT_9001) //2 bytes floating point
   199  		}
   200  
   201  		if dptv, ok := value.(dpt.DatapointValue); ok {
   202  			err = dptv.Unpack(msg.Data)
   203  			if err != nil {
   204  				return fmt.Errorf("Failed to unpack: %s", err.Error())
   205  			}
   206  
   207  			gl.Device.State[gl.Name] = dptv
   208  			gl.Device.Online = true
   209  
   210  			//If temperature and relative humidity is known, calculate absolute humidity
   211  			dh, okh := gl.Device.State["humidity"]
   212  			dt, okt := gl.Device.State["temperature"]
   213  			if okh && okt {
   214  				srh, _ := dh.(dpt.DPT_9001)
   215  				st, _ := dt.(dpt.DPT_9001)
   216  
   217  				//pws := 6.116441 * 10 * ((7.591386 - t) / (t + 240.7263))
   218  				//pw := pws * h / 100
   219  				//a := 2.116679 * pw / (273.15 + t)
   220  				//logrus.Warnf("absolute humidity %.2f", a)
   221  				////gl.Device.State["absolute_humidity"] = a
   222  
   223  				t := float64(st)
   224  				rh := float64(srh)
   225  				mw := 18.01534                                                           // molar mass of water g/mol
   226  				r := 8.31447215                                                          // Universal gas constant J/mol/K
   227  				ah := (6.112 * math.Exp((17.67*t)/(t+243.5)) * rh * mw) / (273.15 + t*r) // in grams/m^3
   228  				logrus.Warnf("Got temp %s (%#v) and humid %s (%#v) %#v -> %f", st, dt, srh, dh, gl.Device.State, ah)
   229  			}
   230  
   231  			tunnel.Node.AddOrUpdate(gl.Device)
   232  		} else {
   233  			return fmt.Errorf("Unsupported type: %s", gl.Type)
   234  		}
   235  	}
   236  	return nil
   237  }
   238  
   239  func (tunnel *tunnel) ClearAllLinks() {
   240  	tunnel.Lock()
   241  	tunnel.Groups = make(map[string][]groupLink)
   242  	tunnel.Unlock()
   243  }
   244  
   245  func (tunnel *tunnel) AddLink(ga string, n string, t string, d *devices.Device) {
   246  	logrus.WithFields(logrus.Fields{
   247  		"dest": ga,
   248  		"name": n,
   249  		"to":   d.ID,
   250  	}).Tracef("Add link")
   251  
   252  	tunnel.Lock()
   253  	if _, ok := tunnel.Groups[ga]; !ok {
   254  		tunnel.Groups[ga] = []groupLink{}
   255  	}
   256  
   257  	tunnel.Groups[ga] = append(tunnel.Groups[ga], groupLink{
   258  		Name:   n,
   259  		Type:   t,
   260  		Device: d,
   261  	})
   262  	tunnel.Unlock()
   263  
   264  	d.State[n] = nil
   265  
   266  	// Trigger a read of the group
   267  	if tunnel.Connected {
   268  		tunnel.triggerRead(ga)
   269  	}
   270  }
   271  
   272  func (tunnel *tunnel) Close() {
   273  	if tunnel.Client != nil {
   274  		tunnel.Client.Close()
   275  		tunnel.wg.Wait()
   276  	}
   277  }