Skip to content

Commit

Permalink
feat: support for vodafone sercomm h300s
Browse files Browse the repository at this point in the history
Tested with:
  Firmware: Vodafone-H-300s-v1.2.02.03 and Vodafone-H-300s-v1.2.02.05
  Hardware: V2
  • Loading branch information
myhomeiot committed Apr 27, 2024
1 parent 24a9493 commit 4608915
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 33 deletions.
106 changes: 73 additions & 33 deletions src/aiovodafone/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,18 +81,26 @@ async def get_device_type(
response_json = await response.json()
if "data" in response_json and "ModelName" in response_json["data"]:
return DeviceType.TECHNICOLOR
async with session.get(
f"https://{host}/login.html",
headers=HEADERS,
ssl=False,
) as response:
# To identify the Sercomm devices before the login
# There's no other sure way to identify a Sercomm device without login
if (
response.status == HTTPStatus.OK
and "var csrf_token = " in await response.text()
):
return DeviceType.SERCOMM

for protocol in ["https", "http"]:
try:
async with session.get(
f"{protocol}://{host}/login.html",
headers=HEADERS,
ssl=False,
) as response:
# To identify the Sercomm devices before the login
# There's no other sure way to identify a Sercomm device
# without login
if (
response.status == HTTPStatus.OK
and "var csrf_token = " in await response.text()
):
return DeviceType.SERCOMM
except aiohttp.client_exceptions.ClientConnectorSSLError:
_LOGGER.debug("Unable to login using protocol %s", protocol)
continue

return None

def __init__(self, host: str, username: str, password: str) -> None:
Expand Down Expand Up @@ -431,6 +439,19 @@ async def _encrypt_string(self, credential: str) -> str:
digestmod=hashlib.sha256,
).hexdigest()

async def _encrypt_with_challenge(self, challenge: str) -> str:
"""Encrypt password with challenge for login."""
return hashlib.sha256(
bytes(self.password + challenge, "utf-8"),
).hexdigest()

async def _get_challenge(self) -> str:
"""Return challenge or login."""
return_dict = await self._get_sercomm_page("/data/login.json")
challenge: str = return_dict["challenge"]
_LOGGER.debug("challenge: <%s>", challenge)
return challenge

async def _reset(self) -> bool:
"""Reset page content before loading."""
payload = {"chk_sys_busy": ""}
Expand All @@ -440,22 +461,23 @@ async def _reset(self) -> bool:

return False

async def _login_json(self, username: str, password: str) -> bool:
async def _login_json(self, payload: dict[str, Any]) -> bool:
"""Login via json page."""
payload = {
"LoginName": username,
"LoginPWD": password,
}
reply_json = await self._post_sercomm_page("/data/login.json", payload)
_LOGGER.debug("Login result: %s[%s]", LOGIN[int(str(reply_json))], reply_json)
reply_str = str(reply_json)
_LOGGER.debug(
"Login result: %s[%s]",
LOGIN[int(reply_str)] if 0 <= int(reply_str) < len(LOGIN) else "unknown",
reply_json,
)

if reply_json == "1":
if reply_str == "1":
return True

if reply_json == "2":
if reply_str == "2":
raise AlreadyLogged

if reply_json in ["3", "4", "5"]:
if reply_str in ["3", "4", "5", "7"]:
raise CannotAuthenticate

raise GenericLoginError
Expand Down Expand Up @@ -553,20 +575,38 @@ async def login(self) -> bool:
await self._set_cookie()
await self._reset()

# First try with both username and password encrypted
# Second try with plain username and password encrypted
try:
_LOGGER.debug("Login first try: username[encrypted], password[encrypted]")
logged = await self._login_json(
await self._encrypt_string(self.username),
await self._encrypt_string(self.password),
)
except CannotAuthenticate:
_LOGGER.debug("Login second try: username[plain], password[encrypted]")
if not self.encryption_key:
_LOGGER.debug("Login: username[plain], password[challenge encrypted]")

challenge = await self._get_challenge()
logged = await self._login_json(
self.username,
await self._encrypt_string(self.password),
{
"LoginName": self.username,
"LoginPWD": await self._encrypt_with_challenge(challenge),
"challenge": challenge,
},
)
else:
# First try with both username and password encrypted
# Second try with plain username and password encrypted
try:
_LOGGER.debug(
"Login first try: username[encrypted], password[encrypted]",
)
logged = await self._login_json(
{
"LoginName": await self._encrypt_string(self.username),
"LoginPWD": await self._encrypt_string(self.password),
},
)
except CannotAuthenticate:
_LOGGER.debug("Login second try: username[plain], password[encrypted]")
logged = await self._login_json(
{
"LoginName": self.username,
"LoginPWD": await self._encrypt_string(self.password),
},
)

return logged

Expand Down
2 changes: 2 additions & 0 deletions src/aiovodafone/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"credential error",
"credential error",
"password mismatch",
"incorrect challenge",
"password mismatch",
]

USER_ALREADY_LOGGED_IN = "MSG_LOGIN_150"
Expand Down

0 comments on commit 4608915

Please sign in to comment.