[C++] 375 SwitchBot管理アプリの製作 その2 GUIの作成 wxWidgets

[Mac M2 Pro 12CPU, Sonoma 14.5, wxWidgets 3.2.5]

wxWidgetsのコードはFLTKとはまた違った独特のクセがあります。

ChatGPTにGUIのコードを書いてもらいました。Swiftと遜色ない洗練された外観です。

やはりGUIは座標を使うのが楽です。SwiftUIやJavaのSwingは書きにくくて苦手です。

#include <wx/wx.h>
#include <wx/slider.h>
#include <wx/stattext.h>
#include <wx/choice.h>
#include <wx/panel.h>
#include <wx/sizer.h>
#include <wx/image.h>
#include <wx/bitmap.h>
#include "functions.h"

class MyFrame : public wxFrame
{
public:
    MyFrame() : wxFrame(NULL, wxID_ANY, "SwitchBot Manager", wxDefaultPosition, wxSize(600, 400))
    {
        // パネルを左右に分割
        wxPanel* leftPanel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(300, 400));
        wxPanel* rightPanel = new wxPanel(this, wxID_ANY, wxPoint(300, 0), wxSize(300, 400));

        // 背景色の設定
        leftPanel->SetBackgroundColour(wxColour("#00CED1"));
        rightPanel->SetBackgroundColour(wxColour("#4B0082"));

        // 下部のパネルを追加して背景色を設定
        wxPanel* bottomPanel = new wxPanel(leftPanel, wxID_ANY, wxPoint(0, 100), wxSize(300, 300));
        bottomPanel->SetBackgroundColour(wxColour("#008B8B"));

        // CSVからのデータ読み込み
        std::vector<double> data = ReadCsvData();

        // 室温と湿度
        new wxStaticText(leftPanel, wxID_ANY, "室温", wxPoint(10, 10), wxSize(80, 20));
        new wxStaticText(leftPanel, wxID_ANY, wxString::Format("%.1f", data[0]), wxPoint(100, 10), wxSize(80, 20));
        new wxStaticText(leftPanel, wxID_ANY, "湿度", wxPoint(10, 40), wxSize(80, 20));
        new wxStaticText(leftPanel, wxID_ANY, wxString::Format("%.0f", data[1]), wxPoint(100, 40), wxSize(80, 20));

        // 不快指数
        new wxStaticText(leftPanel, wxID_ANY, "不快指数", wxPoint(10, 70), wxSize(80, 20));
        new wxStaticText(leftPanel, wxID_ANY, wxString::Format("%.0f", data[2]), wxPoint(100, 70), wxSize(80, 20));

        // エアコン
        new wxStaticText(bottomPanel, wxID_ANY, "エアコン", wxPoint(10, 10), wxSize(80, 20));

        // AUTOボタン
        new wxButton(bottomPanel, wxID_ANY, "AUTO", wxPoint(100, 5), wxSize(60, 30));

        // OFFボタン
        new wxButton(bottomPanel, wxID_ANY, "OFF", wxPoint(180, 5), wxSize(60, 30));

        // 動作温度
        new wxStaticText(bottomPanel, wxID_ANY, "動作温度", wxPoint(10, 51), wxSize(72, 18));
        wxSlider* operationTempSlider = new wxSlider(bottomPanel, wxID_ANY, 26, 26, 28, wxPoint(110, 50), wxSize(120, 20), wxSL_HORIZONTAL);
        wxStaticText* operationTempDisplay = new wxStaticText(bottomPanel, wxID_ANY, "26", wxPoint(240, 50), wxSize(40, 20));

        // 温度幅
        new wxStaticText(bottomPanel, wxID_ANY, "温度幅", wxPoint(10, 81), wxSize(54, 18));
        wxSlider* tempRangeSlider = new wxSlider(bottomPanel, wxID_ANY, 3, 1, 5, wxPoint(110, 80), wxSize(120, 20), wxSL_HORIZONTAL);
        wxStaticText* tempRangeDisplay = new wxStaticText(bottomPanel, wxID_ANY, "0.3", wxPoint(240, 81), wxSize(40, 20));

        // 設定温度
        new wxStaticText(bottomPanel, wxID_ANY, "設定温度", wxPoint(10, 111), wxSize(72, 18));
        wxSlider* setTempSlider = new wxSlider(bottomPanel, wxID_ANY, 22, 20, 25, wxPoint(110, 110), wxSize(120, 20), wxSL_HORIZONTAL);
        wxStaticText* setTempDisplay = new wxStaticText(bottomPanel, wxID_ANY, "22", wxPoint(240, 110), wxSize(40, 20));

