2024-04-09 04:25:36 +00:00
|
|
|
package zwavejs
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"database/sql"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2024-04-24 00:34:57 +00:00
|
|
|
"net"
|
2024-04-09 23:18:43 +00:00
|
|
|
"sync"
|
2024-04-09 04:25:36 +00:00
|
|
|
"time"
|
|
|
|
|
2024-04-24 00:34:57 +00:00
|
|
|
"github.com/failsafe-go/failsafe-go"
|
|
|
|
"github.com/failsafe-go/failsafe-go/retrypolicy"
|
2024-04-09 23:18:43 +00:00
|
|
|
"github.com/google/uuid"
|
2024-04-09 04:25:36 +00:00
|
|
|
"github.com/gorilla/websocket"
|
|
|
|
"github.com/sirupsen/logrus"
|
2024-04-24 00:34:57 +00:00
|
|
|
|
|
|
|
"git.janky.solutions/finn/lockserver/db"
|
2024-04-09 04:25:36 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type Client struct {
|
2024-04-09 23:18:43 +00:00
|
|
|
Server string
|
2024-04-09 04:25:36 +00:00
|
|
|
|
2024-04-09 23:18:43 +00:00
|
|
|
conn *websocket.Conn
|
|
|
|
callbacks map[string]chan Result
|
|
|
|
callbacksLock sync.Mutex
|
|
|
|
}
|
2024-04-09 04:25:36 +00:00
|
|
|
|
2024-04-09 23:18:43 +00:00
|
|
|
func New(server string) (*Client, error) {
|
|
|
|
c := &Client{
|
|
|
|
Server: server,
|
|
|
|
callbacks: make(map[string]chan Result),
|
|
|
|
}
|
2024-04-09 04:25:36 +00:00
|
|
|
return c, nil
|
|
|
|
}
|
|
|
|
|
2024-04-09 23:18:43 +00:00
|
|
|
func (c *Client) DialAndListen(ctx context.Context) {
|
2024-04-24 00:34:57 +00:00
|
|
|
// Retry on ErrConnecting up to 3 times with a 1 second delay between attempts
|
|
|
|
connectRetryPolicy := retrypolicy.Builder[*websocket.Conn]().
|
|
|
|
WithBackoff(time.Second, time.Minute).
|
|
|
|
Build()
|
|
|
|
|
2024-04-09 04:25:36 +00:00
|
|
|
for {
|
2024-04-09 23:18:43 +00:00
|
|
|
logrus.WithField("server", c.Server).Info("connecting to zwave-js server")
|
2024-04-24 00:34:57 +00:00
|
|
|
conn, err := failsafe.Get(func() (*websocket.Conn, error) {
|
|
|
|
conn, _, err := websocket.DefaultDialer.DialContext(ctx, c.Server, nil)
|
|
|
|
return conn, err
|
|
|
|
}, connectRetryPolicy)
|
2024-04-09 04:25:36 +00:00
|
|
|
if err != nil {
|
2024-04-24 00:34:57 +00:00
|
|
|
logrus.WithError(err).Fatal("error connecting to zwavejs server")
|
2024-04-09 04:25:36 +00:00
|
|
|
}
|
|
|
|
c.conn = conn
|
2024-04-09 23:18:43 +00:00
|
|
|
logrus.Info("connected to zwave-js server")
|
2024-04-09 04:25:36 +00:00
|
|
|
|
|
|
|
if err := c.listen(ctx); err != nil {
|
2024-04-24 00:34:57 +00:00
|
|
|
if errors.Is(err, net.ErrClosed) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-04-09 23:18:43 +00:00
|
|
|
logrus.WithError(err).Error("error communicating with zwavejs server")
|
2024-04-24 04:19:36 +00:00
|
|
|
time.Sleep(time.Second * 5)
|
2024-04-09 23:18:43 +00:00
|
|
|
continue
|
2024-04-09 04:25:36 +00:00
|
|
|
}
|
2024-04-24 00:34:57 +00:00
|
|
|
_ = c.conn.Close()
|
2024-04-09 04:25:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-09 23:18:43 +00:00
|
|
|
func (c *Client) listen(ctx context.Context) error {
|
2024-04-09 04:25:36 +00:00
|
|
|
for {
|
2024-04-09 23:18:43 +00:00
|
|
|
var msg IncomingMessage
|
|
|
|
// err := c.conn.ReadJSON(&msg)
|
|
|
|
// if err != nil {
|
|
|
|
// return err
|
|
|
|
// }
|
|
|
|
|
|
|
|
_, rawmsg, err := c.conn.ReadMessage()
|
2024-04-09 04:25:36 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-04-09 23:18:43 +00:00
|
|
|
if err = json.Unmarshal(rawmsg, &msg); err != nil {
|
2024-04-09 04:25:36 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-04-09 23:18:43 +00:00
|
|
|
logrus.WithField("type", msg.Type).Debug("received message from zwave-js")
|
|
|
|
|
|
|
|
switch msg.Type {
|
2024-04-09 04:25:36 +00:00
|
|
|
case "version":
|
2024-04-09 23:18:43 +00:00
|
|
|
if err := c.conn.WriteJSON(OutgoingMessage{Command: CommandStartListening}); err != nil {
|
2024-04-09 04:25:36 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
case "result":
|
2024-04-09 23:18:43 +00:00
|
|
|
if msg.MessageID == "" {
|
|
|
|
if err := syncState(ctx, *msg.Result); err != nil {
|
2024-11-23 03:50:18 +00:00
|
|
|
return fmt.Errorf("error syncing state from zwavejs server: %v", err)
|
2024-04-09 23:18:43 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
c.handleCallback(msg.MessageID, *msg.Result)
|
2024-04-09 04:25:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
case "event":
|
2024-04-09 23:18:43 +00:00
|
|
|
if err := handleEvent(ctx, *msg.Event); err != nil {
|
2024-04-09 04:25:36 +00:00
|
|
|
logrus.WithError(err).Error("error handling event")
|
|
|
|
}
|
|
|
|
|
|
|
|
default:
|
2024-04-09 23:18:43 +00:00
|
|
|
logrus.WithField("type", msg.Type).Warn("received unexpected message type from zwave-js server")
|
2024-04-09 04:25:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-09 23:18:43 +00:00
|
|
|
func (c *Client) Shutdown() error {
|
2024-04-09 04:25:36 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-04-09 23:18:43 +00:00
|
|
|
slotNumber := value.PropertyKey.Int
|
2024-04-09 04:25:36 +00:00
|
|
|
|
|
|
|
lockID = int64(node.NodeID)
|
|
|
|
|
|
|
|
slot := slots[slotNumber] // check if there's an existing entry
|
|
|
|
slot.Slot = int64(slotNumber)
|
|
|
|
|
2024-04-09 23:18:43 +00:00
|
|
|
switch Property(value.PropertyName.String) {
|
2024-04-09 04:25:36 +00:00
|
|
|
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,
|
2024-04-09 04:46:46 +00:00
|
|
|
Slot: int64(event.Parameters.UserID),
|
2024-04-09 04:25:36 +00:00
|
|
|
})
|
|
|
|
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
|
|
|
|
}
|
2024-04-09 23:18:43 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|