Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Read temperature from hwmon devices #204

Merged
merged 1 commit into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions landscape/client/monitor/tests/test_temperature.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
from landscape.client.monitor.temperature import Temperature
from landscape.client.tests.helpers import LandscapeTest
from landscape.client.tests.helpers import MonitorHelper
from landscape.lib.tests.test_sysstats import ThermalZoneTest
from landscape.lib.tests.test_sysstats import SysfsThermalZoneTest


class TemperatureTestWithSampleData(ThermalZoneTest, LandscapeTest):
class TemperatureTestWithSampleData(SysfsThermalZoneTest, LandscapeTest):
"""Tests for the temperature plugin."""

helpers = [MonitorHelper]
Expand Down
62 changes: 28 additions & 34 deletions landscape/lib/sysstats.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import glob
import os.path
import struct
import time
Expand Down Expand Up @@ -95,13 +96,14 @@ def get_uptime(uptime_file="/proc/uptime"):

def get_thermal_zones(thermal_zone_path=None):
if thermal_zone_path is None:
if os.path.isdir("/sys/class/thermal"):
thermal_zone_path = "/sys/class/thermal"
if os.path.isdir("/sys/class/hwmon"):
thermal_zone_path = "/sys/class/hwmon/*/temp*_input"
elif os.path.isdir("/sys/class/thermal"):
thermal_zone_path = "/sys/class/thermal/*/temp"
else:
thermal_zone_path = "/proc/acpi/thermal_zone"
if os.path.isdir(thermal_zone_path):
for zone_name in sorted(os.listdir(thermal_zone_path)):
yield ThermalZone(thermal_zone_path, zone_name)
thermal_zone_path = "/proc/acpi/thermal_zone/*/temperature"
for temperature_path in sorted(glob.glob(thermal_zone_path)):
yield ThermalZone(temperature_path)


class ThermalZone:
Expand All @@ -110,37 +112,29 @@ class ThermalZone:
temperature_value = None
temperature_unit = None

def __init__(self, base_path, name):
self.name = name
self.path = os.path.join(base_path, name)
temperature_path = os.path.join(self.path, "temp")
if os.path.isfile(temperature_path):
try:
with open(temperature_path) as f:
line = f.readline()
try:
self.temperature_value = int(line.strip()) / 1000.0
self.temperature_unit = "C"
self.temperature = "{:.1f} {}".format(
self.temperature_value,
self.temperature_unit,
)
except ValueError:
pass
except OSError:
pass
else:
temperature_path = os.path.join(self.path, "temperature")
if os.path.isfile(temperature_path):
for line in open(temperature_path):
if line.startswith("temperature:"):
self.temperature = line[12:].strip()
try:
def __init__(self, temperature_path):
self.path = os.path.dirname(temperature_path)
self.name = os.path.basename(self.path)
try:
with open(temperature_path) as f:
if os.path.basename(temperature_path) == "temperature":
for line in f:
if line.startswith("temperature:"):
self.temperature = line[12:].strip()
value, unit = self.temperature.split()
self.temperature_value = int(value)
self.temperature_unit = unit
except ValueError:
pass
break
else:
line = f.readline()
self.temperature_value = int(line.strip()) / 1000.0
self.temperature_unit = "C"
self.temperature = "{:.1f} {}".format(
self.temperature_value,
self.temperature_unit,
)
except (ValueError, OSError):
pass


class LoginInfo:
Expand Down
144 changes: 128 additions & 16 deletions landscape/lib/tests/test_sysstats.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
import re
import unittest
from datetime import datetime
from unittest import TestCase, mock

from landscape.lib import testing
from landscape.lib.sysstats import BootTimes
Expand Down Expand Up @@ -44,7 +44,7 @@
class BaseTestCase(
testing.TwistedTestCase,
testing.FSTestCase,
unittest.TestCase,
TestCase,
):
pass

Expand Down Expand Up @@ -146,13 +146,14 @@ def test_valid_uptime_file(self):
class ProcfsThermalZoneTest(BaseTestCase):
def setUp(self):
super().setUp()
self.thermal_zone_path = self.makeDir()
self.base_path = self.makeDir()
self.thermal_zone_path = os.path.join(self.base_path, "*/temperature")

def get_thermal_zones(self):
return list(get_thermal_zones(self.thermal_zone_path))

def write_thermal_zone(self, name, temperature):
zone_path = os.path.join(self.thermal_zone_path, name)
zone_path = os.path.join(self.base_path, name)
if not os.path.isdir(zone_path):
os.mkdir(zone_path)
file = open(os.path.join(zone_path, "temperature"), "w")
Expand All @@ -161,6 +162,14 @@ def write_thermal_zone(self, name, temperature):


class GetProcfsThermalZonesTest(ProcfsThermalZoneTest):
@mock.patch("glob.glob")
@mock.patch("os.path.isdir")
def test_default_thermal_zone_path(self, isdir_mock, glob_mock):
isdir_mock.side_effect = (lambda dir: dir == "/proc/acpi/thermal_zone")
thermal_zones = list(get_thermal_zones(None))
self.assertEqual(thermal_zones, [])
glob_mock.assert_called_with("/proc/acpi/thermal_zone/*/temperature")

def test_non_existent_thermal_zone_directory(self):
thermal_zones = list(get_thermal_zones("/non-existent/thermal_zone"))
self.assertEqual(thermal_zones, [])
Expand All @@ -179,7 +188,7 @@ def test_one_thermal_zone(self):
self.assertEqual(thermal_zones[0].temperature_unit, "C")
self.assertEqual(
thermal_zones[0].path,
os.path.join(self.thermal_zone_path, "THM0"),
os.path.join(self.base_path, "THM0"),
)

def test_two_thermal_zones(self):
Expand Down Expand Up @@ -212,10 +221,7 @@ def test_badly_formatted_with_missing_space(self):

def test_temperature_file_with_missing_label(self):
self.write_thermal_zone("THM0", "SOMETHINGBAD")
temperature_path = os.path.join(
self.thermal_zone_path,
"THM0/temperature",
)
temperature_path = os.path.join(self.base_path, "THM0/temperature")
file = open(temperature_path, "w")
file.write("bad-label: foo bar\n")
file.close()
Expand All @@ -226,24 +232,33 @@ def test_temperature_file_with_missing_label(self):
self.assertEqual(thermal_zones[0].temperature_unit, None)


class ThermalZoneTest(BaseTestCase):
class SysfsThermalZoneTest(BaseTestCase):
def setUp(self):
super().setUp()
self.thermal_zone_path = self.makeDir()
self.base_path = self.makeDir()
self.thermal_zone_path = os.path.join(self.base_path, "*/temp")

def get_thermal_zones(self):
return list(get_thermal_zones(self.thermal_zone_path))

def write_thermal_zone(self, name, temperature):
zone_path = os.path.join(self.thermal_zone_path, name)
zone_path = os.path.join(self.base_path, name)
if not os.path.isdir(zone_path):
os.mkdir(zone_path)
file = open(os.path.join(zone_path, "temp"), "w")
file.write(temperature)
file.close()


class GetSysfsThermalZonesTest(ThermalZoneTest):
class GetSysfsThermalZonesTest(SysfsThermalZoneTest):
@mock.patch("glob.glob")
@mock.patch("os.path.isdir")
def test_default_thermal_zone_path(self, isdir_mock, glob_mock):
isdir_mock.side_effect = (lambda dir: dir == "/sys/class/thermal")
thermal_zones = list(get_thermal_zones(None))
self.assertEqual(thermal_zones, [])
glob_mock.assert_called_with("/sys/class/thermal/*/temp")

def test_non_existent_thermal_zone_directory(self):
thermal_zones = list(get_thermal_zones("/non-existent/thermal_zone"))
self.assertEqual(thermal_zones, [])
Expand All @@ -262,7 +277,7 @@ def test_one_thermal_zone(self):
self.assertEqual(thermal_zones[0].temperature_unit, "C")
self.assertEqual(
thermal_zones[0].path,
os.path.join(self.thermal_zone_path, "THM0"),
os.path.join(self.base_path, "THM0"),
)

def test_two_thermal_zones(self):
Expand All @@ -288,7 +303,7 @@ def test_non_int_temperature(self):
self.assertEqual(thermal_zones[0].temperature_unit, "C")
self.assertEqual(
thermal_zones[0].path,
os.path.join(self.thermal_zone_path, "THM0"),
os.path.join(self.base_path, "THM0"),
)

def test_badly_formatted_temperature(self):
Expand All @@ -301,7 +316,104 @@ def test_badly_formatted_temperature(self):

def test_read_error(self):
self.write_thermal_zone("THM0", "50000")
temperature_path = os.path.join(self.thermal_zone_path, "THM0/temp")
temperature_path = os.path.join(self.base_path, "THM0/temp")
os.chmod(temperature_path, 0o200) # --w-------
thermal_zones = self.get_thermal_zones()
self.assertEqual(len(thermal_zones), 1)
self.assertEqual(thermal_zones[0].temperature, None)
self.assertEqual(thermal_zones[0].temperature_value, None)
self.assertEqual(thermal_zones[0].temperature_unit, None)


class HwmonThermalZoneTest(BaseTestCase):
def setUp(self):
super().setUp()
self.base_path = self.makeDir()
self.thermal_zone_path = os.path.join(self.base_path, "*/temp*_input")

def get_thermal_zones(self):
return list(get_thermal_zones(self.thermal_zone_path))

def write_thermal_zone(self, name, number, temperature):
zone_path = os.path.join(self.base_path, name)
if not os.path.isdir(zone_path):
os.mkdir(zone_path)
file = open(os.path.join(zone_path, f"temp{number}_input"), "w")
file.write(temperature)
file.close()


class GetHwmonThermalZonesTest(HwmonThermalZoneTest):
@mock.patch("glob.glob")
@mock.patch("os.path.isdir")
def test_default_thermal_zone_path(self, isdir_mock, glob_mock):
isdir_mock.side_effect = (lambda dir: dir == "/sys/class/hwmon")
thermal_zones = list(get_thermal_zones(None))
self.assertEqual(thermal_zones, [])
glob_mock.assert_called_with("/sys/class/hwmon/*/temp*_input")

def test_non_existent_thermal_zone_directory(self):
thermal_zones = list(get_thermal_zones("/non-existent/thermal_zone"))
self.assertEqual(thermal_zones, [])

def test_empty_thermal_zone_directory(self):
self.assertEqual(self.get_thermal_zones(), [])

def test_one_thermal_zone(self):
self.write_thermal_zone("THM0", "1", "50000")
thermal_zones = self.get_thermal_zones()
self.assertEqual(len(thermal_zones), 1)

self.assertEqual(thermal_zones[0].name, "THM0")
self.assertEqual(thermal_zones[0].temperature, "50.0 C")
self.assertEqual(thermal_zones[0].temperature_value, 50.0)
self.assertEqual(thermal_zones[0].temperature_unit, "C")
self.assertEqual(
thermal_zones[0].path,
os.path.join(self.base_path, "THM0"),
)

def test_three_thermal_zones(self):
self.write_thermal_zone("THM0", "1", "50000")
self.write_thermal_zone("THM0", "2", "51000")
self.write_thermal_zone("THM1", "1", "52000")
thermal_zones = self.get_thermal_zones()
self.assertEqual(len(thermal_zones), 3)
self.assertEqual(thermal_zones[0].temperature, "50.0 C")
self.assertEqual(thermal_zones[0].temperature_value, 50.0)
self.assertEqual(thermal_zones[0].temperature_unit, "C")
self.assertEqual(thermal_zones[1].temperature, "51.0 C")
self.assertEqual(thermal_zones[1].temperature_value, 51.0)
self.assertEqual(thermal_zones[1].temperature_unit, "C")
self.assertEqual(thermal_zones[2].temperature, "52.0 C")
self.assertEqual(thermal_zones[2].temperature_value, 52.0)
self.assertEqual(thermal_zones[2].temperature_unit, "C")

def test_non_int_temperature(self):
self.write_thermal_zone("THM0", "1", "50432")
thermal_zones = self.get_thermal_zones()
self.assertEqual(len(thermal_zones), 1)

self.assertEqual(thermal_zones[0].name, "THM0")
self.assertEqual(thermal_zones[0].temperature, "50.4 C")
self.assertEqual(thermal_zones[0].temperature_value, 50.432)
self.assertEqual(thermal_zones[0].temperature_unit, "C")
self.assertEqual(
thermal_zones[0].path,
os.path.join(self.base_path, "THM0"),
)

def test_badly_formatted_temperature(self):
self.write_thermal_zone("THM0", "1", "SOMETHING BAD")
thermal_zones = self.get_thermal_zones()
self.assertEqual(len(thermal_zones), 1)
self.assertEqual(thermal_zones[0].temperature, None)
self.assertEqual(thermal_zones[0].temperature_value, None)
self.assertEqual(thermal_zones[0].temperature_unit, None)

def test_read_error(self):
self.write_thermal_zone("THM0", "1", "50000")
temperature_path = os.path.join(self.base_path, "THM0/temp1_input")
os.chmod(temperature_path, 0o200) # --w-------
thermal_zones = self.get_thermal_zones()
self.assertEqual(len(thermal_zones), 1)
Expand Down
18 changes: 9 additions & 9 deletions landscape/sysinfo/tests/test_temperature.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import os

from landscape.lib.tests.test_sysstats import ThermalZoneTest
from landscape.lib.tests.test_sysstats import HwmonThermalZoneTest
from landscape.sysinfo.sysinfo import SysInfoPluginRegistry
from landscape.sysinfo.temperature import Temperature


class TemperatureTest(ThermalZoneTest):
class TemperatureTest(HwmonThermalZoneTest):
def setUp(self):
super().setUp()
self.temperature = Temperature(self.thermal_zone_path)
Expand All @@ -16,31 +16,31 @@ def test_run_returns_succeeded_deferred(self):
self.assertIs(None, self.successResultOf(self.temperature.run()))

def test_run_adds_header(self):
self.write_thermal_zone("THM0", "51000")
self.write_thermal_zone("THM0", "1", "51000")
self.temperature.run()
self.assertEqual(
self.sysinfo.get_headers(),
[("Temperature", "51.0 C")],
)

def test_ignores_bad_files(self):
self.write_thermal_zone("THM0", "")
temperature_path = os.path.join(self.thermal_zone_path, "THM0/temp")
self.write_thermal_zone("THM0", "1", "")
temperature_path = os.path.join(self.base_path, "THM0/temp1_input")
file = open(temperature_path, "w")
file.write("bad-label: 51 C")
file.close()
self.temperature.run()
self.assertEqual(self.sysinfo.get_headers(), [])

def test_ignores_unknown_formats(self):
self.write_thermal_zone("THM0", "FOO")
self.write_thermal_zone("THM0", "1", "FOO")
self.temperature.run()
self.assertEqual(self.sysinfo.get_headers(), [])

def test_picks_highest_temperature(self):
self.write_thermal_zone("THM0", "51000")
self.write_thermal_zone("THM1", "53000")
self.write_thermal_zone("THM2", "52000")
self.write_thermal_zone("THM0", "1", "51000")
self.write_thermal_zone("THM0", "2", "53000")
self.write_thermal_zone("THM1", "1", "52000")
self.temperature.run()
self.assertEqual(
self.sysinfo.get_headers(),
Expand Down
Loading