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.Itoa(device.State.Temperature)), 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, ";") }