diff --git a/requirements.txt b/requirements.txt
index ab90481..3e0c1de 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,3 @@
 lxml
+requests
+cloudscraper
diff --git a/setup.py b/setup.py
index d6289c9..08673a4 100644
--- a/setup.py
+++ b/setup.py
@@ -11,36 +11,25 @@ def read(fname):
 
 
 setup(
-    name='withings-sync',
-    version='3.4.2',
-    author='Masayuki Hamasaki, Steffen Vogel',
-    author_email='post@steffenvogel.de',
-    description='A tool for synchronisation of Withings (ex. Nokia Health Body) to Garmin Connect and Trainer Road.',
-    license='MIT',
-    keywords='garmin withings sync api scale smarthome',
-    url='http://packages.python.org/an_example_pypi_project',
-    packages=['withings_sync'],
-    long_description=read('README.md'),
-    long_description_content_type='text/markdown',
+    name="withings-sync",
+    version="3.5",
+    author="Masayuki Hamasaki, Steffen Vogel",
+    author_email="post@steffenvogel.de",
+    description="A tool for synchronisation of Withings (ex. Nokia Health Body) to Garmin Connect and Trainer Road.",
+    license="MIT",
+    keywords="garmin withings sync api scale smarthome",
+    url="http://packages.python.org/an_example_pypi_project",
+    packages=["withings_sync"],
+    long_description=read("README.md"),
+    long_description_content_type="text/markdown",
     classifiers=[
-        'Topic :: Utilities',
-        'License :: OSI Approved :: MIT License',
-    ],
-    install_requires=[
-        'lxml',
-        'requests',
-        # request has a fix dependency on charset-normalizer charset_normalizer~=2.0.0
-        #  (see https://requests.readthedocs.io/en/latest/user/advanced/#encodings)
-        # We pin it here to avoid conflicts with httpx
-        'charset-normalizer~=2.0.0',
-        'httpx',
-        'httpx[http2]'
+        "Topic :: Utilities",
+        "License :: OSI Approved :: MIT License",
     ],
+    install_requires=["lxml", "requests", "cloudscraper"],
     entry_points={
-        'console_scripts': [
-            'withings-sync=withings_sync.sync:main'
-        ],
+        "console_scripts": ["withings-sync=withings_sync.sync:main"],
     },
     zip_safe=False,
-    include_package_data=True
+    include_package_data=True,
 )
diff --git a/withings_sync/garmin.py b/withings_sync/garmin.py
index ffd4723..bdba85b 100644
--- a/withings_sync/garmin.py
+++ b/withings_sync/garmin.py
@@ -1,149 +1,166 @@
-from datetime import timedelta
+"""This module handles the Garmin connectivity."""
 import urllib.request
-import httpx
 import urllib.error
 import urllib.parse
 import re
-import sys
 import json
 import logging
+import cloudscraper
 
-log = logging.getLogger('garmin')
+
+log = logging.getLogger("garmin")
 
 
 class LoginSucceeded(Exception):
-    pass
+    """Used to raise on LoginSucceeded"""
 
 
 class LoginFailed(Exception):
-    pass
+    """Used to raise on LoginFailed"""
 
 
 class APIException(Exception):
-    pass
+    """Used to raise on APIException"""
+
 
+class GarminConnect:
+    """Main GarminConnect class"""
 
-class GarminConnect(object):
-    LOGIN_URL = 'https://connect.garmin.com/signin'
-    UPLOAD_URL = 'https://connect.garmin.com/modern/proxy/upload-service/upload/.fit'
+    LOGIN_URL = "https://connect.garmin.com/signin"
+    UPLOAD_URL = "https://connect.garmin.com/modern/proxy/upload-service/upload/.fit"
 
     def create_opener(self, cookie):
+        """Garmin opener"""
         this = self
 
         class _HTTPRedirectHandler(urllib.request.HTTPRedirectHandler):
-            def http_error_302(self, req, fp, code, msg, headers):
+            def http_error_302(
+                self, req, fp, code, msg, headers
+            ):  # pylint: disable=too-many-arguments
                 if req.get_full_url() == this.LOGIN_URL:
                     raise LoginSucceeded
 
-                return urllib.request.HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers)
+                return urllib.request.HTTPRedirectHandler.http_error_302(
+                    self, req, fp, code, msg, headers
+                )
 
-        return urllib.request.build_opener(_HTTPRedirectHandler, urllib.request.HTTPCookieProcessor(cookie))
+        return urllib.request.build_opener(
+            _HTTPRedirectHandler, urllib.request.HTTPCookieProcessor(cookie)
+        )
 
     # From https://github.com/cpfair/tapiriik
+    @staticmethod
+    def get_session(email=None, password=None):
+        """tapiriik get_session code"""
+        session = cloudscraper.CloudScraper()
 
