RPAを使ってZoomミーティングの開催を自動化がやっと運用できるレベルになりました。
ちまちま作っていたので、全体の作業時間は72時間もないのではと思います。
目次
0.目的
人の手を介さずに、研究室のオンラインゼミを開催する。
1.仕様
- Googleカレンダーから予定を取得する。
- 予定された時刻になったらZoomミーティングを自動で開催する。
- 予定時刻ぴったりに開催するわけではない。予定時刻より数十秒~1分ほど遅れる。
- ミーティングの終了は実装していない。
- windows環境でのみ動作させることを想定している。
2.外観
3.コード
3-1.メイン部分
import PySimpleGUI as sg | |
import datetime as dt | |
import getschedule as gs | |
import operate_zoom as oz | |
import time | |
import threading | |
sg.theme('DarkBrown1') | |
layout = [ | |
[sg.Text('Next', size = (5,1), justification = 'left'), sg.Text(size = (20,1), key='-START-'),sg.Text('~',size = (6,1)), sg.Text(size=(20,1), key='-END-')], | |
[sg.Text('Now',size = (5,1),justification = 'left'),sg.Text(size=(20,1), key = '-NOW-')], | |
[sg.Text('Log', size =(5,1), justification = 'left')], | |
[sg.Output(size=(100,10), key='-LOG-')], | |
[sg.Button('Start/Stop', focus=True), sg.Quit()] | |
] | |
window = sg.Window('ミーティング開催', layout) | |
count = -1 #初回ループ時に予定取得させるため | |
def start_zoom(now_date): | |
oz.Start_Zoom() | |
print('{} Start meeting'.format(now_date)) | |
#タイミング調整 | |
time.sleep(5) | |
#画面共有開始 | |
oz.screen_sharing() | |
def end_zoom(now_date): | |
global count | |
zoom_flag = True | |
oz.Stop_Zoom() | |
print('{} Quit meeting'.format(now_date)) | |
#ミーティング終了後、即座に予定を再取得する | |
count = -1 | |
def GUI(): | |
global count | |
RPA_running = False | |
next_start_date, next_end_date = '0000-00-00 00:00', '0000-00-00 00:00' | |
interval = 0 #ミーティング開始時、終了時から1分間カウントする変数 | |
zoom_flag = False #何度も起動するのを防ぐため。Trueの場合1分経過でFalseに戻す。 | |
while True: | |
event, values = window.read(timeout=1000) #ループ間隔は1秒 | |
#現在時刻を取得 | |
now = dt.datetime.now() | |
#時刻表示のために整形 | |
now_time = now.strftime('%Y-%m-%d %H:%M:%S') | |
#比較のためにstr型に変換 | |
now_date = now.strftime('%Y-%m-%d %H:%M') | |
#現在時刻の表示 | |
window['-NOW-'].update(now_time) | |
#ボタン操作 | |
if event in ('Quit'): | |
break | |
elif event == 'Start/Stop': | |
RPA_running = not RPA_running | |
if RPA_running: | |
print('{} Start'.format(now_date)) | |
else: | |
print('{} Stop'.format(now_date)) | |
#再開時に予定を取得させるため | |
count = -1 | |
if RPA_running: | |
#30分間隔で予定取得 | |
if count == -1 or count == 1800: | |
#カウンタを戻す | |
count = 0 | |
#予定を5つ取得し、直近の予定の開始時刻と終了時刻を格納 | |
schedule = gs.get_events() | |
start_date = schedule[0] | |
end_date = schedule[1] | |
#予定を整形 | |
start_date = start_date[0:10] + ' ' + start_date[11:16] | |
end_date = end_date[0:10] + ' ' + end_date[11:16] | |
#予定を取得したらログに記述 | |
print('{} Get next schedule'.format(now_date)) | |
#次の予定が変更されたらログに記述 | |
if start_date != next_start_date: | |
next_start_date = start_date | |
next_end_date = end_date | |
print('{} Update next schedule'.format(now_date)) | |
#GUI表示 | |
window['-START-'].update(start_date) | |
window['-END-'].update(end_date) | |
#ミーティング開始時、終了時にカウントを始める | |
if zoom_flag == True: | |
interval += 1 | |
#60秒経過したらフラッグを反転させ、変数を初期値に戻す | |
if interval == 60: | |
zoom_flag = not zoom_flag | |
interval = 0 | |
#開始時刻かつ直前にミーティングを起動していないならミーティングを開始する | |
if start_date == now_date and zoom_flag == False: | |
zoom_flag = True | |
zoom_thread = threading.Thread(target=start_zoom, daemon=True, args=(now_date,)) | |
zoom_thread.start() | |
#終了時刻にかつ直前にミーティングを終了していないならミーティングを終了する | |
if end_date == now_date and zoom_flag == False: | |
zoom_flag = True | |
zoom_thread = threading.Thread(target = end_zoom, daemon = True, args = (now_date,)) | |
zoom_thread.start() | |
#カウンタを増やす | |
count += 1 | |
else: | |
continue | |
window.close() | |
if __name__ == '__main__': | |
GUI_thread = threading.Thread(target=GUI) | |
GUI_thread.start() |
3-2.予定取得
from __future__ import print_function | |
import datetime | |
import pickle | |
import os.path | |
from googleapiclient.discovery import build | |
from google_auth_oauthlib.flow import InstalledAppFlow | |
from google.auth.transport.requests import Request | |
# If modifying these scopes, delete the file token.pickle. | |
SCOPES = ['https://www.googleapis.com/auth/calendar.readonly'] | |
def get_events(): | |
"""Shows basic usage of the Google Calendar API. | |
Prints the start and name of the next 10 events on the user's calendar. | |
""" | |
creds = None | |
# The file token.pickle stores the user's access and refresh tokens, and is | |
# created automatically when the authorization flow completes for the first | |
# time. | |
if os.path.exists('token.pickle'): | |
with open('token.pickle', 'rb') as token: | |
creds = pickle.load(token) | |
# If there are no (valid) credentials available, let the user log in. | |
if not creds or not creds.valid: | |
if creds and creds.expired and creds.refresh_token: | |
creds.refresh(Request()) | |
else: | |
flow = InstalledAppFlow.from_client_secrets_file( | |
'credentials.json', SCOPES) | |
creds = flow.run_local_server(port=0) | |
# Save the credentials for the next run | |
with open('token.pickle', 'wb') as token: | |
pickle.dump(creds, token) | |
service = build('calendar', 'v3', credentials=creds) | |
# Call the Calendar API | |
now = datetime.datetime.utcnow().isoformat() + 'Z' # 'Z' indicates UTC time | |
events_result = service.events().list(calendarId='i44ncf4en0in1jhmaens1bjpe4@group.calendar.google.com', timeMin=now, | |
maxResults=5, singleEvents=True, | |
orderBy='startTime').execute() | |
events = events_result.get('items', []) | |
events_list = [] | |
for event in events: | |
start = event['start'].get('dateTime', event['start'].get('date')) | |
end = event['end'].get('dateTime', event['end'].get('date')) | |
events_list.extend([start,end]) | |
return events_list |
3-3.Zoom操作部
import pyautogui as pg | |
import sys | |
import os | |
# ズーム起動部分(windows専用) | |
def run_zoom(): | |
pg.press('win') | |
pg.PAUSE = 1 | |
pg.write('zoom') | |
pg.PAUSE = 2 | |
pg.press('enter') | |
# 定期ミーティング起動部分 | |
def start_meeting(): | |
# ボタンがアクティブ\非アクティブ状態両方のスクリーンショットをそれぞれリストに入れる。スタートは変数で持つ。 | |
buttton_active = [r'button\active\zoom_meeting_active.PNG', | |
r'button\active\zoom_igrashi_meeting_active.PNG'] | |
button_nonactive = [r'button\nonactive\zoom_meeting_non.PNG', | |
r'button\nonactive\zoom_igarashi_meeting_non.PNG'] | |
button_start = r'button\active\zoom_igarashi-meeting_start.PNG' | |
# 画像認識部分 | |
for button_a, button_n in zip(buttton_active, button_nonactive): | |
# 非アクティブ時のボタンを探す | |
button_loc_n = pg.locateOnScreen(button_n) | |
if button_loc_n is None: | |
# 非アクティブ時のボタンが見つからない場合、アクティブ時のボタンを探す | |
button_loc_a = pg.locateOnScreen(button_a) | |
if button_loc_a is None: | |
# ボタンが見つからないならばプログラムを終了する | |
sys.exit() | |
else: | |
# ボタンがアクティブならば操作は必要ない | |
continue | |
# 公式ドキュメントのサンプルコードを参考に実装 | |
# ボタンの中心の位置の座標を取得しクリックする | |
button_center = pg.center(button_loc_n) | |
button_x, button_y = button_center | |
pg.click(button_x, button_y) | |
# 画面遷移のために1秒停止する | |
pg.PAUSE = 1 | |
# ミーティング開始ボタンを押す部分 | |
button_x, button_y = pg.center(pg.locateOnScreen(button_start)) | |
pg.click(button_x, button_y) | |
def Stop(): | |
#ミーティング終了するときに押すボタン | |
button_quit = r'button\active\zoom_meeting_quit.PNG' | |
#終了ボタンを押す(ショートカットキーで代用) | |
pg.hotkey('alt', 'q') | |
#画面遷移のために1秒停止する | |
pg.PAUSE = 1 | |
#全員に対してミーティングを終了する | |
button_loc = pg.locateOnScreen(button_quit) | |
if button_loc is None: | |
print('404 NOT FOUND') | |
else: | |
button_x, button_y = pg.center(button_loc) | |
pg.click(button_x, button_y) | |
#ミーティング開始時 | |
def Start_Zoom(): | |
try: | |
run_zoom() | |
pg.PAUSE = 5 #Zoomの起動待ち | |
start_meeting() | |
except KeyboardInterrupt: | |
print('ERROR: It can not start zoom meeting.\n') | |
pg.FAILSAFE = True | |
#ミーティング終了時 | |
def Stop_Zoom(): | |
try: | |
Stop() | |
except KeyboardInterrupt: | |
print('ERROR: It can not quit zoom meeting.\n') | |
pg.FAILSAFE = True |
4.工夫した点
4-1.画像認識の順番
用いているOpenCVの画像認識は時間がかかるため、認識させる画像の順番を変えることで、ミーティング開催までの最短時間を短縮しました。
ただ、最低でも30秒はかかるため、画像認識自体を高速化する必要があります。
4-2.モジュール化
Zoomの自動操作や予定取得をモジュール化することで、メイン部分のコードを簡略化しています。
競技プログラミングではモジュール化をやらないので、やってみようと思ってしてみた側面もあります。
4-3.無限ループ開始時に予定を取得させる
#30分間隔で予定取得
if count == -1 or count == 30:
#カウンタを戻す
count = 0
#予定を5つ取得し、直近の予定の開始時刻と終了時刻を格納
schedule = gs.get_events()
start_date = schedule[0]
end_date = schedule[1]
予定取得間隔はtimeout時間である60秒(60000ms)に30を掛けた、30分間隔です。
もともと、timeoutを可変にすることでGoogle Calendarへのリクエスト回数を減らそうとしていましたが、timeoutを固定にしてcountという変数で予定取得間隔を調整するようにしました。
これによって、現在時刻は1分ごとに取得するが、予定は30分ごとに取得するようにでき、リクエスト回数を削減できました。
しかし、私の場合countを30に設定していますが、条件がこれだけだとSTARTを押して30分後に予定を取得することになります。一方でcount=0を条件に加えると2回同時に予定を取得することになり無駄です。
よって、countの初期値をループ内では絶対に取り得ない数値にし、その値を条件に加えることで、STARTを押した後だけ予定を即座に取得するようにできました。
5.新しく会得したこと
5-1.GUIを作る
PySimpleGUIは去年少しだけ触っていましたが、今回は公式ドキュメントやcookbookを見ながら作りました。
所定の場所に特定の情報を出力するときはkeyを設定することや、timeoutの設定や注意点を学びました。
5-2.Google Calendar APIを使う
APIを使ったことがなかったので、初めて使ってみました。quickstartをちょっと弄るだけでできてしまったので、会得したかは微妙。
5-3.モジュール化
書いたコードをモジュールにして分割することをやったことがないので、やってみました。1つのファイルにすべて記述するよりスッキリしましたし、テストもやりやすかったです。
5-4.ドライバの作成
並行して基本情報技術者試験の勉強をしていたときにスタブとドライバが出てきました。
必要な部分をモジュール化していたため、基本情報技術者試験で学習した知識を使ってテストをしていました。そのためにドライバが必要になったので制作しました。
資格勉強の知識が意外と役に立つなあと思いました。
5-5.RPAの作成
sikuliなどではなく、PyAutoGUIというモジュールで作成しました。トラブルがよく起こりましたが、しっかり動作するものができました。
人がする操作をそのとおりに記述するので実装は楽でしたが、うまい実装を考えることに苦労しました。
6.今後の展望
Zoomは一定時間経過すると自動で終了します。しかし、終了ができないのはまずいと思ったので、終了時刻になったら終了ボタンを押すようにしたいと思います。ただ、ゼミの終了時刻はまちまちのためどうするか考えています。
7.GitHub
https://github.com/ateruimashin/auto_zoom.git
8.制作過程
Google Calendar APIを使ってカレンダーから予定を習得する