MilestonePSTools.psm1
Import-Module "$PSScriptRoot\bin\MilestonePSTools.dll" function ConvertFrom-StreamUsage { [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [VideoOS.Platform.ConfigurationItems.StreamUsageChildItem] $StreamUsage ) process { $streamName = $StreamUsage.StreamReferenceIdValues.Keys | Where-Object { $StreamUsage.StreamReferenceIdValues.$_ -eq $StreamUsage.StreamReferenceId } Write-Output $streamName } } function Copy-ConfigurationItem { [CmdletBinding()] param ( [parameter(Mandatory, ValueFromPipeline, Position = 0)] [pscustomobject] $InputObject, [parameter(Mandatory, Position = 1)] [VideoOS.ConfigurationApi.ClientService.ConfigurationItem] $DestinationItem ) process { if (!$DestinationItem.ChildrenFilled) { Write-Verbose "$($DestinationItem.DisplayName) has not been retrieved recursively. Retrieving child items now." $DestinationItem = $DestinationItem | Get-ConfigurationItem -Recurse -Sort } $srcStack = New-Object -TypeName System.Collections.Stack $srcStack.Push($InputObject) $dstStack = New-Object -TypeName System.Collections.Stack $dstStack.Push($DestinationItem) Write-Verbose "Configuring $($DestinationItem.DisplayName) ($($DestinationItem.Path))" while ($dstStack.Count -gt 0) { $dirty = $false $src = $srcStack.Pop() $dst = $dstStack.Pop() if (($src.ItemCategory -ne $dst.ItemCategory) -or ($src.ItemType -ne $dst.ItemType)) { Write-Error "Source and Destination ConfigurationItems are different" return } if ($src.EnableProperty.Enabled -ne $dst.EnableProperty.Enabled) { Write-Verbose "$(if ($src.EnableProperty.Enabled) { "Enabling"} else { "Disabling" }) $($dst.DisplayName)" $dst.EnableProperty.Enabled = $src.EnableProperty.Enabled $dirty = $true } $srcChan = $src.Properties | Where-Object { $_.Key -eq "Channel"} | Select-Object -ExpandProperty Value $dstChan = $dst.Properties | Where-Object { $_.Key -eq "Channel"} | Select-Object -ExpandProperty Value if ($srcChan -ne $dstChan) { Write-Error "Sorting mismatch between source and destination configuration." return } foreach ($srcProp in $src.Properties) { $dstProp = $dst.Properties | Where-Object Key -eq $srcProp.Key if ($null -eq $dstProp) { Write-Verbose "Key '$($srcProp.Key)' not found on $($dst.Path)" Write-Verbose "Available keys`r`n$($dst.Properties | Select-Object Key, Value | Format-Table)" continue } if (!$srcProp.IsSettable -or $srcProp.ValueType -eq 'PathList' -or $srcProp.ValueType -eq 'Path') { continue } if ($srcProp.Value -ne $dstProp.Value) { Write-Verbose "Changing $($dstProp.DisplayName) to $($srcProp.Value) on $($dst.Path)" $dstProp.Value = $srcProp.Value $dirty = $true } } if ($dirty) { if ($dst.ItemCategory -eq "ChildItem") { $result = $lastParent | Set-ConfigurationItem } else { $result = $dst | Set-ConfigurationItem } if (!$result.ValidatedOk) { foreach ($errorResult in $result.ErrorResults) { Write-Error $errorResult.ErrorText } } } if ($src.Children.Count -eq $dst.Children.Count -and $src.Children.Count -gt 0) { foreach ($child in $src.Children) { $srcStack.Push($child) } foreach ($child in $dst.Children) { $dstStack.Push($child) } if ($dst.ItemCategory -eq "Item") { $lastParent = $dst } } elseif ($src.Children.Count -ne 0) { Write-Warning "Number of child items is not equal on $($src.DisplayName)" } } } } function Find-XProtectDeviceDialog { [CmdletBinding()] param () process { Add-Type -AssemblyName PresentationFramework $xaml = [xml]@" <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Search_XProtect" Title="Search XProtect" Height="500" Width="800" FocusManager.FocusedElement="{Binding ElementName=cboItemType}"> <Grid> <GroupBox Name="gboAdvanced" Header="Advanced Parameters" HorizontalAlignment="Left" Height="94" Margin="506,53,0,0" VerticalAlignment="Top" Width="243"/> <Label Name="lblItemType" Content="Item Type" HorizontalAlignment="Left" Margin="57,22,0,0" VerticalAlignment="Top"/> <ComboBox Name="cboItemType" HorizontalAlignment="Left" Margin="124,25,0,0" VerticalAlignment="Top" Width="120" TabIndex="0"> <ComboBoxItem Content="Camera" HorizontalAlignment="Left" Width="118"/> <ComboBoxItem Content="Hardware" HorizontalAlignment="Left" Width="118"/> <ComboBoxItem Content="InputEvent" HorizontalAlignment="Left" Width="118"/> <ComboBoxItem Content="Metadata" HorizontalAlignment="Left" Width="118"/> <ComboBoxItem Content="Microphone" HorizontalAlignment="Left" Width="118"/> <ComboBoxItem Content="Output" HorizontalAlignment="Left" Width="118"/> <ComboBoxItem Content="Speaker" HorizontalAlignment="Left" Width="118"/> </ComboBox> <Label Name="lblName" Content="Name" HorizontalAlignment="Left" Margin="77,53,0,0" VerticalAlignment="Top" IsEnabled="False"/> <Label Name="lblPropertyName" Content="Property Name" HorizontalAlignment="Left" Margin="519,80,0,0" VerticalAlignment="Top" IsEnabled="False"/> <ComboBox Name="cboPropertyName" HorizontalAlignment="Left" Margin="614,84,0,0" VerticalAlignment="Top" Width="120" IsEnabled="False" TabIndex="5"/> <TextBox Name="txtName" HorizontalAlignment="Left" Height="23" Margin="124,56,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="187" IsEnabled="False" TabIndex="1"/> <Button Name="btnSearch" Content="Search" HorizontalAlignment="Left" Margin="306,154,0,0" VerticalAlignment="Top" Width="75" TabIndex="7" IsEnabled="False"/> <DataGrid Name="dgrResults" HorizontalAlignment="Left" Height="207" Margin="36,202,0,0" VerticalAlignment="Top" Width="719" IsReadOnly="True"/> <Label Name="lblAddress" Content="IP Address" HorizontalAlignment="Left" Margin="53,84,0,0" VerticalAlignment="Top" IsEnabled="False"/> <TextBox Name="txtAddress" HorizontalAlignment="Left" Height="23" Margin="124,87,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120" IsEnabled="False" TabIndex="2"/> <Label Name="lblEnabledFilter" Content="Enabled/Disabled" HorizontalAlignment="Left" Margin="506,22,0,0" VerticalAlignment="Top" IsEnabled="False"/> <ComboBox Name="cboEnabledFilter" HorizontalAlignment="Left" Margin="614,26,0,0" VerticalAlignment="Top" Width="120" IsEnabled="False" TabIndex="4"> <ComboBoxItem Content="Enabled" HorizontalAlignment="Left" Width="118"/> <ComboBoxItem Content="Disabled" HorizontalAlignment="Left" Width="118"/> <ComboBoxItem Name="cbiEnabledAll" Content="All" HorizontalAlignment="Left" Width="118" IsSelected="True"/> </ComboBox> <Label Name="lblMACAddress" Content="MAC Address" HorizontalAlignment="Left" Margin="37,115,0,0" VerticalAlignment="Top" IsEnabled="False"/> <TextBox Name="txtMACAddress" HorizontalAlignment="Left" Height="23" Margin="124,118,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120" IsEnabled="False" TabIndex="3"/> <Label Name="lblPropertyValue" Content="Property Value" HorizontalAlignment="Left" Margin="522,108,0,0" VerticalAlignment="Top" IsEnabled="False"/> <TextBox Name="txtPropertyValue" HorizontalAlignment="Left" Height="23" Margin="614,111,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120" IsEnabled="False" TabIndex="6"/> <Button Name="btnExportCSV" Content="Export CSV" HorizontalAlignment="Left" Margin="680,429,0,0" VerticalAlignment="Top" Width="75" TabIndex="9" IsEnabled="False"/> <Label Name="lblNoResults" Content="No results found!" HorizontalAlignment="Left" Margin="345,175,0,0" VerticalAlignment="Top" Foreground="Red" Visibility="Hidden"/> <Button Name="btnResetForm" Content="Reset Form" HorizontalAlignment="Left" Margin="414,154,0,0" VerticalAlignment="Top" Width="75" TabIndex="8"/> <Label Name="lblTotalResults" Content="Total Results:" HorizontalAlignment="Left" Margin="32,423,0,0" VerticalAlignment="Top" FontWeight="Bold"/> <TextBox Name="txtTotalResults" HorizontalAlignment="Left" Height="23" Margin="120,427,0,0" VerticalAlignment="Top" Width="53" IsEnabled="False"/> <Label Name="lblPropertyNameBlank" Content="Property Name cannot be blank if Property
Value has an entry." HorizontalAlignment="Left" Margin="507,152,0,0" VerticalAlignment="Top" Foreground="Red" Width="248" Height="45" Visibility="Hidden"/> <Label Name="lblPropertyValueBlank" Content="Property Value cannot be blank if Property
Name has a selection." HorizontalAlignment="Left" Margin="507,152,0,0" VerticalAlignment="Top" Foreground="Red" Width="248" Height="45" Visibility="Hidden"/> </Grid> </Window> "@ function Clear-Results { $var_dgrResults.Columns.Clear() $var_dgrResults.Items.Clear() $var_txtTotalResults.Clear() $var_lblNoResults.Visibility = "Hidden" $var_lblPropertyNameBlank.Visibility = "Hidden" $var_lblPropertyValueBlank.Visibility = "Hidden" } $reader = [system.xml.xmlnodereader]::new($xaml) $window = [windows.markup.xamlreader]::Load($reader) $searchResults = $null # Create variables based on form control names. # Variable will be named as 'var_<control name>' $xaml.SelectNodes("//*[@Name]") | ForEach-Object { #"trying item $($_.Name)" try { Set-Variable -Name "var_$($_.Name)" -Value $window.FindName($_.Name) -ErrorAction Stop } catch { throw } } # Get-Variable var_* $iconBase64 = "AAABAAEAICAAAAEAIACoEAAAFgAAACgAAAAgAAAAQAAAAAEAIAAAAAAAABAAAMMOAADDDgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADamQCA2pkAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2pkAgNqZAP/amQD/2pkAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANqZAIDamQD/2pkA/9qZAP/amQD/2pkAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADamQCA2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2pkAgNqZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANqZAIDamQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADamQCA2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2pkAgNqZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANqZAIDamQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADamQCA2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2pkAgNqZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANqZAIDamQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADamQCA2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkAgAAAAAAAAAAAAAAAAAAAAAAAAAAA2pkAgNqZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkAgAAAAAAAAAAAAAAAANqZAIDamQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkAgAAAAADamQCA2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkAgNqZAIDamQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQCAAAAAANqZAIDamQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkAgAAAAAAAAAAAAAAAANqZAIDamQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAIAAAAAAAAAAAAAAAAAAAAAAAAAAANqZAIDamQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANqZAIDamQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANqZAIDamQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANqZAIDamQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANqZAIDamQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANqZAIDamQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANqZAIDamQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANqZAIDamQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANqZAIDamQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQD/2pkA/9qZAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANqZAIDamQD/2pkA/9qZAP/amQD/2pkA/9qZAP/amQCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANqZAIDamQD/2pkA/9qZAP/amQD/2pkAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANqZAIDamQD/2pkA/9qZAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANqZAIDamQCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//5////8P///+B////AP///gB///wAP//4AB//8AAP/+AAB//AAAP/gAAB/wAAAP4AAAB8AAAAOAAAABAAAAAAAAAACAAAABwAAAA+AAAAfwAAAP+AAAH/wAAD/+AAB//wAA//+AAf//wAP//+AH///wD///+B////w////+f/8=" $iconBytes = [Convert]::FromBase64String($iconBase64) $window.Icon = $iconBytes $assembly = [System.Reflection.Assembly]::GetAssembly([VideoOS.Platform.ConfigurationItems.Hardware]) $excludedItems = "Folder|Path|Icon|Enabled|DisplayName|RecordingFramerate|ItemCategory|Wrapper|Address|Channel" $var_cboItemType.Add_SelectionChanged( { param($sender, $e) $itemType = $e.AddedItems[0].Content $var_cboPropertyName.Items.Clear() $var_dgrResults.Columns.Clear() $var_dgrResults.Items.Clear() $var_txtTotalResults.Clear() $var_txtPropertyValue.Clear() $var_lblNoResults.Visibility = "Hidden" $var_lblPropertyNameBlank.Visibility = "Hidden" $var_lblPropertyValueBlank.Visibility = "Hidden" $properties = $assembly.GetType("VideoOS.Platform.ConfigurationItems.$itemType").DeclaredProperties.Name + [VideoOS.Platform.ConfigurationItems.IConfigurationChildItem].DeclaredProperties.Name | Where-Object { $_ -notmatch $excludedItems } foreach ($property in $properties) { $newComboboxItem = [System.Windows.Controls.ComboBoxItem]::new() $newComboboxItem.AddChild($property) $var_cboPropertyName.Items.Add($newComboboxItem) } $sortDescription = [System.ComponentModel.SortDescription]::new("Content", "Ascending") $var_cboPropertyName.Items.SortDescriptions.Add($sortDescription) $var_cboEnabledFilter.IsEnabled = $true $var_lblEnabledFilter.IsEnabled = $true $var_cboPropertyName.IsEnabled = $true $var_lblPropertyName.IsEnabled = $true $var_txtPropertyValue.IsEnabled = $true $var_lblPropertyValue.IsEnabled = $true $var_txtName.IsEnabled = $true $var_lblName.IsEnabled = $true $var_btnSearch.IsEnabled = $true if ($itemType -eq "Hardware") { $var_txtAddress.IsEnabled = $true $var_lblAddress.IsEnabled = $true $var_txtMACAddress.IsEnabled = $true $var_lblMACAddress.IsEnabled = $true } else { $var_txtAddress.IsEnabled = $false $var_txtAddress.Clear() $var_lblAddress.IsEnabled = $false $var_txtMACAddress.IsEnabled = $false $var_txtMACAddress.Clear() $var_lblMACAddress.IsEnabled = $false } }) $var_txtName.Add_TextChanged( { Clear-Results }) $var_txtAddress.Add_TextChanged( { Clear-Results }) $var_txtMACAddress.Add_TextChanged( { Clear-Results }) $var_cboEnabledFilter.Add_SelectionChanged( { Clear-Results }) $var_cboPropertyName.Add_SelectionChanged( { Clear-Results }) $var_txtPropertyValue.Add_TextChanged( { Clear-Results }) $var_btnSearch.Add_Click( { if (-not [string]::IsNullOrEmpty($var_cboPropertyName.Text) -and [string]::IsNullOrEmpty($var_txtPropertyValue.Text)) { $var_lblPropertyValueBlank.Visibility = "Visible" Return } elseif ([string]::IsNullOrEmpty($var_cboPropertyName.Text) -and -not [string]::IsNullOrEmpty($var_txtPropertyValue.Text)) { $var_lblPropertyNameBlank.Visibility = "Visible" Return } $script:searchResults = Find-XProtectDeviceSearch -ItemType $var_cboItemType.Text -Name $var_txtName.Text -Address $var_txtAddress.Text -MAC $var_txtMACAddress.Text -Enabled $var_cboEnabledFilter.Text -PropertyName $var_cboPropertyName.Text -PropertyValue $var_txtPropertyValue.Text if ($null -ne $script:searchResults) { $var_btnExportCSV.IsEnabled = $true } else { $var_btnExportCSV.IsEnabled = $false } }) $var_btnExportCSV.Add_Click( { $saveDialog = New-Object Microsoft.Win32.SaveFileDialog $saveDialog.Title = "Save As CSV" $saveDialog.Filter = "Comma delimited (*.csv)|*.csv" $saveAs = $saveDialog.ShowDialog() if ($saveAs -eq $true) { $script:searchResults | Export-Csv -Path $saveDialog.FileName -NoTypeInformation } }) $var_btnResetForm.Add_Click( { $var_dgrResults.Columns.Clear() $var_dgrResults.Items.Clear() $var_cboItemType.SelectedItem = $null $var_cboEnabledFilter.IsEnabled = $false $var_lblEnabledFilter.IsEnabled = $false $var_cbiEnabledAll.IsSelected = $true $var_cboPropertyName.IsEnabled = $false $var_cboPropertyName.Items.Clear() $var_lblPropertyName.IsEnabled = $false $var_txtPropertyValue.IsEnabled = $false $var_txtPropertyValue.Clear() $var_lblPropertyValue.IsEnabled = $false $var_txtName.IsEnabled = $false $var_txtName.Clear() $var_lblName.IsEnabled = $false $var_btnSearch.IsEnabled = $false $var_btnExportCSV.IsEnabled = $false $var_txtAddress.IsEnabled = $false $var_txtAddress.Clear() $var_lblAddress.IsEnabled = $false $var_txtMACAddress.IsEnabled = $false $var_txtMACAddress.Clear() $var_lblMACAddress.IsEnabled = $false $var_txtTotalResults.Clear() $var_lblNoResults.Visibility = "Hidden" $var_lblPropertyNameBlank.Visibility = "Hidden" $var_lblPropertyValueBlank.Visibility = "Hidden" }) $null = $window.ShowDialog() } } function Find-XProtectDeviceSearch { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$ItemType, [Parameter(Mandatory = $false)] [string]$Name, [Parameter(Mandatory = $false)] [string]$Address, [Parameter(Mandatory = $false)] [string]$MAC, [Parameter(Mandatory = $false)] [string]$Enabled, [Parameter(Mandatory = $false)] [string]$PropertyName, [Parameter(Mandatory = $false)] [string]$PropertyValue ) process { $var_dgrResults.Columns.Clear() $var_dgrResults.Items.Clear() $var_lblNoResults.Visibility = "Hidden" $var_lblPropertyNameBlank.Visibility = "Hidden" $var_lblPropertyValueBlank.Visibility = "Hidden" if ([string]::IsNullOrEmpty($PropertyName) -or [string]::IsNullOrEmpty($PropertyValue)) { $PropertyName = "Id" $PropertyValue = $null } if ($ItemType -eq "Hardware" -and $null -eq [string]::IsNullOrEmpty($MAC)) { $results = [array](Find-XProtectDevice -ItemType $ItemType -MacAddress $MAC -EnableFilter $Enabled -Properties @{Name = $Name; Address = $Address; $PropertyName = $PropertyValue }) } elseif ($ItemType -eq "Hardware" -and $null -ne [string]::IsNullOrEmpty($MAC)) { $results = [array](Find-XProtectDevice -ItemType $ItemType -EnableFilter $Enabled -Properties @{Name = $Name; Address = $Address; $PropertyName = $PropertyValue }) } else { $results = [array](Find-XProtectDevice -ItemType $ItemType -EnableFilter $Enabled -Properties @{Name = $Name; $PropertyName = $PropertyValue }) } if ($null -ne $results) { #$columnNames = ($results | Get-Member | Where-Object {$_.MemberType -eq 'NoteProperty'}).Name $columnNames = $results[0].PsObject.Properties | ForEach-Object { $_.Name } } else { $var_lblNoResults.Visibility = "Visible" } foreach ($columnName in $columnNames) { $newColumn = [System.Windows.Controls.DataGridTextColumn]::new() $newColumn.Header = $columnName $newColumn.Binding = New-Object System.Windows.Data.Binding($columnName) $newColumn.Width = "SizeToCells" $var_dgrResults.Columns.Add($newColumn) } if ($ItemType -eq "Hardware") { foreach ($result in $results) { $var_dgrResults.AddChild([pscustomobject]@{Hardware = $result.Hardware; RecordingServer = $result.RecordingServer }) } } else { foreach ($result in $results) { $var_dgrResults.AddChild([pscustomobject]@{$columnNames[0] = $result.((Get-Variable -Name columnNames).Value[0]); Hardware = $result.Hardware; RecordingServer = $result.RecordingServer }) } } $var_txtTotalResults.Text = $results.count } end { return $results } } function Get-HttpSslCertThumbprint { <# .SYNOPSIS Gets the certificate thumbprint from the sslcert binding information put by netsh http show sslcert ipport=$IPPort .DESCRIPTION Gets the certificate thumbprint from the sslcert binding information put by netsh http show sslcert ipport=$IPPort. Returns $null if no binding is present for the given ip:port value. .PARAMETER IPPort The ip:port string representing the binding to retrieve the thumbprint from. .EXAMPLE Get-MobileServerSslCertThumbprint 0.0.0.0:8082 Gets the sslcert thumbprint for the binding found matching 0.0.0.0:8082 which is the default HTTPS IP and Port for XProtect Mobile Server. The value '0.0.0.0' represents 'all interfaces' and 8082 is the default https port. #> [CmdletBinding()] param ( [parameter(Mandatory)] [string] $IPPort ) process { $netshOutput = [string](netsh.exe http show sslcert ipport=$IPPort) if (!$netshOutput.Contains('Certificate Hash')) { Write-Error "No SSL certificate binding found for $ipPort" return } if ($netshOutput -match "Certificate Hash\s+:\s+(\w+)\s+") { $Matches[1] } else { Write-Error "Certificate Hash not found for $ipPort" } } } function Get-ProcessOutput { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string] $FilePath, [Parameter()] [string[]] $ArgumentList ) process { try { $process = New-Object System.Diagnostics.Process $process.StartInfo.UseShellExecute = $false $process.StartInfo.RedirectStandardOutput = $true $process.StartInfo.RedirectStandardError = $true $process.StartInfo.FileName = $FilePath $process.StartInfo.CreateNoWindow = $true if($ArgumentList) { $process.StartInfo.Arguments = $ArgumentList } Write-Verbose "Executing $($FilePath) with the following arguments: $([string]::Join(' ', $ArgumentList))" $null = $process.Start() [pscustomobject]@{ StandardOutput = $process.StandardOutput.ReadToEnd() StandardError = $process.StandardError.ReadToEnd() ExitCode = $process.ExitCode } } finally { $process.Dispose() } } } function GetCodecValueFromStream { param([VideoOS.Platform.ConfigurationItems.StreamChildItem]$Stream) $res = $Stream.Properties.GetValue("Codec") if ($null -ne $res) { ($Stream.Properties.GetValueTypeInfoCollection("Codec") | Where-Object Value -eq $res).Name return } } function GetFpsValueFromStream { param([VideoOS.Platform.ConfigurationItems.StreamChildItem]$Stream) $res = $Stream.Properties.GetValue("FPS") if ($null -ne $res) { $val = ($Stream.Properties.GetValueTypeInfoCollection("FPS") | Where-Object Value -eq $res).Name if ($null -eq $val) { $res } else { $val } return } $res = $Stream.Properties.GetValue("Framerate") if ($null -ne $res) { $val = ($Stream.Properties.GetValueTypeInfoCollection("Framerate") | Where-Object Value -eq $res).Name if ($null -eq $val) { $res } else { $val } return } } function GetResolutionValueFromStream { param([VideoOS.Platform.ConfigurationItems.StreamChildItem]$Stream) $res = $Stream.Properties.GetValue("StreamProperty") if ($null -ne $res) { ($Stream.Properties.GetValueTypeInfoCollection("StreamProperty") | Where-Object Value -eq $res).Name return } $res = $Stream.Properties.GetValue("Resolution") if ($null -ne $res) { ($Stream.Properties.GetValueTypeInfoCollection("Resolution") | Where-Object Value -eq $res).Name return } } function Set-CertKeyPermission { [CmdletBinding()] param( # Specifies the Windows username for the identity to which permissions should be granted. [Parameter(Mandatory)] [string] $UserName, # Specifies the level of access to grant to the private key. [Parameter()] [ValidateSet('Read', 'FullControl')] [string] $Permission = 'Read', # Specifies the access type for the Access Control List rule. [Parameter()] [ValidateSet('Allow', 'Deny')] [string] $PermissionType = 'Allow', # Specifies the certificate store path to locate the certificate specified in Thumbprint. Example: Cert:\LocalMachine\My [Parameter()] [string] $CertificateStore = 'Cert:\LocalMachine\My', # Specifies the thumbprint of the certificate to which private key access should be updated. [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $Thumbprint ) process { $certificate = Get-ChildItem -Path $CertificateStore | Where-Object Thumbprint -eq $Thumbprint $privateKeyContainerName = $certificate.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName $privateKeyPath = Join-Path -Path ([system.environment]::GetFolderPath([system.environment+specialfolder]::CommonApplicationData)) -ChildPath ([io.path]::combine('Microsoft', 'Crypto', 'RSA', 'MachineKeys', $privateKeyContainerName)) if ($null -eq $certificate) { Write-Error "Certificate not found in certificate store '$CertificateStore' matching thumbprint '$Thumbprint'" return } if ($null -eq $privateKeyContainerName) { Write-Error "Private key not found for certificate with thumbprint '$Thumbprint'" return } if (-not (Test-Path -Path $privateKeyPath)) { Write-Error "Expected to find private key file at '$privateKeyPath' but the file does not exist. You may need to re-install the certificate in the certificate store" return } $acl = Get-Acl -Path $privateKeyPath $rule = [Security.AccessControl.FileSystemAccessRule]::new($UserName, $Permission, $PermissionType) $acl.AddAccessRule($rule) $acl | Set-Acl -Path $privateKeyPath } } function ConvertFrom-ConfigurationItem { [CmdletBinding()] param( # Specifies the Milestone Configuration API 'Path' value of the configuration item. For example, 'Hardware[a6756a0e-886a-4050-a5a5-81317743c32a]' where the guid is the ID of an existing Hardware item. [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $Path, # Specifies the Milestone 'ItemType' value such as 'Camera', 'Hardware', or 'InputEvent' [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $ItemType ) begin { $assembly = [System.Reflection.Assembly]::GetAssembly([VideoOS.Platform.ConfigurationItems.Hardware]) $serverId = (Get-Site -ErrorAction Stop).FQID.ServerId } process { if ($Path -eq '/') { [VideoOS.Platform.ConfigurationItems.ManagementServer]::new($serverId) } else { $instance = $assembly.CreateInstance("VideoOS.Platform.ConfigurationItems.$ItemType", $false, [System.Reflection.BindingFlags]::Default, $null, (@($serverId, $Path)), $null, $null) Write-Output $instance } } } function Find-ConfigurationItem { [CmdletBinding()] param ( # Specifies all, or part of the display name of the configuration item to search for. For example, if you want to find a camera named "North West Parking" and you specify the value 'Parking', you will get results for any camera where 'Parking' appears in the name somewhere. The search is not case sensitive. [Parameter()] [string] $Name, # Specifies the type(s) of items to include in the results. The default is to include only 'Camera' items. [Parameter()] [string[]] $ItemType = 'Camera', # Specifies whether all matching items should be included, or whether only enabled, or disabled items should be included in the results. The default is to include all items regardless of state. [Parameter()] [ValidateSet('All', 'Disabled', 'Enabled')] [string] $EnableFilter = 'All', # An optional hashtable of additional property keys and values to filter results. Properties must be string types, and the results will be included if the property key exists, and the value contains the provided string. [Parameter()] [hashtable] $Properties = @{} ) process { $svc = Get-IConfigurationService -ErrorAction Stop $itemFilter = [VideoOS.ConfigurationApi.ClientService.ItemFilter]::new() $itemFilter.EnableFilter = [VideoOS.ConfigurationApi.ClientService.EnableFilter]::$EnableFilter $propertyFilters = New-Object System.Collections.Generic.List[VideoOS.ConfigurationApi.ClientService.PropertyFilter] if (-not [string]::IsNullOrWhiteSpace($Name) -and $Name -ne '*') { $Properties.Name = $Name } foreach ($key in $Properties.Keys) { $propertyFilters.Add([VideoOS.ConfigurationApi.ClientService.PropertyFilter]::new( $key, [VideoOS.ConfigurationApi.ClientService.Operator]::Contains, $Properties.$key )) } $itemFilter.PropertyFilters = $propertyFilters foreach ($type in $ItemType) { $itemFilter.ItemType = $type $svc.QueryItems($itemFilter, [int]::MaxValue) | Foreach-Object { Write-Output $_ } } } } $ItemTypeArgCompleter = { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) ([VideoOS.ConfigurationAPI.ItemTypes] | Get-Member -Static -MemberType Property).Name | Where-Object { $_ -like "$wordToComplete*" } | Foreach-Object { "'$_'" } } Register-ArgumentCompleter -CommandName Find-ConfigurationItem -ParameterName ItemType -ScriptBlock $ItemTypeArgCompleter Register-ArgumentCompleter -CommandName ConvertFrom-ConfigurationItem -ParameterName ItemType -ScriptBlock $ItemTypeArgCompleter function Find-XProtectDevice { [CmdletBinding()] param( # Specifies the ItemType such as Camera, Microphone, or InputEvent. Default is 'Camera'. [Parameter()] [ValidateSet('Hardware', 'Camera', 'Microphone', 'Speaker', 'InputEvent', 'Output', 'Metadata')] [string[]] $ItemType = 'Camera', # Specifies name, or part of the name of the device(s) to find. [Parameter()] [ValidateNotNullOrEmpty()] [string] $Name, # Specifies all or part of the IP or hostname of the hardware device to search for. [Parameter()] [ValidateNotNullOrEmpty()] [string] $Address, # Specifies all or part of the MAC address of the hardware device to search for. Note: Searching by MAC is significantly slower than searching by IP. [Parameter()] [ValidateNotNullOrEmpty()] [string] $MacAddress, # Specifies whether all devices should be returned, or only enabled or disabled devices. Default is to return all matching devices. [Parameter()] [ValidateSet('All', 'Disabled', 'Enabled')] [string] $EnableFilter = 'All', # Specifies an optional hash table of key/value pairs matching properties on the items you're searching for. [Parameter()] [hashtable] $Properties = @{}, [Parameter(ParameterSetName = 'ShowDialog')] [switch] $ShowDialog ) begin { $loginSettings = Get-LoginSettings if ([version]'20.2' -gt [version]$loginSettings.ServerProductInfo.ProductVersion) { throw "The QueryItems feature was added to Milestone XProtect VMS versions starting with version 2020 R2 (v20.2). The current site is running $($loginSettings.ServerProductInfo.ProductVersion). Please upgrade to 2020 R2 or later for access to this feature." } } process { if ($ShowDialog) { Find-XProtectDeviceDialog return } if ($MyInvocation.BoundParameters.ContainsKey('Address')) { $ItemType = 'Hardware' $Properties.Address = $Address } if ($MyInvocation.BoundParameters.ContainsKey('MacAddress')) { $ItemType = 'Hardware' $MacAddress = $MacAddress.Replace(':', '').Replace('-', '') } # When many results are returned, this hashtable helps avoid unnecessary configuration api queries by caching parent items and indexing by their Path property $pathToItemMap = @{} Find-ConfigurationItem -ItemType $ItemType -EnableFilter $EnableFilter -Name $Name -Properties $Properties | Foreach-Object { $item = $_ if (![string]::IsNullOrWhiteSpace($MacAddress)) { $hwid = ($item.Properties | Where-Object Key -eq 'Id').Value $mac = ((Get-ConfigurationItem -Path "HardwareDriverSettings[$hwid]").Children[0].Properties | Where-Object Key -like '*/MacAddress/*' | Select-Object -ExpandProperty Value).Replace(':', '').Replace('-', '') if ($mac -notlike "*$MacAddress*") { return } } $deviceInfo = [ordered]@{} while ($true) { $deviceInfo.($item.ItemType) = $item.DisplayName if ($item.ItemType -eq 'RecordingServer') { break } $parentItemPath = $item.ParentPath -split '/' | Select-Object -First 1 # Set $item to the cached copy of that parent item if available. If not, retrieve it using configuration api and cache it. if ($pathToItemMap.ContainsKey($parentItemPath)) { $item = $pathToItemMap.$parentItemPath } else { $item = Get-ConfigurationItem -Path $parentItemPath $pathToItemMap.$parentItemPath = $item } } [pscustomobject]$deviceInfo } } } function Get-ManagementServerConfig { [CmdletBinding()] param() begin { $configXml = Join-Path ([system.environment]::GetFolderPath([System.Environment+SpecialFolder]::CommonApplicationData)) 'milestone\xprotect management server\serverconfig.xml' if (-not (Test-Path $configXml)) { throw [io.filenotfoundexception]::new('Management Server configuration file not found', $configXml) } } process { $xml = [xml](Get-Content -Path $configXml) $versionNode = $xml.SelectSingleNode('/server/version') $clientRegistrationIdNode = $xml.SelectSingleNode('/server/ClientRegistrationId') $webApiPortNode = $xml.SelectSingleNode('/server/WebApiConfig/Port') $authServerAddressNode = $xml.SelectSingleNode('/server/WebApiConfig/AuthorizationServerUri') $serviceProperties = 'Name', 'PathName', 'StartName', 'ProcessId', 'StartMode', 'State', 'Status' $serviceInfo = Get-CimInstance -ClassName 'Win32_Service' -Property $serviceProperties -Filter "name = 'Milestone XProtect Management Server'" $config = @{ Version = if ($null -ne $versionNode) { [version]::Parse($versionNode.InnerText) } else { [version]::new(0, 0) } ClientRegistrationId = if ($null -ne $clientRegistrationIdNode) { [guid]$clientRegistrationIdNode.InnerText } else { [guid]::Empty } WebApiPort = if ($null -ne $webApiPortNode) { [int]$webApiPortNode.InnerText } else { 0 } AuthServerAddress = if ($null -ne $authServerAddressNode) { [uri]$authServerAddressNode.InnerText } else { $null } ServerCertHash = $null InstallationPath = $serviceInfo.PathName.Trim('"') ServiceInfo = $serviceInfo } $netshResult = Get-ProcessOutput -FilePath 'netsh.exe' -ArgumentList "http show sslcert ipport=0.0.0.0:$($config.WebApiPort)" if ($netshResult.StandardOutput -match 'Certificate Hash\s+:\s+(\w+)\s+') { $config.ServerCertHash = $Matches.1 } Write-Output ([pscustomobject]$config) } } function Get-RecorderConfig { [CmdletBinding()] param() begin { $configXml = Join-Path ([system.environment]::GetFolderPath([System.Environment+SpecialFolder]::CommonApplicationData)) 'milestone\xprotect recording server\recorderconfig.xml' if (-not (Test-Path $configXml)) { throw [io.filenotfoundexception]::new('Recording Server configuration file not found', $configXml) } } process { $xml = [xml](Get-Content -Path $configXml) $versionNode = $xml.SelectSingleNode('/recorderconfig/version') $recorderIdNode = $xml.SelectSingleNode('/recorderconfig/recorder/id') $clientRegistrationIdNode = $xml.SelectSingleNode('/recorderconfig/recorder/ClientRegistrationId') $webServerPortNode = $xml.SelectSingleNode('/recorderconfig/webserver/port') $alertServerPortNode = $xml.SelectSingleNode('/recorderconfig/driverservices/alert/port') $serverAddressNode = $xml.SelectSingleNode('/recorderconfig/server/address') $serverPortNode = $xml.SelectSingleNode('/recorderconfig/server/webapiport') $localServerPortNode = $xml.SelectSingleNode('/recorderconfig/webapi/port') $webApiPortNode = $xml.SelectSingleNode('/server/WebApiConfig/Port') $authServerAddressNode = $xml.SelectSingleNode('/recorderconfig/server/authorizationserveraddress') $clientCertHash = $xml.SelectSingleNode('/recorderconfig/webserver/encryption').Attributes['certificateHash'].Value $serviceProperties = 'Name', 'PathName', 'StartName', 'ProcessId', 'StartMode', 'State', 'Status' $serviceInfo = Get-CimInstance -ClassName 'Win32_Service' -Property $serviceProperties -Filter "name = 'Milestone XProtect Recording Server'" $config = @{ Version = if ($null -ne $versionNode) { [version]::Parse($versionNode.InnerText) } else { [version]::new(0, 0) } RecorderId = if ($null -ne $recorderIdNode) { [guid]$recorderIdNode.InnerText } else { [guid]::Empty } ClientRegistrationId = if ($null -ne $clientRegistrationIdNode) { [guid]$clientRegistrationIdNode.InnerText } else { [guid]::Empty } WebServerPort = if ($null -ne $webServerPortNode) { [int]$webServerPortNode.InnerText } else { 0 } AlertServerPort = if ($null -ne $alertServerPortNode) { [int]$alertServerPortNode.InnerText } else { 0 } ServerAddress = $serverAddressNode.InnerText ServerPort = if ($null -ne $serverPortNode) { [int]$serverPortNode.InnerText } else { 0 } LocalServerPort = if ($null -ne $localServerPortNode) { [int]$localServerPortNode.InnerText } else { 0 } AuthServerAddress = if ($null -ne $authServerAddressNode) { [uri]$authServerAddressNode.InnerText } else { $null } ServerCertHash = $null InstallationPath = $serviceInfo.PathName.Trim('"') DevicePackPath = Get-ItemPropertyValue -Path HKLM:\SOFTWARE\WOW6432Node\VideoOS\DeviceDrivers -Name InstallPath ServiceInfo = $serviceInfo } $netshResult = Get-ProcessOutput -FilePath 'netsh.exe' -ArgumentList "http show sslcert ipport=0.0.0.0:$($config.LocalServerPort)" if ($netshResult.StandardOutput -match 'Certificate Hash\s+:\s+(\w+)\s+') { $config.ServerCertHash = $Matches.1 } Write-Output ([pscustomobject]$config) } } function Export-HardwareCsv { <# .SYNOPSIS Exports basic hardware information from a Milestone VMS to a CSV file .DESCRIPTION Exports hardware information to a CSV file. The following fields are included by default: - HardwareName - HardwareAddress - MacAddress - UserName - Password - DriverNumber - DriverDisplayName - RecordingServerName - RecordingServerId Exporting with -Full will also retrieve all hardware information available through the Configuration API and save a JSON object for each hardware in a file adjacent to the CSV. The name of the *.JSON files will be the name of the CSV, plus the hardware ID, and a column named "ConfigurationId" will be added to the CSV as a reference to the configuration corresponding to each row in the CSV. .PARAMETER InputObject Array of Hardware objects to be exported to CSV .PARAMETER Path Full path, including file name, where the CSV will be saved .PARAMETER Full Export full hardware configuration data into an adjacent JSON file .EXAMPLE Connect-ManagementServer -Server localhost Get-Hardware | Export-HardwareToCsv -Path C:\hardware.csv Logs into the local Management Server as the current Windows user and exports all hardware information to C:\hardware.csv .EXAMPLE Connect-ManagementServer -Server localhost Get-RecordingServer -Name East* | Get-Hardware | Export-HardwareToCsv -Path C:\hardware.csv -Full Logs into the local Management Server as the current Windows user and exports all hardware information from all Recording Servers with names beginning with 'East', to C:\hardware.csv, and includes full hardware configuration details which will be saved to C:\hardware_*.json #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipeline)] [VideoOS.Platform.ConfigurationItems.Hardware[]] $InputObject, [Parameter(Mandatory, Position = 1)] [string] $Path, [Parameter()] [switch] $Full ) begin { $exportDirectory = Split-Path -Path $Path -Parent if (!(Test-Path $exportDirectory)) { $null = New-Item -Path $exportDirectory -ItemType Directory } $recorderMap = @{} Write-Verbose "Caching Recording Server names and IDs" foreach ($recorder in Get-ConfigurationItem -Path /RecordingServerFolder -ChildItems) { $id = $recorder.Properties | Where-Object { $_.Key -eq "Id" } | Select-Object -ExpandProperty Value -First 1 $name = $recorder.Properties | Where-Object { $_.Key -eq "Name" } | Select-Object -ExpandProperty Value -First 1 $recorderMap.Add($recorder.Path, [pscustomobject]@{Id=$id; Name=$name}) } $rows = New-Object System.Collections.ArrayList } process { foreach ($hardware in $InputObject) { Write-Verbose "Retrieving info for $($hardware.Name)" try { $hardwareSettings = $hardware | Get-HardwareSetting -ErrorAction Ignore $mac = if ($hardwareSettings) { $hardwareSettings.MacAddress } else { 'error' } $driver = $hardware | Get-HardwareDriver $row = New-Object System.Management.Automation.PSObject $row | Add-Member -MemberType NoteProperty -Name HardwareName -Value $hardware.Name $row | Add-Member -MemberType NoteProperty -Name HardwareAddress -Value $hardware.Address $row | Add-Member -MemberType NoteProperty -Name MacAddress -Value $mac $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 DriverNumber -Value $driver.Number $row | Add-Member -MemberType NoteProperty -Name DriverDisplayName -Value $driver.DisplayName $row | Add-Member -MemberType NoteProperty -Name RecordingServerName -Value $recorderMap[$hardware.ParentItemPath].Name $row | Add-Member -MemberType NoteProperty -Name RecordingServerId -Value $recorderMap[$hardware.ParentItemPath].Id if ($Full) { $row | Add-Member -MemberType NoteProperty -Name ConfigurationId -Value $hardware.Id $content = $hardware | Get-ConfigurationItem -Recurse -Sort | ConvertTo-Json -Depth 100 -Compress $configPath = Join-Path -Path $exportDirectory -ChildPath "$([System.IO.Path]::GetFileNameWithoutExtension($Path))_$($hardware.Id).json" $content | Set-Content $configPath -Force } $null = $rows.Add($row) } catch { Write-Error "Failed to retrieve info for $($hardware.Name). Error: $_" } } } end { $rows | Export-Csv -Path $Path -NoTypeInformation } } function Import-HardwareCsv { <# .SYNOPSIS Adds hardware to a Milestone VMS using a CSV file .DESCRIPTION Adds hardware to a Milestone VMS using a CSV file. The required columns include - RecordingServerName - The display name of the Recording Server where the device should be added - HardwareAddress - The address of the device to be added in the format "http://ip.add.re.ss" - HardwareName - The desired display name of the new hardware device - UserName - The user name on the device - typically 'root' or 'admin' - Password - The password for the given UserName on the device - GroupPath - Optional. Defines the camera group where new cameras will be placed. Default is '/New Cameras' - DriverNumber - Optional. Add-Hardware is much faster when you know the driver to use. Specify the driver number when possible, but if you leave it blank, the Recording Server will try to scan the hardware to discover the driver. When importing with the Full parameter, a separate file for each row of the CSV file is expected to be found adjascent to the CSV file with a name like "csvname_guid.json" where csvname is the filename of the CSV file provided in -Path, and guid is an ID matching the ConfigurationId column from the CSV file. This command will make an effort to match all settings present in the adjascent JSON files. However, some settings are not available through Configuration API, and advanced settings like secondary streams, or events are not included. .PARAMETER Path Path to the location of the CSV file from where the hardware information will be imported .PARAMETER Full Perform a deep copy of the configuration using the adjascent JSON files generated by the Export-HardwareToCsv command. .PARAMETER RecordingServer Override the Recording Servers designated in the CSV file and add all hardware in the CSV file to this Recording Server instead. .EXAMPLE Connect-ManagementServer -Server localhost Import-HardwareFromCsv -Path C:\hardware.csv Logs into the local Management Server as the current Windows user and imports hardware defined in C:\hardware.csv #> [CmdletBinding()] param ( [Parameter(Mandatory, Position = 1)] [string] $Path, [Parameter()] [switch] $Full, [Parameter()] [VideoOS.Platform.ConfigurationItems.RecordingServer] $RecordingServer ) process { $exportDirectory = Split-Path -Path $Path -Parent $rows = @(Import-Csv -Path $Path) $recorderMap = @{} for ($i = 0; $i -lt $rows.Count; $i++) { try { Write-Verbose "Processing row $($i + 1) of $($rows.Count)" $recorder = if($null -ne $RecordingServer) { $RecordingServer } else { if ($recorderMap.ContainsKey($rows[$i].RecordingServerName)) { $recorderMap[$rows[$i].RecordingServerName] } else { $rec = Get-RecordingServer -Name $rows[$i].RecordingServerName $recorderMap.Add($rec.Name, $rec) $rec } } Write-Verbose "Adding $($rows[$i].HardwareAddress) to $($recorder.HostName)" $hardwareArgs = @{ Name = $rows[$i].HardwareName Address = $rows[$i].HardwareAddress UserName = $rows[$i].UserName Password = $rows[$i].Password GroupPath = if ($rows[$i].GroupPath) { $rows[$i].GroupPath } else { '/New Cameras' } } # Only add DriverId property if DriverNumber is present in this row # Rows where DriverNumber is not present will result in the Recording # Server scanning to discover the right driver to use. if ($rows[$i].DriverNumber) { $hardwareArgs.Add('DriverId', $rows[$i].DriverNumber) } $hw = $null try { $hw = $recorder | Add-Hardware @hardwareArgs -ErrorAction Stop Write-Verbose "Successfully added $($hw.Name) with ID $($hw.Id)" if ($Full -and $null -ne $hw) { $configId = $rows[$i].ConfigurationId $configPath = Join-Path -Path $exportDirectory -ChildPath "$([System.IO.Path]::GetFileNameWithoutExtension($Path))_$configId.json" Get-Content -Path $configPath -Raw | ConvertFrom-Json | Copy-ConfigurationItem -DestinationItem ($hw | Get-ConfigurationItem -Recurse -Sort) -Verbose:$VerbosePreference } Write-Output $hw } catch { Write-Error $_ } } catch { Write-Error $_ } } } } function Get-LicenseDetails { [CmdletBinding()] [OutputType([VideoOS.Platform.ConfigurationItems.LicenseDetailChildItem])] param () process { $site = Get-Site $licenseInfo = Get-LicenseInfo $licenseInfo.LicenseDetailFolder.LicenseDetailChildItems } } function Get-LicensedProducts { [CmdletBinding()] [OutputType([VideoOS.Platform.ConfigurationItems.LicenseInstalledProductChildItem])] param () process { $site = Get-Site $licenseInfo = Get-LicenseInfo $licenseInfo.LicenseInstalledProductFolder.LicenseInstalledProductChildItems } } function Get-LicenseInfo { [CmdletBinding()] [OutputType([VideoOS.Platform.ConfigurationItems.LicenseInformation])] param () process { $site = Get-Site [VideoOS.Platform.ConfigurationItems.LicenseInformation]::new($site.FQID.ServerId, "LicenseInformation[$($site.FQID.ObjectId)]") } } function Get-LicenseOverview { [CmdletBinding()] [OutputType([VideoOS.Platform.ConfigurationItems.LicenseOverviewAllChildItem])] param () process { $site = Get-Site $licenseInfo = Get-LicenseInfo $licenseInfo.LicenseOverviewAllFolder.LicenseOverviewAllChildItems } } function Invoke-LicenseActivation { [CmdletBinding()] param ( # Specifies the My Milestone credentials to use for the license activation request [Parameter(mandatory)] [pscredential] $Credential, # Specifies whether the provided credentials should be saved and re-used for automatic license activation [Parameter()] [switch] $EnableAutomaticActivation, # Specifies that the result of Get-LicenseDetails should be passed into the pipeline after activatino [Parameter()] [switch] $Passthru ) process { $licenseInfo = Get-LicenseInfo $invokeResult = $licenseInfo.ActivateLicense($Credential.UserName, $Credential.Password, $EnableAutomaticActivation) do { $task = $invokeResult | Get-ConfigurationItem $state = $task | Get-ConfigurationItemProperty -Key State Write-Verbose ([string]::Join(', ', $task.Properties.Key)) Start-Sleep -Seconds 1 } while ($state -ne 'Error' -and $state -ne 'Success') if ($state -ne 'Success') { Write-Error ($task | Get-ConfigurationItemProperty -Key 'ErrorText') } if ($Passthru) { Get-LicenseDetails } } } function Get-MobileServerInfo { [CmdletBinding()] param ( ) process { try { $mobServerPath = Get-ItemPropertyValue -Path 'HKLM:\SOFTWARE\WOW6432Node\Milestone\XProtect Mobile Server' -Name INSTALLATIONFOLDER [Xml]$doc = Get-Content "$mobServerPath.config" -ErrorAction Stop $xpath = "/configuration/ManagementServer/Address/add[@key='Ip']" $msIp = $doc.SelectSingleNode($xpath).Attributes['value'].Value $xpath = "/configuration/ManagementServer/Address/add[@key='Port']" $msPort = $doc.SelectSingleNode($xpath).Attributes['value'].Value $xpath = "/configuration/HttpMetaChannel/Address/add[@key='Port']" $httpPort = [int]::Parse($doc.SelectSingleNode($xpath).Attributes['value'].Value) $xpath = "/configuration/HttpMetaChannel/Address/add[@key='Ip']" $httpIp = $doc.SelectSingleNode($xpath).Attributes['value'].Value if ($httpIp -eq '+') { $httpIp = '0.0.0.0'} $xpath = "/configuration/HttpSecureMetaChannel/Address/add[@key='Port']" $httpsPort = [int]::Parse($doc.SelectSingleNode($xpath).Attributes['value'].Value) $xpath = "/configuration/HttpSecureMetaChannel/Address/add[@key='Ip']" $httpsIp = $doc.SelectSingleNode($xpath).Attributes['value'].Value if ($httpsIp -eq '+') { $httpsIp = '0.0.0.0'} try { $hash = Get-HttpSslCertThumbprint -IPPort "$($httpsIp):$($httpsPort)" -ErrorAction Stop } catch { $hash = $null } $info = [PSCustomObject]@{ Version = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($mobServerPath).FileVersion; ExePath = $mobServerPath; ConfigPath = "$mobServerPath.config"; ManagementServerIp = $msIp; ManagementServerPort = $msPort; HttpIp = $httpIp; HttpPort = $httpPort; HttpsIp = $httpsIp; HttpsPort = $httpsPort; CertHash = $hash } $info } catch { Write-Error $_ } } } function Remove-MobileServerCertificate { [CmdletBinding(SupportsShouldProcess, ConfirmImpact='Medium')] param () begin { $Elevated = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (!$Elevated) { throw "Elevation is required for Remove-MobileServerCertificate to work properly. Consider re-launching PowerShell by right-clicking and running as Administrator." } } process { Write-Warning "This function is deprecated and will be removed in a future version. Use Set-XProtectCertificate which takes advantage of the Milestone Server Configurator CLI." try { $mosInfo = Get-MobileServerInfo -Verbose:$VerbosePreference $ipPort = "$($mosInfo.HttpsIp):$($mosInfo.HttpsPort)" if ($mosInfo.CertHash) { if ($PSCmdlet.ShouldProcess($ipPort, "Remove SSL certificate binding and restart Milestone XProtect Mobile Server")) { $result = netsh http delete sslcert ipport=$ipPort if ($result -notcontains 'SSL Certificate successfully deleted') { Write-Warning "Unexpected result from netsh http delete sslcert: $result" } Restart-Service -Name 'Milestone XProtect Mobile Server' -Verbose:$VerbosePreference } } else { Write-Warning "No sslcert binding present for $ipPort" } } catch { Write-Error $_ } } } function Set-MobileServerCertificate { <# .SYNOPSIS DEPRECATED - Use Set-XProtectCertificate .DESCRIPTION Sets the sslcert binding for Milestone XProtect Mobile Server when provided with a certificate, an object with a Thumbprint property, or when the -Thumbprint parameter is explicitly provided. The Thumbprint must represent a publicly signed and trusted certificate located in Cert:\LocalMachine\My where the private key is present. .PARAMETER X509Certificate A [System.Security.Cryptography.X509Certificates.X509Certificate2] object representing a certificate which is present in the path Cert:\LocalMachine\My .PARAMETER Thumbprint The certificate hash, commonly referred to as Thumbprint, representing a certificate which is present in the path Cert:\LocalMachine\My .EXAMPLE gci Cert:\LocalMachine\My | ? Subject -eq 'CN=mobile.example.com' | Set-MobileServerCertificate Gets a certificate for mobile.example.com from Cert:\LocalMachine\My and pipes it to Set-MobileServerCertificate .EXAMPLE Submit-Renewal | Set-MobileServerCertificate Submits an ACME certificate renewal using the Posh-ACME module, and if the certificate is renewed, updates the Mobile Server sslcert binding by piping the output to Set-MobileServerCertificate. The Submit-Renewal and New-PACertificate cmdlets return an object with a Thumbprint property. If using Posh-ACME, you must ensure the New-PACertificate command is executed with elevated permissions, and used with the -Install switch so that the new certificate is installed into the Cert:\LocalMachine\My path. If you have done this, then subsequent executions of Submit-Renewal from an elevated session under the same user context will result the renewed certs being installed as well. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact='Medium')] param ( [parameter(ValueFromPipeline=$true)] [System.Security.Cryptography.X509Certificates.X509Certificate2] $X509Certificate, [parameter(Position = 1, ValueFromPipelineByPropertyName=$true)] [string] $Thumbprint ) begin { $Elevated = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (!$Elevated) { throw "Elevation is required for Set-MobileServerCertificate to work properly. Consider re-launching PowerShell by right-clicking and running as Administrator." } } process { Write-Warning "This function is deprecated and will be removed in a future version. Use Set-XProtectCertificate which takes advantage of the Milestone Server Configurator CLI." try { $mosInfo = Get-MobileServerInfo -Verbose:$VerbosePreference $ipPort = "$($mosInfo.HttpsIp):$($mosInfo.HttpsPort)" $appId = "{00000000-0000-0000-0000-000000000000}" $certHash = if ($null -eq $X509Certificate) { $Thumbprint } else { $X509Certificate.Thumbprint } if ($PSCmdlet.ShouldProcess($ipPort, "Add/update SSL certificate binding and restart Milestone XProtect Mobile Server")) { if ($null -ne $mosInfo.CertHash) { Remove-MobileServerCertificate -Verbose:$VerbosePreference } $result = netsh http add sslcert ipport=$ipPort appid="$appId" certhash=$certHash if ($result -notcontains 'SSL Certificate successfully added') { Write-Error "Failed to add certificate binding. $result" return } else { Write-Verbose [string]$result } Restart-Service -Name 'Milestone XProtect Mobile Server' -Verbose:$VerbosePreference } } catch { Write-Error $_ } } } function Set-XProtectCertificate { <# .SYNOPSIS Sets the certificate to use for a given Milestone XProtect VMS service .DESCRIPTION Sets the certificate to use for a given Milestone XProtect VMS service. Compatible Milestone components include XProtect Management Server, Recording Server, and Mobile Server. The Milestone Server Configurator CLI is used to apply the certificate, and CLI support was introduced in version 2020 R3. If you're running an older version of Milestone XProtect software, you must upgrade to at least version 2020 R3 to use this function. .EXAMPLE PS C:\> Set-XProtectCertificate -VmsComponent MobileServer -Thumbprint $thumbprint -RemoveOldCert -Force Sets the Milestone Mobile Server to use the certificate with thumbprint matching the string in the $thumbprint variable and if successfull, it removes any other certificates with a matching subject name from the Cert:\LocalMachine\My certificate store. Since the Force switch is provided, the Server Configurator will be closed if it's currently open. .EXAMPLE PS C:\> Set-XProtectCertificate -VmsComponent MobileServer -Disable -Force Kills the Server Configurator process if it's currently running, then disables encryption for the Milestone Mobile Server. .NOTES Use the Verbose switch to see the command-line arguments provided to the Server Configurator utility. #> [CmdletBinding(SupportsShouldProcess)] param ( # Specifies the Milestone component on which to update the certificate # - Server: Applies to communication between Management Server and Recording Server, as well as client connections to the HTTPS port for the Management Server. # - StreamingMedia: Applies to all connections to Recording Servers. Typically on port 7563. # - MobileServer: Applies to HTTPS connections to the Milestone Mobile Server. [Parameter(Mandatory)] [ValidateSet('Server', 'StreamingMedia', 'MobileServer')] [string] $VmsComponent, # Specifies that encryption for the specified Milestone XProtect service should be disabled [Parameter(ParameterSetName = 'Disable')] [switch] $Disable, # Specifies the thumbprint of the certificate to apply to Milestone XProtect service [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Enable')] [string] $Thumbprint, # Specifies the Windows user account for which read access to the private key is required [Parameter(ParameterSetName = 'Enable')] [string] $UserName, # Specifies the path to the Milestone Server Configurator executable. The default location is C:\Program Files\Milestone\Server Configurator\ServerConfigurator.exe [Parameter()] [ValidateNotNullOrEmpty()] [string] $ServerConfiguratorPath = 'C:\Program Files\Milestone\Server Configurator\ServerConfigurator.exe', # Specifies that all certificates issued to [Parameter(ParameterSetName = 'Enable')] [switch] $RemoveOldCert, # Specifies that the Server Configurator process should be terminated if it's currently running [switch] $Force ) begin { $principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() $adminRole = [Security.Principal.WindowsBuiltInRole]::Administrator if (-not $principal.IsInRole($adminRole)) { throw "Elevation is required. Consider re-launching PowerShell by right-clicking and running as Administrator." } $certGroups = @{ Server = '84430eb7-847c-422d-aa00-7915cd0d7a65' StreamingMedia = '549df21d-047c-456b-958e-99e65dd8b3ec' MobileServer = '76cfc719-a852-4210-913e-703eadab139a' } $knownExitCodes = @{ 0 = 'Success' -1 = 'Unknown error' -2 = 'Invalid arguments' -3 = 'Invalid argument value' -4 = 'Another instance is running' } } process { $utility = [IO.FileInfo]$ServerConfiguratorPath if (-not $utility.Exists) { $exception = [System.IO.FileNotFoundException]::new("Milestone Server Configurator not found at $ServerConfiguratorPath", $utility.FullName) Write-Error -Message $exception.Message -Exception $exception return } if ($utility.VersionInfo.FileVersion -lt [version]'20.3') { Write-Error "Server Configurator version 20.3 is required as the command-line interface for Server Configurator was introduced in version 2020 R3. The current version appears to be $($utility.VersionInfo.FileVersion). Please upgrade to version 2020 R3 or greater." return } Write-Verbose "Verified Server Configurator version $($utility.VersionInfo.FileVersion) is available at $ServerConfiguratorPath" $newCert = Get-ChildItem -Path "Cert:\LocalMachine\My\$Thumbprint" -ErrorAction Ignore if ($null -eq $newCert -and -not $Disable) { Write-Error "Certificate not found in Cert:\LocalMachine\My with thumbprint '$Thumbprint'. Please make sure the certificate is installed in the correct certificate store." return } elseif ($Thumbprint) { Write-Verbose "Located certificate in Cert:\LocalMachine\My with thumbprint $Thumbprint" } # Add read access to the private key for the specified certificate if UserName was specified if (-not [string]::IsNullOrWhiteSpace($UserName)) { try { Write-Verbose "Ensuring $UserName has the right to read the private key for the specified certificate" $newCert | Set-CertKeyPermission -UserName $UserName } catch { Write-Error -Message "Error granting user '$UserName' read access to the private key for certificate with thumbprint $Thumbprint" -Exception $_.Exception } } if ($Force) { Get-Process -Name ServerConfigurator -ErrorAction Ignore | Foreach-Object { Write-Verbose 'Server Configurator is currently running. The Force switch was provided so it will be terminated.' $_ | Stop-Process } } $procParams = @{ FilePath = $utility.FullName Wait = $true PassThru = $true RedirectStandardOutput = Join-Path -Path ([system.environment]::GetFolderPath([system.environment+specialfolder]::ApplicationData)) -ChildPath ([io.path]::GetRandomFileName()) } if ($Disable) { $procParams.ArgumentList = '/quiet', '/disableencryption', "/certificategroup=$($certGroups.$VmsComponent)" } else { $procParams.ArgumentList = '/quiet', '/enableencryption', "/certificategroup=$($certGroups.$VmsComponent)", "/thumbprint=$Thumbprint" } Write-Verbose "Running Server Configurator with the following arguments: $([string]::Join(' ', $procParams.ArgumentList))" $result = Start-Process @procParams if ($result.ExitCode -ne 0) { Write-Error "Server Configurator exited with code $($result.ExitCode). $($knownExitCodes.$($result.ExitCode))" return } if ($RemoveOldCert) { $oldCerts = Get-ChildItem -Path "Cert:\LocalMachine\My" | Where-Object { $_.Subject -eq $newCert.Subject -and $_.Thumbprint -ne $newCert.Thumbprint } if ($null -eq $oldCerts) { Write-Verbose "No other certificates found matching the subject name $($newCert.Subject)" return } foreach ($cert in $oldCerts) { Write-Verbose "Removing certificate with thumbprint $($cert.Thumbprint)" $cert | Remove-Item } } } } function Get-CameraRecordingStats { <# .SYNOPSIS Get statistics on the recordings of one or more cameras including the number of recording or motion sequence, the amount of time in the given time period with recordings or motion, and the percent of time in the given time period with recordings or motion. .DESCRIPTION Long description .EXAMPLE PS C:\> Select-Camera | Get-CameraRecordingStats Opens a camera selection dialog and the selected camera will be sent to Get-CameraRecordingStats. The result will be a PSCustomObject with the DeviceID and a nested PSCustomObject under the RecordingStats property name. #> [CmdletBinding()] param( # Specifies the Id's of cameras for which to retrieve recording statistics [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [guid[]] $Id, # Specifies the timestamp from which to start retrieving recording statistics. Default is 7 days prior to 12:00am of the current day. [Parameter()] [datetime] $StartTime = (Get-Date).Date.AddDays(-7), # Specifies the timestamp marking the end of the time period for which to retrieve recording statistics. The default is 12:00am of the current day. [Parameter()] [datetime] $EndTime = (Get-Date).Date, # Specifies the type of sequence to get statistics on. Default is RecordingSequence. [Parameter()] [ValidateSet('RecordingSequence', 'MotionSequence')] [string] $SequenceType = 'RecordingSequence', # Specifies that the output should be provided in a complete hashtable instead of one pscustomobject value at a time [Parameter()] [switch] $AsHashTable, # Specifies the runspacepool to use. If no runspacepool is provided, one will be created. [Parameter()] [System.Management.Automation.Runspaces.RunspacePool] $RunspacePool ) process { if ($EndTime -le $StartTime) { throw "EndTime must be greater than StartTime" } $disposeRunspacePool = $true if ($PSBoundParameters.ContainsKey('RunspacePool')) { $disposeRunspacePool = $false } $pool = $RunspacePool if ($null -eq $pool) { Write-Verbose "Creating a runspace pool" $pool = [runspacefactory]::CreateRunspacePool(1, ([int]$env:NUMBER_OF_PROCESSORS + 1)) $pool.Open() } $scriptBlock = { param( [guid]$Id, [datetime]$StartTime, [datetime]$EndTime, [string]$SequenceType ) $sequences = Get-SequenceData -Path "Camera[$Id]" -SequenceType $SequenceType -StartTime $StartTime -EndTime $EndTime -CropToTimeSpan $recordedMinutes = $sequences | Foreach-Object { ($_.EventSequence.EndDateTime - $_.EventSequence.StartDateTime).TotalMinutes } | Measure-Object -Sum | Select-Object -ExpandProperty Sum [pscustomobject]@{ DeviceId = $Id StartTime = $StartTime EndTime = $EndTime SequenceCount = $sequences.Count TimeRecorded = [timespan]::FromMinutes($recordedMinutes) PercentRecorded = [math]::Round(($recordedMinutes / ($EndTime - $StartTime).TotalMinutes * 100), 1) } } try { $threads = New-Object System.Collections.Generic.List[pscustomobject] foreach ($cameraId in $Id) { $ps = [powershell]::Create() $ps.RunspacePool = $pool $asyncResult = $ps.AddScript($scriptBlock).AddParameters(@{ Id = $cameraId StartTime = $StartTime EndTime = $EndTime SequenceType = $SequenceType }).BeginInvoke() $threads.Add([pscustomobject]@{ DeviceId = $cameraId PowerShell = $ps Result = $asyncResult }) } if ($threads.Count -eq 0) { return } $hashTable = @{} $completedThreads = New-Object System.Collections.Generic.List[pscustomobject] while ($threads.Count -gt 0) { foreach ($thread in $threads) { if ($thread.Result.IsCompleted) { if ($AsHashTable) { $hashTable.$($thread.DeviceId.ToString()) = $null } else { $obj = [ordered]@{ DeviceId = $thread.DeviceId.ToString() RecordingStats = $null } } try { $result = $thread.PowerShell.EndInvoke($thread.Result) | ForEach-Object { Write-Output $_ } if ($AsHashTable) { $hashTable.$($thread.DeviceId.ToString()) = $result } else { $obj.RecordingStats = $result } } catch { Write-Error $_ } finally { $thread.PowerShell.Dispose() $completedThreads.Add($thread) if (!$AsHashTable) { Write-Output ([pscustomobject]$obj) } } } } $completedThreads | Foreach-Object { [void]$threads.Remove($_)} $completedThreads.Clear() if ($threads.Count -eq 0) { break; } Start-Sleep -Milliseconds 250 } if ($AsHashTable) { Write-Output $hashTable } } finally { if ($threads.Count -gt 0) { Write-Warning "Stopping $($threads.Count) running PowerShell instances. This may take a minute. . ." foreach ($thread in $threads) { $thread.PowerShell.Dispose() } } if ($disposeRunspacePool) { Write-Verbose "Closing runspace pool in $($MyInvocation.MyCommand.Name)" $pool.Close() $pool.Dispose() } } } } function Get-CameraReport { <# .SYNOPSIS Gets detailed information for all cameras in the current site .DESCRIPTION This camera report is a rewrite of the previous camera report available in version 20.3.450930 and earlier. This version makes use of runspaces to significantly improve the time taken to generate the report, and includes several additional properties in each row that were not present in prior versions. Use Get-CameraReport to get a snapshot in time of most of the configuration properties for each camera and the state of cameras in the VMS. For example, you can use this report to see the current video retention for each camera, or whether any cameras are in media overflow or have communication errors. You might pipe this report to CSV and open the report in Excel, group by Recording Server, and get a sense for the number of cameras per Recording Server or create a pivot table on cameras and their resolutions and frame rates. Thanks to the 60+ columns of information provided for each camera in this report, there are innumerable ways in which you could use it to help identify problems, maintain a consistent configuration, or report data to other departments. Column Notes - State: This is returned by Get-ItemState and represents the Milestone Event Server's understanding of the state of the camera. The physical camera may be responding to ping but still be 'Not Responding' in Milestone due to other issues. - MediaOverflow: This means the Recording Server is unable to keep up with the amount of video being recorded, and is dropping video frames from one or more cameras. Any time this value is true, you should investigate Recording Server performance. - GpsCoordinates: This value will be returned as 'Unknown' if not set. The value comes from the Camera object's GisPoint property. - MediaDatabaseBegin: This timestamp represents the time and date, in UTC, of the first image in the media database. If your video retention is 30 days, you should expect this timestamp to be at least 30 days ago under normal circumstances. - MediaDatabaseEnd: This timestamp represents the time and date, in UTC, of the last recorded image in the media database. If your camera is set to record always, this timestamp should be very recent. - UsedSpaceInBytes: This value represents the disk space utilization for the camera on the Recording Server to which it belongs, including data in any live and archive storage to which the camera is assigned. - PercentRecordedOneWeek: This optional value is only set when you include the -IncludeRecordingStats parameter. It may take significantly longer to generate the report when including this information. The value will be the percentage of time over the previous 7 days for which recordings exist. When recording on motion, in an environment with 50% motion, this value will normally be a little higher than 50% to account for pre and post recording buffers. - LastModified: This should represent the last configuration change made to the camera. - Password: This optional value is only set when you include the -IncludePasswords parameter. - HTTPSEnabled: For cameras supporting HTTPS and where HTTPS is enabled at the hardware level in Management Client, you will see the value "YES". Otherwise, you will see the value "NO". - Firmware: This value may NOT be accurate. For example, if the firmware has been upgraded since the camera was added to Milestone, the value may still reflect the old firmware version. This is only because Milestone may only update our record of the firmware when the camera is added or replaced in Management Client. You might consider this value as an indication that the firmware is 'at least' X.Y for example. - DriverNumber: This represents the Milestone ID number for the device driver used for this camera. When adding hardware using Import-HardwareCsv or Add-Hardware, this is the number Milestone would expect when specifying which driver to use. - DriverRevision: This is an internal value representing the driver number for the specific device driver within the currently installed device pack. - RecordingPath: This value is always relative to the Recording Server. If you see a drive letter F:, for example, then it refers to the F: drive on the Recording Server to which the camera is assigned. - Snapshot: This optional value is only set when you include the -IncludeSnapshots parameter. The value will be an [image] object and it will be up to you to decide what to do with the value once you have it. Exporting to CSV for example will result in the string [drawing.image] rather than an actual image or image data. .EXAMPLE PS C:\> Get-CameraReport | Export-Csv -Path .\camera-report.csv -NoTypeInformation Gets a camera report and saves the contents to a CSV file in the current folder. .EXAMPLE PS C:\> Get-CameraReport -IncludeRecordingStats | Export-Csv -Path .\camera-report.csv -NoTypeInformation Gets a camera report with an additional call to Get-CameraRecordingStats which will provide the percentage of time recorded over the last 7 days for each camera. This uses runspaces to help process the requests in parallel and uses Get-SequenceData under the surface to gather the recording sequence data to determine how much time out of the last week was spent recording for each camera. .NOTES If you see the value "NotAvailable" it typically means the camera was disabled, there were no recordings available, the state of the camera is not "Responding", or the property value requested, such as Resolution, does not exist in the stream properties for the camera. #> [CmdletBinding()] param ( # Specifies one or more Recording Servers from which to generate a camera report [Parameter(ValueFromPipeline)] [VideoOS.Platform.ConfigurationItems.RecordingServer[]] $RecordingServer, # Include plain text hardware passwords in the report [Parameter()] [switch] $IncludePlainTextPasswords, # Specifies that disabled cameras should be excluded from the results [Parameter()] [switch] $IncludeDisabled, # Specifies that a live JPEG snapshot should be requested for each camera. The snapshot will be attached as an [drawing.image] object # and it is up to you to decide how to process these images. Some 3rd party modules may allow you to pipe the camera report to Excel # for example. Alternatively, you may manually process each row of the report to save the images to disk in a path of your choice. [Parameter()] [switch] $IncludeSnapshots, # Specifies the height of the snapshots included if the IncludeSnapshots switch is provided. The default is 300 pixels high. The width will be determined based on the aspect ratio of the original image. # A value of 0 will result in snapshots at the original image resolution of the live stream at the time the report is run. [Parameter()] [ValidateRange(0, [int]::MaxValue)] [int] $SnapshotHeight = 300, # Specifies that you want to add the percentage of time recorded in the past week for each camera [Parameter()] [switch] $IncludeRecordingStats ) begin { $null = Get-ManagementServer -ErrorAction Stop $initialSessionState = [initialsessionstate]::CreateDefault() foreach ($functionName in @('Get-StreamProperties', 'ConvertFrom-StreamUsage', 'Get-ValueDisplayName', 'ConvertFrom-Snapshot', 'ConvertFrom-GisPoint')) { $definition = Get-Content Function:\$functionName -ErrorAction Stop $sessionStateFunction = [System.Management.Automation.Runspaces.SessionStateFunctionEntry]::new($functionName, $definition) $initialSessionState.Commands.Add($sessionStateFunction) } $runspacepool = [runspacefactory]::CreateRunspacePool(1, ([int]$env:NUMBER_OF_PROCESSORS + 1), $initialSessionState, $Host) $runspacepool.Open() $threads = New-Object System.Collections.Generic.List[pscustomobject] $processDevice = { param( [VideoOS.Platform.Messaging.ItemState[]]$States, [VideoOS.Platform.ConfigurationItems.RecordingServer]$RecordingServer, [hashtable]$VideoDeviceStatistics, [hashtable]$CurrentDeviceStatus, [hashtable]$RecordingStats, [hashtable]$StorageTable, [VideoOS.Platform.ConfigurationItems.Hardware]$Hardware, [VideoOS.Platform.ConfigurationItems.Camera]$Camera, [bool]$IncludePasswords, [bool]$IncludeSnapshots, [int]$SnapshotHeight ) $returnResult = [pscustomobject]@{ Data = $null ErrorRecord = $null } try { $cameraEnabled = $Hardware.Enabled -and $Camera.Enabled $streamUsages = $Camera | Get-Stream -All $liveStreamName = $streamUsages | Where-Object LiveDefault | ConvertFrom-StreamUsage $recordStreamName = $streamUsages | Where-Object Record | ConvertFrom-StreamUsage $liveStreamSettings = $Camera | Get-StreamProperties -StreamName $liveStreamName $recordedStreamSettings = if ($liveStreamName -eq $recordStreamName) { $liveStreamSettings } else { $Camera | Get-StreamProperties -StreamName $recordStreamName } $motionDetection = $Camera.MotionDetectionFolder.MotionDetections[0] $hardwareSettings = $Hardware | Get-HardwareSetting $playbackInfo = @{ Begin = 'NotAvailable'; End = 'NotAvailable'} if ($cameraEnabled -and $camera.RecordingEnabled) { $tempPlaybackInfo = $Camera | Get-PlaybackInfo -ErrorAction Ignore -WarningAction Ignore if ($null -ne $tempPlaybackInfo) { $playbackInfo = $tempPlaybackInfo } } $driver = $Hardware | Get-HardwareDriver $password = '' if ($IncludePasswords) { try { $password = $Hardware | Get-HardwarePassword -ErrorAction Ignore } catch { $password = $_.Message } } $cameraState = if ($cameraEnabled -and $null -ne $States) { $States | Where-Object { $_.FQID.ObjectId -eq $Camera.Id } | Select-Object -ExpandProperty State } else { 'NotAvailable' } $cameraStatus = $CurrentDeviceStatus.$($RecordingServer.Id).CameraDeviceStatusArray | Where-Object DeviceId -eq $Camera.Id $statistics = $VideoDeviceStatistics.$($RecordingServer.Id) | Where-Object DeviceId -eq $Camera.Id $currentLiveFps = $statistics | Select-Object -ExpandProperty VideoStreamStatisticsArray | Where-Object LiveStreamDefault | Select-Object -ExpandProperty FPS -First 1 $currentRecFps = $statistics | Select-Object -ExpandProperty VideoStreamStatisticsArray | Where-Object RecordingStream | Select-Object -ExpandProperty FPS -First 1 $expectedRetention = New-Timespan -Minutes ($StorageTable.$($Camera.RecordingStorage) | ForEach-Object { $_; $_.ArchiveStorageFolder.ArchiveStorages } | Sort-Object RetainMinutes -Descending | Select-Object -First 1 -ExpandProperty RetainMinutes) $snapshot = $null if ($IncludeSnapshots -and $cameraEnabled -and $cameraStatus.Started -and $cameraState -eq 'Responding') { $snapshot = $Camera | Get-Snapshot -Live -Quality 100 -ErrorAction Ignore | ConvertFrom-Snapshot if ($SnapshotHeight -ne 0 -and $null -ne $snapshot) { $snapshot = $snapshot | Resize-Image -Height $SnapshotHeight -DisposeSource } } elseif (!$IncludeSnapshots) { $snapshot = 'NotRequested' } $returnResult.Data = [pscustomobject]@{ Name = $Camera.Name Channel = $Camera.Channel Enabled = $cameraEnabled State = $cameraState MediaOverflow = if ($cameraEnabled) { $cameraStatus.ErrorOverflow } else { 'NotAvailable' } DbRepairInProgress = if ($cameraEnabled) { $cameraStatus.DbRepairInProgress } else { 'NotAvailable' } DbWriteError = if ($cameraEnabled) { $cameraStatus.ErrorWritingGop } else { 'NotAvailable' } GpsCoordinates = $Camera | ConvertFrom-GisPoint MediaDatabaseBegin = $playbackInfo.Begin MediaDatabaseEnd = $playbackInfo.End UsedSpaceInBytes = if ($cameraEnabled) { $statistics | Select-Object -ExpandProperty UsedSpaceInBytes } else { 'NotAvailable' } PercentRecordedOneWeek = if ($cameraEnabled -and $RecordingStats.$($Camera.Id).PercentRecorded -is [double]) { $RecordingStats.$($Camera.Id).PercentRecorded } else { 'NotAvailable' } LastModified = $Camera.LastModified Id = $Camera.Id HardwareName = $Hardware.Name Address = $Hardware.Address Username = $Hardware.UserName Password = $password HTTPSEnabled = if ($null -ne $hardwareSettings.HTTPSEnabled) { $hardwareSettings.HTTPSEnabled.ToUpper() } else { 'NO' } MAC = $hardwareSettings.MacAddress Firmware = $hardwareSettings.FirmwareVersion Model = $Hardware.Model Driver = $driver.Name DriverNumber = $driver.Number.ToString() DriverRevision = $driver.DriverRevision HardwareId = $Hardware.Id RecorderName = $RecordingServer.Name RecorderUri = $RecordingServer.WebServerUri RecorderId = $RecordingServer.Id ConfiguredLiveResolution = Get-ValueDisplayName -PropertyList $liveStreamSettings -PropertyName 'Resolution', 'StreamProperty' ConfiguredLiveCodec = Get-ValueDisplayName -PropertyList $liveStreamSettings -PropertyName 'Codec' ConfiguredLiveFPS = Get-ValueDisplayName -PropertyList $liveStreamSettings -PropertyName 'FPS', 'Framerate' LiveMode = $streamUsages | Where-Object LiveDefault | Select-Object -ExpandProperty LiveMode ConfiguredRecordResolution = Get-ValueDisplayName -PropertyList $recordedStreamSettings -PropertyName 'Resolution', 'StreamProperty' #GetResolution -PropertyList $recordedStreamSettings ConfiguredRecordCodec = Get-ValueDisplayName -PropertyList $recordedStreamSettings -PropertyName 'Codec' ConfiguredRecordFPS = Get-ValueDisplayName -PropertyList $recordedStreamSettings -PropertyName 'FPS', 'Framerate' CurrentLiveResolution = if ($cameraEnabled) { $statistics | Select-Object -ExpandProperty VideoStreamStatisticsArray | Where-Object LiveStreamDefault | Select-Object -ExpandProperty ImageResolution -First 1 | Foreach-Object { "$($_.Width)x$($_.Height)" } } else { 'NotAvailable' } CurrentLiveFPS = if ($cameraEnabled -and $currentLiveFps -is [double]) { [math]::Round($currentLiveFps, 1) } else { 'NotAvailable' } CurrentLiveBPS = if ($cameraEnabled) { $statistics | Select-Object -ExpandProperty VideoStreamStatisticsArray | Where-Object LiveStreamDefault | Select-Object -ExpandProperty BPS -First 1 } else { 'NotAvailable' } CurrentRecordedResolution = if ($cameraEnabled) { $statistics | Select-Object -ExpandProperty VideoStreamStatisticsArray | Where-Object RecordingStream | Select-Object -ExpandProperty ImageResolution -First 1 | Foreach-Object { "$($_.Width)x$($_.Height)" } } else { 'NotAvailable' } CurrentRecordedFPS = if ($cameraEnabled -and $currentRecFps -is [double]) { [math]::Round($currentRecFps, 1) } else { 'NotAvailable' } CurrentRecordedBPS = if ($cameraEnabled) { $statistics | Select-Object -ExpandProperty VideoStreamStatisticsArray | Where-Object RecordingStream | Select-Object -ExpandProperty BPS -First 1 } else { 'NotAvailable' } RecordingEnabled = $Camera.RecordingEnabled RecordKeyframesOnly = $Camera.RecordKeyframesOnly RecordOnRelatedDevices = $Camera.RecordOnRelatedDevices PrebufferEnabled = $Camera.PrebufferEnabled PrebufferSeconds = $Camera.PrebufferSeconds PrebufferInMemory = $Camera.PrebufferInMemory RecordingStorageName = $StorageTable.$($Camera.RecordingStorage).Name RecordingPath = [io.path]::Combine($StorageTable.$($Camera.RecordingStorage).DiskPath, $StorageTable.$($Camera.RecordingStorage).Id) ExpectedRetention = $expectedRetention ActualRetention = if ($playbackInfo.Begin -is [string]) { 'NotAvailable' } else { [datetime]::UtcNow - $playbackInfo.Begin } MeetsRetentionPolicy = if ($playbackInfo.Begin -is [string]) { 'NotAvailable' } else { ([datetime]::UtcNow - $playbackInfo.Begin) -ge $expectedRetention } MotionEnabled = $motionDetection.Enabled MotionKeyframesOnly = $motionDetection.KeyframesOnly MotionProcessTime = $motionDetection.ProcessTime MotionSensitivityMode = if ($motionDetection.ManualSensitivityEnabled) { 'Manual' } else { 'Automatic' } MotionManualSensitivity = $motionDetection.ManualSensitivity MotionMetadataEnabled = $motionDetection.GenerateMotionMetadata MotionExcludeRegions = if ($motionDetection.UseExcludeRegions) { 'Yes' } else { 'No' } MotionHardwareAccelerationMode = $motionDetection.HardwareAccelerationMode PrivacyMaskEnabled = $Camera.PrivacyProtectionFolder.PrivacyProtections[0].Enabled Snapshot = $snapshot } } catch { $returnResult.ErrorRecord = $_ } return $returnResult } } process { $progressParams = @{ Activity = 'Camera Report' CurrentOperation = '' Status = 'Preparing to run report' PercentComplete = 0 Completed = $false } if ($null -eq $RecordingServer) { Write-Verbose "Getting a list of all recording servers on $((Get-ManagementServer).Name)" $progressParams.CurrentOperation = 'Getting Recording Servers' Write-Progress @progressParams $RecordingServer = Get-RecordingServer } Write-Verbose 'Getting the current state of all cameras' $progressParams.CurrentOperation = 'Calling Get-ItemState' Write-Progress @progressParams $itemState = Get-ItemState if ($null -eq $itemState) { Write-Warning 'Get-ItemState failed which indicates the Milestone Event Server service may not be 100% functional. It may take longer than normal to run this report if any servers or cameras are not responding.' } Write-Verbose 'Discovering all cameras and retrieving status and statistics' try { $progressParams.CurrentOperation = 'Calling Get-VideoDeviceStatistics on all responding recording servers' Write-Progress @progressParams $respondingRecordingServers = if ($null -eq $itemState) { $RecordingServer.Id } else { $RecordingServer.Id | Where-Object { $id = $_; $id -in $itemState.FQID.ObjectId -and ($itemState | Where-Object { $id -eq $_.FQID.ObjectId }).State -eq 'Server Responding' } } $respondingCameras = if ($null -eq $itemState) { (Get-PlatformItem -Kind ([videoos.platform.kind]::camera)).fqid.objectid } else { ($itemState | Where-Object { $_.FQID.Kind -eq [videoos.platform.kind]::Camera -and $_.State -eq 'Responding' }).FQID.ObjectId } $videoDeviceStatistics = Get-VideoDeviceStatistics -AsHashtable -RecordingServerId $respondingRecordingServers -RunspacePool $runspacepool $progressParams.CurrentOperation = 'Calling Get-CurrentDeviceStatus on all responding recording servers' Write-Progress @progressParams $currentDeviceStatus = Get-CurrentDeviceStatus -AsHashtable -RecordingServerId $respondingRecordingServers -RunspacePool $runspacepool $recordingStats = @{} if ($IncludeRecordingStats -and $respondingCameras.Count -gt 0) { $progressParams.CurrentOperation = "Retrieving 7 days of recording stats for $($respondingCameras.Count) cameras using Get-CameraRecordingStats" Write-Progress @progressParams $recordingStats = Get-CameraRecordingStats -Id $respondingCameras -AsHashTable -RunspacePool $runspacepool } $progressParams.CurrentOperation = 'Adding camera information requests to the queue' Write-Progress @progressParams $storageTable = @{} foreach ($rs in $RecordingServer) { $rs.StorageFolder.Storages | Foreach-Object { $_.FillChildren('StorageArchive') $storageTable.$($_.Path) = $_ } foreach ($hw in $rs | Get-Hardware) { foreach ($cam in $hw | Get-Camera) { if (!$IncludeDisabled -and -not ($cam.Enabled -and $hw.Enabled)) { continue } $ps = [powershell]::Create() $ps.RunspacePool = $runspacepool $asyncResult = $ps.AddScript($processDevice).AddParameters(@{ States = $itemState RecordingServer = $rs VideoDeviceStatistics = $videoDeviceStatistics CurrentDeviceStatus = $currentDeviceStatus RecordingStats = $recordingStats StorageTable = $storageTable Hardware = $hw Camera = $cam IncludePasswords = $IncludePlainTextPasswords IncludeSnapshots = $IncludeSnapshots SnapshotHeight = $SnapshotHeight }).BeginInvoke() $threads.Add([pscustomobject]@{ PowerShell = $ps Result = $asyncResult Camera = $cam.Name }) } } } if ($threads.Count -eq 0) { return } $progressParams.CurrentOperation = 'Processing' $completedThreads = New-Object System.Collections.Generic.List[pscustomobject] $totalDevices = $threads.Count while ($threads.Count -gt 0) { $progressParams.PercentComplete = ($totalDevices - $threads.Count) / $totalDevices * 100 $progressParams.Status = "Processed $($totalDevices - $threads.Count) out of $totalDevices cameras" Write-Progress @progressParams foreach ($thread in $threads) { if ($thread.Result.IsCompleted) { $result = $thread.PowerShell.EndInvoke($thread.Result) if ($null -ne $result.ErrorRecord) { Write-Error -ErrorRecord $result.ErrorRecord } if ($null -ne $result.Data) { Write-Output $result.Data } elseif ($null -eq $result.ErrorRecord) { Write-Error "Expected to receive a row of data for camera named `"$($thread.Camera)`" but received a `$null response instead. This shouldn't happen. Consider reporting an issue on GitHub at https://github.com/MilestoneSystemsInc/PowerShellSamples" } $thread.PowerShell.Dispose() $completedThreads.Add($thread) } } $completedThreads | Foreach-Object { [void]$threads.Remove($_)} $completedThreads.Clear() if ($threads.Count -eq 0) { break; } Start-Sleep -Seconds 1 } } finally { if ($threads.Count -gt 0) { Write-Warning "Stopping $($threads.Count) running PowerShell instances. This may take a minute. . ." foreach ($thread in $threads) { $thread.PowerShell.Dispose() } } $runspacepool.Close() $runspacepool.Dispose() $progressParams.Completed = $true Write-Progress @progressParams } } } function Get-CameraReportV1 { <# .SYNOPSIS Deprecated. The new Get-CameraReport replaces this version. .DESCRIPTION This version of Get-CameraReport is being retained for compatibility reasons only, in order for anyone using the old version of the report to update any automation they may have to use the new version of the report. The decision to replace this old camera report function with a new function with the same name was made because of the relatively low number of people using the report on a regular basis, and an even lower number of people using it in automation. We recognize the change may break existing automation however the significantly improved performance of the new version of the function and the additional information provided is believed to be worth a small amount of disruption. Of course you can always use an earlier version of MilestonePSTools or copy the old function to a script outside of MilestonePSTools if you need to rely on the reporting function staying stable between MilestonePSTools releases. Gets a [PSCustomObject] with a wide range of properties for each camera found on one or more Recording Servers. When the -RecordingServer parameter is omitted, all cameras on all servers in the currently selected site will be included in the report. Otherwise, only the server(s) included in the -RecordingServer array will enumerated for cameras to include in the report. A number of switches can optionally be included to add information to the default result set. For example, camera passwords are only included when the -IncludePasswords switch is present. .PARAMETER RecordingServer Specifies one Recording Server, or an array of Recording Servers. All cameras on the specified servers will be included in the report. If omitted, all cameras from all Recording Servers on the currently selected site will be included in the report. To include only cameras from a subset of Recording Servers, use Get-RecordingServer to select and filter for the desired servers. .PARAMETER IncludeNetworkState When this switch is present, the host and port from the hardware address of all cameras will be used in a call to Test-NetConnection to determine if a device is reachable or unreachable. Note that if the camera network is unreachable from the location where this command is executed, it will always return 'Offline'. If camera networks are segmented and accessible only by Recording Servers, then you should run this cmdlet directly on the Recording Servers. You can use the RecordingServer parameter to ensure each Recording Server produces a report for only the cameras added to itself. .PARAMETER IncludeActualResolutions When this switch is present, a live and playback image will be requested from the Recording Server for every enabled camera in the report. The resolution in the format WidthxHeight for a single image will be included columns named ActualLiveResolution and ActualRecordingResolution. .PARAMETER IncludeDisabledCameras Disabled cameras, which are considered to be any camera where either the camera, or it's parent Hardware object are disabled in Milestone, are excluded from the report by default. To include properties from disabled cameras in addition to enabled cameras, you must supply this switch. For disabled cameras, we will not attempt to retrieve the Actual*Resolution values or the first and last image timestamps from the media database. .PARAMETER IncludePasswords Passwords are only included in the report when this switch is present. Otherwise, only the UserName value will be included. .PARAMETER IncludePlaybackInfo When present, the IncludePlaybackInfo switch will cause the Get-PlaybackInfo command to be executed on each enabled camera and the first and last image timestamps from the media database will be included in the report. .PARAMETER AdditionalHardwareProperties Specifies an array of setting keys which should be included as columns in the report. The available keys vary by hardware driver. You can discover the available columns using the Get-HardwareSetting command. .PARAMETER AdditionalCameraProperties Specifies an array of setting keys which should be included as columns in the report. The available keys vary by hardware driver. You can discover the available columns using the Get-CameraSetting -General command. .EXAMPLE Get-RecordingServer -Name NVR01 | Get-CameraReport -IncludePasswords -AdditionalHardwareProperties HTTPSEnabled Generates a report including all cameras on NVR01, and the report will include hardware passwords, and the HTTPSEnabled value for all hardware devices which have this setting available. #> [CmdletBinding()] param ( [Parameter(ValueFromPipeline)] [VideoOS.Platform.ConfigurationItems.RecordingServer[]] $RecordingServer, [Parameter()] [switch] $IncludeNetworkState, [Parameter()] [switch] $IncludeActualResolutions, [Parameter()] [switch] $IncludeDisabledCameras, [Parameter()] [switch] $IncludePasswords, [Parameter()] [switch] $IncludePlaybackInfo, [Parameter()] [string[]] $AdditionalHardwareProperties, # Additional camera setting values to retrieve using exact key names [Parameter()] [string[]] $AdditionalCameraProperties ) process { $states = Get-ItemState -CamerasOnly $networkStates = @{} $storageRetention = @{} $RecordingServer = if ($null -eq $RecordingServer) { Get-RecordingServer } else { $RecordingServer } foreach ($rec in $RecordingServer) { foreach ($hw in $rec | Get-Hardware) { if ($hw.Enabled -or $IncludeDisabledCameras) { $driver = $hw | Get-HardwareDriver $hwSettings = $hw | Get-HardwareSetting $pass = if ($IncludePasswords) { $hw | Get-HardwarePassword } else { $null } } foreach ($cam in $hw | Get-Camera) { $enabled = $hw.Enabled -and $cam.Enabled if (-not $enabled -and -not $IncludeDisabledCameras) { continue } $liveSize = $null $playbackSize = $null if ($enabled -and $IncludeActualResolutions) { $live = $cam | Get-Snapshot -Live -LiveTimeoutMS 10000 $playback = $cam | Get-Snapshot -Behavior GetEnd $liveSize = if ($null -ne $live.Content -and $live.Content.Length -gt 0) { "$($live.Width)x$($live.Height)" } else { $null } $playbackSize = if ($null -ne $playback.Bytes -and $playback.Bytes.Length -gt 0) { "$($playback.Width)x$($playback.Height)" } else { $null } } $liveStream = $cam | Get-Stream -LiveDefault $recordedStream = $cam | Get-Stream -Recorded $liveStreamName = $liveStream.StreamReferenceIdValues.Keys | ForEach-Object { if ($liveStream.StreamReferenceId -eq $liveStream.StreamReferenceIdValues[$_]) { $_ } } $recordedStreamName = $recordedStream.StreamReferenceIdValues.Keys | ForEach-Object { if ($recordedStream.StreamReferenceId -eq $recordedStream.StreamReferenceIdValues[$_]) { $_ } } $liveStreamSettings = $cam.DeviceDriverSettingsFolder.DeviceDriverSettings[0].StreamChildItems | Where-Object DisplayName -eq $liveStreamName $recordedStreamSettings = $cam.DeviceDriverSettingsFolder.DeviceDriverSettings[0].StreamChildItems | Where-Object DisplayName -eq $recordedStreamName $motion = $cam.MotionDetectionFolder.MotionDetections[0] $storage = $rec.StorageFolder.Storages | Where-Object Path -eq $cam.RecordingStorage if (!$storageRetention.ContainsKey($cam.RecordingStorage)) { if ($storage.ArchiveStorageFolder.ArchiveStorages.Count -eq 0) { $storageRetention.$($cam.RecordingStorage) = $storage.RetainMinutes } else { $storageRetention.$($cam.RecordingStorage) = $storage.ArchiveStorageFolder.ArchiveStorages | Sort-Object RetainMinutes -Descending | Select-Object -First 1 -ExpandProperty RetainMinutes } } $retention = $storageRetention.$($cam.RecordingStorage) $state = ($states | Where-Object { $_.FQID.ObjectId -eq $cam.Id }).State $networkState = $networkStates.($hw.Id) if ($IncludeNetworkState -and $null -eq $networkState) { $networkState = 'Online' if ($state -ne 'Responding') { $uri = [Uri]$hw.Address $responding = Test-NetConnection -ComputerName $uri.Host -Port $uri.Port -InformationLevel Quiet if (-not $responding) { $networkState = 'Offline' } } $networkStates.($hw.Id) = $networkState } $obj = [PSCustomObject]@{ CameraName = $cam.Name Channel = $cam.Channel Enabled = $hw.Enabled -and $cam.Enabled State = $state NetworkState = $networkState LastModified = $cam.LastModified CameraId = $cam.Id HardwareName = $hw.Name HardwareId = $hw.Id RecorderName = $rec.Name RecorderHostName = $rec.HostName RecorderId = $rec.Id Model = $hw.Model Driver = $driver.Name Address = $hw.Address UserName = $hw.UserName Password = $pass MAC = $hwSettings.MacAddress LiveStream = $liveStreamName LiveCodec = GetCodecValueFromStream $liveStreamSettings LiveResolution = GetResolutionValueFromStream $liveStreamSettings LiveFPS = GetFpsValueFromStream $liveStreamSettings RecordingStream = $recordedStreamName RecordingCodec = GetCodecValueFromStream $recordedStreamSettings RecordingResolution = GetResolutionValueFromStream $recordedStreamSettings RecordingFPS = GetFpsValueFromStream $recordedStreamSettings ActualLiveResolution = $liveSize ActualRecordingResolution = $playbackSize RecordingEnabled = $cam.RecordingEnabled RecordKeyframesOnly = $cam.RecordKeyframesOnly PreBufferEnabled = $cam.PrebufferEnabled PreBufferSeconds = $cam.PrebufferSeconds PreBufferInMemory = $cam.PrebufferInMemory StorageName = $storage.Name StoragePath = [IO.Path]::Combine($storage.DiskPath, $storage.Id) StorageRetentionMinutes = $retention MotionEnabled = $motion.Enabled MotionKeyframesOnly = $motion.KeyframesOnly MotionDetectionMethod = $motion.DetectionMethod MotionProcessTime = $motion.ProcessTime MotionManualSensitivity = $motion.ManualSensitivityEnabled MotionMetadataEnabled = $motion.GenerateMotionMetadata MotionHardwareAcceleration = $motion.HardwareAccelerationMode } if ($IncludePlaybackInfo) { $obj | Add-Member -MemberType NoteProperty -Name "OldestImageUtc" -Value $null $obj | Add-Member -MemberType NoteProperty -Name "LatestImageUtc" -Value $null $obj | Add-Member -MemberType NoteProperty -Name "MeetsRetentionPolicy" -Value $null try { $info = $cam | Get-PlaybackInfo -ErrorAction Stop $obj.OldestImageUtc = $info.Begin $obj.LatestImageUtc = $info.End $expectedRetention = [datetime]::UtcNow.AddMinutes(($retention * -1)) $obj.MeetsRetentionPolicy = $info.Begin -le $expectedRetention } catch { Write-Warning "Get-PlaybackInfo failed: $($_.Exception.Message)" } } foreach ($p in $AdditionalHardwareProperties) { $obj | Add-Member -MemberType NoteProperty -Name "Custom_$p" -Value $hwSettings.$p } foreach ($p in $AdditionalCameraProperties) { try { $camSettings = $cam | Get-CameraSetting -General $obj | Add-Member -MemberType NoteProperty -Name "Custom_$p" -Value $camSettings.$p } catch { Write-Error $_ } } $obj } } } } } function Get-CurrentDeviceStatus { <# .SYNOPSIS Gets the current device status of all devices of the desired type from one or more recording servers .DESCRIPTION Uses the RecorderStatusService2 client to call GetCurrentDeviceStatus and receive the current status of all devices of the desired type(s). Specify one or more types in the DeviceType parameter to receive status of more device types than cameras. .EXAMPLE PS C:\> Get-RecordingServer -Name 'My Recording Server' | Get-CurrentDeviceStatus -DeviceType All Gets the status of all devices of all device types from the Recording Server named 'My Recording Server'. .EXAMPLE PS C:\> Get-CurrentDeviceStatus -DeviceType Camera, Microphone Gets the status of all cameras and microphones from all recording servers. #> [CmdletBinding()] [OutputType([pscustomobject])] param ( # Specifies one or more Recording Server ID's to which the results will be limited. Omit this parameter if you want device status from all Recording Servers [Parameter(ValueFromPipelineByPropertyName)] [Alias('Id')] [guid[]] $RecordingServerId, # Specifies the type of devices to include in the results. By default only cameras will be included and you can expand this to include all device types [Parameter()] [ValidateSet('Camera', 'Microphone', 'Speaker', 'Metadata', 'Input event', 'Output', 'Event', 'Hardware', 'All')] [string[]] $DeviceType = 'Camera', # Specifies that the output should be provided in a complete hashtable instead of one pscustomobject value at a time [Parameter()] [switch] $AsHashTable, # Specifies the runspacepool to use. If no runspacepool is provided, one will be created. [Parameter()] [System.Management.Automation.Runspaces.RunspacePool] $RunspacePool ) process { if ($DeviceType -contains 'All') { $DeviceType = @('Camera', 'Microphone', 'Speaker', 'Metadata', 'Input event', 'Output', 'Event', 'Hardware') } $includedDeviceTypes = $DeviceType | Foreach-Object { [videoos.platform.kind]::$_ } $disposeRunspacePool = $true if ($PSBoundParameters.ContainsKey('RunspacePool')) { $disposeRunspacePool = $false } $pool = $RunspacePool if ($null -eq $pool) { Write-Verbose "Creating a runspace pool" $pool = [runspacefactory]::CreateRunspacePool(1, ([int]$env:NUMBER_OF_PROCESSORS + 1)) $pool.Open() } $scriptBlock = { param( [uri]$Uri, [guid[]]$DeviceIds ) try { $client = [VideoOS.Platform.SDK.Proxy.Status2.RecorderStatusService2]::new($Uri) $client.GetCurrentDeviceStatus((Get-Token), $deviceIds) } catch { throw "Unable to get current device status from $Uri" } } Write-Verbose 'Retrieving recording server information' $managementServer = [videoos.platform.configuration]::Instance.GetItems([videoos.platform.itemhierarchy]::SystemDefined) | Where-Object { $_.FQID.Kind -eq [videoos.platform.kind]::Server -and $_.FQID.ObjectId -eq (Get-ManagementServer).Id } $recorders = $managementServer.GetChildren() | Where-Object { $_.FQID.ServerId.ServerType -eq 'XPCORS' -and ($null -eq $RecordingServerId -or $_.FQID.ObjectId -in $RecordingServerId) } Write-Verbose "Retrieving video device statistics from $($recorders.Count) recording servers" try { $threads = New-Object System.Collections.Generic.List[pscustomobject] foreach ($recorder in $recorders) { Write-Verbose "Requesting device status from $($recorder.Name) at $($recorder.FQID.ServerId.Uri)" $folders = $recorder.GetChildren() | Where-Object { $_.FQID.Kind -in $includedDeviceTypes -and $_.FQID.FolderType -eq [videoos.platform.foldertype]::SystemDefined} $deviceIds = [guid[]]($folders | Foreach-Object { $children = $_.GetChildren() if ($null -ne $children -and $children.Count -gt 0) { $children.FQID.ObjectId } }) $ps = [powershell]::Create() $ps.RunspacePool = $pool $asyncResult = $ps.AddScript($scriptBlock).AddParameters(@{ Uri = $recorder.FQID.ServerId.Uri DeviceIds = $deviceIds }).BeginInvoke() $threads.Add([pscustomobject]@{ RecordingServerId = $recorder.FQID.ObjectId RecordingServerName = $recorder.Name PowerShell = $ps Result = $asyncResult }) } if ($threads.Count -eq 0) { return } $hashTable = @{} $completedThreads = New-Object System.Collections.Generic.List[pscustomobject] while ($threads.Count -gt 0) { foreach ($thread in $threads) { if ($thread.Result.IsCompleted) { Write-Verbose "Receiving results from recording server $($thread.RecordingServerName)" if ($AsHashTable) { $hashTable.$($thread.RecordingServerId.ToString()) = $null } else { $obj = @{ RecordingServerId = $thread.RecordingServerId.ToString() CurrentDeviceStatus = $null } } try { $result = $thread.PowerShell.EndInvoke($thread.Result) | ForEach-Object { Write-Output $_ } if ($AsHashTable) { $hashTable.$($thread.RecordingServerId.ToString()) = $result } else { $obj.CurrentDeviceStatus = $result } } catch { Write-Error $_ } finally { $thread.PowerShell.Dispose() $completedThreads.Add($thread) if (!$AsHashTable) { Write-Output ([pscustomobject]$obj) } } } } $completedThreads | Foreach-Object { [void]$threads.Remove($_)} $completedThreads.Clear() if ($threads.Count -eq 0) { break; } Start-Sleep -Milliseconds 250 } if ($AsHashTable) { Write-Output $hashTable } } finally { if ($threads.Count -gt 0) { Write-Warning "Stopping $($threads.Count) running PowerShell instances. This may take a minute. . ." foreach ($thread in $threads) { $thread.PowerShell.Dispose() } } if ($disposeRunspacePool) { Write-Verbose "Closing runspace pool in $($MyInvocation.MyCommand.Name)" $pool.Close() $pool.Dispose() } } } } function Get-VideoDeviceStatistics { <# .SYNOPSIS Gets the camera device statistics including used storage space, and the properties of each video stream being retrieved from the camera .DESCRIPTION Uses the RecorderStatusService2 client to call GetVideoDeviceStatistics and receive the current video device statistics of all cameras, or filtered by Recording Server. .EXAMPLE PS C:\> Get-RecordingServer -Name 'My Recording Server' | Get-VideoDeviceStatistics Gets the video statistics of all cameras on the Recording Server named 'My Recording Server'. .EXAMPLE PS C:\> Get-VideoDeviceStatistics -AsHashTable Gets the video statistics of all cameras and returns the result as a hashtable where the keys are the camera ID's. #> [CmdletBinding()] param ( # Specifies one or more Recording Server ID's to which the results will be limited. Omit this parameter if you want device status from all Recording Servers [Parameter(ValueFromPipelineByPropertyName)] [Alias('Id')] [guid[]] $RecordingServerId, # Specifies that the output should be provided in a complete hashtable instead of one pscustomobject value at a time [Parameter()] [switch] $AsHashTable, # Specifies the runspacepool to use. If no runspacepool is provided, one will be created. [Parameter()] [System.Management.Automation.Runspaces.RunspacePool] $RunspacePool ) process { $disposeRunspacePool = $true if ($PSBoundParameters.ContainsKey('RunspacePool')) { $disposeRunspacePool = $false } $pool = $RunspacePool if ($null -eq $pool) { Write-Verbose "Creating a runspace pool" $pool = [runspacefactory]::CreateRunspacePool(1, ([int]$env:NUMBER_OF_PROCESSORS + 1)) $pool.Open() } $scriptBlock = { param( [uri]$Uri, [guid[]]$DeviceIds ) try { $client = [VideoOS.Platform.SDK.Proxy.Status2.RecorderStatusService2]::new($Uri) $client.GetVideoDeviceStatistics((Get-Token), $deviceIds) } catch { throw "Unable to get video device statistics from $Uri" } } Write-Verbose 'Retrieving recording server information' $managementServer = [videoos.platform.configuration]::Instance.GetItems([videoos.platform.itemhierarchy]::SystemDefined) | Where-Object { $_.FQID.Kind -eq [videoos.platform.kind]::Server -and $_.FQID.ObjectId -eq (Get-ManagementServer).Id } $recorders = $managementServer.GetChildren() | Where-Object { $_.FQID.ServerId.ServerType -eq 'XPCORS' -and ($null -eq $RecordingServerId -or $_.FQID.ObjectId -in $RecordingServerId) } Write-Verbose "Retrieving video device statistics from $($recorders.Count) recording servers" try { $threads = New-Object System.Collections.Generic.List[pscustomobject] foreach ($recorder in $recorders) { Write-Verbose "Requesting video device statistics from $($recorder.Name) at $($recorder.FQID.ServerId.Uri)" $folders = $recorder.GetChildren() | Where-Object { $_.FQID.Kind -eq [videoos.platform.kind]::Camera -and $_.FQID.FolderType -eq [videoos.platform.foldertype]::SystemDefined} $deviceIds = [guid[]]($folders | Foreach-Object { $children = $_.GetChildren() if ($null -ne $children -and $children.Count -gt 0) { $children.FQID.ObjectId } }) $ps = [powershell]::Create() $ps.RunspacePool = $pool $asyncResult = $ps.AddScript($scriptBlock).AddParameters(@{ Uri = $recorder.FQID.ServerId.Uri DeviceIds = $deviceIds }).BeginInvoke() $threads.Add([pscustomobject]@{ RecordingServerId = $recorder.FQID.ObjectId RecordingServerName = $recorder.Name PowerShell = $ps Result = $asyncResult }) } if ($threads.Count -eq 0) { return } $hashTable = @{} $completedThreads = New-Object System.Collections.Generic.List[pscustomobject] while ($threads.Count -gt 0) { foreach ($thread in $threads) { if ($thread.Result.IsCompleted) { Write-Verbose "Receiving results from recording server $($thread.RecordingServerName)" if ($AsHashTable) { $hashTable.$($thread.RecordingServerId.ToString()) = $null } else { $obj = @{ RecordingServerId = $thread.RecordingServerId.ToString() VideoDeviceStatistics = $null } } try { $result = $thread.PowerShell.EndInvoke($thread.Result) | ForEach-Object { Write-Output $_ } if ($AsHashTable) { $hashTable.$($thread.RecordingServerId.ToString()) = $result } else { $obj.VideoDeviceStatistics = $result } } catch { Write-Error $_ } finally { $thread.PowerShell.Dispose() $completedThreads.Add($thread) if (!$AsHashTable) { Write-Output ([pscustomobject]$obj) } } } } $completedThreads | Foreach-Object { [void]$threads.Remove($_)} $completedThreads.Clear() if ($threads.Count -eq 0) { break; } Start-Sleep -Milliseconds 250 } if ($AsHashTable) { Write-Output $hashTable } } finally { if ($threads.Count -gt 0) { Write-Warning "Stopping $($threads.Count) running PowerShell instances. This may take a minute. . ." foreach ($thread in $threads) { $thread.PowerShell.Dispose() } } if ($disposeRunspacePool) { Write-Verbose "Closing runspace pool in $($MyInvocation.MyCommand.Name)" $pool.Close() $pool.Dispose() } } } } function Add-VmsArchiveStorage { [CmdletBinding(SupportsShouldProcess)] [OutputType([VideoOS.Platform.ConfigurationItems.ArchiveStorage])] param( [Parameter(Mandatory, ValueFromPipeline)] [VideoOS.Platform.ConfigurationItems.Storage] $Storage, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Name, [Parameter()] [string] $Description, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Path, [Parameter()] [ValidateScript({ if ($_ -lt [timespan]::FromMinutes(60)) { throw "Retention must be greater than or equal to one hour" } if ($_ -gt [timespan]::FromMinutes([int]::MaxValue)) { throw "Retention must be less than or equal to $([int]::MaxValue) minutes." } $true })] [timespan] $Retention, [Parameter(Mandatory)] [ValidateRange(1, [int]::MaxValue)] [int] $MaximumSizeMB, [Parameter()] [switch] $ReduceFramerate, [Parameter()] [ValidateRange(0.00028, 100)] [double] $TargetFramerate = 5 ) process { $archiveFolder = $Storage.ArchiveStorageFolder if ($PSCmdlet.ShouldProcess("Recording storage '$($Storage.Name)'", "Add new archive storage named '$($Name)' with retention of $($Retention.TotalHours) hours and a maximum size of $($MaximumSizeMB) MB")) { try { $taskInfo = $archiveFolder.AddArchiveStorage($Name, $Description, $Path, $TargetFrameRate, $Retention.TotalMinutes, $MaximumSizeMB) if ($taskInfo.State -ne [videoos.platform.configurationitems.stateenum]::Success) { Write-Error -Message $taskInfo.ErrorText return } $archive = [VideoOS.Platform.ConfigurationItems.ArchiveStorage]::new((Get-ManagementServer).ServerId, $taskInfo.Path) if ($ReduceFramerate) { $invokeInfo = $archive.SetFramerateReductionArchiveStorage() $invokeInfo.SetProperty('FramerateReductionEnabled', 'True') [void]$invokeInfo.ExecuteDefault() } $storage.ClearChildrenCache() Write-Output $archive } catch { Write-Error $_ return } } } } function Add-VmsStorage { [CmdletBinding(DefaultParameterSetName = 'WithoutEncryption', SupportsShouldProcess)] [OutputType([VideoOS.Platform.ConfigurationItems.Storage])] param( [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'WithoutEncryption')] [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'WithEncryption')] [VideoOS.Platform.ConfigurationItems.RecordingServer] $RecordingServer, [Parameter(Mandatory, ParameterSetName = 'WithoutEncryption')] [Parameter(Mandatory, ParameterSetName = 'WithEncryption')] [ValidateNotNullOrEmpty()] [string] $Name, [Parameter(ParameterSetName = 'WithoutEncryption')] [Parameter(ParameterSetName = 'WithEncryption')] [string] $Description, [Parameter(Mandatory, ParameterSetName = 'WithoutEncryption')] [Parameter(Mandatory, ParameterSetName = 'WithEncryption')] [ValidateNotNullOrEmpty()] [string] $Path, [Parameter(ParameterSetName = 'WithoutEncryption')] [Parameter(ParameterSetName = 'WithEncryption')] [ValidateScript({ if ($_ -lt [timespan]::FromMinutes(60)) { throw "Retention must be greater than or equal to one hour" } if ($_ -gt [timespan]::FromMinutes([int]::MaxValue)) { throw "Retention must be less than or equal to $([int]::MaxValue) minutes." } $true })] [timespan] $Retention, [Parameter(Mandatory, ParameterSetName = 'WithoutEncryption')] [Parameter(Mandatory, ParameterSetName = 'WithEncryption')] [ValidateRange(1, [int]::MaxValue)] [int] $MaximumSizeMB, [Parameter(ParameterSetName = 'WithoutEncryption')] [Parameter(ParameterSetName = 'WithEncryption')] [switch] $Default, [Parameter(ParameterSetName = 'WithoutEncryption')] [Parameter(ParameterSetName = 'WithEncryption')] [switch] $EnableSigning, [Parameter(Mandatory, ParameterSetName = 'WithEncryption')] [ValidateSet('Light', 'Strong', IgnoreCase = $false)] [string] $EncryptionMethod, [Parameter(Mandatory, ParameterSetName = 'WithEncryption')] [securestring] $Password ) process { $storageFolder = $RecordingServer.StorageFolder if ($PSCmdlet.ShouldProcess("Recording Server '$($RecordingServer.Name)' at $($RecordingServer.HostName)", "Add new storage named '$($Name)' with retention of $($Retention.TotalHours) hours and a maximum size of $($MaximumSizeMB) MB")) { try { $taskInfo = $storageFolder.AddStorage($Name, $Description, $Path, $EnableSigning, $Retention.TotalMinutes, $MaximumSizeMB) if ($taskInfo.State -ne [videoos.platform.configurationitems.stateenum]::Success) { Write-Error -Message $taskInfo.ErrorText return } } catch { Write-Error $_ return } $storage = [VideoOS.Platform.ConfigurationItems.Storage]::new((Get-ManagementServer).ServerId, $taskInfo.Path) } if ($PSCmdlet.ParameterSetName -eq 'WithEncryption' -and $PSCmdlet.ShouldProcess("Recording Storage '$Name'", "Enable '$EncryptionMethod' Encryption")) { try { $invokeResult = $storage.EnableEncryption($Password, $EncryptionMethod) if ($invokeResult.State -ne [videoos.platform.configurationitems.stateenum]::Success) { throw $invokeResult.ErrorText } $storage = [VideoOS.Platform.ConfigurationItems.Storage]::new((Get-ManagementServer).ServerId, $taskInfo.Path) } catch { [void]$storageFolder.RemoveStorage($taskInfo.Path) Write-Error $_ return } } if ($Default -and $PSCmdlet.ShouldProcess("Recording Storage '$Name'", "Set as default storage configuration")) { try { $invokeResult = $storage.SetStorageAsDefault() if ($invokeResult.State -ne [videoos.platform.configurationitems.stateenum]::Success) { throw $invokeResult.ErrorText } $storage = [VideoOS.Platform.ConfigurationItems.Storage]::new((Get-ManagementServer).ServerId, $taskInfo.Path) } catch { [void]$storageFolder.RemoveStorage($taskInfo.Path) Write-Error $_ return } } if (!$PSBoundParameters.ContainsKey('WhatIf')) { Write-Output $storage } } } function Get-VmsArchiveStorage { [CmdletBinding()] [OutputType([VideoOS.Platform.ConfigurationItems.ArchiveStorage])] param ( [Parameter(Mandatory, ValueFromPipeline)] [VideoOS.Platform.ConfigurationItems.Storage] $Storage, [Parameter()] [ValidateNotNullOrEmpty()] [SupportsWildcards()] [string] $Name = '*' ) process { $storagesMatched = 0 $Storage.ArchiveStorageFolder.ArchiveStorages | ForEach-Object { if ($_.Name -like $Name) { $storagesMatched++ Write-Output $_ } } if ($storagesMatched -eq 0 -and -not [System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($Name)) { Write-Error "No recording storages found matching the name '$Name'" } } } function Get-VmsStorage { [CmdletBinding()] [OutputType([VideoOS.Platform.ConfigurationItems.Storage])] param ( [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'FromName')] [VideoOS.Platform.ConfigurationItems.RecordingServer] $RecordingServer, [Parameter(ParameterSetName = 'FromName')] [ValidateNotNullOrEmpty()] [SupportsWildcards()] [string] $Name = '*', [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'FromPath')] [ValidateScript({ if ($_ -match 'Storage\[.{36}\]') { $true } else { throw "Invalid storage item path. Expected format: Storage[$([guid]::NewGuid())]" } })] [Alias('RecordingStorage', 'Path')] [string] $ItemPath ) process { switch ($PSCmdlet.ParameterSetName) { 'FromName' { $storagesMatched = 0 $RecordingServer.StorageFolder.Storages | ForEach-Object { if ($_.Name -like $Name) { $storagesMatched++ Write-Output $_ } } if ($storagesMatched -eq 0 -and -not [System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($Name)) { Write-Error "No recording storages found matching the name '$Name'" } } 'FromPath' { [VideoOS.Platform.ConfigurationItems.Storage]::new((Get-ManagementServer).ServerId, $ItemPath) } Default { throw "ParameterSetName $($PSCmdlet.ParameterSetName) not implemented" } } } } function Remove-VmsArchiveStorage { [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param ( [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'ByName')] [VideoOS.Platform.ConfigurationItems.Storage] $Storage, [Parameter(Mandatory, ParameterSetName = 'ByName')] [ValidateNotNullOrEmpty()] [string] $Name, [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'ByStorage')] [VideoOS.Platform.ConfigurationItems.ArchiveStorage] $ArchiveStorage ) process { switch ($PSCmdlet.ParameterSetName) { 'ByName' { foreach ($archiveStorage in $Storage | Get-VmsArchiveStorage -Name $Name) { $archiveStorage | Remove-VmsArchiveStorage } } 'ByStorage' { $recorder = [VideoOS.Platform.ConfigurationItems.RecordingServer]::new((Get-ManagementServer).ServerId, $Storage.ParentItemPath) $storage = [VideoOS.Platform.ConfigurationItems.Storage]::new((Get-ManagementServer).ServerId, $ArchiveStorage.ParentItemPath) if ($PSCmdlet.ShouldProcess("Recording server $($recorder.Name)", "Delete archive $($ArchiveStorage.Name) from $($storage.Name)")) { $folder = [VideoOS.Platform.ConfigurationItems.ArchiveStorageFolder]::new((Get-ManagementServer).ServerId, $ArchiveStorage.ParentPath) [void]$folder.RemoveArchiveStorage($ArchiveStorage.Path) } } Default { throw 'Unknown parameter set' } } } } function Remove-VmsStorage { [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param ( [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'ByName')] [VideoOS.Platform.ConfigurationItems.RecordingServer] $RecordingServer, [Parameter(Mandatory, ParameterSetName = 'ByName')] [ValidateNotNullOrEmpty()] [string] $Name, [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'ByStorage')] [VideoOS.Platform.ConfigurationItems.Storage] $Storage ) process { switch ($PSCmdlet.ParameterSetName) { 'ByName' { foreach ($vmsStorage in $RecordingServer | Get-VmsStorage -Name $Name) { $vmsStorage | Remove-VmsStorage } } 'ByStorage' { $recorder = [VideoOS.Platform.ConfigurationItems.RecordingServer]::new((Get-ManagementServer).ServerId, $Storage.ParentItemPath) if ($PSCmdlet.ShouldProcess("Recording server $($recorder.Name)", "Delete $($Storage.Name) and all archives")) { $folder = [VideoOS.Platform.ConfigurationItems.StorageFolder]::new((Get-ManagementServer).ServerId, $Storage.ParentPath) [void]$folder.RemoveStorage($Storage.Path) } } Default { throw 'Unknown parameter set' } } } } function ConvertFrom-GisPoint { [CmdletBinding()] [OutputType([system.device.location.geocoordinate])] param ( # Specifies the GisPoint value to convert to a GeoCoordinate. Milestone stores GisPoint data in the format "POINT ([longitude] [latitude])" or "POINT EMPTY". [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $GisPoint ) process { if ($GisPoint -eq 'POINT EMPTY') { Write-Output ([system.device.location.geocoordinate]::Unknown) } else { $temp = $GisPoint.Substring(7, $GisPoint.Length - 8) $long, $lat, $null = $temp -split ' ' Write-Output ([system.device.location.geocoordinate]::new($lat, $long)) } } } function ConvertFrom-Snapshot { [CmdletBinding()] [OutputType([system.drawing.image])] param( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [Alias('Bytes')] [byte[]] $Content ) process { if ($null -eq $Content -or $Content.Length -eq 0) { return $null } $ms = [io.memorystream]::new($Content) Write-Output ([system.drawing.image]::FromStream($ms)) } } function Get-BankTable { [CmdletBinding()] param ( [Parameter()] [string] $Path, [Parameter()] [string[]] $DeviceId, [Parameter()] [DateTime] $StartTime = [DateTime]::MinValue, [Parameter()] [DateTime] $EndTime = [DateTime]::MaxValue.AddHours(-1) ) process { $di = [IO.DirectoryInfo]$Path foreach ($table in $di.EnumerateDirectories()) { if ($table.Name -match "^(?<id>[0-9a-fA-F\-]{36})(_(?<tag>\w+)_(?<endTime>\d\d\d\d-\d\d-\d\d_\d\d-\d\d-\d\d).*)?") { $tableTimestamp = if ($null -eq $Matches["endTime"]) { (Get-Date).ToString("yyyy-MM-dd_HH-mm-ss") } else { $Matches["endTime"] } $timestamp = [DateTime]::ParseExact($tableTimestamp, "yyyy-MM-dd_HH-mm-ss", [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AssumeLocal) if ($timestamp -lt $StartTime -or $timestamp -gt $EndTime.AddHours(1)) { # Timestamp of table is outside the requested timespan continue } if ($null -ne $DeviceId -and [cultureinfo]::InvariantCulture.CompareInfo.IndexOf($DeviceId, $Matches["id"], [System.Globalization.CompareOptions]::IgnoreCase) -eq -1) { # Device ID for table is not requested continue } [pscustomobject]@{ DeviceId = [Guid]$Matches["id"] EndTime = $timestamp Tag = $Matches["tag"] IsLiveTable = $null -eq $Matches["endTime"] Path = $table.FullName } } } } } function Get-ConfigurationItemProperty { <# .SYNOPSIS Gets the value of a given ConfigurationItem property by key .DESCRIPTION A ConfigurationItem may have zero or more Property objects in the Properties array. Each property has a key name and a value. Since the Properties property on a ConfigurationItem has no string-based indexer, you are required to search the array of properties for the one with the Key you're interested in, and then get the Value property of it. This cmdlet is a simple wrapper which does the Where-Object for you, and throws an error if the Key does not exist. .PARAMETER InputObject A [VideoOS.ConfigurationApi.ClientService.ConfigurationItem] with a property to be retrieved. .PARAMETER Key A string representing the key of the property from which the value should be retrieved. .EXAMPLE PS C:\> $description = Get-ConfigurationItem -Path / | Get-ConfigurationItemProperty -Key Description Gets a ConfigurationItem representing the Management Server, and returns the Description value. The alternative is to do ((Get-ConfigurationItem -Path /).Properties | Where-Object Key -eq Description).Value. #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [VideoOS.ConfigurationApi.ClientService.ConfigurationItem] [ValidateNotNullOrEmpty()] $InputObject, [Parameter(Mandatory)] [string] [ValidateNotNullOrEmpty()] $Key ) process { $property = $InputObject.Properties | Where-Object Key -eq $Key if ($null -eq $property) { Write-Error -Message "Key '$Key' not found on configuration item $($InputObject.Path)" -TargetObject $InputObject -Category InvalidArgument return } $property.Value } } function Get-StreamProperties { <# .SYNOPSIS Get a list of configuration properties from the designated camera stream .DESCRIPTION Get a list of configuration properties from the designated camera stream. These properties provide detailed information including the property key, current value, the value type, and in the case of certain value types, a list of valid values or a range of valid values. .EXAMPLE PS C:\> Select-Camera | Get-StreamProperties -StreamName 'Video stream 1' Opens a dialog to select a camera, then returns the stream properties for 'Video stream 1'. The objects returned are rich property objects with a number of properties attached to them in addition to their keys and values. #> [CmdletBinding()] [OutputType([VideoOS.ConfigurationApi.ClientService.Property[]])] param ( # Specifies the camera to retrieve stream properties for [Parameter(ValueFromPipeline, Mandatory, ParameterSetName = 'ByName')] [Parameter(ValueFromPipeline, Mandatory, ParameterSetName = 'ByNumber')] [VideoOS.Platform.ConfigurationItems.Camera] $Camera, # Specifies a StreamUsageChildItem from Get-Stream [Parameter(ParameterSetName = 'ByName')] [ValidateNotNullOrEmpty()] [string] $StreamName, # Specifies the stream number starting from 0. For example, "Video stream 1" is usually in the 0'th position in the StreamChildItems collection. [Parameter(ParameterSetName = 'ByNumber')] [ValidateRange(0, [int]::MaxValue)] [int] $StreamNumber ) process { switch ($PSCmdlet.ParameterSetName) { 'ByName' { $stream = (Get-ConfigurationItem -Path "DeviceDriverSettings[$($Camera.Id)]").Children | Where-Object { $_.ItemType -eq 'Stream' -and $_.DisplayName -like $StreamName } if ($null -eq $stream -and ![system.management.automation.wildcardpattern]::ContainsWildcardCharacters($StreamName)) { Write-Error "No streams found on $($Camera.Name) matching the name '$StreamName'" return } foreach ($obj in $stream) { Write-Output $obj.Properties } } 'ByNumber' { $streams = (Get-ConfigurationItem -Path "DeviceDriverSettings[$($Camera.Id)]").Children | Where-Object { $_.ItemType -eq 'Stream' } if ($StreamNumber -lt $streams.Count) { Write-Output ($streams[$StreamNumber].Properties) } else { Write-Error "There are $($streams.Count) streams available on the camera and stream number $StreamNumber does not exist. Remember to index the streams from zero." } } Default {} } } } function Get-ValueDisplayName { [CmdletBinding()] [OutputType([string])] param ( [Parameter(Mandatory)] [VideoOS.ConfigurationApi.ClientService.Property[]] $PropertyList, [Parameter(Mandatory)] [string[]] $PropertyName, [Parameter()] [string] $DefaultValue = 'NotAvailable' ) process { $value = $DefaultValue if ($null -eq $PropertyList -or $PropertyList.Count -eq 0) { return $value } $selectedProperty = $null foreach ($property in $PropertyList) { foreach ($name in $PropertyName) { if ($property.Key -like "*/$name/*") { $selectedProperty = $property break } } if ($null -ne $selectedProperty) { break } } if ($null -ne $selectedProperty) { $value = $selectedProperty.Value if ($selectedProperty.ValueType -eq 'Enum') { $displayName = ($selectedProperty.ValueTypeInfos | Where-Object Value -eq $selectedProperty.Value).Name if (![string]::IsNullOrWhiteSpace($displayName)) { $value = $displayName } } } Write-Output $value } } function Install-StableFPS { <# .SYNOPSIS Install the StableFPS driver on a Recording Server .DESCRIPTION The StableFPS driver is used to add any number of virtual cameras simulated from static video files. It includes support for multiple video codecs, audio, metadata, input and output. See the Milestone MIP SDK documentation for more information. This command must be run with elevated permissions due to the fact it must add/modify files in the Device Pack installation path which is typically placed in the protected C:\Program Files (x86)\ path. It also must stop and start the Recording Server service in order for the new driver to be made available. If you re-install the StableFPS driver with different parameters, or if you add new video/audio to the %DevicePackPath%\StableFPS_DATA folder, you will need to perform "Replace Hardware" on each StableFPS hardware device you require to use the new settings/media. .PARAMETER Source The path to the StableFPS folder included with the MIP SDK installation. The default path is "C:\Program Files\Milestone\MIPSDK\Tools\StableFPS". To execute this command on a system without MIP SDK installed, make sure to copy the StableFPS folder to a path available to the target system. If you specify "-Path C:\StableFPS" then this command expects to find the folders C:\StableFPS\StableFPS_DATA and C:\StableFPS\vLatest .PARAMETER Cameras Each StableFPS hardware device can have between 1 and 200 camera channels associated with it. The default value is 32. .PARAMETER Streams Each camera channel can provide up to 5 streams. By default, each channel will provide only one stream. .PARAMETER DevicePackPath By default the DevicePackPath will be determined from the Get-RecorderConfig cmdlet which assumes the StableFPS driver is intended to be installed on the local machine which is also a Recording Server. In some cases you may wish to install the StableFPS driver to a remote machine. If this property is provided, then the driver will be deployed to the path without attempting to restart any Recording Server service or validating the presence of a Recording Server installation. It will then be your responsibility to restart the remote Recording Server to make the new driver available. .EXAMPLE Install-StableFPS -Source C:\StableFPS -Cameras 4 -Streams 2 Installs the StableFPS driver from the source already present at C:\StableFPS. Each StableFPS device added to the Recording Server will have 4 camera channels, each with the option of up to 2 streams. #> [CmdletBinding()] param ( [Parameter()] [string] $Source = "C:\Program Files\Milestone\MIPSDK\Tools\StableFPS", [Parameter()] [int] [ValidateRange(1, 200)] $Cameras = 32, [Parameter()] [int] [ValidateRange(1, 5)] $Streams = 1, [Parameter()] [string] $DevicePackPath ) begin { $Elevated = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (!$Elevated) { throw "Elevation is required for this command to work properly. Consider re-launching PowerShell by right-clicking and running as Administrator." } if (!(Test-Path (Join-Path $Source "StableFPS_DATA"))) { throw "Path not found: $((Join-Path $Source "StableFPS_DATA"))" } if (!(Test-Path (Join-Path $Source "vLatest"))) { throw "Path not found: $((Join-Path $Source "vLatest"))" } } process { $serviceStopped = $false try { $dpPath = if ([string]::IsNullOrWhiteSpace($DevicePackPath)) { (Get-RecorderConfig).DevicePackPath } else { $DevicePackPath } if (!(Test-Path $dpPath)) { throw "DevicePackPath not valid" } if ([string]::IsNullOrWhiteSpace($DevicePackPath)) { $service = Get-Service "Milestone XProtect Recording Server" if ($service.Status -eq [System.ServiceProcess.ServiceControllerStatus]::Running) { $service | Stop-Service -Force $serviceStopped = $true } } $srcData = Join-Path $Source "StableFPS_Data" $srcDriver = Join-Path $Source "vLatest" Copy-Item $srcData -Destination $dpPath -Container -Recurse -Force Copy-Item "$srcDriver\*" -Destination $dpPath -Recurse -Force $tempXml = Join-Path $dpPath "resources\StableFPS_TEMP.xml" $newXml = Join-Path $dpPath "resources\StableFPS.xml" $content = Get-Content $tempXml -Raw $content = $content.Replace("{CAM_NUM_REQUESTED}", $Cameras) $content = $content.Replace("{STREAM_NUM_REQUESTED}", $Streams) $content | Set-Content $newXml Remove-Item $tempXml } catch { throw } finally { if ($serviceStopped -and $null -ne $service) { $service.Refresh() $service.Start() } } } } function Invoke-ServerConfigurator { <# .SYNOPSIS Invokes the Milestone Server Configurator utility using command-line arguments .DESCRIPTION The Server Configurator is the utility responsible for managing the registration of Management Servers, Recording Servers and Data Collectors as well as the configuration of certificates for Management/Recorder communication, Client/Recorder communication and Mobile Server/Web Client/Mobile communication. In the 2020 R3 release, command-line parameters were introduced for the Server Configurator making it possible to automate registration and certificate configuration processes. Since PowerShell offers a more rich environment for discovering parameters and valid values as well as more useful object-based output, this cmdlet was written to wrap the utility with a PowerShell-friendly interface. .EXAMPLE PS C:\> Invoke-ServerConfigurator -ListCertificateGroups Lists the available Certificate Groups such as 'Server certificate', 'Streaming media certificate' and 'Mobile streaming media certificate', and their ID's. .EXAMPLE PS C:\> Invoke-ServerConfigurator -Register -AuthAddress http://MGMT -PassThru Registers all local Milestone components with the authorization server at http://MGMT and outputs a [pscustomobject] with the exit code, and standard output/error from the invocation of the Server Configurator executable. #> [CmdletBinding()] param( # Enable encryption for the CertificateGroup specified [Parameter(ParameterSetName = 'EnableEncryption', Mandatory)] [switch] $EnableEncryption, # Disable encryption for the CertificateGroup specified [Parameter(ParameterSetName = 'DisableEncryption', Mandatory)] [switch] $DisableEncryption, # Specifies the CertificateGroup [guid] identifying which component for which encryption # should be enabled or disabled [Parameter(ParameterSetName = 'EnableEncryption', Mandatory)] [Parameter(ParameterSetName = 'DisableEncryption', Mandatory)] [guid] $CertificateGroup, # Specifies the thumbprint of the certificate to be used to encrypt communications with the # component designated by the CertificateGroup id. [Parameter(ParameterSetName = 'EnableEncryption', Mandatory)] [string] $Thumbprint, # List the available certificate groups on the local machine. Output will be a [hashtable] # where the keys are the certificate group names (which may contain spaces) and the values # are the associated [guid] id's. [Parameter(ParameterSetName = 'ListCertificateGroups')] [switch] $ListCertificateGroups, # Register all local components with the optionally specified AuthAddress. If no # AuthAddress is provided, the last-known address will be used. [Parameter(ParameterSetName = 'Register', Mandatory)] [switch] $Register, # Specifies the address of the Authorization Server which is usually the Management Server # address. A [uri] value is expected, but only the URI host value will be used. The scheme # and port will be inferred based on whether encryption is enabled/disabled and is fixed to # port 80/443 as this is how Server Configurator is currently designed. [Parameter(ParameterSetName = 'Register')] [uri] $AuthAddress, # Specifies the path to the Server Configurator utility. Omit this path and the path will # be discovered using Get-RecorderConfig or Get-ManagementServerConfig by locating the # installation path of the Management Server or Recording Server and assuming the Server # Configurator is located in the same path. [Parameter()] [string] $Path, # Specifies that the standard output from the Server Configurator utility should be written # after the operation is completed. The output will include the following properties: # - StandardOutput # - StandardError # - ExitCode [Parameter(ParameterSetName = 'EnableEncryption')] [Parameter(ParameterSetName = 'DisableEncryption')] [Parameter(ParameterSetName = 'Register')] [switch] $PassThru ) process { $exePath = $Path if ([string]::IsNullOrWhiteSpace($exePath)) { # Find ServerConfigurator.exe by locating either the Management Server or Recording Server installation path $configurationInfo = try { Get-ManagementServerConfig } catch { try { Get-RecorderConfig } catch { $null } } if ($null -eq $configurationInfo) { Write-Error "Could not find a Management Server or Recording Server installation" return } $fileInfo = [io.fileinfo]::new($configurationInfo.InstallationPath) $exePath = Join-Path $fileInfo.Directory.Parent.FullName "Server Configurator\serverconfigurator.exe" if (-not (Test-Path $exePath)) { Write-Error "Expected to find Server Configurator at '$exePath' but failed." return } } # Ensure version is 20.3 (2020 R3) or newer $fileInfo = [io.fileinfo]::new($exePath) if ($fileInfo.VersionInfo.FileVersion -lt [version]"20.3") { Write-Error "Invoke-ServerConfigurator requires Milestone version 2020 R3 or newer as this is when command-line options were introduced. Found Server Configurator version $($fileInfo.VersionInfo.FileVersion)" return } $exitCode = @{ 0 = 'Success' -1 = 'Unknown error' -2 = 'Invalid arguments' -3 = 'Invalid argument value' -4 = 'Another instance is running' } # Get Certificate Group list for either display to user or verification $output = Get-ProcessOutput -FilePath $exePath -ArgumentList /listcertificategroups if ($output.ExitCode -ne 0) { Write-Error "Server Configurator exited with code $($output.ExitCode). $($exitCode.($output.ExitCode))." Write-Error $output.StandardOutput return } Write-Information $output.StandardOutput $groups = @{} foreach ($line in $output.StandardOutput -split ([environment]::NewLine)) { if ($line -match "Found '(?<groupName>.+)' group with ID = (?<groupId>.{36})") { $groups.$($Matches.groupName) = [guid]::Parse($Matches.groupId) } } switch ($PSCmdlet.ParameterSetName) { 'EnableEncryption' { if ($groups.Values -notcontains $CertificateGroup) { Write-Error "CertificateGroup value '$CertificateGroup' not found. Use the ListCertificateGroups switch to discover valid CertificateGroup values" return } $enableArgs = @('/enableencryption', "/certificategroup=$CertificateGroup", "/thumbprint=$Thumbprint", '/quiet') $output = Get-ProcessOutput -FilePath $exePath -ArgumentList $enableArgs if ($output.ExitCode -ne 0) { Write-Error "EnableEncryption failed. Server Configurator exited with code $($output.ExitCode). $($exitCode.($output.ExitCode))." Write-Error $output.StandardOutput } } 'DisableEncryption' { if ($groups.Values -notcontains $CertificateGroup) { Write-Error "CertificateGroup value '$CertificateGroup' not found. Use the ListCertificateGroups switch to discover valid CertificateGroup values" return } $disableArgs = @('/disableencryption', "/certificategroup=$CertificateGroup", '/quiet') $output = Get-ProcessOutput -FilePath $exePath -ArgumentList $disableArgs if ($output.ExitCode -ne 0) { Write-Error "EnableEncryption failed. Server Configurator exited with code $($output.ExitCode). $($exitCode.($output.ExitCode))." Write-Error $output.StandardOutput } } 'ListCertificateGroups' { Write-Output $groups return } 'Register' { $registerArgs = @('/register', '/quiet') if ($PSCmdlet.MyInvocation.BoundParameters -contains 'AuthAddress') { $registerArgs += $AuthAddress.ToString() } $output = Get-ProcessOutput -FilePath $exePath -ArgumentList $registerArgs if ($output.ExitCode -ne 0) { Write-Error "Registration failed. Server Configurator exited with code $($output.ExitCode). $($exitCode.($output.ExitCode))." Write-Error $output.StandardOutput } } Default { } } Write-Information $output.StandardOutput if ($PassThru) { Write-Output $output } } } function Resize-Image { [CmdletBinding()] [OutputType([System.Drawing.Image])] param( [Parameter(Mandatory, ValueFromPipeline)] [System.Drawing.Image] $Image, [Parameter(Mandatory)] [int] $Height, [Parameter()] [long] $Quality = 95, [Parameter()] [ValidateSet('BMP', 'JPEG', 'GIF', 'TIFF', 'PNG')] [string] $OutputFormat, [Parameter()] [switch] $DisposeSource ) process { if ($null -eq $Image -or $Image.Width -le 0 -or $Image.Height -le 0) { Write-Error 'Cannot resize an invalid image object.' return } [int]$width = $image.Width / $image.Height * $Height $bmp = [system.drawing.bitmap]::new($width, $Height) $graphics = [system.drawing.graphics]::FromImage($bmp) $graphics.InterpolationMode = [system.drawing.drawing2d.interpolationmode]::HighQualityBicubic $graphics.DrawImage($Image, 0, 0, $width, $Height) $graphics.Dispose() try { $formatId = if ([string]::IsNullOrWhiteSpace($OutputFormat)) { $Image.RawFormat.Guid } else { ([system.drawing.imaging.imagecodecinfo]::GetImageEncoders() | Where-Object FormatDescription -eq $OutputFormat).FormatID } $encoder = [system.drawing.imaging.imagecodecinfo]::GetImageEncoders() | Where-Object FormatID -eq $formatId $encoderParameters = [system.drawing.imaging.encoderparameters]::new(1) $qualityParameter = [system.drawing.imaging.encoderparameter]::new([system.drawing.imaging.encoder]::Quality, $Quality) $encoderParameters.Param[0] = $qualityParameter Write-Verbose "Saving resized image as $($encoder.FormatDescription) with $Quality% quality" $ms = [io.memorystream]::new() $bmp.Save($ms, $encoder, $encoderParameters) $resizedImage = [system.drawing.image]::FromStream($ms) Write-Output ($resizedImage) } finally { $qualityParameter.Dispose() $encoderParameters.Dispose() $bmp.Dispose() if ($DisposeSource) { $Image.Dispose() } } } } function Select-Camera { <# .SYNOPSIS Offers a UI dialog for selecting items, similar to the item selection interface in Management Client. .DESCRIPTION This cmdlet implements the VideoOS.Platform.UI.ItemPickerUserControl in a custom form to allow the user to select one or more items of any kind using a friendly and customizable user interface. .Parameter Title Specifies the text in the title-bar of the Item Picker window. The default is "Select Camera(s)". .Parameter SingleSelect The ItemPicker allows for multiple items by default. Supply this parameter to force selection of a single item. .Parameter AllowFolders Device groups are considered folders and are not selectable by default. To allow for selecting many items with one click, include this parameter. Consider using this with the FlattenOutput switch unless you specifically need to select a folder item instead of it's child items. .Parameter AllowServers Supply this switch to enable selection of servers. You might choose to do this if you want to select Recording Servers, or you want to select all child items, such as cameras, from a server. Consider using this with the FlattenOutput switch unless you specifically need to select a server item instead of it's child items. .Parameter RemoveDuplicates Automatically remove duplicate cameras from the output before outputing them. Useful when you select a folder which may have the same cameras in more than one child folder. .Parameter OutputAsItem Output cameras as VideoOS.Platform.Item objects instead of converting them to Configuration API Camera objects. Depending on your needs, it may be more performant to use OutputAsItem. For example, if you are using a cmdlet like Get-Snapshot, you can extract the $item.FQID.ObjectId and provide that Guid in the CameraId parameter to avoid an unnecessary conversion between Item, ConfigurationItem, and back again. .EXAMPLE PS C:\> Select-Camera -AllowFolders -AllowServers -RemoveDuplicates Launch the Item Picker and allow the user to add servers or whole groups/folders. The output will be de-duplicated in the event the user-defined groups have the same camera(s) present in more than one child folder. The objects returned will be the same kind of object returned by the Get-Camera cmdlet. .EXAMPLE PS C:\> Select-Camera -OutputAsItem | % { Get-Snapshot -CameraId $_.FQID.ObjectId -Live } Launch the Item Picker and use the resulting Item.FQID.ObjectId properties of the camera(s) to get a live snapshot from the Get-Snapshot cmdlet. #> [CmdletBinding()] param( [Parameter()] [string] $Title = "Select Camera(s)", [Parameter()] [switch] $SingleSelect, [Parameter()] [switch] $AllowFolders, [Parameter()] [switch] $AllowServers, [Parameter()] [switch] $RemoveDuplicates, [Parameter()] [switch] $OutputAsItem ) process { $items = Select-VideoOSItem -Title $Title -Kind ([VideoOS.Platform.Kind]::Camera) -AllowFolders:$AllowFolders -AllowServers:$AllowServers -SingleSelect:$SingleSelect -FlattenOutput $processed = @{} if ($RemoveDuplicates) { foreach ($item in $items) { if ($processed.ContainsKey($item.FQID.ObjectId)) { continue } $processed.Add($item.FQID.ObjectId, $null) if ($OutputAsItem) { Write-Output $item } else { Get-Camera -Id $item.FQID.ObjectId } } } else { if ($OutputAsItem) { Write-Output $items } else { Write-Output ($items | ForEach-Object { Get-Camera -Id $_.FQID.ObjectId }) } } } } function Select-VideoOSItem { [CmdletBinding()] param ( [Parameter()] [string] $Title = "Select Item(s)", [Parameter()] [guid[]] $Kind, [Parameter()] [VideoOS.Platform.Admin.Category[]] $Category, [Parameter()] [switch] $SingleSelect, [Parameter()] [switch] $AllowFolders, [Parameter()] [switch] $AllowServers, [Parameter()] [switch] $KindUserSelectable, [Parameter()] [switch] $CategoryUserSelectable, [Parameter()] [switch] $FlattenOutput, [Parameter()] [switch] $HideGroupsTab, [Parameter()] [switch] $HideServerTab ) process { $form = [MilestonePSTools.UI.CustomItemPickerForm]::new(); $form.KindFilter = $Kind $form.CategoryFilter = $Category $form.AllowFolders = $AllowFolders $form.AllowServers = $AllowServers $form.KindUserSelectable = $KindUserSelectable $form.CategoryUserSelectable = $CategoryUserSelectable $form.SingleSelect = $SingleSelect $form.GroupTabVisable = -not $HideGroupsTab $form.ServerTabVisable = -not $HideServerTab $form.Icon = [System.Drawing.Icon]::FromHandle([VideoOS.Platform.UI.Util]::ImageList.Images[[VideoOS.Platform.UI.Util]::SDK_GeneralIx].GetHicon()) $form.Text = $Title $form.TopMost = $true $form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen $form.BringToFront() $form.Activate() if ($form.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { if ($FlattenOutput) { Write-Output $form.ItemsSelectedFlattened } else { Write-Output $form.ItemsSelected } } } } function Set-ConfigurationItemProperty { <# .SYNOPSIS Sets the value of a given ConfigurationItem property by key .DESCRIPTION A ConfigurationItem may have zero or more Property objects in the Properties array. Each property has a key name and a value. Since the Properties property on a ConfigurationItem has no string-based indexer, you are required to search the array of properties for the one with the Key you're interested in, and then set the Value property on it. This cmdlet is a simple wrapper which does the Where-Object for you, throws an error if the Key does not exist, and optionally passes the modified ConfigurationItem back into the pipeline. .PARAMETER InputObject A [VideoOS.ConfigurationApi.ClientService.ConfigurationItem] with a property to be modified. .PARAMETER Key A string representing the key of the property to be modified. .Parameter Value A string value to be used as the new value for the property named by the given key. .Parameter PassThru Pass the modified ConfigurationItem from $InputObject back into the pipeline. .EXAMPLE PS C:\> Get-ConfigurationItem -Path / | Set-ConfigurationItemProperty -Key Description -Value 'A new description' -PassThru | Set-ConfigurationItem Gets a ConfigurationItem representing the Management Server, changes the Description property, and pushes the change to the Management Server using Set-ConfigurationItem. #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [VideoOS.ConfigurationApi.ClientService.ConfigurationItem] [ValidateNotNullOrEmpty()] $InputObject, [Parameter(Mandatory)] [string] [ValidateNotNullOrEmpty()] $Key, [Parameter(Mandatory)] [string] [ValidateNotNullOrEmpty()] $Value, [Parameter()] [switch] $PassThru ) process { $property = $InputObject.Properties | Where-Object Key -eq $Key if ($null -eq $property) { Write-Error -Message "Key '$Key' not found on configuration item $($InputObject.Path)" -TargetObject $InputObject -Category InvalidArgument return } $property.Value = $Value if ($PassThru) { $InputObject } } } <# Functions in this module are written as independent PS1 files, and to improve module load time they are "comiled" into this PSM1 file. If you're looking at this file prior to build, now you know how all the functions will be loaded later. If you're looking at this file after build, now you know why this file has so many lines :) #> $cmdlets = (Get-Command -Module MilestonePSTools -CommandType Cmdlet).Name Export-ModuleMember -Cmdlet $cmdlets |