packages feed

mellon-web-0.8.0.2: examples/ScheduleUnlock.hs

-- | This is a one-shot program which, when run, checks the specified
-- unlock window against the current time. If the current time is
-- within the window, the program unlocks the specified Mellon
-- controller until the end of the window. If the current time is
-- outside the window, the program locks the controller.
--
-- The program is intended to be run from a cron job which runs once a
-- day at the start of the window, therefore implementing a very
-- simple, "unlock this door every day from time X until time Y"
-- system. Because it checks the current time against the unlock
-- window, it can also be run at intervals (e.g., once a minute) or
-- @reboot to make the system slightly more robust.

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

module Main where

import Control.Monad.Catch.Pure (runCatch)
import Data.ByteString.Char8 as C8 (unpack)
import Data.Monoid ((<>))
import Data.Time.Clock
       (NominalDiffTime, UTCTime(..), addUTCTime)
import qualified Data.Time.LocalTime as Time
       (LocalTime(..), TimeOfDay(..), ZonedTime(..), getZonedTime,
        zonedTimeToUTC)
import Mellon.Web.Client (State(..), putState)
import Network.HTTP.Client (newManager)
import Network.HTTP.Client.TLS (tlsManagerSettings)
import Network.HTTP.Types (Status(..))
import Options.Applicative
import Servant.Client
       (BaseUrl, ClientEnv(..), ServantError(..), parseBaseUrl,
        runClientM)
import System.Exit (ExitCode(..), exitWith)

data GlobalOptions =
  GlobalOptions {_url :: !BaseUrl
                ,_cmd :: !Command}

data Command
  = LocalTime LocalTimeOptions

data LocalTimeOptions =
  LocalTimeOptions {_localTimeStart :: !Time.TimeOfDay
                   ,_localTimeEnd :: !Time.TimeOfDay}

localTimeCmd :: Parser Command
localTimeCmd = LocalTime <$> localTimeOptions

localTimeOptions :: Parser LocalTimeOptions
localTimeOptions =
  LocalTimeOptions <$>
  argument auto (metavar "HH:MM:SS" <>
                 help "Unlock window start time (local time, 24H format)") <*>
  argument auto (metavar "HH:MM:SS" <>
                 help "Unlock window end time (local time, 24H format)")

parseServiceUrl :: String -> ReadM BaseUrl
parseServiceUrl s =
  case runCatch $ parseBaseUrl s of
    Left _ -> readerError $ "Invalid service URL: " ++ s
    Right url -> return url

cmds :: Parser GlobalOptions
cmds =
  GlobalOptions <$>
  argument (str >>= parseServiceUrl)
           (metavar "URL" <>
            help "Mellon server base URL") <*>
  hsubparser
    (command "localtime" (info localTimeCmd (progDesc "Unlock window specified in local time")))

oneDay :: NominalDiffTime
oneDay = 60 * 60 * 24

-- | Convert a 'TimeOfDay' to a 'UTCTime', using the given 'ZonedTime'
-- as the point of reference; i.e., the 'TimeOfDay' is relative to the
-- 'ZonedTime''s (local) day.
timeOfDayToUTC :: Time.TimeOfDay -> Time.ZonedTime -> UTCTime
timeOfDayToUTC tod zt =
  let localDay = Time.localDay $ Time.zonedTimeToLocalTime zt
      localTz = Time.zonedTimeZone zt
      localTime = Time.LocalTime localDay tod
  in Time.zonedTimeToUTC $ Time.ZonedTime localTime localTz

run :: GlobalOptions -> IO ()
run (GlobalOptions baseUrl (LocalTime (LocalTimeOptions localStart localEnd))) =
  do zonedNow <- Time.getZonedTime
     let utcStart = timeOfDayToUTC localStart zonedNow
         adjustEnd =
           if localStart > localEnd
              then oneDay
              else 0
         utcEnd = addUTCTime adjustEnd $ timeOfDayToUTC localEnd zonedNow
         utcNow = Time.zonedTimeToUTC zonedNow
     go utcStart utcEnd utcNow baseUrl
  where
    go :: UTCTime -> UTCTime -> UTCTime -> BaseUrl -> IO ()
    go start end now url =
      let state = if now >= start && now < end
                     then Unlocked end
                     else Locked
      in do manager <- newManager tlsManagerSettings
            let clientEnv = ClientEnv manager url
            runClientM (putState state) clientEnv >>= \case
                        Right status ->
                          do putStrLn $ show status
                             exitWith ExitSuccess
                        Left e ->
                          do putStrLn $ "Mellon service error: " ++ prettyServantError e
                             exitWith $ ExitFailure 1
    prettyServantError :: ServantError -> String
    prettyServantError (FailureResponse _ status _ _) =
      show (statusCode status) ++ " " ++ (C8.unpack $ statusMessage status)
    prettyServantError (DecodeFailure _ _ _) =
      "decode failure"
    prettyServantError (UnsupportedContentType _ _) =
      "unsupported content type"
    prettyServantError (InvalidContentTypeHeader _ _) =
      "invalid content type header"
    prettyServantError (ConnectionError _) =
      "connection refused"

main :: IO ()
main = execParser opts >>= run
  where opts =
          info (helper <*> cmds)
               (fullDesc <>
                progDesc "Unlock a mellon controller within a specified time window" <>
                header "mellon-schedule-unlock")