feat(prayer): implement ai-independent prayer scheduling
Refactor prayer notification system to remove AI dependency for daily scheduling. Changes include: - Added '--at', '--command', and '--delete-after' flags to 'picoclaw cron add' CLI. - Rewrote 'prayer_notify.sh auto_schedule' to use CLI instead of direct JSON writes. - Implemented Indonesia city-to-timezone mapping (WIB/WITA/WIT) for accurate scheduling. - Added cron store reload in 'pkg/tools/cron.go' after command execution. - Updated 'SKILL.md' with new setup instructions.
This commit is contained in:
parent
9902eceec0
commit
78064da29a
4 changed files with 275 additions and 57 deletions
|
|
@ -62,10 +62,13 @@ func cronHelp() {
|
||||||
fmt.Println(" -n, --name Job name")
|
fmt.Println(" -n, --name Job name")
|
||||||
fmt.Println(" -m, --message Message for agent")
|
fmt.Println(" -m, --message Message for agent")
|
||||||
fmt.Println(" -e, --every Run every N seconds")
|
fmt.Println(" -e, --every Run every N seconds")
|
||||||
|
fmt.Println(" -a, --at Run once in N seconds from now")
|
||||||
fmt.Println(" -c, --cron Cron expression (e.g. '0 9 * * *')")
|
fmt.Println(" -c, --cron Cron expression (e.g. '0 9 * * *')")
|
||||||
fmt.Println(" -d, --deliver Deliver response to channel")
|
fmt.Println(" --command Shell command to execute")
|
||||||
|
fmt.Println(" -d, --deliver Deliver response to channel")
|
||||||
fmt.Println(" --to Recipient for delivery")
|
fmt.Println(" --to Recipient for delivery")
|
||||||
fmt.Println(" --channel Channel for delivery")
|
fmt.Println(" --channel Channel for delivery")
|
||||||
|
fmt.Println(" --delete-after Delete job after first run (default for --at)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func cronListCmd(storePath string) {
|
func cronListCmd(storePath string) {
|
||||||
|
|
@ -111,8 +114,11 @@ func cronAddCmd(storePath string) {
|
||||||
name := ""
|
name := ""
|
||||||
message := ""
|
message := ""
|
||||||
var everySec *int64
|
var everySec *int64
|
||||||
|
var atSec *int64
|
||||||
cronExpr := ""
|
cronExpr := ""
|
||||||
|
command := ""
|
||||||
deliver := false
|
deliver := false
|
||||||
|
deleteAfter := false
|
||||||
channel := ""
|
channel := ""
|
||||||
to := ""
|
to := ""
|
||||||
|
|
||||||
|
|
@ -136,13 +142,27 @@ func cronAddCmd(storePath string) {
|
||||||
everySec = &sec
|
everySec = &sec
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
case "-a", "--at":
|
||||||
|
if i+1 < len(args) {
|
||||||
|
var sec int64
|
||||||
|
fmt.Sscanf(args[i+1], "%d", &sec)
|
||||||
|
atSec = &sec
|
||||||
|
i++
|
||||||
|
}
|
||||||
case "-c", "--cron":
|
case "-c", "--cron":
|
||||||
if i+1 < len(args) {
|
if i+1 < len(args) {
|
||||||
cronExpr = args[i+1]
|
cronExpr = args[i+1]
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
case "--command":
|
||||||
|
if i+1 < len(args) {
|
||||||
|
command = args[i+1]
|
||||||
|
i++
|
||||||
|
}
|
||||||
case "-d", "--deliver":
|
case "-d", "--deliver":
|
||||||
deliver = true
|
deliver = true
|
||||||
|
case "--delete-after":
|
||||||
|
deleteAfter = true
|
||||||
case "--to":
|
case "--to":
|
||||||
if i+1 < len(args) {
|
if i+1 < len(args) {
|
||||||
to = args[i+1]
|
to = args[i+1]
|
||||||
|
|
@ -166,13 +186,20 @@ func cronAddCmd(storePath string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if everySec == nil && cronExpr == "" {
|
if atSec == nil && everySec == nil && cronExpr == "" {
|
||||||
fmt.Println("Error: Either --every or --cron must be specified")
|
fmt.Println("Error: One of --at, --every, or --cron must be specified")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var schedule cron.CronSchedule
|
var schedule cron.CronSchedule
|
||||||
if everySec != nil {
|
if atSec != nil {
|
||||||
|
atMS := time.Now().UnixMilli() + *atSec*1000
|
||||||
|
schedule = cron.CronSchedule{
|
||||||
|
Kind: "at",
|
||||||
|
AtMS: &atMS,
|
||||||
|
}
|
||||||
|
deleteAfter = true // at jobs always delete after run
|
||||||
|
} else if everySec != nil {
|
||||||
everyMS := *everySec * 1000
|
everyMS := *everySec * 1000
|
||||||
schedule = cron.CronSchedule{
|
schedule = cron.CronSchedule{
|
||||||
Kind: "every",
|
Kind: "every",
|
||||||
|
|
@ -185,6 +212,11 @@ func cronAddCmd(storePath string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If command is set, deliver should be false
|
||||||
|
if command != "" {
|
||||||
|
deliver = false
|
||||||
|
}
|
||||||
|
|
||||||
cs := cron.NewCronService(storePath, nil)
|
cs := cron.NewCronService(storePath, nil)
|
||||||
job, err := cs.AddJob(name, schedule, message, deliver, channel, to)
|
job, err := cs.AddJob(name, schedule, message, deliver, channel, to)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -192,6 +224,17 @@ func cronAddCmd(storePath string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set command and deleteAfterRun if needed
|
||||||
|
if command != "" {
|
||||||
|
job.Payload.Command = command
|
||||||
|
}
|
||||||
|
if deleteAfter {
|
||||||
|
job.DeleteAfterRun = true
|
||||||
|
}
|
||||||
|
if command != "" || deleteAfter {
|
||||||
|
cs.UpdateJob(job)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("✓ Added job '%s' (%s)\n", job.Name, job.ID)
|
fmt.Printf("✓ Added job '%s' (%s)\n", job.Name, job.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ func (t *CronTool) Name() string {
|
||||||
|
|
||||||
// Description returns the tool description
|
// Description returns the tool description
|
||||||
func (t *CronTool) Description() string {
|
func (t *CronTool) Description() string {
|
||||||
return "Schedule reminders, tasks, or system commands. IMPORTANT: When user asks to be reminded or scheduled, you MUST call this tool. Use 'at_seconds' for one-time reminders (e.g., 'remind me in 10 minutes' → at_seconds=600). Use 'every_seconds' ONLY for recurring tasks (e.g., 'every 2 hours' → every_seconds=7200). Use 'cron_expr' for complex recurring schedules. Use 'command' to execute shell commands directly."
|
return "Schedule reminders, tasks, or system commands. IMPORTANT: When user asks to be reminded or scheduled, you MUST call this tool. Do NOT use system crontab or 'crontab -e'; always use this built-in cron tool. Use 'at_seconds' for one-time reminders (e.g., 'remind me in 10 minutes' → at_seconds=600). Use 'every_seconds' ONLY for recurring tasks (e.g., 'every 2 hours' → every_seconds=7200). Use 'cron_expr' for complex recurring schedules. Use 'command' to execute shell commands directly."
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parameters returns the tool parameters schema
|
// Parameters returns the tool parameters schema
|
||||||
|
|
@ -299,6 +299,11 @@ func (t *CronTool) ExecuteJob(ctx context.Context, job *cron.CronJob) string {
|
||||||
ChatID: chatID,
|
ChatID: chatID,
|
||||||
Content: output,
|
Content: output,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Reload store in case command modified cron/jobs.json directly
|
||||||
|
// (e.g., prayer auto_schedule writes AT jobs to the store file)
|
||||||
|
t.cronService.Load()
|
||||||
|
|
||||||
return "ok"
|
return "ok"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,10 @@ chmod +x skills/prayer-times/scripts/prayer_notify.sh
|
||||||
| `today` | Tampilkan jadwal hari ini |
|
| `today` | Tampilkan jadwal hari ini |
|
||||||
| `schedule [prayers...]` | Output `nama\|HH:MM\|detik` untuk sholat yang belum lewat |
|
| `schedule [prayers...]` | Output `nama\|HH:MM\|detik` untuk sholat yang belum lewat |
|
||||||
| `notify <prayer> <time>` | Kirim ke ntfy + cetak pesan untuk Telegram |
|
| `notify <prayer> <time>` | Kirim ke ntfy + cetak pesan untuk Telegram |
|
||||||
|
| `auto_schedule <channel> <chat_id>` | **Tulis cron job via picoclaw CLI (tanpa AI)** |
|
||||||
| `status` | Tampilkan config dan status data |
|
| `status` | Tampilkan config dan status data |
|
||||||
|
|
||||||
**Auto-fetch**: `schedule` dan `today` otomatis fetch jika data bulan ini belum ada.
|
**Auto-fetch**: `schedule`, `today`, dan `auto_schedule` otomatis fetch jika data bulan ini belum ada.
|
||||||
Jadi meski device mati saat awal bulan, data tetap ter-update saat dipakai.
|
|
||||||
|
|
||||||
## Trigger Phrases
|
## Trigger Phrases
|
||||||
|
|
||||||
|
|
@ -47,7 +47,7 @@ Saat user **pertama kali** minta reminder sholat:
|
||||||
|
|
||||||
### 2. Setup kota (via exec tool, BUKAN cron)
|
### 2. Setup kota (via exec tool, BUKAN cron)
|
||||||
```bash
|
```bash
|
||||||
bash skills/prayer-times/scripts/prayer_notify.sh setup samarinda
|
bash skills/prayer-times/scripts/prayer_notify.sh setup dumai
|
||||||
```
|
```
|
||||||
Ini akan save config ke `skills/prayer-times/data/config` dan fetch data bulan ini.
|
Ini akan save config ke `skills/prayer-times/data/config` dan fetch data bulan ini.
|
||||||
|
|
||||||
|
|
@ -63,21 +63,27 @@ Simpan juga ke MEMORY.md agar tidak lupa.
|
||||||
{"action": "add", "message": "Monthly prayer fetch", "command": "bash skills/prayer-times/scripts/prayer_notify.sh fetch", "cron_expr": "0 1 1 * *"}
|
{"action": "add", "message": "Monthly prayer fetch", "command": "bash skills/prayer-times/scripts/prayer_notify.sh fetch", "cron_expr": "0 1 1 * *"}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. Setup daily scheduler (jam 01:30 setiap hari)
|
### 5. Setup daily auto-scheduler (jam 01:30 setiap hari)
|
||||||
|
|
||||||
|
**PENTING**: Gunakan `command` mode agar script langsung menulis cron job. AI **tidak** perlu terlibat.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"action": "add",
|
"action": "add",
|
||||||
"message": "Baca jadwal sholat hari ini. Jalankan: bash skills/prayer-times/scripts/prayer_notify.sh schedule. Untuk setiap baris output (format: nama|waktu|detik), buat 2 cron job one-time: (1) deliver:true untuk Telegram dengan pesan emoji, (2) command dengan: bash skills/prayer-times/scripts/prayer_notify.sh notify <nama> <waktu> untuk ntfy.",
|
"message": "Daily prayer auto-schedule",
|
||||||
"cron_expr": "30 1 * * *",
|
"command": "bash skills/prayer-times/scripts/prayer_notify.sh auto_schedule telegram CHAT_ID",
|
||||||
"deliver": false
|
"cron_expr": "30 1 * * *"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Ganti `CHAT_ID` dengan chat ID user yang meminta (tersedia di session context).
|
||||||
|
Script akan menambah reminder sholat ke cron via `picoclaw cron add`, tanpa perlu AI.
|
||||||
|
|
||||||
## Config File
|
## Config File
|
||||||
|
|
||||||
Disimpan di `skills/prayer-times/data/config` — **milik skill ini sendiri, tidak shared**:
|
Disimpan di `skills/prayer-times/data/config` — **milik skill ini sendiri, tidak shared**:
|
||||||
```
|
```
|
||||||
CITY="samarinda"
|
CITY="dumai"
|
||||||
SAHUR_MINS="30"
|
SAHUR_MINS="30"
|
||||||
IFTAR_MINS="10"
|
IFTAR_MINS="10"
|
||||||
PRAYERS="shubuh dzuhur ashr magrib isya"
|
PRAYERS="shubuh dzuhur ashr magrib isya"
|
||||||
|
|
@ -89,38 +95,17 @@ User bisa minta ubah via exec tool:
|
||||||
- Ganti kota: `prayer_notify.sh setup <kota_baru>`
|
- Ganti kota: `prayer_notify.sh setup <kota_baru>`
|
||||||
- Ganti ntfy: edit NTFY_TOPIC di config
|
- Ganti ntfy: edit NTFY_TOPIC di config
|
||||||
|
|
||||||
## Cara Agent Memproses Daily Scheduler
|
## Timezone
|
||||||
|
|
||||||
Saat daily scheduler trigger (jam 01:30), agent HARUS:
|
Script otomatis mapping kota Indonesia ke timezone yang benar:
|
||||||
|
|
||||||
### Step 1: Jalankan script via exec tool
|
| Zona | Contoh Kota | TZ |
|
||||||
```bash
|
|------|-------------|-----|
|
||||||
bash skills/prayer-times/scripts/prayer_notify.sh schedule
|
| WIB (UTC+7) | dumai, pekanbaru, jakarta, surabaya | Asia/Jakarta |
|
||||||
```
|
| WITA (UTC+8) | samarinda, makassar, denpasar | Asia/Makassar |
|
||||||
|
| WIT (UTC+9) | jayapura, ambon, manokwari | Asia/Jayapura |
|
||||||
|
|
||||||
Output contoh:
|
Ini penting agar epoch calculation sesuai waktu lokal kota, bukan system timezone.
|
||||||
```
|
|
||||||
shubuh|05:06|12360
|
|
||||||
dzuhur|12:27|52020
|
|
||||||
ashr|15:42|63720
|
|
||||||
magrib|18:30|73800
|
|
||||||
isya|19:39|77940
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Untuk SETIAP baris, buat 2 cron job
|
|
||||||
|
|
||||||
**Telegram** (deliver=true):
|
|
||||||
```json
|
|
||||||
{"action": "add", "message": "🌅 Waktu Shubuh (05:06) — Saatnya menunaikan sholat shubuh.", "at_seconds": 12360}
|
|
||||||
```
|
|
||||||
|
|
||||||
**ntfy** (command):
|
|
||||||
```json
|
|
||||||
{"action": "add", "message": "ntfy: shubuh", "command": "bash skills/prayer-times/scripts/prayer_notify.sh notify shubuh 05:06", "at_seconds": 12360}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Konfirmasi (satu pesan ringkasan)
|
|
||||||
Kirim ringkasan ke user: "✅ Reminder sholat hari ini sudah diset: Shubuh 05:06, Dzuhur 12:27, ..."
|
|
||||||
|
|
||||||
## Waktu yang Tersedia
|
## Waktu yang Tersedia
|
||||||
|
|
||||||
|
|
@ -138,8 +123,6 @@ Kirim ringkasan ke user: "✅ Reminder sholat hari ini sudah diset: Shubuh 05:06
|
||||||
|
|
||||||
1. **Tanya kota** saat pertama kali — jangan asumsi. Jalankan `setup <kota>` sebelum apapun.
|
1. **Tanya kota** saat pertama kali — jangan asumsi. Jalankan `setup <kota>` sebelum apapun.
|
||||||
2. **JANGAN hardcode waktu sholat** — selalu baca dari script output. Waktu berubah setiap hari.
|
2. **JANGAN hardcode waktu sholat** — selalu baca dari script output. Waktu berubah setiap hari.
|
||||||
3. **JANGAN buat cron_expr untuk waktu sholat** — karena waktu berubah harian, gunakan daily scheduler + at_seconds.
|
3. **Gunakan auto_schedule** untuk daily scheduler — ini menulis cron job langsung tanpa AI.
|
||||||
4. **Parse output dengan benar** — format: `nama|HH:MM|detik`. Kolom ke-3 = `at_seconds`.
|
4. **at_seconds auto-delete** — one-time job otomatis hilang setelah trigger.
|
||||||
5. **Dual delivery** — setiap reminder: Telegram (deliver:true) + ntfy (via notify command).
|
5. **Auto-fetch** — script otomatis download data jika file bulan ini belum ada.
|
||||||
6. **at_seconds auto-delete** — one-time job otomatis hilang setelah trigger.
|
|
||||||
7. **Auto-fetch** — script otomatis download data jika file bulan ini belum ada. Aman jika device mati saat awal bulan.
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@
|
||||||
# prayer_notify.sh notify <prayer> <time> — Send notification via ntfy + stdout
|
# prayer_notify.sh notify <prayer> <time> — Send notification via ntfy + stdout
|
||||||
# prayer_notify.sh setup <city> — Set city and fetch initial data
|
# prayer_notify.sh setup <city> — Set city and fetch initial data
|
||||||
# prayer_notify.sh status — Show current config and data status
|
# prayer_notify.sh status — Show current config and data status
|
||||||
|
# prayer_notify.sh auto_schedule <channel> <chat_id>
|
||||||
|
# — Add cron jobs via CLI (no AI)
|
||||||
#
|
#
|
||||||
# Config file: skills/prayer-times/data/config (co-located with skill)
|
# Config file: skills/prayer-times/data/config (co-located with skill)
|
||||||
# Auto-fetch: schedule command auto-fetches if data is missing or stale
|
# Auto-fetch: schedule command auto-fetches if data is missing or stale
|
||||||
|
|
@ -49,6 +51,34 @@ EOF
|
||||||
echo "Config saved to $CONFIG_FILE"
|
echo "Config saved to $CONFIG_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---- Timezone mapping ----
|
||||||
|
|
||||||
|
# Map Indonesian city to IANA timezone.
|
||||||
|
# jadwalsholatorg data is in the city's local time,
|
||||||
|
# so we need the correct TZ for epoch calculation.
|
||||||
|
city_to_tz() {
|
||||||
|
local city="$1"
|
||||||
|
case "$city" in
|
||||||
|
# WIB (UTC+7) — Sumatra, Jawa, Kalimantan Barat/Tengah
|
||||||
|
dumai|pekanbaru|medan|padang|palembang|jambi|bengkulu|lampung|\
|
||||||
|
banda-aceh|batam|tanjung-pinang|pangkal-pinang|bandar-lampung|\
|
||||||
|
jakarta-pusat|jakarta-selatan|jakarta-barat|jakarta-timur|jakarta-utara|\
|
||||||
|
bogor|depok|tangerang|bekasi|bandung|semarang|yogyakarta|surabaya|\
|
||||||
|
malang|solo|cirebon|serang|pontianak|palangka-raya)
|
||||||
|
echo "Asia/Jakarta" ;;
|
||||||
|
# WITA (UTC+8) — Kalimantan Selatan/Timur/Utara, Sulawesi, Bali, NTB, NTT
|
||||||
|
samarinda|makassar|denpasar|balikpapan|banjarmasin|manado|gorontalo|\
|
||||||
|
palu|kendari|mamuju|mataram|kupang|tarakan|bontang)
|
||||||
|
echo "Asia/Makassar" ;;
|
||||||
|
# WIT (UTC+9) — Papua, Maluku
|
||||||
|
jayapura|ambon|manokwari|sorong|ternate|tual|merauke|fakfak)
|
||||||
|
echo "Asia/Jayapura" ;;
|
||||||
|
*)
|
||||||
|
# Default: try to infer from system TZ, fallback to WIB
|
||||||
|
echo "${TZ:-Asia/Jakarta}" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
# ---- Helper functions ----
|
# ---- Helper functions ----
|
||||||
|
|
||||||
get_json_path() {
|
get_json_path() {
|
||||||
|
|
@ -160,9 +190,13 @@ cmd_schedule() {
|
||||||
# Filter: use args if provided, otherwise use config
|
# Filter: use args if provided, otherwise use config
|
||||||
local filter="${*:-$PRAYERS}"
|
local filter="${*:-$PRAYERS}"
|
||||||
|
|
||||||
python3 -c "
|
# Determine the correct timezone for this city
|
||||||
import json, sys, time
|
local city_tz
|
||||||
from datetime import datetime
|
city_tz=$(city_to_tz "$CITY")
|
||||||
|
|
||||||
|
TZ="$city_tz" python3 -c "
|
||||||
|
import json, sys, time, os
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
|
||||||
data = json.load(open('$json_path'))
|
data = json.load(open('$json_path'))
|
||||||
today = '$today'
|
today = '$today'
|
||||||
|
|
@ -258,6 +292,7 @@ cmd_notify() {
|
||||||
cmd_status() {
|
cmd_status() {
|
||||||
echo "=== Prayer Times Config ==="
|
echo "=== Prayer Times Config ==="
|
||||||
echo "Kota : $CITY"
|
echo "Kota : $CITY"
|
||||||
|
echo "Timezone : $(city_to_tz "$CITY")"
|
||||||
echo "Sahur : $SAHUR_MINS menit sebelum Shubuh"
|
echo "Sahur : $SAHUR_MINS menit sebelum Shubuh"
|
||||||
echo "Iftar : $IFTAR_MINS menit sebelum Maghrib"
|
echo "Iftar : $IFTAR_MINS menit sebelum Maghrib"
|
||||||
echo "Prayers : $PRAYERS"
|
echo "Prayers : $PRAYERS"
|
||||||
|
|
@ -284,19 +319,169 @@ cmd_status() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---- Auto-schedule: add cron jobs via CLI, no AI needed ----
|
||||||
|
|
||||||
|
cmd_auto_schedule() {
|
||||||
|
local channel="${1:-}"
|
||||||
|
local chat_id="${2:-}"
|
||||||
|
|
||||||
|
if [ -z "$channel" ] || [ -z "$chat_id" ]; then
|
||||||
|
echo "Usage: prayer_notify.sh auto_schedule <channel> <chat_id>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ensure_data
|
||||||
|
local json_path today now_ts city_tz
|
||||||
|
json_path=$(get_json_path)
|
||||||
|
today=$(today_date)
|
||||||
|
now_ts=$(now_epoch)
|
||||||
|
city_tz=$(city_to_tz "$CITY")
|
||||||
|
|
||||||
|
local filter="$PRAYERS"
|
||||||
|
|
||||||
|
# First, remove old prayer one-time jobs from previous auto_schedule runs
|
||||||
|
local existing_jobs
|
||||||
|
existing_jobs=$(picoclaw cron list 2>/dev/null || true)
|
||||||
|
echo "$existing_jobs" | grep -oP '🕌.*?\((\K[^)]+)' | while read -r job_id; do
|
||||||
|
picoclaw cron remove "$job_id" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
|
||||||
|
# Use Python to calculate times and output picoclaw cron add commands
|
||||||
|
local schedule_output
|
||||||
|
schedule_output=$(TZ="$city_tz" python3 - "$json_path" "$today" "$now_ts" \
|
||||||
|
"$SAHUR_MINS" "$IFTAR_MINS" "$filter" <<'PYTHON_EOF'
|
||||||
|
import json, sys, time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
json_path, today, now_ts_s, sahur_mins_s, iftar_mins_s, filter_arg = sys.argv[1:7]
|
||||||
|
|
||||||
|
now_ts = int(now_ts_s)
|
||||||
|
sahur_mins = int(sahur_mins_s)
|
||||||
|
iftar_mins = int(iftar_mins_s)
|
||||||
|
|
||||||
|
data = json.load(open(json_path))
|
||||||
|
entry = None
|
||||||
|
for e in data:
|
||||||
|
if e['tanggal'] == today:
|
||||||
|
entry = e
|
||||||
|
break
|
||||||
|
|
||||||
|
if not entry:
|
||||||
|
print('ERROR', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def hhmm_to_epoch(hhmm):
|
||||||
|
h, m = map(int, hhmm.split(':'))
|
||||||
|
dt = datetime.strptime(today + f' {h:02d}:{m:02d}:00', '%Y-%m-%d %H:%M:%S')
|
||||||
|
return int(dt.timestamp())
|
||||||
|
|
||||||
|
schedule = []
|
||||||
|
shubuh_epoch = hhmm_to_epoch(entry['shubuh'])
|
||||||
|
sahur_epoch = shubuh_epoch - (sahur_mins * 60)
|
||||||
|
sahur_time = time.strftime('%H:%M', time.localtime(sahur_epoch))
|
||||||
|
schedule.append(('sahur', sahur_time, sahur_epoch))
|
||||||
|
|
||||||
|
for name in ['shubuh', 'dzuhur', 'ashr', 'magrib', 'isya']:
|
||||||
|
schedule.append((name, entry[name], hhmm_to_epoch(entry[name])))
|
||||||
|
|
||||||
|
magrib_epoch = hhmm_to_epoch(entry['magrib'])
|
||||||
|
iftar_epoch = magrib_epoch - (iftar_mins * 60)
|
||||||
|
iftar_time = time.strftime('%H:%M', time.localtime(iftar_epoch))
|
||||||
|
schedule.append(('iftar', iftar_time, iftar_epoch))
|
||||||
|
|
||||||
|
# Filter
|
||||||
|
wanted = set(f.strip().lower() for f in filter_arg.split())
|
||||||
|
schedule = [s for s in schedule if s[0] in wanted]
|
||||||
|
schedule.sort(key=lambda x: x[2])
|
||||||
|
|
||||||
|
# Output future prayers: name|display_time|seconds_from_now
|
||||||
|
for name, display_time, epoch in schedule:
|
||||||
|
diff = epoch - now_ts
|
||||||
|
if diff > 0:
|
||||||
|
print(f'{name}|{display_time}|{diff}')
|
||||||
|
PYTHON_EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ -z "$schedule_output" ]; then
|
||||||
|
echo "ℹ️ Semua waktu sholat hari ini sudah lewat."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Emoji and title maps
|
||||||
|
declare -A emoji_map=(
|
||||||
|
[sahur]="🌙" [shubuh]="🌅" [dzuhur]="☀️"
|
||||||
|
[ashr]="🌤️" [magrib]="🌇" [isya]="🌃" [iftar]="🍽️"
|
||||||
|
)
|
||||||
|
declare -A title_map=(
|
||||||
|
[sahur]="Waktu Sahur" [shubuh]="Waktu Shubuh" [dzuhur]="Waktu Dzuhur"
|
||||||
|
[ashr]="Waktu Ashar" [magrib]="Waktu Maghrib" [isya]="Waktu Isya"
|
||||||
|
[iftar]="Persiapan Buka Puasa"
|
||||||
|
)
|
||||||
|
|
||||||
|
local count=0
|
||||||
|
local summary="🕌 Reminder sholat $today ($CITY) sudah dijadwalkan:"
|
||||||
|
|
||||||
|
while IFS='|' read -r name display_time at_seconds; do
|
||||||
|
local emoji="${emoji_map[$name]:-🕌}"
|
||||||
|
local title="${title_map[$name]:-Waktu Sholat}"
|
||||||
|
local msg
|
||||||
|
|
||||||
|
if [ "$name" = "sahur" ]; then
|
||||||
|
msg="$emoji $title ($display_time) — Ayo bangun sahur! $SAHUR_MINS menit lagi waktu Imsyak."
|
||||||
|
elif [ "$name" = "iftar" ]; then
|
||||||
|
msg="$emoji $title ($display_time) — $IFTAR_MINS menit lagi waktu berbuka puasa!"
|
||||||
|
else
|
||||||
|
msg="$emoji $title ($display_time) — Saatnya menunaikan sholat $name."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add Telegram delivery job
|
||||||
|
picoclaw cron add \
|
||||||
|
-n "🕌 $name $display_time" \
|
||||||
|
-m "$msg" \
|
||||||
|
--at "$at_seconds" \
|
||||||
|
-d \
|
||||||
|
--channel "$channel" \
|
||||||
|
--to "$chat_id" 2>/dev/null
|
||||||
|
|
||||||
|
# Add ntfy job if configured
|
||||||
|
if [ -n "$NTFY_TOPIC" ]; then
|
||||||
|
picoclaw cron add \
|
||||||
|
-n "🕌 ntfy:$name" \
|
||||||
|
-m "ntfy: $name" \
|
||||||
|
--at "$at_seconds" \
|
||||||
|
--command "bash $SCRIPT_DIR/prayer_notify.sh notify $name $display_time" \
|
||||||
|
--channel "$channel" \
|
||||||
|
--to "$chat_id" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
summary="$summary
|
||||||
|
$emoji ${title} ${display_time}"
|
||||||
|
count=$((count + 1))
|
||||||
|
done <<< "$schedule_output"
|
||||||
|
|
||||||
|
local ntfy_status="❌ tidak aktif"
|
||||||
|
[ -n "$NTFY_TOPIC" ] && ntfy_status="✅ aktif"
|
||||||
|
|
||||||
|
echo "$summary"
|
||||||
|
echo ""
|
||||||
|
echo "📍 Timezone: $(city_to_tz "$CITY")"
|
||||||
|
echo "📊 Total: $count reminder | ntfy: $ntfy_status"
|
||||||
|
}
|
||||||
|
|
||||||
# ---- Main ----
|
# ---- Main ----
|
||||||
|
|
||||||
load_config
|
load_config
|
||||||
|
|
||||||
case "${1:-help}" in
|
case "${1:-help}" in
|
||||||
setup) shift; cmd_setup "$@" ;;
|
setup) shift; cmd_setup "$@" ;;
|
||||||
fetch) cmd_fetch ;;
|
fetch) cmd_fetch ;;
|
||||||
today) cmd_today ;;
|
today) cmd_today ;;
|
||||||
schedule) shift; cmd_schedule "$@" ;;
|
schedule) shift; cmd_schedule "$@" ;;
|
||||||
notify) shift; cmd_notify "$@" ;;
|
notify) shift; cmd_notify "$@" ;;
|
||||||
status) cmd_status ;;
|
status) cmd_status ;;
|
||||||
|
auto_schedule) shift; cmd_auto_schedule "$@" ;;
|
||||||
help|*)
|
help|*)
|
||||||
echo "Usage: prayer_notify.sh {setup|fetch|today|schedule|notify|status}"
|
echo "Usage: prayer_notify.sh {setup|fetch|today|schedule|notify|status|auto_schedule}"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Commands:"
|
echo "Commands:"
|
||||||
echo " setup <city> Set kota dan fetch data awal"
|
echo " setup <city> Set kota dan fetch data awal"
|
||||||
|
|
@ -305,6 +490,8 @@ case "${1:-help}" in
|
||||||
echo " schedule [prayers...] Hitung detik-dari-sekarang untuk reminder"
|
echo " schedule [prayers...] Hitung detik-dari-sekarang untuk reminder"
|
||||||
echo " notify <prayer> <time> Kirim notifikasi (ntfy + stdout)"
|
echo " notify <prayer> <time> Kirim notifikasi (ntfy + stdout)"
|
||||||
echo " status Tampilkan config dan status data"
|
echo " status Tampilkan config dan status data"
|
||||||
|
echo " auto_schedule <store> <channel> <chat_id>"
|
||||||
|
echo " Tulis cron job langsung (tanpa AI)"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Config: $CONFIG_FILE"
|
echo "Config: $CONFIG_FILE"
|
||||||
echo "Kota saat ini: $CITY"
|
echo "Kota saat ini: $CITY"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue