PS-NCentral.psm1

## PowerShell Module for N-Central(c) by N-Able
##
## Version : 1.4
## Author : Adriaan Sluis (as@tosch.nl)
##
## !Still some Work In Progress!
##
## Provides a PowerShell Interface for N-Central(c)
## Uses the SOAP-API of N-Central(c) by N-Able
## Completely written in PowerShell for easy reference/analysis.
##

##Copyright 2022 Tosch Automatisering
##
##Licensed under the Apache License, Version 2.0 (the "License");
##you may not use this file except in compliance with the License.
##You may obtain a copy of the License at
##
## http://www.apache.org/licenses/LICENSE-2.0
##
##Unless required by applicable law or agreed to in writing, software
##distributed under the License is distributed on an "AS IS" BASIS,
##WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
##See the License for the specific language governing permissions and
##limitations under the License.
##

## Change log
##
## v1.2 Feb 24, 2021
## -Made PowerShell 7 compatible by removing usage of WebServiceProxy.
## -Sorting CustomProperty-columns by default (NoSort/UnSorted Option available)
## -JWT-option in New-NCentralConnection
##
## v1.3 Mar 23, 2022
## -CustomProperty - Get individual Property Values. (Was list-only)
## -CustomProperty - Add/Remove individual (Comma-separated) values inside the CP.
## -CustomProperty - Optional Base64 Encoding/Decoding.
## -CustomerDetails - ValidationList for standard-/custom-property filled by API-query.
## -Enhanced Get-NCAccessGroupList/Detail
## -Enhanced Get-NCUserRoleList/Detail
##
## v1.4 May 20,2022
## -(issue) NCActiveIssueList -Status 1 and 5 were swapped. -Added NotifStateTxt-field
## -(issue) NCDeviceInfo - Multiple-values now shown comma-separated (was only showing first entry)
## -NCDeviceObject - Options to Include/Exclude categories
## -Optimized API-calls for multiple objects where supported (NCDeviceInfo and NCCDeviceObject)
## -ShowProgress option for NCDeviceInfo and NCDeviceObject using optimized API-calls.
## -Date/Time properties now have date-format (was String)
## -NCHelp shows a list of statuscodes at the bottom.
##
## v1.5 TBD
## -(issue) NCDevicePropertyList and NCCustomerPropertylist - Error on property-names containing spaces.
## -Optimized API-calls for multiple objects where supported (NCDevicePropertyList and NCCustomerPropertylist added)
## -NCCustomerPropertylist - Full-option to include basic properties
## -Backup-NCCustomProperties - Backup of All CustomerProperties and Custom Device-Properties of associated devices.
##
## v2.0 TBD
## -CP -Backup/Restore to/from JSON v2
##

#Region Classes and Generic Functions
using namespace System.Net

