6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-12-07

はじめに

この記事は、SLP KBIT AdventCalendar2021 7日目の記事です。
PCや家電、スマートプラグなんかをiPhoneやMacから操作できるようにする試みの後編です。

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

一連の流れ

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

Homebridge-CMD4について

Homebridge-CMD4とは、ホームappからの操作を行った際に任意のコマンドを実行するプラグインです。
HomeKitが対応しているアクセサリのほとんどを網羅しているため、複雑な操作が可能です。
https://github.com/ztalbot2000/homebridge-cmd4
image.png
どんなアクセサリが使用できるかは以下にざっくり書いてあります。
https://ztalbot2000.github.io/homebridge-cmd4/autoGenerated/CMD4_AccessoryDescriptions.html

NatureRemoのAPIについて

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

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

curl -X GET 'http://<IP or HostName>/messages' -H 'X-Requested-With: curl' -H 'accept: application/json'

また、NatureRemoの状態を取得する場合は以下のコマンドを打つとjson形式で結果が返ってきます。

curl -X GET 'https://api.nature.global/1/devices' -H 'Authorization: Bearer <Token>'

NatureRemo miniで試したところ、このような結果が返ってきました。気温などの情報が含まれていることが分かります。

[
    {
        "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度、風速自動における信号を見てみます。

{"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]}

...はい、さっぱり分かりません!ここに書いてあることを参考にしようと思っていましたが、どうやらどのフォーマットでも無さそうです。
仕方ないので勘を頼りに読み解いていきます。

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

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(キャリーは無視)
暖房かつ風量が1のときのみ*0x9*
2 2桁目 = 0x8 - 12桁目 - 14桁目 - 16桁目
3~11 0x280000000(固定)
12 風速
風速1~4 : 0x4~0x1
自動 : 0x0
13 0x0 (固定)
14 冷房 : 0x1 暖房 : 0x4
15 温度
16℃~30℃ : 0x0~0xE
16 暖房&冷房 : 0x1 風量&温度切替 : 0x0
17~ 0x3009FE1010006314(固定)

操作用のプログラムを作る

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

./HeaterCooler.py Set My_HeaterCooler Active 1

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

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

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から信号を送信する関数を呼び出します。
信号の生成及び送信は以下のプログラムで行っています。

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を変更します。
以下はその一例です。

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はデフォルト値を上書きするため、一部特別な間隔を設定したいものに付与しています。また、プログラムが多重起動するのはファイルを読み書きする関係でマズいのでQueueTypeSequentialに設定しています。

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

ついでにシーリングライトも操作する

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

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()
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に以下を追加しただけです。

{
    "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を使っている方は一度試してみてはいかかでしょうか。

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?