[Mac M2 Pro 12CPU, Sonoma 14.5]
開発経過をGUIで記録します。
[Mac M2 Pro 12CPU, Sonoma 14.5]
開発経過をGUIで記録します。
[Mac M2 Pro 12CPU, Sonoma 14.5]
FLTKのスライダーが機能不足なのでSwiftUIに変えました。
デスクトップではウィジェットの位置を座標で決めていたため、SwiftUIでVStackやHStackを使って位置決めするのはとてもやりずらいです。iOSやwatchOSでは違和感がないのですが。
[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ファイルを表示
[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")
[Mac M2 Pro 12CPU, Sonoma 14.5]
浴室暖房乾燥機で洗濯物を乾かす際の温湿度をモニタリングしました。途中何度か風呂場に素早く入って乾き具合をチェックしました。
絶対湿度33g/m3位で洗濯物が8割方乾いていたので回収し、残りは27g/m3まで下がった時点で完全に乾いていました。
製作するアプリでは絶対湿度33g/m3以下で最初の回収を促し、27g/m3以下で終了を合図するようにします。
物理的にボタンを押してくれるスマートスイッチがあれば、浴室のリモコンまで行かなくても乾燥を自動停止できます。
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.")
[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.")
[Mac M2 Pro 12CPU, Sonoma 14.5]
初めてSwitchBotデバイスを購入してからちょうど2年が経ちました。
Mac用の正式な管理アプリがなく、iPad版はMacではまともに動作しないので、自分でmacOSアプリを製作できないか調査しています。
APIを使ってデバイスからデータを取得し、ある条件を満たせばMacに通知を送信する機能を実装したいです。
[Mac M2 Pro 12CPU, Sonoma 14.5]
最近は麻雀よりも将棋の方に興味が向いています。
AI対局や名局の棋譜並べに最適なShogiHomeというElectronアプリをGitHubから入手して使ってみたのですが、ショートカットで右矢印キーがNEXTではなくLASTになるなど仕様が気になるので、自分用に改変しました。
左矢印がBACK、右矢印はNEXT、上矢印がFIRST、下矢印がLASTになるように割り付けました。
Electronアプリは久しぶりに扱ったため時間を要しました。GUIに特化したフレームワークであるVue.jsについては全くの初見で少々手こずりました。
[Mac M2 Pro 12CPU, Sonoma 14.5, Xcode 16 beta]
今秋リリース予定のiOS 18からiOSアプリのアイコンが3種類(Light, Dark, Tinted)必要になります。
早速、Xcode 16 beta版で登録してみました。
watchOSアプリを手掛けているデベロッパーであれば文字盤のアクセントカラー対応で洗礼を受けているので、特に混乱はないと思います。
ダークモード用のアイコン(背景が透過度100%)はそのままTintedアイコン(モノトーン)としても使えそうな感じがしますが、多色デザインであれば灰色の濃さで表現するため作り直しということになりそうです。
[Mac M2 Pro 12CPU, Ventura 13.6, Python 3.10.4]
Googleカレンダーから入手したicsファイルの内容をgpt-4oに見せて、jsonファイルの作成スクリプトを考えさせました。
これまではGitHubなどから使えそうなコードを引っ張ってきたりしていましたが、AIに作らせる方が手っ取り早いです。
今回の場合はicsファイルに記念日が混在していたので、結局自分で手を入れて仕上げることになりました。
import json
from icalendar import Calendar
from datetime import datetime
# アメリカの祝日リスト
us_holidays = {
"New Year's Day",
"Martin Luther King Jr. Day",
"Presidents' Day",
"Memorial Day",
"Juneteenth",
"Independence Day",
"Labor Day",
"Columbus Day",
"Veterans Day",
"Thanksgiving Day",
"Christmas Day"
}
def parse_ics(file_path):
with open(file_path, 'r') as f:
ics_content = f.read()
calendar = Calendar.from_ical(ics_content)
holidays = {}
for component in calendar.walk():
if component.name == "VEVENT":
summary = str(component.get('summary'))
dtstart = component.get('dtstart').dt
if isinstance(dtstart, datetime):
dtstart = dtstart.date()
dtstart_str = dtstart.strftime('%Y-%m-%d')
# アメリカの祝日リストに含まれているか確認
if any(holiday in summary for holiday in us_holidays):
holidays[dtstart_str] = summary
return holidays
def save_to_json(data, output_file):
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
if __name__ == "__main__":
ics_file_path = 'america.ics' # ICSファイルのパスを指定
output_json_file = 'holidayUS.json' # 出力するJSONファイルの名前
holidays = parse_ics(ics_file_path)
# 日付の昇順で並べ替え
sorted_holidays = dict(sorted(holidays.items()))
save_to_json(sorted_holidays, output_json_file)
print(f"祝日情報が {output_json_file} に保存されました。")
{
"2019-01-01": "New Year's Day",
"2019-01-21": "Martin Luther King Jr. Day",
"2019-02-18": "Presidents' Day",
"2019-05-27": "Memorial Day",
"2019-06-19": "Juneteenth",
"2019-07-04": "Independence Day",
"2019-09-02": "Labor Day",
"2019-11-11": "Veterans Day",
"2019-11-28": "Thanksgiving Day",
"2019-12-25": "Christmas Day",
<以下略>