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

     1  package client
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"os"
     7  	"os/exec"
     8  	"strconv"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/go-audio/wav"
    13  	"github.com/google/uuid"
    14  	"github.com/nats-io/nats.go"
    15  	"github.com/simpleiot/simpleiot/data"
    16  )
    17  
    18  // Rule represent a rule node config
    19  type Rule struct {
    20  	ID              string      `node:"id"`
    21  	Parent          string      `node:"parent"`
    22  	Description     string      `point:"description"`
    23  	Disabled        bool        `point:"disabled"`
    24  	Active          bool        `point:"active"`
    25  	Error           string      `point:"error"`
    26  	Conditions      []Condition `child:"condition"`
    27  	Actions         []Action    `child:"action"`
    28  	ActionsInactive []Action    `child:"actionInactive"`
    29  }
    30  
    31  func (r Rule) String() string {
    32  	ret := fmt.Sprintf("Rule: %v\n", r.Description)
    33  	ret += fmt.Sprintf("  active: %v\n", r.Active)
    34  	ret += fmt.Sprintf("  Disabled: %v\n", r.Disabled)
    35  	for _, c := range r.Conditions {
    36  		ret += fmt.Sprintf("%v", c)
    37  	}
    38  	for _, a := range r.Actions {
    39  		ret += fmt.Sprintf("  ACTION: %v", a)
    40  	}
    41  
    42  	for _, a := range r.ActionsInactive {
    43  		ret += fmt.Sprintf("  ACTION Inactive: %v", a)
    44  	}
    45  
    46  	return ret
    47  }
    48  
    49  // Condition defines parameters to look for in a point or a schedule.
    50  type Condition struct {
    51  	// general parameters
    52  	ID            string  `node:"id"`
    53  	Parent        string  `node:"parent"`
    54  	Description   string  `point:"description"`
    55  	Disabled      bool    `point:"disabled"`
    56  	ConditionType string  `point:"conditionType"`
    57  	MinActive     float64 `point:"minActive"`
    58  	Active        bool    `point:"active"`
    59  	Error         string  `point:"error"`
    60  
    61  	// used with point value rules
    62  	NodeID     string  `point:"nodeID"`
    63  	PointType  string  `point:"pointType"`
    64  	PointKey   string  `point:"pointKey"`
    65  	PointIndex int     `point:"pointIndex"`
    66  	ValueType  string  `point:"valueType"`
    67  	Operator   string  `point:"operator"`
    68  	Value      float64 `point:"value"`
    69  	ValueText  string  `point:"valueText"`
    70  
    71  	// used with shedule rules
    72  	Start    string   `point:"start"`
    73  	End      string   `point:"end"`
    74  	Weekdays []bool   `point:"weekday"`
    75  	Dates    []string `point:"date"`
    76  }
    77  
    78  func (c Condition) String() string {
    79  	value := ""
    80  	switch c.ValueType {
    81  	case data.PointValueOnOff:
    82  		if c.Value == 0 {
    83  			value = "off"
    84  		} else {
    85  			value = "on"
    86  		}
    87  	case data.PointValueNumber:
    88  		value = strconv.FormatFloat(c.Value, 'f', 2, 64)
    89  	case data.PointValueText:
    90  		value = c.ValueText
    91  	}
    92  
    93  	var ret string
    94  
    95  	switch c.ConditionType {
    96  	case data.PointValuePointValue:
    97  		ret = fmt.Sprintf("  COND: %v  Disabled: %v CTYPE:%v  VTYPE:%v  V:%v",
    98  			c.Description, c.ConditionType, c.Disabled, c.ValueType, value)
    99  		if c.NodeID != "" {
   100  			ret += fmt.Sprintf("  NODEID:%v", c.NodeID)
   101  		}
   102  		if c.MinActive > 0 {
   103  			ret += fmt.Sprintf("  MINACT:%v", c.MinActive)
   104  		}
   105  		ret += fmt.Sprintf("  A:%v", c.Active)
   106  		ret += "\n"
   107  	case data.PointValueSchedule:
   108  		ret = fmt.Sprintf("  COND: %v  CTYPE:%v",
   109  			c.Description, c.ConditionType)
   110  		ret += fmt.Sprintf("  W:%v", c.Weekdays)
   111  		ret += fmt.Sprintf("  D:%v", c.Dates)
   112  		ret += "\n"
   113  
   114  	default:
   115  		ret = "Missing String case for condition"
   116  	}
   117  	return ret
   118  }
   119  
   120  // Action defines actions that can be taken if a rule is active.
   121  type Action struct {
   122  	ID          string `node:"id"`
   123  	Parent      string `node:"parent"`
   124  	Description string `point:"description"`
   125  	Disabled    bool   `point:"disabled"`
   126  	Active      bool   `point:"active"`
   127  	Error       string `point:"error"`
   128  	// Action: notify, setValue, playAudio
   129  	Action    string `point:"action"`
   130  	NodeID    string `point:"nodeID"`
   131  	PointType string `point:"pointType"`
   132  	PointKey  string `point:"pointKey"`
   133  	// PointType: number, text, onOff
   134  	ValueType string  `point:"valueType"`
   135  	Value     float64 `point:"value"`
   136  	ValueText string  `point:"valueText"`
   137  	// the following are used for audio playback
   138  	PointChannel  int    `point:"pointChannel"`
   139  	PointDevice   string `point:"pointDevice"`
   140  	PointFilePath string `point:"pointFilePath"`
   141  }
   142  
   143  func (a Action) String() string {
   144  	value := ""
   145  	switch a.ValueType {
   146  	case data.PointValueOnOff:
   147  		if a.Value == 0 {
   148  			value = "off"
   149  		} else {
   150  			value = "on"
   151  		}
   152  	case data.PointValueNumber:
   153  		value = strconv.FormatFloat(a.Value, 'f', 2, 64)
   154  	case data.PointValueText:
   155  		value = a.ValueText
   156  	}
   157  	ret := fmt.Sprintf("%v  Disabled:%v ACT:%v  VTYPE:%v  V:%v",
   158  		a.Description, a.Disabled, a.Action, a.ValueType, value)
   159  	if a.NodeID != "" {
   160  		ret += fmt.Sprintf("  NODEID:%v", a.NodeID)
   161  	}
   162  	if a.PointKey != "" && a.PointKey != "0" {
   163  		ret += fmt.Sprintf(" K:%v", a.PointKey)
   164  	}
   165  	ret += fmt.Sprintf("  A:%v", a.Active)
   166  	ret += "\n"
   167  	return ret
   168  }
   169  
   170  // ActionInactive defines actions that can be taken if a rule is inactive.
   171  // this is defined for use with the client.SendNodeType API
   172  type ActionInactive struct {
   173  	ID          string `node:"id"`
   174  	Parent      string `node:"parent"`
   175  	Description string `point:"description"`
   176  	Active      bool   `point:"active"`
   177  	// Action: notify, setValue, playAudio
   178  	Action    string `point:"action"`
   179  	NodeID    string `point:"nodeID"`
   180  	PointType string `point:"pointType"`
   181  	PointKey  string `point:"pointKey"`
   182  	// PointType: number, text, onOff
   183  	ValueType string  `point:"valueType"`
   184  	Value     float64 `point:"value"`
   185  	ValueText string  `point:"valueText"`
   186  	// the following are used for audio playback
   187  	PointChannel  int    `point:"pointChannel"`
   188  	PointDevice   string `point:"pointDevice"`
   189  	PointFilePath string `point:"pointFilePath"`
   190  }
   191  
   192  // RuleClient is a SIOT client used to run rules
   193  type RuleClient struct {
   194  	nc            *nats.Conn
   195  	config        Rule
   196  	stop          chan struct{}
   197  	newPoints     chan NewPoints
   198  	newEdgePoints chan NewPoints
   199  	newRulePoints chan NewPoints
   200  	upSub         *nats.Subscription
   201  }
   202  
   203  // NewRuleClient constructor ...
   204  func NewRuleClient(nc *nats.Conn, config Rule) Client {
   205  	return &RuleClient{
   206  		nc:            nc,
   207  		config:        config,
   208  		stop:          make(chan struct{}),
   209  		newPoints:     make(chan NewPoints),
   210  		newEdgePoints: make(chan NewPoints),
   211  		newRulePoints: make(chan NewPoints),
   212  	}
   213  }
   214  
   215  // Run runs the main logic for this client and blocks until stopped
   216  func (rc *RuleClient) Run() error {
   217  	// watch all points that flow through parent node
   218  	// TODO: we should optimize this so we only watch the nodes
   219  	// that are in the conditions
   220  	subject := fmt.Sprintf("up.%v.*", rc.config.Parent)
   221  
   222  	var err error
   223  	rc.upSub, err = rc.nc.Subscribe(subject, func(msg *nats.Msg) {
   224  		points, err := data.PbDecodePoints(msg.Data)
   225  		if err != nil {
   226  			log.Println("Error decoding points in rule upSub:", err)
   227  			return
   228  		}
   229  
   230  		// find node ID for points
   231  		chunks := strings.Split(msg.Subject, ".")
   232  		if len(chunks) != 3 {
   233  			log.Println("rule client up sub, malformed subject:", msg.Subject)
   234  			return
   235  		}
   236  
   237  		rc.newRulePoints <- NewPoints{chunks[2], "", points}
   238  	})
   239  
   240  	if err != nil {
   241  		return fmt.Errorf("Rule error subscribing to upsub: %v", err)
   242  	}
   243  
   244  	// TODO schedule ticker is a brute force way to do this
   245  	// we could optimize at some point by creating a timer to expire
   246  	// on the next schedule change
   247  	scheduleTickTime := time.Second * 10
   248  	scheduleTicker := time.NewTicker(scheduleTickTime)
   249  	if !rc.hasSchedule() {
   250  		scheduleTicker.Stop()
   251  	}
   252  
   253  	run := func(id string, pts data.Points) {
   254  		var active, changed bool
   255  		var err error
   256  
   257  		if rc.config.Disabled {
   258  			active = false
   259  		} else {
   260  			if len(pts) > 0 {
   261  				active, changed, err = rc.ruleProcessPoints(id, pts)
   262  				if err != nil {
   263  					log.Println("Error processing rule point:", err)
   264  				}
   265  
   266  				if !changed {
   267  					return
   268  				}
   269  			} else {
   270  				// send a schedule trigger through just in case someone changed a
   271  				// schedule condition
   272  				active, _, err = rc.ruleProcessPoints(rc.config.ID, data.Points{{
   273  					Time: time.Now(),
   274  					Type: data.PointTypeTrigger,
   275  				}})
   276  
   277  				if err != nil {
   278  					log.Println("Error processing rule point:", err)
   279  				}
   280  			}
   281  		}
   282  
   283  		if active {
   284  			err := rc.ruleRunActions(rc.config.Actions, id)
   285  			if err != nil {
   286  				log.Println("Error running rule actions:", err)
   287  			}
   288  
   289  			err = rc.ruleInactiveActions(rc.config.ActionsInactive)
   290  			if err != nil {
   291  				log.Println("Error running rule inactive actions:", err)
   292  			}
   293  		} else {
   294  			err := rc.ruleRunActions(rc.config.ActionsInactive, id)
   295  			if err != nil {
   296  				log.Println("Error running rule actions:", err)
   297  			}
   298  
   299  			err = rc.ruleInactiveActions(rc.config.Actions)
   300  			if err != nil {
   301  				log.Println("Error running rule inactive actions:", err)
   302  			}
   303  		}
   304  	}
   305  
   306  done:
   307  	for {
   308  		select {
   309  		case <-rc.stop:
   310  			break done
   311  		case pts := <-rc.newRulePoints:
   312  			// make sure the point is in a condition before we run the rule
   313  			// otherwise, we can get into a loop
   314  			found := false
   315  			for _, c := range rc.config.Conditions {
   316  				if c.ConditionType != data.PointValuePointValue {
   317  					continue
   318  				}
   319  				if c.NodeID == pts.ID {
   320  					found = true
   321  					break
   322  				}
   323  			}
   324  
   325  			if found {
   326  				// found a condition that matches the point coming in, run the rule
   327  				run(pts.ID, pts.Points)
   328  			}
   329  
   330  		case <-scheduleTicker.C:
   331  			run(rc.config.ID, data.Points{{
   332  				Time: time.Now(),
   333  				Type: data.PointTypeTrigger,
   334  			}})
   335  
   336  		case pts := <-rc.newPoints:
   337  			err := data.MergePoints(pts.ID, pts.Points, &rc.config)
   338  			if err != nil {
   339  				log.Println("error merging rule points:", err)
   340  			}
   341  
   342  			if rc.hasSchedule() {
   343  				scheduleTicker = time.NewTicker(scheduleTickTime)
   344  			} else {
   345  				scheduleTicker.Stop()
   346  			}
   347  
   348  			run("", nil)
   349  
   350  		case pts := <-rc.newEdgePoints:
   351  			err := data.MergeEdgePoints(pts.ID, pts.Parent, pts.Points, &rc.config)
   352  			if err != nil {
   353  				log.Println("error merging rule edge points:", err)
   354  			}
   355  
   356  			run("", nil)
   357  		}
   358  	}
   359  
   360  	return rc.upSub.Unsubscribe()
   361  }
   362  
   363  // Stop sends a signal to the Run function to exit
   364  func (rc *RuleClient) Stop(_ error) {
   365  	close(rc.stop)
   366  }
   367  
   368  // Points is called by the Manager when new points for this
   369  // node are received.
   370  func (rc *RuleClient) Points(nodeID string, points []data.Point) {
   371  	rc.newPoints <- NewPoints{nodeID, "", points}
   372  }
   373  
   374  // EdgePoints is called by the Manager when new edge points for this
   375  // node are received.
   376  func (rc *RuleClient) EdgePoints(nodeID, parentID string, points []data.Point) {
   377  	rc.newEdgePoints <- NewPoints{nodeID, parentID, points}
   378  }
   379  
   380  // sendPoint sets origin to the rule node
   381  func (rc *RuleClient) sendPoint(id string, point data.Point) error {
   382  	if id != rc.config.ID {
   383  		// we must set origin as we are sending a point to something
   384  		// other than the client root node
   385  		// TODO: it might be good to somehow move this into the
   386  		// client manager, so that clients don't need to worry about
   387  		// setting Origin
   388  		point.Origin = rc.config.ID
   389  	}
   390  	return SendNodePoint(rc.nc, id, point, false)
   391  }
   392  
   393  func (rc *RuleClient) hasSchedule() bool {
   394  	for _, c := range rc.config.Conditions {
   395  		if c.ConditionType == data.PointValueSchedule {
   396  			return true
   397  		}
   398  	}
   399  	return false
   400  }
   401  
   402  func (rc *RuleClient) processError(errS string) {
   403  	if errS != "" {
   404  		// always set rule error to the last error we encounter
   405  		if errS != rc.config.Error {
   406  			p := data.Point{
   407  				Type: data.PointTypeError,
   408  				Time: time.Now(),
   409  				Text: errS,
   410  			}
   411  
   412  			err := rc.sendPoint(rc.config.ID, p)
   413  			if err != nil {
   414  				log.Println("Rule error sending point:", err)
   415  			} else {
   416  				rc.config.Error = errS
   417  			}
   418  		}
   419  	} else {
   420  		// check if any other errors still exist
   421  		found := ""
   422  
   423  		for _, c := range rc.config.Conditions {
   424  			if c.Error != "" {
   425  				found = c.Error
   426  				break
   427  			}
   428  		}
   429  
   430  		for _, a := range rc.config.Actions {
   431  			if a.Error != "" {
   432  				found = a.Error
   433  				break
   434  			}
   435  		}
   436  
   437  		for _, a := range rc.config.ActionsInactive {
   438  			if a.Error != "" {
   439  				found = a.Error
   440  				break
   441  			}
   442  		}
   443  
   444  		if found != rc.config.Error {
   445  			p := data.Point{
   446  				Type: data.PointTypeError,
   447  				Time: time.Now(),
   448  				Text: found,
   449  			}
   450  
   451  			err := rc.sendPoint(rc.config.ID, p)
   452  			if err != nil {
   453  				log.Println("Rule error sending point:", err)
   454  			} else {
   455  				rc.config.Error = found
   456  			}
   457  		}
   458  	}
   459  }
   460  
   461  // ruleProcessPoints runs points through a rules conditions and and updates condition
   462  // and rule active status. Returns true if point was processed and active is true.
   463  // Currently, this function only processes the first point that matches -- this should
   464  // handle all current uses.
   465  func (rc *RuleClient) ruleProcessPoints(nodeID string, points data.Points) (bool, bool, error) {
   466  	for _, p := range points {
   467  		for i, c := range rc.config.Conditions {
   468  			var active bool
   469  			var errorActive bool
   470  
   471  			processError := func(err error) {
   472  				errorActive = true
   473  				errS := err.Error()
   474  				if c.Error != errS {
   475  					p := data.Point{
   476  						Type: data.PointTypeError,
   477  						Time: time.Now(),
   478  						Text: errS,
   479  					}
   480  
   481  					log.Printf("Rule cond error %v:%v:%v\n", rc.config.Description, c.Description, err)
   482  					err := rc.sendPoint(c.ID, p)
   483  					if err != nil {
   484  						log.Println("Rule error sending point:", err)
   485  					} else {
   486  						rc.config.Conditions[i].Error = errS
   487  					}
   488  				}
   489  				rc.processError(errS)
   490  			}
   491  
   492  			switch c.ConditionType {
   493  			case data.PointValuePointValue:
   494  				if c.NodeID != "" && c.NodeID != nodeID {
   495  					continue
   496  				}
   497  
   498  				if c.PointKey != "" && c.PointKey != p.Key {
   499  					continue
   500  				}
   501  
   502  				if c.PointType != "" && c.PointType != p.Type {
   503  					continue
   504  				}
   505  				// conditions match, so check value
   506  				switch c.ValueType {
   507  				case data.PointValueNumber:
   508  					switch c.Operator {
   509  					case data.PointValueGreaterThan:
   510  						active = p.Value > c.Value
   511  					case data.PointValueLessThan:
   512  						active = p.Value < c.Value
   513  					case data.PointValueEqual:
   514  						active = p.Value == c.Value
   515  					case data.PointValueNotEqual:
   516  						active = p.Value != c.Value
   517  					}
   518  				case data.PointValueText:
   519  					switch c.Operator {
   520  					case data.PointValueEqual:
   521  					case data.PointValueNotEqual:
   522  					case data.PointValueContains:
   523  					}
   524  				case data.PointValueOnOff:
   525  					condValue := c.Value != 0
   526  					pointValue := p.Value != 0
   527  					active = condValue == pointValue
   528  				default:
   529  					processError(fmt.Errorf("unknown value type: %v", c.ValueType))
   530  				}
   531  			case data.PointValueSchedule:
   532  				if p.Type != data.PointTypeTrigger {
   533  					continue
   534  				}
   535  
   536  				weekdays := []time.Weekday{}
   537  				for i, v := range c.Weekdays {
   538  					if v {
   539  						weekdays = append(weekdays, time.Weekday(i))
   540  					}
   541  				}
   542  				sched := newSchedule(c.Start, c.End, weekdays, c.Dates)
   543  
   544  				var err error
   545  				active, err = sched.activeForTime(p.Time)
   546  				if err != nil {
   547  					processError(fmt.Errorf("Error parsing schedule: %w", err))
   548  					continue
   549  				}
   550  			}
   551  
   552  			if active != c.Active {
   553  				// update condition
   554  				p := data.Point{
   555  					Type:  data.PointTypeActive,
   556  					Time:  time.Now(),
   557  					Value: data.BoolToFloat(active),
   558  				}
   559  
   560  				err := rc.sendPoint(c.ID, p)
   561  				if err != nil {
   562  					log.Println("Rule error sending point:", err)
   563  				}
   564  
   565  				rc.config.Conditions[i].Active = active
   566  			}
   567  
   568  			if !errorActive && c.Error != "" {
   569  				p := data.Point{
   570  					Type: data.PointTypeError,
   571  					Time: time.Now(),
   572  					Text: "",
   573  				}
   574  
   575  				err := rc.sendPoint(c.ID, p)
   576  				if err != nil {
   577  					log.Println("Rule error sending point:", err)
   578  				} else {
   579  					rc.config.Conditions[i].Error = ""
   580  				}
   581  				rc.processError("")
   582  			}
   583  		}
   584  	}
   585  
   586  	allActive := true
   587  	activeConditionCount := 0
   588  
   589  	for _, c := range rc.config.Conditions {
   590  		if !c.Active && !c.Disabled {
   591  			allActive = false
   592  			break
   593  		}
   594  		if c.Active && !c.Disabled {
   595  			activeConditionCount++
   596  		}
   597  	}
   598  
   599  	if activeConditionCount == 0 && allActive {
   600  		allActive = false
   601  	}
   602  
   603  	changed := false
   604  
   605  	if allActive != rc.config.Active {
   606  		p := data.Point{
   607  			Type:  data.PointTypeActive,
   608  			Time:  time.Now(),
   609  			Value: data.BoolToFloat(allActive),
   610  		}
   611  
   612  		err := rc.sendPoint(rc.config.ID, p)
   613  		if err != nil {
   614  			log.Println("Rule error sending point:", err)
   615  		}
   616  		changed = true
   617  
   618  		rc.config.Active = allActive
   619  	}
   620  
   621  	return allActive, changed, nil
   622  }
   623  
   624  // ruleRunActions runs rule actions
   625  func (rc *RuleClient) ruleRunActions(actions []Action, triggerNodeID string) error {
   626  	for i, a := range actions {
   627  		if a.Disabled {
   628  			continue
   629  		}
   630  
   631  		errorActive := false
   632  
   633  		processError := func(err error) {
   634  			errorActive = true
   635  			errS := err.Error()
   636  			if a.Error != errS {
   637  				p := data.Point{
   638  					Type: data.PointTypeError,
   639  					Time: time.Now(),
   640  					Text: errS,
   641  				}
   642  
   643  				log.Printf("Rule action error %v:%v:%v\n", rc.config.Description, a.Description, err)
   644  				err := rc.sendPoint(a.ID, p)
   645  				if err != nil {
   646  					log.Println("Rule error sending point:", err)
   647  				} else {
   648  					actions[i].Error = errS
   649  				}
   650  			}
   651  			rc.processError(errS)
   652  		}
   653  
   654  		switch a.Action {
   655  		case data.PointValueSetValue:
   656  			if a.NodeID == "" {
   657  				processError(fmt.Errorf("Error, node action nodeID must be set"))
   658  				break
   659  			}
   660  
   661  			if a.PointType == "" {
   662  				processError(fmt.Errorf("Error, node action point type must be set"))
   663  				break
   664  			}
   665  
   666  			p := data.Point{
   667  				Time:   time.Now(),
   668  				Type:   a.PointType,
   669  				Key:    a.PointKey,
   670  				Value:  a.Value,
   671  				Text:   a.ValueText,
   672  				Origin: a.ID,
   673  			}
   674  
   675  			err := rc.sendPoint(a.NodeID, p)
   676  			if err != nil {
   677  				log.Println("Error sending rule action point:", err)
   678  			}
   679  		case data.PointValueNotify:
   680  			// get node that fired the rule
   681  			nodes, err := GetNodes(rc.nc, "none", triggerNodeID, "", false)
   682  			if err != nil {
   683  				processError(err)
   684  				break
   685  			}
   686  
   687  			if len(nodes) < 1 {
   688  				processError(fmt.Errorf("trigger node not found"))
   689  				break
   690  			}
   691  
   692  			triggerNode := nodes[0]
   693  
   694  			triggerNodeDesc := triggerNode.Desc()
   695  
   696  			n := data.Notification{
   697  				ID:         uuid.New().String(),
   698  				SourceNode: a.NodeID,
   699  				Message:    rc.config.Description + " fired at " + triggerNodeDesc,
   700  			}
   701  
   702  			// TODO this notify code needs to be reworked
   703  			d, err := n.ToPb()
   704  
   705  			if err != nil {
   706  				return err
   707  			}
   708  
   709  			err = rc.nc.Publish("node."+rc.config.ID+".not", d)
   710  
   711  			if err != nil {
   712  				return err
   713  			}
   714  		case data.PointValuePlayAudio:
   715  			f, err := os.Open(a.PointFilePath)
   716  			if err != nil {
   717  				log.Fatal(err)
   718  			}
   719  			defer f.Close()
   720  
   721  			d := wav.NewDecoder(f)
   722  			d.ReadInfo()
   723  
   724  			format := d.Format()
   725  
   726  			if format.SampleRate < 8000 {
   727  				log.Println("Rule action: invalid wave file sample rate:", format.SampleRate)
   728  				continue
   729  			}
   730  
   731  			channelNum := strconv.Itoa(a.PointChannel)
   732  			sampleRate := strconv.Itoa(format.SampleRate)
   733  
   734  			go func() {
   735  				stderr, err := exec.Command("speaker-test", "-D"+a.PointDevice, "-twav", "-w"+a.PointFilePath, "-c5", "-s"+channelNum, "-r"+sampleRate).CombinedOutput()
   736  				if err != nil {
   737  					log.Println("Play audio error:", err)
   738  					log.Printf("Audio stderr: %s\n", stderr)
   739  				}
   740  			}()
   741  		default:
   742  			processError(fmt.Errorf("Uknown rule action: %v", a.Action))
   743  		}
   744  
   745  		p := data.Point{
   746  			Type:  data.PointTypeActive,
   747  			Value: 1,
   748  		}
   749  		err := rc.sendPoint(a.ID, p)
   750  		if err != nil {
   751  			log.Println("Error sending rule action point:", err)
   752  		}
   753  
   754  		actions[i].Active = true
   755  
   756  		if !errorActive && a.Error != "" {
   757  			p := data.Point{
   758  				Type: data.PointTypeError,
   759  				Time: time.Now(),
   760  				Text: "",
   761  			}
   762  
   763  			err := rc.sendPoint(a.ID, p)
   764  			if err != nil {
   765  				log.Println("Rule error sending point:", err)
   766  			} else {
   767  				actions[i].Error = ""
   768  			}
   769  			rc.processError("")
   770  		}
   771  
   772  	}
   773  	return nil
   774  }
   775  
   776  func (rc *RuleClient) ruleInactiveActions(actions []Action) error {
   777  	for i, a := range actions {
   778  		if a.Disabled {
   779  			continue
   780  		}
   781  
   782  		p := data.Point{
   783  			Type:  data.PointTypeActive,
   784  			Value: 0,
   785  		}
   786  		err := rc.sendPoint(a.ID, p)
   787  		if err != nil {
   788  			log.Println("Error sending rule action point:", err)
   789  		}
   790  		actions[i].Active = false
   791  	}
   792  	return nil
   793  }