def execute(request) -> Dict[str, Any]: """古くなった不要データのクリーンアップを行います。 Arguments: request -- GET リクエスト Returns: Dict[str, Any] -- 処理結果 { // クリーンアップ処理に成功したかどうか "success": False or True, // クリーンアップしたレコードの総数 "count": xxx } """ logger.info(f"API Called.") with common.create_session() as session: count = 0 target_date = dt.combine( dt.now(), datetime.time()) - datetime.timedelta(days=THRESHOLD_DAYS - 1) temporary_products = session \ .query(TemporaryProduct) \ .filter(TemporaryProduct.created_time < target_date) \ .all() count += len(temporary_products) for temporary_product in temporary_products: session.delete(temporary_product) # 本登録データは商品イメージ画像ファイルと合わせて削除 products = session \ .query(Product) \ .filter(Product.expiration_date < target_date) \ .all() count += len(products) for product in products: logger.debug(f"Deleting: {os.path.basename(product.image_path)}") os.remove(product.image_path) session.delete(product) # トランザクション確定 session.commit() response = { "success": True, "count": count, } logger.info(f"API Exit: {response}") return response
def execute(request) -> Dict[str, Any]: """与えられたセッションIDに該当する仮登録レコードを削除します。 Arguments: request -- POST リクエスト { // 仮登録テーブルに紐づけられたセッションID "session_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } Returns: Dict[str, Any] -- 処理結果 { // 仮登録のキャンセルに成功したかどうか "success": False or True, // 仮登録のキャンセルに失敗した原因を表すメッセージ "message": "..." } """ logger.info(f"API Called.") # リクエストパラメーター取り出し session_id = request.json["session_id"] logger.info(f"仮登録セッションID: [{session_id}]") with common.create_session() as session: # 仮登録テーブルの該当レコードを取得 try: target_temporary_product = session \ .query(TemporaryProduct) \ .filter(TemporaryProduct.session_id == session_id) \ .one() except NoResultFound: response = { "success": False, "message": f"指定されたセッションIDから仮登録テーブル上の該当するレコードを特定できませんでした: {session_id}" } logger.info(f"API Exit: {response}") return response # 仮登録テーブルから該当レコードを削除 session.delete(target_temporary_product) session.commit() response = { "success": True, "message": None, } logger.info(f"API Exit: {response}") return response
def emergency() -> Response: """システムモードの 停止 or 再開 状態を反転させます。 Returns: Response -- application/json = { valid: 0 or 1, // 反転後のステート番号 action: "停止" or "再開", // 反転後のステート名 } """ from model.app_state import AppState logger.info(f"[emergency] API Called.") with Common.create_session() as session: # 現在のシステムモードを取得 current_state = Common.get_system_mode(session) if current_state is None: message = "システムモードを取得できませんでした。サーバー上のエラーログを確認して下さい。" return jsonify({"valid": None, "action": message}) # システムモードを反転させて更新 if current_state == Common.SYSTEM_MODE_STOP: next_state = Common.SYSTEM_MODE_RUNNING next_state_name = "再開" else: next_state = Common.SYSTEM_MODE_STOP next_state_name = "停止" try: target_state = session \ .query(AppState) \ .filter(AppState.id == Common.SYSTEM_MODE_APP_STATE_ID) \ .one() except NoResultFound: message = f"アプリケーション状態マスター id={1} のレコードが設定されていません。" logger.error(f"[emergency] API Response. :valid={None} " f":action={message}") return jsonify({"valid": None, "action": message}) target_state.state = next_state target_state.modified_time = dt.now() session.commit() logger.info(f"[emergency] API Response. "\ f":valid={next_state} :action={next_state_name}") return jsonify({"valid": next_state, "action": next_state_name})
def execute(request) -> Dict[str, Any]: """呼び出された時点の日付を起点に数えて {days} 日後に賞味期限が切れるものをピックアップしてSlack通知します。 ただし、呼び出された時点の日付から {days} 日後までの間に期限切れとなるものについては対象外となります。 たとえば、days=3 のとき 1日後、2日後に期限が切れるものは取得できず、3日後に期限が切れるものだけが抽出されます。 Arguments: request -- GET リクエスト request.args.get("days"): {int} ピックアップ対象とする呼出時点の日付を起点とした日数 (+で未来、0で当日、-で過去) Returns: Dict[str, Any] -- 処理結果 { // 操作に成功したかどうか "success": False or True, // 抽出した本登録レコードのIDリスト "targets": [...] } """ logger.info(f"API Called.") # クエリー文字列取り出し days = int(request.args.get("days")) with common.create_session() as session: # 指定された日数後に期限が切れるものを抽出 target_date = dt.combine(dt.now(), datetime.time()) + datetime.timedelta(days=days) target_products = session \ .query(Product) \ .filter(Product.expiration_date == target_date) \ .filter(Product.added_shopping_list == 0) \ .filter(Product.consumed == 0) \ .order_by(Product.created_time) \ .all() # Slackにリマインド通知を送信 _push_remind_to_slack(target_products, days) response = { "success": True, "targets": [product.id for product in target_products], } logger.info(f"API Exit: {response}") return response
def execute(request) -> Dict[str, Any]: """本登録済みの商品のうち、期限前で未アクションの商品イメージ画像と賞味期限をすべてSlack通知します。 Slackコマンドへの応答速度を優先するため、実際の通知は非同期的に行います。 Arguments: request -- POST リクエスト Returns: Dict[str, Any] -- 処理結果 { // 表示メッセージ "text": "..." } """ logger.info(f"API Called.") with common.create_session() as session: # 期限前で未アクションの商品をすべて抽出 target_date = dt.combine(dt.now(), datetime.time()) products = session \ .query(Product) \ .filter(Product.expiration_date >= target_date) \ .filter(Product.added_shopping_list == 0) \ .filter(Product.consumed == 0) \ .order_by(Product.expiration_date) \ .order_by(Product.created_time) \ .all() # 非同期的にSlackに情報を送信 _send_products_async(products) if len(products) > 0: message = f"現在、{len(products)}件の商品が管理されています。" else: message = f"現在管理されている商品はありません。" response = { "text": message, } logger.info(f"API Exit: {response}") return response
def log(): """指定期間、および日当たりそれぞれの時間帯におけるすべてのトイレの使用回数を表す Chart.js グラフ用データを返します。 このAPIでは、ドアが閉じられた回数をもとに集計します。 Arguments: begin_date {str} -- 期間開始日 (%Y%m%d) end_date {str} -- 期間終了日 (%Y%m%d) begin_hours_per_day {int} -- 日当たりの集計始端時間 (0-23) end_hours_per_day {int} -- 日当たりの集計終端時間 (0-23) step_hours {int} -- 期間内におけるサンプリング間隔 (1-24)、始端時間から終端時間の差をこの値で割り切れない場合は割り切れる時間まで延長します。 Returns: Response -- application/json = { "success": True of False, "message": 補足メッセージ, // エラー発生時のみ。正常完了時は空文字 "graphs": [ { "type": "bar", "data": { "labels": [ "2019-01-01 00:00", "2019-01-01 00:05", ..., "2019-01-02 00:00", ... ], "datasets": [ { "label": "4F 男性用トイレ", // トイレグループ単位 "data": [0, 1, 1, ...] // それぞれの単位時間内におけるオープン、クローズのイベント数合計値 } ] } }, ... ] } """ end_date_default = dt.now() begin_date_default = end_date_default - datetime.timedelta(days=10) begin_date = request.args.get( "begin_date", dt.strftime(begin_date_default, Common.PARAM_DATETIME_FORMAT)) end_date = request.args.get( "end_date", dt.strftime(end_date_default, Common.PARAM_DATETIME_FORMAT)) begin_hours_per_day = request.args.get("begin_hours_per_day", 10, type=int) end_hours_per_day = request.args.get("end_hours_per_day", 19, type=int) step_hours = request.args.get("step_hours", 2, type=int) from model.toilet import Toilet from model.toilet_group import ToiletGroup from model.toilet_group_map import ToiletGroupMap from model.toilet_status import ToiletStatus logger.info(f"[log] API Called. "\ f":begin_date={begin_date} :end_date={end_date} "\ f":begin_hours_per_day={begin_hours_per_day} :end_hours_per_day={end_hours_per_day} "\ f":step_hours={step_hours}") # パラメーター形式変換 begin_datetime = dt.strptime(begin_date, Common.PARAM_DATETIME_FORMAT) end_datetime = dt.strptime(end_date, Common.PARAM_DATETIME_FORMAT) if end_datetime < begin_datetime: # 始端日と終端日の指定が逆になっていると判断 temp = begin_datetime begin_datetime = end_datetime end_datetime = temp logger.warning(f"[log] API Parameter Check. :begin_datetime={end_datetime}->{begin_datetime} "\ f":end_datetime={begin_datetime}->{end_datetime}") if end_hours_per_day < begin_hours_per_day: # 始端時間と終端時間の指定が逆になっていると判断 temp = begin_hours_per_day begin_hours_per_day = end_hours_per_day end_hours_per_day = temp logger.warning(f"[log] API Parameter Check. :begin_hours_per_day={end_hours_per_day}->{begin_hours_per_day} "\ f":end_hours_per_day={begin_hours_per_day}->{end_hours_per_day}") if (end_hours_per_day - begin_hours_per_day) < step_hours: # サンプリング間隔は始端時間と終端時間の差を超えることはできない: 日単位とする raw_step_hours = step_hours step_hours = end_hours_per_day - begin_hours_per_day logger.warning( f"[log] API Parameter Check. :step_hours={raw_step_hours}->{step_hours}" ) elif step_hours <= 0: # サンプリング間隔が正しくない場合はデフォルトで3時間刻みとする raw_step_hours = step_hours step_hours = 3 logger.warning( f"[log] API Parameter Check. :step_hours={raw_step_hours}->{step_hours}" ) with Common.create_session() as session: # トイレマスターを取得 toilets = session \ .query( \ Toilet, ToiletGroupMap ) \ .outerjoin(ToiletGroupMap, Toilet.id == ToiletGroupMap.toilet_id) \ .all() # トイレグループマスターを取得: トイレへの紐付け情報も合わせて取得 grouped_toilets = session \ .query( ToiletGroup, ToiletGroupMap, func.count().label("max") ) \ .outerjoin(ToiletGroupMap, ToiletGroup.id == ToiletGroupMap.toilet_group_id) \ .order_by(asc(ToiletGroup.id)) \ .group_by(ToiletGroup.id) \ .all() current_state = Common.get_system_mode(session) if current_state is None: message = "システムモードを取得できませんでした。サーバー上のエラーログを確認して下さい。" return jsonify({"success": False, "message": message}) if current_state == Common.SYSTEM_MODE_STOP: message = "システムモードが停止状態です。入退室ログは返却しません。" logger.info(f"[log] API Response. :success={False} "\ f":message={message}") return jsonify({"success": False, "message": message}) # 横軸ラベルを生成 graphs = [] labels = [] target_begin_and_end_pairs = [] current_begin_hours = begin_hours_per_day current_end_hours = current_begin_hours + step_hours current_datetime = begin_datetime while current_datetime < end_datetime: current_begin_datetime = current_datetime + datetime.timedelta( hours=current_begin_hours) current_end_datetime = current_datetime + datetime.timedelta( hours=current_end_hours) target_begin_and_end_pairs.append({ "begin": current_begin_datetime, "end": current_end_datetime }) labels.append( f"{dt.strftime(current_begin_datetime, '%m-%d %H:%M')}~"\ f"{dt.strftime(current_end_datetime, '%H:%M')}" ) if end_hours_per_day <= current_end_hours: # 次の日へ回す current_begin_hours = begin_hours_per_day current_datetime += datetime.timedelta(days=1) else: current_begin_hours += step_hours current_end_hours = current_begin_hours + step_hours # 系列ごとにグラフデータを分けて作成 for i, series_toilet in enumerate(grouped_toilets): graph = { "type": "bar", "data": { "labels": labels, "datasets": [{ "label": series_toilet.ToiletGroup.name, "data": [] }] } } # この系列に属するトイレマスターのレコードを抽出 target_toilets_id_list = [ x.Toilet.id for x in toilets if x.ToiletGroupMap.toilet_group_id == series_toilet.ToiletGroupMap.toilet_group_id ] # この系列に属するトイレ入退室トランザクションテーブルで区間内に該当するレコードを抽出 target_statuses_all = session \ .query(ToiletStatus) \ .filter( begin_datetime <= ToiletStatus.created_time, ToiletStatus.created_time < end_datetime, ToiletStatus.toilet_id.in_(target_toilets_id_list) ) \ .order_by(asc(ToiletStatus.created_time)) \ .all() # 先頭のサンプリング時間から順にドアクローズイベントの件数を抽出していく data = [] start_index = 0 for one_of_begin_and_end_pairs in target_begin_and_end_pairs: sampling_count = 0 for n, target_status in enumerate( target_statuses_all[start_index:]): if one_of_begin_and_end_pairs["begin"] <= target_status.created_time and \ target_status.created_time < one_of_begin_and_end_pairs["end"]: # 対象区間内のレコードであればカウント sampling_count += 1 elif one_of_begin_and_end_pairs[ "end"] <= target_status.created_time: # 対象区間から出た時点で抜ける start_index = n break data.append(sampling_count) # 出来上がったグラフデータをグラフリストに格納 graph["data"]["datasets"][0]["data"] = data graphs.append(graph) # API側はデータを返すだけで、見た目やオプションはクライアント側で付加してもらうポリシーとする result = {"success": True, "message": "", "graphs": graphs} logger.info( f"[log] API Response. :success={True} :graphs_length={len(graphs)}") return jsonify(result)
def execute(request) -> Dict[str, Any]: """本登録済みの商品に対して任意のアクションを行います。 Arguments: request -- POST リクエスト { "callback_id": "command_xxx", // SlackのAttachmentsに設定されたコールバックID、xxxの部分は本登録テーブル上のID "actions": [ { "name": "command", "value": "...", // used: 既に消費したものとして扱う // shopppinglist: Slackの買い物リストチャンネルに追記して以後通知の対象から外す // remind: 賞味期限が切れたときに再通知する ... }, ], ... } Returns: Dict[str, Any] -- 処理結果 { // 操作に成功したかどうか "success": False or True, // 操作に失敗した原因を表すメッセージ "message": "..." } """ logger.info(f"API Called.") # URLエンコードされた特殊なペイロードを辞書型に変換 payload = common.decode_request_payload(request) logger.debug(f"payload={payload}") # リクエストパラメーター取り出し request_json = json.loads(payload) action = request_json["actions"][0]["value"] original_message = request_json["original_message"] callback_id = request_json["callback_id"] product_id = re.match(r"command_(\d+)", callback_id).groups()[0] attachment_index = [ i for i, attachment in enumerate(request_json["original_message"] ["attachments"]) if attachment["callback_id"] == callback_id ][0] logger.debug(f"attachments[{attachment_index}]: callback_id={callback_id}") with common.create_session() as session: try: product = session \ .query(Product) \ .filter(Product.id == product_id) \ .one() except NoResultFound: response = f"指定された本登録IDに該当するレコードを特定できませんでした: {product_id}" logger.info(f"API Exit: {response}") return response if action == "used": # 既に消費したので以後通知の対象としない product.consumed = 1 elif action == "remind": # 特に何もしない pass elif action == "shoppinglist": # 買い物リストチャンネルに投稿して以後通知の対象としない try: _add_shopping_list(product.image_path) except HTTPError as e: response = f"買い物リストチャンネルへの投稿に失敗しました" logger.exception(response, e) logger.info(f"API Exit: {response}") return response product.added_shopping_list = 1 else: response = f"無効な操作名が指定されました: {action}" logger.info(f"API Exit: {response}") return response session.commit() # コマンドの元となったメッセージのうち今回処理したデータを抜いて返す del original_message["attachments"][attachment_index] if len(original_message["attachments"]) > 0: # まだ他のリマインドが残っていたら元のメッセージを置き換える response = { "text": original_message["text"], "attachments": original_message["attachments"], "replace_original": True, } else: # すべてのリマインドを処理し終わったら元のメッセージを消す response = { "delete_original": True, } logger.info(f"API Exit: {response}") return response
def log(): """指定期間、および日当たりそれぞれの時間帯におけるすべてのトイレの使用回数を表す Chart.js グラフ用データを返します。 このAPIでは、ドアが閉じられた回数をもとに集計します。 Arguments: begin_date {str} -- 期間開始日 (%Y%m%d) end_date {str} -- 期間終了日 (%Y%m%d) begin_hours_per_day {int} -- 日当たりの集計始端時間 (0-23) end_hours_per_day {int} -- 日当たりの集計終端時間 (0-23) step_hours {int} -- 期間内におけるサンプリング間隔 (1-24)、始端時間から終端時間の差をこの値で割り切れない場合は割り切れる時間まで延長します。 Returns: Response -- application/json = { "success": True of False, "message": 補足メッセージ, // エラー発生時のみ。正常完了時は空文字 "graphs": [ { "type": "bar", "data": { "labels": [ "2019-01-01 00:00", "2019-01-01 00:05", ..., "2019-01-02 00:00", ... ], "datasets": [ { "label": "4F 男性用トイレ - N時間あたりの使用頻度", // トイレグループ単位 "data": [0, 1, 1, ...] // それぞれの単位時間内におけるドアクローズのイベント数合計値 } ] } }, ..., { "type": "bar", "data": { "labels": [ 省略 ], "datasets": [ { "label": "4F 男性用トイレ - N時間あたりの占有率", // トイレグループ単位 "data": [0.25, 1.0, 0, ...] // それぞれの単位時間内におけるオープン、クローズのイベント数合計値 } ] } }, ... ] } """ end_date_default = dt.now() begin_date_default = end_date_default - datetime.timedelta(days=10) begin_date = request.args.get( "begin_date", dt.strftime(begin_date_default, Common.PARAM_DATETIME_FORMAT)) end_date = request.args.get( "end_date", dt.strftime(end_date_default, Common.PARAM_DATETIME_FORMAT)) begin_hours_per_day = request.args.get("begin_hours_per_day", 10, type=int) end_hours_per_day = request.args.get("end_hours_per_day", 19, type=int) step_hours = request.args.get("step_hours", 2, type=int) from model.toilet import Toilet from model.toilet_group import ToiletGroup from model.toilet_group_map import ToiletGroupMap from model.toilet_status import ToiletStatus logger.info(f"[log] API Called. "\ f":begin_date={begin_date} :end_date={end_date} "\ f":begin_hours_per_day={begin_hours_per_day} :end_hours_per_day={end_hours_per_day} "\ f":step_hours={step_hours}") # パラメーター形式変換 begin_datetime = dt.strptime(begin_date, Common.PARAM_DATETIME_FORMAT) end_datetime = dt.strptime(end_date, Common.PARAM_DATETIME_FORMAT) if end_datetime < begin_datetime: # 始端日と終端日の指定が逆になっていると判断 temp = begin_datetime begin_datetime = end_datetime end_datetime = temp logger.warning(f"[log] API Parameter Check. :begin_datetime={end_datetime}->{begin_datetime} "\ f":end_datetime={begin_datetime}->{end_datetime}") if end_hours_per_day < begin_hours_per_day: # 始端時間と終端時間の指定が逆になっていると判断 temp = begin_hours_per_day begin_hours_per_day = end_hours_per_day end_hours_per_day = temp logger.warning(f"[log] API Parameter Check. :begin_hours_per_day={end_hours_per_day}->{begin_hours_per_day} "\ f":end_hours_per_day={begin_hours_per_day}->{end_hours_per_day}") if (end_hours_per_day - begin_hours_per_day) < step_hours: # サンプリング間隔は始端時間と終端時間の差を超えることはできない: 日単位とする raw_step_hours = step_hours step_hours = end_hours_per_day - begin_hours_per_day logger.warning( f"[log] API Parameter Check. :step_hours={raw_step_hours}->{step_hours}" ) elif step_hours <= 0: # サンプリング間隔が正しくない場合はデフォルトで3時間刻みとする raw_step_hours = step_hours step_hours = 3 logger.warning( f"[log] API Parameter Check. :step_hours={raw_step_hours}->{step_hours}" ) # 日付計算の都合上、終端日時は 24:00 とする end_datetime = end_datetime + datetime.timedelta(hours=24) with Common.create_session() as session: # トイレマスターを取得 toilets = session \ .query( \ Toilet, ToiletGroupMap ) \ .outerjoin(ToiletGroupMap, Toilet.id == ToiletGroupMap.toilet_id) \ .all() # トイレグループマスターを取得: トイレへの紐付け情報も合わせて取得 grouped_toilets = session \ .query( ToiletGroup, ToiletGroupMap, func.count().label("max") ) \ .outerjoin(ToiletGroupMap, ToiletGroup.id == ToiletGroupMap.toilet_group_id) \ .order_by(asc(ToiletGroup.id)) \ .group_by(ToiletGroup.id) \ .all() current_state = Common.get_system_mode(session) if current_state is None: message = "システムモードを取得できませんでした。サーバー上のエラーログを確認して下さい。" return jsonify({"success": False, "message": message}) if current_state == Common.SYSTEM_MODE_STOP: message = "システムモードが停止状態です。入退室ログは返却しません。" logger.info(f"[log] API Response. :success={False} "\ f":message={message}") return jsonify({"success": False, "message": message}) ##### 使用頻度 (抽出開始時刻よりも前から継続して入室中だったデータはカウント対象に含まれないので注意) ##### # 横軸ラベルを生成 graphs = [] labels = [] target_begin_and_end_pairs = [] current_begin_hours = begin_hours_per_day current_end_hours = current_begin_hours + step_hours current_datetime = begin_datetime while current_datetime < end_datetime: current_begin_datetime = current_datetime + datetime.timedelta( hours=current_begin_hours) current_end_datetime = current_datetime + datetime.timedelta( hours=current_end_hours) target_begin_and_end_pairs.append({ "begin": current_begin_datetime, "end": current_end_datetime }) labels.append( f"{dt.strftime(current_begin_datetime, '%m-%d %H:%M')}~"\ f"{dt.strftime(current_end_datetime, '%H:%M')}" ) if end_hours_per_day <= current_end_hours: # 次の日へ回す current_begin_hours = begin_hours_per_day current_datetime += datetime.timedelta(days=1) else: current_begin_hours += step_hours current_end_hours = current_begin_hours + step_hours # 系列ごとにグラフデータを分けて作成 for i, series_toilet in enumerate(grouped_toilets): graph = { "type": "bar", "data": { "labels": labels, "datasets": [{ "label": f"{series_toilet.ToiletGroup.name} - {step_hours}時間あたりの使用頻度", "data": [] }] } } # この系列に属するトイレマスターのレコードを抽出 target_toilets_id_list = [ x.Toilet.id for x in toilets if x.ToiletGroupMap.toilet_group_id == series_toilet.ToiletGroupMap.toilet_group_id ] # この系列に属するトイレ入退室トランザクションテーブルで区間内に該当するレコードを抽出 target_statuses_all = session \ .query(ToiletStatus) \ .filter( begin_datetime <= ToiletStatus.created_time, ToiletStatus.created_time < end_datetime, ToiletStatus.toilet_id.in_(target_toilets_id_list) ) \ .order_by(asc(ToiletStatus.created_time)) \ .all() # 先頭のサンプリング時間から順にドアクローズイベントの件数を抽出していく data_frequency = [] start_index = 0 for one_of_begin_and_end_pairs in target_begin_and_end_pairs: sampling_count = 0 for n, target_status in enumerate( target_statuses_all[start_index:]): if one_of_begin_and_end_pairs["begin"] <= target_status.created_time and \ target_status.created_time < one_of_begin_and_end_pairs["end"] and \ target_status.is_closed: # 対象区間内のレコードであればカウント sampling_count += 1 elif one_of_begin_and_end_pairs[ "end"] <= target_status.created_time: # 対象区間から出た時点で抜ける start_index = n break data_frequency.append(sampling_count) # 出来上がったグラフデータをグラフリストに格納 graph["data"]["datasets"][0]["data"] = data_frequency graphs.append(graph) ##### 占有率 (抽出開始時刻よりも前から継続して入室中だったデータはカウント対象に含まれないので注意) ##### # 系列ごとにグラフデータを分けて作成 for i, series_toilet in enumerate(grouped_toilets): graph = { "type": "line", "data": { "labels": labels, "datasets": [{ "label": f"{series_toilet.ToiletGroup.name} - {step_hours}時間あたりの占有率", "data": [] }] } } # この系列に属するトイレマスターのレコードを抽出 target_toilets_id_list = [ x.Toilet.id for x in toilets if x.ToiletGroupMap.toilet_group_id == series_toilet.ToiletGroupMap.toilet_group_id ] # この系列に属するトイレ入退室トランザクションテーブルで区間内に該当するレコードを抽出 target_statuses_all = session \ .query(ToiletStatus) \ .filter( begin_datetime <= ToiletStatus.created_time, ToiletStatus.created_time < end_datetime, ToiletStatus.toilet_id.in_(target_toilets_id_list) ) \ .order_by(asc(ToiletStatus.created_time)) \ .all() data_occupancy = [] start_index = 0 # 個室1室あたりが最大占有率になる時間秒数 max_occupancy_time = step_hours * 60 * 60 # トイレIDごとに直前に入室した時刻を記憶できるようにしておく temp_last_start_times = {x: None for x in target_toilets_id_list} # 先頭のサンプリング時間から順に抽出していく for m, one_of_begin_and_end_pairs in enumerate( target_begin_and_end_pairs): # 抽出対象区間をまたいで占有している場合は現在の抽出開始時刻に合わせておく temp_last_start_times = { x: None if temp_last_start_times[x] is None else one_of_begin_and_end_pairs["begin"] for x in target_toilets_id_list } # 抽出対象区間内における個室ごとの占有時間累計秒数 occupied_times = {x: 0 for x in target_toilets_id_list} for n, target_status in enumerate( target_statuses_all[start_index:]): if one_of_begin_and_end_pairs["begin"] <= target_status.created_time and \ target_status.created_time < one_of_begin_and_end_pairs["end"]: if target_status.is_closed: # 対象区間内の入室記録にヒットしたら占有時間の計算を開始する temp_last_start_times[ target_status. toilet_id] = target_status.created_time elif temp_last_start_times[ target_status.toilet_id] is not None: # 対象区間内の退室記録にヒットしたら占有時間の計算を完了する time_delta = target_status.created_time - temp_last_start_times[ target_status.toilet_id] occupied_times[ target_status. toilet_id] += time_delta.total_seconds() temp_last_start_times[ target_status.toilet_id] = None elif one_of_begin_and_end_pairs[ "end"] <= target_status.created_time: # 抽出対象区間から抜けたらその時間の集計を完了する start_index = n # 抽出対象区間の終端まで入室中だったものはその終端で区切る for id, last_start_time in temp_last_start_times.items( ): if last_start_time is not None: time_delta = one_of_begin_and_end_pairs[ "end"] - temp_last_start_times[ target_status.toilet_id] occupied_times[ target_status. toilet_id] += time_delta.total_seconds() # NOTE: 次の抽出対象区間に回った後にその抽出開始時刻で占有時間の計算を開始する break # トイレグループ内の個室ごとの占有時間を理論上のMAX占有時間で割った割合について、トイレグループ内の個室全体で相加平均したものをこのトイレグループの占有率とする data_occupancy.append( sum([(occupied_time / max_occupancy_time) for x, occupied_time in occupied_times.items()]) / len(occupied_times)) # 出来上がったグラフデータをグラフリストに格納 graph["data"]["datasets"][0]["data"] = data_occupancy graphs.append(graph) # API側はデータを返すだけで、見た目やオプションはクライアント側で付加してもらうポリシーとする result = {"success": True, "message": "", "graphs": graphs} logger.info( f"[log] API Response. :success={True} :graphs_length={len(graphs)}") return jsonify(result)
def status(): """現在のトイレ在室状況を返します。 なお、システム停止モードに移行している間はすべて success=False として返します。 Returns: Response -- application/json = { success: True or False, message: "エラーメッセージ", // エラー発生時のみ。正常完了時は空文字 status: [ { name: "4F 男性用トイレ" // トイレグループの名称 valid: True or False, // このトイレグループの状況を取得できたかどうか used: 1, // このトイレグループにおける現在の使用数 max: 2, // このトイレグループの部屋数 rate100: 0-100, // このトイレグループの使用率% (同じ名前のトイレを合算した使用率%) details: [ // このトイレグループの仔細 { name: "4F 男性用トイレ (洋式)", // このトイレの名称 used: True or False, // このトイレが現在使用中であるかどうか valid: True or False, // このトイレが現在トラブルが起きていない状態であるかどうか }, ... ], }, ... ] } """ from model.toilet import Toilet from model.toilet_group import ToiletGroup from model.toilet_group_map import ToiletGroupMap logger.info(f"[status] API Called.") with Common.create_session() as session: current_state = Common.get_system_mode(session) if current_state is None: message = "システムモードを取得できませんでした。サーバー上のエラーログを確認して下さい。" return jsonify({"success": False, "message": message}) if current_state == Common.SYSTEM_MODE_STOP: message = "現在システムモード「停止」のため、現況を取得できません。"\ "現況を取得するためにはシステムモード「再開」に切り替えて下さい。" logger.info(f"[status] API Response. :success={False} "\ f":message={message}") return jsonify({"success": False, "message": message}) # トイレグループマスターを取得: トイレへの紐付け情報も合わせて取得 toilet_groups = session \ .query( ToiletGroup, ToiletGroupMap, func.count().label("max") ) \ .outerjoin(ToiletGroupMap, ToiletGroup.id == ToiletGroupMap.toilet_group_id) \ .order_by(asc(ToiletGroup.id)) \ .group_by(ToiletGroup.id) \ .all() # トイレマスターを取得: トイレグループへの紐付け情報も合わせて取得 toilets = session \ .query( Toilet, ToiletGroupMap, ) \ .outerjoin(ToiletGroupMap, Toilet.id == ToiletGroupMap.toilet_id) \ .order_by(asc(Toilet.id)) \ .all() # 返却用の形式に変換 result = {"status": []} for i, toilet_group in enumerate(toilet_groups): result["status"].append({ "name": toilet_group.ToiletGroup.name, "valid": toilet_group.ToiletGroup.valid, "max": toilet_group.max }) result["status"][-1]["used"] = 0 result["status"][-1]["rate100"] = 0 result["status"][-1]["details"] = [] if not toilet_group.ToiletGroup.valid: # このトイレグループ全体が無効になっている continue if toilet_group.max == 0: # このトイレグループに紐づくトイレが存在しない continue # このグループ内の個々のトイレの仔細をデータに加える for n, toilet in enumerate(toilets): if toilet.ToiletGroupMap.toilet_group_id != toilet_group.ToiletGroup.id: continue if toilet.Toilet.is_closed: result["status"][-1]["used"] += 1 result["status"][-1]["details"].append({ "name": toilet.Toilet.name, "used": toilet.Toilet.is_closed, "valid": toilet.Toilet.valid }) result["status"][-1]["rate100"] = \ int(result["status"][-1]["used"] / toilet_group.max * 100) result["success"] = True result["message"] = "" logger.info( f"[status] API Response. :success={True} :status_length={len(result['status'])}" ) return jsonify(result)
def execute(request) -> Dict[str, Any]: """JPEG形式の画像を整数の配列で表したデータをもとに賞味期限を抽出し、仮登録テーブルに保存します。 Arguments: request -- POST リクエスト { // JPEG圧縮した画像を uint8 配列で並べたデータ "image": [uint8, uint8, ...] } Returns: Dict[str, Any] -- 処理結果 { // 解析に成功したかどうか "success": False or True, // 賞味期限として抽出された年月日 "expiration_date": { "year": yyyy, "month": mm, "day": dd }, // 仮登録テーブルに紐づけられたセッションID "session_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", // 解析に失敗した原因を表すメッセージ "message": "..." } """ logger.info(f"API Called.") # リクエストパラメーター取り出し image = common.convert_request_image_to_ndarray(request.json["image"]) # [デバッグ用] リクエスト画像をファイルに書き出し # cv2.imwrite("./target.jpg", image) # 画像からテキストを解析 response_json = _call_ocr(image) # [デバッグ用] 解析結果をファイルに書き出し # with open("./response.json", "w") as w: # w.write(json.dumps(response_json, ensure_ascii=False, indent=4)) # OCRによって得られた文字列を取り出す found_text = _extract_text_from_ocr_result(response_json) logger.debug(f"OCRから得られたテキスト: [{found_text}]") if found_text == "": response = { "success": False, "expiration_date": None, "session_id": None, "message": f"画像内にテキストが含まれていませんでした", } logger.info(f"API Exit: {response}") return response # 得られた文字列から賞味期限に相当する箇所を解析 expiration_date = _find_expiration_date(found_text) is_success = expiration_date is not None session_id = None message = None if is_success: # 仮登録テーブルにセッションIDと賞味期限のペアを格納 session_id = str(uuid.uuid4()).replace("-", "") with common.create_session() as session: session.add(TemporaryProduct( session_id=session_id, expiration_date=dt.strptime(f"{expiration_date['year']}-{expiration_date['month']:02}-{expiration_date['day']:02}", "%Y-%m-%d"), created_time=dt.now() )) session.commit() message = "賞味期限の取り出しに成功しました" else: message = "OCRによって得られたテキストから賞味期限を抽出できませんでした" response = { "success": is_success, "expiration_date": expiration_date, "session_id": session_id, "message": None if is_success else message, } logger.info(f"API Exit: {response}") return response
def execute(request) -> Dict[str, Any]: """JPEG形式の画像を整数の配列で表したデータとセッションIDを紐づけて本登録を行います。 Arguments: request -- POST リクエスト { // 仮登録テーブルに紐づけられたセッションID "session_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", // JPEG圧縮した画像を uint8 配列で並べたデータ "image": [uint8, uint8, ...] } Returns: Dict[str, Any] -- 処理結果 { // 本登録に成功したかどうか "success": False or True, // 本登録に失敗した原因を表すメッセージ "message": "..." } """ logger.info(f"API Called.") # リクエストパラメーター取り出し session_id = request.json["session_id"] image = common.convert_request_image_to_ndarray(request.json["image"]) logger.info(f"仮登録セッションID: [{session_id}]") # セッションIDのファイル名でサーバーに商品画像を保管する if not os.path.exists(DESTINATION_DIRECTORY_PATH): os.makedirs(DESTINATION_DIRECTORY_PATH, exist_ok=True) image_path = os.path.join(DESTINATION_DIRECTORY_PATH, f"{session_id}.jpg") cv2.imwrite(image_path, image) with common.create_session() as session: # 仮登録テーブルの該当レコードを取得 try: target_temporary_product = session \ .query(TemporaryProduct) \ .filter(TemporaryProduct.session_id == session_id) \ .one() except NoResultFound: response = { "success": False, "message": f"指定されたセッションIDから仮登録テーブル上の該当するレコードを特定できませんでした: {session_id}", } logger.info(f"API Exit: {response}") return response # 本登録テーブルに追加 session.add(Product( image_path=image_path, expiration_date=target_temporary_product.expiration_date, consumed=False, added_shopping_list=False, created_time=dt.now() )) # 仮登録テーブルから該当レコードを削除 session.delete(target_temporary_product) session.commit() response = { "success": True, "message": None, } logger.info(f"API Exit: {response}") return response
def open() -> Response: """トイレのドアが開いたことを記録します。 Arguments: toilet_id {int} -- ターゲットトイレID Returns: Response -- application/json = { success: True or False, message: 補足メッセージ, } """ toilet_id = request.json["toilet_id"] from model.app_state import AppState from model.toilet import Toilet from model.toilet_status import ToiletStatus logger.info(f"[open] API Called. :toilet_id={toilet_id}") with Common.create_session() as session: current_state = Common.get_system_mode(session) if current_state is None: message = "システムモードを取得できませんでした。サーバー上のエラーログを確認して下さい。" return jsonify({"success": False, "message": message}) if current_state == Common.SYSTEM_MODE_STOP: message = "システムモードが停止状態です。すべての入退室ログは記録されません。" logger.info(f"[open] API Response. :success={False} "\ f":message={message}") return jsonify({"success": False, "message": message}) # 現在の在室状況を取得 try: is_closed = session \ .query(Toilet.is_closed) \ .filter(Toilet.id == toilet_id) \ .one() \ .is_closed except NoResultFound: message = f"トイレ #{toilet_id} が見つかりません。トイレマスター上のID設定とAPI呼び出し元のIDが合致することを確認して下さい。" logger.error(f"[open] API Response. :success={False} "\ f":message={message}") return jsonify({"success": False, "message": message}) if not is_closed: message = f"トイレ #{toilet_id} は既に空室です。重複防止のため退室ログは記録されません。" logger.info(f"[open] API Response. :success={False} "\ f":message={message}") return jsonify({"success": False, "message": message}) # 現在の在室状況を更新 try: target_toilet = session \ .query(Toilet) \ .filter(Toilet.id == toilet_id) \ .one() except NoResultFound: message = f"トイレ #{toilet_id} が見つかりません。トイレマスター上のID設定とAPI呼び出し元のIDが合致することを確認して下さい。" logger.error(f"[open] API Response. :success={False} "\ f":message={message}") return jsonify({"success": False, "message": message}) # 前回更新からの経過時間を算出 timedelta = dt.now() - target_toilet.modified_time if timedelta.total_seconds() < MIN_DOOR_EVENT_SPAN_SECONDS: message = f"トイレ #{toilet_id} は {MIN_DOOR_EVENT_SPAN_SECONDS} 秒以内に更新されています。"\ f"過剰反応防止のため、再度時間を置いてから呼び出して下さい。" logger.warning(f"[open] API Response. :success={False} "\ f":message={message}") return jsonify({"success": False, "message": message}) target_toilet.is_closed = False target_toilet.modified_time = dt.now() session.add(target_toilet) # トイレのドアが開いたことを表すイベントをトランザクションテーブルに追加 session.add( ToiletStatus(toilet_id=toilet_id, is_closed=False, created_time=dt.now())) session.commit() message = f"トイレ #{toilet_id} が空室になりました。" logger.info(f"[open] API Response. :success={True} "\ f":message={message}") return jsonify({"success": True, "message": message})