packages feed

taffybar-4.1.2: src/System/Taffybar/Information/Temperature.hs

--------------------------------------------------------------------------------
-- |
-- Module      : System.Taffybar.Information.Temperature
-- Copyright   : (c) Ivan Malison
-- License     : BSD3-style (see LICENSE)
--
-- Maintainer  : Ivan Malison <IvanMalison@gmail.com>
-- Stability   : unstable
-- Portability : unportable
--
-- This module provides functions to read system temperature information from
-- Linux thermal zones and hwmon devices.
--
--------------------------------------------------------------------------------

module System.Taffybar.Information.Temperature
  ( TemperatureInfo(..)
  , ThermalSensor(..)
  , TemperatureUnit(..)
  , discoverSensors
  , readSensorTemperature
  , readAllTemperatures
  , readTemperaturesFrom
  , convertTemperature
  ) where

import Control.Exception (try, SomeException)
import Control.Monad (forM)
import Data.List (sortOn)
import Data.Maybe (catMaybes, fromMaybe)
import System.Directory (doesDirectoryExist, doesFileExist, listDirectory)
import System.FilePath ((</>))
import Text.Read (readMaybe)

-- | Temperature unit for display
data TemperatureUnit = Celsius | Fahrenheit | Kelvin
  deriving (Show, Eq)

-- | Information about a thermal sensor
data ThermalSensor = ThermalSensor
  { sensorName :: String      -- ^ Human-readable sensor name
  , sensorPath :: FilePath    -- ^ Path to the temperature file
  , sensorZone :: String      -- ^ Zone or hwmon identifier
  } deriving (Show, Eq)

-- | Temperature reading from a sensor
data TemperatureInfo = TemperatureInfo
  { tempCelsius :: Double     -- ^ Temperature in Celsius
  , tempSensor :: ThermalSensor  -- ^ The sensor this reading is from
  } deriving (Show, Eq)

-- | Convert temperature from Celsius to another unit
convertTemperature :: TemperatureUnit -> Double -> Double
convertTemperature Celsius t = t
convertTemperature Fahrenheit t = t * 1.8 + 32
convertTemperature Kelvin t = t + 273.15

-- | Discover all available thermal sensors on the system
-- Tries thermal_zone first, then hwmon devices
discoverSensors :: IO [ThermalSensor]
discoverSensors = do
  thermalSensors <- discoverThermalZones
  hwmonSensors <- discoverHwmon
  return $ thermalSensors ++ hwmonSensors

-- | Discover thermal zones from /sys/class/thermal/
discoverThermalZones :: IO [ThermalSensor]
discoverThermalZones = do
  let thermalDir = "/sys/class/thermal"
  exists <- doesDirectoryExist thermalDir
  if not exists
    then return []
    else do
      entries <- listDirectory thermalDir
      let zones = filter (isPrefixOf "thermal_zone") entries
      sensors <- forM zones $ \zone -> do
        let tempPath = thermalDir </> zone </> "temp"
            typePath = thermalDir </> zone </> "type"
        tempExists <- doesFileExist tempPath
        if tempExists
          then do
            sensorType <- readFileSafe typePath
            return $ Just ThermalSensor
              { sensorName = fromMaybe zone sensorType
              , sensorPath = tempPath
              , sensorZone = zone
              }
          else return Nothing
      return $ catMaybes sensors
  where
    isPrefixOf prefix str = take (length prefix) str == prefix

-- | Discover hwmon temperature sensors from /sys/class/hwmon/
discoverHwmon :: IO [ThermalSensor]
discoverHwmon = do
  let hwmonDir = "/sys/class/hwmon"
  exists <- doesDirectoryExist hwmonDir
  if not exists
    then return []
    else do
      entries <- listDirectory hwmonDir
      let hwmons = filter (isPrefixOf "hwmon") entries
      sensorLists <- forM hwmons $ \hwmon -> do
        let hwmonPath = hwmonDir </> hwmon
            namePath = hwmonPath </> "name"
        hwmonName <- readFileSafe namePath
        -- Find all temp*_input files
        allFiles <- listDirectory hwmonPath
        let tempInputs = filter isTempInput allFiles
        forM tempInputs $ \tempFile -> do
          let tempPath = hwmonPath </> tempFile
              sensorId = extractSensorId tempFile
              labelPath = hwmonPath </> ("temp" ++ sensorId ++ "_label")
          labelName <- readFileSafe labelPath
          let name = case (labelName, hwmonName) of
                (Just l, _) -> l
                (_, Just n) -> n ++ "_temp" ++ sensorId
                _ -> hwmon ++ "_temp" ++ sensorId
          return ThermalSensor
            { sensorName = name
            , sensorPath = tempPath
            , sensorZone = hwmon
            }
      return $ concat sensorLists
  where
    isPrefixOf prefix str = take (length prefix) str == prefix
    isTempInput file = take 4 file == "temp" && isSuffixOf "_input" file
    isSuffixOf suffix str =
      let suffixLen = length suffix
          strLen = length str
      in strLen >= suffixLen && drop (strLen - suffixLen) str == suffix
    -- Extract sensor number from "temp1_input" -> "1"
    extractSensorId file =
      let withoutPrefix = drop 4 file  -- remove "temp"
          withoutSuffix = take (length withoutPrefix - 6) withoutPrefix  -- remove "_input"
      in withoutSuffix

-- | Read temperature from a file, returns Nothing if file cannot be read
readFileSafe :: FilePath -> IO (Maybe String)
readFileSafe path = do
  exists <- doesFileExist path
  if not exists
    then return Nothing
    else do
      result <- try $ readFile path :: IO (Either SomeException String)
      case result of
        Left _ -> return Nothing
        Right content -> return $ Just $ strip content
  where
    strip = dropWhile (== ' ') . reverse . dropWhile (== '\n') . reverse . dropWhile (== ' ')

-- | Read temperature from a single sensor
-- Returns Nothing if the sensor cannot be read
readSensorTemperature :: ThermalSensor -> IO (Maybe TemperatureInfo)
readSensorTemperature sensor = do
  result <- try $ readFile (sensorPath sensor) :: IO (Either SomeException String)
  case result of
    Left _ -> return Nothing
    Right content ->
      case readMaybe (strip content) :: Maybe Integer of
        Nothing -> return Nothing
        Just milliDegrees -> return $ Just TemperatureInfo
          { tempCelsius = fromIntegral milliDegrees / 1000.0
          , tempSensor = sensor
          }
  where
    strip = dropWhile (== ' ') . reverse . dropWhile (== '\n') . reverse . dropWhile (== ' ')

-- | Read temperatures from all discovered sensors
-- Returns list sorted by temperature (highest first)
readAllTemperatures :: IO [TemperatureInfo]
readAllTemperatures = do
  sensors <- discoverSensors
  temps <- forM sensors readSensorTemperature
  return $ sortOn (negate . tempCelsius) $ catMaybes temps

-- | Read temperatures from specific sensors (by path)
readTemperaturesFrom :: [ThermalSensor] -> IO [TemperatureInfo]
readTemperaturesFrom sensors = do
  temps <- forM sensors readSensorTemperature
  return $ catMaybes temps