"""Base Abstract class for American Electric Power.""" import re from abc import ABC, abstractmethod from html.parser import HTMLParser from typing import Any import aiohttp from ..const import USER_AGENT from ..exceptions import InvalidAuth class AEPLoginParser(HTMLParser): """HTML parser to extract login input fields.""" def __init__(self, username: str, password: str) -> None: """Initialize.""" super().__init__() self.inputs: dict[str, str] = {} self.username = username self.password = password self.password_field_found = False def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: """Try to extract the login input fields.""" if tag == "input": name = "" value = "" for a in attrs: if a[0] == "name" and a[1] is not None: name = a[1] if a[0] == "value" and a[1] is not None: value = a[1] if "UserID" in name: value = self.username if "Password" in name: value = self.password self.password_field_found = True self.inputs[name] = value class AEPBase(ABC): """Base Abstract class for American Electric Power.""" def __init__(self) -> None: """Initialize.""" super().__init__() self._subdomain: str | None = None def subdomain(self) -> str: """Return the opower.com subdomain for this utility.""" assert self._subdomain, "async_login not called" return self._subdomain @staticmethod def timezone() -> str: """Return the timezone.""" return "America/New_York" @staticmethod @abstractmethod def hostname() -> str: """Return the hostname for login.""" async def async_login( self, session: aiohttp.ClientSession, username: str, password: str, login_data: dict[str, Any], ) -> str: """Login in AEP using user/pass and return the Opower access token.""" # Clear cookies before logging in again, in case old ones are still around session.cookie_jar.clear(lambda c: c["domain"].endswith("opower.com")) login_parser = AEPLoginParser(username, password) # Get the login page and parse the ASP.Net Form Field that have generated names usage_url = f"https://www.{self.hostname()}/account/usage/" async with session.get( usage_url, headers={"User-Agent": USER_AGENT}, raise_for_status=True, ) as resp: text = await resp.text() login_parser.feed(text) # Post the login page with the user credentials async with session.post( usage_url, headers={ "User-Agent": USER_AGENT, "Content-Type": "application/x-www-form-urlencoded", "Referer": usage_url, }, raise_for_status=True, data=login_parser.inputs, ) as resp: html = await resp.text() match = re.search(r'<[^>]*?class="error"[^>]*?>.*?

(.*?)

', html, re.DOTALL) if match: raise InvalidAuth(match.group(1).strip()) match = re.search(r"https://([^.]*).opower.com", html) assert match self._subdomain = match.group(1) async with session.get( f"https://www.{self.hostname()}/account/oauth/ValidToken", headers={ "User-Agent": USER_AGENT, "Accept": "application/json, text/javascript, */*; q=0.01", "X-Requested-With": "XMLHttpRequest", "Referer": usage_url, }, raise_for_status=True, ) as token_resp: token_data = await token_resp.json() if not token_data or not isinstance(token_data, list) or not token_data: raise InvalidAuth("Failed to retrieve access token from AEP") data = token_data[0].get("data") if not data or not data.get("AccessToken"): raise InvalidAuth("Invalid token response structure from AEP") return str(data["AccessToken"])