Finn
054008eb1f
Connects to zwave-js, syncs all locks and codeslots with database, and records an event log. No support for updating code slots.
204 lines
4 KiB
Go
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
|
|
}
|