Homebridgeで家電をHomeKit化してみる(後編)

# はじめに
この記事は、[SLP KBIT AdventCalendar2021](https://adventar.org/calendars/6706) 7日目の記事です。
PCや家電、スマートプラグなんかをiPhoneやMacから操作できるようにする試みの後編です。

ということで前回の記事から5日ほど開きましたが、前回言っていた通りHomebridgeからNatureRemo miniを利用し、赤外線リモコンで動作する家電を操作できるようにします。
前回の記事を見ていない方はこちらからご覧ください。

https://qiita.com/Gomatamago_/items/8585dfdd38a93e41cecf

# 一連の流れ
エアコンやシーリングライトを操作するため、Homebridge-CMD4というプラグインを導入し、自作のプログラム経由でNatureRemo miniを操作します。
NatureRemoにはすでに**Homebridge Nature Remo Cloud Aircon**や**Homebridge Nature Remo Platform**といった便利なプラグインがありますが、
cloud経由だったり風量調節ができなかったりするので今回は見送りました。
私の環境で試した結果なので、参考程度に読んでみてください。

# Homebridge-CMD4について
Homebridge-CMD4とは、ホームappからの操作を行った際に任意のコマンドを実行するプラグインです。
HomeKitが対応しているアクセサリのほとんどを網羅しているため、複雑な操作が可能です。
https://github.com/ztalbot2000/homebridge-cmd4
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/925552/f2ed6b3a-6f74-4478-1005-7ecb6455fd8a.png)
どんなアクセサリが使用できるかは以下にざっくり書いてあります。
https://ztalbot2000.github.io/homebridge-cmd4/autoGenerated/CMD4_AccessoryDescriptions.html

# NatureRemoのAPIについて
以下のサイトでアクセストークンを取得できます。ローカルAPIのみを使用する場合は必要ないです。
https://home.nature.global/

例えば赤外線リモコンのデータを取得したい場合は以下のようなコマンドを実行します。

```cmd
curl -X GET 'http://<IP or HostName>/messages' -H 'X-Requested-With: curl' -H 'accept: application/json'
```
また、NatureRemoの状態を取得する場合は以下のコマンドを打つとjson形式で結果が返ってきます。

```cmd
curl -X GET 'https://api.nature.global/1/devices' -H 'Authorization: Bearer <Token>'
```
NatureRemo miniで試したところ、このような結果が返ってきました。気温などの情報が含まれていることが分かります。

```json
[
    {
        "name": "Remo mini",
        "id": "XXXX",
        "created_at": "2021-08-05T07:32:03Z",
        "updated_at": "2021-09-21T12:42:43Z",
        "mac_address": "XX:XX:XX:XX:XX:XX",
        "serial_number": "XXXXXXXX",
        "firmware_version": "Remo-mini/2.0.62-gf5b5d27",
        "temperature_offset": 0,
        "humidity_offset": 0,
        "users": [
            {
                "id": "XXXX",
                "nickname": "goma",
                "superuser": true
            }
        ],
        "newest_events": {
            "te": {
                "val": 21.9,
                "created_at": "2021-12-05T17:40:51Z"
            }
        }
    }
]
```

# エアコンを操作してみる
では実際に部屋のエアコンを操作するための設定を行います。
リモコンから送信される信号を解析し、それを基にエアコンを操作するためのプログラムを書いていきます。

## リモコンの信号を調べる
部屋のエアコンは富士通製。リモコンの型番はAR-RDC1Jです。
とりあえず冷房、30度、風速自動における信号を見てみます。

