Skip to content

Instantly share code, notes, and snippets.

@joshooaj
Created June 19, 2019 18:12
Show Gist options
  • Save joshooaj/f47c8420468418fa1e11f4eb7097ba9f to your computer and use it in GitHub Desktop.
Save joshooaj/f47c8420468418fa1e11f4eb7097ba9f to your computer and use it in GitHub Desktop.
MilestonePSTools Examples

MilestonePSTools PowerShell Module

Description

MilestonePSTools is my (Josh Hendricks) side project exploring the various ways PowerShell can be used to enable automation and overall enhance the experience of configuring or querying a Milestone XProtect VMS environment. This module is written in C# and is effectively a PowerShell-friendly wrapper around Milestone's MIP SDK.

There is nothing in this module that is not possible to build using the .NET components from the MIP SDK directly in your scripts, but doing so quickly becomes complicated, and difficult to maintain. This was my experience when exploring writing PS1 scripts where I imported MIP SDK dll's directly into my scripts, handled authentication and .NET object instantiation for any and all components I needed. So I watched the PowerShell Cmdlet Development In C# course on PluralSight and set out to make a more PowerShell-friendly set of commands for interacting with a Milestone XProtect VMS environment.

Support / Bugs / Documentation

Because this is my personal side project and is not yet an initiative driven by Product Management / R&D, it is not a tool that can be supported by Milestone's technical support team. Most of our technical staff have very little (if any) knowledge of this module. I am happy to personally receive feedback, bug reports, and feature requests, but there is absolutely no guarantee of ongoing support and you assume all risks of damages as a result of bugs, unintended consequences of features and commands, or loss of access to the module in the event it is decided to discard or limit access to the tool in the future.

I am in the process of adding Get-Help documentation to this module. When I started, it wasn't clear to me how to build this documentation without hand-writing the XML. I later came across a great Nuget package, XmlDoc2CmdletDoc by Redgate. I'm slowly adding documentation to this module, but there are a lot of cmdlets to document (~90 as of today) and as soon as I begin to document a cmdlet, I discover bugs or Parameters I don't like, and it becomes a process of simultaneously documenting and refactoring that command. But as I get through each one, I am doing my best to include descriptions of the parameters and examples of how you would use the cmdlet. So if you're in doubt, check Get-Help -Detailed to see if the documentation is already available.

Demo

The following video demonstrates how to install and begin using this module IMAGE ALT TEXT

Prerequisits for installation/using this module

  • Milestone XProtect VMS 2014 or newer
    • XProtect Corporate, Expert, Professional+, Express+, and Essential+
    • Not yet tested on older than 2018 R2
    • Not compatible with older "non-Plus" or "E-code" products like XProtect Professional
  • .NET 4.7
  • PowerShell 5.1

Prerequisits for building/compiling in Visual Studio

  • Visual Studio 2019
  • .NET 4.7
  • Milestone MIP SDK 2019 R1 or later
    • VideoOS*.dll binaries are expected to be found in C:\Program Files\Milestone\MIPSDK\Bin\

Install MilestonePSTools

This module is currently hosted in PowerShell Gallery so it can be downloaded from an internet-connected PowerShell session by running the following command

# Install the latest module for the current user which does not require elevated privileges, 
# and upgrade if a newer version is available
Install-Module MilestonePSTools -Scope CurrentUser -Force

# Install for all users - requires elevated privileges
Install-Module MilestonePSTools

If you do not have an internet connection where you need to use the module, you can download it manually from PSGallery, and transport it to the computer you want to install it on. Here's what that process looks like.

  1. Go to MilestonePSTools on PowerShell Gallery: https://www.powershellgallery.com/packages/MilestonePSTools
  2. Under Installation Options, choose Manual Download
  3. Click Download the raw nupkg file
    • The nupkg file is effectively just a ZIP file with the module and some nuget metadata inside
  4. Transport the *.nupkg file to the destination computer, rename to *.zip, and extract into either the default "All users" or "Current User" path.
  5. The folder structure should look like the following
    • All Users: C:\Program Files\WindowsPowerShell\Modules\MilestonePSTools<version>*
    • Current User: C:\Users<user>\Documents\WindowsPowerShell\Modules\MilestonePSTools<version>*