        // ファン
        new wxStaticText(bottomPanel, wxID_ANY, "ファン", wxPoint(10, 141), wxSize(53, 18));
        wxArrayString fanChoices;
        fanChoices.Add("Auto");
        fanChoices.Add("Low");
        fanChoices.Add("Medium");
        fanChoices.Add("High");
        new wxChoice(bottomPanel, wxID_ANY, wxPoint(110, 140), wxSize(80, 20), fanChoices);

        // Event bindings
        operationTempSlider->Bind(wxEVT_SLIDER, [operationTempDisplay](wxCommandEvent& event) {
            operationTempDisplay->SetLabel(wxString::Format("%.1f", event.GetInt() / 10.0));
        });

        tempRangeSlider->Bind(wxEVT_SLIDER, [tempRangeDisplay](wxCommandEvent& event) {
            tempRangeDisplay->SetLabel(wxString::Format("%.1f", event.GetInt() / 10.0));
        });

        setTempSlider->Bind(wxEVT_SLIDER, [setTempDisplay](wxCommandEvent& event) {
            setTempDisplay->SetLabel(wxString::Format("%d", event.GetInt()));
        });
    }
};

class MyApp : public wxApp
{
public:
    virtual bool OnInit()
    {
        MyFrame* frame = new MyFrame();
        frame->Show(true);
        return true;
    }
};

wxIMPLEMENT_APP(MyApp);

[C++] 374 SwitchBot管理アプリの製作 その1 wxWidgets

[Mac M2 Pro 12CPU, Sonoma 14.5, wxWidgets 3.2.5]

wxWidgetsのHomebrewでの最新バージョンが3.2.3と遅れているのでGitHub最新の3.2.5を自分でビルドしました。zipではビルドできません。tar.bz2をダウンロードします。

# --with-libtiff=builtinを追加しないとエラーになる

mkdir build-cocoa-debug
cd build-cocoa-debug
../configure --enable-debug --with-libtiff=builtin
make

ビルドしたライブラリとインクルードは/usr/localにコピーしました。アプリのビルド時にwx/setup.hがないというエラーになりましたが、以下のパスにあったのでインクルードフォルダにコピーしました。

/wxWidgets-3.2.5/build-cocoa-debug/lib/wx/include/osx_cocoa-unicode-3.2/wx/setup.h

wxWidgetsを広く普及するためにも、せめてHomebrewに最新版を登録して欲しいところですが、人が足りないのかな。

Makefileは以下の通りです。

# コンパイラ
COMPILER = clang++
DEBUG = -g

# フラグ
CPPFLAGS = -D_FILE_OFFSET_BITS=64 -DWXUSINGDLL -D__WXMAC__ -D__WXOSX__ -D__WXOSX_COCOA__  -std=c++17 
LDFLAGS = -framework IOKit -framework Carbon -framework Cocoa -framework QuartzCore -framework AudioToolbox -framework System -framework OpenGL -lwx_osx_cocoau_xrc-3.2 -lwx_osx_cocoau_html-3.2 -lwx_osx_cocoau_qa-3.2 -lwx_osx_cocoau_core-3.2 -lwx_baseu_xml-3.2 -lwx_baseu_net-3.2 -lwx_baseu-3.2 -lc++

# includeパス(-I)
INCLUDE = -I./include -I/Volumes/DATA_m1/code/cpp/mylib/include -I/usr/local/include/wxWidgets

# ライブラリ(-l)
LIBRARY0 =

# ライブラリパス(-L)
LIBRARY = -L/usr/local/lib/wxWidgets

# ソースファイル
SRCDIR = ./src
SRCS = $(shell find $(SRCDIR) -type f)

# オブジェクトファイル
OBJDIR = ./obj
OBJS = $(addprefix $(OBJDIR), $(patsubst ./src/%.cpp,/%.o,$(SRCS)))

# 実行ファイル
TARGETDIR = ./bin
TARGET = SwitchBotManager

# cppファイルからoファイル作成 $<:依存ファイル
$(OBJDIR)/%.o: $(SRCDIR)/%.cpp
	$(COMPILER) $(CPPFLAGS) $(INCLUDE) $(DEBUG) -o $@ -c $<

