ctucx.git: smartied

[nimlang] smarthome server

1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 
62 
63 
64 
65 
66 
67 
68 
69 
70 
71 
72 
73 
74 
75 
76 
77 
78 
79 
80 
81 
82 
83 
84 
85 
86 
87 
88 
89 
90 
91 
92 
93 
94 
95 
96 
97 
98 
99 
100 
101 
102 
103 
104 
105 
106 
107 
108 
109 
110 
111 
112 
113 
114 
115 
116 
117 
118 
119 
120 
121 
122 
123 
124 
125 
126 import asyncdispatch, asynchttpserver, json, tables, options, sequtils
import ws
import types, vars, utils, deviceActionHandler

proc processWsClient(req: Request) {.async,gcsafe.} =
  var ws: WebSocket
  cleanupConnections()

  try:
    ws = await newWebsocket(req)
    wsConnections.add(ws)
#    setupPings(ws, 2)
    echo "new ws connection: " & ws.key

  except:
    asyncCheck req.respond(Http404, "404")
    return

  try:
    while ws.readyState == Open:
      let req = await ws.receiveStrPacket()

      if req != "":
        try:
          let action   = parseJson(req).to(Action)
          var response = JsonNode()

          if not checkAccessToken(action.accessToken):
            raise newException(OsError, "invalid accessToken")

          if action.type == SetSubscriptionStateAction:
            if action.subscribed:
              echo "adding client: " & ws.key
              subscribedConnections.add(ws)
              await ws.send($(%*server.state))
            else:
              echo "removing client: " & ws.key
              subscribedConnections.keepIf(proc(x: Websocket): bool = x.key != ws.key)
          else:
            response = await handleDeviceAction(action)

          await ws.send($(%*Response(status: Ok, data: response)))

        except:
          let e = getCurrentException()
          await ws.send($(%*Response(status: Err, data: newJString(e.msg))))

  except WebSocketError:
    echo "Error[processWsClient]:\n", getCurrentExceptionMsg()

proc processHttpClient(req: Request) {.async,gcsafe.} =
  if req.reqMethod == HttpGet:
    if req.headers.hasKey("Authorization") and req.headers["Authorization"] == "Bearer " & server.config.serverConfig.accessToken:
      await req.respond(Http200, $(%* server.state))
    else:
      await req.respond(Http401, "401 Unauthorized")

  elif req.reqMethod == HttpPost:
    try:
      let action  = parseJson(req.body).to(Action)
      
      if not checkAccessToken(action.accessToken):
        raise newException(OsError, "invalid accessToken")

      await req.respond(Http200, $(%*Response(status: Ok, data: await handleDeviceAction(action))))

    except:
      let e = getCurrentException()
      await req.respond(Http500, $(%*Response(status: Err, data: newJString(e.msg))))
  else:
    await req.respond(Http405, "405 Method Not Allowed")

proc processPrometheusClient(req: Request) {.async,gcsafe.} =
  if req.reqMethod == HttpGet:
    if req.headers.hasKey("Authorization") and req.headers["Authorization"] == "Bearer " & server.config.serverConfig.accessToken:
      var resp = ""
      for key, device in server.config.devices.pairs():
        if device.type == PowerMeter:
          if server.state[key].frequency == 0:
            continue
          let lastUpdated = server.state[key].lastUpdated.get
          resp.addVal("powermeter_import", key, $(server.state[key].import), lastUpdated)
          resp.addVal("powermeter_cosphi", key, $(server.state[key].cosphi), lastUpdated)
          resp.addVal("powermeter_power", key, $(server.state[key].power), lastUpdated)
          resp.addVal("powermeter_frequency", key, $(server.state[key].frequency), lastUpdated)
          resp.addVal("powermeter_voltage", key, $(server.state[key].voltage), lastUpdated)

        if device.type == RelayBoard:
          for i, val in server.state[key].relays:
            resp.addVal("relayboard_relay", key & "\",relay=\"" & $(i) & "\",name=\"" & $(i), fmtBool(val), 0)

        if device.type == LacrosseTempSensor:
          let lastUpdated = server.state[key].lastUpdated.get
          resp.addVal("lacrossetempsensor_temperature", key, $(server.state[key].temperature), lastUpdated)
          resp.addVal("lacrossetempsensor_weakbattery", key, fmtBool(server.state[key].weakBattery), lastUpdated)
          if server.state[key].humidity < 0:
            continue
          resp.addVal("lacrossetempsensor_humidity", key, $(server.state[key].humidity), lastUpdated)

      await req.respond(Http200, resp)

    else:
      await req.respond(Http401, "401 Unauthorized")

  else:
    await req.respond(Http405, "405 Method Not Allowed")

proc processRequest(req: Request) {.async,gcsafe.} =
  case req.url.path
  of "/":
    await processHttpClient(req)
  of "/ws":
    await processWsClient(req)
  of "/metrics":
    await processPrometheusClient(req)
  else:
    asyncCheck req.respond(Http404, "404")

proc serveFrontend*() {.async.} =
  try:
    var httpServer = newAsyncHttpServer()
    await httpServer.serve(Port(server.config.serverConfig.frontendPort), processRequest)

  except:
    echo "Error[serveFrontend]:\n", getCurrentExceptionMsg()
    quit()