psFitb1t.psm1

#=======================================================================#
#
# Author: Collin Chaffin
# Last Modified: 05-01-2016 12:00PM
# Filename: psFitb1t.psm1
#
#
# Changelog:
#
# v 1.0.0.1 : 03-13-2016 : Initial release
# v 1.0.0.2 : 03-30-2016 : Added Get-HRmonth function
# v 1.0.0.3 : 05-01-2016 : (Re)Published to PS Gallery
#
# Notes:
#
# This module utilizes personal Fitbit's user-specific API
# information to perform OAuth connection to Fitbit and submit the
# request for your heart rate data for any 24 hour period.
#
#
# Installation Instructions:
#
# Run the MSI installer or, if installing manually, copy the
# psFitb1t.psm1 and psFitb1t.psd files to:
# "%PSModulePath%psFitb1t"
#
# HINT: To manually create the module folder prior to copying:
# mkdir "%PSModulePath%psFitb1t"
#
# Once installed/copied, open Windows Powershell and execute:
# Import-Module psFitb1t
#
# Store your Fitbit API information by executing:
# Set-FitbitOAuthTokens
#
# If you have gotten this far, you should be able to query your
# first date by executing:
# Get-HRData -QueryDate "2016-01-01"
#
# Verification:
#
# Check "%PSModulePath%psFitb1t\Logs" folder for a daily rotating log.
# Example log for successful query:
#
# 03/13/2016 12:00:00 :: [INFO] :: START - Get-HRdata function execution
# 03/13/2016 12:00:00 :: [INFO] :: START - Connect-OAuthFitbit function execution
# 03/13/2016 12:00:00 :: [INFO] :: START - Loading DOTNET assemblies
# 03/13/2016 12:00:00 :: [INFO] :: FINISH - Loading DOTNET assemblies
# 03/13/2016 12:00:00 :: [INFO] :: START - Retrieving Fitbit API settings from registry
# 03/13/2016 12:00:00 :: [INFO] :: FINISH - Retrieving Fitbit API settings from registry
# 03/13/2016 12:00:01 :: [INFO] :: FINISH - Connect-OAuthFitbit function execution
# 03/13/2016 12:00:01 :: [INFO] :: START - Sending HTTP POST via REST to Fitbit
# 03/04/2016 12:00:44 :: [INFO] :: FINISH - Sending HTTP POST via REST to Fitbit
# 03/04/2016 12:00:44 :: [INFO] :: FINISH - Get-HRdata function execution
#
#=======================================================================#


#region Globals

#########################################################################
# Global Variables #
#########################################################################

# General Variables
# Disable psFitb1tDebugging for zero output and logging
$psFitb1tDebugging = $true
$psFitb1tLogging = $true
$Script:psFitb1tScope = "activity%20heartrate%20location%20nutrition%20profile%20settings%20sleep%20social%20weight"
$Script:psFitb1tTokenAge = "2592000"

#You should only have to change this redirect URL if you did not follow the directions and set it to this predetermined value:
$Script:psFitb1tRedirectURL = "https://localhost/psfitb1t"
$Script:psFitb1tHRQueryDate = ""
$Script:psFitb1tTokenReturnedAge = ""
$Script:psFitb1tAuthCode = ""

#Auth URL First
#$Script:psFitb1tAuthorizeUrl = "https://www.fitbit.com/oauth2/authorize?response_type=token&client_id=$psFitb1tClientID&redirect_uri=$psFitb1tRedirectURL&scope=$psFitb1tScope&expires_in=$psFitb1tTokenAge"