Examples

Without further ado, here are some examples to inspire you to explore the power of PowerShell and Milestone!

In each example, it is assumed you have the module installed, and have connected to the Management Server. Here are some examples of how you would login

# -Server parameter should be the Management Server IP or hostname

# Login to myserver using the current Windows User
Connect-ManagementServer -Server myserver

# Login to myserver as the Windows or AD user xprotect\josh
Connect-ManagementServer -Server myserver -Username xprotect\josh -Password s3cretp@ss

# Login to myserver as the basic user "Josh"
Connect-ManagementServer -Server myserver -Username Josh -Password s3cretp@ss -BasicUser

# Login to myserver as the Windows or AD user xprotect\josh and also login to all child 
# sites in a Milestone Federated Architecture environment
Connect-ManagementServer -Server myserver -Username xprotect\josh -Password s3cretp@ss -IncludeChildSites

# You should remain connected for the duration of the powershell session or until you disconnect
Disconnect-ManagementServer

Add Hardware Using Universal Driver

# Set $recorderName to the display name of the desired Recording Server
# Set $deviceGroupPath to the desired Camera Group to place new cameras in.
# In this case, /Demo would be a camera group named Demo at the root of the list of camera groups
# Alternatively you could use a path like /Demo/Subgroup which would reference a camera group named 
# Subgroup underneath a group called Demo at the root of the list of camera groups.
$recorderName = 'My Recording Server'
$deviceGroupPath = '/Demo'

# Retrieve Recording Server and Add-Hardware to it
$rs = Get-RecordingServer -Name $recorderName
$hw = $rs | Add-Hardware -Address 'http://wowzaec2demo.streamlock.net' -UseDefaultCredentials -DriverId 421 -GroupPath $deviceGroupPath -Enabled

# Select the camera device on the new hardware and configure it
$camera = $hw | Get-Camera -Channel 0
$camera | Set-CameraSetting -Stream -StreamNumber 0 -Name FPS -Value 25
$camera | Set-CameraSetting -Stream -StreamNumber 0 -Name StreamingMode -Value 'RTP over RTSP (TCP)'
$camera | Set-CameraSetting -Stream -StreamNumber 0 -Name ConnectionURI -Value 'vod/mp4:BigBuckBunny_115k.mov'

# Select the microphone, configure it, and enable it
$microphone = $hw | Get-Microphone -Channel 0
$microphone | Set-MicrophoneSetting -Stream -StreamNumber 0 -Name Codec -Value AAC
$microphone | Set-MicrophoneSetting -Stream -StreamNumber 0 -Name ConnectionURI -Value 'vod/mp4:BigBuckBunny_115k.mov'
$microphone | Set-MicrophoneSetting -Stream -StreamNumber 0 -Name StreamingMode -Value 'RTP over RTSP (TCP)'
$microphone.Enabled = $true; $microphone.Save()
$micGroup = Add-DeviceGroup -DeviceCategory Microphone -Path $deviceGroupPath

Add Hardware from CSV and modify permissions to grant rights to a specific role

$newHardwareRows = Import-Csv -Path .\newhardware.csv
foreach ($record in $newHardwareRows)
{
    $rs = Get-RecordingServer -Name $record.RecordingServer
    $driverNumber = $record.DriverId
    $hw = $rs | Add-Hardware -Address $record.Address -UserName $record.UserName -Password $record.Password -Name $record.Name -GroupPath $record.Group -Enabled
    $acl = $hw | Get-Camera | Get-DeviceAcl -RoleName MyUsers
    
    $acl.SecurityAttributes["GENERIC_READ"] = "True"
    $acl.SecurityAttributes["VIEW_LIVE"] = "True"
    $acl.SecurityAttributes["PLAYBACK"] = "True"
    $acl.SecurityAttributes["PTZ_CONTROL"] = "True"
    $acl.SecurityAttributes["READ_BOOKMARKS"] = "True"
    $acl.SecurityAttributes["READ_EVIDENCE_LOCK"] = "True"
    $acl.SecurityAttributes["READ_SEQUENCES"] = "True"

    $acl | Set-DeviceAcl
}

