You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
281 lines
7.1 KiB
281 lines
7.1 KiB
package models
|
|
|
|
import (
|
|
"context"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type EventHandler struct {
|
|
ID int `json:"id"`
|
|
EventName string `json:"eventName"`
|
|
Conditions map[string]EventCondition `json:"conditions"`
|
|
OneShot bool `json:"oneShot"`
|
|
Priority int `json:"priority"`
|
|
TargetKind ReferenceKind `json:"targetKind"`
|
|
TargetValue string `json:"targetValue"`
|
|
Actions EventAction `json:"actions"`
|
|
From TimeOfDay `json:"from"`
|
|
To TimeOfDay `json:"to"`
|
|
}
|
|
|
|
type EventHandlerRepository interface {
|
|
FindByID(ctx context.Context, id int) (*EventHandler, error)
|
|
FetchAll(ctx context.Context) ([]EventHandler, error)
|
|
Save(ctx context.Context, handler *EventHandler) error
|
|
Delete(ctx context.Context, handler *EventHandler) error
|
|
}
|
|
|
|
type EventCondition struct {
|
|
EQ string `json:"eq,omitempty"`
|
|
GT string `json:"gt,omitempty"`
|
|
GTE string `json:"gte,omitempty"`
|
|
LT string `json:"lt,omitempty"`
|
|
LTE string `json:"lte,omitempty"`
|
|
}
|
|
|
|
type EventHandlerUpdate struct {
|
|
SetEventName *string `json:"setEventName"`
|
|
SetPriority *int `json:"setPriority"`
|
|
SetTargetKind *ReferenceKind `json:"setTargetKind"`
|
|
SetTargetValue *string `json:"setTargetValue"`
|
|
SetOneShot *bool `json:"setOneShot"`
|
|
SetConditions map[string]EventCondition `json:"setConditions"`
|
|
PatchConditions map[string]*EventCondition `json:"patchConditions"`
|
|
SetActions *EventAction `json:"setActions"`
|
|
SetFrom *TimeOfDay `json:"setFrom"`
|
|
SetTo *TimeOfDay `json:"setTo"`
|
|
}
|
|
|
|
func (h *EventHandler) ApplyUpdate(update EventHandlerUpdate) {
|
|
if update.SetEventName != nil {
|
|
h.EventName = *update.SetEventName
|
|
}
|
|
if update.SetPriority != nil {
|
|
h.Priority = *update.SetPriority
|
|
}
|
|
if update.SetTargetKind != nil {
|
|
h.TargetKind = *update.SetTargetKind
|
|
}
|
|
if update.SetTargetValue != nil {
|
|
h.TargetValue = *update.SetTargetValue
|
|
}
|
|
if update.SetActions != nil {
|
|
h.Actions = *update.SetActions
|
|
}
|
|
if update.SetOneShot != nil {
|
|
h.OneShot = *update.SetOneShot
|
|
}
|
|
if update.SetFrom != nil {
|
|
h.From = *update.SetFrom
|
|
}
|
|
if update.SetFrom != nil {
|
|
h.To = *update.SetFrom
|
|
}
|
|
|
|
if update.SetConditions != nil {
|
|
h.Conditions = update.SetConditions
|
|
}
|
|
if update.PatchConditions != nil {
|
|
if h.Conditions == nil {
|
|
h.Conditions = make(map[string]EventCondition)
|
|
}
|
|
|
|
for key, value := range update.PatchConditions {
|
|
if value == nil {
|
|
delete(h.Conditions, key)
|
|
} else {
|
|
h.Conditions[key] = *value
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *EventHandler) MatchesEvent(event Event, targets []Device) bool {
|
|
if event.Name != h.EventName {
|
|
return false
|
|
}
|
|
|
|
for key, condition := range h.Conditions {
|
|
if !strings.ContainsRune(key, '.') && !event.HasPayload(key) {
|
|
return false
|
|
}
|
|
|
|
if !condition.check(key, event.Payload[key], targets) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (h *EventHandler) IsWithinTimeRange() bool {
|
|
if h.From.IsNever() || h.To.IsNever() {
|
|
return true
|
|
}
|
|
|
|
return CurrentTimeOfDay().IsBetween(h.From, h.To)
|
|
}
|
|
|
|
func (c *EventCondition) check(key, value string, targets []Device) bool {
|
|
any := strings.HasPrefix(key, "any.")
|
|
all := strings.HasPrefix(key, "all.")
|
|
if any || all {
|
|
count := 0
|
|
total := 0
|
|
for _, target := range targets {
|
|
matches, skip := c.checkDevice(key[4:], target)
|
|
if skip {
|
|
continue
|
|
}
|
|
|
|
if matches {
|
|
count++
|
|
}
|
|
total++
|
|
}
|
|
|
|
return (any && count > 0) || (all && count == total)
|
|
}
|
|
|
|
return c.matches(value)
|
|
}
|
|
|
|
func (c *EventCondition) checkDevice(key string, device Device) (matches bool, skip bool) {
|
|
switch key {
|
|
case "power":
|
|
if !device.HasCapability(DCPower) {
|
|
return false, true
|
|
}
|
|
|
|
return c.matches(strconv.FormatBool(device.State.Power)), false
|
|
case "color":
|
|
if !device.HasCapability(DCColorKelvin, DCColorHS, DCColorHSK) {
|
|
return false, true
|
|
}
|
|
|
|
return c.matches(device.State.Color.String()), false
|
|
case "intensity":
|
|
if !device.HasCapability(DCIntensity) {
|
|
return false, true
|
|
}
|
|
|
|
return c.matches(strconv.FormatFloat(device.State.Intensity, 'f', -1, 64)), false
|
|
case "temperature":
|
|
if !device.HasCapability(DCTemperatureControl, DCTemperatureSensor) {
|
|
return false, true
|
|
}
|
|
|
|
return c.matches(strconv.FormatFloat(device.State.Temperature, 'f', -1, 64)), false
|
|
case "scene":
|
|
sceneId := -1
|
|
for _, assignment := range device.SceneAssignments {
|
|
duration := time.Duration(assignment.DurationMS) * time.Millisecond
|
|
if duration <= 0 || time.Now().Before(assignment.StartTime.Add(duration)) {
|
|
sceneId = assignment.SceneID
|
|
}
|
|
}
|
|
|
|
return c.matches(strconv.Itoa(sceneId)), false
|
|
default:
|
|
return false, true
|
|
}
|
|
}
|
|
|
|
var numRegex = regexp.MustCompile("^-*[0-9]+(.[0-9]+)*$")
|
|
|
|
func (c *EventCondition) matches(value string) bool {
|
|
if numRegex.MatchString(value) {
|
|
numValue, _ := strconv.ParseFloat(value, 64)
|
|
stillAlive := true
|
|
|
|
if c.LT != "" {
|
|
lt, _ := strconv.ParseFloat(c.LT, 64)
|
|
stillAlive = numValue < lt
|
|
}
|
|
|
|
if stillAlive && c.LTE != "" {
|
|
lte, _ := strconv.ParseFloat(c.LTE, 64)
|
|
stillAlive = numValue <= lte
|
|
}
|
|
|
|
if stillAlive && c.EQ != "" {
|
|
eq, _ := strconv.ParseFloat(c.EQ, 64)
|
|
stillAlive = numValue == eq
|
|
}
|
|
|
|
if stillAlive && c.GTE != "" {
|
|
gte, _ := strconv.ParseFloat(c.GTE, 64)
|
|
stillAlive = numValue >= gte
|
|
}
|
|
|
|
if stillAlive && c.GT != "" {
|
|
gt, _ := strconv.ParseFloat(c.GT, 64)
|
|
stillAlive = numValue > gt
|
|
}
|
|
|
|
return stillAlive
|
|
} else if c.EQ != "" {
|
|
return strings.ToLower(c.EQ) == strings.ToLower(value)
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
type EventAction struct {
|
|
SetPower *bool `json:"setPower"`
|
|
SetColor *string `json:"setColor"`
|
|
SetIntensity *float64 `json:"setIntensity"`
|
|
SetTemperature *int `json:"setTemperature"`
|
|
AddIntensity *float64 `json:"addIntensity"`
|
|
FireEvent *Event `json:"fireEvent"`
|
|
SetScene *DeviceSceneAssignment `json:"setScene"`
|
|
PushScene *DeviceSceneAssignment `json:"pushScene"`
|
|
}
|
|
|
|
func (action *EventAction) Apply(other EventAction) {
|
|
if action.SetPower == nil {
|
|
action.SetPower = other.SetPower
|
|
}
|
|
if action.SetColor == nil {
|
|
action.SetColor = other.SetColor
|
|
}
|
|
if action.SetIntensity == nil && other.SetIntensity != nil {
|
|
action.SetIntensity = other.SetIntensity
|
|
}
|
|
if action.AddIntensity == nil && action.SetIntensity == nil {
|
|
action.AddIntensity = other.AddIntensity
|
|
}
|
|
if action.FireEvent == nil {
|
|
action.FireEvent = other.FireEvent
|
|
}
|
|
if action.SetScene == nil {
|
|
action.SetScene = other.SetScene
|
|
}
|
|
if action.PushScene == nil {
|
|
action.PushScene = other.PushScene
|
|
}
|
|
}
|
|
|
|
func (c EventCondition) String() string {
|
|
str := make([]string, 0, 5)
|
|
if len(c.LT) > 0 {
|
|
str = append(str, "lt:"+c.LT)
|
|
}
|
|
if len(c.LTE) > 0 {
|
|
str = append(str, "lte:"+c.LTE)
|
|
}
|
|
if len(c.EQ) > 0 {
|
|
str = append(str, c.EQ)
|
|
}
|
|
if len(c.GTE) > 0 {
|
|
str = append(str, "gte:"+c.GTE)
|
|
}
|
|
if len(c.GT) > 0 {
|
|
str = append(str, "gte:"+c.GT)
|
|
}
|
|
|
|
return strings.Join(str, ";")
|
|
}
|