From 7fdd02bd33d497d846b2ea567db8ced45491fa65 Mon Sep 17 00:00:00 2001 From: Finn Date: Tue, 9 Apr 2024 16:18:43 -0700 Subject: [PATCH] HTTP endpoints to get and set codes --- cmd/lockserver/main.go | 10 +- config/config.go | 2 + db/lock_code_slots.go | 10 ++ db/locks.sql.go | 11 ++ db/queries/locks.sql | 3 + go.mod | 20 +++ go.sum | 55 +++++- httpserver/server.go | 53 ++++++ httpserver/slots.go | 112 ++++++++++++ oapi-codegen.yaml | 9 + openapi/openapi.go | 320 +++++++++++++++++++++++++++++++++++ openapi/openapi.yaml | 65 +++++++ zwavejs/client.go | 140 +++++++++++---- zwavejs/consts.go | 52 ++++-- zwavejs/json_helpers.go | 66 ++++++++ zwavejs/messages-incoming.go | 105 +++--------- zwavejs/messages-outgoing.go | 22 ++- 17 files changed, 914 insertions(+), 141 deletions(-) create mode 100644 db/lock_code_slots.go create mode 100644 httpserver/server.go create mode 100644 httpserver/slots.go create mode 100644 oapi-codegen.yaml create mode 100644 openapi/openapi.go create mode 100644 openapi/openapi.yaml create mode 100644 zwavejs/json_helpers.go diff --git a/cmd/lockserver/main.go b/cmd/lockserver/main.go index a292380..d777b86 100644 --- a/cmd/lockserver/main.go +++ b/cmd/lockserver/main.go @@ -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") } diff --git a/config/config.go b/config/config.go index a1e4588..62d7f1a 100644 --- a/config/config.go +++ b/config/config.go @@ -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", } diff --git a/db/lock_code_slots.go b/db/lock_code_slots.go new file mode 100644 index 0000000..54b1826 --- /dev/null +++ b/db/lock_code_slots.go @@ -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, + } +} diff --git a/db/locks.sql.go b/db/locks.sql.go index 5fb05e6..7ff2d20 100644 --- a/db/locks.sql.go +++ b/db/locks.sql.go @@ -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 = ? ` diff --git a/db/queries/locks.sql b/db/queries/locks.sql index e259cfc..4e3fe44 100644 --- a/db/queries/locks.sql +++ b/db/queries/locks.sql @@ -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 = ?; diff --git a/go.mod b/go.mod index 556c8ce..808ff02 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index e2d9a89..d948f9b 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/httpserver/server.go b/httpserver/server.go new file mode 100644 index 0000000..c87d7b5 --- /dev/null +++ b/httpserver/server.go @@ -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) +} diff --git a/httpserver/slots.go b/httpserver/slots.go new file mode 100644 index 0000000..978dac6 --- /dev/null +++ b/httpserver/slots.go @@ -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) +} diff --git a/oapi-codegen.yaml b/oapi-codegen.yaml new file mode 100644 index 0000000..78d9210 --- /dev/null +++ b/oapi-codegen.yaml @@ -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 diff --git a/openapi/openapi.go b/openapi/openapi.go new file mode 100644 index 0000000..e148012 --- /dev/null +++ b/openapi/openapi.go @@ -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 +} diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml new file mode 100644 index 0000000..5cc7ba6 --- /dev/null +++ b/openapi/openapi.yaml @@ -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 diff --git a/zwavejs/client.go b/zwavejs/client.go index 85a87fb..3dea68a 100644 --- a/zwavejs/client.go +++ b/zwavejs/client.go @@ -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 { - conn *websocket.Conn + 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") - } - - time.Sleep(time.Second * 5) - } - }() - +func New(server string) (*Client, error) { + c := &Client{ + Server: server, + callbacks: make(map[string]chan Result), + } 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 { - return err + 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 +} diff --git a/zwavejs/consts.go b/zwavejs/consts.go index 6dc2566..8883dcd 100644 --- a/zwavejs/consts.go +++ b/zwavejs/consts.go @@ -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" ) diff --git a/zwavejs/json_helpers.go b/zwavejs/json_helpers.go new file mode 100644 index 0000000..923fa52 --- /dev/null +++ b/zwavejs/json_helpers.go @@ -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") + } +} diff --git a/zwavejs/messages-incoming.go b/zwavejs/messages-incoming.go index 1e45f8b..71a7a23 100644 --- a/zwavejs/messages-incoming.go +++ b/zwavejs/messages-incoming.go @@ -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"` @@ -34,13 +25,13 @@ type Event struct { } type NodeEventArgs struct { - CommandClassName string `json:"commandClassName"` - CommandClass int `json:"commandClass"` - Property string `json:"property"` - Endpoint int `json:"endpoint"` - NewValue NodePropertyValue `json:"newValue"` - PrevValue NodePropertyValue `json:"prevValue"` - PropertyName string `json:"propertyName"` + CommandClassName string `json:"commandClassName"` + CommandClass int `json:"commandClass"` + Property string `json:"property"` + Endpoint int `json:"endpoint"` + NewValue AnyType `json:"newValue"` + PrevValue AnyType `json:"prevValue"` + PropertyName string `json:"propertyName"` } type EventParameters struct { @@ -48,7 +39,8 @@ type EventParameters struct { } type Result struct { - State struct { + Success bool + State struct { Controller Controller Driver Driver Nodes []Node @@ -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 -} diff --git a/zwavejs/messages-outgoing.go b/zwavejs/messages-outgoing.go index 075faaa..c842416 100644 --- a/zwavejs/messages-outgoing.go +++ b/zwavejs/messages-outgoing.go @@ -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"` +}