Skip to content

Instantly share code, notes, and snippets.

@asheroto
Last active October 7, 2026 00:04
Show Gist options
  • Select an option

  • Save asheroto/48337e8451f5a5a01ba2c270426a5350 to your computer and use it in GitHub Desktop.

Select an option

Save asheroto/48337e8451f5a5a01ba2c270426a5350 to your computer and use it in GitHub Desktop.
PowerShell script to control monitor power state: place all displays into standby with -TurnOff or wake them with -TurnOn.

Screen Controller (PowerShell)

A simple PowerShell script to reliably control monitor/display state.

This uses the same method as when a computer display times out through normal power settings. Unlike tools like ControlMyMonitor that change monitor hardware settings and actually power off the monitor, this script uses Windows messaging to place displays into standby (not a full power-off) and runs entirely in PowerShell with no external dependencies. You can wake the screens manually by moving the mouse or pressing a key and allowing a few seconds for them to resume, or wake them remotely through the script.

Use -TurnOff to put displays into standby, or -TurnOn to wake them.

Usage

# Turn monitors off
& '.\Screen Controller.ps1' -TurnOff

# Wake monitors
& '.\Screen Controller.ps1' -TurnOn

# Turn monitors off on a Modern Standby device you have tested (see below)
& '.\Screen Controller.ps1' -TurnOff -Force

Or if calling from cmd:

:: Turn monitors off
powershell.exe -ExecutionPolicy Bypass -File ".\Screen Controller.ps1" -TurnOff

:: Wake monitors
powershell.exe -ExecutionPolicy Bypass -File ".\Screen Controller.ps1" -TurnOn

Environment variables (for RMM use)

RMM platforms usually pass script variables as environment variables. The script reads these so you can expose them as a dropdown and a checkbox instead of editing the command line:

Variable Values Equivalent
action Turn On or Turn Off -TurnOn / -TurnOff
force true or 1 -Force

The switches still work when run by hand.

How it Works

  • TurnOff: Uses Windows messaging to place all connected monitors into standby (not just a black screensaver). The computer itself stays awake and on the network.
    • Skipped on laptops and tablets, since the script is intended for desktops driving monitors.
    • Skipped on Modern Standby devices, where turning the displays off can put the whole device into standby and drop the network. See Modern Standby below for how to fix this or bypass the check.
  • TurnOn: Nudges the mouse 1 pixel and back, then uses Windows messaging to bring the displays back on. No keys are pressed to wake the screens.
    • After waking, the script checks which window is in front. If Windows is showing the lock screen image (time and date, no sign-in required yet), it sends a single Ctrl key press to dismiss it.
    • If the desktop is already showing, or the real sign-in screen is up, nothing is sent.

Modern Standby

On a classic S3 desktop, turning the displays off only turns the displays off. On a Modern Standby (S0 Low Power Idle) device, the screen going off is the entry point to standby. The device then drifts into its low-power idle state, Win32 apps such as RMM agents get throttled, and remote sessions drop. Most laptops and most business desktops sold since 2022 ship this way, so the script skips those devices by default and prints how to fix it.

Check which mode a device is in:

powercfg /a

If Standby (S0 Low Power Idle) is listed as available, the device uses Modern Standby.

Disable it. Run as an administrator, then reboot:

  1. If the BIOS offers it, switch the sleep state to S3:

    • Dell: BIOS (F2 at boot) > Power (older BIOS: Power Management) > Sleep Mode = Force S3. Or, with Dell Command | Configure installed: cctk.exe --sleepmode=forces3
    • Lenovo: BIOS > Config > Power > Sleep State = Linux S3
    • HP: BIOS > Advanced > Power Management > disable Modern Standby / enable S3 (not all models)
  2. Windows decides between S3 and Modern Standby at install time and may not pick up the BIOS change on its own. Set the registry override as well:

    reg add HKLM\SYSTEM\CurrentControlSet\Control\Power /v PlatformAoAcOverride /t REG_DWORD /d 0 /f

After the reboot, powercfg /a should no longer list S0 Low Power Idle as available. It may report no sleep states at all. That is fine for a desktop that should never sleep: the machine cannot drop into standby, so the displays can go off while it stays awake and reachable.

