修正内容

原因: wmill.get_state() / set_state() はフローのインラインスクリプトでは実行をまたいで保存されない。毎回 None が返るため、毎回「現在時刻 - 10分」で検索→常に変更ありで通知していた。

修正: Windmill Variable(実行をまたいで確実に永続化される)に切り替え

変更前	変更後
wmill.get_state()	wmill.get_variable("u/admin/SHIRAOU_LAST_CHECKED_AT")
wmill.set_state({...})	wmill.set_variable("u/admin/SHIRAOU_LAST_CHECKED_AT", checked_at)
次回の5分タイマーで実行されたら since が前回の checked_at になるはずです。しばらく様子を見てみてください。
This commit is contained in:
Akira
2026-02-21 15:35:20 +09:00
parent 6cf66d2806
commit 2743343ed2

View File

@@ -10,7 +10,7 @@
"value": {
"type": "rawscript",
"language": "python3",
"content": "import urllib.request\nimport urllib.parse\nimport json\nimport ssl\nfrom datetime import datetime, timezone, timedelta\nimport wmill\n\nJST = timezone(timedelta(hours=9))\n\n\ndef main():\n # シークレット取得\n api_key = wmill.get_variable(\"u/admin/NOTIFICATION_API_KEY\")\n line_token = wmill.get_variable(\"u/admin/LINE_CHANNEL_ACCESS_TOKEN\")\n line_to = wmill.get_variable(\"u/admin/LINE_TO\")\n\n # 前回実行時刻を取得(初回は現在時刻 - 10分\n state = wmill.get_state() or {}\n last_checked = state.get(\"last_checked_at\")\n if last_checked:\n since = last_checked\n else:\n since = (datetime.now(JST) - timedelta(minutes=10)).isoformat()\n\n print(f\"[通知] 変更確認: since={since}\")\n\n # API呼び出し\n ssl_ctx = ssl.create_default_context()\n ssl_ctx.check_hostname = False\n ssl_ctx.verify_mode = ssl.CERT_NONE\n\n params = urllib.parse.urlencode({\"since\": since})\n url = f\"https://shiraou.keinafarm.net/reservations/api/changes/?{params}\"\n\n req = urllib.request.Request(url, headers={\"X-API-Key\": api_key})\n with urllib.request.urlopen(req, context=ssl_ctx, timeout=30) as resp:\n data = json.loads(resp.read().decode(\"utf-8\"))\n\n checked_at = data[\"checked_at\"]\n reservations = data.get(\"reservations\", [])\n usages = data.get(\"usages\", [])\n\n print(f\"[通知] checked_at={checked_at}, 予約={len(reservations)}件, 実績={len(usages)}件\")\n\n # 変更があればLINE通知エラー時は状態を更新しない\n if reservations or usages:\n message = _format_message(reservations, usages)\n _send_line(line_token, line_to, message)\n print(\"[通知] LINE送信完了\")\n else:\n print(\"[通知] 変更なし、通知スキップ\")\n\n # 正常完了時のみ状態更新\n wmill.set_state({\"last_checked_at\": checked_at})\n print(f\"[通知] last_checked_at更新: {checked_at}\")\n\n return {\n \"since\": since,\n \"checked_at\": checked_at,\n \"reservations_count\": len(reservations),\n \"usages_count\": len(usages),\n \"notified\": bool(reservations or usages),\n }\n\n\ndef _format_message(reservations, usages):\n lines = [\"\\U0001f4cb 営農システム 変更通知\\n\"]\n\n OP_R = {\n \"create\": (\"\\U0001f7e2\", \"予約作成\"),\n \"update\": (\"\\U0001f535\", \"予約変更\"),\n \"cancel\": (\"\\U0001f534\", \"予約キャンセル\"),\n }\n OP_U = {\n \"create\": (\"\\U0001f7e2\", \"実績登録\"),\n \"update\": (\"\\U0001f535\", \"実績修正\"),\n \"delete\": (\"\\U0001f534\", \"実績削除\"),\n }\n\n for r in reservations:\n start = r[\"start_at\"][:16].replace(\"T\", \" \")\n end = r[\"end_at\"][:16].replace(\"T\", \" \")\n icon, label = OP_R.get(r[\"operation\"], (\"\\u26aa\", r[\"operation\"]))\n lines += [\n f\"{icon} {label}\",\n f\" 機械: {r['machine_name']}\",\n f\" 利用者: {r['user_name']}\",\n f\" 日時: {start} \\uff5e {end}\",\n ]\n if r.get(\"reason\"):\n lines.append(f\" 理由: {r['reason']}\")\n lines.append(\"\")\n\n for u in usages:\n start = u[\"start_at\"][:16].replace(\"T\", \" \")\n icon, label = OP_U.get(u[\"operation\"], (\"\\u26aa\", u[\"operation\"]))\n lines += [\n f\"{icon} {label}\",\n f\" 機械: {u['machine_name']}\",\n f\" 利用者: {u['user_name']}\",\n f\" 利用量: {u['amount']}{u['unit']}\",\n f\" 日: {start[:10]}\",\n ]\n if u.get(\"reason\"):\n lines.append(f\" 理由: {u['reason']}\")\n lines.append(\"\")\n\n return \"\\n\".join(lines).strip()\n\n\ndef _send_line(token, to, message):\n payload = json.dumps({\n \"to\": to,\n \"messages\": [{\"type\": \"text\", \"text\": message}],\n }).encode(\"utf-8\")\n\n req = urllib.request.Request(\n \"https://api.line.me/v2/bot/message/push\",\n data=payload,\n headers={\n \"Authorization\": f\"Bearer {token}\",\n \"Content-Type\": \"application/json\",\n },\n method=\"POST\",\n )\n with urllib.request.urlopen(req, timeout=30) as resp:\n return resp.read().decode(\"utf-8\")\n",
"content": "import urllib.request\nimport urllib.parse\nimport json\nimport ssl\nfrom datetime import datetime, timezone, timedelta\nimport wmill\n\nJST = timezone(timedelta(hours=9))\n\n\ndef main():\n # シークレット取得\n api_key = wmill.get_variable(\"u/admin/NOTIFICATION_API_KEY\")\n line_token = wmill.get_variable(\"u/admin/LINE_CHANNEL_ACCESS_TOKEN\")\n line_to = wmill.get_variable(\"u/admin/LINE_TO\")\n\n # 前回実行時刻を取得(初回は現在時刻 - 10分\n try:\n last_checked = wmill.get_variable(\"u/admin/SHIRAOU_LAST_CHECKED_AT\")\n if not last_checked:\n last_checked = None\n except Exception:\n last_checked = None\n\n if last_checked:\n since = last_checked\n else:\n since = (datetime.now(JST) - timedelta(minutes=10)).isoformat()\n\n print(f\"[通知] 変更確認: since={since}\")\n\n # API呼び出し\n ssl_ctx = ssl.create_default_context()\n ssl_ctx.check_hostname = False\n ssl_ctx.verify_mode = ssl.CERT_NONE\n\n params = urllib.parse.urlencode({\"since\": since})\n url = f\"https://shiraou.keinafarm.net/reservations/api/changes/?{params}\"\n\n req = urllib.request.Request(url, headers={\"X-API-Key\": api_key})\n with urllib.request.urlopen(req, context=ssl_ctx, timeout=30) as resp:\n data = json.loads(resp.read().decode(\"utf-8\"))\n\n checked_at = data[\"checked_at\"]\n reservations = data.get(\"reservations\", [])\n usages = data.get(\"usages\", [])\n\n print(f\"[通知] checked_at={checked_at}, 予約={len(reservations)}件, 実績={len(usages)}件\")\n\n # 変更があればLINE通知エラー時は状態を更新しない\n if reservations or usages:\n message = _format_message(reservations, usages)\n _send_line(line_token, line_to, message)\n print(\"[通知] LINE送信完了\")\n else:\n print(\"[通知] 変更なし、通知スキップ\")\n\n # 正常完了時のみ状態更新\n wmill.set_variable(\"u/admin/SHIRAOU_LAST_CHECKED_AT\", checked_at)\n print(f\"[通知] last_checked_at更新: {checked_at}\")\n\n return {\n \"since\": since,\n \"checked_at\": checked_at,\n \"reservations_count\": len(reservations),\n \"usages_count\": len(usages),\n \"notified\": bool(reservations or usages),\n }\n\n\ndef _format_message(reservations, usages):\n lines = [\"\\U0001f4cb 営農システム 変更通知\\n\"]\n\n OP_R = {\n \"create\": (\"\\U0001f7e2\", \"予約作成\"),\n \"update\": (\"\\U0001f535\", \"予約変更\"),\n \"cancel\": (\"\\U0001f534\", \"予約キャンセル\"),\n }\n OP_U = {\n \"create\": (\"\\U0001f7e2\", \"実績登録\"),\n \"update\": (\"\\U0001f535\", \"実績修正\"),\n \"delete\": (\"\\U0001f534\", \"実績削除\"),\n }\n\n for r in reservations:\n start = r[\"start_at\"][:16].replace(\"T\", \" \")\n end = r[\"end_at\"][:16].replace(\"T\", \" \")\n icon, label = OP_R.get(r[\"operation\"], (\"\\u26aa\", r[\"operation\"]))\n lines += [\n f\"{icon} {label}\",\n f\" 機械: {r['machine_name']}\",\n f\" 利用者: {r['user_name']}\",\n f\" 日時: {start} \\uff5e {end}\",\n ]\n if r.get(\"reason\"):\n lines.append(f\" 理由: {r['reason']}\")\n lines.append(\"\")\n\n for u in usages:\n start = u[\"start_at\"][:16].replace(\"T\", \" \")\n icon, label = OP_U.get(u[\"operation\"], (\"\\u26aa\", u[\"operation\"]))\n lines += [\n f\"{icon} {label}\",\n f\" 機械: {u['machine_name']}\",\n f\" 利用者: {u['user_name']}\",\n f\" 利用量: {u['amount']}{u['unit']}\",\n f\" 日: {start[:10]}\",\n ]\n if u.get(\"reason\"):\n lines.append(f\" 理由: {u['reason']}\")\n lines.append(\"\")\n\n return \"\\n\".join(lines).strip()\n\n\ndef _send_line(token, to, message):\n payload = json.dumps({\n \"to\": to,\n \"messages\": [{\"type\": \"text\", \"text\": message}],\n }).encode(\"utf-8\")\n\n req = urllib.request.Request(\n \"https://api.line.me/v2/bot/message/push\",\n data=payload,\n headers={\n \"Authorization\": f\"Bearer {token}\",\n \"Content-Type\": \"application/json\",\n },\n method=\"POST\",\n )\n with urllib.request.urlopen(req, timeout=30) as resp:\n return resp.read().decode(\"utf-8\")\n",
"input_transforms": {},
"lock": ""
}