├── tests ├── __init__.py ├── bandit.yaml └── test_init.py ├── custom_components ├── __init__.py └── tesla_gateway │ ├── const.py │ ├── strings.json │ ├── translations │ └── en.json │ ├── manifest.json │ ├── services.yaml │ ├── config_flow.py │ ├── __init__.py │ └── __init__.py.0eb19d943460c39f631c0777ff8e7ae8.tmp ├── .vscode └── settings.json ├── requirements.test.txt ├── hacs.json ├── .github └── workflows │ ├── hassfest.yaml │ └── validate.yaml ├── README.md ├── setup.cfg ├── .pre-commit-config.yaml └── .gitignore /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /custom_components/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "black" 3 | } -------------------------------------------------------------------------------- /requirements.test.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov==2.9.0 3 | pytest-homeassistant-custom-component 4 | -------------------------------------------------------------------------------- /custom_components/tesla_gateway/const.py: -------------------------------------------------------------------------------- 1 | DOMAIN = "tesla_gateway" 2 | CONF_REFRESH_TOKEN = "refresh_token" 3 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tesla Gateway", 3 | "render_readme": true, 4 | "iot_class": "Cloud Polling" 5 | } 6 | -------------------------------------------------------------------------------- /tests/bandit.yaml: -------------------------------------------------------------------------------- 1 | # https://bandit.readthedocs.io/en/latest/config.html 2 | 3 | tests: 4 | - B108 5 | - B306 6 | - B307 7 | - B313 8 | - B314 9 | - B315 10 | - B316 11 | - B317 12 | - B318 13 | - B319 14 | - B320 15 | - B325 16 | - B602 17 | - B604 18 | -------------------------------------------------------------------------------- /.github/workflows/hassfest.yaml: -------------------------------------------------------------------------------- 1 | name: Validate with hassfest 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | validate: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v2" 14 | - uses: home-assistant/actions/hassfest@master 15 | -------------------------------------------------------------------------------- /tests/test_init.py: -------------------------------------------------------------------------------- 1 | """Test component setup.""" 2 | from homeassistant.setup import async_setup_component 3 | 4 | from custom_components.tesla_gateway.const import DOMAIN 5 | 6 | 7 | async def test_async_setup(hass): 8 | """Test the component gets setup.""" 9 | assert await async_setup_component(hass, DOMAIN, {}) is True 10 | -------------------------------------------------------------------------------- /.github/workflows/validate.yaml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | validate: 9 | runs-on: "ubuntu-latest" 10 | steps: 11 | - uses: "actions/checkout@v2" 12 | - name: HACS validation 13 | uses: "hacs/action@main" 14 | with: 15 | category: "integration" 16 | -------------------------------------------------------------------------------- /custom_components/tesla_gateway/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "error": {}, 4 | "step": { 5 | "user": { 6 | "data": { "username": "Tesla Username", "password": "Tesla Password" }, 7 | "description": "Enter your Tesla Gatway details.", 8 | "title": "Authentication" 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /custom_components/tesla_gateway/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "error": {}, 4 | "step": { 5 | "user": { 6 | "data": { "username": "Tesla Username", "password": "Tesla Password" }, 7 | "description": "Enter your Tesla Gatway details.", 8 | "title": "Authentication" 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /custom_components/tesla_gateway/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "codeowners": ["@carboncoop"], 3 | "config_flow": true, 4 | "dependencies": [], 5 | "documentation": "https://github.com/carboncoop/tesla-gateway-ha-component", 6 | "issue_tracker": "https://github.com/carboncoop/tesla-gateway-ha-component/issues", 7 | "domain": "tesla_gateway", 8 | "name": "Tesla Gateway", 9 | "version": "0.1.0", 10 | "requirements": ["teslapy==1.1.0"], 11 | "iot_class": "cloud_polling" 12 | } 13 | -------------------------------------------------------------------------------- /custom_components/tesla_gateway/services.yaml: -------------------------------------------------------------------------------- 1 | set_operation: 2 | description: > 3 | Changes operation mode 4 | fields: 5 | real_mode: 6 | description: Mode to set to the Tesla gateway. 7 | example: "self_consumption, backup, autonomous" 8 | backup_reserve_percent: 9 | description: Percentage of battery reserve 10 | example: 10 11 | set_reserve: 12 | description: > 13 | Changes battery reserve percent in self_consumption mode 14 | fields: 15 | backup_reserve_percent: 16 | description: Percentage of battery reserve 17 | example: 70 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tesla Powerwall Gateway for Home Assistant 2 | 3 | Uses the Tesla API to control a Powerwall. 4 | 5 | ## Installation 6 | 7 | Install the repository through HACS by adding a custom repository or by manually copying the `custom_components/tesla_gateway` folder into your `custom_components` folder. 8 | 9 | ## Configuration 10 | 11 | The component is now configured through the user interface. 12 | 13 | To setup the integration, got to Configuration -> Integrations, and search for Tesla Gateway 14 | Add your Tesla username and password. 15 | 16 | ## Services 17 | 18 | The integration provides two services - `set_operation` and `set_reserve`. 19 | You can call these from the Developer -> Services page, or include them in automations. 20 | 21 | ### set_operation 22 | 23 | Sets the operation mode of the PowerWall. Possible values include `self_consumption`, `backup` or `autonomous`. 24 | Service data looks like this: 25 | 26 | ``` 27 | real_mode: 'self_consumption' 28 | backup_reserve_percent: 20 29 | ``` 30 | 31 | ### set_reserve 32 | 33 | Changes battery reserve percent. 34 | 35 | ``` 36 | backup_reserve_percent: 10 37 | ``` 38 | -------------------------------------------------------------------------------- /custom_components/tesla_gateway/config_flow.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional 2 | 3 | from homeassistant import config_entries 4 | from homeassistant.const import CONF_PASSWORD, CONF_USERNAME 5 | import homeassistant.helpers.config_validation as cv 6 | import voluptuous as vol 7 | 8 | from .const import DOMAIN 9 | 10 | AUTH_SCHEMA = vol.Schema( 11 | { 12 | vol.Required(CONF_USERNAME): cv.string, 13 | vol.Optional(CONF_PASSWORD): cv.string, 14 | } 15 | ) 16 | 17 | 18 | class TeslaGatewayConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 19 | """Tesla Gateway config flow.""" 20 | 21 | data: Optional[Dict[str, Any]] 22 | 23 | async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None): 24 | """Invoked when a user initiates a flow via the user interface.""" 25 | errors: Dict[str, str] = {} 26 | if user_input is not None: 27 | if not errors: 28 | self.data = user_input 29 | return self.async_create_entry(title="Tesla Gateway", data=self.data) 30 | 31 | return self.async_show_form( 32 | step_id="user", data_schema=AUTH_SCHEMA, errors=errors 33 | ) 34 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [coverage:run] 2 | source = 3 | custom_components 4 | 5 | [coverage:report] 6 | exclude_lines = 7 | pragma: no cover 8 | raise NotImplemented() 9 | if __name__ == '__main__': 10 | main() 11 | show_missing = true 12 | 13 | [tool:pytest] 14 | testpaths = tests 15 | norecursedirs = .git 16 | addopts = 17 | --strict 18 | --cov=custom_components 19 | 20 | [flake8] 21 | # https://github.com/ambv/black#line-length 22 | max-line-length = 88 23 | # E501: line too long 24 | # W503: Line break occurred before a binary operator 25 | # E203: Whitespace before ':' 26 | # D202 No blank lines allowed after function docstring 27 | # W504 line break after binary operator 28 | ignore = 29 | E501, 30 | W503, 31 | E203, 32 | D202, 33 | W504 34 | 35 | [isort] 36 | # https://github.com/timothycrosley/isort 37 | # https://github.com/timothycrosley/isort/wiki/isort-Settings 38 | # splits long import on multiple lines indented by 4 spaces 39 | multi_line_output = 3 40 | include_trailing_comma=True 41 | force_grid_wrap=0 42 | use_parentheses=True 43 | line_length=88 44 | indent = " " 45 | # by default isort don't check module indexes 46 | not_skip = __init__.py 47 | # will group `import x` and `from x import` of the same module. 48 | force_sort_within_sections = true 49 | sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER 50 | default_section = THIRDPARTY 51 | known_first_party = custom_components,tests 52 | forced_separate = tests 53 | combine_as_imports = true 54 | 55 | [mypy] 56 | python_version = 3.7 57 | ignore_errors = true 58 | follow_imports = silent 59 | ignore_missing_imports = true 60 | warn_incomplete_stub = true 61 | warn_redundant_casts = true 62 | warn_unused_configs = true 63 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/asottile/pyupgrade 3 | rev: v2.3.0 4 | hooks: 5 | - id: pyupgrade 6 | args: [--py37-plus] 7 | - repo: https://github.com/psf/black 8 | rev: 19.10b0 9 | hooks: 10 | - id: black 11 | args: 12 | - --safe 13 | - --quiet 14 | files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ 15 | - repo: https://github.com/codespell-project/codespell 16 | rev: v1.16.0 17 | hooks: 18 | - id: codespell 19 | args: 20 | - --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing 21 | - --skip="./.*,*.csv,*.json" 22 | - --quiet-level=2 23 | exclude_types: [csv, json] 24 | - repo: https://gitlab.com/pycqa/flake8 25 | rev: 3.8.1 26 | hooks: 27 | - id: flake8 28 | additional_dependencies: 29 | - flake8-docstrings==1.5.0 30 | - pydocstyle==5.0.2 31 | files: ^(homeassistant|script|tests)/.+\.py$ 32 | - repo: https://github.com/PyCQA/bandit 33 | rev: 1.6.2 34 | hooks: 35 | - id: bandit 36 | args: 37 | - --quiet 38 | - --format=custom 39 | - --configfile=tests/bandit.yaml 40 | files: ^(homeassistant|script|tests)/.+\.py$ 41 | - repo: https://github.com/pre-commit/mirrors-isort 42 | rev: v4.3.21 43 | hooks: 44 | - id: isort 45 | - repo: https://github.com/pre-commit/pre-commit-hooks 46 | rev: v2.4.0 47 | hooks: 48 | - id: check-executables-have-shebangs 49 | stages: [manual] 50 | - id: check-json 51 | - repo: https://github.com/pre-commit/mirrors-mypy 52 | rev: v0.770 53 | hooks: 54 | - id: mypy 55 | args: 56 | - --pretty 57 | - --show-error-codes 58 | - --show-error-context 59 | -------------------------------------------------------------------------------- /custom_components/tesla_gateway/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Monitors and controls the Tesla gateway. 3 | """ 4 | import logging 5 | 6 | import asyncio 7 | import voluptuous as vol 8 | import teslapy 9 | 10 | from homeassistant.const import ( 11 | CONF_USERNAME, 12 | CONF_PASSWORD 13 | ) 14 | import homeassistant.helpers.config_validation as cv 15 | 16 | DOMAIN = 'tesla_gateway' 17 | 18 | _LOGGER = logging.getLogger(__name__) 19 | 20 | CONFIG_SCHEMA = vol.Schema({ 21 | DOMAIN: vol.Schema({ 22 | vol.Required(CONF_USERNAME): cv.string, 23 | vol.Required(CONF_PASSWORD): cv.string 24 | }), 25 | }, extra=vol.ALLOW_EXTRA) 26 | 27 | @asyncio.coroutine 28 | def async_setup(hass, config): 29 | 30 | domain_config = config[DOMAIN] 31 | conf_user = domain_config[CONF_USERNAME] 32 | conf_password = domain_config[CONF_PASSWORD] 33 | 34 | tesla = teslapy.Tesla(domain_config[CONF_USERNAME], domain_config[CONF_PASSWORD]) 35 | 36 | def get_battery(): 37 | batteries = tesla.battery_list() 38 | if len(batteries) > 0: 39 | return batteries[0] 40 | else: 41 | return None 42 | 43 | @asyncio.coroutine 44 | async def set_operation(service): 45 | 46 | battery = await hass.async_add_executor_job(get_battery) 47 | if not battery: 48 | _LOGGER.warning('Battery object is None') 49 | return None 50 | 51 | await hass.async_add_executor_job(battery.set_operation, service.data['real_mode']) 52 | if 'backup_reserve_percent' in service.data: 53 | await hass.async_add_executor_job(battery.set_backup_reserve_percent, service.data['backup_reserve_percent']) 54 | 55 | hass.services.async_register(DOMAIN, 'set_operation', set_operation) 56 | 57 | @asyncio.coroutine 58 | async def set_reserve(service): 59 | 60 | battery = await hass.async_add_executor_job(get_battery) 61 | if not battery: 62 | _LOGGER.warning('Battery object is None') 63 | return None 64 | 65 | if 'backup_reserve_percent' in service.data: 66 | await hass.async_add_executor_job(battery.set_backup_reserve_percent, service.data['backup_reserve_percent']) 67 | 68 | hass.services.async_register(DOMAIN, 'set_reserve', set_reserve) 69 | 70 | return True 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/python,homeassistant,vscode 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=python,homeassistant,vscode 4 | 5 | ### HomeAssistant ### 6 | # Files with personal details 7 | *.crt 8 | *.csr 9 | *.key 10 | .google.token 11 | .uuid 12 | icloud/ 13 | google_calendars.yaml 14 | harmony_media_room.conf 15 | home-assistant.db 16 | home-assistant_v2.db 17 | home-assistant_v2.db-* 18 | html5_push_registrations.conf 19 | ip_bans.yaml 20 | known_devices.yaml 21 | phue.conf 22 | plex.conf 23 | pyozw.sqlite 24 | secrets.yaml 25 | tradfri.conf 26 | 27 | # Temporary files 28 | *.db-journal 29 | *.pid 30 | tts 31 | 32 | # automatically downloaded dependencies 33 | deps 34 | lib 35 | www 36 | 37 | # Log files 38 | home-assistant.log 39 | ozw_log.txt 40 | 41 | 42 | ### Python ### 43 | # Byte-compiled / optimized / DLL files 44 | __pycache__/ 45 | *.py[cod] 46 | *$py.class 47 | 48 | # C extensions 49 | *.so 50 | 51 | # Distribution / packaging 52 | .Python 53 | build/ 54 | develop-eggs/ 55 | dist/ 56 | downloads/ 57 | eggs/ 58 | .eggs/ 59 | lib/ 60 | lib64/ 61 | parts/ 62 | sdist/ 63 | var/ 64 | wheels/ 65 | pip-wheel-metadata/ 66 | share/python-wheels/ 67 | *.egg-info/ 68 | .installed.cfg 69 | *.egg 70 | MANIFEST 71 | 72 | # PyInstaller 73 | # Usually these files are written by a python script from a template 74 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 75 | *.manifest 76 | *.spec 77 | 78 | # Installer logs 79 | pip-log.txt 80 | pip-delete-this-directory.txt 81 | 82 | # Unit test / coverage reports 83 | htmlcov/ 84 | .tox/ 85 | .nox/ 86 | .coverage 87 | .coverage.* 88 | .cache 89 | nosetests.xml 90 | coverage.xml 91 | *.cover 92 | *.py,cover 93 | .hypothesis/ 94 | .pytest_cache/ 95 | pytestdebug.log 96 | 97 | # Translations 98 | *.mo 99 | *.pot 100 | 101 | # Django stuff: 102 | *.log 103 | local_settings.py 104 | db.sqlite3 105 | db.sqlite3-journal 106 | 107 | # Flask stuff: 108 | instance/ 109 | .webassets-cache 110 | 111 | # Scrapy stuff: 112 | .scrapy 113 | 114 | # Sphinx documentation 115 | docs/_build/ 116 | doc/_build/ 117 | 118 | # PyBuilder 119 | target/ 120 | 121 | # Jupyter Notebook 122 | .ipynb_checkpoints 123 | 124 | # IPython 125 | profile_default/ 126 | ipython_config.py 127 | 128 | # pyenv 129 | .python-version 130 | 131 | # pipenv 132 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 133 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 134 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 135 | # install all needed dependencies. 136 | #Pipfile.lock 137 | 138 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 139 | __pypackages__/ 140 | 141 | # Celery stuff 142 | celerybeat-schedule 143 | celerybeat.pid 144 | 145 | # SageMath parsed files 146 | *.sage.py 147 | 148 | # Environments 149 | .env 150 | .venv 151 | env/ 152 | venv/ 153 | ENV/ 154 | env.bak/ 155 | venv.bak/ 156 | pythonenv* 157 | 158 | # Spyder project settings 159 | .spyderproject 160 | .spyproject 161 | 162 | # Rope project settings 163 | .ropeproject 164 | 165 | # mkdocs documentation 166 | /site 167 | 168 | # mypy 169 | .mypy_cache/ 170 | .dmypy.json 171 | dmypy.json 172 | 173 | # Pyre type checker 174 | .pyre/ 175 | 176 | # pytype static type analyzer 177 | .pytype/ 178 | 179 | # profiling data 180 | .prof 181 | 182 | ### vscode ### 183 | .vscode/* 184 | !.vscode/settings.json 185 | !.vscode/tasks.json 186 | !.vscode/launch.json 187 | !.vscode/extensions.json 188 | *.code-workspace 189 | 190 | # End of https://www.toptal.com/developers/gitignore/api/python,homeassistant,vscode 191 | 192 | -------------------------------------------------------------------------------- /custom_components/tesla_gateway/__init__.py.0eb19d943460c39f631c0777ff8e7ae8.tmp: -------------------------------------------------------------------------------- 1 | """ 2 | Monitors and controls the Tesla gateway. 3 | """ 4 | import logging 5 | 6 | import aiohttp 7 | import asyncio 8 | import async_timeout 9 | import base64 10 | import hashlib 11 | import json 12 | import os 13 | import re 14 | import time 15 | from urllib.parse import parse_qs 16 | import voluptuous as vol 17 | from homeassistant import config_entries, core 18 | from homeassistant.const import ( 19 | CONF_USERNAME, 20 | CONF_PASSWORD, 21 | CONF_ACCESS_TOKEN, 22 | ) 23 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 24 | import homeassistant.helpers.config_validation as cv 25 | 26 | from .const import DOMAIN, CONF_REFRESH_TOKEN 27 | 28 | _LOGGER = logging.getLogger(__name__) 29 | 30 | DEFAULT_TIMEOUT = 100 31 | 32 | CONFIG_SCHEMA = vol.Schema( 33 | { 34 | DOMAIN: vol.Schema( 35 | { 36 | vol.Required(CONF_USERNAME): cv.string, 37 | vol.Required(CONF_PASSWORD): cv.string, 38 | vol.Optional(CONF_ACCESS_TOKEN, default=""): cv.string, 39 | vol.Optional(CONF_REFRESH_TOKEN, default=""): cv.string, 40 | } 41 | ), 42 | }, 43 | extra=vol.ALLOW_EXTRA, 44 | ) 45 | 46 | tesla_base_url = "https://owner-api.teslamotors.com" 47 | tesla_auth_url = "https://auth.tesla.com" 48 | 49 | step_max_attempts = 7 50 | step_attempt_sleep = 3 51 | TESLA_CLIENT_ID = "81527cff06843c8634fdc09e8ac0abefb46ac849f38fe1e431c2ef2106796384" 52 | 53 | 54 | @asyncio.coroutine 55 | def async_setup(hass: core.HomeAssistant, config: dict) -> bool: 56 | websession = async_get_clientsession(hass, verify_ssl=False) 57 | hass.data.setdefault(DOMAIN, {}) 58 | domain_config = config[DOMAIN] 59 | setup_common(domain_config=domain_config) 60 | return True 61 | 62 | 63 | @asyncio.coroutine 64 | def async_setup_entry( 65 | hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry 66 | ) -> bool: 67 | websession = async_get_clientsession(hass, verify_ssl=False) 68 | 69 | hass.data.setdefault(DOMAIN, {}) 70 | hass.data[DOMAIN][config_entry.entry_id] = config_entry.data 71 | domain_config = dict(config_entry.data) 72 | # conf_user = domain_config[CONF_USERNAME] 73 | # conf_password = domain_config[CONF_PASSWORD] 74 | setup_common(domain_config=domain_config) 75 | return True 76 | 77 | 78 | @asyncio.coroutine 79 | def setup_common(domain_config: dict): 80 | @asyncio.coroutine 81 | def SSO_login(): 82 | 83 | # Code extracted from https://github.com/enode-engineering/tesla-oauth2/blob/2414d74a50f38ab7b3ad5424de4e867ac2709dcf/tesla.py 84 | # Login process explained at https://tesla-api.timdorr.com/api-basics/authentication 85 | 86 | authorize_url = tesla_auth_url + "/oauth2/v3/authorize" 87 | callback_url = tesla_auth_url + "/void/callback" 88 | 89 | headers = { 90 | "User-Agent": "curl", 91 | "x-tesla-user-agent": "TeslaApp/3.10.9-433/adff2e065/android/10", 92 | "X-Requested-With": "com.teslamotors.tesla", 93 | } 94 | 95 | verifier_bytes = os.urandom(86) 96 | code_verifier = base64.urlsafe_b64encode(verifier_bytes).rstrip(b"=") 97 | code_challenge = ( 98 | base64.urlsafe_b64encode(hashlib.sha256(code_verifier).digest()) 99 | .rstrip(b"=") 100 | .decode("utf-8") 101 | ) 102 | state = base64.urlsafe_b64encode(os.urandom(16)).rstrip(b"=").decode("utf-8") 103 | 104 | params = ( 105 | ("client_id", "ownerapi"), 106 | ("code_challenge", code_challenge), 107 | ("code_challenge_method", "S256"), 108 | ("redirect_uri", callback_url), 109 | ("response_type", "code"), 110 | ("scope", "openid email offline_access"), 111 | ("state", state), 112 | ) 113 | 114 | try: 115 | # Step 1: Obtain the login page 116 | _LOGGER.debug("Step 1: GET %s\nparams %s", authorize_url, params) 117 | for attempt in range(step_max_attempts): 118 | with async_timeout.timeout(DEFAULT_TIMEOUT, loop=hass.loop): 119 | response = yield from websession.get( 120 | authorize_url, 121 | headers=headers, 122 | params=params, 123 | raise_for_status=False, 124 | ) 125 | 126 | returned_text = yield from response.text() 127 | if response.status == 200 and "