RaspberryPiでスマートホーム 〜赤外線リモコンでエアコン操作 その2〜
前回はUSB赤外線リモコンアドバンス向けのデータの解析を行いました。
次はエアコンのコマンド部分を見ていきます。このコマンド部分を組み立てて上記の形式に変換して保存、赤外線送信という流れです。
エアコンのコマンドの解析
エアコンのコマンドを16進数で表示する
前回のbitを表示するツールを改造してdump_data.pyというツールを作りました。
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 |
import sys BIT_1_START = 45 BIT_1_END = 55 BIT_0_START = 15 BIT_0_END = 20 frames = [ {"begin": 7, "length": 64}, {"begin": 73, "length": 64}, {"begin": 139, "length": 152}, ] def get_bits(data): bits = [] for i in range(0, len(data), 4): byte1 = int(data[i + 0], 16) byte2 = int(data[i + 1], 16) byte3 = int(data[i + 2], 16) byte4 = int(data[i + 3], 16) on = (byte1 << 8) | byte2 off = (byte3 << 8) | byte4 if (off >= BIT_1_START and off <= BIT_1_END): bit = "1" elif (off >= BIT_0_START and off <= BIT_0_END): bit = "0" else: bit = "?" hex_on = data[i] + data[i + 1][-2:] hex_off = data[i + 2] + data[i + 3][-2:] item = {"on":on, "off":off, "bit":bit, "hex_on":hex_on, "hex_off":hex_off} bits.append(item) return bits def dump(bits): cnt = 0 for i in range(0, len(bits), 8): b1 = "{}{}{}{}".format(bits[i+7]["bit"], bits[i+6]["bit"], bits[i+5]["bit"], bits[i+4]["bit"]) b2 = "{}{}{}{}".format(bits[i+3]["bit"], bits[i+2]["bit"], bits[i+1]["bit"], bits[i]["bit"]) sys.stdout.write(format(int(b1, 2), "X")) sys.stdout.write(format(int(b2, 2), "X")) sys.stdout.write(" ") cnt += 1 print("") if __name__ == "__main__": data = sys.stdin.readline().split(",") bits = get_bits(data) for frame in frames: dump(bits[frame["begin"]:frame["begin"] + frame["length"]]) |
こちらはLEADERコードやSTOPコードを含まないデータ部分のみを16進数で表示します。なのでどこがデータ部分かをソースの先頭の方のframesという配列で指定してあります。前回参考にしたサイトを元に3つのFrameの配列を作成しました。
こちらもUSB赤外線リモコンアドバンスで記録したファイルを標準入力から流し込んで使います。
cat aircon_cold_28_wind0_swing.txt | python dump_data.py
なんか出ました。
11 DA 27 00 C5 00 00 D7
11 DA 27 00 42 00 00 54
11 DA 27 00 00 39 38 00 BF 00 00 06 60 00 00 C1 00 00 69
Frameが3つあるので3行出てきます。ポイントとしては8バイト単位でビット順を前後逆にして16進数に変換して出力しています。このDAIKINのコマンドはそういう仕様(LSB First)のようです。
で、これから解析なのですが…実はすでに解析している方がいました。
とはいっても、同じDAIKINとはいえ、うちのエアコンと同じコマンドかどうかは実際に調べないと分かりません。何種類かエアコンのコマンドをUSB赤外線リモコンアドバンスで保存して、上記のサイトと照らし合わせて見ました。すると…ぴったり!
プログラムを作成する
このサイトを参考にコマンドの生成&USB赤外線リモコンアドバンス形式に変換してファイルに保存するプログラムを作成します。daikin.pyという名前にしました。こちらもPython3です。
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 |
import configparser import argparse import sys FILENAME_CONFIG = "daikin.conf" FILENAME_COMMAND = "daikin.txt" BIT_0 = "0x00,0x12,0x00,0x12," BIT_1 = "0x00,0x12,0x00,0x31," BIT_LEADER = "0x00,0x86,0x00,0x41," BIT_STOP1 = "0x00,0x12,0x03,0xc1," BIT_STOP2 = "0x00,0x13,0x05,0x22," BIT_STOP3 = "0x00,0x12,0x1e,0x0d" BYTES_FRAME1 = [ 0x11, 0xda, 0x27, 0x00, 0xc5, 0x00, 0x00, 0xd7 ] BYTES_FRAME2 = [ 0x11, 0xda, 0x27, 0x00, 0x42, 0x00, 0x00, 0x54 ] def load_config(config, filename, section): config.read(filename) options = {} options["power"] = config.get(section, "power", fallback="on") options["fan"] = config.getint(section, "fan", fallback=0) options["swing"] = config.get(section, "swing", fallback="on") options["temperature"] = config.getint(section, "temperature", fallback=30) options["mode"] = config.get(section, "mode", fallback="cold") options["delay"] = config.getint(section, "delay", fallback=0) return options def save_config(config, filename, section, options): if (not config.has_section(section)): config.add_section(section) for k, v in options.items(): config.set(section, k, str(v)) with open("./daikin.conf", "w") as f: config.write(f) return def merge_config(config1, config2): for k, v in config2.items(): if (v != None): config1[k] = v; return config1 def get_parameter(parser): parser.add_argument('-p', '--power', type=str, help='on|off') parser.add_argument('-f', '--fan', type=int, help='0..5') parser.add_argument('-s', '--swing', type=str, help='on|off') parser.add_argument('-t', '--temperature', type=int, help='18..30') parser.add_argument('-m', '--mode', type=str, help='cold|heat') parser.add_argument('-d', '--delay', type=int, help='0..5 <0:offtimer >0:ontimer') args = parser.parse_args() return vars(args) def build_mode(mode): bits = "" if (mode == "cold"): bits = "0011" elif (mode == "heat"): bits = "0100" else: raise ValueError("invalid value for mode: '{}'".format(mode)) return bits; def build_timer(delay): bits = "" if (delay == 0): bits = "00" elif (delay > 0): bits = "01" elif (delay < 0): bits = "10" else: raise ValueError("invalid value for delay: '{}'".format(delay)) return bits def build_power(power): bits = "" if (power == "on"): bits = "1" elif (power == "off"): bits = "0" else: raise ValueError("invalid value for power: '{}'".format(power)) return bits def build_fan(fan): bits = "" if (fan == 0): bits = "1011" elif (fan >= 1 and fan <= 5): bits = format(fan + 2, "04b") else: raise ValueError("invalid value for fan: '{}'".format(fan)) return bits def build_swing(swing): bits = "" if (swing == "on"): bits = "1111" elif (swing == "off"): bits = "0000" else: raise ValueError("invalid value for swing: '{}'".format(swing)) return bits def build_temperature(temperature): bits = "" if (temperature >= 18 and temperature <= 30): bits = format(options["temperature"] * 2, "08b") else: raise ValueError("invalid value for temperature: '{}'".format(temperature)) return bits def build_delay1(delay): bits = "" if (delay == 0): bits = "00000000" elif (delay > 0): minutes = delay * 60 bits = format(minutes & 0xff, "08b") elif (delay < 0): bits = "00000000" else: raise ValueError("invalid value for delay: '{}'".format(delay)) return bits def build_delay2(delay): bits = "" if (delay == 0): bits = "00000110" elif (delay > 0): minutes = delay * 60 bits = format((minutes >> 8) & 0xff, "08b") elif (delay < 0): minutes = -1 * delay * 60 if (minutes > 0xff): bits = format(minutes & 0xff, "08b") else: bits = format(0x06 | ((minutes & 0x0f) << 4), "08b") else: raise ValueError("invalid value for delay: '{}'".format(delay)) return bits def build_delay3(delay): bits = "" if (delay == 0): bits = "01100000" elif (delay > 0): bits = "01100000" elif (delay < 0): minutes = -1 * delay * 60 if (minutes > 0xff): bits = format(minutes >> 8, "08b") else: bits = format(minutes >> 4, "08b") else: raise ValueError("invalid value for delay: '{}'".format(delay)) return bits def build_command(options): commands = [] hexcmd = []; hexcmd.append(0x11) hexcmd.append(0xDA) hexcmd.append(0x27) hexcmd.append(0x00) hexcmd.append(0x00) hexcmd.append(int(build_mode(options["mode"]) + "1" + build_timer(options["delay"]) + build_power(options["power"]), 2)) hexcmd.append(int(build_temperature(options["temperature"]), 2)) hexcmd.append(0x00) hexcmd.append(int(build_fan(options["fan"]) + build_swing(options["swing"]), 2)) hexcmd.append(0x00) hexcmd.append(int(build_delay1(options["delay"]), 2)) hexcmd.append(int(build_delay2(options["delay"]), 2)) hexcmd.append(int(build_delay3(options["delay"]), 2)) hexcmd.append(0x00) hexcmd.append(0x00) hexcmd.append(0xC1) hexcmd.append(0x00) hexcmd.append(0x00) hexcmd.append(calc_checksum(hexcmd)) return hexcmd; def calc_checksum(command): total = 0 for item in command: total += item checksum = total & 0xff return checksum def dump_byte(byte): buf = "" for i in range(8): if (byte & 0x01 == 1): buf += BIT_1 else: buf += BIT_0 byte = byte >> 1 return buf def dump_command(options): buf = "" # bit0 x 5 buf += BIT_0 + BIT_0 + BIT_0 + BIT_0 + BIT_0 buf += BIT_STOP1 # frame1 buf += BIT_LEADER for item in BYTES_FRAME1: buf += dump_byte(item) buf += BIT_STOP2 # frame2 buf += BIT_LEADER for item in BYTES_FRAME2: buf += dump_byte(item) buf += BIT_STOP2 # frame3 command = build_command(options) buf += BIT_LEADER for item in command: buf += dump_byte(item) buf += BIT_STOP3 return buf if __name__ == "__main__": config = configparser.SafeConfigParser() parser = argparse.ArgumentParser() options = load_config(config, FILENAME_CONFIG, "default") pars = get_parameter(parser) merge_config(options, pars) buf = dump_command(options) with open(FILENAME_COMMAND, "w") as f: f.write(buf) save_config(config, FILENAME_CONFIG, "default", options) |
使い方
使い方は…
usage: daikin.py [-h] [-p POWER] [-f FAN] [-s SWING] [-t TEMPERATURE]
[-m MODE] [-d DELAY]
パラメーターは
-p, –power | “on” | “off” | 電源オン/オフ |
-m, –mode | “cold” | “heat” | 冷房/暖房 |
-f, –fan | 0〜5 | 風量0(しずか),風量1〜5 |
-s, –swing | “on” | “off” | 風向きスイングオン/オフ |
-t, –temperature | 18〜30 | 温度 |
-d, –delay | 1〜 | 入タイマー |
-1〜 | 切タイマー |
例えば冷房で電源をONは
python daikin.py -p on -m cold
温度を27℃にするには
python daikin.py -t 27
風量を2にするには
python daikin.py -f 2
切タイマーを1時間後にセットは
python daikin.py -d -1
といった具合です。実行すると同じフォルダに”daikin.txt”というファイルが保存されます。このファイルを
$ bto_advanced_USBIR_cmd -d `cat daikin.txt`
としてやれば、エアコンがつきます。このコマンドはあくまでもファイルを生成するだけです。
なおエアコンのリモコンは毎回全ての機能の設定を送ります。つまり引数に指定のないパラメータの分も送らないといけません。なので前回送信したデータを、同じフォルダにある”daikin.conf”に格納しておき、引数に指定のない分はその設定ファイルの設定を使用します。
設定ファイルが存在しない最初の送信時はデフォルトで”冷房”、”30℃”、”風向きスイング”になっています。
コマンド送信後はそのファイルに設定を上書きします。当然ですがコマンド送信に失敗したり、別のリモコンを使うと整合性は崩れます。
ソース解説
このソースの流れです。
- ”daikin.conf”を読み込み現在のステータスを取得する
- 与えられたコマンド引数を処理して上記のステータスを上書きする
- エアコンのコマンドを生成する
- USBアドバンスリモート形式に変換して”daikin.txt”に保存する
- 現在のステータスを”daikin.conf”に書き込む
Frame1とFrame2に関しては常に固定で送信しています。Frame1は”Comfort mode”を使うと変わるようなのですが、私は使ってないので固定にしています。
STOPコードに関しては前回書いたように3種類あるように見えたので、3つ別に作りました。
Bitの0と1の長さは出現回数が多いものを採用しています。
テスト
実際のエアコンのコマンドを記録したファイルと、上記のコマンドで生成したファイルを比較します。なお生のファイルはオンとオフの時間にブレがあるので直接比較はできません。最初に作ったdump_data.pyでエアコンのコマンド部のみを比較して同じかどうか確認します。
これでOKなら実際にコマンドを送信して見ます。
$ bto_advanced_USBIR_cmd -d `cat daikin.txt`
解析してくれてるサイトが既にあったので助かりました。これでかなり時間が短縮できました。
次は我が家のスマートホームシステムにこれを組み込みます。