Class NCentral_Connection {
## Using the Interface ServerEI2_PortType
## See documentation @:
## http://mothership.n-able.com/dms/javadoc_ei2/com/nable/nobj/ei2/ServerEI2_PortType.html

#Region Properties

    ## TODO - Enum-lists for StatusIDs, ErrorIDs, ...
    ## TODO - Cleanup WebProxy code (whole module)

    ## Initialize the API-specific values (as static).
    ## No separate NameSpace needed because of Class-enclosure. Instance NameSpace available as Property.
    #static hidden [String]$NWSNameSpace = "NCentral" + ([guid]::NewGuid()).ToString().Substring(25)
    static hidden [String]$SoapURL = "/dms2/services2/ServerEI2?wsdl"

    
    ## Create Properties
    static [String]$PSNCVersion = "1.5"        ## The PS-NCentral version
    [String]$ConnectionURL                    ## Server FQDN
    [String]$BindingURL                        ## Full SOAP-path
    hidden [PSCredential]$Creds = $null        ## Encrypted Credentials

    #[String]$AllProtocols = 'tls12,tls13' ## Https encryption --> issue on older systems, not supporting 1.3
    [String]$AllProtocols = @(If (([System.Net.SecurityProtocolType]).DeclaredMembers.Name -contains "Tls13") { 'tls12,tls13' } Else { 'tls12' }) ## Https encryption - minimum Tls 1.2 needed
    [int]$RequestTimeOut = 100                ## Default timeout in Seconds
    [Boolean]$IsConnected = $false            ## Connection Status
    #hidden [Object]$NameSpace ## For accessing API-Class Objects (WebServiceProxy)
    hidden [Object]$ConnectedVersion        ## For storing full VersionInfoGet-data
    [String]$NCVersion                        ## The UI-version of the connected server
    [int]$DefaultCustomerID                    ## Used when no CustomerID is supplied in most device-commands
    [Object]$Error                            ## Last known Error

    ## Create a general Key/Value Pair. Will be casted at use. Skipped in most methods for non-reuseablity.
    ## Integrated (available in session only): $KeyPair = New-Object -TypeName ($NameSpace + '.tKeyPair')
    #hidden $KeyPair = [PSObject]@{Key=''; Value='';}

    ## Create Key/Value Pairs container(Array).
    hidden [Array]$KeyPairs = @()

    ## Defaults and ValidationLists
    hidden [Array]$rc                                    #Returned Raw Collection of NCentral-Data.
    hidden [Boolean]$CustomerDataModified = $false        #Cache rebuild flag
    hidden [Collections.ArrayList]$RequestFilter = @()    #Limit/Filter AssetDetail categories

    ## Validation/Lookup-lists
    hidden [Collections.IDictionary]$NCStatus=@{}        #Status Code/Description. Initiated/filled in the constructor.
    hidden [Object]$CustomerData                        #Caching of CustomerData for quick reference. Filled at connect.
    hidden [Array]$CustomerValidation = @()                #Supports decision between Customer- and Organization-properties. Filled at connect.

    ## Work In Progress
    #$tCreds

    ## Testing / Debugging only
    hidden $Testvar
# $this.Testvar = $this.GetType().name
    

#EndRegion
    
#Region Constructors

    #Base Constructors
    ## Using ConstructorHelper for chaining.
    
    NCentral_Connection(){
    
        Try{
            ## [ValidatePattern('^server\d{1,4}$')]
            $ServerFQDN = Read-Host "Enter the fqdn of the N-Central Server"
        }
        Catch{
            Write-Host "Connection Aborted"
            Break
        }
        $PSCreds = Get-Credential -Message "Enter NCentral API-User credentials"
        $this.ConstructorHelper($ServerFQDN,$PSCreds)
    }
    
    NCentral_Connection([String]$ServerFQDN){
        $PSCreds = Get-Credential -Message "Enter NCentral API-User credentials"
        $this.ConstructorHelper($ServerFQDN,$PSCreds)
    }
    
    NCentral_Connection([String]$ServerFQDN,[String]$JWT){
        $SecJWT = (ConvertTo-SecureString $JWT -AsPlainText -Force)
        $PSCreds = New-Object PSCredential ("_JWT", $SecJWT)
        $this.ConstructorHelper($ServerFQDN,$PSCreds)
    }

    NCentral_Connection([String]$ServerFQDN,[PSCredential]$PSCreds){
        $this.ConstructorHelper($ServerFQDN,$PSCreds)
    }

    hidden ConstructorHelper([String]$ServerFQDN,[PSCredential]$Credentials){
        ## Constructor Chaining not Standard in PowerShell. Needs a Helper-Method.
        ##
        ## ToDo: ValidatePattern for $ServerFQDN
            
        If (!$ServerFQDN){    
            Write-Host "Invalid ServerFQDN given."
            Break
        }
        If (!$Credentials){    
            Write-Host "No Credentials given."
            Break
        }

        ## Construct Session-parameters.
        ## Place in Class-Property for later reference.
        $this.ConnectionURL = $ServerFQDN        
        $this.Creds = $Credentials

        #Write-Debug "Connecting to $this.ConnectionURL."
        $this.bindingURL = "https://" + $this.ConnectionURL + [NCentral_Connection]::SoapURL
        
        ## Remove existing/previous default-instance. Clears previous login.
        If($null -ne $Global:_NCSession){
            Remove-Variable _NCSession -scope global
        }

        ## Initiate the session to the NCentral-server.
        $this.Connect()

        ## Fill Reference/Lookup-lists

        ## NCStatus - correct spelling essential for ActiveIssuesList filtering.
        $this.NCStatus.1    = "No Data"
        $this.NCStatus.2    = "Stale"
        $this.NCStatus.3    = "Normal"        ## --> Nothing returned in ActiveIssuesList
        $this.NCStatus.4    = "Warning"
        $this.NCStatus.5    = "Failed"
        $this.NCStatus.6    = "Misconfigured"
        $this.NCStatus.7    = "Disconnected"
        $this.NCStatus.8    = "Disabled"
        $this.NCStatus.11 = "Unacknowledged"
        $this.NCStatus.12 = "Acknowledged"

    }    
    

#EndRegion

#Region Methods
# ## Features
# ## Returns all data as Object-collections to allow pipelines.
# ## Mimic the names of the API-method where possible.
# ## Supports Synchronous Requests only (for now).
# ## NO 'Dangerous' API's are implemented (Delete/Remove).
# ##
# ## To Do
# ## TODO - Check for $this.IsConnected before execution.
# ## TODO - General Error-handling + customized throws.
# ## TODO - Additional Add/Set-methods
# ## TODO - Progress indicator (Write-Progress)
# ## TODO - DeviceAssetInfoExportWithSettings options (Exclude/Include) WIP
# ## TODO - Error on AccessGroupGet
# ## TODO - Async processing
# ##

    #Region ClassSupport

    ## Connection Support
    [void]Connect(){
    
        ## Reset connection-indicator
        $this.IsConnected = $false

        ## Secure communications
        [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
        [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]$this.AllProtocols

        ## Use versionInfoGet, Includes checking SOAP-connection
        ## No credentials needed (yet).
        $VersionEnvelope = @"
            <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:ei2="http://ei2.nobj.nable.com/">
            <soap:Header/>
            <soap:Body>
                <ei2:versionInfoGet/>
            </soap:Body>
            </soap:Envelope>
"@
        ## End of Here-String must be left-lined.

        ## Set the Request-properties in a local Dictionary / Hash-table.
        $RequestProps = @{}
        $RequestProps.Method = "Post"
        $RequestProps.Uri = $this.BindingURL
        $RequestProps.TimeoutSec = $this.RequestTimeOut
        $RequestProps.body =  $VersionEnvelope

        Try{
            #$this.ConnectedVersion = (Invoke-RestMethod -Uri $this.BindingURL -body $VersionEnvelope -Method POST -TimeoutSec $this.RequestTimeOut)."envelope.body.versionInfoGetResponse.return" |
            $this.ConnectedVersion = (Invoke-RestMethod @RequestProps).envelope.body.versionInfoGetResponse.return |
                Select-Object key,value
        }
# Catch [System.Net.WebException]{
# $this.Error = $_
# $this.ErrorHandler()
# }
        Catch {
            $this.Error = $_
            $this.ErrorHandler()
        }
        ## Extract NCental UI version from returned data
        $this.NCVersion = ($this.Connectedversion | 
                            Where-Object {$_.key -eq "Installation: Deployment Product Version"} ).value

        ## TODO Make valid check on connection-error (incl. Try/Catch)
        ## Now checking on version-data retrieval.
# if ($this.Connection){
# if ($this.Connection.useragent){
        if ($this.NCVersion){
            $this.IsConnected = $true
        }

        ## Fill cache-settings and validation-lists.

        ## Store names of standard customer-properties. For differentiating from COPs.
        ## CustomerList-cache is filled in the same go.
        $this.CustomerValidation = ($this.customerlist($true)[0] | 
                                    get-member | 
                                    where-object {$_.membertype -eq "noteproperty"} ).name

    }

    hidden [String]PlainUser(){
        $CredUser = $this.Creds.GetNetworkCredential().UserName

        If ($CredUser -eq '_JWT'){
            Return $null
        }
        Else{
            Return $CredUser
        }
    }
    
    hidden [String]PlainPass(){
        Return $this.Creds.GetNetworkCredential().Password
    }

    [void]ErrorHandler(){
        $this.ErrorHandler($this.Error)
    }

    [void]ErrorHandler($ErrorObject){
    
        #Write-Host$ErrorObject.Exception|Format-List -Force
        #Write-Host ($ErrorObject.Exception.GetType().FullName)
# $global:ErrObj = $ErrorObject


# Write-Host ($ErrorObject.Exception.Message)
        Write-Host ($ErrorObject.ErrorDetails.Message)
        
# Known Errors List:
# Connection-error (https): There was an error downloading ..
# 1012 - Thrown when mandatory settings are not present in "settings".
# 2001 - Required parameter is null - Thrown when null values are entered as inputs.
# 2001 - Unsupported version - Thrown when a version not specified above is entered as input.
# 2001 - Thrown when a bad username-password combination is input, or no PSA integration has been set up.
# 2100 - Thrown when invalid MSP N-central credentials are input.
# 2100 - Thrown when MSP-N-central credentials with MFA are used.
# 3010 - Maximum number of users reached.
# 3012 - Specified email address is already assigned to another user.
# 3014 - Creation of a user for the root customer (CustomerID 1) is not permitted.
# 3014 - When adding a user, must not be an LDAP user.
# 3020 - Account is locked
# 3022 - Customer/Site already exists.
# 3026 - Customer name length has exceeded 120 characters.
# 4000 - SessionID not found or has expired.
# 5000 - An unexpected exception occurred.
# 5000 - Query failed.
# 5000 - javax.validation.ValidationException: Unable to validate UI session
# 9910 - Service Organization already exists.
#
        
        Break
    }

    ## API Requests
    hidden [Object]NCWebRequest([String]$APIMethod,[String]$APIData){

        Return $this.NCWebRequest($APIMethod,$APIData,'')
    }

    hidden [Object]NCWebRequest([String]$APIMethod,[String]$APIData,$Version){
        ## Basic NCentral SOAP-request, invoking Credentials.

        ## Optionally invoke version (specific requests)
        #version - Determines whether MSP N-Central or PSA credentials are to be used. In the case of PSA credentials the number indicates the type of PSA integration setup.
        # "0.0" indicates that MSP N-central credentials are to be used.
        # "1.0" indicates that a ConnectWise PSA integration is to be used.
        # "2.0" indicates that an Autotask PSA integration is to be used.
        # "3.0" indicates than a Tigerpaw PSA integration is to be used.
        $VersionKey = ''
        If($Version){
            $VersionKey = ("
                        <ei2:version>{0}</ei2:version>"
 -f $Version)
        }

        ## Build SoapRequest (Ending Here-String ("@) must always be left-lined.)
        $MySoapRequest =(@"
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:ei2="http://ei2.nobj.nable.com/">
    <soap:Header/>
    <soap:Body>
        <ei2:{0}>{4}
            <ei2:username>{1}</ei2:username>
            <ei2:password>{2}</ei2:password>{3}
        </ei2:{0}>
    </soap:Body>
</soap:Envelope>
"@
 -f $APIMethod, $this.PlainUser(), $this.PlainPass(), $APIData, $VersionKey)

        #Write-host $MySoapRequest ## Debug purposes.
        ## Set the Request-properties in a local Dictionary / Hash-table.
        $RequestProps = @{}
        $RequestProps.Method = "Post"
        $RequestProps.Uri = $this.BindingURL
        $RequestProps.TimeoutSec = $this.RequestTimeOut
        $RequestProps.body =  $MySoapRequest

        $FullReponse = $null
        Try{
                #$FullReponse = Invoke-RestMethod -Uri $this.bindingURL -body $MySoapRequest -Method POST
                $FullReponse = Invoke-RestMethod @RequestProps
            }
# Catch [System.Net.WebException]{
# Write-Host ([string]::Format("Error : {0}", $_.Exception.Message))
# $this.Error = $_
# $this.ErrorHandler()
# }
        Catch {
                Write-Host ([string]::Format("Error : {0}", $_.Exception.Message))
                $this.Error = $_
                $this.ErrorHandler()
            }
                            
        #$ReturnProperty = $$APIMethod + "Response"
        $ReturnClass = $FullReponse.envelope.body | Get-Member -MemberType Property
        $ReturnProperty = $ReturnClass[0].Name
                
        Return     $FullReponse.envelope.body.$ReturnProperty.return
    }

    hidden [Object]GetNCData([String]$APIMethod,[String]$Username,[String]$PassOrJWT,$KeyPairs){
        ## Overload for Backward compatibility only
        Return $this.GetNCData($APIMethod,$KeyPairs,'')
    }

    hidden [Object]GetNCData([String]$APIMethod,[Array]$KeyPairs){

        Return $this.GetNCData($APIMethod,$KeyPairs,'')
    }
        
    hidden [Object]GetNCData([String]$APIMethod,[Array]$KeyPairs,[String]$Version){

        ## Process Keys to Request-settings
        $MyKeys=""
        If ($KeyPairs){
            ForEach($KeyPair in $KeyPairs){ 
                ## KeyValue can be an array with multiple values
                $MyValues=""
                ForEach($KeyValue in $KeyPair.value){
                    $MyValues = $MyValues + ("
                <ei2:value>{0}</ei2:value>"
 -f $KeyValue)
                }

                $MyKeys = $MyKeys + ("
            <ei2:settings>
                <ei2:key>{0}</ei2:key>{1}
            </ei2:settings>"
 -f $KeyPair.Key, $MyValues)
            }
        }
        ## Invoke request
        Return $this.NCWebRequest($APIMethod, $MyKeys,$Version)
    }

    hidden [Object]GetNCDataOP([String]$APIMethod,[Array]$CustomerIDs,[Boolean]$ReverseOrder){
        ## Get OrganizationProperties for (optional) specified customerIDs
        ## Process Array
        $MyKeys=""
        ForEach($CustomerID in $CustomerIDs){ 
            $MyKeys = $MyKeys + ("
            <ei2:customerIds>{0}</ei2:customerIds>"
 -f $CustomerID)
        }
        ## Add mandatory options
        $MyKeys = $MyKeys + ("
            <ei2:reverseOrder>{0}</ei2:reverseOrder>"
 -f ($ReverseOrder.ToString()).ToLower())

        ## Invoke request
        Return $this.NCWebRequest($APIMethod, $MyKeys)
    }

    hidden [Object]GetNCDataDP([String]$APIMethod,[Array]$DeviceIDs,[Array]$DeviceNames,[Array]$FilterIDs,[Array]$FilterNames,[Boolean]$ReverseOrder){
        ## Get DeviceProperties for (optional) filtered devices
        ## Process Arrays
        $MyKeys=""

        If($DeviceIDs){
            ForEach($DeviceID in $DeviceIDs){ 
                $MyKeys = $MyKeys + ("
            <ei2:deviceIDs>{0}</ei2:deviceIDs>"
 -f $DeviceID)
            }
        }

        If($DeviceNames){
            ForEach($DeviceName in $DeviceNames){ 
                $MyKeys = $MyKeys + ("
            <ei2:deviceNames>{0}</ei2:deviceNames>"
 -f $DeviceName)
            }
        }

        If($FilterIDs){
            ForEach($FilterID in $FilterIDs){ 
                $MyKeys = $MyKeys + ("
            <ei2:filterIDs>{0}</ei2:filterIDs>"
 -f $FilterID)
            }
        }

        If($FilterNames){
            ForEach($FilterName in $FilterNames){ 
                $MyKeys = $MyKeys + ("
            <ei2:filterNames>{0}</ei2:filterNames>"
 -f $FilterName)
            }
        }

        $MyKeys = $MyKeys + ("
            <ei2:reverseOrder>{0}</ei2:reverseOrder>"
 -f ($ReverseOrder.ToString()).ToLower())

        ## Invoke request
        Return $this.NCWebRequest($APIMethod, $MyKeys)
    }

    hidden [Object]SetNCDataOP([String]$APIMethod,$OrganizationID,$OrganizationPropertyID,[String]$OrganizationPropertyValue){
        ## Set a single OrganizationProperty
        ## Process Arrays
        $MyKeys=("
            <ei2:organizationProperties>
                <ei2:customerId>{0}</ei2:customerId>
                <ei2:properties>
                    <ei2:propertyId>{1}</ei2:propertyId>
                    <ei2:value>{2}</ei2:value>
                </ei2:properties>
            </ei2:organizationProperties>"
 -f $OrganizationID,$OrganizationPropertyID,$OrganizationPropertyValue)

        ## Invoke request
        Return $this.NCWebRequest($APIMethod, $MyKeys)
    }

    hidden [Object]SetNCDataDP([String]$APIMethod,$DeviceID,$DevicePropertyID,[String]$DevicePropertyValue){
        ## Set a single DeviceProperty
        ## Process Arrays
        $MyKeys=("
            <ei2:deviceProperties>
                <ei2:deviceID>{0}</ei2:deviceID>
                <ei2:properties>
                    <ei2:devicePropertyID>{1}</ei2:devicePropertyID>
                    <ei2:value>{2}</ei2:value>
                </ei2:properties>
            </ei2:deviceProperties>"
 -f $DeviceID, $DevicePropertyID, $DevicePropertyValue)

        ## Invoke request
        Return $this.NCWebRequest($APIMethod, $MyKeys)
    }

    ## Data Management / Processing

    hidden[PSObject]ProcessData1([Array]$InArray){
        Return $this.ProcessData1($InArray,$false)
    }

    <#
    hidden[PSObject]ProcessData1([Array]$InArray,[String]$PairClass){
        Return $this.ProcessData1($InArray,$PairClass,$false)
    }
    #>


    hidden[PSObject]ProcessData1([Array]$InArray,[Boolean]$ShowProgress){
        ## Most Common PairClass is Info or Item.
        ## Fill if not specified.

        # Hard (Pre-)Fill / Default
        $PairClass = "info"

        ## Base on found Array-Properties if possible
        If($InArray.Count -gt 0){
            ## Only one property exists at this level. The name of this property specifies the DeviceClass.
            #$PairClass = ($InArray[0] | Get-member -MemberType Property).Name
            $PairClass = ($InArray[0] | Get-member -MemberType Property)[0].Name
        }
        
        Return $this.ProcessData1($InArray,$PairClass,$ShowProgress)
    }
    
    hidden[PSObject]ProcessData1([Array]$InArray,[String]$PairClass,[Boolean]$ShowProgress){
        
        ## Received Dataset KeyPairs 2 List/Columns
        $OutObjects = @()
        
        if ($InArray){

            $TotalObjects=$InArray.Count
            $CurrentObject = 0
            ## Process all items
            foreach ($InObject in $InArray) {

                If($ShowProgress){
                    $CurrentObject +=1
                    #Write-host ("Processing {0} of {1} devices." -f $CurrentObject, $TotalObjects)
                    $CompletedPercent = ($CurrentObject / $TotalObjects)*100
                    Write-Progress -Activity ("Processing {0} Objects."-f $TotalObjects) -Status ("{0:N1}% Complete:" -f $CompletedPercent)  -PercentComplete $CompletedPercent
                    #Start-Sleep -Milliseconds 250
                }

# $ThisObject = New-Object PSObject ## In this routine the object is created at start. Properties are added with values.
                $Props = @{}                                ## In this routine the object is created at the end. Properties from a list/Hashtable.

                ## Add a Reference-Column at Object-Level (for Custom Properties)
                If ($PairClass -eq "Properties"){
                    ## CustomerLink if Available
                    if(Get-Member -inputobject $InObject -name "CustomerID"){
# $ThisObject | Add-Member -MemberType NoteProperty -Name 'CustomerID' -Value $InObject.CustomerID -Force
                        $Props.add('CustomerID',$InObject.CustomerID)
                    }
                    
                    ## DeviceLink if Available
                    if(Get-Member -inputobject $InObject -name "DeviceID"){
# $ThisObject | Add-Member -MemberType NoteProperty -Name 'DeviceID' -Value $InObject.DeviceID -Force
                        $Props.add('DeviceID',$InObject.DeviceID)
                    }
                }

                ## Convert all (remaining) keypairs to Properties
                foreach ($item in $InObject.$PairClass) {

                    ## Cleanup the Key and/or Value before usage.
                    If ($PairClass -eq "Properties"){
                        $Header = $item.label
                    }
                    Else{
                        If($item.key.split(".")[0] -eq 'asset'){    ##Should use ProcessData2 (ToDo)
                            $Header = $item.key
                        }
                        Else{
                            $Header = $item.key.split(".")[1]
                        }
                    }

                    ## Ensure a Flat Value for now --> work to do?
                    If ($item.value -is [Array]){
                        #$DataValue = $item.Value[0]
                        $DataValue = $item.Value -join ","
                    }
                    Else{
                        $DataValue = $item.Value
                    }

                    ## Now add the Key/Value pairs.
# $ThisObject | Add-Member -MemberType NoteProperty -Name $Header -Value $DataValue -Force

                     # if a key is found that already exists in the hashtable
                    if ($Props.ContainsKey($Header)) {
                        # either overwrite the value 'Last-One-Wins'
                        # or do nothing 'First-One-Wins'
                        #if ($this.allowOverwrite) { $Props[$Header] = $DataValue }
                    }
                    else {
                        #$Props[$Header] = $DataValue
                        #$Props.add($Header,$DataValue)
                        $Props.$Header = $DataValue
                    }                    

                }

                $ThisObject = New-Object -TypeName PSObject -Property $Props    #Alternative option

                ## Add the Object to the list
                $OutObjects += $ThisObject
            }

            ## Convert Date-fields of root-object
            $OutObjects = $this.FixDates($OutObjects)

            If($ShowProgress){
                #Write-Progress -Activity ("Processed {0} Objects."-f $OutObjects.count) -Status "Ready"
                #Start-Sleep -Milliseconds 1000
                Write-Progress -Activity ("Processed {0} Objects."-f $OutObjects.count) -Status "Ready" -Completed
            }

        }

        #Write-host $OutObjects
        ## Return the list of Objects
        Return $OutObjects
    }

    hidden[PSObject]ProcessData2([Array]$InArray){

        Return $this.ProcessData2($InArray,$false)
    }

    hidden[PSObject]ProcessData2([Array]$InArray,[Boolean]$ShowProgress){
        ## Most Common PairClass is Info or Item.
        ## Fill if not specified.
        
        # Hard (Pre-)Fill
        $PairClass = "info"

        ## Base on found Array-Properties if possible
        If($InArray.Count -gt 0){
            $PairClasses = $InArray[0] | Get-member -MemberType Property
            $PairClass = $PairClasses[0].Name
        }

        Return $this.ProcessData2($InArray,$PairClass,$ShowProgress)
    }

    hidden[PSObject]ProcessData2([Array]$InArray,[String]$PairClass,[Boolean]$ShowProgress){

        ## Convert Received Dataset KeyPairs to a multi-level Object
        ## Key-structure: asset.service.caption.28
        ## service - Property or Sub-object
        ## caption - key
        ## ## - Service-item (sub-identifier)
        ##
        ## Each Asset in dataset is processed sepearately and added to the output.

        ## Output-List
        $OutObjects = @()
        
        ## Inputcheck - is there any data to process?
        If ($InArray){

            $TotalObjects=$InArray.Count
            $CurrentObject = 0
            ## Process all devices
            ForEach ($Object in $InArray){

                If($ShowProgress){
                    $CurrentObject +=1
                    #Write-host ("Processing {0} of {1} devices." -f $CurrentObject, $TotalObjects)
                    $CompletedPercent = ($CurrentObject / $TotalObjects)*100
                    Write-Progress -Activity ("Processing {0} Objects."-f $TotalObjects) -Status ("{0:N1}% Complete:" -f $CompletedPercent)  -PercentComplete $CompletedPercent
                    #Start-Sleep -Milliseconds 250
                }

                ## Get the DeviceId to repeat in every Object/Array-Property
                $CurrentDeviceID = ($Object.$PairClass | Where-Object {$_.key -eq 'asset.device.deviceid'}).value
                #Write-Debug "DeviceObject CurrentDeviceID: $CurrentDeviceID"
                
                ## Sort keys for before processing. Column 2 and 4
                $SortedInfo = $Object.$PairClass | Sort-Object @{Expression={$_.key.split(".")[1] + $_.key.split(".")[3]}; Descending=$false}

                ## Init
                $Props = @{}        ## In this routine the object is created at the end. Properties from this list.

                ## For processing properties and additional identifiers (column4)
                $OldArrayID = ""
                [Array]$ArrayProperty = $null
                $OldArrayItemID = ""
                [HashTable]$ArrayItemProperty = $null


                ## Convert the keypairs to a Multi-Layer Object with Properties
                ForEach ($KeyPair in $SortedInfo) {

                    ## Key-structure: asset.service.caption.28
                    ## Outer-loop differenting on column 2 MainObject Array-Property
                    ## Inner-loop differenting on column 4
                    ## ObjectItem is column 2.4 (easysplit) Array-ItemID
                    ## ObjectHeaders are Column 3 Array-Item-PropertyHeader
                    ## ObjectValue = Value
                        
                    ## Treat 'device' as Root
                    ## Add Sub-objects for other headers
                    ## Add deviceid to each sub-object for easy reference
                    ## Add property direct to sub-object if column4 does not exist
                    ## Build and Add Array if column4 is an int

                    $KeySplit = $KeyPair.key.split(".")
                    $KeyValue = $KeyPair.value

                    If(($KeySplit[1]) -eq 'device'){
                        ## Add property as a Non-Array.
                        $Header = $KeySplit[2]

                        ## Ensure a Flat Value for now --> work to do?
                        If ($KeyValue -is [Array]){
                            $Props.$Header = $KeyValue -join ","
                        }
                        Else{
                            $Props.$Header = $KeyValue
                        }
                        
                    }
                    Else{
                        ## Add property as an (Array of) Object(s).
                        ## Make an object-Array Before Adding
                                                
                        ## Create the Property ItemID from the Key-Name
                        If($KeySplit[3]){
                            $ArrayItemId = ("{0}.{1}" -f $KeySplit[1], $KeySplit[3])
                        }
                        Else{
                            $ArrayItemId = $KeySplit[1]
                        }

                        ## Is this a new Array-Item?
                        If($ArrayItemId -ne $OldArrayItemID){
                            ## Add the current object to the array-property and start over
                            If($OldArrayItemID -ne ""){
                                $ArrayItem = New-Object -TypeName PSObject -Property $ArrayItemProperty
                                $ArrayProperty += $ArrayItem
                            }                            
                            ## (Re-)Init
                            $ArrayItemProperty = @{}
                            $OldArrayItemID = $ArrayItemId
                            ## Add an unique ID-Column and the DeviceID to the item.
                            $ArrayItemProperty.ItemId=$ArrayItemId
                            $ArrayItemProperty.DeviceId=$CurrentDeviceID
                        }

                        ## Create the Main Property Name from the Key-Name
                        $ArrayId = $KeySplit[1]
                        ## Is this a new Array?
                        If($ArrayId -ne $OldArrayID){
                            ## Add the current array to the main object and start a new one
                            If($OldArrayID -ne ""){
                                #Write-Debug "ArrayId = $ArrayId"
                                $Props.$OldArrayId = $ArrayProperty
                            }
                            ## (Re-)Init
                            $ArrayProperty = $null
                            $OldArrayID = $ArrayId
                        }

                        ## Add the current item to the array-item
                        $Header2 = $KeySplit[2]
                        $ArrayItemProperty.$Header2=$KeyValue

                    }
                
                ## End of Keypairs-loop
                }

                ## Build object for last Array
                If($ArrayItemProperty){
                    $ArrayItem = New-Object -TypeName PSObject -Property $ArrayItemProperty
                    $ArrayProperty += $ArrayItem
                }

                ## Add the last build array to the main Object
                If($ArrayProperty){
                    $Props.$OldArrayId = $ArrayProperty
                }

                ## Create the Multi-layer Object, using the generated properties.
                $ThisObject = New-Object -TypeName PSObject -Property $Props
                ## Add the Multi-layer Object to the Output-list
                $OutObjects += $ThisObject

            ## End of Objects-loop - Get Next
            }

            ## Convert Date-fields of root-object
            $OutObjects = $this.FixDates($OutObjects)

            If($ShowProgress){
                Write-Progress -Activity ("Processed {0} Objects."-f $OutObjects.count) -Status "Ready" -Completed
                #Start-Sleep -Milliseconds 250
            }

        ## End of Input-check
        }

        ## Return the list of Objects
        Return $OutObjects
    }

    [PSObject]IsEncodedBase64([string]$InputString){
        ## UniCode by default
        Return $this.IsEncodedBase64($InputString,$false)
    }

    [PSObject]IsEncodedBase64([string]$InputString,[Boolean]$UTF8){

        #[OutputType([Boolean])]
        $DataIsEncoded = $true
    
            Try{
                ## Try Decode
                If($UTF8){
                    [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($InputString)) | Out-Null
                }
                Else{
                    [System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String($InputString)) | Out-Null
                }
            }
            Catch{
                ## Data was not encoded yet
                $DataIsEncoded = $false
            }
    
        Return $DataIsEncoded
    }

    [PSObject]ConvertBase64([String]$Data){
        ## Encode and Unicode as default
        Return $this.ConvertBase64($Data,$false,$false)
    }

    [PSObject]ConvertBase64([String]$Data,[Bool]$Decode){
        ## Unicode as default
        Return $this.ConvertBase64($Data,$Decode,$false)
    }

    [PSObject]ConvertBase64([String]$Data,[Bool]$Decode,[Bool]$UTF8){
    
        ## Init
        [string]$ReturnData = $Data
        $DataIsEncrypted = $true            
    
        If($Data){
            ## Test content to avoid double-encoding.
            ## Still needs some work for false positives. Now checks for valid code-length mainly.
            ## Encoded without Byte Order Mark (BOM). Makes recognition difficult.
            Try{
                ## Try Decode
                If($UTF8){
                    $ReturnData = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Data))
                }
                Else{
                    $ReturnData = [System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String($Data))
                }
            }
            Catch{
                ## Data was not valid encoded yet
                $DataIsEncrypted = $false
            }

            ## If data should not be decrypted.
            If (!$Decode){
                If ($DataIsEncrypted){
                    ## Return Already Encrypted Data
                    $ReturnData = $Data
                }
                Else{
                    ## Return Newly Encrypted Data
                    If($UTF8){
                        $Bytes = [System.Text.Encoding]::UTF8.GetBytes($Data)
                    }
                    Else {
                        $Bytes = [System.Text.Encoding]::Unicode.GetBytes($Data)
                    }
                    $Returndata = [System.Convert]::ToBase64String($Bytes)
                }
            }
        }

        Return $Returndata
    }
    
    [PSObject]FixProperties([Array]$ObjectArray){
        ## Unifies the properties for all Objects.
        ## Solves Format-Table and Export-Csv issues not showing all properties.

        $ReturnData = $ObjectArray

        If ($ReturnData){
    
            [System.Collections.ArrayList]$AllColumns=@()
            # Walk through all Objects for Property-names
            $counter = $Returndata.length
            for ($i=0; $i -lt $counter ; $i ++){
                # Get the Property-names
                $Names = ($ReturnData[$i] |Get-Member -type Noteproperty -ErrorAction SilentlyContinue).name

                # Add New or Replace Existing
                $counter2 = $names.count
                for ($j=0; $j -lt $counter2 ; $j ++){
                    $AllColumns += $names[$j]
                }
                # Only unique ColumnNames allowed
                $AllColumns = $AllColumns | Sort-Object -Unique
            }

            If($AllColumns){
                ## Deal with long-names containing spaces. (Custom Properties mainly)
                [String]$ColumnString = $AllColumns -join "," 
                $ReturnData = $ReturnData | Select-Object $ColumnString.split(",")
            }
        }

        Return $ReturnData
    }

    [PSObject]FixDates([Array]$ObjectArray){
        ## Coverts date-strings to Date-properties

        $ReturnData = $ObjectArray

        ## Convert all Time-fields to real dates if possible.
        [System.Collections.ArrayList]$TimeFields = @()
        try{$TimeFields.AddRange(($ReturnData[0]|Get-Member -type Noteproperty -ErrorAction SilentlyContinue).name -like "*Time")}catch{}
        try{$TimeFields.AddRange(($ReturnData[0]|Get-Member -type Noteproperty -ErrorAction SilentlyContinue).name -like "*Date")}catch{}
        try{$TimeFields.AddRange(($ReturnData[0]|Get-Member -type Noteproperty -ErrorAction SilentlyContinue).name -like "*Don")}catch{}
        #$TimeFields.Add("createdon")
    
        #Write-Host $TimeFields ## Debug
        If($TimeFields){
            ForEach ($TimeField in $TimeFields){
                ForEach ($Object in $ReturnData){
                    try {
                        $Object.$TimeField = [datetime]$Object.$TimeField
                    }
                    catch {
                        ## Nothing on error
                    }
                }
            }
        }

        Return $ReturnData
    }


    #EndRegion
        
    #Region CustomerData
    [Object]ActiveIssuesList([Int]$ParentID){
        # No SearchBy-string adds an empty String.
        return $this.ActiveIssuesList($ParentID,"",0)
    }
    
    [Object]ActiveIssuesList([Int]$ParentID,[String]$IssueSearchBy){
        # No SearchBy-string adds an empty String.
        return $this.ActiveIssuesList($ParentID,$IssueSearchBy,0)
    }

    [Object]ActiveIssuesList([Int]$ParentID,[String]$IssueSearchBy,[Int]$IssueStatus){
        ## Refresh / Clean KeyPair-container.
        $this.KeyPairs = @()

        ## Add parameters as KeyPairs.
        $KeyPair1 = [PSObject]@{Key='customerID'; Value=$ParentID;}
        $this.KeyPairs += $KeyPair1

        ## Optional keypair(s) for activeIssuesList. ToDo: Create ENums for choices.

        ## SearchBy
        ## A string-value to search the: so, site, device, deviceClass, service, transitionTime,
        ## notification, features, deviceID, and ip address.
        If ($IssueSearchBy){
            $KeyPair2 = [PSObject]@{Key='searchBy'; Value=$IssueSearchBy;}
            $this.KeyPairs += $KeyPair2
        }
        
        ## OrderBy
        ## Valid inputs are: customername, devicename, servicename, status, transitiontime,numberofacknoledgednotification,
        ## serviceorganization, deviceclass, licensemode, and endpointsecurity.
        ## Default is customername.
# $IssueOrderBy = "transitiontime"
# $KeyPair3 = [PSObject]@{Key='orderBy'; Value=$IssueOrderBy;}
# $this.KeyPairs += $KeyPair3

        ## ReverseOrder
        ## Must be true or false. Default is false.
# $IssueOrderReverse = "true"
# $KeyPair4 = [PSObject]@{Key='reverseorder'; Value=$IssueOrderReverse;}
# $this.KeyPairs += $KeyPair4

        ## Status
        ## Only 1 (last) statusfilter will be applied (if multiple are used in the API).

        $IssueStatusFilter=''
        $IssueAcknowledged = ''
    
        ## Valid inputs are: failed, stale, normal, warning, no data, misconfigured, disconnected
        If($IssueStatus -in (1..7)){
            $IssueStatusFilter=($this.NCStatus.$IssueStatus).ToLower()
        }

        ## Valid inputs are: "Acknowledged" or "Unacknowledged"
        If($IssueStatus -in (11,12)){
            $IssueAcknowledged = $this.NCStatus.$IssueStatus
        }


        ## NOC_View_Status_Filter Reflected in NotifState
        ## Valid inputs are: failed, stale, normal, warning, no data, misconfigured, disconnected
        ## 'normal' does not return any data.
        If ($IssueStatusFilter){
            $KeyPair5 = [PSObject]@{Key='NOC_View_Status_Filter'; Value=$IssueStatusFilter;}
            $this.KeyPairs += $KeyPair5
        }

        ## NOC_View_Notification_Acknowledgement_Filter. Reflected in numberofactivenotification, numberofacknowledgednotification
        ## Valid inputs are: "Acknowledged" or "Unacknowledged"
        If ($IssueAcknowledged){
            $KeyPair6 = [PSObject]@{Key='NOC_View_Notification_Acknowledgement_Filter'; Value=$IssueAcknowledged;}
            $this.KeyPairs += $KeyPair6
        }

        $this.rc = $null

        ## KeyPairs is mandatory in this query. returns limited list
        Try{
# $this.rc = $this.Connection.activeIssuesList($this.PlainUser(), $this.PlainPass(), $this.KeyPairs)
            $this.rc = $this.GetNCData('activeIssuesList', $this.KeyPairs)
        }
        Catch {
            $this.Error = $_
            $this.ErrorHandler()
        }

        ## Needs 'issue' iso 'items' for ReturnObjects
# Return $this.ProcessData1($this.rc, "issue")
        Return $this.ProcessData1($this.rc)
    }

    [Object]JobStatusList([Int]$ParentID){
        ## Uses CustomerID. Reports ONLY Scripting-tasks now (not AMP or discovery).

        ## Refresh / Clean KeyPair-container.
        $this.KeyPairs = @()

        ## Add parameters as KeyPairs.
        $KeyPair1 = [PSObject]@{Key='customerID'; Value=$ParentID;}
        $this.KeyPairs += $KeyPair1

        $this.rc = $null

        Try{
# $this.rc = $this.Connection.jobStatusList($this.PlainUser(), $this.PlainPass(), $this.KeyPairs)
            $this.rc = $this.GetNCData('jobStatusList', $this.KeyPairs)
        }
        Catch {
            $this.Error = $_
            $this.ErrorHandler()
        }

        Return $this.ProcessData1($this.rc)
    }
    
    [Object]CustomerList(){
    
        Return $this.CustomerList($false)
    }
    
    [Object]CustomerList([Boolean]$SOList){
        ## Refresh / Clean KeyPair-container.
        $this.KeyPairs = @()

        If($SOList){
            $KeyPair1 = [PSObject]@{Key='listSOs'; Value='true';}
            $this.KeyPairs += $KeyPair1
        }

        $this.rc = $null

        ## KeyPairs Array must exist, but is not used in this query.
        Try{
# $this.rc = $this.Connection.customerList($this.PlainUser(), $this.PlainPass(), $this.KeyPairs)
            $this.rc = $this.GetNCData('customerList', $this.KeyPairs)
        }
        Catch {
            $this.Error = $_
            $this.ErrorHandler()
        }

# Return $this.ProcessData1($this.rc, "items")
        Return $this.ProcessData1($this.rc)
    }

    [Object]CustomerListChildren([Int]$ParentID){
        ## Refresh / Clean KeyPair-container.
        $this.KeyPairs = @()

        ## Add parameters as KeyPairs.
        $KeyPair1 = [PSObject]@{Key='customerID'; Value=$ParentID;}
        $this.KeyPairs += $KeyPair1

        $this.rc = $null

        ## KeyPairs is mandatory in this query. returns limited list
        Try{
# $this.rc = $this.Connection.customerListChildren($this.PlainUser(), $this.PlainPass(), $this.KeyPairs)
            $this.rc = $this.GetNCData('customerListChildren', $this.KeyPairs)
        }
        Catch {
            $this.Error = $_
            $this.ErrorHandler()
        }

# Return $this.ProcessData1($this.rc, "items")
        Return $this.ProcessData1($this.rc)
    }

    [Object]CustomerPropertyValue([Int]$CustomerID,[String]$PropertyName){

        ## Data-caching for faster future-access / lookup.
        If(!$this.CustomerData -Or $this.CustomerDataModified){
            #$this.CustomerData = $this.customerlist() | Select-Object customerid,customername,parentid
            $this.CustomerData = $this.customerlist() | Select-Object customerid,customername,parentid,* -ErrorAction SilentlyContinue
        }

        ## Retrieve value from cache
        $Returndata = ($this.CustomerData).where({ $_.customerID -eq $CustomerID }).$PropertyName

        Return $ReturnData

    }

    [Int]CustomerAdd([String]$CustomerName,[Int]$ParentID){
        Return $this.CustomerAdd($CustomerName,$ParentID,@{})
    }

    [Int]CustomerAdd([String]$CustomerName,[Int]$ParentID,$CustomerDetails){

        ## Refresh / Clean KeyPair-container.
        $this.KeyPairs = @()

        $KeyPair1 = [PSObject]@{Key='customername'; Value=$CustomerName;}
        $this.KeyPairs += $KeyPair1

        $KeyPair2 = [PSObject]@{Key='parentid'; Value=$ParentID;}
        $this.KeyPairs += $KeyPair2

        ## Only basic properties are allowed others are skipped. Must be an ordered-/hash-list.
        # Check/build list of Basic properties first
        if (!$this.CustomerValidation){
            $this.CustomerValidation = ($this.customerlist($true) | get-member | where-object {$_.membertype -eq "noteproperty"} ).name
        }

        If($CustomerDetails){
            If ($CustomerDetails -is [System.Collections.IDictionary]){
                ForEach($key in $CustomerDetails.keys){
                    If ($this.CustomerValidation -contains $key){
                        ## This is a standard CustomerProperty.
                        #Write-host ("Adding {1} to {0}." -f $key, $CustomerDetails[$key])
                        $KeyPair = [PSObject]@{Key=$key; Value=$CustomerDetails[$key];}
                        $this.KeyPairs += $KeyPair
                    }    
                }
            }Else{
                Write-Host "The customer-details must be given in a Hash or Ordered list."
            }
        }

        $this.rc = $null
        Try{
            ## Default GetNCData can be used for API-request
            $this.rc = $this.GetNCData('customerAdd', $this.KeyPairs)
        }
        Catch {
            $this.Error = $_
            $this.ErrorHandler()
        }

        ## No dataprocessing needed. Return New customerID
        Return $this.rc[0]
    }

    [void]CustomerModify([Int]$CustomerID,[String]$PropertyName,[String]$PropertyValue){
        ## Basic Customer-properties in KeyPairs

        if (!$this.CustomerValidation){
            $this.CustomerValidation = ($this.customerlist($true) | get-member | where-object {$_.membertype -eq "noteproperty"} ).name
        }

        ## Validate $PropertyName
        If(!($this.CustomerValidation -contains $PropertyName)){
            Write-Host "Invalid customer field: $PropertyName."
            Break
        }

        #Mandatory (Key) customerid - (Value) the (customer) id of the ID of the existing service organization/customer/site being modified.
        #Mandatory (Key) customername - (Value) Desired name for the new customer or site. Maximum of 120 characters.
        #Mandatory (Key) parentid - (Value) the (customer) id of the parent service organization or parent customer for the new customer/site.
        
        ## Lookup Data from cache for mandatory fields related to the $CustomerID.
        $CustomerName = $this.CustomerPropertyValue($CustomerID,"CustomerName")
        $ParentID = $this.CustomerPropertyValue($CustomerID,"ParentID")

        ## For an Invalid CustomerID, No additional lookup-data is found.
        If(!$ParentID){
            Write-Host "Unknown CustomerID: $CustomerID."
            Break
        }

        ## Refresh / Clean KeyPair-container.
        $this.KeyPairs = @()

        ## Add Mandatory parameters as KeyPairs.
        $KeyPair1 = [PSObject]@{Key='customerid'; Value=$CustomerID;}
        $this.KeyPairs += $KeyPair1
        
        $KeyPair2 = [PSObject]@{Key="customername"; Value=$CustomerName;}
        $this.KeyPairs += $KeyPair2
        
        $KeyPair3 = [PSObject]@{Key="parentid"; Value=$ParentID;}
        $this.KeyPairs += $KeyPair3

        ## PropertyName already validated at CmdLet.
        $KeyPair4 = [PSObject]@{Key=$PropertyName; Value=$PropertyValue;}
        $this.KeyPairs += $KeyPair4

        ## Using as [void]: No returndata needed/used.
        Try{
# $this.Connection.CustomerModify($this.PlainUser(), $this.PlainPass(), $this.KeyPairs)
            ## Standard GetNCData can be used here.
            $this.GetNCData('customerModify', $this.KeyPairs)
        }
        Catch {
            $this.Error = $_
            $this.ErrorHandler()
        }

        ## CustomerData-cache rebuild initiation.
        $this.CustomerDataModified = $true

    }

    [Object]OrganizationPropertyList(){
        # No FilterArray-parameter adds an empty ParentIDs-Array. Returns all customers
        return $this.OrganizationPropertyList(@())
    }
    
    [Object]OrganizationPropertyList([Array]$ParentIDs){
        # Returns all Custom Customer-Properties and values.

        $this.rc = $null
        Try{
# $this.rc = $this.Connection.organizationPropertyList($this.PlainUser(), $this.PlainPass(), $ParentIDs, $false)
            $this.rc = $this.GetNCDataOP('organizationPropertyList', $ParentIDs, $false)
        }
        Catch {
            $this.Error = $_
            $this.ErrorHandler()
        }

        Return $this.ProcessData1($this.rc, "properties",$false)
    }

    [Int]OrganizationPropertyID([Int]$OrganizationID,[String]$PropertyName){
        ## Search the DevicePropertyID by Name/label (Case InSensitive).
        ## Returns 0 (zero) if not found.
        $OrganizationPropertyID = 0
        
        $this.rc = $null
        $OrganizationProperties = $null
        Try{
            ## Retrieve a list of the properties for the given OrganizationID
# $OrganizationProperties = $this.Connection.OrganizationPropertyList($this.PlainUser(), $this.PlainPass(), $OrganizationID, $false)
            $OrganizationProperties = $this.GetNCDataOP('organizationPropertyList', $OrganizationID, $false)
# $OrganizationProperties = $this.OrganizationPropertyList($OrganizationID)
        }
        Catch {
            $this.Error = $_
            $this.ErrorHandler()
        }
    
        ForEach ($OrganizationProperty in $OrganizationProperties.properties){
            ## Case InSensitive compare.
            If($OrganizationProperty.label -eq $PropertyName){
                $OrganizationPropertyID = $OrganizationProperty.PropertyID
            }
        }        
        
        Return $OrganizationPropertyID
    }

    [void]OrganizationPropertyModify([Int]$OrganizationID,[String]$OrganizationPropertyName,[String]$OrganizationPropertyValue){
    
        ## Find the propertID by name first.
        [Int]$OrganizationPropertyID = $this.OrganizationPropertyID($OrganizationID,$OrganizationPropertyName)
        If ($OrganizationPropertyID -gt 0){
            [void]$this.OrganizationPropertyModify($OrganizationID,$OrganizationPropertyID,$OrganizationPropertyValue)
        }
        Else{
            ## Throw Error
            Write-Host "OrganizationProperty '$OrganizationPropertyName' not found on this Customer."
            Break
        }
    }        
        
    [void]OrganizationPropertyModify([Int]$OrganizationID,[Int]$OrganizationPropertyID,[String]$OrganizationPropertyValue){

        #$OrganizationProperty = [PSObject]@{PropertyID=$OrganizationPropertyID; value=$OrganizationPropertyValue; PropertyIDSpecified='True';}
# $Organization = [PSObject]@{OrganizationID=$OrganizationID; properties=$OrganizationProperty; OrganizationIDSpecified='True';}
        #$OrganizationPropertyArray = [PSObject]@{CustomerID=$OrganizationID; properties=$OrganizationProperty; CustomerIDSpecified='True';}
        
    
        ## Organization-layout:
        # $Organization = [PSObject]@{CustomerID=''; properties=''; CustomerIDSpecified='True';}
        # $Organization = New-Object -TypeName ($this.NameSpace + '.organizationProperties')
        ## properties hold an array of DeviceProperties

        ## Individual OrganizationProperty layout:
        # $OrganizationProperty = [PSObject]@{PropertyID=''; value=''; PropertyIDSpecified='True';}
        # $OrganizationProperty = New-Object -TypeName ($this.NameSpace + '.organizationProperty')

# If ($OrganizationPropertyArray){
            Try{
# $this.Connection.OrganizationPropertyModify($this.PlainUser(), $this.PlainPass(), $OrganizationPropertyArray)
                $this.SetNCDataOP('organizationPropertyModify',$OrganizationID,$OrganizationPropertyID,$OrganizationPropertyValue)
            }
            Catch {
                $this.Error = $_
                $this.ErrorHandler()
            }
# }
# Else{
# Write-Host "INFO:OrganizationPropertyModify - Nothing to save"
# }
        
    }
    
    #EndRegion

    #Region DeviceData
    [Object]DeviceList([Int]$ParentID){
        ## Use default Settings for DeviceList
        Return $this.Devicelist($ParentID,$true,$false)
    }
    
    [Object]DeviceList([Int]$ParentID,[Bool]$Devices,[Bool]$Probes){
        ## Returns only Managed/Imported Items.

        ## Refresh / Clean KeyPair-container.
        $this.KeyPairs = @()

        ## Add parameters as KeyPairs. Need to be unique Objects.
        $KeyPair1 = [PSObject]@{Key='customerID'; Value=$ParentID;}
        $this.KeyPairs += $KeyPair1

        $KeyPair2 = [PSObject]@{Key='devices'; Value=$Devices;}
        $this.KeyPairs += $KeyPair2

        $KeyPair3 = [PSObject]@{Key='probes'; Value=$Probes;}
        $this.KeyPairs += $KeyPair3

        $this.rc = $null
        Try{
# $this.rc = $this.Connection.deviceList($this.PlainUser(), $this.PlainPass(), $this.KeyPairs)
            $this.rc = $this.GetNCData('deviceList', $this.KeyPairs)
        }
        Catch{
            $this.Error = $_
            $this.ErrorHandler()
        }
        
# Return $this.ProcessData1($this.rc, "info")
        Return $this.ProcessData1($this.rc)
    }

    [Object]DeviceGet([Array]$DeviceIDs){
        Return $this.DeviceGet($DeviceIDs,$false)
    }

    [Object]DeviceGet([Array]$DeviceIDs,[Bool]$ShowProgress){
            ## Refresh / Clean KeyPair-container.
        
        $this.KeyPairs = @()
        foreach($DeviceID in $deviceIDs){
            ## Add multiple IDs as KeyPairs.
            #Write-Host "Adding key for $DeviceID"
            $KeyPair1 = [PSObject]@{Key='deviceID'; Value=$DeviceID;}
            $this.KeyPairs += $KeyPair1
        }

        If($ShowProgress){
            Write-Progress -Activity ("Retrieving object data from {0}." -f $this.ConnectionURL)
            #Start-Sleep -Milliseconds 500
        }

        $this.rc = $null
        Try{
# $this.rc = $this.Connection.deviceGet($this.PlainUser(), $this.PlainPass(), $this.KeyPairs)
            $this.rc = $this.GetNCData('deviceGet', $this.KeyPairs)
        }
        Catch {
            $this.Error = $_
            $this.ErrorHandler()
        }
        
        Return $this.ProcessData1($this.rc,$ShowProgress)
    }

    [Object]DeviceGetAppliance([int]$ApplianceID){
        ## Refresh / Clean KeyPair-container.
        $this.KeyPairs = @()

        ## Add parameter as KeyPair.
        $KeyPair1 = [PSObject]@{Key='applianceID'; Value=$ApplianceID;}
        $this.KeyPairs += $KeyPair1

        $this.rc = $null
        Try{
# $this.rc = $this.Connection.deviceGet($this.PlainUser(), $this.PlainPass(), $this.KeyPairs)
            $this.rc = $this.GetNCData('deviceGet', $this.KeyPairs)
        }
        Catch {
            $this.Error = $_
            $this.ErrorHandler()
        }
        
        Return $this.ProcessData1($this.rc)
    }
        
    [Object]DeviceGetStatus([Int]$DeviceID){
        ## Refresh / Clean KeyPair-container.
        $this.KeyPairs = @()

        ## Add parameters as KeyPairs.
        $KeyPair1 = [PSObject]@{Key='deviceID'; Value=$DeviceID;}
        $this.KeyPairs += $KeyPair1

        $this.rc = $null
        Try{
# $this.rc = $this.Connection.deviceGetStatus($this.PlainUser(), $this.PlainPass(), $this.KeyPairs)
            $this.rc = $this.GetNCData('deviceGetStatus', $this.KeyPairs)
        }
        Catch {
            $this.Error = $_
            $this.ErrorHandler()
        }
        
# Return $this.ProcessData1($this.rc, "info")
        Return $this.ProcessData1($this.rc)
    }

    [Object]DevicePropertyList([Array]$DeviceIDs,[Array]$DeviceNames,[Array]$FilterIDs,[Array]$FilterNames){
        ## Reports the Custom Device-Properties and values. Uses filter-arrays.
        ## Names are Case-sensitive.
        ## Returns both Managed and UnManaged Devices.

        $this.rc = $null

        Try{
# $this.rc = $this.Connection.devicePropertyList($this.PlainUser(), $this.PlainPass(), $DeviceIDs,$DeviceNames,$FilterIDs,$FilterNames,$false)
            $this.rc = $this.GetNCDataDP('devicePropertyList',$DeviceIDs,$DeviceNames,$FilterIDs,$FilterNames,$false)
        }
        Catch {
            $this.Error = $_
            $this.ErrorHandler()
        }

        Return $this.ProcessData1($this.rc, "properties", $false)
        #Return $this.rc
    }

    [Int]DevicePropertyID([Int]$DeviceID,[String]$PropertyName){
        ## Search the DevicePropertyID with Name-Filter (Case InSensitive).
        ## Returns 0 (zero) if not found.
        $DevicePropertyID = 0
        
        $DeviceProperties = $null
        Try{
# $DeviceProperties = $this.Connection.devicePropertyList($this.PlainUser(), $this.PlainPass(), $DeviceID,$null,$null,$null,$false)
            $DeviceProperties = $this.GetNCDataDP('devicePropertyList',$DeviceID,$null,$null,$null,$false)
        }
        Catch {
            $this.Error = $_
            $this.ErrorHandler()
        }
    
        ForEach ($DeviceProperty in $DeviceProperties.properties){
            ## Case InSensitive compare.
            If($DeviceProperty.label -eq $PropertyName){
                $DevicePropertyID = $DeviceProperty.devicePropertyID
            }
        }        
        
        Return $DevicePropertyID
    }

    [void]DevicePropertyModify([Int]$DeviceID,[String]$DevicePropertyName,[String]$DevicePropertyValue){
    
        [Int]$DevicePropertyID = $this.DevicePropertyID($DeviceID,$DevicePropertyName)
        If ($DevicePropertyID -gt 0){
            [void]$this.DevicePropertyModify($DeviceID,$DevicePropertyID,$DevicePropertyValue)
        }
        Else{
            ## Throw Error
            Write-Host "DeviceProperty '$DevicePropertyName' not found on this Device."
            Break
# $this.Error = "DeviceProperty '$DevicePropertyName' not found on this Device."
# $this.ErrorHandler()
        }

    }

    [void]DevicePropertyModify([Int]$DeviceID,[Int]$DevicePropertyID,[String]$DevicePropertyValue){

        ## Create a custom DevicePropertyArray. Details below.
# $DeviceProperty = [PSObject]@{devicePropertyID=$DevicePropertyID; value=$DevicePropertyValue; devicePropertyIDSpecified='True';}
# $DevicesPropertyArray = [PSObject]@{deviceID=$DeviceID; properties=$DeviceProperty; deviceIDSpecified='True';}
        
    
        ## Device-layout for WebProxy:
        # $Device = [PSObject]@{deviceID=''; properties=''; deviceIDSpecified='True';}
        # $Device = New-Object -TypeName ($this.NameSpace + '.deviceProperties')
        ## properties hold an array of DeviceProperties

        ## Individual DeviceProperty layout for WebProxy:
        # $DeviceProperty = [PSObject]@{devicePropertyID=''; value=''; devicePropertyIDSpecified='True';}
        # $DeviceProperty = New-Object -TypeName ($this.NameSpace + '.deviceProperty')

# If ($devicesPropertyArray){
# Try{
# $this.Connection.devicePropertyModify($this.PlainUser(), $this.PlainPass(), $devicesPropertyArray)
                $this.SetNCDataDP('devicePropertyModify',$DeviceID,$DevicePropertyID,$DevicePropertyValue)
# }
# Catch {
# $this.Error = $_
# $this.ErrorHandler()
# }
# }
# Else{
# Write-Host "INFO:DevicePropertyModify - Nothing to save"
# }
    }

    [Object]DeviceAssetInfoExportDevice(){
        ## Reports all details for Monitored Assets.
        ## !!! Potentially puts a high load on the NCentral-server!!!
        ## Removed/disabled in this Module.
        ## Only supporting 'DeviceAssetInfoExportDeviceWithSetting' for a single deviceID.
        
# ## Class: DeviceData
# ## deviceAssetInfoExport Deprecated
# ## deviceAssetInfoExportDevice Same as 'WithSettings' without specifying filters.
# ## deviceAssetInfoExportDeviceWithSettings
# ##
# ## Reports all Monitored Assets and Details. No filtering by CustomerID or DeviceID. Reports All Assets.
# ## Use without Header-formatting (has sub-headers). Device.customerid=siteid.
# ## Generating this list takes quite a long time. Might even time-out.
# #$rc = $nws.deviceAssetInfoExport2("0.0", $username, $password) #Error - nonexisting
# #$ri = $nws.deviceAssetInfoExport("0.0", $username, $password) #Error - unsupported version
# #$ri = $nws.deviceAssetInfoExportDevice("0.0", $username, $password)
# #$PairClass="info"

        $this.rc = $null
    
        Try{
# $this.rc = $this.Connection.deviceAssetInfoExportDevice("0.0", $this.PlainUser(), $this.PlainPass()) ## deprecated
# $this.rc = $this.GetNCData('deviceAssetInfoExportDevice','',"0.0") ## Disabled on purpose
            
        }
        Catch {
            $this.Error = $_
            $this.ErrorHandler()
        }
        
        Return $this.rc
# Return $this.ProcessData1($this.rc, "info")
    }

    [Object]DeviceAssetInfoExportDeviceWithSettings([Array]$DeviceIDs){
        ## Reports Monitored Assets.
        ## Calls Full Command with Parameters
        Return $this.DeviceAssetInfoExportDeviceWithSettings($DeviceIds,$null,$null,$null,$null,$null,$false)
    }

    [Object]DeviceAssetInfoExportDeviceWithSettings([Array]$DeviceIDs,[Array]$DeviceNames,[Array]$FilterIDs,[Array]$FilterNames,[Array]$Inclusions,[Array]$Exclusions,[Boolean]$ShowProgress){
        ## Reports Monitored Assets.
        ## Currently returns all categories for the selected devices. TODO: category-filtering.

# From Documentation:
# http://mothership.n-able.com/dms/javadoc_ei2/com/nable/nobj/ei2/ServerEI2_PortType.html
#
# Use only ONE of the following options to limit information to certain devices
# "TargetByDeviceID" - value for this key is an array of deviceids
# "TargetByDeviceName" - value for this key is an array of devicenames
# "TargetByFilterID" - value for this key is an array of filterids
# "TargetByFilterName" - value for this key is an array filternames

        $this.KeyPairs = @()

        $KeyPair1 = $null
        ## Add only one of the parameters as KeyPair. by priority.
        If ($DeviceIDs){
            $KeyPair1 = [PSObject]@{Key='TargetByDeviceID'; Value=$DeviceIDs;}
# ForEach($DeviceID in $DeviceIDs){
# $KeyPair1 = [PSObject]@{Key='TargetByDeviceID'; Value=$DeviceID;}
# $this.KeyPairs += $KeyPair1
# }

        }ElseIf($FilterIDs){
            $KeyPair1 = [PSObject]@{Key='TargetByFilterID'; Value=$FilterIDs;}
        }ElseIF($DeviceNames){
            $KeyPair1 = [PSObject]@{Key='TargetByDeviceName'; Value=$DeviceNames;}
        }ElseIf($FilterNames){
            $KeyPair1 = [PSObject]@{Key='TargetByFilterName'; Value=$FilterNames;}
        }

        ## Do not continue if no filter is specified.
        ## Due to potential heavy server load.
        If (!$KeyPair1){
            ## TODO: Throw Error
            Break
        }
        $this.KeyPairs += $KeyPair1

# ## Without Inclusion/Exclusion ALL categories will be returned.
# ## Documentation On Inclusion/Exclusion:
# ## Key = "InformationCategoriesInclusion" and Value = String[] {"asset.device", "asset.os"} then only information for these two categories will be returned.
# ## Key = "InformationCategoriesExclusion" and Value = String[] {"asset.device", "asset.os"}
# ## Work in Progress
#
# ## Use an ArrayList to allow addition or removal. Using [void] to suppress response (same as | $null at the end).
# [System.collections.ArrayList]$RequestFilter = @()
# [void]$RequestFilter.add("asset.application")
# [void]$RequestFilter.add("asset.device") # Always included (Root-item)
# [void]$RequestFilter.add("asset.device.ncentralassettag")
# [void]$RequestFilter.add("asset.logicaldevice")
# [void]$RequestFilter.add("asset.mappeddrive")
# [void]$RequestFilter.add("asset.mediaaccessdevice")
# [void]$RequestFilter.add("asset.memory")
# [void]$RequestFilter.add("asset.networkadapter")
# [void]$RequestFilter.add("asset.os")
# [void]$RequestFilter.add("asset.osfeatures")
# [void]$RequestFilter.add("asset.patch")
# [void]$RequestFilter.add("asset.physicaldrive")
# [void]$RequestFilter.add("asset.port")
# [void]$RequestFilter.add("asset.printer")
# [void]$RequestFilter.add("asset.raidcontroller")
# [void]$RequestFilter.add("asset.service")
# [void]$RequestFilter.add("asset.socustomer")
# [void]$RequestFilter.add("asset.usbdevice")
# [void]$RequestFilter.add("asset.videocontroller")
#

        ## [ToDo] Category-filtering is not working as documented
        $KeyPair2 = $null

        ## inclusion prevails
        If ($Inclusions){
            $this.RequestFilter.clear()
            ForEach($inclusion in $Inclusions.ToLower()){
                ## Allow for categorynames without prefix.
                If(($inclusion.split("."))[0] -like "asset"){
                    ## Prefix already present.
                    [void]$this.RequestFilter.add($inclusion)
                }
                Else{
                    ## Add with Prefix
                    [void]$this.RequestFilter.add("asset.{0}" -f $inclusion)
                }
            }
            $KeyPair2 = [PSObject]@{Key="InformationCategoriesInclusion"; Value=([Array]$this.RequestFilter);}
        }
        ElseIf ($Exclusions) {
            $this.RequestFilter.clear()
            ForEach($exclusion in $Exclusions.ToLower()){
                ## Allow for categorynames without prefix.
                If(($exclusion.split("."))[0] -like "asset"){
                    ## Prefix already present.
                    [void]$this.RequestFilter.add($exclusion)
                }
                Else{
                    ## Add with Prefix
                    [void]$this.RequestFilter.add("asset.{0}" -f $exclusion)
                }
            }
            $KeyPair2 = [PSObject]@{Key="InformationCategoriesExclusion"; Value=([Array]$this.RequestFilter);}
        }

        If ($KeyPair2){
            $this.KeyPairs += $KeyPair2
        }

        If($ShowProgress){
            Write-Progress -Activity ("Retrieving object data from {0}." -f $this.ConnectionURL)
            #Start-Sleep -Milliseconds 500
        }

        $this.rc = $null
        
        Try{
# $this.rc = $this.Connection.deviceAssetInfoExportDeviceWithSettings("0.0", $this.PlainUser(), $this.PlainPass(), $this.KeyPairs)
            #$this.rc = $this.Connection.deviceAssetInfoExport2("0.0", $this.PlainUser(), $this.PlainPass(), $this.KeyPairs)
            $this.rc = $this.GetNCData('deviceAssetInfoExportDeviceWithSettings',$this.KeyPairs,"0.0")

        }
        Catch {
            $this.Error = $_
            $this.ErrorHandler()
        }
        
        ## Todo: Parameter for what to return:
        ## Flat Object (ProcessData1) or
        ## Multi-Dimensional Object (ProcessData2).
# Return $this.ProcessData2($this.rc, "info")
        Return $this.ProcessData2($this.rc, $ShowProgress)
    }

    #EndRegion

    #Region NCentralAppData
        
