自動化厨のプログラミングメモブログ │ CODE:LIFE

Python/ExcelVBA/JavaScript/Raspberry Piなどで色んなことを自動化

beebotteのクライアントにPowerShellを使ってリモートでWindowsをスリープモードにする

f:id:maru0014:20201220220847p:plain この記事は UG Advent Calendar 2020 22日目の記事です。

今日、何を思ったかPowerShellでMQTTクライアントを動かしてBeebotteのシステムコールトピックに「スリープモード」がパブリッシュされたら自動でデスクトップPCをスリープするというのを作ってみたくなった。

exeファイル単体でWindowsで動くNode-RED Desktopで実現しようか一瞬迷ったけどPowerShellなら10数行で書けるんじゃねと甘く見ての犯行です。

ちょいちょい躓きましたが一応動くものはできました。「OK Google システムコール スリープモード」と言えばPCがスリープモードになります。MQTTなら自宅ネットワークに穴あけなくて済むし安全。

概要

PowerShellのモジュールをぽいっとインストール とはいかないらしい。

C# / .NETで使えるMQTTクライアント「M2Mqtt」をインストールしてPowerShellから操作することで実現する(もはやPowerShellじゃない?いやいやそんなことは...)

ブローカーは無料で50000メッセージ/日まで使えるbeebotte。REST APIでパブリッシュ出来る便利なやつ。MQTTよくわからない使ったことないって人は以下の記事が画像多めでとてもわかり易いので読んでみてください。

beebotteの使い方メモ.md · GitHub

M2MqttをPowerShellで実行させる部分はほぼこの記事のとおりにやりました。日本語の記事無くてねぇ。

www.msxfaq.de

IFTTTの設定は以前の記事を参考にどうぞ

codelife.cafe

M2Mqttをインストール

まずはInstall-Package M2Mqtt -ForceM2Mqttをインストール! できない...。

f:id:maru0014:20201220221010p:plain

よく見るとnuget.exe自体を別途ダウンロードして以下を実行でした。

nuget.exe install M2Mqtt -o c:\lib

PowerShellでMQTTクライアント

M2MqttのPathを追加

Add-Type -Path "C:\lib\M2Mqtt.4.3.0.0\lib\net45\M2Mqtt.Net.dll"

設定値変数

$server = "mqtt.beebotte.com"
$secret = "Beebotteのシークレットキー"
$topic = "チャネル/トピック"

最初はトピックごとのトークンでやるもんだと思ってたから躓いた。

シークレットキーはAccount settingsのAccess Managementタブで確認できます。

f:id:maru0014:20201220221020p:plain

クライアントオブジェクト初期化

$MqttClient = [uPLibrary.Networking.M2Mqtt.MqttClient]($server)

Beebotteのsecretで接続

パスワードは空でOK。

$MqttClient.Connect([guid]::NewGuid(), $secret,"")

受信したときのイベントハンドラーを登録

取得したdataに「スリープモード」が含まれていた場合、アセンブリを読み込んでスリープモードに移行させます。Windowsはシャットダウンや再起動は簡単なコマンドがあるけど、スリープモードは無いらしい。

色々やり方があったなかで一番シンプルに2行で書けるやつにしました。

