feat: 轻雪天气实时天气功能已更新

This commit is contained in:
远野千束 2024-04-15 18:04:19 +08:00
parent 7d0b9662f4
commit 79d8063b5d
87 changed files with 474 additions and 29 deletions

View File

@ -8,14 +8,20 @@ category: 使用手册
## 功能插件命令
### 轻雪天气`liteyuki_weather`
### **轻雪天气`liteyuki_weather`**
配置项
```yaml
weather-key # 和风天气的天气key
weather_key: "" # 和风天气的天气key
weather_dev: false # 是否为开发版
```
命令
```shell
weather <keywords...> # 查询目标地实时天气,例如:"天气 北京 海淀", "weather Tokyo Shinjuku"
bind-city <keywords...> # 绑定查询城市,个人全局生效
别名weather 天气
```
命令别名
```shell
weather 天气, bind-city 绑定城市
```

View File

@ -65,12 +65,18 @@ async def _(bot: T_Bot, event: T_MessageEvent):
async def _(matcher: Matcher, bot: T_Bot, event: T_MessageEvent):
await matcher.send("Liteyuki reloading")
temp_data = common_db.first(TempConfig(), default=TempConfig())
temp_data.data["reload"] = True
temp_data.data["reload_time"] = time.time()
temp_data.data["reload_bot_id"] = bot.self_id
temp_data.data["reload_session_type"] = event.message_type
temp_data.data["reload_session_id"] = event.group_id if event.message_type == "group" else event.user_id
temp_data.data["delta_time"] = 0
temp_data.data.update(
{
"reload" : True,
"reload_time" : time.time(),
"reload_bot_id" : bot.self_id,
"reload_session_type": event.message_type,
"reload_session_id" : event.group_id if event.message_type == "group" else event.user_id,
"delta_time" : 0
}
)
common_db.upsert(temp_data)
Reloader.reload(0)
@ -88,7 +94,12 @@ async def _(matcher: Matcher, bot: T_Bot, event: T_MessageEvent):
Subcommand(
"get",
Args["key", str, None],
alias=["查询"]
alias=["查询", "获取"]
),
Subcommand(
"remove",
Args["key", str],
alias=["删除"]
)
),
permission=SUPERUSER
@ -120,9 +131,17 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, matcher: Matcher
if len(stored_config.config) > 0:
reply += f"\n{ulang.get('liteyuki.stored_config')}\n```dotenv"
for k, v in stored_config.config.items():
reply += f"\n{k}={v}"
reply += f"\n{k}={v} {type(v)}"
reply += "\n```"
await md.send_md(reply, bot, event=event)
elif result.subcommands.get("remove"):
key = result.subcommands.get("remove").args.get("key")
if key in stored_config.config:
stored_config.config.pop(key)
common_db.upsert(stored_config)
await matcher.finish(f"{ulang.get('liteyuki.config_remove_success', KEY=key)}")
else:
await matcher.finish(f"{ulang.get('liteyuki.invalid_command', TEXT=key)}")
@on_alconna(
@ -227,6 +246,6 @@ async def every_day_update():
if result:
await broadcast_to_superusers(f"Liteyuki updated: ```\n{logs}\n```")
nonebot.logger.info(f"Liteyuki updated: {logs}")
Reloader.reload(1)
Reloader.reload(5)
else:
nonebot.logger.info(logs)

View File

@ -35,7 +35,6 @@ async def _(bot: T_Bot, event: T_MessageEvent):
{
"data": await get_stats_data(bot.self_id, ulang.lang_code)
},
debug=True,
wait=1
)
await stats.finish(MessageSegment.image(image))

View File

