Skip to content

Commit

Permalink
feat: add pause http api command (#15)
Browse files Browse the repository at this point in the history
Adds a simple http listener with endpoints `/pause` and `/resume` GET
endpoints, listening on port `8555`. Calling these endpoints will pause
and resume garage door operations respectively. `/pause` can be called
with the optional query string argument `duration`, where the duration
is the number of seconds the operations should be paused for. Omitting
this or setting the value to `0` will pause indefinitely, until the app
is restarted or the `/resume` endpoint is called. `/resume` can also be
called to resume a timed pause operation early.

Only garage door operations are paused with this endpoint. Tracking
operations will continue during a pause.

Examples:
`curl http://geogdo-ip:8555/pause?duration=10` - pauses garage door
operations for 10 seconds
`curl http://geogdo-ip:8555/pause` - pauses indefinitely
`curl http://geogdo-ip:8555/resume` - resumes garage door operations
  • Loading branch information
brchri authored Jan 2, 2024
1 parent 505b38b commit 85a9187
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 6 deletions.
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ A lightweight app that will operate your smart Garage Door Openers (GDOs) based

- [Tesla-GeoGDO](#tesla-geogdo)
- [Supported Smart Garage Door Openers](#supported-smart-garage-door-openers)
- [Current](#current)
- [Deprecated:](#deprecated)
- [Potentially Upcoming](#potentially-upcoming)
- [Prerequisite](#prerequisite)
- [How to use](#how-to-use)
- [How to Use](#how-to-use)
- [Docker](#docker)
- [Supported Environment Variables](#supported-environment-variables)
- [API](#api)
- [Notes](#notes)
- [Geofence Types](#geofence-types)
- [Circular Geofence](#circular-geofence)
- [TeslaMate Defined Geofence](#teslamate-defined-geofence)
- [Polygon Geofence](#polygon-geofence)
- [Operation Cooldown](#operation-cooldown)
- [Credits](#credits)

Expand Down Expand Up @@ -50,6 +51,7 @@ This app is provided as a docker image. You will need to create a `config.yml` f
docker run \
-e TZ=America/New_York \
-v /etc/tesla-geogdo:/app/config \
-p 8555:8555 \
brchri/tesla-geogdo:latest
```

Expand All @@ -63,6 +65,8 @@ services:
container_name: tesla-geogdo
environment:
- TZ=America/New_York # optional, sets timezone for container
ports:
- 8555:8555 # optional, only needed to use api
volumes:
- /etc/tesla-geogdo:/app/config # required, mounts folder containing config file(s) into container
restart: unless-stopped
Expand All @@ -79,6 +83,20 @@ The following Docker environment variables are supported but not required.
| `TESTING` | Bool | Will perform all functions *except* actually operating garage door, and will just output operation *would've* happened |
| `TZ` | String | Sets timezone for container |

### API
There is a very simple API available that will allow you limited control of Tesla-GeoGDO remotely. To use it, you must expose a port mapping to port 8555 in the container (see the docker run and docker compose examples above). There are currently two endpoints available:

* `GET /pause`
* Pauses garage operations. Takes an optional `duration` parameter to define how long garage operations should be paused, in seconds
* Can override previous pause command (e.g. increase the remaining time of a pause command currently in effect)
* Examples:
* `curl http://geogdo-ip:8555/pause?duration=10` (pauses garage operations for 10 seconds)
* `curl http://geogdo-ip:8555/pause` (pauses garage operations indefinitely)
* `GET /resume`
* Resumes garage operations if they are currently paused; otherwise has no effect
* Example:
* `curl http://geogdo-ip:8555/resume`

## Notes
### Geofence Types
You can define 3 different types of geofences to trigger garage operations. You must configure *one and only one* geofence type for each garage door. Each geofence type has separate `open` and `close` configurations (though they can be set to the same values). This is useful for situations where you might want a smaller geofence that closes the door so you can visually confirm it's closing, but you want a larger geofence that opens the door so it will start sooner and be fully opened when you actually arrive.
Expand Down
102 changes: 102 additions & 0 deletions cmd/app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"path/filepath"
Expand All @@ -31,6 +32,7 @@ var (
commitHash string
messageChan chan mqtt.Message // channel to receive mqtt messages
mqttSettings *util.MqttConnectSettings // point to util.Config.Global.MqttSettings.Connection for shorter reference
pauseChan chan int // handles sending message to goroutine that pauses operations based on api calls
)

func init() {
Expand Down Expand Up @@ -95,6 +97,13 @@ func parseArgs() {
}

func main() {

// initialize api handlers
pauseChan = make(chan int)
http.HandleFunc("/pause", apiPauseHandler)
http.HandleFunc("/resume", apiPauseHandler)
go http.ListenAndServe(":8555", nil)

messageChan = make(chan mqtt.Message)

logger.Debug("Setting MQTT Opts:")
Expand Down Expand Up @@ -189,6 +198,8 @@ func main() {
case t.ComplexTopic.Topic:
logger.Debugf("Received payload for complex toipc %s for tracker %v, payload:\n%s", message.Topic(), t.ID, string(message.Payload()))
point, err = processComplexTopicPayload(t, string(message.Payload()))
default:
continue topic // no topic match for this tracker found, move on to next tracker
}

if err != nil {
Expand Down Expand Up @@ -328,3 +339,94 @@ func checkEnvVars() {
logger.Debugf(" DEBUG=%s", value)
}
}

// receives api requests related to pause and resume functions
// expects GET requests at either the /pause or /resume endpoints
// and sends to relevant helper functions for processing
func apiPauseHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}

if r.URL.Path == "/resume" {
resumeOperations()
return
}

query := r.URL.Query()
duration := query.Get("duration")
var durationInt int
if duration != "" && duration != "0" {
var err error
durationInt, err = strconv.Atoi(duration)
if err != nil {
http.Error(w, "Invalid duration parameter", http.StatusBadRequest)
return
}
}
pauseOperations(durationInt)
}

// pauses garage operations either indefinitely or for a finite duration
// all other processing still functions (e.g. tracking, geofence awareness, etc),
// only garage operations are disabled
//
// if a finite duration is provided, a goroutine will be initiated to wait for the duration
// to timeout and re-enable garage operations. this goroutine also monitors a go channel
// in case the pause duration is to be overriden, either by a new duration (finite or infinite),
// or by a resume command, and behaves accordingly
func pauseOperations(duration int) {
if duration == 0 {
duration = -1 // if no duration was defined, set to -1 for infinite pause
}
if duration > 0 {
logger.Infof("Received request to pause operations, pausing for %d seconds, use /resume endpoint to resume garage operations sooner than indicated time", duration)
} else {
logger.Info("Received request to pause operations indefinitely; use /resume endpoint to resume garage operations")
}

if util.Config.MasterOpLock > 0 { // if we have a finite lock in progress, send new duration to channel
pauseChan <- duration
return
}
util.Config.MasterOpLock = duration

// only set a timeout loop if duration > 0, negatives are infinite pauses
if util.Config.MasterOpLock > 0 {
go func() {
for ; util.Config.MasterOpLock > 0; util.Config.MasterOpLock-- {
time.Sleep(1 * time.Second)

// non-blocking select to check for channel message indicating a resume api call has been made and we can break the loop
select {
case msg := <-pauseChan:
util.Config.MasterOpLock = msg
if msg <= 0 {
// either received an indefinite pause (<0) or a resume (=0), so loop with unlock final action is no longer needed
return
}
default:
}
}
logger.Info("Pause timeout reached; unpausing operations")
util.Config.MasterOpLock = 0
}()
}
}

// helper function to resume garage operations
// if there is currently a finite pause in progress, it will send
// the new finite duration to the channel to be consumed by the currently
// runnin goroutine with the updated value; else it will set the lock back to 0,
// which is the disabled value (thereby resuming garage operations)
func resumeOperations() {
logger.Info("Received request to resume operations, resuming...")
if util.Config.MasterOpLock > 0 {
// send signal to pause timeout loop it's no longer needed
// send as goroutine as we only read channel every 1 second, so this ensures fast api response while waiting for channel to be read by loop
go func() { pauseChan <- 0 }()
} else if util.Config.MasterOpLock < 0 {
util.Config.MasterOpLock = 0 // override indefinite pause
}
}
4 changes: 4 additions & 0 deletions internal/geo/geo.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ func CheckGeofence(tracker *Tracker) {
if action == "" {
return // nothing to do
}
if util.Config.MasterOpLock != 0 {
logger.Warnf("Garage operations are currently paused due to user request, will not execute action '%s'. Use /resume api endpoint to resume garage operations", action)
return
}
if tracker.GarageDoor.OpLock {
logger.Debugf("Garage operation is locked (due to either cooldown or current activity), will not execute action '%s'", action)
return
Expand Down
5 changes: 3 additions & 2 deletions internal/util/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ type (
} `yaml:"tracker_mqtt_settings"`
OpCooldown int `yaml:"cooldown"`
} `yaml:"global"`
GarageDoors []*map[string]interface{} `yaml:"garage_doors"` // this will be parsed properly later by the geo package
Testing bool
GarageDoors []*map[string]interface{} `yaml:"garage_doors"` // this will be parsed properly later by the geo package
Testing bool
MasterOpLock int // user-initiated lock (pause), values: 0 = disabled, <0 = indefinite pause, >0 = num seconds left on finite timeout
}

MqttConnectSettings struct {
Expand Down

0 comments on commit 85a9187

Please sign in to comment.