package main import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "os" "strconv" "strings" "sync" "time" "github.com/joho/godotenv" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/labstack/gommon/log" ) const TimeFormat = "2006-01-02 15:04:05" var token string // the condition is: distance >= threshold (is door open) var opened bool // is door locked var locked bool = false // alert the user when door locked and but open? var alert bool = false var gotifyToken string var gotifyURL string var mut sync.Mutex type WriteReq struct { Opened bool `json:"opened"` } func main() { _ = godotenv.Load() token = os.Getenv("TOKEN") alertRaw := strings.ToLower(os.Getenv("USE_ALERTS")) alert = alertRaw == "true" || alertRaw == "t" || alertRaw == "1" || alertRaw == "y" || alertRaw == "yes" gotifyToken = os.Getenv("GOTIFY_TOKEN") gotifyURL = os.Getenv("GOTIFY_URL") if alert && gotifyToken == "" { log.Fatal("GOTIFY_TOKEN can't be empty when alerts are enabled") } if alert && gotifyURL == "" { log.Fatal("GOTIFY_URL can't be empty when alerts are enabled") } app := echo.New() app.Logger.SetLevel(log.INFO) log.SetLevel(log.INFO) app.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ StackSize: 1 << 10, LogLevel: log.ERROR, })) app.Use(middleware.Secure()) app.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ Format: "${time_custom} ${method} ${uri} ---> ${status} in ${latency_human} (${bytes_out} bytes)\n", CustomTimeFormat: TimeFormat, })) log.SetHeader("${time_rfc3339} ${level}") app.Use(middleware.RemoveTrailingSlash()) app.OnAddRouteHandler = func(host string, route echo.Route, handler echo.HandlerFunc, middleware []echo.MiddlewareFunc) { now := time.Now() fmt.Printf("%s registered %-6s %s\n", now.Format(TimeFormat), route.Method, route.Path) } app.HTTPErrorHandler = func(err error, c echo.Context) { if c.Response().Committed { log.Error(err) return } code := http.StatusInternalServerError var response any = err.Error() var httpErr *echo.HTTPError if errors.As(err, &httpErr) { code = httpErr.Code response = httpErr } if code >= 500 { log.Error(err) } err2 := c.JSON(code, response) if err2 != nil { log.Error(err2) } } app.GET("/", func(c echo.Context) error { return c.JSON(http.StatusOK, "Hello, World!") }) app.GET("/read", authed(func(c echo.Context) error { mut.Lock() o := opened mut.Unlock() return c.JSON(http.StatusOK, o) })) app.POST("/write", authed(func(c echo.Context) error { var data WriteReq err := c.Bind(&data) if err != nil { return c.NoContent(http.StatusBadRequest) } mut.Lock() if data.Opened == opened { mut.Unlock() return c.NoContent(http.StatusOK) } opened = data.Opened if locked && alert { var action string if opened { action = "opened" } else { action = "closed" } go sendAlert(action) } mut.Unlock() return c.NoContent(http.StatusOK) })) app.GET("/locked", authed(func(c echo.Context) error { mut.Lock() l := locked mut.Unlock() return c.JSON(http.StatusOK, l) })) app.POST("/lock", authed(func(c echo.Context) error { mut.Lock() locked = true mut.Unlock() return c.NoContent(http.StatusOK) })) app.POST("/unlock", authed(func(c echo.Context) error { mut.Lock() locked = false mut.Unlock() return c.NoContent(http.StatusOK) })) app.POST("/alerts/pause", authed(func(c echo.Context) error { pauseForRaw := c.QueryParam("for") if pauseForRaw != "" { pauseFor, err := strconv.Atoi(pauseForRaw) if err != nil || pauseFor <= 0 { return c.JSON(http.StatusBadRequest, "query param 'for' not valid") } go func() { time.Sleep(time.Duration(pauseFor) * time.Second) mut.Lock() alert = true mut.Unlock() }() } mut.Lock() alert = false mut.Unlock() return c.NoContent(http.StatusOK) })) app.POST("/alerts/resume", authed(func(c echo.Context) error { mut.Lock() alert = false mut.Unlock() return c.NoContent(http.StatusOK) })) log.Fatal(app.Start(":1323")) } func authed(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if c.Request().Header.Get("Authorization") != token { return c.NoContent(http.StatusUnauthorized) } return next(c) } } func sendAlert(action string) { to := fmt.Sprintf("%s/message?token=%s", gotifyURL, gotifyToken) what := echo.Map{ "title": fmt.Sprintf("Your door has been %s", action), "priority": 5, "message": fmt.Sprintf("Your locked door has been %s at %s", action, time.Now().Format(TimeFormat)), } b, err := json.Marshal(&what) if err != nil { log.Error(err) return } res, err := http.Post(to, "application/json", bytes.NewBuffer(b)) if err != nil { log.Error(err) return } body, err := io.ReadAll(res.Body) if err != nil { log.Error(err) return } if res.StatusCode != 200 { log.Errorf("sendAlert response status code is not 200: %d, body:\n%s", res.StatusCode, body) } else { log.Infof("sent alert (%s) to gotify", action) } }