この記事は 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よくわからない使ったことないって人は以下の記事が画像多めでとてもわかり易いので読んでみてください。
M2MqttをPowerShellで実行させる部分はほぼこの記事のとおりにやりました。日本語の記事無くてねぇ。
IFTTTの設定は以前の記事を参考にどうぞ
M2Mqttをインストール
まずはInstall-Package M2Mqtt -Force
でM2Mqttをインストール! できない...。
よく見ると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タブで確認できます。
クライアントオブジェクト初期化
$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のコンソールからテスト送信してスリープモードになれば成功
常駐させる必要があるので無限ループさせておきます
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ウィンドウを非表示にしつつタスクトレイにアイコンを表示
常駐させておくのにウィンドウがずっと出ているのは邪魔なのでタスクトレイにアイコンを表示してウィンドウは非表示にします。以下記事を先頭にコピペすれば実現できました。
こんなかんじでPowerShellのアイコンが表示されます。
コード全文
#@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を試してみたい。