packages feed

tesla 0.1.0.1 → 0.2.0.0

raw patch · 37 files changed

+712/−521 lines, 37 filesdep +exceptionsdep +monad-loggerdep +unliftio-corePVP ok

version bump matches the API change (PVP)

Dependencies added: exceptions, monad-logger, unliftio-core

API changes (from Hackage documentation)

- Tesla: AuthInfo :: String -> String -> String -> String -> String -> AuthInfo
- Tesla: [_bearerToken] :: AuthInfo -> String
- Tesla: [_clientID] :: AuthInfo -> String
- Tesla: [_clientSecret] :: AuthInfo -> String
- Tesla: [_email] :: AuthInfo -> String
- Tesla: [_password] :: AuthInfo -> String
- Tesla: data AuthInfo
- Tesla: instance Data.Aeson.Types.FromJSON.FromJSON Tesla.AuthResponse
- Tesla: instance GHC.Generics.Generic Tesla.AuthResponse
- Tesla: instance GHC.Show.Show Tesla.AuthInfo
- Tesla: instance GHC.Show.Show Tesla.AuthResponse
- Tesla.Car: authInfo :: MonadIO m => Car m AuthInfo
- Tesla.Car: type Car = ReaderT CarEnv
- Tesla.Car: vehicleID :: Monad m => Car m VehicleID
- Tesla.Command: instance Network.Wreq.Internal.Types.FormValue GHC.Types.Bool
- Tesla.Command: mkCommand :: String -> String -> Q [Dec]
- Tesla.Command: mkCommands :: [String] -> Q [Dec]
- Tesla.Command: mkNamedCommands :: [(String, String)] -> Q [Dec]
- Tesla.Command: runCmd :: (MonadIO m, Postable p) => String -> p -> Car m CommandResponse
- Tesla.Command: runCmd' :: MonadIO m => String -> Car m CommandResponse
- Tesla.Command: type Car = ReaderT CarEnv
- Tesla.Command: type CommandResponse = Either Text ()
- Tesla.Command.Alerts: flashLights :: forall m. MonadIO m => Car m CommandResponse
- Tesla.Command.Alerts: honkHorn :: forall m. MonadIO m => Car m CommandResponse
- Tesla.Command.Charging: closeChargePort :: forall m. MonadIO m => Car m CommandResponse
- Tesla.Command.Charging: openChargePort :: forall m. MonadIO m => Car m CommandResponse
- Tesla.Command.Charging: setLimit :: MonadIO m => Int -> Car m CommandResponse
- Tesla.Command.Charging: startCharging :: forall m. MonadIO m => Car m CommandResponse
- Tesla.Command.Charging: stopCharging :: forall m. MonadIO m => Car m CommandResponse
- Tesla.Command.Climate: DriverSeat :: Seat
- Tesla.Command.Climate: PassengerSeat :: Seat
- Tesla.Command.Climate: RearCenterSeat :: Seat
- Tesla.Command.Climate: RearLeftSeat :: Seat
- Tesla.Command.Climate: RearRightSeat :: Seat
- Tesla.Command.Climate: data Seat
- Tesla.Command.Climate: heatSeat :: MonadIO m => Seat -> Int -> Car m CommandResponse
- Tesla.Command.Climate: hvacOff :: forall m. MonadIO m => Car m CommandResponse
- Tesla.Command.Climate: hvacOn :: forall m. MonadIO m => Car m CommandResponse
- Tesla.Command.Climate: setTemps :: MonadIO m => (Double, Double) -> Car m CommandResponse
- Tesla.Command.Climate: wakeUp :: forall m. MonadIO m => Car m CommandResponse
- Tesla.Command.Climate: wheelHeater :: MonadIO m => Bool -> Car m CommandResponse
- Tesla.Command.Climate: wheelHeaterOff :: MonadIO m => Car m CommandResponse
- Tesla.Command.Climate: wheelHeaterOn :: MonadIO m => Car m CommandResponse
- Tesla.Command.Doors: actuateFrontTrunk :: MonadIO m => Car m CommandResponse
- Tesla.Command.Doors: actuateRearTrunk :: MonadIO m => Car m CommandResponse
- Tesla.Command.Homelink: trigger :: MonadIO m => (Double, Double) -> Car m CommandResponse
- Tesla.Command.Media: nextFav :: forall m. MonadIO m => Car m CommandResponse
- Tesla.Command.Media: nextTrack :: forall m. MonadIO m => Car m CommandResponse
- Tesla.Command.Media: prevFav :: forall m. MonadIO m => Car m CommandResponse
- Tesla.Command.Media: prevTrack :: forall m. MonadIO m => Car m CommandResponse
- Tesla.Command.Media: togglePlayback :: forall m. MonadIO m => Car m CommandResponse
- Tesla.Command.Media: volumeDown :: forall m. MonadIO m => Car m CommandResponse
- Tesla.Command.Media: volumeUp :: forall m. MonadIO m => Car m CommandResponse
- Tesla.Command.RemoteStart: remoteStart :: MonadIO m => String -> Car m CommandResponse
- Tesla.Command.Sentry: setSentryMode :: MonadIO m => Bool -> Car m CommandResponse
- Tesla.Command.Sharing: share :: MonadIO m => Text -> Car m CommandResponse
- Tesla.Command.Software: cancelUpdate :: forall m. MonadIO m => Car m CommandResponse
- Tesla.Command.Software: scheduleUpdate :: MonadIO m => Int -> Car m CommandResponse
- Tesla.Command.SpeedLimit: activateSpeedLimit :: MonadIO m => Int -> Car m CommandResponse
- Tesla.Command.SpeedLimit: clearSpeedLimitPIN :: MonadIO m => Int -> Car m CommandResponse
- Tesla.Command.SpeedLimit: deactivateSpeedLimit :: MonadIO m => Int -> Car m CommandResponse
- Tesla.Command.SpeedLimit: speedLimit :: MonadIO m => Int -> Car m CommandResponse
- Tesla.Command.Valet: clearValetPIN :: forall m. MonadIO m => Car m CommandResponse
- Tesla.Command.Valet: setValetMode :: MonadIO m => Bool -> Int -> Car m CommandResponse
- Tesla.Command.Windows: closeSunroof :: MonadIO m => Car m CommandResponse
- Tesla.Command.Windows: closeWindows :: MonadIO m => (Double, Double) -> Car m CommandResponse
- Tesla.Command.Windows: ventSunroof :: MonadIO m => Car m CommandResponse
- Tesla.Command.Windows: ventWindows :: MonadIO m => Car m CommandResponse
+ Tesla: ProductEnergy :: EnergyID -> Product
+ Tesla: ProductPowerWall :: Product
+ Tesla: ProductVehicle :: Text -> VehicleID -> Product
+ Tesla: [_energyID] :: Product -> EnergyID
+ Tesla: [_vehicleID] :: Product -> VehicleID
+ Tesla: [_vehicleName] :: Product -> Text
+ Tesla: _ProductEnergy :: Prism' Product EnergyID
+ Tesla: _ProductPowerWall :: Prism' Product ()
+ Tesla: _ProductVehicle :: Prism' Product (Text, VehicleID)
+ Tesla: data Product
+ Tesla: decodeProducts :: Value -> [Product]
+ Tesla: energyID :: Traversal' Product EnergyID
+ Tesla: energyIDs :: MonadIO m => AuthInfo -> m [EnergyID]
+ Tesla: instance GHC.Classes.Eq Tesla.Product
+ Tesla: instance GHC.Read.Read Tesla.Product
+ Tesla: instance GHC.Show.Show Tesla.Product
+ Tesla: products :: MonadIO m => AuthInfo -> m [Product]
+ Tesla: type EnergyID = Integer
+ Tesla: type VehicleID = Text
+ Tesla: vehicleID :: Traversal' Product VehicleID
+ Tesla: vehicleName :: Traversal' Product Text
+ Tesla.Auth: AuthInfo :: String -> String -> String -> String -> String -> AuthInfo
+ Tesla.Auth: AuthResponse :: String -> Int -> String -> AuthResponse
+ Tesla.Auth: [_access_token] :: AuthResponse -> String
+ Tesla.Auth: [_bearerToken] :: AuthInfo -> String
+ Tesla.Auth: [_clientID] :: AuthInfo -> String
+ Tesla.Auth: [_clientSecret] :: AuthInfo -> String
+ Tesla.Auth: [_email] :: AuthInfo -> String
+ Tesla.Auth: [_expires_in] :: AuthResponse -> Int
+ Tesla.Auth: [_password] :: AuthInfo -> String
+ Tesla.Auth: [_refresh_token] :: AuthResponse -> String
+ Tesla.Auth: access_token :: Lens' AuthResponse String
+ Tesla.Auth: bearerToken :: Lens' AuthInfo String
+ Tesla.Auth: class Monad m => HasTeslaAuth m
+ Tesla.Auth: clientID :: Lens' AuthInfo String
+ Tesla.Auth: clientSecret :: Lens' AuthInfo String
+ Tesla.Auth: data AuthInfo
+ Tesla.Auth: data AuthResponse
+ Tesla.Auth: email :: Lens' AuthInfo String
+ Tesla.Auth: expires_in :: Lens' AuthResponse Int
+ Tesla.Auth: fromToken :: String -> AuthInfo
+ Tesla.Auth: instance Data.Aeson.Types.FromJSON.FromJSON Tesla.Auth.AuthResponse
+ Tesla.Auth: instance GHC.Generics.Generic Tesla.Auth.AuthResponse
+ Tesla.Auth: instance GHC.Show.Show Tesla.Auth.AuthInfo
+ Tesla.Auth: instance GHC.Show.Show Tesla.Auth.AuthResponse
+ Tesla.Auth: password :: Lens' AuthInfo String
+ Tesla.Auth: refresh_token :: Lens' AuthResponse String
+ Tesla.Auth: teslaAuth :: HasTeslaAuth m => m AuthInfo
+ Tesla.Car: _Closed :: forall a_ayQK. Prism' (OpenState a_ayQK) a_ayQK
+ Tesla.Car: _Open :: forall a_ayQK. Prism' (OpenState a_ayQK) a_ayQK
+ Tesla.Car: currentVehicleID :: MonadReader CarEnv m => m VehicleID
+ Tesla.Car: data Car m a
+ Tesla.Car: instance (GHC.Base.Monad m, Control.Monad.IO.Class.MonadIO m, Control.Monad.Reader.Class.MonadReader Tesla.Car.CarEnv m) => Tesla.Auth.HasTeslaAuth m
+ Tesla.Car: instance Control.Monad.Catch.MonadCatch m => Control.Monad.Catch.MonadCatch (Tesla.Car.Car m)
+ Tesla.Car: instance Control.Monad.Catch.MonadMask m => Control.Monad.Catch.MonadMask (Tesla.Car.Car m)
+ Tesla.Car: instance Control.Monad.Catch.MonadThrow m => Control.Monad.Catch.MonadThrow (Tesla.Car.Car m)
+ Tesla.Car: instance Control.Monad.Fail.MonadFail m => Control.Monad.Fail.MonadFail (Tesla.Car.Car m)
+ Tesla.Car: instance Control.Monad.IO.Class.MonadIO m => Control.Monad.IO.Class.MonadIO (Tesla.Car.Car m)
+ Tesla.Car: instance Control.Monad.IO.Unlift.MonadUnliftIO m => Control.Monad.IO.Unlift.MonadUnliftIO (Tesla.Car.Car m)
+ Tesla.Car: instance Control.Monad.Logger.MonadLogger m => Control.Monad.Logger.MonadLogger (Tesla.Car.Car m)
+ Tesla.Car: instance GHC.Base.Applicative m => GHC.Base.Applicative (Tesla.Car.Car m)
+ Tesla.Car: instance GHC.Base.Functor m => GHC.Base.Functor (Tesla.Car.Car m)
+ Tesla.Car: instance GHC.Base.Monad m => Control.Monad.Reader.Class.MonadReader Tesla.Car.CarEnv (Tesla.Car.Car m)
+ Tesla.Car: instance GHC.Base.Monad m => GHC.Base.Monad (Tesla.Car.Car m)
+ Tesla.Car.Command: data Car m a
+ Tesla.Car.Command: instance Network.Wreq.Internal.Types.FormValue GHC.Types.Bool
+ Tesla.Car.Command: mkCommand :: String -> String -> Q [Dec]
+ Tesla.Car.Command: mkCommands :: [String] -> Q [Dec]
+ Tesla.Car.Command: mkNamedCommands :: [(String, String)] -> Q [Dec]
+ Tesla.Car.Command: runCmd :: (MonadIO m, Postable p) => String -> p -> Car m CommandResponse
+ Tesla.Car.Command: runCmd' :: MonadIO m => String -> Car m CommandResponse
+ Tesla.Car.Command: type CommandResponse = Either Text ()
+ Tesla.Car.Command.Alerts: flashLights :: forall m. MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Alerts: honkHorn :: forall m. MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Charging: closeChargePort :: forall m. MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Charging: openChargePort :: forall m. MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Charging: setLimit :: MonadIO m => Int -> Car m CommandResponse
+ Tesla.Car.Command.Charging: startCharging :: forall m. MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Charging: stopCharging :: forall m. MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Climate: DriverSeat :: Seat
+ Tesla.Car.Command.Climate: PassengerSeat :: Seat
+ Tesla.Car.Command.Climate: RearCenterSeat :: Seat
+ Tesla.Car.Command.Climate: RearLeftSeat :: Seat
+ Tesla.Car.Command.Climate: RearRightSeat :: Seat
+ Tesla.Car.Command.Climate: data Seat
+ Tesla.Car.Command.Climate: heatSeat :: MonadIO m => Seat -> Int -> Car m CommandResponse
+ Tesla.Car.Command.Climate: hvacOff :: forall m. MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Climate: hvacOn :: forall m. MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Climate: setTemps :: MonadIO m => (Double, Double) -> Car m CommandResponse
+ Tesla.Car.Command.Climate: wakeUp :: forall m. MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Climate: wheelHeater :: MonadIO m => Bool -> Car m CommandResponse
+ Tesla.Car.Command.Climate: wheelHeaterOff :: MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Climate: wheelHeaterOn :: MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Doors: actuateFrontTrunk :: MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Doors: actuateRearTrunk :: MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Doors: lockDoors :: forall m. MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Doors: unlockDoors :: forall m. MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Homelink: trigger :: MonadIO m => (Double, Double) -> Car m CommandResponse
+ Tesla.Car.Command.Media: nextFav :: forall m. MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Media: nextTrack :: forall m. MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Media: prevFav :: forall m. MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Media: prevTrack :: forall m. MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Media: togglePlayback :: forall m. MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Media: volumeDown :: forall m. MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Media: volumeUp :: forall m. MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.RemoteStart: remoteStart :: MonadIO m => String -> Car m CommandResponse
+ Tesla.Car.Command.Sentry: setSentryMode :: MonadIO m => Bool -> Car m CommandResponse
+ Tesla.Car.Command.Sharing: share :: MonadIO m => Text -> Car m CommandResponse
+ Tesla.Car.Command.Software: cancelUpdate :: forall m. MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Software: scheduleUpdate :: MonadIO m => Int -> Car m CommandResponse
+ Tesla.Car.Command.SpeedLimit: activateSpeedLimit :: MonadIO m => Int -> Car m CommandResponse
+ Tesla.Car.Command.SpeedLimit: clearSpeedLimitPIN :: MonadIO m => Int -> Car m CommandResponse
+ Tesla.Car.Command.SpeedLimit: deactivateSpeedLimit :: MonadIO m => Int -> Car m CommandResponse
+ Tesla.Car.Command.SpeedLimit: speedLimit :: MonadIO m => Int -> Car m CommandResponse
+ Tesla.Car.Command.Valet: clearValetPIN :: forall m. MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Valet: setValetMode :: MonadIO m => Bool -> Int -> Car m CommandResponse
+ Tesla.Car.Command.Windows: closeSunroof :: MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Windows: closeWindows :: MonadIO m => (Double, Double) -> Car m CommandResponse
+ Tesla.Car.Command.Windows: ventSunroof :: MonadIO m => Car m CommandResponse
+ Tesla.Car.Command.Windows: ventWindows :: MonadIO m => Car m CommandResponse
+ Tesla.Energy: data Energy m a
+ Tesla.Energy: instance (GHC.Base.Monad m, Control.Monad.IO.Class.MonadIO m, Control.Monad.Reader.Class.MonadReader Tesla.Energy.EnergyEnv m) => Tesla.Auth.HasTeslaAuth m
+ Tesla.Energy: instance Control.Monad.Catch.MonadCatch m => Control.Monad.Catch.MonadCatch (Tesla.Energy.Energy m)
+ Tesla.Energy: instance Control.Monad.Catch.MonadMask m => Control.Monad.Catch.MonadMask (Tesla.Energy.Energy m)
+ Tesla.Energy: instance Control.Monad.Catch.MonadThrow m => Control.Monad.Catch.MonadThrow (Tesla.Energy.Energy m)
+ Tesla.Energy: instance Control.Monad.Fail.MonadFail m => Control.Monad.Fail.MonadFail (Tesla.Energy.Energy m)
+ Tesla.Energy: instance Control.Monad.IO.Class.MonadIO m => Control.Monad.IO.Class.MonadIO (Tesla.Energy.Energy m)
+ Tesla.Energy: instance Control.Monad.IO.Unlift.MonadUnliftIO m => Control.Monad.IO.Unlift.MonadUnliftIO (Tesla.Energy.Energy m)
+ Tesla.Energy: instance Control.Monad.Logger.MonadLogger m => Control.Monad.Logger.MonadLogger (Tesla.Energy.Energy m)
+ Tesla.Energy: instance GHC.Base.Applicative m => GHC.Base.Applicative (Tesla.Energy.Energy m)
+ Tesla.Energy: instance GHC.Base.Functor m => GHC.Base.Functor (Tesla.Energy.Energy m)
+ Tesla.Energy: instance GHC.Base.Monad m => Control.Monad.Reader.Class.MonadReader Tesla.Energy.EnergyEnv (Tesla.Energy.Energy m)
+ Tesla.Energy: instance GHC.Base.Monad m => GHC.Base.Monad (Tesla.Energy.Energy m)
+ Tesla.Energy: instance GHC.Classes.Eq Tesla.Energy.BadEnergyException
+ Tesla.Energy: instance GHC.Exception.Type.Exception Tesla.Energy.BadEnergyException
+ Tesla.Energy: instance GHC.Show.Show Tesla.Energy.BadEnergyException
+ Tesla.Energy: runEnergy :: MonadIO m => IO AuthInfo -> EnergyID -> Energy m a -> m a
+ Tesla.Energy: siteConfig :: (FromJSON j, MonadIO m) => Energy m j
+ Tesla.Energy: siteData :: (FromJSON j, MonadIO m) => Energy m j

