logparse / parsers / temperature.pyon commit rename parsers, better journald integration (e1f7605)
   1"""
   2Find current temperature of various system components (CPU, motherboard,
   3hard drives, ambient). Detection of motherboard-based temperatures (CPU
   4etc) uses the pysensors library, and produces a similar output to
   5lmsensors. HDD temperatures are obtained from the hddtemp daemon
   6<http://www.guzu.net/linux/hddtemp.php> which was orphaned since 2007. For
   7hddtemp to work, it must be started in daemon mode, either manually or with
   8a unit file. Manually, it would be started like this:
   9    `sudo hddtemp -d /dev/sda /dev/sdb ... /dev/sdX`
  10"""
  11
  12import re
  13import sensors
  14import socket, sys
  15from telnetlib import Telnet
  16from typing import List, Dict, NamedTuple
  17
  18from logparse.formatting import *
  19from logparse.util import readlog
  20from logparse import config
  21
  22import logging
  23logger = logging.getLogger(__name__)
  24from logparse.load_parsers import Parser
  25
  26class Drive(NamedTuple):
  27    path: str
  28    model: str
  29    temperature: int
  30    units: str
  31
  32class HddtempClient:
  33
  34    def __init__(self, host: str='127.0.0.1', port: int=7634, timeout: int=10,
  35            sep: str='|') -> None:
  36        self.host = host
  37        self.port = port
  38        self.timeout = timeout
  39        self.sep = sep
  40
  41    def _parse_drive(self, drive: str) -> Drive:
  42        try:
  43            drive_data = drive.split(self.sep)
  44            return Drive(drive_data[0], drive_data[1], 
  45                    int(drive_data[2]), drive_data[3])
  46        except Exception as e:
  47            logger.warning("Error processing drive: " + str(drive_data))
  48            return None
  49
  50    def _parse(self, data: str) -> List[Drive]:
  51        line = data.lstrip(self.sep).rstrip(self.sep)   # Remove first/last
  52        drives = line.split(self.sep * 2)
  53        parsed_drives = []
  54        for drive in drives:
  55            parsed_drive = self._parse_drive(drive)
  56            if parsed_drive != None:
  57                parsed_drives.append(parsed_drive)
  58
  59        return parsed_drives
  60
  61    def get_drives(self) -> List[Drive]:    # Obtain data from telnet server
  62        try:
  63            with Telnet(self.host, self.port, timeout=self.timeout) as tn:
  64                raw_data = tn.read_all()
  65            return self._parse(raw_data.decode('ascii'))    # Return parsed data
  66        except Exception as e:
  67            logger.warning("Couldn't read data from {0}:{1} - {2}".format(
  68                self.host, self.port, str(e)))
  69            return 1
  70
  71
  72class Temperature(Parser):
  73
  74    def __init__(self):
  75        super().__init__()
  76        self.name = "temperature"
  77        self.info = "Find current temperature of various system components "
  78                "(CPU, motherboard, hard drives, ambient)."
  79
  80    def parse_log(self):
  81
  82        logger.debug("Starting temp section")
  83        section = Section("temperatures")
  84
  85        sensors.init()
  86
  87        systemp = Data("Sys", [])
  88        coretemp = Data("Cores", [])
  89        pkgtemp = Data("Processor", [])
  90
  91        try:
  92            for chip in sensors.iter_detected_chips():
  93                for feature in chip:
  94                    if "Core" in feature.label:
  95                        coretemp.items.append([feature.label, 
  96                            float(feature.get_value())])
  97                        continue
  98                    if "CPUTIN" in feature.label:
  99                        pkgtemp.items.append([feature.label, 
 100                            float(feature.get_value())])
 101                        continue
 102                    if "SYS" in feature.label:
 103                        systemp.items.append([feature.label, 
 104                            float(feature.get_value())])
 105                        continue
 106
 107            logger.debug("Core data is {0}".format(str(coretemp.items)))
 108            logger.debug("Sys data is {0}".format(str(systemp.items)))
 109            logger.debug("Pkg data is {0}".format(str(pkgtemp.items)))
 110            for temp_data in [systemp, coretemp, pkgtemp]:
 111                logger.debug("Looking at temp data {0}".format(
 112                    temp_data.items))
 113                if len(temp_data.items) > 1:
 114                    avg = (float(sum(feature[1] for feature in temp_data.items))
 115                    / len(temp_data.items))
 116                    logger.debug("Avg temp for {0} is {1} {2}{3}".format(
 117                        temp_data.subtitle, avg, DEG, CEL))
 118                    temp_data.subtitle += " (avg {0}{1}{2})".format(
 119                            avg, DEG, CEL)
 120                    temp_data.items = ["{0}: {1}{2}{3}".format(
 121                        feature[0], feature[1], DEG, CEL) 
 122                        for feature in temp_data.items]
 123                else:
 124                    temp_data.items = [str(temp_data.items[0][1]) + DEG + CEL]
 125                section.append_data(temp_data)
 126
 127        finally:
 128            logger.debug("Finished reading onboard temperatures")
 129            sensors.cleanup()
 130
 131
 132        # drive temp
 133
 134        # For this to work, `hddtemp` must be running in daemon mode.
 135        # Start it like this (bash):   sudo hddtemp -d /dev/sda /dev/sdX...
 136        
 137        received = ''
 138        sumtemp = 0.0 
 139        data = ""
 140        hddtemp_data = Data("Disks")
 141        
 142        client = HddtempClient(
 143            host=config.prefs.get("temperatures", "host"),
 144            port=config.prefs.getint("temperatures", "port"),
 145            sep=config.prefs.get("temperatures", "separator"),
 146            timeout=int(config.prefs.get("temperatures", "timeout")))
 147        drives = client.get_drives()
 148        logger.debug("Received drive info: " + str(drives))
 149
 150        for drive in sorted(drives, key=lambda x: x.path):
 151            if drive.path in config.prefs.get("temperatures", "drives").split():
 152                sumtemp += drive.temperature
 153                hddtemp_data.items.append(("{0} ({1})".format(
 154                    drive.path, drive.model)
 155                    if config.prefs.getboolean("temperatures", "show-model") 
 156                    else drive.path) + ": {0}{1}{2}".format(
 157                        drive.temperature, DEG, drive.units))
 158            else:
 159                drives.remove(drive)
 160                logger.debug("Ignoring drive {0} ({1}) due to config".format(
 161                    drive.path, drive.model))
 162        logger.debug("Sorted drive info: " + str(drives))
 163
 164        if not len(drives) == 0:
 165            # use units of first drive
 166            hddavg = '{0:.1f}{1}{2}'.format(
 167                    sumtemp/len(drives), DEG, drives[0].units)
 168            logger.debug("Sum of temperatures: {}; Number of drives: {}; "
 169                "=> Avg disk temp is {}".format(sumtemp, len(drives), hddavg)) 
 170            hddtemp_data.subtitle += " (avg {0}{1}{2})".format(hddavg, DEG, CEL)
 171            section.append_data(hddtemp_data)
 172
 173        logger.debug("Finished processing drive temperatures")
 174        logger.info("Finished temp section")
 175
 176        return section