Skip to content

Commit

Permalink
feat: add restricted zone to poly geo (#28)
Browse files Browse the repository at this point in the history
Adds an option to create a restricted zone for polygon geofences. If a
tracker enters an open geofence or leaves a close geofence *while in or
coming from a restricted zone*, then no action will trigger.
  • Loading branch information
brchri authored Dec 27, 2024
1 parent ee246a1 commit f191483
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 23 deletions.
11 changes: 11 additions & 0 deletions examples/config.polygon.http.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ garage_doors:
lng: -123.79950958978756
- lat: 46.192958467582514
lng: -123.7998033090239
restricted: # when a vehicle moves into an open geofence or out of a close geofence __from or while in a restricted zone__, no action will trigger
- lat: 46.192958467582514
lng: -123.7998033090239
- lat: 46.19279440766502
lng: -123.7998033090239
- lat: 46.19279440766502
lng: -123.79950958978756
- lat: 46.192958467582514
lng: -123.79950958978756
- lat: 46.192958467582514
lng: -123.7998033090239
opener: # defines how to control the garage
type: http # type of garage door opener to use
settings:
Expand Down
31 changes: 16 additions & 15 deletions internal/geo/geo.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,22 @@ type (
}

Tracker struct {
ID interface{} `yaml:"id"` // mqtt identifier for vehicle
GarageDoor *GarageDoor // bidirectional pointer to GarageDoor containing tracker
CurrentLocation Point // current vehicle location
LocationUpdate chan Point // channel to receive location updates
CurDistance float64 // current distance from garagedoor location
PrevGeofence string // geofence previously ascribed to tracker
CurGeofence string // updated geofence ascribed to tracker when published to mqtt
InsidePolyOpenGeo bool // indicates if tracker is currently inside the polygon_open_geofence
InsidePolyCloseGeo bool // indicates if tracker is currently inside the polygon_close_geofence
LastEnteredCloseGeo time.Time // timestamp of when tracker last entered the close geofence; used to prevent flapping
LastLeftOpenGeo time.Time // timestamp of when tracker last left the open geofence; used to prevent flapping
LatTopic string `yaml:"lat_topic"`
LngTopic string `yaml:"lng_topic"`
GeofenceTopic string `yaml:"geofence_topic"` // topic for publishing a geofence name where a tracker resides, e.g. teslamate geofence indicating 'home' or 'not_home'
ComplexTopic struct {
ID interface{} `yaml:"id"` // mqtt identifier for vehicle
GarageDoor *GarageDoor // bidirectional pointer to GarageDoor containing tracker
CurrentLocation Point // current vehicle location
LocationUpdate chan Point // channel to receive location updates
CurDistance float64 // current distance from garagedoor location
PrevGeofence string // geofence previously ascribed to tracker
CurGeofence string // updated geofence ascribed to tracker when published to mqtt
InsidePolyOpenGeo bool // indicates if tracker is currently inside the polygon_open_geofence
InsidePolyCloseGeo bool // indicates if tracker is currently inside the polygon_close_geofence
InsidePolyRestrictedGeo bool // indicates if tracker is currently inside the polygon_restricted_geofence
LastEnteredCloseGeo time.Time // timestamp of when tracker last entered the close geofence; used to prevent flapping
LastLeftOpenGeo time.Time // timestamp of when tracker last left the open geofence; used to prevent flapping
LatTopic string `yaml:"lat_topic"`
LngTopic string `yaml:"lng_topic"`
GeofenceTopic string `yaml:"geofence_topic"` // topic for publishing a geofence name where a tracker resides, e.g. teslamate geofence indicating 'home' or 'not_home'
ComplexTopic struct {
Topic string `yaml:"topic"`
LatJsonKey string `yaml:"lat_json_key"`
LngJsonKey string `yaml:"lng_json_key"`
Expand Down
30 changes: 30 additions & 0 deletions internal/geo/geo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,36 @@ func Test_CheckPolyGeofence_Arriving(t *testing.T) {
assert.Equal(t, checkGeofenceWrapper(polygonTracker), true)
}

// if arriving from a restricted area, should not trigger any action
func Test_CheckPolyGeofence_ArrivingFromRestricted(t *testing.T) {
mockGdo := &mocks.GDO{}
polygonTracker.GarageDoor.Opener = mockGdo
defer mockGdo.AssertExpectations(t)

polygonTracker.InsidePolyCloseGeo = false
polygonTracker.InsidePolyOpenGeo = false
polygonTracker.InsidePolyRestrictedGeo = true
polygonTracker.CurrentLocation.Lat = 46.19243683948096
polygonTracker.CurrentLocation.Lng = -123.80103692981524

assert.Equal(t, checkGeofenceWrapper(polygonTracker), true)
}

// if leaving from a restricted area, should not trigger any action
func Test_CheckPolyGeofence_LeavingFromRestricted(t *testing.T) {
mockGdo := &mocks.GDO{}
polygonTracker.GarageDoor.Opener = mockGdo
defer mockGdo.AssertExpectations(t)

polygonTracker.InsidePolyCloseGeo = true
polygonTracker.InsidePolyOpenGeo = true
polygonTracker.InsidePolyRestrictedGeo = true
polygonTracker.CurrentLocation.Lat = 46.19292902096646
polygonTracker.CurrentLocation.Lng = -123.79984989897177

assert.Equal(t, checkGeofenceWrapper(polygonTracker), true)
}

// if close is not defined, should not trigger any action
func Test_CheckPolyGeofence_Leaving_NoClose(t *testing.T) {
mockGdo := &mocks.GDO{}
Expand Down
22 changes: 15 additions & 7 deletions internal/geo/polygon.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ import (
)

type (
// contains 2 geofences, open and close, each of which are a list of lat/long points defining the polygon
// contains 3 geofences, open, close, and restricted, each of which are a list of lat/long points defining the polygon
PolygonGeofence struct {
Close []Point `yaml:"close,omitempty"` // list of points defining a polygon; when vehicle moves from inside this geofence to outside, garage will close
Open []Point `yaml:"open,omitempty"` // list of points defining a polygon; when vehicle moves from outside this geofence to inside, garage will open
KMLFile string `yaml:"kml_file,omitempty"`
Close []Point `yaml:"close,omitempty"` // list of points defining a polygon; when vehicle moves from inside this geofence to outside, garage will close
Open []Point `yaml:"open,omitempty"` // list of points defining a polygon; when vehicle moves from outside this geofence to inside, garage will open
Restricted []Point `yaml:"restricted,omitempty"` // list of points defining a polygon; when vehicle moves from inside this geofence to inside open geofence, garage will not open
KMLFile string `yaml:"kml_file,omitempty"`
}

// kml schema to parse coordinates from kml file for polygon geofences
Expand Down Expand Up @@ -55,16 +56,20 @@ func (p *PolygonGeofence) getEventChangeAction(tracker *Tracker) (action string)

isInsideCloseGeo := isInsidePolygonGeo(tracker.CurrentLocation, p.Close)
isInsideOpenGeo := isInsidePolygonGeo(tracker.CurrentLocation, p.Open)
isInsideRestrictedGeo := false
if len(p.Restricted) > 0 {
isInsideRestrictedGeo = isInsidePolygonGeo(tracker.CurrentLocation, p.Restricted)
}

if len(p.Close) > 0 {
if tracker.InsidePolyCloseGeo && !isInsideCloseGeo { // if we were inside the close geofence and now we're not, then close
if tracker.InsidePolyCloseGeo && !tracker.InsidePolyRestrictedGeo && !isInsideCloseGeo { // if we were inside the close geofence and now we're not, then close (if also not coming from a restricted zone)
action = ActionClose
} else if !tracker.InsidePolyCloseGeo && isInsideCloseGeo { // if we just entered the close geo, then set LastNoOpEvent to prevent flapping and accidentally triggering an open
tracker.LastEnteredCloseGeo = time.Now()
}
}
if len(p.Open) > 0 {
if !tracker.InsidePolyOpenGeo && isInsideOpenGeo { // if we were not inside the open geo and now we are, then open
if !tracker.InsidePolyOpenGeo && !tracker.InsidePolyRestrictedGeo && isInsideOpenGeo { // if we were not inside the open geo or the restricted geo, and now we are in the open geo, then open
action = ActionOpen
} else if tracker.InsidePolyOpenGeo && !isInsideOpenGeo { // if we just left the open geo, then set LastNoOpEvent to prevent flapping and accidentally triggering an open
tracker.LastLeftOpenGeo = time.Now()
Expand All @@ -73,6 +78,7 @@ func (p *PolygonGeofence) getEventChangeAction(tracker *Tracker) (action string)

tracker.InsidePolyCloseGeo = isInsideCloseGeo
tracker.InsidePolyOpenGeo = isInsideOpenGeo
tracker.InsidePolyRestrictedGeo = isInsideRestrictedGeo

return
}
Expand Down Expand Up @@ -112,7 +118,7 @@ func loadKMLFile(p *PolygonGeofence) error {
for _, placemark := range kml.Document.Placemarks {
var polygonGeoPoints []Point
// geofences must be named `open` or `close` or they're considered irrelevant
if placemark.Name != "open" && placemark.Name != "close" {
if placemark.Name != "open" && placemark.Name != "close" && placemark.Name != "restricted" {
continue
}

Expand Down Expand Up @@ -145,6 +151,8 @@ func loadKMLFile(p *PolygonGeofence) error {
p.Open = polygonGeoPoints
case "close":
p.Close = polygonGeoPoints
case "restricted":
p.Restricted = polygonGeoPoints
}
}

Expand Down
19 changes: 18 additions & 1 deletion resources/polygon_map.kml
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,30 @@
</Polygon>
</Placemark>
<Placemark id="2">
<name>restricted</name>
<ExtendedData></ExtendedData>
<Polygon>
<outerBoundaryIs>
<LinearRing>
<coordinates>
-123.7998033090239,46.192958467582514
-123.7998033090239,46.19279440766502
-123.79950958978756,46.19279440766502
-123.79950958978756,46.192958467582514
-123.7998033090239,46.192958467582514
</coordinates>
</LinearRing>
</outerBoundaryIs>
</Polygon>
</Placemark>
<Placemark id="3">
<name>close_test</name>
<ExtendedData></ExtendedData>
<Point>
<coordinates>-123.79984989897177,46.19292902096646</coordinates>
</Point>
</Placemark>
<Placemark id="3">
<Placemark id="4">
<name>open_test</name>
<ExtendedData></ExtendedData>
<Point>
Expand Down

0 comments on commit f191483

Please sign in to comment.