1#!/usr/local/bin/perl
2
3=head1 NAME
4
5upsmonpro_ - Munin plugin to monitor Powercom UPS via UPSMON PRO program L<http://www.pcm.ru/support/soft/>
6
7=head1 INSTALLATION
8
9  /etc/munin/plugins/upsmonpro_load -> /usr/share/munin/plugins/upsmonpro_
10  /etc/munin/plugins/upsmonpro_status -> /usr/share/munin/plugins/upsmonpro_
11  /etc/munin/plugins/upsmonpro_temp -> /usr/share/munin/plugins/upsmonpro_
12  /etc/munin/plugins/upsmonpro_voltage -> /usr/share/munin/plugins/upsmonpro_
13
14=head1 CONFIGURATION
15
16Environment variables:
17
18  host - UPSMON PRO server host, default localhost
19  port - UPSMON PRO port, default 2601
20
21Example configuration (optional):
22
23  [upsmonpro_*]
24  env.host localhost
25  env.port 2601
26
27=head1 MAGIC MARKERS
28
29  #%# family=auto
30  #%# capabilities=autoconf
31  #%# capabilities=suggest
32
33=head1 AUTHOR
34
35Copyright (C) 2017 pru.mike@gmail.com
36
37=head1 LICENSE
38
39GPLv2.
40
41=cut
42
43use strict;
44use warnings;
45use feature qw/say/;
46use Munin::Plugin;
47use IO::Socket::INET;
48use Time::HiRes qw/usleep/;
49use English qw/-no-math-vars/;
50our $VERSION = '0.0.1';
51$OUTPUT_AUTOFLUSH++;
52
53our $DEFAULT_HOST = 'localhost';
54our $DEFAULT_PORT = 2601;
55our %TYPES        = (
56  voltage => [qw/input output/],
57  load    => [qw/battery_load battery_capacity/],
58  temp    => [q/temp/],
59  status  => [qw/power_failure low_battery voltage_status ups_status  battery_test/]
60);
61our @TYPES = sort keys %TYPES;
62my %DISPATCH_TABLE = ();
63my $pkg            = __PACKAGE__;
64
65$DISPATCH_TABLE{"${pkg}::run_suggest"}  = \&run_suggest;
66$DISPATCH_TABLE{"${pkg}::run_autoconf"} = \&run_autoconf;
67for my $t (@TYPES) {
68  $DISPATCH_TABLE{"${pkg}::run_config_$t"}   = \&{"run_config_$t"};
69  $DISPATCH_TABLE{"${pkg}::run_autoconf_$t"} = \&run_autoconf;
70  $DISPATCH_TABLE{"${pkg}::run_default_$t"}  = sub {
71    run_default(@{ $TYPES{$t} });
72  };
73}
74
75find_key($ARGV[0])->();
76
77sub find_key {
78  my $argv = shift || '';
79  my $type_re = join '|', @TYPES;
80  my $key;
81  if ($argv =~ /(suggest|autoconf)/i) {
82    $key = 'run_' . lc($1);
83  } elsif ($Munin::Plugin::me =~ /upsmonpro_{1,}($type_re)$/) {
84    my $graph = $1;
85    $key = 'run_' . ((grep { $argv eq $_ } qw/autoconf config/) ? $argv : 'default') . "_$graph";
86  } else {
87    die "Could not determine script type [@TYPES] ? name=$Munin::Plugin::me\n";
88  }
89
90  return $DISPATCH_TABLE{"${pkg}::$key"};
91}
92
93sub run_config_voltage {
94  print <<'END';
95graph_title UPS Input/Output Voltage
96graph_vlabel volt
97graph_scale no
98graph_category sensors
99input.label input
100input.info Input Voltage
101input.type GAUGE
102output.label output
103output.info Output Voltage
104output.type GAUGE
105END
106  exit(0);
107}
108
109sub run_config_temp {
110  print <<'END';
111graph_title UPS Temperature
112graph_vlabel celsius
113graph_scale no
114graph_category sensors
115temp.label temperature
116temp.type GAUGE
117END
118  exit(0);
119}
120
121sub run_config_load {
122  print <<'END';
123graph_title UPS Battery Load/Capacity
124graph_vlabel percent (%)
125graph_scale no
126graph_category sensors
127battery_load.label battery_load
128battery_load.type GAUGE
129battery_capacity.label battery_capacity
130battery_capacity.type GAUGE
131END
132  exit(0);
133}
134
135sub run_config_status {
136  print <<'END';
137graph_title UPS Statuses
138graph_vlabel status
139graph_scale no
140graph_category sensors
141power_failure.label power_failure
142power_failure.type GAUGE
143low_battery.label low_battery
144low_battery.type GAUGE
145voltage_status.label voltage_status
146voltage_status.info 0 normal, 1 boost, 2 buck
147voltage_status.type GAUGE
148ups_status.label ups_status
149ups_status.type GAUGE
150battery_test.label battery_test
151battery_test.type GAUGE
152END
153  exit(0);
154}
155
156sub run_default {
157  my $host = $ENV{host} || $DEFAULT_HOST;
158  my $port = $ENV{port} || $DEFAULT_PORT;
159  my @val_list = @_;
160  my $r = gather_data($host, $port);
161  for (@val_list) {
162    die "Wrong value: $_" if not exists $r->{$_};
163    say "${_}.value $r->{$_}";
164  }
165}
166
167sub run_suggest {
168  local $LIST_SEPARATOR = "\n";
169  say "@TYPES";
170  exit(0);
171}
172
173sub run_autoconf {
174  if (gather_data($DEFAULT_HOST, $DEFAULT_PORT)->{response} eq 'ok') {
175    say "yes";
176  } else {
177    say "no ($DEFAULT_HOST:$DEFAULT_PORT not response)";
178  }
179  exit(0);
180}
181
182sub gather_data {
183  my $host = shift;
184  my $port = shift;
185  my %data = map { ($_ => 'U') } map { @{ $TYPES{$_} } } @TYPES;
186  $data{response} = 'failed';
187
188  my $sock = IO::Socket::INET->new(
189    PeerAddr => $host,
190    Proto    => 'udp',
191    PeerPort => $port,
192    Blocking => 0
193  ) or die "Cannot create socket: $@";
194
195  my $pattern = pack 'AAAACAAA', split //, 'PCMG1END';
196  $sock->send($pattern) or die "send to $host: $!";
197  usleep(200);
198  my $data;
199  my $buf;
200  while ($sock->read($buf, 32)) {
201    $data .= $buf;
202  }
203  if (defined $data and $data =~ /^PCMR.*END$/) {
204    my @data = unpack('C*', $data);
205    %data = (
206      response         => 'ok',
207      input            => $data[6] + 256 * $data[5],
208      output           => $data[8] + 256 * $data[7],
209      battery_load     => $data[11],
210      battery_capacity => $data[12],
211      temp             => $data[15],
212      power_failure    => ($data[18] ? 1 : 0),
213      low_battery      => ($data[19] ? 1 : 0),
214
215      #voltage_status: 0 = normal, 3 = buck, 2 = boost
216      voltage_status => ($data[17] == 0 ? 0 : ($data[17] == 3 ? 2 : 1)),
217      ups_status     => ($data[21]      ? 1 : 0),
218      battery_test   => ($data[23]      ? 1 : 0),
219    );
220  }
221  return \%data;
222}
223