packages feed

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

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

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

import Control.Concurrent
import Control.Concurrent.Async
import Control.Concurrent.STM
import Control.Exception (Handler (..), IOException, catches)
import Control.Monad
import Data.Text (Text)

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

import Data.TTN
import Network.MQTT.Client
import qualified Network.URI

import qualified Data.Text
import qualified Data.Text.IO
import qualified Data.ByteString.Lazy

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

data Conf = Conf {
    appId         :: Text
  , appKey        :: Text
  , appRouter     :: 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 <- Data.Text.IO.readFile fpath
  return $ parseIniFile rs iniParser

-- | Try loading config from location in @TTNCFG@ environment variable
-- or from @~/.ttn/config@
envConfCfg :: IO (Conf)
envConfCfg = do
  menv <- System.Environment.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

parseType :: Text -> EventType
parseType 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 = Data.Text.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
  let (Just uri) = Network.URI.parseURI
        $ Data.Text.unpack
        $ Data.Text.concat [ "mqtt://", appId, ":", appKey, "@", appRouter ]

  putStrLn $ "Connecting to " ++ show uri
  mc <- connectURI mqttConfig { _msgCB = SimpleCallback msgReceived } uri

  putStrLn "Connected!"
  void $ subscribe mc [("#", subOptions)] mempty
  waitForClient mc
  where
    msgReceived _ topic msg _p = do
      case parse (Data.ByteString.Lazy.toStrict msg) of
        Left err -> do
          case parseError (Data.ByteString.Lazy.toStrict msg) of
            Left _  -> hPutStrLn stderr $ "Invalid JSON, error: " ++ err
            Right e -> atomically
              $ writeTChan chan
              $ ClientError
              $ Data.Text.unpack
              $ errorMsg e

        Right x -> do
          atomically $ writeTChan chan $ Event (parseType topic) x

withTTN :: (Event -> IO a) -> IO b
withTTN act = do
  c <- newTChanIO
  void $ async $ forever $ do
    msg <- atomically $ readTChan c
    act msg

  forever $ catches (ttnClient c)
    [ Handler (\(ex :: MQTTException) -> handler (show ex))
    , Handler (\(ex :: IOException) -> handler (show ex)) ]

  where
    handler e = putStrLn ("ERROR: " <> e) >> threadDelay 1000000