Initial work on per-lock management
All checks were successful
/ build-container (push) Successful in 8m57s
All checks were successful
/ build-container (push) Successful in 8m57s
This commit is contained in:
parent
58569dee2e
commit
6ec7434ab6
9 changed files with 216 additions and 7 deletions
|
@ -1,6 +1,6 @@
|
|||
{{ template "header.html" . }}
|
||||
<h1>Better Z-Wave Locks</h1>
|
||||
<header>Active Codes</header>
|
||||
<!-- <header>Active Codes</header>
|
||||
[ <a href="{{ .BaseURL }}/add-code">add</a> ]
|
||||
<table border="1">
|
||||
<tr>
|
||||
|
@ -19,7 +19,7 @@
|
|||
<td><a href="#">details</a> | <a href="#">delete</a></td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</table>
|
||||
</table> -->
|
||||
<br /><br />
|
||||
<header>locks</header>
|
||||
<ul>
|
||||
|
|
7
frontend/lock-code-edit.html
Normal file
7
frontend/lock-code-edit.html
Normal 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
7
frontend/lock-edit.html
Normal 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
21
frontend/lock.html
Normal 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" }}
|
|
@ -26,3 +26,7 @@ header {
|
|||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.code-enabled {
|
||||
background-color: #0a0;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{"lock": lock.ID, "code": code}).Debug("pushing code to lock")
|
||||
// send the code to the lock
|
||||
// sample from https://github.com/FutureTense/keymaster/blob/f4f1046bddb7901cbd3ce7820886be1ff7895fe7/tests/test_services.py#L88
|
||||
//
|
||||
|
|
|
@ -1,11 +1,141 @@
|
|||
package httpserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"git.janky.solutions/finn/lockserver/db"
|
||||
"git.janky.solutions/finn/lockserver/zwavejs"
|
||||
echo "github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
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))
|
||||
}
|
||||
|
|
|
@ -19,19 +19,24 @@ const contextKeyZWaveClient = "zwave-client"
|
|||
|
||||
var server *echo.Echo
|
||||
|
||||
func ListenAndServe(client *zwavejs.Client) {
|
||||
func ListenAndServe(zwaveClient *zwavejs.Client) {
|
||||
server = echo.New()
|
||||
server.HideBanner = true
|
||||
server.HidePort = true
|
||||
server.HTTPErrorHandler = handleError
|
||||
server.Renderer = &Template{}
|
||||
server.Use(accessLogMiddleware)
|
||||
server.RouteNotFound("/*", tmpl("404.html"))
|
||||
|
||||
server.StaticFS("/static", frontend.Static)
|
||||
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.POST("/add-code", addCode)
|
||||
server.POST("/add-code", addCode, addZWaveClientToContextKey(zwaveClient))
|
||||
|
||||
logrus.WithField("address", config.C.HTTPBind).Info("starting http server")
|
||||
err := server.Start(config.C.HTTPBind)
|
||||
|
@ -47,6 +52,7 @@ type errorTemplateData struct {
|
|||
|
||||
func handleError(err error, c echo.Context) {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
c.Response().WriteHeader(http.StatusNotFound)
|
||||
err = frontend.Templates.ExecuteTemplate(c.Response(), "404.html", getBaseTemplateData(c.Request().Header))
|
||||
if err == nil {
|
||||
return
|
||||
|
|
33
httpserver/templates.go
Normal file
33
httpserver/templates.go
Normal 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))
|
||||
}
|
Loading…
Reference in a new issue