github.com/simpleiot/simpleiot@v0.18.3/client/shelly.go (about)

     1  package client
     2  
     3  import (
     4  	"log"
     5  	"regexp"
     6  	"strconv"
     7  	"time"
     8  
     9  	"github.com/google/uuid"
    10  	"github.com/nats-io/nats.go"
    11  	"github.com/simpleiot/mdns"
    12  	"github.com/simpleiot/simpleiot/data"
    13  )
    14  
    15  // Shelly describes the shelly client config
    16  type Shelly struct {
    17  	ID          string     `node:"id"`
    18  	Parent      string     `node:"parent"`
    19  	Description string     `point:"description"`
    20  	Disabled    bool       `point:"disabled"`
    21  	IOs         []ShellyIo `child:"shellyIo"`
    22  }
    23  
    24  // ShellyClient is a SIOT particle client
    25  type ShellyClient struct {
    26  	nc              *nats.Conn
    27  	config          Shelly
    28  	stop            chan struct{}
    29  	newPoints       chan NewPoints
    30  	newEdgePoints   chan NewPoints
    31  	newShellyPoints chan NewPoints
    32  }
    33  
    34  // NewShellyClient ...
    35  func NewShellyClient(nc *nats.Conn, config Shelly) Client {
    36  	return &ShellyClient{
    37  		nc:              nc,
    38  		config:          config,
    39  		stop:            make(chan struct{}),
    40  		newPoints:       make(chan NewPoints),
    41  		newEdgePoints:   make(chan NewPoints),
    42  		newShellyPoints: make(chan NewPoints),
    43  	}
    44  }
    45  
    46  // Run runs the main logic for this client and blocks until stopped
    47  func (sc *ShellyClient) Run() error {
    48  	log.Println("Starting shelly client:", sc.config.Description)
    49  
    50  	entriesCh := make(chan *mdns.ServiceEntry, 4)
    51  
    52  	params := mdns.DefaultParams("_http._tcp")
    53  	params.DisableIPv6 = true
    54  	params.Entries = entriesCh
    55  
    56  	scan := func() {
    57  		err := mdns.Query(params)
    58  		if err != nil {
    59  			log.Println("mdns error:", err)
    60  		}
    61  	}
    62  
    63  	go scan()
    64  
    65  	scanTicker := time.NewTicker(time.Minute * 1)
    66  
    67  done:
    68  	for {
    69  		select {
    70  		case <-sc.stop:
    71  			log.Println("Stopping shelly client:", sc.config.Description)
    72  			break done
    73  		case pts := <-sc.newPoints:
    74  			err := data.MergePoints(pts.ID, pts.Points, &sc.config)
    75  			if err != nil {
    76  				log.Println("error merging new points:", err)
    77  			}
    78  
    79  			for _, p := range pts.Points {
    80  				switch p.Type {
    81  				case data.PointTypeDisabled:
    82  				}
    83  			}
    84  
    85  		case pts := <-sc.newEdgePoints:
    86  			err := data.MergeEdgePoints(pts.ID, pts.Parent, pts.Points, &sc.config)
    87  			if err != nil {
    88  				log.Println("error merging new points:", err)
    89  			}
    90  
    91  		case <-scanTicker.C:
    92  			go scan()
    93  
    94  		case e := <-entriesCh:
    95  			typ, id := shellyScanHost(e.Host)
    96  			if len(typ) > 0 {
    97  				found := false
    98  
    99  				var ip string
   100  				if e.AddrV4 != nil {
   101  					ip = e.AddrV4.String()
   102  				} else if e.AddrV6 != nil {
   103  					ip = e.AddrV6.String()
   104  				}
   105  
   106  				for i, io := range sc.config.IOs {
   107  					if io.DeviceID == id {
   108  						// already have this one
   109  						// must set Origin because we are sending a point to another node
   110  						// if we don't set origin, then the client manager will filter out
   111  						// points to the client that owns the node
   112  						found = true
   113  						if io.IP != ip {
   114  							err := SendNodePoint(sc.nc, io.ID, data.Point{
   115  								Type:   data.PointTypeIP,
   116  								Text:   ip,
   117  								Origin: sc.config.ID,
   118  							}, false)
   119  
   120  							if err != nil {
   121  								log.Println("Error setting io ip:", err)
   122  							}
   123  						}
   124  
   125  						if io.Offline {
   126  							err := SendNodePoint(sc.nc, io.ID, data.Point{
   127  								Type:   data.PointTypeOffline,
   128  								Value:  0,
   129  								Origin: sc.config.ID,
   130  							}, false)
   131  
   132  							if err != nil {
   133  								log.Println("Error setting io offline:", err)
   134  							} else {
   135  								sc.config.IOs[i].Offline = false
   136  							}
   137  						}
   138  						break
   139  					}
   140  				}
   141  				if found {
   142  					break
   143  				}
   144  
   145  				newIO := ShellyIo{
   146  					ID:       uuid.New().String(),
   147  					DeviceID: id,
   148  					Parent:   sc.config.ID,
   149  					Type:     typ,
   150  					IP:       ip,
   151  				}
   152  
   153  				ne, err := data.Encode(newIO)
   154  				if err != nil {
   155  					log.Println("Error encoding new shelly IO:", err)
   156  					continue
   157  				}
   158  
   159  				addCompPoints := func(pType string, count int) {
   160  					for i := 0; i < count; i++ {
   161  						iString := strconv.Itoa(i)
   162  						ne.Points = append(ne.Points, data.Point{Type: pType, Key: iString})
   163  					}
   164  				}
   165  
   166  				for _, comp := range shellyCompMap[typ] {
   167  					switch comp.name {
   168  					case "input":
   169  						addCompPoints(data.PointTypeInput, comp.count)
   170  					case "switch":
   171  						addCompPoints(data.PointTypeSwitch, comp.count)
   172  						addCompPoints(data.PointTypeSwitchSet, comp.count)
   173  					case "light":
   174  						addCompPoints(data.PointTypeLight, comp.count)
   175  						addCompPoints(data.PointTypeLightSet, comp.count)
   176  					}
   177  				}
   178  
   179  				err = SendNode(sc.nc, ne, sc.config.ID)
   180  				if err != nil {
   181  					log.Println("Error sending shelly IO:", err)
   182  				}
   183  			}
   184  		}
   185  	}
   186  
   187  	// clean up
   188  	scanTicker.Stop()
   189  	return nil
   190  }
   191  
   192  // Stop sends a signal to the Run function to exit
   193  func (sc *ShellyClient) Stop(_ error) {
   194  	close(sc.stop)
   195  }
   196  
   197  // Points is called by the Manager when new points for this
   198  // node are received.
   199  func (sc *ShellyClient) Points(nodeID string, points []data.Point) {
   200  	sc.newPoints <- NewPoints{nodeID, "", points}
   201  }
   202  
   203  // EdgePoints is called by the Manager when new edge points for this
   204  // node are received.
   205  func (sc *ShellyClient) EdgePoints(nodeID, parentID string, points []data.Point) {
   206  	sc.newEdgePoints <- NewPoints{nodeID, parentID, points}
   207  }
   208  
   209  var reShellyHost = regexp.MustCompile("(?i)shelly(.*)-(.*).local")
   210  
   211  func shellyScanHost(host string) (string, string) {
   212  	m := reShellyHost.FindStringSubmatch(host)
   213  	if len(m) < 3 {
   214  		return "", ""
   215  	}
   216  
   217  	return m[1], m[2]
   218  }