----------------------------------------------------------------------------- -- | -- Module : Plugins.Monitors.Batt.Linux -- Copyright : (c) 2010-2013, 2015, 2016, 2018, 2019, 2022 Jose A Ortega -- (c) 2010 Andrea Rossato, Petr Rockai -- License : BSD-style (see LICENSE) -- -- Maintainer : Jose A. Ortega Ruiz <jao@gnu.org> -- Stability : unstable -- Portability : unportable -- -- A battery monitor for Xmobar -- ----------------------------------------------------------------------------- module Xmobar.Plugins.Monitors.Batt.Linux (readBatteries) where import Xmobar.Plugins.Monitors.Batt.Common ( BattOpts(..) , Result(..) , Status(..) , maybeAlert) import Control.Monad (unless) import Control.Exception (SomeException, handle) import System.FilePath ((</>)) import System.IO (IOMode(ReadMode), hGetLine, withFile, Handle) import Data.List (sort, sortBy, group) import Data.Maybe (fromMaybe) import Data.Ord (comparing) import Text.Read (readMaybe) data Files = Files { fEFull :: String , fCFull :: String , fEFullDesign :: String , fCFullDesign :: String , fENow :: String , fCNow :: String , fVoltage :: String , fVoltageMin :: String , fCurrent :: String , fPower :: String , fStatus :: String , fBat :: String } deriving Eq -- the default basenames of the possibly available attributes exposed -- by the kernel defaultFiles :: Files defaultFiles = Files { fEFull = "energy_full" , fCFull = "charge_full" , fEFullDesign = "energy_full_design" , fCFullDesign = "charge_full_design" , fENow = "energy_now" , fCNow = "charge_now" , fVoltage = "voltage_now" , fVoltageMin = "voltage_min_design" , fCurrent = "current_now" , fPower = "power_now" , fStatus = "status" , fBat = "BAT0" } type FilesAccessor = Files -> String sysDir :: FilePath sysDir = "/sys/class/power_supply" battFile :: FilesAccessor -> Files -> FilePath battFile accessor files = sysDir </> fBat files </> accessor files grabNumber :: (Num a, Read a) => FilesAccessor -> Files -> IO (Maybe a) grabNumber = grabFile (fmap read . hGetLine) grabString :: FilesAccessor -> Files -> IO (Maybe String) grabString = grabFile hGetLine -- grab file contents returning Nothing if the file doesn't exist or -- any other error occurs grabFile :: (Handle -> IO a) -> FilesAccessor -> Files -> IO (Maybe a) grabFile readMode accessor files = handle (onFileError Nothing) (withFile f ReadMode (fmap Just . readMode)) where f = battFile accessor files onFileError :: a -> SomeException -> IO a onFileError returnOnError = const (return returnOnError) -- get the filenames for a given battery name batteryFiles :: String -> Files batteryFiles bat = defaultFiles { fBat = bat } data Battery = Battery { full :: !Float , now :: !Float , power :: !Float , status :: !String } haveAc :: FilePath -> IO Bool haveAc f = handle (onFileError False) $ withFile (sysDir </> f) ReadMode (fmap (== "1") . hGetLine) -- retrieve the currently drawn power in Watt -- sc is a scaling factor which by kernel documentation must be 1e6 readBatPower :: Float -> Files -> IO (Maybe Float) readBatPower sc f = do pM <- grabNumber fPower f cM <- grabNumber fCurrent f vM <- grabNumber fVoltage f return $ case (pM, cM, vM) of (Just pVal, _, _) -> Just $ pVal / sc (_, Just cVal, Just vVal) -> Just $ cVal * vVal / (sc * sc) (_, _, _) -> Nothing -- retrieve the maximum capacity in Watt hours -- sc is a scaling factor which by kernel documentation must be 1e6 -- on getting the voltage: using voltage_min_design will probably underestimate -- the actual energy content of the battery and using voltage_now will probably -- overestimate it. readBatCapacityFull :: Float -> Files -> IO (Maybe Float) readBatCapacityFull sc f = do cM <- grabNumber fCFull f eM <- grabNumber fEFull f cdM <- grabNumber fCFullDesign f edM <- grabNumber fEFullDesign f -- not sure if Voltage or VoltageMin is more accurate and if both -- are always available vM <- grabNumber fVoltageMin f return $ case (eM, cM, edM, cdM, vM) of (Just eVal, _, _, _, _) -> Just $ eVal / sc (_, Just cVal, _, _, Just vVal) -> Just $ cVal * vVal / (sc * sc) (_, _, Just eVal, _, _) -> Just $ eVal / sc (_, _, _, Just cVal, Just vVal) -> Just $ cVal * vVal / (sc * sc) (_, _, _, _, _) -> Nothing -- retrieve the current capacity in Watt hours -- sc is a scaling factor which by kernel documentation must be 1e6 -- on getting the voltage: using voltage_min_design will probably underestimate -- the actual energy content of the battery and using voltage_now will probably -- overestimate it. readBatCapacityNow :: Float -> Files -> IO (Maybe Float) readBatCapacityNow sc f = do cM <- grabNumber fCNow f eM <- grabNumber fENow f vM <- grabNumber fVoltageMin f -- not sure if Voltage or -- VoltageMin is more accurate -- and if both are always -- available return $ case (eM, cM, vM) of (Just eVal, _, _) -> Just $ eVal / sc (_, Just cVal, Just vVal) -> Just $ cVal * vVal / (sc * sc) (_, _, _) -> Nothing readBatStatus :: Files -> IO (Maybe String) readBatStatus = grabString fStatus -- collect all relevant battery values with defaults of not available readBattery :: Float -> Files -> IO Battery readBattery sc files = do cFull <- withDef 0 readBatCapacityFull cNow <- withDef 0 readBatCapacityNow pwr <- withDef 0 readBatPower s <- withDef "Unknown" (const readBatStatus) let cFull' = max cFull cNow -- sometimes the reported max -- charge is lower than return $ Battery (3600 * cFull') -- wattseconds (3600 * cNow) -- wattseconds (abs pwr) -- watts s -- string: Discharging/Charging/Full where withDef d reader = fromMaybe d `fmap` reader sc files -- sortOn is only available starting at ghc 7.10 sortOn :: Ord b => (a -> b) -> [a] -> [a] sortOn f = map snd . sortBy (comparing fst) . map (\x -> let y = f x in y `seq` (y, x)) mostCommonDef :: Eq a => a -> [a] -> a mostCommonDef x xs = head $ last $ [x] : sortOn length (group xs) readBatteries :: BattOpts -> [String] -> IO Result readBatteries opts bfs = do let bfs'' = map batteryFiles bfs bats <- mapM (readBattery (scale opts)) (take 3 bfs'') ac <- haveAc (onlineFile opts) let sign = if ac then 1 else -1 ft = sum (map full bats) -- total capacity when full left = if ft > 0 then sum (map now bats) / ft else 0 watts = sign * sum (map power bats) time = if watts == 0 then 0 else max 0 (sum $ map time' bats) mwatts = if watts == 0 then 1 else sign * watts time' b = (if ac then full b - now b else now b) / mwatts statuses :: [Status] statuses = map (fromMaybe Unknown . readMaybe) (sort (map status bats)) acst = mostCommonDef Unknown $ filter (Unknown/=) statuses racst | acst /= Unknown = acst | time == 0 = Idle | ac = Charging | otherwise = Discharging unless ac (maybeAlert opts left) return $ if isNaN left then NA else Result left watts time racst