PS1EAE.psm1

# BSD 3-Clause License
 #
 # Copyright (c) 2019, Maik Koster
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
 #
 # * Redistributions of source code must retain the above copyright notice, this
 # list of conditions and the following disclaimer.
 #
 # * Redistributions in binary form must reproduce the above copyright notice,
 # this list of conditions and the following disclaimer in the documentation
 # and/or other materials provided with the distribution.
 #
 # * Neither the name of the copyright holder nor the names of its
 # contributors may be used to endorse or promote products derived from
 # this software without specific prior written permission.
 #
 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


#region Public module functions and data

function Find-AEContent {
    <#
    .EXTERNALHELP PS1EAE-help.xml
    #>

    [CmdLetBinding()]
    param(
        # Specified the URI of the 1E ActiveEfficiency server
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$AEServer,

        # Specifies the Content Name to find
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [Alias("ContentName")]
        [string]$Name,

        # Specifies the subnet for the content to find
        # Need to be supplied in CIDR format
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({$_ -match "^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))?$"})]
        [Alias("ipV4Subnet")]
        [string]$Subnet,

        # Specifies the version of the content to find
        [Parameter(Mandatory)]
        [string]$Version,

        # Specifies the extent of the search
        # Can be 'SiteOnly' or 'Subnet'. Default is SiteOnly
        [ValidateSet("SiteOnly", "Subnet")]
        [string]$Extent = "SiteOnly",

        # Specifies the required percentage.
        # Defaults to 100%.
        # Is there a legit reason to look for anything below 100%?
        [int]$Percent = 100,

        # Specifies if all subnets of the site should be included in the search
        [switch]$AllSubnets,

        # Specifies if Device Tag should be excluded from the result
        [switch]$ExcludeDeviceTags,

        # Specifies if the result should be randomized
        [switch]$RandomizeResult,

        # Specifies how many entries should be returned in one batch
        [int]$Top = 100,

        # Specifies how many batches should be skipped
        [int]$Skip = 0
    )

    process{
        $Path = "ContentDelivery"

        $Query = @{
            contentName = $Name
            version = $Version
            percent = $Percent
            ipv4Subnet = $Subnet
            extent = $Extent
        }

        if ($AllSubnets.IsPresent) {
            $Query.allSubnets = $true
        }

        if ($ExcludeDeviceTags.IsPresent) {
            $Query.excludeDeviceTags = $true
        }

        if ($RandomizeResult.IsPresent) {
            $Query.randomizeResult = $true
        }

        # The Web api uses "Take" rather than "Top" on this query
        # Using "Top" as general term in the wrapper to harmonize
        if ($Top -gt 0 ) {
            $Query.Take = $Top
            $Query.Skip = $Skip
        }

        Invoke-AERequest -Method Get -AEServer $AEServer -ResourcePath $Path -Body $Query
    }
}


function Get-AEAllDeviceIDs {
    <#
    .EXTERNALHELP PS1EAE-help.xml
    #>

    [CmdLetBinding()]
    param(
        # Specified the URI of the 1E ActiveEfficiency server
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$AEServer
    )

    process{
        $DeviceIDs = Invoke-AERequest -Method Get -AEServer $AEServer -ResourcePath "AllDeviceIds"

        Write-Output $Device
    }
}


function Get-AEContentDistributionJob {
    <#
    .EXTERNALHELP PS1EAE-help.xml
    #>

    [CmdLetBinding()]
    param(
        # Specified the URI of the 1E ActiveEfficiency server
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$AEServer
    )

    process{
        $Path = "ContentDistributionJobs"

        Invoke-AERequest -Method Get -AEServer $AEServer -ResourcePath $Path
    }
}


