HTTP endpoints to get and set codes

This commit is contained in:
Finn 2024-04-09 16:18:43 -07:00
parent 47a45b8000
commit 7fdd02bd33
17 changed files with 914 additions and 141 deletions

View file

@ -9,6 +9,7 @@ import (
"git.janky.solutions/finn/lockserver/config"
"git.janky.solutions/finn/lockserver/db"
"git.janky.solutions/finn/lockserver/httpserver"
"git.janky.solutions/finn/lockserver/zwavejs"
)
@ -25,13 +26,20 @@ func run() {
logrus.WithError(err).Fatal("error migrating db")
}
zwaveClient, err := zwavejs.New(ctx, config.C.ZWaveJSServer)
zwaveClient, err := zwavejs.New(config.C.ZWaveJSServer)
if err != nil {
logrus.WithError(err).Fatal("error initializing ZWaveJS connection")
}
go zwaveClient.DialAndListen(ctx)
go httpserver.ListenAndServe(zwaveClient)
<-ctx.Done()
if err := httpserver.Shutdown(context.Background()); err != nil {
logrus.WithError(err).Error("error shutting down http server")
}
if err := zwaveClient.Shutdown(); err != nil {
logrus.WithError(err).Error("error shutting down ZWaveJS client")
}

View file

@ -3,9 +3,11 @@ package config
type Config struct {
ZWaveJSServer string
Database string
HTTPBind string
}
var C = Config{
ZWaveJSServer: "ws://home-assistant:3000",
Database: "lockserver.db",
HTTPBind: ":8080",
}

10
db/lock_code_slots.go Normal file
View file

@ -0,0 +1,10 @@
package db
import "git.janky.solutions/finn/lockserver/openapi"
func (l LockCodeSlot) OpenAPI() openapi.LockCodeSlot {
return openapi.LockCodeSlot{
Code: l.Code,
Enabled: l.Enabled,
}
}

View file

@ -20,6 +20,17 @@ func (q *Queries) CreateLock(ctx context.Context, zwaveDeviceID int64) (Lock, er
return i, err
}
const getLock = `-- name: GetLock :one
SELECT id, name, zwave_device_id FROM locks WHERE id = ?
`
func (q *Queries) GetLock(ctx context.Context, id int64) (Lock, error) {
row := q.db.QueryRowContext(ctx, getLock, id)
var i Lock
err := row.Scan(&i.ID, &i.Name, &i.ZwaveDeviceID)
return i, err
}
const getLockByDeviceID = `-- name: GetLockByDeviceID :one
SELECT id, name, zwave_device_id FROM locks WHERE zwave_device_id = ?
`

View file

@ -4,6 +4,9 @@ INSERT INTO locks (zwave_device_id, name) VALUES (?, "") RETURNING *;
-- name: GetLocks :many
SELECT * FROM locks;
-- name: GetLock :one
SELECT * FROM locks WHERE id = ?;
-- name: GetLockByDeviceID :one
SELECT * FROM locks WHERE zwave_device_id = ?;

20
go.mod
View file

