前回ブラウザからスマートプラグを制御したとき,URL の情報が見えていたので,後は HTTP リクエストを送れさえすれば制御は余裕と思っていたらそうではなかった.
リクエスト送信前に署名鍵を Tuya から取得し,その鍵で送信するリクエストデータの署名を作成し送信しないとだめっぽい.
で探してみたら,すでに Unix shell から制御するコードはあったので,ありがたくこれを PowerShell に変換してみたのが一番下のコード.
ハマったポイントは,
- UNIX Time を秒数で得る命令 (Get-Date -UFormat %s) はタイムゾーン分の誤差が発生する (PowerShell のバグっぽい) ので,タイムゾーンオフセット分増減が必要.
- 署名を作るために必要な Client Secret (Tuya developer サイトから取得する) は 16進32桁なので,128bit の数値だと思ったら,これはそのまま文字列データとして 256bit の数値として扱わないと,Tuya 側で有効な署名と認められなかった
でやった結果.Powershell で 10秒毎に消費電力を取得してみた.なかなか安価で消費電力のログ取れる環境は無いので,これはかなり満足度が高いヽ(´ー`)ノ
以下 PowerShell コード.
############################################################################## | |
# 定数設定 | |
$DeviceID = "YOUR_DEVICE_ID_00000" | |
$ClientID = "YOUR_CLIENT_ID_00000" | |
$ClientSecret = "YOUR_CLIENT_SECRET_0000000000000" | |
$BaseUrl = "https://openapi.tuyaus.com" | |
############################################################################## | |
$ClientSecret = ([System.Text.Encoding]::ASCII.GetBytes($ClientSecret)) | |
############################################################################## | |
# SHA256-HMAC 計算 | |
function Get-HMACSHA256 { | |
param( | |
[Parameter(Mandatory=$true)] | |
[byte[]]$Key, | |
[Parameter(Mandatory=$true)] | |
[String]$Data | |
) | |
[byte []] $Data = ([System.Text.Encoding]::UTF8.GetBytes($Data)) | |
$hmac = New-Object System.Security.Cryptography.HMACSHA256 | |
$hmac.Key = $Key | |
$hash = $hmac.ComputeHash($Data) | |
$hmac.Dispose() | |
return [System.BitConverter]::ToString($hash).Replace("-", "") | |
} | |
function Get-SHA256 { | |
param( | |
[Parameter(Mandatory=$true)] | |
[String]$Data | |
) | |
[byte []]$Data = [System.Text.Encoding]::UTF8.GetBytes($Data) | |
$sha256 = New-Object System.Security.Cryptography.SHA256Managed | |
$hashBytes = $sha256.ComputeHash($Data) | |
# ハッシュ値を16進数文字列に変換 | |
return [System.BitConverter]::ToString($hashBytes).Replace("-", "").ToLower() | |
} | |
############################################################################## | |
function InvokeCommand { | |
param( | |
[Parameter(Mandatory=$true)] [String]$URL, | |
$Command, | |
[String]$AccessToken = '' | |
) | |
# Signature 計算 | |
$Hash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" | |
$Method = 'GET' | |
if($PSBoundParameters.ContainsKey('Command')){ | |
$Body = ConvertTo-Json $Command | |
$Hash = Get-SHA256 -Data $Body | |
$Method = "POST" | |
} | |
$StringToSign = "$ClientID$AccessToken$TimeStamp$Method`n$Hash`n`n$URL" | |
$Sign = Get-HMACSHA256 -Key $ClientSecret -Data $StringToSign | |
# ヘッダ生成 | |
$Headers = @{ | |
"sign_method" = "HMAC-SHA256" | |
"client_id" = $ClientID | |
"t" = $TimeStamp | |
"mode" = "cors" | |
"Content-Type" = "application/json" | |
"sign" = $Sign | |
} | |
if($AccessToken -ne ''){ | |
$Headers.access_token = $AccessToken | |
} | |
# HTTP リクエスト送信 | |
try{ | |
if($PSBoundParameters.ContainsKey('Command')){ | |
$Response = Invoke-WebRequest -Uri "$BaseUrl$URL" -Headers $Headers -Method $Method -Body $Body | |
}else{ | |
$Response = Invoke-WebRequest -Uri "$BaseUrl$URL" -Headers $Headers -Method $Method | |
} | |
}catch{ | |
throw $_ | |
} | |
# レスポンス解析 | |
$Content = ConvertFrom-Json $Response.Content | |
if(!$Content.success){ | |
throw "Tyua API access failed: $($Content.msg)" | |
} | |
$Content | |
} | |
############################################################################## | |
# Tyua API | |
function TuyaGetToken { | |
$script:TimeStamp = ((Get-Date -UFormat %s) - (Get-TimeZone).BaseUtcOffset.TotalSeconds) -replace "\..*", "000" | |
$resp = InvokeCommand -URL "/v1.0/token?grant_type=1" | |
$resp.result.access_token | |
} | |
function TuyaInvokeCommand { | |
param( | |
[Parameter(Mandatory=$true)] [String][String]$AccessToken, | |
[Parameter(Mandatory=$true)] $Commands | |
) | |
(InvokeCommand -URL "/v1.0/iot-03/devices/$DeviceID/commands" -AccessToken $AccessToken -Command $Commands).result | |
} | |
function TuyaGetStatus { | |
param( | |
[Parameter(Mandatory=$true)] [String][String]$AccessToken | |
) | |
$Result = @{} | |
foreach($elem in (InvokeCommand -URL "/v1.0/iot-03/devices/$DeviceID/status" -AccessToken $Token).result){ | |
$Result[$elem.code] = $elem.value | |
} | |
$Result | |
} | |
############################################################################## | |
$ProgressPreference = "SilentlyContinue" | |
# コンセント1 を On, コンセント2 を Off にする | |
$Commands = @{ | |
commands = @( | |
@{ | |
code = "switch_1" | |
value = $True | |
}, | |
@{ | |
code = "switch_2" | |
value = $False | |
} | |
) | |
} | |
TuyaInvokeCommand -AccessToken $Token -Commands $Commands | |
# 10秒ごとに消費電力を表示 | |
while($True){ | |
$Token = TuyaGetToken | |
(TuyaGetStatus -AccessToken $Token).cur_power / 10 | |
Start-Sleep 10 | |
} |