function Get-AEDevice {
    <#
    .EXTERNALHELP PS1EAE-help.xml
    #>

    [CmdLetBinding(DefaultParameterSetName="SingleDevice")]
    param(
        # Specified the URI of the 1E ActiveEfficiency server
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$AEServer,

        # Specifies the AE DeviceID
        [Parameter(ParameterSetName="SingleDevice")]
        [string]$DeviceID,

        # Specifies the subnet for all neigboring devices to return
        [Parameter(ParameterSetName="NeighboringDevices")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({$_ -match "^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))?$"})]
        [Alias("ipV4Subnet")]
        [string]$Subnet,

        # Specifies if only the Devide Ids should be returned
        # Doesn't apply if DeviceID is supplied
        [Parameter(ParameterSetName="AllDeviceIDs")]
        [switch]$IDOnly,

        # Specifies how many entries should be returned in one batch
        # Default is 100. Set to 0 to return all entries
        # Doesn't apply if DeviceID is supplied
        [int]$Top = 100,

        # Specifies how many batches should be skipped
        # Doesn't apply if DeviceID is supplied
        [int]$Skip = 0
    )

    process{
        if (-Not([string]::IsNullOrWhiteSpace($DeviceID))) {
            $Path = "Devices/$DeviceID"
        } else {
            if ($Top -gt 0 ) {
                $Body = @{
                    '$Top' = $Top
                    '$Skip' = $Skip
                }
            }

            if (-Not([string]::IsNullOrWhiteSpace($Subnet))) {
                $Path = "Subnets/$($Subnet -replace "/", "!FS")/NeighboringDevices"
            } else {
                if ($IDOnly.IsPresent) {
                    $Path = "AllDeviceIds"
                } else {
                    $Path = "Devices"
                }
            }
        }

        Invoke-AERequest -Method Get -AEServer $AEServer -ResourcePath $Path -Body $Body
    }
}


function Get-AEDeviceAdapterConfiguration {
    <#
    .EXTERNALHELP PS1EAE-help.xml
    #>

    [CmdLetBinding()]
    param(
        # Specified the URI of the 1E ActiveEfficiency server
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$AEServer,

        # Specifies the AE DeviceID
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [Alias("Id")]
        [string]$DeviceID,

        # Specifies the AE device adapter ID.
        [string]$AdapterConfigurationID
    )

    process{
        $Path = "Devices/$DeviceID/AdapterConfigurations"

        if (-Not([string]::IsNullOrWhiteSpace($AdapterConfigurationID))) {
            $Path = "$Path/$AdapterConfigurationID"
        }

        Invoke-AERequest -Method Get -AEServer $AEServer -ResourcePath $Path
    }
}


function Get-AEDeviceContent {
    <#
    .EXTERNALHELP PS1EAE-help.xml
    #>

    [CmdLetBinding()]
    param(
        # Specified the URI of the 1E ActiveEfficiency server
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$AEServer,

        # Specifies the AE DeviceID
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [Alias("Id")]
        [string]$DeviceID,

        # Specifies the AE device content ID.
        [string]$ContentID
    )

    process{
        $Path = "Devices/$DeviceID/ContentDelivery"

        if (-Not([string]::IsNullOrWhiteSpace($ContentID))) {
            $Path = "$Path/$ContentID"
        }

        Invoke-AERequest -Method Get -AEServer $AEServer -ResourcePath $Path
    }
}


function Get-AEDeviceContentDownloadNotification {
    <#
    .EXTERNALHELP PS1EAE-help.xml
    #>

    [CmdLetBinding()]
    param(
        # Specified the URI of the 1E ActiveEfficiency server
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$AEServer,

        # Specifies the AE DeviceID
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$DeviceID,

        # Specifies how many entries should be returned in one batch
        [int]$Top = 100,

        # Specifies how many batches should be skipped
        [int]$Skip = 0,

        # Specifies, if only the latest version should be returned
        [switch]$LatestVersionOnly
    )

    process{
        $Path = "ContentDownloadNotifications"

        $Body = @{
            deviceid=$DeviceID
        }

        if ($Top -gt 0 ) {
            $Body.'$Top' = $Top
            $Body.'$Skip' = $Skip
        }

        if ($LatestVersionOnly.IsPresent) {
            $Body.LatestVersionOnly = $LatestVersionOnly.IsPresent
        }

        Invoke-AERequest -Method Get -AEServer $AEServer -ResourcePath $Path -Body $Body
    }
}