# ## To Do
# ## TODO - User/Role/AccessGroup as user-object.
# ## TODO - Filter/Rule list (Not available through API yet)
    
    [Object]AccessGroupList([Int]$ParentID){
        ## List All Access Groups
        ## Mandatory valid CustomerID (SO/Customer/Site-level), does not seem to use it.

        ## Refresh / Clean KeyPair-container.
        $this.KeyPairs = @()

        ## Add parameters as KeyPairs.
        $KeyPair1 = [PSObject]@{Key='customerID'; Value=$ParentID;}
        $this.KeyPairs += $KeyPair1

        $this.rc = $null

        Try{
# $this.rc = $this.Connection.accessGroupList($this.PlainUser(), $this.PlainPass(), $this.KeyPairs)
            $this.rc = $this.GetNCData('accessGroupList', $this.KeyPairs)
        }
        Catch {
            #$this.ErrorHandler($_)
            $this.Error = $_
            $this.ErrorHandler()
        }
        Return $this.ProcessData1($this.rc)
        #Return $this.ProcessData1($this.rc.where{$_.customerid -eq $parentID})

    }

    [Object]AccessGroupGet([Int]$GroupID){
        ## Defaults to CustomerGroup
        Return $this.AccessGroupGet($GroupID,$null,$true)
    }

    [Object]AccessGroupGet([Int]$GroupID,[Boolean]$IsCustomerGroup){
        Return $this.AccessGroupGet($GroupID,$null,$IsCustomerGroup)
    }

    [Object]AccessGroupGet([Int]$GroupID,[Int]$ParentID,[Boolean]$IsCustomerGroup){
        ## List Access Groups details.
        ## Uses groupID and customerGroup. Gets details for the specified AccessGroup.
        ## Mandatory parameters:
        ## -GroupID Error: '1012 Mandatory settings not present'
        ## -CustomerGroup Error: '4100 Invalid parameters'
        ## Must be in Sync with GroupType
        ## The ParentID/customerID seems unused.

        If ($null -eq $IsCustomerGroup){
            $IsCustomerGroup = $true
        }

        ## Refresh / Clean KeyPair-container.
        $this.KeyPairs = @()

        ## Add parameters as KeyPairs.
        $KeyPair1 = [PSObject]@{Key='groupID'; Value=$GroupID;}
        $this.KeyPairs += $KeyPair1

        if($ParentID){
            $KeyPair2 = [PSObject]@{Key='customerID'; Value=$ParentID;}
            $this.KeyPairs += $KeyPair2
        }

        $KeyPair3 = [PSObject]@{Key='customerGroup'; Value=$IsCustomerGroup;}
        $this.KeyPairs += $KeyPair3

        $this.rc = $null

        Try{
# $this.rc = $this.Connection.accessGroupGet($this.PlainUser(), $this.PlainPass(), $this.KeyPairs)
            $this.rc = $this.GetNCData('accessGroupGet', $this.KeyPairs)
        }
        Catch {
            $this.Error = $_
            $this.ErrorHandler()
        }
    
        Return $this.ProcessData1($this.rc)
    }

    [Object]UserRoleList([Int]$ParentID){
        ## List All User Roles
        ## Mandatory valid CustomerID (SO/Customer/Site-level), does not seem to use it.

        ## Refresh / Clean KeyPair-container.
        $this.KeyPairs = @()

        ## Add parameters as KeyPairs.
        $KeyPair1 = [PSObject]@{Key='customerID'; Value=$ParentID;}
        $this.KeyPairs += $KeyPair1

        $this.rc = $null
        Try{
# $this.rc = $this.Connection.userRoleList($this.PlainUser(), $this.PlainPass(), $this.KeyPairs)
            $this.rc = $this.GetNCData('userRoleList', $this.KeyPairs)
        }
        Catch {
            $this.Error = $_
            $this.ErrorHandler()
        }

        Return $this.ProcessData1($this.rc)

    }

    [Object]UserRoleGet([Int]$UserRoleID){
        Return $this.UserRoleGet($UserRoleID,$null)
    }

    [Object]UserRoleGet([Int]$UserRoleID,[Int]$ParentID){
        ## List User Role details.

        ## Refresh / Clean KeyPair-container.
        $this.KeyPairs = @()

        ## Add parameters as KeyPairs.
        $KeyPair1 = [PSObject]@{Key='userRoleID'; Value=$UserRoleID;}
        $this.KeyPairs += $KeyPair1

        If($ParentID){
            $KeyPair2 = [PSObject]@{Key='customerID'; Value=$ParentID;}
            $this.KeyPairs += $KeyPair2
        }
        
        $this.rc = $null

        Try{
# $this.rc = $this.Connection.userRoleGet($this.PlainUser(), $this.PlainPass(), $this.KeyPairs)
            $this.rc = $this.GetNCData('userRoleGet', $this.KeyPairs)
        }
        Catch {
            $this.Error = $_
            $this.ErrorHandler()
        }
        
        Return $this.ProcessData1($this.rc)
    }

    #EndRegion

