From 054008eb1fd7531dafb42a7209bfd6fc442bc885 Mon Sep 17 00:00:00 2001 From: Finn Date: Mon, 8 Apr 2024 21:25:36 -0700 Subject: [PATCH] Initial commit Connects to zwave-js, syncs all locks and codeslots with database, and records an event log. No support for updating code slots. --- .gitignore | 1 + cmd/lockserver/main.go | 38 +++++ config/config.go | 11 ++ db/db.go | 31 ++++ db/helpers.go | 53 +++++++ db/lock_code_slots.sql.go | 88 +++++++++++ db/lock_log.sql.go | 58 +++++++ db/locks.sql.go | 73 +++++++++ db/migrations/1_init.sql | 35 +++++ db/models.go | 32 ++++ db/queries/lock_code_slots.sql | 8 + db/queries/lock_log.sql | 5 + db/queries/locks.sql | 11 ++ go.mod | 19 +++ go.sum | 187 +++++++++++++++++++++++ sqlc.yaml | 9 ++ zwavejs/client.go | 204 +++++++++++++++++++++++++ zwavejs/consts.go | 16 ++ zwavejs/messages-incoming.go | 267 +++++++++++++++++++++++++++++++++ zwavejs/messages-outgoing.go | 19 +++ 20 files changed, 1165 insertions(+) create mode 100644 .gitignore create mode 100644 cmd/lockserver/main.go create mode 100644 config/config.go create mode 100644 db/db.go create mode 100644 db/helpers.go create mode 100644 db/lock_code_slots.sql.go create mode 100644 db/lock_log.sql.go create mode 100644 db/locks.sql.go create mode 100644 db/migrations/1_init.sql create mode 100644 db/models.go create mode 100644 db/queries/lock_code_slots.sql create mode 100644 db/queries/lock_log.sql create mode 100644 db/queries/locks.sql create mode 100644 go.mod create mode 100644 go.sum create mode 100644 sqlc.yaml create mode 100644 zwavejs/client.go create mode 100644 zwavejs/consts.go create mode 100644 zwavejs/messages-incoming.go create mode 100644 zwavejs/messages-outgoing.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6dd20b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +lockserver.db diff --git a/cmd/lockserver/main.go b/cmd/lockserver/main.go new file mode 100644 index 0000000..a292380 --- /dev/null +++ b/cmd/lockserver/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "context" + "os" + "os/signal" + + "github.com/sirupsen/logrus" + + "git.janky.solutions/finn/lockserver/config" + "git.janky.solutions/finn/lockserver/db" + "git.janky.solutions/finn/lockserver/zwavejs" +) + +func main() { + run() +} + +func run() { + logrus.SetLevel(logrus.DebugLevel) + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + if err := db.Migrate(); err != nil { + logrus.WithError(err).Fatal("error migrating db") + } + + zwaveClient, err := zwavejs.New(ctx, config.C.ZWaveJSServer) + if err != nil { + logrus.WithError(err).Fatal("error initializing ZWaveJS connection") + } + + <-ctx.Done() + + 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 new file mode 100644 index 0000000..a1e4588 --- /dev/null +++ b/config/config.go @@ -0,0 +1,11 @@ +package config + +type Config struct { + ZWaveJSServer string + Database string +} + +var C = Config{ + ZWaveJSServer: "ws://home-assistant:3000", + Database: "lockserver.db", +} diff --git a/db/db.go b/db/db.go new file mode 100644 index 0000000..bf8f8e3 --- /dev/null +++ b/db/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.20.0 + +package db + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/db/helpers.go b/db/helpers.go new file mode 100644 index 0000000..b803a3d --- /dev/null +++ b/db/helpers.go @@ -0,0 +1,53 @@ +package db + +import ( + "database/sql" + "embed" + + _ "github.com/mattn/go-sqlite3" + goose "github.com/pressly/goose/v3" + "github.com/sirupsen/logrus" + + "git.janky.solutions/finn/lockserver/config" +) + +func Get() (*Queries, *sql.DB, error) { + db, err := sql.Open("sqlite3", config.C.Database) + if err != nil { + return nil, nil, err + } + + return New(db), db, nil +} + +//go:embed migrations +var migrations embed.FS + +func Migrate() error { + logrus.WithField("dbfile", config.C.Database).Info("migrating database") + + _, conn, err := Get() + if err != nil { + return err + } + defer conn.Close() + + goose.SetBaseFS(migrations) + + if err := goose.SetDialect("sqlite3"); err != nil { + return err + } + + if err := goose.Up(conn, "migrations"); err != nil { + return err + } + + return nil +} + +func NullString(s string) sql.NullString { + return sql.NullString{ + Valid: s != "", + String: s, + } +} diff --git a/db/lock_code_slots.sql.go b/db/lock_code_slots.sql.go new file mode 100644 index 0000000..2ab3b35 --- /dev/null +++ b/db/lock_code_slots.sql.go @@ -0,0 +1,88 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.20.0 +// source: lock_code_slots.sql + +package db + +import ( + "context" +) + +const getLockCodeBySlot = `-- name: GetLockCodeBySlot :one +SELECT id, lock, code, slot, name, enabled FROM lock_code_slots WHERE lock = ? AND slot = ? +` + +type GetLockCodeBySlotParams struct { + Lock int64 + Slot int64 +} + +func (q *Queries) GetLockCodeBySlot(ctx context.Context, arg GetLockCodeBySlotParams) (LockCodeSlot, error) { + row := q.db.QueryRowContext(ctx, getLockCodeBySlot, arg.Lock, arg.Slot) + var i LockCodeSlot + err := row.Scan( + &i.ID, + &i.Lock, + &i.Code, + &i.Slot, + &i.Name, + &i.Enabled, + ) + return i, err +} + +const getLockCodes = `-- name: GetLockCodes :many +SELECT id, lock, code, slot, name, enabled FROM lock_code_slots WHERE lock = ? +` + +func (q *Queries) GetLockCodes(ctx context.Context, lock int64) ([]LockCodeSlot, error) { + rows, err := q.db.QueryContext(ctx, getLockCodes, lock) + if err != nil { + return nil, err + } + defer rows.Close() + var items []LockCodeSlot + for rows.Next() { + var i LockCodeSlot + if err := rows.Scan( + &i.ID, + &i.Lock, + &i.Code, + &i.Slot, + &i.Name, + &i.Enabled, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const upsertCodeSlot = `-- name: UpsertCodeSlot :exec +INSERT INTO lock_code_slots (lock, slot, code, enabled, name) VALUES (?, ?, ?, ?, "") ON CONFLICT (lock, slot) DO UPDATE SET code=excluded.code, enabled=excluded.enabled +` + +type UpsertCodeSlotParams struct { + Lock int64 + Slot int64 + Code string + Enabled bool +} + +func (q *Queries) UpsertCodeSlot(ctx context.Context, arg UpsertCodeSlotParams) error { + _, err := q.db.ExecContext(ctx, upsertCodeSlot, + arg.Lock, + arg.Slot, + arg.Code, + arg.Enabled, + ) + return err +} diff --git a/db/lock_log.sql.go b/db/lock_log.sql.go new file mode 100644 index 0000000..8477196 --- /dev/null +++ b/db/lock_log.sql.go @@ -0,0 +1,58 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.20.0 +// source: lock_log.sql + +package db + +import ( + "context" + "database/sql" +) + +const addLogEntry = `-- name: AddLogEntry :exec +INSERT INTO lock_log (lock, state, code) VALUES (?, ?, ?) +` + +type AddLogEntryParams struct { + Lock int64 + State string + Code sql.NullInt64 +} + +func (q *Queries) AddLogEntry(ctx context.Context, arg AddLogEntryParams) error { + _, err := q.db.ExecContext(ctx, addLogEntry, arg.Lock, arg.State, arg.Code) + return err +} + +const getLogForLock = `-- name: GetLogForLock :many +SELECT lock, timestamp, state, code FROM lock_log WHERE lock = ? ORDER BY timestamp DESC +` + +func (q *Queries) GetLogForLock(ctx context.Context, lock int64) ([]LockLog, error) { + rows, err := q.db.QueryContext(ctx, getLogForLock, lock) + if err != nil { + return nil, err + } + defer rows.Close() + var items []LockLog + for rows.Next() { + var i LockLog + if err := rows.Scan( + &i.Lock, + &i.Timestamp, + &i.State, + &i.Code, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/db/locks.sql.go b/db/locks.sql.go new file mode 100644 index 0000000..5fb05e6 --- /dev/null +++ b/db/locks.sql.go @@ -0,0 +1,73 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.20.0 +// source: locks.sql + +package db + +import ( + "context" +) + +const createLock = `-- name: CreateLock :one +INSERT INTO locks (zwave_device_id, name) VALUES (?, "") RETURNING id, name, zwave_device_id +` + +func (q *Queries) CreateLock(ctx context.Context, zwaveDeviceID int64) (Lock, error) { + row := q.db.QueryRowContext(ctx, createLock, zwaveDeviceID) + 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 = ? +` + +func (q *Queries) GetLockByDeviceID(ctx context.Context, zwaveDeviceID int64) (Lock, error) { + row := q.db.QueryRowContext(ctx, getLockByDeviceID, zwaveDeviceID) + var i Lock + err := row.Scan(&i.ID, &i.Name, &i.ZwaveDeviceID) + return i, err +} + +const getLocks = `-- name: GetLocks :many +SELECT id, name, zwave_device_id FROM locks +` + +func (q *Queries) GetLocks(ctx context.Context) ([]Lock, error) { + rows, err := q.db.QueryContext(ctx, getLocks) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Lock + for rows.Next() { + var i Lock + if err := rows.Scan(&i.ID, &i.Name, &i.ZwaveDeviceID); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateLockName = `-- name: UpdateLockName :exec +UPDATE locks SET name = ? WHERE id = ? +` + +type UpdateLockNameParams struct { + Name string + ID int64 +} + +func (q *Queries) UpdateLockName(ctx context.Context, arg UpdateLockNameParams) error { + _, err := q.db.ExecContext(ctx, updateLockName, arg.Name, arg.ID) + return err +} diff --git a/db/migrations/1_init.sql b/db/migrations/1_init.sql new file mode 100644 index 0000000..7654af4 --- /dev/null +++ b/db/migrations/1_init.sql @@ -0,0 +1,35 @@ +-- +goose Up +-- +goose StatementBegin +PRAGMA foreign_keys = ON; + +CREATE TABLE locks ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + zwave_device_id INTEGER NOT NULL UNIQUE +); + +CREATE TABLE lock_code_slots ( + id INTEGER PRIMARY KEY, + lock INTEGER NOT NULL REFERENCES locks(id), + code TEXT NOT NULL, + slot INTEGER NOT NULL, + name TEXT NOT NULL, + enabled BOOLEAN NOT NULL DEFAULT 0, + + UNIQUE (lock, slot) +); + +CREATE TABLE lock_log ( + lock INTEGER NOT NULL REFERENCES locks(id), + timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + state TEXT NOT NULL, + code INTEGER REFERENCES lock_code_slots(id) +); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE lock_log; +DROP TABLE lock_code_slots; +DROP TABLE locks; +-- +goose StatementEnd diff --git a/db/models.go b/db/models.go new file mode 100644 index 0000000..f540303 --- /dev/null +++ b/db/models.go @@ -0,0 +1,32 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.20.0 + +package db + +import ( + "database/sql" + "time" +) + +type Lock struct { + ID int64 + Name string + ZwaveDeviceID int64 +} + +type LockCodeSlot struct { + ID int64 + Lock int64 + Code string + Slot int64 + Name string + Enabled bool +} + +type LockLog struct { + Lock int64 + Timestamp time.Time + State string + Code sql.NullInt64 +} diff --git a/db/queries/lock_code_slots.sql b/db/queries/lock_code_slots.sql new file mode 100644 index 0000000..38ce427 --- /dev/null +++ b/db/queries/lock_code_slots.sql @@ -0,0 +1,8 @@ +-- name: UpsertCodeSlot :exec +INSERT INTO lock_code_slots (lock, slot, code, enabled, name) VALUES (?, ?, ?, ?, "") ON CONFLICT (lock, slot) DO UPDATE SET code=excluded.code, enabled=excluded.enabled; + +-- name: GetLockCodeBySlot :one +SELECT * FROM lock_code_slots WHERE lock = ? AND slot = ?; + +-- name: GetLockCodes :many +SELECT * FROM lock_code_slots WHERE lock = ?; diff --git a/db/queries/lock_log.sql b/db/queries/lock_log.sql new file mode 100644 index 0000000..adfe54f --- /dev/null +++ b/db/queries/lock_log.sql @@ -0,0 +1,5 @@ +-- name: AddLogEntry :exec +INSERT INTO lock_log (lock, state, code) VALUES (?, ?, ?); + +-- name: GetLogForLock :many +SELECT * FROM lock_log WHERE lock = ? ORDER BY timestamp DESC; diff --git a/db/queries/locks.sql b/db/queries/locks.sql new file mode 100644 index 0000000..e259cfc --- /dev/null +++ b/db/queries/locks.sql @@ -0,0 +1,11 @@ +-- name: CreateLock :one +INSERT INTO locks (zwave_device_id, name) VALUES (?, "") RETURNING *; + +-- name: GetLocks :many +SELECT * FROM locks; + +-- name: GetLockByDeviceID :one +SELECT * FROM locks WHERE zwave_device_id = ?; + +-- name: UpdateLockName :exec +UPDATE locks SET name = ? WHERE id = ?; diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..556c8ce --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module git.janky.solutions/finn/lockserver + +go 1.21.8 + +require ( + github.com/gorilla/websocket v1.5.1 + github.com/mattn/go-sqlite3 v1.14.22 + github.com/pressly/goose/v3 v3.19.2 + github.com/sirupsen/logrus v1.9.3 +) + +require ( + github.com/mfridman/interpolate v0.0.2 // indirect + github.com/sethvargo/go-retry v0.2.4 // indirect + go.uber.org/multierr v1.11.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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e2d9a89 --- /dev/null +++ b/go.sum @@ -0,0 +1,187 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/ClickHouse/ch-go v0.58.2 h1:jSm2szHbT9MCAB1rJ3WuCJqmGLi5UTjlNu+f530UTS0= +github.com/ClickHouse/ch-go v0.58.2/go.mod h1:Ap/0bEmiLa14gYjCiRkYGbXvbe8vwdrfTYWhsuQ99aw= +github.com/ClickHouse/clickhouse-go/v2 v2.17.1 h1:ZCmAYWpu75IyEi7+Yrs/uaAjiCGY5wfW5kXo64exkX4= +github.com/ClickHouse/clickhouse-go/v2 v2.17.1/go.mod h1:rkGTvFDTLqLIm0ma+13xmcCfr/08Gvs7KmFt1tgiWHQ= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +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/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/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= +github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/cli v24.0.7+incompatible h1:wa/nIwYFW7BVTGa7SWPVyyXU9lgORqUb1xfI36MSkFg= +github.com/docker/cli v24.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= +github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/elastic/go-sysinfo v1.11.2 h1:mcm4OSYVMyws6+n2HIVMGkln5HOpo5Ie1ZmbbNn0jg4= +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/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-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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +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= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +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/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= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4= +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/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= +github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +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/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= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= +github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +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/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +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= +github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= +github.com/opencontainers/runc v1.1.12 h1:BOIssBaW1La0/qbNZHXOOa71dZfZEQOzW7dqQf3phss= +github.com/opencontainers/runc v1.1.12/go.mod h1:S+lQwSfncpBha7XTy/5lBwWgm5+y5Ma/O44Ekby9FK8= +github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= +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/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= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pressly/goose/v3 v3.19.2 h1:z1yuD41jS4iaqLkyjkzGkKBz4rgyz/BYtCyMMGHlgzQ= +github.com/pressly/goose/v3 v3.19.2/go.mod h1:BHkf3LzSBmO8E5FTMPupUYIpMTIh/ZuQVy+YTfhZLD4= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +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/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= +github.com/sethvargo/go-retry v0.2.4/go.mod h1:1afjQuvh7s4gflMObvjLPaWgluLLyhA1wmVZ6KLpICw= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +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/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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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/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= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/ydb-platform/ydb-go-genproto v0.0.0-20240126124512-dbb0e1720dbf h1:ckwNHVo4bv2tqNkgx3W3HANh3ta1j6TR5qw08J1A7Tw= +github.com/ydb-platform/ydb-go-genproto v0.0.0-20240126124512-dbb0e1720dbf/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I= +github.com/ydb-platform/ydb-go-sdk/v3 v3.55.1 h1:Ebo6J5AMXgJ3A438ECYotA0aK7ETqjQx9WoZvVxzKBE= +github.com/ydb-platform/ydb-go-sdk/v3 v3.55.1/go.mod h1:udNPW8eupyH/EZocecFmaSNJacKKYjzQa7cVgX5U2nc= +go.opentelemetry.io/otel v1.20.0 h1:vsb/ggIY+hUjD/zCAQHpzTmndPqv/ml2ArbsbfBYTAc= +go.opentelemetry.io/otel v1.20.0/go.mod h1:oUIGj3D77RwJdM6PPZImDpSZGDvkD9fhesHny69JFrs= +go.opentelemetry.io/otel/trace v1.20.0 h1:+yxVAPZPbQhbC3OfAkeIVTky6iTFpcr4SiY9om7mXSQ= +go.opentelemetry.io/otel/trace v1.20.0/go.mod h1:HJSK7F/hA5RlzpZ0zKDCHCDHm556LCDtKaAo6JmBFUU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w= +golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +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.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= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +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/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.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= +howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk= +modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= +modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= +modernc.org/sqlite v1.29.5 h1:8l/SQKAjDtZFo9lkJLdk8g9JEOeYRG4/ghStDCCTiTE= +modernc.org/sqlite v1.29.5/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 0000000..606d1f1 --- /dev/null +++ b/sqlc.yaml @@ -0,0 +1,9 @@ +version: "2" +sql: + - engine: "sqlite" + schema: "db/migrations" + queries: "db/queries" + gen: + go: + package: "db" + out: "db" diff --git a/zwavejs/client.go b/zwavejs/client.go new file mode 100644 index 0000000..eb44689 --- /dev/null +++ b/zwavejs/client.go @@ -0,0 +1,204 @@ +package zwavejs + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "fmt" + "time" + + "git.janky.solutions/finn/lockserver/db" + "github.com/gorilla/websocket" + "github.com/sirupsen/logrus" +) + +type Client struct { + conn *websocket.Conn +} + +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) + } + }() + + return c, nil +} + +func (c *Client) DialAndListen(ctx context.Context, server string) error { + for { + conn, _, err := websocket.DefaultDialer.DialContext(ctx, server, nil) + if err != nil { + return err + } + c.conn = conn + + if err := c.listen(ctx); err != nil { + return err + } + } +} + +func (c Client) listen(ctx context.Context) error { + for { + _, msg, err := c.conn.ReadMessage() + if err != nil { + return err + } + + var parsed IncomingMessage + if err := json.Unmarshal(msg, &parsed); err != nil { + return err + } + + switch parsed.Type { + case "version": + if err := c.conn.WriteJSON(OutgoingMessage{Command: StartListeningCommand}); err != nil { + return err + } + + case "result": + if err := syncState(ctx, *parsed.Result); err != nil { + return err + } + + case "event": + if err := handleEvent(ctx, *parsed.Event); err != nil { + logrus.WithError(err).Error("error handling event") + } + + default: + fmt.Println(string(msg)) + } + } +} + +func (c Client) Shutdown() error { + return c.conn.Close() +} + +func syncState(ctx context.Context, result Result) error { + queries, dbc, err := db.Get() + if err != nil { + return err + } + defer dbc.Close() + + for _, node := range result.State.Nodes { + slots := make(map[int]db.LockCodeSlot) + lockID := int64(-1) + for _, value := range node.Values { + if value.CommandClass != CommandClassUserCode { + continue + } + + slotNumber, err := value.PropertyKey.Int() + if err != nil { + logrus.WithError(err).WithField("value", value.PropertyKey.String()).Warn("unexpected non-int PropertyKey") + continue + } + + lockID = int64(node.NodeID) + + slot := slots[slotNumber] // check if there's an existing entry + slot.Slot = int64(slotNumber) + + switch value.PropertyName { + case PropertyUserCode: + slot.Code = value.Value.String + case PropertyUserIDStatus: + slot.Enabled = value.Value.Int > 0 + } + + slots[slotNumber] = slot + } + + if len(slots) == 0 || lockID < 0 { + continue + } + + lock, err := queries.GetLockByDeviceID(ctx, lockID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + lock, err = queries.CreateLock(ctx, lockID) + } + + if err != nil { + return err + } + } + + for _, slot := range slots { + err := queries.UpsertCodeSlot(ctx, db.UpsertCodeSlotParams{ + Lock: lock.ID, + Code: slot.Code, + Slot: slot.Slot, + Enabled: slot.Enabled, + }) + if err != nil { + return fmt.Errorf("error upserting slot: %v", err) + } + } + } + + return nil +} + +func handleEvent(ctx context.Context, event Event) error { + if event.Source != EventSourceNode || event.Event != EventTypeNotification { + return nil + } + + queries, dbc, err := db.Get() + if err != nil { + return err + } + defer dbc.Close() + + lock, err := queries.GetLockByDeviceID(ctx, int64(event.NodeID)) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil + } + return fmt.Errorf("error getting lock: %v", err) + } + + code := sql.NullInt64{} + if event.Parameters.UserID > 0 { + slot, err := queries.GetLockCodeBySlot(ctx, db.GetLockCodeBySlotParams{ + Lock: lock.ID, + }) + if err != nil { + return fmt.Errorf("error getting code slot: %v", err) + } + code = sql.NullInt64{ + Int64: slot.ID, + Valid: true, + } + } + + err = queries.AddLogEntry(ctx, db.AddLogEntryParams{ + Lock: lock.ID, + Code: code, + State: event.NotificationLabel, + }) + if err != nil { + return fmt.Errorf("error adding log entry: %v", err) + } + + logrus.WithFields(logrus.Fields{ + "lock": lock.ID, + "code": code, + "state": event.NotificationLabel, + }).Debug("processed lock event") + + return nil +} diff --git a/zwavejs/consts.go b/zwavejs/consts.go new file mode 100644 index 0000000..6dc2566 --- /dev/null +++ b/zwavejs/consts.go @@ -0,0 +1,16 @@ +package zwavejs + +var ( + CommandClassDoorLock = 98 + CommandClassUserCode = 99 + + PropertyUserCode = "userCode" + PropertyUserIDStatus = "userIdStatus" + + EventSourceController = "controller" + EventSourceNode = "node" + + EventTypeValueUpdated = "value updated" + EventTypeStatisticsUpdated = "statistics updated" + EventTypeNotification = "notification" +) diff --git a/zwavejs/messages-incoming.go b/zwavejs/messages-incoming.go new file mode 100644 index 0000000..1e45f8b --- /dev/null +++ b/zwavejs/messages-incoming.go @@ -0,0 +1,267 @@ +package zwavejs + +import ( + "bytes" + "encoding/json" + "fmt" + "strconv" + + "github.com/sirupsen/logrus" +) + +type IncomingMessage struct { + Type string `json:"type"` + MessageID string `json:"messageId"` + Success bool `json:"success"` + + // Values for type = version + DriverVersion string `json:"driverVersion"` + ServerVersion string `json:"serverVersion"` + HomeID int `json:"homeId"` + + Event *Event + Result *Result +} + +type Event struct { + Source string `json:"source"` + Event string `json:"event"` + + NodeID int `json:"nodeId"` + Args NodeEventArgs `json:"args"` + NotificationLabel string `json:"notificationLabel"` + Parameters EventParameters `json:"parameters"` +} + +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"` +} + +type EventParameters struct { + UserID int `json:"userId"` +} + +type Result struct { + State struct { + Controller Controller + Driver Driver + Nodes []Node + } +} + +type Controller struct { + Type int `json:"type"` + HomeID int64 `json:"homeId"` + OwnNodeID int `json:"ownNodeId"` + IsUsingHomeIDFromOtherNetwork bool `json:"isUsingHomeIdFromOtherNetwork"` + IsSISPresent bool `json:"isSISPresent"` + WasRealPrimary bool `json:"wasRealPrimary"` + ManufacturerID int `json:"manufacturerId"` + ProductType int `json:"productType"` + ProductID int `json:"productId"` + SupportedFunctionTypes []int `json:"supportedFunctionTypes"` + SucNodeID int `json:"sucNodeId"` + SupportsTimers bool `json:"supportsTimers"` + Statistics ControllerStatistics `json:"statistics"` +} + +type ControllerStatistics struct { + MessagesTX int `json:"messagesTX"` + MessagesRX int `json:"messagesRX"` + MessagesDroppedRX int `json:"messagesDroppedRX"` + Nak int `json:"NAK"` + Can int `json:"CAN"` + TimeoutACK int `json:"timeoutACK"` + TimeoutResponse int `json:"timeoutResponse"` + TimeoutCallback int `json:"timeoutCallback"` + MessagesDroppedTX int `json:"messagesDroppedTX"` + InclusionState int `json:"inclusionState"` + IsSecondary bool `json:"isSecondary"` + IsStaticUpdateController bool `json:"isStaticUpdateController"` + IsSlave bool `json:"isSlave"` + IsHealNetworkActive bool `json:"isHealNetworkActive"` + LibraryVersion string `json:"libraryVersion"` + SerialAPIVersion string `json:"serialApiVersion"` +} + +type Driver struct { + LogConfig DriverLogConfig + StatisticsEnabled bool +} + +type DriverLogConfig struct { + Enabled bool `json:"enabled"` + Level int `json:"level"` + LogToFile bool `json:"logToFile"` + MaxFiles int `json:"maxFiles"` + Filename string `json:"filename"` + ForceConsole bool `json:"forceConsole"` +} + +type Node struct { + NodeID int `json:"nodeId"` + Index int `json:"index"` + Status int `json:"status"` + Ready bool `json:"ready"` + IsListening bool `json:"isListening"` + IsRouting bool `json:"isRouting"` + ManufacturerID int `json:"manufacturerId"` + ProductID int `json:"productId"` + ProductType int `json:"productType"` + FirmwareVersion string `json:"firmwareVersion"` + DeviceConfig NodeDeviceConfig `json:"deviceConfig"` + Label string `json:"label"` + InterviewAttempts int `json:"interviewAttempts"` + Endpoints []NodeEndpoint `json:"endpoints"` + Values []NodeValues `json:"values"` + InterviewStage int `json:"interviewStage"` + IsFrequentListening any `json:"isFrequentListening"` + MaxBaudRate int `json:"maxBaudRate"` + Version int `json:"version"` + IsBeaming bool `json:"isBeaming"` + DeviceClass NodeDeviceClass `json:"deviceClass"` + InstallerIcon int `json:"installerIcon,omitempty"` + UserIcon int `json:"userIcon,omitempty"` + IsSecure bool `json:"isSecure,omitempty"` + ZwavePlusVersion int `json:"zwavePlusVersion,omitempty"` + NodeType int `json:"nodeType,omitempty"` + RoleType int `json:"roleType,omitempty"` + EndpointCountIsDynamic bool `json:"endpointCountIsDynamic,omitempty"` + EndpointsHaveIdenticalCapabilities bool `json:"endpointsHaveIdenticalCapabilities,omitempty"` + IndividualEndpointCount int `json:"individualEndpointCount,omitempty"` + AggregatedEndpointCount int `json:"aggregatedEndpointCount,omitempty"` +} + +type NodeDeviceConfig struct { + Filename string `json:"filename"` + IsEmbedded bool `json:"isEmbedded"` + Manufacturer string `json:"manufacturer"` + ManufacturerID int `json:"manufacturerId"` + Label string `json:"label"` + Description string `json:"description"` + Devices []NodeDeviceConfigDevice `json:"devices"` + FirmwareVersion NodeDeviceFirmwareVersion `json:"firmwareVersion"` + Preferred bool `json:"preferred"` + Metadata NodeDeviceMetadata `json:"metadata"` +} + +type NodeDeviceConfigDevice struct { + ProductType int `json:"productType"` + ProductID int `json:"productId"` +} + +type NodeDeviceFirmwareVersion struct { + Min string `json:"min"` + Max string `json:"max"` +} + +type NodeDeviceMetadata struct { + Comments []NodeDeviceMetadataComments `json:"comments"` + Wakeup string `json:"wakeup"` + Inclusion string `json:"inclusion"` + Exclusion string `json:"exclusion"` + Reset string `json:"reset"` + Manual string `json:"manual"` +} + +type NodeDeviceMetadataComments struct { + Level string `json:"level"` + Text string `json:"text"` +} + +type NodeEndpoint struct { + NodeID int `json:"nodeId"` + Index int `json:"index"` +} + +type NodeValues 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"` + Metadata NodeValuesMetadata `json:"metadata"` + Value NodePropertyValue `json:"value"` +} + +type NodeValuesMetadata struct { + Type string `json:"type"` + Readable bool `json:"readable"` + Writeable bool `json:"writeable"` + Label string `json:"label"` + Min int `json:"min"` + Max int `json:"max"` +} + +type NodeDeviceClass struct { + Basic string `json:"basic"` + Generic string `json:"generic"` + Specific string `json:"specific"` + 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 new file mode 100644 index 0000000..075faaa --- /dev/null +++ b/zwavejs/messages-outgoing.go @@ -0,0 +1,19 @@ +package zwavejs + +type Command string + +var ( + InitializeCommand Command = "initialize" + StartListeningCommand Command = "start_listening" +) + +type OutgoingMessage struct { + MessageID string `json:"messageId"` + Command Command `json:"command"` +} + +type Initialize struct { + OutgoingMessage + SchemaVersion int `json:"schemaVersion"` + AdditionalUserAgentComponents map[string]string `json:"additionalUserAgentComponents"` +}