function Get-AEDeviceData {
    <#
    .EXTERNALHELP PS1EAE-help.xml
    #>

    [CmdLetBinding()]
    param(
        # Specified the URI of the 1E ActiveEfficiency server
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$AEServer,

        # Specifies the AE DeviceID
        [string]$DeviceID
    )

    process{
        if (-Not([string]::IsNullOrWhiteSpace($DeviceID))) {
            $DeviceURL = "DeviceData/$DeviceID"
        } else  {
            $DeviceURL = "DevicesData"
        }

        Invoke-AERequest -Method Get -AEServer $AEServer -ResourcePath $DeviceURL
    }
}


function Get-AEDeviceProperties {
    <#
    .EXTERNALHELP PS1EAE-help.xml
    #>

    [CmdLetBinding()]
    param(
        # Specified the URI of the 1E ActiveEfficiency server
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$AEServer,

        # Specifies the AE DeviceID
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [Alias("Id")]
        [string]$DeviceID
    )

    process{
        $Path = "Devices/$DeviceID/SystemProperties"

        Invoke-AERequest -Method Get -AEServer $AEServer -ResourcePath $Path
    }
}


function Get-AEDeviceTag {
    <#
    .EXTERNALHELP PS1EAE-help.xml
    #>

    [CmdLetBinding()]
    param(
        # Specified the URI of the 1E ActiveEfficiency server
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$AEServer,

        # Specifies the AE DeviceID
        [Parameter(Mandatory)]
        [string]$DeviceID,

        # Specifies the Tag Category
        [string]$Category,

        # Specifies the Tag name
        [string]$Name,

        # Specifies the Tag Index
        [int]$Index
    )

    process{
        $Path = "devices/$DeviceID/tags"

        if (-Not([string]::IsNullOrWhiteSpace($Category))) {
            $Path = "$Path/$Category"

            if (-Not([string]::IsNullOrWhiteSpace($Name))) {
                $Path = "$Path/$Name"

                if (-Not([string]::IsNullOrWhiteSpace($Category))) {

                }
            }
        }

        Invoke-AERequest -Method Get -AEServer $AEServer -ResourcePath $Path
    }
}


function Get-AELocalDeviceInfo {
    <#
    .EXTERNALHELP PS1EAE-help.xml
    #>

    [CmdLetBinding()]
    param()

    process{
        if (Test-Path -Path "HKLM:\\SOFTWARE\1E\NomadBranch\ActiveEfficiency") {
            $AE = Get-ItemProperty -Path "HKLM:\\SOFTWARE\1E\NomadBranch\ActiveEfficiency" -ErrorAction SilentlyContinue

            if ($null -ne $AE) {
                $AEInfo = [PSCustomObject]@{
                    DeviceID = $AE.DeviceID
                    DeviceType = $AE.NomadDeviceType
                    AdapterConfigurationId = $AE.AdapterConfigurationId
                    ipv4 = $AE.ipv4
                    ipv4Subnet = $AE.ipv4Subnet
                    PlatformUrl = $AE.PlatformUrl
                    ContentProvider = ($AE.ContentProvider -eq 1)
                    ContentRegistration = ($AE.ContentRegistraiton -eq 1)
                }
            }
        }

        if (($null -ne $AEInfo) -and  (Test-Path -Path "HKLM:\\SOFTWARE\1E\Common")) {
            $1E = Get-ItemProperty -Path "HKLM:\\SOFTWARE\1E\Common" -ErrorAction SilentlyContinue

            if ($null -ne $1E) {
                $AEInfo | Add-Member -MemberType NoteProperty -Name "HardwareId" -Value ($1E.HardwareId)
                $AEInfo | Add-Member -MemberType NoteProperty -Name "UniqueIdentifier" -Value ($1E.UniqueIdentifier)
            }
        }

        Write-Output $AEInfo
    }
}


