diff --git a/.gitignore b/.gitignore index ee5ce70..aeb64fb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ secrets/ *.token +# Runtime состояние +.food_state + # Python __pycache__/ *.pyc diff --git a/petkit_food_check.py b/petkit_food_check.py new file mode 100644 index 0000000..be3ecd2 --- /dev/null +++ b/petkit_food_check.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +""" +PetKit food watchdog — проверяет уровень корма в фидере. + +Поведение: + - Читает state.food текущего фидера. + - Хранит последний известный уровень в state-файле. + - Печатает в stdout уведомление ТОЛЬКО при переходе в "мало"(1) или "пусто"(0), + либо когда корм снова долили (возврат к полному) — чтобы сбросить тревогу. + - При первом запуске просто запоминает состояние, ничего не печатает. + +Предназначен для крона в watchdog-режиме: + - непустой stdout -> уведомление доставляется пользователю + - пустой stdout -> тихий прогон, никому ничего не шлём +""" +import asyncio, os, sys +from aiohttp import ClientSession +from petkitaio import PetKitClient + +HERE = os.path.dirname(os.path.abspath(__file__)) +STATE_FILE = os.path.join(HERE, ".food_state") + +# Уровни корма у D4H: 2 = достаточно, 1 = мало, 0 = пусто +# (порог можно скорректировать после наблюдения за реальными значениями) +ALERT_AT = 1 # алертим когда food <= 1 + + +def load_env(): + p = os.path.join(HERE, ".env") + if os.path.exists(p): + for line in open(p): + line = line.strip() + if line and not line.startswith("#") and "=" in line: + k, _, v = line.partition("=") + os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'")) + + +def read_state(): + try: + with open(STATE_FILE) as f: + return int(f.read().strip()) + except Exception: + return None + + +def write_state(v): + with open(STATE_FILE, "w") as f: + f.write(str(v)) + + +async def main(): + load_env() + 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") + + s = ClientSession() + c = PetKitClient(email, password, s, region) + try: + await c.login() + devs = await c.get_petkit_data() + feeders = devs.feeders or {} + if not feeders: + sys.exit(0) # фидер не виден — молчим + fid = list(feeders.keys())[0] + f = feeders[fid] + food = f.data.get("state", {}).get("food") + name = f.data.get("name", fid) + if food is None: + sys.exit(0) + + prev = read_state() + write_state(food) + + # Тревога "мало/пусто" — только при переходе именно в это состояние + if prev is not None and prev > ALERT_AT and food <= ALERT_AT: + state_txt = "ПОЧТИ ПУСТО" if food == 0 else "НА ИСХОДЕ" + print( + f"\U0001f9d4 Фидер «{name}»: КОРМ {state_txt} (уровень {food}). " + f"Проверь и долей корм." + ) + # Сброс тревоги — корм снова долили + elif prev is not None and prev <= ALERT_AT and food > ALERT_AT: + print(f"🟢 Фидер «{name}»: корм долили (уровень {food}). Тревога сброшена.") + + # Остальное — тихо + finally: + await s.close() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/petkit_raw.py b/petkit_raw.py new file mode 100644 index 0000000..bfdc5c6 --- /dev/null +++ b/petkit_raw.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Прямой запрос к PetKit API — сырой вывод без обработки библиотекой.""" +import asyncio, hashlib, json, os + +from aiohttp import ClientSession + +HERE = os.path.dirname(os.path.abspath(__file__)) + + +def load_env(): + p = os.path.join(HERE, ".env") + if os.path.exists(p): + for line in open(p): + line = line.strip() + if line and not line.startswith("#") and "=" in line: + k, _, v = line.partition("=") + os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'")) + + +REGION_URL = "https://passport.petkt.com/6/account/regionservers" +USER_AGENT = "PETKIT/11.2.1 (iPhone; iOS 15.1; Scale/3.00)" + + +async def main(): + load_env() + email = os.environ["PETKIT_EMAIL"] + password = os.environ["PETKIT_PASSWORD"] + region = os.environ.get("PETKIT_REGION", "Russian Federation") + + hdr = { + "Accept": "*/*", + "Accept-Language": "en-US;q=1, it-US;q=0.9", + "Accept-Encoding": "gzip, deflate", + "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": USER_AGENT, + "X-Client": "ios(15.1;iPhone14,3)", + } + + async with ClientSession() as s: + # 1. список региональных серверов + r = await s.post(REGION_URL, headers=hdr, data={}) + sl = (await r.json())["result"]["list"] + region_id = None + base_url = None + for reg in sl: + if reg["name"] == region: + region_id = reg["id"] + base_url = reg["gateway"] + print("=== REGION ===", region, "-> id", region_id, base_url) + + login_url = f"{base_url}user/login" + data = { + "client": '{"deviceModel":"iPhone14,3","systemVersion":"15.1","appVersion":"11.2.1","lang":"en","timezone":"Asia/Yekaterinburg"}', + "encrypt": "1", + "oldVersion": "11.2.1", + "password": hashlib.md5(password.encode()).hexdigest(), + "region": str(region_id), + "username": email, + } + r = await s.post(login_url, headers=hdr, data=data) + login = await r.json() + print("=== LOGIN ===") + print(json.dumps(login, ensure_ascii=False, indent=2)) + token = login["result"]["session"]["id"] + hdr["X-Session"] = token + + # 2. список устройств (device roster) — сырой + roster_url = f"{base_url}discovery/device_roster_v2" + r = await s.post(roster_url, headers=hdr, data={}) + roster = await r.json() + print("\n=== DEVICE ROSTER (сырой) ===") + print(json.dumps(roster, ensure_ascii=False, indent=2)) + + +asyncio.run(main()) diff --git a/petkit_roster.py b/petkit_roster.py new file mode 100644 index 0000000..dbb2418 --- /dev/null +++ b/petkit_roster.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Сырой вывод PetKit device roster (что реально возвращает API), без парсинга.""" +import asyncio, json, os +from datetime import datetime +from aiohttp import ClientSession +from petkitaio import PetKitClient +from petkitaio.constants import Endpoint + +HERE = os.path.dirname(os.path.abspath(__file__)) + + +def load_env(): + p = os.path.join(HERE, ".env") + for line in open(p): + line = line.strip() + if line and not line.startswith("#") and "=" in line: + k, _, v = line.partition("=") + os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'")) + + +async def main(): + load_env() + s = ClientSession() + c = PetKitClient( + os.environ["PETKIT_EMAIL"], + os.environ["PETKIT_PASSWORD"], + s, + os.environ.get("PETKIT_REGION", "Russian Federation"), + ) + await c.login() + print("=== AUTH ===") + print("user_id:", c.user_id, " group_ids:", list(c.group_ids), " base:", c.base_url) + + url = f"{c.base_url}{Endpoint.DEVICE_ROSTER}" + hdr = await c.create_header() + day = str(datetime.now().date()).replace("-", "") + print(f"\n=== RAW DEVICE ROSTER (day={day}) ===") + for gid in c.group_ids: + data = {"day": day, "groupId": gid} + resp = await c._post(url, hdr, data) + print(f"--- group {gid} ---") + print(json.dumps(resp, ensure_ascii=False, indent=2)) + await s.close() + + +asyncio.run(main())