Export Harware Info to CSV

$hardwareInfo = New-Object -TypeName System.Collections.ArrayList
foreach ($rec in Get-RecordingServer)
{
    foreach ($hardware in $rec | Get-Hardware)
    {
        $driver = $hardware | Get-HardwareDriver

        $row = New-Object -TypeName PSObject
        $row | Add-Member -MemberType NoteProperty -Name Name -Value $hardware.Name
        $row | Add-Member -MemberType NoteProperty -Name Enabled -Value $hardware.Enabled
        $row | Add-Member -MemberType NoteProperty -Name Address -Value $hardware.Address
        $row | Add-Member -MemberType NoteProperty -Name UserName -Value $hardware.UserName
        $row | Add-Member -MemberType NoteProperty -Name Password -Value ($hardware | Get-HardwarePassword)
        $row | Add-Member -MemberType NoteProperty -Name MacAddress -Value ($hardware | Get-HardwareSetting -Name MacAddress).Value
        $row | Add-Member -MemberType NoteProperty -Name DriverName -Value $driver.Name
        $row | Add-Member -MemberType NoteProperty -Name DriverNumber -Value $driver.Number
        $row | Add-Member -MemberType NoteProperty -Name HardwareId -Value $hardware.Id
        $row | Add-Member -MemberType NoteProperty -Name RecordingServerName -Value $rec.Name
        $row | Add-Member -MemberType NoteProperty -Name RecordingServerId -Value $rec.Id        
        $hardwareInfo.Add($row)
    }
}

$hardwareInfo | Export-Csv -Path $csvFilePath -NoTypeInformation

Export the last 24 hours-worth of audit logs to CSV

Get-Log -LogType Audit -Tail -Minutes 1440 | Export-Csv -Path .\auditLogs.csv -NoTypeInformation

Export all evidence locked video in XProtect database format

foreach ($evidenceLock in Get-EvidenceLock)
{
    # The -Force flag ensures the export completes even if video for one of the cameras is unreachable
    # due to an unreachable Recording Server or other media database issues.
    $evidenceLock | Start-Export -Path \\path\to\exports -Format DB -Force
}

Export only evidence locks that have not already been exported

This example is meant to be used as a script that is executed daily for example, to ensure all evidence locked video has been backed up to an "offline" location which is immune to any issues that may be encountered on the Recording Server such as disk failures or bugs, as well as accidental deletion by users.

The script performs the following tasks:

  1. Enumerate evidence locks and check in the specified export path whether a folder exists with the same name as the ID (GUID) of the Evidence Lock entry
  2. If the evidence lock has already been exported, it is skipped. If not, we create a new Database export from the devices in the evidence lock
  3. If an error occurs, the export folder is deleted to ensure it is retried on the next execution. If it is successful, the folder with the unfriendly GUID name is modified by placing a Desktop.ini file inside with a LocalizedResourceName property. This forces Windows Explorer to show you a friendly name for the evidence lock (the Header/Title) rather than the real folder name which is in a format like "45391ad2-e927-4ee4-8fd8-d7d4a1bc7d22"
  4. The number of completed, failed, and skipped evidence locks are documented at the end of execution and a transcript log is saved to the export path.
$exportRootPath = "C:\exports"
$server = 'mgmtserver'
$user = 'domain\user'
$pass = 'password'

function New-DesktopIni {
    param (
        [string]
        $Path,
        [string]
        $LocalizedResourceName
    )
    $filePath = Join-Path -Path $Path -ChildPath desktop.ini
    $content = "[.ShellClassInfo]
ConfirmFileOp=0
LocalizedResourceName=$LocalizedResourceName"
    Set-ItemProperty $Path -Name Attributes -Value "ReadOnly,System"
    New-Item -Path $filePath -ItemType file -Value $content | Out-Null
}

$date = Get-Date -Format yyyy-MM-dd
Start-Transcript -Path (Join-Path -Path $exportRootPath -ChildPath "ExportLog_$date.txt") -IncludeInvocationHeader -Append
Connect-ManagementServer -Server $server -Username $user -Password $pass
$status = @{ 
    Total = 0
    Skipped = 0
    Succeeded = 0
    Failed = 0
}
foreach ($lock in Get-EvidenceLock) {
    $status.Total++
    Write-Information "Processing Evidence Lock '$($lock.Header)' with ID '$($lock.Id)'"
    $exportPath = Join-Path -Path $exportRootPath -ChildPath $lock.Id
    if (Test-Path -Path $exportPath) {
        Write-Information "Export already exists at path '$exportPath'"
        $status.Skipped++
        continue 
    }
    try {
        Write-Information "Creating export directory and setting LocalizedResourceName in desktop.ini"
        New-Item -Path $exportPath -ItemType Directory | Out-Null
        New-DesktopIni -Path $exportPath -LocalizedResourceName $lock.Header.Trim()
        Write-Information "Starting export. . ."
        $lock | Start-Export -Path $exportRootPath -Name $lock.Id -Format DB -ErrorAction Stop
        $status.Succeeded++
        Write-Information "Export completed for Evidence Lock with ID '$($lock.Id)'"
    }
    catch {
        $status.Failed++
        Write-Warning "Export failed for Evidence Lock with ID '$($lock.Id)', Error: $($_.Exception.Message)"
        Write-Information "Cleaning up directory of failed export. . ."
        Remove-item -Path $exportPath -Recurse -ErrorAction Stop -Force
    }
}
Write-Information "Evidence Locks Total: $($status.Total), Succeeded: $($status.Succeeded), Failed: $($status.Failed), Skipped: $($status.Skipped)"
Stop-Transcript

Export Name, Description, and semi-colon delimited list of users for each Role to CSV

$roles = @()
foreach ($role in Get-Role) {
    $row = $role | select Name, Description
    $members = ''
    $userList = $role | Get-User
    if ($userList -ne $null) {
        $members = [string]::Join(";", ($userList | % { "$($_.Domain)\$($_.AccountName)" }))
    }
    $row | Add-Member -MemberType NoteProperty -Name Members -Value $members
    $roles += $row
}
$roles | Export-Csv -Path roles.csv -NoTypeInformation

Get JPEG snapshots from all enabled cameras on all sites in an MFA hierarchy

foreach ($site in Get-Site -ListAvailable)
{
    $site | Select-Site
    $enabledHardware = Get-Hardware | Where-Object Enabled
    $enabledCameras = $enabledHardware | Get-Camera | Where-Object Enabled
    
    # Get live jpeg thumbnail with size of 320x240 and save it to a file using the display name of the camera
    $enabledCameras | Get-Snapshot -Live -Width 320 -Height 240 -KeepAspectRatio -IncludeBlackBars -Save -Path C:\demo\thumbnails -UseFriendlyName
    
    # Get a jpeg image at or near 4:09PM local time
    $enabledCameras | Get-Snapshot -Timestamp '2019-04-30 4:09 PM' -LocalTimestamp

    # Get the timestamp of the first recorded image
    $enabledCameras | Get-Snapshot -Behavior GetBegin | select DateTime
}

Find any cameras which seem to have no recordings

$cameras = Get-Hardware | Where-Object Enabled | Get-Camera | Where-Object Enabled
foreach ($camera in $cameras)
{
    $recordingExist = $camera | Test-Playback -Timestamp '2019-04-30 5:35 PM' -LocalTimestamp -Mode Any
    if (!$recordingExist)
    {
        # Do something
    }
}

Copy all hardware devices and their settings/permissions

This script was written to automate the process of setting up a set of redundant Recording Servers under the same Management Server as the original Recording Servers. All cameras were desired to be recording at two different datacenters simultaneously. All cameras had already been added and setup at datacenter A and now we needed to get the hardware added/configured at datacenter B. For nearly 1000 cameras, this would have taken several days and been a very error-prone task.