# Paths
$Script:psFitb1tInvocationPath = $([System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Definition) + "\")
$Script:psFitb1tLogPath = $($psFitb1tInvocationPath) + "Logs\"
# Override this with a static manual path, if so desired or it defaults to \Logs folder in Module location

#########################################################################

#endregion

#region Functions

#########################################################################
# Functions #
#########################################################################


function Connect-OAuthFitbit
{
    <#
    .SYNOPSIS
        This function utilizes personal Fitbit's user-specific API information to perform
        OAuth connection to Fitbit and set up the final OAuth string needed to then retrieve
        24hrs of HR data using the REST API.
         
    .DESCRIPTION
        Author: Collin Chaffin
        Description: This function utilizes personal Fitbit's user-specific API
                        information to perform OAuth connection to Fitbit.
    #>

    [CmdletBinding()]
    [OutputType([System.String])]
    param    (                    
    )    
    BEGIN
    {        
        (Write-Status -Message "START - Connect-OAuthFitbit function execution" -Status "INFO" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
        try
        {
            (Write-Status -Message "START - Loading DOTNET assemblies" -Status "INFO" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
            [Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null
            [Reflection.Assembly]::LoadWithPartialName("System.Net") | Out-Null
            [Reflection.Assembly]::LoadWithPartialName("System.Web") | Out-Null
            (Write-Status -Message "FINISH - Loading DOTNET assemblies" -Status "INFO" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
        }
        catch
        {
            Throw $("ERROR OCCURRED WHILE LOADING REQUIRED DOTNET ASSEMBLIES " + $_.Exception.Message)            
        }
        
        # Retrieve required user-specific Fitbit API info from registry
        try
        {            
            if ($((Test-Path -Path HKCU:\Software\psFitb1t) -eq $false) `
               -or $((Get-Item HKCU:\Software\psFitb1t).getvalue("psFitb1tClientID") -eq $null) `
                -or $((Get-Item HKCU:\Software\psFitb1t).getvalue("psFitb1tRedirectURL") -eq $null)                
               )
            {
                (Write-Status -Message "Fitbit API settings not found - prompting operator" -Status "WARNING" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
                # Call Set-FitbitOAuthTokens function to prompt for credentials and store them
                Set-FitbitOAuthTokens
            }
            else
            {
                (Write-Status -Message "START - Retrieving Fitbit API settings from registry" -Status "INFO" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
                $Script:psFitb1tClientID                = (Get-Item HKCU:\Software\psFitb1t).getvalue("psFitb1tClientID")
                $Script:psFitb1tRedirectURL            = (Get-Item HKCU:\Software\psFitb1t).getvalue("psFitb1tRedirectURL")                
                $Script:psFitb1tTokenReturnedAge     = (Get-Item HKCU:\Software\psFitb1t).getvalue("psFitb1tTokenAge")
                $Script:psFitb1tAuthToken         = (Get-Item HKCU:\Software\psFitb1t).getvalue("psFitb1tAuthToken")
                (Write-Status -Message "FINISH - Retrieving Fitbit API settings from registry" -Status "INFO" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
            }            
        }
        catch
        {
            Throw $("ERROR OCCURRED WHILE LOADING REQUIRED FITBIT API INFORMATION " + $_.Exception.Message)
        }
    }
    PROCESS
    {
        try
        {
            # Create a custom PSObject to store all require oAuth info and simply pass a single object to helper functions
            $objOAuth = @()
            $objOAuth = New-Object -TypeName PSObject
            $objOAuth | Add-Member -Name 'client_id' -Value $($psFitb1tClientID) -MemberType NoteProperty -Force
            $objOAuth | Add-Member -Name 'redirect_uri' -Value $($psFitb1tRedirectURL) -MemberType NoteProperty -Force
            $objOAuth | Add-Member -Name 'scope' -Value $($psFitb1tScope) -MemberType NoteProperty -Force
            $objOAuth | Add-Member -Name 'expires_in' -Value $($psFitb1tTokenAge) -MemberType NoteProperty -Force
            $objOAuth | Add-Member -Name 'token_remaining' -Value $($psFitb1tTokenReturnedAge) -MemberType NoteProperty -Force
            $objOAuth | Add-Member -Name 'date' -Value $($psFitb1tHRQueryDate) -MemberType NoteProperty -Force
            $objOAuth | Add-Member -Name 'access_token' -Value $($psFitb1tAuthToken) -MemberType NoteProperty -Force
                        
            #Do we have a good token already? Check registry
            if ($($Script:psFitb1tTokenReturnedAge) -and  $(Test-Date -inputDate $Script:psFitb1tTokenReturnedAge) -and $( [DateTime]$(get-date ([System.DateTime]::Now) -Format s) -lt [DateTime]$(Get-Date($psFitb1tTokenReturnedAge)) )  )
            {
                write-verbose "Token Okay"
                [string]$oAuthRequestString = $psFitb1tAuthToken
            }
            else
            {
                write-verbose "Token Expired $($Script:psFitb1tTokenReturnedAge)"
                # Finally, generate the final OAuth request string with all the above generated information passing one single custom PSObject
                [string]$oAuthRequestString = (Get-FitbitOAuthToken -objOAuth $objOAuth)                
            }            
            
            # Return the one single oAuth request POST string to hand back to calling function (tweet or direct message) to POST it
            Return $oAuthRequestString;
        }
        catch
        {
            Throw $("ERROR OCCURRED WHILE BUILDING OAUTH REQUEST " + $_.Exception.Message)
        }
    }
    END
    {
        (Write-Status -Message "FINISH - Connect-OAuthFitbit function execution" -Status "INFO" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
    }
}



function Get-FitbitOAuthToken
{
    <#
    .SYNOPSIS
        Generate a new Fitbit oAuth2 token or used stored
 
    .DESCRIPTION
        Author: Collin Chaffin
        Description: This function generates a new Fitbit oAuth2 token or used stored
         
    .PARAMETER objOAuth
        [PSObject] Custom PSObject containing all required OAuth information
     
    .EXAMPLE
        $oAuthSignature = (Get-FitbitOAuthToken -objOAuth $objOAuth)
     
        $oAuthSignature
        ptUHUftvP0l635876583ygrgg346JQoJ+7yBa//uZcE=
    #>

    [CmdletBinding()]
    [OutputType([String])]
    param (
        [Parameter(Position = 0, Mandatory = $true)]        
            [ValidateNotNullOrEmpty()]
            [PSObject]
            $objOAuth
    )
    BEGIN
    {
        (Write-Status -Message "START - Get-FitbitOAuthToken function execution" -Status "INFO" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
    }
    PROCESS
    {
        try
        {
            (Write-Status -Message "START - Building OAuth signature" -Status "INFO" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
            
            # Make GET request to have user authorize and parse for token returned - run with -VERBOSE switch to view the string actually being sent!
            $Script:psFitb1tAuthorizeUrl = "https://www.fitbit.com/oauth2/authorize?response_type=token&client_id=$psFitb1tClientID&redirect_uri=$psFitb1tRedirectURL&scope=$psFitb1tScope&expires_in=$psFitb1tTokenAge"
            Write-Verbose "Sending $($psFitb1tAuthorizeUrl)"
            
            Add-Type -AssemblyName System.Windows.Forms
            $form = New-Object -TypeName System.Windows.Forms.Form -Property @{ Width = 440; Height = 640 }
            $browser = New-Object -TypeName System.Windows.Forms.WebBrowser -Property @{ Width = 420; Height = 600; Url = $psFitb1tAuthorizeUrl }
            
            $onDocumentCompleted = {
                
                $myurl = $browser.Url.AbsoluteUri
                if ($browser.Url.AbsoluteUri -match "access_token=([^&]*)")
                {
                    #Get the magic token
                    $Script:psFitb1tAuthCode = $Matches[1]
                                                            
                    if ($browser.Url.AbsoluteUri -match "expires_in=([^&]*)")
                    {
                        $Script:psFitb1tTokenReturnedAge = $(get-date ([System.DateTime]::Now).AddSeconds([int]$Matches[1]) -Format s)
                        Write-Verbose "Token expires: $($Script:psFitb1tTokenReturnedAge)"
                        
                        #write $Script:psFitb1tTokenReturnedAge to registry
                        New-ItemProperty HKCU:\Software\psFitb1t -name "psFitb1tTokenAge" -value "$Script:psFitb1tTokenReturnedAge" -Force | out-null
                        
                        #write $Script:psFitb1tAuthToken to registry
                        New-ItemProperty HKCU:\Software\psFitb1t -name "psFitb1tAuthToken" -value "$Script:psFitb1tAuthCode" -Force | out-null                                                
                        
                        $form.Close()
                    }
                    #Close it
                    $form.Close()
                }                
                elseif ($browser.Url.AbsoluteUri -match "error=")
                {
                    $form.Close()
                }
            }            
            
            $browser.add_DocumentCompleted($onDocumentCompleted)            
            $form.Controls.Add($browser)
            $null = $form.ShowDialog()
            
            Write-Verbose "Auth Code is: $($psFitb1tAuthCode)"            
            
            (Write-Status -Message "FINISH - Building OAuth signature" -Status "INFO" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
            
            return $psFitb1tAuthCode;
        }
        catch
        {
            Throw $("ERROR OCCURRED GENERATING NEW OAUTH SIGNATURE " + $_.Exception.Message)
        }        
    }
    END
    {
        (Write-Status -Message "FINISH - Get-FitbitOAuthToken function execution" -Status "INFO" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
    }
}


function Test-Date
{
    <#
    .SYNOPSIS
        Test if date is valid
 
    .DESCRIPTION
        Author: Collin Chaffin
        Description: This function tests if date is valid
 
    .PARAMETER inputDate
        [String] Date STRING to test with different possible formats
     
    .EXAMPLE
        Test-Date -inputDate "2016-03-29T03:23:28"
    #>

    param
    (
        [Parameter(Mandatory = $true)]
        $inputDate
    )    
    (($inputDate -as [DateTime]) -ne $null)
}


function Write-Status
{
    <#
    .SYNOPSIS
        Write a status message to console and log if debugging
 
    .DESCRIPTION
        Author: Collin Chaffin
        Description: This function writes a status message out to console
                        appending exact time/date of command execution and will
                        optionally write to daily log
 
    .PARAMETER Message
        [String] Message to write
             
    .PARAMETER Status
        [String] Status code string
     
    .PARAMETER Debugging
        [Bool] If this switch is true then output debugging to console
     
    .EXAMPLE
        Write-Status -Message "Action completed successfully" -Status "SUCCESS" -Debugging $debugging
     
        03/13/2016 12:00:00 :: [SUCCESS] :: Action completed successfully
    #>

    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]        
            [System.String]
            $Message,        
        [Parameter(Mandatory = $false)]
            [System.String]
            $Status = "INFO",
        [Parameter(Mandatory = $false)]
            [Switch]
            $Debugging,
        [Parameter(Mandatory = $false)]
            [Switch]
            $Logging,
        [Parameter(Mandatory = $false)]
            [System.String]
            $LogPath = $(($psFitb1tInvocationPath) + "Logs\")
    )
    BEGIN
    {
        try
        {    
            # Do not do anything unless global script debugging is true
            If ($Debugging -eq $true)
            {
                # Set up variables and log file/path
                [String]$statusTime = (Get-Date -Format "MM/dd/yyyy HH:mm:ss")
                
                # If -Logging passed, set up logging a a DAILY log file (change path in globals at top of script)
                if ($Logging -eq $true)
                {
                    If (!(Test-Path $psFitb1tLogPath)) { New-Item -ItemType Directory -Force -Path ($psFitb1tLogPath) | Out-Null }
                    [String]$logFileDate = (Get-Date -Format "MM-dd-yyyy")
                    [String]$logFile = $($psFitb1tLogPath) + "psFitb1t-" + $logFileDate + ".log"
                }
            }                
        }
        catch
        {
            Throw $("ERROR OCCURRED WHILE WRITING OUTPUT " + $_.Exception.Message)
        }
    }
    PROCESS
    {
        try
        {
            # Do not do anything unless global script debugging is true
            If ($Debugging -eq $true)
            {
                # Ensure custom status is always uppercase
                $Status = $Status.ToUpper()
                
                # Format output message
                $Message = "$statusTime :: [$Status] :: $Message"
                
                # Write out to console
                Write-Host $Message -ForegroundColor Cyan
                    
                # If -Logging passed, set up logging a a DAILY log file (change path in globals at top of script)
                if ($Logging -eq $true)
                {                
                    Add-Content -Path $logFile -Value ($Message)
                }
            }
        }
        catch
        {
            Throw $("ERROR OCCURRED WRITING STATUS" + $_.Exception.Message)
        }
    }
    END
    {
    }
}

Function Set-FitbitOAuthTokens
{
  <#
    .SYNOPSIS
        Stores required Fitbit API OAuth settings providing both GUI wizard and
        command-line options
         
 
    .DESCRIPTION
        Author: Collin Chaffin
        Description: This function stores the required Fitbit API settings
                        provided by the operator interactively into the HKCU
                        registry hive for subsequent sessions providing both
                        GUI wizard and command-line options
 
    .PARAMETER Force
        [Switch] Clear existing stored Fitbit API information and repopulate
 
    .PARAMETER psFitb1tClientID
        [String] Fitbit Client ID
 
    .PARAMETER psFitb1tRedirectURL
        [String] Fitbit Redirection URL
 
    .PARAMETER psFitb1tHRQueryDate
        [String] Fitbit Last Query Date
 
    .PARAMETER psFitb1tTokenAge
        [String] Fitbit Access Token Expiration Datetime
 
    .EXAMPLE
        Set-FitbitOAuthTokens
 
        If Fitbit API settings are not found in the registry, prompt the operator
        interactively via a GUI wizard to provide and open the Fitbit API webpage
        to assist operator in locating their user-specific Fitbit application information
         
        NOTE: Only missing information will be requested via wizard interface
 
    .EXAMPLE
        Set-FitbitOAuthTokens -Force
 
        Remove existing Fitbit API information from registry and repopulate via
        GUI wizard
 
    .EXAMPLE
        Set-FitbitOAuthTokens -Force -psFitb1tClientID "01234567890"
 
        Remove existing Fitbit API information from registry and repopulate via
        automatically detected "command-line" mode. In this case because all
        four required pieces of information were not provided, the missing three
        will be interactively prompted but via standard commandline text prompting
    #>

    [CmdletBinding(DefaultParameterSetName = 'Wizard')]
    [OutputType([System.String])]
    param
    (
        [Parameter(ParameterSetName = 'CmdLine', Mandatory = $false)]
        [Parameter(ParameterSetName = 'Wizard', Mandatory = $false)]
            [Switch]
            $Force,
        [Parameter(ParameterSetName = 'CmdLine', Mandatory = $true, HelpMessage = 'Please enter your Fitbit application CLIENT ID:')]
            [ValidateNotNullOrEmpty()]
            [System.String]
            $psFitb1tClientID,
        [Parameter(ParameterSetName = 'CmdLine', Mandatory = $false, HelpMessage = 'Please enter your Fitbit application REDIRECT URL:')]
            [ValidateNotNullOrEmpty()]
            [System.String]
            $psFitb1tRedirectURL = $Script:psFitb1tRedirectURL
    )
    BEGIN
    {
        (Write-Status -Message "START - Set-FitbitOAuthTokens function execution" -Status "INFO" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
    }
    PROCESS
    {
        # If we were passed -Force switch
        if ($Force.IsPresent)
        {
            try
            {                
                    # Force switch used, remove/clear all stored API info and drop to either wizard or cmdline to repopulate
                    if ($((Get-Item HKCU:\Software\psFitb1t).getvalue("psFitb1tClientID") -ne $null)) { Remove-ItemProperty HKCU:\Software\psFitb1t -name "psFitb1tClientID" }
                    if ($((Get-Item HKCU:\Software\psFitb1t).getvalue("psFitb1tRedirectURL") -ne $null)) { Remove-ItemProperty HKCU:\Software\psFitb1t -name "psFitb1tRedirectURL" }
                    if ($((Get-Item HKCU:\Software\psFitb1t).getvalue("psFitb1tHRQueryDate") -ne $null)) { Remove-ItemProperty HKCU:\Software\psFitb1t -name "psFitb1tHRQueryDate" }
                    if ($((Get-Item HKCU:\Software\psFitb1t).getvalue("psFitb1tTokenAge") -ne $null)) { Remove-ItemProperty HKCU:\Software\psFitb1t -name "psFitb1tTokenAge" }
                    if ($((Get-Item HKCU:\Software\psFitb1t).getvalue("psFitb1tAuthToken") -ne $null)) { Remove-ItemProperty HKCU:\Software\psFitb1t -name "psFitb1tAuthToken" }
                
            }
            catch
            {
                Throw $("ERROR OCCURRED CLEARING FITBIT API INFORMATION FROM REGISTRY " + $_.Exception.Message)
            }
        }
        
        # (Re)Populate the registry with 4 pieces of required Fitbit OAuth info
        try
        {    
            # If any single piece of info is missing, start the process
            if ($((Test-Path -Path HKCU:\Software\psFitb1t) -eq $false) `
               -or $((Get-Item HKCU:\Software\psFitb1t).getvalue("psFitb1tClientID") -eq $null) `
               -or $((Get-Item HKCU:\Software\psFitb1t).getvalue("psFitb1tRedirectURL") -eq $null)               
               )
            {
                Write-Host "`nPlease configure your personal Fitbit application from which you must store the following pieces of information:`n`n""Client ID""`n""Redirect URL""`n`nOpening default browser to: https://dev.fitbit.com" -ForegroundColor Yellow
                
                Start-Process "https://dev.fitbit.com/"
                
                # Entire reg key is missing so create it
                if (!(Test-Path -Path HKCU:\Software\psFitb1t))
                {
                    (Write-Status -Message "START - psFitb1t registry key creation" -Status "INFO" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
                    New-Item -Path HKCU:\Software -Name psFitb1t | out-null
                    (Write-Status -Message "FINISH - psFitb1t registry key creation" -Status "INFO" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
                }
                
                Switch ($PSCmdlet.ParameterSetName)
                {
                    'Wizard'
                    {
                        # Now that we are sure reg key exists, call the wizard form and prompt only for missing value(s)
                        # NOTE: If reg key exists and only 2 pieces of info are missing, operator only receives a wizard with 2 pages with 4 being all info missing
                        Call-psFitb1t-API_psf | Out-Null
                    }
                    'CmdLine'
                    {
                        if (!$psFitb1tClientID)
                        {
                            Write-Host "`n`nEnter Fitbit Client ID:" -ForegroundColor Yellow -NoNewline
                            $psFitb1tClientID = Read-Host                            
                            if ($psFitb1tClientID) { New-ItemProperty HKCU:\Software\psFitb1t -name "psFitb1tClientID" -value "$psFitb1tClientID" | out-null }
                        }
                        else
                        {
                            New-ItemProperty HKCU:\Software\psFitb1t -name "psFitb1tClientID" -value "$psFitb1tClientID" | out-null
                        }
                        if (! $psFitb1tRedirectURL)
                        {
                            Write-Host "Enter Fitbit Redirect URL:" -ForegroundColor Yellow -NoNewline
                            $psFitb1tRedirectURL = Read-Host
                            if ($psFitb1tRedirectURL) { New-ItemProperty HKCU:\Software\psFitb1t -name "psFitb1tRedirectURL" -value "$psFitb1tRedirectURL" | out-null }
                        }
                        else
                        {
                            New-ItemProperty HKCU:\Software\psFitb1t -name "psFitb1tRedirectURL" -value "$psFitb1tRedirectURL" | out-null
                        }
                        
                    }
                }
            }
        }
        catch
        {
            Throw $("ERROR OCCURRED WRITING FITBIT API INFORMATION TO REGISTRY " + $_.Exception.Message)
        }
        finally
        {
            # Now that the reg values are present regardless of method, read back in the values and set our globals
            $Script:psFitb1tClientID = (Get-Item HKCU:\Software\psFitb1t).getvalue("psFitb1tClientID")
            $Script:psFitb1tRedirectURL = (Get-Item HKCU:\Software\psFitb1t).getvalue("psFitb1tRedirectURL")
            
        }
    }
    END
    {
        (Write-Status -Message "FINISH - Set-FitbitOAuthTokens function execution" -Status "INFO" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
    }
    
}



function Get-HRdata
{
    <#
    .SYNOPSIS
        Sends a Fitbit Tweet
     
    .DESCRIPTION
        Author: Collin Chaffin
        Description: Sends a request for 24hrs of HR data using OAuth and REST
     
    .PARAMETER QueryDate
        The single 24hr Date to retrieve HR data (*FITBIT LIMITATION*) per query
     
    .EXAMPLE
        Get-HRdata -QueryDate "2016-03-13"
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [ValidateLength(1, 140)]
        [String]$QueryDate = $(Get-Date ([System.DateTime]::Now).AddDays(-1) -Format "yyyy-MM-dd")
    )
    BEGIN
    {
        (Write-Status -Message "START - Get-HRdata function execution" -Status "INFO" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
    }
    PROCESS
    {
        try
        {
            if (!$(Test-Date -inputDate $QueryDate))
            {
                [String]$QueryDate = $(Get-Date ([System.DateTime]::Now).AddDays(-1) -Format "yyyy-MM-dd")
            }
            else
            {
                [String]$QueryDate = $(Get-Date $QueryDate -Format "yyyy-MM-dd")
            }
            
            $Script:psFitb1tHRQueryDate = $QueryDate
            
            # Call our main connect routine to setup the oAuth
            $psFitb1tAuthCode = Connect-OAuthFitbit
            
            (Write-Status -Message "START - Sending HTTP POST via REST to Fitbit" -Status "INFO" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
            
            $Script:psFitb1tGetHRurl = "https://api.fitbit.com/1/user/-/activities/heart/date/$($psFitb1tHRQueryDate)/1d/1sec/time/00:00/23:59.json"
            
            Write-Verbose "Sending to URL $($psFitb1tGetHRurl)"
            Write-Verbose "This request: Headers=$psFitb1tAuthCode"
            
            # Call the REST API to handle the final OAUTH POST
            $retData = Invoke-RestMethod -Method Get -Uri $psFitb1tGetHRurl -Headers @{ 'Authorization' = "Bearer " + $psFitb1tAuthCode } -ContentType "application/x-www-form-urlencoded"
            
            #Write the output
            $output = @()
            $output = New-Object -TypeName PSObject            
            
            #Assign the dataset to custom obj
            $output = $retData.'activities-heart-intraday'.dataset
            
            #Add the query date to output object
            $output | Add-Member -Name 'Date' -Value $($psFitb1tHRQueryDate) -MemberType NoteProperty -Force
            
            #Write to csv
            $output | Export-Csv -NoTypeInformation "$($Script:psFitb1tInvocationPath)FitbitHR_$($psFitb1tHRQueryDate).csv"
            
            #Write to xls
            Export-FitbitXLS -objInput $output "$($Script:psFitb1tInvocationPath)FitbitHR_$($psFitb1tHRQueryDate).xlsx" -appendSheet:$false -worksheetName "$psFitb1tHRQueryDate" -chartType "xlLine"
            
            
            (Write-Status -Message "FINISH - Sending HTTP POST via REST to Fitbit" -Status "INFO" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
        }
        catch
        {
            Throw $("ERROR OCCURRED RETRIEVING HR DATA " + $_.Exception.Message)
        }
    }
    END
    {
        #write last query date to registry
        New-ItemProperty HKCU:\Software\psFitb1t -name "psFitb1tHRQueryDate" -value "$Script:psFitb1tHRQueryDate" -Force | out-null        
        (Write-Status -Message "FINISH - Get-HRdata function execution" -Status "INFO" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
    }
}


function Get-HRmonth
{
<#
    .SYNOPSIS
        Get entire month of heartrate reports via psFitb1t's Get-HRdata
     
    .DESCRIPTION
        Determines how many possible days in the requested month and calls psFitb1t's Get-HRdata to retrieve the daily reports. It first verifies if
        you have already retrieved any reports for days in the queried month and if so, only processes missing days.
         
        Returns bool for overall success or failure.
     
    .PARAMETER QueryMonth
        A description of the QueryMonth parameter.
     
    .EXAMPLE
                PS C:\> Get-HRmonth -QueryMonth '2016-01'
                This queries all days for January, 2016 that do not already have reports on disk
     
    .NOTES
        Requires the primary Get-HRdata retrieval function.
#>
    
    [CmdletBinding()]
    [OutputType([bool])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $QueryMonth = "2016-01" #This will accept mult formats such as "01/2016","2016-01"
    )
    BEGIN
    {
        (Write-Status -Message "START - Get-HRmonth function execution" -Status "INFO" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
        
        Import-Module -Name psFitb1t -ea 'Stop' | Out-Null
        $nbrDaysInMonth = [DateTime]::DaysInMonth($([DateTime](Get-Date($QueryMonth))).Year, $([Datetime](Get-Date($QueryMonth))).Month)
        $daysToProcess = 0
        $cntProc = 0
    }
    PROCESS
    {
        try
        {
            (Write-Status -Message "START - Requesting monthly HR data from Fitbit" -Status "INFO" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
            
            #Get total real days to process = days in month minus any existing days already processed
            for ($i = 1; $i -le $nbrDaysInMonth; $i++)
            {
                $day = ("{0:D2}" -f $i)
                if (!(Test-Path -Path "$((Get-Module psFitb1t).ModuleBase)\FitbitHR_$(([Datetime](Get-Date($QueryMonth))).ToString('yyyy'))-$(([Datetime](Get-Date($QueryMonth))).ToString('MM'))-$($day).xlsx"))
                {
                    $daysToProcess++
                }
            }
            
            (Write-Status -Message "START - Requesting $($daysToProcess) days of HR data from Fitbit" -Status "INFO" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
            #Process the missing days and provide accurate progress counter based on number computed above
            for ($i = 1; $i -le $nbrDaysInMonth; $i++)
            {
                $day = ("{0:D2}" -f $i)
                if (!(Test-Path -Path "$((Get-Module psFitb1t).ModuleBase)\FitbitHR_$(([Datetime](Get-Date($QueryMonth))).ToString('yyyy'))-$(([Datetime](Get-Date($QueryMonth))).ToString('MM'))-$($day).xlsx"))
                {
                    $cntProc++
                    Write-Verbose "$((Get-Module psFitb1t).ModuleBase)\FitbitHR_$(([Datetime](Get-Date($QueryMonth))).ToString('yyyy'))-$(([Datetime](Get-Date($QueryMonth))).ToString('MM'))-$($day).xlsx missing!"
                    Write-Verbose "Running: Get-HRData -QueryDate ""$($([Datetime](Get-Date($QueryMonth))).ToString('yyyy'))-$($([Datetime](Get-Date($QueryMonth))).ToString('MM'))-$($day)"" `n"
                    Write-Progress -Activity "Retrieving heartrate data for $($([Datetime](Get-Date($QueryMonth))).Year)-$($([Datetime](Get-Date($QueryMonth))).Month)-$($day)" -PercentComplete (($cntProc / $daysToProcess) * 100)
                    (Write-Status -Message "START - Requesting HR data for $($([Datetime](Get-Date($QueryMonth))).Year)-$($([Datetime](Get-Date($QueryMonth))).Month)-$($day) from Fitbit" -Status "INFO" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
                    Get-HRData -QueryDate "$($([Datetime](Get-Date($QueryMonth))).Year)-$($([Datetime](Get-Date($QueryMonth))).Month)-$($day)"                    
                }
            }
            (Write-Status -Message "FINISH - Requesting monthly HR data from Fitbit" -Status "INFO" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
        }
        catch
        {
            Throw $("ERROR OCCURRED RETRIEVING MONTHLY HR DATA " + $_.Exception.Message)
        }
    }
    END
    {
        (Write-Status -Message "FINISH - Get-HRmonth function execution" -Status "INFO" -Debugging:$psFitb1tDebugging -Logging:$psFitb1tLogging -Logpath $psFitb1tLogPath)
    }
}


Function Export-FitbitXLS
{
  <#
.SYNOPSIS
  Saves data to Excel using com object
.DESCRIPTION
  The Export-FitbitXLS function allows you to save data to an Excel file
.PARAMETER objInput
  Specifies the input object
.PARAMETER outputPath
  Specifies the path to the output XLS file
.PARAMETER worksheetName
  The name for the worksheet
.PARAMETER sheetIndex
  Specify if END
.PARAMETER chartType
  Type of chart based on [microsoft.Office.Interop.Excel.XlChartType]
.PARAMETER appendSheet
  Append or overwrite
.EXAMPLE
  Export-FitbitXLS -objInput $obj "$($Script:psFitb1tInvocationPath)FitbitHR_$($psFitb1tHRQueryDate).xlsx" -appendSheet:$false -worksheetName "$psFitb1tHRQueryDate" -chartType "xlLine"
#>

    param (
        [parameter(ValueFromPipeline = $true, Position = 1)]
        [ValidateNotNullOrEmpty()]
        $objInput,
        [parameter(Position = 2)]
        [ValidateNotNullOrEmpty()]
        [string]$outputPath,
        [string]$worksheetName = ("Sheet $((Get-Date).Ticks)"),
        [switch]$newSheetLast = $true,
        [PSObject]$chartType,
        [switch]$appendSheet = $true
    )    
    BEGIN
    {
        #Nested internal helper clipboard function specific to this XLS function
        #Adds txt strings to txtbox then to clipboard
        function Add-ClipBoardTxt
        {
            param (
                [String]$txtInput
            )
            process
            {
                try
                {                    
                    Add-Type -AssemblyName System.Windows.Forms | Out-Null
                    $tmpTextbox = New-Object System.Windows.Forms.TextBox
                    $tmpTextbox.Multiline = $true
                    $tmpTextbox.Text = $txtInput
                    $tmpTextbox.SelectAll()
                    $tmpTextbox.Copy()
                }
                catch
                {
                    Throw $("ERROR OCCURRED COPYING EXCEL DATA TO CLIPBOARD " + $_.Exception.Message)
                }
            }
        }
        #Nested internal helper clipboard function specific to this XLS function
        #Builds internal array with header row and sends all to clipboard at once
        #To send each of thousands cells one at time to Excel takes far too long this is much faster
        function Send-ToClipboard
        {
            param (
                [PSObject[]]$objConvert,
                [Switch]$headerRow
            )
            process
            {
                try
                {
                    $tmpArray = @()
                    if ($headerRow)
                    {
                        $line = ""
                        $objConvert | Get-Member -MemberType Property, NoteProperty, CodeProperty | Select -Property Name | %{ $line += ($_.Name.tostring() + "`t") }
                        $tmpArray += ($line.TrimEnd("`t") + "`r")
                    }
                    else
                    {
                        foreach ($obj in $objConvert)
                        {
                            $line = ""
                            $obj | Get-Member -MemberType Property, NoteProperty | %{
                                $Name = $_.Name
                                if (!$obj.$Name) { $obj.$Name = "" }
                                $line += ([string]$obj.$Name + "`t")
                            }
                            $tmpArray += ($line.TrimEnd("`t") + "`r")
                        }
                    }
                    Add-ClipBoardTxt $tmpArray
                }
                catch
                {
                    Throw $("ERROR OCCURRED ADDING EXCEL DATA TO CLIPBOARD " + $_.Exception.Message)
                }
            }
        }        
        try
        {            
            [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Interop.Excel") | Out-Null
            if ($chartType)
            {
                [Microsoft.Office.Interop.Excel.XlChartType]$chartType = $chartType
            }
            $excelObj = New-Object -ComObject "Excel.Application"
            $originalAlerts = $excelObj.DisplayAlerts
            $excelObj.DisplayAlerts = $false
            if (Test-Path -Path $outputPath -PathType "Leaf")
            {
                $excelWorkbook = $excelObj.Workbooks.Open($outputPath)
            }
            else
            {
                $excelWorkbook = $excelObj.Workbooks.Add()
            }
            $excelSheet = $excelObj.Worksheets.Add($excelWorkbook.Worksheets.Item(1))
            if (!$appendSheet)
            {
                $excelWorkbook.Sheets | where { $_ -ne $excelSheet } | %{ $_.Delete() }
            }
            $excelSheet.Name = $worksheetName
            if ($newSheetLast -eq $true -and $excelWorkbook.Sheets.Count -ge 2)
            {
                $sheetCount = $excelWorkbook.Sheets.Count
                2..($sheetCount) | %{
                    $excelWorkbook.Sheets.Item($_).Move($excelWorkbook.Sheets.Item($_ - 1))
                }
            }
            $excelSheet.Activate()
            $tmpArray = @()            
        }
        catch
        {
            Throw $("ERROR OCCURRED RELEASING COM OBJECTS " + $_.Exception.Message)
        }        
    }    
    PROCESS
    {
        try
        {
            $tmpArray += $objInput
        }
        catch
        {
            Throw $("ERROR OCCURRED CREATING EXCEL ARRAY " + $_.Exception.Message)
        }
    }    
    END
    {
        try
        {
            Send-ToClipboard $tmpArray -headerRow:$True
            $selection = $excelSheet.Range("A1")
            $selection.Select() | Out-Null
            $excelSheet.Paste()
            $excelSheet.UsedRange.HorizontalAlignment = [microsoft.Office.Interop.Excel.XlHAlign]::xlHAlignCenter
            
            Send-ToClipboard $tmpArray
            $selection = $excelSheet.Range("A2")
            $selection.Select() | Out-Null
            $excelSheet.Paste() | Out-Null
            $selection = $excelSheet.Range("A1")
            $selection.Select() | Out-Null
            
            $excelSheet.UsedRange.EntireColumn.AutoFit() | Out-Null
            $excelWorkbook.Sheets.Item(1).Select()
            if ($chartType)
            {
                $chart = $excelSheet.Shapes.AddChart().Chart
                $chart.ChartType = $chartType
                $chart.ChartTitle.Text = "Fitbit Heartrates for: $($Script:psFitb1tHRQueryDate)"
                $excelSheet.Shapes.Item("Chart 1").top = 120
                $excelSheet.Shapes.Item("Chart 1").width = 1200
                $excelSheet.Shapes.Item("Chart 1").Height = 400
                $excelSheet.Shapes.Item("Chart 1").Left = 180
            }
            $range = $excelSheet.Range("o2", "s3")
            $range.Merge() | Out-Null
            $range.VerticalAlignment = -4160
            $range.Style = 'Title'
            
            $selection = $excelSheet.Range("P5", "R5")
            $selection.Select() | Out-Null
            $excelSheet.UsedRange.HorizontalAlignment = [microsoft.Office.Interop.Excel.XlHAlign]::xlHAlignCenter
            $excelSheet.UsedRange.EntireColumn.AutoFit() | Out-Null
            
            $excelSheet.columns.item('p').NumberFormat = "[Blue]#0"
            $excelSheet.columns.item('q').NumberFormat = "[Blue]#0"
            $excelSheet.columns.item('r').NumberFormat = "[Blue]#0"
            
            $excelObj.Cells.Item(2, 15).Value() = "Fitbit Heartrates for: $($Script:psFitb1tHRQueryDate)"
            
            $excelObj.Cells.Item(1, 2).Value() = "Time"
            $excelObj.Cells.Item(1, 3).Value() = "HeartRate"
            
            $excelObj.Cells.Item(5, 16).Value() = "Minimum"
            $excelObj.Cells.Item(5, 17).Value() = "Maximum"
            $excelObj.Cells.Item(5, 18).Value() = "Average"
            $strFormula1 = "=MIN(C2:C99999)"
            $strFormula2 = "=MAX(C2:C99999)"
            $strFormula3 = "=AVERAGE(C2:C99999)"
            
            $excelObj.Cells.Item(6, 16).Formula = $strFormula1
            $excelObj.Cells.Item(6, 17).Formula = $strFormula2
            $excelObj.Cells.Item(6, 18).Formula = $strFormula3
            $excelObj.Cells.Item(5, 16).Font.Bold = $True
            $excelObj.Cells.Item(5, 17).Font.Bold = $True
            $excelObj.Cells.Item(5, 18).Font.Bold = $True
            
            #Auto fit everything so it looks better
            $usedRange = $excelSheet.UsedRange
            $usedRange.EntireColumn.AutoFit() | Out-Null
            $excelWorkbook.Sheets.Item(1).Select()
            
            $excelWorkbook.SaveAs($outputPath)
            $excelWorkbook.Saved = $True
            $excelWorkbook.Close()
            $excelObj.DisplayAlerts = $originalAlerts
            $excelObj.Quit()
            
            #Cleanup all this com object crap what a PITA Excel com objects are!!
            Release-Ref $chart
            Release-Ref $selection
            Release-Ref $range
            Release-Ref $usedRange
            Release-Ref $excelSheet
            Release-Ref $excelWorkbook
            Release-Ref $excelObj            
            Remove-Variable chart | Out-Null
            Remove-Variable selection | Out-Null
            Remove-Variable range | Out-Null
            Remove-Variable usedRange | Out-Null
            Remove-Variable excelSheet | Out-Null
            Remove-Variable excelWorkbook | Out-Null
            Remove-Variable excelObj | Out-Null
            Start-Sleep 5
            [void][System.GC]::Collect()
            [void][System.GC]::WaitForPendingFinalizers()
        }
        catch
        {
            Throw $("ERROR OCCURRED CREATING EXCEL CHART AND SAVING FILE " + $_.Exception.Message)
        }
    }
}

function Release-Ref
{
<#
.SYNOPSIS
  Kills in use com objects (Excel is a PITA)
.DESCRIPTION
  Kills off com objects properly
.PARAMETER inputObj
  Specifies the objects to be released
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.__ComObject]$inputObj
    )
    BEGIN
    {
    }
    PROCESS
    {
        try
        {
            [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject([System.__ComObject]$inputObj)
        }
        catch
        {
            Throw $("ERROR OCCURRED RELEASING COM OBJECTS " + $_.Exception.Message)
        }
    }
    END
    {
    }
}


#########################################################################

#endregion

#region Call-psFitb1t-API_psf

function Call-psFitb1t-API_psf
{
    #----------------------------------------------
    #region Import the Assemblies
    #----------------------------------------------
    [void][reflection.assembly]::Load('mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')
    [void][reflection.assembly]::Load('System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')
    [void][reflection.assembly]::Load('System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')
    [void][reflection.assembly]::Load('System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')
    [void][reflection.assembly]::Load('System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a')
    [void][reflection.assembly]::Load('System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')
    [void][reflection.assembly]::Load('System.DirectoryServices, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a')
    [void][reflection.assembly]::Load('System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')
    [void][reflection.assembly]::Load('System.ServiceProcess, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a')
    #endregion Import Assemblies
    
    #----------------------------------------------
    #region Form Objects
    #----------------------------------------------
    [System.Windows.Forms.Application]::EnableVisualStyles()
    $frmFitbitAPIInformation = New-Object 'System.Windows.Forms.Form'
    $buttonCancel = New-Object 'System.Windows.Forms.Button'
    $buttonBack = New-Object 'System.Windows.Forms.Button'
    $buttonFinish = New-Object 'System.Windows.Forms.Button'
    $tabcontrolWizard = New-Object 'System.Windows.Forms.TabControl'
    $tabpageStep1 = New-Object 'System.Windows.Forms.TabPage'
    $txtpsFitb1tClientID = New-Object 'System.Windows.Forms.TextBox'
    $labelpsFitb1tClientID = New-Object 'System.Windows.Forms.Label'
    $tabpageStep2 = New-Object 'System.Windows.Forms.TabPage'
    $txtpsFitb1tRedirectURL = New-Object 'System.Windows.Forms.TextBox'
    $labelpsFitb1tRedirectURL = New-Object 'System.Windows.Forms.Label'
    $tabpageStep3 = New-Object 'System.Windows.Forms.TabPage'
    $txtpsFitb1tHRQueryDate = New-Object 'System.Windows.Forms.TextBox'
    $labelpsFitb1tHRQueryDate = New-Object 'System.Windows.Forms.Label'
    $tabpageStep4 = New-Object 'System.Windows.Forms.TabPage'
    $txtpsFitb1tTokenAge = New-Object 'System.Windows.Forms.TextBox'
    $labelpsFitb1tTokenAge = New-Object 'System.Windows.Forms.Label'
    $buttonNext = New-Object 'System.Windows.Forms.Button'
    $InitialFormWindowState = New-Object 'System.Windows.Forms.FormWindowState'
    #endregion Form Objects
    
    
    function Validate-WizardPage
    {
        [OutputType([boolean])]
        param ([System.Windows.Forms.TabPage]$tabPage)
        
        if ($tabPage -eq $tabpageStep1)
        {
            if (-not $txtpsFitb1tClientID.Text)
            {
                return $false
            }
            
            return $true
        }
        elseif ($tabPage -eq $tabpageStep2)
        {
            if (-not $txtpsFitb1tRedirectURL.Text)
            {
                return $false
            }
            
            return $true
        }
        elseif ($tabPage -eq $tabpageStep3)
        {
            if (-not $txtpsFitb1tHRQueryDate.Text)
            {
                return $false
            }
            
            return $true
        }
        elseif ($tabPage -eq $tabpageStep4)
        {
            if (-not $txtpsFitb1tTokenAge.Text)
            {
                return $false
            }
            
            return $true
        }
        return $false
    }
    
    $buttonFinish_Click = {
        if ($txtpsFitb1tClientID.Text) { New-ItemProperty HKCU:\Software\psFitb1t -name "psFitb1tClientID" -value "$($txtpsFitb1tClientID.Text)" | out-null }
        if ($txtpsFitb1tRedirectURL.Text) { New-ItemProperty HKCU:\Software\psFitb1t -name "psFitb1tRedirectURL" -value "$($txtpsFitb1tRedirectURL.Text)" | out-null }
        if ($txtpsFitb1tHRQueryDate.Text) { New-ItemProperty HKCU:\Software\psFitb1t -name "psFitb1tHRQueryDate" -value "$($txtpsFitb1tHRQueryDate.Text)" | out-null }
        if ($txtpsFitb1tTokenAge.Text) { New-ItemProperty HKCU:\Software\psFitb1t -name "psFitb1tTokenAge" -value "$($txtpsFitb1tTokenAge.Text)" | out-null }
    }
    
    #region Events and Functions
    $frmFitbitAPIInformation_Load = {
        Update-NavButtons
        
        # Reg key is there, but we must have a missing value(s)
        $psFitb1tClientID = (Get-Item HKCU:\Software\psFitb1t).getvalue("psFitb1tClientID")
        $psFitb1tRedirectURL = (Get-Item HKCU:\Software\psFitb1t).getvalue("psFitb1tRedirectURL")
        $psFitb1tHRQueryDate = (Get-Item HKCU:\Software\psFitb1t).getvalue("psFitb1tHRQueryDate")
        $psFitb1tTokenAge = (Get-Item HKCU:\Software\psFitb1t).getvalue("psFitb1tTokenAge")
        
        # Check for any single missing values and prompt for those that are missing
        if ($psFitb1tClientID)
        {
            $tabcontrolWizard.TabPages.Remove($tabpageStep1)
        }
        if ($psFitb1tRedirectURL)
        {
            $tabcontrolWizard.TabPages.Remove($tabpageStep2)
        }
        #if ($psFitb1tHRQueryDate)
        #{
            $tabcontrolWizard.TabPages.Remove($tabpageStep3)
        #}
        #if ($psFitb1tTokenAge)
        #{
            $tabcontrolWizard.TabPages.Remove($tabpageStep4)
        #}
    }
    
    function Update-NavButtons
    {
        <#
            .DESCRIPTION
            Validates the current tab and Updates the Next, Prev and Finish buttons.
        #>

        $enabled = Validate-WizardPage $tabcontrolWizard.SelectedTab
        $buttonNext.Enabled = $enabled -and ($tabcontrolWizard.SelectedIndex -lt $tabcontrolWizard.TabCount - 1)
        $buttonBack.Enabled = $tabcontrolWizard.SelectedIndex -gt 0
        $buttonFinish.Enabled = $enabled -and ($tabcontrolWizard.SelectedIndex -eq $tabcontrolWizard.TabCount - 1)
        #Uncomment to Hide Buttons
        #$buttonNext.Visible = ($tabcontrolWizard.SelectedIndex -lt $tabcontrolWizard.TabCount - 1)
        #$buttonFinish.Visible = ($tabcontrolWizard.SelectedIndex -eq $tabcontrolWizard.TabCount - 1)
    }
    
    $script:DeselectedIndex = -1
    $tabcontrolWizard_Deselecting = [System.Windows.Forms.TabControlCancelEventHandler]{
        #Event Argument: $_ = [System.Windows.Forms.TabControlCancelEventArgs]
        # Store the previous tab index
        $script:DeselectedIndex = $_.TabPageIndex
    }
    
    $tabcontrolWizard_Selecting = [System.Windows.Forms.TabControlCancelEventHandler]{
        #Event Argument: $_ = [System.Windows.Forms.TabControlCancelEventArgs]
        # We only validate if we are moving to the Next TabPage.
        # Users can move back without validating
        if ($script:DeselectedIndex -ne -1 -and $script:DeselectedIndex -lt $_.TabPageIndex)
        {
            #Validate each page until we reach the one we want
            for ($index = $script:DeselectedIndex; $index -lt $_.TabPageIndex; $index++)
            {
                $_.Cancel = -not (Validate-WizardPage $tabcontrolWizard.TabPages[$index])
                
                if ($_.Cancel)
                {
                    # Cancel and Return if validation failed.
                    return;
                }
            }
        }
        Update-NavButtons
    }
    
    $buttonBack_Click = {
        #Go to the previous tab page
        if ($tabcontrolWizard.SelectedIndex -gt 0)
        {
            $tabcontrolWizard.SelectedIndex--
        }
    }
    
    $buttonNext_Click = {
        #Go to the next tab page
        if ($tabcontrolWizard.SelectedIndex -lt $tabcontrolWizard.TabCount - 1)
        {
            $tabcontrolWizard.SelectedIndex++
        }
    }
    
    #endregion
    
    #------------------------------------------------------
    # Events: Call Update-NavButtons to trigger validation
    #------------------------------------------------------
    
    $txtpsFitb1tClientID_TextChanged = {
        Update-NavButtons
    }
    
    $txtpsFitb1tRedirectURL_TextChanged = {
        Update-NavButtons
    }
    
    $txtpsFitb1tHRQueryDate_TextChanged = {
        Update-NavButtons
    }
    
    $txtpsFitb1tTokenAge_TextChanged = {
        Update-NavButtons
    }
    
    
    $tabcontrolWizard_SelectedIndexChanged = {
        Update-NavButtons
    }
    
    $buttonCancel_Click = {
        $frmFitbitAPIInformation.Close()
    }
    
    #----------------------------------------------
    #region cleanup Events
    #----------------------------------------------
    
    $Form_StateCorrection_Load =
    {
        #Correct the initial state of the form to prevent the .Net maximized form issue
        $frmFitbitAPIInformation.WindowState = $InitialFormWindowState
    }
    
    $Form_StoreValues_Closing =
    {
        #Store the control values
        $script:psFitb1t_API_txtpsFitb1tClientID = $txtpsFitb1tClientID.Text
        $script:psFitb1t_API_txtpsFitb1tRedirectURL = $txtpsFitb1tRedirectURL.Text
        $script:psFitb1t_API_txtpsFitb1tHRQueryDate = $txtpsFitb1tHRQueryDate.Text
        $script:psFitb1t_API_txtpsFitb1tTokenAge = $txtpsFitb1tTokenAge.Text
    }
    
    
    $Form_Cleanup_FormClosed =
    {
        #Remove all event handlers from the controls
        try
        {
            $buttonCancel.remove_Click($buttonCancel_Click)
            $buttonBack.remove_Click($buttonBack_Click)
            $buttonFinish.remove_Click($buttonFinish_Click)
            $txtpsFitb1tClientID.remove_TextChanged($txtpsFitb1tClientID_TextChanged)
            $txtpsFitb1tRedirectURL.remove_TextChanged($txtpsFitb1tRedirectURL_TextChanged)
            $txtpsFitb1tHRQueryDate.remove_TextChanged($txtpsFitb1tHRQueryDate_TextChanged)
            $txtpsFitb1tTokenAge.remove_TextChanged($txtpsFitb1tTokenAge_TextChanged)
            $tabcontrolWizard.remove_SelectedIndexChanged($tabcontrolWizard_SelectedIndexChanged)
            $tabcontrolWizard.remove_Selecting($tabcontrolWizard_Selecting)
            $tabcontrolWizard.remove_Deselecting($tabcontrolWizard_Deselecting)
            $buttonNext.remove_Click($buttonNext_Click)
            $frmFitbitAPIInformation.remove_Load($frmFitbitAPIInformation_Load)
            $frmFitbitAPIInformation.remove_Load($Form_StateCorrection_Load)
            $frmFitbitAPIInformation.remove_Closing($Form_StoreValues_Closing)
            $frmFitbitAPIInformation.remove_FormClosed($Form_Cleanup_FormClosed)
        }
        catch [Exception]
        { }
    }
    #endregion cleanup Events
    
    #----------------------------------------------
    #region Generated Form Code
    #----------------------------------------------
    $frmFitbitAPIInformation.SuspendLayout()
    $tabcontrolWizard.SuspendLayout()
    $tabpageStep1.SuspendLayout()
    $tabpageStep2.SuspendLayout()
    $tabpageStep3.SuspendLayout()
    $tabpageStep4.SuspendLayout()
    #
    # frmFitbitAPIInformation
    #
    $frmFitbitAPIInformation.Controls.Add($buttonCancel)
    $frmFitbitAPIInformation.Controls.Add($buttonBack)
    $frmFitbitAPIInformation.Controls.Add($buttonFinish)
    $frmFitbitAPIInformation.Controls.Add($tabcontrolWizard)
    $frmFitbitAPIInformation.Controls.Add($buttonNext)
    $frmFitbitAPIInformation.AcceptButton = $buttonFinish
    $frmFitbitAPIInformation.CancelButton = $buttonCancel
    $frmFitbitAPIInformation.ClientSize = '537, 180'
    $frmFitbitAPIInformation.FormBorderStyle = 'FixedDialog'
    $frmFitbitAPIInformation.MaximizeBox = $False
    $frmFitbitAPIInformation.Name = "frmFitbitAPIInformation"
    $frmFitbitAPIInformation.StartPosition = 'CenterScreen'
    $frmFitbitAPIInformation.Text = "Fitbit API Information"
    $frmFitbitAPIInformation.add_Load($frmFitbitAPIInformation_Load)
    #
    # buttonCancel
    #
    $buttonCancel.Anchor = 'Bottom, Right'
    $buttonCancel.DialogResult = 'Cancel'
    $buttonCancel.Location = '369, 145'
    $buttonCancel.Name = "buttonCancel"
    $buttonCancel.Size = '75, 23'
    $buttonCancel.TabIndex = 4
    $buttonCancel.Text = "&Cancel"
    $buttonCancel.UseVisualStyleBackColor = $True
    $buttonCancel.add_Click($buttonCancel_Click)
    #
    # buttonBack
    #
    $buttonBack.Anchor = 'Bottom, Left'
    $buttonBack.Location = '13, 145'
    $buttonBack.Name = "buttonBack"
    $buttonBack.Size = '75, 23'
    $buttonBack.TabIndex = 1
    $buttonBack.Text = "< &Back"
    $buttonBack.UseVisualStyleBackColor = $True
    $buttonBack.add_Click($buttonBack_Click)
    #
    # buttonFinish
    #
    $buttonFinish.Anchor = 'Bottom, Right'
    $buttonFinish.DialogResult = 'OK'
    $buttonFinish.Location = '450, 145'
    $buttonFinish.Name = "buttonFinish"
    $buttonFinish.Size = '75, 23'
    $buttonFinish.TabIndex = 3
    $buttonFinish.Text = "&Finish"
    $buttonFinish.UseVisualStyleBackColor = $True
    $buttonFinish.add_Click($buttonFinish_Click)
    #
    # tabcontrolWizard
    #
    $tabcontrolWizard.Controls.Add($tabpageStep1)
    $tabcontrolWizard.Controls.Add($tabpageStep2)
    $tabcontrolWizard.Controls.Add($tabpageStep3)
    $tabcontrolWizard.Controls.Add($tabpageStep4)
    $tabcontrolWizard.Anchor = 'Top, Bottom, Left, Right'
    $tabcontrolWizard.Location = '13, 12'
    $tabcontrolWizard.Name = "tabcontrolWizard"
    $tabcontrolWizard.SelectedIndex = 0
    $tabcontrolWizard.Size = '512, 127'
    $tabcontrolWizard.TabIndex = 0
    $tabcontrolWizard.add_SelectedIndexChanged($tabcontrolWizard_SelectedIndexChanged)
    $tabcontrolWizard.add_Selecting($tabcontrolWizard_Selecting)
    $tabcontrolWizard.add_Deselecting($tabcontrolWizard_Deselecting)
    #
    # tabpageStep1
    #
    $tabpageStep1.Controls.Add($txtpsFitb1tClientID)
    $tabpageStep1.Controls.Add($labelpsFitb1tClientID)
    $tabpageStep1.Location = '4, 22'
    $tabpageStep1.Name = "tabpageStep1"
    $tabpageStep1.Padding = '3, 3, 3, 3'
    $tabpageStep1.Size = '504, 101'
    $tabpageStep1.TabIndex = 0
    $tabpageStep1.Text = "Client ID"
    $tabpageStep1.UseVisualStyleBackColor = $True
    #
    # txtpsFitb1tClientID
    #
    $txtpsFitb1tClientID.Location = '168, 43'
    $txtpsFitb1tClientID.Name = "txtpsFitb1tClientID"
    $txtpsFitb1tClientID.Size = '259, 20'
    $txtpsFitb1tClientID.TabIndex = 1
    $txtpsFitb1tClientID.add_TextChanged($txtpsFitb1tClientID_TextChanged)
    #
    # labelpsFitb1tClientID
    #
    $labelpsFitb1tClientID.AutoSize = $True
    $labelpsFitb1tClientID.Location = '115, 46'
    $labelpsFitb1tClientID.Name = "labelpsFitb1tClientID"
    $labelpsFitb1tClientID.Size = '47, 13'
    $labelpsFitb1tClientID.TabIndex = 0
    $labelpsFitb1tClientID.Text = "Client ID"
    #
    # tabpageStep2
    #
    $tabpageStep2.Controls.Add($txtpsFitb1tRedirectURL)
    $tabpageStep2.Controls.Add($labelpsFitb1tRedirectURL)
    $tabpageStep2.Location = '4, 22'
    $tabpageStep2.Name = "tabpageStep2"
    $tabpageStep2.Padding = '3, 3, 3, 3'
    $tabpageStep2.Size = '504, 101'
    $tabpageStep2.TabIndex = 1
    $tabpageStep2.Text = "Redirect URL"
    $tabpageStep2.UseVisualStyleBackColor = $True
    #
    # txtpsFitb1tRedirectURL
    #
    $txtpsFitb1tRedirectURL.Location = '168, 42'
    $txtpsFitb1tRedirectURL.Name = "txtpsFitb1tRedirectURL"
    $txtpsFitb1tRedirectURL.Size = '259, 20'
    $txtpsFitb1tRedirectURL.TabIndex = 3
    $txtpsFitb1tRedirectURL.add_TextChanged($txtpsFitb1tRedirectURL_TextChanged)
    #
    # labelpsFitb1tRedirectURL
    #
    $labelpsFitb1tRedirectURL.AutoSize = $True
    $labelpsFitb1tRedirectURL.Location = '87, 45'
    $labelpsFitb1tRedirectURL.Name = "labelpsFitb1tRedirectURL"
    $labelpsFitb1tRedirectURL.Size = '72, 13'
    $labelpsFitb1tRedirectURL.TabIndex = 2
    $labelpsFitb1tRedirectURL.Text = "Redirect URL"
    #
    # tabpageStep3
    #
    $tabpageStep3.Controls.Add($txtpsFitb1tHRQueryDate)
    $tabpageStep3.Controls.Add($labelpsFitb1tHRQueryDate)
    $tabpageStep3.Location = '4, 22'
    $tabpageStep3.Name = "tabpageStep3"
    $tabpageStep3.Size = '504, 101'
    $tabpageStep3.TabIndex = 2
    $tabpageStep3.Text = "Last Query Date"
    $tabpageStep3.UseVisualStyleBackColor = $True
    #
    # txtpsFitb1tHRQueryDate
    #
    $txtpsFitb1tHRQueryDate.Location = '168, 43'
    $txtpsFitb1tHRQueryDate.Name = "txtpsFitb1tHRQueryDate"
    $txtpsFitb1tHRQueryDate.Size = '259, 20'
    $txtpsFitb1tHRQueryDate.TabIndex = 5
    $txtpsFitb1tHRQueryDate.add_TextChanged($txtpsFitb1tHRQueryDate_TextChanged)
    #
    # labelpsFitb1tHRQueryDate
    #
    $labelpsFitb1tHRQueryDate.AutoSize = $True
    $labelpsFitb1tHRQueryDate.Location = '78, 46'
    $labelpsFitb1tHRQueryDate.Name = "labelpsFitb1tHRQueryDate"
    $labelpsFitb1tHRQueryDate.Size = '84, 13'
    $labelpsFitb1tHRQueryDate.TabIndex = 4
    $labelpsFitb1tHRQueryDate.Text = "Last Query Date"
    #
    # tabpageStep4
    #
    $tabpageStep4.Controls.Add($txtpsFitb1tTokenAge)
    $tabpageStep4.Controls.Add($labelpsFitb1tTokenAge)
    $tabpageStep4.Location = '4, 22'
    $tabpageStep4.Name = "tabpageStep4"
    $tabpageStep4.Padding = '3, 3, 3, 3'
    $tabpageStep4.Size = '504, 101'
    $tabpageStep4.TabIndex = 3
    $tabpageStep4.Text = "Token Expiration"
    $tabpageStep4.UseVisualStyleBackColor = $True
    #
    # txtpsFitb1tTokenAge
    #
    $txtpsFitb1tTokenAge.Location = '168, 44'
    $txtpsFitb1tTokenAge.Name = "txtpsFitb1tTokenAge"
    $txtpsFitb1tTokenAge.Size = '259, 20'
    $txtpsFitb1tTokenAge.TabIndex = 7
    $txtpsFitb1tTokenAge.add_TextChanged($txtpsFitb1tTokenAge_TextChanged)
    #
    # labelpsFitb1tTokenAge
    #
    $labelpsFitb1tTokenAge.AutoSize = $True
    $labelpsFitb1tTokenAge.Location = '26, 47'
    $labelpsFitb1tTokenAge.Name = "labelpsFitb1tTokenAge"
    $labelpsFitb1tTokenAge.Size = '136, 13'
    $labelpsFitb1tTokenAge.TabIndex = 6
    $labelpsFitb1tTokenAge.Text = "Token Expiration DateTime"
    #
    # buttonNext
    #
    $buttonNext.Anchor = 'Bottom, Right'
    $buttonNext.Location = '288, 145'
    $buttonNext.Name = "buttonNext"
    $buttonNext.Size = '75, 23'
    $buttonNext.TabIndex = 2
    $buttonNext.Text = "&Next >"
    $buttonNext.UseVisualStyleBackColor = $True
    $buttonNext.add_Click($buttonNext_Click)
    $tabpageStep4.ResumeLayout()
    $tabpageStep3.ResumeLayout()
    $tabpageStep2.ResumeLayout()
    $tabpageStep1.ResumeLayout()
    $tabcontrolWizard.ResumeLayout()
    $frmFitbitAPIInformation.ResumeLayout()
    #endregion Generated Form Code
    
    #----------------------------------------------
    
    #Save the initial state of the form
    $InitialFormWindowState = $frmFitbitAPIInformation.WindowState
    #Init the OnLoad event to correct the initial state of the form
    $frmFitbitAPIInformation.add_Load($Form_StateCorrection_Load)
    #Clean up the control events
    $frmFitbitAPIInformation.add_FormClosed($Form_Cleanup_FormClosed)
    #Store the control values when form is closing
    $frmFitbitAPIInformation.add_Closing($Form_StoreValues_Closing)
    #Show the Form
    return $frmFitbitAPIInformation.ShowDialog()
    
}
#endregion

Export-ModuleMember Get-HRdata
Export-ModuleMember Get-HRmonth
Export-ModuleMember Set-FitbitOAuthTokens