スマート電球(Wiz)をESP32で制御する

No Image

スマート家電が増えてきて非常に便利な昨今ですが、基本的にはスマホのアプリから制御する仕様になっています。
そうなるとセンサなどを組み合わせた制御はなかなか難しくなります。
PCから制御できたり、もっといいのはESP32などを使ったシンプルなモジュールから直接制御できたらいいなと思っていたので挑戦してみました。

スポンサーリンク

購入したスマート電球

買ったのはWiZというブランドのスマート電球です。
どうやらフィリップスから分社化したシグニファイという企業から展開されているらしいです。
そこそこ安価でいろんなサービスに対応してるっぽいという理由で買ってみました。

実はこれがアタリだったような気がします...

他にもいろんなスマート電球はありますが、制御が複雑なものが多い気がします。
これは後で気づいたことなんですけどね...

設定とIPアドレスの確認

アプリによる初期設定

初期設定はアプリを使います。
V2がついた最新版(?)もあるみたいですが、基本的な操作方法はどちらも同じみたいです。

V2の方が評価が低いのは使いにくいからなんでしょうか...

何回か電源のON/OFFを繰り返すと、設定モードに移行する仕様になっています。
そしてアプリから設定でコントローラと同じネットワークに入れておきます。

IPアドレスを確認する

アプリからIPアドレスやMACアドレスは確認できませんので、Advanced IP Scanner などを使って確認しておきます。
MACアドレスは分かる場所には書かれていません。

MACアドレスはベンダーコードがありますので、判別しやすいかと思います。
WiZのベンダーコードは44:4F:8Eです。
つまりMACアドレスが44:4F:8E:XX:XX:XXというデバイスを探せばいいわけです。

WiZのスマート電球を見つける
結構簡単に見つかります。
ローカルIPとMACアドレスくらいなら公開しても支障ないですが、一応モザイク入れてます(笑)

問題はDHCPの関係でIPアドレスが変わってしまったときに制御できなくなってしまうことです。
ホスト名があればいいんですが、このスマート電球にはなさそうです。
ルータ側から固定IPに割り当てるとかならできそうですが...
MACアドレスからIPアドレスを割り出す事ができたらいいんですけどね。

ESP32でWiZのスマート電球を直接操作する

UDPで接続する

WiZのスマート電球はUDPで接続してJSON形式のコマンドを送るだけです。
めっちゃくちゃ簡単にハックできてしまいます。
UDPポートは38899でアクセスします。

ONコマンド
{"method":"setPilot","params":{"state":0}}

成功した場合の応答データは下記のデータが送られてきます。

{"method":"setPilot","env":"pro","result":{"success":true}}
OFFコマンド
{"method":"setPilot","params":{"state":0}}
明るさ調整コマンド

下記のコマンドは明るさを50%にするコマンドです。
ここまで書いてると分かってくると思いますが、JSONなのでこのあたりは組み合わせできます。

{"method":"setPilot","params":{"dimming":50}}
状態の取得
{"method":"getPilot"}

状態取得に応答した場合は下記のようなデータが送られてきます。

{
  "method":"getPilot",
  "env":"pro",
  "result":
  {
    "mac":"444f8e07701a",
    "rssi":-56,
    "state":false,
    "sceneId":12,
    "temp":2700,
    "dimming":100
  }
}

分かりやすいように改行やインデントを入れていますが、実際は1行でずらっと送られてきます。
この場合だと"state"が"falseですので、電球はOFF状態といったような状態が分かりますね。

ESP32で直接制御する

UDPで接続すれば簡単に制御できることが分かったので、ESP32でも複雑なプログラムでなくても制御できますね。
UDPの使い方さえわかれば大丈夫です。
テスト用に作ったプログラムは下記のような感じになりました。

#include <WiFi.h>
#include <WiFiUdp.h>

#include <ArduinoJson.h>
#include <ButtonEvents.h>

#define MAN_BTN 22

#define SSID "ssid"
#define PASS "password"

const unsigned int local_port     = 65098;
const unsigned int wiz_light_port = 38899;
const char get_buffer[] = "{\"method\":\"getPilot\"}";
const char on_buffer[]  = "{\"method\":\"setPilot\",\"params\":{\"state\": 1}}";
const char off_buffer[] = "{\"method\":\"setPilot\",\"params\":{\"state\": 0}}";
const IPAddress wiz_light_ip = IPAddress(192, 168, 1, 26);
char packet_buf[255];