#EndRegion
}
## Class-section Ends here


#Region Generic Functions

Function Convert-Base64 {
    <#
    .Synopsis
    Encode or Decode a string to or from Base64.
     
    .Description
    Encode or Decode a string to or from Base64.
    Use Unicode (UTF16) by default, UTF8 is optional.
    Protected against double-encoding.
     
    #>


    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$false,        ## to create more descriptive error-message.
               Position = 0,
               HelpMessage = 'Data to process')]
            [String]$Data,

        [Parameter(Mandatory=$false,
               HelpMessage = 'Decode')]
            [switch]$Decode,
    
        [Parameter(Mandatory=$false,
                HelpMessage = 'Use UTF8')]
                [Alias("NoUnicode")]
            [switch]$UTF8
        )

    Begin{
        #check parameters. Use defaults if needed/available
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
        If (!$Data){
            Write-Host ("No data specfied for {0}." -f $MyInvocation.MyCommand.Name)
            Break
        }
    }
    Process{
    }
    End{
        Return [String]$NCsession.convertbase64($Data,$Decode,$UTF8)
    }
}

Function Format-Properties {
    <#
    .Synopsis
    Unifies the properties for all Objects in a list.
     
    .Description
    Unifies the properties for all Objects in a list.
    Solves Get-Member, Format-Table and Export-Csv
    issue for not showing all properties.
     
    #>


    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$false,        ## to create more descriptive error-message.
               #ValueFromPipeline = $true,
               Position = 0,
               HelpMessage = 'Array Containing PS-Objects')]
            [Array]$ObjectArray
        )

    Begin{
        #check parameters. Use defaults if needed/available
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
        If (!$ObjectArray){
            Write-Host ("No data specfied for {0}." -f $MyInvocation.MyCommand.Name)
            Break
        }
    }
    Process{
    }
    End{
        #Write-Output
        $NCsession.FixProperties($ObjectArray)
    }

}

