【Java】Dysonの家電をプログラム経由で操作したり室温を取得したりする

2020年12月31日

今回はDysonの空調家電、Pure Hot + Cool LinkをJavaから操作してみましょう。

テストに使う型番はHP03ですが他のDyson扇風機や加湿器などでも同じように操作できると予想されますので参考にしてみてください。

プログラムを実行する前にDyson家電をセットアップしておきましょう。
セットアップは前回の記事で紹介しています。

HP03はMQTTというプロトコルを使用します。
ライブラリを使えば楽なのですが、MQTTについて少しでも理解を深めたいので、あえて標準ライブラリのSocketクラスでセンサーデータ取得や操作を行います。

操作するためにはID、パスワードなどの情報が必要です。情報取得のためのツールは前回の記事で紹介しています。

準備ができたら処理を実行してみましょう。

やってることの流れ

・認証
・subscribeリクエスト(受信登録)
・publishリクエスト(家電操作やセンサー情報取得要求)

です。

プログラムはこんな感じになりました。

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;

public class DysonTest {
	private static String clientId="test_001";//client idは自分で適当に設定しても動きます
	private static String userName="ユーザー名";//パケットキャプチャで解析が必要
	private static String password="パスワード";//パケットキャプチャで解析が必要
	private static String model="モデル番号";//パケットキャプチャで解析が必要(HP03は455)
	private static String host="IPアドレス";//ルータなどにアクセスして調べる
	private static int port=1883;//恐らく1883で問題なし

	public static void main(String[] args) throws InterruptedException {

		try (SocketChannel sock = SocketChannel.open(new InetSocketAddress(host, port))){

			//データ受信スレッド起動
			new Thread(()->{
				try {
					receive(sock);
				} catch (IOException e) {
				} catch (InterruptedException e) {
				}
			}).start();

			//認証データを送信
			sendAuthMsg(sock);

			Thread.sleep(500);

			//データ取得出来るようにするsubscribeリクエスト
			sendSubscribeMsg(sock);

			Thread.sleep(500);

			//現在のセンサーなどのデータをこちらに送りつけるように指示する
			sendCurrentStatusGetMsg(sock,(short)2);

			Thread.sleep(500);

			//電源をつける
			sendCtrlMsg(sock, "{\"fmod\": \"FAN\"}", (short)3);

			Thread.sleep(3000);

			//首振りON
			sendCtrlMsg(sock, "{\"oson\": \"ON\"}", (short)4);

			Thread.sleep(3000);

			//風量を最小に
			sendCtrlMsg(sock, "{\"fnsp\": \"0001\"}", (short)5);

			Thread.sleep(3000);

			//暖房ON
			sendCtrlMsg(sock, "{\"hmod\": \"HEAT\"}", (short)6);

			Thread.sleep(3000);

			//暖房OFF
			sendCtrlMsg(sock, "{\"hmod\": \"OFF\"}", (short)7);

			Thread.sleep(3000);

			//風量を最大に
			sendCtrlMsg(sock, "{\"fnsp\": \"0010\"}", (short)8);

			Thread.sleep(3000);

			//首振りOFF
			sendCtrlMsg(sock, "{\"oson\": \"OFF\"}", (short)9);

			Thread.sleep(3000);

			//電源を消す
			sendCtrlMsg(sock, "{\"fmod\": \"OFF\"}", (short)10);

			Thread.sleep(3000);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	private static void sendCtrlMsg(SocketChannel sock, String ctrlJson, short indentifer) throws IOException {

		ByteBuffer result = ByteBuffer.allocate(1024);

		//ヘッダー
		result.put((byte)0x32);

		//データ
		ByteBuffer data = ByteBuffer.allocate(1024);

		String topic = model+"/"+userName+"/command";

		//Topic サイズ
		data.putShort((short)topic.length());

		//Topic
		data.put(topic.getBytes("UTF-8"));

		//Identifier
		data.putShort((short)indentifer);

		//データ取得リクエストJSON
		String json = "{\"data\": "+ctrlJson+",\"mode-reason\": \"iot\",\"time\": \"2020-05-17T12:21:14Z\",\"msg\":\"STATE-SET\"}";
		data.put(json.getBytes("UTF-8"));

		//サイズ計算
		putSize(data, result);

		data.flip();

		result.put(data);

		result.flip();

		//データの送信
		sock.write(result);
	}

	//データ取得リクエスト
	private static void sendCurrentStatusGetMsg(SocketChannel sock, short indentifer) throws IOException {

		ByteBuffer result = ByteBuffer.allocate(1024);

		//ヘッダー
		result.put((byte)0x32);

		//データ
		ByteBuffer data = ByteBuffer.allocate(1024);

		String topic = model+"/"+userName+"/command";

		//Topic サイズ
		data.putShort((short)topic.length());

		//Topic
		data.put(topic.getBytes("UTF-8"));

		//Identifier
		data.putShort((short)indentifer);

		//データ取得リクエストJSON
		String json = "{\"mode-reason\":\"iot\",\"time\":\"2020-05-17T20:21:33Z\",\"msg\":\"REQUEST-CURRENT-STATE\"}";
		data.put(json.getBytes("UTF-8"));

		//サイズ計算
		putSize(data, result);

		data.flip();

		result.put(data);

		result.flip();

		//データの送信
		sock.write(result);
	}

	//データ取得できるようにするリクエスト
	private static void sendSubscribeMsg(SocketChannel sock) throws IOException {

		ByteBuffer result = ByteBuffer.allocate(1024);

		//ヘッダー
		result.put((byte)0x82);

		//データ
		ByteBuffer data = ByteBuffer.allocate(1024);

		//Identifier
		data.putShort((short)0x0001);


		String topic = model+"/"+userName+"/status/current";

		//Topic サイズ
		data.putShort((short)topic.length());

		//Topic
		data.put(topic.getBytes("UTF-8"));

		//Requested QoS
		data.put((byte)0x01);

		//サイズ計算
		putSize(data, result);

		data.flip();

		result.put(data);

		result.flip();

		//データの送信
		sock.write(result);
	}

	//認証
	private static void sendAuthMsg(SocketChannel sock) throws IOException {

		ByteBuffer result = ByteBuffer.allocate(1024);

		//ヘッダー
		result.put((byte)0x10);

		//データ
		ByteBuffer data = ByteBuffer.allocate(1024);

		//プロトコル名サイズ
		data.putShort((short)0x0004);

		//プロトコル名(MQTT)
		data.putInt(0x4d515454);

		//MQTTバージョンv3.1.1
		data.put((byte)0x04);

		//Connect Flags
		data.put((byte)0xc2);

		//KeepAlive
		data.putShort((short)0x003c);

		//クライアントID Length
		data.putShort((short)clientId.length());

		//クライアントID
		data.put(clientId.getBytes("UTF-8"));

		//ユーザー名 サイズ
		data.putShort((short)userName.length());

		//ユーザー名
		data.put(userName.getBytes("UTF-8"));

		//パスワード サイズ
		data.putShort((short)password.length());

		//パスワード
		data.put(password.getBytes("UTF-8"));

		//サイズ計算
		putSize(data, result);

		data.flip();

		result.put(data);

		result.flip();

		//データの送信
		sock.write(result);
	}

	//ヘッダ以外のサイズ計算
	private static void putSize(ByteBuffer src, ByteBuffer dest){

		int x = src.position();
		int digit = 0;

		do {
			digit = x % 128;
			x = x / 128;
			if (x > 0) digit = digit | 0x80;
			dest.put((byte)(digit&0x000000ff));
		}while(x>0);
	}

	//データ受信
	private static void receive(SocketChannel in) throws IOException, InterruptedException {
		ByteBuffer readData = ByteBuffer.allocate(4096);
		while(true) {
			readData.clear();
			int r = in.read(readData);
			readData.flip();
			if ( r == -1 ) {
				System.out.println("connection end.");
				break;
			}
			if ( (readData.get(0) & 0x30) == 0x30 ) {
				//dysonからのメッセージ受信
				//本当はレスポンスを返すべき
			}
			printByte(readData);
		}
	}

	//データの表示
	private static void printByte(ByteBuffer bb) throws UnsupportedEncodingException {
		System.out.println("--------------------------------------");
		for ( int i = 0;i < bb.limit();i++ ) {
			byte b = bb.get(i);
			String ss = Integer.toHexString(Byte.toUnsignedInt(b)).toUpperCase();
			if ( ss.length() < 2 ) ss ="0"+ss;
			System.out.print("0x"+ss+",");
		}
		System.out.println();
		System.out.println(StandardCharsets.UTF_8.decode(bb));
		System.out.println("--------------------------------------");
	}
}

実行結果

実行結果(コンソール一部)

{“msg”:”ENVIRONMENTAL-CURRENT-SENSOR-DATA”,”time”:”2020-06-22T13:07:02.001Z”,”data”:{“tact”:”OFF”,”hact”:”OFF”,”pact”:”0003″,”vact”:”0001″,”sltm”:”OFF”}}

プログラム内には適当にコメント入れておいたので、MQTTの動きをちょっとでも理解したい場合は、参考にしてみてください。

その他、私が調べたパラメータ情報を載せておきます。

操作時の送信パラメータ情報

設定名 設定値 説明
fmod FAN,OFF,AUTO ファンのオンオフ制御
hmod HEAT,OFF 暖房機能のオンオフ
hmax 設定温度=(摂氏温度+273)*10 暖房機能の設定温度
fnsp 0001~0010 ファンの速度
oson ON,OFF 首振り
ffoc ON,OFF ディフューズドモード
nmod ON,OFF ナイトモード
sltm [分単位の数値],OFF スリープタイマー

センサーデータ取得時のパラメータ情報

センサー名 説明
tact 室温([(摂氏温度+273)*10]を表す数値)
hact 湿度
pact 空気汚染レベル(低いほどきれい)

これで、MQTTについて少しは理解できたかなぁ。。。

IoT, Java

Posted by nompor