From 2743343ed28c0d7fe4bfb803cd3846adfba285bd Mon Sep 17 00:00:00 2001 From: Akira Date: Sat, 21 Feb 2026 15:35:20 +0900 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E5=86=85=E5=AE=B9=20?= =?UTF-8?q?=E5=8E=9F=E5=9B=A0:=20wmill.get=5Fstate()=20/=20set=5Fstate()?= =?UTF-8?q?=20=E3=81=AF=E3=83=95=E3=83=AD=E3=83=BC=E3=81=AE=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E3=83=A9=E3=82=A4=E3=83=B3=E3=82=B9=E3=82=AF=E3=83=AA?= =?UTF-8?q?=E3=83=97=E3=83=88=E3=81=A7=E3=81=AF=E5=AE=9F=E8=A1=8C=E3=82=92?= =?UTF-8?q?=E3=81=BE=E3=81=9F=E3=81=84=E3=81=A7=E4=BF=9D=E5=AD=98=E3=81=95?= =?UTF-8?q?=E3=82=8C=E3=81=AA=E3=81=84=E3=80=82=E6=AF=8E=E5=9B=9E=20None?= =?UTF-8?q?=20=E3=81=8C=E8=BF=94=E3=82=8B=E3=81=9F=E3=82=81=E3=80=81?= =?UTF-8?q?=E6=AF=8E=E5=9B=9E=E3=80=8C=E7=8F=BE=E5=9C=A8=E6=99=82=E5=88=BB?= =?UTF-8?q?=20-=2010=E5=88=86=E3=80=8D=E3=81=A7=E6=A4=9C=E7=B4=A2=E2=86=92?= =?UTF-8?q?=E5=B8=B8=E3=81=AB=E5=A4=89=E6=9B=B4=E3=81=82=E3=82=8A=E3=81=A7?= =?UTF-8?q?=E9=80=9A=E7=9F=A5=E3=81=97=E3=81=A6=E3=81=84=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修正: 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 になるはずです。しばらく様子を見てみてください。 --- flows/shiraou_notification.flow.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flows/shiraou_notification.flow.json b/flows/shiraou_notification.flow.json index 672e7db..12a2a7e 100644 --- a/flows/shiraou_notification.flow.json +++ b/flows/shiraou_notification.flow.json @@ -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": "" }