ctucx.git: nimtradfri

[nimlang] incomplete library to interact with ikea tradfri-gateways

commit 2149d7385accadf3deded63643c88309d734850b
Author: ctucx <c@ctu.cx>
Date: Mon, 14 Sep 2020 13:01:28 +0200

init
11 files changed, 618 insertions(+), 0 deletions(-)
A
coapClient.nim
|
25
+++++++++++++++++++++++++
A
device.nim
|
212
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
deviceHelpers.nim
|
68
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
deviceTypes.nim
|
102
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
gateway.nim
|
40
++++++++++++++++++++++++++++++++++++++++
A
gatewayTypes.nim
|
30
++++++++++++++++++++++++++++++
A
helpers.nim
|
11
+++++++++++
A
mappings.nim
|
59
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
tradfri.nim
|
20
++++++++++++++++++++
A
tradfriCli.nim
|
51
+++++++++++++++++++++++++++++++++++++++++++++++++++
A
types.nim
|
0
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
diff --git a/types.nim b/types.nim