Skip to content

Commit

Permalink
refactor the config structure
Browse files Browse the repository at this point in the history
  • Loading branch information
brchri committed Aug 5, 2023
1 parent 3e13a97 commit c829513
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 129 deletions.
105 changes: 76 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ A lightweight app that will operate your MyQ connected garage doors based on the
- [How to use](#how-to-use)
- [Docker](#docker)
- [Supported Environment Variables](#supported-environment-variables)
- [Portable App](#portable-app)
- [Notes](#notes)
- [Serials](#serials)
- [Geofence Radii](#geofence-radii)
- [Custom Geofence vs TeslaMate Geofence](#custom-geofence-vs-teslamate-geofence)
- [Triggers](#triggers)
- [Geofence Types](#geofence-types)
- [Circular Geofence](#circular-geofence)
- [TeslaMate Defined Geofence](#teslamate-defined-geofence)
- [Polygon Geofence](#polygon-geofence)
- [Operation Cooldown](#operation-cooldown)
- [Credits](#credits)

<!-- /TOC -->
Expand All @@ -23,7 +24,7 @@ This app uses the MQTT broker bundled with [TeslaMate](https://github.com/adrian

## How to use
### Docker
There is now a docker image available and will be the only supported release type going forward. You will need to download the [config.example.yml](https://github.com/brchri/tesla-youq/blob/main/config.example.yml) file and edit it appropriately, and then mount it to the container at runtime. For example:
This app is provided as a docker image. You will need to download the [config.example.yml](https://github.com/brchri/tesla-youq/blob/main/config.example.yml) file and edit it appropriately, and then mount it to the container at runtime. For example:

```bash
docker run \
Expand All @@ -39,7 +40,6 @@ Or you can use a docker compose file like this:
```yaml
version: '3.8'
services:

tesla-youq:
image: brchri/tesla-youq:latest
container_name: tesla-youq
Expand All @@ -65,22 +65,6 @@ 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 |

### Portable App
Deprecated after release `v0.1.0`. Please refer to the [Docker](#docker) instructions for more recent versions. For earlier versions, continue on.

Download the release zip containing the binary and sample `config.example.yml` file. Edit the yml file to have the settings appropriate for your use case (see Notes section below for more info).

Open a terminal (e.g. bash on linux or cmd/powershell on Windows), `cd` to the directory containing the downloaded binary, and execute it with a `-c` flag to point to your config file. Here's an example:

`tesla-youq -c /etc/tesla-youq/config.yml`

You can also set `CONFIG_FILE` environment variable to pass the config file directory:

```bash
export CONFIG_FILE=/etc/tesla-youq/config.yml
tesla-youq
```

## Notes

### Serials
Expand All @@ -100,16 +84,79 @@ Portable app:

`[email protected] MYQ_PASS=supersecretpass tesla-youq -d`

### Geofence Radii
There are separate geofence radii for opening and closing a garage. This is to facilitate closing the garage more immediately when leaving, but opening it sooner so it's already open when you arrive. This is useful due to delays in receiving positional data from the Tesla API. The recommendation is to set a larger value for `open_radius` and a smaller one for `close_radius`, but this is up to you.
### 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 you can set them to have 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.

#### Circular Geofence
This is the simplist geofence to configure, which is just a circular fence. You provide a latitude and longitude coordinate as the center point, and the distance from the center point to trigger the garage action (in kilometers). You can use a tool such as [free map tools](https://www.freemaptools.com/radius-around-point.htm) to determine what the center latitude and longitude coordinates are, as well as how big your want your radius to be. An example of a garage door configured with this type of geofence would look like this:

### Custom Geofence vs TeslaMate Geofence
As of `v0.1.2`, you can either define your own geofence triggers in the `config.yml` file (using `location`, `close_radius`, and `open_radius` parameters), or you can reference the geofences that can be created directly through the TeslaMate web UI (using `trigger_close_geofence` and `trigger_open_geofence` parameters). However, it should be noted that if using the Geofences defined in the TeslaMate web UI, Tesla-YouQ will subscribe to the Geofence topic for updates to trigger garage door actions. This topic does not appear to be updated in realtime, but rather in a polling manner, which can cause significant delays in updates that trigger a garage close action. It also does not (currently) allow for overlapping geofence definitions. It is therefore recommended to define your own geofences in the `config.yml` file rather than relying on TeslaMate's geofencing feature. See [this PR](https://github.com/brchri/tesla-youq/pull/12) for more detailed observations.
```yaml
garage_doors:
- circular_geofence:
center:
lat: 48.858195
lng: 2.294689
close_distance: .03503
open_distance: .23138
myq_serial: myq_serial_1
cars:
- teslamage_car_id: 1
```

#### TeslaMate Defined Geofence
You can choose to use geofences defined in TeslaMate. To define these geofences, go to your TeslaMate page and click `Geo-Fences` at the top, and create a new fence (or reference your existing fences). Some notes about using TeslaMate Defined Geofences:
* TeslaMate does not update its geofence calculations in realtime. *This will cause delays in your garage door operations*.
* You cannot define overlapping geofences in TeslaMate, as it will cause TeslaMate to behave unexpectedly as it cannot determine which Geofence you should be in when you're in more than one. This means you cannot define separate open and close geofences and should only use a single geofence.
* You must configure TeslaMate to have a "default" geofence when no defined geofences apply. You do this by configuring an environment variable for TeslaMate, such as `DEFAULT_GEOFENCE=not_home`.
* In general, it is not recommended to use this method as it is the least reliable due to how TeslaMate updates the Geofence data.

An example of a garage door configured with this type of geofence would look like this:

```yaml
garage_doors:
- teslamate_geofence:
close_trigger:
from: home
to: not_home
open_trigger:
from: not_home
to: home
myq_serial: myq_serial_1
cars:
- teslamage_car_id: 1
```

#### Polygon Geofence
This is the most customizable method of defining a geofence, which allows you to specifically define a polygonal geofence using a list of latitude and longitude coordinates. You can use a tool like [geojson.io](https://geojson.io/) to assist with creating a geofence and providing latitude and longitude points. An example of a garage door configured with this type of geofence would look like this:

### Triggers
Tesla-YouQ allows you to define separate geofence radii for open and close triggers. This means that there can be an overlap where, for example, you're leaving home and cross the "close door" threshold, which shares a space with the "open door" zone for when you return, as the "open door" zone is larger since you want the garage to start opening sooner so it's ready when you arrive. To account for this and remove the possibility of flapping, The close door event will only trigger when you go from *inside* the "close door" geofence and move *outside* it, indicating you are moving away from the garage. Conversely, the open door event will only trigger when you go from *outside* the "open door" geofence and move *inside* it.
```yaml
garage_doors:
- polygon_geofence:
open:
- lat: 48.8580729
lng: 2.2924089
- lat: 48.8553411
lng: 2.296679
- lat: 48.8585317
lng: 2.2964001
- lat: 48.8580729
lng: 2.2924089
close:
- lat: 48.8580603
lng: 2.2924089
- lat: 48.8553411
lng: 2.296679
- lat: 48.8585317
lng: 2.2964001
- lat: 48.8580729
lng: 2.2924089
myq_serial: myq_serial_1
cars:
- teslamage_car_id: 1
```

There is also a configurable "operation cooldown" property in the `global` section of the `config.yml` file, which will prevent any operations against a recently operated door until the number of minutes specified has passed. This can be set to 0 if you don't want any operation cooldown.
### Operation Cooldown
There's a configurable `cooldown` parameter in the `config.yml` file that will allow you to specify how many minutes Tesla-YouQ should wait after operating a garage door before it attemps any further operations. This helps prevent potential flapping if that's a concern.

## Credits
* [TeslaMate](https://github.com/adriankumpf/teslamate)
Expand Down
24 changes: 10 additions & 14 deletions cmd/app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ func init() {
util.LoadConfig(configFile)
checkEnvVars()
for _, garageDoor := range util.Config.GarageDoors {
if garageDoor.GeofenceType == "" {
log.Fatalf("error: no supported geofences defined for garage door %v", garageDoor)
}
for _, car := range garageDoor.Cars {
car.GarageDoor = garageDoor
cars = append(cars, car)
Expand Down Expand Up @@ -95,17 +98,10 @@ func main() {
}
fmt.Println()

for _, car := range cars {
if car.GarageDoor.IsPolygonGeofenceDefined() {
car.GarageDoor.GeofenceType = util.PolygonGeofence
geo.SortPointsClockwise(car.GarageDoor.PolygonOpenGeofence) // sort points to ensure simple polygon
geo.SortPointsClockwise(car.GarageDoor.PolygonCloseGeofence)
} else if car.GarageDoor.TriggerCloseGeofence.IsGeofenceDefined() && car.GarageDoor.TriggerOpenGeofence.IsGeofenceDefined() {
car.GarageDoor.GeofenceType = util.TeslamateGeofence
} else if car.GarageDoor.Location.IsPointDefined() {
car.GarageDoor.GeofenceType = util.DistanceGeofence
} else {
log.Fatalf("must define a valid location and radii for garage door or open and close geofence triggers")
for _, garageDoor := range util.Config.GarageDoors {
if garageDoor.GeofenceType == util.PolygonGeofenceType {
geo.SortPointsClockwise(garageDoor.PolygonGeofence.Open) // sort points to ensure simple polygon
geo.SortPointsClockwise(garageDoor.PolygonGeofence.Close)
}
}

Expand Down Expand Up @@ -204,11 +200,11 @@ func onMqttConnect(client mqtt.Client) {
// define which topics are relevant for each car based on config
var topics []string
switch car.GarageDoor.GeofenceType {
case util.PolygonGeofence:
case util.PolygonGeofenceType:
topics = []string{"latitude", "longitude"}
case util.DistanceGeofence:
case util.CircularGeofenceType:
topics = []string{"latitude", "longitude"}
case util.TeslamateGeofence:
case util.TeslamateGeofenceType:
topics = []string{"geofence"}
}

Expand Down
80 changes: 43 additions & 37 deletions config.example.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# In this config example, there are two garage doors we wish to control that are connected to the same account.
# The first garage door houses 2 Tesla vehicles, while the second garage door houses a third Tesla vehicle.
# This is an example config file with all available options and explanations for anything that isn't necessarily obvious.
# For a simpler example, please refer to the config.simple.example.yml file. Please also refer to the README for more information.

# In this config example, there are three garage doors we wish to control that are connected to the same account.
# The first garage door houses 2 Tesla vehicles, while the second and third garage doors house a single Tesla vehicle each.
# Anytime a garage door is operated by this app, it will wait the configured 5 minutes "cooldown" before allowing
# further operations on that specific garage door.

Expand All @@ -16,47 +19,50 @@ global:
myq_pass: super_secret_password # password to auth to myq account; can also be passed as env var MYQ_PASS

garage_doors:
- # main garage
# the following defines the latitude and longitude of the garage door location, which will be the center of the geofence radius
# you can use a tool like https://www.freemaptools.com/radius-around-point.htm to get the latitude, longitude, and radius sizes to fit your needs
location:
lat: 48.858195
lng: 2.294689
close_radius: .03503 # distance in kilometers car must travel away from garage location to close garage door
open_radius: .23138 # distance in kilometers car must be in range of garage location to open garage door
- # main garage example
circular_geofence: # circular geofence with a center point, open and close distances (radii)
center:
lat: 48.858195
lng: 2.294689
close_distance: .03503 # distance in kilometers car must travel away from garage location to close garage door
open_distance: .23138 # distance in kilometers car must be in range of garage location while traveling closer to it to open garage door
myq_serial: myq_serial_1 # serial number of garage door opener; see README for more info
cars:
cars: # list of cars that use this garage door
- teslamage_car_id: 1 # id used for the first vehicle in TeslaMate's MQTT broker
- teslamate_car_id: 2 # id used for the second vehicle in TeslaMate's MQTT broker
- # 3rd car garage
trigger_close_geofence: # uses geofences defined in teslamate; this method is less reliable and not recommended; see Notes section in the README for details
from: home
to: close_to_home
trigger_open_geofence: # uses geofences defined in teslamate; this method is less reliable and not recommended; see Notes section in the README for details
from: not_home
to: close_to_home

- # 3rd car garage example
teslamate_geofence: # uses geofences defined in teslamate; this method is less reliable and not recommended; see Notes section in the README for details
close_trigger: # define which geofence changes trigger a close action (e.g. moving from `home` geofence to `not_home`)
from: home
to: not_home
open_trigger: # define which geofence changes trigger an open action (e.g. moving from `not_home` geofence to `home`)
from: not_home
to: home
myq_serial: myq_serial_2 # serial number of garage door opener; see README for more info
cars:
- teslamage_car_id: 3 # id used for the third vehicle in TeslaMate's MQTT broker
- # 4th car detached garage
polygon_open_geofence: # custom defined polygonal geofence
- lat: 48.8580729
lng: 2.2924089
- lat: 48.8553411
lng: 2.296679
- lat: 48.8585317
lng: 2.2964001
- lat: 48.8580729
lng: 2.2924089
polygon_close_geofence: # TODO: update this to be different than open
- lat: 48.8580729
lng: 2.2924089
- lat: 48.8553411
lng: 2.296679
- lat: 48.8585317
lng: 2.2964001
- lat: 48.8580729
lng: 2.2924089

- # 4th car detached garage example
polygon_geofence: # custom defined polygonal geofence
open: # when vehicle moves from outside to inside this geofence, garage will open
- lat: 48.8580729
lng: 2.2924089
- lat: 48.8553411
lng: 2.296679
- lat: 48.8585317
lng: 2.2964001
- lat: 48.8580729
lng: 2.2924089
close: # when vehicle moves from inside to outside this geofence, garage will close
- lat: 48.8580603
lng: 2.2924089
- lat: 48.8553411
lng: 2.296679
- lat: 48.8585317
lng: 2.2964001
- lat: 48.8580729
lng: 2.2924089
myq_serial: myq_serial_3 # serial number of garage door opener; see README for more info
cars:
- teslamage_car_id: 4 # id used for the third vehicle in TeslaMate's MQTT broker
20 changes: 20 additions & 0 deletions config.simple.example.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# This config file is meant to serve as an example of how simple the config file can be if you don't need the extra options
# To keep it clean, it won't explain any parameters; please refer to the full config.example.yml file for parameter explanations

global:
mqtt_host: localhost
mqtt_port: 1883
cooldown: 5
myq_email: [email protected]
myq_pass: super_secret_password

garage_doors:
- circular_geofence:
center:
lat: 48.858195
lng: 2.294689
close_distance: .03503
open_distance: .23138
myq_serial: myq_serial_1
cars:
- teslamage_car_id: 1
24 changes: 12 additions & 12 deletions internal/geo/geo.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ func CheckGeoFence(config util.ConfigStruct, car *util.Car) {
// get action based on either geo cross events or distance threshold cross events
var action string
switch car.GarageDoor.GeofenceType {
case util.TeslamateGeofence:
case util.TeslamateGeofenceType:
action = getGeoChangeEventAction(config, car)
case util.DistanceGeofence:
case util.CircularGeofenceType:
action = getDistanceChangeAction(config, car)
case util.PolygonGeofence:
case util.PolygonGeofenceType:
action = getPolygonGeoChangeEventAction(config, car)
}

Expand Down Expand Up @@ -123,13 +123,13 @@ func getDistanceChangeAction(config util.ConfigStruct, car *util.Car) string {

// update car's current distance, and store the previous distance in a variable
prevDistance := car.CurDistance
car.CurDistance = distance(carLocation, car.GarageDoor.Location)
car.CurDistance = distance(carLocation, car.GarageDoor.CircularGeofence.Center)

// check if car has crossed a geofence and set an appropriate action
var action string
if prevDistance <= car.GarageDoor.CloseRadius && car.CurDistance > car.GarageDoor.CloseRadius { // car was within close geofence, but now beyond it (car left geofence)
if prevDistance <= car.GarageDoor.CircularGeofence.CloseDistance && car.CurDistance > car.GarageDoor.CircularGeofence.CloseDistance { // car was within close geofence, but now beyond it (car left geofence)
action = myq.ActionClose
} else if prevDistance >= car.GarageDoor.OpenRadius && car.CurDistance < car.GarageDoor.OpenRadius { // car was outside of open geofence, but is now within it (car entered geofence)
} else if prevDistance >= car.GarageDoor.CircularGeofence.OpenDistance && car.CurDistance < car.GarageDoor.CircularGeofence.OpenDistance { // car was outside of open geofence, but is now within it (car entered geofence)
action = myq.ActionOpen
}
return action
Expand All @@ -138,11 +138,11 @@ func getDistanceChangeAction(config util.ConfigStruct, car *util.Car) string {
// gets action based on if there was a relevant geofence event change
func getGeoChangeEventAction(config util.ConfigStruct, car *util.Car) string {
var action string
if car.PrevGeofence == car.GarageDoor.TriggerCloseGeofence.From &&
car.CurGeofence == car.GarageDoor.TriggerCloseGeofence.To {
if car.PrevGeofence == car.GarageDoor.TeslamateGeofence.Close.From &&
car.CurGeofence == car.GarageDoor.TeslamateGeofence.Close.To {
action = "close"
} else if car.PrevGeofence == car.GarageDoor.TriggerOpenGeofence.From &&
car.CurGeofence == car.GarageDoor.TriggerOpenGeofence.To {
} else if car.PrevGeofence == car.GarageDoor.TeslamateGeofence.Open.From &&
car.CurGeofence == car.GarageDoor.TeslamateGeofence.Open.To {
action = "open"
}
return action
Expand Down Expand Up @@ -176,8 +176,8 @@ func getPolygonGeoChangeEventAction(config util.ConfigStruct, car *util.Car) str
}

p := util.Point{Lat: car.CurLat, Lng: car.CurLng}
isInsideCloseGeo := isInsidePolygonGeo(p, car.GarageDoor.PolygonCloseGeofence)
isInsideOpenGeo := isInsidePolygonGeo(p, car.GarageDoor.PolygonOpenGeofence)
isInsideCloseGeo := isInsidePolygonGeo(p, car.GarageDoor.PolygonGeofence.Close)
isInsideOpenGeo := isInsidePolygonGeo(p, car.GarageDoor.PolygonGeofence.Open)

if car.InsideCloseGeo && !isInsideCloseGeo { // if we were inside the close geofence and now we're not, then close
action = "close"
Expand Down
Loading

0 comments on commit c829513

Please sign in to comment.