-    def _get_session(self, record=None, email=None, password=None):
-        session = httpx.Client(http2=True)
-
-        # JSIG CAS, cool I guess.
-        # Not quite OAuth though, so I'll continue to collect raw credentials.
-        # Commented stuff left in case this ever breaks because of missing parameters...
         data = {
-            'username': email,
-            'password': password,
-            '_eventId': 'submit',
-            'embed': 'true',
-            # 'displayNameRequired': 'false'
+            "username": email,
+            "password": password,
+            "_eventId": "submit",
+            "embed": "true",
         }
         params = {
-            'service': 'https://connect.garmin.com/modern',
-            # 'redirectAfterAccountLoginUrl': 'http://connect.garmin.com/modern',
-            # 'redirectAfterAccountCreationUrl': 'http://connect.garmin.com/modern',
-            # 'webhost': 'olaxpw-connect00.garmin.com',
-            'clientId': 'GarminConnect',
-            'gauthHost': 'https://sso.garmin.com/sso',
-            # 'rememberMeShown': 'true',
-            # 'rememberMeChecked': 'false',
-            'consumeServiceTicket': 'false',
-            # 'id': 'gauth-widget',
-            # 'embedWidget': 'false',
-            # 'cssUrl': 'https://static.garmincdn.com/com.garmin.connect/ui/src-css/gauth-custom.css',
-            # 'source': 'http://connect.garmin.com/en-US/signin',
-            # 'createAccountShown': 'true',
-            # 'openCreateAccount': 'false',
-            # 'usernameShown': 'true',
-            # 'displayNameShown': 'false',
-            # 'initialFocus': 'true',
-            # 'locale': 'en'
+            "service": "https://connect.garmin.com/modern",
+            "clientId": "GarminConnect",
+            "gauthHost": "https://sso.garmin.com/sso",
+            "consumeServiceTicket": "false",
         }
 
         headers = {
-                'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36',
-                'Referer': 'https://jhartman.pl',
-                'origin': 'https://sso.garmin.com'
-            }
-
-        # I may never understand what motivates people to mangle a perfectly good protocol like HTTP in the ways they do...
-        preResp = session.get('https://sso.garmin.com/sso/signin', params=params, headers=headers)
-        if preResp.status_code != 200:
-            raise APIException('SSO prestart error %s %s' % (preResp.status_code, preResp.text))
-
-        ssoResp = session.post('https://sso.garmin.com/sso/login', params=params, data=data, headers=headers)
-        
-        if ssoResp.status_code != 200 or 'temporarily unavailable' in ssoResp.text:
-            raise APIException('SSO error %s %s' % (ssoResp.status_code, ssoResp.text))
-
-        if '>sendEvent(\'FAIL\')' in ssoResp.text:
-            raise APIException('Invalid login')
-        
-        if '>sendEvent(\'ACCOUNT_LOCKED\')' in ssoResp.text:
-            raise APIException('Account Locked')
-
-        if 'renewPassword' in ssoResp.text:
-            raise APIException('Reset password')
+            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 "
+            + "(KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36",
+            "Referer": "https://jhartman.pl",
+            "origin": "https://sso.garmin.com",
+        }
+
+        # I may never understand what motivates people to mangle a perfectly
+        # good protocol like HTTP in the ways they do...
+        preresp = session.get(
+            "https://sso.garmin.com/sso/signin", params=params, headers=headers
+        )
+        if preresp.status_code != 200:
+            raise APIException(
+                f"SSO prestart error {preresp.status_code} {preresp.text}"
+            )
+
+        ssoresp = session.post(
+            "https://sso.garmin.com/sso/login",
+            params=params,
+            data=data,
+            allow_redirects=False,
+            headers=headers,
+        )
+
+        if ssoresp.status_code == 429:
+            raise APIException(
+                "SSO error 429: You are being rate limited: "
+                + "The owner of this website (sso.garmin.com) "
+                + "has banned you temporarily from accessing this website."
+            )
+
+        if ssoresp.status_code != 200 or "temporarily unavailable" in ssoresp.text:
+            raise APIException(f"SSO error {ssoresp.status_code} {ssoresp.text}")
+
+        if ">sendEvent('FAIL')" in ssoresp.text:
+            raise APIException("Invalid login")
+
+        if ">sendEvent('ACCOUNT_LOCKED')" in ssoresp.text:
+            raise APIException("Account Locked")
+
+        if "renewPassword" in ssoresp.text:
+            raise APIException("Reset password")
 
         # self.print_cookies(cookies=session.cookies)
 
         # ...AND WE'RE NOT DONE YET!
 
-        gcRedeemResp = session.get('https://connect.garmin.com/modern', headers=headers)
-        if gcRedeemResp.status_code != 302:
-            raise APIException(f'GC redeem-start error {gcRedeemResp.status_code} {gcRedeemResp.text}')
+        gcredeemresp = session.get(
+            "https://connect.garmin.com/modern", allow_redirects=False, headers=headers
+        )
+        if gcredeemresp.status_code != 302:
+            raise APIException(
+                f"GC redeem-start error {gcredeemresp.status_code} {gcredeemresp.text}"
+            )
 