#Eventhandler registrieren
Register-ObjectEvent `
    -inputObject $MqttClient `
    -EventName MqttMsgPublishReceived `
    -Action {
        $result = [System.Text.Encoding]::UTF8.GetString($args[1].Message) | ConvertFrom-Json
        $command = $result.data.Replace(" ","")
    if($command -Like "*スリープモード*"){
            Add-Type -Assembly System.Windows.Forms
            [System.Windows.Forms.Application]::SetSuspendState('Suspend', $false, $false)
        }
    }

対象のトピックをサブスクライブ

# Subscribe
$MqttClient.Subscribe($topic,0)

Beebotteのコンソールからテスト送信してスリープモードになれば成功

f:id:maru0014:20201220221036p:plain

常駐させる必要があるので無限ループさせておきます

While($true){Start-Sleep 10}

Windows起動時に実行されるように設定

このスクリプトをスタートアップ時に自動実行されるようにタスクスケジューラに設定しておきます。以下は C:\Scripts にps1を配置した場合の設定例です。

設定
トリガー ログオン時(任意のユーザー)
操作 プログラムの開始
プログラム C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
引数の追加 -ExecutionPolicy RemoteSigned ".\remote-sleep.ps1"
開始    C:\Scripts

ps1は直接プログラムに設定しても実行できず、powershell.exeの引数として渡してあげる必要があります。

PowerShellウィンドウを非表示にしつつタスクトレイにアイコンを表示

常駐させておくのにウィンドウがずっと出ているのは邪魔なのでタスクトレイにアイコンを表示してウィンドウは非表示にします。以下記事を先頭にコピペすれば実現できました。

qiita.com

こんなかんじでPowerShellのアイコンが表示されます。

f:id:maru0014:20201221204311p:plain
タスクトレイに格納された実行中のps1

コード全文

#@Powershell -NoP -W Hidden -C "$PSCP='%~f0';$PSSR='%~dp0'.TrimEnd('\');&([ScriptBlock]::Create((gc '%~f0'|?{$_.ReadCount -gt 1}|Out-String)))" %* & exit/b
# by earthdiver1  V1.05
if ($PSCommandPath) {
    $PSCP = $PSCommandPath
    $PSSR = $PSScriptRoot
    $code = '[DllImport("user32.dll")]public static extern bool ShowWindowAsync(IntPtr hWnd,int nCmdShow);'
    $type = Add-Type -MemberDefinition $code -Name Win32ShowWindowAsync -PassThru
    [void]$type::ShowWindowAsync((Get-Process -PID $PID).MainWindowHandle,0) }
Add-Type -AssemblyName System.Windows.Forms, System.Drawing
$menuItem = New-Object System.Windows.Forms.MenuItem "Exit"
$menuItem.add_Click({$notifyIcon.Visible=$False;while(-not $status.IsCompleted){Start-Sleep 1};$appContext.ExitThread()})
$contextMenu = New-Object System.Windows.Forms.ContextMenu
$contextMenu.MenuItems.AddRange($menuItem)
$notifyIcon = New-Object System.Windows.Forms.NotifyIcon
$notifyIcon.ContextMenu = $contextMenu
$notifyIcon.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon($PSCP)
$notifyIcon.Text = (Get-ChildItem $PSCP).BaseName
$notifyIcon.Visible = $True
$_syncHash = [hashtable]::Synchronized(@{})
$_syncHash.NI   = $notifyIcon
$_syncHash.PSCP = $PSCP
$_syncHash.PSSR = $PSSR
$runspace = [RunspaceFactory]::CreateRunspace()
$runspace.ApartmentState = "STA"
$runspace.ThreadOptions  = "ReuseThread"
$runspace.Open()
$runspace.SessionStateProxy.SetVariable("_syncHash",$_syncHash)
$scriptBlock = Get-Content $PSCP | ?{ $on -or $_[1] -eq "!" }| %{ $on=1; $_ } | Out-String
$action=[ScriptBlock]::Create(@'
#   param($Param1, $Param2)
    # Start-Transcript -LiteralPath ($_syncHash.PSCP -Replace '\..*?$',".log") -Append
    Function Start-Sleep { [CmdletBinding(DefaultParameterSetName="S")]
        param([parameter(Position=0,ParameterSetName="M")][Int]$Milliseconds,
              [parameter(Position=0,ParameterSetName="S")][Int]$Seconds,[Switch]$NoExit)
        if ($PsCmdlet.ParameterSetName -eq "S") {
            $int = 5
            for ($i = 0; $i -lt $Seconds; $i += $int) {
                if (-not($NoExit -or $_syncHash.NI.Visible)) { exit }
                Microsoft.PowerShell.Utility\Start-Sleep -Seconds $int }
        } else {
            $int = 100
            for ($i = 0; $i -lt $Milliseconds; $i += $int) {
                if (-not($NoExit -or $_syncHash.NI.Visible)) { exit }
                Microsoft.PowerShell.Utility\Start-Sleep -Milliseconds $int }}}
    $script:PSCommandPath = $_syncHash.PSCP
    $script:PSScriptRoot  = $_syncHash.PSSR
'@ + $scriptBlock)
$PS = [PowerShell]::Create().AddScript($action) #.AddArgument($Param1).AddArgument($Param2)
$PS.Runspace = $runspace
$status = $PS.BeginInvoke()
$appContext = New-Object System.Windows.Forms.ApplicationContext
[void][System.Windows.Forms.Application]::Run($appContext)
exit
#! ---------- ScriptBlock (Line No. 28) begins here ---------- DO NOT REMOVE THIS LINE

# M2Mqtt読み込み
Add-Type -Path "C:\lib\M2Mqtt.4.3.0.0\lib\net45\M2Mqtt.Net.dll"

$server = "mqtt.beebotte.com"
$secret = "<beebotteのシークレットキー>"
$topic = "<Channel>/<Topic>"

# Client Init
$MqttClient = [uPLibrary.Networking.M2Mqtt.MqttClient]($server)

# Connect with Beebotte secret
$MqttClient.Connect([guid]::NewGuid(), $secret,"")

#Eventhandler registrieren
# Write-Host "Event Found Topic: $($args[1].topic)  Message $([System.Text.Encoding]::UTF8.GetString($args[1].Message))"
# $mqttresults[$args[1].topic] = $args[1].Message
Register-ObjectEvent `
    -inputObject $MqttClient `
    -EventName MqttMsgPublishReceived `
    -Action {
        $result = [System.Text.Encoding]::UTF8.GetString($args[1].Message) | ConvertFrom-Json
        $command = $result.data.Replace(" ","")
    if($command -Like "*スリープモード*"){
            Add-Type -Assembly System.Windows.Forms
            [System.Windows.Forms.Application]::SetSuspendState('Suspend', $false, $false)
        }
    }

# Subscribe
$MqttClient.Subscribe($topic,0)

While($true){Start-Sleep 10}

# Disconnect
# $MqttClient.Disconnect()

あとがき

ちょっと無理やり感ありますがなんとか実現出来ました。ぶっちゃけPythonでも良かったかな。

でもPowerShellの勉強にはなりました。

今度はNode-RED Desktopを試してみたい。