Skip to content

Commit

Permalink
Re-worked logon method since Anycubic broke it - thanks
Browse files Browse the repository at this point in the history
  • Loading branch information
WaresWichall committed Sep 2, 2024
1 parent 6475c1b commit 1270eee
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 104 deletions.
23 changes: 18 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Anycubic Cloud Home Assistant Integration

## NEW LOGIN METHOD

Anycubic decided to break the login method I was using before, so you'll need to follow the install steps below to get a token, and set up the integration from a fresh start.

Unfortunately this means that if you get logged out (by logging in elsewhere), you'll need to get a fresh token as below and re-authenticate the integration.

## WORK IN PROGRESS

Component is working very well so far with:
Expand Down Expand Up @@ -54,11 +60,18 @@ If you find updates for any sensors are only received every minute, please open

## How to Install

1. Add this repository to HACS under ... > Custom Repositories as an **Integration**
2. Restart Home Assistant
3. Go to Settings>Integrations>Add New and search Anycubic
4. Enter your **email** and password, then press enter
5. Select your printer, then you're good to go!
1. Go to the [Anycubic Cloud Website](https://cloud-universe.anycubic.com/file)
2. Log in
3. Open Developer Tools in your browser
4. Paste `window.localStorage["XX-Token"]` into the **console**
5. Copy/save the long string of numbers and letters without the `''` - this is your user token.
6. Add this repository to HACS under ... > Custom Repositories as an **Integration**
7. Restart Home Assistant
8. Go to Settings > Integrations > Add New and search Anycubic
9. Paste your **user token** into the `User Token` box.
10. Select your printer, then you're good to go!

<img width="400" alt="" src="https://raw.githubusercontent.com/WaresWichall/hass-anycubic_cloud/dev/screenshots/anycubic_api_token.png">


## UI
Expand Down
125 changes: 92 additions & 33 deletions custom_components/anycubic_cloud/anycubic_cloud_api/anycubic_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,23 +99,24 @@
class AnycubicAPI:
def __init__(
self,
api_username,
api_password,
session,
cookie_jar,
debug_logger=None,
auth_as_app=False,
auth_sig_token=None,
):
# Cache
self._cache_key_path = None
self._cache_tokens_path = None
self._cache_sig_token_path = None
# API
self.base_url = f"https://{BASE_DOMAIN}/"
self._public_api_root = f"{self.base_url}{PUBLIC_API_ENDPOINT}"
# Internal
self._api_username = api_username
self._api_password = api_password
self._api_username = None
self._api_password = None
self._api_user_id = None
self._api_user_email = None
self._session: aiohttp.ClientSession = session
self._sessionjar = cookie_jar
self._cookie_state = generate_cookie_state()
Expand All @@ -132,13 +133,39 @@ def __init__(
# ANYCUBIC AUTH VARS
self._login_auth_code = None
self._auth_access_token = None
self._auth_sig_token = None
self._auth_sig_token = auth_sig_token
self._auth_referer = None

def set_username_password(
self,
api_username,
api_password,
):
self._api_username = api_username
self._api_password = api_password

def set_auth_sig_token(
self,
auth_sig_token,
):
self._auth_sig_token = auth_sig_token

@property
def tokens_changed(self):
return self._tokens_changed

@property
def api_user_id(self):
return self._api_user_id

@property
def api_user_email(self):
return self._api_user_email

@property
def api_user_identifier(self):
return self._api_user_email or self._api_user_id

def _log_to_debug(self, msg):
if self._debug_logger:
self._debug_logger.debug(msg)
Expand Down Expand Up @@ -359,6 +386,12 @@ async def _password_logon(self):
query=query,
params=params
)

if resp is None or not isinstance(resp['data'], str):
err_msg = "Unexpected response for login, rate limited?"
self._log_to_warn(err_msg)
raise AnycubicAPIParsingError(err_msg)

self._login_auth_code = resp['data']
self._log_to_debug("Successfully logged in.")

Expand Down Expand Up @@ -473,6 +506,16 @@ async def _save_main_tokens(self):
async with aio_file_open(self._cache_key_path, mode='w') as wo:
await wo.write(json.dumps(self.build_token_dict()))

async def _load_cached_sig_token(self):
if self._cache_sig_token_path is not None and (await aio_path.exists(self._cache_sig_token_path)):

try:
async with aio_file_open(self._cache_sig_token_path, mode='r') as wo:
self._auth_sig_token = await wo.read()

except Exception:
pass

async def _load_main_tokens(self):
tokens_loaded = False
if self._cache_tokens_path is not None and (await aio_path.exists(self._cache_tokens_path)):
Expand Down Expand Up @@ -522,40 +565,55 @@ async def _check_can_access_api(
self,
use_known=True,
):
if not self._api_tokens_loaded():
cached_tokens = await self._load_main_tokens()
else:
cached_tokens = True
if cached_tokens:
try:
await self.get_user_info()
return True
except APIAuthTokensExpired:
cached_tokens = None
self._log_to_debug("Tokens expired.")
if not cached_tokens:
try:
await self._login_retrieve_tokens(
use_known=use_known
)
await self._save_main_tokens()
except Exception:
return False
self._set_known_app_vars()
await self._load_cached_sig_token()
try:
await self.get_user_info()
except Exception:
return True
except APIAuthTokensExpired:
self._log_to_debug("Tokens expired.")
return False
return True

# async def _check_can_access_api(
# self,
# use_known=True,
# ):
# if not self._api_tokens_loaded():
# cached_tokens = await self._load_main_tokens()
# else:
# cached_tokens = True
# if cached_tokens:
# try:
# await self.get_user_info()
# return True
# except APIAuthTokensExpired:
# cached_tokens = None
# self._log_to_debug("Tokens expired.")
# if not cached_tokens:
# try:
# await self._login_retrieve_tokens(
# use_known=use_known
# )
# await self._save_main_tokens()
# except Exception:
# return False
# try:
# await self.get_user_info()
# except Exception:
# return False
# return True

async def check_api_tokens(self):
if not await self._check_can_access_api(True):
if self._api_tokens_loaded() and not self._auth_as_app:
self._log_to_debug(
"Login failed, retrying with new variables..."
)
return await self._check_can_access_api(False)
else:
return False
return False

# if self._api_tokens_loaded() and not self._auth_as_app:
# self._log_to_debug(
# "Login failed, retrying with new variables..."
# )
# return await self._check_can_access_api(False)
# else:
# return False

return True

Expand Down Expand Up @@ -694,6 +752,7 @@ async def get_user_info(
raise APIAuthTokensExpired('Invalid credentials.')

self._api_user_id = data['id']
self._api_user_email = data['user_email']

return data

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@ def __init__(
mqtt_callback_printer_busy=None,
**kwargs,
):
self._api_username = None
self._api_password = None
self._auth_sig_token = None
self._api_user_id = None
self._mqtt_client = None
Expand Down Expand Up @@ -93,15 +91,15 @@ def _md5_hex_of_string(self, input_string):
return hashlib.md5(input_string.encode('utf-8')).hexdigest().lower()

def _build_mqtt_client_id(self):
username_md5 = self._md5_hex_of_string(self._api_username)
username_md5 = self._md5_hex_of_string(self._api_user_email)
return username_md5

def _build_mqtt_login_info(self):
token_md5 = self._md5_hex_of_string(self._auth_sig_token)
token_bcrypt = bcrypt.hashpw(token_md5.encode('utf-8'), bcrypt.gensalt())
username_md5 = self._md5_hex_of_string(self._api_username)
username_md5 = self._md5_hex_of_string(self._api_user_email)
sig_md5 = self._md5_hex_of_string(f"{username_md5}{token_bcrypt.decode('utf-8')}{username_md5}")
sig_str = f"user|app|{self._api_username}|{sig_md5}"
sig_str = f"user|app|{self._api_user_email}|{sig_md5}"

return (sig_str, token_bcrypt.decode('utf-8'))

Expand Down
Loading

0 comments on commit 1270eee

Please sign in to comment.