function Get-AELocation {
    <#
    .EXTERNALHELP PS1EAE-help.xml
    #>

    [CmdLetBinding()]
    param(
        # Specified the URI of the 1E ActiveEfficiency server
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$AEServer,

        # Specifies the AE LocationID
        [string]$LocationID,

        # Specifies how many entries should be returned in one batch
        # Default is 100. Set to 0 to return all entries
        # Doesn't apply if LocationID is supplied
        [int]$Top = 100,

        # Specifies how many batches should be skipped
        # Doesn't apply if LocationID is supplied
        [int]$Skip = 0
    )

    process{
        $Path = "Locations"

        if (-Not([string]::IsNullOrWhiteSpace($LocationID))) {
            $Path = "$Path/$LocationID"
        } else {
            if ($Top -gt 0 ) {
                $Body = @{
                    '$Top' = $Top
                    '$Skip' = $Skip
                }
            }
        }

        Invoke-AERequest -Method Get -AEServer $AEServer -ResourcePath $Path -Body $Body
    }
}


function New-AEDevice {
    <#
    .EXTERNALHELP PS1EAE-help.xml
    #>

    [CmdLetBinding(DefaultParameterSetName="ByUUID")]
    param(
        # Specified the URI of the 1E ActiveEfficiency server
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$AEServer,

        [Parameter(Mandatory)]
        [string]$HostName,

        [Parameter(Mandatory)]
        [string]$DomainName,

        # Specifies the Fqdn of the machine.
        # If not specified, it will be created based on Hostname and Domain name
        [string]$Fqdn,

        # Specifies a list of Identities. At least one must be defined
        # Each Identity should contain
        # - Source (eg "SMBIOS", or "FQDN")
        # - Identity (eg "AEC72B25-6D7E-11E1-8967-452301000030", or "MyMachine.Domain.com")
        # - CreatedBy
        [Parameter(Mandatory, ParameterSetName="ByIdentities")]
        [PSCustomObject[]]$Identities,

        # Specifies the SMBIOS GUID
        [Parameter(Mandatory, ParameterSetName="ByUUID")]
        [string]$SMBiosGuid,

        # Specifies the Nomad UniqueIdentifier
        # Typically stored in HKLM:\SOFTWARE\1E\Common
        [Parameter(ParameterSetName="ByUUID")]
        [string]$NomadUUID,

        # Specifies the Device Type
        [ValidateSet("Unknown", "Desktop", "Laptop", "Server")]
        [string]$Type = "Unknown",

        # Specifies the Device Type ID
        # Can be specified if the underlying value is known
        # Takes precedence over "Type" if specified
        [int]$TypeID = -1,

        # Specifies the Creator of the entry
        [string]$CreatedBy = "1E Nomad"
    )

    process{

        $Path = "Devices"

        if ([string]::IsNullOrWhiteSpace($Fqdn)) {
            if (-Not([string]::IsNullOrWhiteSpace($DomainName))) {
                $Fqdn = "{0}.{1}" -f $Hostname, $DomainName
            }
        }

        if ($TypeID -lt 0) {
            switch ($Type) {
                "Unknown" {$TypeID = 0; break}
                "Desktop" {$TypeID = 1; break}
                "Laptop" {$TypeID = 2; break}
                "Server" {$TypeID = 3; break}
            }
        }

        if (-Not([string]::IsNullOrWhiteSpace($SMBiosGuid))) {
            $DeviceIdentities = @(@{Source = "SMBIOS"; Identity = $SMBiosGuid}, @{Source = "FQDN"; Identity = $FQDN})

            if (-Not([string]::IsNullOrWhiteSpace($NomadUUID))) {
                $DeviceIdentities += @{Source = "MACHINE_ID"; Identity = $NomadUUID}
            }
        } else {
            $DeviceIdentities = $Identities
        }

        $Body = @{
            Hostname = $HostName
            DomainName = $DomainName
            Fqdn = $Fqdn
            CreatedBy = $CreatedBy
            Type = $TypeID
            DeviceIdentities = $DeviceIdentities
        }

        Invoke-AERequest -Method Put -AEServer $AEServer -ResourcePath $Path -Body $Body
    }
}


