This commit is contained in:
Finn 2024-11-22 19:50:18 -08:00
parent b957988957
commit 58569dee2e
27 changed files with 623 additions and 220 deletions

View file

@ -8,7 +8,8 @@ jobs:
- run: apk add --no-cache nodejs git
- name: login to container registry
run: echo "${{ secrets.PACKAGE_PUBLISH_TOKEN }}" | docker login --username finn --password-stdin git.janky.solutions
- uses: https://github.com/docker/metadata-action@v5
- name: gather metadata for container image tags
uses: https://github.com/docker/metadata-action@v5
id: meta
with:
images: git.janky.solutions/finn/lockserver

View file

@ -9,15 +9,17 @@ import (
)
type Config struct {
ZWaveJSServer string `json:"zwave-js-server"`
SqliteDatabase string `json:"sqlite-database"`
HTTPBind string `json:"http-bind"`
ZWaveJSServer string `json:"zwave-js-server"`
SqliteDatabase string `json:"sqlite-database"`
HTTPBind string `json:"http-bind"`
GeneratedCodeLength int `json:"generated-code-length"`
}
var C = Config{
ZWaveJSServer: "ws://addon_core_zwave_js:3000",
SqliteDatabase: "/data/lockserver.db",
HTTPBind: ":8099",
ZWaveJSServer: "ws://addon_core_zwave_js:3000",
SqliteDatabase: "/data/lockserver.db",
HTTPBind: ":8099",
GeneratedCodeLength: 10,
}
var configFiles = []string{"lockserver.json", "/etc/lockserver.json"}

View file