@ -3,17 +3,37 @@ module git.janky.solutions/finn/lockserver
go 1.21.8
require (
github.com/getkin/kin-openapi v0.124.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.1
github.com/labstack/echo/v4 v4.11.4
github.com/mattn/go-sqlite3 v1.14.22
github.com/oapi-codegen/runtime v1.1.1
github.com/pressly/goose/v3 v3.19.2
github.com/sirupsen/logrus v1.9.3
)
require (
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/go-openapi/jsonpointer v0.20.2 // indirect
github.com/go-openapi/swag v0.22.8 // indirect
github.com/invopop/yaml v0.2.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/sethvargo/go-retry v0.2.4 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

55
go.sum
View file

@ -10,10 +10,14 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 h1:goHVqTbFX3AIo0tzGr14pgfAW2ZfPChKO21Z9MGf/gk=
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM=
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8=
@ -35,14 +39,23 @@ github.com/elastic/go-sysinfo v1.11.2 h1:mcm4OSYVMyws6+n2HIVMGkln5HOpo5Ie1ZmbbNn
github.com/elastic/go-sysinfo v1.11.2/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJa7AfT8HpBFQ=
github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss=
github.com/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M=
github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI=
github.com/go-faster/errors v0.6.1/go.mod h1:5MGV2/2T9yvlrbhe9pD9LO5Z/2zCSq2T8j+Jpi2LAyY=
github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q=
github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs=
github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw=
github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI=
github.com/go-sql-driver/mysql v1.8.0 h1:UtktXaU2Nb64z/pLiGIxY4431SJ4/dR5cjMmlVHgnT4=
github.com/go-sql-driver/mysql v1.8.0/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
@ -57,6 +70,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY=
github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
@ -69,10 +84,26 @@ github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/libsql/sqlite-antlr4-parser v0.0.0-20230802215326-5cb5bb604475 h1:6PfEMwfInASh9hkN83aR0j4W/eKaAZt/AURtXAXlas0=
github.com/libsql/sqlite-antlr4-parser v0.0.0-20230802215326-5cb5bb604475/go.mod h1:20nXSmcf0nAscrzqsXeC2/tA3KkV2eCiJqYuyAgl+ss=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
@ -83,8 +114,12 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI=
@ -95,6 +130,8 @@ github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4a
github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg=
github.com/paulmach/orb v0.10.0 h1:guVYVqzxHE/CQ1KpfGO077TR0ATHSNjp4s6XGLn3W9s=
github.com/paulmach/orb v0.10.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -107,6 +144,8 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sethvargo/go-retry v0.2.4 h1:T+jHEQy/zKJf5s95UkguisicE0zuF9y7+/vgz08Ocec=
@ -115,11 +154,20 @@ github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5g
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tursodatabase/libsql-client-go v0.0.0-20240220085343-4ae0eb9d0898 h1:1MvEhzI5pvP27e9Dzz861mxk9WzXZLSJwzOU67cKTbU=
github.com/tursodatabase/libsql-client-go v0.0.0-20240220085343-4ae0eb9d0898/go.mod h1:9bKuHS7eZh/0mJndbUOrCx8Ej3PlsRDszj4L7oVYMPQ=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vertica/vertica-sql-go v1.3.3 h1:fL+FKEAEy5ONmsvya2WH5T8bhkvY27y/Ik3ReR2T+Qw=
github.com/vertica/vertica-sql-go v1.3.3/go.mod h1:jnn2GFuv+O2Jcjktb7zyc4Utlbu9YVqpHH/lx63+1M4=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
@ -149,6 +197,8 @@ golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
@ -162,9 +212,12 @@ google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9Y
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=

53
httpserver/server.go Normal file
View file

@ -0,0 +1,53 @@
package httpserver
import (
"context"
"database/sql"
"errors"
"net/http"
echo "github.com/labstack/echo/v4"
"github.com/sirupsen/logrus"
"git.janky.solutions/finn/lockserver/config"
"git.janky.solutions/finn/lockserver/openapi"
"git.janky.solutions/finn/lockserver/zwavejs"
)
var server *echo.Echo
type lockserver struct {
ZWaveJS *zwavejs.Client
}
func ListenAndServe(client *zwavejs.Client) {
server = echo.New()
server.HideBanner = true
server.HidePort = true
server.HTTPErrorHandler = handleError
openapi.RegisterHandlersWithBaseURL(server, lockserver{ZWaveJS: client}, "/api")
err := server.Start(config.C.HTTPBind)
if err != http.ErrServerClosed {
logrus.WithError(err).Fatal("error starting http server")
}
}
func handleError(err error, c echo.Context) {
if errors.Is(err, sql.ErrNoRows) {
_ = c.JSON(http.StatusNotFound, map[string]string{"error": "not found"})
return
}
logrus.WithError(err).Error("error handling request")
_ = c.JSON(http.StatusInternalServerError, map[string]string{"error": "internal server error"})
}
func Shutdown(ctx context.Context) error {
if server == nil {
return nil
}
return server.Shutdown(ctx)
}

112
httpserver/slots.go Normal file
View file

@ -0,0 +1,112 @@
package httpserver
import (
"net/http"
echo "github.com/labstack/echo/v4"
"git.janky.solutions/finn/lockserver/db"
"git.janky.solutions/finn/lockserver/openapi"
"git.janky.solutions/finn/lockserver/zwavejs"
)
func (lockserver) GetLockCodeSlot(c echo.Context, lock int, slot int) error {
queries, dbc, err := db.Get()
if err != nil {
return err
}
defer dbc.Close()
slotData, err := queries.GetLockCodeBySlot(c.Request().Context(), db.GetLockCodeBySlotParams{
Lock: int64(lock),
Slot: int64(slot),
})
if err != nil {
return err
}
return c.JSON(http.StatusFound, slotData.OpenAPI())
}
func (l lockserver) PutLockCodeSlot(c echo.Context, lockID int, slot int) error {
queries, dbc, err := db.Get()
if err != nil {
return err
}
defer dbc.Close()
var body openapi.LockCodeSlot
if err := c.Bind(&body); err != nil {
return err
}
ctx := c.Request().Context()
lock, err := queries.GetLock(ctx, int64(lockID))
if err != nil {
return err
}
// 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 = l.ZWaveJS.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: slot},
}, zwavejs.AnyType{Type: zwavejs.AnyTypeString, String: body.Code})
if err != nil {
return err
}
enabled := 0
if body.Enabled {
enabled = 1
}
err = l.ZWaveJS.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.PropertyUserIDStatus)},
PropertyName: zwavejs.AnyType{Type: zwavejs.AnyTypeString, String: string(zwavejs.PropertyUserIDStatus)},
PropertyKey: zwavejs.AnyType{Type: zwavejs.AnyTypeInt, Int: slot},
}, zwavejs.AnyType{Type: zwavejs.AnyTypeInt, Int: enabled})
if err != nil {
return err
}
err = queries.UpsertCodeSlot(ctx, db.UpsertCodeSlotParams{
Lock: lock.ID,
Slot: int64(slot),
Code: body.Code,
Enabled: body.Enabled,
})
if err != nil {
return err
}
return c.JSON(http.StatusOK, body)
}

