lockserver/zwavejs/client.go
Finn 054008eb1f Initial commit
Connects to zwave-js, syncs all locks and codeslots with database, and records an event log. No support for updating code slots.
2024-04-08 21:25:36 -07:00

204 lines
4 KiB
Go

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
}