#EndRegion

#EndRegion - Classes and Generic Functions

#Region PowerShell CmdLets
# ## To Do
# ## TODO - Error-handling at CmdLet Level.
# ## TODO - Add Examples to in-line documentation.
# ## TODO - Additional CmdLets (DataExport, PSA, CustomerObject, ...)
# ##

#Region Module-support
Function New-NCentralConnection{
<#
.Synopsis
Connect to the NCentral server.
 
.Description
Connect to the NCentral server.
Https is always used, since the data itself is unencrypted.
 
The returned connection-object allows to extract and manipulate
NCentral Data through methods of the NCentral_Connection Class.
 
To show available Commands, type:
Get-NCHelp
 
.Parameter ServerFQDN
Specify the Server DNS-name for this Connection.
The server needs to have a valid certficate for HTTPS.
 
.Parameter PSCredential
PowerShell-Credential object containing Username and
Password for N-Central access. No MFA.
 
.Parameter JWT
String Containing the JavaWebToken for N-Central access.
 
.Parameter DefaultCustomerID
Sets the default CustomerID for this instance.
The CustomerID can be found in the customerlist.
    CustomerID 1 Root / System
    CustomerID 50 First ServiceOrganization (Default)
 
.Example
$PSUserCredential = Get-Credential -Message "Enter NCentral API-User credentials"
New-NCentralConnection NCserver.domain.com $PSUserCredential
 
 
.Example
New-NCentralConnection -ServerFQDN <Server> -JWT <Java Web Token>
 
Use the line above inside a script for a fully-automated connection.
 
#>


    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$false)][String]$ServerFQDN,
        [Parameter(Mandatory=$false)][PSCredential]$PSCredential,
        [Parameter(Mandatory=$false)][String]$JWT,
        [Parameter(Mandatory=$false)][Int]$DefaultCustomerID = 50
    )
    Begin{
        ## Check parameters

        ## Clear the ServerFQDN if there is no . in it. Will create dialog.
        If ($ServerFQDN -notmatch "\.") {
            $ServerFQDN = $null
        }

    }
    Process{
        ## Store the session in a global variable as the default connection.

        # Initiate the connection with the given information.
        # Prompts for additional information if needed.
        If ($ServerFQDN){
            If ($PSCredential){
                #Write-Host "Using Credentials"
                $Global:_NCSession = [NCentral_Connection]::New($ServerFQDN, $PSCredential)
            }
            Elseif($JWT){
                #Write-Host "Using JWT"
                $Global:_NCSession = [NCentral_Connection]::New($ServerFQDN, $JWT)
            }
            Else {
                $Global:_NCSession = [NCentral_Connection]::New($ServerFQDN)
            }
        }
        Else {
            $Global:_NCSession = [NCentral_Connection]::New()
        }

        ## ToDo: Check for succesful connection.
        #Write-Host ("Connection to {0} is {1}." -f $Global:_NCSession.ConnectionURL,$Global:_NCSession.IsConnected)

        # Set the default CustomerID for this session.
        $Global:_NCSession.DefaultCustomerID = $DefaultCustomerID
    }
    End{
        ## Return the initiated Class
        #Write-Output
        $Global:_NCSession
    }
}

Function NcConnected{
    <#
    .Synopsis
    Checks or initiates the NCentral connection.
     
    .Description
    Checks or initiates the NCentral connection.
    Returns $true if a connection established.
     
    #>

        
    $NcConnected = $false
    
    If (!$Global:_NCSession){
# Write-Host "No connection to NCentral Server found.`r`nUsing 'New-NCentralConnection' to connect."
        New-NCentralConnection
    }

    ## Succesful connection?
    If ($Global:_NCSession){
        $NcConnected = $true
    }
    Else{
        Write-Host "No valid connection to NCentral Server."
    }
    
    Return $NcConnected
}
    
Function Get-NCHelp{
<#
.Synopsis
Shows a list of available PS-NCentral commands and the synopsis.
 
.Description
Shows a list of available PS-NCentral commands and the synopsis.
 
#>

    Get-Command -Module PS-NCentral | 
    #Where-Object {"IsEncodedBase64" -notmatch $_.name} |
    Select-Object Name |
    Get-Help | 
    Select-Object Name,Synopsis

    #"`n`n`rNCentral statuscodes"
    $_NCsession.ncstatus.getenumerator()|
    Select-object @{n="StatusCode";e={$_.Key}},
                @{n="Description";e={$_.Value}} |
    Sort-Object StatusCode | 
    Format-Table

}

Function Get-NCVersion{
    <#
    .Synopsis
    Returns the N-Central Version(s) of the connected server.
     
    .Description
    Returns the N-Central Version(s) of the connected server,
    and a list of status codes and descriptions.
     
    #>

    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$false,
                HelpMessage = 'Show API version info')]
                [Alias('FullVersionList','Full')]
        [Switch]$APIVersion,
        
        [Parameter(Mandatory=$false,
                HelpMessage = 'Show GUI version only')]
                [Alias('VersionOnly')]
        [Switch]$Plain,
        
        [Parameter(Mandatory=$false)]$NcSession
    )
    
    Begin{
        #check parameters. Use defaults if needed/available
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
    }
    Process{
        ## API-version info
        If ($APIVersion){
            #Write-Output
            $NcSession.ConnectedVersion | Format-table
        }

        ## Connection info
        If ($Plain){
            #Write-Output
            $NcSession.NCVersion
        }
        Else{
            #Write-Output
            $NcSession
        }
    }
    End{
    }
}

Function Get-NCTimeOut{
<#
.Synopsis
Returns the max. time in seconds to wait for data returning from a (Synchronous) NCentral API-request.
 
.Description
Shows the maximum time to wait for synchronous data-request. Dialog in seconds.
 
#>

    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$false,
                HelpMessage = 'Existing NCentral_Connection')]
        $NcSession
    )
    Begin{
            If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
    }
    Process{
# Write-Output ($NCSession.Connection.TimeOut/1000)
        #Write-Output ($NCSession.RequestTimeOut)
        $NCSession.RequestTimeOut
    }
    End{}
}

Function Set-NCTimeOut{
<#
.Synopsis
Sets the max. time in seconds to wait for data returning from a (Synchronous) NCentral API-request.
 
.Description
Sets the maximum time to wait for synchronous data-request. Time in seconds.
Range: 15-600. Default is 100.
 
#>

    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$false,
                HelpMessage = 'TimeOut for NCentral Requests in Seconds')]
        [Int]$TimeOut,

        [Parameter(Mandatory=$false,
                HelpMessage = 'Existing NCentral_Connection')]
        $NcSession
    )
    Begin{
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }

        ## Limit Range. Set to Default (100000) if too small or no value is given.
# $TimeOut = $TimeOut * 1000
        If ($TimeOut -lt 15){
            Write-Host "Minimum TimeOut is 15 Seconds. Is now reset to default; 100 seconds"
            $TimeOut = 100
        }
        If ($TimeOut -gt 600){
            Write-Host "Maximum TimeOut is 600 Seconds. Is now reset to Max; 600 seconds"
            $TimeOut = 600
        }
    }
    Process{
# $NCSession.Connection.TimeOut = ($TimeOut * 1000)
        $NCSession.RequestTimeOut = $TimeOut
# Write-Output ($NCSession.Connection.TimeOut)
        #Write-Output ($NCSession.RequestTimeOut)
        $NCSession.RequestTimeOut
    }
    End{}
}
#EndRegion

#Region Customers
Function Get-NCServiceOrganizationList{
<#
.Synopsis
Returns a list of all ServiceOrganizations and their data.
 
.Description
Returns a list of all ServiceOrganizations and their data.
 
#>

    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$false,
                HelpMessage = 'Existing NCentral_Connection')]
        $NcSession
    )
    
    Begin{
        #check parameters. Use defaults if needed/available
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
    }    
    Process{

    }
    End{
        #Write-Output
        $NcSession.CustomerList($true)
    }
}

Function Get-NCCustomerList{
<#
.Synopsis
Returns a list of all customers and their data. ChildrenOnly when CustomerID is specified.
 
.Description
Returns a list of all customers and their data.
ChildrenOnly when CustomerID is specified.
 
 
## TODO - Integrate Custom-properties
#>

    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$false,
# ValueFromPipeline = $true,
               ValueFromPipelineByPropertyName = $true,
               Position = 0,
               HelpMessage = 'Existing Customer ID')]
        ## Default-value is essential for output-selection.
        $CustomerID = 0,
        
        [Parameter(Mandatory=$false,
                HelpMessage = 'Existing NCentral_Connection')]
        $NcSession
    )
    
    Begin{
        #check parameters. Use defaults if needed/available
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
    }    
    Process{
        Write-Debug "CustomerID: $CustomerID"
        If ($CustomerID -eq 0){
            ## Return all Customers
# Write-Output $NcSession.CustomerList()
            $ReturnData = $NcSession.CustomerList()
        }
        Else{
            ## Return direct children only.
# Write-Output $NcSession.CustomerListChildren($CustomerID)
            $ReturnData =  $NcSession.CustomerListChildren($CustomerID)
        }
    }
    End{
        ## Alphabetical Columnnames
        $ReturnData | 
        Select-Object ($ReturnData|Get-Member -type Noteproperty -ErrorAction SilentlyContinue).name -ErrorAction SilentlyContinue |
        ## Put important fields in front.
        Select-Object customerid,customername,externalid,externalid2,* -ErrorAction SilentlyContinue 
        #| Write-Output
    }
}

Function Set-NCCustomerDefault{
    <#
    .Synopsis
    Sets the DefaultCustomerID to be used.
     
    .Description
    Sets the DefaultCustomerID to be used, when not supplied as parameter.
    Standard-value: 50 (First Service Organization created).
         
    #>

    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$false,
# ValueFromPipeline = $true,
                ValueFromPipelineByPropertyName = $true,
                Position = 0,
                HelpMessage = 'Existing Customer ID')]
        $CustomerID,
        
        [Parameter(Mandatory=$false,
                HelpMessage = 'Existing NCentral_Connection')]
        $NcSession
    )
    
    Begin{
        #check parameters. Use defaults if needed/available
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
        If(!$CustomerID){
            $CustomerID = 50
        }
    }    
    Process{
        $NcSession.DefaultCustomerID = $CustomerID
        Write-Host ("Default CustomerID now set to: {0}" -f $CustomerID)
    }
    End{
    }
}

Function Get-NCCustomerPropertyList{
<#
.Synopsis
Returns a list of all Custom-Properties for the selected CustomerID(s).
 
.Description
Returns a list of all Custom-Properties for the selected customers.
If no customerIDs are supplied, data for all customers will be returned.
 
## TODO - Integrate this in the default NCCustomerList.
#>

    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$false,
               ValueFromPipeline = $true,
               #ValueFromPipelineByPropertyName = $true,
               Position = 0,
               HelpMessage = 'Array of Existing Customer IDs')]
            [Alias("CustomerID")]
        [Array]$CustomerIDs,
        
        [Parameter(Mandatory=$false,
            HelpMessage = 'No Sorting of the output')]
            [Alias('All')]
        [Switch]$Full,

        [Parameter(Mandatory=$false,
            HelpMessage = 'No Sorting of the output')]
            [Alias('UnSorted')]
        [Switch]$NoSort,

        [Parameter(Mandatory=$false)]$NcSession
    )
    
    Begin{
        #check parameters. Use defaults if needed/available
    
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
    
    }
    Process{

        #Create an Array of all IDs To collect in one Call if possible.
        #Write-host "init"

        ## Recieved List of Objects or individual IDs?
        $IsObjectsArray = $false
        Try{
            If($CustomerIDs[0].customerid){$IsObjectsArray = $true}
        }
        Catch{}

        [System.Collections.ArrayList]$CustomerArray = @()
        foreach($CustomerID in $CustomerIDs){
            #Write-host "loop"
            If($IsObjectsArray){
                #Write-host ("Test1: {0}" -f $dID.deviceid)
                [void]$CustomerArray.Add($CustomerID.customerid)
            }else{
                #Write-host ("Test2: {0}" -f $dID)
                [void]$CustomerArray.Add($CustomerID)
            }
        }

        $ReturnData = $NcSession.OrganizationPropertyList($CustomerArray)
        #$ReturnData = $NcSession.OrganizationPropertyList($CustomerIDs)

        If ($Full -and $ReturnData){
            ## Add all standard properties
            ## from $NCSession.customerlist()

            ## Init
            $Customers = $ReturnData
            $Returndata2 = @()

            ## Build new Object(s)
            ForEach($Customer in $Customers ){
                ## (Re-)init Object-properties.
                $CustomerSettings = @{}

                # Add Custom Properties - from result
                $CCustomerProps = ($Customer|Get-Member -type Noteproperty).name
                foreach($CProperty in $CCustomerProps){
                    $PropertyValue = $Customer."$CProperty"
                    $CustomerSettings."$CProperty" = $PropertyValue
                }
                # Add Standard Properties - from internal cache
                $CustomerProps = ($NCSession.customerlist()).Where({ $_.customerid -eq $Customer.customerid})
                foreach($SProperty in $NCsession.CustomerValidation){
                    $PropertyValue = $CustomerProps.$SProperty
                    $CustomerSettings.$SProperty = $PropertyValue
                }

                ## Add each Object to the new Array.
                $ReturnData2 += (New-Object -TypeName PSObject -Property $CustomerSettings)
            }
            ## Return list of enhanced objects
            $ReturnData = $ReturnData2
        }

        If($NoSort){
            $ReturnData 
            #| Write-Output
        }
        Else{
            ## Determine all unique colums over all items. Columns can vary per asset-class.
            $NCSession.FixProperties($ReturnData) | 
            ## Alphabetical Columnnames --> included in fixproperties
            #$ReturnData | Select-Object ($ReturnData|Get-Member -type Noteproperty -ErrorAction SilentlyContinue).name -ErrorAction SilentlyContinue |
            ## Make CustomerID the first column.
            Select-Object customerid,* -ErrorAction SilentlyContinue 
            #| Write-Output
        }
    }
    End{
    }
}

