From 58569dee2ed90b9ad8e56cc19f1a6579167450c4 Mon Sep 17 00:00:00 2001 From: Finn Date: Fri, 22 Nov 2024 19:50:18 -0800 Subject: [PATCH] WIP --- .forgejo/workflows/docker-build.yaml | 3 +- config/config.go | 14 +-- db/helpers.go | 4 +- db/issued_codes.sql.go | 138 +++++++++++++++++++++++++++ db/lock_code_slots.sql.go | 71 +++++++++++++- db/lock_log.sql.go | 3 +- db/migrations/1_init.sql | 22 ++--- db/models.go | 43 ++++----- db/queries/issued_codes.sql | 17 ++++ db/queries/lock_code_slots.sql | 11 ++- db/queries/user_codes.sql | 14 --- db/queries/users.sql | 8 -- db/users.sql.go | 41 -------- frontend/404.html | 4 + frontend/500.html | 6 ++ frontend/add-code.html | 9 ++ frontend/footer.html | 7 ++ frontend/frontend.go | 27 +----- frontend/header.html | 13 +++ frontend/index.html | 57 ++++++----- frontend/static/main.css | 14 ++- httpserver/codes.go | 126 ++++++++++++++++++++++++ httpserver/index.go | 49 +++------- httpserver/server.go | 56 +++++++++-- zwavejs/client.go | 2 +- zwavejs/json_helpers.go | 82 ++++++++++++++++ zwavejs/messages-incoming.go | 2 +- 27 files changed, 623 insertions(+), 220 deletions(-) create mode 100644 db/issued_codes.sql.go create mode 100644 db/queries/issued_codes.sql delete mode 100644 db/queries/user_codes.sql delete mode 100644 db/queries/users.sql delete mode 100644 db/users.sql.go create mode 100644 frontend/404.html create mode 100644 frontend/500.html create mode 100644 frontend/add-code.html create mode 100644 frontend/footer.html create mode 100644 frontend/header.html create mode 100644 httpserver/codes.go diff --git a/.forgejo/workflows/docker-build.yaml b/.forgejo/workflows/docker-build.yaml index a88f650..968d42d 100644 --- a/.forgejo/workflows/docker-build.yaml +++ b/.forgejo/workflows/docker-build.yaml @@ -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 diff --git a/config/config.go b/config/config.go index e2a056a..4cb034c 100644 --- a/config/config.go +++ b/config/config.go @@ -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"} diff --git a/db/helpers.go b/db/helpers.go index c49f805..a6c1062 100644 --- a/db/helpers.go +++ b/db/helpers.go @@ -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 } diff --git a/db/issued_codes.sql.go b/db/issued_codes.sql.go new file mode 100644 index 0000000..8f54f9a --- /dev/null +++ b/db/issued_codes.sql.go @@ -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 +} diff --git a/db/lock_code_slots.sql.go b/db/lock_code_slots.sql.go index 2ab3b35..a900a5e 100644 --- a/db/lock_code_slots.sql.go +++ b/db/lock_code_slots.sql.go @@ -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 } diff --git a/db/lock_log.sql.go b/db/lock_log.sql.go index 8477196..b777285 100644 --- a/db/lock_log.sql.go +++ b/db/lock_log.sql.go @@ -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 } diff --git a/db/migrations/1_init.sql b/db/migrations/1_init.sql index 6aca675..983ab77 100644 --- a/db/migrations/1_init.sql +++ b/db/migrations/1_init.sql @@ -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; diff --git a/db/models.go b/db/models.go index 14f2e0d..eee7334 100644 --- a/db/models.go +++ b/db/models.go @@ -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 } diff --git a/db/queries/issued_codes.sql b/db/queries/issued_codes.sql new file mode 100644 index 0000000..65450f2 --- /dev/null +++ b/db/queries/issued_codes.sql @@ -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'); diff --git a/db/queries/lock_code_slots.sql b/db/queries/lock_code_slots.sql index 38ce427..87c51f4 100644 --- a/db/queries/lock_code_slots.sql +++ b/db/queries/lock_code_slots.sql @@ -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; diff --git a/db/queries/user_codes.sql b/db/queries/user_codes.sql deleted file mode 100644 index 46baabe..0000000 --- a/db/queries/user_codes.sql +++ /dev/null @@ -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; diff --git a/db/queries/users.sql b/db/queries/users.sql deleted file mode 100644 index 6ca39e7..0000000 --- a/db/queries/users.sql +++ /dev/null @@ -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 = ?; diff --git a/db/users.sql.go b/db/users.sql.go deleted file mode 100644 index 2858d7f..0000000 --- a/db/users.sql.go +++ /dev/null @@ -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 -} diff --git a/frontend/404.html b/frontend/404.html new file mode 100644 index 0000000..85a80e3 --- /dev/null +++ b/frontend/404.html @@ -0,0 +1,4 @@ +{{ template "header.html" . }} +