9
oapi-codegen.yaml Normal file
View file

@ -0,0 +1,9 @@
package: openapi
generate:
models: true
embedded-spec: true
strict-server: true
echo-server: true
output-options:
skip-prune: true
output: openapi/openapi.go

320
openapi/openapi.go Normal file
View file

@ -0,0 +1,320 @@
// Package openapi provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/deepmap/oapi-codegen/v2 version v2.1.0 DO NOT EDIT.
package openapi
import (
"bytes"
"compress/gzip"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
"path"
"strings"
"github.com/getkin/kin-openapi/openapi3"
"github.com/labstack/echo/v4"
"github.com/oapi-codegen/runtime"
strictecho "github.com/oapi-codegen/runtime/strictmiddleware/echo"
)
// LockCodeSlot defines model for LockCodeSlot.
type LockCodeSlot struct {
Code string `json:"code"`
Enabled bool `json:"enabled"`
}
// PutLockCodeSlotJSONRequestBody defines body for PutLockCodeSlot for application/json ContentType.
type PutLockCodeSlotJSONRequestBody = LockCodeSlot
// ServerInterface represents all server handlers.
type ServerInterface interface {
// (GET /locks/{lock}/slots/{slot})
GetLockCodeSlot(ctx echo.Context, lock int, slot int) error
// (PUT /locks/{lock}/slots/{slot})
PutLockCodeSlot(ctx echo.Context, lock int, slot int) error
}
// ServerInterfaceWrapper converts echo contexts to parameters.
type ServerInterfaceWrapper struct {
Handler ServerInterface
}
// GetLockCodeSlot converts echo context to params.
func (w *ServerInterfaceWrapper) GetLockCodeSlot(ctx echo.Context) error {
var err error
// ------------- Path parameter "lock" -------------
var lock int
err = runtime.BindStyledParameterWithOptions("simple", "lock", ctx.Param("lock"), &lock, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter lock: %s", err))
}
// ------------- Path parameter "slot" -------------
var slot int
err = runtime.BindStyledParameterWithOptions("simple", "slot", ctx.Param("slot"), &slot, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter slot: %s", err))
}
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.GetLockCodeSlot(ctx, lock, slot)
return err
}
// PutLockCodeSlot converts echo context to params.
func (w *ServerInterfaceWrapper) PutLockCodeSlot(ctx echo.Context) error {
var err error
// ------------- Path parameter "lock" -------------
var lock int
err = runtime.BindStyledParameterWithOptions("simple", "lock", ctx.Param("lock"), &lock, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter lock: %s", err))
}
// ------------- Path parameter "slot" -------------
var slot int
err = runtime.BindStyledParameterWithOptions("simple", "slot", ctx.Param("slot"), &slot, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter slot: %s", err))
}
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.PutLockCodeSlot(ctx, lock, slot)
return err
}
// This is a simple interface which specifies echo.Route addition functions which
// are present on both echo.Echo and echo.Group, since we want to allow using
// either of them for path registration
type EchoRouter interface {
CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
}
// RegisterHandlers adds each server route to the EchoRouter.
func RegisterHandlers(router EchoRouter, si ServerInterface) {
RegisterHandlersWithBaseURL(router, si, "")
}
// Registers handlers, and prepends BaseURL to the paths, so that the paths
// can be served under a prefix.
func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) {
wrapper := ServerInterfaceWrapper{
Handler: si,
}
router.GET(baseURL+"/locks/:lock/slots/:slot", wrapper.GetLockCodeSlot)
router.PUT(baseURL+"/locks/:lock/slots/:slot", wrapper.PutLockCodeSlot)
}
type GetLockCodeSlotRequestObject struct {
Lock int `json:"lock"`
Slot int `json:"slot"`
}
type GetLockCodeSlotResponseObject interface {
VisitGetLockCodeSlotResponse(w http.ResponseWriter) error
}
type GetLockCodeSlot200JSONResponse LockCodeSlot
func (response GetLockCodeSlot200JSONResponse) VisitGetLockCodeSlotResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type PutLockCodeSlotRequestObject struct {
Lock int `json:"lock"`
Slot int `json:"slot"`
Body *PutLockCodeSlotJSONRequestBody
}
type PutLockCodeSlotResponseObject interface {
VisitPutLockCodeSlotResponse(w http.ResponseWriter) error
}
// StrictServerInterface represents all server handlers.
type StrictServerInterface interface {
// (GET /locks/{lock}/slots/{slot})
GetLockCodeSlot(ctx context.Context, request GetLockCodeSlotRequestObject) (GetLockCodeSlotResponseObject, error)
// (PUT /locks/{lock}/slots/{slot})
PutLockCodeSlot(ctx context.Context, request PutLockCodeSlotRequestObject) (PutLockCodeSlotResponseObject, error)
}
type StrictHandlerFunc = strictecho.StrictEchoHandlerFunc
type StrictMiddlewareFunc = strictecho.StrictEchoMiddlewareFunc
func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface {
return &strictHandler{ssi: ssi, middlewares: middlewares}
}
type strictHandler struct {
ssi StrictServerInterface
middlewares []StrictMiddlewareFunc
}
// GetLockCodeSlot operation middleware
func (sh *strictHandler) GetLockCodeSlot(ctx echo.Context, lock int, slot int) error {
var request GetLockCodeSlotRequestObject
request.Lock = lock
request.Slot = slot
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
return sh.ssi.GetLockCodeSlot(ctx.Request().Context(), request.(GetLockCodeSlotRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "GetLockCodeSlot")
}
response, err := handler(ctx, request)
if err != nil {
return err
} else if validResponse, ok := response.(GetLockCodeSlotResponseObject); ok {
return validResponse.VisitGetLockCodeSlotResponse(ctx.Response())
} else if response != nil {
return fmt.Errorf("unexpected response type: %T", response)
}
return nil
}
// PutLockCodeSlot operation middleware
func (sh *strictHandler) PutLockCodeSlot(ctx echo.Context, lock int, slot int) error {
var request PutLockCodeSlotRequestObject
request.Lock = lock
request.Slot = slot
var body PutLockCodeSlotJSONRequestBody
if err := ctx.Bind(&body); err != nil {
return err
}
request.Body = &body
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
return sh.ssi.PutLockCodeSlot(ctx.Request().Context(), request.(PutLockCodeSlotRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "PutLockCodeSlot")
}
response, err := handler(ctx, request)
if err != nil {
return err
} else if validResponse, ok := response.(PutLockCodeSlotResponseObject); ok {
return validResponse.VisitPutLockCodeSlotResponse(ctx.Response())
} else if response != nil {
return fmt.Errorf("unexpected response type: %T", response)
}
return nil
}
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"H4sIAAAAAAAC/+SSsY7bMAyGX8VgOzqW296k7S4FigM6FLipONygyEyiqyzqJDpFYOjdCyo+JAGydOnS",
"SbZI/qS+nzNYGiMFDJxBz5DtHkdTP7+T/bWmAZ88sfzHRBETO6xRSwPKyceIoCFzcmEHpQUMZuNxuIht",
"iDyaAKW0kPBtcknCzyeJc8FL+15Am1e0DEUKXNiSaHlnMeTaMphRstbr1cPP1dP96q7roYUpedCwZ45Z",
"K2UTGnYHtDSOFHJHaacWiaw2R3XX9UqmZcdexOSxGdMBE7RwwJQdBdDQd5+6XvIoYjDRgYYv9aqFaHhf",
"SSgvtWqWo6jsibOa5SgS3WGFJ+gMOwqPA2j4hnxFV9SSGZExZdDPMwyYbXKRT1M8fm1o24g+CBDQtTm0",
"7ySWyJktpwnbxcsLI1xg3GESI263EEeafJroRp8l8hd9XiQ7RxLskvC570/LExhDBWNi9M5WNOo1yyzz",
"heDHhFvQ8EGdt1QtK6quCNZduX7SfSV28aaaE6cbfvyY/ic/3ibM/EDD8Z9ZwXtsAv5uDsZP2GwpNXJz",
"c/xytTFh8r6UUv4EAAD///FFWy6nBAAA",
}
// GetSwagger returns the content of the embedded swagger specification file
// or error if failed to decode
func decodeSpec() ([]byte, error) {
zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, ""))
if err != nil {
return nil, fmt.Errorf("error base64 decoding spec: %w", err)
}
zr, err := gzip.NewReader(bytes.NewReader(zipped))
if err != nil {
return nil, fmt.Errorf("error decompressing spec: %w", err)
}
var buf bytes.Buffer
_, err = buf.ReadFrom(zr)
if err != nil {
return nil, fmt.Errorf("error decompressing spec: %w", err)
}
return buf.Bytes(), nil
}
var rawSpec = decodeSpecCached()
// a naive cached of a decoded swagger spec
func decodeSpecCached() func() ([]byte, error) {
data, err := decodeSpec()
return func() ([]byte, error) {
return data, err
}
}
// Constructs a synthetic filesystem for resolving external references when loading openapi specifications.
func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) {
res := make(map[string]func() ([]byte, error))
if len(pathToFile) > 0 {
res[pathToFile] = rawSpec
}
return res
}
// GetSwagger returns the Swagger specification corresponding to the generated code
// in this file. The external references of Swagger specification are resolved.
// The logic of resolving external references is tightly connected to "import-mapping" feature.
// Externally referenced files must be embedded in the corresponding golang packages.
// Urls can be supported but this task was out of the scope.
func GetSwagger() (swagger *openapi3.T, err error) {
resolvePath := PathToRawSpec("")
loader := openapi3.NewLoader()
loader.IsExternalRefsAllowed = true
loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) {
pathToFile := url.String()
pathToFile = path.Clean(pathToFile)
getSpec, ok := resolvePath[pathToFile]
if !ok {
err1 := fmt.Errorf("path not found: %s", pathToFile)
return nil, err1
}
return getSpec()
}
var specData []byte
specData, err = rawSpec()
if err != nil {
return
}
swagger, err = loader.LoadFromData(specData)
if err != nil {
return
}
return
}