function New-AEDeviceAdapterConfiguration {
    <#
    .EXTERNALHELP PS1EAE-help.xml
    #>

    [CmdLetBinding(SupportsShouldProcess)]
    param(
        # Specified the URL of the 1E ActiveEfficiency server
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$AEServer,

        # Specifies the AE DeviceID
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$DeviceID,

        # Specifies the IPv4 address of the adapter
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [Alias("IPv4")]
        [string]$IP,

        # Specifies the IPv4 subnet for the adapter
        # Need to be supplied in CIDR format
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({$_ -match "^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))?$"})]
        [Alias("ipV4Subnet")]
        [string]$Subnet
    )

    process{
        $Path = "Devices/$DeviceID/AdapterConfigurations"

        $Adapter = @{
            Ipv4 = $IP
            Ipv4Subet = $Subnet
        }

        Invoke-AERequest -Method Post -AEServer $AEServer -ResourcePath $Path -Body $Adapter
    }
}


function New-AEDeviceContent {
    <#
    .EXTERNALHELP PS1EAE-help.xml
    #>

    [CmdLetBinding()]
    param(
        # Specified the URL of the 1E ActiveEfficiency server
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$AEServer,

        # Specifies the AE DeviceID
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$DeviceID,

        # Specifies the content name
        # Typically the SCCM PackageID or Content ID of the application or update
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [Alias("ContentName")]
        [string]$Name,

        # Specifies the content version
        [Parameter(Mandatory)]
        [ValidateScript({$_ -ge 1})]
        [int]$Version,

        # Specifies the size of the content in Bytes
        [ValidateScript({$_ -gt 0})]
        [int]$Size,

        # Specifies the number of files of the content.
        [int]$NumberOfFiles = 1,

        # Specifies the current percentage of the downloaded content
        # Will be 100% on default.
        [int]$Percent = 100,

        # Specifies the starttime of the download
        [datetime]$StartTime = (Get-Date),

        # Specifies the endtime of the download
        [datetime]$EndTime = (Get-Date)
    )

    process{
        $Path = "Devices/$DeviceID/ContentDelivery"

        $Content = @{
            DeviceId = $DeviceID
            ContentName = $Name
            Version = $Version
            Percent = $Percent
            Size = $Size
            NumberOfFiles = $NumberOfFiles
            StartTime = $StartTime.ToUniversalTime().ToString('u')
            EndTime = $EndTime.ToUniversalTime().ToString('u')
        }

        Invoke-AERequest -Method Post -AEServer $AEServer -ResourcePath $Path -Body $Content
    }
}


function New-AEDeviceTag {
    <#
    .EXTERNALHELP PS1EAE-help.xml
    #>

    [CmdLetBinding()]
    param(
        # Specified the URL of the 1E ActiveEfficiency server
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$AEServer,

        # Specifies the AE DeviceID
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [Alias("Id")]
        [string]$DeviceID,

        # Specifies the category (namespace) for the tag.
        # The category and name uniquely identify the resource.
        # Category is mandatory. It must not be null, empty or only whitespace.
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Category,

        # Specifies the tag name.
        # Name is mandatory. It must not be null, empty or only whitespace.
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        # Specifies the tag index.
        # Will be 0 on default.
        [int]$Index,

        # Specifies the actual value of the tag.
        # TODO: Implement handling of different types
        [string]$Value,

        # Specifies the name of the client creating the tag entry.
        # Currently not mandatory.
        [string]$CreatedBy = "1E Nomad"
    )

    process{
        $Path = "devices/$DeviceID/tags"

        $Tag = @{
            Category = $Category
            Name = $Name
            Index = $Index
            Type = 0
            StringValue = ($Value.ToString())
        }

        if (-Not([string]::IsNullOrWhiteSpace($CreatedBy))) {
            $Tag.CreatedBy = $CreatedBy
        }

        Invoke-AERequest -Method Post -AEServer $AEServer -ResourcePath $Path -Body $Tag
    }
}


