Initial work on per-lock management
All checks were successful
/ build-container (push) Successful in 8m57s

This commit is contained in:
Finn 2024-11-22 21:15:21 -08:00
parent 58569dee2e
commit 6ec7434ab6
9 changed files with 216 additions and 7 deletions

View file

@ -1,6 +1,6 @@
{{ template "header.html" . }} {{ template "header.html" . }}
<h1>Better Z-Wave Locks</h1> <h1>Better Z-Wave Locks</h1>
<header>Active Codes</header> <!-- <header>Active Codes</header>
[ <a href="{{ .BaseURL }}/add-code">add</a> ] [ <a href="{{ .BaseURL }}/add-code">add</a> ]
<table border="1"> <table border="1">
<tr> <tr>
@ -19,7 +19,7 @@
<td><a href="#">details</a> | <a href="#">delete</a></td> <td><a href="#">details</a> | <a href="#">delete</a></td>
</tr> </tr>
{{ end }} {{ end }}
</table> </table> -->
<br /><br /> <br /><br />
<header>locks</header> <header>locks</header>
<ul> <ul>

View file

@ -0,0 +1,7 @@
{{ template "header.html" . }}
<header>Rename {{ if eq .Data.Name "" }}Lock #{{ .Data.ID }}{{ else }}{{ .Data.Name }}{{ end }}</header>
<form method="post">
Name: <input type="text" name="name" value="{{ .Data.Name }}" /><br />
<input type="submit" value="save" />
</form>
{{ template "footer.html" }}

7
frontend/lock-edit.html Normal file
View file

@ -0,0 +1,7 @@
{{ template "header.html" . }}
<header>Rename {{ if eq .Data.Name "" }}Lock #{{ .Data.ID }}{{ else }}{{ .Data.Name }}{{ end }}</header>
<form method="post">
Name: <input type="text" name="name" value="{{ .Data.Name }}" /><br />
<input type="submit" value="save" />
</form>
{{ template "footer.html" }}

21
frontend/lock.html Normal file
View file

@ -0,0 +1,21 @@
{{ template "header.html" . }}
<header>{{ if eq .Data.lock.Name "" }}Lock #{{ .Data.lock.ID }}{{ else }}{{ .Data.lock.Name }}{{ end }}</header>
[ <a href="/locks/{{ .Data.lock.ID }}/edit">rename</a> ]
<br />
<table border="1">
<tr>
<td>Slot</td>
<td>Code</td>
<td>Enabled?</td>
<td>Actions</td>
</tr>
{{ range $_, $code := .Data.codes }}
<tr class="code-{{ if $code.Enabled }}enabled{{ else }}disabled{{ end }}">
<td>{{ $code.Slot }}</td>
<td>{{ $code.Code }}</td>
<td>{{ if $code.Enabled }}enabled{{ else }}disabled{{ end }}</td>
<td>[ <a href="/locks/{{ $.Data.lock.ID }}/codes/{{ $code.Slot }}">edit</a> ]</td>
</tr>
{{ end }}
</table>
{{ template "footer.html" }}

View file

@ -26,3 +26,7 @@ header {
font-size: 1.5em; font-size: 1.5em;
font-weight: bold; font-weight: bold;
} }
.code-enabled {
background-color: #0a0;
}

View file

@ -70,6 +70,7 @@ func addCode(c echo.Context) error {
return fmt.Errorf("error looking for empty code slot on lock %s (ZWaveDeviceID=%d ID=%d): %v", lock.Name, lock.ZwaveDeviceID, lock.ID, err) return fmt.Errorf("error looking for empty code slot on lock %s (ZWaveDeviceID=%d ID=%d): %v", lock.Name, lock.ZwaveDeviceID, lock.ID, err)
} }
logrus.WithFields(logrus.Fields{"lock": lock.ID, "code": code}).Debug("pushing code to lock")
// send the code to the lock // send the code to the lock
// sample from https://github.com/FutureTense/keymaster/blob/f4f1046bddb7901cbd3ce7820886be1ff7895fe7/tests/test_services.py#L88 // sample from https://github.com/FutureTense/keymaster/blob/f4f1046bddb7901cbd3ce7820886be1ff7895fe7/tests/test_services.py#L88
// //