Function Set-NCCustomerProperty{
<#
.Synopsis
Fills the specified property(name) for the given CustomerID(s). Base64 optional.
 
.Description
Fills the specified property(name) for the given CustomerID(s).
This can be a default or custom property.
CustomerID(s) must be supplied.
Properties are cleared if no Value is supplied.
Optional Base64 encoding (UniCode/UTF16).
 
#>

    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$true,
# ValueFromPipeline = $true,
               ValueFromPipelineByPropertyName = $true,
               Position = 0,
               HelpMessage = 'Array of Existing Customer IDs')]
            [Alias("CustomerID")]
        [Array]$CustomerIDs,

        [Parameter(Mandatory=$true,
# ValueFromPipeline = $true,
               ValueFromPipelineByPropertyName = $true,
               Position = 1,
               HelpMessage = 'Name of the Customer Custom-Property')]
            [Alias("PropertyName")]
        [String]$PropertyLabel,

        [Parameter(Mandatory=$false,
# ValueFromPipeline = $true,
               ValueFromPipelineByPropertyName = $true,
               Position = 2,
               HelpMessage = 'Value for the Customer Property')]
        [String]$PropertyValue = '',

        [Parameter(Mandatory=$false,
            HelpMessage = 'Encode the PropertyValue')]
            [Alias("Encode")]
        [Switch]$Base64,
            
        [Parameter(Mandatory=$false,        ## Optional with $Base64
            HelpMessage = 'Use UTF8 Encoding iso UniCode')]
        [Switch]$UTF8,
            
        [Parameter(Mandatory=$false)]$NcSession
    )
    
    Begin{
        #check parameters. Use defaults if needed/available
        $CustomerProperty = $false
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
# If (!$PropertyValue){
# Write-Host "CustomerProperty '$PropertyLabel' will be cleared."
# }
        If ($NcSession.CustomerValidation -contains $PropertyLabel){
            ## This is a standard CustomerProperty.
            $CustomerProperty = $true
        }
    }
    Process{
        ## Encode Data if requested
        If ($Base64){
            $PropertyValue = $NCSession.ConvertBase64($PropertyValue,$false,$UTF8)
        }

        ForEach($CustomerID in $CustomerIDs ){
            ## Differentiate between Standard(Customer) and Custom(Organization) properties.
            If ($CustomerProperty){
                $NcSession.CustomerModify($CustomerID, $PropertyLabel, $PropertyValue)
            }
            Else{
                $NcSession.OrganizationPropertyModify($CustomerID, $PropertyLabel, $PropertyValue)
            }
        }
    }
    End{
    }
}

Function Get-NCCustomerProperty{
    <#
    .Synopsis
    Retrieve the Value of the specified property(name) for the Customer(ID). Base64 optional.
     
    .Description
    Retrieve the Value of the specified property(name) for the Customer(ID).
    This can be a default or custom property.
    CustomerID and Propertyname must be supplied.
    (Save) Base64 decoding optional.
     
    #>

    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$true,
# ValueFromPipeline = $true,
                ValueFromPipelineByPropertyName = $true,
                Position = 0,
                HelpMessage = 'Existing Customer ID')]
            [Alias("ID")]
        [int]$CustomerID,

        [Parameter(Mandatory=$true,
# ValueFromPipeline = $true,
                ValueFromPipelineByPropertyName = $true,
                Position = 1,
                HelpMessage = 'Name of the Customer Custom-Property')]
            [Alias("PropertyName")]
        [String]$PropertyLabel,
            
        [Parameter(Mandatory=$false,
            HelpMessage = 'Decode the PropertyValue if needed')]
            [Alias("Decode")]
        [Switch]$Base64,

        [Parameter(Mandatory=$false,        ## Optional with $Base64
            HelpMessage = 'Use UTF8 Encoding iso UniCode')]
        [Switch]$UTF8,
            
        [Parameter(Mandatory=$false)]$NcSession
    )
    
    Begin{
        #check parameters. Use defaults if needed/available
        $CustomerProperty = $false
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
        If ($NcSession.CustomerValidation -contains $PropertyLabel){
            ## This is a standard CustomerProperty.
            $CustomerProperty = $true
        }
    }
    Process{
        ## Differentiate between Standard(Customer) and Custom(Organization) properties.
        If ($CustomerProperty){
            #$this.CustomerPropertyValue($CustomerID,"CustomerName")
            $ReturnData = $NcSession.CustomerPropertyValue($CustomerID,$PropertyLabel)
        }
        Else{
            $ReturnData = ($NcSession.OrganizationPropertyList($CustomerID)).$PropertyLabel
        }

        ## Decode if requested
        If ($Base64){
            If($Returndata){
                $Returndata = $NCSession.ConvertBase64($ReturnData,$true,$UTF8)
            }
        }
    }
    End{
        $Returndata 
        #| Write-Output
    }
}

Function Add-NCCustomerPropertyValue{
    <#
    .Synopsis
    The Value is added to the comma-separated string of unique values in the Customer Property.
     
    .Description
    The Value is added to the comma-separated string of unique values in the Customer Property.
    Case-sensivity is optional.
 
     
    #>

    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$true,
# ValueFromPipeline = $true,
                ValueFromPipelineByPropertyName = $true,
                Position = 0,
                HelpMessage = 'Existing Customer ID')]
        [int]$CustomerID,
            
        [Parameter(Mandatory=$true,
# ValueFromPipeline = $true,
                ValueFromPipelineByPropertyName = $true,
                Position = 1,
                HelpMessage = 'Existing Property (name)')]
            [Alias("PropertyLabel","Property","CustomProperty")]
        [string]$PropertyName,

        [Parameter(Mandatory=$true,
# ValueFromPipeline = $true,
                ValueFromPipelineByPropertyName = $true,
                Position = 2,
                HelpMessage = 'Value to Add to the String')]
            [Alias("Value")]
        [string]$ValueToInsert,

        [Parameter(Mandatory=$false,
            HelpMessage = 'Preserve Case')]
            [Alias('UseCase')]
        [Switch]$CaseSensitive,

        [Parameter(Mandatory=$false)]$NcSession
    )
    
    Begin{
        #check parameters. Use defaults if needed/available
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
    }
    Process{
        ## Remove Existing
        If ($CaseSensitive){
            [system.collections.arraylist]$ValueList = (Remove-NCCustomerPropertyValue $CustomerID $PropertyName $ValueToInsert -UseCase) -split ","
        }
        Else{
            [system.collections.arraylist]$ValueList = (Remove-NCCustomerPropertyValue $CustomerID $PropertyName $ValueToInsert) -split ","
        }

        ## Refresh empty List
        If(!$ValueList){
            $ValueList = @()
        }

        ## Add the new Value
        $ValueList += $ValueToInsert.Trim()

        ## Sort, Convert and Save
        $ReturnData = ($ValueList | Sort-Object ) -join ","
        ## Write data back to DeviceProperty
        Set-NCCustomerProperty $CustomerID $PropertyName $ReturnData
    }
    End{
        ## Return new values
        $ReturnData 
        #| Write-Output
    }
}

Function Remove-NCCustomerPropertyValue{
    <#
    .Synopsis
    The Value is removed from the comma-separated string of unique values in the Customer Property.
     
    .Description
    The Value is removed from the comma-separated string of unique values in the Customer Property.
    Case-sensivity is optional.
 
     
    #>

        [CmdletBinding()]
    
        Param(
            [Parameter(Mandatory=$true,
    # ValueFromPipeline = $true,
                    ValueFromPipelineByPropertyName = $true,
                    Position = 0,
                    HelpMessage = 'Existing Customer ID')]
            [int]$CustomerID,
                
            [Parameter(Mandatory=$true,
    # ValueFromPipeline = $true,
                    ValueFromPipelineByPropertyName = $true,
                    Position = 1,
                    HelpMessage = 'Existing Property (name)')]
                [Alias("PropertyLabel","Property","CustomProperty")]
            [string]$PropertyName,

            [Parameter(Mandatory=$true,
    # ValueFromPipeline = $true,
                    ValueFromPipelineByPropertyName = $true,
                    Position = 2,
                    HelpMessage = 'Value to Remove from the String')]
                [Alias("Value")]
            [string]$ValueToDelete,

            [Parameter(Mandatory=$false,
                HelpMessage = 'Preserve Case')]
                [Alias('UseCase')]
            [Switch]$CaseSensitive,

            [Parameter(Mandatory=$false)]$NcSession
        )
        
        Begin{
            #check parameters. Use defaults if needed/available
            If (!$NcSession){
                If (-not (NcConnected)){
                    Break
                }
                $NcSession = $Global:_NCSession
            }
        }

        Process{
            $ReturnData = $null
            [system.collections.arraylist]$ValueList = (Get-NCCustomerProperty $CustomerID $PropertyName) -split ","

            ## Check if values are retrieved
            If($ValueList){
                If ($CaseSensitive){
                    $ValueList.remove($ValueToDelete)
                }
                Else{
                    $ValueList =$ValueList.Where{$_ -ne "$ValueToDelete"}
                    #$ValueList =$ValueList | Where-Object -FilterScript {$_ -ne "$ValueToDelete"}
                }
                $ReturnData = ( $ValueList | Sort-Object ) -join ","

                ## Write data back to DeviceProperty
                Set-NCCustomerProperty $CustomerID $PropertyName $ReturnData
            }
        }
        End{
            $ReturnData 
            #| Write-Output
        }
}

Function Get-NCProbeList{
<#
.Synopsis
Returns the Probes for the given CustomerID(s).
 
.Description
Returns the Probes for the given CustomerID(s).
If no customerIDs are supplied, all probes will be returned.
 
#>

    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$false,
               ValueFromPipeline = $true,
               ValueFromPipelineByPropertyName = $true,
               Position = 0,
               HelpMessage = 'Existing Customer ID')]
            [Alias("CustomerID")]
        [Array]$CustomerIDs,

        [Parameter(Mandatory=$false)]$NcSession
    )
    Begin{
        #check parameters. Use defaults if needed/available
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
        If (!$CustomerIDs){
            If (!$NcSession.DefaultCustomerID){
                Write-Host "No CustomerID specified."
                Break
            }
            $CustomerIDs = $NcSession.DefaultCustomerID
            Write-Host ("Using current default CustomerID {0} for NCProbeList." -f $CustomerIDs)
        }
    }
    Process{
        ForEach ($CustomerID in $CustomerIDs){
            $NcSession.DeviceList($CustomerID,$false,$true)|
            Select-Object deviceid,@{n="customerid"; e={$CustomerID}},customername,longname,url,* -ErrorAction SilentlyContinue 
            #| Write-Output
        }
    }
    End{
    }

}
#EndRegion

#Region Devices
Function Get-NCDeviceList{
<#
.Synopsis
Returns the Managed Devices for the given CustomerID(s) and Sites below.
 
.Description
Returns the Managed Devices for the given CustomerID(s) and Sites below.
If no customerIDs are supplied, all managed devices will be returned.
 
## TODO - Confirmation if no CustomerID(s) are supplied (Full List).
#>

    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$false,
               #ValueFromPipeline = $true,
               ValueFromPipelineByPropertyName = $true,
               Position = 0,
               HelpMessage = 'Existing Customer ID')]
            [Alias("customerid")]
        [array]$CustomerIDs,

        [Parameter(Mandatory=$false,
            HelpMessage = 'No Sorting of the output')]
            [Alias('UnSorted')]
        [Switch]$NoSort,

        [Parameter(Mandatory=$false)]$NcSession
    )
    Begin{
        #check parameters. Use defaults if needed/available
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
        If (!$CustomerIDs){
            ## this should be skipped when coming from an empty pipeline object.
            Write-host ("CustomerIDs = {0}" -f $CustomerIDs)
            If (!$NcSession.DefaultCustomerID){
                Write-Host "No CustomerID specified."
                Break
            }
            $CustomerIDs = $NcSession.DefaultCustomerID
            Write-Host ("Using current default CustomerID {0} for NCDeviceList." -f $CustomerIDs)
        }
    }
    Process{
        ForEach ($CustomerID in $CustomerIDs){
            #Write-host ("CustomerID = {0}." -f $CustomerID)
            $ReturnData = $NcSession.DeviceList($CustomerID)

            If($NoSort){
                $ReturnData 
                #| Write-Output
            }
            Else{
                $ReturnData | 
                ## Alphabetical Columnnames
                Select-Object ($ReturnData|Get-Member -type Noteproperty -ErrorAction SilentlyContinue).name -ErrorAction SilentlyContinue |
                ## CustomerID is not returned by default. Added as custom field.
                Select-Object deviceid,@{n="customerid"; e={$CustomerID}},customername,sitename,longname,uri,* -ErrorAction SilentlyContinue 
                #| Write-Output
            }
        }
    }
    End{
    }
}

Function Get-NCDeviceID{
    <#
    .Synopsis
    Returns the DeviceID(s) for the given DeviceName(s). Case Sensitive, No Wildcards.
 
    .Description
    The returned objects contain extra information for verification.
    The supplied name(s) are Case Sensitive, No Wildcards allowed.
    Also not-managed devices are returned.
    Nothing is returned for names not found.
     
    #>

    
    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$true,
               ValueFromPipeline = $true,
# ValueFromPipelineByPropertyName = $true,
               Position = 0,
               HelpMessage = 'Array of existing Filter IDs')]
# [Alias("Name")]
        [Array]$DeviceNames,
        
        [Parameter(Mandatory=$false)]$NcSession
    )
    
    Begin{
        #check parameters. Use defaults if needed/available
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
        If (!$DeviceNames){
            Write-Host "No DeviceName(s) given."
            Break
        }
    }
    Process{
        ## Collect the data for all Names. Case Sensitive, No Wildcards.
        ## Only Returns found devices.
                
        ForEach ($DeviceName in $DeviceNames){
            ## Use the NameFilter of the DevicePropertyList to find the DeviceID for now.
            ## Limited Filter-options, but fast.
            $NcSession.DevicePropertyList($null,$DeviceName,$null,$null) |
            ## Add additional Info and return only selected fields/Columns
            Get-NCDeviceInfo |
            Select-Object DeviceID,LongName,DeviceClass,CustomerID,CustomerName,IsManagedAsset 
            #| Write-Output
        }
    
    }
    End{
    }
}

Function Get-NCDeviceLocal{
    <#
    .Synopsis
    Returns the DeviceID, CustomerID and some more Info for the Local Computer.
 
    .Description
    Queries the local ApplicationID and returns the NCentral DeviceID.
    No Parameters recquired.
     
    #>

    
    [CmdletBinding()]

    Param(
    )
    
    Begin{
        #check parameters. Use defaults if needed/available
        $ApplianceConfig = ("{0}\N-able Technologies\Windows Agent\config\ApplianceConfig.xml" -f ${Env:ProgramFiles(x86)})
        $ServerConfig = ("{0}\N-able Technologies\Windows Agent\config\ServerConfig.xml" -f ${Env:ProgramFiles(x86)})

        If (-not (Test-Path $ApplianceConfig -PathType leaf)){
            Write-Host "No Local NCentral-agent Configuration found."
            Write-Host "Try using 'Get-NCDeviceID $Env:ComputerName'."
            Break
        }
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
    }
    Process{
        # Get appliance id
        $ApplianceXML = [xml](Get-Content -Path $ApplianceConfig)
        $ApplianceID = $ApplianceXML.ApplianceConfig.ApplianceID
        # Get management Info.
        $ServerXML = [xml](Get-Content -Path $ServerConfig)
        $ServerIP = $ServerXML.ServerConfig.ServerIP
        $ConnectIP = $NcSession.ConnectionURL

        If($ServerIP -ne $ConnectIP){
            Write-Host "The Local Device is Managed by $ServerIP. You are connected to $ConnectIP."
        }
        
        $NcSession.DeviceGetAppliance($ApplianceID)|
        ## Return all Info, since already collected.
        Select-Object deviceid,longname,@{Name="managedby"; Expression={$ServerIP}},customerid,customername,deviceclass,licensemode,* -ErrorAction SilentlyContinue 
        #| Write-Output
    }
    End{
    }
}

Function Get-NCDevicePropertyList{
<#
.Synopsis
Returns the Custom Properties of the DeviceID(s).
 
.Description
Returns the Custom Properties of the DeviceID(s).
If no devviceIDs are supplied, all managed devices
and their Custom Properties will be returned.
 
## TODO - Confirmation if no DeviceID(s) are supplied (Full List). Only warning now.
## Issue: Only properties of first item are added/displayed for all Devices in a list.
#>

    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$false,
               ValueFromPipeline = $true,
               #ValueFromPipelineByPropertyName = $true,
               Position = 0,
               HelpMessage = 'Existing Device IDs')]
            [Alias("DeviceID")]
        [Array]$DeviceIDs,

        [Parameter(Mandatory=$false,
                HelpMessage = 'Existing Device Names')]
            [Alias("DeviceName")]
        [Array]$DeviceNames,

        [Parameter(Mandatory=$false,
                HelpMessage = 'Existing Filter IDs')]
            [Alias("FilterID")]
        [Array]$FilterIDs,

        [Parameter(Mandatory=$false,
                HelpMessage = 'Existing Filter Names')]
            [Alias("FilterName")]
        [Array]$FilterNames,

        [Parameter(Mandatory=$false,
            HelpMessage = 'No Sorting of the output')]
            [Alias('UnSorted')]
        [Switch]$NoSort,

        [Parameter(Mandatory=$false)]$NcSession
    )
    
    Begin{
        #check parameters. Use defaults if needed/available
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
    }
    Process{
        #Create an Array of all IDs To collect in one Call if possible.
        #Write-host "init"

        ## Recieved List of Objects or individual IDs?
        $IsObjectsArray = $false
        Try{
            If($DeviceIDs[0].deviceid){$IsObjectsArray = $true}
        }
        Catch{}

        [System.Collections.ArrayList]$DeviceArray = @()
        foreach($DeviceID in $DeviceIDs){
            If($IsObjectsArray){
                [void]$DeviceArray.Add($DeviceID.deviceid)
            }else{
                [void]$DeviceArray.Add($DeviceID)
            }
        }

        #Write-Host $DeviceIDs
        #Write-Host ("Sent {0}." -f [array]$DeviceArray -join ",")

        If ($DeviceArray -or $DeviceNames -or $FilterIDs -or $FilterNames ){
            #$Returndata = $NcSession.DevicePropertyList($DeviceIDs,$DeviceNames,$FilterIDs,$FilterNames)
            $Returndata = $NcSession.DevicePropertyList($DeviceArray,$DeviceNames,$FilterIDs,$FilterNames)
            #Write-host ("Received {0}." -f $ReturnData.deviceid -join ",")
        }
        Else{
            Write-Host "Generating a full DevicePropertyList may take some time."
            
            $ReturnData = $NcSession.DevicePropertyList($null,$null,$null,$null)

            Write-Host "Data retrieved, processing output."
        }

        If ($NoSort){
            $ReturnData 
            #| Write-Output
        }
        Else{
            ## Determine all unique colums over all items. Columns can vary per asset-class.
            $NCSession.FixProperties($ReturnData) | 
            ## Alphabetical Columnnames --> included in fixproperties
            #Select-Object ($ReturnData|Get-Member -type Noteproperty -ErrorAction SilentlyContinue).name -ErrorAction SilentlyContinue |

            ## Make DeviceID the first column.
            Select-Object deviceid,* -ErrorAction SilentlyContinue | Write-Output
        }
    }
    End{
    }
}

