diff --git a/.pylintrc b/.pylintrc index 5071b8a..c73587e 100644 --- a/.pylintrc +++ b/.pylintrc @@ -4,6 +4,6 @@ disable=missing-docstring [FORMAT] -max-args=6 +max-args=7 max-attributes=8 max-line-length=88 diff --git a/pyhon/__main__.py b/pyhon/__main__.py index 9e9b25b..28bfcdf 100755 --- a/pyhon/__main__.py +++ b/pyhon/__main__.py @@ -38,9 +38,7 @@ def get_arguments() -> Dict[str, Any]: "translate", help="language (de, en, fr...)", metavar="LANGUAGE" ) translation.add_argument("--json", help="print as json", action="store_true") - parser.add_argument( - "-i", "--import", help="import pyhon data", nargs="?", default=Path().cwd() - ) + parser.add_argument("-i", "--import", help="import pyhon data", nargs="?") return vars(parser.parse_args()) @@ -73,9 +71,8 @@ async def main() -> None: if language := args.get("translate"): await translate(language, json_output=args.get("json", "")) return - async with Hon( - *get_login_data(args), test_data_path=Path(args.get("import", "")) - ) as hon: + test_data_path = Path(path) if (path := args.get("import", "")) else None + async with Hon(*get_login_data(args), test_data_path=test_data_path) as hon: for device in hon.appliances: if args.get("export"): anonymous = args.get("anonymous", False) diff --git a/pyhon/appliance.py b/pyhon/appliance.py index 3586f4a..3754aca 100644 --- a/pyhon/appliance.py +++ b/pyhon/appliance.py @@ -72,12 +72,10 @@ class HonAppliance: return self.info[item] @overload - def get(self, item: str, default: None = None) -> Any: - ... + def get(self, item: str, default: None = None) -> Any: ... @overload - def get(self, item: str, default: T) -> T: - ... + def get(self, item: str, default: T) -> T: ... def get(self, item: str, default: Optional[T] = None) -> Any: try: diff --git a/pyhon/command_loader.py b/pyhon/command_loader.py index 94b0277..fd615ec 100644 --- a/pyhon/command_loader.py +++ b/pyhon/command_loader.py @@ -109,7 +109,7 @@ class HonCommandLoader: categories: Optional[Dict[str, "HonCommand"]] = None, category_name: str = "", ) -> Optional[HonCommand]: - """Try to crate HonCommand object""" + """Try to create HonCommand object""" if not isinstance(data, dict): self._additional_data[command_name] = data return None diff --git a/pyhon/commands.py b/pyhon/commands.py index d88cbd1..facf419 100644 --- a/pyhon/commands.py +++ b/pyhon/commands.py @@ -89,8 +89,11 @@ class HonCommand: def parameter_value(self) -> Dict[str, Union[str, float]]: return {n: p.value for n, p in self._parameters.items()} - def _load_parameters(self, attributes: Dict[str, Dict[str, Any]]) -> None: + def _load_parameters(self, attributes: Dict[str, Dict[str, Any] | Any]) -> None: for key, items in attributes.items(): + if not isinstance(items, dict): + _LOGGER.info("Loading Attributes - Skipping %s", str(items)) + continue for name, data in items.items(): self._create_parameters(data, name, key) for rule in self._rules: diff --git a/pyhon/connection/api.py b/pyhon/connection/api.py index 2cb7dfe..30a9cd2 100644 --- a/pyhon/connection/api.py +++ b/pyhon/connection/api.py @@ -24,12 +24,16 @@ class HonAPI: email: str = "", password: str = "", anonymous: bool = False, + mobile_id: str = "", + refresh_token: str = "", session: Optional[ClientSession] = None, ) -> None: super().__init__() self._email: str = email self._password: str = password self._anonymous: bool = anonymous + self._mobile_id: str = mobile_id + self._refresh_token: str = refresh_token self._hon_handler: Optional[HonConnectionHandler] = None self._hon_anonymous_handler: Optional[HonAnonymousConnectionHandler] = None self._session: Optional[ClientSession] = session @@ -69,7 +73,10 @@ class HonAPI: ).create() if not self._anonymous: self._hon_handler = await HonConnectionHandler( - self._email, self._password, self._session + self._email, + self._password, + session=self._session, + mobile_id=self._mobile_id, ).create() return self diff --git a/pyhon/connection/auth.py b/pyhon/connection/auth.py index cf75410..2096010 100644 --- a/pyhon/connection/auth.py +++ b/pyhon/connection/auth.py @@ -199,7 +199,7 @@ class HonAuth: if access_token := re.findall("access_token=(.*?)&", text): self._auth.access_token = access_token[0] if refresh_token := re.findall("refresh_token=(.*?)&", text): - self._auth.refresh_token = refresh_token[0] + self._auth.refresh_token = parse.unquote(refresh_token[0]) if id_token := re.findall("id_token=(.*?)&", text): self._auth.id_token = id_token[0] return bool(access_token and refresh_token and id_token) @@ -264,7 +264,9 @@ class HonAuth: except exceptions.HonNoAuthenticationNeeded: return - async def refresh(self) -> bool: + async def refresh(self, refresh_token: str = "") -> bool: + if refresh_token: + self._auth.refresh_token = refresh_token params = { "client_id": const.CLIENT_ID, "refresh_token": self._auth.refresh_token, diff --git a/pyhon/connection/device.py b/pyhon/connection/device.py index 730ef72..0923aae 100644 --- a/pyhon/connection/device.py +++ b/pyhon/connection/device.py @@ -1,16 +1,15 @@ -import secrets from typing import Dict from pyhon import const class HonDevice: - def __init__(self) -> None: + def __init__(self, mobile_id: str = "") -> None: self._app_version: str = const.APP_VERSION self._os_version: int = const.OS_VERSION self._os: str = const.OS self._device_model: str = const.DEVICE_MODEL - self._mobile_id: str = secrets.token_hex(8) + self._mobile_id: str = mobile_id or const.MOBILE_ID @property def app_version(self) -> str: diff --git a/pyhon/connection/handler/hon.py b/pyhon/connection/handler/hon.py index 8fbeca9..988c8f4 100644 --- a/pyhon/connection/handler/hon.py +++ b/pyhon/connection/handler/hon.py @@ -19,12 +19,18 @@ _LOGGER = logging.getLogger(__name__) class HonConnectionHandler(ConnectionHandler): def __init__( - self, email: str, password: str, session: Optional[aiohttp.ClientSession] = None + self, + email: str, + password: str, + session: Optional[aiohttp.ClientSession] = None, + mobile_id: str = "", + refresh_token: str = "", ) -> None: super().__init__(session=session) - self._device: HonDevice = HonDevice() + self._device: HonDevice = HonDevice(mobile_id) self._email: str = email self._password: str = password + self._refresh_token: str = refresh_token if not self._email: raise HonAuthenticationError("An email address must be specified") if not self._password: @@ -43,10 +49,17 @@ class HonConnectionHandler(ConnectionHandler): async def create(self) -> Self: await super().create() - self._auth = HonAuth(self.session, self._email, self._password, self._device) + self._auth = HonAuth( + self.session, + self._email, + self._password, + self._device, + ) return self async def _check_headers(self, headers: Dict[str, str]) -> Dict[str, str]: + if self._refresh_token: + await self.auth.refresh(self._refresh_token) if not (self.auth.cognito_token and self.auth.id_token): await self.auth.authenticate() headers["cognito-token"] = self.auth.cognito_token diff --git a/pyhon/const.py b/pyhon/const.py index 8ea749f..0bc1a02 100644 --- a/pyhon/const.py +++ b/pyhon/const.py @@ -6,8 +6,9 @@ CLIENT_ID = ( "3MVG9QDx8IX8nP5T2Ha8ofvlmjLZl5L_gvfbT9." "HJvpHGKoAS_dcMN8LYpTSYeVFCraUnV.2Ag1Ki7m4znVO6" ) -APP_VERSION = "2.4.7" -OS_VERSION = 31 +APP_VERSION = "2.6.5" +OS_VERSION = 999 OS = "android" -DEVICE_MODEL = "exynos9820" -USER_AGENT = "Chrome/110.0.5481.153" +DEVICE_MODEL = "pyhOn" +USER_AGENT = "Chrome/999.999.999.999" +MOBILE_ID = "pyhOn" diff --git a/pyhon/hon.py b/pyhon/hon.py index d6c73e2..7f53fb4 100644 --- a/pyhon/hon.py +++ b/pyhon/hon.py @@ -21,6 +21,8 @@ class Hon: email: Optional[str] = "", password: Optional[str] = "", session: Optional[ClientSession] = None, + mobile_id: str = "", + refresh_token: str = "", test_data_path: Optional[Path] = None, ): self._email: Optional[str] = email @@ -29,6 +31,8 @@ class Hon: self._appliances: List[HonAppliance] = [] self._api: Optional[HonAPI] = None self._test_data_path: Path = test_data_path or Path().cwd() + self._mobile_id: str = mobile_id + self._refresh_token: str = refresh_token async def __aenter__(self) -> Self: return await self.create() @@ -61,7 +65,11 @@ class Hon: async def create(self) -> Self: self._api = await HonAPI( - self.email, self.password, session=self._session + self.email, + self.password, + session=self._session, + mobile_id=self._mobile_id, + refresh_token=self._refresh_token, ).create() await self.setup() return self @@ -103,8 +111,12 @@ class Hon: ) await self._create_appliance(appliance, self.api) if ( - test_data := self._test_data_path / "hon-test-data" / "test_data" - ).exists() or (test_data := test_data / "test_data").exists(): + self._test_data_path + and ( + test_data := self._test_data_path / "hon-test-data" / "test_data" + ).exists() + or (test_data := test_data / "..").exists() + ): api = TestAPI(test_data) for appliance in await api.load_appliances(): await self._create_appliance(appliance, api) diff --git a/pyhon/parameter/range.py b/pyhon/parameter/range.py index 6eef53e..fa00de0 100644 --- a/pyhon/parameter/range.py +++ b/pyhon/parameter/range.py @@ -69,4 +69,9 @@ class HonParameterRange(HonParameter): @property def values(self) -> List[str]: - return [str(i) for i in range(int(self.min), int(self.max) + 1, int(self.step))] + result = [] + i = self.min + while i < self.max: + i += self.step + result.append(str(i)) + return result diff --git a/pyhon/typedefs.py b/pyhon/typedefs.py index 51a137b..68ddf73 100644 --- a/pyhon/typedefs.py +++ b/pyhon/typedefs.py @@ -14,8 +14,7 @@ if TYPE_CHECKING: class Callback(Protocol): # pylint: disable=too-few-public-methods def __call__( self, url: str | URL, *args: Any, **kwargs: Any - ) -> aiohttp.client._RequestContextManager: - ... + ) -> aiohttp.client._RequestContextManager: ... Parameter = Union[ diff --git a/requirements.txt b/requirements.txt index bc45835..81e74ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -aiohttp>=3.8 +aiohttp>=3.8.6 yarl>=1.8 typing-extensions>=4.8 diff --git a/setup.py b/setup.py index 3887039..dd638c3 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ with open("README.md", "r", encoding="utf-8") as f: setup( name="pyhOn", - version="0.15.14", + version="0.16.0", author="Andre Basche", description="Control hOn devices with python", long_description=long_description, @@ -21,7 +21,7 @@ setup( packages=find_packages(), include_package_data=True, python_requires=">=3.10", - install_requires=["aiohttp>=3.8", "typing-extensions>=4.8", "yarl>=1.8"], + install_requires=["aiohttp>=3.8.6", "typing-extensions>=4.8", "yarl>=1.8"], classifiers=[ "Development Status :: 4 - Beta", "Environment :: Console",