0

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
sbagnato
  • 603
  • 3
  • 11
  • 35
  • 1
    Might be better to run a script once a day that checks the dawn/dusk times for that day and schedules the 2 Windows tasks to run the other script at those times. You can use `Set-ScheduledTask` for this – Daniel May 04 '22 at 20:38
  • @Daniel great idea! I'll also add, sunrise-sunset.org returns unformatted times in UTC. So I noticed that, being in EDT right now, if I run the script after 8pm, it gives tomorrow's values. I assume that's expected, but that partially factored into my (ugly) method of one continuous script. – sbagnato May 04 '22 at 20:45

1 Answers1

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();
Darin
  • 1,423
  • 1
  • 10
  • 12
  • thank you! No problem if there is a bug or two, I'll test it. I setup a standalone hardened SFF PC simply for this, but I'd still probably rather kick off via task scheduler as opposed to a forever loop just so it doesn't end up dying at some point. – sbagnato May 05 '22 at 12:52
  • 1
    @sbagnato, realized last night, after I went to bed, that I had a plan for using the Boolean value returned by SleepUntil. Will try to update the code this morning. – Darin May 05 '22 at 13:11
  • 1
    @sbagnato, re-wrote a portion of the code and added ReportRelativeTimeIfSleepUntil function. Also experimented with in code documentation for SleepUntil and ReportRelativeTimeIfSleepUntil functions. I seldom document anything, so this is good practice that I need. I still need to do testing and verifying the code, especially the DoCountDown function. – Darin May 05 '22 at 22:56
  • awesome! Attempted to test it and it appears to work. Couple of questions though. If I run the entire script, it just sits forever. If I run the DoCountDown function prior to dusk it worked; kicked off my AHK script. However, then it ends. So would I need a scheduled to task to kick it off daily? I started it again, and it just sat. Assuming it would just sit until 2 hours before dawn tomorrow? I guess just trying to learn everything you did and also understand how to set it live when the time comes. – sbagnato May 06 '22 at 00:39
  • 1
    @sbagnato, sorry, I'm replacing my computer desk and going through some disruption related to that and just now finding your question. If DoCountDown functions as intended (which I aim to do some testing on), it is a one shot count down to the nearest Dawn/Dusk event. If you want the script to keep going, place DoCountDown in a loop, for example `While(2 -gt 1){DoCountDown}`. Lets say you want the script to run for 7 days, place 2 DoCountDown calls in a loop that repeats 7 times. Or you could place 2 DoCountDown calls in the script and use a task to start it at midnight everyday. – Darin May 06 '22 at 14:14
  • Darin, So I "think" I have everything as I like it. I'll take your suggestion and have 2 DoCountDown calls, and have it start at midnight everyday. What I am trying to figure out is, what is the correct method to call it each day? Obviously via task scheduler, but just running (name).ps1 never does anything. FYI I added the current code to the OP and made a note so you can see it... – sbagnato May 11 '22 at 21:23
  • 1
    @sbagnato, you should be able to alter this answer to get what you want: (https://stackoverflow.com/a/70995208/4190564). Then create a batch file with the .CMD extension, place in this batch file the header part of the batch file in this answer: (https://stackoverflow.com/a/71272954/4190564), BUT replace the powershell call with this ` PowerShell -NoProfile -ExecutionPolicy RemoteSigned -Command ".([scriptblock]::Create((get-content -raw $Env:F0)))"`. Then place your DoCountDown code in that batch file after the header part. This batch is what will be called every day. – Darin May 12 '22 at 00:41