import logging import subprocess import os import datetime import requests from functools import wraps from zoneinfo import ZoneInfo from dateutil import parser from typing import Dict, List, Optional, Tuple from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram.ext import ( Application, CommandHandler, CallbackQueryHandler, ContextTypes, JobQueue ) # ============================================================================= # LOOGLE BOT V7.9 (ULTIMATE + GLOBAL GFS FIX) # - Dashboard Sistema # - Meteo Smart: Arome (EU), Icon (EU-Est), JMA (JP), GFS (Mondo) # - Multi-User Security # ============================================================================= # --- CONFIGURAZIONE --- BOT_TOKEN = os.environ.get('BOT_TOKEN') # Gestione Multi-Utente allowed_users_raw = os.environ.get('ALLOWED_USER_ID', '') ALLOWED_IDS = [int(x.strip()) for x in allowed_users_raw.split(',') if x.strip().isdigit()] # Configurazione Sistema SSH_USER = "daniely" NAS_USER = "daniely" MASTER_IP = "192.168.128.80" # Configurazione Meteo HOME_LAT = 43.9356 HOME_LON = 12.4296 HOME_NAME = "🏠 Casa (Strada Cà Toro)" TZ = "Europe/Rome" TZINFO = ZoneInfo(TZ) OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast" GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search" HTTP_HEADERS = {"User-Agent": "loogle-bot-v7.9"} # --- LISTE DISPOSITIVI --- CORE_DEVICES = [ {"name": "🍓 Pi-1 (Master)", "ip": MASTER_IP, "type": "pi", "user": SSH_USER}, {"name": "🍓 Pi-2 (Backup)", "ip": "127.0.0.1", "type": "local", "user": ""}, {"name": "🗄️ NAS 920+", "ip": "192.168.128.100", "type": "nas", "user": NAS_USER}, {"name": "🗄️ NAS 214", "ip": "192.168.128.90", "type": "nas", "user": NAS_USER} ] INFRA_DEVICES = [ {"name": "📡 Router", "ip": "192.168.128.1"}, {"name": "📶 WiFi Sala", "ip": "192.168.128.101"}, {"name": "📶 WiFi Luca", "ip": "192.168.128.102"}, {"name": "📶 WiFi Taverna", "ip": "192.168.128.103"}, {"name": "📶 WiFi Dado", "ip": "192.168.128.104"}, {"name": "🔌 Sw Sala", "ip": "192.168.128.105"}, {"name": "🔌 Sw Main", "ip": "192.168.128.106"}, {"name": "🔌 Sw Lav", "ip": "192.168.128.107"}, {"name": "🔌 Sw Tav", "ip": "192.168.128.108"} ] logging.basicConfig(format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO) logger = logging.getLogger(__name__) # ============================================================================= # SEZIONE 1: FUNZIONI SISTEMA (SSH, PING, UTILS) # ============================================================================= def run_cmd(command, ip=None, user=None): try: if ip == "127.0.0.1" or ip is None: return subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, timeout=5).decode('utf-8').strip() else: safe_cmd = command.replace("'", "'\\''") full_cmd = f"ssh -o LogLevel=ERROR -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=3 {user}@{ip} '{safe_cmd}'" return subprocess.check_output(full_cmd, shell=True, stderr=subprocess.STDOUT, timeout=8).decode('utf-8').strip() except Exception: return "Err" def get_ping_icon(ip): try: subprocess.run(["ping", "-c", "1", "-W", "1", ip], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=0.8, check=True) return "✅" except Exception: return "🔴" def get_device_stats(device): ip, user, dtype = device['ip'], device['user'], device['type'] uptime_raw = run_cmd("uptime -p 2>/dev/null || uptime", ip, user) if not uptime_raw or "Err" in uptime_raw: return "🔴 **OFFLINE**" uptime = uptime_raw.replace("up ", "").split(", load")[0].split(", ")[0] temp = "N/A" if dtype in ["pi", "local"]: t = run_cmd("cat /sys/class/thermal/thermal_zone0/temp", ip, user) if t.isdigit(): temp = f"{int(t)/1000:.1f}°C" elif dtype == "nas": t = run_cmd("cat /sys/class/hwmon/hwmon0/temp1_input 2>/dev/null || cat /sys/class/thermal/thermal_zone0/temp", ip, user) if t.isdigit(): v = int(t); temp = f"{v/1000:.1f}°C" if v > 1000 else f"{v}°C" if dtype == "nas": ram_cmd = "free | grep Mem | awk '{printf \"%.0f%%\", $3*100/$2}'" else: ram_cmd = "free -m | awk 'NR==2{if ($2>0) printf \"%.0f%%\", $3*100/$2; else print \"0%\"}'" disk_path = "/" if dtype != "nas" else "/volume1" disk_cmd = f"df -h {disk_path} | awk 'NR==2{{print $5}}'" return f"✅ **ONLINE**\n⏱️ Up: {uptime}\n🌡️ Temp: {temp} | 🧠 RAM: {run_cmd(ram_cmd, ip, user)} | 💾 Disk: {run_cmd(disk_cmd, ip, user)}" def read_log_file(filepath, lines=15): if not os.path.exists(filepath): return "⚠️ File non trovato." try: return subprocess.check_output(['tail', '-n', str(lines), filepath], stderr=subprocess.STDOUT).decode('utf-8') except Exception as e: return f"Errore: {str(e)}" def run_speedtest(): try: return subprocess.check_output("speedtest-cli --simple", shell=True, timeout=50).decode('utf-8') except: return "Errore Speedtest" # ============================================================================= # SEZIONE 2: METEO INTELLIGENTE (MULTI-MODELLO MOSAICO) # ============================================================================= def now_local() -> datetime.datetime: return datetime.datetime.now(TZINFO) def parse_time(t: str) -> datetime.datetime: dt = parser.isoparse(t) if dt.tzinfo is None: return dt.replace(tzinfo=TZINFO) return dt.astimezone(TZINFO) def degrees_to_cardinal(d: int) -> str: dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"] return dirs[round(d / 45) % 8] def get_icon_set(prec, snow, code, is_day, cloud, vis, temp, rain, gust, cape): sky = "☁️" if code in (95, 96, 99): sky = "⛈️" if prec > 0 else "🌩️" elif (code in (45, 48) or vis < 1000) and prec < 1: sky = "🌫️" elif prec >= 0.1: sky = "🌨️" if snow > 0 else "🌧️" elif cloud <= 20: sky = "☀️" if is_day else "🌙" elif cloud <= 40: sky = "🌤️" if is_day else "🌙" elif cloud <= 60: sky = "⛅️" elif cloud <= 80: sky = "🌥️" sgx = "-" if snow > 0 or (code in (71,73,75,77,85,86) if code else False): sgx = "☃️" elif temp < 0 or (code in (66,67) if code else False): sgx = "🧊" elif cape > 2000: sgx = "🌪️" elif cape > 1000: sgx = "⚡" elif temp > 35: sgx = "🥵" elif rain > 4: sgx = "☔️" elif gust > 50: sgx = "💨" return sky, sgx def get_coordinates(city_name: str): """ Cerca le coordinate della città con fallback EN. """ # 1. Tentativo ITALIANO params = {"name": city_name, "count": 10, "language": "it", "format": "json"} try: r = requests.get(GEOCODING_URL, params=params, headers=HTTP_HEADERS, timeout=10) data = r.json() if "results" in data and len(data["results"]) > 0: res = data["results"][0] cc = res.get("country_code", "IT").upper() name = f"{res.get('name')} ({cc})" return res["latitude"], res["longitude"], name, cc except Exception as e: logger.error(f"Geocoding IT error: {e}") # 2. Tentativo FALLBACK INGLESE try: params["language"] = "en" r = requests.get(GEOCODING_URL, params=params, headers=HTTP_HEADERS, timeout=10) data = r.json() if "results" in data and len(data["results"]) > 0: res = data["results"][0] cc = res.get("country_code", "IT").upper() name = f"{res.get('name')} ({cc})" return res["latitude"], res["longitude"], name, cc except Exception as e: logger.error(f"Geocoding EN error: {e}") return None def choose_best_model(lat, lon, cc): """ Seleziona il modello migliore. Fallback Generale: NOAA GFS (perché ha sempre Visibilità/Cape). """ # 1. GIAPPONE -> JMA MSM if cc == 'JP': return "jma_msm", "JMA MSM (5km)" # 2. SCANDINAVIA -> Yr.no if cc in ['NO', 'SE', 'FI', 'DK', 'IS']: return "metno_nordic", "Yr.no (Nordic)" # 3. UK & IRLANDA -> UK Met Office if cc in ['GB', 'IE']: return "ukmo_global", "UK MetOffice" # 4. TUNING ITALIA (Mosaico) if cc == 'IT' or cc == 'SM': # ZONA 1: Nord-Ovest, Tirreno, Sardegna (Lon <= 13.0, Lat > 40.5) if lon <= 13.0 and lat > 40.5: return "meteofrance_arome_france_hd", "Arome HD" # ZONA 2: Nord-Est, Adriatico Nord/Centro if lat >= 43.0: return "icon_d2", "ICON-D2 (2km)" # ZONA 3: Sud Italia -> ICON-EU (Meglio di GFS per locale) return "icon_eu", "ICON-EU (7km)" # 5. RESTO DEL MONDO (Europa Centrale ICON-D2) if cc in ['DE', 'AT', 'CH', 'LI']: return "icon_d2", "ICON-D2" # 6. RESTO DEL MONDO -> NOAA GFS # Usiamo GFS invece di ECMWF perché ECMWF spesso manca di 'visibility'/'cape' # facendo crashare o svuotare il report. GFS è completo. return "gfs_global", "NOAA GFS Global" def get_forecast(lat, lon, model): params = { "latitude": lat, "longitude": lon, "timezone": TZ, "forecast_days": 3, "models": model, "wind_speed_unit": "kmh", "precipitation_unit": "mm", "hourly": "temperature_2m,relative_humidity_2m,cloudcover,windspeed_10m,winddirection_10m,windgusts_10m,precipitation,rain,snowfall,weathercode,is_day,cape,visibility" } try: r = requests.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=25) r.raise_for_status() return r.json() except Exception as e: logger.error(f"Meteo API error: {e}"); return None def safe_get_list(hourly_data, key, length, default=None): """Estrae una lista sicura, gestendo chiavi mancanti""" if key in hourly_data and hourly_data[key] is not None: return hourly_data[key] return [default] * length def generate_weather_report(lat, lon, location_name, cc="IT") -> str: model_id, model_name = choose_best_model(lat, lon, cc) data = get_forecast(lat, lon, model_id) if not data: return f"❌ Errore API Meteo ({model_name})." hourly = data.get("hourly", {}) times = hourly.get("time", []) if not times: return "❌ Dati orari mancanti." L = len(times) try: l_temp = safe_get_list(hourly, "temperature_2m", L, 0) l_rh = safe_get_list(hourly, "relative_humidity_2m", L, 0) l_cl = safe_get_list(hourly, "cloudcover", L, 0) l_prec = safe_get_list(hourly, "precipitation", L, 0) l_rain = safe_get_list(hourly, "rain", L, 0) l_snow = safe_get_list(hourly, "snowfall", L, 0) l_wspd = safe_get_list(hourly, "windspeed_10m", L, 0) l_gust = safe_get_list(hourly, "windgusts_10m", L, 0) l_wdir = safe_get_list(hourly, "winddirection_10m", L, 0) l_code = safe_get_list(hourly, "weathercode", L, 0) l_day = safe_get_list(hourly, "is_day", L, 1) l_cape = safe_get_list(hourly, "cape", L, 0) l_vis = safe_get_list(hourly, "visibility", L, 10000) except Exception as e: return f"❌ Errore elaborazione dati meteo: {e}" now = now_local().replace(minute=0, second=0, microsecond=0) blocks = [] for (label, hours_duration, step) in [("Prime 24h", 24, 1), ("Successive 24h", 24, 2)]: end_time = now + datetime.timedelta(hours=hours_duration) lines = [f"{'LT':<2} {'T°':>3} {'h%':>3} {'mm':>2} {'Vento':<5} {'Nv%':>3} {'Sky':<2} {'Sgx':<3}", "-" * 30] count = 0 for i, t_str in enumerate(times): try: dt = parse_time(t_str) except: continue if dt < now or dt >= end_time: continue if dt.hour % step != 0: continue try: T = float(l_temp[i]) Rh = int(l_rh[i] or 0) Cl = int(l_cl[i] or 0) Pr = float(l_prec[i] or 0) Rn = float(l_rain[i] or 0) Sn = float(l_snow[i] or 0) Wspd = float(l_wspd[i] or 0) Gust = float(l_gust[i] or 0) Wdir = int(l_wdir[i] or 0) Cape = float(l_cape[i] or 0) Vis = float(l_vis[i] or 10000) Code = int(l_code[i]) if l_code[i] is not None else None IsDay = int(l_day[i] if l_day[i] is not None else 1) t_s = f"{int(round(T))}" p_s = "--" if Pr < 0.2 else f"{int(round(Pr))}" card = degrees_to_cardinal(Wdir) w_val = Gust if (Gust - Wspd) > 15 else Wspd w_txt = f"{card} {int(round(w_val))}" if (Gust - Wspd) > 15: g_txt = f"G{int(round(w_val))}" if len(card)+len(g_txt) <= 5: w_txt = f"{card}{g_txt}" elif len(card)+1+len(g_txt) <= 5: w_txt = f"{card} {g_txt}" else: w_txt = g_txt w_fmt = f"{w_txt:<5}" sky, sgx = get_icon_set(Pr, Sn, Code, IsDay, Cl, Vis, T, Rn, Gust, Cape) lines.append(f"{dt.strftime('%H'):<2} {t_s:>3} {Rh:>3} {p_s:>2} {w_fmt} {Cl:>3} {sky:<2} {sgx:<3}") count += 1 except: continue if count > 0: day_label = f"{['Lun','Mar','Mer','Gio','Ven','Sab','Dom'][now.weekday()]} {now.day}" blocks.append(f"*{day_label} ({label})*\n```text\n" + "\n".join(lines) + "\n```") now = end_time return f"🌤️ *METEO REPORT*\n📍 {location_name}\n🧠 Fonte: {model_name}\n\n" + "\n\n".join(blocks) # ============================================================================= # SEZIONE 3: BOT HANDLERS & SCHEDULER # ============================================================================= # Decoratore Sicurezza Multi-Utente def restricted(func): @wraps(func) async def wrapped(update: Update, context: ContextTypes.DEFAULT_TYPE, *args, **kwargs): user_id = update.effective_user.id if user_id not in ALLOWED_IDS: logger.warning(f"⚠️ ACCESSO NEGATO: User {user_id}") return return await func(update, context, *args, **kwargs) return wrapped @restricted async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: keyboard = [ [InlineKeyboardButton("🖥️ Core Server", callback_data="menu_core"), InlineKeyboardButton("🔍 Scan LAN", callback_data="menu_lan")], [InlineKeyboardButton("🛡️ Pi-hole", callback_data="menu_pihole"), InlineKeyboardButton("🌐 Rete", callback_data="menu_net")], [InlineKeyboardButton("🌤️ Meteo Casa", callback_data="req_meteo_home"), InlineKeyboardButton("📜 Logs", callback_data="menu_logs")] ] text = "🎛 **Loogle Control Center v7.9**\nComandi disponibili:\n🔹 `/meteo `\n🔹 Pulsanti sotto" if update.message: await update.message.reply_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") else: await update.callback_query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") @restricted async def meteo_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if not context.args: await update.message.reply_text("⚠️ Usa: `/meteo ` (es. `/meteo Rimini`)", parse_mode="Markdown") return city = " ".join(context.args) await update.message.reply_text(f"🔄 Cerco '{city}'...", parse_mode="Markdown") coords = get_coordinates(city) if coords: lat, lon, name, cc = coords report = generate_weather_report(lat, lon, name, cc) await update.message.reply_text(report, parse_mode="Markdown") else: await update.message.reply_text(f"❌ Città '{city}' non trovata.", parse_mode="Markdown") async def scheduled_morning_report(context: ContextTypes.DEFAULT_TYPE) -> None: logger.info("⏰ Invio report automatico meteo...") # Forza "SM" per casa -> Arome/IconD2 in base alla posizione report = generate_weather_report(HOME_LAT, HOME_LON, HOME_NAME, "SM") for uid in ALLOWED_IDS: try: await context.bot.send_message(chat_id=uid, text=report, parse_mode="Markdown") except: pass @restricted async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: query = update.callback_query await query.answer() data = query.data if data == "main_menu": await start(update, context) elif data == "req_meteo_home": await query.edit_message_text("⏳ **Scaricamento Meteo Casa...**", parse_mode="Markdown") # Forza "SM" per casa report = generate_weather_report(HOME_LAT, HOME_LON, HOME_NAME, "SM") keyboard = [[InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]] await query.edit_message_text(report, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") elif data == "menu_core": keyboard = [] for i, dev in enumerate(CORE_DEVICES): keyboard.append([InlineKeyboardButton(dev['name'], callback_data=f"stat_{i}")]) keyboard.append([InlineKeyboardButton("📊 Report Completo", callback_data="stat_all")]) keyboard.append([InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]) await query.edit_message_text("🖥️ **Core Servers**", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") elif data == "stat_all": await query.edit_message_text("⏳ **Analisi...**", parse_mode="Markdown") report = "📊 **REPORT CORE**\n" for dev in CORE_DEVICES: report += f"\n🔹 **{dev['name']}**\n{get_device_stats(dev)}\n" await query.edit_message_text(report, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_core")]]), parse_mode="Markdown") elif data.startswith("stat_"): dev = CORE_DEVICES[int(data.split("_")[1])] await query.edit_message_text(f"⏳ Controllo {dev['name']}...", parse_mode="Markdown") await query.edit_message_text(f"🔹 **{dev['name']}**\n\n{get_device_stats(dev)}", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_core")]]), parse_mode="Markdown") elif data == "menu_lan": await query.edit_message_text("⏳ **Scansione LAN...**", parse_mode="Markdown") report = "🔍 **DIAGNOSTICA LAN**\n\n" try: for dev in CORE_DEVICES: report += f"{get_ping_icon(dev['ip'])} `{dev['ip']}` - {dev['name']}\n" report += "\n" for dev in INFRA_DEVICES: report += f"{get_ping_icon(dev['ip'])} `{dev['ip']}` - {dev['name']}\n" except Exception as e: report += f"\n⚠️ Errore: {e}" keyboard = [[InlineKeyboardButton("⚡ Menu Riavvio", callback_data="menu_reboot")], [InlineKeyboardButton("🔄 Aggiorna", callback_data="menu_lan"), InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]] await query.edit_message_text(report, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") elif data == "menu_reboot": keyboard = [] for i, dev in enumerate(INFRA_DEVICES): if "Router" not in dev['name']: keyboard.append([InlineKeyboardButton(f"⚡ {dev['name']}", callback_data=f"reboot_{i}")]) keyboard.append([InlineKeyboardButton("⬅️ Indietro", callback_data="menu_lan")]) await query.edit_message_text("⚠️ **RIAVVIO REMOTO**", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") elif data.startswith("reboot_"): dev = INFRA_DEVICES[int(data.split("_")[1])] res = run_cmd("reboot", dev['ip'], "admin") await query.edit_message_text(f"⚡ Inviato a {dev['name']}...\n\n`{res}`", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_reboot")]]), parse_mode="Markdown") elif data == "menu_pihole": status_raw = run_cmd("sudo pihole status", MASTER_IP, SSH_USER) icon = "✅" if "Enabled" in status_raw or "enabled" in status_raw else "🔴" text = f"🛡️ **Pi-hole Master**\nStato: {icon}\n\n`{status_raw}`" keyboard = [[InlineKeyboardButton("⏸️ 5m", callback_data="ph_disable_300"), InlineKeyboardButton("⏸️ 30m", callback_data="ph_disable_1800")], [InlineKeyboardButton("▶️ Attiva", callback_data="ph_enable"), InlineKeyboardButton("🔄 Restart", callback_data="ph_restart")], [InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]] await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") elif data.startswith("ph_"): if "disable" in data: run_cmd(f"sudo pihole disable {data.split('_')[2]}s", MASTER_IP, SSH_USER) elif "enable" in data: run_cmd("sudo pihole enable", MASTER_IP, SSH_USER) elif "restart" in data: run_cmd("sudo systemctl restart pihole-FTL", MASTER_IP, SSH_USER) await query.edit_message_text("✅ Comando inviato.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_pihole")]]), parse_mode="Markdown") elif data == "menu_net": ip = run_cmd("curl -s ifconfig.me") keyboard = [[InlineKeyboardButton("🚀 Speedtest", callback_data="net_speedtest")], [InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]] await query.edit_message_text(f"🌐 **Rete**\n🌍 IP: `{ip}`", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") elif data == "net_speedtest": await query.edit_message_text("🚀 **Speedtest...**", parse_mode="Markdown") res = run_speedtest() await query.edit_message_text(f"🚀 **Risultato:**\n\n`{res}`", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_net")]]), parse_mode="Markdown") elif data == "menu_logs": keyboard = [[InlineKeyboardButton("🐶 Watchdog", callback_data="log_wd"), InlineKeyboardButton("💾 Backup", callback_data="log_bk")], [InlineKeyboardButton("🔄 NPM Sync", callback_data="log_npm"), InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]] await query.edit_message_text("📜 **Logs**", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") elif data.startswith("log_"): paths = {"log_wd": "/logs/dhcp-watchdog.log", "log_bk": "/logs/raspiBackup.log", "log_npm": "/logs/sync-npm.log"} log_c = read_log_file(paths[data]) await query.edit_message_text(f"📜 **Log:**\n\n`{log_c}`", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_logs")]]), parse_mode="Markdown") def main(): logger.info("Avvio Loogle Bot v7.9 (Global GFS Fix)...") application = Application.builder().token(BOT_TOKEN).build() # Handlers application.add_handler(CommandHandler("start", start)) application.add_handler(CommandHandler("meteo", meteo_command)) application.add_handler(CallbackQueryHandler(button_handler)) # SCHEDULER job_queue = application.job_queue job_queue.run_daily(scheduled_morning_report, time=datetime.time(hour=8, minute=0, tzinfo=TZINFO), days=(0, 1, 2, 3, 4, 5, 6)) application.run_polling() if __name__ == "__main__": main()