'Powershell - Script to determine dawn/dusk and run exe

I am working on a Powershell script to adjust configs of security cameras at dusk and dawn (you'd think the vendor would have a scheduling function...). What it's supposed to do is use sunrise-sunset.org to pull the Civil Twilight values, start checking to see when it's dawn (incrementing from large to small timeframes), and then kick off an AHK script to make the changes in the web config. The script then pauses for six hours before starting to check for dusk and then kicks off another AHK script.

However, I know I have errors as it doesn't seem to run. For example, while the finalized script will be set with Task Scheduled to start at 4am, I manually kick it off before I go to bed, but when I get up it's still sitting at "At least 120 minutes to dawn". But if I look at the values of Dawn and Now, it "should" have worked. Additionally, if I remove the API part and literally just set a "then" (let's say in 30 mins) and "now" and use a couple of do-until loops, it works.

So,

A) any ideas on what is wrong? B) I am near certain this code can be cleaned up/shortened.

# Clear old variables
Remove-Variable * -ErrorAction SilentlyContinue; Remove-Module *; $error.Clear();

# Get Civil Twilight values and "now"
$Daylight = (Invoke-RestMethod "https://api.sunrise-sunset.org/json?lat=35.608081&lng=-78.647666&formatted=0").results
$Dawn = [datetime] $Daylight.civil_twilight_begin
$Dusk = [datetime] $Daylight.civil_twilight_end
$Now = Get-Date

# Start iterations until dawn
do {
    "At least 120 minutes til dawn."
    Start-Sleep -S 7200
} until([datetime]::Now -ge $Dawn.AddMinutes(-120))

do {
    "At least 60 minutes til dawn."
    Start-Sleep -S 3600
} until([datetime]::Now -ge $Dawn.AddMinutes(-60))

do {
    "At least 30 minutes til dawn."
    Start-Sleep -S 1800
} until([datetime]::Now -ge $Dawn.AddMinutes(-30))

do {
    "At least 15 minutes til dawn."
    Start-Sleep -S 900
} until([datetime]::Now -ge $Dawn.AddMinutes(-15))

do {
    "At least 10 minutes til dawn."
    Start-Sleep -S 600
} until([datetime]::Now -ge $Dawn.AddMinutes(-10))

do {
    "At least 5 minutes til dawn."
    Start-Sleep -S 30
} until([datetime]::Now -ge $Dawn.AddMinutes(-5))

do {
    "At least 3 minutes til dawn."
    Start-Sleep -S 180
} until([datetime]::Now -ge $Dawn.AddMinutes(-3))

do {
    "At least a minute til dawn."
    Start-Sleep -S 60
} until([datetime]::Now -ge $Dawn.AddSeconds(-60))

do {
    "About to execute..."
    Start-Sleep -S 30
} until([datetime]::Now -ge $Dawn.AddSeconds(-30))

# Execute day script
C:\AHK\day.exe | Invoke-Expression

# Rest til the afternoon
Start-Sleep -S 21600

# Update "now"
$Now = Get-Date

# Start iterations until dusk
do {
    "At least 120 minutes til dusk."
    Start-Sleep -S 7200
} until([datetime]::Now -ge $Dusk.AddMinutes(-120))

do {
    "At least 60 minutes til dusk."
    Start-Sleep -S 3600
} until([datetime]::Now -ge $Dusk.AddMinutes(-60))

do {
    "At least 30 minutes til dusk."
    Start-Sleep -S 1800
} until([datetime]::Now -ge $Dusk.AddMinutes(-30))

do {
    "At least 15 minutes til dusk."
    Start-Sleep -S 900
} until([datetime]::Now -ge $Dusk.AddMinutes(-15))

do {
    "At least 10 minutes til dusk."
    Start-Sleep -S 600
} until([datetime]::Now -ge $Dusk.AddMinutes(-10))

do {
    "At least 5 minutes til dusk."
    Start-Sleep -S 30
} until([datetime]::Now -ge $Dusk.AddMinutes(-5))

do {
    "At least 3 minutes til dusk."
    Start-Sleep -S 180
} until([datetime]::Now -ge $Dawn.AddMinutes(-3))

do {
    "At least a minute til dusk."
    Start-Sleep -S 60
} until([datetime]::Now -ge $Dusk.AddSeconds(-60))

do {
    "About to execute..."
    Start-Sleep -S 30
} until([datetime]::Now -ge $Dusk.AddSeconds(-30))

# Execute night script
C:\AHK\night.exe | Invoke-Expression

# Rest 5 minutes
Start-Sleep -S 300

# Clear variables and end
Remove-Variable * -ErrorAction SilentlyContinue; Remove-Module *; $error.Clear();
Exit

EDIT: This is the current script with all credit to @Darin

Darin, refer to my question below....

Remove-Variable * -ErrorAction SilentlyContinue; Remove-Module *; $error.Clear();

function SleepUntil {...}

