----------------------------------------------------------------------------- -- | -- 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 -- 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