```json
{"format":"us","freq":38,"data":[39974,65535,0,34630,3264,1675,399,435,405,438,419,1235,403,443,399,1257,376,459,382,466,398,442,399,1258,407,1242,381,464,375,468,398,441,401,1255,398,1259,397,436,382,466,396,439,405,437,403,436,384,464,398,442,399,443,398,440,399,443,398,438,384,460,376,467,398,1259,398,435,381,463,401,441,402,444,372,465,402,441,373,461,381,1280,398,444,398,442,399,441,400,438,378,1279,377,1281,398,1257,402,1255,407,1245,401,1254,400,1259,397,1258,399,442,373,461,380,1282,398,443,408,427,404,440,378,461,380,459,380,463,403,441,376,463,400,1256,411,1244,402,446,395,440,375,1282,401,433,415,423,386,462,378,469,372,1277,401,1256,400,1256,377,1281,397,436,418,419,387,466,395,445,374,466,399,440,411,420,411,434,416,428,404,438,379,466,373,461,404,438,405,434,418,420,417,432,377,466,398,441,377,461,403,437,404,438,378,465,375,470,373,466,397,437,404,438,406,433,384,467,396,441,376,467,375,463,378,461,402,440,379,466,373,466,400,439,399,439,380,459,382,461,403,444,397,446,371,465,378,1278,398,436,382,1279,401,442,376,466,375,461,376,1281,400,1256,377,466,398,436,382,462,413,1241,405,1254,401]}
```
...はい、さっぱり分かりません！[**ここ**](http://elm-chan.org/docs/ir_format.html)に書いてあることを参考にしようと思っていましたが、どうやらどのフォーマットでも無さそうです。
仕方ないので勘を頼りに読み解いていきます。

いくつか試して結果を眺めた結果、停止状態から起動したときは先頭に決まった長さの信号が付与されることや、highの状態がだいたい[380, 1270]、lowが[380, 460]であることが分かりました。
ということで、ここからは信号をデコードしていきます。以下のプログラムを用いて読みやすい16進数の形にします。
信号の長さは固定なので、起動用のシグナルは無視して2進数に変換します。
また、ビットの並びが逆順のようなので、順番をひっくり返してから16進数に変換します。

```python:decode.py
import json

with open('data_set.json', 'r') as f:
  obj_set = json.load(f)

for key, i in obj_set.items():
  code = i['data']
  data = []
  
  if len(code) >= 263:
    data1 = code[7::2]
  else:
    data1 = code[3::2]
  data = list(map(lambda x: str(x // 1200), data1))
  data = list(reversed(data))

  data = [ "".join(data[i:i+4]) for i in range(0,len(data)-3,4) ]
  data = [ int(e,2) for e in data ]
  data = [ format(e , 'x') for e in data ]
  data = "".join(data)

  print(key + "\n" + data)
```
先程の信号をこのプログラムでデコードすると、以下のような結果が得られます。

```
c6280000000001e13009fe1010006314
```
先程よりはだいぶ読みやすくなりました。

その後、様々な信号を比較し、以下のような結果を得ました。桁は16進表記の左から1としています。
なお、冷房と暖房及び風速の変更を目的とするため、それ以外のパターンは解析していません。

| 桁 | 説明 |
| ----------- | ---- |
| 1 | 1桁目 ＋ 15桁目 ＝ *`0xA`*（キャリーは無視）<br>暖房かつ風量が1のときのみ*`0x9`* |
| 2 | 2桁目 ＝ *`0x8`* － 12桁目 － 14桁目 － 16桁目 |
| 3~11 | *`0x280000000`*（固定） |
| 12 | 風速<br>風速1~4 : *`0x4`*~*`0x1`*<br>自動 : *`0x0`* |
| 13 | *`0x0`* （固定） |
| 14 | 冷房 : *`0x1`*&emsp;暖房 : *`0x4`* |
| 15 | 温度<br>16℃~30℃ : *`0x0`*~*`0xE`* |
| 16 | 暖房＆冷房 : *`0x1`*&emsp;風量&温度切替 : *`0x0`* |
| 17~ | *`0x3009FE1010006314`*（固定） |

## 操作用のプログラムを作る
Homebridge-cmd4から実行されるプログラムを作ります。
エアコンの場合、

```cmd
./HeaterCooler.py Set My_HeaterCooler Active 1
```
みたいな感じでプログラムが実行されるので、コマンドライン引数から動作を決定します。
`Get`ならデバイス名より後の**characterristic**(デバイスの状態を表す要素)の値を読み取り、printします。
`Set`ならデバイス名より後の**characterristic**の値をさらにその後の数値に置き換え、反映させます。

以下は呼び出されるプログラムの中身です。
Pythonで雑に作ったので読みづらいのは許してください。

```python3:HeaterCooler
import sys
import os
import pathlib
import json
import requests
import math
import ac_signal

status = {
  "CurrentTemperature": 25,
  # Minimum Value 0
  # Maximum Value 100
  # Step Value 0.1

  "SwingMode": 0,
  # 0 - "Swing disabled"
  # 1 - "Swing enabled"

  "RotationSpeed": 100,
  # Minimum Value: 0
  # Maximum Value: 100
  # Step Value: 1
  # Unit: percentage

  "TargetHeaterCoolerState": 2,
  # Valid Values
  # 0 - AUTO
  # 1 - HEAT
  # 2 - COOL

  "HeatingThresholdTemperature": 20,
  # Minimum Value: 0
  # Maximum Value: 25
  # Step Value: 0.1
  # Unit: celcius

  "CoolingThresholdTemperature": 27,
  # Minimum Value: 10
  # Maximum Value: 35
  # Step Value: 0.1
  # Unit: celcius

  "CurrentHeaterCoolerState": 3,
  # 0 - INACTIVE
  # 1 - IDLE
  # 2 - HEATING
  # 3 - COOLING

  "Active": 0
  # 0 - "Inactive"
  # 1 - "Active"
}

# acDataHolderからエアコンの状態を読み取る。気温はCloudAPIから取得
def get_status():
  global status

  with open('/home/homebridge/Cmd4Scripts/acDataHolder.json', 'r') as f:
    contents = f.read()

  if contents == '':
    save_status()
  else:
    df = json.loads(contents)
    for key, item in df.items():
      status[key] = item

  if sys.argv[3] == 'CurrentTemperature':
    headers = {
      'Authorization': 'Bearer <token>',    # NatureRemoのトークン
    }
    response = requests.get('https://api.nature.global/1/devices', headers=headers)
    data = response.json()

    try:
      status['CurrentTemperature'] = data[0]['newest_events']['te']['val']
    except KeyError:
      return

    save_status()

# 状態を変更し、statusを書き換える
def set_status():
  global status
  chara = sys.argv[3]
  value = float(sys.argv[4])
  
  if chara == 'HeatingThresholdTemperature':
    if value > 30:
      value = 30
    elif value < 16:
      value = 16
  elif chara == 'CoolingThresholdTemperature':
    if value > 30:
      value = 30
    elif value < 18:
      value = 18
  elif chara == 'RotationSpeed':
    if value < 10:
      value = 0
    elif value < 30:
      value = 20
    elif value < 50:
      value = 40
    elif value < 70:
      value = 60
    elif value < 90:
      value = 80
    else:
      value = 100

  value = math.ceil(value)

  if status[chara] != value:
    status[chara] = value

    if status['Active'] == 1:
      if status['TargetHeaterCoolerState'] == 1:
        status['CurrentHeaterCoolerState'] = 2
      elif status['TargetHeaterCoolerState'] == 2:
        status['CurrentHeaterCoolerState'] = 3
      else:
        status['CurrentHeaterCoolerState'] = 1
    else:
      status['CurrentHeaterCoolerState'] = 0

    save_status()
    ac_signal.send(status, chara)

# statusをacDataHolderに保存する
def save_status():
  global status
  with open('/home/homebridge/Cmd4Scripts/acDataHolder.json', 'w') as f:
    json.dump(status, f, indent=2)

if __name__ == "__main__":
  # statusを保存しておくファイルがなければ作る
  if os.path.exists('/home/homebridge/Cmd4Scripts/acDataHolder.json') == False:
    cmd = pathlib.Path('/home/homebridge/Cmd4Scripts/acDataHolder.json')
    cmd.touch()

  get_status()

  if sys.argv[1] == 'Get':
    result = status[sys.argv[3]]
  elif sys.argv[1] == 'Set':
    set_status()
    result = sys.argv[4]

  print(result)
  sys.exit()
```
エアコンの状態はjson形式で保存・更新しています。
`Get`が来たときはjsonから読み取った値を返します。ただ、`CurrentTemperature`の時だけはクラウドAPI経由でNatureRemoの値を取得しています。
`Set`の場合は、変更箇所があることを確認してエアコンに合わせて値を整形。jsonファイルの値を書き換えると同時にNatureRemoから信号を送信する関数を呼び出します。
信号の生成及び送信は以下のプログラムで行っています。

```python3:ac_signal.py
import json
import requests

# リモコン用のシグナルデータ
SIGNAL = {
  'on': [39970, 65535, 0, 34630],
  'leader': [3246, 1670],
  'high': [380, 1270],
  'low': [380, 460],
  'last': 380
}

HEX_CODE = {
  'off': 'fd021010006314',
  'c3_11': '280000000',
  'c13': '0',
  'c17': '3009fe1010006314',
  'mode':[0, 4, 1], # AUTO, HEAT, COOL
  'max_temp': 0xe,
  'type': [0, 1],  # 温度風量, 暖房冷房
  'wind': [4, 4, 3, 2, 1, 0] # 1, 2, 3, 4, AUTO
}

# 停止用の信号を生成
def off_data():
  s_data = conv_data(HEX_CODE['off'])
  return s_data

# statusを反映させた信号を生成
def gen_data(status, t):
  if status['TargetHeaterCoolerState'] == 2:
    temp = status['CoolingThresholdTemperature']
    mode = HEX_CODE['mode'][2]
  elif status['TargetHeaterCoolerState'] == 1:
    temp = status['HeatingThresholdTemperature']
    mode = HEX_CODE['mode'][1]
  else:
    temp = status['CoolingThresholdTemperature']
    mode = HEX_CODE['mode'][2]

  if t == 1:
    s_type = HEX_CODE['type'][1]
  else:
    s_type = HEX_CODE['type'][0]

  wind = HEX_CODE['wind'][status['RotationSpeed'] // 20]

  temp = HEX_CODE['max_temp'] - (30 - temp)
  head = 10 - temp
  if mode == 4 and s_type == 1 and wind == 4:
    head -= 1
  if head < 0:
    head += 16

  check = 8 - wind - mode - s_type
  if check < 0:
    check += 16

  head = format(head, 'x')
  check = format(check, 'x')
  temp = format(temp, 'x')
  mode = format(mode, 'x')
  s_type = format(s_type, 'x')
  wind = format(wind, 'x')

  data = head + check + HEX_CODE['c3_11'] + wind + HEX_CODE['c13'] + mode + temp + s_type + HEX_CODE['c17']
  s_data = conv_data(data)

  return s_data

# データを送信用の形に変換
def conv_data(data):
  s_data = [list(format(int(i, 16), '04b')) for i in list(data)]
  s_data = [x for row in s_data for x in row]

  s_data.reverse()

  s_data = [SIGNAL['high'] if i == '1' else SIGNAL['low'] for i in s_data]
  s_data = [x for row in s_data for x in row]
  s_data.append(SIGNAL['last'])

  return s_data

# 先頭に制御用の信号を追加
def merge_data(s_data, t):
  if t:
    s_data = SIGNAL['on'] + SIGNAL['leader'] + s_data
  else:
    s_data = SIGNAL['leader'] + s_data

  return s_data

# NatureRemoに信号をPOST
def send_data(s_data):
  j_data = {
    "format": "us",
    "freq": 38,
    "data": s_data
  }
  
  headers = {
    'X-Requested-With': 'local',
  }
  requests.post('http://192.168.0.122/messages', headers=headers, json=j_data)

# 信号の生成から送信を行う
def send(status, chara):
  if chara == 'Active':
    if status['Active'] == 0:
      s_data = off_data()
      s_data = merge_data(s_data, 0)
    else:
      s_data = gen_data(status, 1)
      s_data = merge_data(s_data, 1)
  elif status['Active'] == 1:
    s_data = gen_data(status, 0)
    s_data = merge_data(s_data, 0)
  else:
    return
  send_data(s_data)
```
行っている処理は単純で、更新後のエアコンの状態と変更箇所の情報を、先程解析した信号の生成法則に当てはめることで目的の信号を生成しています。
エアコンの状態を基にHEX_CODEを組み合わせて16進数のデータを生成し、それを2進数変換、さらにバイト列を逆順にして赤外線送信用のシグナルに変換します。
すなわちリモコンを解析した時と全く逆の手順で信号を生成しています。
あとはこれをjson形式に変換し、NatureRemoにPOSTします。こちらはローカルAPIを使っています。(クラウドだとたまに不安定なので)

## Homebridge-CMD4からプログラムを呼び出す
作ったプログラムは適当な場所に置いてcmd4から呼び出すようにconfigを変更します。
以下はその一例です。

```json:platforms
{
    "platform": "Cmd4",
    "name": "Cmd4",
    "outputConstants": false,
    "timeout": 1000,
    "interval": 300,
    "QueueTypes": [
        {
            "Queue": "A",
            "QueueType": "Sequential"
        }
    ],
    "accessories": [
        {
            "Type": "HeaterCooler",
            "DisplayName": "エアコン",
            "Name": "HeaterCooler",
            "Active": 0,
            "CurrentHeaterCoolerState": 0,
            "TargetHeaterCoolerState": 2,
            "CurrentTemperature": 25,
            "RotationSpeed": 100,
            "CoolingThresholdTemperature": 27,
            "HeatingThresholdTemperature": 20,
            "TemperatureDisplayUnits": "CELSIUS",
            "Manufacturer": "Fujitsu",
            "Model": "AS-W22B-W",
            "StateChangeResponseTime": 1,
            "Queue": "A",
            "polling": [
                {
                    "Characteristic": "Active"
                },
                {
                    "Characteristic": "TargetHeaterCoolerState"
                },
                {
                    "Characteristic": "CoolingThresholdTemperature",
                    "interval": 60
                },
                {
                    "Characteristic": "HeatingThresholdTemperature",
                    "interval": 60
                },
                {
                    "Characteristic": "CurrentHeaterCoolerState"
                },
                {
                    "Characteristic": "CurrentTemperature",
                    "interval": 120
                },
                {
                    "Characteristic": "RotationSpeed"
                }
            ],
            "State_cmd": "python3 /home/homebridge/Cmd4Scripts/HeaterCooler.py"
        }
    ]
}
```
`Type`でアクセサリの種類を指定し、続けて操作したい**characterristic**を定義しています。呼び出すプログラムは`State_cmd`で指定します。
`polling`には定期的に状態を更新する**characterristic**を追加します。`poling`の中に書いている`interval`はデフォルト値を上書きするため、一部特別な間隔を設定したいものに付与しています。また、プログラムが多重起動するのはファイルを読み書きする関係でマズいので`QueueType`を**Sequential**に設定しています。

以上のように設定することでエアコンをHomeKitに組み込むことができました。

# ついでにシーリングライトも操作する
シーリングライトもリモコン式だったのでこちらもNatureRemoから操作できるようにします。
やってることはエアコンとあまり変わりません。ただ、エアコンと違って状態をまとめて送信するのではなく、操作に対応した信号を逐次送信しているので仕組みは簡単になっています。

```python3:Light.py
import sys
import os
import pathlib
import json
import time
import lt_signal

status = {
  "On": 0,
  # 0 - "On"
  # 1 - "Off"

  "Brightness": 0,
  # Minimum Value: 0
  # Maximum Value: 100
  # Step Value: 1
  # Unit: percentage

  "CurrentBrightness": 23,
  # Minimum Value: 0
  # Maximum Value: 23
  # Step Value: 1

  "CurrentNightBrightness": 5,
  # Minimum Value: 0
  # Maximum Value: 5
  # Step Value: 1

  "CurrentState": 0
  # 0 - "Light"
  # 1 - "NightLight"
}

def main():
  if os.path.exists('/home/homebridge/Cmd4Scripts/ltDataHolder.json') == False:
    cmd = pathlib.Path('/home/homebridge/Cmd4Scripts/ltDataHolder.json')
    cmd.touch()

  get_status()

  if sys.argv[1] == 'Get':
    result = status[sys.argv[3]]
  elif sys.argv[1] == 'Set':
    set_status()
    result = sys.argv[4]

  print(result)

  sys.exit()

def get_status():
  global status
  contents = ''

  with open('/home/homebridge/Cmd4Scripts/ltDataHolder.json', 'r') as f:
    contents = f.read()

  if contents == '':
    save_status()
  else:
    df = json.loads(contents)
    for key, item in df.items():
      status[key] = item

def set_status():
  global status
  chara = sys.argv[3]
  value = int(sys.argv[4])

  if chara == 'On' and value == 0:
    lt_signal.gen_off()
    time.sleep(1)
    status['On'] = 0

  elif chara == 'On' and value == 1 and status['On'] == 0:
    if status['Brightness'] > 25:
      lt_signal.gen_on()
    else:
      lt_signal.gen_night_on()
    time.sleep(3)
    status['On'] = 1

  elif chara == 'Brightness':
    if value > 25:
      if status['CurrentState'] == 1 or status['On'] == 0:
        status['CurrentState'] = 0
        status['On'] = 1
        lt_signal.gen_on()
        time.sleep(3)
      status['CurrentBrightness'] = lt_signal.gen_light_lv(status['CurrentBrightness'], value)

    elif value > 1:
      if status['CurrentState'] == 0 or status['On'] == 0:
        status['CurrentState'] = 1
        status['On'] = 1
        lt_signal.gen_night_on()
        time.sleep(3)
      status['CurrentNightBrightness'] = lt_signal.gen_dark_lv(status['CurrentNightBrightness'], value)

    status['Brightness'] = value

  save_status()

def save_status():
  global status
  with open('/home/homebridge/Cmd4Scripts/ltDataHolder.json', 'w') as f:
    json.dump(status, f, indent=2)

if __name__ == "__main__":
  main()
```

```python3:lt_signal.py
import requests
import time

SIGNAL = {
  "on": [
    3426,1778,388,476,388,477,386,1347,384,1350,385,479,383,1352,379,479,389,476,387,482,380,1348,388,481,381,481,386,1346,386,478,383,1353,380,481,385,1351,384,474,388,477,385,1348,388,483,380,482,381,481,386,477,386,1347,386,480,383,1349,386,1346,383,479,386,1347,385,480,385,479,387,474,392,474,389,1346,386,480,382,482,385,1348,384,479,383,479,390,65535,0,9083,3417,1791,376,481,386,474,387,1351,384,1348,382,482,382,1347,388,480,381,485,380,486,378,1345,386,481,383,477,388,1350,381,483,381,1347,388,481,382,1347,384,476,390,480,382,1351,382,481,386,476,385,477,388,479,388,1348,384,480,380,1349,385,1349,382,476,389,1349,383,484,382,480,383,476,391,474,390,1346,383,486,381,481,381,1348,383,479,386,481,385
  ],
  "night_on": [
    3418,1779,385,480,382,483,381,1348,384,1350,384,476,388,1351,380,486,381,481,381,482,386,1346,386,474,387,485,382,1349,380,486,382,1346,382,479,386,1352,379,484,382,482,384,1345,383,480,387,475,387,485,382,479,384,481,383,1351,382,1346,384,1351,384,480,385,1351,379,481,383,481,384,1348,383,1353,378,1352,383,482,383,474,391,1347,384,480,385,479,384,65535,0,9083,3417,1786,379,486,381,479,384,1351,385,1348,381,483,384,1349,384,476,388,477,386,480,389,1350,379,483,382,486,376,1352,379,479,389,1350,380,485,382,1350,382,481,384,481,381,1351,384,482,379,484,382,482,381,480,385,483,382,1350,381,1353,379,1355,379,487,377,1351,383,481,380,483,381,1352,382,1349,385,1347,384,481,385,483,382,1350,380,479,390,479,383
  ],
  "off": [
    3417,1786,382,479,382,488,376,1349,385,1349,384,478,386,1353,376,485,381,482,381,481,386,1350,382,476,385,480,385,1348,383,481,384,1350,384,481,381,1352,379,484,383,480,382,1351,380,487,380,476,387,482,382,480,384,1353,380,1348,384,1352,381,1353,376,485,379,1350,381,484,383,476,387,481,385,1352,377,1353,378,483,382,479,385,1352,388,474,386,481,382,65535,0,9073,3420,1787,384,480,382,481,382,1354,376,1350,383,486,379,1351,382,482,381,484,380,479,388,1351,378,481,385,484,379,1350,386,480,381,1353,378,479,388,1349,384,481,382,485,382,1349,380,481,383,485,379,486,381,480,385,1350,381,1351,380,1351,380,1350,385,485,379,1351,381,481,383,481,386,483,379,1353,383,1347,386,481,381,481,384,1353,383,480,383,484,382
  ],
  "up": [
    3416,1786,381,487,376,486,379,1350,384,1350,380,481,383,1354,377,486,382,482,380,484,382,1351,381,482,383,480,384,1353,378,479,388,1346,387,477,383,1353,382,480,386,477,383,1350,384,480,386,482,381,479,388,475,386,480,383,1350,383,476,393,1346,381,487,379,1346,384,477,387,476,388,1353,383,1344,387,480,384,474,387,479,386,1349,386,482,378,483,386
  ],
  "up_continue": [
    65535,0,9084,3419,1782,388,477,387,480,384,1346,408,1323,386,479,383,1348,388,476,388,474,390,475,389,1349,384,480,385,479,418,1311,411,449,413,1323,422,442,410,1322,409,455,409,454,411,1319,423,440,422,446,408,454,410,454,413,451,413,1321,411,453,390,1343,390,476,387,1348,385,474,390,474,389,1348,383,1350,385,475,412,453,413,451,424,1312,385,480,386,478,388
  ],
  "down": [
    3417,1784,381,484,383,482,381,1353,378,1351,382,477,390,1348,383,483,382,481,381,483,382,1350,381,481,386,483,380,1349,382,480,385,1351,384,476,393,1344,382,485,377,485,381,1348,385,481,382,477,387,481,383,482,382,1352,383,1346,383,477,390,1349,383,480,382,1350,383,474,391,474,389,483,378,1352,381,487,381,476,386,480,383,1351,381,482,381,484,384
  ],
  "down_continue": [
    65535,0,9082,3420,1787,380,481,382,484,382,1352,377,1355,379,481,383,1354,382,482,380,483,383,481,382,1357,381,481,383,481,380,1355,378,481,383,1355,383,483,377,1354,381,483,380,490,377,1352,381,481,383,484,381,484,380,483,382,1351,383,1352,392,471,380,1356,377,488,379,1350,381,483,383,485,382,483,379,1352,383,487,377,481,383,480,383,1355,379,486,382,482,382
  ]
}

def gen_off():
  send_data(SIGNAL['off'])

def gen_on():
  send_data(SIGNAL['on'])

def gen_night_on():
  send_data(SIGNAL['night_on'])

def gen_light_lv(last_value, value):
  value = (value - 29) // 3
  if value < 0:
    value = 0

  diff = value - last_value

  if diff == 0:
    return value
  elif diff > 0:
    s_data = SIGNAL['up'].copy()
    add = SIGNAL['up_continue'].copy()
    if value == 23:
      diff += 1
    merge_data(s_data, add, diff)
  else:
    s_data = SIGNAL['down'].copy()
    add = SIGNAL['down_continue'].copy()
    if value == 0:
      diff -= 1
    merge_data(s_data, add, -diff)

  return value

def gen_dark_lv(last_value, value):
  value = (value -  1) // 4
  if value > 5:
    value = 5

  diff = value - last_value

  if diff == 0:
    return value
  elif diff > 0:
    s_data = SIGNAL['up'].copy()
    add = SIGNAL['up_continue'].copy()
    if value == 5:
      diff += 1
    merge_Ndata(s_data, add, diff)
  else:
    s_data = SIGNAL['down'].copy()
    add = SIGNAL['down_continue'].copy()
    if value == 0:
      diff -= 1
    merge_Ndata(s_data, add, -diff)

  return value

def merge_data(data, add_data, n):
  lists = [6] * (n // 6)
  if n % 6:
    lists.append(n % 6)
  
  for i in lists:
    t_data = data.copy()
    for j in range(i):
      t_data.extend(add_data)
    send_data(t_data)
    time.sleep(0.5)

def merge_Ndata(data, add_data, n):
  for i in range(n):
    t_data = data.copy()
    t_data.extend(add_data)
    send_data(t_data)
    time.sleep(0.5)

def send_data(s_data):
  j_data = {
    "format": "us",
    "freq": 37,
    "data": s_data
  }
  
  headers = {
    'X-Requested-With': 'local',
  }
  requests.post('http://192.168.0.122/messages', headers=headers, json=j_data)
```
configも`accessories`に以下を追加しただけです。

```json
{
    "Type": "Lightbulb",
    "DisplayName": "ライト",
    "On": 0,
    "Brightness": 0,
    "Name": "light",
    "Manufacturer": "Panasonic",
    "Model": "HH-CD0818D",
    "StateChangeResponseTime": 1,
    "timeout": 1000,
    "Queue": "A",
    "polling": [
        {
            "Characteristic": "On"
        },
        {
            "Characteristic": "Brightness"
        }
    ],
    "State_cmd": "python3 /home/homebridge/Cmd4Scripts/Lightbulb.py"
}
```

# おわりに
前編ではHomebridgeの導入といくつかのプラグインの紹介、後編ではHomebridge-CMD4を使ってエアコンとシーリングライトの操作を実現しました。

Homebridgeには便利なプラグインが沢山公開されているので、やり方を工夫すればどんなものでもHomeKit化できます。特に今回紹介したHomebridge-CMD4は非常に自由度が高いのでおすすめです。私は特定の気温、特定の時間で自動で冷暖房を行うようにしているので、布団から出られないほど寒い日でも快適に起きれるようになりました！
IoTに興味があって、iPhoneを使っている方は一度試してみてはいかかでしょうか。