View file

@ -1,11 +1,141 @@
package httpserver package httpserver
import ( import (
"errors" "fmt"
"net/http"
"strconv"
"git.janky.solutions/finn/lockserver/db"
"git.janky.solutions/finn/lockserver/zwavejs"
echo "github.com/labstack/echo/v4" echo "github.com/labstack/echo/v4"
) )
func lockHandler(c echo.Context) error { func lockHandler(c echo.Context) error {
return errors.New("not yet implemented") lockID, err := strconv.ParseInt(c.Param("lock"), 10, 32)
if err != nil {
return fmt.Errorf("invalid lock ID: %v", err)
}
ctx := c.Request().Context()
queries, dbc, err := db.Get()
if err != nil {
return err
}
defer dbc.Close()
lock, err := queries.GetLock(ctx, lockID)
if err != nil {
return err
}
codes, err := queries.GetAllLockCodesByLock(ctx, lockID)
if err != nil {
return err
}
return c.Render(http.StatusFound, "lock.html", map[string]interface{}{
"lock": lock,
"codes": codes,
})
}
func lockEditHandler(c echo.Context) error {
lockID, err := strconv.ParseInt(c.Param("lock"), 10, 32)
if err != nil {
return fmt.Errorf("invalid lock ID: %v", err)
}
ctx := c.Request().Context()
queries, dbc, err := db.Get()
if err != nil {
return err
}
defer dbc.Close()
if c.Request().Method == http.MethodGet {
lock, err := queries.GetLock(ctx, lockID)
if err != nil {
return err
}
// return frontend.Templates.ExecuteTemplate(c.Response(), "lock-edit.html", lock)
return c.Render(http.StatusFound, "lock-edit.html", lock)
}
err = queries.UpdateLockName(ctx, db.UpdateLockNameParams{
ID: lockID,
Name: c.FormValue("name"),
})
if err != nil {
return err
}
return c.Redirect(http.StatusFound, fmt.Sprintf("/locks/%d", lockID))
}
func lockCodeEditHandler(c echo.Context) error {
lockID, err := strconv.ParseInt(c.Param("lock"), 10, 32)
if err != nil {
return fmt.Errorf("invalid lock ID: %v", err)
}
slot, err := strconv.ParseInt(c.Param("slot"), 10, 32)
if err != nil {
return fmt.Errorf("invalid lock ID: %v", err)
}
ctx := c.Request().Context()
queries, dbc, err := db.Get()
if err != nil {
return err
}
defer dbc.Close()
code, err := queries.GetLockCodeBySlot(ctx, db.GetLockCodeBySlotParams{
Lock: lockID,
Slot: slot,
})
if err != nil {
return err
}
if c.Request().Method == http.MethodGet {
return c.Render(http.StatusFound, "lock-code-edit.html", code)
}
lock, err := queries.GetLock(ctx, lockID)
if err != nil {
return err
}
newCode := c.FormValue("code")
zwaveClient := c.Get(contextKeyZWaveClient).(*zwavejs.Client)
err = zwaveClient.SetNodeValue(ctx, int(lock.ZwaveDeviceID), zwavejs.NodeValue{
CCVersion: 1,
CommandClassName: zwavejs.CommandClassNameUserCode,
CommandClass: zwavejs.CommandClassUserCode,
Endpoint: 0,
Property: zwavejs.AnyType{Type: zwavejs.AnyTypeString, String: string(zwavejs.PropertyUserCode)},
PropertyName: zwavejs.AnyType{Type: zwavejs.AnyTypeString, String: string(zwavejs.PropertyUserCode)},
PropertyKey: zwavejs.AnyType{Type: zwavejs.AnyTypeInt, Int: int(slot)},
}, zwavejs.AnyType{Type: zwavejs.AnyTypeString, String: newCode})
if err != nil {
return fmt.Errorf("error pushing code to lock %s (ZWaveDeviceID=%d ID=%d): %v", lock.Name, lock.ZwaveDeviceID, lock.ID, err)
}
err = queries.UpsertCodeSlot(ctx, db.UpsertCodeSlotParams{
Lock: lockID,
Slot: slot,
Code: newCode,
Enabled: true,
})
if err != nil {
return err
}
return c.Redirect(http.StatusFound, fmt.Sprintf("/locks/%d", lockID))
} }

