PowerShell: Fun with Weather Underground's API (part 2)

PowerShell: Fun with Weather Underground's API (part 2)

In the last post we learned how to use Weather Underground's API to resolve a city name and get data back from it. Once we get those two URLs we can use them to get data back from the API.  I also tied this into my LIFX light, which I posted about here.

Requirements

Setup

[cmdletbinding()]
param(
    [string]
    $city = (Read-Host "City?"),
    [string]
    $forecast = 'forecast',
    [boolean]
    $sendEmail,
    [boolean]
    $lifx,
    [boolean]
    $sendAlertEmail
)

if (!$foregroundColor) {$foregroundColor = 'green'}
$baseURL      = 'http://api.wunderground.com/api/'
$apiKey       = 'yourAPIKey'
$acceptHeader = 'application/json'

#"password" | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString | Out-File .\emlpassword.txt

$emailPass    = Get-Content .\emlpassword.txt | ConvertTo-SecureString  
$weatherList  = Get-Content .\emails.txt
$alertList    = Get-Content .\alertEmails.txt
$emailUser    = 'yourEmailUsername'
$emailFrom    = 'youremail@service.com'
$smtpServer   = 'smtp.gmail.com'
$smtpPort     = '587'

The Setup

  • Name: Get-Weather.ps1
  • Variables:
    • $baseURL = (Weather Underground's base URL for the API, should be fine with default).
    • $apiKey = Your API key that you get from signing up.
    • $acceptHeader = Header/formatting settings (default is good).
    • $emailPass = Set your password by running the commented line above it. This will be user/machine encrypted.
    • $weatherList = Create a text file (named emails.txt) in the script directory that contains a list of people to email the forecasts to. 
    • $alertList  = Create a text file (named alertEmails.txt) in the script directory that contains a list of people to email/text alerts to. I made this different so I could setup text message alerts.
    • $emailUser = Your user name for the email address we're sending from.
    • $emailFrom  = Your from email address.
    • $smtpServer  = Your SMTP server. Default is Google's.
    • $smtpPort = Your SMTP server's port. This is Google's default.
  • Parameters
    • city (will prompt for one if left blank)
    • forecast (Will default to 4 day which is the value 'forecast')
      Values:
      • forecast  (4 day)
      • hourly (next 36 hours)
      • camera (emails an email address a random webcam from the city specified)
    • sendEmail (accepts $true, defaults to $false) This is for sending the email forecast.
    • lifx (accepts $true, defaults to $false) This is for LIFX light integration. Currently it will flash your LIFX light red when there is an alert. More coming soon with this!
    • sendAlertEmail (accepts $true, defaults to $false) This is to send an email (which I use the carrier's email to text address to text myself) if an alert is found.

The first function: Get-Weather

This function will first use the auto-complete API and then from there create the URLs and use them to get the weather data. It will also return the API results at the end. That way you can run the script like this:

$weatherChicago = .\Get-Weather.ps1 -city Chicago

And it will return what the API returns, as well as display some information:

The variable $weatherChicago will contain what the API returned. 

With this information you can really drill down and see what is available. It was by doing this that I got the initial ideas for parts of this script. Here's an example of the information returned via:

$weatherChicago.current_observation

After the function finds the city and displays the basic info, it then moves forward to check which forecast was specified and if that forecast is to be emailed. 

Get-Weather function code

function Get-Weather {
    param()
   
    $findMe = $city
    $find   = Invoke-RestMethod -Uri "http://autocomplete.wunderground.com/aq?query=$findMe"

    if ($find) {
        
        $cityAPI  = $find.Results[0].l
        $city     = $find.Results[0].name
        
        $fullURL  = $baseURL + $apiKey + "/features/conditions/hourly/forecast/webcams/alerts" + "$cityAPI.json"
        $radarURL = "http://api.wunderground.com/api/$apiKey/animatedradar/animatedsatellite" + "$cityAPI.gif?num=6&delay=50&interval=30"
        
        Write-Host `n"API URLS for $city" -foregroundcolor $foregroundColor
        Write-Host `t$fullURL
        Write-Host `t$radarURL
        
        $weatherForecast = Invoke-RestMethod -Uri $fullURL -ContentType $acceptHeader
        
        $currentCond     = $weatherForecast.current_observation
        
        Write-Host `n"Current Conditions for: $city" -foregroundColor $foregroundColor
        Write-Host $currentCond.Weather 
        Write-Host "Temperature:" $currentCond.temp_f"F"
        Write-Host "Winds:" $currentCond.wind_string
        
        $curAlerts = $weatherForecast.alerts 
        
        if ($curAlerts) {
            
            if ($lifx) { Get-LIFXAPI -action flashcolor -brightness 1 -color Red -state on }
            
            $typeName = Get-WeatherFunction -Weather 'alert' -value $weatherForecast.alerts
            
            $alertDate  = $curAlerts.date
            $alertExp   = $curAlerts.expires 
            $alertMsg   = $curAlerts.message
            
            Write-Host `n"Weather Alert! ($typeName)" -foregroundcolor Red
            Write-Host "Date: $alertDate Expires: $alertExp"
            Write-Host "$alertMsg"    
            
            if ($sendAlertEmail) {

                Foreach ($email in $alertList) {
                    
                    Send-WeatherEmail -to $email -Subject "Weather Alert!" -Body "Alert Type: $typeName City: $city Message: $alertMsg"
                
                }
            }                  

        } 
        
    }

    Switch ($forecast) {
    
        {$_ -eq 'hourly'} {
 
            if ($sendEmail) {

                $hourlyForecast = $weatherForecast.hourly_forecast
                
                $body = "<p></p>"
                $body += "<p>Here is your hourly forecast!</p>"
                
                $selCam   = Get-Random $weatherForecast.webcams.count
                
                $camImg   = $weatherforecast.webcams[$selCam].CURRENTIMAGEURL
                $camName  = $weatherForecast.webcams[$selCam].linktext
                $camLink  = $weatherForecast.webcams[$selCam].link
                
                $body += "<p>Random webcam shot from: <a href=`"$camLink`">$camName</a></p>"
                $body += "<p><img src=`"$camImg`"></p>"                 
                                
                $body += "<p>$city Radar:</p>"
                $body += "<p><img src=`"$radarURL`"></p>"  
                
                if ($curAlerts) {
                    
                    $body += "<p><b><font color=`"red`">Weather Alert! ($typeName)</font></b></p>"
                    $body += "<p>Date: $alertDate Expires: $alertExp</p>"
                    $body += "<p>$alertMsg</p>"    
    
                }           
                
                foreach ($hour in $hourlyForecast) {
                    
                    $body += "<p></p>"
                    $body += "<p></p>"
                    
                    $prettyTime       = $hour.fcttime.pretty
                    $hourTemp         = $hour.temp.english  
                    $hourImg          = $hour.icon_url
                    
                    [int]$hourChill   = $hour.windchill.english
                    
                    if ($hourChill -eq -9999) {
                    
                        $hourChilltxt = 'N/A'
                        
                    } else {
                        
                        $hourChilltxt = $hourChill.ToString() + 'F'
                   
                    }
                                        
                    $hourWind         = $hour.wspd.english
                    $windDir          = $hour.wdir.dir
                    $hourUV           = $hour.uvi
                    $dewPoint         = $hour.dewpoint.english
                    $hourFeels        = $hour.feelslike.english
                    $hourHum          = $hour.humidity
                    $conditions       = $hour.condition
                    [int]$hourPrecip  = $hour.pop
                    
                    $popText = Get-WeatherFunction -Weather 'preciptext' -value $hourPrecip
                    
                    $body += "<p><b>$prettyTime</b></p>"
                    $body += "<p><img src=`"$hourImg`">$conditions</p>"
                    $body += "<p>Chance of precipitation: $hourPrecip% / $popText</p>"
                    $body += "<p>Current Temp: $hourTemp`F Wind Chill: $hourChilltxt Feels Like: $hourFeels`F</p>"
                    $body += "<p>Dew Point: $dewPoint</p>"
                    $body += "<p>Wind Speed: $hourWind`mph Direction: $windDir</p>"
                    $body += "<p>Humidity: $hourHum%</p>"
                    $body += "<p>UV Index: $hourUV"     
                    
                }
                
                foreach ($email in $weatherList) {Send-WeatherEmail -To $email -Subject "Your hourly forecast for $city" -body $body}
            
            }            
        
        }
        
        {$_ -eq 'forecast'} {                
              
            if ($sendEmail) {

                $todayForecast = $weatherForecast.forecast.simpleforecast.forecastday
                
                $body = "<p></p>"
                $body += "<p>Here is your 4 day forecast!</p>"
                
                $selCam   = Get-Random $weatherForecast.webcams.count
                
                $camImg   = $weatherforecast.webcams[$selCam].CURRENTIMAGEURL
                $camName  = $weatherForecast.webcams[$selCam].linktext
                $camLink  = $weatherForecast.webcams[$selCam].link
                
                $body += "<p>Random webcam shot from: <a href=`"$camLink`">$camName</a></p>"
                $body += "<p><img src=`"$camImg`"></p>"                 
                
                $body += "<p>$city Radar:</p>"
                $body += "<p><img src=`"$radarURL`"></p>"      
                
                $curAlerts = $weatherForecast.alerts 
                
                if ($curAlerts) {
                    
                    $body += "<p><b><font color=`"red`">Weather Alert! ($typeName)</font></b></p>"
                    $body += "<p>Date: $alertDate Expires: $alertExp</p>"
                    $body += "<p>$alertMsg</p>"    

                }                   
               
                foreach ($day in $todayForecast) {
                    
                    $body += "<p></p>"
                    $body += "<p></p>"
                    
                    $dayImg          = $day.icon_url
                    $dayMonth        = $day.date.monthname
                    $dayDay          = $day.date.day
                    $dayName         = $day.date.weekday
                    $dayHigh         = $day.high.fahrenheit  
                    $dayLow          = $day.low.fahrenheit
                    $maxWind         = $day.maxwind.mph
                    $aveWind         = $day.avewind.mph
                    $aveHum          = $day.avehumidity
                    $conditions      = $day.conditions
                    [int]$dayPrecip  = $day.pop
                    
                    $popText = Get-WeatherFunction -Weather 'preciptext' -value $dayPrecip
                    
                    $body += "<p><b>$dayName, $dayMonth $dayDay</b></p>"
                    $body += "<p><img src=`"$dayImg`">$conditions</p>"
                    $body += "<p>Chance of precipitation: $dayPrecip% / $popText</p>"
                    $body += "<p>High: $dayHigh`F Low: $dayLow`F</p>"
                    $body += "<p>Ave Winds: $aveWind`mph Max Winds: $maxWind`mph</p>"
                    $body += "<p>Humidity: $aveHum%</p>"
         
                }

                foreach ($email in $weatherList) {Send-WeatherEmail -To $email -Subject "Your 4 day forecast for $city" -body $body}
            
            }  
            
        }

        {$_ -eq 'camera'} {
            
            $selCam    = Get-Random $weatherForecast.webcams.count
            
            $camImg    = $weatherforecast.webcams[$selCam].CURRENTIMAGEURL
            $camName   = $weatherForecast.webcams[$selCam].linktext
            $camLink   = $weatherForecast.webcams[$selCam].link
            
            $fileExt   = $camImg.SubString($camImg.LastIndexOf("."),4)
            
            $cityShort = $city.substring(0,$city.lastindexof(","))
            
            $fileName  = $cityShort + $fileExt
            
            $location  = (Get-Location).Path
            
            $camFile   = Invoke-WebRequest -Uri $camImg -OutFile "$location\$fileName"   
            
            $file = $location + "\" + $fileName
            
            $gallery = 'YourSquareSpaceGalleryOrEmailToSendAttachmentTo' 
            
            $SMTPClient             = New-Object Net.Mail.SmtpClient($SmtpServer, 587)
            $SMTPMessage            = New-Object System.Net.Mail.MailMessage($emailFrom,$gallery,"$city cam","$city cam")
            $SMTPClient.Credentials = New-Object System.Net.NetworkCredential($emailUser,$emailPass)
            
            $att                    = New-Object Net.Mail.Attachment($file)
            $SMTPClient.EnableSsl   = $true
           
            Switch (($fileName.substring($fileName.LastIndexOf(".")+1)).tolower()) {
                
                {$_ -like "*png*"} {
                
                    $typeExt = 'png'    
                    
                }
                
                {$_ -like "*jpg*"} {
                
                    $typeExt = 'jpg'     
                    
                }
                
                {$_ -like "*gif*"} {
                    
                    $typeExt = 'gif' 
                    
                }                                
                
            }
            
            $SMTPMessage.Attachments.Add($att)
            ($smtpmessage.Attachments).contenttype.mediatype = "image/$typeExt"
            
            $SMTPClient.Send($SMTPMessage)
            
            $att.Dispose()
            $SMTPMessage.Dispose()
            
            Remove-Item $file
            
        }
    
    } 
    
    Return $weatherForecast

}

Let's take a look at some of the code

The 'camera' option uses its own email sending commands since I had to change the MIME type of the attachment. There is also a variable used to specify where to send the email to. This variable is within the switch for now (until I clean it up!) as $gallery. You'll want to change that to an email address you want to receive the attached webcam shot.

 For some reason when sending an image file PowerShell sends it with type information stating it is an application. The following code is what I used to fix that. I noticed this issue when it wouldn't post to SquareSpace unless I first sent it to my Gmail, and then forwarded it to SquareSpace. Once I forwarded it I noticed it would post. It was then I decided to take a look at the full email details and saw the difference in attachment types.

That led me to write the following section of code so I could send the random webcam shot to the gallery and have it post.

{$_ -eq 'camera'} {
            
            $selCam    = Get-Random $weatherForecast.webcams.count
            
            $camImg    = $weatherforecast.webcams[$selCam].CURRENTIMAGEURL
            $camName   = $weatherForecast.webcams[$selCam].linktext
            $camLink   = $weatherForecast.webcams[$selCam].link
            
            $fileExt   = $camImg.SubString($camImg.LastIndexOf("."),4)
            
            $cityShort = $city.substring(0,$city.lastindexof(","))
            
            $fileName  = $cityShort + $fileExt
            
            $location  = (Get-Location).Path
            
            $camFile   = Invoke-WebRequest -Uri $camImg -OutFile "$location\$fileName"   
            
            $file = $location + "\" + $fileName
            
            $gallery = 'your SquareSpace gallery URL or Email address to send pictures to' 
            
            $SMTPClient             = New-Object Net.Mail.SmtpClient($SmtpServer, 587)
            $SMTPMessage            = New-Object System.Net.Mail.MailMessage($emailFrom,$gallery,"$city cam","$city cam")
            $SMTPClient.Credentials = New-Object System.Net.NetworkCredential($emailUser,$emailPass)
            
            $att                    = New-Object Net.Mail.Attachment($file)
            $SMTPClient.EnableSsl   = $true
           
            Switch (($fileName.substring($fileName.LastIndexOf(".")+1)).tolower()) {
                
                {$_ -like "*png*"} {
                
                    $typeExt = 'png'    
                    
                }
                
                {$_ -like "*jpg*"} {
                
                    $typeExt = 'jpg'     
                    
                }
                
                {$_ -like "*gif*"} {
                    
                    $typeExt = 'gif' 
                    
                }                                
                
            }
            
            $SMTPMessage.Attachments.Add($att)
            ($smtpmessage.Attachments).contenttype.mediatype = "image/$typeExt"
            
            $SMTPClient.Send($SMTPMessage)
            
            $att.Dispose()
            $SMTPMessage.Dispose()
            
            Remove-Item $file
            
        }

The above code downloads the file, looks at what type of image it is, and then changes the type accordingly. After that it constructs an email object, attaches the file, send the email, and finally removes the file.

The next function is Send-WeatherEmail

function Send-WeatherEmail {
    [cmdletbinding()]
    param(
    [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
    [string]
    $To,
    
    [string]
    [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
    $Subject,
    
    [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
    $Body,
    
    [string]
    $attachment
    )

    if (!$to)      {Write-Error "No recipient specified";break}
    if (!$subject) {Write-Error "No subject specified";break}
    if (!$body)    {Write-Error "No body specified";break}
   
    $gmailCredential = New-Object System.Management.Automation.PSCredential($emailUser,$emailPass)
       
    Send-MailMessage -To $to -From $emailFrom -Body $body -BodyAsHtml:$true -Subject $Subject -SmtpServer $smtpServer -Port $smtpPort -UseSsl -Credential $gmailCredential
 
}

This function is a short and sweet function that wraps up Send-MailMessage so you can use it throughout the script easily.

Now onto the function Get-WeatherFunction

function Get-WeatherFunction {
    [cmdletbinding()]
    param(
        [string]
        $weather,
        $value       
    )
    
    Switch ($weather) {
        
        {$_ -eq 'preciptext'} {
        
            Switch ($value) {
                
                {$_ -lt 20} {
                    
                    $popText = 'No mention'
                    
                }
                
                {$_ -eq 20} {
                    
                    $popText = 'Slight Chance'
                    
                }
                
                {($_ -lt 50 -and $_ -gt 20)} {
                    
                    $popText = 'Chance'
                    
                }
                
                {$_ -eq 50} {
                    
                    $popText = 'Good chance'
                    
                }
                
                {($_ -lt 70 -and $_ -gt 50)} {
                    
                    $popText = 'Likely'
                    
                }
                
                {$_ -ge 70} {
                    
                    $popText = 'Extremely likely'
                    
                }
                
            }
            
            Return $popText
            
        }   
        
        {$_ -eq 'alert'}    {
            
            Switch ($curAlerts.type) {
                
                'HEA' {$typeName = 'Heat Advisory'}
                'TOR' {$typeName = 'Tornado Warning'}
                'TOW' {$typeName = 'Tornado Watch'}
                'WRN' {$typeName = 'Severe Thunderstorm Warning'}
                'SEW' {$typeName = 'Severe Thunderstorm Watch'}
                'WIN' {$typeName = 'Winter Weather Advisory'}
                'FLO' {$typeName = 'Flood Warning'}
                'WAT' {$typeName = 'Flood Watch / Statement'}
                'WND' {$typeName = 'High Wind Advisory'}
                'SVR' {$typeName = 'Severe Weather Statement'}
                'HEA' {$typeName = 'Heat Advisory'}
                'FOG' {$typeName = 'Dense Fog Advisory'}
                'SPE' {$typeName = 'Special Weather Statement'}
                'FIR' {$typeName = 'Fire Weather Advisory'}
                'VOL' {$typeName = 'Volcanic Activity Statement'}
                'HWW' {$typeName = 'High Wind Warning'}
                'REC' {$typeName = 'Record Set'}
                'REP' {$typeName = 'Public Reports'}
                'PUB' {$typeName = 'Public Information Statement'}
                    
            }
                
            Return $typeName
    
        }    
    }
    
}

This is perhaps one of the gnarliest functions I've ever named. I'll have to rethink the name, perhaps. For now, let's look at what it does. 

Currently it will take the parameter $weather and take different actions based on the input. If you specify 'preciptext' it will return the precipitation chance in text format based on the number it is given. For example if you run: 

Get-WeatherFunction -Weather 'preciptext' -value 71

It will return 'Extremely Likely'.

If you specify 'alert' then you'll be able to provide the 3 letter alert code Weather Underground returns via the API and return the full alert text. For example: 

Get-WeatherFunction -Weather 'alert' -value 'HEA'

This would return 'Heat Advisory'.

The final function: Get-LIFXApi

function Get-LIFXApi {
[cmdletbinding()]
param(
    [string]
    $action = 'setstate',
    [string]
    $state = 'on',
    [string]
    $color = 'white',
    [double]
    $brightness = '0.4'
)

$apiKey          = 'YourLifxApiKey'
$base64Key       =  [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(($apiKey)))
$headers         =  @{Authorization=("Basic {0}" -f $base64Key)}
$allURL          = 'https://api.lifx.com/v1/lights/all' 
$acceptheader    = 'application/json'
$baseURL         = 'https://api.lifx.com/v1/lights/'
$foregroundColor = 'white'
[array]$colors   = @('white','red','orange','yellow','cyan','green','blue','purple','pink')

$ninjaLights     = Invoke-RestMethod -headers $headers -Uri $allURL

function Change-LightState {
    [cmdletbinding()]
    param(
        [string]
        $state,
        [string]
        $color,
        [double]
        $brightness,
        [string]
        $selector
    )
    
    $ninjaID  = $ninjaLights.id
    $selector = 'id:' + $ninjaID
    $fullURL  = $baseurl + $selector + '/state'

    $payloadBuilder = "{
                    `"power`": `"$state`",
                    `"color`": `"$color`",
                    `"brightness`" : $brightness 
                    }"

    Write-Host "Changing light to:" -ForegroundColor $foregroundcolor
    Write-Host `t"State     :" $state
    Write-Host `t"Color     :" $color
    Write-Host `t"Brightness:" ($brightness * 100)"%" `n

    $stateReturn = Invoke-RestMethod -headers $headers -uri $fullURL -Method Put -Body $payloadBuilder
    
    Write-Host "API status:" -ForegroundColor $foregroundcolor
    Write-Host `t"Light :" $stateReturn.results.label
    Write-Host `t"Status:" $stateReturn.results.status `n
    
    
}

Switch ($action) {
    
    'setstate' { Change-LightState -state $state -color $color -brightness $brightness -selector $selector }
    
    'fluxtest' { 
        
        $originalBrightness = $ninjaLights.Brightness
        $originalColor      = $ninjalights.Color
        $originalState      = $ninjalights.Power
        $colorString        = "hue:" + $originalcolor.hue + " saturation:" + $originalcolor.saturation + " kelvin:" + $originalColor.Kelvin       
          
        $i = 0
        
        While ($i -le 20) {
            
            $color              = Get-Random $colors
            [double]$brightness = "{0:n2}" -f (Get-Random 0.99)
 
            Change-LightState -color $color -brightness $brightness -selector $selector -state $state
            Start-Sleep -seconds 1
            $i++
            
        }
        
        Change-LightState -state $originalState -color $colorString -brightness $originalBrightness -selector $selector  
        
    } 
    
    'flashcolor' {
        
        $originalBrightness = $ninjaLights.Brightness
        $originalColor      = $ninjalights.Color
        $originalState      = $ninjalights.Power
        $colorString        = "hue:" + $originalcolor.hue + " saturation:" + $originalcolor.saturation + " kelvin:" + $originalColor.Kelvin
        
        Change-LightState -state $state -color $color -brightness $brightness -selector $selector
        Start-Sleep -Seconds 1
        
        Change-LightState -state $originalState -color $colorString -brightness $originalBrightness -selector $selector
        Start-Sleep -Seconds 1
                
        Change-LightState -state $state -color $color -brightness $brightness -selector $selector
        Start-Sleep -Seconds 1
        
        Change-LightState -state $originalState -color $colorString -brightness $originalBrightness -selector $selector
        Start-Sleep -Seconds 1        
        
        Change-LightState -state $state -color $color -brightness $brightness -selector $selector
        Start-Sleep -Seconds 1
        
        Change-LightState -state $originalState -color $colorString -brightness $originalBrightness -selector $selector
                        
    }
}    
    
    
}

I wrote the above function as a script initially when I learned that LIFX provides and API you can use to control your light! It is currently called if the parameter lifx is $true, and if there is a current alert.

if ($lifx) { Get-LIFXAPI -action flashcolor -brightness 1 -color Red -state on }

It then performs the switch option 'flashcolor' which flashes the light red at 100%. It then returns the light to whatever setting it was set to before it started flashing. To get this function to work you'll need to change $apiKey to your LIFX Cloud API key. For more details on working with the LIFX API, check out my post here.

Full code

[cmdletbinding()]
param(
    [string]
    $city = (Read-Host "City?"),
    [string]
    $forecast = 'forecast',
    [boolean]
    $sendEmail,
    [boolean]
    $lifx,
    [boolean]
    $sendAlertEmail
)

if (!$foregroundColor) {$foregroundColor = 'green'}
$baseURL      = 'http://api.wunderground.com/api/'
$apiKey       = 'yourAPIKey'
$acceptHeader = 'application/json'

#"password" | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString | Out-File .\emlpassword.txt

$emailPass    = Get-Content .\emlpassword.txt | ConvertTo-SecureString  
$weatherList  = Get-Content .\emails.txt
$alertList    = Get-Content .\alertEmails.txt
$emailUser    = 'yourEmailUsername'
$emailFrom    = 'youremail@service.com'
$smtpServer   = 'smtp.gmail.com'
$smtpPort     = '587'

function Get-Weather {
    param()
   
    $findMe = $city
    $find   = Invoke-RestMethod -Uri "http://autocomplete.wunderground.com/aq?query=$findMe"

    if ($find) {
        
        $cityAPI  = $find.Results[0].l
        $city     = $find.Results[0].name
        
        $fullURL  = $baseURL + $apiKey + "/features/conditions/hourly/forecast/webcams/alerts" + "$cityAPI.json"
        $radarURL = "http://api.wunderground.com/api/$apiKey/animatedradar/animatedsatellite" + "$cityAPI.gif?num=6&delay=50&interval=30"
        
        Write-Host `n"API URLS for $city" -foregroundcolor $foregroundColor
        Write-Host `t$fullURL
        Write-Host `t$radarURL
        
        $weatherForecast = Invoke-RestMethod -Uri $fullURL -ContentType $acceptHeader
        
        $currentCond     = $weatherForecast.current_observation
        
        Write-Host `n"Current Conditions for: $city" -foregroundColor $foregroundColor
        Write-Host $currentCond.Weather 
        Write-Host "Temperature:" $currentCond.temp_f"F"
        Write-Host "Winds:" $currentCond.wind_string
        
        $curAlerts = $weatherForecast.alerts 
        
        if ($curAlerts) {
            
            if ($lifx) { Get-LIFXAPI -action flashcolor -brightness 1 -color Red -state on }
            
            $typeName = Get-WeatherFunction -Weather 'alert' -value $weatherForecast.alerts
            
            $alertDate  = $curAlerts.date
            $alertExp   = $curAlerts.expires 
            $alertMsg   = $curAlerts.message
            
            Write-Host `n"Weather Alert! ($typeName)" -foregroundcolor Red
            Write-Host "Date: $alertDate Expires: $alertExp"
            Write-Host "$alertMsg"    
            
            if ($sendAlertEmail) {

                Foreach ($email in $alertList) {
                    
                    Send-WeatherEmail -to $email -Subject "Weather Alert!" -Body "Alert Type: $typeName City: $city Message: $alertMsg"
                
                }
            }                  

        } 
        
    }

    Switch ($forecast) {
    
        {$_ -eq 'hourly'} {
 
            if ($sendEmail) {

                $hourlyForecast = $weatherForecast.hourly_forecast
                
                $body = "<p></p>"
                $body += "<p>Here is your hourly forecast!</p>"
                
                $selCam   = Get-Random $weatherForecast.webcams.count
                
                $camImg   = $weatherforecast.webcams[$selCam].CURRENTIMAGEURL
                $camName  = $weatherForecast.webcams[$selCam].linktext
                $camLink  = $weatherForecast.webcams[$selCam].link
                
                $body += "<p>Random webcam shot from: <a href=`"$camLink`">$camName</a></p>"
                $body += "<p><img src=`"$camImg`"></p>"                 
                                
                $body += "<p>$city Radar:</p>"
                $body += "<p><img src=`"$radarURL`"></p>"  
                
                if ($curAlerts) {
                    
                    $body += "<p><b><font color=`"red`">Weather Alert! ($typeName)</font></b></p>"
                    $body += "<p>Date: $alertDate Expires: $alertExp</p>"
                    $body += "<p>$alertMsg</p>"    
    
                }           
                
                foreach ($hour in $hourlyForecast) {
                    
                    $body += "<p></p>"
                    $body += "<p></p>"
                    
                    $prettyTime       = $hour.fcttime.pretty
                    $hourTemp         = $hour.temp.english  
                    $hourImg          = $hour.icon_url
                    
                    [int]$hourChill   = $hour.windchill.english
                    
                    if ($hourChill -eq -9999) {
                    
                        $hourChilltxt = 'N/A'
                        
                    } else {
                        
                        $hourChilltxt = $hourChill.ToString() + 'F'
                   
                    }
                                        
                    $hourWind         = $hour.wspd.english
                    $windDir          = $hour.wdir.dir
                    $hourUV           = $hour.uvi
                    $dewPoint         = $hour.dewpoint.english
                    $hourFeels        = $hour.feelslike.english
                    $hourHum          = $hour.humidity
                    $conditions       = $hour.condition
                    [int]$hourPrecip  = $hour.pop
                    
                    $popText = Get-WeatherFunction -Weather 'preciptext' -value $hourPrecip
                    
                    $body += "<p><b>$prettyTime</b></p>"
                    $body += "<p><img src=`"$hourImg`">$conditions</p>"
                    $body += "<p>Chance of precipitation: $hourPrecip% / $popText</p>"
                    $body += "<p>Current Temp: $hourTemp`F Wind Chill: $hourChilltxt Feels Like: $hourFeels`F</p>"
                    $body += "<p>Dew Point: $dewPoint</p>"
                    $body += "<p>Wind Speed: $hourWind`mph Direction: $windDir</p>"
                    $body += "<p>Humidity: $hourHum%</p>"
                    $body += "<p>UV Index: $hourUV"     
                    
                }
                
                foreach ($email in $weatherList) {Send-WeatherEmail -To $email -Subject "Your hourly forecast for $city" -body $body}
            
            }            
        
        }
        
        {$_ -eq 'forecast'} {                
              
            if ($sendEmail) {

                $todayForecast = $weatherForecast.forecast.simpleforecast.forecastday
                
                $body = "<p></p>"
                $body += "<p>Here is your 4 day forecast!</p>"
                
                $selCam   = Get-Random $weatherForecast.webcams.count
                
                $camImg   = $weatherforecast.webcams[$selCam].CURRENTIMAGEURL
                $camName  = $weatherForecast.webcams[$selCam].linktext
                $camLink  = $weatherForecast.webcams[$selCam].link
                
                $body += "<p>Random webcam shot from: <a href=`"$camLink`">$camName</a></p>"
                $body += "<p><img src=`"$camImg`"></p>"                 
                
                $body += "<p>$city Radar:</p>"
                $body += "<p><img src=`"$radarURL`"></p>"      
                
                $curAlerts = $weatherForecast.alerts 
                
                if ($curAlerts) {
                    
                    $body += "<p><b><font color=`"red`">Weather Alert! ($typeName)</font></b></p>"
                    $body += "<p>Date: $alertDate Expires: $alertExp</p>"
                    $body += "<p>$alertMsg</p>"    

                }                   
               
                foreach ($day in $todayForecast) {
                    
                    $body += "<p></p>"
                    $body += "<p></p>"
                    
                    $dayImg          = $day.icon_url
                    $dayMonth        = $day.date.monthname
                    $dayDay          = $day.date.day
                    $dayName         = $day.date.weekday
                    $dayHigh         = $day.high.fahrenheit  
                    $dayLow          = $day.low.fahrenheit
                    $maxWind         = $day.maxwind.mph
                    $aveWind         = $day.avewind.mph
                    $aveHum          = $day.avehumidity
                    $conditions      = $day.conditions
                    [int]$dayPrecip  = $day.pop
                    
                    $popText = Get-WeatherFunction -Weather 'preciptext' -value $dayPrecip
                    
                    $body += "<p><b>$dayName, $dayMonth $dayDay</b></p>"
                    $body += "<p><img src=`"$dayImg`">$conditions</p>"
                    $body += "<p>Chance of precipitation: $dayPrecip% / $popText</p>"
                    $body += "<p>High: $dayHigh`F Low: $dayLow`F</p>"
                    $body += "<p>Ave Winds: $aveWind`mph Max Winds: $maxWind`mph</p>"
                    $body += "<p>Humidity: $aveHum%</p>"
         
                }

                foreach ($email in $weatherList) {Send-WeatherEmail -To $email -Subject "Your 4 day forecast for $city" -body $body}
            
            }  
            
        }

        {$_ -eq 'camera'} {
            
            $selCam    = Get-Random $weatherForecast.webcams.count
            
            $camImg    = $weatherforecast.webcams[$selCam].CURRENTIMAGEURL
            $camName   = $weatherForecast.webcams[$selCam].linktext
            $camLink   = $weatherForecast.webcams[$selCam].link
            
            $fileExt   = $camImg.SubString($camImg.LastIndexOf("."),4)
            
            $cityShort = $city.substring(0,$city.lastindexof(","))
            
            $fileName  = $cityShort + $fileExt
            
            $location  = (Get-Location).Path
            
            $camFile   = Invoke-WebRequest -Uri $camImg -OutFile "$location\$fileName"   
            
            $file = $location + "\" + $fileName
            
            $gallery = 'YourSquareSpaceGalleryOrEmailToSendAttachmentTo' 
            
            $SMTPClient             = New-Object Net.Mail.SmtpClient($SmtpServer, 587)
            $SMTPMessage            = New-Object System.Net.Mail.MailMessage($emailFrom,$gallery,"$city cam","$city cam")
            $SMTPClient.Credentials = New-Object System.Net.NetworkCredential($emailUser,$emailPass)
            
            $att                    = New-Object Net.Mail.Attachment($file)
            $SMTPClient.EnableSsl   = $true
           
            Switch (($fileName.substring($fileName.LastIndexOf(".")+1)).tolower()) {
                
                {$_ -like "*png*"} {
                
                    $typeExt = 'png'    
                    
                }
                
                {$_ -like "*jpg*"} {
                
                    $typeExt = 'jpg'     
                    
                }
                
                {$_ -like "*gif*"} {
                    
                    $typeExt = 'gif' 
                    
                }                                
                
            }
            
            $SMTPMessage.Attachments.Add($att)
            ($smtpmessage.Attachments).contenttype.mediatype = "image/$typeExt"
            
            $SMTPClient.Send($SMTPMessage)
            
            $att.Dispose()
            $SMTPMessage.Dispose()
            
            Remove-Item $file
            
        }
    
    } 
    
    Return $weatherForecast

}

function Send-WeatherEmail {
    [cmdletbinding()]
    param(
    [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
    [string]
    $To,
    
    [string]
    [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
    $Subject,
    
    [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
    $Body,
    
    [string]
    $attachment
    )

    if (!$to)      {Write-Error "No recipient specified";break}
    if (!$subject) {Write-Error "No subject specified";break}
    if (!$body)    {Write-Error "No body specified";break}
   
    $gmailCredential = New-Object System.Management.Automation.PSCredential($emailUser,$emailPass)
       
    Send-MailMessage -To $to -From $emailFrom -Body $body -BodyAsHtml:$true -Subject $Subject -SmtpServer $smtpServer -Port $smtpPort -UseSsl -Credential $gmailCredential
 
}

function Get-WeatherFunction {
    [cmdletbinding()]
    param(
        [string]
        $weather,
        $value       
    )
    
    Switch ($weather) {
        
        {$_ -eq 'preciptext'} {
        
            Switch ($value) {
                
                {$_ -lt 20} {
                    
                    $popText = 'No mention'
                    
                }
                
                {$_ -eq 20} {
                    
                    $popText = 'Slight Chance'
                    
                }
                
                {($_ -lt 50 -and $_ -gt 20)} {
                    
                    $popText = 'Chance'
                    
                }
                
                {$_ -eq 50} {
                    
                    $popText = 'Good chance'
                    
                }
                
                {($_ -lt 70 -and $_ -gt 50)} {
                    
                    $popText = 'Likely'
                    
                }
                
                {$_ -ge 70} {
                    
                    $popText = 'Extremely likely'
                    
                }
                
            }
            
            Return $popText
            
        }   
        
        {$_ -eq 'alert'}    {
            
            Switch ($curAlerts.type) {
                
                'HEA' {$typeName = 'Heat Advisory'}
                'TOR' {$typeName = 'Tornado Warning'}
                'TOW' {$typeName = 'Tornado Watch'}
                'WRN' {$typeName = 'Severe Thunderstorm Warning'}
                'SEW' {$typeName = 'Severe Thunderstorm Watch'}
                'WIN' {$typeName = 'Winter Weather Advisory'}
                'FLO' {$typeName = 'Flood Warning'}
                'WAT' {$typeName = 'Flood Watch / Statement'}
                'WND' {$typeName = 'High Wind Advisory'}
                'SVR' {$typeName = 'Severe Weather Statement'}
                'HEA' {$typeName = 'Heat Advisory'}
                'FOG' {$typeName = 'Dense Fog Advisory'}
                'SPE' {$typeName = 'Special Weather Statement'}
                'FIR' {$typeName = 'Fire Weather Advisory'}
                'VOL' {$typeName = 'Volcanic Activity Statement'}
                'HWW' {$typeName = 'High Wind Warning'}
                'REC' {$typeName = 'Record Set'}
                'REP' {$typeName = 'Public Reports'}
                'PUB' {$typeName = 'Public Information Statement'}
                    
            }
                
            Return $typeName
    
        }    
    }
    
}

function Get-LIFXApi {
[cmdletbinding()]
param(
    [string]
    $action = 'setstate',
    [string]
    $state = 'on',
    [string]
    $color = 'white',
    [double]
    $brightness = '0.4'
)

$apiKey          = 'YourLifxApiKey'
$base64Key       =  [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(($apiKey)))
$headers         =  @{Authorization=("Basic {0}" -f $base64Key)}
$allURL          = 'https://api.lifx.com/v1/lights/all' 
$acceptheader    = 'application/json'
$baseURL         = 'https://api.lifx.com/v1/lights/'
$foregroundColor = 'white'
[array]$colors   = @('white','red','orange','yellow','cyan','green','blue','purple','pink')

$ninjaLights     = Invoke-RestMethod -headers $headers -Uri $allURL

function Change-LightState {
    [cmdletbinding()]
    param(
        [string]
        $state,
        [string]
        $color,
        [double]
        $brightness,
        [string]
        $selector
    )
    
    $ninjaID  = $ninjaLights.id
    $selector = 'id:' + $ninjaID
    $fullURL  = $baseurl + $selector + '/state'

    $payloadBuilder = "{
                    `"power`": `"$state`",
                    `"color`": `"$color`",
                    `"brightness`" : $brightness 
                    }"

    Write-Host "Changing light to:" -ForegroundColor $foregroundcolor
    Write-Host `t"State     :" $state
    Write-Host `t"Color     :" $color
    Write-Host `t"Brightness:" ($brightness * 100)"%" `n

    $stateReturn = Invoke-RestMethod -headers $headers -uri $fullURL -Method Put -Body $payloadBuilder
    
    Write-Host "API status:" -ForegroundColor $foregroundcolor
    Write-Host `t"Light :" $stateReturn.results.label
    Write-Host `t"Status:" $stateReturn.results.status `n
    
    
}

Switch ($action) {
    
    'setstate' { Change-LightState -state $state -color $color -brightness $brightness -selector $selector }
    
    'fluxtest' { 
        
        $originalBrightness = $ninjaLights.Brightness
        $originalColor      = $ninjalights.Color
        $originalState      = $ninjalights.Power
        $colorString        = "hue:" + $originalcolor.hue + " saturation:" + $originalcolor.saturation + " kelvin:" + $originalColor.Kelvin       
          
        $i = 0
        
        While ($i -le 20) {
            
            $color              = Get-Random $colors
            [double]$brightness = "{0:n2}" -f (Get-Random 0.99)
 
            Change-LightState -color $color -brightness $brightness -selector $selector -state $state
            Start-Sleep -seconds 1
            $i++
            
        }
        
        Change-LightState -state $originalState -color $colorString -brightness $originalBrightness -selector $selector  
        
    } 
    
    'flashcolor' {
        
        $originalBrightness = $ninjaLights.Brightness
        $originalColor      = $ninjalights.Color
        $originalState      = $ninjalights.Power
        $colorString        = "hue:" + $originalcolor.hue + " saturation:" + $originalcolor.saturation + " kelvin:" + $originalColor.Kelvin
        
        Change-LightState -state $state -color $color -brightness $brightness -selector $selector
        Start-Sleep -Seconds 1
        
        Change-LightState -state $originalState -color $colorString -brightness $originalBrightness -selector $selector
        Start-Sleep -Seconds 1
                
        Change-LightState -state $state -color $color -brightness $brightness -selector $selector
        Start-Sleep -Seconds 1
        
        Change-LightState -state $originalState -color $colorString -brightness $originalBrightness -selector $selector
        Start-Sleep -Seconds 1        
        
        Change-LightState -state $state -color $color -brightness $brightness -selector $selector
        Start-Sleep -Seconds 1
        
        Change-LightState -state $originalState -color $colorString -brightness $originalBrightness -selector $selector
                        
    }
}    
    
    
}

Get-Weather

Examples:

.\Get-Weather.ps1 -city houston -forecast hourly -sendEmail $true

And here is the email:

$weather = .\Get-Weather.ps1 -city Chicago -lifx $true

As you can see in the above example, the light will flash red. Then it begins  alternating with the state it was in beforehand, and finally it returns the light to the original state it was in.

More to come!

I've been writing this Get-Weather script for a little while now, and there's more I want to do with it. For now that list is:

  • Clean up the code a bit.
  • Add help nodes.
  • Add more error checking.
  • Find out more ways to utilize both the Weather Underground API and the LIFX API.

In the next post I will detail how I use this script in a scheduled task to send out daily weather emails.

As always, let me know if you have any questions, feedback, or ideas!

-Ginger Ninja

PowerShell: Control your LIFX light!

PowerShell: Control your LIFX light!

LET THERE BE LIGHT!

This post has been updated on 5/18/17 to reflect changes made to the Lifx API!

I stumbled upon the fact that LIFX has an API you can use today. Having found that out I wanted to do it in PowerShell and see what I can do. As it turns out there's a decent amount of things you can do and their API is "limited" to around 1 call per second.

If you would like to get into even more detail, step-by-step, and behind the scenes of how this works, check out this post!

Requirements

To get started with this you'll need...

Getting started

Here's what I did to get started. 

  1. Created a PowerShell script named Get-LifxApi.ps1.
  2. Opened the script up with basic parameters and variables to get started.
  3. Somewhat annoying my girlfriend with the light changing so much  ❤️

Starting code

[cmdletbinding(DefaultParameterSetName='SetState')]
param(
    [Parameter(
        ParameterSetName = "StateSetting"
    )]        
    [ValidateSet('SetState','FlashColor','RandomColors','ListLights')]    
    [string]
    $Action = 'SetState',
    [Parameter(
        ParameterSetName = "StateSetting"
    )]    
    [ValidateSet('on','off')]
    [string]
    $State = 'on',
    [Parameter(
        ParameterSetName = "StateSetting"
    )]    
    [ValidateSet('white','red','orange','yellow','cyan','green','blue','purple','pink')]
    [string]
    $Color = 'white',
    [Parameter(
        ParameterSetName = "StateSetting"
    )]    
    [ValidateRange(0.0,1)]
    [double]
    $Brightness = '0.4',
    [Parameter(
        ParameterSetName = "Toggle"    
    )]    
    [Switch]
    $Toggle
)

$apiKey          = 'YourKeyGoesHere'
$headers         =  @{Authorization=("Bearer {0}" -f $apiKey)}
$allURL          = 'https://api.lifx.com/v1/lights/all' 
$acceptHeader    = 'application/json'
$baseURL         = 'https://api.lifx.com/v1/lights'
$foregroundColor = 'white'
[array]$colors   = @('white','red','orange','yellow','cyan','green','blue','purple','pink')

Notes

You'll want to change the $apiKey to match your API key from the LIFX cloud.
The $allURL variable will return all your lights, and then the $baseURL variable is used later when formatting calls to change colors or do other things. $foregroundColor let's you specify a color for some of the Write-Host returns, and $colors is an array of colors currently accepted by the API (and you'll see why it is there later in this post). 

Let's make our first call!

I store the results of my light(s) in the $lifxLights variable.

$lifxLights = Invoke-RestMethod -headers $headers -Uri $allURL -ContentType $acceptheader

And now to write our first function in this script.

This function will allow us to utilize the API's state setting option. I named this function Set-LightState

function Set-LightState { #Begin function Set-LightState
    [cmdletbinding()]
    param(
        [Parameter()]
        [ValidateSet('on','off')]
        [string]
        $state,
        [Parameter()]  
        [string]
        $color,
        [Parameter()]
        [ValidateRange(0.0,1)]
        [double]
        $brightness,
        [string]
        $selector
    )

    if ($lifxLight.Connected) {          

        $fullURL  = "$baseurl/$($lifxLight.id)/state"        
        $payload  = [PSCustomObject]@{

            power      = $state
            color      = $color
            brightness = $brightness

        }

        $payloadJson = $payload | ConvertTo-Json

        Write-Host "Attempting to Change light to:" -ForegroundColor $foregroundcolor
        Write-Host `t"State     :" $state
        Write-Host `t"Color     :" $color
        Write-Host `t"Brightness:" ($brightness * 100)"%" `n

        $stateReturn = Invoke-RestMethod -headers $headers -uri $fullURL -Method Put -Body $payloadJson -ContentType $acceptheader
    
        Write-Host "API status:" -ForegroundColor $foregroundcolor
        Write-Host `t"Light :" $stateReturn.results.label
        Write-Host `t"Status:" $stateReturn.results.status `n
    
    } else {
        
        $lightName = $lifxLight.label
        Write-Host "$lightName is offline or unreachable :("
        
    }    

} #End function Set-LightState

I only have one LIFX light. In the start of this function I set my 'selector' (a required field for API calls to LIFX when setting states) as the light's ID.  I then use the value of light's ID to setup the URL structure for the call. $fullURL becomes the URL used when accessing the API to set the state of the light.

Let's take some action!

At the start of the script I have a parameter for action. 
Below is how it is used, in conjunction with toggle.

if ($toggle) { #If we're just toggling
             
    $fullURL  = "$baseurl/$($lifxLight.id)/toggle"  
    $toggleResult = Invoke-RestMethod -Uri $fullURL -Method Post -ContentType $acceptHeader -Headers $headers

    if ($toggleResult.results.status -eq 'ok') {

        Write-Host "Light [$($toggleResult.results.label)] was toggled!"

    } else {

        Write-Host "Something went wrong: [$($toggleResult.results.status)]"
    }

} else { #Else we perform the actions!

    Switch ($action) { #Begin switch for script actions
        
        'SetState' {         
            
            Set-LightState -state $state -color $color -brightness $brightness

        }    
        
        'ListLights' {
            
            return $lifxLight
            
        }       

        'RandomColors' { 
            
            $originalBrightness = $lifxLight.Brightness
            $originalColor      = $lifxLight.Color
            $originalState      = $lifxLight.Power
            $colorString        = "hue:" + $originalcolor.hue + " saturation:" + $originalcolor.saturation + " kelvin:" + $originalColor.Kelvin       
            
            $i = 0
            
            While ($i -le 10) {
                
                $color              = Get-Random $colors
                [double]$brightness = "{0:n2}" -f (Get-Random -Maximum 1 -Minimum 0.00)
    
                Set-LightState -color $color -brightness $brightness -selector $selector -state $state
                Start-Sleep -seconds 1
                $i++
                
            }
            
            Set-LightState -state $originalState -color $colorString -brightness $originalBrightness

        }
        
        'FlashColor' {
            
            $originalBrightness = $lifxLights.Brightness
            $originalColor      = $lifxLights.Color
            $originalState      = $lifxLights.Power
            $colorString        = "hue:" + $originalcolor.hue + " saturation:" + $originalcolor.saturation + " kelvin:" + $originalColor.Kelvin
            
            Set-LightState -state $state -color $color -brightness $brightness
            Start-Sleep -Seconds 1
            
            Set-LightState -state $originalState -color $colorString -brightness $originalBrightness
            Start-Sleep -Seconds 1
                    
            Set-LightState -state $state -color $color -brightness $brightness
            Start-Sleep -Seconds 1
            
            Set-LightState -state $originalState -color $colorString -brightness $originalBrightness 
            Start-Sleep -Seconds 1        
            
            Set-LightState -state $state -color $color -brightness $brightness
            Start-Sleep -Seconds 1
            
            Set-LightState -state $originalState -color $colorString -brightness $originalBrightness
                            
        }    
    
    } #End switch for light actions
}

My favorite action is the RandomColors. I have that set to use Get-Random to set the color and brightness randomly.  The other action is setstate. What that will do is set the light to a state you specify. If you do not give it options it will default to turning the light on to a brightness of 40% (0.4) and the color value of white (this is defined as per the default options for the parameters at the start of this script). You can change them by supplying the arguments when you run the script.

$fullURL example contents:

https://api.lifx.com/v1/lights/az0321123abc/state

Examples

.\Get-LifxAPI.ps1 -Action setstate -State on -Color blue -Brightness 0.5
lifx.PNG

Did it work?! 
(Yup)

.\Get-LifxAPI.ps1 -Action setstate

The above example is the simplest way to run it as it utilizes the default parameters. (actually, you could just run Get-LifxAPI.ps1)

Now for some fun!

.\Get-LifxAPI.ps1 -Action RandomColors

More to come!

I will be adding to this as I work on it more and clean it up. I will get adding:

  • Help nodes for Get-Help (done!).
  • More options to leverage different parts of the LIFX API (coming).
  • And more as I think of cool things to do with this!

Use:

Get-Help .\Get-LifxAPI.ps1 -Detailed

To see the help that is included. Be sure to change Get-LifxAPI.ps1 to what you named your script to!

The Code

<#
.SYNOPSIS   
    Use this script to control your Lifx light via PowerShell!
.DESCRIPTION 
    You can use this script to turn your light on and off, as well as change the brightness and colors.
.PARAMETER Action

    Argument: The action you'd like to take:
    SetState, FlashColor, RandomColors, and ListLights are valid actions.

.PARAMETER State
    This parameter accepts the state you'd like to set: on or off.
.PARAMETER Color
    Argument: The color you'd like to set the light to. 
    Valid colors can be tab completed.
.PARAMETER Brightness
    Argument: The brightness you want to set.
    Valid range: between 0-1 (0.5 being 50%)  
.PARAMETER Toggle
    Toggles the light      
.NOTES   
    Name: Get-LifxAPI.ps1
    Author: Mike Roberts aka Ginger Ninja
    DateCreated: 2/6/2016
    Last Modified: 5/18/2017

.EXAMPLE   
    $response = .\Get-LifxAPI.ps1 -Action ListLights
    ---------------------------------------------------------------

    1. Get an object returned that contains the result of the API call
    
    p$[18:52:20]> $response
    id                 : nun-ya
    uuid               : more nun-ya
    label              : Ninja room
    connected          : True
    power              : on
    color              : @{hue=249.9977111467155; saturation=0; kelvin=3400}
    brightness         : 0.48508430609597925
    group              : @{id=nun-ya; name=My Room}
    location           : @{id=nun-ya; name=My Home}
    product            : @{name=Original 1000; identifier=lifx_original_a21; company=LIFX; capabilities=}
    last_seen          : 2017-05-19T01:51:49Z
    seconds_since_seen : 32

.EXAMPLE   
    .\Get-LifxAPI.ps1 -Action RandomColors
    ---------------------------------------------------------------

    1. Light toggles between random colors 10 times

.EXAMPLE   
    .\Get-LifxAPI.ps1 -Toggle
    ---------------------------------------------------------------

    1. Toggles light state (on or off respectively)

.EXAMPLE   
    .\Get-LifxAPI.ps1 -Action SetState -State on -Color blue -Brightness 0.5
    ---------------------------------------------------------------

    1. Turns light on
    2. Changes color to blue
    3. Changes brightness to 0.5
.OUTPUTS
    If you use ListLights as the action, the output will be an
    object containing the results of the api call.

.LINK  
    http://www.gngrninja.com/script-ninja/2016/2/6/powershell-control-your-lifx-light
#>
[cmdletbinding(DefaultParameterSetName='SetState')]
param(
    [Parameter(
        ParameterSetName = "StateSetting"
    )]        
    [ValidateSet('SetState','FlashColor','RandomColors','ListLights')]    
    [string]
    $Action = 'SetState',
    [Parameter(
        ParameterSetName = "StateSetting"
    )]    
    [ValidateSet('on','off')]
    [string]
    $State = 'on',
    [Parameter(
        ParameterSetName = "StateSetting"
    )]    
    [ValidateSet('white','red','orange','yellow','cyan','green','blue','purple','pink')]
    [string]
    $Color = 'white',
    [Parameter(
        ParameterSetName = "StateSetting"
    )]    
    [ValidateRange(0.0,1)]
    [double]
    $Brightness = '0.4',
    [Parameter(
        ParameterSetName = "Toggle"    
    )]    
    [Switch]
    $Toggle
)

$apiKey          = 'YourKeyGoesHere'
$headers         =  @{Authorization=("Bearer {0}" -f $apiKey)}
$allURL          = 'https://api.lifx.com/v1/lights/all' 
$acceptHeader    = 'application/json'
$baseURL         = 'https://api.lifx.com/v1/lights'
$foregroundColor = 'white'
[array]$colors   = @('white','red','orange','yellow','cyan','green','blue','purple','pink')

function Set-LightState { #Begin function Set-LightState
    [cmdletbinding()]
    param(
        [Parameter()]
        [ValidateSet('on','off')]
        [string]
        $state,
        [Parameter()]  
        [string]
        $color,
        [Parameter()]
        [ValidateRange(0.0,1)]
        [double]
        $brightness,
        [string]
        $selector
    )

    if ($lifxLight.Connected) {          

        $fullURL  = "$baseurl/$($lifxLight.id)/state"             
        $payload  = [PSCustomObject]@{

            power      = $state
            color      = $color
            brightness = $brightness

        }

        $payloadJson = $payload | ConvertTo-Json

        Write-Host "Attempting to Change light to:" -ForegroundColor $foregroundcolor
        Write-Host `t"State     :" $state
        Write-Host `t"Color     :" $color
        Write-Host `t"Brightness:" ($brightness * 100)"%" `n

        $stateReturn = Invoke-RestMethod -headers $headers -uri $fullURL -Method Put -Body $payloadJson -ContentType $acceptheader
    
        Write-Host "API status:" -ForegroundColor $foregroundcolor
        Write-Host `t"Light :" $stateReturn.results.label
        Write-Host `t"Status:" $stateReturn.results.status `n
    
    } else {
        
        $lightName = $lifxLight.label
        Write-Host "$lightName is offline or unreachable :("
        
    }    

} #End function Set-LightState

#Get Lifx Lights
$lifxLights = Invoke-RestMethod -headers $headers -Uri $allURL -ContentType $acceptheader
if ($lifxLights.Count -gt 1) {

    Write-Host "Pick a light:"`n
    for ($i = 0;$i -le $lifxLights.Count - 1;$i++) {

        Write-Host "[$i] -> $($lifxLights[$i].label)"

    }

    $picked = $false

    Do {
        [int]$number = Read-Host "Which [#]?"
        Write-Host "$number"
        if (![String]::IsNullOrEmpty($number) -and ($number -le ($lifxLights.Count - 1) -and $number -gt -1)) {

            $picked    = $true
            $lifxLight = $number  
            write-Host "sup"

        }
    } 
    While ($picked -eq $false)

} else {

    $lifxLight = $lifxLights[0]

}

if ($toggle) { #If we're just toggling
             
    $fullURL  = "$baseurl/$($lifxLight.id)/toggle"  
    $toggleResult = Invoke-RestMethod -Uri $fullURL -Method Post -ContentType $acceptHeader -Headers $headers

    if ($toggleResult.results.status -eq 'ok') {

        Write-Host "Light [$($toggleResult.results.label)] was toggled!"

    } else {

        Write-Host "Something went wrong: [$($toggleResult.results.status)]"
    }

} else { #Else we perform the actions!

    Switch ($action) { #Begin switch for script actions
        
        'SetState' {         
            
            Set-LightState -state $state -color $color -brightness $brightness

        }    
        
        'ListLights' {
            
            return $lifxLight
            
        }       

        'RandomColors' { 
            
            $originalBrightness = $lifxLight.Brightness
            $originalColor      = $lifxLight.Color
            $originalState      = $lifxLight.Power
            $colorString        = "hue:" + $originalcolor.hue + " saturation:" + $originalcolor.saturation + " kelvin:" + $originalColor.Kelvin       
            
            $i = 0
            
            While ($i -le 10) {
                
                $color              = Get-Random $colors
                [double]$brightness = "{0:n2}" -f (Get-Random -Maximum 1 -Minimum 0.00)
    
                Set-LightState -color $color -brightness $brightness -selector $selector -state $state
                Start-Sleep -seconds 1
                $i++
                
            }
            
            Set-LightState -state $originalState -color $colorString -brightness $originalBrightness

        }
        
        'FlashColor' {
            
            $originalBrightness = $lifxLights.Brightness
            $originalColor      = $lifxLights.Color
            $originalState      = $lifxLights.Power
            $colorString        = "hue:" + $originalcolor.hue + " saturation:" + $originalcolor.saturation + " kelvin:" + $originalColor.Kelvin
            
            Set-LightState -state $state -color $color -brightness $brightness
            Start-Sleep -Seconds 1
            
            Set-LightState -state $originalState -color $colorString -brightness $originalBrightness
            Start-Sleep -Seconds 1
                    
            Set-LightState -state $state -color $color -brightness $brightness
            Start-Sleep -Seconds 1
            
            Set-LightState -state $originalState -color $colorString -brightness $originalBrightness 
            Start-Sleep -Seconds 1        
            
            Set-LightState -state $state -color $color -brightness $brightness
            Start-Sleep -Seconds 1
            
            Set-LightState -state $originalState -color $colorString -brightness $originalBrightness
                            
        }    
    
    } #End switch for light actions
}

Leave comment if you have any ideas or feedback!

-Ginger Ninja

PowerShell: Fun with Weather Underground's API (part 1)

PowerShell: Fun with Weather Underground's API (part 1)

Free APIs can be fun!

After doing some work with the Victor OPs API, I personally wanted to see what else was out there. I haven't messed with APIs too much in the past, but now I'm all about it. I'm a bit of a weather geek so I was excited when I found out that Weather Underground has one available for free!

What I've done so far

So far I've created a script that can:

  • Email you an HTML email with the following:
    • Hourly/4 day weather forecast
    • Current (randomly selected) webcam shot from the city you specified
    • Radar animated gif
  • Email a Squarespace gallery with a random camera shot from the city you specify.
  • Return (in PowerShell) the forecast for the city you specify

I'm thinking of things to add to it all the time now... like perhaps an alert check on an interval that sends you a text if there is a weather alert.

I'm doing all this to learn the ins and outs of handling APIs with PowerShell, as well as to feed my inner weather and script geek.

Getting started

This post will be a multi part post. For this portion I will stick to the part that you don't even need and API key for!  (Although, you may want to grab one, here)

Requirements

You'll need to setup the following variables:

  • $apiKey (Can be blank now or just 'yourAPIKeyHere') as it is not actually needed yet.
  • $city (Specify a city you'd like to look up using their free autocomplete API)
  • $baseURL  = 'http://api.wunderground.com/api/'

The Code

$city    = Read-Host "City?"
$apiKey  = 'yourAPIKeyHere' 
$baseURL = 'http://api.wunderground.com/api/'

   $findMe = $city
    $find   = Invoke-RestMethod -Uri "http://autocomplete.wunderground.com/aq?query=$findMe"
    
    if ($find) {
        
        $cityAPI  = $find.Results[0].l
        $city     = $find.Results[0].name
        
        $fullURL  = $baseURL + $apiKey + "/features/conditions/hourly/forecast/webcams/alerts" + "$cityAPI.json"
        $radarURL = "http://api.wunderground.com/api/$apiKey/animatedradar/animatedsatellite" + "$cityAPI.gif?num=6&delay=50&interval=30"
        
        Write-Host `n"API URLS for $city" -foregroundcolor $foregroundColor
        Write-Host `t$fullURL
        Write-Host `t$radarURL

}

Results / Going forward

If you put the above code in a script and run it, the results will be the API URLs it found for the first city it returns. If you want to narrow down it for a different $city, you'll need to specify 'City, State'.

Here are the results for $city = 'Portland'

If you want to get a jump start on this and you have your API key, check out the API documentation from Weather Underground!

Here is a teaser of what can be done (all via PowerShell!):

 

Let me know if you have any ideas or comments!

-Ginger Ninja

PowerShell: Reset a user's AD password

PowerShell: Reset a user's AD password

I cant login

"My password isn't working!" ...  Can you reset it?" These words are typically heard at least once or twice a day by front line support at most places. No matter how much we try to inform and remind users about their expiring passwords, there will be those that forget to change it before it expires or end up locking themselves out one way or another (iPhone/Exchange anyone?)

AD functions in PowerShell

I have written a few AD functions in PowerShell. One of them gets information about a user and then passes that along to possible actions to take. The function I'm writing about is one of those actions. 

Requirements

  • Quest AD tools
  • Resolve-UserName function (will give the code for that below)
  • Set $foregroundColor to something you like so it looks pretty.

Actions!

What this function will do is take an account name (or ask you for one), and then give you 3 options.

  • Reset and flag for change
  • Reset and don't flag for a change
  • Just flag for reset on next logon (or unflag if already flagged)

Parameters

-actName (Account name to reset)

The Code

This is the code for the Resolve-Username function:

function Resolve-UserName {

    param ($Username, [switch]$Verbose=$false)

    if (!$Username) {
        $Username = Read-Host "Enter username" 
    }

    if($Verbose){ Write-Host "Attempting to resolve `"$Username`"..." }
    $ResolvedName = Get-QADUser $Username -IncludeAllProperties
    do{
        If ($ResolvedName.Length -gt 1) {
    
            Write-Host `n"Multiple users found for `"$Username`", please select the intended user."`n -ForegroundColor $foregroundColor
            
            $i = 0
    
            Foreach ($u in $ResolvedName) {
                Write-Host "$i -> " $u.Name
                $i++
            }
            $selUsr = Read-Host "Which #?"
            $ResolvedName = Get-QADUser $ResolvedName[$selUsr] -IncludeAllProperties
        }
        elseif($ResolvedName.Length -eq 0){
            Write-Host "User does not exist. Try again." -ForegroundColor Red
            $ResolvedName = Get-QADUser (Read-Host "Enter username") -IncludeAllProperties
        }
    }until($ResolvedName.length -eq 1)
    if($Verbose){ Write-Host "Resolved `"$Username`" to" $ResolvedName.SamAccountName }
    Return $ResolvedName
}

This is the code for the Reset-ADPassword function:

function Reset-ADPassword {

    param($actName)
    
    if(!$actName){$actName = Read-Host "Enter username"}
    $usrObj = Resolve-UserName $actName

    If ($usrObj) {    
        
        Write-Host `n"Password reset:" $usrObj.Name -ForegroundColor $foregroundColor
        
        Write-Host "1. Reset and flag"
        Write-Host "2. Reset and do not flag"
        Write-Host "3. Just flag (or unflag if flagged)"`n
        
    
        $psdOp = Read-Host "Please specify an option #" 
        Write-Host "Note: You will be prompted to confirm any option you select." -ForegroundColor $foregroundColor
    
        Switch ($psdOp)  {
    
            1 { 
                
                $resetTo = Read-Host "Reset password to" -AsSecureString
                $resetTo = $resetTo | ConvertFrom-SecureString
                $PlainTextPassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR( (ConvertTo-SecureString $resetTo) ))
                Set-QADUser $usrObj -UserPassword $PlainTextPassword -UserMustchangePassword $true -Credential $adminCredential -confirm }
    
            2 { 
                
                $resetTo = Read-Host "Reset password to" -AsSecureString
                $resetTo = $resetTo | ConvertFrom-SecureString
                $PlainTextPassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR( (ConvertTo-SecureString $resetTo) ))
                Set-QADUser $usrObj -UserPassword $PlainTextPassword -UserMustchangePassword $false -Credential $adminCredential -confirm }
    
            3 { 
                
                if ($usrObj.UserMustChangePassword -eq $true) {
                    Set-QADUser $usrObj -UserMustChangePassword  $false  -Credential $adminCredential -confirm }
                else { Set-QADUser $usrObj -UserMustchangePassword $true -Credential $adminCredential -confirm } }  
        }
    } 
}

Let me know if you have any questions or comments!

 

PowerShell Quick Tip: Basic use of the Switch statement

PowerShell Quick Tip: Basic use of the Switch statement

Why use Switch?

In PowerShell you can use an if statement to compare things and determine actions based upon the result. You also can use elseif to keep comparing values until you reach a desired (or not so desired) result.

Switch allows you to take a variable you define, and then based upon its value perform different actions.

Example

In this example I will show you a switch statement I used in a script that uses the Weather Underground API to send HTML emails detailing your requested weather forecast. Consider this a teaser until I post that script itself.

What I wanted to do

In this example I wanted to take a returned percentage, and then change a variable to display the appropriate text result. It compares the returned value of the hour's chance of precipitation set in $hourPrecip, and then based on that number changes the variable $popText accordingly.

The Code

Switch ($hourPrecip) {
                        
                        {$_ -lt 20} {
                            
                            $popText = 'No mention'
                            
                        }
                        
                        {$_ -eq 20} {
                            
                            $popText = 'Slight Chance'
                            
                        }
                        
                        {($_ -lt 50 -and $_ -gt 20)} {
                            
                            $popText = 'Chance'
                            
                        }
                        
                        {$_ -eq 50} {
                            
                            $popText = 'Good chance'
                            
                        }
                        
                        {($_ -lt 70 -and $_ -gt 50)} {
                            
                            $popText = 'Likely'
                            
                        }
                        
                        {$_ -ge 70} {
                            
                            $popText = 'Extremely likely'
                            
                        }
                        
                    }

So now you display the percentage, as well as its correlating text value which describes what the percentage means.

If the value was 51 for $hourPrecip, then $popText will be set to Likely

The Weather Underground post will be coming soon!

Do you use switches in your scripts, and if so, how do you like to use them?

PowerShell: Building a basic text menu system (part 1)

PowerShell: Building a basic text menu system (part 1)

Menus?! But...why?

As I dove deeper into PowerShell years ago I started to think about ways to help those utilizing my scripts. I wrote a script that became a module that the whole team would use.

In this module there were a ton of functions. Some functions were for creating new hires, others for terminations, and even more for general AD user account and Exchange management. Once there were more than 6 different functions I created a basic menu system for the Helpdesk to use when using PowerShell with my module.

Where to start

The first thing you'll need to do is present the user with options. Write-Host is my favorite way to do this. You'll want to define the variable $foregroundColor to see the full effect of the following code. I like to keep that set as a global variable so it can be changed later if desired. This is of course, very important, as who doesn't want to sometimes tweak the color output of your scripts? 

I also keep a version number in the variable $ncVer.

Building the menu:

Write-Host `n"Ninja Center v" $ncVer -ForeGroundColor $foregroundcolor
Write-Host `n"Type 'q' or hit enter to drop to shell"`n
Write-Host -NoNewLine "<" -foregroundcolor $foregroundColor
Write-Host -NoNewLine "Active Directory"
Write-Host -NoNewLine ">" -foregroundcolor $foregroundColor
Write-Host -NoNewLine "["
Write-Host -NoNewLine "A" -foregroundcolor $foregroundColor
Write-Host -NoNewLine "]"

Write-Host -NoNewLine `t`n "A1 - " -foregroundcolor $foregroundcolor
Write-host -NoNewLine "Look up a user"
Write-Host -NoNewLine `t`n "A2 - " -foregroundcolor $foregroundcolor
Write-host -NoNewLine "Enter PowerShell session on DC"
Write-Host -NoNewLine `t`n "A3 - " -foregroundcolor $foregroundcolor
Write-host -NoNewLine "Run DCDiag on a DC (or all DCs)"`n`n

Write-Host -NoNewLine "<" -foregroundcolor $foregroundColor
Write-Host -NoNewLine "Exchange"
Write-Host -NoNewLine ">" -foregroundcolor $foregroundColor
Write-Host -NoNewLine "["
Write-Host -NoNewLine "E" -foregroundcolor $foregroundColor
Write-Host -NoNewLine "]"

Write-Host -NoNewLine `t`n "E1 - " -foregroundcolor $foregroundcolor
Write-host -NoNewLine "Forward a mailbox"
Write-Host -NoNewLine `t`n "E2 - " -foregroundcolor $foregroundcolor
Write-host -NoNewLine "Clear a mailbox forward"
Write-Host -NoNewLine `t`n "E3 - " -foregroundcolor $foregroundcolor
Write-host -NoNewLine "See if an IP address is being relayed"`n`n

Write-Host -NoNewLine "<" -foregroundcolor $foregroundColor
Write-Host -NoNewLine "Storage"
Write-Host -NoNewLine ">" -foregroundcolor $foregroundColor
Write-Host -NoNewLine "["
Write-Host -NoNewLine "S" -foregroundcolor $foregroundColor
Write-Host -NoNewLine "]"

Write-Host -NoNewLine `t`n "S1 - " -foregroundcolor $foregroundcolor
Write-host -NoNewLine "Connect to a NetApp controller"`n`n

When starting out I would use Write-Host to display information as I wrote scripts. This was nice, and was useful for troubleshooting. Then as my knowledge expanded I began to realize how else it worked. My new favorite, especially when building menus, is -NoNewLine. 

What -NoNewLine allows you to do is alternate different colors on the same line. Again, extremely uhhh... important. The output does look nice, though!

There are also some escaped string options I like to use. They are `t (horizontal tab), and -n(new line). These are used to keep the menu looking nice and not too cluttered.

OK so I built something that shows a bunch of stuff... what now?

Now we'll need to get some input from the user and take action! To do this we'll use Read-Host combined with a Switch to deal with the input.

$sel = Read-Host "Which option?"

Switch ($sel) {
    "A1" {Get-ADinfo;Load-NinjaCenter}
    "A2" {Enter-DCSession}
    "A3" {
        $DCs      = Read-Host "DC (specify name or put 'all' for all)?"
        $test     = Read-Host "Enter 'error' or 'full' for test feedback"         
       
        
        $global:dcDiagResults = Get-DCDiagInfo -DC $DCs -Type $test -Verbose
        
        Write-Host `n"Results stored in the variable: dcDiagResults"`n
        Write-Host -NoNewLine "Type "
        Write-Host -NoNewLine "Load-NinjaCenter " -foregroundcolor $foregroundcolor
        Write-Host -NoNewLine "to load the menu again."`n
    }
    
    "E1" {Forward-Email}
    "E2" {Clear-Forward}
    "E3" {Check-EXRelayIP}
    
    "S1" {
        Connect-NetAppController
    
        Write-Host -NoNewLine "Type "
        Write-Host -NoNewLine "Load-NinjaCenter " -foregroundcolor $foregroundcolor
        Write-Host -NoNewLine "to load the menu again."`n
}

    {($_ -like "*q*") -or ($_ -eq "")} {
        
        Write-Host `n"No input or 'q' seen... dropping to shell" -foregroundColor $foregroundColor
        Write-Host "Type Load-NinjaCenter to load them menu again"`n
        
        
    }          
        
}

Alright, now let's take some action!

As you can see above, the switch handles the user input. There are some more advanced ways to parse the input, which I will go over in a later post. For now, you can see a few things.

One is that you can stage other functions based on the input. For example, with the switch option "A3", (which is the menu option for running DC Diag against DCs), you can see that we provide the user with some more prompts to prep some variables to pass to the Get-DCDiagInfo function.

I actually wrote about my Get-DCDiagInfo (function, but also works as a standalone script), and you can read about it here.

This allows you to wrap your existing functions in different ways and present them to folks that will use them however you'd like to. For a Helpdesk environment, or those not super deep into PowerShell, this is a nifty way for them to encapsulate and utilize what you've written. 

Let's put it all together.

Here is the full code for the menu function:

function Load-NinjaCenter {  
[cmdletbinding()]
param()

    ##########################################
    #            Ninja Center Menu           #
    ##########################################
    Write-Host `n"Ninja Center v" $ncVer -ForeGroundColor $foregroundcolor
    Write-Host `n"Type 'q' or hit enter to drop to shell"`n
    Write-Host -NoNewLine "<" -foregroundcolor $foregroundColor
    Write-Host -NoNewLine "Active Directory"
    Write-Host -NoNewLine ">" -foregroundcolor $foregroundColor
    Write-Host -NoNewLine "["
    Write-Host -NoNewLine "A" -foregroundcolor $foregroundColor
    Write-Host -NoNewLine "]"

    Write-Host -NoNewLine `t`n "A1 - " -foregroundcolor $foregroundcolor
    Write-host -NoNewLine "Look up a user"
    Write-Host -NoNewLine `t`n "A2 - " -foregroundcolor $foregroundcolor
    Write-host -NoNewLine "Enter PowerShell session on DC"
    Write-Host -NoNewLine `t`n "A3 - " -foregroundcolor $foregroundcolor
    Write-host -NoNewLine "Run DCDiag on a DC (or all DCs)"`n`n

    Write-Host -NoNewLine "<" -foregroundcolor $foregroundColor
    Write-Host -NoNewLine "Exchange"
    Write-Host -NoNewLine ">" -foregroundcolor $foregroundColor
    Write-Host -NoNewLine "["
    Write-Host -NoNewLine "E" -foregroundcolor $foregroundColor
    Write-Host -NoNewLine "]"

    Write-Host -NoNewLine `t`n "E1 - " -foregroundcolor $foregroundcolor
    Write-host -NoNewLine "Forward a mailbox"
    Write-Host -NoNewLine `t`n "E2 - " -foregroundcolor $foregroundcolor
    Write-host -NoNewLine "Clear a mailbox forward"
    Write-Host -NoNewLine `t`n "E3 - " -foregroundcolor $foregroundcolor
    Write-host -NoNewLine "See if an IP address is being relayed"`n`n

    Write-Host -NoNewLine "<" -foregroundcolor $foregroundColor
    Write-Host -NoNewLine "Storage"
    Write-Host -NoNewLine ">" -foregroundcolor $foregroundColor
    Write-Host -NoNewLine "["
    Write-Host -NoNewLine "S" -foregroundcolor $foregroundColor
    Write-Host -NoNewLine "]"

    Write-Host -NoNewLine `t`n "S1 - " -foregroundcolor $foregroundcolor
    Write-host -NoNewLine "Connect to a NetApp controller"`n`n

    $sel = Read-Host "Which option?"

    Switch ($sel) {
        "A1" {Get-ADinfo;Load-NinjaCenter}
        "A2" {Enter-DCSession}
        "A3" {
            $DCs      = Read-Host "DC (specify name or put 'all' for all)?"
            $test     = Read-Host "Enter 'error' or 'full' for test feedback"         
        
            
            $global:dcDiagResults = Get-DCDiagInfo -DC $DCs -Type $test -Verbose
            
            Write-Host `n"Results stored in the variable: dcDiagResults"`n
            Write-Host -NoNewLine "Type "
            Write-Host -NoNewLine "Load-NinjaCenter " -foregroundcolor $foregroundcolor
            Write-Host -NoNewLine "to load the menu again."`n
        }
        
        "E1" {Forward-Email}
        "E2" {Clear-Forward}
        "E3" {Check-EXRelayIP}
        
        "S1" {
            Connect-NetAppController
        
            Write-Host -NoNewLine "Type "
            Write-Host -NoNewLine "Load-NinjaCenter " -foregroundcolor $foregroundcolor
            Write-Host -NoNewLine "to load the menu again."`n
    }

        {($_ -like "*q*") -or ($_ -eq "")} {
            
            Write-Host `n"No input or 'q' seen... dropping to shell" -foregroundColor $foregroundColor
            Write-Host "Type Load-NinjaCenter to load them menu again"`n
            
            
        }          
            
    }
    ##########################################
    #        End Ninja Center Menu           #
    ##########################################

}

What you'd do to run it is put it in a module you created and then call the function when the module is imported at the end of your script. Once exited, it could be called by its function name at any time to bring the menu back up.

Some things I've noticed

When I started sharing my scripts this way I noticed that people will find ways to break your code. This is a great way to learn the weak points of your scripts and become even better at writing them. I hope you've found this useful, and as always I appreciate any feedback!

PowerShell: Using the VictorOps REST API and PRTG Sensor Output

PowerShell: Using the VictorOps REST API and PRTG Sensor Output

What's it do?

I utilized the VictorOps REST API with PowerShell to return data in a user readable format, or to use with PRTG sensors.

I wanted a way to display who was currently on call via a PRTG map that other users can publicly access.

Note: This is something I wrote in a day, and it will be evolving into a cleaner, error checked format!

What you'll need

Parameters

-getInfo

  • ack               = List/ack any outstanding alerts. Will prompt for message and confirm before acknowledging.
  • OnCall         = List who is currently on call (must specify team with -team parameter) (compatible with -format parameter and PRTG sensors)
  • UserSched  = Get a user's oncall schedule. Currently best to return this into your own variable for now. (Must specify -user parameter)
  • Upcoming   = See who is on call for a team in the future. (must specify team with -team parameter)
  • Incident       = List all incidents (compatible with -format parameter and PRTG sensors)

-team

  • Specify the name of the team. If you have not setup the switch information later in this script, do so to ensure it lines up.
     

-user

  • Specify the username you're getting info for.

-format

  • Specify 'prtg' for certain -getInfo parameters and it will dump it as PRTG sensor information. Leave blank or specify 'user' to return a human readable response. Use 'raw' to return the raw API data. It would be best to call this script into a variable when doing this. $rawAPI = .\Script-Name.ps1 -getInfo 'oncall' -team 'teamname' -format 'raw' 

Examples

Here is some example output.

 .\Get-VictorOpsInfo.ps1 -getInfo incidents
.\Get-VictorOpsInfo.ps1 -getInfo incidents -format prtg

Setting it up

You'll need your API Key and ID from VictorOps. Once you have that, be sure to set them in the script.

$apiKey = ''
$apiID= ''

You can generate a key in VictorOps by going to "Settings -> API."

Be sure to change this switch to match your team setup in VictorOps.

Switch ($team) {

{$team -like "*team1*"}{

$teamName ='team1name'

}

{$team -like "*team2*"} {

$teamName = 'team2name'

}

}

Your username will be set to the username you're logged into the machine with. This is important to note as it sends username when you use the script to ack alerts. To change this, edit the following (you'll want to do this if your username does not match the one in VictorOps):

$ackUser = (Get-ChildItem ENV:\USERNAME).Value 

PRTG Setup

To use this and report info into PRTG, setup the script and ensure Quest AD PowerShell tools are installed on the PRTG machine. 

  1. Drop the script in your Custom Sensor folder (Example: D:\PRTG Network Monitor\Custom Sensors\EXEXML\)
  2. Setup an "EXE/Script Advanced" sensor on PRTG and name it something like "VictorOps Incidents". Select the script in the drop-down.
  3. In the settings specify the following parameters: -getInfo incidents -format PRTG

 

You can also setup a sensor for team on call reporting.
To do this, perform the above steps, but specify the parameters -getInfo OnCall -team unix -format prtg. 
 

That will return a sensor that returns a text message of who is on call with their phone number (pulled from AD).

You can then create a map in PRTG of who is on call that can be provided publicly. To get the on call info use the "Status Icons -> Sensor Message only" Properties in the PRTG map builder.

Scanning Interval

Be sure to change the scanning interval in PRTG to something that makes sense. There probably isn't a reason to keep it at every minute, so change it!


The code

<#   
.SYNOPSIS   
   This script utilizes the VictorOps API to return requested information.
.DESCRIPTION 
   This script can be used in various ways. This implementation returns data and interacts with the API to ack alerts. 
   It can also return values which can be used by PRTG sensors.
   
   Variable structure:
    $apiKey       = Your Victor OPs API Key   
    $apiID        = Your Victor OPs API ID
    $baseURL      = The base URL for the VictorOPs API. You shouldn't need to change this. 
    $acceptheader = "application/json" - this default should be fine
    
    The below information is gathered using the above variables.
    
    $headers = @{ 
    'Accept'       = $acceptheader
    'X-VO-Api-Id'  = $apiID
    'X-VO-Api-Key' = $apiKey    
    }
    
    For more info on the VictorOps API, visit https://portal.victorops.com/public/api-docs.html#/.
.PARAMETER getInfo
    Specify the information you'd like returned or action to take.
    
    ack       = List/ack any outstanding alerts
    OnCall    = List who is currently on call (must specify team with -team parameter) (compatible with -format parameter and PRTG sensors)
    UserSched = Get a user's oncall schedule. Best to return this into your own variable for now. (Must specify -user parameter)
    Upcoming  = See who is on call for a team in the future. (must speficy team with -team parameter)
    Incident  = List all incidents (compatible with -format parameter and PRTG sensors)
 
.PARAMETER team
    Specify the name of the team. If you have not setup the switch information later in this script, do so to ensure it lines up.
    
.PARAMETER user
    Specify the username you're getting info for.

.PARAMETER format
    Specify 'prtg' for certain -getInfo parameters and it will dump it as PRTG sensor information.
    Leave blank or specify 'user' to return a human readable response.
    Use 'raw' to return the raw API data. It would be best to call this script into a variable when doing this. $rawAPI = .\Script-Name.ps1 -getInfo 'oncall' -team 'teamname' -format 'raw' 
    
.NOTES   
    Name: Get-VictorOpsInfo.ps1
    Author: Ginger Ninja (Mike Roberts)
    DateCreated: 1/28/16
.LINK  
    http://www.gngrninja.com/script-ninja/2016/1/28/powershell-using-the-victorops-rest-api   
.EXAMPLE   
    .\Get-VictorOpsInfo.ps1 -getInfo OnCall -team unix
 .Example
    .\Get-VictorOpsInfo.ps1 -getInfo ack
#> 
[cmdletbinding()]
param(
    [string]
    $team,
    [string]
    $getInfo = 'notset',
    [string]
    $user,
    [string]
    $format = 'user'
)

if (!((Get-PSSnapin -name "Quest.ActiveRoles.ADManagement" -ea SilentlyContinue).name -eq "Quest.ActiveRoles.ADManagement")) {
  
        Write-Host `n"Loading Quest Active Directory Tools Snapin..."`n -ForegroundColor $foregroundColor
     
        Add-PSSnapin Quest.ActiveRoles.ADManagement | Out-Null
    }  if(!$?) {

        Write-Host `n"You need to install Quest Active Directory Tools Snapin from http://software.dell.com/products/activeroles-server/powershell.aspx"`n -ForegroundColor Red

        Start-Process "http://software.dell.com/products/activeroles-server/powershell.aspx"
        Break;
}

$scriptName   = $myInvocation.MyCommand
$path         = Split-Path -Path $MyInvocation.MyCommand.Path
$apiKey       = 'yourAPIKey'
$apiID        = 'Your API ID'
$baseURL      = 'https://api.victorops.com' 
$acceptheader = "application/json"

$headers = @{
    'Accept'       = $acceptheader
    'X-VO-Api-Id'  = $apiID
    'X-VO-Api-Key' = $apiKey
}

function Get-OnCallSchedule {
    [cmdletbinding()]
    param(
        [string]
        $baseURL,
        [string]
        $onCallURL
    )
    

    $fullURL = $baseURL + $onCallURL
    
    if ($fullURL) {
        
        $onCallSchedule = Invoke-RestMethod $fullURL -Headers $headers
        
    }
    
    return $onCallSchedule
    
}

function Get-Incident {
    [cmdletbinding()]
    param($baseURL)
    
    $incidentURL     = '/api-public/v1/incidents'
    $fullURL         = $baseURL + $incidentURL
    $onCallIncidents = (Invoke-RestMethod $fullURL -Headers $headers).Incidents

    Return $onCallIncidents
    
}

function Resolve-Incident {
    [cmdletbinding()]
    param ($baseURL)

    $incidents = Get-Incident $baseURL
    
    $i = 0
    #Switch for current phase?
    foreach ($in in $incidents) {
        
        if ($in.currentPhase -like 'UNACKED') {
            
            $i++
            
            $ackURL  = "/api-public/v1/incidents/ack"
            
            $fullURL = $baseURL + $ackURL
            
            Write-Host $in.EntityDisplayName
            Write-Host $in.incidentNumber
            
            $inName = $in.EntityDisplayName
            $inNum  = $in.incidentNumber
            
            Write-Host $fullURL
            
            if ((Read-Host "Ack this alert?") -like "*y*") {       
    
                $ackUser = (Get-ChildItem ENV:\USERNAME).Value  
                $ackMsg  = Read-Host "Ack message?"    
                $outputString = $null
                $outputString = "{
                `"userName`": `"$ackUser`",
                `"incidentNames`": [
                `"$inNum`"
                ],
                `"message`": `"$ackMsg`"
                }"

                Invoke-RestMethod -URi $fullURL -Method Patch -contentType "application/json" -Headers $headers -body $outputString
            
            }
           
            } elseif ($in.currentPhase -like 'ACKED') {
                
                $i++
                
                $ackURL  = "/api-public/v1/incidents/resolve"
                
                $fullURL = $baseURL + $ackURL
                
                Write-Host $in.EntityDisplayName
                Write-Host $in.incidentNumber
                
                $inName = $in.EntityDisplayName
                $inNum  = $in.incidentNumber
                
                if ((Read-Host "Resolve this alert?") -like "*y*") {       
        
                    $ackUser = (Get-ChildItem ENV:\USERNAME).Value  
                    $ackMsg  = Read-Host "Resolution message?"    
                    $outputString = $null
                    $outputString = "{
                    `"userName`": `"$ackUser`",
                    `"incidentNames`": [
                    `"$inNum`"
                    ],
                    `"message`": `"$ackMsg`"
                    }"

                    Invoke-RestMethod -URi $fullURL -Method Patch -contentType "application/json" -Headers $headers -body $outputString
                 
                } 
           }   
    }
    
    if ($i -eq 0) { Write-Host "No alerts to ack or resolve!" }
    
}


Switch ($team) {
  
    {$team -like "*team1*"}  {
        
        $teamName ='team1fullname'
    
    }

    {$team -like "*team2*"} {
        
        $teamName = 'team2fullname'
    
    }  
    
}

Switch ($getInfo) {
    
    {$_ -like "OnCall"} {
       
        if (!$team) {Write-Host `n"Team not specified!"`n -foregroundcolor Red;break}
        
        $onCallURL      = "/api-public/v1/team/$teamName/oncall/schedule"
        $onCallSchedule = Get-OnCallSchedule $baseurl $oncallurl  
        
        if (!$onCallSchedule.Schedule.overrideOnCall) {
    
            $onCallUser     = Get-QADuser $onCallSchedule.Schedule.onCall
           
        } else {
            
             $onCallUser     = Get-QADuser $onCallSchedule.Schedule.overrideOnCall
            
        }        
        
        if ($onCallUser) {
           
            $name   = $onCallUser.Name
            $mobile =  $onCalluser.MobilePhone
        
            Switch ($format) {
         
                {$_ -eq 'user'} {
                 
                    Write-Host `n"Currently on call for team: $team"
                    Write-Host $name "($mobile)"`n
                 
                }
             
                {$_ -eq 'prtg'} {
             
                    Write-Host "<prtg>"                     
                    Write-Host
                    "<result>"
                    "<channel>Team: $team</channel>"
                    "<value>1</value>"
                    "</result>" 
                    "<text>$name ($mobile)</text>"
                    Write-Host "</prtg>"
                    
                }
                
                {$_ -eq 'raw'} {
             
                    Return $onCallSchedule
                    
                }           
             }
         
         }
    }  
  
  {$_ -like "*Upcoming*"} {

        if (!$team) {Write-Host `n"Team not specified!"`n -foregroundcolor Red;break}
        
        $onCallURL      = "/api-public/v1/team/$teamName/oncall/schedule"
        $onCallSchedule = Get-OnCallSchedule $baseurl $oncallurl  
        $upcoming       = $onCallSchedule.schedule.rolls
        
        $upcoming | ForEach-Object{$WeekOf = $_.change.SubString(0,10);$Who = (Get-QADUser $_.OnCall).Name; Write-Host $WeekOf $Who}
          
  } 
  
  {$_ -like "*incidents*"} {
      
        $incidents = Get-Incident $baseURL
        #Maybe make this a switch...
        $unAcked   = $incidents | Where-Object{$_.currentPhase -eq 'UNACKED'}
        $unRes     = $incidents | Where-Object{$_.currentPhase -eq 'ACKED'}
        $resAlerts = $incidents | Where-Object{$_.currentPhase -eq 'RESOLVED'}
        
        if ($unAcked) {
            
            $counta = 1
            if ($unAcked.Count) {$counta = $unAcked.Count}
            $texta    = $unAcked.entityDisplayName 
          
        } else {$counta = 0;$texta = "No alarms in VictorOps!" }
        
        if ($unRes) {
            
            $countr = 1
            if ($unRes.Count) {$countr = $unRes.Count}
            $texta    = "Alert list: " + $unRes.entityDisplayName 
          
        } else {$countr = 0}  
              
        if ($resAlerts) {
       
            $countre = 1
            if ($resAlerts.Count) {$countre = $resAlerts.Count} 
          
        } else {$countre = 0}          
        
        Switch ($format) {
            
            {$_ -eq 'prtg'} {
                
                Write-Host "<prtg>"    
                                 
                Write-Host
                "<result>"
                "<channel>Unacknowledged Alerts</channel>"
                "<value>$counta</value>"
                "</result>" 
                "<text>$texta</text>"
            
                Write-Host
                "<result>"
                "<channel>Unresolved Alerts</channel>"
                "<value>$countr</value>"
                "</result>" 
                "<text>$textr</text>"
        
                Write-Host
                "<result>"
                "<channel>Resolved Alerts</channel>"
                "<value>$countre</value>"
                "</result>" 
                "<text>$textre</text>"
                
                Write-Host "</prtg>"        
                     
            }
            
            {$_ -eq 'user'} {
                
                Write-Host `n"Unacknowledged Alerts" 
                Write-Host `t$counta `n -foregroundcolor Red
                
                Write-Host "List:"`n
                
                foreach ($alert in $unAcked) {
                
                   Write-Host `t $alert.entityDisplayName `n
                
                }                
                
                Write-Host "Unresolved Alerts" 
                Write-Host `t$countr `n -foregroundcolor Red

                Write-Host "List:"`n
                
                foreach ($alert in $unRes) {
                
                   Write-Host `t $alert.entityDisplayName `n
                
                }

                
                Write-Host "Resolved Alerts" 
                Write-Host `t$countre`n -foregroundcolor Green
                
                Write-Host "List:"`n
                
                foreach ($alert in $resAlerts) {
                
                   Write-Host `t $alert.entityDisplayName `n
                
                }
            }
            
            {$_ -eq 'raw'} {
                
                Return $incidents 
             
            }            
            
        }

        
  }
  
  {$_ -like "*usersched*"} {
      
        if ($user) {
      
            $onCallURL      = "/api-public/v1/user/$user/oncall/schedule"
            $onCallSchedule = Get-OnCallSchedule $baseurl $oncallurl
            Return $onCallSchedule

        } else {Write-Host `n"User not specified!"`n -foregroundcolor Red}
  }
  
  {$_ -like "ack"} {
      
      Resolve-Incident $baseURL
      
  }
   
  {$_ -eq 'notset'} {
      
      Write-Host `n"Please specify the action you'd like to take via the -getInfo parameter" -foregroundColor Red
      Write-Host "For help type Get-Help $path\$scriptName"`n
      
  } 
                    
}

Have any ideas on how to use this, or general feedback? Leave a comment!

 

PowerShell: Check for NetApp failed and unowned disks (CDOT)

PowerShell: Check for NetApp failed and unowned disks (CDOT)

Why I needed this

I received a call from a technician in another site one day to let me know that we had 3 amber lights in our NetApp cluster. While autosupport should have caught this, it didn't (one controller showed them pre-failed, and the other showed them as failed disks).

So I turned to my love of PowerShell and added to the NetApp functions I wrote that perform actions when connected to NetApp controllers. If you haven't seen those posts, check them out!

How to use this

You'll need to first be connected to a NetApp controller and have the NetApp PowerShell module installed. Once that part is completed, you can use the code below. I also like to set a global variable for $foregroundColor, and this script utilizes that.

The Code

$netAppSystemInfo = Get-NCNode

        Write-Host `n"Controller information (CDOT)"`n -ForegroundColor $foregroundColor

        foreach ($node in $netappSystemInfo) {
            
            Write-Host "Node" $node.Node ` -foregroundcolor $foregroundcolor
            Write-Host "Model   :" $node.NodeModel
            Write-Host "Serial #:" $node.NodeSerialNumber
            Write-Host "Location:" $node.NodeLocation
            Write-Host "Healthy?:" $node.IsNodeHealthy
            Write-Host "Uptime  :" $node.NodeUptime
            Write-Host "ONTAPver:" $node.ProductVersion `n
            
        }
$failedDisks = Get-NcDisk | ?{ $_.DiskRaidInfo.ContainerType -eq "broken" }
        
        if ($failedDisks) {
        
            Write-Host `n"There are failed disks on this NetApp cluster!" -foregroundColor Red
            Write-Host "Failed Disks:"`n -foregroundColor $foregroundcolor
            
            foreach ($disk in $failedDisks){
                
                Write-Host "Disk Name      :" $disk.DiskName
                Write-Host "Disk UID       :" $disk.DiskUID
                Write-Host "Disk Controller:" $disk.NcController
                Write-Host "Disk Bay       :" $disk.Bay
                Write-Host "Disk Shelf     :" $disk.Shelf
                Write-Host "Disk model/FW  :" $disk.Model "/" $disk.FW `n
                
                
            }
            
            $openCase = Read-Host "Would you like to open a support case?"
            if ($openCase -like "*y*") {
                
                Start-Process "http://mysupport.netapp.com/cssportal/faces/oracle/webcenter/portalapp/pages/css/casesparts/CreateCaseLanding.jspx?_adf.no-new-window-redirect=true"
                
            }
            
        }
        
        
        $unOwnedDisks =  get-NCDisk
        
        foreach ($d in $unOwnedDisks) { 

            if ($d.diskownershipinfo.OwnerNodeName -eq $null) {
                
                Write-Host `n"Unowned disks found!" -foregroundColor Red
                Write-Host "Unowned Disks:"`n -foregroundcolor $foregroundColor
                Write-Host "Disk Name:" $d.DiskName
                
                $setOwner = Read-Host "Set owner to one of the nodes in the cluster?"
                
                if ($setOwner -like "*y*") {
                    
                    $i = 0 
                    foreach ($node in $netAppSystemInfo) {
                        
                        Write-Host $i -> "Node:" $node.Node
                        $i++
                    
                }
                
                $node = Read-Host "Which node #?"
                $nodeSystemID = $netAppSystemInfo[$node].NodeSystemID
                $nodeName = $netAppSystemInfo[$node].node
                $diskname = $d.DiskName
                $confirm = Read-Host "Set disk owner for disk $diskName to:" $nodeName "with systemID" $nodeSystemID"?"
                
                if ($confirm -like "*y*") {
                    
                    Set-NCDiskOwner -DiskName $diskName -NodeId $nodeSystemID
                    
                }
                    
                    
                    
                }

            } 
        
    
        }

What it will do

It will hunt down any failed or unowned disks. It will then prompt you to open the support page to open a new case. If it finds any unowned disks, it will give you the option to assign them to the controller of your choice.

As always, let me know if you have any feedback!

-Ginger Ninja

 

PowerShell: Email yourself a workout routine

PowerShell: Email yourself a workout routine

I've been into fitness for quite some time. Personally, for logging my workouts I love to use Fitocracy. As for finding my rep max and getting back into a workout routine... I took a geekier approach.

Why and what it taught me

This script was my way to learn how to do some math in PowerShell. From percentages to basic rounding, and then some. I wanted to do the rounding so the "plate" math made more sense.

Here are some ways to run the script:

.\531.ps1 -Max 145 -Reps 10

Running it like this will give you an output in the console of what your 5/3/1 routine at this max and rep range would be.

Another way to run it would be:

.\531.ps1 -Max 145 -Reps 10 -sendEmail email@address.com -workoutName Squats

The above would send the routine to that email address with the workout name in the subject line.

What you'll need to setup

If you want to use the email portion of this script you'll want to change the variables $emailUser, $emailFrom, $emlSignature, and if you're not using Gmail $SMTPServer.

You can use the line that's commented out to store your password locally, machine key encrypted.

#"password" | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString | Out-File .\emlpassword.txt

The Code

# Let's get our variables set
[cmdletbinding()]
param (
    [int]
    $Max,
    [int]
    $Reps,
    [double]
    $TMp,
    [string]
    $sendEmail,
    [string]
    $workoutName
)

if (!$Max)     {[int]$Max      = read-host "Weight?"}
if (!$Reps)    {[int]$Reps     = read-host "for how many reps?"}
if (!$TMp)     {[double]$TMp = .85}


#"password" | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString | Out-File .\emlpassword.txt

$emailPass    = Get-Content .\emlpassword.txt | ConvertTo-SecureString  

#Setup the email username and from address.
$emailUser    = 'emailuser'
$emailFrom    = 'youremail@gmail.com'
$emlSignature = '-Your Name'
$SMTPServer   = 'smtp.gmail.com'
$tmPretty     = $TMp * 100
$tmPretty     = $tmpretty.ToString() + "%"
[int]$roundBy = 10 


#Numbers boys! (and girls too...)
[int]$weight   = ([system.math]::round(($max * $reps * 0.0333 + $max)/$roundBy))*$roundBy 
[int]$tm       = ([system.math]::round(($tmp * $weight)/$roundBy))*$roundBy
[int]$warm     = ([system.math]::round((.10 * $tm)/$roundBy))*$roundBy

#Work sets
[int]$work1    = ([system.math]::round((.65 * $tm)/$roundBy))*$roundBy
[int]$work2    = ([system.math]::round((.75 * $tm)/$roundBy))*$roundBy
[int]$work3    = ([system.math]::round((.85 * $tm)/$roundBy))*$roundBy
[int]$work12   = ([system.math]::round((.70 * $tm)/$roundBy))*$roundBy
[int]$work22   = ([system.math]::round((.80 * $tm)/$roundBy))*$roundBy
[int]$work32   = ([system.math]::round((.90 * $tm)/$roundBy))*$roundBy
[int]$work13   = ([system.math]::round((.75 * $tm)/$roundBy))*$roundBy
[int]$work23   = ([system.math]::round((.85 * $tm)/$roundBy))*$roundBy
[int]$work33   = ([system.math]::round((.95 * $tm)/$roundBy))*$roundBy
[int]$bbbfwks  = ([system.math]::round((.40 * $tm)/$roundBy))*$roundBy
[int]$bbbfwks2 = ([system.math]::round((.50 * $tm)/$roundBy))*$roundBy

#Warm up sets
[int]$wset1    = $work1 - $warm
[int]$wset2    = $wset1 - $warm
[int]$wset3    = $wset2 - $warm
[int]$wset12   = $work12 - $warm
[int]$wset22   = $wset12 - $warm
[int]$wset32   = $wset22 - $warm
[int]$wset13   = $work13 - $warm
[int]$wset23   = $wset13 - $warm
[int]$wset33   = $wset23 - $warm

#Let's build the body
$body =  Write-Output `n "531 Workout!" `n
$body += Write-Output "Your estimated 1RM is: $weight" 
$body += Write-Output "Your $tmPretty training max is: $tm" 
$body += Write-Output "Warm up lbs: $warm" `n

$body += Write-Output "Week 1" `n

$body += Write-Output "Warm up sets:" `n

$body += Write-Output "$wset3 x 5"
$body += Write-Output "$wset2 x 5"
$body += Write-Output "$wset1 x 3" `n

$body += Write-Output "Work sets:" `n
$body += Write-Output "$work1 x 5"
$body += Write-Output "$work2 x 5"
$body += Write-Output "$work3 x 5+" `n
$body += Write-Output "BBB" 
$body += Write-Output "5x10 @ $bbbfwks" `n

$body += Write-Output "Week 2" `n

$body += Write-Output "Warm up sets:" `n
$body += Write-Output "$wset32 x 5"
$body += Write-Output "$wset22 x 5"
$body += Write-Output "$wset12 x 3" `n

$body += Write-Output "Work sets:" `n
$body += Write-Output "$work12 x 5"
$body += Write-Output "$work22 x 5"
$body += Write-Output "$work32 x 3+" `n
$body += Write-Output "BBB" 
$body += Write-Output "5x10 @ $bbbfwks" `n

$body += Write-Output "Week 3" `n

$body += Write-Output "Warm up sets:" `n
$body += Write-Output "$wset33 x 5"
$body += Write-Output "$wset23 x 5"
$body += Write-Output "$wset13 x 3" `n

$body += Write-Output "Work sets:" `n
$body += Write-Output "$work13 x 5"
$body += Write-Output "$work23 x 5"
$body += Write-Output "$work33 x 1+" `n
$body += Write-Output "BBB" 
$body += Write-Output "5x10 @ $bbbfwks2" `n `n
$body += Write-Output $emlSignature
$body = $body | Out-String

function Send-531Email {

    [cmdletbinding()]
    param(
    [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
    [string]
    $To,
    
    [string]
    [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
    $Subject,
    
    [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
    $Body)

    if (!$to)      {Write-Error "No recipient specified";break}
    if (!$subject) {Write-Error "No subject specified";break}
    if (!$body)    {Write-Error "No body specified";break}
   
       
    $SMTPClient  = New-Object Net.Mail.SmtpClient($SmtpServer, 587) 
    $SMTPMessage = New-Object System.Net.Mail.MailMessage($EmailFrom,$To,$Subject,$Body)

    $SMTPClient.EnableSsl = $true 
    $SMTPClient.Credentials = New-Object System.Net.NetworkCredential($emailUser,$emailPass); 
    
    $SMTPClient.Send($SMTPMessage)

}

if ($sendEmail) {Send-531Email -to $sendEmail -Subject "531 workout schedule for: $workoutName" -Body $body} else { $body }

PowerShell: Working with CSV Files

PowerShell: Working with CSV Files

Why did I want to do this?

I wanted to learn how to manipulate CSV files for logging purposes as well as for part of a script I was writing to email out a different quest each day. 

In particular I wanted to see if today was completed, and if not, update it to say so and then add the next day with Completed = No.

The setup

This function is part of a larger script that I'll write a post about after covering more of the basics.

You'll need the following things to get it running:

A file named runlog.csv in the same folder with the following contents:

“DateTime”,”Day”,”Completed”
”1/18/2016”,”1”,”No”

Preferably you'll want the first DateTime to match the current date, and day to start at 1. The script should be able to catch a gap in the DateTime and continue on, however.

Now you'll need to setup the following variables:

$todayDate        = (Get-Date).ToShortDateString()
$tomorrowDate     = (Get-Date).AddDays(1).ToShortDateString()
$runLog           = Import-CSV .\runlog.csv
$logHeaders       = @{
    "DateTime"  = '' 
    "Day"       = ''
    "Completed" = '' 
}

How to call the function

You'll want to call the function like so:

if ($updateLog)   {Log-DayQuest $todayDate $tomorrowDate $logHeaders}

At the beginning of the script you'd want something like:

[cmdletbinding()]
Param(
    [boolean]
    $updateLog
)

The code (let's put it all together!)

[cmdletbinding()]
Param(
    [boolean]
    $updateLog
)

$todayDate        = (Get-Date).ToShortDateString()
$tomorrowDate     = (Get-Date).AddDays(1).ToShortDateString()
$runLog           = Import-CSV .\runlog.csv
$logHeaders       = @{
    "DateTime"  = '' 
    "Day"       = ''
    "Completed" = '' 
} 

function Log-DayQuest {
    [cmdletbinding()]
    param($todayDate,$tomorrowDate,$logHeaders)
    
    [int]$questDay   = ($runLog | Where-Object {$_.DateTime -eq $todayDate} | Select-Object Day).Day
    
        if (($runLog | Where-Object {$_.DateTime -eq $todayDate} | Select-Object Completed).Completed -eq "Yes") {
    
            Write-Host "Log already updated!"
 
        } Elseif ($runLog | Where-Object {$_.DateTime -eq $todayDate})  { 

            [int]$day = ($runLog | Where-Object {$_.DateTime -eq $todayDate} | Select-Object Day).Day
        
            #Log today as completed
            ($runLog | Where-Object {$_.DateTime -eq $todayDate}).Completed = "Yes"        

            $runLog | Export-CSV .\runlog.csv -NoTypeInformation
          
            #Log tomorrow as not completed
            
            $logHeaders.DateTime  = $tomorrowDate
            $logheaders.Day       = $day+1
            $logheaders.Completed = "No"

            $newrow = New-Object PSObject -Property $logHeaders
            Export-CSV .\runLog.csv -InputObject $newrow -append -Force
            
            Write-Host "Log updated!"
        
        } elseif($runLog | Where-Object {$_.DateTime -eq $todayDate} -eq $null) {
            
            Write-Host "No entry for today... creating entry and updating"
            [int]$day = ($runlog[$runlog.count-1]).day 
            $logHeaders.DateTime  = $todayDate
            $logheaders.Day       = $day+1
            $logheaders.Completed = "Yes"
            
            $newrow = New-Object PSObject -Property $logHeaders
            Export-CSV .\runLog.csv -InputObject $newrow -append -Force

        }
}

if ($updateLog)   {Log-DayQuest $todayDate $tomorrowDate $logHeaders}

The results

Here are the results of me testing the script.

More on how it works coming up!

Please let me know what you think or if you have a quicker way to accomplish the same thing.

One of the things I love about PowerShell are the different ways to accomplish the same thing. That's the best way to learn. 

PowerShell: Working with the NetApp module (part 2) 7-Mode

PowerShell: Working with the NetApp module (part 2) 7-Mode

A couple weeks ago I shared part of a script that enables you to separate out the connection to 7-Mode and CDOT controllers.

If you missed that, check it out!

Today I will share the part that expands upon what you can do with the 7-Mode information.

The Code:

Function Get-7ModeInfo {


param ($naCredentials,
       $naController)
    
    if (Get-NASystemVersion) {
        $netAppSystemInfo = Get-NASystemInfo

        Write-Host `n"Controller information (7 Mode)" -ForegroundColor $foregroundColor

        Write-Host   "System Name      : " $netAppSystemInfo.SystemName 
        Write-Host   "System ID        : " $netAppSystemInfo.SystemId
        Write-Host   "System Revision  : " $netAppSystemInfo.SystemRevision 
        Write-Host   "System Serial    : " $NetAppSystemInfo.SystemSerialNumber
    
        Write-Host `n"Partner Information" -ForegroundColor $foregroundColor

        Write-Host   "Partner          : " $netAppSystemInfo.PartnerSystemName 
        Write-Host   "Partner System ID: " $netAppSystemInfo.PartnerSystemId

        Write-Host `n"Options..." -ForegroundColor $foregroundColor
        Write-Host "1. Reset your password"
        Write-Host "2. Search through volumes"
        Write-Host "3. Volume Space Report"
        Write-Host "4. Lun Report"`n
        Write-Host "'q' drops to shell"`n

        $selection = Read-Host "Which #?"
        
        Switch ($selection) {

            1 {
    
                $userName     = $NetAppCredential.UserName 
                $oldPass      = $netAppCredential.Password | ConvertFrom-SecureString        
                $newPassword  = Get-Credential -Message "Enter new password below" -UserName "Doesn't Matter"
                $newdPassword = $newPassword.GetNetworkCredential().Password

                Set-NaUserPassword -User $userName -OldPassword $oldPass -NewPassword $newdPassword
                
                Get-7ModeInfo

            }

            2 {

            }

            3{ 

            Get-NaVol | Select @{Name="VolumeName";Expression={$_.name}},@{Name="TotalSize(GB)";Expression={[math]::Round([decimal]$_.SizeTotal/1gb,2)}},@{Name="AvailableSize(GB)";Expression={[math]::Round([decimal]$_.SizeAvailable/1gb,2)}},@{Name="UsedSize(GB)";Expression={[math]::Round([decimal]$_.SizeUsed/1gb,2)}},@{Name="SnapshotBlocksReserved(GB)";Expression={[math]::Round([decimal]$_.SnapshotBlocksReserved/1gb,2)}},SnapshotPercentReserved,Aggregate,IsDedupeEnabled,type,DiskCount,RaidStatus,Autosize,ChecksumStyle,state | Export-Csv -LiteralPath $Location\$nacontroller.csv -Force -NoTypeInformation -Verbose
            Start-Process .\$nacontroller.csv
            
            Get-7ModeInfo
            
            }

            4{

            Get-NaLun | Select Path,@{Name="TotalSize(GB)";Expression={[math]::Round([decimal]$_.TotalSize/1gb,2)}},@{Name="UsedSize(GB)";Expression={[math]::Round([decimal]$_.SizeUsed/1gb,2)}},Protocol,Online,Thin,Comment | Export-Csv -LiteralPath $Location\$nacontroller"luns".csv -Force -NoTypeInformation -Verbose
            Start-Process .\$nacontroller"luns".csv
            
            Get-7ModeInfo

            }
             
            q {Write-Host "I see 'q', dropping to shell..." -foregroundcolor $foregroundcolor;break}

            }
        }
}

This is very basic and some of the switch options are not even set up yet. I used it as a shell to get some ideas for further administration. Coming up next will will be the same thing but with CDOT commands!

As always, let me know if you have any ideas or comments.

Get VMware Guest OS List With PowerCLI

Get VMware Guest OS List With PowerCLI

Get VMware Guest OS List With PowerCLI

What it does...

This function I wrote will return an object which contains the unique name of each OS for guests on a vCenter server. It will also show you the total number of VMs with that OS.

What it needs to run...

This function takes the parameter $vCenter. This should be the name of a vCenter server in your environment. You can also call the function as part of a foreach loop if you have multiple vCenter servers, running it once for each server and then returning that into another variable.

The Code:

function Get-VMOSList {
    [cmdletbinding()]
    param($vCenter)
    
    Connect-VIServer $vCenter  | Out-Null
    
    [array]$osNameObject       = $null
    $vmHosts                   = Get-VMHost
    $i = 0
    
    foreach ($h in $vmHosts) {
        
        Write-Progress -Activity "Going through each host in $vCenter..." -Status "Current Host: $h" -PercentComplete ($i/$vmHosts.Count*100)
        $osName = ($h | Get-VM | Get-View).Summary.Config.GuestFullName
        [array]$guestOSList += $osName
        Write-Verbose "Found OS: $osName"
        
        $i++    
 
    
    }
    
    $names = $guestOSList | Select-Object -Unique
    
    $i = 0
    
    foreach ($n in $names) { 
    
        Write-Progress -Activity "Going through VM OS Types in $vCenter..." -Status "Current Name: $n" -PercentComplete ($i/$names.Count*100)
        $vmTotal = ($guestOSList | ?{$_ -eq $n}).Count
        
        $osNameProperty  = @{'Name'=$n} 
        $osNameProperty += @{'Total VMs'=$vmTotal}
        $osNameProperty += @{'vCenter'=$vcenter}
        
        $osnO             = New-Object PSObject -Property $osNameProperty
        $osNameObject     += $osnO
        
        $i++
    
    }    
    Disconnect-VIserver -force -confirm:$false
        
    Return $osNameObject
}

Here is some example output (I tested it within another script I wrote). 
I blacked out the vCenter server name for security reasons.

 

Here is an example of the code I use to wrap it:

In this example the variables that already exist are:

$vCenterServers (this is an array of my vCenter servers)
$vCenter              (this is a parameter of my main script, the value of "all" is what the example below shows)
$getGuestOSList (this is a boolean parameter my script uses. What you see below is what happens if it is $true)

The below code will go through each of the individual vCenter servers and use the function above to build the variable $guestOSObjects with the output.

if (Get-View $DefaultViserver.ExtensionData.Client.ServiceContent.SessionManager) {Disconnect-VIServer * -confirm:$false -force;Write-Host "Disconnecting VI Server Sessions" -foregroundColor $foregroundColor}

if ($getGuestOSList) {
    
    Switch ($vCenter) {
    {$_ -like "all"} {
        
        $i = 0
        
        Foreach ($v in $vCenterServers) {
           
            $vCenter = $vcenterServers[$i]
            Write-Progress -Activity "Looking up VM Guests" -Status "Current vCenter Server: $vCenter" -PercentComplete ($i/$vCenterServers.Count*100)
            [array]$GuestOSObjects += Get-VMOSList $vCenter
      
            $i++
        
        }
        
    Return $guestOSOBjects
    
    }
    
}

PowerShell: Working with the NetApp module (part 1)

PowerShell: Working with the NetApp module (part 1)

I've been tasked with heading up a storage migration where I am currently employed.

We have both 7-Mode and CDOT systems here (NetApp), and I wanted a way to run different functions with varying options based on each OS. 

To do this I created a couple variables up front:

$accountRun and $NetAppControllers.

$accountRun simply gets the account name you're running PowerShell as.

$accountRun               = (Get-ChildItem ENV:username).Value

$netAppControllers is an array of your controllers.

$global:NetAppControllers = @("controller1","controller2-c")

Notice how controller2 has -c after it. This is used in the below code to delineate between 7-Mode and CDOT controllers.

function Connect-NetAppController {

    $i = 0
    Write-Host `n"NetApp Controllers"`n -ForegroundColor $foregroundColor

    foreach ($n in $NetAppControllers) { 
    
        if ($n.Split("-")[1] -like "*c*"){

            Write-Host $i -NoNewline
            Write-Host -> -NoNewLine -ForegroundColor $foregroundcolor 
            Write-Host $n.Split("-")[0]  -NoNewline
            Write-Host "(CDOT)" -ForegroundColor $foregroundColor

        } else {

            Write-Host $i -NoNewline
            Write-Host -> -NoNewLine -ForegroundColor $foregroundcolor 
            Write-Host $n 

        }

        $i++

}
    Write-Host `n

    $selection = Read-Host "Which #?"
    
    if (($netAppControllers[$selection]).Split("-")[1] -like "*c*") { 
    Write-Host ($netAppControllers[$selection]).Split("-")[0]
        $nController = $netAppControllers[$selection].Split("-")[0]
        
        Write-Host `n"Attempting to connect to: " $nController "(CDOT)"`n -ForegroundColor $foregroundColor
        
        if (Test-Connection $nController -Count 1 -Quiet) {
            
        Write-Host `n"We are able to connect to:" $nController "... gathering credentials."`n -foregroundcolor $foregroundColor
        
        $netAppCredential = Get-Credential -UserName $accountRun.ToLower() -Message "Enter NetApp Credentials for $nController"
       
        Connect-NcController $nController -Credential $netAppCredential | Out-Null

        Get-CDOTInfo $netAppCredential $nController
    
        } Else {
            
            Write-Host `n"Unable to ping: " $nController`n -foregroundcolor Red
            
        }

    } else {

        $7Controller = $NetAppControllers[$selection]
        
        Write-Host `n"Attempting to connect to: " $7Controller`n -ForegroundColor $foregroundColor

        if (Test-Connection $NetAppControllers[$selection] -Count 1 -Quiet) {
        
        $7Controller = $NetAppControllers[$selection]
            
        Write-Host `n"We are able to connect to:" $7Controller "... gathering credentials."`n -foregroundcolor $foregroundColor 
            
        $netAppCredential = Get-Credential -UserName $accountRun.ToLower() -Message "Enter NetApp Credentials for $nController"

        Connect-NaController $NetAppControllers[$selection] -Credential $netAppCredential | Out-Null

        Get-7ModeInfo $netAppCredential $NetAppControllers[$selection]
       
         } Else {
            
            Write-Host `n"Unable to ping: " $7Controller`n -foregroundcolor Red
            
        }
         
     }
  }

Let me know what you think, xor you have any ideas!

PowerShell: Get-DCDiag script for getting domain controller diagnostic information

PowerShell: Get-DCDiag script for getting domain controller diagnostic information

I wrote a script that will run DCDiag on domain controllers that you specify or all DCs in your environment. 

I will be working to improve the script as much as I can. This was a way for me to learn how advanced parameters and object building works in PowerShell.

The "all" and "full" options currently return an object that you can manipulate. The other options will do that soon once I update the script and test it more.

<#   
.SYNOPSIS   
   Display DCDiag information on domain controllers.
.DESCRIPTION 
   Display DCDiag information on domain controllers. $adminCredential and $ourDCs should be set externally.
   $ourDCs should be an array of all your domain controllers. This function will attempt to set it if it is not set via QAD tools.
   $adminCredential should contain a credential object that has access to the DCs. This function will prompt for credentials if not set.
   If the all dc option is used along side -Type full, it will return an object you can manipulate.
.PARAMETER DC 
    Specify the DC you'd like to run dcdiag on. Use "all" for all DCs.
.PARAMETER Type 
    Specify the type of information you'd like to see. Default is "error". You can specify "full"           
.NOTES   
    Name: Get-DCDiagInfo
    Author: Ginger Ninja (Mike Roberts)
    DateCreated: 12/08/2015
.LINK  
    https://www.gngrninja.com/script-ninja/2015/12/29/powershell-get-dcdiag-commandlet-for-getting-dc-diagnostic-information      
.EXAMPLE   
    Get-DCDiagInfo -DC idcprddc1 -Type full
    $DCDiagInfo = Get-DCDiagInfo -DC all -type full -Verbose
#>  
    [cmdletbinding()]
    param(
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [String]
        $DC,
        
        [Parameter()]
        [ValidateScript({$_ -like "Full" -xor $_ -like "Error"})]
        [String]
        $Type,
        
        [Parameter(Mandatory=$false,ValueFromPipeline=$false)]
        [String]
        $Utility
        )
    
    try {
        
    if (!$ourDCs) {
        
        $ourDCs = get-QADComputer -computerRole 'DomainController'
    
    }
    
    if (!$adminCredential) {
        
        $adminCredential = Get-Credential -Message "Please enter Domain Admin credentials"
        
    }
    
    Switch ($dc) {
    
    {$_ -eq $null -or $_ -like "*all*" -or $_ -eq ""} {
    
        Switch ($type) {  
            
        {$_ -like "*error*" -or $_ -like $null} {  
             
            [array]$dcErrors = $null
            $i               = 0
            
            foreach ($d in $ourDCs){
            
                $name = $d.Name    
                
                Write-Verbose "Domain controller: $name"
                
                Write-Progress -Activity "Connecting to DC and running dcdiag..." -Status "Current DC: $name" -PercentComplete ($i/$ourDCs.Count*100)
                
                $session = New-PSSession -ComputerName $d.Name -Credential $adminCredential
                
                Write-Verbose "Established PSSession..."
                
                $dcdiag  = Invoke-Command -Session $session -Command  { dcdiag }
                
                Write-Verbose "dcdiag command ran via Invoke-Command..."
            
                if ($dcdiag | ?{$_ -like "*failed test*"}) {
                    
                    Write-Verbose "Failure detected!"
                    $failed = $dcdiag | ?{$_ -like "*failed test*"}
                    Write-Verbose $failed
                    [array]$dcErrors += $failed.Replace(".","").Trim("")
            
                } else {
                
                    $name = $d.Name    
                
                    Write-Verbose "$name passed!"
                    
                }
                
                
                Remove-PSSession -Session $session
                
                Write-Verbose "PSSession closed to: $name"
                $i++
            }
            
            Return $dcErrors
        } 
            
        {$_ -like "*full*"}    {
            
            [array]$dcFull             = $null
            [array]$dcDiagObject       = $null
            $defaultDisplaySet         = 'Name','Error','Diag'
            $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet',[string[]]$defaultDisplaySet)
            $PSStandardMembers         = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet)
            $i                         = 0
            
            foreach ($d in $ourDCs){
                
                $diagError = $false
                $name      = $d.Name
                
                Write-Verbose "Domain controller: $name"
                
                Write-Progress -Activity "Connecting to DC and running dcdiag..." -Status "Current DC: $name" -PercentComplete ($i/$ourDCs.Count*100)
                
                $session = New-PSSession -ComputerName $d.Name -Credential $adminCredential
                
                Write-Verbose "Established PSSession..."
                
                $dcdiag  = Invoke-Command -Session $session -Command  { dcdiag }
                
                Write-Verbose "dcdiag command ran via Invoke-Command..."
                
                $diagstring = $dcdiag | Out-String
                
                Write-Verbose $diagstring
                if ($diagstring -like "*failed*") {$diagError = $true}
                
                $dcDiagProperty  = @{Name=$name}
                $dcDiagProperty += @{Error=$diagError}
                $dcDiagProperty += @{Diag=$diagstring}
                $dcO             = New-Object PSObject -Property $dcDiagProperty
                $dcDiagObject   += $dcO
                
                Remove-PSSession -Session $session
                
                Write-Verbose "PSSession closed to: $name"
                
                $i++
            }
            
            $dcDiagObject.PSObject.TypeNames.Insert(0,'User.Information')
            $dcDiagObject | Add-Member MemberSet PSStandardMembers $PSStandardMembers
            
            Return $dcDiagObject
        
            }
        
        }
         break         
    }
   
   
    {$_ -notlike "*all*" -or $_ -notlike $null} {
   
        Switch ($type) {
        
        {$_ -like "*error*" -or $_ -like $null} {
        
            if (Get-ADDomainController $dc) { 
    
                Write-Host "Domain controller: " $dc `n -foregroundColor $foregroundColor
            
                $session = New-PSSession -ComputerName $dc -Credential $adminCredential
                $dcdiag  = Invoke-Command -Session $session -Command  { dcdiag }
       
                if ($dcdiag | ?{$_ -like "*failed test*"}) {
                
                    Write-Host "Failure detected!"
                
                    $failed = $dcdiag | ?{$_ -like "*failed test*"}
                
                    Write-Output $failed 
                
                } else { 
                
                    Write-Host $dc " passed!"
                
                }
                    
            Remove-PSSession -Session $session       
            } 
        }
        
        {$_ -like "full"} {
            
            if (Get-ADDomainController $dc) { 
    
                Write-Host "Domain controller: " $dc `n -foregroundColor $foregroundColor
            
                $session = New-PSSession -ComputerName $dc -Credential $adminCredential
                $dcdiag  = Invoke-Command -Session $session -Command  { dcdiag }
                $dcdiag     
                    
                Remove-PSSession -Session $session       
            }     
                
        }
        
    }
    
    }
    
    }
    
    }
    
    Catch  [System.Management.Automation.RuntimeException] {
      
        Write-Warning "Error occured: $_"
 
        
     }
    
    Finally { Write-Verbose "Get-DCDiagInfo function execution completed."}

You can run this as a script or as a function within a script depending on your needs. Let me know if you have any feedback!