WiFiUDP      udp;
ButtonEvents manual_btn;

void setup() {
  manual_btn.attach(MAN_BTN, INPUT_PULLUP);

  // Serial Port Initialize
  Serial.begin(115200);
  delay(100);
  Serial.println("\nBooting...");

  // Wi-Fi Connection
  WiFi.begin(SSID, PASS);
  Serial.print("Connecting Wifi...");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected");
  Serial.print("IP Address:\t");
  Serial.println(WiFi.localIP());
  udp.begin(local_port);

}

void loop() {
  manual_btn.update();

  static bool light_state = false;
  if (manual_btn.tapped()) {
    if (!light_state) {
      wiz_light_on(wiz_light_ip, wiz_light_port);
      Serial.println("Send ON");
    }
    else {
      wiz_light_off(wiz_light_ip, wiz_light_port);
      Serial.println("Send OFF");
    }
    light_state = !light_state;
  }

  if (manual_btn.held()) {
    Serial.println(is_light_state(wiz_light_ip, wiz_light_port) ? "State:ON" : "State:OFF");
  }
}


void wiz_light_on(const IPAddress light_ip, const uint16_t light_port) {
  udp.beginPacket(light_ip, light_port);
  udp.print(on_buffer);
  udp.endPacket();
}


void wiz_light_off(const IPAddress light_ip, const uint16_t light_port) {
  udp.beginPacket(light_ip, light_port);
  udp.print(off_buffer);
  udp.endPacket();
}


bool is_light_state(const IPAddress light_ip, const uint16_t light_port) {
  bool light_on = false;

  // flush RX buffer to avoid reading old packets
  while (udp.parsePacket() > 0) udp.flush();
  udp.beginPacket(light_ip, light_port);
  udp.print(get_buffer);
  udp.endPacket();

  int packet_size = udp.parsePacket();
  int timeout = 10;
  while (!packet_size && timeout > 0) {
    packet_size = udp.parsePacket();
    timeout--;
    delay(200);
  }
  
  if (timeout == 0) {
    Serial.print("light timed out: ");
    Serial.print(light_ip);
    Serial.print(" on port ");
    Serial.println(light_port);
    return false;
  }
  if (packet_size) {
    int len = udp.read(packet_buf, 255);
    if (len > 0) {
      packet_buf[len] = 0;
    }

    Serial.println("Packet received: ");
    Serial.println(packet_buf);

    // declare a JSON document to hold the response
    const int capacity = JSON_OBJECT_SIZE(3) + JSON_OBJECT_SIZE(7);
    StaticJsonDocument<capacity> response;

    // get the current state of the light, whether it's on or off
    DeserializationError err = deserializeJson(response, packet_buf);
    if (err.code() == DeserializationError::Ok) {
      light_on = response["result"]["state"];
    }
  }

  return light_on;
}

スポンサーリンク

SSIDやパスワード、スマート電球のIPアドレスは環境に合わせて書き換えてください。
UDPのポート(12行目、65098)が他の機器とかぶる場合も変更が必要です。

ハードウェアとしてはタクトスイッチを22番ピンに接続しています。
ESP32側も非常にシンプルです。

別途使用するライブラリ

ArduinoJSONライブラリ

ArduinoでJSON形式を簡単に扱えるようにするためのライブラリがあると便利です。

今回のプログラムではデータを受信して状態を取得するところに使用しています。

ButtonEventsライブラリ

スイッチの制御には ButtonEvents というライブラリを使用しています。
普通に押したときと長押ししたときで動作を分けているだけですけどね。
このライブラリとしてはダブルクリックもいけちゃいます。
あとチャタリング処理をしてくれているのも嬉しいところです。

動作確認

スイッチをポチポチするだけですが、電球がちゃんとON/OFFしているのが分かるかと思います。


他のスマート電球だとこんな簡単には制御できなかったのかと思うと、ちょっとラッキーだったかもしれません。
次はスマートプラグで簡単に制御できるものを探したいところです。
ESP32で色々と制御できるのが分かると夢が広がりますね。

スマート電球(Wiz)をESP32で制御する

スポンサーリンク

Leave a Comment