Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 35 additions & 33 deletions petsseries/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,29 @@
import logging
from typing import Any, Dict, Optional

import aiohttp
import aiohttp # type: ignore[import-not-found]

from .auth import AuthManager
from .config import Config
from .events import EventsManager

# Import MealsManager
from .meals import MealsManager
from .models import (
User,
Home,
Device,
Consumer,
Device,
Home,
ModeDevice,
User,
)
from .config import Config
from .session import create_ssl_context

# Import MealsManager
from .meals import MealsManager
from .events import EventsManager

# Optional import for Tuya
try:
from .tuya import TuyaClient, TuyaError
except ImportError:
TuyaClient = None
TuyaError = Exception
TuyaClient = None # type: ignore[assignment, misc]
TuyaError = Exception # type: ignore[assignment, misc]

_LOGGER = logging.getLogger(__name__)

Expand All @@ -53,8 +53,8 @@ def __init__(
):
self.auth = AuthManager(token_file, access_token, refresh_token)
self.session = None
self.headers = {}
self.headers_token = {}
self.headers: Dict[str, str] = {}
self.headers_token: Dict[str, str] = {}
self.timeout = aiohttp.ClientTimeout(total=10.0)
self.config = Config()
self.tuya_client: Optional[TuyaClient] = None # type: ignore
Expand All @@ -74,7 +74,7 @@ def __init__(
client_id=tuya_credentials["client_id"],
ip=tuya_credentials["ip"],
local_key=tuya_credentials["local_key"],
version=tuya_credentials.get("version", 3.4),
version=float(tuya_credentials.get("version", 3.4)),
)
_LOGGER.info("TuyaClient initialized successfully.")
except TuyaError as e:
Expand Down Expand Up @@ -102,7 +102,9 @@ async def initialize(self) -> None:
Initialize the client by loading tokens and refreshing the access token if necessary.
"""
if self.auth.access_token and self.auth.refresh_token:
await self.auth.save_tokens(str(self.auth.access_token), str(self.auth.refresh_token))
await self.auth.save_tokens(
str(self.auth.access_token), str(self.auth.refresh_token)
)
await self.auth.load_tokens()
if await self.auth.is_token_expired():
_LOGGER.info("Access token expired, refreshing...")
Expand Down Expand Up @@ -188,8 +190,12 @@ async def get_consumer(self) -> Consumer:
) as response:
response.raise_for_status()
data = await response.json()
# New consumer endpoint observed returns: id, identities, installations, url, identitiesUrl, installationsUrl, language
# countryCode may be absent; set to empty string if missing for backward compatibility
return Consumer(
id=data["id"], country_code=data["countryCode"], url=data["url"]
id=str(data.get("id", "")),
country_code=str(data.get("countryCode", "")),
url=str(data.get("url", "")),
)
except aiohttp.ClientResponseError as e:
_LOGGER.error("Failed to get Consumer: %s %s", e.status, e.message)
Expand All @@ -210,16 +216,19 @@ async def get_homes(self) -> list[Home]:
) as response:
response.raise_for_status()
homes_data = await response.json()
items = homes_data.get(
"item", homes_data if isinstance(homes_data, list) else []
)
homes = [
Home(
id=home["id"],
name=home["name"],
shared=home["shared"],
number_of_devices=home["numberOfDevices"],
external_id=home["externalId"],
number_of_activities=home["numberOfActivities"],
id=home.get("id", ""),
name=home.get("name", ""),
shared=bool(home.get("shared", False)),
number_of_devices=int(home.get("numberOfDevices", 0)),
external_id=str(home.get("externalId", "")),
number_of_activities=int(home.get("numberOfActivities", 0)),
)
for home in homes_data
for home in items
]
return homes
except aiohttp.ClientResponseError as e:
Expand All @@ -234,10 +243,7 @@ async def get_devices(self, home: Home) -> list[Device]:
Get devices for the selected home.
"""
await self.ensure_token_valid()
url = (
f"https://petsseries-backend.prod.eu-hs.iot.versuni.com/"
f"api/homes/{home.id}/devices"
)
url = f"{self.config.base_url}/api/homes/{home.id}/devices"
session = await self.get_client()
try:
async with session.get(url, headers=self.headers) as response:
Expand Down Expand Up @@ -269,10 +275,7 @@ async def get_mode_devices(self, home: Home) -> list[ModeDevice]:
Get mode devices for the selected home.
"""
await self.ensure_token_valid()
url = (
f"https://petsseries-backend.prod.eu-hs.iot.versuni.com/"
f"api/homes/{home.id}/modes/home/devices"
)
url = f"{self.config.base_url}/api/homes/{home.id}/modes/home/devices"
session = await self.get_client()
try:
async with session.get(url, headers=self.headers) as response:
Expand All @@ -298,8 +301,7 @@ async def update_device_settings(
"""
await self.ensure_token_valid()
url = (
f"https://petsseries-backend.prod.eu-hs.iot.versuni.com/"
f"api/homes/{home.id}/modes/home/devices/{device_id}"
f"{self.config.base_url}/api/homes/{home.id}/modes/home/devices/{device_id}"
)