Function Get-NCDevicePropertyListFilter{
<#
.Synopsis
Returns the Custom Properties of the Devices within the FilterID(s).
 
.Description
Returns the Custom Properties of the Devices within the FilterID(s).
A filterID must be supplied. Hoover over the filter in the GUI to reveal its ID.
 
Replaced by the Get-NCDevicePropertyList -FilterID.
Included for backward compatibility
 
#>

    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$true,
               ValueFromPipeline = $true,
               ValueFromPipelineByPropertyName = $true,
               Position = 0,
               HelpMessage = 'Array of existing Filter IDs')]
            [Alias("FilterID")]
        [Array]$FilterIDs,
        
        [Parameter(Mandatory=$false,
            HelpMessage = 'No Sorting of the output')]
            [Alias('UnSorted')]
        [Switch]$NoSort,

        [Parameter(Mandatory=$false)]$NcSession
    )
    
    Begin{
        #check parameters. Use defaults if needed/available
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
        If (!$FilterIDs){
            Write-Host "No FilterIDs given."
            Break
        }
    }
    Process{
        #Collect the data for all IDs.
        
        If($NoSort){
            Get-NCDevicePropertyList -FilterId $FilterIDs -NoSort
        }
        Else{
            Get-NCDevicePropertyList -FilterId $FilterIDs
        }
<#
        ForEach ($FilterID in $FilterIDs){
            $ReturnData = $NcSession.DevicePropertyList($null,$null,$FilterID,$null)
 
            If($NoSort){
                $ReturnData | Write-Output
            }
            Else{
                ## Alphabetical Columnnames
                $ReturnData | Select-Object ($ReturnData|Get-Member -type Noteproperty -ErrorAction SilentlyContinue).name -ErrorAction SilentlyContinue |
                ## Make DeviceID the first column.
                Select-Object deviceid,* -ErrorAction SilentlyContinue |
                Write-Output
            }
 
        }
#>
    
    }
    End{
    }
}

Function Set-NCDeviceProperty{
    <#
    .Synopsis
    Set the value of the Custom Property for the DeviceID(s). Base64 optional.
     
    .Description
    Set the value of the Custom Property for the DeviceID(s).
    Existing values are overwritten, Properties are cleared if no Value is supplied.
    Optional Base64 Encoding (Unicode/UTF16).
     
    #>

    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$true,
# ValueFromPipeline = $true,
                ValueFromPipelineByPropertyName = $true,
                Position = 0,
                HelpMessage = 'Existing Device IDs')]
            [Alias("DeviceID")]
        [Array]$DeviceIDs,

        [Parameter(Mandatory=$true,
# ValueFromPipeline = $true,
# ValueFromPipelineByPropertyName = $true,
                Position = 1,
                HelpMessage = 'Name of the Device Custom-Property')]
                [Alias("PropertyName")]
            [String]$PropertyLabel,

        [Parameter(Mandatory=$false,
# ValueFromPipeline = $true,
# ValueFromPipelineByPropertyName = $true,
                Position = 2,
                HelpMessage = 'Value for the Device Custom-Property or empty')]
                [Alias("Value")]
            [String]$PropertyValue,
        
        [Parameter(Mandatory=$false,
                HelpMessage = 'Encode the PropertyValue')]
                [Alias("Encode")]
            [Switch]$Base64,
                    
        [Parameter(Mandatory=$false,        ## Optional with $Base64
            HelpMessage = 'Use UTF8 Encoding iso UniCode')]
        [Switch]$UTF8,
            
        [Parameter(Mandatory=$false)]$NcSession
    )
    
    Begin{
        #check parameters. Use defaults if needed/available
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
# If (!$DeviceIDs){
# ## Issue when value comes from pipeline. Use Parameter-validation.
# Write-Host "No DeviceID specified."
# Break
# }
# If (!$PropertyLabel){
# ## Use Parameter-validation.
# Write-Host "No Property-name specified."
# Break
# }
        If (!$PropertyValue){
            #Write-Host "DeviceProperty '$PropertyLabel' will be cleared."
            $PropertyValue=$null
        }
    }
    Process{
        ## Encode if requested
        If ($Base64){
            $PropertyValue = $NCSession.ConvertBase64($PropertyValue,$false,$UTF8)
        }

        ForEach($DeviceID in $DeviceIDs ){
            $NcSession.DevicePropertyModify($DeviceID, $PropertyLabel, $PropertyValue)
        }
    }
    End{
    }
}

Function Get-NCDeviceProperty{
    <#
    .Synopsis
    Returns the Value of the Custom Device Property. Base64 optional.
     
    .Description
    Returns the Value of the Custom Device Property.
    (Save) Base64 decoding optional.
     
    #>

    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$true,
# ValueFromPipeline = $true,
                ValueFromPipelineByPropertyName = $true,
                Position = 0,
                HelpMessage = 'Existing Device ID')]
            [Alias("ID")]
        [int]$DeviceID,
            
        [Parameter(Mandatory=$true,
# ValueFromPipeline = $true,
                ValueFromPipelineByPropertyName = $true,
                Position = 1,
                HelpMessage = 'Existing Property')]
            [Alias("Property","CustomProperty")]
        [string]$PropertyName,
            
        [Parameter(Mandatory=$false,
            HelpMessage = 'Decode the PropertyValue if needed')]
            [Alias("Decode")]
        [Switch]$Base64,
        
        [Parameter(Mandatory=$false,        ## Optional with $Base64
            HelpMessage = 'Use UTF8 Encoding iso UniCode')]
        [Switch]$UTF8,
            
        [Parameter(Mandatory=$false)]$NcSession
    )
    
    Begin{
        #check parameters. Use defaults if needed/available
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
    }
    Process{
        $ReturnData = ($NcSession.DevicePropertyList($DeviceID,$null,$null,$null).$PropertyName)

        ## Decode if requested
        If($Base64){
            If($ReturnData){
                $ReturnData = $NCSession.ConvertBase64($ReturnData,$true,$UTF8)
            }
        }
    }
    End{
        $ReturnData 
        #| Write-Output
    }
#>
}

Function Add-NCDevicePropertyValue{
    <#
    .Synopsis
    The Value is added to the comma-separated string of unique values in the Custom Device Property.
     
    .Description
    The Value is added to the comma-separated string of unique values in the Custom Device Property.
    Case-sensivity is optional.
 
     
    #>

        [CmdletBinding()]
    
        Param(
            [Parameter(Mandatory=$true,
    # ValueFromPipeline = $true,
                   ValueFromPipelineByPropertyName = $true,
                   Position = 0,
                   HelpMessage = 'Existing Device ID')]
            [int]$DeviceID,
                
            [Parameter(Mandatory=$true,
    # ValueFromPipeline = $true,
                   ValueFromPipelineByPropertyName = $true,
                   Position = 1,
                   HelpMessage = 'Existing Property')]
                [Alias("PropertyLabel","Property","CustomProperty")]
            [string]$PropertyName,

            [Parameter(Mandatory=$true,
    # ValueFromPipeline = $true,
                   ValueFromPipelineByPropertyName = $true,
                   Position = 2,
                   HelpMessage = 'Value to Add to the String')]
                [Alias("Value")]
            [string]$ValueToInsert,

            [Parameter(Mandatory=$false,
                HelpMessage = 'Preserve Case')]
                [Alias('UseCase')]
            [Switch]$CaseSensitive,

            [Parameter(Mandatory=$false)]$NcSession
        )
        
        Begin{
            #check parameters. Use defaults if needed/available
            If (!$NcSession){
                If (-not (NcConnected)){
                    Break
                }
                $NcSession = $Global:_NCSession
            }
        }
        Process{
            ## Retrieve Existing values
            #[system.collections.arraylist]$ValueList = (Get-NCDeviceProperty $DeviceID $PropertyName) -split ","

            ## Remove Existing
            If ($CaseSensitive){
                [system.collections.arraylist]$ValueList = (Remove-NCDevicePropertyValue $DeviceID $PropertyName $ValueToInsert -UseCase) -split ","
            }
            Else{
                [system.collections.arraylist]$ValueList = (Remove-NCDevicePropertyValue $DeviceID $PropertyName $ValueToInsert) -split ","
            }

            ## Refresh empty List
            If(!$ValueList){
                $ValueList = @()
            }

            ## Add the new Value
            $ValueList += $ValueToInsert.Trim()
<#
            ## Remove duplicate values. Is applied to existing values also.
            If ($CaseSensitive){
                $ReturnData = ($ValueList | Select-Object -Unique |Sort-Object) -join ","
                #$ReturnData = ($ValueList | Get-Unique -AsString | Sort-Object) -join ","
            }
            Else{
                $ReturnData = ($ValueList | Sort-Object -Unique ) -join ","
            }
#>

            ## Sort, Convert and Save
            $ReturnData = ($ValueList | Sort-Object ) -join ","
            ## Write data back to DeviceProperty
            Set-NCDeviceProperty $DeviceID $PropertyName $ReturnData
        }
        End{
            ## Return new values
            $ReturnData 
            #| Write-Output
        }
}

Function Remove-NCDevicePropertyValue{
    <#
    .Synopsis
    The Value is removed from the comma-separated string of unique values in the Custom Device Property.
     
    .Description
    The Value is removed from the comma-separated string of unique values in the Custom Device Property.
    Case-sensivity is optional.
 
     
    #>

        [CmdletBinding()]
    
        Param(
            [Parameter(Mandatory=$true,
    # ValueFromPipeline = $true,
                   ValueFromPipelineByPropertyName = $true,
                   Position = 0,
                   HelpMessage = 'Existing Device ID')]
            [int]$DeviceID,
                
            [Parameter(Mandatory=$true,
    # ValueFromPipeline = $true,
                   ValueFromPipelineByPropertyName = $true,
                   Position = 1,
                   HelpMessage = 'Existing Property')]
                [Alias("Property","CustomProperty")]
            [string]$PropertyName,

            [Parameter(Mandatory=$true,
    # ValueFromPipeline = $true,
                   ValueFromPipelineByPropertyName = $true,
                   Position = 2,
                   HelpMessage = 'Value to Remove from the String')]
                [Alias("Value")]
            [string]$ValueToDelete,

            [Parameter(Mandatory=$false,
                HelpMessage = 'Preserve Case')]
                [Alias('UseCase')]
            [Switch]$CaseSensitive,

            [Parameter(Mandatory=$false)]$NcSession
        )
        
        Begin{
            #check parameters. Use defaults if needed/available
            If (!$NcSession){
                If (-not (NcConnected)){
                    Break
                }
                $NcSession = $Global:_NCSession
            }
        }

        Process{
            $ReturnData = $null
            [system.collections.arraylist]$ValueList = (Get-NCDeviceProperty $DeviceID $PropertyName) -split ","

            ## Check if values are retrieved
            If($ValueList){
                If ($CaseSensitive){
                    $ValueList.remove($ValueToDelete)
                }
                Else{
                    $ValueList =$ValueList.Where{$_ -ne "$ValueToDelete"}
                    #$ValueList =$ValueList | Where-Object -FilterScript {$_ -ne "$ValueToDelete"}
                }
                $ReturnData = ( $ValueList | Sort-Object ) -join ","

                ## Write data back to DeviceProperty
                Set-NCDeviceProperty $DeviceID $PropertyName $ReturnData
            }
        }
        End{
            $ReturnData 
            #| Write-Output
        }
}

Function Get-NCDeviceInfo{
<#
.Synopsis
Returns the General details for the DeviceID(s).
 
.Description
Returns the General details for the DeviceID(s).
DeviceID(s) must be supplied, as a parameter or by PipeLine.
Use Get-NCDeviceObject tot retrieve ALL details of a device.
 
#>

    [CmdletBinding(DefaultParameterSetName='NonPipeline')]

    Param(
        [Parameter(Mandatory=$true,
                ValueFromPipeline = $true,
                #ValueFromPipelineByPropertyName = $false,
                Position = 0,
                HelpMessage = 'Existing Device IDs')]
            [Alias("DeviceID")]
        [Array]$DeviceIDs,
        
        [Parameter(Mandatory=$false,
            HelpMessage = 'Show processing progress')]
            [Alias('Progress')]
        [Switch]$ShowProgress,

        [Parameter(Mandatory=$false)]$NcSession
    )
    
    Begin{
        #check parameters. Use defaults if needed/available
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
    }
    Process{
        #Create an Array of all IDs To collect in one Call if possible.
        #Write-host "init"

        ## Recieved List of Objects or individual IDs?
        $IsObjectsArray = $false
        Try{
            If($DeviceIDs[0].deviceid){$IsObjectsArray = $true}
        }
        Catch{}

        [System.Collections.ArrayList]$DeviceArray = @()
        foreach($DeviceID in $DeviceIDs){
            If($IsObjectsArray){
                [void]$DeviceArray.Add($DeviceID.deviceid)
            }else{
                [void]$DeviceArray.Add($DeviceID)
            }
        }

        ## Mutiple IDs are sent to the server in one call.
        $NcSession.DeviceGet($DeviceArray,$ShowProgress)|
        Select-Object deviceid,longname,customerid,customername,deviceclass,licensemode,* -ErrorAction SilentlyContinue 
        #| Write-Output
    }
    End{
    }
}
    
Function Get-NCDeviceObject{
<#
.Synopsis
Returns a Device and the asset-properties as an object.
 
.Description
Returns a Device and the asset-properties as an object.
The asset-properties may contain multiple entries.
 
You can pass an array of properties to be Included or Excluded.
 
#>


    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$false,
                ValueFromPipeline = $true,
                #ValueFromPipelineByPropertyName = $true,
                Position = 0,
                HelpMessage = 'Existing Device IDs')]
# [ValidateScript({ $_ | ForEach-Object {(Get-Item $_).PSIsContainer}})]
            [Alias("DeviceID")]
            [Array]$DeviceIDs,
#<#
        [Parameter(Mandatory=$false,
# Position = 3,
                HelpMessage = 'Existing Device Names')]
            [Alias("DeviceName")]
            [Array]$DeviceNames,

        [Parameter(Mandatory=$false,
# Position = 4,
                HelpMessage = 'Existing Filter IDs')]
            [Alias("FilterID")]
            [Array]$FilterIDs,

        [Parameter(Mandatory=$false,
# Position = 5,
                HelpMessage = 'Existing Filter Names')]
            [Alias("FilterName")]
            [Array]$FilterNames,
#>
        [Parameter(Mandatory=$false,
                Position = 1,
                HelpMessage = 'Include categories')]
            [Alias("CategoryInclude")]
            [Array]$Include,

        [Parameter(Mandatory=$false,
                        Position = 2,
                        HelpMessage = 'Exclude categories')]
            [Alias("CategoryExclude")]
            [Array]$Exclude,

        [Parameter(Mandatory=$false,
            HelpMessage = 'Show processing progress')]
            [Alias('Progress')]
        [Switch]$ShowProgress,

        [Parameter(Mandatory=$false)]$NcSession
    )
    
    Begin{
        #check parameters. Use defaults if needed/available

        #If(($DeviceIds.count + $DeviceNames.count + $FilterIDs.count + $FilterNames.count) -eq 0){
            #Write-host "At least one device-selector must be supplied."
            #Exit
        #}

        ## Always include basic Device-properties
        If($Include){
            [System.Collections.ArrayList]$IncludeFilter = @()
            [void]$includeFilter.AddRange($include)
            [void]$includeFilter.Add("device")
        }

        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }

    }
    Process{
        #Collect the data for all IDs.
        #Write-Host "init"

        ## Additional processing when data comes from pipeline
        $IsObjectsArray = $false
        Try{
            If($DeviceIDs[0].deviceid){$IsObjectsArray = $true}
        }
        Catch{}

        [System.Collections.ArrayList]$DeviceArray = @()
        foreach($DeviceID in $DeviceIDs){
            If($IsObjectsArray){
                [void]$DeviceArray.Add($DeviceID.deviceid)
            }else{
                [void]$DeviceArray.Add($DeviceID)
            }
        }

        ## Multiple IDs can be passed to the Class-method directly
        #$ReturnData = $NcSession.DeviceAssetInfoExportDeviceWithSettings($DeviceIDs,$DeviceNames,$FilterIDs,$FilterNames,$Include,$Exclude)
        $NcSession.DeviceAssetInfoExportDeviceWithSettings($DeviceArray,$DeviceNames,$FilterIDs,$FilterNames,$IncludeFilter,$Exclude,$ShowProgress)|
        Select-Object ($Returndata | Get-Member -type Noteproperty -ErrorAction SilentlyContinue).name -ErrorAction SilentlyContinue |
        Select-Object deviceid,longname,customerid,deviceclass,* -ErrorAction SilentlyContinue 
        #| Write-Output
    }
    End{
    }
}
#EndRegion

