PSDaikin.psm1

#Region '.\prefix.ps1' 0
# The content of this file will be prepended to the top of the psm1 module file. This is useful for custom module setup is needed on import.
#EndRegion '.\prefix.ps1' 2
#Region '.\Private\Assert-FolderExist.ps1' 0
function Assert-FolderExist
{
    <#
    .SYNOPSIS
        Verify and create folder
    .DESCRIPTION
        Verifies that a folder path exists, if not it will create it
    .PARAMETER Path
        Defines the path to be validated
    .EXAMPLE
        'C:\Temp' | Assert-FolderExist
 
        This will verify that the path exists and if it does not the folder will be created
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [string]
        $Path
    )

    process
    {
        $exists = Test-Path -Path $Path -PathType Container
        if (!$exists)
        {
            $null = New-Item -Path $Path -ItemType Directory
        }
    }
}
#EndRegion '.\Private\Assert-FolderExist.ps1' 31
#Region '.\Private\Convert-DaikinResponse.ps1' 0
function Convert-DaikinResponse
{
    <#
        .DESCRIPTION
            Converts string response to a ordered dictionary. By default property names are translated to a more readable alternative.
        .PARAMETER String
            Plain string response from the daikin device
        .PARAMETER Raw
            Defines that no attribute name mapping should be done
        .EXAMPLE
            Convert-DaikinResponse -String 'ret=OK,pow=1,mode=7'
 
            Name Value
            ---- -----
            ret OK
            PowerOn True
            Mode AUTO
        .EXAMPLE
            Convert-DaikinResponse -String 'ret=OK,pow=1,mode=7'
 
            Name Value
            ---- -----
            ret OK
            pow 1
            mode 7
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'False positive, $raw not used but it is...')]
    [CmdletBinding()] # Enabled advanced function support
    param(
        [Parameter(Mandatory)]$String,
        [switch]$Raw
    )

    BEGIN
    {
        $Translation = @{
            'id'       = 'Identity'
            'pow'      = 'PowerOn'
            'type'     = 'DeviceType'
            'reg'      = 'Region'
            'ver'      = 'Version'
            'rev'      = 'Revision'
            'err'      = 'Error'
            'pw'       = 'Password'
            'led'      = 'LED_Enabled'
            'grp_name' = 'GroupName'
            'location' = 'Location'
            'stemp'    = 'TargetTemp'
            'htemp'    = 'InsideTemp'
            'otemp'    = 'OutsideTemp'
            'hhum'     = 'InsideHumidity'
            'mode'     = 'Mode'
            'f_rate'   = 'FanSpeed'
            'f_dir'    = 'FanDirection'
            'shum'     = 'TargetHumidity'
            'dh1'      = 'Mem_AUTO_TargetHumidity'
            'dh2'      = 'Mem_DEHUMDIFICATOR_TargetHumidity'
            'dh3'      = 'Mem_COLD_TargetHumidity'
            'dh4'      = 'Mem_HEAT_TargetHumidity'
            'dh5'      = 'Mem_FAN_TargetHumidity'
            'dh7'      = 'Mem_AUTO_TargetHumidity2'
            'dt1'      = 'Mem_AUTO_TargetTemp'
            'dt2'      = 'Mem_DEHUMDIFICATOR_TargetTemp'
            'dt3'      = 'Mem_COLD_TargetTemp'
            'dt4'      = 'Mem_HEAT_TargetTemp'
            'dt5'      = 'Mem_FAN_TargetTemp'
            'dt7'      = 'Mem_AUTO_TargetTemp2'
        }
    }

    PROCESS
    {
        $Hash = [ordered]@{}
        $String.Split(',') | ForEach-Object {
            $Property = $PSItem.Split('=')[0]
            $Value = $PSItem.Split('=')[1]

            if (-not $Raw)
            {
                # Translate keys
                if ($Translation.ContainsKey($Property))
                {
                    $Property = $Translation[$Property]
                }

                switch ($Property)
                {
                    { @('PowerOn') -contains $PSItem }
                    {
                        $Value = [bool][int]$Value
                    }
                    'Mode'
                    {
                        switch ($Value)
                        {
                            { 0, 1, 7 -contains $PSItem }
                            {
                                $Value = 'AUTO'
                            }
                            2
                            {
                                $Value = 'DRY'
                            }
                            3
                            {
                                $Value = 'COLD'
                            }
                            4
                            {
                                $Value = 'HEAT'
                            }
                            6
                            {
                                $Value = 'FAN'
                            }
                        }
                    }
                    'FanSpeed'
                    {
                        switch ($Value)
                        {
                            'A'
                            {
                                $Value = 'AUTO'
                            }
                            'B'
                            {
                                $Value = 'SILENT'
                            }
                            '3'
                            {
                                $Value = 'Level_1'
                            }
                            '4'
                            {
                                $Value = 'Level_2'
                            }
                            '5'
                            {
                                $Value = 'Level_3'
                            }
                            '6'
                            {
                                $Value = 'Level_4'
                            }
                            '7'
                            {
                                $Value = 'Level_5'
                            }
                        }
                    }
                    'FanDirection'
                    {
                        switch ($Value)
                        {
                            '0'
                            {
                                $Value = 'Stopped'
                            }
                            '1'
                            {
                                $Value = 'VerticalSwing'
                            }
                            '2'
                            {
                                $Value = 'HorizontalSwing'
                            }
                            '3'
                            {
                                $Value = 'BothSwing'
                            }
                        }
                    }
                }
            }
            $Hash.$Property = $Value
        }
        $Hash
    }
}
#endregion
#EndRegion '.\Private\Convert-DaikinResponse.ps1' 183
#Region '.\Private\Get-DaikinBasicInfo.ps1' 0
function Get-DaikinBasicInfo
{
    <#
        .DESCRIPTION
            Retreives daikin basic info object
        .PARAMETER Hostname
            Defines the IP or hostname of the Daikin unit
        .EXAMPLE
            Get-DaikinBasicInfo -Hostname 192.168.1.1
 
            Returns the basic info response from the device
    #>


    [CmdletBinding()] # Enabled advanced function support
    param(
        $Hostname
    )
    PROCESS
    {
        try
        {
            $Result = Invoke-RestMethod -Uri ('http://{0}/common/basic_info' -f $Hostname) -Method GET -ErrorAction Stop
        }
        catch
        {
            throw 'Failed to invoke rest method'
        }

        try
        {
            $Result = Convert-DaikinResponse -String $Result -ErrorAction Stop
        }
        catch
        {
            throw 'Failed to convert daikin response'
        }

        return $Result
    }
}
#endregion
#EndRegion '.\Private\Get-DaikinBasicInfo.ps1' 42
#Region '.\Private\Get-DaikinControlInfo.ps1' 0
function Get-DaikinControlInfo
{
    <#
        .DESCRIPTION
            Retrevies daikin control info response and optionally converts it into a more readable format
        .PARAMETER Hostname
            Defines the hostname of the Daikin unit
        .PARAMETER Raw
            Defines that no attribute name mapping should be done
        .EXAMPLE
            Get-DaikinControlInfo -hostname daikin.network.com
            Returns the control info object converted to a readable format
        .EXAMPLE
            Get-DaikinControlInfo -hostname -daikin.network.com -raw
            Returns the control info object with as-is property names
    #>


    [CmdletBinding()] # Enabled advanced function support
    param(
        [Parameter(Mandatory)]$Hostname,
        [switch]$Raw
    )
    PROCESS
    {
        try
        {
            $Result = Invoke-RestMethod -Uri ('http://{0}/aircon/get_control_info' -f $Hostname) -Method GET -ErrorAction Stop
        }
        catch
        {
            throw $_.exception.message
        }

        try
        {
            $Result = Convert-DaikinResponse -String $Result -Raw:$Raw -ErrorAction Stop
        }
        catch
        {
            throw $_.exception.message
        }

        return $Result
    }
}
#endregion
#EndRegion '.\Private\Get-DaikinControlInfo.ps1' 47
#Region '.\Private\Get-DaikinModelInfo.ps1' 0
function Get-DaikinModelInfo
{
    <#
        .DESCRIPTION
            Get Daikin model info from unit
        .PARAMETER Hostname
            Defines the hostname of the Daikin unit
        .PARAMETER Raw
            Defines that no attribute name mapping should be done
        .EXAMPLE
            Get-DaikinModelInfo
            Description of example
    #>


    [CmdletBinding()] # Enabled advanced function support
    param(
        $Hostname,
        $Raw
    )
    PROCESS
    {
        try
        {
            $Result = Invoke-RestMethod -Uri ('http://{0}/aircon/get_model_info' -f $Hostname) -Method GET -ErrorAction Stop
        }
        catch
        {
            throw $_.exception.message
        }

        try
        {
            $Result = Convert-DaikinResponse -String $Result -Raw:$Raw -ErrorAction Stop
        }
        catch
        {
            throw $_.exception.message
        }
        return $Result
    }
}
#endregion
#EndRegion '.\Private\Get-DaikinModelInfo.ps1' 43
#Region '.\Private\Get-DaikinPollingConfiguration.ps1' 0
function Get-DaikinPollingConfiguration
{
    <#
        .DESCRIPTION
            Get Daikin polling configuration from unit
        .PARAMETER Hostname
            Defines the hostname of the Daikin unit
        .PARAMETER Raw
            Defines that no attribute name mapping should be done
        .EXAMPLE
            Get-DaikinPollingConfiguration
            Description of example
    #>


    [CmdletBinding()] # Enabled advanced function support
    param(
        $Hostname,
        $Raw
    )
    PROCESS
    {
        try
        {
            $Result = Invoke-RestMethod -Uri ('http://{0}//common/get_remote_method' -f $Hostname) -Method GET -ErrorAction Stop
        }
        catch
        {
            throw $_.exception.message
        }

        try
        {
            $Result = Convert-DaikinResponse -String $Result -Raw:$Raw -ErrorAction Stop
        }
        catch
        {
            throw $_.exception.message
        }
        return $Result
    }
}
#endregion
#EndRegion '.\Private\Get-DaikinPollingConfiguration.ps1' 43
#Region '.\Private\Get-DaikinSensorInfo.ps1' 0
function Get-DaikinSensorInfo
{
    <#
        .DESCRIPTION
            Get Daikin sensor info from Daikin unit
        .PARAMETER Hostname
            Defines the hostname of the Daikin unit
        .PARAMETER Raw
            Defines that no attribute name mapping should be done
        .EXAMPLE
            Get-DaikinSensorInfo
            Description of example
    #>


    [CmdletBinding()] # Enabled advanced function support
    param(
        $Hostname,
        $Raw
    )

    PROCESS
    {
        try
        {
            $Result = Invoke-RestMethod -Uri ('http://{0}/aircon/get_sensor_info' -f $Hostname) -Method GET -ErrorAction Stop
        }
        catch
        {
            throw $_.exception.message
        }

        try
        {
            $Result = Convert-DaikinResponse -String $Result -Raw:$Raw -ErrorAction Stop
        }
        catch
        {
            throw $_.exception.message
        }
        return $Result
    }
}
#endregion
#EndRegion '.\Private\Get-DaikinSensorInfo.ps1' 44
#Region '.\Private\Get-DaikinWeekStats.ps1' 0
function Get-DaikinWeekStats
{
    <#
        .DESCRIPTION
            Get Daikin Week statistics from Daikin unit
        .PARAMETER Hostname
            Defines the hostname of the Daikin unit
        .PARAMETER Raw
            Defines that no attribute name mapping should be done
        .EXAMPLE
            Get-DaikinWeekStats
            Description of example
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Stat is short to static which is missleading')]
    [CmdletBinding()] # Enabled advanced function support
    param(
        $Hostname,
        $Raw
    )

    PROCESS
    {
        try
        {
            $Result = Invoke-RestMethod -Uri ('http://{0}/aircon/get_week_power' -f $Hostname) -Method GET -ErrorAction Stop
        }
        catch
        {
            throw $_.exception.message
        }

        try
        {
            $Result = Convert-DaikinResponse -String $Result -Raw:$Raw -ErrorAction Stop
        }
        catch
        {
            throw $_.exception.message
        }
        return $Result
    }
}
#endregion
#EndRegion '.\Private\Get-DaikinWeekStats.ps1' 45
#Region '.\Private\Get-DaikinYearStats.ps1' 0
function Get-DaikinYearStats
{
    <#
        .DESCRIPTION
            Get Daikin year statistics from Daikin unit
        .PARAMETER Hostname
            Defines the hostname of the Daikin unit
        .PARAMETER Raw
            Defines that no attribute name mapping should be done
        .EXAMPLE
            Get-DaikinYearStats
            Description of example
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Stat is short to static which is missleading')]
    [CmdletBinding()] # Enabled advanced function support
    param(
        $Hostname,
        $Raw
    )

    PROCESS
    {
        try
        {
            $Result = Invoke-RestMethod -Uri ('http://{0}/aircon/get_year_power' -f $Hostname) -Method GET -ErrorAction Stop
        }
        catch
        {
            throw $_.exception.message
        }

        try
        {
            $Result = Convert-DaikinResponse -String $Result -Raw:$Raw -ErrorAction Stop
        }
        catch
        {
            throw $_.exception.message
        }
        return $Result
    }
}
#endregion
#EndRegion '.\Private\Get-DaikinYearStats.ps1' 45
#Region '.\Private\Invoke-GarbageCollect.ps1' 0
function Invoke-GarbageCollect
{
    <#
    .SYNOPSIS
        Calls system.gc collect method. Purpose is mainly for readability.
    .DESCRIPTION
        Calls system.gc collect method. Purpose is mainly for readability.
    .EXAMPLE
        Invoke-GarbageCollect
    #>

    [system.gc]::Collect()
}
#EndRegion '.\Private\Invoke-GarbageCollect.ps1' 13
#Region '.\Private\pslog.ps1' 0
function pslog
{
    <#
    .SYNOPSIS
        This is simple logging function that automatically log to file. Logging to console is maintained.
    .DESCRIPTION
        This is simple logging function that automatically log to file. Logging to console is maintained.
    .PARAMETER Severity
        Defines the type of log, valid vales are, Success,Info,Warning,Error,Verbose,Debug
    .PARAMETER Message
        Defines the message for the log entry
    .PARAMETER Source
        Defines a source, this is useful to separate log entries in categories for different stages of a process or for each function, defaults to default
    .PARAMETER Throw
        Specifies that when using severity error pslog will throw. This is useful in catch statements so that the terminating error is propagated upwards in the stack.
    .PARAMETER LogDirectoryOverride
        Defines a hardcoded log directory to write the log file to. This defaults to %appdatalocal%\<modulename\logs.
    .PARAMETER DoNotLogToConsole
        Specifies that logs should only be written to the log file and not to the console.
    .EXAMPLE
        pslog Verbose 'Successfully wrote to logfile'
        Explanation of the function or its result. You can include multiple examples with additional .EXAMPLE lines
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Sole purpose of function is logging, including console')]
    [cmdletbinding()]
    param(
        [parameter(Position = 0)]
        [ValidateSet('Success', 'Info', 'Warning', 'Error', 'Verbose', 'Debug')]
        [Alias('Type')]
        [string]
        $Severity,

        [parameter(Mandatory, Position = 1)]
        [string]
        $Message,

        [parameter(position = 2)]
        [string]
        $source = 'default',

        [parameter(Position = 3)]
        [switch]
        $Throw,

        [parameter(Position = 4)]
        [string]
        $LogDirectoryOverride,

        [parameter(Position = 5)]
        [switch]
        $DoNotLogToConsole
    )

    begin
    {
        if (-not $LogDirectoryOverride)
        {
            $localappdatapath = [Environment]::GetFolderPath('localapplicationdata') # ie C:\Users\<username>\AppData\Local
            $modulename = $MyInvocation.MyCommand.Module
            $logdir = "$localappdatapath\$modulename\logs"
        }
        else
        {
            $logdir = $LogDirectoryOverride
        }
        $logdir | Assert-FolderExist -Verbose:$VerbosePreference
        $timestamp = (Get-Date)
        $logfilename = ('{0}.log' -f $timestamp.ToString('yyy-MM-dd'))
        $timestampstring = $timestamp.ToString('yyyy-MM-ddThh:mm:ss.ffffzzz')
    }

    process
    {
        switch ($Severity)
        {
            'Success'
            {
                "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                if (-not $DoNotLogToConsole)
                {
                    Write-Host -Object "SUCCESS: $timestampstring`t$source`t$message" -ForegroundColor Green
                }
            }
            'Info'
            {
                "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                if (-not $DoNotLogToConsole)
                {
                    Write-Information -MessageData "$timestampstring`t$source`t$message"
                }
            }
            'Warning'
            {
                "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                if (-not $DoNotLogToConsole)
                {
                    Write-Warning -Message "$timestampstring`t$source`t$message"
                }
            }
            'Error'
            {
                "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                if (-not $DoNotLogToConsole)
                {
                    Write-Error -Message "$timestampstring`t$source`t$message"
                }
                if ($throw)
                {
                    throw
                }
            }
            'Verbose'
            {
                if ($VerbosePreference -ne 'SilentlyContinue')
                {
                    "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                }
                if (-not $DoNotLogToConsole)
                {
                    Write-Verbose -Message "$timestampstring`t$source`t$message"
                }
            }
            'Debug'
            {
                if ($DebugPreference -ne 'SilentlyContinue')
                {
                    "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                }
                if (-not $DoNotLogToConsole)
                {
                    Write-Debug -Message "$timestampstring`t$source`t$message"
                }
            }
        }
    }
}
#EndRegion '.\Private\pslog.ps1' 137
#Region '.\Private\Resolve-DaikinHostname.ps1' 0
function Resolve-DaikinHostname
{
    <#
        .DESCRIPTION
            Resolves the IP address if hostname is specified as FQDN/Hostname
        .PARAMETER Hostname
            IP or FQDN for Dakin unit
        .EXAMPLE
            Resolve-DaikinHostname -Hostname daikin.network.com
            Returns the IP address of the target device
    #>


    [CmdletBinding()] # Enabled advanced function support
    param(
        [Parameter(Mandatory)]$Hostname
    )
    BEGIN
    {
        function internal_resolvednsname
        {
            param
            (
                $hostname
            )
            <#
                Test-Connection is disqualified because the returned property name for the
                IP address in Test-Connection are different depending on Powershell edition. Which meant
                that the function had to check for edition before retreiving the value. And in turn would
                never fulfill complete code coverage tests.
 
                Resolve-DNSName is disqualified because it is not available on Linux and MacOS.
 
                Resorted to .NET class System.Net.Dns and the method GetHostEntry which works on all editions and platforms.
 
                The operation is separated in a internal function to allow unit test mocks
 
            #>

            return [System.Net.Dns]::GetHostEntry($hostname).AddressList.Where( { $_.AddressFamily -eq 'InterNetwork' })[0].IPAddressToString
        }
    }

    PROCESS
    {
        $SavedProgressPreference = $global:progresspreference
        $Global:ProgressPreference = 'SilentlyContinue'
        try
        {
            if (Test-DaikinConnectivity -HostName:$Hostname)
            {
                if ($Hostname -match '(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)')
                {
                    return $hostname
                }
                else
                {
                    try
                    {
                        return (internal_resolvednsname -hostname $hostname)
                    }
                    catch
                    {
                        throw "Failed to resolve hostname $hostname to IP address with error: $PSItem"
                    }
                }
            }
            else
            {
                throw 'Device does not respond'
            }
        }
        catch
        {
            throw "Failed to resolve IP address of hostname with error: $PSItem"
        }
        finally
        {
            $global:ProgressPreference = $SavedProgressPreference
        }
    }
}
#endregion
#EndRegion '.\Private\Resolve-DaikinHostname.ps1' 82
#Region '.\Private\Test-DaikinConnectivity.ps1' 0
function Test-DaikinConnectivity
{
    <#
        .DESCRIPTION
            Function tests connection to specified target
        .PARAMETER Hostname
            IP or FQDN for Daikin unit
        .EXAMPLE
            Test-DaikinConnectivity -Hostname daikin.network.com
            Returns true or false depending on if the device responds
    #>


    [CmdletBinding()] # Enabled advanced function support
    [OutputType([boolean])]
    param(
        [Parameter(Mandatory)]$Hostname
    )

    PROCESS
    {
        $SavedProgressPreference = $global:ProgressPreference
        $global:ProgressPreference = 'SilentlyContinue'
        try
        {
            if (Test-Connection -ComputerName $Hostname -Quiet -WarningAction SilentlyContinue)
            {
                return $true
            }
            else
            {
                return $false
            }
        }
        catch
        {
            throw "Failed to check status of daikin device with error: $PSItem"
        }
        finally
        {
            $global:ProgressPreference = $SavedProgressPreference
        }
    }

}
#endregion
#EndRegion '.\Private\Test-DaikinConnectivity.ps1' 46
#Region '.\Private\Write-PSProgress.ps1' 0
function Write-PSProgress
{
    <#
    .SYNOPSIS
        Wrapper for PSProgress
    .DESCRIPTION
        This function will automatically calculate items/sec, eta, time remaining
        as well as set the update frequency in case the there are a lot of items processing fast.
    .PARAMETER Activity
        Defines the activity name for the progressbar
    .PARAMETER Id
        Defines a unique ID for this progressbar, this is used when nesting progressbars
    .PARAMETER Target
        Defines a arbitrary text for the currently processed item
    .PARAMETER ParentId
        Defines the ID of a parent progress bar
    .PARAMETER Completed
        Explicitly tells powershell to set the progress bar as completed removing
        it from view. In some cases the progress bar will linger if this is not done.
    .PARAMETER Counter
        The currently processed items counter
    .PARAMETER Total
        The total number of items to process
    .PARAMETER StartTime
        Sets the start datetime for the progressbar, this is required to calculate items/sec, eta and time remaining
    .PARAMETER DisableDynamicUpdateFrquency
        Disables the dynamic update frequency function and every item will update the status of the progressbar
    .PARAMETER NoTimeStats
        Disables calculation of items/sec, eta and time remaining
    .EXAMPLE
        1..10000 | foreach-object -begin {$StartTime = Get-Date} -process {
            Write-PSProgress -Activity 'Looping' -Target $PSItem -Counter $PSItem -Total 10000 -StartTime $StartTime
        }
        Explanation of the function or its result. You can include multiple examples with additional .EXAMPLE lines
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Standard')]
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Completed')]
        [string]
        $Activity,

        [Parameter(Position = 1, ParameterSetName = 'Standard')]
        [Parameter(Position = 1, ParameterSetName = 'Completed')]
        [ValidateRange(0, 2147483647)]
        [int]
        $Id,

        [Parameter(Position = 2, ParameterSetName = 'Standard')]
        [string]
        $Target,

        [Parameter(Position = 3, ParameterSetName = 'Standard')]
        [Parameter(Position = 3, ParameterSetName = 'Completed')]
        [ValidateRange(-1, 2147483647)]
        [int]
        $ParentId,

        [Parameter(Position = 4, ParameterSetname = 'Completed')]
        [switch]
        $Completed,

        [Parameter(Mandatory = $true, Position = 5, ParameterSetName = 'Standard')]
        [long]
        $Counter,

        [Parameter(Mandatory = $true, Position = 6, ParameterSetName = 'Standard')]
        [long]
        $Total,

        [Parameter(Position = 7, ParameterSetName = 'Standard')]
        [datetime]
        $StartTime,

        [Parameter(Position = 8, ParameterSetName = 'Standard')]
        [switch]
        $DisableDynamicUpdateFrquency,

        [Parameter(Position = 9, ParameterSetName = 'Standard')]
        [switch]
        $NoTimeStats
    )

    # Define current timestamp
    $TimeStamp = (Get-Date)

    # Define a dynamic variable name for the global starttime variable
    $StartTimeVariableName = ('ProgressStartTime_{0}' -f $Activity.Replace(' ', ''))

    # Manage global start time variable
    if ($PSBoundParameters.ContainsKey('Completed') -and (Get-Variable -Name $StartTimeVariableName -Scope Global -ErrorAction SilentlyContinue))
    {
        # Remove the global starttime variable if the Completed switch parameter is users
        try
        {
            Remove-Variable -Name $StartTimeVariableName -ErrorAction Stop -Scope Global
        }
        catch
        {
            throw $_
        }
    }
    elseif (-not (Get-Variable -Name $StartTimeVariableName -Scope Global -ErrorAction SilentlyContinue))
    {
        # Global variable do not exist, create global variable
        if ($null -eq $StartTime)
        {
            # No start time defined with parameter, use current timestamp as starttime
            Set-Variable -Name $StartTimeVariableName -Value $TimeStamp -Scope Global
            $StartTime = $TimeStamp
        }
        else
        {
            # Start time defined with parameter, use that value as starttime
            Set-Variable -Name $StartTimeVariableName -Value $StartTime -Scope Global
        }
    }
    else
    {
        # Global start time variable is defined, collect and use it
        $StartTime = Get-Variable -Name $StartTimeVariableName -Scope Global -ErrorAction Stop -ValueOnly
    }

    # Define frequency threshold
    $Frequency = [Math]::Ceiling($Total / 100)
    switch ($PSCmdlet.ParameterSetName)
    {
        'Standard'
        {
            # Only update progress is any of the following is true
            # - DynamicUpdateFrequency is disabled
            # - Counter matches a mod of defined frequecy
            # - Counter is 0
            # - Counter is equal to Total (completed)
            if (($DisableDynamicUpdateFrquency) -or ($Counter % $Frequency -eq 0) -or ($Counter -eq 1) -or ($Counter -eq $Total))
            {

                # Calculations for both timestats and without
                $Percent = [Math]::Round(($Counter / $Total * 100), 0)

                # Define count progress string status
                $CountProgress = ('{0}/{1}' -f $Counter, $Total)

                # If percent would turn out to be more than 100 due to incorrect total assignment revert back to 100% to avoid that write-progress throws
                if ($Percent -gt 100)
                {
                    $Percent = 100
                }

                # Define write-progress splat hash
                $WriteProgressSplat = @{
                    Activity         = $Activity
                    PercentComplete  = $Percent
                    CurrentOperation = $Target
                }

                # Add ID if specified
                if ($Id)
                {
                    $WriteProgressSplat.Id = $Id
                }

                # Add ParentID if specified
                if ($ParentId)
                {
                    $WriteProgressSplat.ParentId = $ParentId
                }

                # Calculations for either timestats and without
                if ($NoTimeStats)
                {
                    $WriteProgressSplat.Status = ('{0} - {1}%' -f $CountProgress, $Percent)
                }
                else
                {
                    # Total seconds elapsed since start
                    $TotalSeconds = ($TimeStamp - $StartTime).TotalSeconds

                    # Calculate items per sec processed (IpS)
                    $ItemsPerSecond = ([Math]::Round(($Counter / $TotalSeconds), 2))

                    # Calculate seconds spent per processed item (for ETA)
                    $SecondsPerItem = if ($Counter -eq 0)
                    {
                        0
                    }
                    else
                    {
 ($TotalSeconds / $Counter)
                    }

                    # Calculate seconds remainging
                    $SecondsRemaing = ($Total - $Counter) * $SecondsPerItem
                    $WriteProgressSplat.SecondsRemaining = $SecondsRemaing

                    # Calculate ETA
                    $ETA = $(($Timestamp).AddSeconds($SecondsRemaing).ToShortTimeString())

                    # Add findings to write-progress splat hash
                    $WriteProgressSplat.Status = ('{0} - {1}% - ETA: {2} - IpS {3}' -f $CountProgress, $Percent, $ETA, $ItemsPerSecond)
                }

                # Call writeprogress
                Write-Progress @WriteProgressSplat
            }
        }
        'Completed'
        {
            Write-Progress -Activity $Activity -Id $Id -Completed
        }
    }
}
#EndRegion '.\Private\Write-PSProgress.ps1' 214
#Region '.\Public\Get-DaikinStatus.ps1' 0
function Get-DaikinStatus
{
    <#
        .DESCRIPTION
            Retreives the current configuration of the Daikin AirCon device
        .PARAMETER Hostname
            Hostname or IP of the Daikin Aircon device.
        .EXAMPLE
            Get-DaikinStatus -Hostname daikin.local.network
 
            PowerOn : True
            Mode : HEAT
            TargetTemp : 22.0
            ...
    #>


    [CmdletBinding()] # Enabled advanced function support
    param(
        $HostName
    )

    BEGIN
    {
        $Hostname = Resolve-DaikinHostname -Hostname:$Hostname
        $ControlInfo = Get-DaikinControlInfo -Hostname:$HostName
        Write-Verbose -Message 'Collected ControlInfo via REST API'
        # $ModelInfo = Get-DaikinModelInfo -Hostname:$HostName
        # Write-Verbose -Message 'Collected ModelInfo via REST API'
        $BasicInfo = Get-DaikinBasicInfo -Hostname:$HostName
        Write-Verbose -Message 'Collected BasicInfo via REST API'
        $SensorInfo = Get-DaikinSensorInfo -HostName:$HostName
        Write-Verbose -Message 'Collected SensorInfo via REST API'
    }

    PROCESS
    {
        $ObjectHash = [ordered]@{
            'PowerOn'        = $ControlInfo.PowerOn
            'Mode'           = $ControlInfo.Mode
            'TargetTemp'     = $ControlInfo.TargetTemp
            'TargetHumidity' = $ControlInfo.TargetHumidity
            'FanSpeed'       = $ControlInfo.FanSpeed
            'FanDirection'   = $ControlInfo.FanDirection
            'InsideTemp'     = $SensorInfo.InsideTemp
            'InsideHumidity' = $SensorInfo.InsideHumidity
            'OutsideTemp'    = $SensorInfo.OutsideTemp
            'DeviceType'     = $BasicInfo.DeviceType
            'Region'         = $BasicInfo.Region
            'Version'        = $BasicInfo.Version.Replace('_', '.')
            'Revision'       = $BasicInfo.Revision
            'Port'           = $BasicInfo.port
            'Identity'       = $BasicInfo.Identity
            'MACAddress'     = $BasicInfo.mac
        }
        return [pscustomobject]$ObjectHash
    }
}
#endregion
#EndRegion '.\Public\Get-DaikinStatus.ps1' 59
#Region '.\Public\Set-DaikinAirCon.ps1' 0
function Set-DaikinAirCon
{
    <#
        .DESCRIPTION
            Cmdlet allows to configure the aircon device to the desired setting.
        .PARAMETER Hostname
            Hostname or IP of the Daikin AirCon device.
        .PARAMETER PowerOn
            Set to $true or $false to specify if the master power should be on or off. This does not the affect the connectivity of the control surfice.
        .PARAMETER Temp
            Specified the target temperature in celcius.
        .PARAMETER Mode
            Specifies the operating mode of the aircon device. Allowed values are;
            AUTO : Switches between heat and cold depending on the current and target temperature.
            DRY : Sets the device lower the humidity of the air.
            COLD : Sets the device chill the air if needed. No heat will be provided.
            HEAT : Sets the device heat the air if needed. No chilling of air will be provided.
            FAN : Sets the device to only circulate air without affecting temperature or humidity.
        .PARAMETER FanSpeed
            Specifies the strenght of the fan. Allowed values are;
            AUTO : Sets the device to manage fan speed to keep the target climate.
            SILENT : Sets the fan speed to minimize noise.
            Level_1 -> Level_5 : Sets the fan speed to the target level.
        .PARAMETER FanDirection
            Specifies how the direction of airflow is controlled. Allowed values are
            Stopped, VerticalSwing, HorizontalSwing and BothSwing
        .EXAMPLE
            Set-DaikinAirCon -HostName daikin.local.network -PowerOn:$true -Temp 19 -Mode AUTO -FanSpeed AUTO -FanDirection Stopped
 
            This example configures the aircon device to the specified parameter values.
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidInvokingEmptyMembers', '', Justification = 'asd')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Not relevant for this use case')]
    [CmdletBinding()] # Enabled advanced function support
    param(
        $HostName,
        [boolean]$PowerOn,
        [ValidateRange(10, 41)][int]$Temp,
        [ValidateSet('AUTO', 'DRY', 'COLD', 'HEAT', 'FAN')]$Mode,
        [ValidateSet('AUTO', 'SILENT', 'Level_1', 'Level_2', 'Level_3', 'Level_4', 'Level_5')]$FanSpeed,
        [ValidateSet('Stopped', 'VerticalSwing', 'HorizontalSwing', 'BothSwing')]$FanDirection
    )

    BEGIN
    {
        $ModeTranslation = @{
            'AUTO' = '1'
            'DRY'  = '2'
            'COLD' = '3'
            'HEAT' = '4'
            'FAN'  = '6'
        }
        $FanSpeedTranslation = @{
            'AUTO'    = 'A'
            'SILENT'  = 'B'
            'Level_1' = 'lvl_1'
            'Level_2' = 'lvl_2'
            'Level_3' = 'lvl_3'
            'Level_4' = 'lvl_4'
            'Level_5' = 'lvl_5'

        }
        $FanDirectionTranslation = @{
            'Stopped'         = '0'
            'VerticalSwing'   = '1'
            'HorizontalSwing' = '2'
            'BothSwing'       = '3'
        }

        $CurrentSettings = Get-DaikinControlInfo -Hostname:$Hostname -Raw
        $NewSettings = [ordered]@{
            'pow'    = $CurrentSettings.pow
            'mode'   = $CurrentSettings.mode
            'stemp'  = $CurrentSettings.stemp
            'shum'   = $CurrentSettings.shum
            'f_rate' = $CurrentSettings.f_rate
            'f_dir'  = $CurrentSettings.f_dir
        }
    }

    PROCESS
    {
        foreach ($Key in $PSBoundParameters.Keys)
        {
            if ($Key -eq 'HostName')
            {
                continue
            }
            switch ($Key)
            {
                'Temp'
                {
                    $NewSettings.stemp = $PSBoundParameters.$Key
                }
                'PowerOn'
                {
                    $NewSettings.pow = $PSBoundParameters.$Key
                }
                'Mode'
                {
                    $NewSettings.mode = $ModeTranslation.($PSBoundParameters.$Key)
                }
                'FanSpeed'
                {
                    $NewSettings.f_rate = $FanSpeedTranslation.($PSBoundParameters.$Key)
                }
                'FanDirection'
                {
                    $NewSettings.f_dir = $FanDirectionTranslation.($PSBoundParameters.$Key)
                }
            }
        }
        if ($NewSettings.stemp -eq '--')
        {
            $NewSettings.stemp = $CurrentSettings.('dt{0}' -f $NewSettings.Mode)
        }
        if ($NewSettings.shum -eq '--')
        {
            $NewSettings.shum = $CurrentSettings.('dh{0}' -f $NewSettings.Mode)
        }
    }

    END
    {
        $String = @()
        foreach ($Key in $NewSettings.Keys)
        {
            $String += ('{0}={1}' -f $Key, $NewSettings.$Key)
        }
        $PropertyString = $String -join '&'

        $URI = ('http://{0}/aircon/set_control_info?{1}' -f $HostName, $PropertyString)
        $Result = Invoke-RestMethod -Uri $uri -Method post
        $Result = Convert-DaikinResponse -String $Result

        switch ($Result.ret)
        {
            'OK'
            {
                [pscustomobject]@{
                    result = 'Success'
                }
            }
            default
            {
                Write-Error -Message ('Unknown message returned: {0}' -f $PSItem) -ErrorAction Stop
            }
        }
    }

}
#endregion
#EndRegion '.\Public\Set-DaikinAirCon.ps1' 154
#Region '.\suffix.ps1' 0
# The content of this file will be appended to the top of the psm1 module file. This is useful for custom procesedures after all module functions are loaded.
#EndRegion '.\suffix.ps1' 2

