ctucx.git: smartied

[nimlang] smarthome server

commit 5dcb4e1245ceae7df282cadcb10c65116ca57af0
parent 8fe522fe9ecaa5aa1aea2841f1555043aba29be1
Author: Leah (ctucx) <leah@ctu.cx>
Date: Thu, 18 Feb 2021 17:07:56 +0100

make things more failsafe
9 files changed, 132 insertions(+), 99 deletions(-)
M
src/devices/modbusPowermeter.nim
|
3
+--
M
src/devices/modbusRelayboard.nim
|
3
+--
M
src/devices/zigbee2mqttLamp.nim
|
41
++++++++++++++++++++++-------------------
M
src/devices/zigbee2mqttRelay.nim
|
26
++++++++++++++------------
M
src/devices/zigbee2mqttRemote.nim
|
14
+++++++++-----
M
src/frontend.nim
|
33
+++++++++++++++------------------
M
src/influx.nim
|
66
++++++++++++++++++++++++++++++++++++------------------------------
M
src/mqtt.nim
|
15
++++++++++-----
M
src/utils.nim
|
30
++++++++++++++++++++++++------
diff --git a/src/devices/modbusPowermeter.nim b/src/devices/modbusPowermeter.nim
@@ -44,8 +44,7 @@ proc updatePowermeters () {.async.} =
     try:
       await updatePowermeter(key, device)
     except:
-      let e = getCurrentException()
-      echo("error while updating powermeter ", key, ": ", e.msg)
+      echo "Error[updatePowermeters]:\n", getCurrentExceptionMsg()
 
 proc powermetersLoop () {.async.} =
   await sleepAsync(500)
diff --git a/src/devices/modbusRelayboard.nim b/src/devices/modbusRelayboard.nim
@@ -23,8 +23,7 @@ proc updateRelayboards () {.async.} =
       let data = await mb.asyncReadBits(device.address.get, device.firstRegister, device.count)
       server.state[key] = DeviceState(type: RelayBoard, relays: data)
     except:
-      let e = getCurrentException()
-      echo("error while updating relayboard ", key, ": ", e.msg)
+      echo "Error[updateRelayboards]:\n", getCurrentExceptionMsg()
   
   broadcastServerState()
 
