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

     1  package client
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"time"
     7  
     8  	"github.com/nats-io/nats.go"
     9  	"github.com/simpleiot/simpleiot/data"
    10  	"golang.org/x/exp/constraints"
    11  )
    12  
    13  // ShellyIOClient is a SIOT particle client
    14  type ShellyIOClient struct {
    15  	nc              *nats.Conn
    16  	config          ShellyIo
    17  	points          data.Points
    18  	stop            chan struct{}
    19  	newPoints       chan NewPoints
    20  	newEdgePoints   chan NewPoints
    21  	newShellyPoints chan NewPoints
    22  	errorCount      int
    23  	comps           []shellyComp
    24  }
    25  
    26  // NewShellyIOClient ...
    27  func NewShellyIOClient(nc *nats.Conn, config ShellyIo) Client {
    28  	// we need a copy of points with timestamps so we know when to send up new data
    29  	ne, err := data.Encode(config)
    30  	if err != nil {
    31  		log.Println("Error encoding shelly config:", err)
    32  	}
    33  
    34  	return &ShellyIOClient{
    35  		nc:              nc,
    36  		config:          config,
    37  		comps:           shellyCompMap[config.Type],
    38  		points:          ne.Points,
    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 (sioc *ShellyIOClient) Run() error {
    48  	log.Println("Starting shelly IO client:", sioc.config.Description)
    49  
    50  	sampleRate := time.Second * 2
    51  	sampleRateOffline := time.Minute * 10
    52  
    53  	syncConfigTicker := time.NewTicker(sampleRateOffline)
    54  	sampleTicker := time.NewTicker(sampleRate)
    55  
    56  	if sioc.config.Offline {
    57  		sampleTicker = time.NewTicker(sampleRateOffline)
    58  	}
    59  
    60  	if sioc.config.Disabled {
    61  		sampleTicker.Stop()
    62  	}
    63  
    64  	shellyError := func() {
    65  		sioc.errorCount++
    66  		if !sioc.config.Offline && sioc.errorCount > 5 {
    67  			log.Printf("Shelly device %v is offline", sioc.config.Description)
    68  			sioc.config.Offline = true
    69  			err := SendNodePoint(sioc.nc, sioc.config.ID, data.Point{
    70  				Type: data.PointTypeOffline, Value: 1}, false)
    71  
    72  			if err != nil {
    73  				log.Println("ShellyIO: error sending node point:", err)
    74  			}
    75  			sampleTicker = time.NewTicker(sampleRateOffline)
    76  		}
    77  	}
    78  
    79  	shellyCommOK := func() {
    80  		sioc.errorCount = 0
    81  		if sioc.config.Offline {
    82  			log.Printf("Shelly device %v is online", sioc.config.Description)
    83  			sioc.config.Offline = false
    84  			err := SendNodePoint(sioc.nc, sioc.config.ID, data.Point{
    85  				Type: data.PointTypeOffline, Value: 0}, false)
    86  
    87  			if err != nil {
    88  				log.Println("ShellyIO: error sending node point:", err)
    89  			}
    90  			sampleTicker = time.NewTicker(sampleRate)
    91  		}
    92  	}
    93  
    94  	syncConfig := func() {
    95  		config, err := sioc.config.getConfig()
    96  		if err != nil {
    97  			shellyError()
    98  			log.Println("Error getting shelly IO settings:", sioc.config.Desc(), err)
    99  			return
   100  		}
   101  
   102  		shellyCommOK()
   103  
   104  		if sioc.config.Description == "" && config.Name != "" {
   105  			sioc.config.Description = config.Name
   106  			err := SendNodePoint(sioc.nc, sioc.config.ID, data.Point{
   107  				Type: data.PointTypeDescription, Text: config.Name}, false)
   108  			if err != nil {
   109  				log.Println("Error sending shelly io description:", err)
   110  			}
   111  		} else if sioc.config.Description != config.Name {
   112  			err := sioc.config.SetName(sioc.config.Description)
   113  			if err != nil {
   114  				log.Println("Error setting name on Shelly device:", err)
   115  			}
   116  		}
   117  	}
   118  
   119  	syncConfig()
   120  
   121  done:
   122  	for {
   123  		select {
   124  		case <-sioc.stop:
   125  			log.Println("Stopping shelly IO client:", sioc.config.Description)
   126  			break done
   127  		case pts := <-sioc.newPoints:
   128  			err := data.MergePoints(pts.ID, pts.Points, &sioc.config)
   129  			if err != nil {
   130  				log.Println("error merging new points:", err)
   131  			}
   132  
   133  			for _, p := range pts.Points {
   134  				switch p.Type {
   135  				case data.PointTypeDescription:
   136  					syncConfig()
   137  				case data.PointTypeDisabled:
   138  					if p.Value == 0 {
   139  						sampleTicker = time.NewTicker(sampleRate)
   140  					} else {
   141  						sampleTicker.Stop()
   142  					}
   143  				case data.PointTypeOffline:
   144  					if p.Value == 0 {
   145  						// defice is online
   146  						// the discovery mechanism may have set the IO back online
   147  						sampleTicker = time.NewTicker(sampleRate)
   148  					} else {
   149  						sampleTicker = time.NewTicker(sampleRateOffline)
   150  					}
   151  				}
   152  			}
   153  
   154  		case pts := <-sioc.newEdgePoints:
   155  			err := data.MergeEdgePoints(pts.ID, pts.Parent, pts.Points, &sioc.config)
   156  			if err != nil {
   157  				log.Println("error merging new points:", err)
   158  			}
   159  
   160  		case <-syncConfigTicker.C:
   161  			syncConfig()
   162  
   163  		case <-sampleTicker.C:
   164  			if sioc.config.Disabled {
   165  				fmt.Println("Shelly IO is disabled, why am I ticking?")
   166  				continue
   167  			}
   168  			points, err := sioc.config.GetStatus()
   169  			if err != nil {
   170  				log.Printf("Error getting status for %v: %v\n", sioc.config.Description, err)
   171  				shellyError()
   172  				break
   173  			}
   174  
   175  			if sioc.config.Controlled {
   176  				switchCount := min(len(sioc.config.Switch), len(sioc.config.SwitchSet))
   177  				for i := 0; i < switchCount; i++ {
   178  					if sioc.config.Switch[i] != sioc.config.SwitchSet[i] {
   179  						pts, err := sioc.config.SetOnOff("switch", i, sioc.config.SwitchSet[i])
   180  						if err != nil {
   181  							log.Printf("Error setting %v: %v\n", sioc.config.Description, err)
   182  						}
   183  
   184  						if len(pts) > 0 {
   185  							points = append(points, pts...)
   186  						} else {
   187  							// get current status as the set did not return status
   188  							points, err = sioc.config.GetStatus()
   189  							if err != nil {
   190  								log.Printf("Error getting status for %v: %v\n", sioc.config.Description, err)
   191  								shellyError()
   192  								break
   193  							}
   194  						}
   195  					}
   196  				}
   197  
   198  				lightCount := min(len(sioc.config.Light), len(sioc.config.LightSet))
   199  				for i := 0; i < lightCount; i++ {
   200  					if sioc.config.Light[i] != sioc.config.LightSet[i] {
   201  						pts, err := sioc.config.SetOnOff("light", i, sioc.config.LightSet[i])
   202  						if err != nil {
   203  							log.Printf("Error setting %v: %v\n", sioc.config.Description, err)
   204  						}
   205  
   206  						if len(pts) > 0 {
   207  							points = append(points, pts...)
   208  						} else {
   209  							// get current status as the set did not return status
   210  							points, err = sioc.config.GetStatus()
   211  							if err != nil {
   212  								log.Printf("Error getting status for %v: %v\n", sioc.config.Description, err)
   213  								shellyError()
   214  								break
   215  							}
   216  						}
   217  					}
   218  				}
   219  
   220  			}
   221  
   222  			shellyCommOK()
   223  
   224  			newPoints := sioc.points.Merge(points, time.Minute*15)
   225  			if len(newPoints) > 0 {
   226  				err := data.MergePoints(sioc.config.ID, newPoints, &sioc.config)
   227  				if err != nil {
   228  					log.Println("shelly io: error merging newPoints:", err)
   229  				}
   230  				err = SendNodePoints(sioc.nc, sioc.config.ID, newPoints, false)
   231  				if err != nil {
   232  					log.Println("shelly io: error sending newPoints:", err)
   233  				}
   234  			}
   235  		}
   236  	}
   237  
   238  	// clean up
   239  	return nil
   240  }
   241  
   242  // Stop sends a signal to the Run function to exit
   243  func (sioc *ShellyIOClient) Stop(_ error) {
   244  	close(sioc.stop)
   245  }
   246  
   247  // Points is called by the Manager when new points for this
   248  // node are received.
   249  func (sioc *ShellyIOClient) Points(nodeID string, points []data.Point) {
   250  	sioc.newPoints <- NewPoints{nodeID, "", points}
   251  }
   252  
   253  // EdgePoints is called by the Manager when new edge points for this
   254  // node are received.
   255  func (sioc *ShellyIOClient) EdgePoints(nodeID, parentID string, points []data.Point) {
   256  	sioc.newEdgePoints <- NewPoints{nodeID, parentID, points}
   257  }
   258  
   259  func min[T constraints.Ordered](a, b T) T {
   260  	if a < b {
   261  		return a
   262  	}
   263  	return b
   264  }