function ReportRelativeTimeIfSleepUntil {...}

function GetApiTodayTomorrow {...}

function GetNextChange {...}

function DoCountDown {...}

DoCountDown

Start-Sleep S 600

DoCountDown


Solution 1:[1]

EDIT: Partial re-write of the re-write of the original code. Still needs testing and verifying each function - but in far better shape than original answer. Focused on SleepUntil and ReportRelativeTimeIfSleepUntil, both should be in good shape. But changes may broke something else, so will have to do further testing.

This code gets the next future time of change (dawn today, dusk today, or dawn tomorrow) and does a count down to that time. You could put the DoCountDown function in a loop, or you could have the script started by task scheduler twice a day, or even started once a day with DoCountDown ran twice.

# Clear old variables
Remove-Variable * -ErrorAction SilentlyContinue; Remove-Module *; $error.Clear();
<#
.SYNOPSIS
Suspends the activity in a script or session until either a specified time, or calculated time relative to the
specified time.

.DESCRIPTION
SleepUntil accepts a time, and optionally a number of either minutes or seconds to adjust the time by, to
create a point in time to suspend activity until that point in time is reached.  If the point in time is in
the past, relative to the current time, then activity is not suspended and the value $false is returned.  If
the point in time is in the future, then activity is suspended until that point in time and the value $true
is returned.  SleepUntil is intended as a method to activate a script a certain number of minutes or seconds
before or after an event.

.PARAMETER When
The reference time that the computer should awake from a sleep, where the exact time to awake is calculated
based on the optionally supplied values of -RelativeSeconds, or -RelativeMinutes, added to this reference
time.

.PARAMETER RelativeSeconds
Optional parameter used to calculate the number of seconds prior to, or after, the time provided by
parameter -EventTime. If this parameter is positive, then calculated time will be after -EventTime, and if negative then it
will be prior to -EventTime.  Cannot be used with -RelativeMinutes.

.PARAMETER RelativeMinutes
Optional parameter used to calculate the number of minutes prior to, or after, the time provided by
parameter -EventTime. If this parameter is positive, then calculated time will be after -EventTime, and if negative then it
will be prior to -EventTime.  Cannot be used with -RelativeSeconds.

.EXAMPLE
#   Sleep until 10 minutes prior to DateTime in $PowerOffEvent
SleepUntil -EventTime $PowerOffEvent -RelativeMinutes -10
#   Sleep until 30 seconds after $OpeningTime
SleepUntil $OpeningTime -RelativeSeconds 30 
#   Essentially same as: Start-Sleep -s 15
SleepUntil -EventTime (Get-Date) -RelativeSeconds 30


.NOTES
Internally, SleepUntil uses Start-Sleep, so in theory Ctrl+C should break out of SleepUntil.
#>
function SleepUntil {
    [CmdLetBinding(DefaultParameterSetName = 'NotRelative')]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [datetime]$EventTime,
        [Parameter(Position = 1, ParameterSetName = 'RelSec')]
        [int]$RelativeSeconds = 0,
        [Parameter(Position = 1, ParameterSetName = 'RelMin')]
        [int]$RelativeMinutes = 0
    )
    $AwakeTime = if($PsCmdlet.ParameterSetName -eq 'RelMin') {
        $EventTime.AddMinutes($RelativeMinutes)
    } elseif($PsCmdlet.ParameterSetName -eq 'RelSec') {
        $EventTime.AddSeconds($RelativeSeconds)
    } else {
        $EventTime
    }
    $SecondsToSleep = ($AwakeTime - (Get-Date)).TotalSeconds
    if($SecondsToSleep -lt 0) {
        $false 
    } else {
        Start-Sleep -S $SecondsToSleep
        $true
    }
}
<#
.SYNOPSIS
Calls SleepUntil, and if SleepUntil reports success, then returns string reporting minutes and/or 
seconds until/after event's time.

.DESCRIPTION
ReportRelativeTimeIfSleepUntil is useful for calling SleepUntil and then getting a string describing
how much time until or after an event.

.PARAMETER EventName
Descriptive name of the vent that will be inserted into the returning string.

.PARAMETER EventTime
The time of the event, see SleepUntil for more details.

.PARAMETER RelativeSeconds
Number of seconds relative to event, see SleepUntil for more details.

.PARAMETER RelativeMinutes
Number of minutes relative to event, see SleepUntil for more details.

.EXAMPLE
ReportRelativeTimeIfSleepUntil -EventName "power off" -EventTime $PowerOffEvent -RelativeMinutes -10
10 minutes until power off.
ReportRelativeTimeIfSleepUntil "SomeEvent" (Get-Date).AddSeconds(30) -RelativeSeconds 72
1 minute and 12 seconds after SomeEvent.
ReportRelativeTimeIfSleepUntil "SomeEvent" (Get-Date).AddSeconds(30) -RelativeSeconds -30
30 seconds until SomeEvent.