# oファイルから実行ファイルとappファイル作成
$(TARGET): $(OBJS)
	$(COMPILER) -o $(TARGETDIR)/$@ $(OBJS) $(LIBRARY0) $(LDFLAGS) $(LIBRARY)
	mkdir -p $(TARGET).app/Contents/MacOS
	mkdir -p $(TARGET).app/Contents/Resources
	cp $(TARGETDIR)/$(TARGET) $(TARGET).app/Contents/MacOS/$(TARGET)
	cp ./images/$(TARGET).icns $(TARGET).app/Contents/Resources
	echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" > $(TARGET).app/Contents/Info.plist
	echo "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">" >> $(TARGET).app/Contents/Info.plist
	echo "<plist version=\"1.0\">" >> $(TARGET).app/Contents/Info.plist
	echo "<dict>" >> $(TARGET).app/Contents/Info.plist
	echo "  <key>CFBundleExecutable</key>" >> $(TARGET).app/Contents/Info.plist
	echo "  <string>$(TARGET)</string>" >> $(TARGET).app/Contents/Info.plist
	echo "  <key>CFBundleIconFile</key>" >> $(TARGET).app/Contents/Info.plist
	echo "  <string>$(TARGET).icns</string>" >> $(TARGET).app/Contents/Info.plist
	echo "  <key>CFBundleIdentifier</key>" >> $(TARGET).app/Contents/Info.plist
	echo "  <string>com.yourcompany.$(TARGET)</string>" >> $(TARGET).app/Contents/Info.plist
	echo "  <key>CFBundleName</key>" >> $(TARGET).app/Contents/Info.plist
	echo "  <string>$(TARGET)</string>" >> $(TARGET).app/Contents/Info.plist
	echo "  <key>CFBundleVersion</key>" >> $(TARGET).app/Contents/Info.plist
	echo "  <string>1.0</string>" >> $(TARGET).app/Contents/Info.plist
	echo "</dict>" >> $(TARGET).app/Contents/Info.plist
	echo "</plist>" >> $(TARGET).app/Contents/Info.plist

# 全ソース強制コンパイル
.PHONY:all
all: clean $(TARGET)

# 全ファイル削除
.PHONY:clean
clean:
	rm -rf $(OBJS) $(TARGETDIR)/$(TARGET) $(TARGET).app

[SwitchBot] 08 管理アプリMac版の製作 その4 wxWidgetsに変更 C++

[Mac M2 Pro 12CPU, Sonoma 14.5]

SwiftUIではNSOpenPanelやfileImporterでファイル選択しないとローカルファイルを読み込めないことが判明したため、C++に戻りwxWidgetsに挑戦することにしました。

以前wxWidgetsを扱った時は実行ファイルは作成できてもappファイル作成ができなかったのですが、今回はChatGPTに手伝ってもらいあっさりMakefileを完成させました。

ウィジェットのデザインはOS依存ですから、Swiftアプリ風の見た目になります。

次の記事

[SwitchBot] 05 管理アプリMac版の製作 その1 C++, FLTK

[Mac M2 Pro 12CPU, Sonoma 14.5]

SwitchBot管理アプリMac版の製作に着手しました。手順は以下の通りです。

1.Adobe XDでGUIをデザイン。座標とサイズを自作プラグインで抽出
2.ChatGPT用プロンプトを作成しレスポンスを得る
3.手直ししたC++コードをビルド

このコードを土台に肉付けしていきます。

FLTKアプリを作成します
C++コードを考えてください
ウィンドウのサイズは600*400
各ウィジェットの座標は以下の通り
{
  "エアコングラフ": [15, 200, 270, 180],
  "ファン選択": [110, 140, 60, 20],
  "ファン": [10, 141, 53, 18],
  "温度幅表示": [240, 81, 40, 20],
  "温度幅スライダー": [110, 80, 120, 20],
  "温度幅": [10, 81, 54, 18],
  "動作温度表示": [240, 50, 40, 20],
  "動作温度スライダー": [110, 50, 120, 20],
  "動作温度": [10, 51, 72, 18],
  "OFFボタン": [190, 10, 60, 30],
  "AUTOボタン": [110, 10, 60, 30],
  "エアコン": [10, 15, 80, 20],
  "設定温度表示": [240, 110, 40, 20],
  "設定温度スライダー": [110, 110, 120, 20],
  "設定温度": [10, 111, 72, 18]
}

エアコン:Fl_Boxに"エアコン"を表示
AUTOボタン:ラベルが"AUTO"のFl_Button
OFFボタン:ラベルが"OFF"のFl_Button

