commit 439b9ee1933af48fd2f8283676c1d7bab83549b6
parent 583f9b189f449a53d74c586c0737eb96b8dbd7d5
Author: Leah (ctucx) <leah@ctu.cx>
Date: Thu, 18 Feb 2021 10:07:08 +0100
parent 583f9b189f449a53d74c586c0737eb96b8dbd7d5
Author: Leah (ctucx) <leah@ctu.cx>
Date: Thu, 18 Feb 2021 10:07:08 +0100
removed tcp backend, refactor code, implement influx export, implement mqtt support, implement device: zigbee2mqttLamp
23 files changed, 764 insertions(+), 474 deletions(-)
A
|
77
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
|
67
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
|
86
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
|
119
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
diff --git a/config.json b/config.json @@ -22,42 +22,39 @@ "model": "SDM120", "address": 60 }, + "tradfri-lamp1": { + "type": "Zigbee2MqttLamp", + "lampType": "RGB", + "friendlyName": "ikea_lamp_rgb" + }, + "tradfri-lamp2": { + "type": "Zigbee2MqttLamp", + "lampType": "WhiteSpectrum", + "friendlyName": "ikea_lamp_whitespectrum" + }, + "tradfri-lamp3": { + "type": "Zigbee2MqttLamp", + "lampType": "Switchable", + "friendlyName": "ikea_lamp_switchable" + }, "lacrosse-raum": { "type": "LacrosseTempSensor", - "id": "21", - "address": 21 + "id": "21" }, "lacrosse-kuehlschrank": { "type": "LacrosseTempSensor", - "id": "3a", - "address": 31 + "id": "3a" }, "lacrosse-draussen": { "type": "LacrosseTempSensor", - "id": "26", - "address": 26 + "id": "26" }, "lacrosse-bad": { "type": "LacrosseTempSensor", - "id": "3f", - "address": 3 + "id": "3f" } }, "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": { "views": [ { @@ -70,7 +67,10 @@ { "name": "Decke", "device": "modbus-10", "relay": 2 }, { "name": "Küche", "device": "modbus-10", "relay": 1 }, { "name": "Bett", "device": "modbus-10", "relay": 3 }, - { "name": "Bad", "device": "modbus-20", "relay": 0 } + { "name": "Bad", "device": "modbus-20", "relay": 0 }, + { "name": "Decke: RGB", "device": "tradfri-lamp1", "relay": 0}, + { "name": "Decke: Weiß-Spektrum", "device": "tradfri-lamp2", "relay": 0}, + { "name": "Decke: Schaltbar", "device": "tradfri-lamp3", "relay": 0} ] }, { @@ -129,14 +129,27 @@ ] } }, - "httpPort": 5000, - "tcpPort": 5001, - "wsPort": 5002, - "prometheusPort": 5003, - "modbusAddr": "10.0.0.1", - "modbusPort": 502, - "lacrosseAddr": "10.0.0.1", - "lacrossePort": 2342, - "powermeterUpdateIntervalSec": 20, - "accessToken": "penis123" + "serverConfig": { + "frontendPort": 5000, + "modbus": { + "host": "10.0.0.1", + "port": 502 + }, + "mqtt": { + "host": "10.0.0.1", + "port": 1883 + }, + "lacrosse": { + "host": "10.0.0.1", + "port": 2342 + }, + "influx": { + "host": "10.0.0.1", + "port": 8086, + "powermeterDatabase": "powermeter", + "lacrosseDatabase": "temperatureSensors" + }, + "powermeterUpdateIntervalSec": 20, + "accessToken": "penis123" + } }
diff --git a/smartied.nimble b/smartied.nimble @@ -1,7 +1,7 @@ # Package -version = "0.1.0" -author = "Milan P\xC3\xA4ssler" +version = "0.2.0" +author = "Leah(ctucx), Milan P\xC3\xA4ssler" description = "A new awesome nimble package" license = "AGPL-3.0" srcDir = "src" @@ -12,4 +12,5 @@ bin = @["smartied"] # Dependencies requires "nim >= 0.20.0" -requires "ws" +requires "ws == 0.4.3" +requires "nmqtt == 1.0.4"
diff --git a/src/actionHandler.nim b/src/actionHandler.nim @@ -0,0 +1,77 @@ +import asyncdispatch, json, tables, options +import types, vars, utils +import devices/[modbusRelayboard, zigbee2mqttLamp] + +proc handleAction*(action: Action): Future[JsonNode] {.async.} = + case action.type + of SwitchStateAction: + let config = server.config.devices[action.deviceName] + + if action.state.isNone and action.toggle.isNone: + return JsonNode() + + if action.state.isSome and action.toggle.isSome: + return JsonNode() + + if action.state.isSome: + let state = action.state.get + + if config.type == RelayBoard: + if action.relay.isNone: + return JsonNode() + else: + await setRelay(action.deviceName, action.relay.get, state) + broadcastServerState() + return JsonNode() + + if config.type == Zigbee2MqttLamp: + await setLampState(action.deviceName, state) + return JsonNode() + + if action.toggle.isSome: + if config.type == RelayBoard: + if action.relay.isNone: + return JsonNode() + else: + await toggleRelay(action.deviceName, action.relay.get) + broadcastServerState() + return JsonNode() + + if config.type == Zigbee2MqttLamp: + await toggleLamp(action.deviceName) + return JsonNode() + + of SetBrightnessAction: + let config = server.config.devices[action.deviceName] + + if config.type != Zigbee2MqttLamp: + return JsonNode() + + await setLampBrightness(action.deviceName, action.brightness) + return JsonNode() + + of SetColorXYAction: + let config = server.config.devices[action.deviceName] + + if config.type != Zigbee2MqttLamp: + return JsonNode() + + await setLampColor(action.deviceName, action.colorX, action.colorY) + return JsonNode() + + of SetColorTemperatureAction: + let config = server.config.devices[action.deviceName] + + if config.type != Zigbee2MqttLamp: + return JsonNode() + + await setLampColorTemperature(action.deviceName, action.colorTemperature) + return JsonNode() + + of GetClientConfigAction: + let clientConfig: JsonNode = server.config.clientConfigs[action.configName] + return clientConfig + + else: + return JsonNode() +
diff --git a/src/backend_lacrosse.nim b/src/backend_lacrosse.nim @@ -1,54 +0,0 @@ -import asyncdispatch -import asyncnet -import types -import tables -import util -import json -import vars -import times - -proc lacrosseHandleLoop(sock: AsyncSocket) {.async.} = - while true: - try: - let line = await sock.recvLine() - if line == "": - break - - let msg = parseJson(line).to(LacrosseMessage) - - for key, device in server.config.devices.pairs(): - if device.type != LacrosseTempSensor: - continue - if device.id != msg.id: - continue - - if msg.hum == 106: - server.state[key].humidity = -1 - else: - server.state[key].humidity = msg.hum - server.state[key].temperature = msg.temp - server.state[key].weakBattery = bool(msg.weakBatt) - server.state[key].lastUpdated2 = toUnix(getTime()) - - broadcast($(%*server.state)) - except: - let e = getCurrentException() - #echo("error while updating lacrosse: ", e.msg) - -proc lacrosseConnectLoop() {.async.} = - while true: - try: - let sock = await asyncnet.dial(server.config.lacrosseAddr, Port(server.config.lacrossePort)) - await lacrosseHandleLoop(sock) - except: - let e = getCurrentException() - echo("error while connectiong to lacrosse relay: ", e.msg) - await sleepAsync(1000) - -proc initBackendLacrosse*() = - for key, device in server.config.devices.pairs(): - if device.type != LacrosseTempSensor: - continue - server.state[key] = DeviceState(type: LacrosseTempSensor) - - asyncCheck lacrosseConnectLoop()
diff --git a/src/backend_powermeter.nim b/src/backend_powermeter.nim @@ -1,51 +0,0 @@ -import asyncdispatch -import modbus -import types -import tables -import util -import json -import vars -import times - -proc updatePowermeter(key: string, device: DeviceConfig) {.async.} = - let voltage = await mb.asyncReadFloat(device.address, 0) - let frequency = await mb.asyncReadFloat(device.address, 70) - let `import` = await mb.asyncReadFloat(device.address, 72) - let cosphi = await mb.asyncReadFloat(device.address, 30) - let power = await mb.asyncReadFloat(device.address, 12) - - server.state[key].voltage = voltage - server.state[key].frequency = frequency - server.state[key].`import` = `import` - server.state[key].cosphi = cosphi - server.state[key].power = power - - server.state[key].lastUpdated = toUnix(getTime()) - - broadcast($(%*server.state)) - -proc updatePowermeters() {.async.} = - for key, device in server.config.devices.pairs(): - if device.type != PowerMeter: - continue - - try: - await updatePowermeter(key, device) - except: - let e = getCurrentException() - echo("error while updating powermeter ", key, ": ", e.msg) - -proc powermetersLoop() {.async.} = - await sleepAsync(500) - while true: - await updatePowermeters() - await sleepAsync(int(server.config.powermeterUpdateIntervalSec * 1000)) - -proc initBackendPowermeter*() = - for key, device in server.config.devices.pairs(): - if device.type != PowerMeter: - continue - - server.state[key] = DeviceState(type: PowerMeter) - - asyncCheck powermetersLoop()
diff --git a/src/backend_relayboard.nim b/src/backend_relayboard.nim @@ -1,25 +0,0 @@ -import asyncdispatch -import types -import modbus -import tables -import vars - -proc updateRelayboards() {.async.} = - echo "updating relayboards" - for key, device in server.config.devices.pairs(): - if device.type != RelayBoard: - continue - - try: - let data = await mb.asyncReadBits(device.address, device.firstRegister, device.count) - server.state[key] = DeviceState(type: RelayBoard, relays: data) - except: - let e = getCurrentException() - echo("error while updating relayboard ", key, ": ", e.msg) - -proc initBackendRelayboard*() = - for key, device in server.config.devices.pairs(): - if device.type != RelayBoard: - continue - server.state[key] = DeviceState(type: RelayBoard) - asyncCheck updateRelayboards()
diff --git a/src/devices/lacrosseSensors.nim b/src/devices/lacrosseSensors.nim @@ -0,0 +1,67 @@ +import asyncdispatch, asyncnet, tables, json, times, options +import ../types, ../vars, ../utils, ../influx + +proc lacrosseHandleLoop (sock: AsyncSocket) {.async.} = + while true: + try: + let line = await sock.recvLine() + if line == "": + break + + let msg = parseJson(line).to(LacrosseMessage) + + for key, device in server.config.devices.pairs(): + if device.type != LacrosseTempSensor: + continue + if device.id != msg.id: + continue + + if msg.hum == 106: + server.state[key].humidity = -1 + else: + server.state[key].humidity = msg.hum + server.state[key].temperature = msg.temp.isaRound(2) + server.state[key].weakBattery = bool(msg.weakBatt) + server.state[key].lastUpdated = some(toUnix(getTime())) + + + if server.config.serverConfig.influx.isSome: + let config = server.config.serverConfig.influx.get + + if config.lacrosseDatabase.isSome: + var tags, fields = initTable[string, string]() + + tags["device"] = key + + if msg.hum != 106: + fields["humidity"] = $msg.hum + + fields["temperature"] = $msg.temp.isaRound(2) + + discard await config.insertDatabase(config.lacrosseDatabase.get, key, tags, fields, server.state[key].lastUpdated.get) + + + broadcastServerState() + + except: + let e = getCurrentException() + echo("error while updating lacrosse: ", e.msg) + +proc lacrosseConnectLoop () {.async.} = + while true: + try: + let config = server.config.serverConfig.lacrosse.get + let sock = await asyncnet.dial(config.host, Port(config.port)) + await lacrosseHandleLoop(sock) + except: + let e = getCurrentException() + echo("error while connectiong to lacrosse relay: ", e.msg) + await sleepAsync(1000) + +proc initLacrosseSensors* () = + for key, device in server.config.devices.pairs(): + if device.type != LacrosseTempSensor: + continue + server.state[key] = DeviceState(type: LacrosseTempSensor) + + asyncCheck lacrosseConnectLoop()
diff --git a/src/devices/modbusPowermeter.nim b/src/devices/modbusPowermeter.nim @@ -0,0 +1,63 @@ +import asyncdispatch, tables, times, options, tables +import ../types, ../vars, ../modbus, ../influx, ../utils + +proc updatePowermeter (key: string, device: DeviceConfig) {.async.} = + let deviceAddress = device.address.get + + let voltage = await mb.asyncReadFloat(deviceAddress, 0) + let frequency = await mb.asyncReadFloat(deviceAddress, 70) + let `import` = await mb.asyncReadFloat(deviceAddress, 72) + let cosphi = await mb.asyncReadFloat(deviceAddress, 30) + let power = await mb.asyncReadFloat(deviceAddress, 12) + + server.state[key].voltage = voltage + server.state[key].frequency = frequency + server.state[key].`import` = `import` + server.state[key].cosphi = cosphi + server.state[key].power = power + + server.state[key].lastUpdated = some(toUnix(getTime())) + + if server.config.serverConfig.influx.isSome: + let config = server.config.serverConfig.influx.get + + if config.powermeterDatabase.isSome: + var tags, fields = initTable[string, string]() + + tags["device"] = key + + fields["voltage"] = $voltage.isaRound(2) + fields["frequency"] = $frequency.isaRound(2) + fields["import"] = $`import`.isaRound(3) + fields["cosphi"] = $cosphi.isaRound(3) + fields["power"] = $power.isaRound(2) + + discard await config.insertDatabase(config.powermeterDatabase.get, key, tags, fields, server.state[key].lastUpdated.get) + + broadcastServerState() + +proc updatePowermeters () {.async.} = + for key, device in server.config.devices.pairs(): + if device.type != PowerMeter: + continue + + try: + await updatePowermeter(key, device) + except: + let e = getCurrentException() + echo("error while updating powermeter ", key, ": ", e.msg) + +proc powermetersLoop () {.async.} = + await sleepAsync(500) + while true: + await updatePowermeters() + await sleepAsync(int(server.config.serverConfig.powermeterUpdateIntervalSec * 1000)) + +proc initModbusPowermeters* () = + for key, device in server.config.devices.pairs(): + if device.type != PowerMeter: + continue + + server.state[key] = DeviceState(type: PowerMeter) + + asyncCheck powermetersLoop()
diff --git a/src/devices/modbusRelayboard.nim b/src/devices/modbusRelayboard.nim @@ -0,0 +1,38 @@ +import asyncdispatch, tables, options +import ../types, ../vars, ../utils, ../modbus + +proc setRelay*(relayboard: string, relay: uint8, value: bool) {.async.} = + let config = server.config.devices[relayboard] + + server.state[relayboard].relays[relay] = value + await mb.asyncWriteBit(config.address.get, config.firstRegister + relay, server.state[relayboard].relays[relay]) + +proc toggleRelay*(relayboard: string, relay: uint8) {.async.} = + let config = server.config.devices[relayboard] + + server.state[relayboard].relays[relay] = not server.state[relayboard].relays[relay] + await mb.asyncWriteBit(config.address.get, config.firstRegister + relay, server.state[relayboard].relays[relay]) + +proc updateRelayboards() {.async.} = + echo "Get relayboard state" + for key, device in server.config.devices.pairs(): + if device.type != RelayBoard: + continue + + try: + 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) + + broadcastServerState() + +proc initModbusRelayboards*() = + for key, device in server.config.devices.pairs(): + if device.type != RelayBoard: + continue + + server.state[key] = DeviceState(type: RelayBoard) + + asyncCheck updateRelayboards()
diff --git a/src/devices/zigbee2mqttLamp.nim b/src/devices/zigbee2mqttLamp.nim @@ -0,0 +1,85 @@ +import asyncdispatch, strutils, json, tables, options, times +import ../types, ../vars, ../utils +import nmqtt + +proc setLampState* (deviceName: string, value: bool) {.async.} = + let config = server.config.devices[deviceName] + var sendState = "OFF" + + if config.type != Zigbee2MqttLamp: + return + + if value == true: + sendState = "ON" + + await mqttContext.publish("zigbee2mqtt/" & config.friendlyName & "/set", "{\"state\": \"" & sendState & "\"}") + +proc toggleLamp* (deviceName: string) {.async.} = + let config = server.config.devices[deviceName] + + if config.type != Zigbee2MqttLamp: + return + + await mqttContext.publish("zigbee2mqtt/" & config.friendlyName & "/set", "{\"state\": \"TOGGLE\"}") + +proc setLampBrightness* (deviceName: string, brightness: uint8) {.async.} = + let config = server.config.devices[deviceName] + + if config.type != Zigbee2MqttLamp: + return + + await mqttContext.publish("zigbee2mqtt/" & config.friendlyName & "/set", "{\"brightness\": \"" & $brightness & "\"}") + +proc setLampColor* (deviceName: string, colorX: float, colorY: float) {.async.} = + let config = server.config.devices[deviceName] + + if config.type != Zigbee2MqttLamp: + return + + await mqttContext.publish("zigbee2mqtt/" & config.friendlyName & "/set", "{\"color\": {\"X\": \"" & $colorX & "\", \"Y\": \"" & $colorY & "\"}}") + +proc setLampColorTemperature* (deviceName: string, colorTemperature: int) {.async.} = + let config = server.config.devices[deviceName] + + if config.type != Zigbee2MqttLamp: + return + + await mqttContext.publish("zigbee2mqtt/" & config.friendlyName & "/set", "{\"color_temp\": \"" & $colorTemperature & "\"}") + +proc updateLamp (topic: string, message: string) = + echo message + let deviceName = zigbee2mqttDevices[topic] + let recivedData = parseJson(message) + + server.state[deviceName].lastUpdated = some(toUnix(getTime())) + + if recivedData.hasKey("linkquality"): + server.state[deviceName].lampLinkquality = recivedData["linkquality"].getInt + + if recivedData.hasKey("state"): + if recivedData["state"].getStr == "ON": + server.state[deviceName].lampState = true + else: + server.state[deviceName].lampState = false + + if recivedData.hasKey("brightness"): + server.state[deviceName].lampBrightness = recivedData["brightness"].getInt + + if recivedData.hasKey("color"): + server.state[deviceName].lampColorX = recivedData["color"]["x"].getFloat + server.state[deviceName].lampColorY = recivedData["color"]["y"].getFloat + + if recivedData.hasKey("color_temp"): + server.state[deviceName].lampColorTemperature = recivedData["color_temp"].getInt + + broadcastServerState() + + +proc initZigbee2MqttLamps* () {.async.} = + for key, device in server.config.devices.pairs(): + if device.type != Zigbee2MqttLamp: continue + + zigbee2mqttDevices["zigbee2mqtt/" & device.friendlyName] = key + server.state[key] = DeviceState(type: Zigbee2MqttLamp, lampType: device.lampType) + + await mqttContext.subscribe("zigbee2mqtt/" & device.friendlyName, 2, updateLamp)+ \ No newline at end of file
diff --git a/src/frontend.nim b/src/frontend.nim @@ -0,0 +1,119 @@ +import asyncdispatch, asynchttpserver, json, tables, options +import ws +import types, vars, utils, actionHandler + +proc processWsClient(req: Request) {.async,gcsafe.} = + var ws: WebSocket + cleanupConnections() + + try: + ws = await newWebsocket(req) + lastClientId += 1 + setupPings(ws, 2) + + 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" + subscribedConnections.add(ws) + else: + echo "removing client (todo)" + + else: + response = await handleAction(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 "socket closed:", 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 handleAction(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.} = + var httpServer = newAsyncHttpServer() + await httpServer.serve(Port(server.config.serverConfig.frontendPort), processRequest)
diff --git a/src/frontend_http.nim b/src/frontend_http.nim @@ -1,24 +0,0 @@ -import asynchttpserver -import asyncdispatch -import util -import types -import sequtils -import json -import vars - -proc processHttpClient(req: Request) {.async,gcsafe.} = - if req.reqMethod == HttpGet: - if req.headers.hasKey("Authorization") and req.headers["Authorization"] == "Bearer " & server.config.accessToken: - await req.respond(Http200, $(%* server.state)) - else: - await req.respond(Http401, "401 Unauthorized") - elif req.reqMethod == HttpPost: - let client = Client(id: lastClientId, sendProc: proc (msg: string) {.async.} = await req.respond(Http200, msg)) - lastClientId += 1 - await client.tryHandleRequest(req.body) - else: - await req.respond(Http405, "405 Method Not Allowed") - -proc serveHttp*() {.async.} = - var httpServer = newAsyncHttpServer() - await httpServer.serve(Port(server.config.httpPort), processHttpClient)
diff --git a/src/frontend_prometheus.nim b/src/frontend_prometheus.nim @@ -1,59 +0,0 @@ -import asynchttpserver -import asyncdispatch -import util -import types -import sequtils -import json -import vars -import tables - -proc addVal(resp: var string, name: string, key: string, val: string, lastUpdated: int64) = - resp = resp & name & "{" - resp = resp & "device=\"" & key & "\"" - resp = resp & "} " - resp = resp & val - if lastUpdated != 0: - resp = resp & " " - resp = resp & $(lastUpdated * 1000) - resp = resp & "\n" - -proc fmtBool(b: bool): string = - if b: - return "1" - else: - return "0" - -proc processPrometheusClient(req: Request) {.async,gcsafe.} = - if req.reqMethod == HttpGet: - if req.headers.hasKey("Authorization") and req.headers["Authorization"] == "Bearer " & server.config.accessToken: - var resp = "" - for key, device in server.config.devices.pairs(): - if device.type == PowerMeter: - if server.state[key].frequency == 0: - continue - resp.addVal("powermeter_import", key, $(server.state[key].import), server.state[key].lastUpdated) - resp.addVal("powermeter_cosphi", key, $(server.state[key].cosphi), server.state[key].lastUpdated) - resp.addVal("powermeter_power", key, $(server.state[key].power), server.state[key].lastUpdated) - resp.addVal("powermeter_frequency", key, $(server.state[key].frequency), server.state[key].lastUpdated) - resp.addVal("powermeter_voltage", key, $(server.state[key].voltage), server.state[key].lastUpdated) - - if device.type == RelayBoard: - for i, val in server.state[key].relays: - resp.addVal("relayboard_relay", key & "\",relay=\"" & $(i), fmtBool(val), 0) - - if device.type == LacrosseTempSensor: - resp.addVal("lacrossetempsensor_temperature", key, $(server.state[key].temperature), server.state[key].lastUpdated2) - resp.addVal("lacrossetempsensor_weakbattery", key, fmtBool(server.state[key].weakBattery), server.state[key].lastUpdated2) - if server.state[key].humidity < 0: - continue - resp.addVal("lacrossetempsensor_humidity", key, $(server.state[key].humidity), server.state[key].lastUpdated2) - - await req.respond(Http200, resp) - else: - await req.respond(Http401, "401 Unauthorized") - else: - await req.respond(Http405, "405 Method Not Allowed") - -proc servePrometheus*() {.async.} = - var prometheusServer = newAsyncHttpServer() - await prometheusServer.serve(Port(server.config.prometheusPort), processPrometheusClient)
diff --git a/src/frontend_tcp.nim b/src/frontend_tcp.nim @@ -1,30 +0,0 @@ -import asyncnet -import asyncdispatch -import util -import types -import sequtils -import json -import vars - -proc processTcpClient(sock: AsyncSocket) {.async.} = - let client = Client(id: lastClientId, sendProc: proc (msg: string) {.async.} = await sock.send(msg & '\n')) - lastClientId += 1 - try: - while true: - let req = await sock.recvLine() - if req == "": - sock.close() - break - await client.tryHandleRequest(req) - except: - sock.close() - -proc serveTcp*() {.async.} = - var socket = newAsyncSocket() - socket.setSockOpt(OptReuseAddr, true) - socket.bindAddr(Port(server.config.tcpPort)) - socket.listen() - - while true: - let sock = await socket.accept() - asyncCheck processTcpClient(sock)
diff --git a/src/frontend_ws.nim b/src/frontend_ws.nim @@ -1,30 +0,0 @@ -import asynchttpserver -import asyncdispatch -import util -import types -import ws -import sequtils -import json -import vars - -proc processWsClient(req: Request) {.async,gcsafe.} = - var ws: WebSocket - var client: Client - try: - ws = await newWebsocket(req) - lastClientId += 1 - client = Client(id: lastClientId, sendProc: proc (msg: string) {.async.} = await ws.send(msg)) - except: - asyncCheck req.respond(Http404, "404") - return - - try: - while true: - let req = await ws.receiveStrPacket() - await client.tryHandleRequest(req) - except: - ws.close() - -proc serveWs*() {.async.} = - var httpServer = newAsyncHttpServer() - await httpServer.serve(Port(server.config.wsPort), processWsClient)
diff --git a/src/influx.nim b/src/influx.nim @@ -0,0 +1,63 @@ +import asyncdispatch, strutils, tables, httpclient, options, json, base64, uri +import types, vars + +proc existsDatabase* (config: InfluxConfig, databaseName: string): Future[bool] {.async.}= + var client = newAsyncHttpClient() + + 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) + + client.close() + + if data["results"][0].hasKey("error"): + return false + + return true + +proc insertDatabase* (config: InfluxConfig, databaseName: string, tableName: string, tags: Table[string, string], fields: Table[string, string], timestamp: int64): Future[bool] {.async.} = + var client = newAsyncHttpClient() + + 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 & "/" + + var tagsCombined: seq[string] + var fieldsCombined: seq[string] + + for key, value in pairs(tags): + tagsCombined.add(key & "=" & value) + + for key, value in pairs(fields): + fieldsCombined.add(key & "=" & value) + + let body = tableName & "," & tagsCombined.join(",") & " " & fieldsCombined.join(",") & " " & $timestamp + + let response = await client.request(baseUrl & "write?db=" & databaseName, httpMethod = HttpPost, body = body) + + if response.code != Http204: + return false + + return true + +proc initInflux* () = + let config = server.config.serverConfig.influx.get + + try: + if config.powermeterDatabase.isSome: + if not waitFor config.existsDatabase(config.powermeterDatabase.get): + echo "Specified Influxdatabase for Powermeter does not exist!" + quit() + + if config.lacrosseDatabase.isSome: + if not waitFor config.existsDatabase(config.lacrosseDatabase.get): + echo "Specified Influxdatabase for Lacrosse does not exist!" + quit() + + except: + let e = getCurrentException() + echo e.msg
diff --git a/src/modbus.nim b/src/modbus.nim @@ -1,9 +1,5 @@ -import types -import tables -import sequtils -import asyncdispatch -import posix -import vars +import asyncdispatch, options, posix, tables +import types, vars {.passL: "-lmodbus".} proc modbus_new_tcp*(ad: cstring, port: cint): modbus {.importc, dynlib: "libmodbus.so.5"} @@ -69,8 +65,9 @@ proc asyncReadBits*(mb: modbus, ad: uint8, reg: uint8, nb: uint8): Future[seq[bo return @data[0..nb-1] proc initModbus*() = - let port: cint = int32(server.config.modbusPort) - mb = modbus_new_tcp(server.config.modbusAddr, port) + let config = server.config.serverConfig.modbus.get + let port: cint = int32(config.port) + mb = modbus_new_tcp(config.host, port) discard mb.modbus_connect() proc deinitModbus*() =
diff --git a/src/mqtt.nim b/src/mqtt.nim @@ -0,0 +1,12 @@ +import asyncdispatch, options +import types, vars +import nmqtt + + +proc initMqtt* () {.async.} = + var config = server.config.serverConfig.mqtt.get + + mqttContext = newMqttCtx("smartied") + mqttContext.set_host(config.host, config.port) + + await mqttContext.start()+ \ No newline at end of file
diff --git a/src/smartied.nim b/src/smartied.nim @@ -1,32 +1,41 @@ -import asyncdispatch, types, modbus, json, vars, util, tables, os -import frontend_tcp, frontend_ws, frontend_http, frontend_prometheus -import backend_powermeter, backend_relayboard, backend_lacrosse +import asyncdispatch, json, tables, os +import types, modbus, mqtt, influx, vars, utils, options +import frontend +import devices/[modbusPowermeter, modbusRelayboard, lacrosseSensors, zigbee2mqttLamp] -var configFile = "./config.json" +proc main() {.async.} = + var configFile = "./config.json" -if getEnv("CONFIG_PATH") != "": - configFile = getEnv("CONFIG_PATH") + if getEnv("CONFIG_PATH") != "": + configFile = getEnv("CONFIG_PATH") -if not fileExists(configFile): - echo "Config file not found" - quit() + if not fileExists(configFile): + echo "Config file not found" + quit() -server = Server(config: parseFile(configFile).to(Config)) + server = Server(config: parseFile(configFile).to(Config)) -if getEnv("ACCESS_TOKEN") != "": - server.config.accessToken = getEnv("ACCESS_TOKEN") + if getEnv("ACCESS_TOKEN") != "": + server.config.serverConfig.accessToken = getEnv("ACCESS_TOKEN") -proc main() {.async.} = initUtil() - initModbus() - initBackendPowermeter() - initBackendRelayboard() - initBackendLacrosse() - - asyncCheck serveTcp() - asyncCheck serveWs() - asyncCheck serveHttp() - asyncCheck servePrometheus() + + if server.config.serverConfig.influx.isSome(): + initInflux() + + if server.config.serverConfig.modbus.isSome(): + initModbus() + initModbusPowermeters() + initModbusRelayboards() + + if server.config.serverConfig.lacrosse.isSome(): + initLacrosseSensors() + + if server.config.serverConfig.mqtt.isSome(): + asyncCheck initMqtt() + asyncCheck initZigbee2MqttLamps() + + asyncCheck serveFrontend() runForever()
diff --git a/src/types.nim b/src/types.nim @@ -1,40 +1,18 @@ -import asyncnet -import asyncdispatch -import json -import tables -import sequtils +import json, tables, options type DeviceType* = enum PowerMeter, RelayBoard, - LacrosseTempSensor + LacrosseTempSensor, + Zigbee2MqttLamp -type DeviceConfig* = object - address*: uint8 - case type*: DeviceType - of PowerMeter: - model*: string - of RelayBoard: - firstRegister*: uint8 - count*: uint8 - of LacrosseTempSensor: - id*: string - -type Config* = object - tcpPort*: uint16 - wsPort*: uint16 - httpPort*: uint16 - prometheusPort*: uint16 - modbusAddr*: string - modbusPort*: uint16 - lacrosseAddr*: string - lacrossePort*: uint16 - powermeterUpdateIntervalSec*: uint - devices*: Table[string, DeviceConfig] - clientConfigs*: Table[string, JsonNode] - accessToken*: string +type Zigbee2MqttLampType* = enum + RGB, + WhiteSpectrum + Switchable type DeviceState* = object + lastUpdated*: Option[int64] case type*: DeviceType of PowerMeter: power*: float32 @@ -42,41 +20,104 @@ type DeviceState* = object voltage*: float32 `import`*: float32 frequency*: float32 - lastUpdated*: int64 of RelayBoard: relays*: seq[bool] of LacrosseTempSensor: humidity*: float32 temperature*: float32 weakBattery*: bool - lastUpdated2*: int64 + of Zigbee2MqttLamp: + lampType*: Zigbee2MqttLampType + lampState*: bool + lampBrightness*: int + lampColorX*: float + lampColorY*: float + lampColorTemperature*: int + lampLinkquality*: int -type Server* = object - state*: Table[string, DeviceState] - config*: Config - clients*: seq[AsyncSocket] type ActionType* = enum - SetRelayAction, - ToggleRelayAction, + SwitchStateAction, + SetBrightnessAction, + SetColorXYAction, + SetColorTemperatureAction, GetClientConfigAction, SetSubscriptionStateAction type Action* = object - accessToken*: string + accessToken*: Option[string] + deviceName*: string case type*: ActionType - of SetRelayAction: - setRelayBoard*: string - setRelay*: uint8 - setValue*: bool - of ToggleRelayAction: - toggleRelayBoard*: string - toggleRelay*: uint8 + of SwitchStateAction: + relay*: Option[uint8] + state*: Option[bool] + toggle*: Option[bool] + of SetBrightnessAction: + brightness*: uint8 + of SetColorXYAction: + colorX*: float + colorY*: float + of SetColorTemperatureAction: + colorTemperature*: int of GetClientConfigAction: configName*: string of SetSubscriptionStateAction: subscribed*: bool +type DeviceConfig* = object + address*: Option[uint8] + case type*: DeviceType + of PowerMeter: + model*: string + of RelayBoard: + firstRegister*: uint8 + count*: uint8 + of LacrosseTempSensor: + id*: string + of Zigbee2MqttLamp: + friendlyName*: string + lampType*: Zigbee2MqttLampType + +type ModbusConfig* = object + host*: string + port*: uint16 + +type MqttConfig* = object + host*: string + port*: int + username*: Option[string] + password*: Option[string] + +type LacrosseConfig* = object + host*: string + port*: uint16 + +type InfluxConfig* = object + host*: string + port*: int + username*: Option[string] + password*: Option[string] + powermeterDatabase*: Option[string] + lacrosseDatabase*: Option[string] + +type SmartiedConfig* = object + frontendPort*: uint16 + modbus*: Option[ModbusConfig] + mqtt*: Option[MqttConfig] + lacrosse*: Option[LacrosseConfig] + influx*: Option[InfluxConfig] + powermeterUpdateIntervalSec*: uint + accessToken*: string + +type Config* = object + devices*: Table[string, DeviceConfig] + clientConfigs*: Table[string, JsonNode] + serverConfig*: SmartiedConfig + +type Server* = object + state*: Table[string, DeviceState] + config*: Config + type ResponseStatus* = enum Err, Ok @@ -93,6 +134,3 @@ type LacrosseMessage* = object type modbus* = pointer -type Client* = object - sendProc*: proc (msg: string): Future[void] {.gcsafe.} - id*: int
diff --git a/src/util.nim b/src/util.nim @@ -1,83 +0,0 @@ -import json -import asyncdispatch -import modbus -import types -import tables -import backend_relayboard -import vars -import sequtils - -proc trySend*(client: Client, msg: string) {.async.} = - try: - await client.sendProc(msg) - except: - let e = getCurrentException() - echo("error while sending data to client: ", e.msg) - echo("removing client ", client.id) - echo clients.map(proc(x: Client): int = x.id) - clients.keepIf(proc(x: Client): bool = x.id != client.id) - echo clients.map(proc(x: Client): int = x.id) - -proc broadcast*(msg: string) = - for client in clients.filter(proc (x: Client): bool = true): - asyncCheck client.trySend(msg) - -proc initUtil*() = - addTimer(1000, false, proc (fd: AsyncFD): bool {.gcsafe.} = - broadcast("") - ) - - lastClientId = 1 - clients = @[] - -proc handleRequest*(client: Client, req: string): Future[JsonNode] {.async.} = - let action = parseJson(req).to(Action) - - if action.accessToken != server.config.accessToken: - raise newException(OsError, "invalid accessToken") - - case action.type - of SetRelayAction: - let config = server.config.devices[action.setRelayBoard] - - server.state[action.setRelayBoard].relays[action.setRelay] = action.setValue - await mb.asyncWriteBit(config.address, config.firstRegister + action.setRelay, server.state[action.setRelayBoard].relays[action.setRelay]) - - broadcast($(%*server.state)) - return JsonNode() - of ToggleRelayAction: - let config = server.config.devices[action.toggleRelayBoard] - - server.state[action.toggleRelayBoard].relays[action.toggleRelay] = not server.state[action.toggleRelayBoard].relays[action.toggleRelay] - await mb.asyncWriteBit(config.address, config.firstRegister + action.toggleRelay, server.state[action.toggleRelayBoard].relays[action.toggleRelay]) - - broadcast($(%*server.state)) - return JsonNode() - of GetClientConfigAction: - let clientConfig: JsonNode = server.config.clientConfigs[action.configName] - return clientConfig - of SetSubscriptionStateAction: - if action.subscribed: - echo("adding client ", client.id) - echo clients.map(proc(x: Client): int = x.id) - clients.keepIf(proc(x: Client): bool = x.id != client.id) - echo clients.map(proc(x: Client): int = x.id) - clients.add(client) - echo clients.map(proc(x: Client): int = x.id) - await client.sendProc($(%*server.state)) - else: - echo("removing client ", client.id) - echo clients.map(proc(x: Client): int = x.id) - clients.keepIf(proc(x: Client): bool = x.id != client.id) - echo clients.map(proc(x: Client): int = x.id) - return JsonNode() - -proc tryHandleRequest*(client: Client, req: string): Future[void] {.async.} = - GC_fullCollect() - var resp: Response - try: - resp = Response(status: Ok, data: await client.handleRequest(req)) - except: - let e = getCurrentException() - resp = Response(status: Err, data: newJString(e.msg)) - await client.sendProc($(%*resp))
diff --git a/src/utils.nim b/src/utils.nim @@ -0,0 +1,60 @@ +import asyncdispatch, sequtils, json, tables, math, options +import ws +import types, vars + +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 & "\"" + resp = resp & "} " + resp = resp & val + if lastUpdated != 0: + resp = resp & " " + resp = resp & $(lastUpdated * 1000) + resp = resp & "\n" + +proc fmtBool* (b: bool): string = + if b: + return "1" + else: + return "0" + +proc cleanupConnections*() = +# echo "subscribedConnections: " & $subscribedConnections.len + subscribedConnections.keepIf(proc(x: Websocket): bool = x.readyState != Closed) +# echo "subscribedConnections(after clenup): " & $subscribedConnections.len + +proc broadcast* (msg: string) = + cleanupConnections() + + try: + for socket in subscribedConnections: + if socket.readyState == Open: + asyncCheck socket.send(msg) + + except WebSocketError: + echo "socket closed:", getCurrentExceptionMsg() + +proc broadcastServerState* () = + broadcast($(%*server.state)) + +proc isaRound* [T: float32|float64](value: T, places: int = 0): float = + if places == 0: + result = round(value) + else: + result = value * pow(10.0, T(places)) + result = floor(result) + result = result / pow(10.0, T(places)) + +proc checkAccessToken* (token: Option[string]): bool = + if not token.isNone: + if server.config.serverConfig.accessToken != token.get: + return false + else: + if server.config.serverConfig.accessToken != "": + return false + + return true
diff --git a/src/vars.nim b/src/vars.nim @@ -1,8 +1,10 @@ +import tables +import ws, nmqtt import types -import ws -import asyncnet -var clients* {.threadvar.}: seq[Client] +var subscribedConnections* {.threadvar.}: seq[Websocket] var server* {.threadvar.}: Server var mb* {.threadvar.}: modbus +var mqttContext* {.threadvar.}: MqttCtx var lastClientId* {.threadvar.}: int +var zigbee2mqttDevices* {.threadvar.}: Table[string, string]