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 }