Created
December 10, 2025 05:00
-
-
Save mdz/96251cb12d48e2871b56ea0e28b67aa0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| SmartTub Authentication Diagnostic Script | |
| This script helps diagnose authentication issues with the SmartTub API. | |
| It will attempt to authenticate and report the exact error response | |
| (without exposing your credentials). | |
| Usage: | |
| python diagnose_auth.py | |
| You will be prompted for your SmartTub email and password. | |
| The password is not displayed as you type. | |
| """ | |
| import asyncio | |
| import getpass | |
| import json | |
| import sys | |
| try: | |
| import aiohttp | |
| except ImportError: | |
| print("Error: aiohttp is required. Install with: pip install aiohttp") | |
| sys.exit(1) | |
| # Auth0 configuration (same as SmartTub app) | |
| AUTH_URL = "https://smarttub.auth0.com/oauth/token" | |
| AUTH_AUDIENCE = "https://api.operation-link.com/" | |
| AUTH_CLIENT_ID = "dB7Rcp3rfKKh0vHw2uqkwOZmRb5WNjQC" | |
| AUTH_REALM = "Username-Password-Authentication" | |
| AUTH_GRANT_TYPE = "http://auth0.com/oauth/grant-type/password-realm" | |
| AUTH_SCOPE = "openid email offline_access User Admin" | |
| async def diagnose_auth(username: str, password: str) -> dict: | |
| """Attempt authentication and return diagnostic info.""" | |
| result = { | |
| "success": False, | |
| "http_status": None, | |
| "error_code": None, | |
| "error_description": None, | |
| "raw_response": None, | |
| "exception": None, | |
| } | |
| async with aiohttp.ClientSession() as session: | |
| try: | |
| payload = { | |
| "audience": AUTH_AUDIENCE, | |
| "client_id": AUTH_CLIENT_ID, | |
| "grant_type": AUTH_GRANT_TYPE, | |
| "realm": AUTH_REALM, | |
| "scope": AUTH_SCOPE, | |
| "username": username, | |
| "password": password, | |
| } | |
| async with session.post(AUTH_URL, json=payload) as response: | |
| result["http_status"] = response.status | |
| # Try to parse response as JSON | |
| try: | |
| body = await response.json() | |
| if response.status == 200: | |
| result["success"] = True | |
| # Don't include tokens in output | |
| result["raw_response"] = { | |
| "token_type": body.get("token_type"), | |
| "expires_in": body.get("expires_in"), | |
| "scope": body.get("scope"), | |
| # Redact sensitive fields | |
| "access_token": "[REDACTED]" if "access_token" in body else None, | |
| "refresh_token": "[REDACTED]" if "refresh_token" in body else None, | |
| "id_token": "[REDACTED]" if "id_token" in body else None, | |
| } | |
| else: | |
| # Error response | |
| result["error_code"] = body.get("error") | |
| result["error_description"] = body.get("error_description") | |
| result["raw_response"] = body | |
| except aiohttp.ContentTypeError: | |
| # Not JSON | |
| result["raw_response"] = await response.text() | |
| except aiohttp.ClientError as e: | |
| result["exception"] = f"{type(e).__name__}: {e}" | |
| except Exception as e: | |
| result["exception"] = f"{type(e).__name__}: {e}" | |
| return result | |
| def print_diagnostic_report(result: dict): | |
| """Print a formatted diagnostic report.""" | |
| print("\n" + "=" * 60) | |
| print("SmartTub Authentication Diagnostic Report") | |
| print("=" * 60) | |
| if result["success"]: | |
| print("\n✅ Authentication SUCCESSFUL") | |
| print(f" HTTP Status: {result['http_status']}") | |
| print("\n Your credentials are working correctly.") | |
| print(" If you're having issues with Home Assistant, the problem") | |
| print(" may be elsewhere (network, HA configuration, etc.)") | |
| else: | |
| print("\n❌ Authentication FAILED") | |
| print(f"\n HTTP Status: {result['http_status']}") | |
| if result["error_code"]: | |
| print(f" Error Code: {result['error_code']}") | |
| if result["error_description"]: | |
| print(f" Error Description: {result['error_description']}") | |
| if result["exception"]: | |
| print(f" Exception: {result['exception']}") | |
| if result["raw_response"]: | |
| print(f"\n Raw Response:") | |
| if isinstance(result["raw_response"], dict): | |
| print(f" {json.dumps(result['raw_response'], indent=2)}") | |
| else: | |
| print(f" {result['raw_response'][:500]}") | |
| print("\n" + "=" * 60) | |
| print("Please share this report (above the line) when reporting issues.") | |
| print("Do NOT share your email or password.") | |
| print("=" * 60 + "\n") | |
| async def main(): | |
| print("SmartTub Authentication Diagnostic Tool") | |
| print("-" * 40) | |
| print() | |
| # Get credentials | |
| email = input("Enter your SmartTub email: ").strip() | |
| if not email: | |
| print("Error: Email is required") | |
| sys.exit(1) | |
| password = getpass.getpass("Enter your SmartTub password: ") | |
| if not password: | |
| print("Error: Password is required") | |
| sys.exit(1) | |
| print("\nAttempting authentication...") | |
| result = await diagnose_auth(email, password) | |
| print_diagnostic_report(result) | |
| # Return exit code based on success | |
| sys.exit(0 if result["success"] else 1) | |
| if __name__ == "__main__": | |
| asyncio.run(main()) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment