#! /usr/bin/perl -w # Author: Ton Hospel # Gnu copyleft use 5.006; use strict; use Getopt::Long; use Config::General; # use Data::Dumper; my $VERSION = "0.05"; &Getopt::Long::config("bundling", "require_order"); my $sys_mount = "/sys"; my $config_file = "/etc/psensors.conf"; my $alarm = "ALARM"; # Old version when files were called things like in_input0 # my $input_format = "%s%s_input%d"; # New version when files are called things like in0_input my $input_format = "%s%s%d_input"; my ($version, $unsafe, $help, $mount); GetOptions("file=s" => \$config_file, "f=s" => \$config_file, "mount=s" => \$mount, "m=s" => \$mount, "version!" => \$version, "unsafe!" => \$unsafe, "U" => \$unsafe, "help!" => \$help, "h" => \$help) || die "Could not parse your command line"; if ($version) { print<<"EOF"; psensors (Ton Utils) $VERSION EOF exit 0; } if ($help) { require Config; $ENV{PATH} .= ":" unless $ENV{PATH} eq ""; $ENV{PATH} = "$ENV{PATH}$Config::Config{'installscript'}"; exec("perldoc", "-F", $unsafe ? "-U" : (), $0) || exit 1; # make parser happy %Config::Config = (); } die "$0 (currently) doesn't take arguments\n" if @ARGV; my %sensor_unit = (in => [1/1000, "%6.2f V"], temp => [1/1000, "%6.2f°C"], cur => [1/1000, "%6.2f A"], fan => [1, "%4d RPM"], fan_div => [1, "%3d "], ); sub get_line { my $file = shift; open(my $fh, "<", $file) || die "Could not open $file: $!"; defined (my $line = <$fh>) || die "unexpected EOF reading from $file"; defined(<$fh>) && die "Unexpected extra lines in $file"; chomp($line); return $line; } sub i2c_devices { my @devices; my $dir = $sys_mount; $dir =~ s|/*\z|/bus/i2c/devices/|; opendir(my $dhandle, $dir) || die "could not open $dir: $!\n"; while (defined(my $f = readdir($dhandle))) { push @devices, { name => get_line("$dir$f/name"), container => get_line("$dir$f/../name"), id => $f, dir => "$dir$f/", } unless $f eq "." || $f eq ".."; } return \@devices; } # For the moment only bother with linear formulas sub coef { my $formula = shift; my $value = 0; my $y0 = eval $formula; $value = 1e6; my $div = eval($formula)-$y0 || die "Formula $formula is a constant or not linear\n"; $value /= $div; return [$value, -$value*$y0] } { my %coef; sub invert { my ($formula, $value) = @_; return $value if $formula eq ""; my $coef = $coef{$formula} ||= coef($formula); return $coef->[1]*$value + $coef->[0]; } } { my $expression; $expression = qr{\s*-?(?:\((??{$expression})\)|\d+(?:\.\d*)?|\@)\s*(?:[*/+-](??{$expression}))*}; sub evaluate { return undef unless defined(my $str = shift); return "" if $str eq ""; $str =~ /^$expression$/ || die "'$str' is not a valid expression\n"; if ($str =~ /\@/) { die "Unexpected \@ placeholder\n" unless shift; $str =~ s/\@/\$value/g; return $str; } else { die "No \@ placeholder\n" if shift; return eval $str; } } } # Remove a string from $hash sub get_string { my ($hash, $name, $default, $override) = @_; if (defined(my $str = delete $hash->{$name})) { ref($str) eq "" || die "The value for '$name' is a reference, not a string\n"; return $override && defined($default) ? $default : $str; } die "Missing mandatory option '$name'\n" if @_ < 3; return $default; } # Remove an evaluated formula from $hash sub get_value { my ($hash, $name, $default, $override) = @_; if (defined(my $str = delete $hash->{$name})) { ref($str) eq "" || die "The value for '$name' is a reference, not a numeric value\n"; return evaluate($override && defined($default) ? $default : $str); } die "Missing mandatory option '$name'\n" if @_ < 3; return evaluate($default); } # Remove an unevaluated formula from $hash sub get_formula { my ($hash, $name, $default, $override) = @_; if (defined(my $str = delete $hash->{$name})) { ref($str) eq "" || die "The value for '$name' is a reference, not a formula\n"; return evaluate($override && defined($default) ? $default : $str, 1); } die "Missing mandatory option '$name'\n" if @_ < 3; return evaluate($default, 1); } # Remove a hash reference from $hash sub get_hash { my ($hash, $name, $default) = @_; if (defined(my $h = delete $hash->{$name})) { ref($h) eq "HASH" || die "The value for '$name' is '$h', not a hash\n"; return $h; } die "Missing mandatory option '$name'\n" if @_ < 3; return $default; } # parse a section sub parse_sensors { my $sensors = shift; my (%sensors, $error); for my $sensor (keys %$sensors) { eval { my $options = $sensors->{$sensor}; my %options; $options{formula} = get_formula($options, "compute", ""); $options{hyst} = get_value($options, "hysteresis", undef); $options{div} = get_value($options, "divisor", undef); $options{min} = get_value($options, "min", undef); $options{max} = get_value($options, "max", undef); if (defined($options{max})) { die "Minimum $options{min} is actually bigger than maximum $options{max}\n" if defined($options{min}) && $options{min} > $options{max}; die "Hysteresis $options{hyst} is actually bigger than maximum $options{max}\n" if defined($options{hyst}) && $options{hyst} > $options{max}; } $options{label} = get_string($options, "label", $sensor); die "Unknown option(s): ", join(",", keys %$options),"\n" if %$options; $sensors{$sensor} = \%options; }; $error .= "sensor '$sensor': $@" if $@; } die $error if $error; return \%sensors; } # parse a section sub parse_chips { my $chips = shift; my (%chips, $error); for my $chip (keys %$chips) { eval { my $options = $chips->{$chip}; my %options; $options{sensor} = parse_sensors(get_hash($options, "sensor", {})); die "Unknown option(s): ", join(",", keys %$options),"\n" if %$options; $chips{$chip} = \%options; }; $error .= "chip '$chip': $@" if $@; } die $error if $error; return \%chips; } # parse section sub parse_groups { my $groups = shift; my $error; for my $group (keys %$groups) { eval { my $options = $groups->{$group}; my @options = ($sensor_unit{$group}[0] || 1, $sensor_unit{$group}[1] || "%3d "); $options[0] = get_value( $options, "factor", $options[0]); $options[1] = get_string($options, "format", $options[1]); $sensor_unit{$group} = \@options; }; $error .= "group '$group': $@" if $@; } die $error if $error; } # Display the stats for $device as configured by $conf sub show { my ($device, $conf) = @_; print "Chip: $device->{name} in $device->{container}\n"; my $dir = $device->{dir}; my $sensors = $conf->{sensor}; # 5 = 1 current + 4 possible attributes my @len = (0) x 5; my @end = (1e50) x 5; for (keys %$sensors) { my $sensor = $sensors->{$_}; if (my ($name, $num) = /^([a-z]+)_(\d+)$/i) { my $unit = $sensor_unit{$name} || die "Unknown sensor type $name\n"; my $value = get_line(sprintf($input_format, $dir, $name, $num)) * $unit->[0]; $value = eval $sensor->{formula} if $sensor->{formula} ne ""; $sensor->{val}[0] = ["$sensor->{label}: ", sprintf($unit->[1], $value) ]; if (defined $sensor->{min}) { push @{$sensor->{val}}, ["min= ", sprintf($unit->[1], $sensor->{min})]; $sensor->{alarm}++ if $value < $sensor->{min}; } if (defined $sensor->{hyst}) { push @{$sensor->{val}}, ["hyst.= ", sprintf($unit->[1], $sensor->{hyst})]; } if (defined $sensor->{max}) { push @{$sensor->{val}}, ["max= ", sprintf($unit->[1], $sensor->{max})]; $sensor->{alarm}++ if $value > $sensor->{max}; } if (defined $sensor->{div}) { push @{$sensor->{val}}, ["div= ", sprintf($sensor_unit{fan_div}[1], $sensor->{div} * $sensor_unit{fan_div}[0])]; } if (@{$sensor->{val}} > 1) { $sensor->{val}[1][0] = "($sensor->{val}[1][0]"; $sensor->{val}[$_][1] =~ s/(\s*)$/$1,/ for 1..$#{$sensor->{val}}-1; $sensor->{val}[-1][1] =~ s/(\s*)$/\)$1/; } for (0..$#{$sensor->{val}}) { $sensor->{val}[$_][1] =~ s/^\s*//; my $l = length($sensor->{val}[$_][0])+ length($sensor->{val}[$_][1]); $len[$_] = $l if $len[$_] < $l; $sensor->{val}[$_][1] =~ /\s*\z/; $end[$_] = $+[0]-$-[0] if $+[0]-$-[0] < $end[$_]; } } else { die "Unhandled sensor type '$_'\n"; } } my $format = " "; # Initial indent -1 for (0..$#len) { my $l = $len[$_]-$end[$_]; $format .= $l > 0 ? " %-.${l}s" : "%.0s"; } $format .= "%s\n"; # The ALARM string my $last = ""; for (sort keys %$sensors) { /^([a-z]+)/; if ($1 ne $last) { print "\n" if $last; $last = $1; } my $sensor = $sensors->{$_}; my $val = $sensor->{val}; printf($format, map({ if ($val->[$_]) { my $spaces = " " x ($len[$_]- length($val->[$_][0])- length($val->[$_][1])); "$val->[$_][0]$spaces$val->[$_][1]"; } else { ""; } } 0..$#len), $sensor->{alarm} ? " $alarm" : ""); } } my $config = eval { Config::General->new (-file => $config_file, -LowerCaseNames => "yes", -AllowMultiOptions => "no", -IncludeRelative => "yes", -UseApacheInclude => "yes"); }; if ($@) { $@ =~ s/\n at .*\n\z/\nTry $0 -h for help\n/; die $@; } my %args = $config->getall; $alarm = get_string(\%args, "alarmstring", $alarm); my $chips = parse_chips(get_hash(\%args, "chip", {})); parse_groups(get_hash(\%args, "group", {})); $sys_mount = defined($mount) ? $mount : get_string(\%args, "sysmount", $sys_mount); die "Unknown option(s): ", join(",", keys %args), "\n" if %args; my $devices = i2c_devices; die "No i2c devices found\n" unless @$devices; my $matches = 0; for my $chip (keys %$chips) { my $match = $chip; $match =~ s/(\W)/$1 eq "*" ? ".*" : $1 eq "?" ? "." : "\\$1"/eg; $match = qr/^$match$/i; # print "Match=$match\n"; for (@$devices) { if ($_->{name} =~ $match) { show($_, $chips->{$chip}); $matches++; } } } die("None of the i2c devices (", join(", ", map $_->{name}, @$devices), ") has a matching config entry in $config_file\n") unless $matches; __END__ =head1 NAME psensors - Reporting sensors information on Linux using sysfs =head1 SYNOPSIS psensors [-f config_file] [--file config_file] [-m mount_point] [--mount mount_point] psensors --version psensors [-U] [--unsafe] [-h] [--help] =head1 OPTIONS =over 4 =item -f config_file, --file config_file Use the given file as config file. Default is F =item -m mount_point, --mount mount_point Assume sys is mounted on mount_point. Defaults to F. If explicitely given, it will override any entry in the configuration file. =item -h, --help Show this help. =item -U, --unsafe Allow even root to run the perldoc. Remember, the reason this is off by default is because it B unsafe. =item --version Print version info. =back =head1 EXAMPLE CONFIGURATION FILE # Elite's K7s5a mainboard # # format "%8.2f V" # Label VCore Min 1.75*0.95 Max 1.75*1.05 Label Vcc+2.5V Min 2.4 Max 2.6 Label Vcc+3.3V Min 3.3*0.95 Max 3.3*1.05 Label + 5V Min 5.0*0.95 Max 5.0*1.05 Compute ((6.8/10)+1)*@ Label +12V Min 12*0.95 Max 12*1.05 Compute ((30/10)+1)*@ Label -12V Max -12*0.95 Min -12*1.05 Compute -(36/10)*@ Label - 5V Max -5*0.95 Min -5*1.05 Compute -(56/10)*@ Label SB 5V Min 5*0.95 Max 5*1.05 Compute ((6.8/10)+1)*@ Label V battery Min 3.3*0.95 Max 3.3*1.05 Label System Hysteresis 35 Max 40 Label CPU Hysteresis 55 Max 75 Label CPU fan Min 3000 Divisor 2 Which on my system can cause running F to output: Chip it87 CPU fan: 4115 RPM (min= 3000 RPM, div= 2) VCore: 1.74 V (min= 1.66 V, max= 1.84 V) Vcc+2.5V: 2.48 V (min= 2.40 V, max= 2.60 V) Vcc+3.3V: 3.23 V (min= 3.13 V, max= 3.46 V) + 5V: 4.91 V (min= 4.75 V, max= 5.25 V) +12V: 11.68 V (min= 11.40 V, max= 12.60 V) -12V: -12.20 V (min= -12.60 V, max= -11.40 V) - 5V: -4.98 V (min= -5.25 V, max= -4.75 V) SB 5V: 4.94 V (min= 4.75 V, max= 5.25 V) V battery: 3.37 V (min= 3.13 V, max= 3.46 V) System: 33.00°C (hyst.= 35.00°C, max= 40.00°C) CPU: 46.00°C (hyst.= 55.00°C, max= 75.00°C) =head1 CONFIGURATION FILE The configuration file is read using the L perl module, whose exact format is described in the L. Look there for all the possible ways to specify things. Basically it is the well known apache format. Sections get started with
and ended by
. Sections can nest to any level. Attributes for a given section (or the toplevel) are entered by giving a keyword followed by the attribute value, leading and trailing spaces are ignored, but you can give a value inside quotes. Section- and attribute names are case insensitive. Comments can be shell style, started by C<#> and continueing to the end of the line or in C-style, starting with C and ending with C<*/>. You can include files by using: include SomeConfig The recognized top level attributes are: =over 4 =item SysMount The mountpoint of the sys filesystem. Defaults to F and can be overridden from the commandline =item AlarmString The string used on a line with an alarm. Defaults to C. =back The top level sections are: =over 4 =item Chip match This describes what to display for a particular chip and how to display it. Match is a case insensitive wildcarded string (? representing a single character and * any amount of any characters), and compared to all chips announced in sysfs (as represented by their F file). If there is a match, the corresponding device will be displayed under control of this section. The body will consist of a number of sensor descriptors, one for each one you are interested in. So the only subsection is: =over 4 =item Sensor Type_Num A Type_Num string will represent sensor number Num of type Type. The builtin supported types are: =over 4 =item in Represents a voltage. You typically declare minimum and maximum values. =item temp Represents a temperature. You typically declare a hysteresis and a maximum. =item cur Represents a current. You typically declare minimum and maximum values. =item fan Represents a fan speed. You typically declare a minimum value and a divisor. =back If you have other devices in there, you can declare how they are to be displayed using a > section. The attributes you can set inside a Sensor section are: =over 4 =item label The string displayed i front of the values for this sensor. Defaults to the Sensor Section name. =item compute Quite often a sensor really reports some value derived from what we are actually interested in. A high voltage for example is often scaled using a few external resistors. To get the value we are actually interested in, a little calculation may be needed. For example: actual V = (Vmeasured * (1 + Rin/Rf)) - (Vref * (Rin/Rf)) and for -12V: Rin/Rf = 6.68 Vref = 4.096V So we get: actual V = 7.67 * Vmeasured - 27.36 This attribute represents the actual value using C<@>, so the configuration line will be: Compute 7.67 * @ - 27.36 =item min Represents the minimum acceptable value for this sensor. If it goes below that, an alarm should be raised. Ignored if not given. If L is also given, the minimum should be below the maximum =item max Represents the maximum acceptable value for this sensor. If it goes below that, an alarm should be raised. Ignored if not given. =item hysteresis On some sensors once a (hardware) alarm is raised, it remains on until the value drops below the hysteresis. This is typically what's used for temperature sensors. If L is also given, the hysteresis should be below the maximum Ignored if not given. The current code only does its own alarm detection, so this value effectively always gets ignored. =item divisor The divisor applied to the clock which measures the fan speed. RPM = (60 * 22500) / (count * divisor) The divisor is already accounted for in the RPM calculation, so changing the divisor does not change the reproted fan speed. It only impacts the possible resolution. Note that most chips only support fan divisors of 1, 2, 4, and 8. Ignored if not given. Note that the current code does no setting, so for now this attribute is pretty useless. =back =back =item Group sensor_type Used to declare how to represent sensors not (currently) built in (please report such cases to the author). You can also use it to override the values used for the builtin sensor types. Sensor_type represents the sensor family. Attributes can be: =over 4 =item factor formula The values reported in sysfs are often scaled. This gives the multiplication factor to be applied. Defaults to 1 =item format string For presentation the value is sent through a standard perl L. Here you can say which format string should be used. Notice that the value presentation columns are aligned on the end of the strings, so you can push a value to the left by adding spaces at the end. Leading spaces get squeezed out in the display, so they are irrelevant. Defaults to C<"%3d "> =back For example, if voltage reporting was not built in, you could make it available using: # sysfs prefix used to report voltages is "in" factor 1/1000 # sysfs value / 1000 is the actual voltage format "%6.2f V" # 2 digits after the period, followed by a V =back =head1 BUGS None known. =head1 AUTHOR Ton Hospel (psensors@ton.iguana.be) =head1 SEE ALSO L, L =cut