Files
TaiYangGongGong/ping_monitor.py
T
2026-04-11 22:02:38 +08:00

562 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
LAN IP 在线监控脚本
- 运行时间: 每天凌晨2点到晚上10点
- 监控多个固定局域网IP
- 首次上线发邮件通知
- 早上8:15前未上线则发送"未到"通知
"""
import os
import time
import datetime
import subprocess
import smtplib
import json
import re
import urllib.parse
import urllib.request
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
# ============== 配置区域 ==============
# 导入敏感配置
from config import (
SMTP_SERVER, SMTP_PORT, SMTP_USER, SMTP_PASSWORD,
FROM_NAME, TO_EMAILS, YOUR_NAME,
DEEPSEEK_API_KEY, DEEPSEEK_API_URL, DEEPSEEK_MODEL,
)
# 监控的局域网IP列表
TARGET_IPS = [
"192.168.2.60",
]
# 天气位置配置
WEATHER_LOCATION = "成都"
# 运行时间配置
START_HOUR = 2 # 凌晨2点开始
END_HOUR = 22 # 晚上10点结束
ALERT_HOUR = 8 # 8点
ALERT_MINUTE = 15 # 15分
# 状态文件目录
STATE_DIR = "/home/lhr/storage/TaiYangGongGong/.ping_state"
# 天气和新闻缓存过期时间(秒)
CACHE_EXPIRE = 3600 # 1小时
# 天气/新闻缓存文件
WEATHER_CACHE_FILE = os.path.join(STATE_DIR, "weather_cache.json")
NEWS_CACHE_FILE = os.path.join(STATE_DIR, "news_cache.json")
DEFAULT_HTTP_HEADERS = {
"User-Agent": "Mozilla/5.0",
}
# ============== 邮件发送函数 ==============
def send_email(subject, html_body):
"""发送邮件"""
msg = MIMEMultipart('alternative')
msg['From'] = Header(FROM_NAME, 'utf-8').encode() + f" <{SMTP_USER}>"
msg['To'] = ", ".join(TO_EMAILS)
msg['Subject'] = str(Header(subject, 'utf-8'))
msg.attach(MIMEText(html_body, 'html', 'utf-8'))
try:
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
server.starttls()
server.login(SMTP_USER, SMTP_PASSWORD)
server.sendmail(SMTP_USER, TO_EMAILS, msg.as_string())
print(f"[{datetime.datetime.now()}] 邮件发送成功: {subject}")
return True
except Exception as e:
print(f"[{datetime.datetime.now()}] 邮件发送失败: {e}")
return False
def generate_morning_message(status, arrived_time=None, weather_summary=None, news_summary=None):
"""让AI生成自然的早安文案,第一段单独生成不受干扰,第二段衔接第一段,第三段直接用"""
# 第一段:太阳公公状态(AI单独生成,保留关键信息)
sun_status = f"已起床,{arrived_time}就溜出来了" if status == "arrived" else "还在睡觉,还没看到影子"
sun_prompt = f"""请根据太阳公公的起床情况,写一段50字左右的早安问候语:
太阳公公状态:{sun_status}
⚠️重要:时间已经是24小时制(如 21:48),直接用这个时间写进文案,不要自己换算或加"上午/下午"
要求:
- 语气俏皮可爱,像朋友聊天
- 开头加一个和内容相符的emoji
- 50字左右,不要太长
- 不要用分隔符或标题
- 只返回这段文字"""
first_paragraph = ask_deepseek(sun_prompt, system_prompt="你是一个贴心的朋友,写温暖俏皮的早安文案。")
first_paragraph = first_paragraph if first_paragraph else (
f"🌅 太阳公公终于起床啦,{arrived_time}就偷偷溜出来了~" if status == "arrived"
else "😴 太阳公公这会儿还没醒呢,别急,我帮你盯着着呢~"
)
# 第二段:AI根据第一段生成天气段
weather_prompt = f"""请根据【第一段】的语气,自然过渡到今日天气:
【第一段】
{first_paragraph}
【今日天气】
{weather_summary if weather_summary else '今天天气信息获取失败'}
要求:
- 语气要接住【第一段】,像朋友聊天一样自然过渡到天气
- 50-80字左右
- 不要用分隔符或标题
- 不要用列表格式
- 只返回第二段"""
second_paragraph = ask_deepseek(weather_prompt, system_prompt="你是一个贴心的朋友,写温暖自然的早安文案。")
second_paragraph = second_paragraph if second_paragraph else f"今日天气:{weather_summary}"
# 第三段直接用news_summary(已经是串联好的内容)
return f"{first_paragraph}\n\n{second_paragraph}\n\n{news_summary}"
def build_email_html(status, arrived_time=None, weather_summary=None, news_summary=None):
"""构建邮件HTML内容"""
if status == "arrived":
subject = f"😤 早上坏"
else:
subject = f"😊 早上好"
# 获取AI生成的文案
morning_message = generate_morning_message(status, arrived_time, weather_summary, news_summary)
html = f"""
<html>
<body>
<div style="font-size: 16px; line-height: 1.8; color: #333;">
{morning_message.replace('\n', '<br>')}
</div>
<hr>
<p style="color: #888; font-size: 12px;">此邮件由系统自动发送</p>
</body>
</html>
"""
return subject, html
# ============== Ping 检测函数 ==============
def ping_host(ip, timeout=2):
"""检测主机是否在线"""
try:
# Windows 使用 ping -n, 其他使用 ping -c
cmd = ["ping", "-c", "1", "-W", str(timeout), ip]
result = subprocess.run(cmd, capture_output=True, text=True)
return result.returncode == 0
except Exception:
return False
# ============== 状态管理 ==============
def get_today_str():
return datetime.datetime.now().strftime("%Y-%m-%d")
def load_state():
"""加载状态文件"""
state_file = os.path.join(STATE_DIR, "monitor_state.json")
if not os.path.exists(state_file):
return {}
try:
import json
with open(state_file, 'r') as f:
return json.load(f)
except Exception:
return {}
def save_state(state):
"""保存状态文件"""
os.makedirs(STATE_DIR, exist_ok=True)
state_file = os.path.join(STATE_DIR, "monitor_state.json")
try:
import json
with open(state_file, 'w') as f:
json.dump(state, f, indent=2)
except Exception as e:
print(f"[{datetime.datetime.now()}] 保存状态失败: {e}")
def should_send_arrived_today(state, ip):
"""检查今天是否已发送过到岗通知"""
today = get_today_str()
ip_state = state.get(ip, {})
return ip_state.get("arrived_date") == today and ip_state.get("arrived_sent", False)
def should_send_not_arrived_today(state, ip):
"""检查今天是否已发送过未到通知"""
today = get_today_str()
ip_state = state.get(ip, {})
return ip_state.get("not_arrived_date") == today and ip_state.get("not_arrived_sent", False)
def mark_arrived_sent(state, ip):
"""标记已发送到岗通知"""
today = get_today_str()
if ip not in state:
state[ip] = {}
state[ip]["arrived_date"] = today
state[ip]["arrived_sent"] = True
state[ip]["arrived_time"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
save_state(state)
def mark_not_arrived_sent(state, ip):
"""标记已发送未到通知"""
today = get_today_str()
if ip not in state:
state[ip] = {}
state[ip]["not_arrived_date"] = today
state[ip]["not_arrived_sent"] = True
save_state(state)
def reset_daily_state(state):
"""重置新一天的标记"""
today = get_today_str()
changed = False
for ip in state:
ip_state = state[ip]
if ip_state.get("arrived_date") != today:
ip_state["arrived_sent"] = False
ip_state["not_arrived_sent"] = False
changed = True
elif ip_state.get("not_arrived_date") != today:
ip_state["not_arrived_sent"] = False
changed = True
return changed
# ============== 天气获取函数 ==============
def is_failed_content(content):
"""判断内容是否属于失败结果"""
return not content or "获取失败" in content
def fetch_text(url, headers=None, timeout=10):
"""发起HTTP请求并返回文本内容"""
request_headers = dict(DEFAULT_HTTP_HEADERS)
if headers:
request_headers.update(headers)
req = urllib.request.Request(url, headers=request_headers)
with urllib.request.urlopen(req, timeout=timeout) as response:
return response.read().decode('utf-8', errors='ignore')
def get_weather():
"""获取当天天气信息(带缓存)"""
today = get_today_str()
# 检查缓存
if os.path.exists(WEATHER_CACHE_FILE):
try:
with open(WEATHER_CACHE_FILE, 'r') as f:
cache = json.load(f)
cached_weather = cache.get("weather", "")
if (
cache.get("date") == today
and cache.get("location") == WEATHER_LOCATION
and time.time() - cache.get("timestamp", 0) < CACHE_EXPIRE
and not is_failed_content(cached_weather)
):
return cached_weather
except Exception:
pass
# 天气信息来源 - 使用wttr.in(免费无需API Key
weather_info = ""
try:
encoded_location = urllib.parse.quote(WEATHER_LOCATION)
url = f"https://wttr.in/{encoded_location}?format=%l:+%C,+%t,+%h,+feeling:+%f,+%p"
weather_info = fetch_text(url)
if not weather_info.strip():
weather_info = "天气信息获取失败"
except Exception as e:
print(f"[{datetime.datetime.now()}] 获取天气失败: {e}")
weather_info = "天气信息获取失败"
# 保存缓存
os.makedirs(STATE_DIR, exist_ok=True)
cache_data = {
"date": today,
"location": WEATHER_LOCATION,
"timestamp": time.time(),
"weather": weather_info
}
try:
with open(WEATHER_CACHE_FILE, 'w') as f:
json.dump(cache_data, f)
except Exception:
pass
return weather_info
# ============== 新闻获取函数 ==============
def get_hot_news():
"""获取当天热搜新闻(带缓存)"""
today = get_today_str()
# 检查缓存
if os.path.exists(NEWS_CACHE_FILE):
try:
with open(NEWS_CACHE_FILE, 'r') as f:
cache = json.load(f)
cached_news = cache.get("news", "")
if (
cache.get("date") == today
and time.time() - cache.get("timestamp", 0) < CACHE_EXPIRE
and not is_failed_content(cached_news)
):
return cached_news
except Exception:
pass
# 从百度热搜页提取热榜关键词
news_info = ""
try:
html = fetch_text("https://top.baidu.com/board?tab=realtime")
raw_titles = re.findall(r'"word":"(.*?)"', html)
news_items = []
seen_titles = set()
for raw_title in raw_titles:
title = raw_title.strip()
if not title or title in seen_titles:
continue
seen_titles.add(title)
news_items.append(title)
if len(news_items) >= 10:
break
if not news_items:
raise ValueError("未能从百度热搜页面提取标题")
news_info = "\n".join(
f"{index}. {title}" for index, title in enumerate(news_items, 1)
)
except Exception as e:
print(f"[{datetime.datetime.now()}] 获取新闻失败: {e}")
if not news_info:
news_info = "新闻信息获取失败"
# 保存缓存
os.makedirs(STATE_DIR, exist_ok=True)
cache_data = {
"date": today,
"timestamp": time.time(),
"news": news_info
}
try:
with open(NEWS_CACHE_FILE, 'w') as f:
json.dump(cache_data, f)
except Exception:
pass
return news_info
# ============== DeepSeek AI 处理函数 ==============
def ask_deepseek(prompt, system_prompt="你是一个有用的助手,用简洁温暖的语言回复。"):
"""调用DeepSeek API获取回复"""
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {DEEPSEEK_API_KEY}"
}
payload = {
"model": DEEPSEEK_MODEL,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
],
"temperature": 0.7,
"max_tokens": 500
}
try:
req = urllib.request.Request(
DEEPSEEK_API_URL,
data=json.dumps(payload).encode('utf-8'),
headers=headers,
method='POST'
)
with urllib.request.urlopen(req, timeout=30) as response:
result = json.loads(response.read().decode('utf-8'))
return result.get("choices", [{}])[0].get("message", {}).get("content", "")
except Exception as e:
print(f"[{datetime.datetime.now()}] DeepSeek API调用失败: {e}")
return None
def get_weather_summary(weather_info):
"""让DeepSeek总结天气信息"""
if not weather_info or "获取失败" in weather_info:
return weather_info
prompt = f"""请用简洁温暖的语言总结以下天气信息,控制在50字以内,适合放在早安邮件里:
{weather_info}
请直接返回总结内容,不要多余的话。"""
summary = ask_deepseek(prompt, system_prompt="你是一个贴心的早安助手,用温暖简洁的语言回复。")
return summary if summary else weather_info
def get_news_summary(news_info):
"""让DeepSeek总结新闻要点,找出关联,串联成自然的段落"""
if not news_info or "获取失败" in news_info:
return news_info
prompt = f"""请阅读以下今日热搜新闻,试着找出它们之间的关联或共同话题,然后写成一段200字左右的自然段落,像朋友聊天一样分享这些新闻:
{news_info}
要求:
- 找出新闻之间的联系,用自己的话串起来讲,不要念稿
- 讲的时候像跟朋友吐槽聊天那样,说到哪个新闻就直接说,别用引号包着
- 语气像在说相声或脱口秀,自然调侃,不要端着
- 不要用列表或条目,要连贯的段落
- 字数200字左右
- 只返回这段新闻解读,不要多余的话"""
summary = ask_deepseek(prompt, system_prompt="你是一个贴心的早安助手,用温暖自然的语言回复。")
return summary if summary else news_info
# ============== 时间判断 ==============
def is_in_monitor_window():
"""检查是否在监控时间窗口内"""
now = datetime.datetime.now()
current_hour = now.hour
# 凌晨2点到晚上10点
if current_hour >= START_HOUR and current_hour < END_HOUR:
return True
return False
def is_alert_time():
"""检查是否到了8:15提醒时间"""
now = datetime.datetime.now()
if now.hour == ALERT_HOUR and now.minute == ALERT_MINUTE:
return True
return False
# ============== 主监控循环 ==============
# 缓存天气和新闻摘要
_cached_weather_summary = None
_cached_news_summary = None
def get_cached_content():
"""获取缓存的天气和新闻摘要"""
global _cached_weather_summary, _cached_news_summary
# 如果没有缓存,先获取
if _cached_weather_summary is None:
print(f"[{datetime.datetime.now()}] 正在获取天气信息...")
weather_info = get_weather()
_cached_weather_summary = get_weather_summary(weather_info)
print(f"[{datetime.datetime.now()}] 天气摘要: {_cached_weather_summary[:50]}...")
if _cached_news_summary is None:
print(f"[{datetime.datetime.now()}] 正在获取新闻信息...")
news_info = get_hot_news()
_cached_news_summary = get_news_summary(news_info)
print(f"[{datetime.datetime.now()}] 新闻摘要: {_cached_news_summary[:50]}...")
return _cached_weather_summary, _cached_news_summary
def reset_daily_content_cache():
"""重置每日内容缓存"""
global _cached_weather_summary, _cached_news_summary
_cached_weather_summary = None
_cached_news_summary = None
def monitor_once(state):
"""执行一次监控检查"""
weather_summary, news_summary = get_cached_content()
for ip in TARGET_IPS:
online = ping_host(ip)
if online:
print(f"[{datetime.datetime.now()}] {ip} 在线")
if not should_send_arrived_today(state, ip):
arrived_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
subject, html = build_email_html("arrived", arrived_time, weather_summary, news_summary)
if send_email(subject, html):
mark_arrived_sent(state, ip)
else:
print(f"[{datetime.datetime.now()}] {ip} 离线")
def check_not_arrived_alert(state):
"""检查是否需要发送未到提醒"""
weather_summary, news_summary = get_cached_content()
for ip in TARGET_IPS:
# 只有在目标仍然离线的情况下才发送未到提醒
if not ping_host(ip) and not should_send_not_arrived_today(state, ip):
subject, html = build_email_html("not_arrived", None, weather_summary, news_summary)
if send_email(subject, html):
mark_not_arrived_sent(state, ip)
def main():
print(f"[{datetime.datetime.now()}] LAN监控脚本启动")
print(f"监控IP: {TARGET_IPS}")
print(f"运行时间: {START_HOUR}:00 - {END_HOUR}:00")
print(f"提醒时间: {ALERT_HOUR}:{ALERT_MINUTE:02d}")
print(f"DeepSeek API: {DEEPSEEK_API_URL}")
print("-" * 50)
os.makedirs(STATE_DIR, exist_ok=True)
last_alert_day = None
last_content_day = None
while True:
now = datetime.datetime.now()
current_day = now.strftime("%Y-%m-%d")
# 加载状态
state = load_state()
# 检查是否需要重置每日标记
if reset_daily_state(state):
save_state(state)
print(f"[{now}] 新的一天,重置通知状态")
# 检查是否需要重置内容缓存
if last_content_day != current_day:
reset_daily_content_cache()
last_content_day = current_day
print(f"[{now}] 新的一天,重置天气新闻缓存")
# 检查是否在监控时间窗口内
if is_in_monitor_window():
# 执行一次监控
monitor_once(state)
# 检查是否到8:15提醒时间(每小时检查一次)
if is_alert_time() and last_alert_day != current_day:
check_not_arrived_alert(state)
last_alert_day = current_day
else:
print(f"[{now}] 不在监控时间窗口 ({START_HOUR}:00 - {END_HOUR}:00),等待中...")
# 每30秒检查一次
time.sleep(30)
if __name__ == "__main__":
main()