Or bypass the check. -Force skips the Modern Standby check and turns the displays off anyway. Only use it on a device you have tested and confirmed stays reachable with the displays off for 15 to 20 minutes. When -Force is set the script still runs the check and tells you whether the bypass was actually needed. The laptop/tablet check still applies.

Common Use Case

Set up with Task Scheduler, Group Policy Preferences, or an RMM to automatically:

  • Turn monitors off at 6 PM
  • Wake monitors at 6 AM

Well suited to unattended desktops such as check-in computers and lobby displays.

Exit Codes

Code Meaning
0 Success
1 Bad arguments (specify exactly one of -TurnOn or -TurnOff)
2 The display command failed
3 No interactive user session (running as SYSTEM or non-interactive)
4 Skipped (laptop/tablet or Modern Standby device)

Requirements

  • Windows 10/11
  • PowerShell 5.1 or later
  • Must run in the context of a logged-in user (interactive session), not as SYSTEM. In Task Scheduler, choose "Run only when user is logged on". In RMM, run as the current logged-on user.
<#
.SYNOPSIS
Controls monitor power state via PowerShell.
.DESCRIPTION
Use -TurnOff to place all displays into standby. Use -TurnOn to wake them.
Works like the normal Windows display idle timeout (standby), not a black screensaver.
.PARAMETER TurnOff
Put all attached displays into standby.
.PARAMETER TurnOn
Wake displays by sending input, then set them to on.
.PARAMETER Force
With -TurnOff, bypass the Modern Standby check. Use only on a device you have tested and confirmed stays reachable with the displays off. The laptop/tablet check still applies.
Can also be set through the environment variable "force" (true or 1) for RMM use.
.EXAMPLE
& '.\Screen Controller.ps1' -TurnOff
.EXAMPLE
& '.\Screen Controller.ps1' -TurnOff -Force
.EXAMPLE
& '.\Screen Controller.ps1' -TurnOn
.NOTES
Author: asheroto
URL: https://gist.github.com/asheroto/48337e8451f5a5a01ba2c270426a5350
Requires: Windows 10/11, PowerShell 5.1+
Important: Must run in the interactive user session. If using Task Scheduler or an RMM, set Run only when user is logged on and run as the current logged-in user.
Environment variables for RMM use: "action" set to "Turn On" or "Turn Off" selects the mode, and "force" set to true or 1 maps to -Force. The -TurnOn/-TurnOff switches still work for command-line use.
Intended for desktops driving monitors (for example check-in computers). -TurnOff skips laptops and tablets, and also skips Modern Standby (S0 low power idle) devices, where turning displays off can drop the device into connected standby and disconnect the network.
Exit codes: 0 = success, 1 = bad arguments, 2 = SendMessageTimeout failed, 3 = no interactive user session, 4 = skipped (laptop/tablet or Modern Standby device).
#>
#requires -version 5.1
param(
[switch]$TurnOn,
[switch]$TurnOff,
[switch]$Force
)
Set-StrictMode -Version 2.0
$ErrorActionPreference = 'Stop'
# Environment variables for RMM use
# "action" = "Turn On" / "Turn Off" selects the mode
# "force" = true or 1 maps to -Force
switch -Regex ($env:action) {
'^turn\s*on$' { $TurnOn = $true }
'^turn\s*off$' { $TurnOff = $true }
}
if ($env:force -in 'true', '1') { $Force = $true }
function Assert-UserContext {
<#
.SYNOPSIS
Ensures the script is running in an interactive user session.
#>
$id = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$sid = $id.User.Value
$name = $id.Name
$isSystem = ($sid -eq 'S-1-5-18') -or ($name -eq 'NT AUTHORITY\SYSTEM') -or ($name -eq 'SYSTEM')
if ($isSystem -or -not [Environment]::UserInteractive) {
Write-Output "[Error] No logged-in user session detected. Run as the current logged-in user."
Write-Output "[Hint] If scheduling, set 'Run only when user is logged on' and choose the user account."
exit 3
}
}
function Set-MonitorPower {
<#
.SYNOPSIS
Set displays to on (-1), low-power (1), or standby (2).
#>
param([ValidateSet(-1,1,2)][int]$State)
$HWND_BROADCAST = [IntPtr]0xffff
$WM_SYSCOMMAND = 0x0112
$SC_MONITORPOWER = 0xF170
$SMTO_ABORTIFHUNG = 0x0002
[IntPtr]$res = [IntPtr]::Zero
$ok = [ScreenCtl.Native]::SendMessageTimeout($HWND_BROADCAST, $WM_SYSCOMMAND, [IntPtr]$SC_MONITORPOWER, [IntPtr]$State, $SMTO_ABORTIFHUNG, 2000, [ref]$res)
if ($ok -eq [IntPtr]::Zero) {
$err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
Write-Output "[Error] SendMessageTimeout failed (Win32 error $err)."
exit 2
}
switch ($State) {
-1 { Write-Output "Displays turned on." }
1 { Write-Output "Displays set to low-power mode." }
2 { Write-Output "Displays turned off (standby mode)." }
}
}
function Test-ModernStandby {
<#
.SYNOPSIS
Returns $true if the device uses Modern Standby (S0 low power idle).
#>
# SystemPowerCapabilities (level 4) fills SYSTEM_POWER_CAPABILITIES (76 bytes); AoAc is the BOOLEAN at offset 20
$buf = New-Object byte[] 76
$status = [ScreenCtl.Native]::CallNtPowerInformation(4, [IntPtr]::Zero, 0, $buf, 76)
if ($status -ne 0) {
# Fail safe: if we cannot tell, treat it as Modern Standby so the device is not put to sleep
Write-Output "[Warning] Could not read power capabilities (NTSTATUS $status)."
return $true
}
return ($buf[20] -ne 0)
}
function Test-PortableChassis {
<#
.SYNOPSIS
Returns $true if the chassis is a laptop, notebook, tablet, convertible, or detachable.
#>
# SMBIOS chassis types: 8 Portable, 9 Laptop, 10 Notebook, 11 Hand Held, 12 Docking Station, 14 Sub Notebook, 30 Tablet, 31 Convertible, 32 Detachable
$portable = 8, 9, 10, 11, 12, 14, 30, 31, 32
$types = @(Get-CimInstance -ClassName Win32_SystemEnclosure | ForEach-Object { $_.ChassisTypes })
return [bool]($types | Where-Object { $portable -contains $_ })
}
function Get-ScreenState {
<#
.SYNOPSIS
Returns 'Desktop', 'LockScreen' (lock screen image showing, no sign-in needed yet), or 'Locked' (secure sign-in screen).
#>
$hwnd = [ScreenCtl.Native]::GetForegroundWindow()
# No foreground window on the user desktop means the secure desktop (sign-in screen) is active
if ($hwnd -eq [IntPtr]::Zero) { return 'Locked' }
[uint32]$procId = 0
[void][ScreenCtl.Native]::GetWindowThreadProcessId($hwnd, [ref]$procId)
$proc = Get-Process -Id $procId -ErrorAction SilentlyContinue
if ($proc -and $proc.ProcessName -eq 'LockApp') { return 'LockScreen' }
return 'Desktop'
}
function Send-WakeInput {
<#
.SYNOPSIS
Nudges the mouse 1 pixel and back to wake displays.
#>
# Mouse instead of a keypress: a keystroke while displays are off can bring up the lock screen image on the primary display
$MOUSEEVENTF_MOVE = 0x0001
[ScreenCtl.Native]::mouse_event($MOUSEEVENTF_MOVE, 1, 0, 0, [IntPtr]::Zero)
Start-Sleep -Milliseconds 50
[ScreenCtl.Native]::mouse_event($MOUSEEVENTF_MOVE, -1, 0, 0, [IntPtr]::Zero)
Write-Output "Wake input sent."
}
# Arg validation
if (($TurnOn -and $TurnOff) -or (-not $TurnOn -and -not $TurnOff)) {
Write-Output "Specify exactly one of -TurnOn or -TurnOff."
exit 1
}
# Ensure interactive user context
Assert-UserContext
# user32 interop (compiled only once arguments and context are valid)
if (-not ('ScreenCtl.Native' -as [type])) {
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
namespace ScreenCtl {
public static class Native {
[DllImport("user32.dll", SetLastError=true)]
public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam, uint fuFlags, uint uTimeout, out IntPtr lpdwResult);
[DllImport("user32.dll", SetLastError=true)]
public static extern void mouse_event(int dwFlags, int dx, int dy, int dwData, IntPtr dwExtraInfo);
[DllImport("user32.dll")]
public static extern void keybd_event(byte bVk, byte bScan, int dwFlags, IntPtr dwExtraInfo);
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("powrprof.dll")]
public static extern uint CallNtPowerInformation(int InformationLevel, IntPtr InputBuffer, uint InputBufferLength, byte[] OutputBuffer, uint OutputBufferLength);
}
}
"@ | Out-Null
}
if ($TurnOff) {
if (Test-PortableChassis) {
Write-Output "[Skipped] Laptop or tablet detected. This script is intended for desktop monitors only."
exit 4
}
if ($Force) {
if (Test-ModernStandby) {
Write-Output "[Force] Modern Standby device detected. Bypassing the check; confirm the device stays reachable with the displays off."
} else {
Write-Output "[Force] Not needed: Modern Standby is not detected on this device. The check would have passed without -Force."
}
} elseif (Test-ModernStandby) {
Write-Output "[Skipped] Modern Standby device detected. Turning displays off could put the device into standby and drop the network. Use -Force to bypass this check on a device you have tested."
Write-Output "[Hint] Confirm with: powercfg /a"
Write-Output "[Hint] 'Standby (S0 Low Power Idle)' listed as available means Modern Standby. To disable it, run as admin, then reboot:"
Write-Output "[Hint] Dell: BIOS (F2 at boot) > Power (older BIOS: Power Management) > Sleep Mode = Force S3"
Write-Output "[Hint] or, if Dell Command | Configure is installed: & 'C:\Program Files (x86)\Dell\Command Configure\X86_64\cctk.exe' --sleepmode=forces3"
Write-Output "[Hint] Lenovo: BIOS > Config > Power > Sleep State = Linux S3"
Write-Output "[Hint] HP: BIOS > Advanced > Power Management > disable Modern Standby / enable S3 (not all models)"
Write-Output "[Hint] All: reg add HKLM\SYSTEM\CurrentControlSet\Control\Power /v PlatformAoAcOverride /t REG_DWORD /d 0 /f"
Write-Output "[Hint] After the reboot, powercfg /a should no longer list S0 Low Power Idle as available. It may report no sleep states at all, which is fine for a desktop that should never sleep."
exit 4
}
# Let the Enter key release from an interactive console pass so it does not wake the displays again
Start-Sleep -Milliseconds 500
Set-MonitorPower -State 2
exit 0
}
if ($TurnOn) {
Send-WakeInput
Set-MonitorPower -State -1
# Give Windows a moment to show the lock screen image, if it is going to
Start-Sleep -Seconds 2
$state = Get-ScreenState
Write-Output "Screen state: $state"
# Only dismiss the lock screen image; never send input to the desktop app or the secure sign-in screen
if ($state -eq 'LockScreen') {
$VK_CONTROL = 0x11
$KEYEVENTF_KEYUP = 0x0002
[ScreenCtl.Native]::keybd_event([byte]$VK_CONTROL, 0, 0, [IntPtr]::Zero)
Start-Sleep -Milliseconds 50
[ScreenCtl.Native]::keybd_event([byte]$VK_CONTROL, 0, $KEYEVENTF_KEYUP, [IntPtr]::Zero)
Start-Sleep -Seconds 1
Write-Output "Sent Ctrl to dismiss lock screen. Screen state now: $(Get-ScreenState)"
}
exit 0
}
@asheroto

asheroto commented Oct 6, 2026

Copy link
Copy Markdown
Author

Updated 2026-10-06

  • Fixed: "On" now uses the documented SC_MONITORPOWER value (-1 instead of 0)
  • Fixed: A hung app can no longer stall the display command (SMTO_ABORTIFHUNG)
  • Fixed: Running -TurnOff from a console no longer wakes the screens straight away when you let go of Enter
  • Changed: -TurnOn now wakes the screens with a 1-pixel mouse nudge instead of a Shift key press
  • New: -TurnOn detects the time and date lock screen after waking and dismisses it with a single Ctrl press. Nothing is sent if the desktop or the real sign-in screen is showing
  • New: -TurnOff skips laptops, tablets and Modern Standby devices, where the whole device could go into standby and drop off the network
  • New: Exit codes for failures and skips (see README)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment