initial commit

This commit is contained in:
Stian Fredrik Aune
2021-05-20 22:03:23 +02:00
commit 5a56763526
21 changed files with 661 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
package models
type Bridge struct {
ID int
Name string
Driver DriverType
ConnectionString string
}
type DriverType string
var (
DTHue DriverType = "Hue"
DTNanoLeaf DriverType = "NanoLeaf"
)
+61
View File
@@ -0,0 +1,61 @@
package models
import (
"fmt"
"regexp"
"strconv"
)
type ColorValue struct {
Hue int `json:"h,omitempty"`
Saturation int `json:"s,omitempty"`
Kelvin int `json:"kelvin,omitempty"`
}
func (c *ColorValue) IsHueSat() bool {
return !c.IsKelvin()
}
func (c *ColorValue) IsKelvin() bool {
return c.Kelvin > 0
}
func (c *ColorValue) String() string {
if c.Kelvin > 0 {
return fmt.Sprintf("kelvin:%d", c.Kelvin)
}
return fmt.Sprintf("hsv:%d,%d", c.Hue, c.Saturation)
}
var kelvinRegex = regexp.MustCompile("kelvin:([0-9]+)")
var hsRegex = regexp.MustCompile("hs:([0-9]+),([0-9]+)")
func ParseColorValue(raw string) (ColorValue, error) {
if kelvinRegex.MatchString(raw) {
part := kelvinRegex.FindString(raw)
parsedPart, err := strconv.Atoi(part)
if err != nil {
return ColorValue{}, ErrBadInput
}
return ColorValue{Kelvin: parsedPart}, nil
}
if hsRegex.MatchString(raw) {
parts := kelvinRegex.FindAllString(raw, 2)
if len(parts) < 2 {
return ColorValue{}, ErrUnknownColorFormat
}
part1, err1 := strconv.Atoi(parts[0])
part2, err2 := strconv.Atoi(parts[1])
if err1 != nil || err2 != nil {
return ColorValue{}, ErrBadInput
}
return ColorValue{Hue: part1, Saturation: part2}, nil
}
return ColorValue{}, ErrUnknownColorFormat
}
+122
View File
@@ -0,0 +1,122 @@
package models
import (
"context"
"strings"
)
type Device struct {
ID int `json:"id"`
BridgeID int `json:"bridgeID"`
Icon string `json:"icon"`
Name string `json:"name"`
Capabilities []DeviceCapability `json:"capabilities"`
Properties map[string]string `json:"properties"`
State DeviceState `json:"state"`
Tags []string `json:"tags"`
}
// DeviceState contains optional state values that
// - Power: Whether the device is powered on
// - Color: Color value, if a color setting can be set on the device
// - Intensity: e.g. brightness, from 0-255
// - Temperature: e.g. for thermostats
type DeviceState struct {
Power bool `json:"power"`
Color ColorValue `json:"color,omitempty"`
Intensity int `json:"intensity,omitempty"`
Temperature int `json:"temperature"`
}
type NewDeviceState struct {
Power *bool `json:"power"`
Color *string `json:"color"`
Intensity int `json:"intensity"`
Temperature int `json:"temperature"`
}
type DeviceCapability string
type DeviceRepository interface {
FindByID(ctx context.Context, id int) (*Device, error)
FetchByReference(ctx context.Context, kind ReferenceKind, value string) ([]Device, error)
Save(ctx context.Context, device *Device) error
Delete(ctx context.Context, device *Device) error
}
var (
DCPower DeviceCapability = "Power"
DCColorHS DeviceCapability = "ColorHS"
DCColorKelvin DeviceCapability = "ColorKelvin"
DCButtonDefault DeviceCapability = "ButtonDefault"
DCButtonOn DeviceCapability = "ButtonOn"
DCButtonOff DeviceCapability = "ButtonOff"
DCButtonPlus DeviceCapability = "ButtonPlus"
DCButtonMinus DeviceCapability = "ButtonMinus"
DCButtonToggle DeviceCapability = "ButtonToggle"
DCIntensity DeviceCapability = "Intensity"
DCTemperature DeviceCapability = "Temperature"
)
var Capabilities = []DeviceCapability{
DCPower,
DCColorHS,
DCColorKelvin,
DCButtonDefault,
DCButtonOn,
DCButtonOff,
DCButtonPlus,
DCButtonMinus,
DCButtonToggle,
DCIntensity,
DCTemperature,
}
func (d *Device) Validate() error {
d.Name = strings.Trim(d.Name, " \t\n ")
if d.Name == "" {
return ErrInvalidName
}
newCaps := make([]DeviceCapability, 0, len(d.Capabilities))
for _, currCap := range d.Capabilities {
for _, validCap := range Capabilities {
if currCap == validCap {
newCaps = append(newCaps, currCap)
break
}
}
}
d.Capabilities = newCaps
return nil
}
func (d *Device) HasCapability(capacity DeviceCapability) bool {
for _, c := range d.Capabilities {
if c == capacity {
return true
}
}
return false
}
func (d *Device) SetState(newState NewDeviceState) error {
if newState.Power != nil && d.HasCapability(DCPower) {
d.State.Power = *newState.Power
}
if newState.Color != nil {
parsed, err := ParseColorValue(*newState.Color)
if err != nil {
return err
}
if (parsed.IsKelvin() && d.HasCapability(DCColorKelvin)) || (parsed.IsHueSat() && d.HasCapability(DCColorHS)) {
d.State.Color = parsed
}
}
return nil
}
+14
View File
@@ -0,0 +1,14 @@
package models
import "context"
type DriverResolver interface {
ResolveFor(bridge Bridge) (Driver, error)
}
type Driver interface {
SearchBridge(ctx context.Context, address string) (Bridge, error)
SearchDevices(ctx context.Context, bridge Bridge) ([]Device, error)
Consume(ctx context.Context, bridge Bridge, devices []Device, ch chan Event) (chan <-struct{}, error)
Publish(ctx context.Context, bridge Bridge, device Device) error
}
+8
View File
@@ -0,0 +1,8 @@
package models
import "errors"
var ErrInvalidName = errors.New("invalid name")
var ErrBadInput = errors.New("bad input")
var ErrBadColor = errors.New("bad color")
var ErrUnknownColorFormat = errors.New("unknown color format")
+18
View File
@@ -0,0 +1,18 @@
package models
type Event struct {
Name string `json:"name"`
Payload map[string]string `json:"payload,omitempty"`
}
func (e *Event) AddPayload(key, value string) {
if e.Payload == nil {
e.Payload = make(map[string]string, 8)
}
e.Payload[key] = value
}
func (e *Event) HasPayload(key string) bool {
return e.Payload != nil && e.Payload[key] != ""
}
+121
View File
@@ -0,0 +1,121 @@
package models
import (
"context"
"regexp"
"strconv"
"strings"
)
type EventHandler struct {
ID int `json:"id"`
EventName string `json:"eventName"`
Conditions map[string]EventCondition `json:"conditions"`
TargetKind ReferenceKind `json:"targetType"`
TargetValue string `json:"targetValue"`
}
type EventHandlerRepository interface {
FindByID(ctx context.Context, id int) (EventHandler, error)
FetchAll(ctx context.Context) ([]EventHandler, error)
Save(ctx context.Context, handler *EventHandler)
Delete(ctx context.Context, handler *EventHandler)
}
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"`
}
func (h *EventHandler) MatchesEvent(event Event, targets []Device) bool {
if event.Name != h.EventName {
return false
}
for key, condition := range h.Conditions {
if !event.HasPayload(key) {
return false
}
if !condition.check(key, event.Payload[key], targets) {
return false
}
}
return true
}
func (c *EventCondition) check(key, value string, targets []Device) bool {
any := strings.Index(key, "any.") == 0
all := strings.Index(key, "all.") == 0
if any || all && len(key) > 4 {
count := 0
for _, target := range targets {
if c.checkDevice(key[4:], target) {
count++
}
}
return (any && count > 0) || (all && count == len(targets))
}
return c.matches(value)
}
func (c *EventCondition) checkDevice(key string, device Device) bool {
switch key {
case "power":
return c.matches(strconv.FormatBool(device.State.Power))
case "color":
return c.matches(device.State.Color.String())
case "intensity":
return c.matches(strconv.Itoa(device.State.Intensity))
case "temperature":
return c.matches(strconv.Itoa(device.State.Temperature))
}
return false
}
var numRegex = regexp.MustCompile("^{-[0-9].}+$")
func (c *EventCondition) matches(value string) bool {
if numRegex.MatchString(value) {
numValue, _ := strconv.ParseFloat(c.LT, 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
}
}
+11
View File
@@ -0,0 +1,11 @@
package models
type ReferenceKind string
var (
RTDeviceID ReferenceKind = "DeviceID"
RTBridgeID ReferenceKind = "BridgeID"
RTTag ReferenceKind = "Tag"
)