GUI Creation with PowerShell (Part 2): The Notify Icon or How to make your own HDD Health Monitor

(read the Part 1 of the series here)

PowerShell opens up a lot of possibilities when it comes to task automation. And while it’s often been used to automate currently pending tasks, it can also be used for continuously monitoring things in the background.

Whether you need to monitor the utilization of a file server at work or the temperature of your hard drives at home, the principle is always the same: the tool or script should work silently and invisible in the background, until there is something you need to know.

There are a few ways to achieve that with PowerShell, one is to use a NotifyIcon and a Timer.

The Notify Icon

With the NotifyIcon class you can display an icon in the system tray, which can be equipped with a tooltip text for on demand status information:

image

and a balloon tip to get your attention when necessary:

image

You also can change the icon at any time when necessary, to further visualize a status message for example. And the best is, that the usage of the NotifyIcon class is –as you will see- very easy.

The Timer Object

It’s in the nature of a monitoring script, that most of the time it has nothing to do. It checks something, waits a little, checks it again, waits a little and so on. The easiest way to accomplish such behavior, is to use a loop and the start-sleep command.
Don’t do that.
In fact, whenever you’re using a GUI in your script, you should completely forget that the start-sleep command exists. Because this “sleep” isn’t like a little nap, its more like a deep hibernation. And as long as your script does that, your GUI will completely freeze and it won’t react to any events (remember those?), like a mouse click for example, because you decided that you would like to end the script.

A much more elegant way to let a script do stuff from time to time, is a Timer Object. You simple create it, set a time interval and whenever its time is up, an event gets raised and it executes the code you specified, e.g. calls a function. In between those times, while it does nothing, your GUI stays responsive and can react to other events. Sounds better, doesn’t it?

Alright, enough theory, lets see them in action:

Example: HDD Health Monitor

For this example we will create something that a lot of people have running on their desktops: a little HDD Health Monitor. They are usually based on the S.M.A.R.T. data, which every modern hard drive can report.

For the purpose of this script, we will focus on the hard disk temperature, but when we’re done, you will be able to easily amend it to monitor any other SMART attribute.

Reading the S.M.A.R.T. data

I originally intended to read the SMART attributes via WMI, but the trouble is, WMI reports only the plain values of all the different vendor specific attributes. Its basically just a bunch of numbers and you would need to find the right attribute among hundreds. While we could accomplish that, the real problem is, that which attributes are available and how they are implemented is drive specific. And as we’re not trying to learn the specifics of S.M.A.R.T. implementations here, i decided to simply use Smartmontools. If you don’t know it, its a free, small and easy to use command line tool to read SMART data and it does it’s job very good.

You can download it from here.

After you installed it, type “smartctl -a /dev/hda” in a new console window. The “a” in “hda” stands for the first hard drive in your system, so the second (if present) would be “hdb” and so on. Note that these letters don’t correlate in any way with the drive letters in Windows explorer.

The lower part of the output should look like this:

[...]
Vendor Specific SMART Attributes with Thresholds:
ID# ATTRIBUTE_NAME          FLAG     VALUE WORST THRESH TYPE     WHEN_FAILED RAW_VALUE
  1 Raw_Read_Error_Rate     0x000f   117   099   000    Pre-fail     -       140339126
  3 Spin_Up_Time            0x0003   091   091   000    Pre-fail     -       0
  4 Start_Stop_Count        0x0032   100   100   000    Old_age      -       271
  5 Reallocated_Sector_Ct   0x0033   100   100   000    Pre-fail     -       0
[...]
190 Airflow_Temperature_Cel 0x0022   058   052   000    Old_age      -       42 (Lifetim
e Min/Max 24/43)
194 Temperature_Celsius     0x0022   042   049   000    Old_age      -       42 (0 16 0
0)
195 Hardware_ECC_Recovered  0x001a   049   038   000    Old_age      -       140339126
197 Current_Pending_Sector  0x0012   100   100   000    Old_age      -       0
198 Offline_Uncorrectable   0x0010   100   100   000    Old_age      -       0
199 UDMA_CRC_Error_Count    0x003e   200   200   000    Old_age      -       1
[...]

The value of attribute 194 (Temperature_Celsius) is what we need, so we write a short function that filters out all the rest:
(note that we’ll execute smartctl with the parameter “-A”, not “-a” as above, as this gives you just the SMART attributes without the rest of the drive info.)

