ctucx.git: smartied

[nimlang] smarthome server

commit bcd32ef15c1a9820ab3ca150843d2e8a19eef410
Author: Milan Pässler <me@pbb.lc>
Date: Sat, 13 Jul 2019 10:31:04 +0200

initial commit
12 files changed, 438 insertions(+), 0 deletions(-)
A
.gitignore
|
1
+
A
config.json
|
92
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
smartied.nimble
|
15
+++++++++++++++
A
src/frontend_tcp.nim
|
47
+++++++++++++++++++++++++++++++++++++++++++++++
A
src/frontend_ws.nim
|
42
++++++++++++++++++++++++++++++++++++++++++
A
src/modbus.nim
|
52
++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/powermeter.nim
|
28
++++++++++++++++++++++++++++
A
src/serial.nim
|
22
++++++++++++++++++++++
A
src/serial.nim.cfg
|
2
++
A
src/smartied.nim
|
16
++++++++++++++++
A
src/types.nim
|
73
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/util.nim
|
48
++++++++++++++++++++++++++++++++++++++++++++++++
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1 @@
+smartied
diff --git a/config.json b/config.json
@@ -0,0 +1,92 @@
+{
+ 	"devices": {
+		"modbus-10": {
+			"type": "RelayBoard",
+			"firstRegister": 101,
+			"count": 8,
+			"address": 10
+		},
+		"modbus-50": {
+			"type": "PowerMeter",
+			"model": "SDM120",
+			"address": 50
+		},
+		"modbus-60": {
+			"type": "PowerMeter",
+			"model": "SDM120",
+			"address": 60
+		}
+	},
+	"clientConfigs": {
+		"fernbedienung": {
+			"views": [
+				{
+					"name": "Lights",
+					"type": "switches",
+					"switches": [
+						{ "name": "Decke", "device": "modbus-10", "relay": 0 },
+						{ "name": "Bett", "device": "modbus-10", "relay": 1 },
+						{ "name": "Küche", "device": "modbus-10", "relay": 2 },
+						{ "name": "Bad", "device": "modbus-10", "relay": 3 }
+					]
+				}
+			]
+		},
+		"smarthome-pwa": {
+			"source": "https://git.pbb.lc/petabyteboy/smarthome-pwa",
+			"views": [
+				{
+					"url": "lights",
+					"name": "Lights",
+					"icon": "lightbulb",
+					"type": "switches",
+					"switches": [
+						{ "name": "Decke", "device": "modbus-10", "relay": 0 },
+						{ "name": "Bett", "device": "modbus-10", "relay": 1 },
+						{ "name": "Küche", "device": "modbus-10", "relay": 2 },
+						{ "name": "Bad", "device": "modbus-10", "relay": 3 }
+					]
+				},
+				{
+					"url": "switches",
+					"name": "Switches",
+					"icon": "switch",
+					"type": "switches",
+					"switches": [
+						{ "name": "Verstärker", "device": "modbus-10", "relay": 7 }
+					]
+				},
+				{
+					"url": "powermeter",
+					"name": "Power Meter",
+					"icon": "power",
+					"type": "powermeter",
+					"meters": [
+						{ "name": "Sonstiges", "device": "modbus-50" },
+						{ "name": "Küche", "device": "modbus-60" }
+					]
+				},
+				{
+					"url": "departures",
+					"name": "Departures",
+					"icon": "departure_board",
+					"type": "departures",
+					"source": "https://utils.ctu.cx/departures.php"
+				},
+				{
+					"url": "netdata",
+					"name": "netdata",
+					"icon": "show_chart",
+					"type": "redirect",
+					"destination": "/netdata/"
+				}
+			]
+		}
+	},
+	"httpPort": 5000,
+	"tcpPort": 5001,
+	"wsPort": 5002,
+	"modbusAddr": "192.168.1.1",
+	"modbusPort": 502,
+	"powermeterUpdateIntervalSec": 20
+}
diff --git a/smartied.nimble b/smartied.nimble
@@ -0,0 +1,15 @@
+# Package
+
+version       = "0.1.0"
+author        = "Milan P\xC3\xA4ssler"
+description   = "A new awesome nimble package"
+license       = "AGPL-3.0"
+srcDir        = "src"
+bin           = @["smartied"]
+
+
+
+# Dependencies
+
+requires "nim >= 0.20.0"
+requires "ws"
diff --git a/src/frontend_tcp.nim b/src/frontend_tcp.nim
@@ -0,0 +1,46 @@
+import asyncnet
+import asyncdispatch
+import util
+import types
+import sequtils
+import json
+
+var clients: seq[AsyncSocket] = @[]
+
+proc broadcast(msg: string) {.locks: true.}=
+  for client in clients:
+    try:
+      asyncCheck client.send(msg)
+    except:
+      client.close()
+      clients.keepIf(proc(x: AsyncSocket): bool = x != client)
+
+registerBroadcastHandler(broadcast)
+
+proc processTcpClient(client: AsyncSocket) {.async.} =
+  try:
+    await client.send($(%*server.state))
+    while true:
+      let req = await client.recvLine()
+      if req == "":
+        client.close()
+        break
+
+      let resp = await tryHandleRequest(req)
+      await client.send(resp & '\n')
+  except:
+    client.close()
+    clients.keepIf(proc(x: AsyncSocket): bool = x != client)
+
+proc serveTcp*() {.async.} =
+  var socket = newAsyncSocket()
+  socket.setSockOpt(OptReuseAddr, true)
+  socket.bindAddr(Port(server.config.tcpPort))
+  socket.listen()
+
+  echo("listening on port ", server.config.tcpPort)
+
+  while true:
+    let client = await socket.accept()
+    clients.add(client)
+    asyncCheck processTcpClient(client)+
\ No newline at end of file
diff --git a/src/frontend_ws.nim b/src/frontend_ws.nim
@@ -0,0 +1,42 @@
+import asynchttpserver
+import asyncdispatch
+import util
+import types
+import ws
+import sequtils
+import json
+
+var clients: seq[WebSocket] = @[]
+
+proc broadcast(msg: string) {.locks: true.} =
+  for client in clients:
+    try:
+      asyncCheck client.send(msg)
+    except:
+      client.close()
+      clients.keepIf(proc(x: WebSocket): bool = x != client)
+
+registerBroadcastHandler(broadcast)
+
+proc processWsClient(req: Request) {.gcsafe, async.} =
+  var ws: WebSocket
+  try:
+    ws = await newWebsocket(req)
+    clients.add(ws)
+    await ws.send($(%*server.state))
+  except:
+    asyncCheck req.respond(Http404, "404")
+    return
+
+  try:
+    while true:
+      let req = await ws.receiveStrPacket()
+      let resp = await tryHandleRequest(req)
+      await ws.send(resp)
+  except:
+    ws.close()
+    clients.keepIf(proc(x: WebSocket): bool = x != ws)
+
+proc serveWs*() {.async.} =
+  var httpServer = newAsyncHttpServer()
+  asyncCheck httpServer.serve(Port(server.config.wsPort), processWsClient)
diff --git a/src/modbus.nim b/src/modbus.nim
@@ -0,0 +1,51 @@
+import types
+import tables
+import sequtils
+
+{.passL: "-lmodbus".}
+type modbus = ref object
+proc modbus_new_tcp*(ad: cstring, port: cint): modbus {.importc, dynlib: "libmodbus.so.5"}
+proc modbus_connect*(mb: modbus): cint {.importc.}
+proc modbus_close*(mb: modbus): void {.importc.}
+proc modbus_free*(mb: modbus): void {.importc.}
+proc modbus_set_slave*(mb: modbus, ad: cint): void {.importc.}
+proc modbus_read_bits*(mb: modbus, ad: cint, nb: cint, dest: pointer): cint {.importc.}
+proc modbus_write_bit*(mb: modbus, ad: cint, status: cint): cint {.importc.}
+proc modbus_read_registers*(mb: modbus, ad: cint, nb: cint, dest: pointer): cint {.importc.}
+proc modbus_get_float_abcd*(mb: modbus, src: pointer): cfloat {.importc.}
+
+var mb*: modbus
+
+proc modbus_read_float*(mb: modbus, ad: cint): cfloat =
+  var first = 0u16
+  var second = 0u16
+  echo mb.modbus_read_registers(cint(ad), 1, first.addr)
+  echo mb.modbus_read_registers(cint(ad+1), 1, second.addr)
+  let res = float32(uint32(second) + (uint32(first) * 65536))
+  echo(ad, ' ', first, ' ', second, ' ', res)
+  return res
+
+proc initModbus*() =
+  let port: cint = int32(server.config.modbusPort)
+  mb = modbus_new_tcp(server.config.modbusAddr, port)
+  discard mb.modbus_connect()
+
+  for key, device in server.config.devices.pairs():
+    if device.type == PowerMeter:
+      server.state[key] = DeviceState(
+        type: PowerMeter,
+        power: 0f,
+        cosphi: 0f,
+        voltage: 0f,
+        `import`: 0f,
+        frequency: 0f
+          )
+    elif device.type == RelayBoard:
+      var data: array[255, bool]
+      mb.modbus_set_slave(cint(device.address))
+      discard mb.modbus_read_bits(cint(device.firstRegister), cint(device.count), data.addr)
+      server.state[key] = DeviceState(type: RelayBoard, relays: @data[0..device.count-1])
+
+proc deinitModbus*() =
+  mb.modbus_close()
+  mb.modbus_free()+
\ No newline at end of file
diff --git a/src/powermeter.nim b/src/powermeter.nim
@@ -0,0 +1,27 @@
+import asyncdispatch
+import modbus
+import types
+import tables
+import util
+import json
+
+proc updatePowermeters*(fd: AsyncFD): bool {.gcsafe.} =
+  echo "updating powermeters"
+  for key, device in server.config.devices.pairs():
+    if device.type != PowerMeter:
+      continue
+
+    echo device.address
+    mb.modbus_set_slave(cint(device.address))
+    server.state[key].voltage = mb.modbus_read_float(0)
+    server.state[key].frequency = mb.modbus_read_float(70)
+    server.state[key].`import` = mb.modbus_read_float(72)
+    server.state[key].cosphi = mb.modbus_read_float(30)
+    server.state[key].power = mb.modbus_read_float(12)
+
+    broadcast($(%*server.state))
+
+  addTimer(int(server.config.powermeterUpdateIntervalSec * 1000), true, updatePowermeters)
+  return false
+
+addTimer(1000, true, updatePowermeters)+
\ No newline at end of file
diff --git a/src/serial.nim b/src/serial.nim
@@ -0,0 +1,21 @@
+import asyncdispatch, asyncnet, os, threadpool
+
+proc asyncReadLine*(file: File): Future[string] =
+  var fut = newFuture[string]()
+  var flowVar = spawn file.readLine()
+
+  addTimer(50, false, proc(fd: AsyncFD): bool =
+      if flowVar.isReady():
+        fut.complete(^flowVar)
+        return true
+    )
+
+  return fut
+
+proc main() {.async.} =
+  let file = open("/dev/ttyS2", fmReadWrite)
+  while true:
+    let line = await file.asyncReadLine()
+    echo line
+
+waitFor main()+
\ No newline at end of file
diff --git a/src/serial.nim.cfg b/src/serial.nim.cfg
@@ -0,0 +1 @@
+--threads:on+
\ No newline at end of file
diff --git a/src/smartied.nim b/src/smartied.nim
@@ -0,0 +1,15 @@
+import asyncdispatch
+import frontend_tcp
+import frontend_ws
+import powermeter
+import types
+import modbus
+import json
+
+initModbus()
+
+asyncCheck serveTcp()
+asyncCheck serveWs()
+runForever()
+
+deinitModbus()+
\ No newline at end of file
diff --git a/src/types.nim b/src/types.nim
@@ -0,0 +1,72 @@
+import asyncnet
+import json
+import tables
+import sequtils
+
+type DeviceType* = enum
+  PowerMeter,
+  RelayBoard
+
+type DeviceConfig* = object
+  address*: uint8
+  case type*: DeviceType
+  of PowerMeter:
+    model*: string
+  of RelayBoard:
+    firstRegister*: uint8
+    count*: uint8
+
+type Config* = object
+  tcpPort*: uint16
+  wsPort*: uint16
+  httpPort*: uint16
+  modbusAddr*: string
+  modbusPort*: uint16
+  powermeterUpdateIntervalSec*: uint
+  devices*: Table[string, DeviceConfig]
+  clientConfigs*: Table[string, JsonNode]
+
+type DeviceState* = object
+  case type*: DeviceType
+  of PowerMeter:
+    power*: float32
+    cosphi*: float32
+    voltage*: float32
+    `import`*: float32
+    frequency*: float32
+  of RelayBoard:
+    relays*: seq[bool]
+
+type Server* = object
+  state*: Table[string, DeviceState]
+  config*: Config
+  clients*: seq[AsyncSocket]
+
+type ActionType* = enum
+  SetRelayAction,
+  ToggleRelayAction,
+  GetClientConfigAction
+
+type Action* = object
+  case type*: ActionType
+  of SetRelayAction:
+    setRelayBoard*: string
+    setRelay*: uint8
+    setValue*: bool
+  of ToggleRelayAction:
+    toggleRelayBoard*: string
+    toggleRelay*: uint8
+  of GetClientConfigAction:
+    configName*: string
+
+type ResponseStatus* = enum
+  Err,
+  Ok
+
+type Response* = object
+  status*: ResponseStatus
+  data*: JsonNode
+
+var server* = Server(config: parseJson(readFile("../config.json")).to(Config))
+
+echo server.config+
\ No newline at end of file
diff --git a/src/util.nim b/src/util.nim
@@ -0,0 +1,47 @@
+import json
+import asyncdispatch
+import modbus
+import types
+import tables
+
+var broadcastHandlers: seq[proc (msg: string)] = @[]
+
+proc registerBroadcastHandler*(handler: proc (msg: string) {.locks: true.}) =
+  broadcastHandlers.add(handler)
+
+proc broadcast*(msg: string) =
+  for broadcastHandler in broadcastHandlers:
+    broadcastHandler(msg)
+
+proc handleRequest*(req: string): Future[JsonNode] {.async.} =
+  let action = parseJson(req).to(Action)
+
+  if action.type == SetRelayAction:
+    let config = server.config.devices[action.setRelayBoard]
+
+    server.state[action.setRelayBoard].relays[action.setRelay] = action.setValue
+    mb.modbus_set_slave(cint(config.address))
+    discard mb.modbus_write_bit(cint(config.firstRegister + action.setRelay), cint(server.state[action.setRelayBoard].relays[action.setRelay]))
+
+    broadcast($(%*server.state))
+    return JsonNode()
+  elif action.type == ToggleRelayAction:
+    let config = server.config.devices[action.toggleRelayBoard]
+
+    server.state[action.toggleRelayBoard].relays[action.toggleRelay] = not server.state[action.toggleRelayBoard].relays[action.toggleRelay]
+    mb.modbus_set_slave(cint(config.address))
+    discard mb.modbus_write_bit(cint(config.firstRegister + action.toggleRelay), cint(server.state[action.toggleRelayBoard].relays[action.toggleRelay]))
+
+    broadcast($(%*server.state))
+    return JsonNode()
+  elif action.type == GetClientConfigAction:
+    let clientConfig: JsonNode = server.config.clientConfigs[action.configName]
+    return clientConfig
+
+proc tryHandleRequest*(req: string): Future[string] {.async.} =
+  var resp: Response
+  try:
+    resp = Response(status: Ok, data: await handleRequest(req))
+  except:
+    resp = Response(status: Err, data: JsonNode())
+  return $(%*resp)+
\ No newline at end of file