diff --git a/src/devices/zigbee2mqttLamp.nim b/src/devices/zigbee2mqttLamp.nim
@@ -47,32 +47,35 @@ proc setZigbee2MqttLampColorTemperature* (deviceName: string, colorTemperature: 
     await mqttContext.publish("zigbee2mqtt/" & config.deviceName.get & "/set", "{\"color_temp\": \"" & $colorTemperature & "\"}")
 
 proc updateLamp (topic: string, message: string) =
-  let deviceName = zigbee2mqttDevices[topic]
-  let recivedData = parseJson(message)
-  
-  server.state[deviceName].lastUpdated = some(toUnix(getTime()))
+  try:
+    let deviceName = zigbee2mqttDevices[topic]
+    let recivedData = parseJson(message)
 
-  if recivedData.hasKey("linkquality"):
-    server.state[deviceName].lampLinkquality = recivedData["linkquality"].getInt
+    server.state[deviceName].lastUpdated = some(toUnix(getTime()))
 
-  if recivedData.hasKey("state"):
-    if recivedData["state"].getStr != "ON":
-      server.state[deviceName].lampState = false
-    else:
-      server.state[deviceName].lampState = true      
+    if recivedData.hasKey("linkquality"):
+      server.state[deviceName].lampLinkquality = recivedData["linkquality"].getInt
 
-  if recivedData.hasKey("brightness"):
-    server.state[deviceName].lampBrightness = recivedData["brightness"].getInt
+    if recivedData.hasKey("state"):
+      if recivedData["state"].getStr != "ON":
+        server.state[deviceName].lampState = false
+      else:
+        server.state[deviceName].lampState = true
 
-  if recivedData.hasKey("color"):
-    server.state[deviceName].lampColorX = recivedData["color"]["x"].getFloat
-    server.state[deviceName].lampColorY = recivedData["color"]["y"].getFloat
+    if recivedData.hasKey("brightness"):
+      server.state[deviceName].lampBrightness = recivedData["brightness"].getInt
 
-  if recivedData.hasKey("color_temp"):
-    server.state[deviceName].lampColorTemperature = recivedData["color_temp"].getInt
+    if recivedData.hasKey("color"):
+      server.state[deviceName].lampColorX = recivedData["color"]["x"].getFloat
+      server.state[deviceName].lampColorY = recivedData["color"]["y"].getFloat
 
-  broadcastServerState()
+    if recivedData.hasKey("color_temp"):
+      server.state[deviceName].lampColorTemperature = recivedData["color_temp"].getInt
 
+    broadcastServerState()
+
+  except:
+    echo "Error[updateLamp]:\n", getCurrentExceptionMsg()
 
 proc initZigbee2MqttLamps* () {.async.} =
   for key, device in server.config.devices.pairs():
diff --git a/src/devices/zigbee2mqttRelay.nim b/src/devices/zigbee2mqttRelay.nim
@@ -23,22 +23,24 @@ proc toggleZigbee2MqttRelayState* (deviceName: string) {.async.} =
   await mqttContext.publish("zigbee2mqtt/" & config.deviceName.get & "/set", "{\"state\": \"TOGGLE\"}")
 
 proc updateRelay (topic: string, message: string) =
-  let deviceName = zigbee2mqttDevices[topic]
-  let recivedData = parseJson(message)
+  try:
+    let deviceName = zigbee2mqttDevices[topic]
+    let recivedData = parseJson(message)
   
-  server.state[deviceName].lastUpdated = some(toUnix(getTime()))
+    server.state[deviceName].lastUpdated = some(toUnix(getTime()))
 
-  if recivedData.hasKey("linkquality"):
-    server.state[deviceName].relayLinkquality = recivedData["linkquality"].getInt
+    if recivedData.hasKey("linkquality"):
+      server.state[deviceName].relayLinkquality = recivedData["linkquality"].getInt
 
-  if recivedData.hasKey("state"):
-    if recivedData["state"].getStr != "ON":
-      server.state[deviceName].relayState = false
-    else:
-      server.state[deviceName].relayState = true      
-
-  broadcastServerState()
+    if recivedData.hasKey("state"):
+      if recivedData["state"].getStr != "ON":
+        server.state[deviceName].relayState = false
+      else:
+        server.state[deviceName].relayState = true
 
+    broadcastServerState()
+  except:
+    echo "Error[updateRelay]:\n", getCurrentExceptionMsg()
 
 proc initZigbee2MqttRelays* () {.async.} =
   for key, device in server.config.devices.pairs():
diff --git a/src/devices/zigbee2mqttRemote.nim b/src/devices/zigbee2mqttRemote.nim
@@ -3,12 +3,16 @@ import ../types, ../vars, ../deviceActionHandler
 import nmqtt
 
 proc handleRemote (topic: string, message: string) =
-  let deviceName  = zigbee2mqttDevices[topic]
-  let config      = server.config.devices[deviceName]
-  let recivedData = parseJson(message)
+  try:
+    let deviceName  = zigbee2mqttDevices[topic]
+    let config      = server.config.devices[deviceName]
+    let recivedData = parseJson(message)
 
-  if config.actions.hasKey(recivedData["action"].getStr):
-    discard waitFor handleDeviceAction(config.actions[recivedData["action"].getStr])
+    if config.actions.hasKey(recivedData["action"].getStr):
+      discard waitFor handleDeviceAction(config.actions[recivedData["action"].getStr])
+
+  except:
+    echo "Error[handleRemote]:\n", getCurrentExceptionMsg()
 
 proc initZigbee2MqttRemotes* () {.async.} =
   for key, device in server.config.devices.pairs():
diff --git a/src/frontend.nim b/src/frontend.nim
@@ -1,4 +1,4 @@
-import asyncdispatch, asynchttpserver, json, tables, options
+import asyncdispatch, asynchttpserver, json, tables, options, sequtils
 import ws
 import types, vars, utils, deviceActionHandler
 

@@ -9,7 +9,8 @@ proc processWsClient(req: Request) {.async,gcsafe.} =
   try:
     ws = await newWebsocket(req)
     wsConnections.add(ws)
-    setupPings(ws, 2)
+#    setupPings(ws, 2)
+    echo "new ws connection: " & ws.key
 
   except:
     asyncCheck req.respond(Http404, "404")

@@ -29,11 +30,11 @@ proc processWsClient(req: Request) {.async,gcsafe.} =
 
           if action.type == SetSubscriptionStateAction:
             if action.subscribed:
-              echo "adding client"
+              echo "adding client: " & ws.key
               subscribedConnections.add(ws)
             else:
-              echo "removing client (todo)"
-
+              echo "removing client: " & ws.key
+              subscribedConnections.keepIf(proc(x: Websocket): bool = x.key != ws.key)
           else:
             response = await handleDeviceAction(action)
 

@@ -44,7 +45,7 @@ proc processWsClient(req: Request) {.async,gcsafe.} =
           await ws.send($(%*Response(status: Err, data: newJString(e.msg))))
 
   except WebSocketError:
-    echo "socket closed:", getCurrentExceptionMsg()
+    echo "Error[processWsClient]:\n", getCurrentExceptionMsg()
 
 proc processHttpClient(req: Request) {.async,gcsafe.} =
   if req.reqMethod == HttpGet:

@@ -115,14 +116,10 @@ proc processRequest(req: Request) {.async,gcsafe.} =
     asyncCheck req.respond(Http404, "404")
 
 proc serveFrontend*() {.async.} =
-  var httpServer = newAsyncHttpServer()
-  await httpServer.serve(Port(server.config.serverConfig.frontendPort), processRequest)
-  #send empty paket every 2 seconds to every subscribed client
-  addTimer(2000, false, proc (fd: AsyncFD): bool {.gcsafe.} =
-      broadcast("")
-    )
-
-  #clean up broken websocket connections every 60 seconds 
-  addTimer(60000, false, proc (fd: AsyncFD): bool {.gcsafe.} =
-      cleanupConnections()
-    )-
\ No newline at end of file
+  try:
+    var httpServer = newAsyncHttpServer()
+    await httpServer.serve(Port(server.config.serverConfig.frontendPort), processRequest)
+
+  except:
+    echo "Error[serveFrontend]:\n", getCurrentExceptionMsg()
+    quit()+
\ No newline at end of file
diff --git a/src/influx.nim b/src/influx.nim
@@ -2,49 +2,55 @@ import asyncdispatch, strutils, tables, httpclient, options, json, base64, uri
 import types, vars
 
 proc existsDatabase* (config: InfluxConfig, databaseName: string): Future[bool] {.async.}=
-  var client = newAsyncHttpClient()
+  try:
+    var client = newAsyncHttpClient()
+
+    if config.username.isSome and config.password.isSome:
+      client.headers["Authorization"] = "Basic " & base64.encode(config.username.get & ":" & config.password.get)
 
-  if config.username.isSome and config.password.isSome:
-    client.headers["Authorization"] = "Basic " & base64.encode(config.username.get & ":" & config.password.get)
-  
-  let baseUrl  = "http://" & config.host & ":" & $config.port & "/"
-  let response = await client.getContent(baseUrl & "query?" & encodeQuery({"db": databaseName, "q": "select * FROM test LIMIT 1"}))
-  let data     = parseJson(response)
+    let baseUrl  = "http://" & config.host & ":" & $config.port & "/"
+    let response = await client.getContent(baseUrl & "query?" & encodeQuery({"db": databaseName, "q": "select * FROM test LIMIT 1"}))
+    let data     = parseJson(response)
 
-  client.close()
+    client.close()
 
-  if data["results"][0].hasKey("error"):
-   return false
+    if data["results"][0].hasKey("error"):
+     return false
 
-  return true  
+    return true
+  except:
+    echo "Error[existsDatabase]:\n", getCurrentExceptionMsg()
 
 proc insertDatabase* (config: InfluxConfig, databaseName: string, tableName: string, tags: Table[string, string], fields: Table[string, string], timestamp: int64): Future[bool] {.async.} =
-  var client = newAsyncHttpClient()
+  try:
+    var client = newAsyncHttpClient()
+
+    if config.username.isSome and config.password.isSome:
+      client.headers["Authorization"] = "Basic " & base64.encode(config.username.get & ":" & config.password.get)
 
-  if config.username.isSome and config.password.isSome:
-    client.headers["Authorization"] = "Basic " & base64.encode(config.username.get & ":" & config.password.get)
-  
-  let baseUrl  = "http://" & config.host & ":" & $config.port & "/"
+    let baseUrl  = "http://" & config.host & ":" & $config.port & "/"
 
-  var tagsCombined: seq[string]
-  var fieldsCombined: seq[string]
+    var tagsCombined: seq[string]
+    var fieldsCombined: seq[string]
 
-  for key, value in pairs(tags):
-    tagsCombined.add(key & "=" & value)
+    for key, value in pairs(tags):
+      tagsCombined.add(key & "=" & value)
 
-  for key, value in pairs(fields):
-    fieldsCombined.add(key & "=" & value)
+    for key, value in pairs(fields):
+      fieldsCombined.add(key & "=" & value)
 
-  let body = tableName & "," & tagsCombined.join(",") & " " & fieldsCombined.join(",") & " " & $timestamp
+    let body = tableName & "," & tagsCombined.join(",") & " " & fieldsCombined.join(",") & " " & $timestamp
 
-  let response = await client.request(baseUrl & "write?db=" & databaseName, httpMethod = HttpPost, body = body)
+    let response = await client.request(baseUrl & "write?db=" & databaseName, httpMethod = HttpPost, body = body)
 
-  client.close()
+    client.close()
 
-  if response.code != Http204:
-    return false
+    if response.code != Http204:
+      return false
 
-  return true
+    return true
+  except:
+    echo "Error[insertDatabase]:\n", getCurrentExceptionMsg()
 
 proc initInflux* () =
   let config = server.config.serverConfig.influx.get

@@ -61,5 +67,5 @@ proc initInflux* () =
         quit()
 
   except:
-    let e = getCurrentException()
-    echo e.msg
+    echo "Error[initInflux]:\n", getCurrentExceptionMsg()
+    quit()
diff --git a/src/mqtt.nim b/src/mqtt.nim
@@ -4,9 +4,14 @@ import nmqtt
 
 
 proc initMqtt* () {.async.} =
-  var config = server.config.serverConfig.mqtt.get
+  try:
+    var config = server.config.serverConfig.mqtt.get
 
-  mqttContext = newMqttCtx("smartied")
-  mqttContext.set_host(config.host, config.port)
+    mqttContext = newMqttCtx("smartied")
+    mqttContext.set_host(config.host, config.port)
   
-  await mqttContext.start()-
\ No newline at end of file
+    await mqttContext.start()
+
+  except:
+    echo "Error[initMqtt]:\n", getCurrentExceptionMsg()
+    quit()+
\ No newline at end of file
diff --git a/src/utils.nim b/src/utils.nim
@@ -2,10 +2,6 @@ import asyncdispatch, asyncnet, sequtils, json, tables, math, options
 import ws, nmqtt
 import types, vars, modbus
 
-proc initUtil*() =
-  lastClientId = 1
-  subscribedConnections = @[]
-
 proc addVal* (resp: var string, name: string, key: string, val: string, lastUpdated: int64) =
   resp = resp & name & "{"
   resp = resp & "device=\"" & key & "\""

@@ -42,6 +38,15 @@ proc broadcast* (msg: string) =
 proc broadcastServerState* () =
   broadcast($(%*server.state))
 
+proc sendPing* () =
+  try:
+    for socket in wsConnections:
+      if socket.readyState == Open:
+        asyncCheck socket.send("")
+
+  except WebSocketError:
+    echo "socket closed:", getCurrentExceptionMsg()
+
 proc isaRound* [T: float32|float64](value: T, places: int = 0): float = 
   if places == 0:
     result = round(value)

@@ -72,4 +77,18 @@ proc closeOpenConnections* () =
 
   #close all websocket connections 
   for wsConnection in wsConnections:
-    wsConnection.close()-
\ No newline at end of file
+    wsConnection.close()
+
+proc initUtil*() =
+  lastClientId = 1
+  subscribedConnections = @[]
+
+  #send empty paket every 2 seconds to every subscribed client
+  addTimer(2000, false, proc (fd: AsyncFD): bool {.gcsafe.} =
+      sendPing()
+    )
+
+  #clean up broken websocket connections every 60 seconds 
+  addTimer(60000, false, proc (fd: AsyncFD): bool {.gcsafe.} =
+      cleanupConnections()
+    )