動作温度:Fl_Boxに"動作温度"を表示
動作温度スライダー:Fl_Sliderを表示 26から28まで0.1きざみ
動作温度表示:Fl_Boxに動作温度スライダーの値を表示。

温度幅:Fl_Boxに"温度幅"を表示
温度幅スライダー:Fl_Sliderを表示 0.1から0.5まで0.1きざみ
温度幅表示:Fl_Boxに温度幅スライダーの値を表示。

設定温度:Fl_Boxに"設定温度"を表示
設定温度スライダー:Fl_Sliderを表示 20から25まで1.0きざみ
設定温度表示:Fl_Boxに設定温度スライダーの値を表示。

ファン:Fl_Boxに"ファン"を表示
ファン選択:ブルダウンで選択 # fan speed includes 1 (auto), 2 (low), 3 (medium), 4 (high);
エアコングラフ:pngファイルを表示

[SwitchBot] 04 エアコンを操作

[Mac M2 Pro 12CPU, Sonoma 14.5]

Pythonでエアコンを操作してみました。

このスクリプトを応用すれば、温湿度計のデータと連携させて0.1度単位でトリガーを設定し、エアコンを操作できるようになります。

純正アプリでは最小で0.5度の温度幅ですが、0.1度まで狭くすることが理屈では可能です。

ただ、SwitchBot APIへのコール回数は1日10000回までになっています。毎秒測定なら1日86400回になるので、10秒に1回程度に減らす必要があります。cronを使えば毎分が最多で1440回です。

import os
import time
import json
import hashlib
import hmac
import base64
import uuid
import requests
import datetime

dir_name = "/SwitchBot/data"

device_id = "XXX"
token = 'XXX'
secret = 'XXX'

# エアコン設定
temperature = 26.5
mode = 2 # modes include 0/1 (auto), 2 (cool), 3 (dry), 4 (fan), 5 (heat);
fanspeed = 1 # fan speed includes 1 (auto), 2 (low), 3 (medium), 4 (high);
power_state = "on" # power state includes on and off

nonce = str(uuid.uuid4())
t = int(round(time.time() * 1000))
string_to_sign = "{}{}{}".format(token, t, nonce)
string_to_sign = bytes(string_to_sign, "utf-8")
secret = bytes(secret, "utf-8")
sign = base64.b64encode(
    hmac.new(secret, msg=string_to_sign, digestmod=hashlib.sha256).digest()
)

HEADERS = {
    "Authorization": token,
    "Content-Type": "application/json",
    "charset": "utf8",
    "t": str(t),
    "sign": str(sign, "utf-8"),
    "nonce": nonce
}

# コマンド内容
command_body = {
    "command": "setAll",
    "parameter": f"{temperature},{mode},{fanspeed},{power_state}",
    "commandType": "command"
}

response = requests.post(
    f"https://api.switch-bot.com/v1.1/devices/{device_id}/commands",
    headers=HEADERS,
    data=json.dumps(command_body)
)

status = response.json()

timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
response_file = f"{dir_name}/AirCon/status_{device_id}_{timestamp}.json"
with open(response_file, "w") as f:
    json.dump(status, f, ensure_ascii=False, indent=4)

print("Success operate Air Conditioner")

[SwitchBot] 03 温湿度データ取得&グラフ化 浴室乾燥テスト

[Mac M2 Pro 12CPU, Sonoma 14.5]

浴室暖房乾燥機で洗濯物を乾かす際の温湿度をモニタリングしました。途中何度か風呂場に素早く入って乾き具合をチェックしました。

絶対湿度33g/m3位で洗濯物が8割方乾いていたので回収し、残りは27g/m3まで下がった時点で完全に乾いていました。

製作するアプリでは絶対湿度33g/m3以下で最初の回収を促し、27g/m3以下で終了を合図するようにします。

物理的にボタンを押してくれるスマートスイッチがあれば、浴室のリモコンまで行かなくても乾燥を自動停止できます。

乾燥開始30分後から測定開始、23:24乾燥停止
import os
import time
import json
import hashlib
import hmac
import base64
import uuid
import requests
import datetime
import pandas as pd
import matplotlib.pyplot as plt
from os.path import exists
import subprocess
import numpy as np

dir_name = "/SwitchBot/data"

device_id = "XXX"
token = 'XXX'
secret = 'XXX'

