commit 2149d7385accadf3deded63643c88309d734850b
Author: ctucx <c@ctu.cx>
Date: Mon, 14 Sep 2020 13:01:28 +0200
Author: ctucx <c@ctu.cx>
Date: Mon, 14 Sep 2020 13:01:28 +0200
init
11 files changed, 618 insertions(+), 0 deletions(-)
diff --git a/coapClient.nim b/coapClient.nim @@ -0,0 +1,25 @@ +import osproc, json + +type + CoapException* = object of ValueError + + +proc makeCoapRequest* (host: string, port: int, reqMethod: string, user: string, password: string, endpoint: string, reqPayload: JsonNode): JsonNode = + var arguments = @["-B", "2", "-m", reqMethod, "-u", user, "-k", password] + + if reqMethod == "put": + arguments.add("-e") + arguments.add($reqPayload) + + arguments.add("coaps://" & host & ":" & $port & endpoint) + + let reqResult = execProcess("coap-client", args = arguments, options = {poUsePath}) + + try: + if reqMethod == "put" and reqResult == "": + return %* {} + + return parseJson(reqResult) + + except JsonParsingError: + raise newException(CoapException, reqResult)
diff --git a/device.nim b/device.nim @@ -0,0 +1,212 @@ +import json, strutils, options + +import coapClient +import gatewayTypes, deviceTypes +import mappings, helpers + +proc parseDevice (data: JsonNode): TradfriDevice = + let deviceType = TradfriDeviceType(data[ParameterType].getInt) + var state: TradfriDeviceState + + case deviceType: + of Remote: + state = TradfriDeviceState( + kind: deviceType, + remoteSupported: false + ) + + of slaveRemote: + state = TradfriDeviceState( + kind: deviceType, + slaveRemoteSupported: false + ) + + of Lightbulb: + state = TradfriDeviceState( + kind: deviceType, + lightPowered: parseBool($data[DeviceLightbulb][0][ParameterPowerState].getInt), + lightBrightness: data[DeviceLightbulb][0][ParameterDimmerValue].getInt + ) + + #get hue and saturation (only for RGB-bulbs) + if data[DeviceLightbulb][0].hasKey(ParameterHue): + state.lightHue = some(data[DeviceLightbulb][0][ParameterHue].getInt) + state.lightSaturation = some(data[DeviceLightbulb][0][ParameterSaturation].getInt) + else: + state.lightHue = none(int) + state.lightSaturation = none(int) + + + #get color hex-value (for white-spectrum and RGB, but only some presets) + if data[DeviceLightbulb][0].hasKey(ParameterColorHex): + state.lightColorHex = some(data[DeviceLightbulb][0][ParameterColorHex].getStr) + else: + state.lightColorHex = none(string) + + #get colorX and colorY values (can be used to set any color on RGB bulbs) + if data[DeviceLightbulb][0].hasKey(ParameterColorX): + state.lightColorX = some(data[DeviceLightbulb][0][ParameterColorX].getFloat) + state.lightColorY = some(data[DeviceLightbulb][0][ParameterColorY].getFloat) + else: + state.lightColorX = none(float) + state.lightColorY = none(float) + + + #get color-specturm value + if data[DeviceLightbulb][0].hasKey(ParameterColorTemperature): + state.lightColorTemperature = some(data[DeviceLightbulb][0][ParameterColorTemperature].getInt) + else: + state.lightColorTemperature = none(int) + + #determine type of bulb + if state.lightHue.isSome: + state.lightSpectrum = RGB + elif state.lightColorTemperature.isSome: + state.lightSpectrum = White + else: + state.lightSpectrum = None + + of Plug: + state = TradfriDeviceState( + kind: deviceType, + plugPowered: parseBool($data[DevicePlug][0][ParameterPowerState].getInt), + plugDimmer: data[DevicePlug][0][ParameterDimmerValue].getInt + ) + + of motionSensor: + state = TradfriDeviceState( + kind: deviceType, + motionSensorSupported: false + ) + + of signalRepeater: + state = TradfriDeviceState( + kind: deviceType, + signalRepeaterSupported: false + ) + + of Blind: + state = TradfriDeviceState( + kind: deviceType, + blindPosition: data["3"][ParameterBlindPosition].getFloat, + blindTrigger: data["3"][ParameterBlindTrigger].getFloat + ) + + of soundRemote: + state = TradfriDeviceState( + kind: deviceType, + soundRemoteSupported: false + ) + + return TradfriDevice( + `type`: deviceType, + id: data[ParameterId].getInt, + name: data[ParameterName].getStr, + alive: intToBool(data[ParameterAlive].getInt), + createdAt: data[ParameterCreatedAt].getInt, + lastSeen: data[ParameterLastSeen].getInt, + state: state, + info: TradfriDeviceInfo( + manufacturer: data["3"]["0"].getStr, + modelNumber: data["3"]["1"].getStr, + serialNumber: data["3"]["2"].getStr, + firmwareVersion: data["3"]["3"].getStr, + power: TradfriPowerSource(data["3"]["6"].getInt), + battery: data["3"]{"9"}.getInt + ) + ) + + +proc operateDevice* (device: TradfriDevice, action: TradfriDeviceAction): bool = + var requestParams = %* {} + + template CheckDeviceType(typeId: TradfriDeviceType) = + if device.`type` != TradfriDeviceType(typeId): + raise newException(ValueError, "Wrong action for this Devicetype") + + case action.kind: + of DeviceRename: + requestParams.add(ParameterName, %action.deviceName) + + of LightSetPowerState: + CheckDeviceType(Lightbulb) + + requestParams.add(DeviceLightbulb, %* [{ + ParameterPowerState: boolToInt(action.lightPowerState), + ParameterTransitionTime: action.transitionTime + }]) + + of LightSetBrightness: + CheckDeviceType(Lightbulb) + + requestParams.add(DeviceLightbulb, %* [{ + ParameterDimmerValue: action.lightBrightness, + ParameterTransitionTime: action.transitionTime + }]) + + of LightSetColorHex: + CheckDeviceType(Lightbulb) + + requestParams.add(DeviceLightbulb, %* [{ + ParameterColorHex: action.lightColorHex, + ParameterTransitionTime: action.transitionTime + }]) + + of LightSetColorXY: + CheckDeviceType(Lightbulb) + + requestParams.add(DeviceLightbulb, %* [{ + ParameterColorX: action.lightColorX, + ParameterColorY: action.lightColorY, + ParameterTransitionTime: action.transitionTime + }]) + + of LightSetHueSaturation: + CheckDeviceType(Lightbulb) + + requestParams.add(DeviceLightbulb, %* [{ + ParameterHue: action.lightHue, + ParameterSaturation: action.lightSaturation, + ParameterTransitionTime: action.transitionTime + }]) + + of LightSetColorTemperature: + CheckDeviceType(Lightbulb) + + requestParams.add(DeviceLightbulb, %* [{ + ParameterColorTemperature: action.lightColorTemperature, + ParameterTransitionTime: action.transitionTime + }]) + + of PlugSetPowerState: + CheckDeviceType(Plug) + + requestParams.add(DevicePlug, %* [{ + ParameterPowerState: action.plugPowerState, + }]) + + of PlugSetDimmerValue: + CheckDeviceType(Plug) + + requestParams.add(DevicePlug, %* [{ + ParameterDimmerValue: action.plugDimmerValue, + }]) + + + discard makeCoapRequest(device.gatewayRef.host, device.gatewayRef.port, "put", device.gatewayRef.user, device.gatewayRef.pass, EndpointDevices & $device.id, requestParams) + + +proc getDevice* (gatewayRef: TradfriGatewayRef, deviceId: int): TradfriDevice = + let request = makeCoapRequest(gatewayRef.host, gatewayRef.port, "get", gatewayRef.user, gatewayRef.pass, EndpointDevices & $deviceId, %* {}) + + result = parseDevice(request) + result.gatewayRef = gatewayRef + + +proc getDevices* (gatewayRef: TradfriGatewayRef): seq[TradfriDevice] = + let request = makeCoapRequest(gatewayRef.host, gatewayRef.port, "get", gatewayRef.user, gatewayRef.pass, EndpointDevices, %* {}) + + result = newSeq[TradfriDevice]() + + for id in request: + result.add(getDevice(gatewayRef, id.getInt))
diff --git a/deviceHelpers.nim b/deviceHelpers.nim @@ -0,0 +1,68 @@ +import colors +import deviceTypes, helpers, device + +proc setPowerState* (device: TradfriDevice, state: bool): bool = + if device.`type` == Lightbulb: + return device.operateDevice(TradfriDeviceAction( + kind: LightSetPowerState, + lightPowerState: state + )) + + if device.`type` == Plug: + return device.operateDevice(TradfriDeviceAction( + kind: PlugSetPowerState, + plugPowerState: state + )) + + +proc togglePowerState* (device: TradfriDevice): bool = + if device.`type` == Lightbulb: + return device.operateDevice(TradfriDeviceAction( + kind: LightSetPowerState, + lightPowerState: invertBool(device.state.lightPowered) + )) + + if device.`type` == Plug: + return device.operateDevice(TradfriDeviceAction( + kind: PlugSetPowerState, + plugPowerState: invertBool(device.state.plugPowered) + )) + + +proc setBrightness* (device: TradfriDevice, brightness: int): bool = + return device.operateDevice(TradfriDeviceAction( + kind: LightSetBrightness, + lightBrightness: brightness + )) + + +proc setColorHex* (device: TradfriDevice, color: string): bool = + return device.operateDevice(TradfriDeviceAction( + kind: LightSetColorHex, + lightColorHex: color + )) + + +proc setColorXY* (device: TradfriDevice, colorX: float, colorY: float): bool = + return device.operateDevice(TradfriDeviceAction( + kind: LightSetColorXY, + lightColorX: colorX, + lightColorY: colorY + )) + + +proc setColorXYfromHex* (device: TradfriDevice, color: string): bool = + let color = extractRGB(parseColor(color)) + + let x = (0.4124 * toFloat(color.r)) + (0.3576 * toFloat(color.g)) + (0.1805 * toFloat(color.b)) + let y = (0.2126 * toFloat(color.r)) + (0.7152 * toFloat(color.g)) + (0.0722 * toFloat(color.b)) + let z = (0.0193 * toFloat(color.r)) + (0.1192 * toFloat(color.g)) + (0.9505 * toFloat(color.b)) + + let X = (x / (x + y + z)) + let Y = (y / (x + y + z)) + + return device.operateDevice(TradfriDeviceAction( + kind: LightSetColorXY, + lightColorX: X, + lightColorY: Y + ))
diff --git a/deviceTypes.nim b/deviceTypes.nim @@ -0,0 +1,102 @@ +import options +import gatewayTypes + +type + TradfriDeviceType* = enum + Remote, slaveRemote, Lightbulb, Plug, motionSensor, signalRepeater, Blind, soundRemote + + TradfriPowerSource* = enum + Unknown, internalBattery, externalBattery, Battery, POE, USB, AC, Solar + + TradfriLightSpectrum* = enum + RGB, White, None + + TradfriDeviceActionType* = enum + DeviceRename, LightSetPowerState, LightSetBrightness, LightSetColorHex, LightSetColorXY, LightSetHueSaturation, LightSetColorTemperature, PlugSetPowerState, PlugSetDimmerValue + + TradfriDeviceInfo* = object + manufacturer*: string + modelNumber*: string + serialNumber*: string + firmwareVersion*: string + power*: TradfriPowersource + battery*: int + + + TradfriDeviceState* = object + case kind*: TradfriDeviceType + of Remote: + remoteSupported*: bool + + of slaveRemote: + slaveRemoteSupported*: bool + + of Lightbulb: + lightPowered*: bool + lightSpectrum*: TradfriLightSpectrum + lightHue*: Option[int] + lightSaturation*: Option[int] + lightColorHex*: Option[string] + lightColorX*: Option[float] + lightColorY*: Option[float] + lightColorTemperature*: Option[int] + lightBrightness*: int + + of Plug: + plugPowered*: bool + plugDimmer*: int # 1 - 254 (but currently no dimmable plugs available) + + of motionSensor: + motionSensorSupported*: bool + + of signalRepeater: + signalRepeaterSupported*: bool + + of Blind: + blindPosition*: float # 0 - 100 + blindTrigger*: float # ? + + of soundRemote: + soundRemoteSupported*: bool + + TradfriDevice* = object + gatewayRef*: TradfriGatewayRef + `type`*: TradfriDeviceType + id*: int + name*: string + alive*: bool + createdAt*: int + lastSeen*: int + info*: TradfriDeviceInfo + state*: TradfriDeviceState + + TradfriDeviceAction* = object + transitionTime*: int + case kind*: TradfriDeviceActionType + of DeviceRename: + deviceName*: string + of LightSetPowerState: + lightPowerState*: bool + + of LightSetBrightness: + lightBrightness*: int + + of LightSetColorHex: + lightColorHex*: string + + of LightSetColorXY: + lightColorX*: float + lightColorY*: float + + of LightSetHueSaturation: + lightHue*: int + lightSaturation*: int + + of LightSetColorTemperature: + lightColorTemperature*: int + + of PlugSetPowerState: + plugPowerState*: bool + + of PlugSetDimmerValue: + plugDimmerValue*: int
diff --git a/gateway.nim b/gateway.nim @@ -0,0 +1,40 @@ +import json + +import types, gatewayTypes +import mappings, helpers +import coapClient + +proc newTradfrigateway* (host: string, port: int, user: string, pass: string): TradfriGatewayRef = + return TradfriGatewayRef( + host: host, + port: port, + user: user, + pass: pass + ) + +proc getGatewayDetails* (gatewayRef: TradfriGatewayRef): TradfriGatewayDetails = + let request = makeCoapRequest(gatewayRef.host, gatewayRef.port, "get", gatewayRef.user, gatewayRef.pass, EndpointGatewayDetails, %* {}) + + echo pretty request + + return TradfriGatewayDetails( + alexaPaired: intToBool(request[ParameterGatewayAlexaPaired].getInt), + googleHomePaired: intToBool(request[ParameterGatewayAlexaPaired].getInt), + currUnixTimestampUTC: request[ParameterGatewayUtcNowUnixTimestamp].getInt, + timeSource: request[ParameterGatewayTimeSource].getInt, + ntpServer: request[ParameterGatewayNtpServerUrl].getStr, + version: request[ParameterGatewayVersion].getStr, + otaUpdateRunning: intToBool(request[ParameterGatewayOtaUpdateState].getInt), + otaUpdateProgress: request[ParameterGatewayOtaUpdateProgress].getInt, + otaUpdatePriority: TradfriUpdatePriority(request[ParameterGatewayOtaUpdatePriority].getInt), + otaUpdateAcceptedTimestamp: request[ParameterGatewayOtaUpdateAcceptedTimestamp].getInt, + dstStartMonth: request[ParameterGatewayDstStartMonth].getInt, + dstStartDay: request[ParameterGatewayDstStartDay].getInt, + dstStartHour: request[ParameterGatewayDstStartHour].getInt, + dstStartMinute: request[ParameterGatewayDstStartMinute].getInt, + dstEndMonth: request[ParameterGatewayDstEndMonth].getInt, + dstEndDay: request[ParameterGatewayDstEndDay].getInt, + dstEndHour: request[ParameterGatewayDstEndHour].getInt, + dstEndMinute: request[ParameterGatewayDstEndMinute].getInt, + dstTimeOffset: request[ParameterGatewayDstTimeOffset].getInt, + )
diff --git a/gatewayTypes.nim b/gatewayTypes.nim @@ -0,0 +1,30 @@ +type + TradfriGatewayRef* = object + host*: string + port*: int + user*: string + pass*: string + + TradfriUpdatePriority* = enum + Normal = 0, Critical = 1, Required = 2, Forced = 5 + + TradfriGatewayDetails* = object + alexaPaired*: bool + googleHomePaired*: bool + currUnixTimestampUTC*: int + timeSource*: int + ntpServer*: string + version*: string + otaUpdateRunning*: bool + otaUpdateProgress*: int + otaUpdatePriority*: TradfriUpdatePriority + otaUpdateAcceptedTimestamp*: int + dstStartMonth*: int + dstStartDay*: int + dstStartHour*: int + dstStartMinute*: int + dstEndMonth*: int + dstEndDay*: int + dstEndHour*: int + dstEndMinute*: int + dstTimeOffset*: int
diff --git a/helpers.nim b/helpers.nim @@ -0,0 +1,11 @@ +proc boolToInt* (value: bool): int = + if value != true: return 0 + else: return 1 + +proc intToBool* (value: int): bool = + if value != 1: return false + else: return true + +proc invertBool* (value: bool): bool = + if value != true: return true + else: return false
diff --git a/mappings.nim b/mappings.nim @@ -0,0 +1,59 @@ +const EndpointDevices* = "/15001/" +const EndpointGroups* = "/15004/" +const EndpointScenes* = "/15005/" +const EndpointNotifications* = "/15006/" +const EndpointSmartTasks* = "/15010/" +const EndpointGatewayReboot* = "/15011/9030" +const EndpointGatewayReset* = "/15011/9031" +const EndpointGatewayUpdateFW* = "/15011/9034" +const EndpointGatewayAuth* = "/15011/9063" +const EndpointGatewayDetails* = "/15011/15012" + +const DeviceMotionSensor* = "3300" +const DeviceLightbulb* = "3311" +const DevicePlug* = "3312" +const DeviceBlind* = "15015" + +const ParameterName* = "9001" +const ParameterCreatedAt* = "9002" +const ParameterId* = "9003" +const ParameterAlive* = "9019" +const ParameterLastSeen* = "9020" +const ParameterType* = "5750" + +const ParameterPowerState* = "5850" +const ParameterDimmerValue* = "5851" + +const ParameterColorHex* = "5706" +const ParameterHue* = "5707" +const ParameterSaturation* = "5708" +const ParameterColorX* = "5709" +const ParameterColorY* = "5710" +const ParameterColorTemperature* = "5711" +const ParameterTransitionTime* = "5712" + +const ParameterBlindTrigger* = "5523" +const ParameterBlindPosition* = "5536" + +const ParameterGatewayNtpServerUrl* = "9023" +const ParameterGatewayVersion* = "9029" +const ParameterGatewayForceOtaUpdateCheck* = "9032" +const ParameterGatewayOtaUpdateState* = "9054" +const ParameterGatewayOtaUpdateProgress* = "9055" +const ParameterGatewayUtcNowUnixTimestamp* = "9059" +const ParameterGatewayUtcNowISODate* = "9060" +const ParameterGatewayCommissioningMode* = "9061" +const ParameterGatewayOtaUpdatePriority* = "9066" +const ParameterGatewayOtaUpdateAcceptedTimestamp* = "9069" +const ParameterGatewayTimeSource* = "9071" +const ParameterGatewayDstStartMonth* = "9072" +const ParameterGatewayDstStartDay* = "9073" +const ParameterGatewayDstStartHour* = "9074" +const ParameterGatewayDstStartMinute* = "9075" +const ParameterGatewayDstEndMonth* = "9076" +const ParameterGatewayDstEndDay* = "9077" +const ParameterGatewayDstEndHour* = "9078" +const ParameterGatewayDstEndMinute* = "9079" +const ParameterGatewayDstTimeOffset* = "9080" +const ParameterGatewayAlexaPaired* = "9093" +const ParameterGatewayGoogleHomePaired* = "9105"
diff --git a/tradfri.nim b/tradfri.nim @@ -0,0 +1,20 @@ +import gatewayTypes, deviceTypes, mappings, helpers +import gateway +import device, deviceHelpers + +#gateway related stuff +export newTradfriGateway +export getGatewayDetails + +#device related stuff +export getDevices +export getDevice +export operateDevice +export setPowerState +export togglePowerstate + +#lightbulb helpers +export setBrightness +export setColorHex +export setColorXY +export setColorXYfromHex
diff --git a/tradfriCli.nim b/tradfriCli.nim @@ -0,0 +1,51 @@ +import json, os, strutils +import tradfri + +let tradfriGateway = newTradfriGateway( + host = "192.168.100.225", + port = 5684, + user = "ctucx", + pass = "JrSGx6WkAVJUl53b" + ) + + +let devices = tradfriGateway.getDevices() + +case paramStr(1): +of "list": + echo "list of connected devices:" + echo "=======================" + + var id = 0 + + for device in devices: + echo $id & ": \tType:\t" & $device.`type` + echo "\tName:\t" & $device.name + echo "" + id = id+1 + +of "toggle": + let deviceId = parseInt(paramStr(2)) + + if devices[deviceId].name == "": + echo "This device doesn't exist." + quit(0) + + discard devices[deviceId].togglePowerState() + +of "setColor": + let deviceId = parseInt(paramStr(2)) + + if devices[deviceId].name == "": + echo "This device doesn't exist." + quit(0) + + discard devices[deviceId].setColorXYfromHex(paramStr(3)) + +of "devices-json": + let devicesJson = %* devices + echo devicesJson + +of "devices-json-pretty": + let devicesJson = %* devices + echo pretty devicesJson