Initial work on per-lock management
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" . }}
|
{{ 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>
|
||||||
|
|
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-size: 1.5em;
|
||||||
font-weight: bold;
|
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)
|
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
|
||||||
//
|
//
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
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