nonce = str(uuid.uuid4())
t = int(round(time.time() * 1000))
string_to_sign = "{}{}{}".format(token, t, nonce)
string_to_sign = bytes(string_to_sign, "utf-8")
secret = bytes(secret, "utf-8")
sign = base64.b64encode(
    hmac.new(secret, msg=string_to_sign, digestmod=hashlib.sha256).digest()
)

apiHeader = {}
apiHeader["Authorization"] = token
apiHeader["Content-Type"] = "application/json"
apiHeader["charset"] = "utf8"
apiHeader["t"] = str(t)
apiHeader["sign"] = str(sign, "utf-8")
apiHeader["nonce"] = nonce

response = requests.get(
    f"https://api.switch-bot.com/v1.1/devices/{device_id}/status",
    headers=apiHeader,
)
devices = response.json()

timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
response_file = f"{dir_name}/json/status_{device_id}_{timestamp}.json"
with open(response_file, "w") as f:
    json.dump(devices, f, ensure_ascii=False, indent=4)

print("Success get device status.")

# CSVファイルを作成(データ行を追加する)
timestamp_date = datetime.datetime.now().strftime("%Y%m%d")
csv_file = f"{dir_name}/{timestamp_date}_{device_id}_data.csv"

# デバイスのステータスから必要なデータを抽出
temperature = devices['body']['temperature']
humidity = devices['body']['humidity']
battery = devices['body']['battery']

# 湿度が60%以下の場合に通知を送る(動作不可)
if humidity <= 60:
    subprocess.run(['osascript', '-e', f'display notification "Humidity is {humidity}%" with title "Humidity Alert"'])

# 絶対湿度を計算
absolute_humidity = (6.112 * np.exp((17.67 * temperature) / (temperature + 243.5)) * humidity * 2.1674) / (273.15 + temperature)

# CSVファイルにデータを追加
data = {
    'timestamp': [timestamp],
    'temperature': [temperature],
    'relative_humidity': [humidity],
    'absolute_humidity': [absolute_humidity],
    'battery': [battery]
}

df = pd.DataFrame(data)

if exists(csv_file):
    df.to_csv(csv_file, mode='a', header=False, index=False)
else:
    df.to_csv(csv_file, mode='w', header=True, index=False)

print("Success append data to CSV.")

# CSVファイルからグラフを作成する(上書き更新)
plot_file = f"{dir_name}/{timestamp_date}_{device_id}_data_plot.png"

# CSVファイルを読み込む
df = pd.read_csv(csv_file)

# タイムスタンプをdatetime型に変換
df['timestamp'] = pd.to_datetime(df['timestamp'], format='%Y%m%d%H%M%S')

# 絶対湿度の移動平均を計算(5分間の移動平均)
df.set_index('timestamp', inplace=True)
df['absolute_humidity_ma'] = df['absolute_humidity'].rolling('5T').mean()

# グラフを作成
fig, ax1 = plt.subplots(figsize=(10, 5))

ax1.set_xlabel('Timestamp')
ax1.set_ylabel('Temperature (°C)', color='tab:red')
ax1.plot(df.index, df['temperature'], label='Temperature', color='tab:red')
ax1.tick_params(axis='y', labelcolor='tab:red')
ax1.set_ylim(20, 50)

ax2 = ax1.twinx()
ax2.set_ylabel('Absolute Humidity (g/m³)', color='tab:blue')
ax2.plot(df.index, df['absolute_humidity_ma'], label='Absolute Humidity (5-min MA)', color='tab:blue')
ax2.tick_params(axis='y', labelcolor='tab:blue')
ax2.set_ylim(20, 60)

fig.tight_layout(rect=[0, 0, 1, 0.95])  # タイトルが切れないように調整
plt.title('Temperature and Absolute Humidity (5-min MA) over Time', pad=20)
plt.xticks(rotation=45)
ax1.xaxis.set_major_formatter(plt.matplotlib.dates.DateFormatter('%H:%M:%S'))

# グラフを保存
plt.savefig(plot_file)
plt.close()

print("Success create plot.")

[SwitchBot] 02 温湿度データ取得&グラフ化 cron

[Mac M2 Pro 12CPU, Sonoma 14.5]

浴室の温湿度データを毎分取得し、都度グラフ化するようにしました。

以前から解決できていなかったのですが、osascriptコマンドを使ってもMacに通知を送ることができません。システム音を鳴らすこともできないです。つまりAppleScriptが動作しないです。

