7 Commits
Author SHA1 Message Date
gisle 6947fc3461 add delete light endpoint and forget for Hue.
continuous-integration/drone/push Build is passing
2022-01-31 23:09:00 +01:00
gisle 600c90a614 undo group nonsense in hue bridge.
continuous-integration/drone/push Build is passing
2022-01-28 23:01:44 +01:00
Stian Fredrik Aune a08685c3cd Location, location, location.
continuous-integration/drone Build is passing
2022-01-21 19:26:06 +01:00
Stian Fredrik Aune 15536c6078 lucy factory - sensor support 2022-01-20 22:07:04 +01:00
Stian Fredrik Aune 2bb2154e5b SceneDimmer bug 2022-01-12 17:23:56 +01:00
Stian Fredrik Aune 8c2ef2dac3 factory command 2022-01-12 17:01:56 +01:00
Stian Fredrik Aune f4e584938b enable 'lucy handler delete all' 2022-01-08 21:02:35 +01:00
15 changed files with 649 additions and 90 deletions
+41
View File
@@ -7,6 +7,7 @@ import (
"git.aiterp.net/lucifer/new-server/models"
"github.com/gin-gonic/gin"
"log"
"strconv"
"time"
)
@@ -265,6 +266,46 @@ func Devices(r gin.IRoutes) {
return withSceneState(devices), nil
}))
r.DELETE("/:fetch", handler(func(c *gin.Context) (interface{}, error) {
id, err := strconv.Atoi(c.Param("fetch"))
if err != nil {
return nil, err
}
device, err := config.DeviceRepository().Find(ctxOf(c), id)
if err != nil {
return nil, err
}
if c.Query("forgetmenot") != "true" {
bridge, err := config.BridgeRepository().Find(ctxOf(c), device.BridgeID)
if err != nil {
return nil, err
}
driver, err := config.DriverProvider().Provide(bridge.Driver)
if err != nil {
return nil, err
}
forgettableDriver, ok := driver.(models.ForgettableDriver)
if !ok {
return nil, models.ErrCannotForget
}
err = forgettableDriver.ForgetDevice(ctxOf(c), bridge, *device)
if err != nil {
return nil, err
}
}
err = config.DeviceRepository().Delete(ctxOf(c), device)
if err != nil {
return nil, err
}
return device, nil
}))
}
func withSceneState(devices []models.Device) []models.Device {
+18 -1
View File
@@ -1,17 +1,34 @@
package config
import (
"log"
"os"
"strconv"
"time"
)
func Location() *time.Location {
env, ok := os.LookupEnv("LUCIFER_LOCATION")
if !ok {
return time.UTC
}
loc, err := time.LoadLocation(env)
if err != nil {
log.Printf("Location lookup failed: %s (%s)", env, err.Error())
return time.UTC
}
return loc
}
func MySqlHost() string { return strEnv("LUCIFER_MYSQL_HOST") }
func MySqlPort() int { return intEnv("LUCIFER_MYSQL_PORT") }
func MySqlSchema() string { return strEnv("LUCIFER_MYSQL_SCHEMA") }
func MySqlUsername() string { return strEnv("LUCIFER_MYSQL_USERNAME") }
func MySQlPassword() string { return strEnv("LUCIFER_MYSQL_PASSWORD") }
func ServerPort() int { return intEnv("LUCIFER_SERVER_PORT") }
func ServerPort() int { return intEnv("LUCIFER_SERVER_PORT") }
func strEnv(key string) string {
env, ok := os.LookupEnv(key)
+4 -1
View File
@@ -7,13 +7,14 @@ import (
"git.aiterp.net/lucifer/new-server/app/config"
"git.aiterp.net/lucifer/new-server/app/services"
"git.aiterp.net/lucifer/new-server/app/services/publisher"
"git.aiterp.net/lucifer/new-server/models"
"github.com/gin-gonic/gin"
"log"
"time"
)
func StartServer() {
setupCtx, cancel := context.WithTimeout(context.Background(), time.Second * 10)
setupCtx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
err := publisher.Initialize(setupCtx)
@@ -38,5 +39,7 @@ func StartServer() {
api.EventHandlers(apiGin.Group("/event-handlers"))
api.Scenes(apiGin.Group("/scenes"))
models.TimeOfDayTimeZone = config.Location()
log.Fatal(ginny.Run(fmt.Sprintf("0.0.0.0:%d", config.ServerPort())))
}
+62
View File
@@ -0,0 +1,62 @@
package handlerfactory
import (
"git.aiterp.net/lucifer/new-server/models"
"gopkg.in/yaml.v2"
"io"
"io/ioutil"
"strings"
)
type FactoryConfig struct {
Mode FCMOde `yaml:"mode"`
Target string `yaml:"target"`
SceneNames []string `yaml:"scenes"`
TriggerDeviceID string `yaml:"trigger_device_id"`
After []After `yaml:"after"`
From string `yaml:"from"`
To string `yaml:"to"`
}
type FCMOde string
var (
SceneDimmer FCMOde = "SceneDimmer"
MotionSensor FCMOde = "MotionSensor"
)
type After struct {
Minutes int `yaml:"minutes"`
NewState models.NewDeviceState `yaml:"new_state"`
}
func ParseConfigs(reader io.Reader) ([]FactoryConfig, error) {
readAll, err := ioutil.ReadAll(reader)
if err != nil {
return nil, err
}
splits := strings.Split(string(readAll), "\n---\n")
configs := make([]FactoryConfig, 0, 8)
for _, split := range splits {
var factoryConfig FactoryConfig
err := yaml.NewDecoder(strings.NewReader(split)).Decode(&factoryConfig)
if err != nil {
return nil, err
}
configs = append(configs, factoryConfig)
}
return configs, nil
}
func (f *FactoryConfig) FromTime() models.TimeOfDay {
tod, _ := models.ParseTimeOfDay(f.From)
return tod
}
func (f *FactoryConfig) ToTime() models.TimeOfDay {
tod, _ := models.ParseTimeOfDay(f.To)
return tod
}
+235
View File
@@ -0,0 +1,235 @@
package handlerfactory
import (
"fmt"
"git.aiterp.net/lucifer/new-server/models"
)
func CreateHandlers(
config FactoryConfig,
scenes []models.Scene,
existing []models.EventHandler,
) ([]models.EventHandler, []models.EventHandler) {
switch config.Mode {
case SceneDimmer:
return sceneDimmer(config, scenes, existing)
case MotionSensor:
return motionSensor(config, existing)
default:
return []models.EventHandler{}, []models.EventHandler{}
}
}
func sceneDimmer(
config FactoryConfig,
scenes []models.Scene,
existing []models.EventHandler,
) ([]models.EventHandler, []models.EventHandler) {
kind, refValue := models.ParseFetchString(config.Target)
toRemove := make([]models.EventHandler, 0, 8)
for _, old := range existing {
if old.Conditions["deviceId"].EQ == config.TriggerDeviceID {
toRemove = append(toRemove, old)
}
}
resolvedScenes := make([]models.Scene, 0, len(config.SceneNames))
for _, name := range config.SceneNames {
for _, scene := range scenes {
if name == scene.Name {
resolvedScenes = append(resolvedScenes, scene)
break
}
}
}
if len(scenes) < 2 {
return []models.EventHandler{}, []models.EventHandler{}
}
handlers := make([]models.EventHandler, 0, len(scenes)+4)
handlers = append(handlers, models.EventHandler{
EventName: "ButtonPressed",
Conditions: map[string]models.EventCondition{
"deviceId": {EQ: config.TriggerDeviceID},
"buttonName": {EQ: "On"},
},
OneShot: false,
Priority: 100,
TargetKind: kind,
TargetValue: refValue,
Actions: models.EventAction{SetPower: &myTrue},
From: config.FromTime(),
To: config.ToTime(),
})
handlers = append(handlers, models.EventHandler{
EventName: "ButtonPressed",
Conditions: map[string]models.EventCondition{
"deviceId": {EQ: config.TriggerDeviceID},
"buttonName": {EQ: "Off"},
},
OneShot: false,
Priority: 100,
TargetKind: kind,
TargetValue: refValue,
Actions: models.EventAction{SetPower: &myFalse},
From: config.FromTime(),
To: config.ToTime(),
})
for i, scene := range resolvedScenes {
if i > 0 {
handlers = append(handlers, models.EventHandler{
EventName: "ButtonPressed",
Conditions: map[string]models.EventCondition{
"deviceId": {EQ: config.TriggerDeviceID},
"buttonName": {EQ: "DimDown"},
"any.scene": {EQ: fmt.Sprintf("%d", scene.ID)},
},
OneShot: false,
Priority: 100,
TargetKind: kind,
TargetValue: refValue,
Actions: models.EventAction{SetScene: &models.DeviceSceneAssignment{
SceneID: resolvedScenes[i-1].ID,
Group: refValue,
DurationMS: 0,
}},
From: config.FromTime(),
To: config.ToTime(),
})
}
if i < len(resolvedScenes)-1 {
handlers = append(handlers, models.EventHandler{
EventName: "ButtonPressed",
Conditions: map[string]models.EventCondition{
"deviceId": {EQ: config.TriggerDeviceID},
"buttonName": {EQ: "DimUp"},
"any.scene": {EQ: fmt.Sprintf("%d", scene.ID)},
},
OneShot: false,
Priority: 100,
TargetKind: kind,
TargetValue: refValue,
Actions: models.EventAction{SetScene: &models.DeviceSceneAssignment{
SceneID: resolvedScenes[i+1].ID,
Group: refValue,
DurationMS: 0,
}},
From: config.FromTime(),
To: config.ToTime(),
})
}
}
handlers = append(handlers, models.EventHandler{
EventName: "ButtonPressed",
Conditions: map[string]models.EventCondition{
"deviceId": {EQ: config.TriggerDeviceID},
"buttonName": {EQ: "DimDown"},
},
OneShot: false,
Priority: 90,
TargetKind: kind,
TargetValue: refValue,
Actions: models.EventAction{SetScene: &models.DeviceSceneAssignment{
SceneID: resolvedScenes[len(resolvedScenes)-1].ID,
Group: refValue,
DurationMS: 0,
}},
From: config.FromTime(),
To: config.ToTime(),
})
handlers = append(handlers, models.EventHandler{
EventName: "ButtonPressed",
Conditions: map[string]models.EventCondition{
"deviceId": {EQ: config.TriggerDeviceID},
"buttonName": {EQ: "DimUp"},
},
OneShot: false,
Priority: 90,
TargetKind: kind,
TargetValue: refValue,
Actions: models.EventAction{SetScene: &models.DeviceSceneAssignment{
SceneID: resolvedScenes[0].ID,
Group: refValue,
DurationMS: 0,
}},
From: config.FromTime(),
To: config.ToTime(),
})
return optimizeLists(handlers, toRemove)
}
func motionSensor(
config FactoryConfig,
existing []models.EventHandler,
) ([]models.EventHandler, []models.EventHandler) {
kind, refValue := models.ParseFetchString(config.Target)
toRemove := make([]models.EventHandler, 0, 8)
for _, old := range existing {
if old.From != config.FromTime() || old.To != config.ToTime() {
continue
}
if old.Conditions["deviceId"].EQ == config.TriggerDeviceID {
toRemove = append(toRemove, old)
}
}
handlers := make([]models.EventHandler, 0, len(config.After))
for _, after := range config.After {
if after.Minutes == 0 {
handlers = append(handlers, models.EventHandler{
EventName: "SensorPresenceStarted",
Conditions: map[string]models.EventCondition{
"deviceId": {EQ: config.TriggerDeviceID},
},
OneShot: false,
Priority: 100,
TargetKind: kind,
TargetValue: refValue,
Actions: models.EventAction{
SetPower: after.NewState.Power,
SetColor: after.NewState.Color,
SetIntensity: after.NewState.Intensity,
SetTemperature: after.NewState.Temperature,
},
From: config.FromTime(),
To: config.ToTime(),
})
} else {
handlers = append(handlers, models.EventHandler{
EventName: "SensorPresenceEnded",
Conditions: map[string]models.EventCondition{
"deviceId": {EQ: config.TriggerDeviceID},
"minutesElapsed": {EQ: fmt.Sprintf("%d", after.Minutes)},
},
OneShot: false,
Priority: 100,
TargetKind: kind,
TargetValue: refValue,
Actions: models.EventAction{
SetPower: after.NewState.Power,
SetColor: after.NewState.Color,
SetIntensity: after.NewState.Intensity,
SetTemperature: after.NewState.Temperature,
},
From: config.FromTime(),
To: config.ToTime(),
})
}
}
return optimizeLists(handlers, toRemove)
}
var myTrue = true
var myFalse = false
+72
View File
@@ -0,0 +1,72 @@
package handlerfactory
import (
"git.aiterp.net/lucifer/new-server/models"
)
func optimizeLists(
add []models.EventHandler,
del []models.EventHandler,
) ([]models.EventHandler, []models.EventHandler) {
newAdd := make([]models.EventHandler, 0, len(add))
newDel := make([]models.EventHandler, 0, len(del))
OptimizeLoop1:
for _, addItem := range add {
for _, delItem := range del {
if checkDuplicate(&addItem, &delItem) {
continue OptimizeLoop1
}
}
newAdd = append(newAdd, addItem)
}
OptimizeLoop2:
for _, delItem := range del {
for _, addItem := range add {
if checkDuplicate(&addItem, &delItem) {
continue OptimizeLoop2
}
}
newDel = append(newDel, delItem)
}
return newAdd, newDel
}
func checkDuplicate(a *models.EventHandler, b *models.EventHandler) bool {
check := true
check = check && a.EventName == b.EventName
check = check && len(a.Conditions) == len(b.Conditions)
for k := range a.Conditions {
check = check && a.Conditions[k] == b.Conditions[k]
}
check = check && a.OneShot == b.OneShot
check = check && a.Priority == b.Priority
check = check && a.TargetKind == b.TargetKind
check = check && a.TargetValue == b.TargetValue
check = check && a.From == b.From
check = check && a.To == b.To
aPower := a.Actions.SetPower
bPower := b.Actions.SetPower
powerEq := (aPower == nil && bPower == nil) || (aPower != nil && bPower != nil && *aPower == *bPower)
aColor := a.Actions.SetColor
bColor := b.Actions.SetColor
colorEq := (aColor == nil && bColor == nil) || (aColor != nil && bColor != nil && *aColor == *bColor)
aInt := a.Actions.SetIntensity
bInt := b.Actions.SetIntensity
intEq := (aInt == nil && bInt == nil) || (aInt != nil && bInt != nil && *aInt == *bInt)
aTemp := a.Actions.SetTemperature
bTemp := b.Actions.SetTemperature
tempEq := (aTemp == nil && bTemp == nil) || (aTemp != nil && bTemp != nil && *aTemp == *bTemp)
check = check && powerEq && colorEq && intEq && tempEq
return check
}
+75
View File
@@ -0,0 +1,75 @@
package main
import (
"context"
"fmt"
"git.aiterp.net/lucifer/new-server/app/client"
"git.aiterp.net/lucifer/new-server/app/services/handlerfactory"
"log"
"os"
)
func factoryCmd(
ctx context.Context,
c client.Client,
) {
cmd := parseCommand(os.Args[2:])
switch cmd.Name {
case "run":
fileName := cmd.Params.Get(0).String()
if fileName == nil {
log.Fatalln("Missing filename")
}
scenes, err := c.GetScenes(ctx)
if err != nil {
log.Fatalln(err.Error())
}
handlers, err := c.GetHandlers(ctx)
if err != nil {
log.Fatalln(err.Error())
}
file, err := os.Open(*fileName)
if err != nil {
log.Fatalln("Failed to open file:", err)
}
defer file.Close()
configs, err := handlerfactory.ParseConfigs(file)
if err != nil {
log.Fatalln("Failed to parse file:", err)
}
for _, config := range configs {
toAdd, toRemove := handlerfactory.CreateHandlers(config, scenes, handlers)
for _, removing := range toRemove {
_, err := c.DeleteHandler(ctx, removing.ID)
if err != nil {
log.Fatalln(err.Error())
}
log.Printf("Removed handler %d", removing.ID)
}
for _, adding := range toAdd {
res, err := c.AddHandler(ctx, adding)
if err != nil {
log.Fatalln(err.Error())
}
log.Printf("Added handler %d", res.ID)
}
}
default:
if cmd.Name != "help" {
log.Println("Unknown command:", cmd.Name)
}
_, _ = fmt.Fprintln(os.Stderr, helpString[1:])
}
}
+25 -9
View File
@@ -76,17 +76,33 @@ func handlerCmd(
WriteHandlerInfoTable(os.Stdout, []models.EventHandler{*returned})
case "delete":
id := cmd.Params.Get(0).Int()
if id == nil {
log.Fatalln("ID missing")
}
if cmd.Params.Get(0).StringOr("_not") == "all" {
handlers, err := c.GetHandlers(ctx)
if err != nil {
log.Fatalln(err)
}
handler, err := c.DeleteHandler(ctx, *id)
if err != nil {
log.Fatalln(err)
}
for _, handler := range handlers {
_, err = c.DeleteHandler(ctx, handler.ID)
if err != nil {
log.Fatalln(err)
}
}
WriteHandlerInfoTable(os.Stdout, []models.EventHandler{*handler})
log.Printf("Deleted %d handlers", len(handlers))
} else {
id := cmd.Params.Get(0).Int()
if id == nil {
log.Fatalln("ID missing")
}
handler, err := c.DeleteHandler(ctx, *id)
if err != nil {
log.Fatalln(err)
}
WriteHandlerInfoTable(os.Stdout, []models.EventHandler{*handler})
}
default:
if cmd.Name != "help" {
log.Println("Unknown command:", cmd.Name)
+32
View File
@@ -32,6 +32,38 @@ EVENT HANDLER COMMANDS
<-fire> <-fire.payload.*>
handler delete <id>
EVENT HANDLER FACTORY COMMANDS
factory run <file.yaml>
File example (use '---' to delimit multiple configs in one file):
mode: SceneDimmer
target: tag:Soverom
scenes:
- kveldslys
- landskap
- vasking
trigger_device_id: 14
---
mode: MotionSensor
target: tag:Bod
trigger_device_id: 36
after:
- minutes: 0
new_state:
power: true
color: hs:240,0.6
intensity: 0.8
- minutes: 1
new_state:
power: false
from: 08:00
to: 15:59:59
---
(more modes coming soon)
WARNING: Similar event handlers will be removed!
EVENT COMMANDS
run <event name> <*=S>
+5 -3
View File
@@ -82,8 +82,8 @@ func main() {
case "update":
{
devices, err := c.PutDevice(ctx, cmd.Params.Get(0).StringOr("all"), models.DeviceUpdate{
Name: cmd.Params.Get("name").String(),
Icon: cmd.Params.Get("icon").String(),
Name: cmd.Params.Get("name").String(),
Icon: cmd.Params.Get("icon").String(),
UserProperties: cmd.Params.Subset("prop.").StringPtrMap(),
})
if err != nil {
@@ -93,6 +93,9 @@ func main() {
WriteDeviceInfoTable(os.Stdout, devices)
}
case "factory":
factoryCmd(ctx, c)
case "handler":
handlerCmd(ctx, c)
@@ -126,4 +129,3 @@ func main() {
_, _ = fmt.Fprintln(os.Stderr, helpString[1:])
}
}
+61 -73
View File
@@ -6,8 +6,8 @@ import (
"encoding/json"
"fmt"
"git.aiterp.net/lucifer/new-server/models"
"golang.org/x/sync/errgroup"
"io"
"log"
"net"
"net/http"
"strconv"
@@ -23,6 +23,7 @@ type Bridge struct {
externalID int
lightStates []*hueLightState
sensorStates []*hueSensorState
quarantine map[string]time.Time
syncingPublish uint32
}
@@ -34,6 +35,10 @@ func (b *Bridge) Refresh(ctx context.Context) error {
b.mu.Lock()
for index, light := range lightMap {
if time.Now().Before(b.quarantine[light.Uniqueid]) {
continue
}
var state *hueLightState
for _, existingState := range b.lightStates {
if existingState.index == index {
@@ -68,6 +73,10 @@ func (b *Bridge) Refresh(ctx context.Context) error {
b.mu.Lock()
for index, sensor := range sensorMap {
if time.Now().Before(b.quarantine[sensor.UniqueID]) {
continue
}
var state *hueSensorState
for _, existingState := range b.sensorStates {
if existingState.index == index {
@@ -98,17 +107,17 @@ func (b *Bridge) Refresh(ctx context.Context) error {
func (b *Bridge) SyncStale(ctx context.Context) error {
indices := make([]int, 0, 4)
arrayIndices := make([]int, 0, 4)
inputs := make([]LightStateInput, 0, 4)
eg, ctx := errgroup.WithContext(ctx)
b.mu.Lock()
for i, state := range b.lightStates {
for _, state := range b.lightStates {
if !state.stale {
continue
}
indices = append(indices, state.index)
arrayIndices = append(arrayIndices, i)
inputs = append(inputs, state.input)
}
b.mu.Unlock()
@@ -117,83 +126,60 @@ func (b *Bridge) SyncStale(ctx context.Context) error {
return nil
}
groups := make([]*syncGroup, 0, 4)
for i := range inputs {
input := inputs[i]
for i, input := range inputs {
iCopy := i
index := indices[i]
inputCopy := input
found := false
for _, group := range groups {
if group.State.Equal(input) {
group.Indexes = append(group.Indexes, indices[i])
group.ArrayIndexes = append(group.ArrayIndexes, arrayIndices[i])
found = true
}
}
if !found {
groups = append(groups, &syncGroup{
GroupIndex: -1,
State: input,
Indexes: []int{indices[i]},
ArrayIndexes: []int{arrayIndices[i]},
})
}
}
groupMap, err := b.getGroups(ctx)
if err != nil {
return err
}
for id, data := range groupMap {
for _, group := range groups {
if group.Matches(&data) {
group.GroupIndex = id
break
}
}
}
for _, group := range groups {
if group.GroupIndex == -1 {
data := GroupData{
Name: "lucifer_auto_group",
Lights: []string{},
}
for _, idx := range group.Indexes {
data.Lights = append(data.Lights, strconv.Itoa(idx))
}
id, err := b.postGroup(ctx, data)
eg.Go(func() error {
err := b.putLightState(ctx, index, inputCopy)
if err != nil {
return err
}
group.GroupIndex = id
}
b.lightStates[iCopy].stale = false
return nil
})
}
log.Println("Updating", len(inputs), "lights on Hue bridge", b.externalID, "in", len(groups), "groups")
return eg.Wait()
}
for _, group := range groups {
err := b.putGroupLightState(ctx, group.GroupIndex, group.State)
if err != nil {
return err
}
b.mu.Lock()
for _, arrayIndex := range group.ArrayIndexes {
b.lightStates[arrayIndex].stale = false
}
b.mu.Unlock()
if groupMap[group.GroupIndex].Name == "lucifer_auto_group" {
err := b.deleteGroup(ctx, group.GroupIndex)
if err != nil {
log.Println("Could not delete temporary group", err)
}
func (b *Bridge) ForgetDevice(ctx context.Context, device models.Device) error {
// Find index
b.mu.Lock()
found := false
index := -1
for i, ls := range b.lightStates {
if ls.uniqueID == device.InternalID {
found = true
index = i
}
}
b.mu.Unlock()
if !found {
return models.ErrNotFound
}
// Delete light from bridge
err := b.deleteLight(ctx, index)
if err != nil {
return err
}
// Remove light state from local list. I don't know if the quarantine is necessary, but let's have it anyway.
b.mu.Lock()
for i, ls := range b.lightStates {
if ls.uniqueID == device.InternalID {
b.lightStates = append(b.lightStates[:i], b.lightStates[i+1:]...)
}
}
if b.quarantine == nil {
b.quarantine = make(map[string]time.Time, 1)
}
b.quarantine[device.InternalID] = time.Now().Add(time.Second * 30)
b.mu.Unlock()
return nil
}
@@ -236,6 +222,10 @@ func (b *Bridge) putGroupLightState(ctx context.Context, index int, input LightS
return b.put(ctx, fmt.Sprintf("groups/%d/action", index), input, nil)
}
func (b *Bridge) deleteLight(ctx context.Context, index int) error {
return b.delete(ctx, fmt.Sprintf("lights/%d", index), nil)
}
func (b *Bridge) getToken(ctx context.Context) (string, error) {
result := make([]CreateUserResponse, 0, 1)
err := b.post(ctx, "", CreateUserInput{DeviceType: "git.aiterp.net/lucifer"}, &result)
@@ -296,7 +286,6 @@ func (b *Bridge) postGroup(ctx context.Context, input GroupData) (int, error) {
return id, err
}
func (b *Bridge) deleteGroup(ctx context.Context, index int) error {
return b.delete(ctx, "groups/"+strconv.Itoa(index), nil)
}
@@ -343,7 +332,6 @@ func (b *Bridge) delete(ctx context.Context, resource string, target interface{}
return json.NewDecoder(res.Body).Decode(target)
}
func (b *Bridge) post(ctx context.Context, resource string, body interface{}, target interface{}) error {
rb, err := reqBody(body)
if err != nil {
+9
View File
@@ -372,6 +372,15 @@ func (d *Driver) Run(ctx context.Context, bridge models.Bridge, ch chan<- models
}
}
func (d *Driver) ForgetDevice(ctx context.Context, bridge models.Bridge, device models.Device) error {
b, err := d.ensureBridge(ctx, bridge)
if err != nil {
return err
}
return b.ForgetDevice(ctx, device)
}
func (d *Driver) ensureBridge(ctx context.Context, info models.Bridge) (*Bridge, error) {
d.mu.Lock()
for _, bridge := range d.bridges {
+4
View File
@@ -16,3 +16,7 @@ type Driver interface {
Publish(ctx context.Context, bridge Bridge, devices []Device) error
Run(ctx context.Context, bridge Bridge, ch chan<- Event) error
}
type ForgettableDriver interface {
ForgetDevice(ctx context.Context, bridge Bridge, device Device) error
}
+1
View File
@@ -16,6 +16,7 @@ var ErrUnexpectedResponse = errors.New("driver api returned unexpected response
var ErrBridgeSearchFailed = errors.New("bridge search failed")
var ErrAddressOnlyDryRunnable = errors.New("this address may only be used for a dry run")
var ErrCannotForwardRequest = errors.New("driver is not able to forward requests")
var ErrCannotForget = errors.New("forget not supported on this bridge type")
var ErrInvalidAddress = errors.New("invalid mac address")
var ErrPayloadTooShort = errors.New("payload too short")
+5 -3
View File
@@ -9,6 +9,8 @@ import (
"time"
)
var TimeOfDayTimeZone = time.UTC
type TimeOfDay int
func (t *TimeOfDay) UnmarshalJSON(v []byte) error {
@@ -29,7 +31,6 @@ func (t *TimeOfDay) UnmarshalJSON(v []byte) error {
}
}
t2, err := ParseTimeOfDay(str)
if err != nil {
return err
@@ -82,11 +83,12 @@ func (t TimeOfDay) IsBetween(from TimeOfDay, to TimeOfDay) bool {
}
func CurrentTimeOfDay() TimeOfDay {
return TimeOfDayFromDate(time.Now().Local())
return TimeOfDayFromDate(time.Now().In(TimeOfDayTimeZone))
}
func TimeOfDayFromDate(date time.Time) TimeOfDay {
return NewTimeOfDay(date.Hour(), date.Minute(), date.Second())
localDate := date.In(TimeOfDayTimeZone)
return NewTimeOfDay(localDate.Hour(), localDate.Minute(), localDate.Second())
}
func NewTimeOfDay(hours, minutes, seconds int) TimeOfDay {