From b2304b6129014df0b06bbc4cc90b6e27c648f73d Mon Sep 17 00:00:00 2001 From: brchri <126272303+brchri@users.noreply.github.com> Date: Thu, 30 Nov 2023 14:19:11 -0700 Subject: [PATCH] fix: mitigate geofence flapping (#6) 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. --- .github/workflows/publish-release.yml | 3 +++ internal/geo/geo.go | 21 ++++++++++++++++----- internal/geo/geo_test.go | 3 +++ internal/util/config.go | 8 +++++++- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index e9cc45f..2de88fb 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -3,6 +3,8 @@ name: Publish Docker Image on: workflow_dispatch: push: + branches: + - main tags: - 'v*' @@ -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 diff --git a/internal/geo/geo.go b/internal/geo/geo.go index fe58c1d..6843fa7 100644 --- a/internal/geo/geo.go +++ b/internal/geo/geo.go @@ -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 @@ -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 }() } @@ -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) } } } diff --git a/internal/geo/geo_test.go b/internal/geo/geo_test.go index 1cd6668..514012d 100644 --- a/internal/geo/geo_test.go +++ b/internal/geo/geo_test.go @@ -1,6 +1,7 @@ package geo import ( + "os" "path/filepath" "testing" "time" @@ -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) { diff --git a/internal/util/config.go b/internal/util/config.go index effb4f2..1f3dc98 100644 --- a/internal/util/config.go +++ b/internal/util/config.go @@ -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)