404

+

page could not be found, maybe you want to return home.

+{{ template "footer.html" }} diff --git a/frontend/500.html b/frontend/500.html new file mode 100644 index 0000000..c2a158f --- /dev/null +++ b/frontend/500.html @@ -0,0 +1,6 @@ +{{ template "header.html" . }} +

Unexpected Error

+

An error occured while trying to handle your request, maybe you should return home.

+
+
{{ .Error }}
+{{ template "footer.html" }} diff --git a/frontend/add-code.html b/frontend/add-code.html new file mode 100644 index 0000000..b6b6384 --- /dev/null +++ b/frontend/add-code.html @@ -0,0 +1,9 @@ +{{ template "header.html" . }} +
Add Code
+
+
+
+
+ +
+{{ template "footer.html" }} diff --git a/frontend/footer.html b/frontend/footer.html new file mode 100644 index 0000000..21627dc --- /dev/null +++ b/frontend/footer.html @@ -0,0 +1,7 @@ + + + + + diff --git a/frontend/frontend.go b/frontend/frontend.go index d8458c2..eb6b1f2 100644 --- a/frontend/frontend.go +++ b/frontend/frontend.go @@ -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 -} diff --git a/frontend/header.html b/frontend/header.html new file mode 100644 index 0000000..58e2bf2 --- /dev/null +++ b/frontend/header.html @@ -0,0 +1,13 @@ + + + + + Better Z-Wave Locks for Home Assistant + + + + + + +
+
diff --git a/frontend/index.html b/frontend/index.html index e5419b9..886ef78 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,29 +1,28 @@ - - - - - Better Z-Wave Locks for Home Assistant - - - - - - -
-
-

Better Z-Wave Locks

-
-
-
-

locks

- -
-
- {{ version }} -
-
- - - +{{ template "header.html" . }} +

Better Z-Wave Locks

+
Active Codes
+[ add ] + + + + + + + + + {{ range .ActiveCodes }} + + + + + + + + {{ end }} +
NameCodeLast usedExpiresActions
{{ .Name.Value }}{{ .Code }}0.005ms ago on Back Door{{ .End.Value }}details | delete
+

+
locks
+ +{{ template "footer.html" }} diff --git a/frontend/static/main.css b/frontend/static/main.css index 33271c9..2f78eaf 100644 --- a/frontend/static/main.css +++ b/frontend/static/main.css @@ -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; +} diff --git a/httpserver/codes.go b/httpserver/codes.go new file mode 100644 index 0000000..7d4f774 --- /dev/null +++ b/httpserver/codes.go @@ -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/ + + 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() +} diff --git a/httpserver/index.go b/httpserver/index.go index 20bbf9e..8b02a53 100644 --- a/httpserver/index.go +++ b/httpserver/index.go @@ -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, }) } diff --git a/httpserver/server.go b/httpserver/server.go index e45530a..150efa9 100644 --- a/httpserver/server.go +++ b/httpserver/server.go @@ -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)) + } +} diff --git a/zwavejs/client.go b/zwavejs/client.go index 736db7b..636ed06 100644 --- a/zwavejs/client.go +++ b/zwavejs/client.go @@ -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) diff --git a/zwavejs/json_helpers.go b/zwavejs/json_helpers.go index 923fa52..4b4be06 100644 --- a/zwavejs/json_helpers.go +++ b/zwavejs/json_helpers.go @@ -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") + } +} diff --git a/zwavejs/messages-incoming.go b/zwavejs/messages-incoming.go index de6ce14..5d9404a 100644 --- a/zwavejs/messages-incoming.go +++ b/zwavejs/messages-incoming.go @@ -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 {