function New-AELocation {
    <#
    .EXTERNALHELP PS1EAE-help.xml
    #>

    [CmdLetBinding()]
    param(
        # Specified the URL of the 1E ActiveEfficiency server
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$AEServer,

        # Specifies the Site name
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$Site,

        # Specifies the Subnet identifier.
        # Must be in CIDR format
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({$_ -match "^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))?$"})]
        [string]$Subnet
    )

    process{
        $Path = "Locations"

        $Location = @{
            Site = $Site
            Subnet = $Subnet
        }

        Invoke-AERequest -Method Post -AEServer $AEServer -ResourcePath $Path -Body $Location
    }
}


function Remove-AEDeviceAdapterConfiguration {
    <#
    .EXTERNALHELP PS1EAE-help.xml
    #>

    [CmdLetBinding(SupportsShouldProcess)]
    param(
        # Specified the URI of the 1E ActiveEfficiency server
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$AEServer,

        # Specifies the AE DeviceID
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$DeviceID,

        # Specifies the AE DeviceID
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [Alias("Id")]
        [string]$AdapterConfigurationID
    )

    process{
        $Path = "Devices/$DeviceID/AdapterConfigurations/$AdapterConfigurationID"

        Invoke-AERequest -Method Delete -AEServer $AEServer -ResourcePath $Path
    }
}


function Remove-AEDeviceContent {
    <#
    .EXTERNALHELP PS1EAE-help.xml
    #>

    [CmdLetBinding(SupportsShouldProcess)]
    param(
        # Specified the URI of the 1E ActiveEfficiency server
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$AEServer,

        # Specifies the AE DeviceID
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$DeviceID,

        # Specifies the AE device content ID.
        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias("Id")]
        [string]$ContentID
    )

    process{
        $Path = "devices/$DeviceID/ContentDelivery"

        if (-Not([string]::IsNullOrWhiteSpace($ContentID))) {
            $Path = "$Path/$ContentID"
        }
        Invoke-AERequest -Method Delete -AEServer $AEServer -ResourcePath $Path
    }
}


function Remove-AELocation {
    <#
    .EXTERNALHELP PS1EAE-help.xml
    #>

    [CmdLetBinding(SupportsShouldProcess)]
    param(
        # Specified the URI of the 1E ActiveEfficiency server
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$AEServer,

        # Specifies the AE LocationID
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [Alias("Id")]
        [string]$LocationID
    )

    process{
        $Path = "Locations/$LocationID"

        Invoke-AERequest -Method Delete -AEServer $AEServer -ResourcePath $Path
    }
}


function Update-AEDevice {
    <#
    .EXTERNALHELP PS1EAE-help.xml
    #>

    [CmdLetBinding()]
    param(
        # Specified the URI of the 1E ActiveEfficiency server
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$AEServer,

        # Specifies the Device Id
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [Alias("Id")]
        [string]$DeviceID,

        # Specifies the Hostname
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string]$HostName,

        # Specifies the Domain Name
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]$DomainName,

        # Specifies the Fqdn of the machine.
        # If not specified, it will be created based on Hostname and Domain name
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]$Fqdn,

        # Specifies a list of Identities. At least one must be defined
        # Each Identity should contain
        # - Source (eg "SMBIOS", or "FQDN")
        # - Identity (eg "AEC72B25-6D7E-11E1-8967-452301000030", or "MyMachine.Domain.com")
        # - CreatedBy
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [PSCustomObject[]]$Identities,

        [Parameter(ValueFromPipelineByPropertyName)]
        [int]$Type,

        # Specifies the Creator of the entry
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]$CreatedBy = "1E Nomad"
    )

    process{

        $Path = "Devices"

        if ([string]::IsNullOrWhiteSpace($Fqdn)) {
            if (-Not([string]::IsNullOrWhiteSpace($DomainName))) {
                $Fqdn = "{0}.{1}" -f $Hostname, $DomainName
            }
        }

        $Body = @{
            Id = $DeviceID
            Hostname = $HostName
            DomainName = $DomainName
            Fqdn = $Fqdn
            CreatedBy = $CreatedBy
            Type = $Type
            DeviceIdentities = $Identities
        }

        Invoke-AERequest -Method Put -AEServer $AEServer -ResourcePath $Path -Body $Body
    }
}


