家電の電源制御のために市販の赤外線リモコン・コンセントに自作の赤外線リモコン送信機を組み合わせて使っていたが、2つのHWの設置場所の制約に加えコンセントも余計に占有してしまうため、なんとかならんもんかなと常々思っていたところ、な~んと、TP-LINKのスマートプラグ(HS100/105/110)のプロトコルを解析してしまった猛者がいるのを発見してしまった。世の中には凄い人がいるもんだ。(/・ω・)/
Reverse Engineering the TP-Link HS110
早速、その内容を元に試して見た所、WiFiスマートプラグ(HS105)をESP32(Arduino)から制御することに見事に成功。(/・ω・)/
TPLinkSmartPlugクラスは家電制御向けに有用と思われる機能をESP32に依存しないよう汎用的な実装を心がけたつもりなのでTCPが使える全てのArduinoで使えるはず。試してないけど...
未実装な機能は沢山あるが使うのが面倒な機能や管理系機能が多いので初期設定やその他の機能についてはKasaアプリを利用するのが良いだろう。ちなみにローカルLAN上で直接スマートプラグと通信するためKasaのクラウド・アカウントは必要ない。それと、スマートプラグのプロトコル仕様が変更されたりすると動作しなくなる可能性が考えられるので気になる人はファームウェア更新はしないほうが良いとは思うが、仕様変更などしたら稼働中の全世界中のTP-LINKスマートホーム・デバイスが一斉に動作しなくなるという大問題が発生するためそう気にする必要もないだろう。
昨今、IoTと呼ばれる様々な手法があるが家電制御するためになんでもかんでもインターネットに繋ぐというのはセキュリティ的に問題があるばかりでなく動作遅延や故障率的にも問題がありすぎると言わざる得ない。家の中と外のネットワークは明確にわけて必要な機能のみ部分的に連携するべきと思うがそんなことを考えるのは私だけだろうか...
【約1秒毎にオンオフを切り替えるサンプルスケッチ】
【修正履歴】
2021-09-15
read()とdiscovery_response()でWDT例外が発生する場合がある点を修正。
2020-01-18
ライセンスを変更。それとインクルード順序の関係でコンパイル・エラーになる場合があったので順序を変更。Arduinoのインクルードは標準インクルードファイルの後でインクルードしたほうが良いみたいだ。
2018-09-03
複数端末へアクセスする場合、途中で動作を停止した端末があったりすると、その後、アクセス毎に即時的discoveryを実行し無駄に時間を消費してしまうため、即時的discoveryは最初の一回のみに制限してみた。
★ちなみに原因不明の動作遅延についてはESP32が原因だった...PCから操作してみたところ全く問題なし。ESP8266のようにESP32も通信処理の遅延がそれなりに起こるようだ。このCPUは何をするにも簡単なんだけど、品質的、電気的、ソフトウェア的に洗練しきれていないように感じる。なので実験用としてはいいかもしれないが実用には使いものにならない。我が家でも一時は8個ぐらい常時稼働してみたことがあったが2年も経過しないうちに3個も壊れてしまったので運用的に問題なさそうな2個を残し全て他のCPUに入れ替えている。個人的な所感ではあるがこのCPUで実用システムを組むことはお勧めできない。と思う。
2018-09-02
複数端末が存在する場合、一回のDiscovery処理では端末の見落としが発生しやすいことがわかりポーリング方式に変更。さらに一定時間間隔(60秒)毎にDiscoveryを繰り返すように変更。追加関数は下記の通り。
下記のどちらかををloop()内で呼び出さなければならない。
handle()
delay(uint32_t ms)
ついでに端末名のほかにMACアドレスでも発見できるように改良。
2018-09-01
ArduinoのEthernetクラスにも対応できるようコンストラクタで指定していた引数をbegin()で指定するように変更。それにともないbegin()で指定した引数は新たなsetTarget()により指定するように変更。
それと、より確実なDiscoveryのためにロジックを若干修正。
2018-08-31
端末を発見できない可能性があったので確実に発見できるように改良。
2018-08-30
TP-Link Smart plug Discovery Protocol に対応し、Kasaアプリの初期設定にて設定した端末名をbegin()にて指定出来るようにした。これに伴い同じ名前を持つ全ての端末を同時制御する仕様に変更。
2018-08-25
TCPクライアント指定をコンストラクタで指定できるように変更。スマートプラグのアドレス指定にIPAdressを使っていたが、より柔軟性のある文字列指定に変更。バイトオーダー変更のためのhtonl()/ntohl()が存在しないボード環境があったので自前の関数に置き換え。同じくvsprintf()が存在しないボード環境があったのでsnprintf()へ変更。
あと、失敗するわけではないが、時々、送信に時間がかかることがあるので試しに受信ループにdelay(1)を入れてみたが状況変わらず。失敗しないということはスマートプラグの反応が遅いだけかな?
【ライブラリの概要説明】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
class TPLinkSmartPlug { void begin(Client& client, UDP& udp) { // 通信のためのTCPクライアントとUDPを設定する。 } void setTarget(const char *target) { // スマートプラグの端末名又はIPアドレスを設定する。複数デバイス制御時は再実行すればOK。 } void setTarget(const String target) { // スマートプラグの端末名又はIPアドレスを設定する。複数デバイス制御時は再実行すればOK。 } char *getSysInfo() { // 成功するとJSON形式の文字列が返される。失敗時はNULL。 // JSONパーサーを使うことにより各種状態を知ることが可能。 } size_t reboot(uint8_t delay = 1) { // begin("端末名")とした場合、同じ端末名を持つ全てのプラグを制御する。 // スマートプラグを再起動する。成功したプラグ数を返す、失敗時は0。 } size_t setRelayState(bool on) { // begin("端末名")とした場合、同じ端末名を持つ全てのプラグを制御する。 // 電源のオンオフ制御を行う。成功したプラグ数を返す、失敗時は0。 } size_t setLedOff(bool off) { // begin("端末名")とした場合、同じ端末名を持つ全てのプラグを制御する。 // LEDのオンオフ制御をを行う。成功したプラグ数を返す、失敗時は0。 } void handle() { // Discovery応答を処理する。 } void delay(uint32_t ms) { // handle()を実行しながら指定時間経過を待つ。 } }; |
【サンプル・スケッチ】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
#include "WiFi.h" #include "WiFiClient.h" #include "WiFiUdp.h" #include "TPLinkSmartPlug.h" #define GPIO_BOARD_LED 2 #define TARGET "My Smart Plug Mini" // スマートプラグの端末名かIPアドレスを指定 WiFiClient client; WiFiUDP udp; TPLinkSmartPlug smartplug; void setup() { pinMode(GPIO_BOARD_LED, OUTPUT); Serial.begin(115200); while (!Serial); WiFi.mode(WIFI_STA); WiFi.begin("ssid", "password"); // WiFi-APに接続するためのSSID/PASSWORDを指定 Serial.print("WiFi connecting"); while (WiFi.status() != WL_CONNECTED) { Serial.print("."); delay(100); } Serial.println(" connected"); Serial.print("IP address: "); Serial.println(WiFi.localIP()); smartplug.begin(client, udp); smartplug.setTarget(TARGET); } void loop() { static bool sw = false; sw = !sw; digitalWrite(GPIO_BOARD_LED, sw); if (!smartplug.setRelayState(sw)) Serial.printf("setRelayState(%s) = failed\n", sw ? "true" : "false"); // char *info = smartplug.getSysInfo(); // Serial.printf("getSysInfo(): %s\n", info ? info : "(failed)"); smartplug.delay(1000); } |
【ライブラリ】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 |
/* TPLinkSmartPlug.h - TP-LINK WiFi Smart Plug functions for ESP32/ESP8266 HS-100/HS-105/HS-110 Supported. Copyright (c) 2020 Sasapea's Lab. All right reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _TPLINKSMARTPLUG_H #define _TPLINKSMARTPLUG_H #include <stdio.h> #include <string.h> #include <string> #include <vector> #include <unordered_set> #include <Arduino.h> #include <Client.h> #include <Udp.h> #include <IPAddress.h> #define TPLINK_SMARTPLUG_DISCOVERY_BROADCAST "255.255.255.255" #define TPLINK_SMARTPLUG_DISCOVERY_INTERVAL 60000 #define TPLINK_SMARTPLUG_DISCOVERY_TIMEOUT 10000 #define TPLINK_SMARTPLUG_PORT 9999 #define TPLINK_SMARTPLUG_KEY 171 class TPLinkSmartPlug { private: typedef struct { String mac; String alias; IPAddress addr; } device_t; Client *_client; UDP *_udp; String _target; std::vector<device_t> _device_list; std::vector<IPAddress> _target_list; std::unordered_set<std::string> _target_set; struct { uint32_t size; char data[768]; } _packet; uint32_t _interval; void _htonl(uint32_t *n, uint32_t h) { ((uint8_t *)n)[0] = h >> 24; ((uint8_t *)n)[1] = h >> 16; ((uint8_t *)n)[2] = h >> 8; ((uint8_t *)n)[3] = h; } uint32_t _ntohl(uint32_t *n) { return ((uint32_t)((uint8_t *)n)[0] << 24) | ((uint32_t)((uint8_t *)n)[1] << 16) | ((uint16_t)((uint8_t *)n)[2] << 8) | ((uint8_t *)n)[3]; } void encrypt(char *buf, size_t len) { char key = TPLINK_SMARTPLUG_KEY; for (uint32_t i = 0; i < len; ++i) buf[i] = key = key ^ buf[i]; } void decrypt(char *buf, size_t len) { char key = TPLINK_SMARTPLUG_KEY; for (uint32_t i = 0; i < len; ++i) { char b = buf[i]; buf[i] = key ^ b; key = b; } } void encryptPacket() { encrypt(_packet.data, _packet.size); _htonl(&_packet.size, _packet.size); } void decryptPacket() { _packet.size = _ntohl(&_packet.size); decrypt(_packet.data, _packet.size); _packet.data[_packet.size] = 0; } bool read(uint8_t *buf, size_t size) { ssize_t n; for (size_t i = 0; i < size; i += n) { n = _client->read(buf + i, size - i); if (n <= 0) { if (!_client->connected()) return false; n = 0; } yield(); } return true; } bool control() { bool rv = false; size_t size = sizeof(_packet.size) + _packet.size; encryptPacket(); if (_client->write((uint8_t *)&_packet, size) == size) { if (read((uint8_t *)&_packet.size, sizeof(_packet.size))) { size = _ntohl(&_packet.size); if (size < sizeof(_packet.data) - 1) { if (read((uint8_t *)&_packet.data, size)) { decryptPacket(); rv = true; } } } } _client->stop(); return rv; } bool connect(IPAddress addr) { if (_client->connect(addr, TPLINK_SMARTPLUG_PORT)) return true; for (auto it = _device_list.begin(); it != _device_list.end(); ++it) { if (it->addr == addr) { _target_set.erase(it->alias.c_str()); _target_set.erase(it->mac.c_str()); _device_list.erase(it); break; } } return false; } // // TP-Link SmartPlug Discovery Protocol (Broadcast) // bool discovery_start() { size_t len = snprintf(_packet.data, sizeof(_packet.data), "%s", "{\"system\":{\"get_sysinfo\":null}}"); encrypt(_packet.data, len); if (_udp->beginPacket(TPLINK_SMARTPLUG_DISCOVERY_BROADCAST, TPLINK_SMARTPLUG_PORT)) { _udp->write((uint8_t *)_packet.data, len); return _udp->endPacket(); } return false; } bool getValue(String& value, const char *name) { char *p = strstr(_packet.data, name); if (p) { p += strlen(name); char *q = strchr(p, '\"'); if (q) { *q = 0; value = p; *q = '\"'; return true; } } return false; } // // Receive Discovery Response // bool discovery_response() { bool rv = false; int len; while ((len = _udp->parsePacket()) > 0) { if (_udp->read(_packet.data, len) == len) { const char MAC[] = "\"mac\":\""; const char ALIAS[] = "\"alias\":\""; decrypt(_packet.data, len); // // Parse JSON String // device_t info = {"", "", _udp->remoteIP()}; if (getValue(info.mac, MAC) && getValue(info.alias, ALIAS)) { bool append = true; for (auto it = _device_list.begin(); it != _device_list.end(); ++it) { if (it->mac == info.mac) { it->alias = info.alias; it->addr = info.addr; append = false; break; } else if (it->mac > info.mac) { _device_list.insert(it, info); append = false; break; } } if (append) _device_list.push_back(info); if ((info.alias == _target) || (info.mac == _target)) rv = true; } } yield(); } return rv; } void discovery_wait() { uint32_t t = millis(); while (millis() - t < TPLINK_SMARTPLUG_DISCOVERY_TIMEOUT) { if (discovery_response()) break; } } size_t collect() { _target_list.clear(); for(auto it = _device_list.begin(); it != _device_list.end(); ++it) { if ((it->alias == _target) || (it->mac == _target)) _target_list.push_back(it->addr); } return _target_list.size(); } size_t targets() { IPAddress addr; if (addr.fromString(_target)) { _target_list.clear(); _target_list.push_back(addr); return 1; } return collect(); } public: TPLinkSmartPlug() { } void begin(Client& client, UDP& udp) { _client = &client; _udp = &udp; udp.begin(0); _interval = millis(); } void setTarget(const char *target) { IPAddress addr; _target = target; if (!addr.fromString(target) && (_target_set.count(target) == 0)) { _target_set.insert(target); if (collect() == 0) { if (discovery_start()) discovery_wait(); } } } void setTarget(const String target) { setTarget(target.c_str()); } char *getSysInfo() { char *rv = NULL; if (targets() == 1) { if (connect(_target_list[0])) { _packet.size = snprintf(_packet.data, sizeof(_packet.data), "{\"system\":{\"get_sysinfo\":null}}"); if (control()) rv = _packet.data; } } return rv; } size_t reboot(uint8_t delay = 1) { size_t rv = 0; if (targets()) { for(auto it = _target_list.begin(); it != _target_list.end(); ++it) { if (connect(*it)) { _packet.size = snprintf(_packet.data, sizeof(_packet.data), "{\"system\":{\"reboot\":{\"delay\":%d}}}", delay); if (control()) ++rv; } } } return rv; } size_t setRelayState(bool on) { size_t rv = 0; if (targets()) { for(auto it = _target_list.begin(); it != _target_list.end(); ++it) { if (connect(*it)) { _packet.size = snprintf(_packet.data, sizeof(_packet.data), "{\"system\":{\"set_relay_state\":{\"state\":%c}}}", on ? '1' : '0'); if (control()) ++rv; } } } return rv; } size_t setLedOff(bool off) { size_t rv = 0; if (targets()) { for(auto it = _target_list.begin(); it != _target_list.end(); ++it) { if (connect(*it)) { _packet.size = snprintf(_packet.data, sizeof(_packet.data), "{\"system\":{\"set_led_off\":{\"off\":%c}}}", off ? '1' : '0'); if (control()) ++rv; } } } return rv; } void handle() { if (millis() - _interval >= TPLINK_SMARTPLUG_DISCOVERY_INTERVAL) { _interval += TPLINK_SMARTPLUG_DISCOVERY_INTERVAL; discovery_start(); } discovery_response(); } void delay(uint32_t ms) { uint32_t t = millis(); while (millis() - t < ms) handle(); } }; #endif // _TPLINKSMARTPLUG_H |
【関連投稿】
TP-LINK WiFiスマートプラグをESP32(Arduino)から制御する
TP-LINK WiFiスマートプラグをLinux/Windowsから直接制御する
TP-LINK TAPO P100/P105をESP32から直接制御する
TP-LINK TAPO P100/P105をLinux/Windowsから直接制御する