UnifiStockTracker.psm1
function Write-Color { <# .SYNOPSIS Write-Color is a wrapper around Write-Host delivering a lot of additional features for easier color options. .DESCRIPTION Write-Color is a wrapper around Write-Host delivering a lot of additional features for easier color options. It provides: - Easy manipulation of colors, - Logging output to file (log) - Nice formatting options out of the box. - Ability to use aliases for parameters .PARAMETER Text Text to display on screen and write to log file if specified. Accepts an array of strings. .PARAMETER Color Color of the text. Accepts an array of colors. If more than one color is specified it will loop through colors for each string. If there are more strings than colors it will start from the beginning. Available colors are: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White .PARAMETER BackGroundColor Color of the background. Accepts an array of colors. If more than one color is specified it will loop through colors for each string. If there are more strings than colors it will start from the beginning. Available colors are: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White .PARAMETER StartTab Number of tabs to add before text. Default is 0. .PARAMETER LinesBefore Number of empty lines before text. Default is 0. .PARAMETER LinesAfter Number of empty lines after text. Default is 0. .PARAMETER StartSpaces Number of spaces to add before text. Default is 0. .PARAMETER LogFile Path to log file. If not specified no log file will be created. .PARAMETER DateTimeFormat Custom date and time format string. Default is yyyy-MM-dd HH:mm:ss .PARAMETER LogTime If set to $true it will add time to log file. Default is $true. .PARAMETER LogRetry Number of retries to write to log file, in case it can't write to it for some reason, before skipping. Default is 2. .PARAMETER Encoding Encoding of the log file. Default is Unicode. .PARAMETER ShowTime Switch to add time to console output. Default is not set. .PARAMETER NoNewLine Switch to not add new line at the end of the output. Default is not set. .PARAMETER NoConsoleOutput Switch to not output to console. Default all output goes to console. .EXAMPLE Write-Color -Text "Red ", "Green ", "Yellow " -Color Red,Green,Yellow .EXAMPLE Write-Color -Text "This is text in Green ", "followed by red ", "and then we have Magenta... ", "isn't it fun? ", "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan .EXAMPLE Write-Color -Text "This is text in Green ", "followed by red ", "and then we have Magenta... ", "isn't it fun? ", "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan -StartTab 3 -LinesBefore 1 -LinesAfter 1 .EXAMPLE Write-Color "1. ", "Option 1" -Color Yellow, Green Write-Color "2. ", "Option 2" -Color Yellow, Green Write-Color "3. ", "Option 3" -Color Yellow, Green Write-Color "4. ", "Option 4" -Color Yellow, Green Write-Color "9. ", "Press 9 to exit" -Color Yellow, Gray -LinesBefore 1 .EXAMPLE Write-Color -LinesBefore 2 -Text "This little ","message is ", "written to log ", "file as well." ` -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt" -TimeFormat "yyyy-MM-dd HH:mm:ss" Write-Color -Text "This can get ","handy if ", "want to display things, and log actions to file ", "at the same time." ` -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt" .EXAMPLE Write-Color -T "My text", " is ", "all colorful" -C Yellow, Red, Green -B Green, Green, Yellow Write-Color -t "my text" -c yellow -b green Write-Color -text "my text" -c red .EXAMPLE Write-Color -Text "Testuję czy się ładnie zapisze, czy będą problemy" -Encoding unicode -LogFile 'C:\temp\testinggg.txt' -Color Red -NoConsoleOutput .NOTES Understanding Custom date and time format strings: https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings Project support: https://github.com/EvotecIT/PSWriteColor Original idea: Josh (https://stackoverflow.com/users/81769/josh) #> [alias('Write-Colour')] [CmdletBinding()] param ( [alias ('T')] [String[]]$Text, [alias ('C', 'ForegroundColor', 'FGC')] [ConsoleColor[]]$Color = [ConsoleColor]::White, [alias ('B', 'BGC')] [ConsoleColor[]]$BackGroundColor = $null, [alias ('Indent')][int] $StartTab = 0, [int] $LinesBefore = 0, [int] $LinesAfter = 0, [int] $StartSpaces = 0, [alias ('L')] [string] $LogFile = '', [Alias('DateFormat', 'TimeFormat')][string] $DateTimeFormat = 'yyyy-MM-dd HH:mm:ss', [alias ('LogTimeStamp')][bool] $LogTime = $true, [int] $LogRetry = 2, [ValidateSet('unknown', 'string', 'unicode', 'bigendianunicode', 'utf8', 'utf7', 'utf32', 'ascii', 'default', 'oem')][string]$Encoding = 'Unicode', [switch] $ShowTime, [switch] $NoNewLine, [alias('HideConsole')][switch] $NoConsoleOutput ) if (-not $NoConsoleOutput) { $DefaultColor = $Color[0] if ($null -ne $BackGroundColor -and $BackGroundColor.Count -ne $Color.Count) { Write-Error "Colors, BackGroundColors parameters count doesn't match. Terminated." return } if ($LinesBefore -ne 0) { for ($i = 0; $i -lt $LinesBefore; $i++) { Write-Host -Object "`n" -NoNewline } } # Add empty line before if ($StartTab -ne 0) { for ($i = 0; $i -lt $StartTab; $i++) { Write-Host -Object "`t" -NoNewline } } # Add TABS before text if ($StartSpaces -ne 0) { for ($i = 0; $i -lt $StartSpaces; $i++) { Write-Host -Object ' ' -NoNewline } } # Add SPACES before text if ($ShowTime) { Write-Host -Object "[$([datetime]::Now.ToString($DateTimeFormat))] " -NoNewline } # Add Time before output if ($Text.Count -ne 0) { if ($Color.Count -ge $Text.Count) { # the real deal coloring if ($null -eq $BackGroundColor) { for ($i = 0; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline } } else { for ($i = 0; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline } } } else { if ($null -eq $BackGroundColor) { for ($i = 0; $i -lt $Color.Length ; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline } for ($i = $Color.Length; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -NoNewline } } else { for ($i = 0; $i -lt $Color.Length ; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline } for ($i = $Color.Length; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -BackgroundColor $BackGroundColor[0] -NoNewline } } } } if ($NoNewLine -eq $true) { Write-Host -NoNewline } else { Write-Host } # Support for no new line if ($LinesAfter -ne 0) { for ($i = 0; $i -lt $LinesAfter; $i++) { Write-Host -Object "`n" -NoNewline } } # Add empty line after } if ($Text.Count -and $LogFile) { # Save to file $TextToFile = "" for ($i = 0; $i -lt $Text.Length; $i++) { $TextToFile += $Text[$i] } $Saved = $false $Retry = 0 Do { $Retry++ try { if ($LogTime) { "[$([datetime]::Now.ToString($DateTimeFormat))] $TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false } else { "$TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false } $Saved = $true } catch { if ($Saved -eq $false -and $Retry -eq $LogRetry) { Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Tried ($Retry/$LogRetry))" } else { Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Retrying... ($Retry/$LogRetry)" } } } Until ($Saved -eq $true -or $Retry -ge $LogRetry) } } function Get-UnifiStock { <# .SYNOPSIS Get the stock status of Ubiquiti products from their online store. .DESCRIPTION Get the stock status of Ubiquiti products from their online store. .PARAMETER Store The store to check for stock. Valid values are: Europe, USA If you want to use a different store you can use Get-UnifiStockLegacy for other countries. This is because the legacy store has a different format for the JSON data, and are not yet migrated to new "look" .PARAMETER Collection Which collection to list. .EXAMPLE Get-UnifiStock -Store USA -Collection AccessoriesCabling, CableBox | Sort-Object -Property Name | Format-Table .EXAMPLE Get-UnifiStock -Store Europe -Collection HostingAndGatewaysCloud,DreamMachine, DreamRouter | Sort-Object -Property Name | Format-Table .EXAMPLE Get-UnifiStock -Store Europe | Sort-Object -Property Name | Format-Table .NOTES General notes #> [cmdletbinding()] param( [ValidateSet('Europe', 'USA')] [Parameter(Mandatory)][string] $Store, [ValidateSet( 'AccessoriesCabling', 'AccessPointMounting', 'AccessPointSkins', 'CableBox', 'CablePatch', 'CableSFP', 'CameraEnhancers', 'CameraSecurityBulletDSLR', 'CameraSecurityBulletHighPerformance', 'CameraSecurityBulletStandard', 'CameraSecurityCompactPoEWired', 'CameraSecurityCompactWiFiConnected', 'CameraSecurityDome360', 'CameraSecurityDomeSlim', 'CameraSecurityDoorAccessAccessories', 'CameraSecurityDoorAccessReaders', 'CameraSecurityDoorAccessStarterKit', 'CameraSecurityInteriorDesign', 'CameraSecurityNVRLargeScale', 'CameraSecurityNVRMidScale', 'CameraSecurityPTZ', 'CameraSecuritySpecialChime', 'CameraSecuritySpecialSensor', 'CameraSecuritySpecialViewport', 'CameraSecuritySpecialWiFiDoorbell', 'CameraSkins', 'DesktopStands', 'DeviceMounting', 'DreamMachine', 'DreamRouter', 'HDDStorage', 'HostingAndGatewaysCloud', 'HostingAndGatewaysLargeScale', 'HostingAndGatewaysSmallScale', 'InstallationsRackmount', 'InternetBackup', 'NewIntegrationsAVDisplayMounting', 'NewIntegrationsAVGiantPoETouchscreens', 'NewIntegrationsPhoneATA', 'NewIntegrationsPhoneCompact', 'NewIntegrationsPhoneExecutive', 'PoEAndPower', 'PoEPower', 'PowerTechPowerRedundancy', 'PowerTechUninterruptiblePoE', 'SwitchingEnterpriseAggregation', 'SwitchingEnterprisePoE', 'SwitchingProEthernet', 'SwitchingProPoE', 'SwitchingStandardEthernet', 'SwitchingStandardPoE', 'SwitchingUtility10GbpsEthernet', 'SwitchingUtilityMini', 'SwitchingUtilityPoE', 'WiFiBuildingBridge10Gigabit', 'WiFiFlagshipCompact', 'WiFiFlagshipHighCapacity', 'WiFiInWallOutletMesh', 'WiFiMan', 'WiFiOutdoorFlexible', 'WiFiFlagshipLongRange', 'WiFiOutdoorLongRange', "NewIntegrationsMobileRouting", "CameraSecurityDoorAccessHub", "NewIntegrationsAVDigitalSignage", "SwitchingUtilityIndustrial", "SwitchingUtilityIndoorOutdoor", "SwitchingEnterprise10GbpsEthernet", "SwitchingUtilityHiPowerPoE", "WiFiMegaCapacity", "PowerTechUninterruptiblePower", "PowerTechPowerDistribution", "CameraSecuritySpecialFloodlight", "DreamWall", "NewIntegrationsEVCharging", "CloudKeyRackMount", "AmpliFiMesh", "AmpliFiAlien", "WiFiInWallCompact", "WiFiInWallHighCapacity", "AccessPointAntennas", "CameraSecurityBulletEnhancedAI", "WiFiBuildingBridgeGigabit", # organizational collections "Cabling", "AccessPointMounting", "AccessPointSkins", "CableSFP", "CameraEnhancers", "CameraSkins", "DeviceMounting", "DisplayMounting", "HostingAndGatewaysCloud", "HostingAndGatewaysLargeScale", "HostingAndGatewaysSmallScale", "PoEAndPower", "PoEPower", "WiFiManager", "InternetBackup", "PowerRedundancy", "ProEthernetSwitching", "StandardEthernetSwitching", "StandardPoESwitching", "10GbpsEthernetSwitching", "PoESwitching", "FlagshipCompactWiFi", "FlagshipHighCapacityWiFi", "InWallHighCapacityWiFi", "OutdoorFlexibleWiFi", "UICare" )][string[]] $Collection ) $Stores = @{ Europe = 'eu' USA = 'us' } $StoreLinks = @{ Europe = 'https://eu.store.ui.com/eu/en' USA = 'https://store.ui.com/us/en' } $Accessories = @{ # direct collections "uisp-accessories-cabling" = "AccessoriesCabling" "unifi-accessory-tech-access-point-mounting" = "AccessPointMounting" "unifi-accessory-tech-access-point-skins" = "AccessPointSkins" "unifi-accessory-tech-cable-box" = "CableBox" "unifi-accessory-tech-cable-patch" = "CablePatch" "unifi-accessory-tech-cable-sfp" = "CableSFP" "unifi-accessory-tech-camera-enhancers" = "CameraEnhancers" "unifi-accessory-tech-camera-skins" = "CameraSkins" "unifi-accessory-tech-desktop-stands" = "DesktopStands" "unifi-accessory-tech-device-mounting" = "DeviceMounting" "unifi-accessory-tech-hdd-storage" = "HDDStorage" "unifi-accessory-tech-hosting-and-gateways-cloud" = "HostingAndGatewaysCloud" "unifi-accessory-tech-hosting-and-gateways-large-scale" = "HostingAndGatewaysLargeScale" "unifi-accessory-tech-hosting-and-gateways-small-scale" = "HostingAndGatewaysSmallScale" "unifi-accessory-tech-installations-rackmount" = "InstallationsRackmount" "unifi-accessory-tech-poe-and-power" = "PoEAndPower" "unifi-accessory-tech-poe-power" = "PoEPower" "unifi-accessory-tech-wifiman" = "WiFiMan" "unifi-camera-security-bullet-dslr" = "CameraSecurityBulletDSLR" "unifi-camera-security-bullet-high-performance" = "CameraSecurityBulletHighPerformance" "unifi-camera-security-bullet-standard" = "CameraSecurityBulletStandard" "unifi-camera-security-compact-poe-wired" = "CameraSecurityCompactPoEWired" "unifi-camera-security-compact-wifi-connected" = "CameraSecurityCompactWiFiConnected" "unifi-camera-security-dome-360" = "CameraSecurityDome360" "unifi-camera-security-dome-slim" = "CameraSecurityDomeSlim" "unifi-camera-security-door-access-accessories" = "CameraSecurityDoorAccessAccessories" "unifi-camera-security-door-access-readers" = "CameraSecurityDoorAccessReaders" "unifi-camera-security-door-access-starter-kit" = "CameraSecurityDoorAccessStarterKit" "unifi-camera-security-interior-design" = "CameraSecurityInteriorDesign" "unifi-camera-security-nvr-large-scale" = "CameraSecurityNVRLargeScale" "unifi-camera-security-nvr-mid-scale" = "CameraSecurityNVRMidScale" "unifi-camera-security-ptz" = "CameraSecurityPTZ" "unifi-camera-security-special-chime" = "CameraSecuritySpecialChime" "unifi-camera-security-special-sensor" = "CameraSecuritySpecialSensor" "unifi-camera-security-special-viewport" = "CameraSecuritySpecialViewport" "unifi-camera-security-special-wifi-doorbell" = "CameraSecuritySpecialWiFiDoorbell" "unifi-dream-machine" = "DreamMachine" "unifi-dream-router" = "DreamRouter" "unifi-internet-backup" = "InternetBackup" "unifi-new-integrations-av-display-mounting" = "NewIntegrationsAVDisplayMounting" "unifi-new-integrations-av-giant-poe-touchscreens" = "NewIntegrationsAVGiantPoETouchscreens" "unifi-new-integrations-phone-ata" = "NewIntegrationsPhoneATA" "unifi-new-integrations-phone-compact" = "NewIntegrationsPhoneCompact" "unifi-new-integrations-phone-executive" = "NewIntegrationsPhoneExecutive" "unifi-power-tech-power-redundancy" = "PowerTechPowerRedundancy" "unifi-power-tech-uninterruptible-poe" = "PowerTechUninterruptiblePoE" "unifi-switching-enterprise-aggregation" = "SwitchingEnterpriseAggregation" "unifi-switching-enterprise-power-over-ethernet" = "SwitchingEnterprisePoE" "unifi-switching-pro-ethernet" = "SwitchingProEthernet" "unifi-switching-pro-power-over-ethernet" = "SwitchingProPoE" "unifi-switching-standard-ethernet" = "SwitchingStandardEthernet" "unifi-switching-standard-power-over-ethernet" = "SwitchingStandardPoE" "unifi-switching-utility-10-gbps-ethernet" = "SwitchingUtility10GbpsEthernet" "unifi-switching-utility-mini" = "SwitchingUtilityMini" "unifi-switching-utility-poe" = "SwitchingUtilityPoE" "unifi-wifi-building-bridge-10-gigabit" = "WiFiBuildingBridge10Gigabit" "unifi-wifi-flagship-compact" = "WiFiFlagshipCompact" "unifi-wifi-flagship-high-capacity" = "WiFiFlagshipHighCapacity" "unifi-wifi-inwall-outlet-mesh" = "WiFiInWallOutletMesh" "unifi-wifi-outdoor-flexible" = "WiFiOutdoorFlexible" "unifi-wifi-flagship-long-range" = "WiFiFlagshipLongRange" "unifi-wifi-outdoor-long-range" = "WiFiOutdoorLongRange" "unifi-new-integrations-mobile-routing" = "NewIntegrationsMobileRouting" "unifi-camera-security-door-access-hub" = "CameraSecurityDoorAccessHub" "unifi-new-integrations-av-digital-signage" = "NewIntegrationsAVDigitalSignage" "unifi-switching-utility-industrial" = "SwitchingUtilityIndustrial" "unifi-switching-utility-indoor-outdoor" = "SwitchingUtilityIndoorOutdoor" "unifi-switching-enterprise-10-gbps-ethernet" = "SwitchingEnterprise10GbpsEthernet" "unifi-switching-utility-hi-power-poe" = "SwitchingUtilityHiPowerPoE" "unifi-wifi-mega-capacity" = "WiFiMegaCapacity" "unifi-power-tech-uninterruptible-power" = "PowerTechUninterruptiblePower" "unifi-power-tech-power-distribution" = "PowerTechPowerDistribution" "unifi-camera-security-special-floodlight" = "CameraSecuritySpecialFloodlight" "unifi-dream-wall" = "DreamWall" "unifi-new-integrations-ev-charging" = "NewIntegrationsEVCharging" "cloud-key-rack-mount" = "CloudKeyRackMount" "amplifi-mesh" = "AmpliFiMesh" "amplifi-alien" = "AmpliFiAlien" "unifi-wifi-inwall-compact" = "WiFiInWallCompact" "unifi-wifi-inwall-high-capacity" = "WiFiInWallHighCapacity" "unifi-accessory-tech-access-point-antennas" = "AccessPointAntennas" "unifi-camera-security-bullet-enhanced-ai" = "CameraSecurityBulletEnhancedAI" "unifi-wifi-building-bridge-gigabit" = "WiFiBuildingBridgeGigabit" # organizational collection "accessories-cabling" = "Cabling" "accessory-tech-access-point-mounting" = "AccessPointMounting" "accessory-tech-access-point-skins" = "AccessPointSkins" "accessory-tech-cable-sfp" = "CableSFP" "accessory-tech-camera-enhancers" = "CameraEnhancers" "accessory-tech-camera-skins" = "CameraSkins" "accessory-tech-device-mounting" = "DeviceMounting" "accessory-tech-display-mounting" = "DisplayMounting" "accessory-tech-hosting-and-gateways-cloud" = "HostingAndGatewaysCloud" "accessory-tech-hosting-and-gateways-large-scale" = "HostingAndGatewaysLargeScale" "accessory-tech-hosting-and-gateways-small-scale" = "HostingAndGatewaysSmallScale" "accessory-tech-poe-and-power" = "PoEAndPower" "accessory-tech-poe-power" = "PoEPower" "accessory-tech-wifiman" = "WiFiManager" "internet-backup" = "InternetBackup" "power-tech-power-redundancy" = "PowerRedundancy" "switching-pro-ethernet" = "ProEthernetSwitching" "switching-standard-ethernet" = "StandardEthernetSwitching" "switching-standard-power-over-ethernet" = "StandardPoESwitching" "switching-utility-10-gbps-ethernet" = "10GbpsEthernetSwitching" "switching-utility-poe" = "PoESwitching" "wifi-flagship-compact" = "FlagshipCompactWiFi" "wifi-flagship-high-capacity" = "FlagshipHighCapacityWiFi" "wifi-inwall-high-capacity" = "InWallHighCapacityWiFi" "wifi-outdoor-flexible" = "OutdoorFlexibleWiFi" "ui-care" = "UICare" } $UrlStore = $Stores[$Store] $UrlStoreLink = $StoreLinks[$Store] $ProgressPreference = 'SilentlyContinue' try { Write-Verbose -Message "Get-UnifiStock - Getting Unifi products" $Output = Invoke-RestMethod -UseBasicParsing -Uri "https://ecomm.svc.ui.com/graphql" -Method Post -ContentType "application/json" -Body "{`"operationName`":`"GetProductsForLandingPagePro`",`"variables`":{`"input`":{`"limit`":250,`"offset`":0,`"filter`":{`"storeId`":`"$UrlStore`",`"language`":`"en`",`"line`":`"Unifi`"}}},`"query`":`"query GetProductsForLandingPagePro(`$input: StorefrontProductListInput!) {\n storefrontProducts(input: `$input) {\n pagination {\n limit\n offset\n total\n __typename\n }\n items {\n ...LandingProProductFragment\n __typename\n }\n __typename\n }\n}\n\nfragment LandingProProductFragment on StorefrontProduct {\n id\n title\n shortTitle\n name\n slug\n collectionSlug\n organizationalCollectionSlug\n shortDescription\n tags {\n name\n __typename\n }\n gallery {\n ...ImageOnlyGalleryFragment\n __typename\n }\n options {\n id\n title\n values {\n id\n title\n __typename\n }\n __typename\n }\n variants {\n id\n sku\n status\n title\n galleryItemIds\n isEarlyAccess\n optionValueIds\n displayPrice {\n ...MoneyFragment\n __typename\n }\n hasPurchaseHistory\n __typename\n }\n __typename\n}\n\nfragment ImageOnlyGalleryFragment on Gallery {\n id\n items {\n id\n data {\n __typename\n ... on Asset {\n id\n mimeType\n url\n height\n width\n __typename\n }\n }\n __typename\n }\n type\n __typename\n}\n\nfragment MoneyFragment on Money {\n amount\n currency\n __typename\n}`"}" $Products = $Output.data.storefrontProducts.items } catch { Write-Color -Text "Unable to get Unifi products. Error: $($_.Exception.Message)" -Color Red return } if ($Products) { Write-Verbose -Message "Get-UnifiStock - Got $($Products.Count) products" $UnifiProducts = foreach ($Product in $Products) { foreach ($Variant in $Product.variants) { if ($Product.collectionSlug) { $Category = $Accessories[$Product.collectionSlug] } elseif ($Product.organizationalCollectionSlug) { $Category = $Accessories[$Product.organizationalCollectionSlug] } else { $Category = 'Unknown' } if ($Collection) { if ($Category -notin $Collection) { continue } } [PSCustomObject] @{ Name = $Product.title ShortName = $Product.shortTitle Available = $Variant.status -eq 'AVAILABLE' Category = $Category Collection = $Product.collectionSlug OrganizationalCollectionSlug = $Product.organizationalCollectionSlug SKU = $Variant.sku SKUName = $Variant.title EarlyAccess = $Variant.isEarlyAccess ProductUrl = "$UrlStoreLink/collections/$($Product.collectionSlug)/products/$($Product.slug)" #Tags = $Product.tags } } } $UnifiProducts } } function Get-UnifiStockLegacy { <# .SYNOPSIS Get the stock status of Ubiquiti products from their online store. .DESCRIPTION Get the stock status of Ubiquiti products from their online store. .PARAMETER Store The store to check for stock. Valid values are: Europe, USA, Brazil, India, Japan, Taiwan, Signapore, Mexico, China .PARAMETER Collection Which collection to list. .EXAMPLE Get-UnifiStock -Store USA -Collection Protect, ProtectAccessories, ProtectNVR | Sort-Object -Property Name | Format-Table .EXAMPLE Get-UnifiStock -Store Europe -Collection Protect, NetworkWifi | Sort-Object -Property Name | Format-Table .EXAMPLE Get-UnifiStock -Store Europe | Sort-Object -Property Name | Format-Table .NOTES General notes #> [cmdletbinding()] param( [ValidateSet('Brazil', 'India', 'Japan', 'Taiwan', 'Signapore', 'Mexico', 'China')] [Parameter(Mandatory)] [string] $Store, [ValidateSet( 'EarlyAccess', 'EarlyAccessConnect', 'EarlyAccessDoorAccess', 'EarlyAccessSmartpower', 'EarlyAccessUispFiber', 'EarlyAccessUispWired', 'EarlyAccessUispWireless', 'EarlyAccessUnifiNetworkHost', 'NetworkHost', 'NetworkOS', 'NetworkRoutingOffload', 'NetworkRoutingSwitching', 'NetworkSmartPower', 'NetworkSwitching', 'NetworkWifi', 'OperatorAirmaxAndLtu', 'OperatorIspInfrastructure', 'Protect', 'ProtectAccessories', 'ProtectNVR', 'UnifiAccessories', 'UnifiConnect', 'UnifiDoorAccess', 'UnifiPhoneSystem' )] [string[]] $Collection ) $Stores = @{ Europe = 'https://eu.store.ui.com' USA = 'https://store.ui.com' Brazil = 'https://br.store.ui.com' India = 'https://store-ui.in' Japan = 'https://jp.store.ui.com' Taiwan = 'https://tw.store.ui.com' Singapore = 'https://sg.store.ui.com' Mexico = 'https://mx.store.ui.com' China = 'https://store.ui.com.cn' } $Collections = @{ Protect = 'unifi-protect' ProtectNVR = 'unifi-protect-nvr' ProtectAccessories = 'unifi-protect-accessories' NetworkOS = 'unifi-network-unifi-os-consoles' NetworkRoutingSwitching = 'unifi-network-routing-switching' NetworkSmartPower = 'unifi-network-smartpower' NetworkRoutingOffload = 'unifi-network-routing-offload' NetworkHost = 'unifi-network-host' NetworkSwitching = 'unifi-network-switching' NetworkWifi = 'unifi-network-wireless' UnifiAccessories = 'unifi-accessories' EarlyAccess = 'early-access' EarlyAccessDoorAccess = 'early-access-door-access' EarlyAccessConnect = 'early-access-connect' EarlyAccessSmartpower = 'early-access-smartpower' EarlyAccessUispFiber = 'early-access-uisp-fiber' EarlyAccessUispWired = 'early-access-uisp-wired' EarlyAccessUispWireless = 'early-access-uisp-wireless' EarlyAccessUnifiNetworkHost = 'early-access-unifi-network-host' UnifiConnect = 'unifi-connect' UnifiDoorAccess = 'unifi-door-access' OperatorAirmaxAndLtu = 'operator-airmax -and -ltu' OperatorIspInfrastructure = 'operator-isp-infrastructure' UnifiPhoneSystem = 'unifi-phone-system' } $UrlStore = $Stores[$Store] if (-not $Collection) { $Collection = $Collections.Keys } foreach ($Category in $Collection) { $UrlCollection = $Collections[$Category] $Url = "$UrlStore/collections/$UrlCollection" $UrlProducts = "$Url/products.json" $ProgressPreference = 'SilentlyContinue' try { Write-Verbose -Message "Get-UnifiStock - Getting $UrlProducts" $Output = Invoke-WebRequest -Uri $UrlProducts -ErrorAction Stop -Verbose:$false } catch { Write-Color -Text "Unable to get $UrlProducts. Error: $($_.Exception.Message)" -Color Red return } if ($Output) { $OutputJSON = $Output.Content | ConvertFrom-Json $UnifiProducts = foreach ($Product in $OutputJSON.products) { foreach ($Variant in $Product.variants) { try { $DateCreated = [DateTime]::Parse($Variant.created_at) #$DateCreated = [DateTime]::ParseExact($Variant.created_at, 'yyyy-MM-ddTHH:mm:sszzz', $null) } catch { Write-Verbose -Message "Unable to parse date: $($Variant.created_at). Skipping" $DateCreated = $null } try { $DateUpdated = [DateTime]::Parse($Variant.updated_at) #$DateUpdated = [DateTime]::ParseExact($Variant.updated_at, 'yyyy-MM-ddTHH:mm:sszzz', $null) } catch { Write-Verbose -Message "Unable to parse date: $($Variant.updated_at). Skipping" $DateUpdated = $null } [PSCustomObject] @{ Name = $Product.title Available = $Variant.available Category = $Category Price = $Variant.price SKU = $Variant.sku SKUName = $Variant.title #Inventory = $Variant.inventory_quantity Created = $DateCreated Updated = $DateUpdated ProductUrl = "$Url/products/$($Product.handle)" Tags = $Product.tags } } } $UnifiProducts } } } function Wait-UnifiStock { <# .SYNOPSIS When run waits for the specified SKU or Product to be in stock in Ubiquiti's online store. .DESCRIPTION When run waits for the specified SKU or Product to be in stock in Ubiquiti's online store. Once the product is in stock the function will play a beep, read which product is in stock and open a browser to specific product page. .PARAMETER ProductName One or more products to wait for to be in stock with search by it's Name .PARAMETER ProductSKU One or more products to wait for to be in stock with search by it's SKU .PARAMETER Store The store to check for stock. Valid values are Europe, USA. If you want to use a different store you can use Wait-UnifiStockLegacy for other countries. This is because the legacy store has a different format for the JSON data, and are not yet migrated to new "look" .PARAMETER Seconds The number of seconds to wait between checks. Default is 60 seconds. .PARAMETER DoNotOpenWebsite If specified the website will not be opened when the product is in stock. .PARAMETER DoNotPlaySound If specified the sound will not be played when the product is in stock. .PARAMETER DoNotUseBeep If specified the beep will not be played when the product is in stock. .EXAMPLE Wait-UnifiStock -ProductSKU 'UDR-EU' -ProductName 'Switch Flex XG' -Seconds 60 -Store Europe .EXAMPLE Wait-UnifiStock -ProductName 'UniFi6 Mesh', 'G4 Doorbell Pro', 'Camera G4 Pro', 'Test' -Seconds 60 -Store Europe .EXAMPLE Wait-UnifiStock -ProductName 'UniFi6 Mesh', 'G4 Doorbell Pro', 'Camera G4 Pro', 'Test' -Seconds 60 -DoNotUseBeep -Store Europe .NOTES General notes #> [cmdletBinding()] param( [string[]] $ProductName, [string[]] $ProductSKU, [parameter(Mandatory)][ValidateSet('Europe', 'USA')][string] $Store, [int] $Seconds = 60, [switch] $DoNotOpenWebsite, [switch] $DoNotPlaySound, [switch] $DoNotUseBeep ) $Cache = [ordered] @{} $CurrentStock = Get-UnifiStock -Store $Store foreach ($Product in $CurrentStock) { $Cache[$Product.Name] = $Product $Cache[$Product.SKU] = $Product } [Array] $ApplicableProducts = @( foreach ($Name in $ProductName) { $Found = $false foreach ($StockName in $CurrentStock.Name) { if ($StockName -like "$Name") { $StockName $found = $true } } if (-not $Found) { Write-Color -Text "Product Name '$Name' not found in stock. Ignoring" -Color Red } } foreach ($Name in $ProductSKU) { if ($Name -in $CurrentStock.SKU) { $Name } else { Write-Color -Text "Product SKU '$Name' not found in stock. Ignoring" -Color Red } } ) $ApplicableProducts = $ApplicableProducts | Sort-Object -Unique if ($ApplicableProducts.Count -eq 0) { Write-Color -Text "No products requested by user not found on list of available products. Exiting" -Color Red return } # $Collections = @( # foreach ($Product in $ApplicableProducts) { # $Cache[$Product].Category # } # ) | Select-Object -Unique Write-Color -Text "Setting up monitoring for ", ($ApplicableProducts -join ", ") -Color Yellow, Green $Count = 0 Do { if ($Count -ne 0) { Start-Sleep -Seconds $Seconds } Write-Color -Text "Checking stock..." -Color Yellow $CurrentResults = Get-UnifiStock -Store $Store | Where-Object { $_.Name -in $ApplicableProducts -or $_.SKU -in $ApplicableProducts } | Sort-Object -Property Name Write-Color -Text "Checking stock... Done, sleeping for $Seconds seconds" -Color Green $Count++ } While ($CurrentResults.Available -notcontains $true) foreach ($Product in $CurrentResults | Where-Object { $_.Available -eq $true }) { Write-Color -Text "Product ", $($Product.Name), " is in stock! ", "SKU: $($Product.SKU)" -Color Yellow, Green, Yellow, Green if (-not $DoNotOpenWebsite) { Start-Process $Product.ProductUrl } if (-not $DoNotPlaySound) { try { $Voice = New-Object -ComObject Sapi.spvoice -ErrorAction Stop } catch { Write-Color -Text "Failed to create voice object. Error: $($_.Exception.Message)" -Color Red } if ($Voice) { # Set the speed - positive numbers are faster, negative numbers, slower $voice.rate = 0 # Say something try { $null = $voice.speak("Hey,there is stock available for $($Product.Name)") } catch { Write-Color -Text "Failed to speak. Error: $($_.Exception.Message)" -Color Red } } } if (-not $DoNotUseBeep) { [console]::beep(500, 300) } } } function Wait-UnifiStockLegacy { <# .SYNOPSIS When run waits for the specified SKU or Product to be in stock in Ubiquiti's online store. .DESCRIPTION When run waits for the specified SKU or Product to be in stock in Ubiquiti's online store. Once the product is in stock the function will play a beep, read which product is in stock and open a browser to specific product page. .PARAMETER ProductName One or more products to wait for to be in stock with search by it's Name .PARAMETER ProductSKU One or more products to wait for to be in stock with search by it's SKU .PARAMETER Store The store to check for stock. Valid values are Brazil, India, Japan, Taiwan, Signapore, Mexico, China If you want EU/USA store you can use Wait-UnifiStock for those. .PARAMETER Seconds The number of seconds to wait between checks. Default is 60 seconds. .PARAMETER DoNotOpenWebsite If specified the website will not be opened when the product is in stock. .PARAMETER DoNotPlaySound If specified the sound will not be played when the product is in stock. .PARAMETER DoNotUseBeep If specified the beep will not be played when the product is in stock. .EXAMPLE Wait-UnifiStockLegacy -ProductSKU 'UDR-EU' -ProductName 'Switch Flex XG' -Seconds 60 -Store Brazil .EXAMPLE Wait-UnifiStockLegacy -ProductName 'UniFi6 Mesh', 'G4 Doorbell Pro', 'Camera G4 Pro', 'Test' -Seconds 60 -Store Brazil .EXAMPLE Wait-UnifiStockLegacy -ProductName 'UniFi6 Mesh', 'G4 Doorbell Pro', 'Camera G4 Pro', 'Test' -Seconds 60 -DoNotUseBeep -Store Brazil .NOTES General notes #> [cmdletBinding()] param( [string[]] $ProductName, [string[]] $ProductSKU, [parameter(Mandatory)][ValidateSet('Brazil', 'India', 'Japan', 'Taiwan', 'Signapore', 'Mexico', 'China')][string] $Store, [int] $Seconds = 60, [switch] $DoNotOpenWebsite, [switch] $DoNotPlaySound, [switch] $DoNotUseBeep ) $Cache = [ordered] @{} $CurrentStock = Get-UnifiStockLegacy -Store $Store foreach ($Product in $CurrentStock) { $Cache[$Product.Name] = $Product $Cache[$Product.SKU] = $Product } [Array] $ApplicableProducts = @( foreach ($Name in $ProductName) { $Found = $false foreach ($StockName in $CurrentStock.Name) { if ($StockName -like "$Name") { $StockName $found = $true } } if (-not $Found) { Write-Color -Text "Product Name '$Name' not found in stock. Ignoring" -Color Red } } foreach ($Name in $ProductSKU) { if ($Name -in $CurrentStock.SKU) { $Name } else { Write-Color -Text "Product SKU '$Name' not found in stock. Ignoring" -Color Red } } ) if ($ApplicableProducts.Count -eq 0) { Write-Color -Text "No products requested by user not found on list of available products. Exiting" -Color Red return } $Collections = @( foreach ($Product in $ApplicableProducts) { $Cache[$Product].Category } ) | Select-Object -Unique Write-Color -Text "Setting up monitoring for ", ($ApplicableProducts -join ", ") -Color Yellow, Green $Count = 0 Do { if ($Count -ne 0) { Start-Sleep -Seconds $Seconds } Write-Color -Text "Checking stock..." -Color Yellow $CurrentResults = Get-UnifiStockLegacy -Store $Store -Collection $Collections | Where-Object { $_.Name -in $ApplicableProducts -or $_.SKU -in $ApplicableProducts } | Sort-Object -Property Name Write-Color -Text "Checking stock... Done, sleeping for $Seconds seconds" -Color Green $Count++ } While ($CurrentResults.Available -notcontains $true) foreach ($Product in $CurrentResults | Where-Object { $_.Available -eq $true }) { Write-Color -Text "Product ", $($Product.Name), " is in stock! ", "SKU: $($Product.SKU)" -Color Yellow, Green, Yellow, Green if (-not $DoNotOpenWebsite) { Start-Process $Product.ProductUrl } if (-not $DoNotPlaySound) { try { $Voice = New-Object -ComObject Sapi.spvoice -ErrorAction Stop } catch { Write-Color -Text "Failed to create voice object. Error: $($_.Exception.Message)" -Color Red } if ($Voice) { # Set the speed - positive numbers are faster, negative numbers, slower $voice.rate = 0 # Say something try { $null = $voice.speak("Hey,there is stock available for $($Product.Name)") } catch { Write-Color -Text "Failed to speak. Error: $($_.Exception.Message)" -Color Red } } } if (-not $DoNotUseBeep) { [console]::beep(500, 300) } } } # Export functions and aliases as required Export-ModuleMember -Function @('Get-UnifiStock', 'Get-UnifiStockLegacy', 'Wait-UnifiStock', 'Wait-UnifiStockLegacy') -Alias @() # SIG # Begin signature block # MIInPgYJKoZIhvcNAQcCoIInLzCCJysCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAJGqrCOc1oEZSR # hYfXHutUcAp3WxzV80mTdRvjrdZguKCCITcwggO3MIICn6ADAgECAhAM5+DlF9hG # /o/lYPwb8DA5MA0GCSqGSIb3DQEBBQUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV # BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBa # Fw0zMTExMTAwMDAwMDBaMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lD # ZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC # AQoCggEBAK0OFc7kQ4BcsYfzt2D5cRKlrtwmlIiq9M71IDkoWGAM+IDaqRWVMmE8 # tbEohIqK3J8KDIMXeo+QrIrneVNcMYQq9g+YMjZ2zN7dPKii72r7IfJSYd+fINcf # 4rHZ/hhk0hJbX/lYGDW8R82hNvlrf9SwOD7BG8OMM9nYLxj+KA+zp4PWw25EwGE1 # lhb+WZyLdm3X8aJLDSv/C3LanmDQjpA1xnhVhyChz+VtCshJfDGYM2wi6YfQMlqi # uhOCEe05F52ZOnKh5vqk2dUXMXWuhX0irj8BRob2KHnIsdrkVxfEfhwOsLSSplaz # vbKX7aqn8LfFqD+VFtD/oZbrCF8Yd08CAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGG # MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEXroq/0ksuCMS1Ri6enIZ3zbcgP # MB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBBQUA # A4IBAQCiDrzf4u3w43JzemSUv/dyZtgy5EJ1Yq6H6/LV2d5Ws5/MzhQouQ2XYFwS # TFjk0z2DSUVYlzVpGqhH6lbGeasS2GeBhN9/CTyU5rgmLCC9PbMoifdf/yLil4Qf # 6WXvh+DfwWdJs13rsgkq6ybteL59PyvztyY1bV+JAbZJW58BBZurPSXBzLZ/wvFv # hsb6ZGjrgS2U60K3+owe3WLxvlBnt2y98/Efaww2BxZ/N3ypW2168RJGYIPXJwS+ # S86XvsNnKmgR34DnDDNmvxMNFG7zfx9jEB76jRslbWyPpbdhAbHSoyahEHGdreLD # +cOZUbcrBwjOLuZQsqf6CkUvovDyMIIFMDCCBBigAwIBAgIQBAkYG1/Vu2Z1U0O1 # b5VQCDANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGln # aUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtE # aWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMTMxMDIyMTIwMDAwWhcNMjgx # MDIyMTIwMDAwWjByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5j # MRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBT # SEEyIEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMIIBIjANBgkqhkiG9w0BAQEF # AAOCAQ8AMIIBCgKCAQEA+NOzHH8OEa9ndwfTCzFJGc/Q+0WZsTrbRPV/5aid2zLX # cep2nQUut4/6kkPApfmJ1DcZ17aq8JyGpdglrA55KDp+6dFn08b7KSfH03sjlOSR # I5aQd4L5oYQjZhJUM1B0sSgmuyRpwsJS8hRniolF1C2ho+mILCCVrhxKhwjfDPXi # TWAYvqrEsq5wMWYzcT6scKKrzn/pfMuSoeU7MRzP6vIK5Fe7SrXpdOYr/mzLfnQ5 # Ng2Q7+S1TqSp6moKq4TzrGdOtcT3jNEgJSPrCGQ+UpbB8g8S9MWOD8Gi6CxR93O8 # vYWxYoNzQYIH5DiLanMg0A9kczyen6Yzqf0Z3yWT0QIDAQABo4IBzTCCAckwEgYD # VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYB # BQUHAwMweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5k # aWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0 # LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4 # oDaGNGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJv # b3RDQS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy # dEFzc3VyZWRJRFJvb3RDQS5jcmwwTwYDVR0gBEgwRjA4BgpghkgBhv1sAAIEMCow # KAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCgYIYIZI # AYb9bAMwHQYDVR0OBBYEFFrEuXsqCqOl6nEDwGD5LfZldQ5YMB8GA1UdIwQYMBaA # FEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBCwUAA4IBAQA+7A1aJLPz # ItEVyCx8JSl2qB1dHC06GsTvMGHXfgtg/cM9D8Svi/3vKt8gVTew4fbRknUPUbRu # pY5a4l4kgU4QpO4/cY5jDhNLrddfRHnzNhQGivecRk5c/5CxGwcOkRX7uq+1UcKN # JK4kxscnKqEpKBo6cSgCPC6Ro8AlEeKcFEehemhor5unXCBc2XGxDI+7qPjFEmif # z0DLQESlE/DmZAwlCEIysjaKJAL+L3J+HNdJRZboWR3p+nRka7LrZkPas7CM1ekN # 3fYBIM6ZMWM9CBoYs4GbT8aTEAb8B4H6i9r5gkn3Ym6hU/oSlBiFLpKR6mhsRDKy # ZqHnGKSaZFHvMIIFPTCCBCWgAwIBAgIQBNXcH0jqydhSALrNmpsqpzANBgkqhkiG # 9w0BAQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkw # FwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEy # IEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMB4XDTIwMDYyNjAwMDAwMFoXDTIz # MDcwNzEyMDAwMFowejELMAkGA1UEBhMCUEwxEjAQBgNVBAgMCcWabMSFc2tpZTER # MA8GA1UEBxMIS2F0b3dpY2UxITAfBgNVBAoMGFByemVteXPFgmF3IEvFgnlzIEVW # T1RFQzEhMB8GA1UEAwwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMIIBIjANBgkq # hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7KB3iyBrhkLUbbFe9qxhKKPBYqDBqln # r3AtpZplkiVjpi9dMZCchSeT5ODsShPuZCIxJp5I86uf8ibo3vi2S9F9AlfFjVye # 3dTz/9TmCuGH8JQt13ozf9niHecwKrstDVhVprgxi5v0XxY51c7zgMA2g1Ub+3ti # i0vi/OpmKXdL2keNqJ2neQ5cYly/GsI8CREUEq9SZijbdA8VrRF3SoDdsWGf3tZZ # zO6nWn3TLYKQ5/bw5U445u/V80QSoykszHRivTj+H4s8ABiforhi0i76beA6Ea41 # zcH4zJuAp48B4UhjgRDNuq8IzLWK4dlvqrqCBHKqsnrF6BmBrv+BXQIDAQABo4IB # xTCCAcEwHwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0OBBYE # FBixNSfoHFAgJk4JkDQLFLRNlJRmMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK # BggrBgEFBQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdpY2Vy # dC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2NybDQu # ZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUwQzA3 # BglghkgBhv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQu # Y29tL0NQUzAIBgZngQwBBAEwgYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNpZ25p # bmdDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAmr1sz4ls # LARi4wG1eg0B8fVJFowtect7SnJUrp6XRnUG0/GI1wXiLIeow1UPiI6uDMsRXPHU # F/+xjJw8SfIbwava2eXu7UoZKNh6dfgshcJmo0QNAJ5PIyy02/3fXjbUREHINrTC # vPVbPmV6kx4Kpd7KJrCo7ED18H/XTqWJHXa8va3MYLrbJetXpaEPpb6zk+l8Rj9y # G4jBVRhenUBUUj3CLaWDSBpOA/+sx8/XB9W9opYfYGb+1TmbCkhUg7TB3gD6o6ES # Jre+fcnZnPVAPESmstwsT17caZ0bn7zETKlNHbc1q+Em9kyBjaQRcEQoQQNpezQu # g9ufqExx6lHYDjCCBY0wggR1oAMCAQICEA6bGI750C3n79tQ4ghAGFowDQYJKoZI # hvcNAQEMBQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ # MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNz # dXJlZCBJRCBSb290IENBMB4XDTIyMDgwMTAwMDAwMFoXDTMxMTEwOTIzNTk1OVow # YjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ # d3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290 # IEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAv+aQc2jeu+RdSjww # IjBpM+zCpyUuySE98orYWcLhKac9WKt2ms2uexuEDcQwH/MbpDgW61bGl20dq7J5 # 8soR0uRf1gU8Ug9SH8aeFaV+vp+pVxZZVXKvaJNwwrK6dZlqczKU0RBEEC7fgvMH # hOZ0O21x4i0MG+4g1ckgHWMpLc7sXk7Ik/ghYZs06wXGXuxbGrzryc/NrDRAX7F6 # Zu53yEioZldXn1RYjgwrt0+nMNlW7sp7XeOtyU9e5TXnMcvak17cjo+A2raRmECQ # ecN4x7axxLVqGDgDEI3Y1DekLgV9iPWCPhCRcKtVgkEy19sEcypukQF8IUzUvK4b # A3VdeGbZOjFEmjNAvwjXWkmkwuapoGfdpCe8oU85tRFYF/ckXEaPZPfBaYh2mHY9 # WV1CdoeJl2l6SPDgohIbZpp0yt5LHucOY67m1O+SkjqePdwA5EUlibaaRBkrfsCU # tNJhbesz2cXfSwQAzH0clcOP9yGyshG3u3/y1YxwLEFgqrFjGESVGnZifvaAsPvo # ZKYz0YkH4b235kOkGLimdwHhD5QMIR2yVCkliWzlDlJRR3S+Jqy2QXXeeqxfjT/J # vNNBERJb5RBQ6zHFynIWIgnffEx1P2PsIV/EIFFrb7GrhotPwtZFX50g/KEexcCP # orF+CiaZ9eRpL5gdLfXZqbId5RsCAwEAAaOCATowggE2MA8GA1UdEwEB/wQFMAMB # Af8wHQYDVR0OBBYEFOzX44LScV1kTN8uZz/nupiuHA9PMB8GA1UdIwQYMBaAFEXr # oq/0ksuCMS1Ri6enIZ3zbcgPMA4GA1UdDwEB/wQEAwIBhjB5BggrBgEFBQcBAQRt # MGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEF # BQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJl # ZElEUm9vdENBLmNydDBFBgNVHR8EPjA8MDqgOKA2hjRodHRwOi8vY3JsMy5kaWdp # Y2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMBEGA1UdIAQKMAgw # BgYEVR0gADANBgkqhkiG9w0BAQwFAAOCAQEAcKC/Q1xV5zhfoKN0Gz22Ftf3v1cH # vZqsoYcs7IVeqRq7IviHGmlUIu2kiHdtvRoU9BNKei8ttzjv9P+Aufih9/Jy3iS8 # UgPITtAq3votVs/59PesMHqai7Je1M/RQ0SbQyHrlnKhSLSZy51PpwYDE3cnRNTn # f+hZqPC/Lwum6fI0POz3A8eHqNJMQBk1RmppVLC4oVaO7KTVPeix3P0c2PR3WlxU # jG/voVA9/HYJaISfb8rbII01YBwCA8sgsKxYoA5AY8WYIsGyWfVVa88nq2x2zm8j # LfR+cWojayL/ErhULSd+2DrZ8LaHlv1b0VysGMNNn3O3AamfV6peKOK5lDCCBq4w # ggSWoAMCAQICEAc2N7ckVHzYR6z9KGYqXlswDQYJKoZIhvcNAQELBQAwYjELMAkG # A1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRp # Z2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0MB4X # DTIyMDMyMzAwMDAwMFoXDTM3MDMyMjIzNTk1OVowYzELMAkGA1UEBhMCVVMxFzAV # BgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVk # IEc0IFJTQTQwOTYgU0hBMjU2IFRpbWVTdGFtcGluZyBDQTCCAiIwDQYJKoZIhvcN # AQEBBQADggIPADCCAgoCggIBAMaGNQZJs8E9cklRVcclA8TykTepl1Gh1tKD0Z5M # om2gsMyD+Vr2EaFEFUJfpIjzaPp985yJC3+dH54PMx9QEwsmc5Zt+FeoAn39Q7SE # 2hHxc7Gz7iuAhIoiGN/r2j3EF3+rGSs+QtxnjupRPfDWVtTnKC3r07G1decfBmWN # lCnT2exp39mQh0YAe9tEQYncfGpXevA3eZ9drMvohGS0UvJ2R/dhgxndX7RUCyFo # bjchu0CsX7LeSn3O9TkSZ+8OpWNs5KbFHc02DVzV5huowWR0QKfAcsW6Th+xtVhN # ef7Xj3OTrCw54qVI1vCwMROpVymWJy71h6aPTnYVVSZwmCZ/oBpHIEPjQ2OAe3Vu # JyWQmDo4EbP29p7mO1vsgd4iFNmCKseSv6De4z6ic/rnH1pslPJSlRErWHRAKKtz # Q87fSqEcazjFKfPKqpZzQmiftkaznTqj1QPgv/CiPMpC3BhIfxQ0z9JMq++bPf4O # uGQq+nUoJEHtQr8FnGZJUlD0UfM2SU2LINIsVzV5K6jzRWC8I41Y99xh3pP+OcD5 # sjClTNfpmEpYPtMDiP6zj9NeS3YSUZPJjAw7W4oiqMEmCPkUEBIDfV8ju2TjY+Cm # 4T72wnSyPx4JduyrXUZ14mCjWAkBKAAOhFTuzuldyF4wEr1GnrXTdrnSDmuZDNIz # tM2xAgMBAAGjggFdMIIBWTASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBS6 # FtltTYUvcyl2mi91jGogj57IbzAfBgNVHSMEGDAWgBTs1+OC0nFdZEzfLmc/57qY # rhwPTzAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgwdwYIKwYB # BQUHAQEEazBpMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20w # QQYIKwYBBQUHMAKGNWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy # dFRydXN0ZWRSb290RzQuY3J0MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwz # LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRSb290RzQuY3JsMCAGA1UdIAQZ # MBcwCAYGZ4EMAQQCMAsGCWCGSAGG/WwHATANBgkqhkiG9w0BAQsFAAOCAgEAfVmO # wJO2b5ipRCIBfmbW2CFC4bAYLhBNE88wU86/GPvHUF3iSyn7cIoNqilp/GnBzx0H # 6T5gyNgL5Vxb122H+oQgJTQxZ822EpZvxFBMYh0MCIKoFr2pVs8Vc40BIiXOlWk/ # R3f7cnQU1/+rT4osequFzUNf7WC2qk+RZp4snuCKrOX9jLxkJodskr2dfNBwCnzv # qLx1T7pa96kQsl3p/yhUifDVinF2ZdrM8HKjI/rAJ4JErpknG6skHibBt94q6/ae # sXmZgaNWhqsKRcnfxI2g55j7+6adcq/Ex8HBanHZxhOACcS2n82HhyS7T6NJuXdm # kfFynOlLAlKnN36TU6w7HQhJD5TNOXrd/yVjmScsPT9rp/Fmw0HNT7ZAmyEhQNC3 # EyTN3B14OuSereU0cZLXJmvkOHOrpgFPvT87eK1MrfvElXvtCl8zOYdBeHo46Zzh # 3SP9HSjTx/no8Zhf+yvYfvJGnXUsHicsJttvFXseGYs2uJPU5vIXmVnKcPA3v5gA # 3yAWTyf7YGcWoWa63VXAOimGsJigK+2VQbc61RWYMbRiCQ8KvYHZE/6/pNHzV9m8 # BPqC3jLfBInwAM1dwvnQI38AC+R2AibZ8GV2QqYphwlHK+Z/GqSFD/yYlvZVVCsf # gPrA8g4r5db7qS9EFUrnEw4d2zc4GqEr9u3WfPwwggbAMIIEqKADAgECAhAMTWly # S5T6PCpKPSkHgD1aMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAlVTMRcwFQYD # VQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBH # NCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwHhcNMjIwOTIxMDAwMDAw # WhcNMzMxMTIxMjM1OTU5WjBGMQswCQYDVQQGEwJVUzERMA8GA1UEChMIRGlnaUNl # cnQxJDAiBgNVBAMTG0RpZ2lDZXJ0IFRpbWVzdGFtcCAyMDIyIC0gMjCCAiIwDQYJ # KoZIhvcNAQEBBQADggIPADCCAgoCggIBAM/spSY6xqnya7uNwQ2a26HoFIV0Mxom # rNAcVR4eNm28klUMYfSdCXc9FZYIL2tkpP0GgxbXkZI4HDEClvtysZc6Va8z7GGK # 6aYo25BjXL2JU+A6LYyHQq4mpOS7eHi5ehbhVsbAumRTuyoW51BIu4hpDIjG8b7g # L307scpTjUCDHufLckkoHkyAHoVW54Xt8mG8qjoHffarbuVm3eJc9S/tjdRNlYRo # 44DLannR0hCRRinrPibytIzNTLlmyLuqUDgN5YyUXRlav/V7QG5vFqianJVHhoV5 # PgxeZowaCiS+nKrSnLb3T254xCg/oxwPUAY3ugjZNaa1Htp4WB056PhMkRCWfk3h # 3cKtpX74LRsf7CtGGKMZ9jn39cFPcS6JAxGiS7uYv/pP5Hs27wZE5FX/NurlfDHn # 88JSxOYWe1p+pSVz28BqmSEtY+VZ9U0vkB8nt9KrFOU4ZodRCGv7U0M50GT6Vs/g # 9ArmFG1keLuY/ZTDcyHzL8IuINeBrNPxB9ThvdldS24xlCmL5kGkZZTAWOXlLimQ # prdhZPrZIGwYUWC6poEPCSVT8b876asHDmoHOWIZydaFfxPZjXnPYsXs4Xu5zGcT # B5rBeO3GiMiwbjJ5xwtZg43G7vUsfHuOy2SJ8bHEuOdTXl9V0n0ZKVkDTvpd6kVz # HIR+187i1Dp3AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/ # BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEE # AjALBglghkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8w # HQYDVR0OBBYEFGKK3tBh/I8xFO2XC809KpQU31KcMFoGA1UdHwRTMFEwT6BNoEuG # SWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQw # OTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQG # CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKG # TGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJT # QTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIB # AFWqKhrzRvN4Vzcw/HXjT9aFI/H8+ZU5myXm93KKmMN31GT8Ffs2wklRLHiIY1UJ # RjkA/GnUypsp+6M/wMkAmxMdsJiJ3HjyzXyFzVOdr2LiYWajFCpFh0qYQitQ/Bu1 # nggwCfrkLdcJiXn5CeaIzn0buGqim8FTYAnoo7id160fHLjsmEHw9g6A++T/350Q # p+sAul9Kjxo6UrTqvwlJFTU2WZoPVNKyG39+XgmtdlSKdG3K0gVnK3br/5iyJpU4 # GYhEFOUKWaJr5yI+RCHSPxzAm+18SLLYkgyRTzxmlK9dAlPrnuKe5NMfhgFknADC # 6Vp0dQ094XmIvxwBl8kZI4DXNlpflhaxYwzGRkA7zl011Fk+Q5oYrsPJy8P7mxNf # arXH4PMFw1nfJ2Ir3kHJU7n/NBBn9iYymHv+XEKUgZSCnawKi8ZLFUrTmJBFYDOA # 4CPe+AOk9kVH5c64A0JH6EE2cXet/aLol3ROLtoeHYxayB6a1cLwxiKoT5u92Bya # UcQvmvZfpyeXupYuhVfAYOd4Vn9q78KVmksRAsiCnMkaBXy6cbVOepls9Oie1FqY # yJ+/jbsYXEP10Cro4mLueATbvdH7WwqocH7wl4R44wgDXUcsY6glOJcB0j862uXl # 9uab3H4szP8XTE0AotjWAQ64i+7m4HJViSwnGWH2dwGMMYIFXTCCBVkCAQEwgYYw # cjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ # d3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVk # IElEIENvZGUgU2lnbmluZyBDQQIQBNXcH0jqydhSALrNmpsqpzANBglghkgBZQME # AgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEM # BgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqG # SIb3DQEJBDEiBCDE1TGmvZiM0aG0shI9bMD1BqF6Mz3TPOpqRLrDZNVhrTANBgkq # hkiG9w0BAQEFAASCAQAEcLe/IJwwWGgZ7RZi0rTIMKcySZeJlQYzzzoqzT5WqCUG # PEH8V7g17bg/EYv9AcZl0ZRjxC8OS4zHS+2K7nUgYhp6zTi4mWS1XmM+2DTwEeeI # KgLG2D4Pw85VsM3mGhoJkMbLk+VzHBJPm8/oUrK28Or63EJe60pNuoVuymu2Ipxp # 9EdIk/A2TnXUCMFk3AwHlL9D59Wz1RD3h5Cavf/xeJxC2DwL/kmHAM1Ose+mIi3q # aykKmLu2604H5BFF/7NHHYyddZ40vT+8uN7361xfKoBFlEgFBX6OOT5CIL7EQYhw # jlTXL6c0tv13juz/0pDtLq2ZQawc08pvmJObKJraoYIDIDCCAxwGCSqGSIb3DQEJ # BjGCAw0wggMJAgEBMHcwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0 # LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hB # MjU2IFRpbWVTdGFtcGluZyBDQQIQDE1pckuU+jwqSj0pB4A9WjANBglghkgBZQME # AgEFAKBpMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X # DTIzMDYwNDE4NTE1M1owLwYJKoZIhvcNAQkEMSIEIAeOYavXtX2VFNoI5YtpZ61Q # +xnJNnavmFDyhQAIIZ1iMA0GCSqGSIb3DQEBAQUABIICACm9ZhQB3Fg9BGgCtgaE # txrZyBs5pD9RnDhwNyjk+sBmew03z6qzQYNMvZvxNIKWDsrGhMItYjrtLAqPc8mr # yLlN6ztfOAaOdnoQB50Url4JP6LVHgvfiEHOiBuUt0A+bEelgIXcHPvMgDy1vJZN # 2RPFglBjutx7gVlDO8TBSBd8hBwLDIiPSvg7bkozRI8/Rouk+GsajiEkH7EIGaZD # xk7ekDiWYUIesXEestsm5ZptxV5oaaGf4uXN9WlT+9Vy7ENCtYwBqX7MoNN2hHUx # Y87RIvUTzLZeDOhGapWTKEP5+949YwZWPqrVRel2YiTBrXM60gY3gzF4FUJeMBWP # ixPHOk0qlVNLu2UjE2a82fyy3Ftlqx25LeHpvsJbu4yHMoyE/jSzxgFjRMaPQ/2L # OxFIMjqEoLHAE3E24Ttxey3Nr2piS+xJmgpxQfUyNsCPOyrQPDvtfk1ZVVVkWyVM # zDU1i/WGfTauY4fsfaMAOEuFSbCGyL2SI889PdxjZgxq9b7qMUoWtgF9M6z9N0wI # ObJ5B4mrqWBAH6PE38F4FZHjV9R7bXfxX7ybug1eQZDagPgUL6rhFNelxjNiJhac # Y8raK9ZD6DkYny83jk+X9XCRmaL09xH6Q2vMYH0ktu1wBP8Ooggima4muo7U3C2S # vtY55TpEUn2znMxtrLIGssmF # SIG # End signature block |