#Region Services and Tasks
Function Get-NCActiveIssuesList{
<#
.Synopsis
Returns the Active Issues on the CustomerID-level and below.
 
.Description
Returns the Active Issues on the CustomerID-level and below.
An additional Search/Filter-string can be supplied.
 
If no customerID is supplied, Default Customer is used.
The SiteID of the devices is returned (Not CustomerID).
 
-IssueSearchBy searches the given string in the fields:
so, site, device, deviceClass, service, transitionTime, notification, features, deviceID, and ip address.
 
-IssueStatus option (or as 3th parameter):
 1 No Data
 2 Stale
 3 Normal --> Nothing returned
 4 Warning
 5 Failed
 6 Misconfigured
 7 Disconnected
11 Unacknowledged
12 Acknowledged
 
The API does not allow combinations of these filters.
1-7 are reflected in the notifstate-property.
11 and 12 relate to the properties numberofactivenotification and numberofacknowledgednotification.
 
#>

    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$false,
               #ValueFromPipeline = $true,
               ValueFromPipelineByPropertyName = $true,
               Position = 0,
               HelpMessage = 'Existing Customer ID')]
        [Int]$CustomerID,

        [Parameter(Mandatory=$false,
               ValueFromPipeline = $true,
               ValueFromPipelineByPropertyName = $true,
               Position = 1,
               HelpMessage = 'Text to look for')]
        [String]$IssueSearchBy = "",

        [Parameter(Mandatory=$false,
               ValueFromPipeline = $true,
               ValueFromPipelineByPropertyName = $true,
               Position = 2,
               HelpMessage = 'Status Code')]
        [Int]$IssueStatus = 0,

        [Parameter(Mandatory=$false)]$NcSession
    )
    
    Begin{
        #check parameters. Use defaults if needed/available
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
        If (!$CustomerID){
            If (!$NcSession.DefaultCustomerID){
                Write-Host "No CustomerID specified."
                Break
            }
            $CustomerID = $NcSession.DefaultCustomerID
            Write-Host ("Using current default CustomerID {0} for NCActiveIssuesList." -f $CustomerID)
        }
    }
    Process{
        $ReturnData = $NcSession.ActiveIssuesList($CustomerID, $IssueSearchBy, $IssueStatus)
    }
    End{
        $ReturnData |
        ## Alphabetical Columnnames
        Select-Object ($ReturnData|Get-Member -type Noteproperty -ErrorAction SilentlyContinue).name -ErrorAction SilentlyContinue |
        #Sort-Object TransitionTime -Descending |
        ## Put important fields in front. and Add Custom Fields
        Select-Object taskid,
                    @{n="siteid"; e={$_.CustomerID}},
                    CustomerName,
                    DeviceID,
                    DeviceName,
                    DeviceClass,
                    ServiceName,
                    NotifState,
                    @{n="notifstatetxt"; e={$NcSession.NCStatus.[int]$_.notifstate}},
                    TransitionTime,
                    * -ErrorAction SilentlyContinue
                    # | Write-Output
    }
}

Function Get-NCJobStatusList{
    <#
    .Synopsis
    Returns the Scheduled Jobs on the CustomerID-level and below.
     
    .Description
    Returns the Scheduled Jobs on the CustomerID-level and below.
    Including Discovery Jobs
         
    If no customerID is supplied, all Jobs are returned.
    The SiteID of the devices is returned (Not CustomerID).
     
    #>

        [CmdletBinding()]
    
        Param(
            [Parameter(Mandatory=$false,
                   #ValueFromPipeline = $true,
                   ValueFromPipelineByPropertyName = $true,
                   Position = 0,
                   HelpMessage = 'Existing Customer ID')]
            [Int]$CustomerID
        )
        
        Begin{
            #check parameters. Use defaults if needed/available
            If (!$NcSession){
                If (-not (NcConnected)){
                    Break
                }
                $NcSession = $Global:_NCSession
            }
            If (!$CustomerID){
                If (!$NcSession.DefaultCustomerID){
                    Write-Host "No CustomerID specified."
                    Break
                }
                $CustomerID = $NcSession.DefaultCustomerID
                Write-Host ("Using current default CustomerID {0} for NCJobStatusList." -f $CustomerID)
            }
        }
        Process{
            $NcSession.JobStatusList($CustomerID)|
            Select-Object CustomerID,CustomerName,DeviceID,DeviceName,DeviceClass,JobName,ScheduledTime,* -ErrorAction SilentlyContinue |
    # Sort-Object ScheduledTime -Descending | Select-Object @{n="SiteID"; e={$_.CustomerID}},CustomerName,DeviceID,DeviceName,DeviceClass,ServiceName,TransitionTime,NotifState,* -ErrorAction SilentlyContinue |
            Write-Output
        }
        End{
        }
}

Function Get-NCDeviceStatus{
<#
.Synopsis
Returns the Services for the DeviceID(s).
 
.Description
Returns the Services for the DeviceID(s).
DeviceID(s) must be supplied, as a parameter or by PipeLine.
 
#>

    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$true,
# ValueFromPipeline = $true,
               ValueFromPipelineByPropertyName = $true,
               Position = 0,
               HelpMessage = 'Existing Device IDs')]
            [Alias("DeviceID")]
        [Array]$DeviceIDs,
        
        [Parameter(Mandatory=$false)]$NcSession
    )
    
    Begin{
        #check parameters. Use defaults if needed/available
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
    }
    Process{
        ForEach($DeviceID in $DeviceIDs){
            $NcSession.DeviceGetStatus($DeviceID)|
            Select-Object deviceid,devicename,serviceid,modulename,statestatus,transitiontime,* -ErrorAction SilentlyContinue 
            #| Write-Output
        }
    }
    End{
    }
}
#EndRegion

#Region Access Control
Function Get-NCAccessGroupList{
<#
.Synopsis
Returns the list of AccessGroups at the specified CustomerID level.
 
.Description
Returns the list of AccessGroups at the specified CustomerID level.
 
#>

    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$false,
               #ValueFromPipeline = $true,
               ValueFromPipelineByPropertyName = $true,
               Position = 0,
            HelpMessage = 'Existing Customer IDs')]
            [Alias("customerid")]
        [array]$CustomerIDs,
               
        [Parameter(Mandatory=$false,
            HelpMessage = 'Return only used AccessGroups')]
            [Alias("UsedOnly")]
        [Switch]$Filter,

        [Parameter(Mandatory=$false,
            HelpMessage = 'No Sorting of the output columns')]
            [Alias('UnSorted')]
        [Switch]$NoSort,

        [Parameter(Mandatory=$false)]$NcSession
    )
    
    Begin{
        #check parameters. Use defaults if needed/available
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
        If (!$CustomerIDs){
            If (!$NcSession.DefaultCustomerID){
                Write-Host "No CustomerID specified."
                Break
            }
            $CustomerIDs = $NcSession.DefaultCustomerID
            Write-Host ("Using current default CustomerID {0} for NCAccessGroupList." -f $CustomerIDs)
        }
    }
    Process{
        $ReturnData = @()
        ForEach($CustomerID in $customerIDs){
            #Write-Output $NcSession.AccessGroupList($CustomerID)
            $ReturnData += $NcSession.AccessGroupList($CustomerID)
            # | Where-Object {$_.customerid -eq $Customerid}
        }
        ## Return only used groups.
        If($Filter){
            $Returndata = $Returndata | Where-object {$_.usernames -ne "[]"}
        }
    }
    End{
        If($NoSort){
            $ReturnData 
            #| Write-Output
        }
        Else{
            ## Alphabetical Columnnames
            $ReturnData | Select-Object ($ReturnData|Get-Member -type Noteproperty -ErrorAction SilentlyContinue).name -ErrorAction SilentlyContinue |
            ## Important fields in front. ToDo: Boolean customergroup, derived from groupType.
            Select-Object groupid,grouptype,customerid,groupname,* -ErrorAction SilentlyContinue 
            #| Write-Output
        }
    }
}
Function Get-NCAccessGroupDetails{
<#
.Synopsis
Returns the details of the specified (CustomerAccess) GroupID.
 
.Description
Returns the details of the specified (CustomerAccess) GroupID.
 
#>

    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$true,
                #ValueFromPipeline = $true,
                ValueFromPipelineByPropertyName = $true,
                Position = 0,
                HelpMessage = 'Existing Group IDs')]
                [Alias("groupid")]
            [array]$GroupIDs,
                
        [Parameter(Mandatory=$false)]$NcSession
    )
    
    Begin{
        #check parameters. Use defaults if needed/available
        If (!$GroupIDs){
            Write-Host "No GroupID specified."
            Break
        }

        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
    }
    Process{
        $ReturnData = @()
        ForEach($GroupID in $GroupIDs){
            ## Only Customer-AccessGroups. ToDo: Implement DeviceGroups (now error: 4100 Invalid parameters)
            $ReturnData += $NcSession.AccessGroupGet($GroupID)
        }
    }
    End{
        $ReturnData 
        #| Write-Output
    }
}
    
Function Get-NCUserRoleList{
<#
.Synopsis
Returns the list of Roles at the specified CustomerID level.
 
.Description
Returns the list of Roles at the specified CustomerID level.
 
#>

    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$false,
               #ValueFromPipeline = $true,
               ValueFromPipelineByPropertyName = $true,
               Position = 0,
            HelpMessage = 'Existing Customer IDs')]
            [Alias("customerid")]
        [array]$CustomerIDs,

        [Parameter(Mandatory=$false,
            HelpMessage = 'Return only used Roles')]
            [Alias("UsedOnly")]
        [Switch]$Filter,

        [Parameter(Mandatory=$false,
            HelpMessage = 'No Sorting of the output colums')]
            [Alias('UnSorted')]
        [Switch]$NoSort,

        [Parameter(Mandatory=$false)]$NcSession
    )
    
    Begin{
        #check parameters. Use defaults if needed/available
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
        If (!$CustomerIDs){
            If (!$NcSession.DefaultCustomerID){
                Write-Host "No CustomerID specified."
                Break
            }
            $CustomerIDs = $NcSession.DefaultCustomerID
            Write-Host ("Using current default CustomerID {0} for NCUserRoleList." -f $CustomerIDs)
        }
    }
    Process{
        $Returndata = @()
        ForEach($CustomerID in $CustomerIDs){
            #Write-Output $NcSession.UserRoleList($CustomerID)
            ## Customerid is not returned inside the query-result.
            $ReturnData += $NcSession.UserRoleList($CustomerID) |
                            Select-Object @{n="customerid"; e={$customerid}},
                                        * -ErrorAction SilentlyContinue 
        }
        ## All-parameter NOT provided returns only filtered results.
        If($Filter){
            $Returndata = $Returndata | Where-object {$_.usernames -ne "[]"}
        }
    }
    End{
        If($NoSort){
            $ReturnData 
            #| Write-Output
        }
        Else{
            ## Alphabetical Columnnames
            $ReturnData | Select-Object ($ReturnData|Get-Member -type Noteproperty -ErrorAction SilentlyContinue).name -ErrorAction SilentlyContinue |
            ## Important fields in front. ToDo: Boolean customergroup, derived from groupType.
            Select-Object roleid,readonly,customerid,rolename,* -ErrorAction SilentlyContinue 
            #| Write-Output
        }
    }
}

Function Get-NCUserRoleDetails{
<#
.Synopsis
Returns the Details of the specified RoleID.
 
.Description
Returns the Details of the specified RoleID.
 
#>

    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$false,
                #ValueFromPipeline = $true,
                ValueFromPipelineByPropertyName = $true,
                Position = 0,
                HelpMessage = 'Existing Role IDs')]
                [Alias("roleid")]
        [array]$RoleIDs,

        [Parameter(Mandatory=$false)]$NcSession
    )
    
    Begin{
        #check parameters. Use defaults if needed/available
        If (!$RoleIDs){
            Write-Host "No RoleID specified."
            Break
        }
        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
    }
    Process{
        $Returndata = @()
        ForEach($RoleID in $RoleIDs){
            #Write-Output $NcSession.UserRoleGet($RoleID)
            $ReturnData +=  $NcSession.UserRoleGet($RoleID)
        }
    }
    End{
        $ReturnData 
        #| Write-Output
    }
}
#EndRegion

#Region Tools

Function Backup-NCCustomProperties{
    <#
    .Synopsis
    Backup CustomProperties to a file. Customer or Device. WIP
     
    .Description
    Backup CustomProperties to a file. Customer or Device.
    PathName must be supplied.
    Work In Progress. Currently Customer-Data only.
     
 
    #>

    [CmdletBinding()]

    Param(
        [Parameter(Mandatory=$true,
# ValueFromPipeline = $true,
                ValueFromPipelineByPropertyName = $true,
                Position = 0,
                HelpMessage = 'Existing Customer ID')]
            [Alias("IDs,CustomerID")]
        [array]$CustomerIDs,

        [Parameter(Mandatory=$true,
# ValueFromPipeline = $true,
                #ValueFromPipelineByPropertyName = $true,
                Position = 1,
                HelpMessage = 'Name of the Backup Path')]
            [Alias("Path")]
        [String]$BackupPath,
            
        [Parameter(Mandatory=$false)]$NcSession
    )
    
    Begin{
        #check parameters. Use defaults if needed/available
        
        #Valid Path / sufficient rights
        $DateID =(get-date).ToString(‘yyyyMMdd’)
        $ExportFile = ("{0}\Backup_{1}.json" -f $BackupPath,$DateId)

        ## [System.IO.File]::Exists($ExportFile)
        ## Test-Path $ExportFile -PathType Leaf
        If(Test-Path $ExportFile -PathType Leaf){
            Write-Warning "File already existed"
            Remove-Item -Path $ExportFile -Force
        }


        $ReturnData = $null

        If (!$NcSession){
            If (-not (NcConnected)){
                Break
            }
            $NcSession = $Global:_NCSession
        }
    }
    Process{
        Write-Host "Backup-NCCustomerProperties - Work in Progress"
        Write-host ("Exporting data for Customer(s) {0} to file {1}" -f ($CustomerIDs -join ","),$ExportFile)

        ## Add information about the backup to the output.
        $BUHeader = @{}
        #$BUHeader.Date = $DateID
        $BUHeader.TimeStamp = get-date
        $BUHeader.Customers = ($CustomerIDs -join ",")


        ## Customer-properties (Full)
        $CustomerData = Get-NCCustomerPropertyList $CustomerIDs -full

#<#
        ## Device-Properties (Custom Only)
        [System.Collections.ArrayList]$DeviceData = @()
        Foreach($Customer in $CustomerData){
            $DeviceInfo = ,(Get-NCDeviceList $Customer.CustomerID | 
                Tee-Object -Variable LookupList) | 
                Get-NCDevicePropertyList |
                Select-Object deviceid,
                        @{n="DeviceName" ; e= {$DID=$_.deviceid ; (@($LookUpList).Where({ $_.deviceid -eq $DID})).longname}},
                        @{n="CustomerID" ; e= {$DID=$_.deviceid ; (@($LookUpList).Where({ $_.deviceid -eq $DID})).customerid}},
                        @{n="LastLoggedInUser" ; e= {$DID=$_.deviceid ; (@($LookUpList).Where({ $_.deviceid -eq $DID})).lastloggedinuser}},
                        * -ErrorAction SilentlyContinue

            $DeviceData += $Deviceinfo
        }
#>

        ## ToDo - Filtering/Exclusion based on
        ## -selection(array)
        ## -non-null value
        ## -customer-data only
        ## - Encoding output

        ## Add data to Backup-set
        $BUHeader.CustomerData = $CustomerData
        $BUHeader.DeviceData = $DeviceData
# $BUHeader.CustomerData = $NCSession.ConvertBase64($CustomerData,$false,$false)

        ## Export Backup-set as JSON (Overwrite existing)
        ($BUHeader | 
            ConvertTo-Json -depth 10).ToString() | 
            Out-File $ExportFile -NoClobber -Force

    }
    End{

        #$Returndata
        #| Write-Output
    }
}


#EndRegion


#EndRegion

#Region Module management
# Best practice - Export the individual Module-commands.
## https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/export-modulemember

Export-ModuleMember -Function Get-NCHelp,
Convert-Base64,
Format-Properties,
NcConnected,
New-NCentralConnection,
Get-NCTimeOut,
Set-NCTimeOut,
Get-NCServiceOrganizationList,
Get-NCCustomerList,
Set-NCCustomerDefault,
Get-NCCustomerPropertyList,
Set-NCCustomerProperty,
Get-NCCustomerProperty,
Add-NCCustomerPropertyValue,
Remove-NCCustomerPropertyValue,
Get-NCProbeList,
Get-NCJobStatusList,
Get-NCDeviceList,
Get-NCDeviceID,
Get-NCDeviceLocal,
Get-NCDevicePropertyList,
Get-NCDevicePropertyListFilter,
Set-NCDeviceProperty,
Get-NCDeviceProperty,
Add-NCDevicePropertyValue,
Remove-NCDevicePropertyValue,
Get-NCActiveIssuesList,
Get-NCDeviceInfo,
Get-NCDeviceObject,
Get-NCDeviceStatus,
Get-NCAccessGroupList,
Get-NCAccessGroupDetails,
Get-NCUserRoleList,
Get-NCUserRoleDetails,
Backup-NCCustomProperties,
Get-NCVersion




Write-Debug "Module PS-NCentral loaded"

#EndRegion