【猫監視システムへの道】2.SwitchBotをPCから操作する
自宅のスマート家電を一括操作したい
前提
我が家にあるスマート家電と、スマート家電から操作している家電の一覧です。
メーカー | 商品名/型番 | 備考 |
---|---|---|
Amazon | Amazon echo 第3世代 | |
SwitchBot | Hub mini | |
SwitchBot | 湿温度計 | |
xydled | LEDスポットライトGU10 RGBW | RGB調光対応のLEDライト。リビング用。 |
SONY | Bravia KJ-43X8000H | ほぼYouTubeを見るだけに使っているTV。リビング用。 |
ダイキン | AN22XEBKS-W | エアコン。リビング用と寝室用の2台。 |
ECOVACS | DEEBOT N8 PRO+ | ロボット掃除機 |
Tp-link | Tapo C210 V2 | ネットワークカメラ。New!! |
この中では、Amazon echoが一番使っていません。
SwitchBotとの連携ができない期間が長かったのと、
Alexaに「すみません。よくわかりませんでした」と返されるたびにイラっとして
今ではほとんど使わなくなってしまいました。
寝室の照明が赤外線リモコン対応ではないので、
おそらく他の家で一番使われているであろう「Alexaおはよう(おやすみ)」すら
我が家では出番がありません。
ちなみに、猫のためにリビングのエアコンは24h稼働ですが、
私自身はエアコンの28度も寒いと感じるので、寝室のエアコンはほぼ使いません。
そのような状況なので、SwitchBotの操作だけなら
スマホのSwitchBotアプリや、(たぶん)Alexaでも十分なのですが、
カメラが届く前に、まずはPCからSwitchBotを制御するツールを作ってみる事にしました。
実現したいこと
PC上で下記を実現したい。
- SwitchBotの湿温度計から、5分毎に温度と湿度を取得
- エアコンのON/OFF操作
- TVのON/OFF操作、音量の増減操作
- 照明のON/OFF操作、明るさの増減操作
- これらを1つのGUI上にまとめる
参考サイト
環境
- Windows11
- Python3.8.9(64bit)
- SwitchBot Appのバージョン: 7.3
- SwitchBot Hub miniのファームウエアバージョン: V5.4-3.8
事前準備
-
Pythonのインストール
別件でインストール済だった、Python3.8.9(64bit)をそのまま使用。 -
Pythonのライブラリをインストール
pip install requests==2.31.0
-
トークンを取得
参考にしたサイトと、若干表記が異なっていた。
※アプリのバージョンの違いによるものだと思われる。
(1)SwitchBot Appを起動。
(2)「プロフィール」の「設定」を開く
(3)「アプリバージョン」部分を10回タップ。
(4)「デベロッパーオプション」が表示されるので開く。
(5)トークンを取得。 -
SwitchBotのアプリで、エアコンや照明操作用のデバイスを登録
(1)SwitchBot Appを起動。
(2)「Home」の「+」
→「デバイスの追加」
→「赤外線リモコン」
→エアコン、TV、照明などを選択してリモコンを学習させる。
(3)登録したデバイスの動作テストを行う。 -
デバイスIDの取得
こんな感じのスクリプトを書いて、コマンドプロンプトから実行。
# coding: utf-8
import json
import requests
#-------------------------------
# SwitchBot Hub miniとの接続設定
#-------------------------------
TOKEN = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
API_HOST = 'https://api.switch-bot.com'
class SwitchBot:
def __init__(self, token, host):
self.token = token
self.host = host
self.devlist_url = f'{self.host}/v1.0/devices'
self.headers = {
'Authorization': self.token,
'Content-Type': 'application/json; charset=utf8'
}
def print_device_list(self):
"""
デバイス一覧を出力する
"""
devices = self.get_device_list()
# SwitchBot関連のデバイスリストを取得する。
self.pub_device_list = devices.get('deviceList')
# 赤外線操作対象のデバイスリストを取得する。
self.ir_device_list = devices.get('infraredRemoteList')
# 湿温度計の情報を取得
print('デバイスID一覧')
for device in self.pub_device_list:
print(device)
print('\n赤外線リモコン操作ID一覧')
# 赤外線操作対象
for device in self.ir_device_list:
print(device)
def get_device_list(self):
'''
デバイスIDの一覧を取得する
'''
try:
return self.get_request(self.devlist_url)['body']
except:
print('デバイスIDを検出できませんでした')
return None
def get_request(self, url):
try:
res = requests.get(url, headers=self.headers, timeout=2.0)
data = res.json()
if data['message'] == 'success':
return res.json()
else:
print(data['statusCode'])
print(data['message'])
except Timeout:
print('コマンド送信がタイムアウトしました')
except Exception as err:
print(err)
return {}
if __name__ == '__main__':
sb = SwitchBot(TOKEN, API_HOST)
sb.print_device_list()
結果
※デバイスID部分は置き換えています。
デバイスID一覧
{'deviceId': 'ハブミニのデバイスID', 'deviceName': 'ハブミニ 30', 'deviceType': 'Hub Mini', 'enableCloudService': False, 'hubDeviceId': ''}
{'deviceId': '温湿度計のデバイスID', 'deviceName': '温湿度計 4D', 'deviceType': 'Meter', 'enableCloudService': True, 'hubDeviceId': 'ハブミニのデバイスID'}
赤外線リモコン操作ID一覧
{'deviceId': '赤外線リモコン操作ID', 'deviceName': 'リビングライト', 'remoteType': 'Light', 'hubDeviceId': 'ハブミニのデバイスID'}
{'deviceId': '赤外線リモコン操作ID', 'deviceName': 'テレビ', 'remoteType': 'TV', 'hubDeviceId': 'ハブミニのデバイスID'}
{'deviceId': '赤外線リモコン操作ID', 'deviceName': 'エアコン冷房28度', 'remoteType': 'DIY Air Conditioner', 'hubDeviceId': 'ハブミニのデバイスID'}
{'deviceId': '赤外線リモコン操作ID', 'deviceName': 'エアコンドライ', 'remoteType': 'DIY Air Conditioner', 'hubDeviceId': 'ハブミニのデバイスID'}
{'deviceId': '赤外線リモコン操作ID', 'deviceName': 'エアコン暖房25度', 'remoteType': 'DIY Air Conditioner', 'hubDeviceId': 'ハブミニのデバイスID'}
{'deviceId': '赤外線リモコン操作ID', 'deviceName': 'Alexa', 'remoteType': 'Others', 'hubDeviceId': 'ハブミニのデバイスID'}
SwitchBot制御用GUI作成
ファイル構成
ファイル名 | 用途 |
---|---|
main.py | GUIデザイン部分 |
util\switch_bot.py | SwitchBotとの通信機能 |
util\sb_command.py | SwitchBot制御用コマンド |
コマンド定義ファイル
※デバイスID部分は要置き換え。
# coding: utf-8
#-------------------------------
# デバイスID
#-------------------------------
DIC_DIV_ID = {
'Hub Mini': 'ハブミニのデバイスID',
'Meter' : '湿温度計のデバイスID',
}
#-------------------------------
# 基本コマンド設定
#-------------------------------
# 電源ON用コマンド。TV、照明、エアコン等で共通。
CMD_TURN_ON = {
'command' : 'turnOn',
'parameter' : 'default',
'commandType' : 'command'
}
# 電源ON用コマンド。TV、照明、エアコン等で共通。
CMD_TURN_OFF = {
'command' : 'turnOff',
'parameter' : 'default',
'commandType' : 'command'
}
# エアコン設定
# 下記説明を元にしたコマンドを作成すると、160(No this command)が返るため、
# TVや照明操作時と同じコマンドを送信している。
# https://github.com/OpenWonderLabs/SwitchBotAPI#infrared-remote-device-example
# https://github.com/OpenWonderLabs/SwitchBotAPI#command-set-for-virtual-infrared-remote-devices
CMD_AIR_CON_COOL28 = CMD_TURN_ON
CMD_AIR_CON_DRY = CMD_TURN_ON
CMD_AIR_CON_HOT25 = CMD_TURN_ON
CMD_AIR_CON_OFF = CMD_TURN_OFF
# 照明操作
CMD_LIGHT_UP = {
'command' : 'brightnessUp',
'parameter' : 'default',
'commandType': 'command'
}
CMD_LIGHT_DOWN = {
'command' : 'brightnessDown',
'parameter' : 'default',
'commandType': 'command'
}
# TV操作
CMD_TV_VUP = {
'command' : 'volumeAdd',
'parameter' : 'default',
'commandType': 'command'
}
CMD_TV_VDOWN = {
'command' : 'volumeSub',
'parameter' : 'default',
'commandType': 'command'
}
#-------------------------------
# ボタンクリック時の引数と、デバイスID、コマンドを紐付ける
#-------------------------------
DIC_CMD = {
# エアコン
'air_cool28': ('赤外線リモコン操作ID', CMD_AIR_CON_COOL28),
'air_dry' : ('赤外線リモコン操作ID', CMD_AIR_CON_DRY),
'air_hot25' : ('赤外線リモコン操作ID', CMD_AIR_CON_HOT25),
'air_off' : ('赤外線リモコン操作ID', CMD_AIR_CON_OFF),
# 照明操作
'light_on' : ('赤外線リモコン操作ID', CMD_TURN_ON),
'light_off' : ('赤外線リモコン操作ID', CMD_TURN_OFF),
'light_up' : ('赤外線リモコン操作ID', CMD_LIGHT_UP),
'light_down': ('赤外線リモコン操作ID', CMD_LIGHT_DOWN),
# TV操作
'tv_on' : ('赤外線リモコン操作ID', CMD_TURN_ON),
'tv_off' : ('赤外線リモコン操作ID', CMD_TURN_OFF),
'tv_vup' : ('赤外線リモコン操作ID', CMD_TV_VUP),
'tv_vdown' : ('赤外線リモコン操作ID', CMD_TV_VDOWN)
}
エアコンのコマンドに関しては、SwitchBot APIの赤外線操作コマンドの説明や、SwitchBot APIの記述例だと下記のようになっていますが、
{
"command": "setAll",
"parameter": "26,1,3,on",
"commandType": "command"
}
私の環境ではレスポンスコード160(No this command)が返りました。
SwitchBot制御部分
前出のget_device_list.pyに手を加えたもの。
# coding: utf-8
import json
import requests
import util.sb_command as cmd
class SwitchBot():
def __init__(self, token, host):
self.token = token
self.host = host
self.devlist_url = f'{self.host}/v1.0/devices'
self.headers = {
'Authorization': self.token,
'Content-Type': 'application/json; charset=utf8'
}
def check_meter(self):
'''
SwitchBotの湿温度計チェック
'''
try:
status = self.get_device_status(cmd.DIC_DIV_ID.get('Meter'))
return ('{0:4.1f}'.format(status.get('temperature')),
'{0:4.1f}'.format(status.get('humidity')))
except Exception as e:
print(f"Request error: {e}")
return (None, None)
def send_ir(self, key):
device_id = cmd.DIC_CMD.get(key)[0]
params = cmd.DIC_CMD.get(key)[1]
url = f'{self.host}/v1.0/devices/{device_id}/commands'
self.post_request(url, params)
def get_device_status(self, device_id):
'''
Switchbotデバイスのステータスを取得する。
'''
url = f"{self.host}/v1.0/devices/{device_id}/status"
try:
r = requests.get(url, headers=self.headers)
r.raise_for_status()
except HTTPError as e:
raise HTTPError(f"HTTP error: {e}")
except RequestException as e:
raise RequestException(e)
else:
return r.json()["body"]
def post_request(self, url, params):
try:
print(json.dumps(params))
res = requests.post(url, data=json.dumps(params), headers=self.headers)
data = res.json()
if data['message'] == 'success':
print('-> Success')
return res.json()
else:
print(data['statusCode'])
print(data['message'])
except Timeout:
print('コマンド送信がタイムアウトしました')
except Exception as err:
print(err)
return {}
GUI
# coding: utf-8
import datetime
import json
import os
import sys
import time
import tkinter as tk
import tkinter.ttk as ttk
import requests
import util.switch_bot ad SwitchBot
import util.sb_command as cmd
#-------------------------------
# フォント(DSEG v046を使用)
#-------------------------------
# https://www.keshikan.net/fonts.html
FONT_DIG = 'DSEG14 Classic Mini'
#-------------------------------
# SwitchBot Hub miniとの接続設定
#-------------------------------
SB_TOKEN = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
SB_API_HOST = 'https://api.switch-bot.com'
class Application(tk.Frame):
def __init__(self, master = None):
super().__init__(master)
self.sb = SwitchBot(SB_TOKEN, SB_API_HOST)
self.temperature, self.humidity = self.sb.check_meter()
#-------------------------------
# Window作成
#-------------------------------
self.pack()
master.title('Smart Home Controller')
master.geometry('800x180')
master.resizable(False, False) # リサイズ不可に設定
#-------------------------------
# Tabを定義
#-------------------------------
frame_top = tk.Frame(master, width=800, height=180)
frame_top.propagate(False)
frame_top.place(x=0, y=0)
# タブのインスタンスを作成
nb = ttk.Notebook(master, width=800, height=125)
nb.place(x=0, y=40)
tab1 = tk.Frame(nb)
# タブに表示する文字列の設定
nb.add(tab1, text='リビング', padding=4)
#-------------------------------
# Tab1のFrameを定義
#-------------------------------
# tab1
frame1 = tk.Frame(tab1, width=130, height=95, borderwidth=1, relief='solid', padx=5, pady=3)
frame2 = tk.Frame(tab1, width=250, height=95, borderwidth=1, relief='solid', padx=5, pady=3)
frame3 = tk.Frame(tab1, width=180, height=95, borderwidth=1, relief='solid', padx=5, pady=3)
frame4 = tk.Frame(tab1, width=180, height=95, borderwidth=1, relief='solid', padx=5, pady=3)
# Frameサイズを固定
frame1.propagate(False)
frame2.propagate(False)
frame3.propagate(False)
frame4.propagate(False)
# Frameを配置
frame1.place(x= 0, y= 0)
frame2.place(x=145, y= 0)
frame3.place(x=410, y= 0)
frame4.place(x=605, y= 0)
#-------------------------------
# Tab1のWidget配置
#-------------------------------
# frame_top: 日付と時刻(YYYY/MM/DD hh:mm)
dt = datetime.datetime.now()
self.label_ta = tk.Label(frame_top,
text=self._clock(),
font=('メイリオ', 14, 'bold'))
self.label_ta.place(x=0, y=0)
# 繰り返し処理(1000ms=1secごと)
self.label_ta.after(1000, self._update_clock)
# frame1: 湿温度計の情報を表示
label_1a = tk.Label(frame1, text='湿温度計', font=('メイリオ', 12))
label_1a.place(x=0, y=0)
self.label_1b = tk.Label(frame1,
text=self.temperature,
fg='green',
bg='lightgray',
font=(FONT_DIG, 12))
self.label_1b.place(x=20, y=30)
label_1c = tk.Label(frame1, text='°C'.format(28.4), font=('メイリオ', 10))
label_1c.place(x=70, y=30)
self.label_1d = tk.Label(frame1,
text=self.humidity,
fg='green',
bg='lightgray',
font=(FONT_DIG, 12))
self.label_1d.place(x=20, y=60)
# 繰り返し処理(5分ごと)
self.label_1d.after(5 * 60 * 1000, self._update_meter)
label_1e = tk.Label(frame1, text='%'.format(81.2), font=('メイリオ', 10))
label_1e.place(x=70, y=60)
# frame2: エアコン操作
label_2a = tk.Label(frame2, text='エアコン', font=('メイリオ', 12))
label_2a.place(x=0, y=0)
button_2a = tk.Button(frame2, text='冷房28度', width='8', command=lambda:self._click_btn_sw('air_cool28'))
button_2a.place(x=20, y=30)
button_2b = tk.Button(frame2, text='ドライ', width='8', command=lambda:self._click_btn_sw('air_dry'))
button_2b.place(x=90, y=30)
button_2b = tk.Button(frame2, text='暖房25度', width='8', command=lambda:self._click_btn_sw('air_hot25'))
button_2b.place(x=160, y=30)
button_2c = tk.Button(frame2, text='OFF', width='8', command=lambda:self._click_btn_sw('air_off'), bg='red')
button_2c.place(x=20, y=60)
# frame3: リビングの照明操作
label_3a = tk.Label(frame3, text='照明', font=('メイリオ', 12))
label_3a.place(x=0, y=0)
button_3a = tk.Button(frame3, text='ON', width='8', command=lambda:self._click_btn_sw('light_on'))
button_3a.place(x=20, y=30)
button_3b = tk.Button(frame3, text='明るさ▲', width='8', command=lambda:self._click_btn_sw('light_up'))
button_3b.place(x=90, y=30)
button_3c = tk.Button(frame3, text='OFF', width='8', command=lambda:self._click_btn_sw('light_off'), bg='red')
button_3c.place(x=20, y=60)
button_3d = tk.Button(frame3, text='明るさ▼', width='8', command=lambda:self._click_btn_sw('light_down'))
button_3d.place(x=90, y=60)
# frame4: TV操作
label_4a = tk.Label(frame4, text='TV', font=('メイリオ', 12))
label_4a.place(x=0, y=0)
button_4a = tk.Button(frame4, text='ON', width='8', command=lambda:self._click_btn_sw('tv_on'))
button_4a.place(x=20, y=30)
button_4b = tk.Button(frame4, text='Vol▲', width='8', command=lambda:self._click_btn_sw('tv_vup'))
button_4b.place(x=90, y=30)
button_4b = tk.Button(frame4, text='OFF', width='8', command=lambda:self._click_btn_sw('tv_off'), bg='red')
button_4b.place(x=20, y=60)
button_4d = tk.Button(frame4, text='Vol▼', width='8', command=lambda:self._click_btn_sw('tv_vdown'))
button_4d.place(x=90, y=60)
def _click_btn_sw(self, key=None):
"""
SwitchBot操作用ボタンのクリック時に呼ばれる関数。
"""
print('push {} botton'.format(key))
self.sb.send_ir(key)
def _clock(self):
'''
日付と時刻(YYYY/MM/DD hh:mm:ss)
'''
dt = datetime.datetime.now()
return dt.strftime('%Y/%m/%d(%a) %H:%M:%S')
def _update_clock(self):
'''
日付と時刻を1秒ごとに更新する
'''
self.label_ta.configure(text=self._clock())
self.label_ta.after(1000, self._update_clock)
def _update_meter(self):
'''
湿温度計を5分ごとに更新する
'''
temp, hum = self.sb.check_meter()
self.label_1b.configure(text=temp)
self.label_1d.configure(text=hum)
print(f'湿温度計を更新しました。{temp}度 {hum}%')
self.label_1d.after(5 * 60 * 1000, self._update_meter)
if __name__ == "__main__":
root = tk.Tk()
app = Application(master = root)
app.mainloop()
結果
実現したいことがすべて実現できました!
Discussion