@ -12,8 +12,10 @@ import (
"git.janky.solutions/finn/lockserver/config"
)
const sqliteParams = "?_pragma=foreign_keys(1)&_time_format=sqlite"
func Get() (*Queries, *sql.DB, error) {
db, err := sql.Open("sqlite", config.C.SqliteDatabase)
db, err := sql.Open("sqlite", config.C.SqliteDatabase+sqliteParams)
if err != nil {
return nil, nil, err
}

138
db/issued_codes.sql.go Normal file
View file

@ -0,0 +1,138 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.20.0
// source: issued_codes.sql
package db
import (
"context"
"database/sql"
)
const assignIssuedCodeSlot = `-- name: AssignIssuedCodeSlot :exec
INSERT INTO issued_code_slots (issued_code, lock, slot) VALUES (?, ?, ?)
`
type AssignIssuedCodeSlotParams struct {
IssuedCode interface{}
Lock interface{}
Slot interface{}
}
func (q *Queries) AssignIssuedCodeSlot(ctx context.Context, arg AssignIssuedCodeSlotParams) error {
_, err := q.db.ExecContext(ctx, assignIssuedCodeSlot, arg.IssuedCode, arg.Lock, arg.Slot)
return err
}
const createIssuedCode = `-- name: CreateIssuedCode :one
INSERT INTO issued_codes (name, code, start, end) VALUES (?, ?, ?, ?) RETURNING id
`
type CreateIssuedCodeParams struct {
Name sql.NullString
Code string
Start sql.NullTime
End sql.NullTime
}
func (q *Queries) CreateIssuedCode(ctx context.Context, arg CreateIssuedCodeParams) (int64, error) {
row := q.db.QueryRowContext(ctx, createIssuedCode,
arg.Name,
arg.Code,
arg.Start,
arg.End,
)
var id int64
err := row.Scan(&id)
return id, err
}
const deleteIssuedCode = `-- name: DeleteIssuedCode :exec
DELETE FROM issued_codes WHERE id = ?
`
func (q *Queries) DeleteIssuedCode(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, deleteIssuedCode, id)
return err
}
const getActiveCodes = `-- name: GetActiveCodes :many
SELECT id, name, code, start, "end" FROM issued_codes WHERE start < datetime('now') AND end > datetime('now')
`
func (q *Queries) GetActiveCodes(ctx context.Context) ([]IssuedCode, error) {
rows, err := q.db.QueryContext(ctx, getActiveCodes)
if err != nil {
return nil, err
}
defer rows.Close()
var items []IssuedCode
for rows.Next() {
var i IssuedCode
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Code,
&i.Start,
&i.End,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAllIssuedCodes = `-- name: GetAllIssuedCodes :many
SELECT id, name, code, start, "end" FROM issued_codes
`
func (q *Queries) GetAllIssuedCodes(ctx context.Context) ([]IssuedCode, error) {
rows, err := q.db.QueryContext(ctx, getAllIssuedCodes)
if err != nil {
return nil, err
}
defer rows.Close()
var items []IssuedCode
for rows.Next() {
var i IssuedCode
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Code,
&i.Start,
&i.End,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const unassignIssuedCodeSlot = `-- name: UnassignIssuedCodeSlot :exec
DELETE FROM issued_code_slots WHERE issued_code = ? AND lock = ?
`
type UnassignIssuedCodeSlotParams struct {
IssuedCode interface{}
Lock interface{}
}
func (q *Queries) UnassignIssuedCodeSlot(ctx context.Context, arg UnassignIssuedCodeSlotParams) error {
_, err := q.db.ExecContext(ctx, unassignIssuedCodeSlot, arg.IssuedCode, arg.Lock)
return err
}

View file

@ -9,6 +9,69 @@ import (
"context"
)
const countUsedSlots = `-- name: CountUsedSlots :one
SELECT COUNT(*) FROM lock_code_slots WHERE lock = ? AND enabled = 1
`
func (q *Queries) CountUsedSlots(ctx context.Context, lock int64) (int64, error) {
row := q.db.QueryRowContext(ctx, countUsedSlots, lock)
var count int64
err := row.Scan(&count)
return count, err
}
const getAllLockCodesByLock = `-- name: GetAllLockCodesByLock :many
SELECT id, lock, code, slot, name, enabled FROM lock_code_slots WHERE lock = ?
`
func (q *Queries) GetAllLockCodesByLock(ctx context.Context, lock int64) ([]LockCodeSlot, error) {
rows, err := q.db.QueryContext(ctx, getAllLockCodesByLock, lock)
if err != nil {
return nil, err
}
defer rows.Close()
var items []LockCodeSlot
for rows.Next() {
var i LockCodeSlot
if err := rows.Scan(
&i.ID,
&i.Lock,
&i.Code,
&i.Slot,
&i.Name,
&i.Enabled,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getEmptySlot = `-- name: GetEmptySlot :one
SELECT id, lock, code, slot, name, enabled FROM lock_code_slots WHERE lock = ? AND enabled = 0 LIMIT 1
`
func (q *Queries) GetEmptySlot(ctx context.Context, lock int64) (LockCodeSlot, error) {
row := q.db.QueryRowContext(ctx, getEmptySlot, lock)
var i LockCodeSlot
err := row.Scan(
&i.ID,
&i.Lock,
&i.Code,
&i.Slot,
&i.Name,
&i.Enabled,
)
return i, err
}
const getLockCodeBySlot = `-- name: GetLockCodeBySlot :one
SELECT id, lock, code, slot, name, enabled FROM lock_code_slots WHERE lock = ? AND slot = ?
`
@ -32,12 +95,12 @@ func (q *Queries) GetLockCodeBySlot(ctx context.Context, arg GetLockCodeBySlotPa
return i, err
}
const getLockCodes = `-- name: GetLockCodes :many
SELECT id, lock, code, slot, name, enabled FROM lock_code_slots WHERE lock = ?
const getLockCodesByCode = `-- name: GetLockCodesByCode :many
SELECT id, lock, code, slot, name, enabled FROM lock_code_slots WHERE code = ?
`
func (q *Queries) GetLockCodes(ctx context.Context, lock int64) ([]LockCodeSlot, error) {
rows, err := q.db.QueryContext(ctx, getLockCodes, lock)
func (q *Queries) GetLockCodesByCode(ctx context.Context, code string) ([]LockCodeSlot, error) {
rows, err := q.db.QueryContext(ctx, getLockCodesByCode, code)
if err != nil {
return nil, err
}

View file

@ -26,7 +26,7 @@ func (q *Queries) AddLogEntry(ctx context.Context, arg AddLogEntryParams) error
}
const getLogForLock = `-- name: GetLogForLock :many
SELECT lock, timestamp, state, code FROM lock_log WHERE lock = ? ORDER BY timestamp DESC
SELECT lock, timestamp, state, code, issued_code FROM lock_log WHERE lock = ? ORDER BY timestamp DESC
`
func (q *Queries) GetLogForLock(ctx context.Context, lock int64) ([]LockLog, error) {
@ -43,6 +43,7 @@ func (q *Queries) GetLogForLock(ctx context.Context, lock int64) ([]LockLog, err
&i.Timestamp,
&i.State,
&i.Code,
&i.IssuedCode,
); err != nil {
return nil, err
}

View file

@ -23,37 +23,31 @@ CREATE TABLE lock_log (
lock INTEGER NOT NULL REFERENCES locks(id),
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
state TEXT NOT NULL,
code INTEGER REFERENCES lock_code_slots(id)
code INTEGER REFERENCES lock_code_slots(id),
issued_code INTEGER REFERENCES issued_codes(id)
);
CREATE TABLE users (
CREATE TABLE issued_codes (
id INTEGER PRIMARY KEY,
name TEXT UNIQUE NOT NULL
);
CREATE TABLE user_codes (
id INTEGER PRIMARY KEY,
user INTEGER REFERENCES users(id),
name TEXT,
code TEXT UNIQUE NOT NULL,
start DATETIME,
end DATETIME
);
CREATE TABLE user_code_slots (
user_code REFERENCES user_codes(id),
CREATE TABLE issued_code_slots (
issued_code REFERENCES issued_codes(id),
lock REFERENCES locks(id),
slot REFERENCES lock_code_slots(id),
UNIQUE (user_code, lock)
UNIQUE (issued_code, lock)
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE user_code_slots;
DROP TABLE user_codes;
DROP TABLE users;
DROP TABLE issued_code_slots;
DROP TABLE issued_codes;
DROP TABLE lock_log;
DROP TABLE lock_code_slots;
DROP TABLE locks;

View file

@ -9,6 +9,20 @@ import (
"time"
)
type IssuedCode struct {
ID int64
Name sql.NullString
Code string
Start sql.NullTime
End sql.NullTime
}
type IssuedCodeSlot struct {
IssuedCode interface{}
Lock interface{}
Slot interface{}
}
type Lock struct {
ID int64
Name string
@ -25,28 +39,9 @@ type LockCodeSlot struct {
}
type LockLog struct {
Lock int64
Timestamp time.Time
State string
Code sql.NullInt64
}
type User struct {
ID int64
Name string
}
type UserCode struct {
ID int64
User sql.NullInt64
Name sql.NullString
Code string
Start sql.NullTime
End sql.NullTime
}
type UserCodeSlot struct {
UserCode interface{}
Lock interface{}
Slot interface{}
Lock int64
Timestamp time.Time
State string
Code sql.NullInt64
IssuedCode sql.NullInt64
}

View file

@ -0,0 +1,17 @@
-- name: CreateIssuedCode :one
INSERT INTO issued_codes (name, code, start, end) VALUES (?, ?, ?, ?) RETURNING id;
-- name: DeleteIssuedCode :exec
DELETE FROM issued_codes WHERE id = ?;
-- name: AssignIssuedCodeSlot :exec
INSERT INTO issued_code_slots (issued_code, lock, slot) VALUES (?, ?, ?);
-- name: UnassignIssuedCodeSlot :exec
DELETE FROM issued_code_slots WHERE issued_code = ? AND lock = ?;
-- name: GetAllIssuedCodes :many
SELECT * FROM issued_codes;
-- name: GetActiveCodes :many
SELECT * FROM issued_codes WHERE start < datetime('now') AND end > datetime('now');

View file

@ -4,5 +4,14 @@ INSERT INTO lock_code_slots (lock, slot, code, enabled, name) VALUES (?, ?, ?, ?
-- name: GetLockCodeBySlot :one
SELECT * FROM lock_code_slots WHERE lock = ? AND slot = ?;
-- name: GetLockCodes :many
-- name: GetAllLockCodesByLock :many
SELECT * FROM lock_code_slots WHERE lock = ?;
-- name: GetLockCodesByCode :many
SELECT * FROM lock_code_slots WHERE code = ?;
-- name: GetEmptySlot :one
SELECT * FROM lock_code_slots WHERE lock = ? AND enabled = 0 LIMIT 1;
-- name: CountUsedSlots :one
SELECT COUNT(*) FROM lock_code_slots WHERE lock = ? AND enabled = 1;

View file

@ -1,14 +0,0 @@
-- name: CreateUserCode :one
INSERT INTO user_codes (user, code, start, end) VALUES (?, ?, ?, ?) RETURNING id;
-- name: DeleteUserCode :exec
DELETE FROM user_codes WHERE id = ?;
-- name: AssignUserCodeSlot :exec
INSERT INTO user_code_slots (user_code, lock, slot) VALUES (?, ?, ?);
-- name: UnassignUserCodeSlot :exec
DELETE FROM user_code_slots WHERE user_code = ? AND lock = ?;
-- name: GetAllUserCodes :many
SELECT user_codes.*, users.name FROM user_codes, users WHERE user_codes.user = users.id;

View file

@ -1,8 +0,0 @@
-- name: CreateUser :exec
INSERT INTO users (name) VALUES (?);
-- name: GetUserByName :one
SELECT * FROM users WHERE name = ?;
-- name: GetUserByID :one
SELECT * FROM users WHERE id = ?;

View file

@ -1,41 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.20.0
// source: users.sql
package db
import (
"context"
)
const createUser = `-- name: CreateUser :exec
INSERT INTO users (name) VALUES (?)
`
func (q *Queries) CreateUser(ctx context.Context, name string) error {
_, err := q.db.ExecContext(ctx, createUser, name)
return err
}
const getUserByID = `-- name: GetUserByID :one
SELECT id, name FROM users WHERE id = ?
`
func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
row := q.db.QueryRowContext(ctx, getUserByID, id)
var i User
err := row.Scan(&i.ID, &i.Name)
return i, err
}
const getUserByName = `-- name: GetUserByName :one
SELECT id, name FROM users WHERE name = ?
`
func (q *Queries) GetUserByName(ctx context.Context, name string) (User, error) {
row := q.db.QueryRowContext(ctx, getUserByName, name)
var i User
err := row.Scan(&i.ID, &i.Name)
return i, err
}

4
frontend/404.html Normal file
View file

@ -0,0 +1,4 @@
{{ template "header.html" . }}
<h3>404</h3>
<p>page could not be found, maybe you want to <a href="{{ .BaseURL }}/">return home</a>.</p>
{{ template "footer.html" }}

6
frontend/500.html Normal file
View file

@ -0,0 +1,6 @@
{{ template "header.html" . }}
<h3>Unexpected Error</h3>
<p>An error occured while trying to handle your request, maybe you should <a href="{{ .BaseURL }}/">return home</a>.</p>
<br />
<pre>{{ .Error }}</pre>
{{ template "footer.html" }}

9
frontend/add-code.html Normal file
View file

@ -0,0 +1,9 @@
{{ template "header.html" . }}
<header>Add Code</header>
<form method="post">
<label for="name">Name: <input type="text" name="name" id="name" /></label><br />
<label for="code">Code: <input type="text" name="code" id="code" /></label><br />
<br />
<input type="submit" value="add code" />
</form>
{{ template "footer.html" }}

7
frontend/footer.html Normal file
View file

@ -0,0 +1,7 @@
</div>
<footer>
<code>{{ version }}</code>
</footer>
</div>
</body>
</html>

View file

@ -3,10 +3,7 @@ package frontend
import (
"embed"
"html/template"
"io"
"io/fs"
"github.com/sirupsen/logrus"
)
var (
@ -19,9 +16,7 @@ var (
Templates *template.Template
funcs = template.FuncMap{
"version": func() string {
return "better-zwave-locks v0.x.x aaaaaaaa"
},
"version": func() string { return "better-zwave-locks v0.x.x aaaaaaaa" },
}
)
@ -39,23 +34,3 @@ func init() {
panic(err)
}
}
func staticFn(filename string) (string, error) {
f, err := Static.Open(filename)
if err != nil {
return "", err
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return "", err
}
logrus.WithFields(logrus.Fields{
"filename": filename,
"data": string(data),
}).Debug("reading static file for template")
return string(data), nil
}

13
frontend/header.html Normal file
View file

@ -0,0 +1,13 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Better Z-Wave Locks for Home Assistant</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<link rel="stylesheet" href="{{ .BaseURL }}/static/main.css" />
</head>
<body>
<div id="wrapper">
<div id="main">

View file

@ -1,29 +1,28 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Better Z-Wave Locks for Home Assistant</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<link rel="stylesheet" href="{{ .BaseURL }}/static/main.css" />
</head>
<body>
<div id="wrapper">
<header>
<h1>Better Z-Wave Locks</h1>
<div id="user"></div>
</header>
<div id="main">
<h3>locks</h3>
<ul>
{{ range .Locks }}<li><a href="/lock/{{ .ID }}">{{ .Name }} ({{ .ZwaveDeviceID }})</a></li>{{ end }}
</ul>
</div>
<footer>
<code>{{ version }}</code>
</footer>
</div>
</body>
</html>
{{ template "header.html" . }}
<h1>Better Z-Wave Locks</h1>
<header>Active Codes</header>
[ <a href="{{ .BaseURL }}/add-code">add</a> ]
<table border="1">
<tr>
<th>Name</th>
<th>Code</th>
<th>Last used</th>
<th>Expires</th>
<th>Actions</th>
</tr>
{{ range .ActiveCodes }}
<tr>
<td>{{ .Name.Value }}</td>
<td>{{ .Code }}</td>
<td>0.005ms ago on <a href="{{ $.BaseURL }}/locks/1">Back Door</a></td>
<td>{{ .End.Value }}</td>
<td><a href="#">details</a> | <a href="#">delete</a></td>
</tr>
{{ end }}
</table>
<br /><br />
<header>locks</header>
<ul>
{{ range .Locks }}<li><a href="{{ $.BaseURL }}/locks/{{ .ID }}">{{ .Name }} ({{ .ZwaveDeviceID }})</a></li>{{ end }}
</ul>
{{ template "footer.html" }}

View file

@ -3,6 +3,7 @@ html, body {
margin: 0;
background-color: #1c1c1c;
color: #fff;
font-family: sans-serif;
}
a {
@ -11,8 +12,17 @@ a {
#wrapper {
min-height: calc(100% - 2em);
display: grid;
grid-template-rows: auto 1fr auto;
display: flex;
flex-direction: column;
row-gap: 1em;
padding: 1em;
}
#main {
flex-grow: 1;
}
header {
font-size: 1.5em;
font-weight: bold;
}

126
httpserver/codes.go Normal file
View file

@ -0,0 +1,126 @@
package httpserver
import (
"database/sql"
"errors"
"fmt"
"math/rand"
"strings"
"git.janky.solutions/finn/lockserver/config"
"git.janky.solutions/finn/lockserver/db"
"git.janky.solutions/finn/lockserver/zwavejs"
"github.com/labstack/echo/v4"
"github.com/sirupsen/logrus"
)
func addCode(c echo.Context) error {
// name := c.FormValue("name")
code := c.FormValue("code")
queries, dbc, err := db.Get()
if err != nil {
return err
}
defer dbc.Close()
ctx := c.Request().Context()
// generate a code that isn't used on any lock or if the user
// supplied a code, check if it's already used on any lock and
// error out if it is
noCodeSupplied := code == "" // if no code is supplied, we must generate one
for {
if noCodeSupplied {
code = generateCode()
logrus.WithField("code", code).Debug("generated code")
}
row, err := queries.GetLockCodesByCode(ctx, code)
if err != nil {
return fmt.Errorf("error checking if code was already in use: %v", err)
}
if len(row) == 0 {
break
}
if noCodeSupplied {
continue
}
return fmt.Errorf("requested code (%s) is already in use on some or all locks", code)
}
locks, err := queries.GetLocks(ctx)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return errors.New("no locks currently registered, cannot add code")
}
return fmt.Errorf("error getting locks: %v", err)
}
client := c.Get(contextKeyZWaveClient).(*zwavejs.Client)
for _, lock := range locks {
slot, err := queries.GetEmptySlot(ctx, lock.ID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("no empty code slots found on lock %s (ZWaveDeviceID=%d ID=%d)", lock.Name, lock.ZwaveDeviceID, lock.ID)
}
return fmt.Errorf("error looking for empty code slot on lock %s (ZWaveDeviceID=%d ID=%d): %v", lock.Name, lock.ZwaveDeviceID, lock.ID, err)
}
// send the code to the lock
// sample from https://github.com/FutureTense/keymaster/blob/f4f1046bddb7901cbd3ce7820886be1ff7895fe7/tests/test_services.py#L88
//
// {
// "ccVersion": 1,
// "commandClassName": "User Code",
// "commandClass": 99,
// "endpoint": 0,
// "property": "userCode",
// "propertyName": "userCode",
// "propertyKey": 1,
// "propertyKeyName": "1",
// "metadata": {
// "type": "string",
// "readable": True,
// "writeable": True,
// "minLength": 4,
// "maxLength": 10,
// "label": "User Code (1)",
// },
// "value": "123456",
// }
err = client.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.ID)},
}, zwavejs.AnyType{Type: zwavejs.AnyTypeString, String: code})
if err != nil {
return fmt.Errorf("error pushing code to lock %s (ZWaveDeviceID=%d ID=%d): %v", lock.Name, lock.ZwaveDeviceID, lock.ID, err)
}
// set the code on the lock
// set the code in the db
}
// add row to user_codes
// add row to user_code_slots
// redirect to /user-codes/<code id>
return errors.New("not yet implemented")
}
var codeCharSet = "0123456789"
func generateCode() string {
var builder strings.Builder
for i := 0; i < config.C.GeneratedCodeLength; i++ {
builder.WriteByte(codeCharSet[rand.Int31n(int32(len(codeCharSet)))])
}
return builder.String()
}

View file

@ -3,7 +3,6 @@ package httpserver
import (
"database/sql"
"errors"
"net/http"
echo "github.com/labstack/echo/v4"
@ -11,45 +10,11 @@ import (
"git.janky.solutions/finn/lockserver/frontend"
)
type baseTemplateData struct {
BaseURL string
Username string
UserDisplayName string
}
type indexTemplateData struct {
baseTemplateData
Locks []db.Lock
}
// header=X-Forwarded-Proto value="[http]"
// header=Accept value="[text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8]"
// header=Referer value="[https://ha.herzfeld.casa/hassio/store]"
// header=X-Forwarded-For value="[10.5.0.235, 10.5.1.245, 172.30.32.1]"
// header=X-Forwarded-Host value="[ha.herzfeld.casa]"
// header=Sec-Fetch-Mode value="[same-origin]"
// header=X-Ingress-Path value="[/api/hassio_ingress/VBVnN1BaXJun6ydy3xCuRoy1HyGH__attJ-gN193OU0]"
// header=Accept-Encoding value="[gzip, deflate, br]"
// header=Content-Length value="[0]"
// header=X-Remote-User-Id value="[a65ffe9ec5664336b4c08def63384aa0]"
// header=Accept-Language value="[en-US,en;q=0.5]"
// header=Upgrade-Insecure-Requests value="[1]"
// header=Sec-Fetch-Dest value="[empty]"
// header=X-Hass-Source value="[core.ingress]"
// header=X-Remote-User-Name value="[finn]"
// header=X-Remote-User-Display-Name value="[Finn]"
// header=Cookie value="[ingress_session=3dcd2006c013774475c5fec0a4301689803eacd343a3554ba2d920440bced0d973808524139c998d74b614e7950fe5f08cead286cde56e3541a3980b7d506a85]"
// header=User-Agent value="[Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0]"
// header=Sec-Fetch-Site value="[same-origin]"
// header=Connection value="[close]"
func getBaseTemplateData(headers http.Header) baseTemplateData {
return baseTemplateData{
BaseURL: headers.Get("X-Ingress-Path"),
Username: headers.Get("X-Remote-User-Name"),
UserDisplayName: headers.Get("X-Remote-User-Display-Name"),
}
ActiveCodes []db.IssuedCode
Locks []db.Lock
}
func indexHandler(c echo.Context) error {
@ -59,13 +24,21 @@ func indexHandler(c echo.Context) error {
}
defer dbc.Close()
locks, err := queries.GetLocks(c.Request().Context())
ctx := c.Request().Context()
activeCodes, err := queries.GetActiveCodes(ctx)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return err
}
locks, err := queries.GetLocks(ctx)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return err
}
return frontend.Templates.ExecuteTemplate(c.Response(), "index.html", indexTemplateData{
baseTemplateData: getBaseTemplateData(c.Request().Header),
ActiveCodes: activeCodes,
Locks: locks,
})
}

View file

@ -15,11 +15,9 @@ import (
"git.janky.solutions/finn/lockserver/zwavejs"
)
var server *echo.Echo
const contextKeyZWaveClient = "zwave-client"
type lockserver struct {
ZWaveJS *zwavejs.Client
}
var server *echo.Echo
func ListenAndServe(client *zwavejs.Client) {
server = echo.New()
@ -27,10 +25,13 @@ func ListenAndServe(client *zwavejs.Client) {
server.HidePort = true
server.HTTPErrorHandler = handleError
server.Use(accessLogMiddleware)
server.RouteNotFound("/*", tmpl("404.html"))
server.StaticFS("/static", frontend.Static)
server.GET("/", indexHandler)
server.GET("/locks/{id}", lockHandler)
server.GET("/locks/:id", lockHandler)
server.GET("/add-code", tmpl("add-code.html"))
server.POST("/add-code", addCode)
logrus.WithField("address", config.C.HTTPBind).Info("starting http server")
err := server.Start(config.C.HTTPBind)
@ -39,10 +40,17 @@ func ListenAndServe(client *zwavejs.Client) {
}
}
type errorTemplateData struct {
baseTemplateData
Error error
}
func handleError(err error, c echo.Context) {
if errors.Is(err, sql.ErrNoRows) {
_ = c.JSON(http.StatusNotFound, map[string]string{"error": "not found"})
return
err = frontend.Templates.ExecuteTemplate(c.Response(), "404.html", getBaseTemplateData(c.Request().Header))
if err == nil {
return
}
}
logrus.WithFields(logrus.Fields{
@ -50,7 +58,10 @@ func handleError(err error, c echo.Context) {
"method": c.Request().Method,
"error": err,
}).Error("error handling request")
_ = c.JSON(http.StatusInternalServerError, map[string]string{"error": "internal server error"})
frontend.Templates.ExecuteTemplate(c.Response(), "500.html", errorTemplateData{
baseTemplateData: getBaseTemplateData(c.Request().Header),
Error: err,
})
}
func Shutdown(ctx context.Context) error {
@ -81,3 +92,32 @@ func accessLogMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return err
}
}
func addZWaveClientToContextKey(client *zwavejs.Client) func(echo.HandlerFunc) echo.HandlerFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Set(contextKeyZWaveClient, client)
return next(c)
}
}
}
type baseTemplateData struct {
BaseURL string
Username string
UserDisplayName string
}
func getBaseTemplateData(headers http.Header) baseTemplateData {
return baseTemplateData{
BaseURL: headers.Get("X-Ingress-Path"),
Username: headers.Get("X-Remote-User-Name"),
UserDisplayName: headers.Get("X-Remote-User-Display-Name"),
}
}
func tmpl(filename string) echo.HandlerFunc {
return func(c echo.Context) error {
return frontend.Templates.ExecuteTemplate(c.Response(), filename, getBaseTemplateData(c.Request().Header))
}
}

View file

@ -94,7 +94,7 @@ func (c *Client) listen(ctx context.Context) error {
case "result":
if msg.MessageID == "" {
if err := syncState(ctx, *msg.Result); err != nil {
return err
return fmt.Errorf("error syncing state from zwavejs server: %v", err)
}
} else {
c.handleCallback(msg.MessageID, *msg.Result)

View file

@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/sirupsen/logrus"
)
@ -64,3 +65,84 @@ func (n AnyType) MarshalJSON() ([]byte, error) {
return nil, errors.New("anytype of unknown type")
}
}
type NodeValueTypeType int
const (
NodeValueTypeString NodeValueTypeType = iota
NodeValueTypeInt
NodeValueTypeBool
NodeValueTypeList
NodeValueTypeIntWithUnit
)
type NodeValueType struct {
Type NodeValueTypeType
String string
Int int
Bool bool
List []NodeValueType
}
func (n *NodeValueType) UnmarshalJSON(data []byte) error {
if bytes.HasPrefix(data, []byte("\"")) {
n.Type = NodeValueTypeString
return json.Unmarshal(data, &n.String)
}
if bytes.Equal(data, []byte("true")) || bytes.Equal(data, []byte("false")) {
n.Type = NodeValueTypeBool
return json.Unmarshal(data, &n.Bool)
}
if bytes.HasPrefix(data, []byte("[")) {
n.Type = NodeValueTypeList
return json.Unmarshal(data, &n.List)
}
if bytes.HasPrefix(data, []byte("{")) {
var value map[string]interface{}
if err := json.Unmarshal(data, &value); err != nil {
return fmt.Errorf("error unmarshalling NodeValueType object %s: %v", string(data), err)
}
if intval, ok := value["value"].(int); ok {
n.Int = intval
}
if unit, ok := value["unit"].(string); ok {
n.String = unit
}
n.Type = NodeValueTypeIntWithUnit
return nil
}
n.Type = NodeValueTypeInt
if err := json.Unmarshal(data, &n.Int); err != nil {
logrus.WithField("value", string(data)).Debug("error while parsing node property value of ambiguous type")
return err
}
return nil
}
func (n NodeValueType) MarshalJSON() ([]byte, error) {
switch n.Type {
case NodeValueTypeString:
return json.Marshal(n.String)
case NodeValueTypeBool:
return json.Marshal(n.Bool)
case NodeValueTypeList:
return json.Marshal(n.List)
case NodeValueTypeInt:
return json.Marshal(n.Int)
case NodeValueTypeIntWithUnit:
return json.Marshal(map[string]interface{}{
"value": n.Int,
"unit": n.String,
})
default:
return nil, errors.New("NodeValueType of unknown type")
}
}

View file

@ -181,7 +181,7 @@ type NodeValue struct {
PropertyKey AnyType `json:"propertyKey"`
CCVersion int `json:"ccVersion"`
Metadata NodeValuesMetadata `json:"metadata"`
Value AnyType `json:"value"`
Value NodeValueType `json:"value"`
}
type NodeValuesMetadata struct {