commit bcd32ef15c1a9820ab3ca150843d2e8a19eef410
Author: Milan Pässler <me@pbb.lc>
Date: Sat, 13 Jul 2019 10:31:04 +0200
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(-)
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