Files

src/Tesla.hs view
@@ -6,105 +6,93 @@ documented at https://tesla-api.timdorr.com/ -} -{-# LANGUAGE DeriveGeneric     #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards   #-}+{-# LANGUAGE TemplateHaskell   #-}+{-# LANGUAGE TupleSections     #-}  module Tesla     ( authenticate, refreshAuth, AuthResponse(..),-      AuthInfo(..),-      vehicles,-      fromToken, authOpts, baseURL+      Product(..), vehicleName, vehicleID, energyID, _ProductVehicle, _ProductEnergy, _ProductPowerWall,+      VehicleID, vehicles, products,+      EnergyID, energyIDs,+      fromToken, authOpts, baseURL,+      decodeProducts     ) where   import           Control.Lens import           Control.Monad.IO.Class (MonadIO (..))-import           Data.Aeson             (FromJSON (..), Options (..),-                                         Value (..), defaultOptions,-                                         fieldLabelModifier, genericParseJSON)-import           Data.Aeson.Lens        (key, values, _String)-import qualified Data.ByteString.Char8  as BC+import           Data.Aeson             (Value (..))+import           Data.Aeson.Lens        (key, _Array, _Integer, _String)+import           Data.Foldable          (asum) import           Data.Map.Strict        (Map) import qualified Data.Map.Strict        as Map+import           Data.Maybe             (catMaybes) import           Data.Text              (Text)-import           Generics.Deriving.Base (Generic)-import           Network.Wreq           (FormParam (..), Options, Response,-                                         asJSON, defaults, getWith, header,-                                         postWith, responseBody)+import           Network.Wreq           (FormParam (..)) +import           Tesla.Auth+import           Tesla.Internal.HTTP+ baseURL :: String baseURL = "https://owner-api.teslamotors.com/" authURL :: String authURL = baseURL <> "oauth/token" authRefreshURL :: String authRefreshURL = baseURL <> "oauth/token"-vehiclesURL :: String-vehiclesURL = baseURL <> "api/1/vehicles"+productsURL :: String+productsURL = baseURL <> "api/1/products"  -userAgent :: BC.ByteString-userAgent = "github.com/dustin/tesla 0.1"+-- | Authenticate to the Tesla service.+authenticate :: AuthInfo -> IO AuthResponse+authenticate AuthInfo{..} =+  jpostWith defOpts authURL ["grant_type" := ("password" :: String),+                             "client_id" := _clientID,+                             "client_secret" := _clientSecret,+                             "email" := _email,+                             "password" := _password] -defOpts :: Network.Wreq.Options-defOpts = defaults & header "User-Agent" .~ [userAgent]+-- | Refresh authentication credentials using a refresh token.+refreshAuth :: AuthInfo -> AuthResponse -> IO AuthResponse+refreshAuth AuthInfo{..} AuthResponse{..} =+  jpostWith defOpts authRefreshURL ["grant_type" := ("refresh_token" :: String),+                                    "client_id" := _clientID,+                                    "client_secret" := _clientSecret,+                                    "refresh_token" := _refresh_token] --- | An Authentication request.-data AuthInfo = AuthInfo {-  _clientID       :: String-  , _clientSecret :: String-  , _email        :: String-  , _password     :: String-  , _bearerToken  :: String-  } deriving(Show) --- | Get an AuthInfo instance from a bearer token.-fromToken :: String -> AuthInfo-fromToken t = AuthInfo{_bearerToken=t, _clientID="", _clientSecret="", _email="", _password=""}--jsonOpts :: Data.Aeson.Options-jsonOpts = defaultOptions {-  fieldLabelModifier = dropWhile (== '_')-  }---- | An Authentication response.-data AuthResponse = AuthResponse {-  _access_token    :: String-  , _expires_in    :: Int-  , _refresh_token :: String-  } deriving(Generic, Show)+-- | A VehicleID.+type VehicleID = Text -instance FromJSON AuthResponse where-  parseJSON = genericParseJSON jsonOpts+-- | An energy site ID.+type EnergyID = Integer --- | Authenticate to the Tesla service.-authenticate :: AuthInfo -> IO AuthResponse-authenticate AuthInfo{..} = do-  r <- asJSON =<< postWith defOpts authURL ["grant_type" := ("password" :: String),-                                            "client_id" := _clientID,-                                            "client_secret" := _clientSecret,-                                            "email" := _email,-                                            "password" := _password] :: IO (Response AuthResponse)-  pure $ r ^. responseBody+-- | Tesla Product Types.+data Product = ProductVehicle { _vehicleName :: Text, _vehicleID :: VehicleID }+             | ProductEnergy { _energyID :: EnergyID }+             | ProductPowerWall deriving (Show, Read, Eq) --- | Refresh authentication credentials using a refresh token.-refreshAuth :: AuthInfo -> AuthResponse -> IO AuthResponse-refreshAuth AuthInfo{..} AuthResponse{..} = do-  r <- asJSON =<< postWith defOpts authRefreshURL ["grant_type" := ("refresh_token" :: String),-                                                   "client_id" := _clientID,-                                                   "client_secret" := _clientSecret,-                                                   "refresh_token" := _refresh_token] :: IO (Response AuthResponse)-  pure $ r ^. responseBody+makePrisms ''Product+makeLenses ''Product +decodeProducts :: Value -> [Product]+decodeProducts = catMaybes . toListOf (key "response" . _Array . folded . to prod)+  where+    prod o = asum [ prodCar, prodSolar, Nothing ]+      where+        prodCar = ProductVehicle <$> (o ^? key "display_name" . _String) <*> (o ^? key "id_s" . _String)+        prodSolar = ProductEnergy <$> (o ^? key "energy_site_id" . _Integer) --- | Get a set of wreq options from an 'AuthInfo'.-authOpts :: AuthInfo -> Network.Wreq.Options-authOpts AuthInfo{..} = defOpts & header "Authorization" .~ ["Bearer " <> BC.pack _bearerToken]+-- | Get all products associated with this account.+products :: MonadIO m => AuthInfo -> m [Product]+products ai = decodeProducts <$> jgetWith (authOpts ai) productsURL  -- | Get a mapping of vehicle name to vehicle ID. vehicles :: MonadIO m => AuthInfo -> m (Map Text Text)-vehicles ai = do-  r <- liftIO (asJSON =<< getWith (authOpts ai) vehiclesURL :: IO (Response Value))-  let vals = r ^.. responseBody . key "response" . values . key "id_s" . _String-      keys = r ^.. responseBody . key "response" . values . key "display_name" . _String-  pure (Map.fromList $ zip keys vals)+vehicles = fmap (Map.fromList . toListOf (folded . _ProductVehicle)) . products++-- | Get a list of Solar ID installations.+energyIDs :: MonadIO m => AuthInfo -> m [EnergyID]+energyIDs = fmap (toListOf $ folded . _ProductEnergy) . products
+ src/Tesla/Auth.hs view
@@ -0,0 +1,58 @@+{-|+Module:      Tesla.Auth+Description: Tesla Authentication structures.++Authentication related data structures.+-}++{-# LANGUAGE DeriveGeneric     #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell   #-}++module Tesla.Auth (AuthInfo(..),+                   clientID, clientSecret, email, password, bearerToken,+                   fromToken,+                   AuthResponse(..), access_token, expires_in, refresh_token,+                   HasTeslaAuth(..)+                  ) where++import           Control.Lens+import           Data.Aeson             (FromJSON (..), Options, defaultOptions, fieldLabelModifier, genericParseJSON)+import           Generics.Deriving.Base (Generic)++-- | An Authentication request.+data AuthInfo = AuthInfo {+  _clientID       :: String+  , _clientSecret :: String+  , _email        :: String+  , _password     :: String+  , _bearerToken  :: String+  } deriving(Show)++makeLenses ''AuthInfo++-- | Get an AuthInfo instance from a bearer token.+fromToken :: String -> AuthInfo+fromToken t = AuthInfo{_bearerToken=t, _clientID="", _clientSecret="", _email="", _password=""}++jsonOpts :: Data.Aeson.Options+jsonOpts = defaultOptions {+  fieldLabelModifier = dropWhile (== '_')+  }++-- | An Authentication response.+data AuthResponse = AuthResponse {+  _access_token    :: String+  , _expires_in    :: Int+  , _refresh_token :: String+  } deriving(Generic, Show)++makeLenses ''AuthResponse++-- | A Monad may have a 'HasTeslaAuth' instance to indicate it knows+-- how to authenticate against the Tesla service.+class Monad m => HasTeslaAuth m where+  teslaAuth :: m AuthInfo++instance FromJSON AuthResponse where+  parseJSON = genericParseJSON jsonOpts
src/Tesla/Car.hs view
@@ -5,13 +5,18 @@ Access of car-specific APIs. -} -{-# LANGUAGE DeriveGeneric          #-}-{-# LANGUAGE DuplicateRecordFields  #-}-{-# LANGUAGE FunctionalDependencies #-}-{-# LANGUAGE MultiParamTypeClasses  #-}-{-# LANGUAGE OverloadedStrings      #-}-{-# LANGUAGE RecordWildCards        #-}-{-# LANGUAGE TemplateHaskell        #-}+{-# OPTIONS_GHC -Wno-orphans #-}+{-# LANGUAGE DeriveGeneric              #-}+{-# LANGUAGE DuplicateRecordFields      #-}+{-# LANGUAGE FlexibleContexts           #-}+{-# LANGUAGE FlexibleInstances          #-}+{-# LANGUAGE FunctionalDependencies     #-}+{-# LANGUAGE GeneralisedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings          #-}+{-# LANGUAGE RecordWildCards            #-}+{-# LANGUAGE ScopedTypeVariables        #-}+{-# LANGUAGE TemplateHaskell            #-}+{-# LANGUAGE UndecidableInstances       #-}  module Tesla.Car (   -- * Car Monad and related types.@@ -21,7 +26,7 @@   vehicleData, nearbyChargers,   -- * Convenience functions for examining VehicleData   VehicleData, isUserPresent, isCharging, teslaTS, maybeTeslaTS,-  Door(..), OpenState(..), doors, openDoors,+  Door(..), OpenState(..), _Open, _Closed, doors, openDoors,   -- * Charger Info   Location(..), DestinationCharger(..), Supercharger(..), Charger(..),   superchargers, destinationChargers,@@ -29,60 +34,68 @@   lat, lon, _SC, _DC,   name, location, distance_miles, available_stalls, total_stalls, site_closed,   -- * Probably uninteresting internals-  vehicleURL, authInfo, vehicleID+  vehicleURL, currentVehicleID       ) where -import           Control.Exception      (Exception, throwIO)+import           Control.Exception       (Exception, throwIO) import           Control.Lens-import           Control.Monad          ((<=<))-import           Control.Monad.IO.Class (MonadIO (..))-import           Control.Monad.Reader   (ReaderT (..), asks, runReaderT)-import           Data.Aeson             (FromJSON (..), Options (..),-                                         Result (..), Value (..), decode,-                                         defaultOptions, fieldLabelModifier,-                                         fromJSON, genericParseJSON, withObject,-                                         (.:))-import           Data.Aeson.Lens        (key, _Array, _Bool, _Integer)-import qualified Data.ByteString.Lazy   as BL-import qualified Data.Map.Strict        as Map-import           Data.Maybe             (fromJust, fromMaybe)+import           Control.Monad           ((<=<))+import           Control.Monad.Catch     (MonadCatch (..), MonadMask (..), MonadThrow (..))+import           Control.Monad.Fail      (MonadFail (..))+import           Control.Monad.IO.Class  (MonadIO (..))+import           Control.Monad.IO.Unlift (MonadUnliftIO, withRunInIO)+import           Control.Monad.Logger    (MonadLogger)+import           Control.Monad.Reader    (MonadReader, ReaderT (..), asks, runReaderT)+import           Data.Aeson              (FromJSON (..), Options (..), Result (..), Value (..), decode, defaultOptions,+                                          fieldLabelModifier, fromJSON, genericParseJSON, withObject, (.:))+import           Data.Aeson.Lens         (key, values, _Bool, _Integer)+import qualified Data.ByteString.Lazy    as BL+import qualified Data.Map.Strict         as Map+import           Data.Maybe              (fromJust, fromMaybe) import           Data.Ratio-import           Data.Text              (Text, unpack)-import           Data.Time.Clock        (UTCTime)-import           Data.Time.Clock.POSIX  (posixSecondsToUTCTime)-import qualified Data.Vector            as V-import           Generics.Deriving.Base (Generic)-import           Network.Wreq           (Response, asJSON, getWith,-                                         responseBody)+import           Data.Text               (Text, unpack)+import           Data.Time.Clock         (UTCTime)+import           Data.Time.Clock.POSIX   (posixSecondsToUTCTime)+import           Generics.Deriving.Base  (Generic)+import           Network.Wreq            (getWith, responseBody)  import           Tesla+import           Tesla.Auth+import           Tesla.Internal.HTTP  -- | Get the URL for a named endpoint for a given vehicle. vehicleURL :: VehicleID -> String -> String vehicleURL v c = mconcat [baseURL, "api/1/vehicles/", unpack v, "/", c] --- | A VehicleID.-type VehicleID = Text- data CarEnv = CarEnv {   _authInfo :: IO AuthInfo,   _vid      :: VehicleID   } --- | Get authInfo from the Car Monad.-authInfo :: MonadIO m => Car m AuthInfo-authInfo = liftIO =<< asks _authInfo- -- | Get the current vehicle ID from the Car Monad.-vehicleID :: Monad m => Car m VehicleID-vehicleID = asks _vid+currentVehicleID :: MonadReader CarEnv m => m VehicleID+currentVehicleID = asks _vid  -- | Car Monad for accessing car-specific things.-type Car = ReaderT CarEnv+newtype Car m a = Car { runCarM :: ReaderT CarEnv m a }+  deriving (Applicative, Functor, Monad, MonadIO,+            MonadCatch, MonadThrow, MonadMask, MonadReader CarEnv,+            MonadFail, MonadLogger) +{- solonarv's thing almost works:+deriving newtype instance (MonadUnliftIO m, forall a a'. Coercible a a' => Coercible (m a) (m a')) => MonadUnliftIO (Car m)+-}++instance MonadUnliftIO m => MonadUnliftIO (Car m) where+  -- ((forall a. m a -> IO a) -> IO b) -> m b+  withRunInIO inner = Car $ withRunInIO $ \run -> inner (run . runCarM)++instance (Monad m, MonadIO m, MonadReader CarEnv m) => HasTeslaAuth m where+  teslaAuth = liftIO =<< asks _authInfo+ -- | Run a Car Monad with the given Vehicle ID runCar :: MonadIO m => IO AuthInfo -> VehicleID -> Car m a -> m a-runCar ai vi f = runReaderT f (CarEnv ai vi)+runCar ai vi f = runReaderT (runCarM f) (CarEnv ai vi)  newtype BadCarException = BadCar String deriving Eq @@ -116,8 +129,8 @@ -- | Fetch the VehicleData. vehicleData :: MonadIO m => Car m VehicleData vehicleData = do-  a <- authInfo-  v <- vehicleID+  a <- teslaAuth+  v <- currentVehicleID   r <- liftIO $ getWith (authOpts a) (vehicleURL v "vehicle_data")   pure . fromJust . inner $ r ^. responseBody     where inner = BL.stripPrefix "{\"response\":" <=< BL.stripSuffix "}"@@ -128,19 +141,16 @@  -- | True if a user is present in the vehicle. isUserPresent :: VehicleData -> Bool-isUserPresent b = let mb = maybeVal b ^? _Just . key "vehicle_state" . key "is_user_present" . _Bool in-                    fromMaybe False mb+isUserPresent = fromMaybe False . preview (_Just . key "vehicle_state" . key "is_user_present" . _Bool) . maybeVal  -- | True of the vehicle is currently charging. isCharging :: VehicleData -> Bool-isCharging b = let mi = maybeVal b ^? _Just . key "charge_state" . key "charger_power" . _Integer in-                 fromMaybe 0 mi > 0+isCharging = maybe False (> 0) . preview (_Just . key "charge_state" . key "charger_power" . _Integer) . maybeVal  -- | Get the timestamp from this VehicleData if present. maybeTeslaTS :: VehicleData -> Maybe UTCTime-maybeTeslaTS b = pt <$> mv-  where mv = maybeVal b ^? _Just . key "vehicle_state" . key "timestamp" . _Integer-        pt x = posixSecondsToUTCTime . fromRational $ x % 1000+maybeTeslaTS b = maybeVal b ^? _Just . key "vehicle_state" . key "timestamp" . _Integer . to pt+  where pt x = posixSecondsToUTCTime . fromRational $ x % 1000  -- | Get the timestamp from this VehicleData or error if there isn't one. teslaTS :: VehicleData -> UTCTime@@ -164,15 +174,7 @@ -- 0 or 32 for rt data OpenState a = Closed a | Open a deriving (Show, Eq) --- | True if the given OpenState represents an open door.-isOpen :: OpenState a -> Bool-isOpen (Closed _) = False-isOpen _          = True---- | Return the thing that is open in this OpenState.-fromOpenState :: OpenState a -> a-fromOpenState (Open d)   = d-fromOpenState (Closed d) = d+makePrisms ''OpenState  -- | Return a list of doors and their OpenState. doors :: VehicleData -> Maybe [OpenState Door]@@ -184,7 +186,7 @@  -- | Return a list of open doors. openDoors :: VehicleData -> [Door]-openDoors b = maybe [] (map fromOpenState . filter isOpen) (doors b)+openDoors = toListOf (_Just . folded . _Open) . doors  -- | Location, Location, Location. data Location = Location { _lat :: Double, _lon :: Double } deriving (Show, Generic)@@ -242,15 +244,13 @@ -- | Get the nearby chargers. nearbyChargers :: MonadIO m => Car m [Charger] nearbyChargers = do-  a <- authInfo-  v <- vehicleID-  r <- liftIO (asJSON =<< getWith (authOpts a) (vehicleURL v "nearby_charging_sites") :: IO (Response Value))-  let rb = r ^. responseBody-      chargers = parseOne rb SC "superchargers" <> parseOne rb DC "destination_charging"-  pure (V.toList chargers)+  v <- currentVehicleID+  rb <- jgetAuth (vehicleURL v "nearby_charging_sites")+  pure $ parseOne rb SC "superchargers" <> parseOne rb DC "destination_charging"      where-      parseOne rb f k =  let rs = traverse fromJSON (rb ^. key "response" . key k . _Array) in+      parseOne :: FromJSON a => Value -> (a -> Charger) -> Text -> [Charger]+      parseOne rb f k =  let rs = traverse fromJSON (rb ^.. key "response" . key k . values) in                            f <$> case rs of                                    Error e   -> error e                                    Success s -> s
+ src/Tesla/Car/Command.hs view
@@ -0,0 +1,83 @@+{-# LANGUAGE OverloadedStrings   #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-|+Module      : Tesla.Car.Command+Description : Commands executed on a car.++Executing commands within the Car Monad.+-}+{-# OPTIONS_GHC -Wno-orphans #-}++module Tesla.Car.Command (+  runCmd, runCmd', CommandResponse, Car,+  -- * TH support for generating commands.+  mkCommand, mkCommands, mkNamedCommands) where++import           Control.Lens+import           Control.Monad.IO.Class (MonadIO (..))+import           Data.Aeson+import           Data.Aeson.Lens        (key, _Bool, _String)+import qualified Data.ByteString.Lazy   as BL+import           Data.Text              (Text)+import           Language.Haskell.TH+import           Network.Wreq.Types     (FormValue (..), Postable)+import           Text.Casing            (fromSnake, toCamel)++import           Tesla.Car+import           Tesla.Internal.HTTP++-- | A CommandResponse wraps an Either such that Left represents a+-- failure message and Right suggests the command was successful.+type CommandResponse = Either Text ()++-- | Run a command with a payload.+runCmd :: (MonadIO m, Postable p) => String -> p -> Car m CommandResponse+runCmd cmd p = do+  v <- currentVehicleID+  j :: Value <- jpostAuth (vehicleURL v $ "command/" <> cmd) p+  pure $ case j ^? key "response" . key "result" . _Bool of+    Just True -> Right ()+    _         -> Left $ j ^. key "response" . key "reason" . _String++-- | Run command without a payload+runCmd' :: MonadIO m => String -> Car m CommandResponse+runCmd' cmd = runCmd cmd BL.empty++instance FormValue Bool where+  renderFormValue True  = "true"+  renderFormValue False = "false"++-- | Build a simple named command car that posts to the given named endpoint.+mkCommand :: String -> String -> Q [Dec]+mkCommand s u = do+  let m = mkName "m"+  pure [+    SigD (mkName s) (ForallT [PlainTV m] [AppT (ConT (mkName "MonadIO")) (VarT m)]+                     (AppT (AppT (ConT (mkName "Car")) (VarT m)) (ConT (mkName "CommandResponse")))),+    FunD (mkName s) [Clause [] (NormalB expr) []]]+  where expr = LamE [] (AppE (VarE (mkName "runCmd'")) (LitE (StringL u)))++cmapM :: (Monoid b, Applicative f) => (a -> f b) -> [a] -> f b+cmapM f xs = mconcat <$> traverse f xs++-- | Build a bunch of commands from a list of named endpoints, defining+-- functions by removing the common prefix.+mkCommands :: [String] -> Q [Dec]+mkCommands targets = cmapM easyCMD targets+  where+    prefix = commonPrefix targets+    easyCMD :: String -> Q [Dec]+    easyCMD target = do+      let s = drop (length prefix) target+          mn = (toCamel . fromSnake) s+      mkCommand mn target++    commonPrefix = fmap head . takeWhile (\(x:xs) -> all (== x) xs) . tp+      where+        tp xs+          | any null xs = []+          | otherwise = (head <$> xs) : tp (tail <$> xs)++-- | Make commands with given names.+mkNamedCommands :: [(String, String)] -> Q [Dec]+mkNamedCommands = cmapM (uncurry mkCommand)
+ src/Tesla/Car/Command/Alerts.hs view
@@ -0,0 +1,11 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell   #-}++module Tesla.Car.Command.Alerts (+  honkHorn, flashLights+  ) where++import           Control.Monad.IO.Class (MonadIO (..))+import           Tesla.Car.Command++mkCommands ["honk_horn", "flash_lights"]
+ src/Tesla/Car/Command/Charging.hs view
@@ -0,0 +1,19 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell   #-}++module Tesla.Car.Command.Charging (+  startCharging, stopCharging, setLimit, openChargePort, closeChargePort+  ) where++import           Control.Monad.IO.Class (MonadIO (..))+import           Network.Wreq           (FormParam (..))++import           Tesla.Car.Command++setLimit :: MonadIO m => Int -> Car m CommandResponse+setLimit to = runCmd "set_charge_limit" ["percent" := to ]++mkNamedCommands [("startCharging", "charge_start"),+                 ("stopCharging", "charge_stop"),+                 ("openChargePort", "charge_port_door_open"),+                 ("closeChargePort", "charge_port_door_close")]
+ src/Tesla/Car/Command/Climate.hs view
@@ -0,0 +1,46 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell   #-}++module Tesla.Car.Command.Climate (+  hvacOn, hvacOff, heatSeat, Seat(..),+  setTemps, wheelHeater, wheelHeaterOff, wheelHeaterOn,+  wakeUp+  ) where++import           Control.Monad.IO.Class (MonadIO (..))+import           Network.Wreq           (FormParam (..))++import           Tesla.Car.Command++-- | Turn on the steering wheel heater+wheelHeater :: MonadIO m => Bool -> Car m CommandResponse+wheelHeater on = runCmd "remote_steering_wheel_heater_request" ["on" := on]++wheelHeaterOn :: MonadIO m => Car m CommandResponse+wheelHeaterOn = wheelHeater True++wheelHeaterOff :: MonadIO m => Car m CommandResponse+wheelHeaterOff = wheelHeater False++data Seat = DriverSeat | PassengerSeat | RearLeftSeat | RearCenterSeat | RearRightSeat++-- | Set heating levels for various seats.+heatSeat :: MonadIO m => Seat -> Int -> Car m CommandResponse+heatSeat seat level = runCmd "remote_seat_heater_request" ["heater" := seatNum seat, "level" := level]+  where+    seatNum :: Seat -> Int+    seatNum DriverSeat     = 0+    seatNum PassengerSeat  = 1+    seatNum RearLeftSeat   = 2+    seatNum RearCenterSeat = 4+    seatNum RearRightSeat  = 5++-- | Set the main HVAC temperatures.+setTemps :: MonadIO m => (Double, Double) -> Car m CommandResponse+setTemps (driver, passenger) = runCmd "set_temps" ["driver_temp" := driver, "passenger_temp" := passenger]+++mkNamedCommands [("hvacOn", "auto_conditioning_start"),+                 ("hvacOff", "auto_conditioning_stop"),+                 ("wakeUp", "wake_up")]+
+ src/Tesla/Car/Command/Doors.hs view
@@ -0,0 +1,23 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell   #-}++module Tesla.Car.Command.Doors (+  actuateFrontTrunk, actuateRearTrunk,+  lockDoors, unlockDoors+  ) where++import           Control.Monad.IO.Class (MonadIO (..))+import           Network.Wreq           (FormParam (..))+import           Tesla.Car.Command++atr :: MonadIO m => String -> Car m CommandResponse+atr w = runCmd "actuate_trunk" [ "which_trunk" := w ]++actuateFrontTrunk :: MonadIO m => Car m CommandResponse+actuateFrontTrunk = atr "front"++actuateRearTrunk :: MonadIO m => Car m CommandResponse+actuateRearTrunk = atr "rear"++mkNamedCommands [("lockDoors", "door_lock"),+                 ("unlockDoors", "door_unlock")]
+ src/Tesla/Car/Command/Homelink.hs view
@@ -0,0 +1,11 @@+{-# LANGUAGE OverloadedStrings #-}++module Tesla.Car.Command.Homelink (trigger) where++import           Control.Monad.IO.Class (MonadIO (..))+import           Network.Wreq           (FormParam (..))+import           Tesla.Car.Command++-- | Trigger nearby homelink with the given (lat,lon)+trigger :: MonadIO m => (Double, Double) -> Car m CommandResponse+trigger (lat,lon) = runCmd "flash_lights" ["lat" := lat, "lon" := lon]
+ src/Tesla/Car/Command/Media.hs view
@@ -0,0 +1,18 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell   #-}++module Tesla.Car.Command.Media where++import           Control.Monad.IO.Class (MonadIO (..))++import           Tesla.Car.Command++mkCommands [+  "media_toggle_playback",+  "media_next_track",+  "media_prev_track",+  "media_next_fav",+  "media_prev_fav",+  "media_volume_up",+  "media_volume_down"+  ]
+ src/Tesla/Car/Command/RemoteStart.hs view
@@ -0,0 +1,14 @@+{-# LANGUAGE OverloadedStrings #-}++module Tesla.Car.Command.RemoteStart where++import           Control.Monad.IO.Class (MonadIO (..))+import           Network.Wreq           (FormParam (..))++import           Tesla.Car.Command++-- | Enables keyless driving.+--+-- This requires your account password, so, you know, be careful where you use it.+remoteStart :: MonadIO m => String -> Car m CommandResponse+remoteStart pw = runCmd "remote_start_drive" ["password" := pw]
+ src/Tesla/Car/Command/Sentry.hs view
@@ -0,0 +1,13 @@+{-# LANGUAGE OverloadedStrings #-}++module Tesla.Car.Command.Sentry (+  setSentryMode+  ) where++import           Control.Monad.IO.Class (MonadIO (..))+import           Network.Wreq           (FormParam (..))++import           Tesla.Car.Command++setSentryMode :: MonadIO m => Bool -> Car m CommandResponse+setSentryMode on = runCmd "set_sentry_mode" [ "on" := on ]
+ src/Tesla/Car/Command/Sharing.hs view
@@ -0,0 +1,20 @@+{-# LANGUAGE OverloadedStrings #-}++module Tesla.Car.Command.Sharing (share) where++import           Control.Monad.IO.Class (MonadIO (..))+import           Data.Aeson+import           Data.Text              (Text)+import           Data.Time.Clock.POSIX  (getCurrentTime, utcTimeToPOSIXSeconds)++import           Tesla.Car.Command++share :: MonadIO m => Text -> Car m CommandResponse+share to = do+  now <- fst . properFraction . utcTimeToPOSIXSeconds <$> liftIO getCurrentTime+  runCmd "share" $ object [+    "type" .= ("share_ext_content_raw" :: Text),+    "value" .= object [ "android.intent.extra.TEXT" .= to ],+    "locale" .= ("en-US" :: Text),+    "timestamp_ms" .= (now * (1000::Int))+    ]
+ src/Tesla/Car/Command/Software.hs view
@@ -0,0 +1,19 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell   #-}++module Tesla.Car.Command.Software (+  scheduleUpdate, cancelUpdate+  ) where++import           Control.Monad.IO.Class (MonadIO (..))+import           Network.Wreq           (FormParam (..))++import           Tesla.Car.Command++-- | Schedule a software update in this many seconds.+scheduleUpdate :: MonadIO m => Int -> Car m CommandResponse+scheduleUpdate secs = runCmd "schedule_software_update" ["offset_sec" := secs]++-- | Cancel a scheduled software update.+mkCommand "cancelUpdate" "cancel_software_update"+
+ src/Tesla/Car/Command/SpeedLimit.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE OverloadedStrings #-}++module Tesla.Car.Command.SpeedLimit (+  speedLimit, activateSpeedLimit, deactivateSpeedLimit, clearSpeedLimitPIN+  ) where++import           Control.Monad.IO.Class (MonadIO (..))+import           Network.Wreq           (FormParam (..))+import           Tesla.Car.Command++speedLimit :: MonadIO m => Int -> Car m CommandResponse+speedLimit to = runCmd "speed_limit_set_limit" ["limit_mph" := to ]++speedo :: MonadIO m => String -> Int -> Car m CommandResponse+speedo c pin = runCmd c ["pin" := pin ]++activateSpeedLimit :: MonadIO m => Int -> Car m CommandResponse+activateSpeedLimit = speedo "speed_limit_activate"++deactivateSpeedLimit :: MonadIO m => Int -> Car m CommandResponse+deactivateSpeedLimit = speedo "speed_limit_deactivate"++clearSpeedLimitPIN :: MonadIO m => Int -> Car m CommandResponse+clearSpeedLimitPIN = speedo "speed_limit_clear_pin"
+ src/Tesla/Car/Command/Valet.hs view
@@ -0,0 +1,16 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell   #-}++module Tesla.Car.Command.Valet (+  setValetMode, clearValetPIN+  ) where++import           Control.Monad.IO.Class (MonadIO (..))+import           Network.Wreq           (FormParam (..))++import           Tesla.Car.Command++setValetMode :: MonadIO m => Bool -> Int -> Car m CommandResponse+setValetMode on pin = runCmd "set_valet_mode" [ "on" := on, "password" := pin]++mkCommand "clearValetPIN" "reset_valet_pin"
+ src/Tesla/Car/Command/Windows.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE OverloadedStrings #-}++module Tesla.Car.Command.Windows (+  ventWindows, closeWindows, ventSunroof, closeSunroof+  ) where++import           Control.Monad.IO.Class (MonadIO (..))+import           Data.Text              (Text)+import           Network.Wreq           (FormParam (..))++import           Tesla.Car.Command++windowControl :: MonadIO m => Text -> (Double, Double) -> Car m CommandResponse+windowControl x (lat,lon) = runCmd "window_control" [ "command" := x, "lat" := lat, "lon" := lon]++ventWindows :: MonadIO m => Car m CommandResponse+ventWindows = windowControl "vent" (0,0)++closeWindows :: MonadIO m => (Double, Double) -> Car m CommandResponse+closeWindows = windowControl "close"++sc :: MonadIO m => Text -> Car m CommandResponse+sc c = runCmd "sun_roof_control" [ "state" := c ]++ventSunroof :: MonadIO m => Car m CommandResponse+ventSunroof = sc "vent"++closeSunroof :: MonadIO m => Car m CommandResponse+closeSunroof = sc "close"
+ src/Tesla/Car/Commands.hs view
@@ -0,0 +1,25 @@+{-|+Module:      Tesla.Commands+Description: Convenience module for importing all commands at once.++All of the commands supported by this library.++This is meant to be imported qualified, e.g.:++> import qualified Tesla.Commands as CMD+-}++module Tesla.Car.Commands ( module CMD ) where++import           Tesla.Car.Command.Alerts     as CMD+import           Tesla.Car.Command.Charging   as CMD+import           Tesla.Car.Command.Climate    as CMD+import           Tesla.Car.Command.Doors      as CMD+import           Tesla.Car.Command.Homelink   as CMD+import           Tesla.Car.Command.Media      as CMD+import           Tesla.Car.Command.Sentry     as CMD+import           Tesla.Car.Command.Sharing    as CMD+import           Tesla.Car.Command.Software   as CMD+import           Tesla.Car.Command.SpeedLimit as CMD+import           Tesla.Car.Command.Valet      as CMD+import           Tesla.Car.Command.Windows    as CMD
− src/Tesla/Command.hs
@@ -1,87 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-|-Module      : Tesla.Command-Description : Commands executed on a car.--Executing commands within the Car Monad.--}-{-# OPTIONS_GHC -Wno-orphans #-}--module Tesla.Command (-  runCmd, runCmd', CommandResponse, Car,-  -- * TH support for generating commands.-  mkCommand, mkCommands, mkNamedCommands) where--import           Control.Lens-import           Control.Monad.IO.Class (MonadIO (..))-import           Data.Aeson-import           Data.Aeson.Lens        (key, _Bool, _String)-import qualified Data.ByteString.Lazy   as BL-import           Data.Text              (Text)-import           Language.Haskell.TH-import           Network.Wreq           (Response, asJSON, postWith,-                                         responseBody)-import           Network.Wreq.Types     (FormValue (..), Postable)-import           Text.Casing            (fromSnake, toCamel)---import           Tesla-import           Tesla.Car---- | A CommandResponse wraps an Either such that Left represents a--- failure message and Right suggests the command was successful.-type CommandResponse = Either Text ()---- | Run a command with a payload.-runCmd :: (MonadIO m, Postable p) => String -> p -> Car m CommandResponse-runCmd cmd p = do-  a <- authInfo-  v <- vehicleID-  r <- liftIO (asJSON =<< postWith (authOpts a) (vehicleURL v $ "command/" <> cmd) p :: IO (Response Value))-  pure $ case r ^? responseBody . key "response" . key "result" . _Bool of-    Just True  -> Right ()-    _ -> Left $ r ^. responseBody . key "response" . key "reason" . _String---- | Run command without a payload-runCmd' :: MonadIO m => String -> Car m CommandResponse-runCmd' cmd = runCmd cmd BL.empty--instance FormValue Bool where-  renderFormValue True  = "true"-  renderFormValue False = "false"----- | Build a simple named command car that posts to the given named endpoint.-mkCommand :: String -> String -> Q [Dec]-mkCommand s u = do-  let m = mkName "m"-  pure $ [-    SigD (mkName s) (ForallT [PlainTV m] [AppT (ConT (mkName "MonadIO")) (VarT m)]-                     (AppT (AppT (ConT (mkName "Car")) (VarT m)) (ConT (mkName "CommandResponse")))),-    FunD (mkName s) [Clause [] (NormalB expr) []]]-  where expr = LamE [] (AppE (VarE (mkName "runCmd'")) (LitE (StringL u)))--cmapM :: (Monoid b, Applicative f) => (a -> f b) -> [a] -> f b-cmapM f xs = mconcat <$> traverse f xs---- | Build a bunch of commands from a list of named endpoints, defining--- functions by removing the common prefix.-mkCommands :: [String] -> Q [Dec]-mkCommands targets = cmapM easyCMD targets-  where-    prefix = commonPrefix targets-    easyCMD :: String -> Q [Dec]-    easyCMD target = do-      let s = drop (length prefix) target-          mn = (toCamel . fromSnake) s-      mkCommand mn target--    commonPrefix = fmap head . takeWhile (\(x:xs) -> all (== x) xs) . tp-      where-        tp xs-          | any null xs = []-          | otherwise = (head <$> xs) : tp (tail <$> xs)---- Make commands with given names.-mkNamedCommands :: [(String, String)] -> Q [Dec]-mkNamedCommands = cmapM (uncurry mkCommand)
− src/Tesla/Command/Alerts.hs
@@ -1,11 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell   #-}--module Tesla.Command.Alerts (-  honkHorn, flashLights-  ) where--import           Control.Monad.IO.Class (MonadIO (..))-import           Tesla.Command--mkCommands ["honk_horn", "flash_lights"]
− src/Tesla/Command/Charging.hs
@@ -1,19 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell   #-}--module Tesla.Command.Charging (-  startCharging, stopCharging, setLimit, openChargePort, closeChargePort-  ) where--import           Control.Monad.IO.Class (MonadIO (..))-import           Network.Wreq           (FormParam (..))--import           Tesla.Command--setLimit :: MonadIO m => Int -> Car m CommandResponse-setLimit to = runCmd "set_charge_limit" ["percent" := to ]--mkNamedCommands [("startCharging", "charge_start"),-                 ("stopCharging", "charge_stop"),-                 ("openChargePort", "charge_port_door_open"),-                 ("closeChargePort", "charge_port_door_close")]
− src/Tesla/Command/Climate.hs
@@ -1,46 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell   #-}--module Tesla.Command.Climate (-  hvacOn, hvacOff, heatSeat, Seat(..),-  setTemps, wheelHeater, wheelHeaterOff, wheelHeaterOn,-  wakeUp-  ) where--import           Control.Monad.IO.Class (MonadIO (..))-import           Network.Wreq           (FormParam (..))--import           Tesla.Command---- | Turn on the steering wheel heater-wheelHeater :: MonadIO m => Bool -> Car m CommandResponse-wheelHeater on = runCmd "remote_steering_wheel_heater_request" ["on" := on]--wheelHeaterOn :: MonadIO m => Car m CommandResponse-wheelHeaterOn = wheelHeater True--wheelHeaterOff :: MonadIO m => Car m CommandResponse-wheelHeaterOff = wheelHeater False--data Seat = DriverSeat | PassengerSeat | RearLeftSeat | RearCenterSeat | RearRightSeat---- | Set heating levels for various seats.-heatSeat :: MonadIO m => Seat -> Int -> Car m CommandResponse-heatSeat seat level = runCmd "remote_seat_heater_request" ["heater" := seatNum seat, "level" := level]-  where-    seatNum :: Seat -> Int-    seatNum DriverSeat     = 0-    seatNum PassengerSeat  = 1-    seatNum RearLeftSeat   = 2-    seatNum RearCenterSeat = 4-    seatNum RearRightSeat  = 5---- | Set the main HVAC temperatures.-setTemps :: MonadIO m => (Double, Double) -> Car m CommandResponse-setTemps (driver, passenger) = runCmd "set_temps" ["driver_temp" := driver, "passenger_temp" := passenger]---mkNamedCommands [("hvacOn", "auto_conditioning_start"),-                 ("hvacOff", "auto_conditioning_stop"),-                 ("wakeUp", "wake_up")]-
− src/Tesla/Command/Doors.hs
@@ -1,18 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--module Tesla.Command.Doors (-  actuateFrontTrunk, actuateRearTrunk-  ) where--import           Control.Monad.IO.Class (MonadIO (..))-import           Network.Wreq           (FormParam (..))-import           Tesla.Command--atr :: MonadIO m => String -> Car m CommandResponse-atr w = runCmd "actuate_trunk" [ "which_trunk" := w ]--actuateFrontTrunk :: MonadIO m => Car m CommandResponse-actuateFrontTrunk = atr "front"--actuateRearTrunk :: MonadIO m => Car m CommandResponse-actuateRearTrunk = atr "rear"
− src/Tesla/Command/Homelink.hs
@@ -1,11 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--module Tesla.Command.Homelink (trigger) where--import           Control.Monad.IO.Class (MonadIO (..))-import           Network.Wreq           (FormParam (..))-import           Tesla.Command---- | Trigger nearby homelink with the given (lat,lon)-trigger :: MonadIO m => (Double, Double) -> Car m CommandResponse-trigger (lat,lon) = runCmd "flash_lights" ["lat" := lat, "lon" := lon]
− src/Tesla/Command/Media.hs
@@ -1,18 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell   #-}--module Tesla.Command.Media where--import           Control.Monad.IO.Class (MonadIO (..))--import           Tesla.Command--mkCommands [-  "media_toggle_playback",-  "media_next_track",-  "media_prev_track",-  "media_next_fav",-  "media_prev_fav",-  "media_volume_up",-  "media_volume_down"-  ]
− src/Tesla/Command/RemoteStart.hs
@@ -1,14 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--module Tesla.Command.RemoteStart where--import           Control.Monad.IO.Class (MonadIO (..))-import           Network.Wreq           (FormParam (..))--import           Tesla.Command---- | Enables keyless driving.------ This requires your account password, so, you know, be careful where you use it.-remoteStart :: MonadIO m => String -> Car m CommandResponse-remoteStart pw = runCmd "remote_start_drive" ["password" := pw]
− src/Tesla/Command/Sentry.hs
@@ -1,12 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--module Tesla.Command.Sentry (-  setSentryMode-  ) where--import           Control.Monad.IO.Class (MonadIO (..))-import           Network.Wreq           (FormParam (..))-import           Tesla.Command--setSentryMode :: MonadIO m => Bool -> Car m CommandResponse-setSentryMode on = runCmd "set_sentry_mode" [ "on" := on ]
− src/Tesla/Command/Sharing.hs
@@ -1,20 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--module Tesla.Command.Sharing (share) where--import           Control.Monad.IO.Class (MonadIO (..))-import           Data.Aeson-import           Data.Text              (Text)-import           Data.Time.Clock.POSIX  (getCurrentTime, utcTimeToPOSIXSeconds)--import           Tesla.Command--share :: MonadIO m => Text -> Car m CommandResponse-share to = do-  now <- fst . properFraction . utcTimeToPOSIXSeconds <$> liftIO getCurrentTime-  runCmd "share" $ object [-    "type" .= ("share_ext_content_raw" :: Text),-    "value" .= object [ "android.intent.extra.TEXT" .= to ],-    "locale" .= ("en-US" :: Text),-    "timestamp_ms" .= (now * (1000::Int))-    ]
− src/Tesla/Command/Software.hs
@@ -1,19 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell   #-}--module Tesla.Command.Software (-  scheduleUpdate, cancelUpdate-  ) where--import           Control.Monad.IO.Class (MonadIO (..))-import           Network.Wreq           (FormParam (..))--import           Tesla.Command---- | Schedule a software update in this many seconds.-scheduleUpdate :: MonadIO m => Int -> Car m CommandResponse-scheduleUpdate secs = runCmd "schedule_software_update" ["offset_sec" := secs]---- | Cancel a scheduled software update.-mkCommand "cancelUpdate" "cancel_software_update"-
− src/Tesla/Command/SpeedLimit.hs
@@ -1,24 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--module Tesla.Command.SpeedLimit (-  speedLimit, activateSpeedLimit, deactivateSpeedLimit, clearSpeedLimitPIN-  ) where--import           Control.Monad.IO.Class (MonadIO (..))-import           Network.Wreq           (FormParam (..))-import           Tesla.Command--speedLimit :: MonadIO m => Int -> Car m CommandResponse-speedLimit to = runCmd "speed_limit_set_limit" ["limit_mph" := to ]--speedo :: MonadIO m => String -> Int -> Car m CommandResponse-speedo c pin = runCmd c ["pin" := pin ]--activateSpeedLimit :: MonadIO m => Int -> Car m CommandResponse-activateSpeedLimit = speedo "speed_limit_activate"--deactivateSpeedLimit :: MonadIO m => Int -> Car m CommandResponse-deactivateSpeedLimit = speedo "speed_limit_deactivate"--clearSpeedLimitPIN :: MonadIO m => Int -> Car m CommandResponse-clearSpeedLimitPIN = speedo "speed_limit_clear_pin"
− src/Tesla/Command/Valet.hs
@@ -1,15 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell   #-}--module Tesla.Command.Valet (-  setValetMode, clearValetPIN-  ) where--import           Control.Monad.IO.Class (MonadIO (..))-import           Network.Wreq           (FormParam (..))-import           Tesla.Command--setValetMode :: MonadIO m => Bool -> Int -> Car m CommandResponse-setValetMode on pin = runCmd "set_valet_mode" [ "on" := on, "password" := pin]--mkCommand "clearValetPIN" "reset_valet_pin"
− src/Tesla/Command/Windows.hs
@@ -1,28 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--module Tesla.Command.Windows (-  ventWindows, closeWindows, ventSunroof, closeSunroof-  ) where--import           Control.Monad.IO.Class (MonadIO (..))-import           Data.Text              (Text)-import           Network.Wreq           (FormParam (..))-import           Tesla.Command--windowControl :: MonadIO m => Text -> (Double, Double) -> Car m CommandResponse-windowControl x (lat,lon) = runCmd "window_control" [ "command" := x, "lat" := lat, "lon" := lon]--ventWindows :: MonadIO m => Car m CommandResponse-ventWindows = windowControl "vent" (0,0)--closeWindows :: MonadIO m => (Double, Double) -> Car m CommandResponse-closeWindows = windowControl "close"--sc :: MonadIO m => Text -> Car m CommandResponse-sc c = runCmd "sun_roof_control" [ "state" := c ]--ventSunroof :: MonadIO m => Car m CommandResponse-ventSunroof = sc "vent"--closeSunroof :: MonadIO m => Car m CommandResponse-closeSunroof = sc "close"
− src/Tesla/Commands.hs
@@ -1,25 +0,0 @@-{-|-Module:      Tesla.Commands-Description: Convenience module for importing all commands at once.--All of the commands supported by this library.--This is meant to be imported qualified, e.g.:--> import qualified Tesla.Commands as CMD--}--module Tesla.Commands ( module CMD ) where--import           Tesla.Command.Alerts     as CMD-import           Tesla.Command.Charging   as CMD-import           Tesla.Command.Climate    as CMD-import           Tesla.Command.Doors      as CMD-import           Tesla.Command.Homelink   as CMD-import           Tesla.Command.Media      as CMD-import           Tesla.Command.Sentry     as CMD-import           Tesla.Command.Sharing    as CMD-import           Tesla.Command.Software   as CMD-import           Tesla.Command.SpeedLimit as CMD-import           Tesla.Command.Valet      as CMD-import           Tesla.Command.Windows    as CMD
+ src/Tesla/Energy.hs view
@@ -0,0 +1,84 @@+{-|+Module:      Tesla.Energy+Description: Tesla energy-specific APIs.++Access of energy-specific APIs.+-}++{-# OPTIONS_GHC -Wno-orphans #-}+{-# LANGUAGE DuplicateRecordFields      #-}+{-# LANGUAGE FlexibleContexts           #-}+{-# LANGUAGE FlexibleInstances          #-}+{-# LANGUAGE GeneralisedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses      #-}+{-# LANGUAGE OverloadedStrings          #-}+{-# LANGUAGE UndecidableInstances       #-}++module Tesla.Energy (+  runEnergy, siteData, siteConfig, Energy+  ) where++import           Control.Exception       (Exception)+import           Control.Monad.Catch     (MonadCatch (..), MonadMask (..), MonadThrow (..))+import           Control.Monad.Fail      (MonadFail (..))+import           Control.Monad.IO.Class  (MonadIO (..))+import           Control.Monad.IO.Unlift (MonadUnliftIO, withRunInIO)+import           Control.Monad.Logger    (MonadLogger)+import           Control.Monad.Reader    (MonadReader, ReaderT (..), asks, runReaderT)+import           Data.Aeson              (FromJSON (..))++import           Tesla+import           Tesla.Auth+import           Tesla.Internal.HTTP++-- | Get the URL for a named endpoint for a given vehicle.+energyURL :: EnergyID -> String -> String+energyURL v c = mconcat [baseURL, "api/1/energy_sites/", show v, "/", c]++data EnergyEnv = EnergyEnv {+  _authInfo :: IO AuthInfo,+  _eid      :: EnergyID+  }++-- | Get the current energy ID from the Energy Monad.+currentEnergyID :: Monad m => Energy m EnergyID+currentEnergyID = asks _eid++-- | Energy Monad for accessing energy-specific things.+newtype Energy m a = Energy { runEnergyM :: ReaderT EnergyEnv m a }+  deriving (Applicative, Functor, Monad, MonadIO,+            MonadCatch, MonadThrow, MonadMask, MonadReader EnergyEnv,+            MonadFail, MonadLogger)++instance MonadUnliftIO m => MonadUnliftIO (Energy m) where+  withRunInIO inner = Energy $ withRunInIO $ \run -> inner (run . runEnergyM)++instance (Monad m, MonadIO m, MonadReader EnergyEnv m) => HasTeslaAuth m where+  teslaAuth = liftIO =<< asks _authInfo++-- | Run a Energy Monad with the given Vehicle ID+runEnergy :: MonadIO m => IO AuthInfo -> EnergyID -> Energy m a -> m a+runEnergy ai ei f = runReaderT (runEnergyM f) (EnergyEnv ai ei)++newtype BadEnergyException = BadEnergy String deriving Eq++instance Show BadEnergyException where+  show (BadEnergy s) = "BadEnergy: " <> s++instance Exception BadEnergyException++{- 404?+siteSummary :: (FromJSON j, MonadIO m) => Energy m j+siteSummary = currentEnergyID >>= \e -> jget (energyURL e "status")+-}++siteData :: (FromJSON j, MonadIO m) => Energy m j+siteData = currentEnergyID >>= \e -> jgetAuth (energyURL e "live_status")++siteConfig :: (FromJSON j, MonadIO m) => Energy m j+siteConfig = currentEnergyID >>= \e -> jgetAuth (energyURL e "site_info")++{- Errors that 'kind' is required.+siteHistory :: (FromJSON j, MonadIO m) => Energy m j+siteHistory = currentEnergyID >>= \e -> jget (energyURL e "history")+-}
+ src/Tesla/Internal/HTTP.hs view
@@ -0,0 +1,37 @@+{-# OPTIONS_HADDOCK hide, prune, ignore-exports #-}++module Tesla.Internal.HTTP where++import           Control.Lens+import           Control.Monad.IO.Class (MonadIO (..))+import           Data.Aeson             (FromJSON (..))+import qualified Data.ByteString.Char8  as BC+import           Network.Wreq           (Options, asJSON, defaults, get, getWith, header, postWith, responseBody)+import           Network.Wreq.Types     (Postable)++import           Tesla.Auth++userAgent :: BC.ByteString+userAgent = "github.com/dustin/tesla 0.1"++defOpts :: Options+defOpts = defaults & header "User-Agent" .~ [userAgent]++-- | Get a set of wreq options from an 'AuthInfo'.+authOpts :: AuthInfo -> Options+authOpts AuthInfo{..} = defOpts & header "Authorization" .~ ["Bearer " <> BC.pack _bearerToken]++jget :: (FromJSON j, MonadIO m) => String -> m j+jget u = view responseBody <$> liftIO (asJSON =<< get u)++jgetWith :: (FromJSON j, MonadIO m) => Options -> String -> m j+jgetWith opts u = view responseBody <$> liftIO (asJSON =<< getWith opts u)++jgetAuth :: (HasTeslaAuth m, FromJSON j, MonadIO m) => String -> m j+jgetAuth u = teslaAuth >>= \a -> view responseBody <$> liftIO (asJSON =<< getWith (authOpts a) u)++jpostWith :: (FromJSON j, Postable a, MonadIO m) => Options -> String -> a -> m j+jpostWith opts u v = view responseBody <$> liftIO (postWith opts u v >>= asJSON)++jpostAuth :: (HasTeslaAuth m, FromJSON j, Postable a, MonadIO m) => String -> a -> m j+jpostAuth u v = teslaAuth >>= \a -> jpostWith (authOpts a) u v
tesla.cabal view
@@ -1,13 +1,13 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.31.2.+-- This file has been generated from package.yaml by hpack version 0.33.0. -- -- see: https://github.com/sol/hpack ----- hash: bc86432b798b84de1d75a14ba3e27975fe6c0c2431111c061bfcd48bdf851848+-- hash: 8d58aba9e4939289760f458ed52ae77cefb0e80437c28f6672627ef98afe987f  name:           tesla-version:        0.1.0.1+version:        0.2.0.0 synopsis:       Tesla API client. description:    Please see the README on GitHub at <https://github.com/dustin/tesla#readme> category:       Web@@ -30,22 +30,25 @@ library   exposed-modules:       Tesla+      Tesla.Auth       Tesla.Car-      Tesla.Command-      Tesla.Command.Alerts-      Tesla.Command.Charging-      Tesla.Command.Climate-      Tesla.Command.Doors-      Tesla.Command.Homelink-      Tesla.Command.Media-      Tesla.Command.RemoteStart-      Tesla.Command.Sentry-      Tesla.Command.Sharing-      Tesla.Command.Software-      Tesla.Command.SpeedLimit-      Tesla.Command.Valet-      Tesla.Command.Windows-      Tesla.Commands+      Tesla.Car.Command+      Tesla.Car.Command.Alerts+      Tesla.Car.Command.Charging+      Tesla.Car.Command.Climate+      Tesla.Car.Command.Doors+      Tesla.Car.Command.Homelink+      Tesla.Car.Command.Media+      Tesla.Car.Command.RemoteStart+      Tesla.Car.Command.Sentry+      Tesla.Car.Command.Sharing+      Tesla.Car.Command.Software+      Tesla.Car.Command.SpeedLimit+      Tesla.Car.Command.Valet+      Tesla.Car.Command.Windows+      Tesla.Car.Commands+      Tesla.Energy+      Tesla.Internal.HTTP   other-modules:       Paths_tesla   hs-source-dirs:@@ -58,13 +61,16 @@     , bytestring >=0.10 && <0.11     , casing >=0.1.4 && <0.2     , containers >=0.6 && <0.7+    , exceptions     , generic-deriving >=1.12 && <1.14     , lens >=4.17 && <4.19     , lens-aeson >=1.0 && <1.2+    , monad-logger     , mtl >=2.2 && <2.3     , template-haskell >=2.14 && <2.16     , text >=1.2 && <1.3     , time >=1.8 && <1.10+    , unliftio-core     , vector >=0.12.0 && <0.13     , wreq >=0.5 && <0.6   default-language: Haskell2010@@ -85,9 +91,11 @@     , bytestring >=0.10 && <0.11     , casing >=0.1.4 && <0.2     , containers >=0.6 && <0.7+    , exceptions     , generic-deriving >=1.12 && <1.14     , lens >=4.17 && <4.19     , lens-aeson >=1.0 && <1.2+    , monad-logger     , mtl >=2.2 && <2.3     , tasty     , tasty-hunit@@ -96,6 +104,7 @@     , tesla     , text >=1.2 && <1.3     , time >=1.8 && <1.10+    , unliftio-core     , vector >=0.12.0 && <0.13     , wreq >=0.5 && <0.6   default-language: Haskell2010
test/Spec.hs view
@@ -4,6 +4,8 @@ import           Test.Tasty.HUnit import           Test.Tasty.QuickCheck as QC +import           Data.Aeson+import           Data.Maybe            (fromJust) import           Data.Ratio import           Data.Time.Clock       (UTCTime) import           Data.Time.Clock.POSIX (posixSecondsToUTCTime)@@ -14,6 +16,9 @@ sampleVehicleData :: VehicleData sampleVehicleData = "{\"id\":484265022836,\"user_id\":872475,\"vehicle_id\":842742,\"vin\":\"5YJXCDE2XHF000001\",\"display_name\":\"Bob\",\"option_codes\":\"AD15,MDL3,PBSB,RENA,BT37,ID3W,RF3G,S3PB,DRLH,DV2W,W39B,APF0,COUS,BC3B,CH07,PC30,FC3P,FG31,GLFR,HL31,HM31,IL31,LTPB,MR31,FM3B,RS3H,SA3P,STCP,SC04,SU3C,T3CA,TW00,TM00,UT3P,WR00,AU3P,APH3,AF00,ZCST,MI00,CDM0\",\"color\":null,\"tokens\":[\"token1\",\"token2\"],\"state\":\"online\",\"in_service\":false,\"id_s\":\"484265022836\",\"calendar_enabled\":true,\"api_version\":6,\"backseat_token\":null,\"backseat_token_updated_at\":null,\"charge_state\":{\"battery_heater_on\":false,\"battery_level\":79,\"battery_range\":225.71,\"charge_current_request\":48,\"charge_current_request_max\":48,\"charge_enable_request\":false,\"charge_energy_added\":0.0,\"charge_limit_soc\":80,\"charge_limit_soc_max\":100,\"charge_limit_soc_min\":50,\"charge_limit_soc_std\":90,\"charge_miles_added_ideal\":0.0,\"charge_miles_added_rated\":0.0,\"charge_port_cold_weather_mode\":null,\"charge_port_door_open\":true,\"charge_port_latch\":\"Engaged\",\"charge_rate\":0.0,\"charge_to_max_range\":false,\"charger_actual_current\":0,\"charger_phases\":null,\"charger_pilot_current\":48,\"charger_power\":13,\"charger_voltage\":0,\"charging_state\":\"Stopped\",\"conn_charge_cable\":\"SAE\",\"est_battery_range\":161.61,\"fast_charger_brand\":\"<invalid>\",\"fast_charger_present\":false,\"fast_charger_type\":\"ACSingleWireCAN\",\"ideal_battery_range\":282.78,\"managed_charging_active\":false,\"managed_charging_start_time\":null,\"managed_charging_user_canceled\":false,\"max_range_charge_counter\":0,\"minutes_to_full_charge\":20,\"not_enough_power_to_heat\":false,\"scheduled_charging_pending\":true,\"scheduled_charging_start_time\":1569738600,\"time_to_full_charge\":0.33,\"timestamp\":1569718275490,\"trip_charging\":false,\"usable_battery_level\":79,\"user_charge_enable_request\":null},\"climate_state\":{\"battery_heater\":false,\"battery_heater_no_power\":false,\"climate_keeper_mode\":\"off\",\"defrost_mode\":0,\"driver_temp_setting\":22.0,\"fan_status\":0,\"inside_temp\":22.3,\"is_auto_conditioning_on\":false,\"is_climate_on\":false,\"is_front_defroster_on\":false,\"is_preconditioning\":false,\"is_rear_defroster_on\":false,\"left_temp_direction\":317,\"max_avail_temp\":28.0,\"min_avail_temp\":15.0,\"outside_temp\":15.5,\"passenger_temp_setting\":22.0,\"remote_heater_control_enabled\":false,\"right_temp_direction\":317,\"seat_heater_left\":0,\"seat_heater_rear_center\":0,\"seat_heater_rear_left\":0,\"seat_heater_rear_right\":0,\"seat_heater_right\":0,\"side_mirror_heaters\":false,\"smart_preconditioning\":false,\"steering_wheel_heater\":false,\"timestamp\":1569718275490,\"wiper_blade_heater\":false},\"drive_state\":{\"gps_as_of\":1569718274,\"heading\":125,\"latitude\":34.13245,\"longitude\":-120.823475,\"native_latitude\":34.13245,\"native_location_supported\":1,\"native_longitude\":-120.823475,\"native_type\":\"wgs\",\"power\":0,\"shift_state\":\"P\",\"speed\":null,\"timestamp\":1569718275490},\"gui_settings\":{\"gui_24_hour_time\":true,\"gui_charge_rate_units\":\"kW\",\"gui_distance_units\":\"mi/hr\",\"gui_range_display\":\"Rated\",\"gui_temperature_units\":\"C\",\"show_range_units\":true,\"timestamp\":1569718275490},\"vehicle_config\":{\"can_accept_navigation_requests\":true,\"can_actuate_trunks\":true,\"car_special_type\":\"base\",\"car_type\":\"modelx\",\"charge_port_type\":\"US\",\"eu_vehicle\":false,\"exterior_color\":\"SteelGrey\",\"has_air_suspension\":true,\"has_ludicrous_mode\":false,\"motorized_charge_port\":true,\"plg\":true,\"rear_seat_heaters\":3,\"rear_seat_type\":4,\"rhd\":false,\"roof_color\":\"None\",\"seat_type\":0,\"spoiler_type\":\"Passive\",\"sun_roof_installed\":0,\"third_row_seats\":\"None\",\"timestamp\":1569718275490,\"trim_badging\":\"100d\",\"use_range_badging\":false,\"wheel_type\":\"AeroTurbine20\"},\"vehicle_state\":{\"api_version\":6,\"autopark_state_v2\":\"standby\",\"autopark_style\":\"standard\",\"calendar_supported\":true,\"car_version\":\"2019.32.11 bac8c51\",\"center_display_state\":0,\"df\":0,\"dr\":0,\"fd_window\":0,\"fp_window\":0,\"ft\":0,\"homelink_device_count\":1,\"homelink_nearby\":false,\"is_user_present\":true,\"last_autopark_error\":\"no_error\",\"locked\":true,\"media_state\":{\"remote_control_enabled\":true},\"notifications_supported\":true,\"odometer\":49081.642123,\"parsed_calendar_supported\":true,\"pf\":0,\"pr\":0,\"rd_window\":0,\"remote_start\":false,\"remote_start_enabled\":true,\"remote_start_supported\":true,\"rp_window\":0,\"rt\":32,\"sentry_mode\":false,\"sentry_mode_available\":true,\"smart_summon_available\":true,\"software_update\":{\"download_perc\":0,\"expected_duration_sec\":2700,\"install_perc\":1,\"status\":\"\",\"version\":\"\"},\"speed_limit_mode\":{\"active\":false,\"current_limit_mph\":80.0,\"max_limit_mph\":90,\"min_limit_mph\":50,\"pin_code_set\":true},\"summon_standby_mode_enabled\":true,\"timestamp\":1569718275489,\"valet_mode\":false,\"vehicle_name\":\"Bob\"}}" +sampleProducts :: Value+sampleProducts = fromJust . decode $ "{\"response\":[{\"backseat_token_updated_at\":null,\"backseat_token\":null,\"state\":\"online\",\"display_name\":\"MyCar\",\"option_codes\":\"AD15,MDL3,PBSB,RENA,BT37,ID3W,RF3G,S3PB,DRLH,DV2W,W39B,APF0,COUS,BC3B,CH07,PC30,FC3P,FG31,GLFR,HL31,HM31,IL31,LTPB,MR31,FM3B,RS3H,SA3P,STCP,SC04,SU3C,T3CA,TW00,TM00,UT3P,WR00,AU3P,APH3,AF00,ZCST,MI00,CDM0\",\"color\":null,\"api_version\":7,\"tokens\":[\"tok1\",\"tok2\"],\"id_s\":\"848528\",\"vehicle_id\":87576,\"id\":848528,\"in_service\":false,\"calendar_enabled\":true,\"user_id\":361508,\"vin\":\"SOMEVIN\"},{\"solar_type\":\"pv_panel\",\"energy_site_id\":2848535,\"breaker_alert_enabled\":false,\"solar_power\":0,\"id\":\"SOME_UU_I_D\",\"resource_type\":\"solar\",\"sync_grid_alert_enabled\":false}],\"count\":2}"+ testIsUserPresent :: Assertion testIsUserPresent = assertBool "expected user not present" $ isUserPresent sampleVehicleData @@ -34,13 +39,19 @@ testOpenDoors :: Assertion testOpenDoors =  assertEqual "open" [RearTrunk] (openDoors sampleVehicleData) +testParseProducts :: Assertion+testParseProducts = assertEqual "products" [ProductVehicle "MyCar" "848528",+                                            ProductEnergy 2848535]+                    (decodeProducts sampleProducts)+ tests :: [TestTree] tests = [   testCase "is user present" testIsUserPresent,   testCase "charging" testIsCharging,   testCase "timestamp" testTimestamp,   testCase "doors" testDoors,-  testCase "open doors" testOpenDoors+  testCase "open doors" testOpenDoors,+  testCase "products parsing" testParseProducts   ]  main :: IO ()