function Update-AEDeviceAdapterConfiguration {
    <#
    .EXTERNALHELP PS1EAE-help.xml
    #>

    [CmdLetBinding(SupportsShouldProcess)]
    param(
        # Specified the URL of the 1E ActiveEfficiency server
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$AEServer,

        # Specifies the AE DeviceID
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$DeviceID,

        # Specifies the AE DeviceID
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [Alias("Id")]
        [string]$AdapterConfigurationID,

        # Specifies the IPv4 address of the adapter
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [Alias("IPv4")]
        [string]$IP,

        # Specifies the IPv4 subnet for the adapter
        # Need to be supplied in CIDR format
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({$_ -match "^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))?$"})]
        [Alias("ipV4Subnet")]
        [string]$Subnet
    )

    process{
        $Path = "Devices/$DeviceID/AdapterConfigurations"

        $Adapter = @{
            Id = $AdapterConfigurationID
            Ipv4 = $IP
            Ipv4Subet = $Subnet
        }

        Invoke-AERequest -Method Put -AEServer $AEServer -ResourcePath $Path -Body $Adapter
    }
}


function Update-AEDeviceContent {
    <#
    .EXTERNALHELP PS1EAE-help.xml
    #>

    [CmdLetBinding(SupportsShouldProcess)]
    param(
        # Specified the URL of the 1E ActiveEfficiency server
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$AEServer,

        # Specifies the AE DeviceID
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$DeviceID,

        # Specifies the AE ContentID
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [Alias("Id")]
        [string]$ContentID,

        # Specifies the current percentage of the downloaded content
        # Will be 100% on default.
        [Parameter(ValueFromPipelineByPropertyName)]
        [int]$Percent = 100,

        # Specifies the endtime of the download
        [Parameter(ValueFromPipelineByPropertyName)]
        [datetime]$EndTime
    )

    process{
        $Path = "Devices/$DeviceID/ContentDelivery"

        $Content = @{
            Id = $ContentID
            DeviceId = $DeviceID
        }

        if ($Percent -gt 0) {
            $Content.Percent = $Percent
        }

        if ($null -ne $EndTime) {
            $Content.EndTime = $EndTime.ToUniversalTime().ToString('u')
        }

        Invoke-AERequest -Method Put -AEServer $AEServer -ResourcePath $Path -Body $Content
    }
}


function Update-AEDeviceTag {
    <#
    .EXTERNALHELP PS1EAE-help.xml
    #>

    [CmdLetBinding()]
    param(
        # Specified the URL of the 1E ActiveEfficiency server
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$AEServer,

        # Specifies the AE DeviceID
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [Alias("Id")]
        [string]$DeviceID,

        # Specifies the category (namespace) for the tag.
        # The category and name uniquely identify the resource.
        # Category is mandatory. It must not be null, empty or only whitespace.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$Category,

        # Specifies the tag name.
        # Name is mandatory. It must not be null, empty or only whitespace.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        # Specifies the tag index.
        # Will be 0 on default.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [int]$Index,

        # Specifies the actual value of the tag.
        # TODO: Implement handling of different types
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]$Value,

        # Specifies the name of the client creating the tag entry.
        # Currently not mandatory.
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]$CreatedBy = "1E Nomad"
    )

    process{
        $Path = "devices/$DeviceID/tags"

        $Tag = @{
            Category = $Category
            Name = $Name
            Index = $Index
            Type = 0
            StringValue = ($Value.ToString())
        }

        if (-Not([string]::IsNullOrWhiteSpace($CreatedBy))) {
            $Tag.CreatedBy = $CreatedBy
        }

        Invoke-AERequest -Method Post -AEServer $AEServer -ResourcePath $Path -Body $Tag
    }
}


