summaryrefslogtreecommitdiffhomepage
path: root/src/Xmobar/Plugins/Monitors/Batt/Linux.hs
blob: 5389be0a66f64aabaec1773e391012c1d63815b9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
-----------------------------------------------------------------------------
-- |
-- 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