function FNreadtemp ($id) {
 
   # Execute smartctl and assign the output to a variable
 
    $smartctl ="smartctl -A /dev/hd"+$id
    $smartdata = Invoke-Expression  $smartctl
 
   # Feed the contents of the variable to the pipeline and if a line matches "Temperature_Celsius", then:
   # - Split the line at the hyphen: .split("-")
   # - Use only the last element of the resulting array: [-1]
   # - Trim the leading blanks: .trim()
   # - and finally take the first two characters of the remaining string: .substring(0,2) and return them
 
    return ((($smartdata | Where-Object {$_ -match "Temperature_Celsius"}).split("-"))[-1]).trim().substring(0,2)
}

Now we can read the temperature value of any disk in the system with the command FNreadtemp plus a or b etc.
This should work for most hard drives. If the smartctl output for your disk is different from the one above, you may need to adjust this function.

Icons

We will also need two status icons for our tool, one for normal and one high temperature. You can either go to your favorite drawing program and create them yourself, or you can select some from the tons of free ones that are available on the web. :)

I chose these two: Icon1 , Icon2 (click on “Download .ico” on each site). You can of course use whatever icons you like, but please check the license under which they are published.

Form Creation

Now lets create the form objects we need: an empty form window (necessary for the other objects), the notify icon, a context menu with a context menu entry (to have a way to end the script), the timer object and two icon objects (you need to change the path to where you put the ico files):

[void][System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")
 
$form1 = New-Object System.Windows.Forms.form
$NotifyIcon= New-Object System.Windows.Forms.NotifyIcon
$ContextMenu = New-Object System.Windows.Forms.ContextMenu
$MenuItem = New-Object System.Windows.Forms.MenuItem
$TimerHDD = New-Object System.Windows.Forms.Timer
$iconOK = New-Object System.Drawing.Icon("D:\tmp\Temperature-1.ico")
$iconWarn = New-Object System.Drawing.Icon("D:\tmp\Temperature-5.ico")

Because we won’t have any visible objects in our form, we should make sure that this empty form window is not visible. To make it disappear, we simply set its state to minimized and don’t allow it to be visible in the taskbar:

$form1.ShowInTaskbar = $false
$form1.WindowState = "minimized"

Then we set the properties for the notify icon. First we set the icon we assigned to the variable $iconOk, as the icon to use at the beginning. Because this will be our standard icon.

Next we assign the the context menu object ($ContextMenu) as the icons context menu. This context menu is invisible until we add at least one entry, in this case $MenuItem (which we will define later).

In opposite to most other form objects, the property that decides if an object is visible or not (“.visible”), is per default $false for NotifyIcon objects. So we need to set this to $true.

$NotifyIcon.Icon =  $iconOK
$NotifyIcon.ContextMenu = $ContextMenu
$NotifyIcon.contextMenu.MenuItems.AddRange($MenuItem)
$NotifyIcon.Visible = $True

Next we define the Timer object:
The time after which an event gets raised (and the assigned code get executed) is defined in the property “.interval”. Its in milliseconds, so i did set it to 300000, which means that the timer will raise the event every 5 minutes. You can change that to every value you like, just be aware that smartctl should have time to start and return the output, so I’d say at least 3 seconds. And although smartctl has normally no measurable impact on the computers resources, if you call it every other second, it quite possibly will.

Then we add the event “Tick” and add a function call, for a function we will name “FNcheckSMART”. Last we start the timer.

$TimerHDD.Interval = 300000 # (5 min)
$TimerHDD.add_Tick({FNcheckSMART})
$TimerHDD.start()

The last object we need to define is the menu item, which we already assigned to the context menu of the icon. We add the text and the event which should be raised when you click on the item. When its been raised we want it to close the form (which will usually end the script), but we need to clean up first, which means stopping the timer and removing the icon. If you don’t do that, the icon will stay there until the taskbar gets refreshed by Windows.

$MenuItem.Text = "Exit"
$MenuItem.add_Click({
   $TimerHDD.stop()
   $NotifyIcon.Visible = $False
   $form1.close()
})

Alright, thats the form, now we need a function that calls FNreadtemp for every hard disk and warn us if the temperature exceeds a certain value. The function looks like this:

function FNcheckSMART {
      $ta =  FNreadtemp a
     if ($ta -ge 48) { $warn = "HDA-Temp is "+$ta+" degrees!`n`r" }
 
      $tb = FNreadtemp b
      if ($tb -ge 48) { $warn = $warn + "HDB-Temp is "+$tb+" degrees!`n`r" }
 
    $NotifyIcon.Text = "HDD-Temp ("+(Get-Date -Format HH:mm)+") A: $ta° B: $tb°"
 
    if ($warn) {
        $NotifyIcon.Icon =  $iconWarn
      $NotifyIcon.ShowBalloonTip(30000,"Attention!",$warn,[system.windows.forms.ToolTipIcon]"Warning")
      Remove-Variable warn
    }
      else {
        $NotifyIcon.Icon =  $iconOK
      }
}

– The function calls FNreadtemp for two hard disks (a and b) as an example. If you have more or less, you need to change that here. In case the temperature is 48° (random value, set it to another value that suits your needs) or higher its creates a warning text and assigns it to $warn.

– Then the tooltip text ($NotifyIcon.Text) for the icon gets created and assigned to the icon

– If $warn does exist, the icon will be changed to the warning icon, a BalloonTip will be displayed and the variable $warn will be removed

The .ShowBalloonTip method needs four parameters (in this order): how long (in milliseconds) the balloon tip should be displayed; the title; the text message; and one of four possible ToolTipIcons.

– If after the next interval $warn shouldn’t exist anymore, the icon will be changed back to the standard icon

As this is just an example, i kept this very simple. If you’re going to use this method for your own script, depending on the importance of the warning message, you might want to add sending an email or displaying a popup. In that case, you should change the function, so that you –despite the timer interval- get an email only every hour or so . :)