# SIG # Begin signature block
# MIIbmAYJKoZIhvcNAQcCoIIbiTCCG4UCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQU4aFwV532o+S0BMWHf66BgRNG
# Z3qgghYPMIIDBDCCAeygAwIBAgIQFqvAHPTHupdKm+zXSmZcFTANBgkqhkiG9w0B
# AQUFADAaMRgwFgYDVQQDDA9IYW5uZXNQYWxtcXVpc3QwHhcNMjExMTA2MjEzODU5
# WhcNMjYxMTA2MjE0ODU5WjAaMRgwFgYDVQQDDA9IYW5uZXNQYWxtcXVpc3QwggEi
# MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6WNXwWLjX4EAPDxg9U54O83Sr
# Vdf41bEPJz8V0wDQm4MJnnBGZTCqPBENXsfSbIPhHwad+tcsd3nc1ZNuQEJTV3W1
# pXTOtNK+yV0r51VSJvFYYo3qXFyvWdi9n8Z8/yydoWy1vXkx7CsbmIuniODGEzIE
# C1z0QYgrjyNmXw1CewOjX+oW2cgjiJqjuxsS/sLnmqAOMOeV3047GnEEKTyderWR
# L/U+ohEu2WcMao1UCKEtUmPo5rncRyHjY7wzwevby1I8oLgHWm8NlH9K2Kp43tx9
# +Mcpn64hTYW+jd7VCFmrEIDfZEWdY5EBfJZDwnol0vXvFPl+7E+WaCtKe6zNAgMB
# AAGjRjBEMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNV
# HQ4EFgQU0+0dMijn0qYkRTkT8lp0KErfo+swDQYJKoZIhvcNAQEFBQADggEBACoT
# mkcM6C7Atq/ZzwVOH0A1qd5t1KsnAX393xLk73ssnmOW/c4P2fcoeEmQ8Idk6FJf
# v+D6MkZIRb7PefqTCVhgi2ZZynNCWE2zqt5VxEZIPiG1Amke/OhEI8fxL/01HWZj
# kGZD3cquXMA3SGS86Y7tX2S31/rAG812ew57ghfOL3EUPfBwCrSKV679ncNL0Td9
# AKiK2pUraM32/UppF7kEZlSTPtPwCPSp+xihf6ZRJM95gB8bSa+K/1wd8CD9f6Ng
# XkyiSesaUjMhfh+hhg8otqB1ZqvsqsNtsibKo1IAHWWNDxa3sv8g4rErTQFARx4p
# 3pksn4fYdZXhQzOHMtIwggWNMIIEdaADAgECAhAOmxiO+dAt5+/bUOIIQBhaMA0G
# CSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lDZXJ0
# IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBaFw0zMTExMDkyMzU5
# NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNV
# BAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQg
# Um9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/mkHNo3rvk
# XUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/zG6Q4FutWxpdt
# HauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZanMylNEQRBAu
# 34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7sWxq868nPzaw0
# QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL2pNe3I6PgNq2
# kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfbBHMqbpEBfCFM
# 1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3JFxGj2T3wWmI
# dph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3cAORFJYm2mkQZ
# K37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqxYxhElRp2Yn72
# gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0viastkF13nqs
# X40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aLT8LWRV+dIPyh
# HsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIBNjAPBgNVHRMBAf8E
# BTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzAfBgNVHSMEGDAW
# gBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMCAYYweQYIKwYBBQUH
# AQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQwYI
# KwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFz
# c3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0aHR0cDovL2NybDMu
# ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDARBgNVHSAE
# CjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0NcVec4X6CjdBs9thbX
# 979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnovLbc47/T/gLn4offy
# ct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65ZyoUi0mcudT6cGAxN3
# J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFWjuyk1T3osdz9HNj0
# d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPFmCLBsln1VWvPJ6ts
# ds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9ztwGpn1eqXijiuZQw
# ggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1
# c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqG
# SIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbS
# g9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9
# /UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXn
# HwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0
# VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4f
# sbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40Nj
# gHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0
# QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvv
# mz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T
# /jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk
# 42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5r
# mQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E
# FgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5n
# P+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcG
# CCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu
# Y29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln
# aUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8v
# Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNV
# HSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIB
# AH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxp
# wc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIl
# zpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQ
# cAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfe
# Kuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+j
# Sbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJsh
# IUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6
# OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDw
# N7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR
# 81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2
# VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIGwDCCBKigAwIBAgIQ
# DE1pckuU+jwqSj0pB4A9WjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEX
# MBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0
# ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTIyMDkyMTAw
# MDAwMFoXDTMzMTEyMTIzNTk1OVowRjELMAkGA1UEBhMCVVMxETAPBgNVBAoTCERp
# Z2lDZXJ0MSQwIgYDVQQDExtEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMiAtIDIwggIi
# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDP7KUmOsap8mu7jcENmtuh6BSF
# dDMaJqzQHFUeHjZtvJJVDGH0nQl3PRWWCC9rZKT9BoMW15GSOBwxApb7crGXOlWv
# M+xhiummKNuQY1y9iVPgOi2Mh0KuJqTku3h4uXoW4VbGwLpkU7sqFudQSLuIaQyI
# xvG+4C99O7HKU41Agx7ny3JJKB5MgB6FVueF7fJhvKo6B332q27lZt3iXPUv7Y3U
# TZWEaOOAy2p50dIQkUYp6z4m8rSMzUy5Zsi7qlA4DeWMlF0ZWr/1e0BubxaompyV
# R4aFeT4MXmaMGgokvpyq0py2909ueMQoP6McD1AGN7oI2TWmtR7aeFgdOej4TJEQ
# ln5N4d3CraV++C0bH+wrRhijGfY59/XBT3EuiQMRoku7mL/6T+R7Nu8GRORV/zbq
# 5Xwx5/PCUsTmFntafqUlc9vAapkhLWPlWfVNL5AfJ7fSqxTlOGaHUQhr+1NDOdBk
# +lbP4PQK5hRtZHi7mP2Uw3Mh8y/CLiDXgazT8QfU4b3ZXUtuMZQpi+ZBpGWUwFjl
# 5S4pkKa3YWT62SBsGFFguqaBDwklU/G/O+mrBw5qBzliGcnWhX8T2Y15z2LF7OF7
# ucxnEweawXjtxojIsG4yeccLWYONxu71LHx7jstkifGxxLjnU15fVdJ9GSlZA076
# XepFcxyEftfO4tQ6dwIDAQABo4IBizCCAYcwDgYDVR0PAQH/BAQDAgeAMAwGA1Ud
# EwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwIAYDVR0gBBkwFzAIBgZn
# gQwBBAIwCwYJYIZIAYb9bAcBMB8GA1UdIwQYMBaAFLoW2W1NhS9zKXaaL3WMaiCP
# nshvMB0GA1UdDgQWBBRiit7QYfyPMRTtlwvNPSqUFN9SnDBaBgNVHR8EUzBRME+g
# TaBLhklodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRS
# U0E0MDk2U0hBMjU2VGltZVN0YW1waW5nQ0EuY3JsMIGQBggrBgEFBQcBAQSBgzCB
# gDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFgGCCsGAQUF
# BzAChkxodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVk
# RzRSU0E0MDk2U0hBMjU2VGltZVN0YW1waW5nQ0EuY3J0MA0GCSqGSIb3DQEBCwUA
# A4ICAQBVqioa80bzeFc3MPx140/WhSPx/PmVOZsl5vdyipjDd9Rk/BX7NsJJUSx4
# iGNVCUY5APxp1MqbKfujP8DJAJsTHbCYidx48s18hc1Tna9i4mFmoxQqRYdKmEIr
# UPwbtZ4IMAn65C3XCYl5+QnmiM59G7hqopvBU2AJ6KO4ndetHxy47JhB8PYOgPvk
# /9+dEKfrALpfSo8aOlK06r8JSRU1NlmaD1TSsht/fl4JrXZUinRtytIFZyt26/+Y
# siaVOBmIRBTlClmia+ciPkQh0j8cwJvtfEiy2JIMkU88ZpSvXQJT657inuTTH4YB
# ZJwAwuladHUNPeF5iL8cAZfJGSOA1zZaX5YWsWMMxkZAO85dNdRZPkOaGK7DycvD
# +5sTX2q1x+DzBcNZ3ydiK95ByVO5/zQQZ/YmMph7/lxClIGUgp2sCovGSxVK05iQ
# RWAzgOAj3vgDpPZFR+XOuANCR+hBNnF3rf2i6Jd0Ti7aHh2MWsgemtXC8MYiqE+b
# vdgcmlHEL5r2X6cnl7qWLoVXwGDneFZ/au/ClZpLEQLIgpzJGgV8unG1TnqZbPTo
# ntRamMifv427GFxD9dAq6OJi7ngE273R+1sKqHB+8JeEeOMIA11HLGOoJTiXAdI/
# Otrl5fbmm9x+LMz/F0xNAKLY1gEOuIvu5uByVYksJxlh9ncBjDGCBPMwggTvAgEB
# MC4wGjEYMBYGA1UEAwwPSGFubmVzUGFsbXF1aXN0AhAWq8Ac9Me6l0qb7NdKZlwV
# MAkGBSsOAwIaBQCgeDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3
# DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEV
# MCMGCSqGSIb3DQEJBDEWBBRnc9yryO2ngRLWvrypUMErvo2g0DANBgkqhkiG9w0B
# AQEFAASCAQCCXU8BCTpzjw80H1OTSC63A0TdLnU9AiBcIbaxXYXOnQJGlZ0ScDWa
# T/ytTzpc+DSLC9jZorxg9/pzbURy50vhxxv+ZZq1lFGOpkXkjtckQMD9Ta520/MI
# oB/GhN/fZ/TEQdHpTm1KMivBSFdeVlWoDdK18BfhnLUVz1AikSb1EVLeMl62i6Di
# c8t7T4v0TUi47uEjHEqHshfVu//dnPSmgJ4VY8fJArRlcddCSnDpVg8p/CtjZGHU
# che20ubUBhIz4PHdtjEj146r0MMe8+3BojhSf1MYVt6JS8H0atJYnVkUYoVtUC5e
# U44DNfnDxGPLTQ/4FpUJun5OWsBPVGFHoYIDIDCCAxwGCSqGSIb3DQEJBjGCAw0w
# ggMJAgEBMHcwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMu
# MTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hBMjU2IFRp
# bWVTdGFtcGluZyBDQQIQDE1pckuU+jwqSj0pB4A9WjANBglghkgBZQMEAgEFAKBp
# MBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIyMTEx
# MzE5MDAwOFowLwYJKoZIhvcNAQkEMSIEIKrYfdufbt1q059Bkc2XoH+L28KfYMUy
# CvfIi+NZ51bgMA0GCSqGSIb3DQEBAQUABIICAHtg3QRpXG/flchg9K6BRXPH2meb
# htbPeTJl0pCTvpwjkoR0DrnmWW1iRXt6aWFaRNok+ld7G8U/5U8NPWXCYM+Qe2sE
# DRSGP4Z6oybqtEzAyasVBCXecS0ClJXEtNadHLUy93pHX61mgby18XCNBotAZgz9
# qQpBHCmHafGLcJ6FF2RkUIk5my8pOyu2aW/pu56qGFXOmK/B77oNZe3cirADyuAG
# 2kRSSn3IJS/3PbLrBiT/szf+J3RQjAax+ZptvIc3OpGEn/FQmACeLO1liJ31Zelk
# jlulSsvQqVjXgOtn7LWJZXLPk/lN7sAtHXVEx/a7ODmol7NKa5EUavjO25pkG9dl
# mJ3rr6sUHA+VoQspsXe8feQpIaNYoctk4sDNaMVEG8SQYRyxfqxlWDJgncvrQr/F
# ok1el47fJJY7PBIBeetOBSuzPhGmEspQ87WGK3piQvNmvylQ50ymPr08FJ56xcB4
# VCJ+AwzP0dEtNuy1WOw0cUkOmlaM4oWPGC25Ko0nMreGTKCmjR9ZwmcV9EMkLQ6J
# MAoGvatNPx1nfb3wGwUPdaYcxiF/zzHeWgf6P67x7kVwFMAG44h1QjQIXp9ykcKS
# PLUZFYNyEq3CITZ7mJnrCXKsojfO9P3MI2Wvmnt6oJfE7WgcZccBUTJD0U2ur6e6
# 5V9JeMJE8PGajGdl
# SIG # End signature block