#endregion

#region Private Module functions and data

# Helper function to convert the Nomad Datetimestring into a usable DateTime format
function Convert-DateString {
    [CmdLetBinding()]
    param(
        [Parameter(ValueFromPipeline)]
        [String]$Date,
        [String[]]$Format = 'yyyyMMddHHmmssfff'
    )

    process{
        $result = New-Object DateTime

        $convertible = [DateTime]::TryParseExact(
            $Date,
            $Format,
            [System.Globalization.CultureInfo]::InvariantCulture,
            [System.Globalization.DateTimeStyles]::None,
            [ref]$result)

        if ($convertible) { $result }
    }
 }

# Just a generic function to centrally handle errors, logging etc when executing the web requests
function Invoke-AERequest {

    [CmdLetBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$AEServer,

        [Parameter(Mandatory)]
        [string]$ResourcePath,

        [ValidateSet("Get", "Post", "Put", "Delete")]
        [string]$Method = "Get",

        $Body
    )

    process {
        $InvokeParams = @{
            Method = $Method
            Uri = Join-Parts -Separator '/' -ReplaceSeparator '\' -Parts $AEServer, $ResourcePath
            ContentType = "application/json"
            Headers = @{ACCEPT="application/json"}
        }

        # Add Body if supplied
        if ($null -ne $Body) {
            if ($Method -eq "Get") {
                # Get can't handle json body.
                # will add the parameters to the url instead.
                $InvokeParams.Body = $Body
            } else {
                $InvokeParams.Body = ConvertTo-Json $Body
            }
        }

        try {
            $Result = Invoke-RestMethod @InvokeParams -ErrorVariable RestError -ErrorAction SilentlyContinue
        } catch {
            # Handle certain exceptions in a better way
            if ($_.exception -is [System.Net.WebException]) {
                $HttpStatusCode = $_.exception.Response.StatusCode.Value__
                $HttpStatusDescription = $_.exception.Response.StatusDescription

                if ($HttpStatusCode -eq 404) {
                    # Resource not found. Just return $null.
                    Write-Verbose $HttpStatusDescription
                } else {
                    Throw $_
                }
            } else {
                Throw $_
            }
        }

        if ($RestError) {
            $HttpStatusCode = $RestError.ErrorRecord.Exception.Response.StatusCode.value__
            $HttpStatusDescription = $RestError.ErrorRecord.Exception.Response.StatusDescription
            Write-Verbose $HttpStatusDescription
            #Throw "Http Status Code: $($HttpStatusCode) `nHttp Status Description: $($HttpStatusDescription)"
        }

        Write-Output $Result
    }
}

function Join-Parts {
    <#
        .SYNOPSIS
        Joins multiple parts of an URI.
 
        .DESCRIPTION
        Helper method to properly join multiple parts of an URI
 
    #>

    [CmdLetBinding()]
    param(

        $Parts = $null,
        $Separator = '',
        $ReplaceSeparator
    )

    process {
        ($Parts | Where-Object { $_ } | Foreach-Object{if(-Not([string]::IsNullOrEmpty($ReplaceSeparator))) {$_ -replace [regex]::Escape($ReplaceSeparator), $Separator} else {$_}} | ForEach-Object { ([string]$_).trim($Separator) } | Where-Object { $_ } ) -join $Separator
    }
}

#endregion

#region Module Initialization

# Put initialization code here
#endregion