headers = {
Expand Down
8 changes: 3 additions & 5 deletions petsseries/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,12 @@ class Config:
Represents the configuration for the PetsSeries system.
"""

base_url: str = "https://petsseries-backend.prod.eu-hs.iot.versuni.com"
base_url: str = "https://petseries.prd.nbx.iot.versuni.com"
user_info_url: str = (
"https://cdc.accounts.home.id/oidc/op/v1.0/4_JGZWlP8eQHpEqkvQElolbA/userinfo"
)
consumer_url: str = (
"https://nbx-discovery.prod.eu-hs.iot.versuni.com/api/petsseries/consumer"
)
homes_url: str = base_url + "/api/v1/home-management/available-homes"
consumer_url: str = base_url + "/api/consumer"
homes_url: str = base_url + "/api/homes"
token_url: str = (
"https://cdc.accounts.home.id/oidc/op/v1.0/4_JGZWlP8eQHpEqkvQElolbA/token"
)
130 changes: 63 additions & 67 deletions petsseries/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,25 @@
"""

import logging
from typing import List
import urllib.parse
from typing import List

import aiohttp
import aiohttp # type: ignore[import-not-found]

from .config import Config
from .models import (
Home,
DeviceOfflineEvent,
DeviceOnlineEvent,
Event,
MotionEvent,
MealDispensedEvent,
MealUpcomingEvent,
FoodLevelLowEvent,
MealEnabledEvent,
FilterReplacementDueEvent,
FoodLevelLowEvent,
FoodOutletStuckEvent,
DeviceOfflineEvent,
DeviceOnlineEvent,
Home,
MealDispensedEvent,
MealEnabledEvent,
MealUpcomingEvent,
MotionEvent,
)
from .config import Config

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -88,8 +88,7 @@ async def get_events(
to_date_encoded = urllib.parse.quote(to_date.isoformat())

url = (
f"https://petsseries-backend.prod.eu-hs.iot.versuni.com/"
f"api/homes/{home.id}/events"
f"{self.config.base_url}/api/homes/{home.id}/events"
f"?from={from_date_encoded}&to={to_date_encoded}&clustered={clustered}"
f"{types_param}"
)
Expand Down Expand Up @@ -126,10 +125,7 @@ async def get_event(self, home: Home, event_id: str) -> Event:
Exception: For any unexpected errors.
"""
await self.client.ensure_token_valid()
url = (
f"https://petsseries-backend.prod.eu-hs.iot.versuni.com/"
f"api/homes/{home.id}/events/{event_id}"
)
url = f"{self.config.base_url}/api/homes/{home.id}/events/{event_id}"
session = await self.client.get_client()
try:
async with session.get(url, headers=self.client.headers) as response:
Expand Down Expand Up @@ -159,11 +155,11 @@ def parse_event(self, event: dict) -> Event:
match event_type:
case "motion_detected":
return MotionEvent(
id=event.get("id"),
type=event_type,
source=event.get("source"),
time=event.get("time"),
url=event.get("url"),
id=str(event.get("id", "")),
type=str(event_type),
source=str(event.get("source", "")),
time=str(event.get("time", "")),
url=str(event.get("url", "")),
cluster_id=event.get("clusterId"),
metadata=event.get("metadata"),
thumbnail_key=event.get("thumbnailKey"),
Expand All @@ -175,11 +171,11 @@ def parse_event(self, event: dict) -> Event:
)
case "meal_dispensed":
return MealDispensedEvent(
id=event.get("id"),
type=event_type,
source=event.get("source"),
time=event.get("time"),
url=event.get("url"),
id=str(event.get("id", "")),
type=str(event_type),
source=str(event.get("source", "")),
time=str(event.get("time", "")),
url=str(event.get("url", "")),
cluster_id=event.get("clusterId"),
metadata=event.get("metadata"),
meal_name=event.get("mealName"),
Expand All @@ -192,11 +188,11 @@ def parse_event(self, event: dict) -> Event:
)
case "meal_upcoming":
return MealUpcomingEvent(
id=event.get("id"),
type=event_type,
source=event.get("source"),
time=event.get("time"),
url=event.get("url"),
id=str(event.get("id", "")),
type=str(event_type),
source=str(event.get("source", "")),
time=str(event.get("time", "")),
url=str(event.get("url", "")),
cluster_id=event.get("clusterId"),
metadata=event.get("metadata"),
meal_name=event.get("mealName"),
Expand All @@ -209,11 +205,11 @@ def parse_event(self, event: dict) -> Event:
)
case "food_level_low":
return FoodLevelLowEvent(
id=event.get("id"),
type=event_type,
source=event.get("source"),
time=event.get("time"),
url=event.get("url"),
id=str(event.get("id", "")),
type=str(event_type),
source=str(event.get("source", "")),
time=str(event.get("time", "")),
url=str(event.get("url", "")),
cluster_id=event.get("clusterId"),
metadata=event.get("metadata"),
device_id=event.get("deviceId"),
Expand All @@ -223,11 +219,11 @@ def parse_event(self, event: dict) -> Event:
)
case "meal_enabled":
return MealEnabledEvent(
id=event.get("id"),
type=event_type,
source=event.get("source"),
time=event.get("time"),
url=event.get("url"),
id=str(event.get("id", "")),
type=str(event_type),
source=str(event.get("source", "")),
time=str(event.get("time", "")),
url=str(event.get("url", "")),
cluster_id=event.get("clusterId"),
metadata=event.get("metadata"),
meal_amount=event.get("mealAmount"),
Expand All @@ -241,11 +237,11 @@ def parse_event(self, event: dict) -> Event:
)
case "filter_replacement_due":
return FilterReplacementDueEvent(
id=event.get("id"),
type=event_type,
source=event.get("source"),
time=event.get("time"),
url=event.get("url"),
id=str(event.get("id", "")),
type=str(event_type),
source=str(event.get("source", "")),
time=str(event.get("time", "")),
url=str(event.get("url", "")),
cluster_id=event.get("clusterId"),
metadata=event.get("metadata"),
device_id=event.get("deviceId"),
Expand All @@ -255,11 +251,11 @@ def parse_event(self, event: dict) -> Event:
)
case "food_outlet_stuck":
return FoodOutletStuckEvent(
id=event.get("id"),
type=event_type,
source=event.get("source"),
time=event.get("time"),
url=event.get("url"),
id=str(event.get("id", "")),
type=str(event_type),
source=str(event.get("source", "")),
time=str(event.get("time", "")),
url=str(event.get("url", "")),
cluster_id=event.get("clusterId"),
metadata=event.get("metadata"),
device_id=event.get("deviceId"),
Expand All @@ -269,11 +265,11 @@ def parse_event(self, event: dict) -> Event:
)
case "device_offline":
return DeviceOfflineEvent(
id=event.get("id"),
type=event_type,
source=event.get("source"),
time=event.get("time"),
url=event.get("url"),
id=str(event.get("id", "")),
type=str(event_type),
source=str(event.get("source", "")),
time=str(event.get("time", "")),
url=str(event.get("url", "")),
cluster_id=event.get("clusterId"),
metadata=event.get("metadata"),
device_id=event.get("deviceId"),
Expand All @@ -283,11 +279,11 @@ def parse_event(self, event: dict) -> Event:
)
case "device_online":
return DeviceOnlineEvent(
id=event.get("id"),
type=event_type,
source=event.get("source"),
time=event.get("time"),
url=event.get("url"),
id=str(event.get("id", "")),
type=str(event_type),
source=str(event.get("source", "")),
time=str(event.get("time", "")),
url=str(event.get("url", "")),
cluster_id=event.get("clusterId"),
metadata=event.get("metadata"),
device_id=event.get("deviceId"),
Expand All @@ -299,9 +295,9 @@ def parse_event(self, event: dict) -> Event:
_LOGGER.warning("Unknown event type: %s", event_type)
# Generic event
return Event(
id=event["id"],
type=event_type,
source=event["source"],
time=event["time"],
url=event["url"],
id=str(event.get("id", "")),
type=str(event_type),
source=str(event.get("source", "")),
time=str(event.get("time", "")),
url=str(event.get("url", "")),
)
Loading
Loading