packages feed

ttn-client-0.1.0.1: src/Data/TTN/Client.hs

{-# Language DataKinds #-}
{-# Language OverloadedStrings #-}
{-# Language RecordWildCards #-}

module Data.TTN.Client (
    ttnClient
  , ttnClientConf
  , Event
  , EventType(..)
  , Conf(..)
  , envConfCfg
  , parseConfCfg
  ) where

import Control.Concurrent
import Control.Concurrent.STM
import Control.Monad

import System.Exit (exitFailure)
import System.IO (hPutStrLn, stderr)

import Data.TTN
import qualified Network.MQTT as MQTT

import qualified Data.Text as T
import qualified Data.Text.IO as T
import qualified Data.ByteString.Char8 as BS

import Data.Ini.Config
import System.Environment
import System.Directory
import System.FilePath.Posix

data Conf = Conf {
    appId :: T.Text
  , appKey :: T.Text
  , appRouter :: T.Text
  , appRouterPort :: Integer
  }
  deriving (Eq, Show)

iniParser :: IniParser Conf
iniParser = section "app" $ do
  appId <- field "id"
  appKey <- field "key"
  appRouter <- fieldDef "router" "eu.thethings.network"
  appRouterPort <- fieldDefOf "port" number 1883
  return $ Conf {..}

-- | Try parsing config from given 'FilePath'
parseConfCfg :: FilePath -> IO (Either String Conf)
parseConfCfg fpath = do
  rs <- T.readFile fpath
  return $ parseIniFile rs iniParser

-- | Try loading config from location in @TTNCFG@ environment variable or @~/.ttn/config@
envConfCfg :: IO (Conf)
envConfCfg = do
  menv <- lookupEnv "TTNCFG"
  case menv of
    Nothing -> do
      udir <- getHomeDirectory
      let userConf = udir </> ".ttn" </> "config"
      hasCfg <- doesFileExist userConf
      case hasCfg of
        False -> putStrLn ("Unable to load config: no ~/.ttn/config or TTNCFG env variable set") >> exitFailure
        True -> do
          res <- parseConfCfg userConf
          case res of
            Left err -> putStrLn ("Unable to parse config: " ++ err) >> exitFailure
            Right cfg -> return cfg
    Just env -> do
      res <- parseConfCfg env
      case res of
        Left err -> putStrLn ("Unable to parse config: " ++ err) >> exitFailure
        Right cfg -> return cfg


data EventType a =
    Up a
  | Down a
  | DownAcked a
  | DownSent a
  | DownScheduled a
  | Activation a
  | Create a
  | Update a
  | Delete a
  | Unknown a
  deriving (Eq, Ord, Show)

type Event = Either String (EventType Uplink)

topic:: MQTT.Topic
topic = "#"

parseType (MQTT.MqttText t) = typ
  where
    typ = case drop 3 sp of
      ["up"]                         -> Up
      ["down"]                       -> Down
      ["events", "down", "acks"]     -> DownAcked
      ["events", "down", "sent"]     -> DownSent
      ["events", "down", "schedule"] -> DownScheduled
      ["events", "activations"]      -> Activation
      ["events", "create"]           -> Create
      ["events", "update"]           -> Update
      ["events", "delete"]           -> Delete
      _                              -> Unknown

    sp = T.splitOn "/" t

-- | Try to load config from default locations and start actual client
ttnClient :: TChan Event -> IO ()
ttnClient chan = do
  conf <- envConfCfg
  ttnClientConf conf chan

-- | Start client with custom `Conf` config
ttnClientConf :: Conf -> TChan Event -> IO ()
ttnClientConf Conf{..} chan = do
  cmds <- MQTT.mkCommands
  pubChan <- newTChanIO
  let conf = (MQTT.defaultConfig cmds pubChan)
              { MQTT.cUsername = Just $ appId
              , MQTT.cHost = T.unpack appRouter
              , MQTT.cPort = fromInteger appRouterPort
              , MQTT.cPassword = Just appKey
              }

  forever $ do
    tID <- forkIO $ do
      qosGranted <- MQTT.subscribe conf [(topic, MQTT.Handshake)]
      case qosGranted of
        [MQTT.Handshake] -> forever $ atomically (readTChan pubChan) >>= (mqttHandleChan chan)
        _ -> do
          hPutStrLn stderr $ "Wanted QoS Handshake, got " ++ show qosGranted
          exitFailure

    terminated <- MQTT.run conf
    hPutStrLn stderr $ "Terminated, restarting. Reason: " ++ show terminated
    killThread tID

mqttHandleChan :: TChan Event -> MQTT.Message MQTT.PUBLISH -> IO ()
mqttHandleChan chan msg = do
    -- sometimes it's useful to ignore retained messages
    unless (MQTT.retain $ MQTT.header msg) $ do
      let t = MQTT.topic $ MQTT.body msg
          p = MQTT.payload $ MQTT.body msg

      putStrLn $ "Received on topic " ++ (show t)
      case parse p of
        Left err -> do
          case parseError p of
            Left _  -> hPutStrLn stderr $ "Invalid JSON, error: " ++ err
            Right e -> atomically $ writeTChan chan $ Left $ T.unpack $ errorMsg e

        Right u@Uplink{..} -> do
          let typ = parseType $ MQTT.fromTopic t

          atomically $ writeTChan chan $ Right $ typ u