@ -0,0 +1,142 @@
from .qw_models import *
import httpx
language_map = {
"zh-CN" : "zh",
"zh-HK" : "zh-hant",
"en-US" : "en",
"ja-JP" : "ja",
"ko-KR" : "ko",
"fr-FR" : "fr",
"es-ES" : "es",
"de-DE" : "de",
"it-IT" : "it",
"ru-RU" : "ru",
"ar-SA" : "ar",
"pt-BR" : "pt",
"nl-NL" : "nl",
"pl-PL" : "pl",
"tr-TR" : "tr",
"th-TH" : "th",
"vi-VN" : "vi",
"id-ID" : "id",
"ms-MY" : "ms",
"fil-PH": "fil",
} # 其他使用默认对应
dev_url = "https://devapi.qweather.com/" # 开发HBa
com_url = "https://api.qweather.com/" # 正式环境
async def city_lookup(
location: str,
key: str,
adm: str = "",
number: int = 20,
lang: str = "zh",
) -> CityLookup:
"""
通过关键字搜索城市信息
Args:
location:
key:
adm:
number:
lang: 可传入标准i18n语言代码如zh-CNen-US等
Returns:
"""
url = "https://geoapi.qweather.com/v2/city/lookup?"
params = {
"location": location,
"adm" : adm,
"number" : number,
"key" : key,
"lang" : language_map.get(lang, lang),
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return CityLookup.parse_obj(resp.json())
async def get_weather_now(
key: str,
location: str,
lang: str = "zh",
unit: str = "m",
dev: bool = True,
) -> dict:
url_path = "v7/weather/now?"
url = dev_url + url_path if dev else com_url + url_path
params = {
"location": location,
"key" : key,
"lang" : language_map.get(lang, lang),
"unit" : unit,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()
async def get_weather_daily(
key: str,
location: str,
lang: str = "zh",
unit: str = "m",
dev: bool = True,
) -> dict:
url_path = "v7/weather/%dd?" % (7 if dev else 30)
url = dev_url + url_path if dev else com_url + url_path
params = {
"location": location,
"key" : key,
"lang" : language_map.get(lang, lang),
"unit" : unit,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()
async def get_weather_hourly(
key: str,
location: str,
lang: str = "zh",
unit: str = "m",
dev: bool = True,
) -> dict:
url_path = "v7/weather/%dh?" % (24 if dev else 168)
url = dev_url + url_path if dev else com_url + url_path
params = {
"location": location,
"key" : key,
"lang" : language_map.get(lang, lang),
"unit" : unit,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()
async def get_airquality(
key: str,
location: str,
lang: str,
pollutant: bool = False,
station: bool = False,
dev: bool = True
) -> dict:
url_path = f"airquality/v1/now/{location}?"
url = dev_url + url_path if dev else com_url + url_path
params = {
"key" : key,
"lang" : language_map.get(lang, lang),
"pollutant": pollutant,
"station" : station,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()

View File

@ -19,12 +19,12 @@ class Location(LiteModel):
license: str = ""
class CityLookupResponse(LiteModel):
class CityLookup(LiteModel):
code: str = ""
location: Location = Location()
location: list[Location] = [Location()]
class WeatherNow(LiteModel):
class Now(LiteModel):
obsTime: str = ""
temp: str = ""
feelsLike: str = ""
@ -44,8 +44,19 @@ class WeatherNow(LiteModel):
license: str = ""
class WeatherNowResponse(LiteModel):
class WeatherNow(LiteModel):
code: str = ""
updateTime: str = ""
fxLink: str = ""
now: WeatherNow = WeatherNow()
now: Now = Now()
class Daily(LiteModel):
pass
class WeatherDaily(LiteModel):
code: str = ""
updateTime: str = ""
fxLink: str = ""
daily: list[str] = []

View File

@ -1,7 +1,16 @@
from nonebot import require
from nonebot.adapters.onebot.v11 import MessageSegment
from nonebot.internal.matcher import Matcher
from liteyuki.utils.base.config import get_config
from liteyuki.utils.base.ly_typing import T_MessageEvent
from .qw_api import *
from ...utils.base.data_manager import User, user_db
from ...utils.base.language import get_user_lang
from ...utils.base.resource import get_path
from ...utils.message.html_tool import template2image
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import on_alconna, Alconna, Args, MultiVar, Arparma
@ -10,11 +19,67 @@ from nonebot_plugin_alconna import on_alconna, Alconna, Args, MultiVar, Arparma
aliases={"天气"},
command=Alconna(
"weather",
Args["keywords", MultiVar(str)],
Args["keywords", MultiVar(str), []],
),
).handle()
async def _(result: Arparma, event: T_MessageEvent):
async def _(result: Arparma, event: T_MessageEvent, matcher: Matcher):
"""await alconna.send("weather", city)"""
print(result["keywords"])
if len(result["keywords"]) == 0:
pass
ulang = get_user_lang(str(event.user_id))
qw_lang = language_map.get(ulang.lang_code, ulang.lang_code)
key = get_config("weather_key")
is_dev = get_config("weather_dev")
user: User = user_db.first(User(), "user_id = ?", str(event.user_id), default=User())
# params
unit = user.profile.get("unit", "m")
stored_location = user.profile.get("location", None)
if not key:
await matcher.finish(ulang.get("weather.no_key"))
kws = result.main_args.get("keywords")
if kws:
if len(kws) >= 2:
adm = kws[0]
city = kws[-1]
else:
adm = ""
city = kws[0]
city_info = await city_lookup(city, key, adm=adm, lang=qw_lang)
city_name = " ".join(kws)
else:
if not stored_location:
await matcher.finish(ulang.get("liteyuki.invalid_command", TEXT="location"))
city_info = await city_lookup(stored_location, key, lang=qw_lang)
city_name = stored_location
if city_info.code == "200":
location_data = city_info.location[0]
else:
await matcher.finish(ulang.get("weather.city_not_found", CITY=city_name))
weather_now = await get_weather_now(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev)
weather_daily = await get_weather_daily(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev)
weather_hourly = await get_weather_hourly(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev)
aqi = await get_airquality(key, location_data.id, lang=qw_lang, dev=is_dev)
image = await template2image(
template=get_path("templates/weather_now.html", abs_path=True),
templates={
"data": {
"params" : {
"unit": unit,
"lang": ulang.lang_code,
},
"weatherNow" : weather_now,
"weatherDaily" : weather_daily,
"weatherHourly": weather_hourly,
"aqi" : aqi,
"location" : location_data.dump(),
}
},
debug=True,
wait=1
)
await matcher.finish(MessageSegment.image(image))

View File

@ -24,7 +24,7 @@ liteyuki.stats.friends=好友
liteyuki.stats.plugins=插件
liteyuki.image_mode_on=开启Markdown图片模式
liteyuki.image_mode_off=关闭Markdown图片模式
liteyuki.invalid_command=无效的命令或参数
liteyuki.invalid_command=无效的命令或参数 {TEXT}
liteyuki.reload_resources=重载资源
liteyuki.list_resources=资源包列表
liteyuki.reload_resources_success=资源重载成功,共计 {NUM} 个资源包
@ -44,6 +44,7 @@ liteyuki.change_priority_failed=资源包 {NAME} 优先级修改失败
liteyuki.group_already=群 {GROUP} 已经是 {STATUS} 状态,无需重复操作
liteyuki.group_success=群 {GROUP} {STATUS} 成功
liteyuki.permission_denied=权限不足
liteyuki.config_remove_success=配置项 {KEY} 移除成功
main.current_language=当前配置语言为: {LANG}
main.enable_webdash=已启用网页监控面板: {URL}
@ -121,3 +122,7 @@ user.profile.set_failed=设置 {ATTR} 失败,请检查输入是否合法
rpm.move_up=上移
rpm.move_down=下移
rpm.move_top=置顶
weather.city_not_found=未找到城市 {CITY}
weather.weather_not_found=未找到城市 {CITY} 的天气信息
weather.no_key=未设置天气api key请在配置文件添加weather-key

View File

@ -1,4 +1,4 @@
#data-storage {
.data-storage {
display: none;
}
@ -6,6 +6,7 @@ body {
background-repeat: repeat-y;
background-size: cover;
background-position: center;
text-shadow: 1px 1px 2px black;
margin: 20px;
}

View File

@ -71,6 +71,6 @@
font-style: italic;
}
body {
* {
font-family: 'MapleMono', 'MiSans', sans-serif;
}

View File

@ -0,0 +1,83 @@
#weather-info {
color: white;
/*justify-content: center;*/
/*align-items: center;*/
/*align-content: center;*/
}
#main-info {
display: flex;
justify-content: center;
align-items: center;
}
#time {
font-size: 25px;
font-weight: bold;
font-style: italic;
text-align: right;
color: #aaa;
}
#adm {
font-size: 30px;
font-weight: bold;
text-align: center;
color: #aaa;
}
#city {
margin-top: 20px;
font-size: 70px;
font-weight: bold;
text-align: center;
}
#temperature {
display: flex;
align-items: baseline;
}
#temperature-now {
font-size: 70px;
font-weight: bold;
}
#temperature-range {
font-size: 40px;
font-weight: bold;
color: #aaa;
}
#description {
font-size: 50px;
font-weight: bold;
}
#aqi {
height: 50px;
display: flex;
border-radius: 60px;
padding: 5px;
font-size: 40px;
text-align: center;
align-content: center;
align-items: center;
justify-content: center;
}
#aqi-dot {
height: 80%;
aspect-ratio: 1 / 1;
border-radius: 50%;
background-color: #aaa;
margin-right: 20px;
}
.main-icon {
width: 240px;
height: 240px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -0,0 +1,59 @@
/**
* @typedef {Object} Location
* @property {string} city - The city name.
* @property {string} country - The country name.
*
* @typedef {Object} Weather
* @property {number} temperature - The current temperature.
* @property {string} description - The weather description.
*
* @typedef {Object} Data
* @property {Location} location - The location data.
* @property {Weather} weather - The weather data.
*/
/** @type {Data} */
let data = JSON.parse(document.getElementById("data").innerText)
let weatherNow = data["weatherNow"]
let weatherDaily = data["weatherDaily"]
let weatherHourly = data["weatherHourly"]
let aqi = data["aqi"]
let locationData = data["location"]
// set info
// document.getElementById("time").innerText = weatherNow["now"]["obsTime"]
// document.getElementById("city").innerText = locationData["name"]
// document.getElementById("adm").innerText = locationData["country"] + " " + locationData["adm1"] + " " + locationData["adm2"]
// document.getElementById("temperature-now").innerText = weatherNow["now"]["temp"] + "°"
// document.getElementById("temperature-range").innerText = weatherNow["now"]["feelsLike"] + "°"
// document.getElementById("description").innerText = weatherNow["now"]["text"]
// 处理aqi
let aqiValue = 0
aqi["aqi"].forEach(
(item) => {
if (item["defaultLocalAqi"]) {
document.getElementById("aqi-data").innerText = "AQI " + item["valueDisplay"] + " " + item["category"]
// 将(255,255,255)这种格式的颜色设置给css
document.getElementById("aqi-dot").style.backgroundColor = "rgb(" + item["color"] + ")"
}
}
)
templates = {
"time": weatherNow["now"]["obsTime"],
"city": locationData["name"],
"adm": locationData["country"] + " " + locationData["adm1"] + " " + locationData["adm2"],
"temperature-now": weatherNow["now"]["temp"] + "°",
"temperature-range": weatherDaily["daily"][0]["tempMin"] + "°/" + weatherDaily["daily"][0]["tempMax"] + "°",
"description": weatherNow["now"]["text"]
}
// 遍历每一个id给其赋值
for (let id in templates) {
document.getElementById(id).innerText = templates[id]
}

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Liteyuki Status</title>

View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Liteyuki Status</title>
<link rel="stylesheet" href="./css/card.css">
<link rel="stylesheet" href="./css/fonts.css">
<link rel="stylesheet" href="./css/weather_now.css">
</head>
<!-- qw_icon: https://a.hecdn.net/img/common/icon/202106d/%d.png-->
<body>
<div class="data-storage" id="data">{{ data | tojson }}</div>
<div class="info-box" id="weather-info">
<div id="detail-info">
<div id="time">1145-01-12 22:22:22</div>
<div id="adm">国家 一级 二级</div>
<div id="city">城市</div>
</div>
<div id="main-info">
<div>
<img class="main-icon" src="./img/qw_icon/101.png" alt="AAA">
</div>
<div>
<div id="temperature">
<div id="temperature-now">
90°
</div>
<div id="temperature-range">
10°~90°
</div>
</div>
<div id="description">
好天气
</div>
</div>
</div>
<div id="aqi">
<div id="aqi-dot"></div>
<div id="aqi-data"> AQI 114 优</div>
</div>
</div>
<div class="info-box" id="sub-info"></div>
<div class="info-box" id="hours-info"></div>
<div class="info-box" id="days-info"></div>
<script src="./js/card.js"></script>
<script src="./js/weather_now.js"></script>
</body>

View File

@ -12,6 +12,7 @@ from pydantic import BaseModel
class LiteModel(BaseModel):
TABLE_NAME: str = None
id: int = None
def dump(self, *args, **kwargs):
if pydantic.__version__ < "1.8.2":
return self.dict(*args, **kwargs)

View File

@ -6,10 +6,13 @@ import aiofiles
import nonebot
from nonebot import require
from liteyuki.utils.base.resource import load_resources
require("nonebot_plugin_htmlrender")
from nonebot_plugin_htmlrender import *
async def template2html(
template: str,
templates: dict,
@ -56,9 +59,11 @@ async def template2image(
}
template_path = os.path.dirname(template)
template_name = os.path.basename(template)
print(template_path, template_name)
if debug:
# 重载资源
load_resources()
raw_html = await template_to_html(
template_name=template_name,
template_path=template_path,

View File

@ -3,6 +3,7 @@ aiofiles==23.2.1
colored==2.2.4
dash==2.16.1
GitPython==3.1.42
httpx==0.27.0
jieba==0.42.1
nb-cli==1.4.1
nonebot2[fastapi,httpx,websockets]==2.2.1