minor/formatting
This commit is contained in:
4
base.py
4
base.py
@@ -101,7 +101,9 @@ routes: dict = {
|
|||||||
"meme": importlib.import_module("endpoints.meme").Meme(app, util, constants),
|
"meme": importlib.import_module("endpoints.meme").Meme(app, util, constants),
|
||||||
"trip": importlib.import_module("endpoints.rip").RIP(app, util, constants),
|
"trip": importlib.import_module("endpoints.rip").RIP(app, util, constants),
|
||||||
"auth": importlib.import_module("endpoints.auth").Auth(app),
|
"auth": importlib.import_module("endpoints.auth").Auth(app),
|
||||||
"lighting": importlib.import_module("endpoints.lighting").Lighting(app, util, constants),
|
"lighting": importlib.import_module("endpoints.lighting").Lighting(
|
||||||
|
app, util, constants
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Misc endpoint depends on radio endpoint instance
|
# Misc endpoint depends on radio endpoint instance
|
||||||
|
@@ -10,8 +10,8 @@ import redis
|
|||||||
from lyric_search.sources import private
|
from lyric_search.sources import private
|
||||||
from auth.deps import get_current_user
|
from auth.deps import get_current_user
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from pycync.user import User # type: ignore
|
from pycync.user import User # type: ignore
|
||||||
from pycync.cync import Cync as Cync # type: ignore
|
from pycync.cync import Cync as Cync # type: ignore
|
||||||
from pycync import Auth # type: ignore
|
from pycync import Auth # type: ignore
|
||||||
from pycync.exceptions import TwoFactorRequiredError, AuthFailedError # type: ignore
|
from pycync.exceptions import TwoFactorRequiredError, AuthFailedError # type: ignore
|
||||||
|
|
||||||
@@ -28,26 +28,35 @@ class Lighting(FastAPI):
|
|||||||
if not self.cync_device_name:
|
if not self.cync_device_name:
|
||||||
missing_vars.append("CYNC_DEVICE_NAME")
|
missing_vars.append("CYNC_DEVICE_NAME")
|
||||||
if missing_vars:
|
if missing_vars:
|
||||||
raise Exception(f"Missing required environment variables: {', '.join(missing_vars)}")
|
raise Exception(
|
||||||
|
f"Missing required environment variables: {', '.join(missing_vars)}"
|
||||||
|
)
|
||||||
# Cast to str after check to silence linter
|
# Cast to str after check to silence linter
|
||||||
cync_email: str = self.cync_email # type: ignore
|
cync_email: str = self.cync_email # type: ignore
|
||||||
cync_password: str = self.cync_password # type: ignore
|
cync_password: str = self.cync_password # type: ignore
|
||||||
|
|
||||||
# Check if session is closed or missing
|
# Check if session is closed or missing
|
||||||
if not self.session or getattr(self.session, 'closed', False):
|
if not self.session or getattr(self.session, "closed", False):
|
||||||
self.session = aiohttp.ClientSession()
|
self.session = aiohttp.ClientSession()
|
||||||
cached_user = self._load_cached_user()
|
cached_user = self._load_cached_user()
|
||||||
if cached_user:
|
if cached_user:
|
||||||
self.auth = Auth(session=self.session,
|
self.auth = Auth(
|
||||||
user=cached_user,
|
session=self.session,
|
||||||
username=cync_email,
|
user=cached_user,
|
||||||
password=cync_password)
|
username=cync_email,
|
||||||
|
password=cync_password,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.auth = Auth(session=self.session, username=cync_email,
|
self.auth = Auth(
|
||||||
password=cync_password)
|
session=self.session, username=cync_email, password=cync_password
|
||||||
|
)
|
||||||
# Try to refresh token
|
# Try to refresh token
|
||||||
self.cync_user = None
|
self.cync_user = None
|
||||||
if self.auth.user and hasattr(self.auth.user, 'expires_at') and self.auth.user.expires_at > time.time():
|
if (
|
||||||
|
self.auth.user
|
||||||
|
and hasattr(self.auth.user, "expires_at")
|
||||||
|
and self.auth.user.expires_at > time.time()
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
await self.auth.async_refresh_user_token()
|
await self.auth.async_refresh_user_token()
|
||||||
self.cync_user = self.auth.user
|
self.cync_user = self.auth.user
|
||||||
@@ -60,10 +69,12 @@ class Lighting(FastAPI):
|
|||||||
self.cync_user = await self.auth.login()
|
self.cync_user = await self.auth.login()
|
||||||
self._save_cached_user(self.cync_user)
|
self._save_cached_user(self.cync_user)
|
||||||
except TwoFactorRequiredError:
|
except TwoFactorRequiredError:
|
||||||
logging.error("Cync 2FA required. Set CYNC_2FA_CODE in env if needed.")
|
logging.error(
|
||||||
|
"Cync 2FA required. Set CYNC_2FA_CODE in env if needed."
|
||||||
|
)
|
||||||
raise Exception("Cync 2FA required.")
|
raise Exception("Cync 2FA required.")
|
||||||
except AuthFailedError as e:
|
except AuthFailedError as e:
|
||||||
logging.error(f"Failed to authenticate with Cync API: {e}")
|
logging.error("Failed to authenticate with Cync API: %s", e)
|
||||||
raise Exception("Cync authentication failed.")
|
raise Exception("Cync authentication failed.")
|
||||||
self.cync_api = await Cync.create(self.auth)
|
self.cync_api = await Cync.create(self.auth)
|
||||||
# Also check if cync_api is None (shouldn't happen, but just in case)
|
# Also check if cync_api is None (shouldn't happen, but just in case)
|
||||||
@@ -72,6 +83,7 @@ class Lighting(FastAPI):
|
|||||||
logging.critical("self.auth: %s", self.auth)
|
logging.critical("self.auth: %s", self.auth)
|
||||||
return
|
return
|
||||||
self.cync_api = await Cync.create(self.auth)
|
self.cync_api = await Cync.create(self.auth)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Lighting Endpoints
|
Lighting Endpoints
|
||||||
"""
|
"""
|
||||||
@@ -82,7 +94,9 @@ class Lighting(FastAPI):
|
|||||||
self.app: FastAPI = app
|
self.app: FastAPI = app
|
||||||
self.util = util
|
self.util = util
|
||||||
self.constants = constants
|
self.constants = constants
|
||||||
self.redis_client = redis.Redis(password=private.REDIS_PW, decode_responses=True)
|
self.redis_client = redis.Redis(
|
||||||
|
password=private.REDIS_PW, decode_responses=True
|
||||||
|
)
|
||||||
self.lighting_key = "lighting:state"
|
self.lighting_key = "lighting:state"
|
||||||
|
|
||||||
# Cync config
|
# Cync config
|
||||||
@@ -107,20 +121,31 @@ class Lighting(FastAPI):
|
|||||||
if not self.cync_device_name:
|
if not self.cync_device_name:
|
||||||
missing_vars.append("CYNC_DEVICE_NAME")
|
missing_vars.append("CYNC_DEVICE_NAME")
|
||||||
if missing_vars:
|
if missing_vars:
|
||||||
raise Exception(f"Missing required environment variables: {', '.join(missing_vars)}")
|
raise Exception(
|
||||||
|
f"Missing required environment variables: {', '.join(missing_vars)}"
|
||||||
|
)
|
||||||
|
|
||||||
self.session = aiohttp.ClientSession()
|
self.session = aiohttp.ClientSession()
|
||||||
cached_user = self._load_cached_user()
|
cached_user = self._load_cached_user()
|
||||||
if cached_user:
|
if cached_user:
|
||||||
self.auth = Auth(session=self.session, user=cached_user,
|
self.auth = Auth(
|
||||||
username=self.cync_email or "",
|
session=self.session,
|
||||||
password=self.cync_password or "")
|
user=cached_user,
|
||||||
|
username=self.cync_email or "",
|
||||||
|
password=self.cync_password or "",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.auth = Auth(session=self.session,
|
self.auth = Auth(
|
||||||
username=self.cync_email or "",
|
session=self.session,
|
||||||
password=self.cync_password or "")
|
username=self.cync_email or "",
|
||||||
|
password=self.cync_password or "",
|
||||||
|
)
|
||||||
# Try to refresh token
|
# Try to refresh token
|
||||||
if self.auth.user and hasattr(self.auth.user, 'expires_at') and self.auth.user.expires_at > time.time():
|
if (
|
||||||
|
self.auth.user
|
||||||
|
and hasattr(self.auth.user, "expires_at")
|
||||||
|
and self.auth.user.expires_at > time.time()
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
await self.auth.async_refresh_user_token()
|
await self.auth.async_refresh_user_token()
|
||||||
self.cync_user = self.auth.user
|
self.cync_user = self.auth.user
|
||||||
@@ -133,10 +158,12 @@ class Lighting(FastAPI):
|
|||||||
self.cync_user = await self.auth.login()
|
self.cync_user = await self.auth.login()
|
||||||
self._save_cached_user(self.cync_user)
|
self._save_cached_user(self.cync_user)
|
||||||
except TwoFactorRequiredError:
|
except TwoFactorRequiredError:
|
||||||
logging.error("Cync 2FA required. Set CYNC_2FA_CODE in env if needed.")
|
logging.error(
|
||||||
|
"Cync 2FA required. Set CYNC_2FA_CODE in env if needed."
|
||||||
|
)
|
||||||
raise Exception("Cync 2FA required.")
|
raise Exception("Cync 2FA required.")
|
||||||
except AuthFailedError as e:
|
except AuthFailedError as e:
|
||||||
logging.error(f"Failed to authenticate with Cync API: {e}")
|
logging.error("Failed to authenticate with Cync API: %s", e)
|
||||||
raise Exception("Cync authentication failed.")
|
raise Exception("Cync authentication failed.")
|
||||||
# Create persistent Cync API object
|
# Create persistent Cync API object
|
||||||
self.cync_api = await Cync.create(self.auth)
|
self.cync_api = await Cync.create(self.auth)
|
||||||
@@ -151,7 +178,10 @@ class Lighting(FastAPI):
|
|||||||
handler,
|
handler,
|
||||||
methods=["GET"],
|
methods=["GET"],
|
||||||
include_in_schema=True,
|
include_in_schema=True,
|
||||||
dependencies=[Depends(RateLimiter(times=10, seconds=2)), Depends(get_current_user)],
|
dependencies=[
|
||||||
|
Depends(RateLimiter(times=10, seconds=2)),
|
||||||
|
Depends(get_current_user),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
app.add_api_route(
|
app.add_api_route(
|
||||||
@@ -159,7 +189,10 @@ class Lighting(FastAPI):
|
|||||||
self.set_lighting_state,
|
self.set_lighting_state,
|
||||||
methods=["POST"],
|
methods=["POST"],
|
||||||
include_in_schema=True,
|
include_in_schema=True,
|
||||||
dependencies=[Depends(RateLimiter(times=10, seconds=2)), Depends(get_current_user)],
|
dependencies=[
|
||||||
|
Depends(RateLimiter(times=10, seconds=2)),
|
||||||
|
Depends(get_current_user),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def _load_cached_user(self):
|
def _load_cached_user(self):
|
||||||
@@ -172,10 +205,10 @@ class Lighting(FastAPI):
|
|||||||
refresh_token=data["refresh_token"],
|
refresh_token=data["refresh_token"],
|
||||||
authorize=data["authorize"],
|
authorize=data["authorize"],
|
||||||
user_id=data["user_id"],
|
user_id=data["user_id"],
|
||||||
expires_at=data["expires_at"]
|
expires_at=data["expires_at"],
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(f"Failed to load cached Cync user: {e}")
|
logging.warning("Failed to load cached Cync user: %s", e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _save_cached_user(self, user):
|
def _save_cached_user(self, user):
|
||||||
@@ -185,13 +218,13 @@ class Lighting(FastAPI):
|
|||||||
"refresh_token": user.refresh_token,
|
"refresh_token": user.refresh_token,
|
||||||
"authorize": user.authorize,
|
"authorize": user.authorize,
|
||||||
"user_id": user.user_id,
|
"user_id": user.user_id,
|
||||||
"expires_at": user.expires_at
|
"expires_at": user.expires_at,
|
||||||
}
|
}
|
||||||
with open(self.token_cache_path, "w") as f:
|
with open(self.token_cache_path, "w") as f:
|
||||||
json.dump(data, f)
|
json.dump(data, f)
|
||||||
logging.info("Saved Cync user tokens to disk.")
|
logging.info("Saved Cync user tokens to disk.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(f"Failed to save Cync user tokens: {e}")
|
logging.warning("Failed to save Cync user tokens: %s", e)
|
||||||
|
|
||||||
async def get_lighting_state(self) -> JSONResponse:
|
async def get_lighting_state(self) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
@@ -209,11 +242,11 @@ class Lighting(FastAPI):
|
|||||||
default_state = {
|
default_state = {
|
||||||
"power": "off",
|
"power": "off",
|
||||||
"brightness": 50,
|
"brightness": 50,
|
||||||
"color": {"r": 255, "g": 255, "b": 255}
|
"color": {"r": 255, "g": 255, "b": 255},
|
||||||
}
|
}
|
||||||
return JSONResponse(content=default_state)
|
return JSONResponse(content=default_state)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error getting lighting state: {e}")
|
logging.error("Error getting lighting state: %s", e)
|
||||||
raise HTTPException(status_code=500, detail="Internal server error")
|
raise HTTPException(status_code=500, detail="Internal server error")
|
||||||
|
|
||||||
async def set_lighting_state(self, request: Request) -> JSONResponse:
|
async def set_lighting_state(self, request: Request) -> JSONResponse:
|
||||||
@@ -224,7 +257,9 @@ class Lighting(FastAPI):
|
|||||||
state = await request.json()
|
state = await request.json()
|
||||||
# Validate state (basic validation)
|
# Validate state (basic validation)
|
||||||
if not isinstance(state, dict):
|
if not isinstance(state, dict):
|
||||||
raise HTTPException(status_code=400, detail="State must be a JSON object")
|
raise HTTPException(
|
||||||
|
status_code=400, detail="State must be a JSON object"
|
||||||
|
)
|
||||||
|
|
||||||
# Store in Redis
|
# Store in Redis
|
||||||
self.redis_client.set(self.lighting_key, json.dumps(state))
|
self.redis_client.set(self.lighting_key, json.dumps(state))
|
||||||
@@ -234,20 +269,30 @@ class Lighting(FastAPI):
|
|||||||
# Apply to Cync device
|
# Apply to Cync device
|
||||||
power = state.get("power", "off")
|
power = state.get("power", "off")
|
||||||
if power not in ["on", "off"]:
|
if power not in ["on", "off"]:
|
||||||
raise HTTPException(status_code=400, detail=f"Invalid power state: {power}")
|
raise HTTPException(
|
||||||
|
status_code=400, detail=f"Invalid power state: {power}"
|
||||||
|
)
|
||||||
|
|
||||||
brightness = state.get("brightness", 50)
|
brightness = state.get("brightness", 50)
|
||||||
if not isinstance(brightness, (int, float)) or not (0 <= brightness <= 100):
|
if not isinstance(brightness, (int, float)) or not (0 <= brightness <= 100):
|
||||||
raise HTTPException(status_code=400, detail=f"Invalid brightness: {brightness}")
|
raise HTTPException(
|
||||||
|
status_code=400, detail=f"Invalid brightness: {brightness}"
|
||||||
|
)
|
||||||
|
|
||||||
color = state.get("color")
|
color = state.get("color")
|
||||||
if color and isinstance(color, dict) and all(k in color for k in ["r", "g", "b"]):
|
if (
|
||||||
|
color
|
||||||
|
and isinstance(color, dict)
|
||||||
|
and all(k in color for k in ["r", "g", "b"])
|
||||||
|
):
|
||||||
rgb = (color["r"], color["g"], color["b"])
|
rgb = (color["r"], color["g"], color["b"])
|
||||||
elif all(k in state for k in ["red", "green", "blue"]):
|
elif all(k in state for k in ["red", "green", "blue"]):
|
||||||
rgb = (state["red"], state["green"], state["blue"])
|
rgb = (state["red"], state["green"], state["blue"])
|
||||||
for val, name in zip(rgb, ["red", "green", "blue"]):
|
for val, name in zip(rgb, ["red", "green", "blue"]):
|
||||||
if not isinstance(val, int) or not (0 <= val <= 255):
|
if not isinstance(val, int) or not (0 <= val <= 255):
|
||||||
raise HTTPException(status_code=400, detail=f"Invalid {name} color value: {val}")
|
raise HTTPException(
|
||||||
|
status_code=400, detail=f"Invalid {name} color value: {val}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
rgb = None
|
rgb = None
|
||||||
|
|
||||||
@@ -256,10 +301,22 @@ class Lighting(FastAPI):
|
|||||||
raise HTTPException(status_code=500, detail="Cync API not initialized.")
|
raise HTTPException(status_code=500, detail="Cync API not initialized.")
|
||||||
devices = self.cync_api.get_devices()
|
devices = self.cync_api.get_devices()
|
||||||
if not devices or not isinstance(devices, (list, tuple)):
|
if not devices or not isinstance(devices, (list, tuple)):
|
||||||
raise HTTPException(status_code=500, detail="No devices returned from Cync API.")
|
raise HTTPException(
|
||||||
light = next((d for d in devices if hasattr(d, 'name') and d.name == self.cync_device_name), None)
|
status_code=500, detail="No devices returned from Cync API."
|
||||||
|
)
|
||||||
|
light = next(
|
||||||
|
(
|
||||||
|
d
|
||||||
|
for d in devices
|
||||||
|
if hasattr(d, "name") and d.name == self.cync_device_name
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
if not light:
|
if not light:
|
||||||
raise HTTPException(status_code=404, detail=f"Device '{self.cync_device_name}' not found")
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=f"Device '{self.cync_device_name}' not found",
|
||||||
|
)
|
||||||
|
|
||||||
# Set power
|
# Set power
|
||||||
if power == "on":
|
if power == "on":
|
||||||
@@ -275,10 +332,19 @@ class Lighting(FastAPI):
|
|||||||
if rgb:
|
if rgb:
|
||||||
await light.set_rgb(rgb)
|
await light.set_rgb(rgb)
|
||||||
|
|
||||||
logging.info(f"Successfully applied state to device '{self.cync_device_name}': {state}")
|
logging.info(
|
||||||
return JSONResponse(content={"message": "Lighting state updated and applied", "state": state})
|
"Successfully applied state to device '%s': %s",
|
||||||
|
self.cync_device_name,
|
||||||
|
state,
|
||||||
|
)
|
||||||
|
return JSONResponse(
|
||||||
|
content={
|
||||||
|
"message": "Lighting state updated and applied",
|
||||||
|
"state": state,
|
||||||
|
}
|
||||||
|
)
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error setting lighting state: {e}")
|
logging.error("Error setting lighting state: %s", e)
|
||||||
raise HTTPException(status_code=500, detail="Internal server error")
|
raise HTTPException(status_code=500, detail="Internal server error")
|
||||||
|
@@ -16,4 +16,4 @@ def decode_jwt(token: str) -> dict | None:
|
|||||||
return None
|
return None
|
||||||
return jwt.decode(token, key, algorithms=[JWT_ALGORITHM])
|
return jwt.decode(token, key, algorithms=[JWT_ALGORITHM])
|
||||||
except JWTError:
|
except JWTError:
|
||||||
return None
|
return None
|
||||||
|
Reference in New Issue
Block a user