The script enumerates hardware from the source recording server, uses the information to add that hardware to the designated destination recording server, then enumerates all camera, mic, speaker, input, output, and metadata to set their display names, enabled/disabled state, and enumerates all of their stream settings and PTZ presets to ensure they match on the destination server, and also enumerates all roles and copies the permissions from the source devices to the destination devices to ensure users have the same permissions on all device instances whether they be present in datacenter A or B.

Function CopyAcls($source, $destination, $roles)
{
    foreach ($role in $roles)
    {
        $acl = $source | Get-DeviceAcl -Role $role -RoleName *
        $acl.Path = $destination.Path
        Write-Host "Setting permissions for $($role.Name) on $($destination.Name)"
        $acl | Set-DeviceAcl
    }
}

# CSV format example
# SourceRecorderId, DestinationRecorderId
# 4703a5ba-b41b-4eb7-8a10-8ac138f75db7, 7d1796ac-58b6-44c6-b0c8-be02a7d7935c
# dcac73f4-5a53-402f-932b-7703ad3a232e, 3f8f6d47-238b-4af3-9184-3c1eda827b88

$recorderMap = Import-Csv -Path C:\Demo\recordermap.csv

foreach ($row in $recorderMap)
{
    $roles = Get-Role | Where-Object RoleType -EQ 'UserDefined'
    $srcRS = Get-RecordingServer -Id $row.SourceRecorderId
    $dstRS = Get-RecordingServer -Id $row.DestinationRecorderId
    $existingHardware = $dstRS | Get-Hardware
    foreach ($hw in $srcRS | Get-Hardware | Where-Object Name -NotLike '*Stable*')
    {
        if ($existingHardware | Where-Object Address -eq $hw.Address)
        {
            continue
        }
        $pw = $hw | Get-HardwarePassword
        $driver = $hw | Get-HardwareDriver
        Write-Host "Adding $($hw.Address) to $($dstRS.Name)"
        $newHardware = $dstRS | Add-Hardware -Address $hw.Address -UserName $hw.UserName -Password $pw -DriverId $driver.Number -Name $hw.Name -GroupPath /Testing -Verbose
        if (!$newHardware)
        {
            Write-Host "An error occurred adding hardware"
            continue;
        }
        $newHardware.Description = $hw.Description
        $newHardware.Enabled = $hw.Enabled
        $newHardware.Save();

        $hwSettings = $hw | Get-HardwareSetting -IncludeReadWriteOnly
        foreach ($key in $hwSettings | Get-Member -MemberType NoteProperty | select -ExpandProperty Name)
        {
            $value = $hwSettings.$key
            $newHardware | Set-HardwareSetting -Name $key -Value $value
        }

        foreach ($srcCam in $hw | Get-Camera)
        {
            $dstCam = $newHardware | Get-Camera -Channel $srcCam.Channel
            $dstCam.Name = $srcCam.Name
            $dstCam.Enabled = $srcCam.Enabled
            $dstCam.Description = $srcCam.Description
            $dstCam.EdgeStorageEnabled = $srcCam.EdgeStorageEnabled
            $dstCam.EdgeStoragePlaybackEnabled = $srcCam.EdgeStoragePlaybackEnabled
            $dstCam.GisPoint = $srcCam.GisPoint
            $dstCam.CoverageDepth = $srcCam.CoverageDepth
            $dstCam.CoverageDirection = $srcCam.CoverageDirection
            $dstCam.CoverageFieldOfView = $srcCam.CoverageFieldOfView
            $dstCam.ManualRecordingTimeoutEnabled = $srcCam.ManualRecordingTimeoutEnabled
            $dstCam.ManualRecordingTimeoutMinutes = $srcCam.ManualRecordingTimeoutMinutes
            $dstCam.PrebufferEnabled = $srcCam.PrebufferEnabled
            $dstCam.PrebufferInMemory = $srcCam.PrebufferInMemory
            $dstCam.PrebufferSeconds = $srcCam.PrebufferSeconds
            $dstCam.RecordingEnabled = $srcCam.RecordingEnabled
            $dstCam.RecordingFramerate = $srcCam.RecordingFramerate
            $dstCam.ShortName = $srcCam.ShortName
            $dstCam.Save()

            $settings = $srcCam | Get-CameraSetting -General
            foreach ($key in $settings | Get-Member -MemberType NoteProperty | select -ExpandProperty Name)
            {
                $value = ($settings.$key)
                $dstCam | Set-CameraSetting -General -Name $key -Value $value
            }

            $streamCount = ($srcCam | Get-CameraSetting -Stream).Count
            for ($i = 0; $i -lt $streamCount; $i++)
            {
                $settings = $srcCam | Get-CameraSetting -Stream -StreamNumber $i
                foreach ($key in $settings | Get-Member -MemberType NoteProperty | select -ExpandProperty Name)
                {
                    $value = ($settings.$key)
                    $dstCam | Set-CameraSetting -Stream -StreamNumber $i -Name $key -Value $value -Verbose
                }            
            }

            $srcStream = $srcCam | Get-Stream -LiveDefault
            $dstStream = $dstCam | Get-Stream -LiveDefault
            $dstStream | Set-Stream -StreamId $srcStream.StreamReferenceId -Name $srcStream.Name

            foreach ($ptz in $srcCam.PtzPresetFolder.PtzPresets)
            {
                Write-Host "Creating PTZ Preset '$($ptz.Name)'"
                $serverTask = $dstCam.PtzPresetFolder.AddPtzPreset($ptz.Name, $ptz.Description, $ptz.Pan, $ptz.Tilt, $ptz.Zoom)
                if ($serverTask.State -ne 'Success')
                {
                    Write-Warning "Error creating PTZ Preset Position: $($serverTask.ErrorText)"
                }
            }

            CopyAcls -source $srcCam -destination $dstCam -roles $roles
        }

        foreach ($srcMic in $hw | Get-Microphone)
        {
            $dstMic = $newHardware | Get-Microphone -Channel $srcMic.Channel            
            $dstMic.CoverageDepth = $srcMic.CoverageDepth
            $dstMic.CoverageDirection = $srcMic.CoverageDirection
            $dstMic.CoverageFieldOfView = $srcMic.CoverageFieldOfView
            $dstMic.Description = $srcMic.Description
            $dstMic.EdgeStorageEnabled = $srcMic.EdgeStorageEnabled
            $dstMic.EdgeStoragePlaybackEnabled = $srcMic.EdgeStoragePlaybackEnabled
            $dstMic.Enabled = $srcMic.Enabled
            $dstMic.GisPoint = $srcMic.GisPoint
            $dstMic.ManualRecordingTimeoutEnabled = $srcMic.ManualRecordingTimeoutEnabled
            $dstMic.ManualRecordingTimeoutMinutes = $srcMic.ManualRecordingTimeoutMinutes
            $dstMic.Name = $srcMic.Name
            $dstMic.PrebufferEnabled = $srcMic.PrebufferEnabled
            $dstMic.PrebufferInMemory = $srcMic.PrebufferInMemory
            $dstMic.PrebufferSeconds = $srcMic.PrebufferSeconds
            $dstMic.RecordingEnabled = $srcMic.RecordingEnabled
            $dstMic.ShortName = $srcMic.ShortName
            $dstMic.Save()
            CopyAcls -source $srcMic -destination $dstMic -roles $roles
        }

        foreach ($srcSpkr in $hw | Get-Speaker)
        {
            $dstSpkr = $newHardware | Get-Speaker -Channel $srcSpkr.Channel
            
            $dstSpkr.CoverageDepth = $srcSpkr.CoverageDepth
            $dstSpkr.CoverageDirection = $srcSpkr.CoverageDirection
            $dstSpkr.CoverageFieldOfView = $srcSpkr.CoverageFieldOfView
            $dstSpkr.Description = $srcSpkr.Description
            $dstSpkr.EdgeStorageEnabled = $srcSpkr.EdgeStorageEnabled
            $dstSpkr.EdgeStoragePlaybackEnabled = $srcSpkr.EdgeStoragePlaybackEnabled
            $dstSpkr.Enabled = $srcSpkr.Enabled
            $dstSpkr.GisPoint = $srcSpkr.GisPoint
            $dstSpkr.ManualRecordingTimeoutEnabled = $srcSpkr.ManualRecordingTimeoutEnabled
            $dstSpkr.ManualRecordingTimeoutMinutes = $srcSpkr.ManualRecordingTimeoutMinutes
            $dstSpkr.Name = $srcSpkr.Name
            $dstSpkr.PrebufferEnabled = $srcSpkr.PrebufferEnabled
            $dstSpkr.PrebufferInMemory = $srcSpkr.PrebufferInMemory
            $dstSpkr.PrebufferSeconds = $srcSpkr.PrebufferSeconds
            $dstSpkr.RecordingEnabled = $srcSpkr.RecordingEnabled
            $dstSpkr.ShortName = $srcSpkr.ShortName
            $dstSpkr.Save()
            CopyAcls -source $srcSpkr -destination $dstSpkr -roles $roles
        }

        foreach ($srcMeta in $hw | Get-Metadata)
        {
            $dstMeta = $newHardware | Get-Metadata -Channel $srcMeta.Channel
            $dstMeta.CoverageDepth = $srcMeta.CoverageDepth
            $dstMeta.CoverageDirection = $srcMeta.CoverageDirection
            $dstMeta.CoverageFieldOfView = $srcMeta.CoverageFieldOfView
            $dstMeta.Description = $srcMeta.Description
            $dstMeta.EdgeStorageEnabled = $srcMeta.EdgeStorageEnabled
            $dstMeta.EdgeStoragePlaybackEnabled = $srcMeta.EdgeStoragePlaybackEnabled
            $dstMeta.Enabled = $srcMeta.Enabled
            $dstMeta.GisPoint = $srcMeta.GisPoint
            $dstMeta.ManualRecordingTimeoutEnabled = $srcMeta.ManualRecordingTimeoutEnabled
            $dstMeta.ManualRecordingTimeoutMinutes = $srcMeta.ManualRecordingTimeoutMinutes
            $dstMeta.Name = $srcMeta.Name
            $dstMeta.PrebufferEnabled = $srcMeta.PrebufferEnabled
            $dstMeta.PrebufferInMemory = $srcMeta.PrebufferInMemory
            $dstMeta.PrebufferSeconds = $srcMeta.PrebufferSeconds
            $dstMeta.RecordingEnabled = $srcMeta.RecordingEnabled
            $dstMeta.ShortName = $srcMeta.ShortName
            $dstMeta.Save()
            CopyAcls -source $srcMeta -destination $dstMeta -roles $roles
        }

        foreach ($srcIn in $hw | Get-Input)
        {
            $dstIn = $newHardware | Get-Input -Channel $srcIn.Channel
            $dstIn.CoverageDepth = $srcIn.CoverageDepth
            $dstIn.CoverageDirection = $srcIn.CoverageDirection
            $dstIn.CoverageFieldOfView = $srcIn.CoverageFieldOfView
            $dstIn.Description = $srcIn.Description
            $dstIn.Enabled = $srcIn.Enabled
            $dstIn.GisPoint = $srcIn.GisPoint
            $dstIn.Name = $srcIn.Name
            $dstIn.ShortName = $srcIn.ShortName
            $dstIn.Save()
            CopyAcls -source $srcIn -destination $dstIn -roles $roles
        }

        foreach ($srcOut in $hw | Get-Output)
        {
            $dstOut = $newHardware | Get-Output -Channel $srcOut.Channel
            $dstOut.CoverageDepth = $srcOut.CoverageDepth
            $dstOut.CoverageDirection = $srcOut.CoverageDirection
            $dstOut.CoverageFieldOfView = $srcOut.CoverageFieldOfView
            $dstOut.Description = $srcOut.Description
            $dstOut.Enabled = $srcOut.Enabled
            $dstOut.GisPoint = $srcOut.GisPoint
            $dstOut.Name = $srcOut.Name
            $dstOut.ShortName = $srcOut.ShortName
            $dstOut.Save()
            CopyAcls -source $srcOut -destination $dstOut -roles $roles
        }        
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment