jbromley /bin/sysmon.rb

Ruby system monitor for use with dzen2
#!/usr/bin/env ruby
# System monitor that dumps stats to standard out.
# This script is meant to be used with dzen and other programs that read from
# standard input and display meters, etc.
# Copyright (C) 2008 by J. Bromley 
# $Id: sysmon.rb,v 0d77dcc54282 2008/07/19 05:23:26 jbromley $

require 'logger'

LOGGER = Logger.new('/tmp/sysmon.log')
LOGGER.info "INIT"
LOGGER.level = Logger::DEBUG

# configuration ========================================================
config = {}
config[:interval] = 1

config[:cpu_usage]  = { }
config[:cpu_usage][:interval] = 1

config[:cpu_frequency] = { }
config[:cpu_frequency][:interval] = 1

config[:cpu_temperature] = { }
config[:cpu_temperature][:interval] = 6

config[:battery] = { }
config[:battery][:interval] = 6
config[:battery][:low_action] =
  'zenity --warning --text "The battery is getting low."'
config[:battery][:critical_action] =
  'zenity --error --text "The battery is critically low."'

config[:tpvol] = { }
# config[:tpvol][:interval] = 1

config[:xkb] = { }
# config[:xkb][:interval] = 1

config[:clock] = { }
config[:clock][:interval] = 30
config[:clock][:format] = "%a %D %I:%M %P"

# helper functions -----------------------------------------------------
def image(file)
  "^fg(Orange)^i(/home/jay/images/xbm8x8/#{file})^fg()"
end

def image_nocolor(file)
  "^i(/home/jay/images/xbm8x8/#{file})"
end

# base monitor class ---------------------------------------------------
class Monitor
  def initialize(config)
    @interval = config[:interval] || 1
    @output = ''
    @last_update = Time.at(0)
  end
  def update()
    now = Time.new
    if now - @last_update >= @interval
      @output = update_stats()
      @last_update = now
    end
    return @output
  end
end

# cpu information ------------------------------------------------------
class CpuUsage < Monitor
  def initialize(config)
    super(config)
    @cpu_keys = [:user, :system, :nice, :idle]
    @prev_stats = Hash.new { |h, k| h[k] = 0.0 }
    @cpu_keys = [:user, :system, :nice, :idle].each { |k| @prev_stats[k] }
    @output = "#{image('cpu.xbm')} --%"
  end
  def update_stats()
    load = '--'
    begin
      cpu_data = IO.readlines('/proc/stat').grep(/^cpu\s+/).first.split
      stats =  @cpu_keys.zip(cpu_data[1..4]).inject({}) do |h, v|
        h[v.first] = v.last.to_i
        h
      end
      dtotal = @cpu_keys.inject(0) do |accum, key|
        accum += (stats[key] - @prev_stats[key])
        accum
      end
      if dtotal > 0.0
        load = "%02d" % (100 - ((stats[:idle] - @prev_stats[:idle]) * 100 / dtotal))
      end
      @prev_stats.merge!(stats)
      return "#{image('cpu.xbm')} #{load}%"
    rescue StandardError
      LOGGER.error "#{$!.class}: #{$!.message}"
    end
  end
end

class CpuFrequency < Monitor
  def initialize(config)
    super(config)
  end
  def update_stats()
    freq = '---MHz'
    begin
      freq = IO.readlines('/proc/cpuinfo').grep(/cpu MHz/).first.split[-1].sub(/\..*$/,'')
    rescue StandardError
      LOGGER.error "#{$!.class}: #{$!.message}"
    end
    return "#{freq}MHz"
  end
end

class CpuTemperature < Monitor
  def initialize(config)
    super(config)
    @temp_file = config[:temperature_file] || '/proc/acpi/thermal_zone/THM0/temperature'
  end
  def update_stats()
    temperature = '--C'
    begin
      temp_data = IO.read(@temp_file).split
      temperature = "#{temp_data[1]}#{temp_data[2]}"
    rescue StandardError
      LOGGER.error "#{$!.class}: #{$!.message}"
    end
    return temperature
  end
end

# battery_info ---------------------------------------------------------
class BatteryMonitor < Monitor
  def initialize(config)
    super(config)
    @state_file = config[:state_file] || '/proc/acpi/battery/BAT0/state'
    @info_file = config[:info_file] || '/proc/acpi/battery/BAT0/info'
    @level_low = config[:low] || 5
    @low_action = config[:low_action] ||
      'echo "Low battery" | xmessage -center -buttons quit:0 -default quit -file -'
    @level_critical = config[:critical] || 2
    @critical_action = config[:critical_action] ||
      'echo "Critical battery" | xmessage -center -buttons quit:0 -default quit -file -'
    @warned_low = false
    @warned_critical = false
    @output = '?---%[-:--]?'
  end
  def update_stats()
    state = IO.readlines(@state_file)
    info = IO.readlines(@info_file)
    present = info[0].gsub(/.*:\s*/,'').chomp
    output = "#{image('bat_empty_01.xbm')} N/A"
    if present == "yes"
      remaining_cap = state[4].gsub(/.*:\s*/,'').chomp.chomp("mWh").to_f
      full_cap = info[2].gsub(/.*:\s*/,'').chomp.chomp("mWh").to_f
      batt_percent = ((remaining_cap / full_cap) * 100).to_i
      batt_percent = 100 if batt_percent > 100
      charging_state = state[2].gsub(/.*:\s*/,'').chomp

      # Take action in case battery is low/critical
      if charging_state == "discharging" && batt_percent <= @level_critical
        unless @warned_critical
          LOGGER.info "Warning about critical battery."
          system("ssid #{@critical_action} &")
          @warned_critical = true
        end
      elsif charging_state == "discharging" && batt_percent <= @level_low
        unless @warned_low
          LOGGER.info "Warning about low battery."
          system("ssid #{@low_action} &")
          @warned_low = true
        end
      else
        @warned_low = false
        @warned_critical = false
      end

      # Calculate remaining time for discharge/charge.
      time_remaining = '-:--'
      present_rate = state[3].gsub(/.*:\s*/,'').chomp.chomp("mW").to_f
      if charging_state == 'discharging'
        time_left =  remaining_cap / present_rate if present_rate > 0
      elsif charging_state == 'charging'
        time_left = (full_cap - remaining_cap) / present_rate if present_rate > 0
      end
      if time_left
        hours = time_left.to_i
        minutes = (time_left % 1 * 60).to_i
        time_remaining = "%d:%02d" % [hours, minutes]
      end

      # Set charging state character
      charging_state = '=' if charging_state == "charged" ||
        (charging_state == "discharging" && batt_percent >= 97)
      charging_state = '+' if charging_state == "charging"
      charging_state = '-' if charging_state == "discharging"

      # Check for the AC adapter state
      ac_state = IO.read('/proc/acpi/ac_adapter/AC/state')[/on-line/]
      if ac_state == "on-line"
        ac_state = image_nocolor('ac_01.xbm')
      else
        ac_state = '-'
      end

      # Select a battery icon based on battery level
      case
        when batt_percent <= @level_critical
        icon = image('bat_empty_01.xbm')
        when batt_percent <= @level_low
        icon = image('bat_low_01.xbm')
        else
        icon = image('bat_full_01.xbm')
      end
      output= "#{icon} #{charging_state}#{batt_percent}%[#{time_remaining}]#{ac_state}"
    end
    return output
  end
end

# tp_volume ----------------------------------------------------------
class ThinkPadVolume < Monitor
  def initialize(config)
    super(config)
    @mixers = config[:mixer] || 'Master'
    @mixers = [@mixers] if !(Array === @mixers)
    @output = '--%'
  end

  def update_stats()
    if @mixers.empty?
      return "vol: off"
    end
    status = ''
    @mixers.reverse.each do |mixer| # show status of first mixer in list
      status = IO.read('/proc/acpi/ibm/volume')
    end
    volume_abs = status[/^level:\s*(\d+)/, 1].to_i
    if status[/on/]
      volume = 'muted'
      icon = image('spkr_02.xbm')
    else
      volume = "#{volume_abs * 100 / 14}%"
      icon = image('spkr_01.xbm')
    end
    return "#{icon} #{volume}".strip
  end
end

# xkeyboard ------------------------------------------------------------
require 'ruby-wmii/xkb'
class XkbMonitor < Monitor
  def initialize(config)
    super(config)
    begin
      @xkb = Xkb::XKeyboard.new
    rescue StandardError
      LOGGER.error "#{$!.class}: #{$!.message}"
    end
    @output = "#{image('info_02.xbm')} --"
  end
  def update()
    layout = @xkb.current_group_symbol
    return "#{image('info_02.xbm')} #{layout}"
  end
end

# clock ----------------------------------------------------------------
class DateTimeMonitor < Monitor
  def initialize(config)
    super(config)
    @time_format = config[:format]
  end
  def update()
    icon = image('clock.xbm')
    datetime = Time.new.strftime(@time_format)
    "#{icon} #{datetime}"
  end
end

# Main loop ============================================================
interval = config[:interval] || 1
monitors = []
monitors.push(CpuUsage.new(config[:cpu_usage]))
monitors.push(CpuFrequency.new(config[:cpu_frequency]))
monitors.push(CpuTemperature.new(config[:cpu_temperature]))
monitors.push(BatteryMonitor.new(config[:battery]))
monitors.push(ThinkPadVolume.new(config[:tpvol]))
monitors.push(XkbMonitor.new(config[:xkb]))
monitors.push(DateTimeMonitor.new(config[:clock]))

loop do
  output = monitors.inject('') do |out, m|
    out += m.update + " "
  end
  output += image('arch_10x10.xbm')
  begin
    puts output
    STDOUT.flush
  rescue StandardError
    LOGGER.error "#{$!.class}: #{$!.message}"
    exit!
  end
  sleep interval
end