65
openapi/openapi.yaml Normal file
View file

@ -0,0 +1,65 @@
openapi: "3.1.0"
info:
version: 0.1.0
title: Lockserver
license:
name: CC-BY-SA-4.0
url: https://creativecommons.org/licenses/by/4.0/
paths:
/locks/{lock}/slots/{slot}:
get:
operationId: get_lock_code_slot
parameters:
- name: lock
in: path
description: ID of lock
required: true
schema:
type: integer
- name: slot
in: path
description: ID of code slot
required: true
schema:
type: integer
responses:
'200':
description: A lock code slot
content:
application/json:
schema:
$ref: '#/components/schemas/LockCodeSlot'
put:
operationId: put_lock_code_slot
parameters:
- name: lock
in: path
description: ID of lock
required: true
schema:
type: integer
- name: slot
in: path
description: ID of code slot
required: true
schema:
type: integer
requestBody:
description: the new value for the slot
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LockCodeSlot'
components:
schemas:
LockCodeSlot:
type: object
required:
- code
- enabled
properties:
code:
type: string
enabled:
type: boolean

View file

@ -6,82 +6,97 @@ import (
"encoding/json"
"errors"
"fmt"
"sync"
"time"
"git.janky.solutions/finn/lockserver/db"
"github.com/google/uuid"
"github.com/gorilla/websocket"
"github.com/sirupsen/logrus"
)
type Client struct {
Server string
conn *websocket.Conn
callbacks map[string]chan Result
callbacksLock sync.Mutex
}
func New(ctx context.Context, server string) (*Client, error) {
c := &Client{}
go func() {
for {
if err := c.DialAndListen(ctx, server); err != nil {
logrus.WithError(err).Error("error from ZWaveJS server")
func New(server string) (*Client, error) {
c := &Client{
Server: server,
callbacks: make(map[string]chan Result),
}
time.Sleep(time.Second * 5)
}
}()
return c, nil
}
func (c *Client) DialAndListen(ctx context.Context, server string) error {
func (c *Client) DialAndListen(ctx context.Context) {
for {
conn, _, err := websocket.DefaultDialer.DialContext(ctx, server, nil)
logrus.WithField("server", c.Server).Info("connecting to zwave-js server")
conn, _, err := websocket.DefaultDialer.DialContext(ctx, c.Server, nil)
if err != nil {
return err
logrus.WithError(err).Error("error connecting to zwavejs server")
time.Sleep(time.Second * 10)
continue
}
c.conn = conn
logrus.Info("connected to zwave-js server")
if err := c.listen(ctx); err != nil {
return err
logrus.WithError(err).Error("error communicating with zwavejs server")
time.Sleep(time.Second * 10)
continue
}
}
}
func (c Client) listen(ctx context.Context) error {
func (c *Client) listen(ctx context.Context) error {
for {
_, msg, err := c.conn.ReadMessage()
var msg IncomingMessage
// err := c.conn.ReadJSON(&msg)
// if err != nil {
// return err
// }
_, rawmsg, err := c.conn.ReadMessage()
if err != nil {
return err
}
var parsed IncomingMessage
if err := json.Unmarshal(msg, &parsed); err != nil {
if err = json.Unmarshal(rawmsg, &msg); err != nil {
return err
}
switch parsed.Type {
logrus.WithField("type", msg.Type).Debug("received message from zwave-js")
switch msg.Type {
case "version":
if err := c.conn.WriteJSON(OutgoingMessage{Command: StartListeningCommand}); err != nil {
if err := c.conn.WriteJSON(OutgoingMessage{Command: CommandStartListening}); err != nil {
return err
}
case "result":
if err := syncState(ctx, *parsed.Result); err != nil {
if msg.MessageID == "" {
if err := syncState(ctx, *msg.Result); err != nil {
return err
}
} else {
c.handleCallback(msg.MessageID, *msg.Result)
}
case "event":
if err := handleEvent(ctx, *parsed.Event); err != nil {
if err := handleEvent(ctx, *msg.Event); err != nil {
logrus.WithError(err).Error("error handling event")
}
default:
fmt.Println(string(msg))
logrus.WithField("type", msg.Type).Warn("received unexpected message type from zwave-js server")
}
}
}
func (c Client) Shutdown() error {
func (c *Client) Shutdown() error {
return c.conn.Close()
}
@ -100,18 +115,14 @@ func syncState(ctx context.Context, result Result) error {
continue
}
slotNumber, err := value.PropertyKey.Int()
if err != nil {
logrus.WithError(err).WithField("value", value.PropertyKey.String()).Warn("unexpected non-int PropertyKey")
continue
}
slotNumber := value.PropertyKey.Int
lockID = int64(node.NodeID)
slot := slots[slotNumber] // check if there's an existing entry
slot.Slot = int64(slotNumber)
switch value.PropertyName {
switch Property(value.PropertyName.String) {
case PropertyUserCode:
slot.Code = value.Value.String
case PropertyUserIDStatus:
@ -203,3 +214,62 @@ func handleEvent(ctx context.Context, event Event) error {
return nil
}
func (c *Client) handleCallback(messageID string, result Result) error {
c.callbacksLock.Lock()
defer c.callbacksLock.Unlock()
cb, ok := c.callbacks[messageID]
if !ok {
logrus.WithField("message_id", messageID).Warn("got response to a message we didn't send")
return nil
}
// TODO: set a timeout
cb <- result
return nil
}
func (c *Client) sendMessage(message OutgoingMessageIface) (Result, error) {
messageID := uuid.New().String()
message.SetMessageID(messageID)
ch := make(chan Result)
c.callbacksLock.Lock()
c.callbacks[messageID] = ch
c.callbacksLock.Unlock()
if err := c.conn.WriteJSON(message); err != nil {
return Result{}, err
}
result := <-ch
close(ch)
c.callbacksLock.Lock()
delete(c.callbacks, messageID)
c.callbacksLock.Unlock()
return result, nil
}
func (c *Client) SetNodeValue(ctx context.Context, nodeID int, valueID NodeValue, value AnyType) error {
msg := NodeSetValueMessage{
OutgoingMessage: OutgoingMessage{Command: CommandNodeSetValue},
NodeID: nodeID,
ValueID: valueID,
Value: value,
}
result, err := c.sendMessage(&msg)
if err != nil {
return err
}
if !result.Success {
return errors.New("non-successful response from zwave-js server")
}
return nil
}

View file

@ -1,16 +1,44 @@
package zwavejs
var (
CommandClassDoorLock = 98
CommandClassUserCode = 99
type CommandClass int
PropertyUserCode = "userCode"
PropertyUserIDStatus = "userIdStatus"
EventSourceController = "controller"
EventSourceNode = "node"
EventTypeValueUpdated = "value updated"
EventTypeStatisticsUpdated = "statistics updated"
EventTypeNotification = "notification"
const (
CommandClassDoorLock CommandClass = 98
CommandClassUserCode CommandClass = 99
)
type CommandClassName string
const (
CommandClassNameUserCode CommandClassName = "User Code"
)
type Property string
const (
PropertyUserCode Property = "userCode"
PropertyUserIDStatus Property = "userIdStatus"
)
type EventSource string
const (
EventSourceController EventSource = "controller"
EventSourceNode EventSource = "node"
)
type EventType string
const (
EventTypeValueUpdated EventType = "value updated"
EventTypeStatisticsUpdated EventType = "statistics updated"
EventTypeNotification EventType = "notification"
)
type Command string
const (
CommandInitialize Command = "initialize"
CommandStartListening Command = "start_listening"
CommandNodeSetValue Command = "node.set_value"
)

66
zwavejs/json_helpers.go Normal file
View file

@ -0,0 +1,66 @@
package zwavejs
import (
"bytes"
"encoding/json"
"errors"
"github.com/sirupsen/logrus"
)
type AnyTypeType int
const (
AnyTypeString AnyTypeType = iota
AnyTypeInt
AnyTypeBool
AnyTypeList
)
type AnyType struct {
Type AnyTypeType
String string
Int int
Bool bool
List []AnyType
}
func (n *AnyType) UnmarshalJSON(data []byte) error {
if bytes.HasPrefix(data, []byte("\"")) {
n.Type = AnyTypeString
return json.Unmarshal(data, &n.String)
}
if bytes.Equal(data, []byte("true")) || bytes.Equal(data, []byte("false")) {
n.Type = AnyTypeBool
return json.Unmarshal(data, &n.Bool)
}
if bytes.HasPrefix(data, []byte("[")) {
n.Type = AnyTypeList
return json.Unmarshal(data, &n.List)
}
n.Type = AnyTypeInt
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 AnyType) MarshalJSON() ([]byte, error) {
switch n.Type {
case AnyTypeString:
return json.Marshal(n.String)
case AnyTypeBool:
return json.Marshal(n.Bool)
case AnyTypeList:
return json.Marshal(n.List)
case AnyTypeInt:
return json.Marshal(n.Int)
default:
return nil, errors.New("anytype of unknown type")
}
}

View file

@ -1,14 +1,5 @@
package zwavejs
import (
"bytes"
"encoding/json"
"fmt"
"strconv"
"github.com/sirupsen/logrus"
)
type IncomingMessage struct {
Type string `json:"type"`
MessageID string `json:"messageId"`
@ -24,8 +15,8 @@ type IncomingMessage struct {
}
type Event struct {
Source string `json:"source"`
Event string `json:"event"`
Source EventSource `json:"source"`
Event EventType `json:"event"`
NodeID int `json:"nodeId"`
Args NodeEventArgs `json:"args"`
@ -38,8 +29,8 @@ type NodeEventArgs struct {
CommandClass int `json:"commandClass"`
Property string `json:"property"`
Endpoint int `json:"endpoint"`
NewValue NodePropertyValue `json:"newValue"`
PrevValue NodePropertyValue `json:"prevValue"`
NewValue AnyType `json:"newValue"`
PrevValue AnyType `json:"prevValue"`
PropertyName string `json:"propertyName"`
}
@ -48,6 +39,7 @@ type EventParameters struct {
}
type Result struct {
Success bool
State struct {
Controller Controller
Driver Driver
@ -119,7 +111,7 @@ type Node struct {
Label string `json:"label"`
InterviewAttempts int `json:"interviewAttempts"`
Endpoints []NodeEndpoint `json:"endpoints"`
Values []NodeValues `json:"values"`
Values []NodeValue `json:"values"`
InterviewStage int `json:"interviewStage"`
IsFrequentListening any `json:"isFrequentListening"`
MaxBaudRate int `json:"maxBaudRate"`
@ -180,15 +172,16 @@ type NodeEndpoint struct {
Index int `json:"index"`
}
type NodeValues struct {
type NodeValue struct {
Endpoint int `json:"endpoint"`
CommandClass int `json:"commandClass"`
CommandClassName string `json:"commandClassName"`
PropertyName string `json:"propertyName"`
PropertyKey StringOrInt `json:"propertyKey"`
CcVersion int `json:"ccVersion"`
CommandClass CommandClass `json:"commandClass"`
CommandClassName CommandClassName `json:"commandClassName"`
Property AnyType `json:"property"`
PropertyName AnyType `json:"propertyName"`
PropertyKey AnyType `json:"propertyKey"`
CCVersion int `json:"ccVersion"`
Metadata NodeValuesMetadata `json:"metadata"`
Value NodePropertyValue `json:"value"`
Value AnyType `json:"value"`
}
type NodeValuesMetadata struct {
@ -207,61 +200,3 @@ type NodeDeviceClass struct {
MandatorySupportedCCs []string `json:"mandatorySupportedCCs"`
MandatoryControlCCs []string `json:"mandatoryControlCCs"`
}
type StringOrInt string
func (s *StringOrInt) UnmarshalJSON(data []byte) error {
var str string
if bytes.HasPrefix(data, []byte("\"")) {
if err := json.Unmarshal(data, &str); err != nil {
return err
}
} else if bytes.Equal(data, []byte("true")) || bytes.Equal(data, []byte("false")) {
str = string(data)
} else {
var i int
if err := json.Unmarshal(data, &i); err != nil {
return err
}
str = fmt.Sprintf("%d", i)
}
*s = StringOrInt(str)
return nil
}
func (s StringOrInt) String() string {
return string(s)
}
func (s StringOrInt) Int() (int, error) {
return strconv.Atoi(string(s))
}
type NodePropertyValue struct {
String string
Int int
Bool bool
Values []NodePropertyValue
}
func (n *NodePropertyValue) UnmarshalJSON(data []byte) error {
if bytes.HasPrefix(data, []byte("\"")) {
return json.Unmarshal(data, &n.String)
}
if bytes.Equal(data, []byte("true")) || bytes.Equal(data, []byte("false")) {
return json.Unmarshal(data, &n.Bool)
}
if bytes.HasPrefix(data, []byte("[")) {
return json.Unmarshal(data, &n.Values)
}
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
}

View file

@ -1,19 +1,27 @@
package zwavejs
type Command string
var (
InitializeCommand Command = "initialize"
StartListeningCommand Command = "start_listening"
)
type OutgoingMessageIface interface {
SetMessageID(string)
}
type OutgoingMessage struct {
MessageID string `json:"messageId"`
Command Command `json:"command"`
}
type Initialize struct {
func (o *OutgoingMessage) SetMessageID(id string) {
o.MessageID = id
}
type InitializeMessage struct {
OutgoingMessage
SchemaVersion int `json:"schemaVersion"`
AdditionalUserAgentComponents map[string]string `json:"additionalUserAgentComponents"`
}
type NodeSetValueMessage struct {
OutgoingMessage
NodeID int `json:"nodeId"`
ValueID NodeValue `json:"valueId"`
Value AnyType `json:"value"`
}