35d88e8caeb0721d7e884cffdcc4ed4179c40460
   1#
   2#   temperature.py
   3#   
   4#   Find current temperature of various system components (CPU, motherboard,
   5#   hard drives, ambient). Detection of motherboard-based temperatures (CPU
   6#   etc) uses the pysensors library, and produces a similar output to
   7#   lmsensors. HDD temperatures are obtained from the hddtemp daemon
   8#   (http://www.guzu.net/linux/hddtemp.php) which was orphaned since 2007. For
   9#   hddtemp to work, it must be started in daemon mode, either manually or with
  10#   a unit file. Manually, it would be started like this:
  11#
  12#       sudo hddtemp -d /dev/sda /dev/sdb ... /dev/sdX
  13#
  14
  15import re
  16import sensors
  17import socket, sys
  18from telnetlib import Telnet
  19from typing import List, Dict, NamedTuple
  20
  21from ..formatting import *
  22from ..util import readlog, resolve
  23from .. import config
  24
  25import logging
  26logger = logging.getLogger(__name__)
  27
  28class Drive(NamedTuple):
  29    path: str
  30    model: str
  31    temperature: int
  32    units: str
  33
  34class HddtempClient:
  35
  36    def __init__(self, host: str='localhost', port: int=7634, timeout: int=10, sep: str='|') -> None:
  37        self.host = host
  38        self.port = port
  39        self.timeout = timeout
  40        self.sep = sep
  41
  42    def _parse_drive(self, drive: str) -> Drive:
  43        drive_data = drive.split(self.sep)
  44        return Drive(drive_data[0], drive_data[1], int(drive_data[2]), drive_data[3])
  45
  46    def _parse(self, data: str) -> List[Drive]:
  47        line = data.lstrip(self.sep).rstrip(self.sep)   # Remove first/last
  48        drives = line.split(self.sep * 2)
  49        return [self._parse_drive(drive) for drive in drives]
  50
  51    def get_drives(self) -> List[Drive]:    # Obtain data from telnet server
  52        try:
  53            with Telnet(self.host, self.port, timeout=self.timeout) as tn:
  54                data = tn.read_all()
  55            return self._parse(data.decode('ascii'))    # Return parsed data
  56        except:
  57            logger.warning("Couldn't read data from {0}:{1}".format(self.host, self.port))
  58            return 1
  59
  60
  61def parse_log():
  62    logger.debug("Starting temp section")
  63    output = writetitle("temperatures")
  64    output += opentag('div', 1, 'temp', 'section')
  65
  66    # cpu temp
  67
  68    sensors.init()
  69    coretemps = []
  70    pkgtemp = 0
  71    systemp = 0
  72    try:
  73        for chip in sensors.iter_detected_chips():
  74            for feature in chip:
  75                if "Core" in feature.label:
  76                    coretemps.append([feature.label, feature.get_value()])
  77                    logger.debug("found core " + feature.label + " at temp " + str(feature.get_value()))
  78                if "CPUTIN" in feature.label:
  79                    pkgtemp = str(feature.get_value())
  80                    logger.debug("found cpu package at temperature " + pkgtemp)
  81                if "SYS" in feature.label:
  82                    systemp = feature.get_value()
  83                    logger.debug("found sys input " + feature.label + " at temp " + str(feature.get_value()))
  84        logger.debug("Core temp data is: " + str(coretemps))
  85#        core_avg = reduce(lambda x, y: x[1] + y[1], coretemps) / len(coretemps)
  86        core_avg = sum(core[1] for core in coretemps) / len(coretemps)
  87        logger.debug("average cpu temp is " + str(core_avg))
  88        coretemps.append(["avg", str(core_avg)])
  89        coretemps.append(["pkg", pkgtemp])
  90        coretemps = [x[0] + ": " + str(x[1]) + DEG + CEL for x in coretemps]
  91    finally:
  92        sensors.cleanup()
  93
  94    if (systemp != 0):
  95        output += writedata("sys: " + str(systemp) + DEG)
  96    if (coretemps != ''):
  97        output += writedata("cores", coretemps)
  98    
  99    logger.info("Finished reading onboard temperatures")
 100
 101    # drive temp
 102
 103    # For this to work, `hddtemp` must be running in daemon mode.
 104    # Start it like this (bash):   sudo hddtemp -d /dev/sda /dev/sdX...
 105    
 106    received = ''
 107    sumtemp = 0.0 
 108    data = ""
 109    fields = []
 110    
 111    client = HddtempClient(host=config.prefs['hddtemp']['host'], port=int(config.prefs['hddtemp']['port']), sep=config.prefs['hddtemp']['separator'], timeout=int(config.prefs['hddtemp']['timeout']))
 112    drives = client.get_drives()
 113    for drive in sorted(drives, key=lambda x: x.path):
 114        if drive.path in config.prefs['hddtemp']['drives']:
 115            sumtemp += drive.temperature
 116            fields.append(("{0} ({1})".format(drive.path, drive.model) if config.prefs['hddtemp']['show-model'] else drive.path) + ": {0}{1}{2}".format(drive.temperature, DEG, drive.units))
 117        else:
 118            drives.remove(drive)
 119            logger.debug("Ignoring drive {0} ({1})due to config".format(drive.path, drive.model))
 120    logger.debug("Received drive info: " + str(drives))
 121
 122    hddavg = '{0:.1f}{1}{2}'.format(sumtemp/len(drives), DEG, drives[0].units) # use units of first drive
 123    logger.debug("Sum of temperatures: {}; Number of drives: {}; => Avg disk temp is {}".format(str(sumtemp), str(len(drives)), hddavg)) 
 124    fields.append("avg: " + str(hddavg))
 125
 126    if (prefs['hddtemp']['drives'] != ''):
 127        output += writedata("disks", fields)
 128    logger.info("Finished processing drive temperatures")
 129
 130
 131    output += closetag('div', 1)
 132    logger.info("Finished temp section")
 133    return output