Skip to content

Commit

Permalink
fix: mitigate geofence flapping (#6)
Browse files Browse the repository at this point in the history
Because lat and long must be processed for geofence changes
individually, it's possible that when crossing geofences, there may be
some flapping. In the scenario where a user does not manually define a
cooldown or utilize garage status checks, this can cause the app to spam
the garage with commands during a brief window.

This adds a base cooldown to allow a full geofence crossing to take
place after the first garage command is sent to mitigate the flapping.
  • Loading branch information
brchri authored Nov 30, 2023
1 parent 19d7d88 commit b2304b6
Show file tree
Hide file tree
Showing 4 changed files with 29 additions and 6 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ name: Publish Docker Image
on:
workflow_dispatch:
push:
branches:
- main
tags:
- 'v*'

Expand Down Expand Up @@ -99,6 +101,7 @@ jobs:
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=edge,branch=main
- name: Login to Docker Hub
uses: docker/login-action@v2
Expand Down
21 changes: 16 additions & 5 deletions internal/geo/geo.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,12 @@ func CheckGeofence(car *Car) {
// get action based on either geo cross events or distance threshold cross events
action := car.GarageDoor.Geofence.getEventChangeAction(car)

if action == "" || car.GarageDoor.OpLock {
return // only execute if there's a valid action to execute and the garage door isn't on cooldown
if action == "" {
return // nothing to do
}
if car.GarageDoor.OpLock {
logger.Debugf("Garage operation is locked (due to either cooldown or current activity), will not execute action '%s'", action)
return
}

car.GarageDoor.OpLock = true // set lock so no other threads try to operate the garage before the cooldown period is complete
Expand Down Expand Up @@ -114,8 +118,15 @@ func CheckGeofence(car *Car) {
}
}

time.Sleep(time.Duration(util.Config.Global.OpCooldown) * time.Minute) // keep opLock true for OpCooldown minutes to prevent flapping in case of overlapping geofences
car.GarageDoor.OpLock = false // release garage door's operation lock
if util.Config.Global.OpCooldown > 0 {
time.Sleep(time.Duration(util.Config.Global.OpCooldown) * time.Minute) // keep opLock true for OpCooldown minutes to prevent flapping in case of overlapping geofences
} else if os.Getenv("GDO_SKIP_FLAP_DELAY") != "true" {
// because lat and long are processed individually, it's possible that a car may flap briefly on the geofence crossing which can spam action calls to the gdo
// add a small sleep to prevent this
logger.Debugf("Garage for car %d retaining oplock for 5s to mitigate flapping when crossing geofence...", car.ID)
time.Sleep(5000 * time.Millisecond)
}
car.GarageDoor.OpLock = false // release garage door's operation lock
}()
}

Expand Down Expand Up @@ -151,7 +162,7 @@ func ParseGarageDoorConfig() {

// initialize location update channel
for _, c := range g.Cars {
c.LocationUpdate = make(chan Point, 2)
c.LocationUpdate = make(chan Point)
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions internal/geo/geo_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package geo

import (
"os"
"path/filepath"
"testing"
"time"
Expand Down Expand Up @@ -50,6 +51,8 @@ func init() {
polygonGeofence, _ = polygonCar.GarageDoor.Geofence.(*PolygonGeofence) // type cast geofence interface

util.Config.Global.OpCooldown = 0

os.Setenv("GDO_SKIP_FLAP_DELAY", "true") // for testing, skip 1.5s delay after gdo ops meant to prevent spam from flapping
}

func Test_getEventChangeAction_Circular(t *testing.T) {
Expand Down
8 changes: 7 additions & 1 deletion internal/util/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,13 @@ func formatLevel(level logger.Level) string {
// 01/02/2006 15:04:05 [LEVEL] Message...
func (f *CustomFormatter) Format(entry *logger.Entry) ([]byte, error) {
// Use the timestamp from the log entry to format it as you like
timestamp := entry.Time.Format("01/02/2006 15:04:05")
var timestamp string
if os.Getenv("DEBUG") == "true" {
// include milliseconds for debug
timestamp = entry.Time.Format("01/02/2006 15:04:05.000")
} else {
timestamp = entry.Time.Format("01/02/2006 15:04:05")
}

// Ensure the log level string is always 5 characters
paddedLevel := formatLevel(entry.Level)
Expand Down

0 comments on commit b2304b6

Please sign in to comment.