[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.")

参考サイト

[Python] 363 カレンダーicsファイルをjsonファイルに変換

[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",
<以下略>

[Python] 362 OpenAI DALL-E 3による画像生成

[Mac M2 Pro 12CPU, Ventura 13.6, Python 3.10.4]

DALL-E 3からAPIで使えるようになったので、早速試してみました。

生駒山近景を出力したところ存在しないロープウェイを含む景色でした。2回目は信仰の山らしく祠の画像でした。六甲山も出してみました。

どうも画のタッチが好みではありません。そこら辺を調整できれば、使い出がありそうです。

# openaiライブラリを導入済みの場合はアップグレードする(旧Verにはopenai.OpenAIがない)
pip install --upgrade openai
import os
from openai import OpenAI

# 環境変数からAPIキーを取得
api_key = os.getenv('CHATGPT_API_KEY')
if not api_key:
    raise ValueError("OpenAI API Keyが環境変数に設定されていません")

# APIキーを設定
os.environ["OPENAI_API_KEY"] = api_key

client = OpenAI()

response = client.images.generate(
  model="dall-e-3",
  prompt="大阪府・奈良県にある生駒山の近景",
  size="1024x1024",
  quality="standard",
  n=1,
)

image_url = response.data[0].url
print(image_url)
生駒山近景1
生駒山近景2
六甲山近景

[JavaScript] 19 Adobe XDのアイテムデータを取得するプラグイン作成 その5 作業手順 / UXP for Adobe XD / Pythonでjson化

[Mac M2 Pro 12CPU, Ventura 13.6]

作業手順をまとめておきます。

1.Adobe XDのデザインタブでXY座標を取得したい項目を選択する

2. メニューのプラグインからItemXYGetterを選択する。出力ファイルを指定して実行する。

3.出力されたtxtファイルをtxtTojson.pyで処理し、jsonファイルにする。

時間があればJavaScriptとPythonのスクリプトを統合し、JavaScriptのプラグインだけで処理できるようにしたいです。

※ ItemXYGetterのjsスクリプトの内容等は過去記事で紹介しています。画面左の検索窓で探してみてください。

Pythonスクリプトでjsonファイルに変換

[Python] 361 Excelセルの日付をシート名にする openpyxl

[Mac M2 Pro 12CPU, Ventura 13.6, Python 3.10.4]

Excelシートの1枚目A2セルにある日付をyymmdd形式に変換し、シート名にするスクリプトです。

ChatGPTのAPIが不調のため、以前のようにネット検索で調べました。やはりネット検索スキルはAI時代でも必須ですね。

import openpyxl
from openpyxl import load_workbook
from datetime import datetime, timedelta

def sheet_naming():
      import warnings
      warnings.simplefilter(action='ignore', category=UserWarning)

      # Excelファイルを読み込む
      file = 'test.xlsx'
      bk = openpyxl.load_workbook(file)

      # 先頭シートを取得 
      sheet = bk.worksheets[0]
      # 日付セルA2の値(5桁整数,表示はmm/dd)を取得
      date0 = sheet['A2'].value

      # date0をyymmdd形式に変換する
      date = datetime(1899, 12, 30) + timedelta(days=date0)
      date_str = date.strftime('%y%m%d')
      print("date_str: " + date_str)

      # シート名を変更する
      sheet.title = date_str
      # 変更を保存する
      bk.save(file)

[Python] 360 PDFファイルを結合

[Mac M2 Pro 12CPU, Ventura 13.6, Python 3.10.4]

PDFを色々加工するPythonスクリプトがたまってきました。

C++に移植してGUIアプリにまとめようかと考えています。

最近ファイル名やファイルパスを加工するのにosモジュールをよく使います。これまではsplitメソッドなどを使った文字列加工を多用していましたが、osモジュールのメソッドの方がさすがに使いやすいですね。

import os
from PyPDF2 import PdfMerger

# PDFファイルのディレクトリ
pdf_folder = '/images/'

# PDFファイルのリストを作成
pdf_files = [os.path.join(pdf_folder, f) for f in os.listdir(pdf_folder) if f.endswith('.pdf')]
pdf_files.sort()

# 先頭PDFファイル名を元に結合ファイル名を作成
pdf_name = os.path.splitext(pdf_files[0])[0] + "_join" + os.path.splitext(pdf_files[0])[1]
pdf_path = os.path.join(pdf_folder, pdf_name)

# PDFファイルを結合
merger = PdfMerger()
for pdf_file in pdf_files:
    merger.append(pdf_file)
merger.write(pdf_name)
merger.close()

[Python] 359 ファイル名の数字を引き算してリネーム

[Mac M2 Pro 12CPU, Ventura 13.6, Python 3.10.4]

画像ファイル名に含まれる数字を抽出して引き算しリネームするスクリプトを書きました。

下記スクリプトでは2桁の数字から2を引き算した新ファイル名にリネームしています。

import os, shutil

srcDir = "/images"
dstDir = "/images2" # 移動先

for filename in os.listdir(srcDir):
    if filename.endswith(".jpeg"):
        newName1 = (filename.split(".")[0]).split(" ")[0]
        newName2 = str(int((filename.split(".")[0]).split(" ")[1]) - 2).zfill(2)
        print(newName1 + newName2)
        
        new_filename = newName1 + " " + newName2 + ".jpeg"
        oldPath = os.path.join(srcDir, filename)
        print(oldPath)
        
        newPath = os.path.join(dstDir, new_filename)
        print(newPath)
        
        os.rename(oldPath, newPath)      # ファイル移動
        # shutil.copy2(oldPath, newPath) # ファイルコピー

[C++,Python] 316 BBS閲覧アプリの製作 その2 スレッドタイトルの取得 Python編

[M1 Mac, MacOS Ventura 13.3.1, Python 3.10.4]

スレッドタイトルとスレッドIDを取得するPythonスクリプトを書きました。

次はこのスクリプトをC++へ変換したいです。難しくなるようであればモジュール化します。

機能はPythonで下書きしてC++へ変換あるいはモジュール化、GUIはFLTK(C++)でコーディングしていきます。

import requests
import re
from bs4 import BeautifulSoup

# URLからHTMLファイルを取り込む
url = 'スレッドタイトル表示URL(HTMLファイル)'
response = requests.get(url)
response.encoding = response.apparent_encoding
html = response.text

# BeautifulSoupを使用してHTMLを解析する
soup = BeautifulSoup(html, 'html.parser')

# <small id="trad">タグで囲まれた部分の内容を取得する
trad_tags = soup.find_all('small', id='trad')

thread_list = []
for trad_tag in trad_tags:
    content = trad_tag.get_text()

    # <a>タグのhref属性の値を正規表現で取得する
    pattern = r'<a\s+href=[\'"]([^\'"]+)[\'"]'
    href = re.findall(pattern, str(trad_tag))

    # contentをaタグごとに分割してリストにする
    content_list = re.split(r'<a\s+href=[\'"][^\'"]+[\'"]', str(trad_tag))
    
    print(f"hrefの要素数: {len(href)}")
    print(f"content_listの要素数: {len(content_list)}")

    # 辞書型データのリストを作成する
    for i in range(len(href)):
        thread_list.append({'href': (href[i])[:-4], 'content': (content_list[i+1].strip().replace("</a>","").replace("</small>",""))[1:]})

# リストの内容を出力する
for thread in thread_list:
    print(thread['href'])
    print(thread['content'])