-        url_prefix = 'https://connect.garmin.com'
+        url_prefix = "https://connect.garmin.com"
 
         # There are 6 redirects that need to be followed to get the correct cookie
         # ... :(
         max_redirect_count = 7
         current_redirect_count = 1
         while True:
-            url = gcRedeemResp.headers['location']
+            url = gcredeemresp.headers["location"]
 
             # Fix up relative redirects.
-            if url.startswith('/'):
+            if url.startswith("/"):
                 url = url_prefix + url
-            url_prefix = '/'.join(url.split('/')[:3])
-            gcRedeemResp = session.get(url)
-
-            if (current_redirect_count >= max_redirect_count and
-                gcRedeemResp.status_code != 200):
-                raise APIException(f'GC redeem {current_redirect_count}/'
-                                   '{max_redirect_count} error '
-                                   '{gcRedeemResp.status_code} '
-                                   '{gcRedeemResp.text}')
-
-            if gcRedeemResp.status_code in [200, 404]:
+            url_prefix = "/".join(url.split("/")[:3])
+            gcredeemresp = session.get(url, allow_redirects=False)
+
+            if (
+                current_redirect_count >= max_redirect_count
+                and gcredeemresp.status_code != 200
+            ):
+                raise APIException(
+                    f"GC redeem {current_redirect_count}/"
+                    "{max_redirect_count} error "
+                    "{gcredeemresp.status_code} "
+                    "{gcredeemresp.text}"
+                )
+
+            if gcredeemresp.status_code in [200, 404]:
                 break
 
             current_redirect_count += 1
             if current_redirect_count > max_redirect_count:
                 break
 
-        # self.print_cookies(session.cookies)
+        # GarminConnect.print_cookies(session.cookies)
         session.headers.update(headers)
 
         return session
 
-
     @staticmethod
     def get_json(page_html, key):
         """Return json from text."""
@@ -153,52 +170,46 @@ def get_json(page_html, key):
             return json.loads(json_text)
         return None
 
-
-    def print_cookies(self, cookies):
-        log.debug('Cookies: ')
+    @staticmethod
+    def print_cookies(cookies):
+        """print cookies"""
+        log.debug("Cookies: ")
         for key, value in list(cookies.items()):
-            log.debug(' %s = %s', key, value)
-
-
-    def login(self, username, password):
-
-        session = self._get_session(email=username, password=password)
+            log.debug(" %s = %s", key, value)
 
+    @staticmethod
+    def login(username, password):
+        """login to Garmin"""
+        session = GarminConnect.get_session(email=username, password=password)
         try:
-            dashboard = session.get('http://connect.garmin.com/modern',follow_redirects=True)
+            dashboard = session.get("http://connect.garmin.com/modern")
             userdata = GarminConnect.get_json(dashboard.text, "VIEWER_SOCIAL_PROFILE")
-            username = userdata['userName']
+            username = userdata["displayName"]
 
-            log.info('Garmin Connect User Name: %s', username)
+            log.info("Garmin Connect User Name: %s", username)
 
-        except Exception as e:
-            log.error(e)
-            log.error('Unable to retrieve Garmin username! Most likely: '
-                      'incorrect Garmin login or password!')
+        except Exception as exception:  # pylint: disable=broad-except
+            log.error(exception)
+            log.error(
+                "Unable to retrieve Garmin username! Most likely: "
+                "incorrect Garmin login or password!"
+            )
             log.debug(dashboard.text)
 
         return session
 
-    def upload_file(self, f, session):
-        files = {
-            'data': (
-                'withings.fit', f
-            )
-        }
-
-        res = session.post(self.UPLOAD_URL,
-                           files=files,
-                           headers={'nk': 'NT'})
-
+    def upload_file(self, ffile, session):
+        """upload fit file to Garmin connect"""
+        files = {"data": ("withings.fit", ffile)}
+        res = session.post(self.UPLOAD_URL, files=files, headers={"nk": "NT"})
         try:
             resp = res.json()
-
-            if 'detailedImportResult' not in resp:
+            if "detailedImportResult" not in resp:
                 raise KeyError
         except (ValueError, KeyError):
-            if res.status_code == 204:   # HTTP result 204 - 'no content'
-                log.error('No data to upload, try to use --fromdate and --todate')
+            if res.status_code == 204:  # HTTP result 204 - 'no content'
+                log.error("No data to upload, try to use --fromdate and --todate")
             else:
-                log.error('Bad response during GC upload: %s', res.status_code)
+                log.error("Bad response during GC upload: %s", res.status_code)
 
         return res.status_code in [200, 201, 204]