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