Add PetKit CLI wrapper (status/feed/fountain/dump)
This commit is contained in:
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Секреты — не заливать в репо
|
||||||
|
.env
|
||||||
|
secrets/
|
||||||
|
*.token
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
113
petkit_cli.py
Normal file
113
petkit_cli.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
PetKit CLI wrapper — статус фидеров/фонтанов + дамп структуры.
|
||||||
|
|
||||||
|
Режимы:
|
||||||
|
status — показать понятный статус всех устройств
|
||||||
|
dump — вывести сырую JSON-структуру фидеров (для отладки полей)
|
||||||
|
feed ГР — покормить в граммах
|
||||||
|
fountain РЕЖИМ — переключить фонтан (smart|normal|pause ...)
|
||||||
|
|
||||||
|
Креды читаются из .env рядом со скриптом:
|
||||||
|
PETKIT_EMAIL, PETKIT_PASSWORD
|
||||||
|
Регион задаётся через PETKIT_REGION (по умолчанию "Russian Federation").
|
||||||
|
"""
|
||||||
|
import asyncio, json, os, sys
|
||||||
|
|
||||||
|
from aiohttp import ClientSession
|
||||||
|
from petkitaio import PetKitClient
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
def load_env():
|
||||||
|
env_path = os.path.join(HERE, ".env")
|
||||||
|
if os.path.exists(env_path):
|
||||||
|
with open(env_path) as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#") or "=" not in line:
|
||||||
|
continue
|
||||||
|
k, _, v = line.partition("=")
|
||||||
|
os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
|
||||||
|
|
||||||
|
|
||||||
|
async def get_client() -> PetKitClient:
|
||||||
|
email = os.environ.get("PETKIT_EMAIL")
|
||||||
|
password = os.environ.get("PETKIT_PASSWORD")
|
||||||
|
region = os.environ.get("PETKIT_REGION", "Russian Federation")
|
||||||
|
if not email or not password:
|
||||||
|
sys.exit("ОШИБКА: задайте PETKIT_EMAIL и PETKIT_PASSWORD в .env (PetKit не менял пользователя).")
|
||||||
|
session = ClientSession()
|
||||||
|
client = PetKitClient(email, password, session, region)
|
||||||
|
await client.login()
|
||||||
|
return client, session
|
||||||
|
|
||||||
|
|
||||||
|
async def cmd_status(client: PetKitClient):
|
||||||
|
devices = await client.get_petkit_data()
|
||||||
|
lines = []
|
||||||
|
fids = {}
|
||||||
|
|
||||||
|
feeders = devices.feeders or {}
|
||||||
|
for fid, feeder in feeders.items():
|
||||||
|
fids[fid] = feeder
|
||||||
|
d = feeder.data
|
||||||
|
st = d.get("state", {})
|
||||||
|
lines.append(f"[Feeder] {d.get('name', fid)} (id={fid}, type={feeder.type})")
|
||||||
|
lines.append(f" food sensor: {st.get('food')} feeding: {st.get('feeding')}")
|
||||||
|
lines.append(f" battery: {st.get('batteryPower')} ({st.get('batteryStatus')})")
|
||||||
|
lines.append(f" foodWarn(food low alarm): {d.get('foodWarn', st.get('foodWarn', '?'))}")
|
||||||
|
|
||||||
|
for wid, wf in (devices.water_fountains or {}).items():
|
||||||
|
d = wf.data
|
||||||
|
lines.append(f"[Fountain] {d.get('name', wid)} (id={wid})")
|
||||||
|
|
||||||
|
if not lines:
|
||||||
|
lines.append("Нет устройств (или не удалось получить данные).")
|
||||||
|
print("\n".join(lines))
|
||||||
|
return fids
|
||||||
|
|
||||||
|
|
||||||
|
async def cmd_dump(client: PetKitClient):
|
||||||
|
devices = await client.get_petkit_data()
|
||||||
|
out = {"feeders": {}, "fountains": {}}
|
||||||
|
for fid, feeder in (devices.feeders or {}).items():
|
||||||
|
out["feeders"][str(fid)] = feeder.data
|
||||||
|
for wid, wf in (devices.water_fountains or {}).items():
|
||||||
|
out["fountains"][str(wid)] = wf.data
|
||||||
|
print(json.dumps(out, ensure_ascii=False, indent=2, default=str))
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
load_env()
|
||||||
|
mode = sys.argv[1] if len(sys.argv) > 1 else "status"
|
||||||
|
client, session = await get_client()
|
||||||
|
try:
|
||||||
|
if mode == "dump":
|
||||||
|
await cmd_dump(client)
|
||||||
|
elif mode == "feed":
|
||||||
|
amount = int(sys.argv[2]) if len(sys.argv) > 2 else 10
|
||||||
|
devices = await client.get_petkit_data()
|
||||||
|
fid = list((devices.feeders or {}).keys())[0]
|
||||||
|
await client.manual_feeding(feeder=devices.feeders[fid], amount=amount)
|
||||||
|
print(f"Покормил {amount}г (feeder {fid})")
|
||||||
|
elif mode == "fountain":
|
||||||
|
command = sys.argv[2] if len(sys.argv) > 2 else "smart"
|
||||||
|
from petkitaio.constants import W5Command
|
||||||
|
devices = await client.get_petkit_data()
|
||||||
|
wid = list((devices.water_fountains or {}).keys())[0]
|
||||||
|
cmd_map = {"smart": W5Command.SMART, "normal": W5Command.NORMAL}
|
||||||
|
wcmd = cmd_map.get(command)
|
||||||
|
if not wcmd:
|
||||||
|
sys.exit(f"Неизвестный режим фонтана: {command}")
|
||||||
|
await client.control_water_fountain(water_fountain=devices.water_fountains[wid], command=wcmd)
|
||||||
|
print(f"Фонтан {wid} -> {command}")
|
||||||
|
else:
|
||||||
|
await cmd_status(client)
|
||||||
|
finally:
|
||||||
|
await session.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
Reference in New Issue
Block a user