このスクリプトをcronに登録するスクリプトが別に必要ですが、ChatGPTに聞けば教えてくれます。

import os
import time
import json
import hashlib
import hmac
import base64
import uuid
import requests
import datetime
import pandas as pd
import matplotlib.pyplot as plt
from os.path import exists
import subprocess

dir_name = "/SwitchBot/data"

device_id = "XXX"
token = 'XXX'
secret = 'XXX'

nonce = str(uuid.uuid4())
t = int(round(time.time() * 1000))
string_to_sign = "{}{}{}".format(token, t, nonce)
string_to_sign = bytes(string_to_sign, "utf-8")
secret = bytes(secret, "utf-8")
sign = base64.b64encode(
    hmac.new(secret, msg=string_to_sign, digestmod=hashlib.sha256).digest()
)

apiHeader = {}
apiHeader["Authorization"] = token
apiHeader["Content-Type"] = "application/json"
apiHeader["charset"] = "utf8"
apiHeader["t"] = str(t)
apiHeader["sign"] = str(sign, "utf-8")
apiHeader["nonce"] = nonce

response = requests.get(
    f"https://api.switch-bot.com/v1.1/devices/{device_id}/status",
    headers=apiHeader,
)
devices = response.json()

timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
response_file = f"{dir_name}/json/status_{device_id}_{timestamp}.json"
with open(response_file, "w") as f:
    json.dump(devices, f, ensure_ascii=False, indent=4)

print("Success get device status.")

# CSVファイルを作成(データ行を追加する)
timestamp_date = datetime.datetime.now().strftime("%Y%m%d")
csv_file = f"{dir_name}/{timestamp_date}_{device_id}_data.csv"

# デバイスのステータスから必要なデータを抽出
temperature = devices['body']['temperature']
humidity = devices['body']['humidity']
battery = devices['body']['battery']

# 湿度が60%以下の場合に通知を送る(動作不可)
if humidity <= 60:
    subprocess.run(['osascript', '-e', f'display notification "Humidity is {humidity}%" with title "Humidity Alert"'])

# CSVファイルにデータを追加
data = {
    'timestamp': [timestamp],
    'temperature': [temperature],
    'humidity': [humidity],
    'battery': [battery]
}

df = pd.DataFrame(data)

if exists(csv_file):
    df.to_csv(csv_file, mode='a', header=False, index=False)
else:
    df.to_csv(csv_file, mode='w', header=True, index=False)

print("Success append data to CSV.")

# CSVファイルからグラフを作成する(上書き更新)
plot_file = f"{dir_name}/{timestamp_date}_{device_id}_data_plot.png"

# CSVファイルを読み込む
df = pd.read_csv(csv_file)

# タイムスタンプをdatetime型に変換
df['timestamp'] = pd.to_datetime(df['timestamp'], format='%Y%m%d%H%M%S')

# グラフを作成
fig, ax1 = plt.subplots(figsize=(10, 5))

ax1.set_xlabel('Timestamp')
ax1.set_ylabel('Temperature (°C)', color='tab:red')
ax1.plot(df['timestamp'], df['temperature'], label='Temperature', color='tab:red')
ax1.tick_params(axis='y', labelcolor='tab:red')
ax1.set_ylim(20, 50)

ax2 = ax1.twinx()
ax2.set_ylabel('Humidity (%)', color='tab:blue')
ax2.plot(df['timestamp'], df['humidity'], label='Humidity', color='tab:blue')
ax2.tick_params(axis='y', labelcolor='tab:blue')
ax2.set_ylim(60, 100)

fig.tight_layout(rect=[0, 0, 1, 0.95])  # タイトルが切れないように調整
plt.title('Temperature and Humidity over Time', pad=20)
plt.xticks(rotation=45)
ax1.xaxis.set_major_formatter(plt.matplotlib.dates.DateFormatter('%H:%M:%S'))

# グラフを保存
plt.savefig(plot_file)
plt.close()

print("Success create plot.")

参考サイト

[SwitchBot] 01 管理アプリ開発検討

[Mac M2 Pro 12CPU, Sonoma 14.5]

初めてSwitchBotデバイスを購入してからちょうど2年が経ちました。

Mac用の正式な管理アプリがなく、iPad版はMacではまともに動作しないので、自分でmacOSアプリを製作できないか調査しています。

APIを使ってデバイスからデータを取得し、ある条件を満たせばMacに通知を送信する機能を実装したいです。