View file

@ -19,19 +19,24 @@ const contextKeyZWaveClient = "zwave-client"
var server *echo.Echo var server *echo.Echo
func ListenAndServe(client *zwavejs.Client) { func ListenAndServe(zwaveClient *zwavejs.Client) {
server = echo.New() server = echo.New()
server.HideBanner = true server.HideBanner = true
server.HidePort = true server.HidePort = true
server.HTTPErrorHandler = handleError server.HTTPErrorHandler = handleError
server.Renderer = &Template{}
server.Use(accessLogMiddleware) server.Use(accessLogMiddleware)
server.RouteNotFound("/*", tmpl("404.html")) server.RouteNotFound("/*", tmpl("404.html"))
server.StaticFS("/static", frontend.Static) server.StaticFS("/static", frontend.Static)
server.GET("/", indexHandler) server.GET("/", indexHandler)
server.GET("/locks/:id", lockHandler) server.GET("/locks/:lock", lockHandler)
server.GET("/locks/:lock/edit", lockEditHandler)
server.POST("/locks/:lock/edit", lockEditHandler)
server.GET("/locks/:lock/codes/:slot", lockCodeEditHandler)
server.POST("/locks/:lock/codes/:slot", lockCodeEditHandler, addZWaveClientToContextKey(zwaveClient))
server.GET("/add-code", tmpl("add-code.html")) server.GET("/add-code", tmpl("add-code.html"))
server.POST("/add-code", addCode) server.POST("/add-code", addCode, addZWaveClientToContextKey(zwaveClient))
logrus.WithField("address", config.C.HTTPBind).Info("starting http server") logrus.WithField("address", config.C.HTTPBind).Info("starting http server")
err := server.Start(config.C.HTTPBind) err := server.Start(config.C.HTTPBind)
@ -47,6 +52,7 @@ type errorTemplateData struct {
func handleError(err error, c echo.Context) { func handleError(err error, c echo.Context) {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
c.Response().WriteHeader(http.StatusNotFound)
err = frontend.Templates.ExecuteTemplate(c.Response(), "404.html", getBaseTemplateData(c.Request().Header)) err = frontend.Templates.ExecuteTemplate(c.Response(), "404.html", getBaseTemplateData(c.Request().Header))
if err == nil { if err == nil {
return return

33
httpserver/templates.go Normal file
View file

@ -0,0 +1,33 @@
package httpserver
import (
"io"
"git.janky.solutions/finn/lockserver/frontend"
echo "github.com/labstack/echo/v4"
)
type Template struct {
}
type templateData struct {
BaseURL string
Username string
UserDisplayName string
Data any
}
func buildTemplateData(c echo.Context, data any) templateData {
headers := c.Request().Header
return templateData{
BaseURL: headers.Get("X-Ingress-Path"),
Username: headers.Get("X-Remote-User-Name"),
UserDisplayName: headers.Get("X-Remote-User-Display-Name"),
Data: data,
}
}
func (t *Template) Render(w io.Writer, name string, data any, c echo.Context) error {
return frontend.Templates.ExecuteTemplate(c.Response(), name, buildTemplateData(c, data))
}