.NOTES
Assigning output to a variable, and check if the variable contains $null will indicate that
SleepUntil returned $false and activity was not suspended.
#>
function ReportRelativeTimeIfSleepUntil {
    [CmdLetBinding(DefaultParameterSetName = 'NotRelative')]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$EventName,        
        [Parameter(Mandatory = $true, Position = 1)]
        [datetime]$EventTime,
        [Parameter(Position = 1, ParameterSetName = 'RelSec')]
        [int]$RelativeSeconds = 0,
        [Parameter(Position = 1, ParameterSetName = 'RelMin')]
        [int]$RelativeMinutes = 0
    )
    $RelativeTimeInSeconds = if($PsCmdlet.ParameterSetName -eq 'RelMin') {
        $RelativeMinutes * 60
    } elseif($PsCmdlet.ParameterSetName -eq 'RelSec') {
        $RelativeSeconds
    } else {
        0
    }
    $DidSleep = SleepUntil $EventTime -RelativeSeconds $RelativeTimeInSeconds
    if($DidSleep) {
        $Prior = $RelativeTimeInSeconds -lt 0
        if($Prior) {
            $RelativeTimeInSeconds = -$RelativeTimeInSeconds
            $UntilAfter = 'until'
        } else {
            $UntilAfter = 'after'
        }
        $SecondsOut = $RelativeTimeInSeconds % 60
        $MinutesOut = ($RelativeTimeInSeconds - $SecondsOut)/60
        $SPlural = if($SecondsOut -ne 1) {'s'} else {''}
        $MPlural = if($MinutesOut -ne 1) {'s'} else {''}
        if($MinutesOut -ne 0) {
            if($SecondsOut -ne 0) {
                "$MinutesOut minute$MPlural and $SecondsOut second$SPlural $UntilAfter $EventName."
            } else {
                "$MinutesOut minute$MPlural $UntilAfter $EventName."
            }
        } else {
            if($SecondsOut) {
                "$SecondsOut second$SPlural $UntilAfter $EventName."
            } else {
                "$EventName is happening now."
            }
        }
    }
}
function GetApiTodayTomorrow {
    [CmdLetBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [datetime]$When
    )
    "$($When.Year)-$($When.Month)-$($When.Day)"
    $When = $When.AddDays(1)
    "$($When.Year)-$($When.Month)-$($When.Day)"
}
function GetNextChange {
    $Now = Get-Date
    $ThisDate, $NextDate = GetApiTodayTomorrow $Now
    $Daylight = (Invoke-RestMethod "https://api.sunrise-sunset.org/json?lat=35.608081&lng=-78.647666&date=$ThisDate&formatted=0").results
    $Dawn = [datetime] $Daylight.civil_twilight_begin
    $Dusk = [datetime] $Daylight.civil_twilight_end
    if($Now -lt $Dawn) {
        'Dawn'
        $Dawn
    } elseif ($Now -lt $Dusk) {
        'Dusk'
        $Dusk
    } else {
        $Daylight = (Invoke-RestMethod "https://api.sunrise-sunset.org/json?lat=35.608081&lng=-78.647666&date=$NextDate&formatted=0").results
        $Dawn = [datetime] $Daylight.civil_twilight_begin
        $Dusk = [datetime] $Daylight.civil_twilight_end
        if($Now -lt $Dawn) {
            'Dawn'
            $Dawn
        } else {
            'Dusk'
            $Dusk
        }
    }
}
function DoCountDown {
    $EventName, $EventTime = GetNextChange

    # Start iterations until dawn
    ReportRelativeTimeIfSleepUntil $EventName $EventTime -RelativeMinutes -120
    ReportRelativeTimeIfSleepUntil $EventName $EventTime -RelativeMinutes -60
    ReportRelativeTimeIfSleepUntil $EventName $EventTime -RelativeMinutes -30
    ReportRelativeTimeIfSleepUntil $EventName $EventTime -RelativeMinutes -15
    ReportRelativeTimeIfSleepUntil $EventName $EventTime -RelativeMinutes -10
    ReportRelativeTimeIfSleepUntil $EventName $EventTime -RelativeMinutes -5
    ReportRelativeTimeIfSleepUntil $EventName $EventTime -RelativeMinutes -3
    ReportRelativeTimeIfSleepUntil $EventName $EventTime -RelativeMinutes -1
    ReportRelativeTimeIfSleepUntil $EventName $EventTime -RelativeSeconds -30
    $null = SleepUntil $EventTime
    if($EventName -eq 'Dawn') {
        "Executing Day"
        # Execute day script
        C:\AHK\day.exe | Invoke-Expression
    } else {
        "Executing Night"
        # Execute night script
        C:\AHK\night.exe | Invoke-Expression
    }

}

DoCountDown

# Rest 5 minutes
SleepUntil (Get-Date) -RelativeMinutes 5

# Clear variables and end
Remove-Variable * -ErrorAction SilentlyContinue; Remove-Module *; $error.Clear();

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1