Ok, finally we need to display the form and call the SMART function once, so that the icons tooltip already has data when it’s displayed first:

FNcheckSMART
[void][System.Windows.Forms.Application]::Run($form1)

And here is the whole script:

[void][System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")
 
$form1 = New-Object System.Windows.Forms.form
$NotifyIcon= New-Object System.Windows.Forms.NotifyIcon
$ContextMenu = New-Object System.Windows.Forms.ContextMenu
$MenuItem = New-Object System.Windows.Forms.MenuItem
$TimerHDD = New-Object System.Windows.Forms.Timer
$iconOK = New-Object System.Drawing.Icon("D:\tmp\Temperature-1.ico")
$iconWarn = New-Object System.Drawing.Icon("D:\tmp\Temperature-5.ico")
 
$form1.ShowInTaskbar = $false
$form1.WindowState = "minimized"
 
$NotifyIcon.Icon =  $iconOK
$NotifyIcon.ContextMenu = $ContextMenu
$NotifyIcon.contextMenu.MenuItems.AddRange($MenuItem)
$NotifyIcon.Visible = $True
 
$TimerHDD.Interval =  300000  # (5 min)
$TimerHDD.add_Tick({FNcheckSMART})
$TimerHDD.start()
 
$MenuItem.Text = "Exit"
$MenuItem.add_Click({
   $TimerHDD.stop()
   $NotifyIcon.Visible = $False
    $form1.close()
})
 
function FNreadtemp ($id) {
  $smartctl ="smartctl -A /dev/hd"+$id
  $smartdata = Invoke-Expression  $smartctl
  return ((($smartdata | ? {$_ -match "Temperature_Celsius"}).split("-"))[-1]).trim().substring(0,2)
}
 
function FNcheckSMART {
      $ta =  FNreadtemp a
    if ($ta -ge 48) { $warn = "HDA-Temp is "+$ta+" degrees!`n`r" }
 
      $tb = FNreadtemp b
      if ($tb -ge 48) { $warn = $warn + "HDB-Temp is "+$tb+" degrees!`n`r" }
 
    $NotifyIcon.Text = "HDD-Temp ("+(Get-Date -Format HH:mm)+") A: $ta° B: $tb°"
 
    if ($warn) {
        $NotifyIcon.Icon =  $iconWarn
      $NotifyIcon.ShowBalloonTip(30000,"Attention!",$warn,[system.windows.forms.ToolTipIcon]"Warning")
      Remove-Variable warn
    }
      else {
        $NotifyIcon.Icon =  $iconOK
      }
}
 
   FNcheckSMART
   [void][System.Windows.Forms.Application]::Run($form1)

That’s it for today. As you can see, with just a few simple objects you can create a useful monitoring tool.
Monitoring software, especially for enterprises, is a huge market and they are often excessively expensive. But apart from the shiny surface, what they do is often quite simple. Powershell gives you the opportunity to create your own customized monitoring tool.
If you liked this tutorial or you have any wishes for the next Windows Forms tutorial or had any trouble with this one, please drop me a comment or send me an email.

Have a nice day,
Denniver

  1. #1 by Walker on Monday - 14:38

    Great tut! And very detailed. Thanks…

  2. #3 by Greg (@Greg) on Wednesday - 14:19

    Thanks for this great tutorial. But I have a “problem” with these lines:

    $form1.ShowInTaskbar = $false
    $form1.WindowState = “minimized”

    This effectively hide the form but if explorer.exe is not started (if we start the script as a service – my case – or if explorer.exe simply crashes), the form will be visible in the bottom-left corner of the screen (you can reproduce this by killing explorer.exe and minimizing any program). And nothing chaneg after starting/restarting explorer.exe…

    I tried to play with the property “Visible” like this:

    $objForm.Visible = $false

    But it closes the form and therefore end the script…I really need to launch the script before the user login, but this window is cumbersome.

    Would you have a solution to this problem? Thank you in advance
    Greg

    • #4 by Greg (@Greg) on Wednesday - 15:19

      That’s probably not the best solution but I find a way to REALLY hide the form without desactivating it by searching form properties in PrimalForms :

      $objForm.Opacity = 0

      Thus, the form is no longer visible and even no usable (guess that 0% disable buttons, controls…)

      Any other solution will be welcome. Thanks again for this script which was my base for a system tray based application.

    • #5 by Denniver on Wednesday - 21:55

      Hi Greg,
      interesting question. I intended to write an article about that, but didn’t get around to it yet.
      The opacity-method can result in difficulties with some graphic drivers or on terminal servers etc., so i don’t recommend it.
      In general, I would say just wait until the “explorer”-process is created before you show the form (could be done with a timer object) or simply start the service with a delay.

      But if for some reason you need to do it like you did, there is another method to hide the window. Here is an example script, it completely hides the form and then shows it again after 5 seconds:

      [void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
      $Form1 = New-Object System.Windows.Forms.Form
      $form1.Add_Shown({FNhideForm})  # Raise event after Form is shown
      
      function FNhideForm {
      $window = Add-Type -memberDefinition @"
      [DllImport("user32.dll")]
      public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
      "@ -name "Win32ShowWindowAsync" -namespace Win32Functions -passThru
      
       [void] $window::ShowWindow($form1.Handle, 0) # HIDE
       sleep 5
       [void] $window::ShowWindow($form1.Handle, 1) # SHOW
      }
      
      [void][System.Windows.Forms.Application]::Run($Form1)
  3. #6 by Greg (@Greg) on Thursday - 08:57

    Hi Denniver,

    Thank you so much !!! Your code works perfectly. I wondered if I could get parts of your code, but without the DllImport. And apparently it works, we don’t even need the function FNhideForm. Here is my tests.

    This code will close immediately the form (bad) :

    $form1.Add_Shown({ $objForm.Visible = $false })
    $form1.ShowDialog()

    Whereas this code works perfectly and hides the window even when explorer.exe isn’t running :

    $form1.Add_Shown({ $objForm.Visible = $false })
    [void][System.Windows.Forms.Application]::Run($Form1)

    One line added. :) Do you think this is a good compromise?
    Greg

    • #7 by Denniver on Thursday - 16:10

      Well sometimes the solution is closer than you think. :) Btw.: this works as well:
      $objForm.Hide()

  4. #8 by Per on Sunday - 22:59

    Great guide! There are not many tutorials regarding ps and winforms ón the web. How about a tut creating a nice summery / report over something a script just done.? Maybe something in HTML with stylesheet or something. Just a idea :)

  5. #9 by Chrissy LeMaire (@cl) on Monday - 00:54

    Nice work! Exactly what I was looking for.

  6. #10 by Ciaran on Tuesday - 16:49

    Hi
    Great tutorial. I have been looking for a way to delete an icon placed in the Notify tray by an application I’m installing. Is there a way to read the icons in the notify tray and select one for deletion?
    Thanks
    Ciarán

  1. Episode 156 – Brandon Shell on The Coud, Bunnies and VDI « PowerScripting Podcast
  2. GUI Creation with PowerShell: The Basics « BYTECOOKIE
  3. Summary of 2nd Dutch PowerShell User Group – DuPSUG meeting with additional resources | blog.bjornhouben.com
  4. GUI Creation with PowerShell
  5. Creating Pop-ups by Using PowerShell - Hey, Scripting Guy! Blog - Site Home - TechNet Blogs

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: