mirror of
				https://github.com/home-assistant/supervisor.git
				synced 2025-10-29 21:49:37 +00:00 
			
		
		
		
	Compare commits
	
		
			41 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | ebe9ae2341 | ||
|   | e777bbd024 | ||
|   | 2116d56124 | ||
|   | 0b6a82b018 | ||
|   | b4ea28af4e | ||
|   | 22f59712df | ||
|   | efe95f7bab | ||
|   | 200c68f67f | ||
|   | dcefec7b99 | ||
|   | 5db798bcf8 | ||
|   | 70005296cc | ||
|   | f2bf8dea93 | ||
|   | fee858c956 | ||
|   | e3ae48c8ff | ||
|   | fa9e20385e | ||
|   | f51c9704e0 | ||
|   | 57c58d81c0 | ||
|   | 1ec1082068 | ||
|   | 35b7c2269c | ||
|   | cc3e6ec6fd | ||
|   | 4df42e054d | ||
|   | 1b481e0b37 | ||
|   | 3aa4cdf540 | ||
|   | 029f277945 | ||
|   | e7e0b9adda | ||
|   | 5fbff75da8 | ||
|   | 58299a0389 | ||
|   | 1151d7e17b | ||
|   | b56ed547e3 | ||
|   | a71ebba940 | ||
|   | 4fcb516c75 | ||
|   | 22142d32d2 | ||
|   | 21194f1411 | ||
|   | 09df046fa8 | ||
|   | 63d3889d5c | ||
|   | 0ffc0559e2 | ||
|   | 78118a502c | ||
|   | 946cc3d618 | ||
|   | c40a3f18e9 | ||
|   | f01945bf8c | ||
|   | 0f72db45f9 | 
							
								
								
									
										4
									
								
								.github/release-drafter.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.github/release-drafter.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | template: | | ||||||
|  |   ## What's Changed | ||||||
|  |  | ||||||
|  |   $CHANGES | ||||||
							
								
								
									
										12
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								.travis.yml
									
									
									
									
									
								
							| @@ -1,12 +1,6 @@ | |||||||
| sudo: false | sudo: true | ||||||
| matrix: | dist: xenial | ||||||
|   fast_finish: true |  | ||||||
|   include: |  | ||||||
|     - python: "3.6" |  | ||||||
|  |  | ||||||
| cache: |  | ||||||
|   directories: |  | ||||||
|     - $HOME/.cache/pip |  | ||||||
| install: pip install -U tox | install: pip install -U tox | ||||||
| language: python | language: python | ||||||
|  | python: 3.7 | ||||||
| script: tox | script: tox | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								API.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								API.md
									
									
									
									
									
								
							| @@ -273,7 +273,9 @@ return: | |||||||
| ```json | ```json | ||||||
| { | { | ||||||
|     "version": "2.3", |     "version": "2.3", | ||||||
|  |     "version_cli": "7", | ||||||
|     "version_latest": "2.4", |     "version_latest": "2.4", | ||||||
|  |     "version_cli_latest": "8", | ||||||
|     "board": "ova|rpi" |     "board": "ova|rpi" | ||||||
| } | } | ||||||
| ``` | ``` | ||||||
| @@ -285,6 +287,13 @@ return: | |||||||
| } | } | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | - POST `/hassos/update/cli` | ||||||
|  | ```json | ||||||
|  | { | ||||||
|  |     "version": "optional" | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
| - POST `/hassos/config/sync` | - POST `/hassos/config/sync` | ||||||
|  |  | ||||||
| Load host configs from a USB stick. | Load host configs from a USB stick. | ||||||
| @@ -372,6 +381,7 @@ Output is the raw Docker log. | |||||||
|     "port": "port for access hass", |     "port": "port for access hass", | ||||||
|     "ssl": "bool", |     "ssl": "bool", | ||||||
|     "password": "", |     "password": "", | ||||||
|  |     "refresh_token": "", | ||||||
|     "watchdog": "bool", |     "watchdog": "bool", | ||||||
|     "startup_time": 600 |     "startup_time": 600 | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,7 +6,6 @@ ENV LANG C.UTF-8 | |||||||
|  |  | ||||||
| # Setup base | # Setup base | ||||||
| RUN apk add --no-cache \ | RUN apk add --no-cache \ | ||||||
|         python3 \ |  | ||||||
|         git \ |         git \ | ||||||
|         socat \ |         socat \ | ||||||
|         glib \ |         glib \ | ||||||
| @@ -14,12 +13,11 @@ RUN apk add --no-cache \ | |||||||
|         eudev-libs \ |         eudev-libs \ | ||||||
|     && apk add --no-cache --virtual .build-dependencies \ |     && apk add --no-cache --virtual .build-dependencies \ | ||||||
|         make \ |         make \ | ||||||
|         python3-dev \ |  | ||||||
|         g++ \ |         g++ \ | ||||||
|     && pip3 install --no-cache-dir \ |     && pip3 install --no-cache-dir \ | ||||||
|         uvloop==0.10.2 \ |         uvloop==0.11.0 \ | ||||||
|         cchardet==2.1.1 \ |         cchardet==2.1.1 \ | ||||||
|         pycryptodome==3.4.11 \ |         pycryptodome==3.6.4 \ | ||||||
|     && apk del .build-dependencies |     && apk del .build-dependencies | ||||||
|  |  | ||||||
| # Install HassIO | # Install HassIO | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ from concurrent.futures import ThreadPoolExecutor | |||||||
| import logging | import logging | ||||||
| import sys | import sys | ||||||
|  |  | ||||||
| import hassio.bootstrap as bootstrap | from hassio import bootstrap | ||||||
|  |  | ||||||
| _LOGGER = logging.getLogger(__name__) | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ from voluptuous.humanize import humanize_error | |||||||
|  |  | ||||||
| from .validate import ( | from .validate import ( | ||||||
|     validate_options, SCHEMA_ADDON_SNAPSHOT, RE_VOLUME, RE_SERVICE) |     validate_options, SCHEMA_ADDON_SNAPSHOT, RE_VOLUME, RE_SERVICE) | ||||||
| from .utils import check_installed | from .utils import check_installed, remove_data | ||||||
| from ..const import ( | from ..const import ( | ||||||
|     ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_BOOT, ATTR_MAP, |     ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_BOOT, ATTR_MAP, | ||||||
|     ATTR_OPTIONS, ATTR_PORTS, ATTR_SCHEMA, ATTR_IMAGE, ATTR_REPOSITORY, |     ATTR_OPTIONS, ATTR_PORTS, ATTR_SCHEMA, ATTR_IMAGE, ATTR_REPOSITORY, | ||||||
| @@ -636,7 +636,7 @@ class Addon(CoreSysAttributes): | |||||||
|         if self.path_data.is_dir(): |         if self.path_data.is_dir(): | ||||||
|             _LOGGER.info( |             _LOGGER.info( | ||||||
|                 "Remove Home-Assistant addon data folder %s", self.path_data) |                 "Remove Home-Assistant addon data folder %s", self.path_data) | ||||||
|             shutil.rmtree(str(self.path_data)) |             await remove_data(self.path_data) | ||||||
|  |  | ||||||
|         # Cleanup audio settings |         # Cleanup audio settings | ||||||
|         if self.path_asound.exists(): |         if self.path_asound.exists(): | ||||||
| @@ -856,12 +856,12 @@ class Addon(CoreSysAttributes): | |||||||
|             # restore data |             # restore data | ||||||
|             def _restore_data(): |             def _restore_data(): | ||||||
|                 """Restore data.""" |                 """Restore data.""" | ||||||
|                 if self.path_data.is_dir(): |  | ||||||
|                     shutil.rmtree(str(self.path_data), ignore_errors=True) |  | ||||||
|                 shutil.copytree(str(Path(temp, "data")), str(self.path_data)) |                 shutil.copytree(str(Path(temp, "data")), str(self.path_data)) | ||||||
|  |  | ||||||
|  |             _LOGGER.info("Restore data for addon %s", self._id) | ||||||
|  |             if self.path_data.is_dir(): | ||||||
|  |                 await remove_data(self.path_data) | ||||||
|             try: |             try: | ||||||
|                 _LOGGER.info("Restore data for addon %s", self._id) |  | ||||||
|                 await self.sys_run_in_executor(_restore_data) |                 await self.sys_run_in_executor(_restore_data) | ||||||
|             except shutil.Error as err: |             except shutil.Error as err: | ||||||
|                 _LOGGER.error("Can't restore origin data: %s", err) |                 _LOGGER.error("Can't restore origin data: %s", err) | ||||||
|   | |||||||
| @@ -45,12 +45,13 @@ class GitRepo(CoreSysAttributes): | |||||||
|         async with self.lock: |         async with self.lock: | ||||||
|             try: |             try: | ||||||
|                 _LOGGER.info("Load addon %s repository", self.path) |                 _LOGGER.info("Load addon %s repository", self.path) | ||||||
|                 self.repo = await self.sys_loop.run_in_executor( |                 self.repo = await self.sys_run_in_executor( | ||||||
|                     None, git.Repo, str(self.path)) |                     git.Repo, str(self.path)) | ||||||
|  |  | ||||||
|             except (git.InvalidGitRepositoryError, git.NoSuchPathError, |             except (git.InvalidGitRepositoryError, git.NoSuchPathError, | ||||||
|                     git.GitCommandError) as err: |                     git.GitCommandError) as err: | ||||||
|                 _LOGGER.error("Can't load %s repo: %s.", self.path, err) |                 _LOGGER.error("Can't load %s repo: %s.", self.path, err) | ||||||
|  |                 self._remove() | ||||||
|                 return False |                 return False | ||||||
|  |  | ||||||
|             return True |             return True | ||||||
| @@ -62,7 +63,9 @@ class GitRepo(CoreSysAttributes): | |||||||
|                 attribute: value |                 attribute: value | ||||||
|                 for attribute, value in ( |                 for attribute, value in ( | ||||||
|                     ('recursive', True), |                     ('recursive', True), | ||||||
|                     ('branch', self.branch) |                     ('branch', self.branch), | ||||||
|  |                     ('depth', 1), | ||||||
|  |                     ('shallow-submodules', True) | ||||||
|                 ) if value is not None |                 ) if value is not None | ||||||
|             } |             } | ||||||
|  |  | ||||||
| @@ -76,6 +79,7 @@ class GitRepo(CoreSysAttributes): | |||||||
|             except (git.InvalidGitRepositoryError, git.NoSuchPathError, |             except (git.InvalidGitRepositoryError, git.NoSuchPathError, | ||||||
|                     git.GitCommandError) as err: |                     git.GitCommandError) as err: | ||||||
|                 _LOGGER.error("Can't clone %s repo: %s.", self.url, err) |                 _LOGGER.error("Can't clone %s repo: %s.", self.url, err) | ||||||
|  |                 self._remove() | ||||||
|                 return False |                 return False | ||||||
|  |  | ||||||
|             return True |             return True | ||||||
| @@ -87,18 +91,43 @@ class GitRepo(CoreSysAttributes): | |||||||
|             return False |             return False | ||||||
|  |  | ||||||
|         async with self.lock: |         async with self.lock: | ||||||
|  |             _LOGGER.info("Update addon %s repository", self.url) | ||||||
|  |             branch = self.repo.active_branch.name | ||||||
|  |  | ||||||
|             try: |             try: | ||||||
|                 _LOGGER.info("Pull addon %s repository", self.url) |                 # Download data | ||||||
|                 await self.sys_loop.run_in_executor( |                 await self.sys_run_in_executor(ft.partial( | ||||||
|                     None, self.repo.remotes.origin.pull) |                     self.repo.remotes.origin.fetch, **{ | ||||||
|  |                         'update-shallow': True, | ||||||
|  |                         'depth': 1, | ||||||
|  |                     })) | ||||||
|  |  | ||||||
|  |                 # Jump on top of that | ||||||
|  |                 await self.sys_run_in_executor(ft.partial( | ||||||
|  |                     self.repo.git.reset, f"origin/{branch}", hard=True)) | ||||||
|  |  | ||||||
|  |                 # Cleanup old data | ||||||
|  |                 await self.sys_run_in_executor(ft.partial( | ||||||
|  |                     self.repo.git.clean, "-xdf")) | ||||||
|  |  | ||||||
|             except (git.InvalidGitRepositoryError, git.NoSuchPathError, |             except (git.InvalidGitRepositoryError, git.NoSuchPathError, | ||||||
|                     git.GitCommandError) as err: |                     git.GitCommandError) as err: | ||||||
|                 _LOGGER.error("Can't pull %s repo: %s.", self.url, err) |                 _LOGGER.error("Can't update %s repo: %s.", self.url, err) | ||||||
|                 return False |                 return False | ||||||
|  |  | ||||||
|             return True |             return True | ||||||
|  |  | ||||||
|  |     def _remove(self): | ||||||
|  |         """Remove a repository.""" | ||||||
|  |         if not self.path.is_dir(): | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         def log_err(funct, path, _): | ||||||
|  |             """Log error.""" | ||||||
|  |             _LOGGER.warning("Can't remove %s", path) | ||||||
|  |  | ||||||
|  |         shutil.rmtree(str(self.path), onerror=log_err) | ||||||
|  |  | ||||||
|  |  | ||||||
| class GitRepoHassIO(GitRepo): | class GitRepoHassIO(GitRepo): | ||||||
|     """HassIO addons repository.""" |     """HassIO addons repository.""" | ||||||
| @@ -121,12 +150,6 @@ class GitRepoCustom(GitRepo): | |||||||
|         super().__init__(coresys, path, url) |         super().__init__(coresys, path, url) | ||||||
|  |  | ||||||
|     def remove(self): |     def remove(self): | ||||||
|         """Remove a custom addon.""" |         """Remove a custom repository.""" | ||||||
|         if self.path.is_dir(): |         _LOGGER.info("Remove custom addon repository %s", self.url) | ||||||
|             _LOGGER.info("Remove custom addon repository %s", self.url) |         self._remove() | ||||||
|  |  | ||||||
|             def log_err(funct, path, _): |  | ||||||
|                 """Log error.""" |  | ||||||
|                 _LOGGER.warning("Can't remove %s", path) |  | ||||||
|  |  | ||||||
|             shutil.rmtree(str(self.path), onerror=log_err) |  | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| """Util addons functions.""" | """Util addons functions.""" | ||||||
|  | import asyncio | ||||||
| import hashlib | import hashlib | ||||||
| import logging | import logging | ||||||
| import re | import re | ||||||
| @@ -33,3 +34,20 @@ def check_installed(method): | |||||||
|         return await method(addon, *args, **kwargs) |         return await method(addon, *args, **kwargs) | ||||||
|  |  | ||||||
|     return wrap_check |     return wrap_check | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def remove_data(folder): | ||||||
|  |     """Remove folder and reset privileged.""" | ||||||
|  |     try: | ||||||
|  |         proc = await asyncio.create_subprocess_exec( | ||||||
|  |             "rm", "-rf", str(folder), | ||||||
|  |             stdout=asyncio.subprocess.DEVNULL | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         _, error_msg = await proc.communicate() | ||||||
|  |     except OSError as err: | ||||||
|  |         error_msg = str(err) | ||||||
|  |  | ||||||
|  |     if proc.returncode == 0: | ||||||
|  |         return | ||||||
|  |     _LOGGER.error("Can't remove Add-on Data: %s", error_msg) | ||||||
|   | |||||||
| @@ -58,6 +58,7 @@ class RestAPI(CoreSysAttributes): | |||||||
|             web.post('/host/reboot', api_host.reboot), |             web.post('/host/reboot', api_host.reboot), | ||||||
|             web.post('/host/shutdown', api_host.shutdown), |             web.post('/host/shutdown', api_host.shutdown), | ||||||
|             web.post('/host/reload', api_host.reload), |             web.post('/host/reload', api_host.reload), | ||||||
|  |             web.post('/host/options', api_host.options), | ||||||
|             web.get('/host/services', api_host.services), |             web.get('/host/services', api_host.services), | ||||||
|             web.post('/host/services/{service}/stop', api_host.service_stop), |             web.post('/host/services/{service}/stop', api_host.service_stop), | ||||||
|             web.post('/host/services/{service}/start', api_host.service_start), |             web.post('/host/services/{service}/start', api_host.service_start), | ||||||
| @@ -75,6 +76,7 @@ class RestAPI(CoreSysAttributes): | |||||||
|         self.webapp.add_routes([ |         self.webapp.add_routes([ | ||||||
|             web.get('/hassos/info', api_hassos.info), |             web.get('/hassos/info', api_hassos.info), | ||||||
|             web.post('/hassos/update', api_hassos.update), |             web.post('/hassos/update', api_hassos.update), | ||||||
|  |             web.post('/hassos/update/cli', api_hassos.update_cli), | ||||||
|             web.post('/hassos/config/sync', api_hassos.config_sync), |             web.post('/hassos/config/sync', api_hassos.config_sync), | ||||||
|         ]) |         ]) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,7 +5,9 @@ import logging | |||||||
| import voluptuous as vol | import voluptuous as vol | ||||||
|  |  | ||||||
| from .utils import api_process, api_validate | from .utils import api_process, api_validate | ||||||
| from ..const import ATTR_VERSION, ATTR_BOARD, ATTR_VERSION_LATEST | from ..const import ( | ||||||
|  |     ATTR_VERSION, ATTR_BOARD, ATTR_VERSION_LATEST, ATTR_VERSION_CLI, | ||||||
|  |     ATTR_VERSION_CLI_LATEST) | ||||||
| from ..coresys import CoreSysAttributes | from ..coresys import CoreSysAttributes | ||||||
|  |  | ||||||
| _LOGGER = logging.getLogger(__name__) | _LOGGER = logging.getLogger(__name__) | ||||||
| @@ -23,7 +25,9 @@ class APIHassOS(CoreSysAttributes): | |||||||
|         """Return hassos information.""" |         """Return hassos information.""" | ||||||
|         return { |         return { | ||||||
|             ATTR_VERSION: self.sys_hassos.version, |             ATTR_VERSION: self.sys_hassos.version, | ||||||
|  |             ATTR_VERSION_CLI: self.sys_hassos.version_cli, | ||||||
|             ATTR_VERSION_LATEST: self.sys_hassos.version_latest, |             ATTR_VERSION_LATEST: self.sys_hassos.version_latest, | ||||||
|  |             ATTR_VERSION_CLI_LATEST: self.sys_hassos.version_cli_latest, | ||||||
|             ATTR_BOARD: self.sys_hassos.board, |             ATTR_BOARD: self.sys_hassos.board, | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -35,6 +39,14 @@ class APIHassOS(CoreSysAttributes): | |||||||
|  |  | ||||||
|         await asyncio.shield(self.sys_hassos.update(version)) |         await asyncio.shield(self.sys_hassos.update(version)) | ||||||
|  |  | ||||||
|  |     @api_process | ||||||
|  |     async def update_cli(self, request): | ||||||
|  |         """Update HassOS CLI.""" | ||||||
|  |         body = await api_validate(SCHEMA_VERSION, request) | ||||||
|  |         version = body.get(ATTR_VERSION, self.sys_hassos.version_cli_latest) | ||||||
|  |  | ||||||
|  |         await asyncio.shield(self.sys_hassos.update_cli(version)) | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     def config_sync(self, request): |     def config_sync(self, request): | ||||||
|         """Trigger config reload on HassOS.""" |         """Trigger config reload on HassOS.""" | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ from ..const import ( | |||||||
|     ATTR_PORT, ATTR_PASSWORD, ATTR_SSL, ATTR_WATCHDOG, ATTR_CPU_PERCENT, |     ATTR_PORT, ATTR_PASSWORD, ATTR_SSL, ATTR_WATCHDOG, ATTR_CPU_PERCENT, | ||||||
|     ATTR_MEMORY_USAGE, ATTR_MEMORY_LIMIT, ATTR_NETWORK_RX, ATTR_NETWORK_TX, |     ATTR_MEMORY_USAGE, ATTR_MEMORY_LIMIT, ATTR_NETWORK_RX, ATTR_NETWORK_TX, | ||||||
|     ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_WAIT_BOOT, ATTR_MACHINE, |     ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_WAIT_BOOT, ATTR_MACHINE, | ||||||
|     CONTENT_TYPE_BINARY) |     ATTR_REFRESH_TOKEN, CONTENT_TYPE_BINARY) | ||||||
| from ..coresys import CoreSysAttributes | from ..coresys import CoreSysAttributes | ||||||
| from ..validate import NETWORK_PORT, DOCKER_IMAGE | from ..validate import NETWORK_PORT, DOCKER_IMAGE | ||||||
|  |  | ||||||
| @@ -30,6 +30,8 @@ SCHEMA_OPTIONS = vol.Schema({ | |||||||
|     vol.Optional(ATTR_WATCHDOG): vol.Boolean(), |     vol.Optional(ATTR_WATCHDOG): vol.Boolean(), | ||||||
|     vol.Optional(ATTR_WAIT_BOOT): |     vol.Optional(ATTR_WAIT_BOOT): | ||||||
|         vol.All(vol.Coerce(int), vol.Range(min=60)), |         vol.All(vol.Coerce(int), vol.Range(min=60)), | ||||||
|  |     # Required once we enforce user system | ||||||
|  |     vol.Optional(ATTR_REFRESH_TOKEN): str, | ||||||
| }) | }) | ||||||
|  |  | ||||||
| SCHEMA_VERSION = vol.Schema({ | SCHEMA_VERSION = vol.Schema({ | ||||||
| @@ -83,8 +85,10 @@ class APIHomeAssistant(CoreSysAttributes): | |||||||
|         if ATTR_WAIT_BOOT in body: |         if ATTR_WAIT_BOOT in body: | ||||||
|             self.sys_homeassistant.wait_boot = body[ATTR_WAIT_BOOT] |             self.sys_homeassistant.wait_boot = body[ATTR_WAIT_BOOT] | ||||||
|  |  | ||||||
|  |         if ATTR_REFRESH_TOKEN in body: | ||||||
|  |             self.sys_homeassistant.refresh_token = body[ATTR_REFRESH_TOKEN] | ||||||
|  |  | ||||||
|         self.sys_homeassistant.save_data() |         self.sys_homeassistant.save_data() | ||||||
|         return True |  | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def stats(self, request): |     async def stats(self, request): | ||||||
| @@ -109,11 +113,7 @@ class APIHomeAssistant(CoreSysAttributes): | |||||||
|         body = await api_validate(SCHEMA_VERSION, request) |         body = await api_validate(SCHEMA_VERSION, request) | ||||||
|         version = body.get(ATTR_VERSION, self.sys_homeassistant.last_version) |         version = body.get(ATTR_VERSION, self.sys_homeassistant.last_version) | ||||||
|  |  | ||||||
|         if version == self.sys_homeassistant.version: |         await asyncio.shield(self.sys_homeassistant.update(version)) | ||||||
|             raise RuntimeError("Version {} is already in use".format(version)) |  | ||||||
|  |  | ||||||
|         return await asyncio.shield( |  | ||||||
|             self.sys_homeassistant.update(version)) |  | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     def stop(self, request): |     def stop(self, request): | ||||||
|   | |||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								hassio/api/panel/chunk.0ef4ef1053fe3d5107b5.js.gz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								hassio/api/panel/chunk.0ef4ef1053fe3d5107b5.js.gz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -1 +1 @@ | |||||||
| !function(e){function n(n){for(var t,o,i=n[0],u=n[1],a=0,c=[];a<i.length;a++)o=i[a],r[o]&&c.push(r[o][0]),r[o]=0;for(t in u)Object.prototype.hasOwnProperty.call(u,t)&&(e[t]=u[t]);for(f&&f(n);c.length;)c.shift()()}var t={},r={6:0};function o(n){if(t[n])return t[n].exports;var r=t[n]={i:n,l:!1,exports:{}};return e[n].call(r.exports,r,r.exports,o),r.l=!0,r.exports}o.e=function(e){var n=[],t=r[e];if(0!==t)if(t)n.push(t[2]);else{var i=new Promise(function(n,o){t=r[e]=[n,o]});n.push(t[2]=i);var u,a=document.getElementsByTagName("head")[0],f=document.createElement("script");f.charset="utf-8",f.timeout=120,o.nc&&f.setAttribute("nonce",o.nc),f.src=function(e){return o.p+"chunk."+{0:"f3880aa331d3ef2ddf32",1:"a8e86d80be46b3b6e16d",2:"fdf0834c750e40935b6f",3:"ff92199b0d422767d108",4:"c77b56beea1d4547ff5f",5:"c93f37c558ff32991708"}[e]+".js"}(e),u=function(n){f.onerror=f.onload=null,clearTimeout(c);var t=r[e];if(0!==t){if(t){var o=n&&("load"===n.type?"missing":n.type),i=n&&n.target&&n.target.src,u=new Error("Loading chunk "+e+" failed.\n("+o+": "+i+")");u.type=o,u.request=i,t[1](u)}r[e]=void 0}};var c=setTimeout(function(){u({type:"timeout",target:f})},12e4);f.onerror=f.onload=u,a.appendChild(f)}return Promise.all(n)},o.m=e,o.c=t,o.d=function(e,n,t){o.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:t})},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(e,n){if(1&n&&(e=o(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(o.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var r in e)o.d(t,r,function(n){return e[n]}.bind(null,r));return t},o.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(n,"a",n),n},o.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},o.p="/api/hassio/app/",o.oe=function(e){throw console.error(e),e};var i=window.webpackJsonp=window.webpackJsonp||[],u=i.push.bind(i);i.push=n,i=i.slice();for(var a=0;a<i.length;a++)n(i[a]);var f=u;o(o.s=0)}([function(e,n,t){window.loadES5Adapter().then(function(){Promise.all([t.e(0),t.e(3)]).then(t.bind(null,1)),Promise.all([t.e(0),t.e(1),t.e(2)]).then(t.bind(null,2))})}]); | !function(e){function n(n){for(var t,o,i=n[0],u=n[1],a=0,l=[];a<i.length;a++)o=i[a],r[o]&&l.push(r[o][0]),r[o]=0;for(t in u)Object.prototype.hasOwnProperty.call(u,t)&&(e[t]=u[t]);for(f&&f(n);l.length;)l.shift()()}var t={},r={6:0};function o(n){if(t[n])return t[n].exports;var r=t[n]={i:n,l:!1,exports:{}};return e[n].call(r.exports,r,r.exports,o),r.l=!0,r.exports}o.e=function(e){var n=[],t=r[e];if(0!==t)if(t)n.push(t[2]);else{var i=new Promise(function(n,o){t=r[e]=[n,o]});n.push(t[2]=i);var u,a=document.getElementsByTagName("head")[0],f=document.createElement("script");f.charset="utf-8",f.timeout=120,o.nc&&f.setAttribute("nonce",o.nc),f.src=function(e){return o.p+"chunk."+{0:"f3880aa331d3ef2ddf32",1:"a8e86d80be46b3b6e16d",2:"0ef4ef1053fe3d5107b5",3:"ff92199b0d422767d108",4:"c77b56beea1d4547ff5f",5:"c93f37c558ff32991708"}[e]+".js"}(e),u=function(n){f.onerror=f.onload=null,clearTimeout(l);var t=r[e];if(0!==t){if(t){var o=n&&("load"===n.type?"missing":n.type),i=n&&n.target&&n.target.src,u=new Error("Loading chunk "+e+" failed.\n("+o+": "+i+")");u.type=o,u.request=i,t[1](u)}r[e]=void 0}};var l=setTimeout(function(){u({type:"timeout",target:f})},12e4);f.onerror=f.onload=u,a.appendChild(f)}return Promise.all(n)},o.m=e,o.c=t,o.d=function(e,n,t){o.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:t})},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(e,n){if(1&n&&(e=o(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(o.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var r in e)o.d(t,r,function(n){return e[n]}.bind(null,r));return t},o.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(n,"a",n),n},o.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},o.p="/api/hassio/app/",o.oe=function(e){throw console.error(e),e};var i=window.webpackJsonp=window.webpackJsonp||[],u=i.push.bind(i);i.push=n,i=i.slice();for(var a=0;a<i.length;a++)n(i[a]);var f=u;o(o.s=0)}([function(e,n,t){window.loadES5Adapter().then(function(){Promise.all([t.e(0),t.e(3)]).then(t.bind(null,1)),Promise.all([t.e(0),t.e(1),t.e(2)]).then(t.bind(null,2))})}]); | ||||||
										
											Binary file not shown.
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -1,15 +1,18 @@ | |||||||
| """Utils for HomeAssistant Proxy.""" | """Utils for HomeAssistant Proxy.""" | ||||||
| import asyncio | import asyncio | ||||||
|  | from contextlib import asynccontextmanager | ||||||
| import logging | import logging | ||||||
|  |  | ||||||
| import aiohttp | import aiohttp | ||||||
| from aiohttp import web | from aiohttp import web | ||||||
| from aiohttp.web_exceptions import HTTPBadGateway, HTTPInternalServerError | from aiohttp.web_exceptions import ( | ||||||
|  |     HTTPBadGateway, HTTPInternalServerError, HTTPUnauthorized) | ||||||
| from aiohttp.hdrs import CONTENT_TYPE | from aiohttp.hdrs import CONTENT_TYPE | ||||||
| import async_timeout | import async_timeout | ||||||
|  |  | ||||||
| from ..const import HEADER_HA_ACCESS | from ..const import HEADER_HA_ACCESS | ||||||
| from ..coresys import CoreSysAttributes | from ..coresys import CoreSysAttributes | ||||||
|  | from ..exceptions import HomeAssistantAuthError, HomeAssistantAPIError | ||||||
|  |  | ||||||
| _LOGGER = logging.getLogger(__name__) | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| @@ -23,49 +26,43 @@ class APIProxy(CoreSysAttributes): | |||||||
|         addon = self.sys_addons.from_uuid(hassio_token) |         addon = self.sys_addons.from_uuid(hassio_token) | ||||||
|  |  | ||||||
|         if not addon: |         if not addon: | ||||||
|             _LOGGER.warning("Unknown Home-Assistant API access!") |             _LOGGER.warning("Unknown HomeAssistant API access!") | ||||||
|  |         elif not addon.access_homeassistant_api: | ||||||
|  |             _LOGGER.warning("Not permitted API access: %s", addon.slug) | ||||||
|         else: |         else: | ||||||
|             _LOGGER.info("%s access from %s", request.path, addon.slug) |             _LOGGER.info("%s access from %s", request.path, addon.slug) | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         raise HTTPUnauthorized() | ||||||
|  |  | ||||||
|  |     @asynccontextmanager | ||||||
|     async def _api_client(self, request, path, timeout=300): |     async def _api_client(self, request, path, timeout=300): | ||||||
|         """Return a client request with proxy origin for Home-Assistant.""" |         """Return a client request with proxy origin for Home-Assistant.""" | ||||||
|         url = f"{self.sys_homeassistant.api_url}/api/{path}" |  | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
|             data = None |  | ||||||
|             headers = {} |  | ||||||
|             method = getattr(self.sys_websession_ssl, request.method.lower()) |  | ||||||
|             params = request.query or None |  | ||||||
|  |  | ||||||
|             # read data |             # read data | ||||||
|             with async_timeout.timeout(30): |             with async_timeout.timeout(30): | ||||||
|                 data = await request.read() |                 data = await request.read() | ||||||
|  |  | ||||||
|             if data: |             if data: | ||||||
|                 headers.update({CONTENT_TYPE: request.content_type}) |                 content_type = request.content_type | ||||||
|  |             else: | ||||||
|  |                 content_type = None | ||||||
|  |  | ||||||
|             # need api password? |             async with self.sys_homeassistant.make_request( | ||||||
|             if self.sys_homeassistant.api_password: |                     request.method.lower(), f'api/{path}', | ||||||
|                 headers = { |                     content_type=content_type, | ||||||
|                     HEADER_HA_ACCESS: self.sys_homeassistant.api_password, |                     data=data, | ||||||
|                 } |                     timeout=timeout, | ||||||
|  |             ) as resp: | ||||||
|             # reset headers |                 yield resp | ||||||
|             if not headers: |                 return | ||||||
|                 headers = None |  | ||||||
|  |  | ||||||
|             client = await method( |  | ||||||
|                 url, data=data, headers=headers, timeout=timeout, |  | ||||||
|                 params=params |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|             return client |  | ||||||
|  |  | ||||||
|  |         except HomeAssistantAuthError: | ||||||
|  |             _LOGGER.error("Authenticate error on API for request %s", path) | ||||||
|         except aiohttp.ClientError as err: |         except aiohttp.ClientError as err: | ||||||
|             _LOGGER.error("Client error on API %s request %s.", path, err) |             _LOGGER.error("Client error on API %s request %s", path, err) | ||||||
|  |  | ||||||
|         except asyncio.TimeoutError: |         except asyncio.TimeoutError: | ||||||
|             _LOGGER.error("Client timeout error on API request %s.", path) |             _LOGGER.error("Client timeout error on API request %s", path) | ||||||
|  |  | ||||||
|         raise HTTPBadGateway() |         raise HTTPBadGateway() | ||||||
|  |  | ||||||
| @@ -74,30 +71,25 @@ class APIProxy(CoreSysAttributes): | |||||||
|         self._check_access(request) |         self._check_access(request) | ||||||
|  |  | ||||||
|         _LOGGER.info("Home-Assistant EventStream start") |         _LOGGER.info("Home-Assistant EventStream start") | ||||||
|         client = await self._api_client(request, 'stream', timeout=None) |         async with self._api_client(request, 'stream', timeout=None) as client: | ||||||
|  |             response = web.StreamResponse() | ||||||
|  |             response.content_type = request.headers.get(CONTENT_TYPE) | ||||||
|  |             try: | ||||||
|  |                 await response.prepare(request) | ||||||
|  |                 while True: | ||||||
|  |                     data = await client.content.read(10) | ||||||
|  |                     if not data: | ||||||
|  |                         break | ||||||
|  |                     await response.write(data) | ||||||
|  |  | ||||||
|         response = web.StreamResponse() |             except aiohttp.ClientError: | ||||||
|         response.content_type = request.headers.get(CONTENT_TYPE) |                 pass | ||||||
|         try: |  | ||||||
|             await response.prepare(request) |  | ||||||
|             while True: |  | ||||||
|                 data = await client.content.read(10) |  | ||||||
|                 if not data: |  | ||||||
|                     await response.write_eof() |  | ||||||
|                     break |  | ||||||
|                 await response.write(data) |  | ||||||
|  |  | ||||||
|         except aiohttp.ClientError: |             finally: | ||||||
|             await response.write_eof() |                 client.close() | ||||||
|  |                 _LOGGER.info("Home-Assistant EventStream close") | ||||||
|  |  | ||||||
|         except asyncio.CancelledError: |             return response | ||||||
|             pass |  | ||||||
|  |  | ||||||
|         finally: |  | ||||||
|             client.close() |  | ||||||
|             _LOGGER.info("Home-Assistant EventStream close") |  | ||||||
|  |  | ||||||
|         return response |  | ||||||
|  |  | ||||||
|     async def api(self, request): |     async def api(self, request): | ||||||
|         """Proxy HomeAssistant API Requests.""" |         """Proxy HomeAssistant API Requests.""" | ||||||
| @@ -105,14 +97,13 @@ class APIProxy(CoreSysAttributes): | |||||||
|  |  | ||||||
|         # Normal request |         # Normal request | ||||||
|         path = request.match_info.get('path', '') |         path = request.match_info.get('path', '') | ||||||
|         client = await self._api_client(request, path) |         async with self._api_client(request, path) as client: | ||||||
|  |             data = await client.read() | ||||||
|         data = await client.read() |             return web.Response( | ||||||
|         return web.Response( |                 body=data, | ||||||
|             body=data, |                 status=client.status, | ||||||
|             status=client.status, |                 content_type=client.content_type | ||||||
|             content_type=client.content_type |             ) | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     async def _websocket_client(self): |     async def _websocket_client(self): | ||||||
|         """Initialize a websocket api connection.""" |         """Initialize a websocket api connection.""" | ||||||
| @@ -123,19 +114,44 @@ class APIProxy(CoreSysAttributes): | |||||||
|                 url, heartbeat=60, verify_ssl=False) |                 url, heartbeat=60, verify_ssl=False) | ||||||
|  |  | ||||||
|             # handle authentication |             # handle authentication | ||||||
|             for _ in range(2): |             data = await client.receive_json() | ||||||
|                 data = await client.receive_json() |  | ||||||
|                 if data.get('type') == 'auth_ok': |  | ||||||
|                     return client |  | ||||||
|                 elif data.get('type') == 'auth_required': |  | ||||||
|                     await client.send_json({ |  | ||||||
|                         'type': 'auth', |  | ||||||
|                         'api_password': self.sys_homeassistant.api_password, |  | ||||||
|                     }) |  | ||||||
|  |  | ||||||
|             _LOGGER.error("Authentication to Home-Assistant websocket") |             if data.get('type') == 'auth_ok': | ||||||
|  |                 return client | ||||||
|  |  | ||||||
|         except (aiohttp.ClientError, RuntimeError) as err: |             if data.get('type') != 'auth_required': | ||||||
|  |                 # Invalid protocol | ||||||
|  |                 _LOGGER.error( | ||||||
|  |                     'Got unexpected response from HA websocket: %s', data) | ||||||
|  |                 raise HTTPBadGateway() | ||||||
|  |  | ||||||
|  |             if self.sys_homeassistant.refresh_token: | ||||||
|  |                 await self.sys_homeassistant.ensure_access_token() | ||||||
|  |                 await client.send_json({ | ||||||
|  |                     'type': 'auth', | ||||||
|  |                     'access_token': self.sys_homeassistant.access_token, | ||||||
|  |                 }) | ||||||
|  |             else: | ||||||
|  |                 await client.send_json({ | ||||||
|  |                     'type': 'auth', | ||||||
|  |                     'api_password': self.sys_homeassistant.api_password, | ||||||
|  |                 }) | ||||||
|  |  | ||||||
|  |             data = await client.receive_json() | ||||||
|  |  | ||||||
|  |             if data.get('type') == 'auth_ok': | ||||||
|  |                 return client | ||||||
|  |  | ||||||
|  |             # Renew the Token is invalid | ||||||
|  |             if (data.get('type') == 'invalid_auth' and | ||||||
|  |                     self.sys_homeassistant.refresh_token): | ||||||
|  |                 self.sys_homeassistant.access_token = None | ||||||
|  |                 return await self._websocket_client() | ||||||
|  |  | ||||||
|  |             _LOGGER.error( | ||||||
|  |                 "Failed authentication to Home-Assistant websocket: %s", data) | ||||||
|  |  | ||||||
|  |         except (RuntimeError, HomeAssistantAPIError) as err: | ||||||
|             _LOGGER.error("Client error on websocket API %s.", err) |             _LOGGER.error("Client error on websocket API %s.", err) | ||||||
|  |  | ||||||
|         raise HTTPBadGateway() |         raise HTTPBadGateway() | ||||||
| @@ -157,13 +173,19 @@ class APIProxy(CoreSysAttributes): | |||||||
|  |  | ||||||
|             # Check API access |             # Check API access | ||||||
|             response = await server.receive_json() |             response = await server.receive_json() | ||||||
|             hassio_token = response.get('api_password') |             hassio_token = (response.get('api_password') or | ||||||
|  |                             response.get('access_token')) | ||||||
|             addon = self.sys_addons.from_uuid(hassio_token) |             addon = self.sys_addons.from_uuid(hassio_token) | ||||||
|  |  | ||||||
|             if not addon: |             if not addon or not addon.access_homeassistant_api: | ||||||
|                 _LOGGER.warning("Unauthorized websocket access!") |                 _LOGGER.warning("Unauthorized websocket access!") | ||||||
|             else: |                 await server.send_json({ | ||||||
|                 _LOGGER.info("Websocket access from %s", addon.slug) |                     'type': 'auth_invalid', | ||||||
|  |                     'message': 'Invalid access', | ||||||
|  |                 }) | ||||||
|  |                 return server | ||||||
|  |  | ||||||
|  |             _LOGGER.info("Websocket access from %s", addon.slug) | ||||||
|  |  | ||||||
|             await server.send_json({ |             await server.send_json({ | ||||||
|                 'type': 'auth_ok', |                 'type': 'auth_ok', | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| """Init file for HassIO util for rest api.""" | """Init file for HassIO util for rest api.""" | ||||||
| import json | import json | ||||||
| import hashlib |  | ||||||
| import logging | import logging | ||||||
|  |  | ||||||
| from aiohttp import web | from aiohttp import web | ||||||
| @@ -94,9 +93,3 @@ async def api_validate(schema, request): | |||||||
|         raise RuntimeError(humanize_error(data, ex)) from None |         raise RuntimeError(humanize_error(data, ex)) from None | ||||||
|  |  | ||||||
|     return data |     return data | ||||||
|  |  | ||||||
|  |  | ||||||
| def hash_password(password): |  | ||||||
|     """Hash and salt our passwords.""" |  | ||||||
|     key = ")*()*SALT_HASSIO2123{}6554547485HSKA!!*JSLAfdasda$".format(password) |  | ||||||
|     return hashlib.sha256(key.encode()).hexdigest() |  | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from ipaddress import ip_network | from ipaddress import ip_network | ||||||
|  |  | ||||||
| HASSIO_VERSION = '112' | HASSIO_VERSION = '120' | ||||||
|  |  | ||||||
| URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons" | URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons" | ||||||
| URL_HASSIO_VERSION = \ | URL_HASSIO_VERSION = \ | ||||||
| @@ -50,7 +50,7 @@ CONTENT_TYPE_JSON = 'application/json' | |||||||
| CONTENT_TYPE_TEXT = 'text/plain' | CONTENT_TYPE_TEXT = 'text/plain' | ||||||
| CONTENT_TYPE_TAR = 'application/tar' | CONTENT_TYPE_TAR = 'application/tar' | ||||||
| HEADER_HA_ACCESS = 'x-ha-access' | HEADER_HA_ACCESS = 'x-ha-access' | ||||||
| HEADER_TOKEN = 'X-HASSIO-KEY' | HEADER_TOKEN = 'x-hassio-key' | ||||||
|  |  | ||||||
| ENV_TOKEN = 'HASSIO_TOKEN' | ENV_TOKEN = 'HASSIO_TOKEN' | ||||||
| ENV_TIME = 'TZ' | ENV_TIME = 'TZ' | ||||||
| @@ -174,6 +174,10 @@ ATTR_DEVICETREE = 'devicetree' | |||||||
| ATTR_CPE = 'cpe' | ATTR_CPE = 'cpe' | ||||||
| ATTR_BOARD = 'board' | ATTR_BOARD = 'board' | ||||||
| ATTR_HASSOS = 'hassos' | ATTR_HASSOS = 'hassos' | ||||||
|  | ATTR_HASSOS_CLI = 'hassos_cli' | ||||||
|  | ATTR_VERSION_CLI = 'version_cli' | ||||||
|  | ATTR_VERSION_CLI_LATEST = 'version_cli_latest' | ||||||
|  | ATTR_REFRESH_TOKEN = 'refresh_token' | ||||||
|  |  | ||||||
| SERVICE_MQTT = 'mqtt' | SERVICE_MQTT = 'mqtt' | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,8 +6,7 @@ import logging | |||||||
| from .coresys import CoreSysAttributes | from .coresys import CoreSysAttributes | ||||||
| from .const import ( | from .const import ( | ||||||
|     STARTUP_SYSTEM, STARTUP_SERVICES, STARTUP_APPLICATION, STARTUP_INITIALIZE) |     STARTUP_SYSTEM, STARTUP_SERVICES, STARTUP_APPLICATION, STARTUP_INITIALIZE) | ||||||
| from .exceptions import HassioError | from .exceptions import HassioError, HomeAssistantError | ||||||
| from .utils.dt import fetch_timezone |  | ||||||
|  |  | ||||||
| _LOGGER = logging.getLogger(__name__) | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| @@ -21,10 +20,8 @@ class HassIO(CoreSysAttributes): | |||||||
|  |  | ||||||
|     async def setup(self): |     async def setup(self): | ||||||
|         """Setup HassIO orchestration.""" |         """Setup HassIO orchestration.""" | ||||||
|         # update timezone |         # Load Supervisor | ||||||
|         if self.sys_config.timezone == 'UTC': |         await self.sys_supervisor.load() | ||||||
|             self.sys_config.timezone = \ |  | ||||||
|                 await fetch_timezone(self.sys_websession) |  | ||||||
|  |  | ||||||
|         # Load DBus |         # Load DBus | ||||||
|         await self.sys_dbus.load() |         await self.sys_dbus.load() | ||||||
| @@ -35,9 +32,6 @@ class HassIO(CoreSysAttributes): | |||||||
|         # Load HassOS |         # Load HassOS | ||||||
|         await self.sys_hassos.load() |         await self.sys_hassos.load() | ||||||
|  |  | ||||||
|         # Load Supervisor |  | ||||||
|         await self.sys_supervisor.load() |  | ||||||
|  |  | ||||||
|         # Load Home Assistant |         # Load Home Assistant | ||||||
|         await self.sys_homeassistant.load() |         await self.sys_homeassistant.load() | ||||||
|  |  | ||||||
| @@ -93,7 +87,8 @@ class HassIO(CoreSysAttributes): | |||||||
|  |  | ||||||
|             # run HomeAssistant |             # run HomeAssistant | ||||||
|             if self.sys_homeassistant.boot: |             if self.sys_homeassistant.boot: | ||||||
|                 await self.sys_homeassistant.start() |                 with suppress(HomeAssistantError): | ||||||
|  |                     await self.sys_homeassistant.start() | ||||||
|  |  | ||||||
|             # start addon mark as application |             # start addon mark as application | ||||||
|             await self.sys_addons.boot(STARTUP_APPLICATION) |             await self.sys_addons.boot(STARTUP_APPLICATION) | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ class Hostname(DBusInterface): | |||||||
|  |  | ||||||
|         Return a coroutine. |         Return a coroutine. | ||||||
|         """ |         """ | ||||||
|         return self.dbus.SetStaticHostname(hostname) |         return self.dbus.SetStaticHostname(hostname, False) | ||||||
|  |  | ||||||
|     @dbus_connected |     @dbus_connected | ||||||
|     def get_properties(self): |     def get_properties(self): | ||||||
|   | |||||||
| @@ -216,8 +216,8 @@ class DockerAddon(DockerInterface): | |||||||
|         # DeviceTree support |         # DeviceTree support | ||||||
|         if self.addon.with_devicetree: |         if self.addon.with_devicetree: | ||||||
|             volumes.update({ |             volumes.update({ | ||||||
|                 "/sys/firmware/devicetree": { |                 "/sys/firmware/devicetree/base": { | ||||||
|                     'bind': "/sys/firmware/devicetree", 'mode': 'ro' |                     'bind': "/device-tree", 'mode': 'ro' | ||||||
|                 }, |                 }, | ||||||
|             }) |             }) | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										37
									
								
								hassio/docker/hassos_cli.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								hassio/docker/hassos_cli.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  | """HassOS Cli docker object.""" | ||||||
|  | import logging | ||||||
|  |  | ||||||
|  | import docker | ||||||
|  |  | ||||||
|  | from .interface import DockerInterface | ||||||
|  | from ..coresys import CoreSysAttributes | ||||||
|  |  | ||||||
|  | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class DockerHassOSCli(DockerInterface, CoreSysAttributes): | ||||||
|  |     """Docker hassio wrapper for HassOS Cli.""" | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def image(self): | ||||||
|  |         """Return name of HassOS cli image.""" | ||||||
|  |         return f"homeassistant/{self.sys_arch}-hassio-cli" | ||||||
|  |  | ||||||
|  |     def _stop(self): | ||||||
|  |         """Don't need stop.""" | ||||||
|  |         return True | ||||||
|  |  | ||||||
|  |     def _attach(self): | ||||||
|  |         """Attach to running docker container. | ||||||
|  |         Need run inside executor. | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             image = self.sys_docker.images.get(self.image) | ||||||
|  |  | ||||||
|  |         except docker.errors.DockerException: | ||||||
|  |             _LOGGER.warning("Can't find a HassOS cli %s", self.image) | ||||||
|  |  | ||||||
|  |         else: | ||||||
|  |             self._meta = image.attrs | ||||||
|  |             _LOGGER.info("Found HassOS cli %s with version %s", | ||||||
|  |                          self.image, self.version) | ||||||
| @@ -11,7 +11,7 @@ _LOGGER = logging.getLogger(__name__) | |||||||
|  |  | ||||||
|  |  | ||||||
| class DockerSupervisor(DockerInterface, CoreSysAttributes): | class DockerSupervisor(DockerInterface, CoreSysAttributes): | ||||||
|     """Docker hassio wrapper for HomeAssistant.""" |     """Docker hassio wrapper for Supervisor.""" | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def name(self): |     def name(self): | ||||||
|   | |||||||
| @@ -1,4 +1,7 @@ | |||||||
| """Core Exceptions.""" | """Core Exceptions.""" | ||||||
|  | import asyncio | ||||||
|  |  | ||||||
|  | import aiohttp | ||||||
|  |  | ||||||
|  |  | ||||||
| class HassioError(Exception): | class HassioError(Exception): | ||||||
| @@ -11,6 +14,29 @@ class HassioNotSupportedError(HassioError): | |||||||
|     pass |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # HomeAssistant | ||||||
|  |  | ||||||
|  | class HomeAssistantError(HassioError): | ||||||
|  |     """Home Assistant exception.""" | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class HomeAssistantUpdateError(HomeAssistantError): | ||||||
|  |     """Error on update of a Home Assistant.""" | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class HomeAssistantAuthError(HomeAssistantError): | ||||||
|  |     """Home Assistant Auth API exception.""" | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class HomeAssistantAPIError( | ||||||
|  |         HomeAssistantAuthError, asyncio.TimeoutError, aiohttp.ClientError): | ||||||
|  |     """Home Assistant API exception.""" | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
| # HassOS | # HassOS | ||||||
|  |  | ||||||
| class HassOSError(HassioError): | class HassOSError(HassioError): | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| """HassOS support on supervisor.""" | """HassOS support on supervisor.""" | ||||||
|  | import asyncio | ||||||
| import logging | import logging | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
|  |  | ||||||
| @@ -7,6 +8,7 @@ from cpe import CPE | |||||||
|  |  | ||||||
| from .coresys import CoreSysAttributes | from .coresys import CoreSysAttributes | ||||||
| from .const import URL_HASSOS_OTA | from .const import URL_HASSOS_OTA | ||||||
|  | from .docker.hassos_cli import DockerHassOSCli | ||||||
| from .exceptions import HassOSNotSupportedError, HassOSUpdateError, DBusError | from .exceptions import HassOSNotSupportedError, HassOSUpdateError, DBusError | ||||||
|  |  | ||||||
| _LOGGER = logging.getLogger(__name__) | _LOGGER = logging.getLogger(__name__) | ||||||
| @@ -18,6 +20,7 @@ class HassOS(CoreSysAttributes): | |||||||
|     def __init__(self, coresys): |     def __init__(self, coresys): | ||||||
|         """Initialize HassOS handler.""" |         """Initialize HassOS handler.""" | ||||||
|         self.coresys = coresys |         self.coresys = coresys | ||||||
|  |         self.instance = DockerHassOSCli(coresys) | ||||||
|         self._available = False |         self._available = False | ||||||
|         self._version = None |         self._version = None | ||||||
|         self._board = None |         self._board = None | ||||||
| @@ -32,11 +35,31 @@ class HassOS(CoreSysAttributes): | |||||||
|         """Return version of HassOS.""" |         """Return version of HassOS.""" | ||||||
|         return self._version |         return self._version | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def version_cli(self): | ||||||
|  |         """Return version of HassOS cli.""" | ||||||
|  |         return self.instance.version | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def version_latest(self): |     def version_latest(self): | ||||||
|         """Return version of HassOS.""" |         """Return version of HassOS.""" | ||||||
|         return self.sys_updater.version_hassos |         return self.sys_updater.version_hassos | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def version_cli_latest(self): | ||||||
|  |         """Return version of HassOS.""" | ||||||
|  |         return self.sys_updater.version_hassos_cli | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def need_update(self): | ||||||
|  |         """Return true if a HassOS update is available.""" | ||||||
|  |         return self.version != self.version_latest | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def need_cli_update(self): | ||||||
|  |         """Return true if a HassOS cli update is available.""" | ||||||
|  |         return self.version_cli != self.version_cli_latest | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def board(self): |     def board(self): | ||||||
|         """Return board name.""" |         """Return board name.""" | ||||||
| @@ -56,6 +79,10 @@ class HassOS(CoreSysAttributes): | |||||||
|         try: |         try: | ||||||
|             _LOGGER.info("Fetch OTA update from %s", url) |             _LOGGER.info("Fetch OTA update from %s", url) | ||||||
|             async with self.sys_websession.get(url) as request: |             async with self.sys_websession.get(url) as request: | ||||||
|  |                 if request.status != 200: | ||||||
|  |                     raise HassOSUpdateError() | ||||||
|  |  | ||||||
|  |                 # Download RAUCB file | ||||||
|                 with raucb.open('wb') as ota_file: |                 with raucb.open('wb') as ota_file: | ||||||
|                     while True: |                     while True: | ||||||
|                         chunk = await request.content.read(1048576) |                         chunk = await request.content.read(1048576) | ||||||
| @@ -66,7 +93,7 @@ class HassOS(CoreSysAttributes): | |||||||
|             _LOGGER.info("OTA update is downloaded on %s", raucb) |             _LOGGER.info("OTA update is downloaded on %s", raucb) | ||||||
|             return raucb |             return raucb | ||||||
|  |  | ||||||
|         except aiohttp.ClientError as err: |         except (aiohttp.ClientError, asyncio.TimeoutError) as err: | ||||||
|             _LOGGER.warning("Can't fetch versions from %s: %s", url, err) |             _LOGGER.warning("Can't fetch versions from %s: %s", url, err) | ||||||
|  |  | ||||||
|         except OSError as err: |         except OSError as err: | ||||||
| @@ -86,7 +113,7 @@ class HassOS(CoreSysAttributes): | |||||||
|             cpe = CPE(self.sys_host.info.cpe) |             cpe = CPE(self.sys_host.info.cpe) | ||||||
|             assert cpe.get_product()[0] == 'hassos' |             assert cpe.get_product()[0] == 'hassos' | ||||||
|         except (AssertionError, NotImplementedError): |         except (AssertionError, NotImplementedError): | ||||||
|             _LOGGER.debug("Ignore HassOS") |             _LOGGER.debug("Found no HassOS") | ||||||
|             return |             return | ||||||
|  |  | ||||||
|         # Store meta data |         # Store meta data | ||||||
| @@ -95,6 +122,7 @@ class HassOS(CoreSysAttributes): | |||||||
|         self._board = cpe.get_target_hardware()[0] |         self._board = cpe.get_target_hardware()[0] | ||||||
|  |  | ||||||
|         _LOGGER.info("Detect HassOS %s on host system", self.version) |         _LOGGER.info("Detect HassOS %s on host system", self.version) | ||||||
|  |         await self.instance.attach() | ||||||
|  |  | ||||||
|     def config_sync(self): |     def config_sync(self): | ||||||
|         """Trigger a host config reload from usb. |         """Trigger a host config reload from usb. | ||||||
| @@ -142,3 +170,17 @@ class HassOS(CoreSysAttributes): | |||||||
|         _LOGGER.error( |         _LOGGER.error( | ||||||
|             "HassOS update fails with: %s", rauc_status.get('LastError')) |             "HassOS update fails with: %s", rauc_status.get('LastError')) | ||||||
|         raise HassOSUpdateError() |         raise HassOSUpdateError() | ||||||
|  |  | ||||||
|  |     async def update_cli(self, version=None): | ||||||
|  |         """Update local HassOS cli.""" | ||||||
|  |         version = version or self.version_cli_latest | ||||||
|  |  | ||||||
|  |         if version == self.version_cli: | ||||||
|  |             _LOGGER.warning("Version %s is already installed for CLI", version) | ||||||
|  |             raise HassOSUpdateError() | ||||||
|  |  | ||||||
|  |         if await self.instance.update(version): | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         _LOGGER.error("HassOS CLI update fails.") | ||||||
|  |         raise HassOSUpdateError() | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| """HomeAssistant control object.""" | """HomeAssistant control object.""" | ||||||
| import asyncio | import asyncio | ||||||
|  | from contextlib import asynccontextmanager, suppress | ||||||
| import logging | import logging | ||||||
| import os | import os | ||||||
| import re | import re | ||||||
| @@ -7,15 +8,19 @@ import socket | |||||||
| import time | import time | ||||||
|  |  | ||||||
| import aiohttp | import aiohttp | ||||||
| from aiohttp.hdrs import CONTENT_TYPE | from aiohttp import hdrs | ||||||
| import attr | import attr | ||||||
|  |  | ||||||
| from .const import ( | from .const import ( | ||||||
|     FILE_HASSIO_HOMEASSISTANT, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_UUID, |     FILE_HASSIO_HOMEASSISTANT, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_UUID, | ||||||
|     ATTR_BOOT, ATTR_PASSWORD, ATTR_PORT, ATTR_SSL, ATTR_WATCHDOG, |     ATTR_BOOT, ATTR_PASSWORD, ATTR_PORT, ATTR_SSL, ATTR_WATCHDOG, | ||||||
|     ATTR_WAIT_BOOT, HEADER_HA_ACCESS, CONTENT_TYPE_JSON) |     ATTR_WAIT_BOOT, ATTR_REFRESH_TOKEN, | ||||||
|  |     HEADER_HA_ACCESS) | ||||||
| from .coresys import CoreSysAttributes | from .coresys import CoreSysAttributes | ||||||
| from .docker.homeassistant import DockerHomeAssistant | from .docker.homeassistant import DockerHomeAssistant | ||||||
|  | from .exceptions import ( | ||||||
|  |     HomeAssistantUpdateError, HomeAssistantError, HomeAssistantAPIError, | ||||||
|  |     HomeAssistantAuthError) | ||||||
| from .utils import convert_to_ascii, process_lock | from .utils import convert_to_ascii, process_lock | ||||||
| from .utils.json import JsonConfig | from .utils.json import JsonConfig | ||||||
| from .validate import SCHEMA_HASS_CONFIG | from .validate import SCHEMA_HASS_CONFIG | ||||||
| @@ -25,7 +30,7 @@ _LOGGER = logging.getLogger(__name__) | |||||||
| RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml") | RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml") | ||||||
|  |  | ||||||
| # pylint: disable=invalid-name | # pylint: disable=invalid-name | ||||||
| ConfigResult = attr.make_class('ConfigResult', ['valid', 'log']) | ConfigResult = attr.make_class('ConfigResult', ['valid', 'log'], frozen=True) | ||||||
|  |  | ||||||
|  |  | ||||||
| class HomeAssistant(JsonConfig, CoreSysAttributes): | class HomeAssistant(JsonConfig, CoreSysAttributes): | ||||||
| @@ -38,6 +43,8 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): | |||||||
|         self.instance = DockerHomeAssistant(coresys) |         self.instance = DockerHomeAssistant(coresys) | ||||||
|         self.lock = asyncio.Lock(loop=coresys.loop) |         self.lock = asyncio.Lock(loop=coresys.loop) | ||||||
|         self._error_state = False |         self._error_state = False | ||||||
|  |         # We don't persist access tokens. Instead we fetch new ones when needed | ||||||
|  |         self.access_token = None | ||||||
|  |  | ||||||
|     async def load(self): |     async def load(self): | ||||||
|         """Prepare HomeAssistant object.""" |         """Prepare HomeAssistant object.""" | ||||||
| @@ -175,6 +182,16 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): | |||||||
|         """Return a UUID of this HomeAssistant.""" |         """Return a UUID of this HomeAssistant.""" | ||||||
|         return self._data[ATTR_UUID] |         return self._data[ATTR_UUID] | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def refresh_token(self): | ||||||
|  |         """Return the refresh token to authenticate with HomeAssistant.""" | ||||||
|  |         return self._data.get(ATTR_REFRESH_TOKEN) | ||||||
|  |  | ||||||
|  |     @refresh_token.setter | ||||||
|  |     def refresh_token(self, value): | ||||||
|  |         """Set Home Assistant refresh_token.""" | ||||||
|  |         self._data[ATTR_REFRESH_TOKEN] = value | ||||||
|  |  | ||||||
|     @process_lock |     @process_lock | ||||||
|     async def install_landingpage(self): |     async def install_landingpage(self): | ||||||
|         """Install a landingpage.""" |         """Install a landingpage.""" | ||||||
| @@ -186,7 +203,11 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): | |||||||
|             await asyncio.sleep(60) |             await asyncio.sleep(60) | ||||||
|  |  | ||||||
|         # Run landingpage after installation |         # Run landingpage after installation | ||||||
|         await self._start() |         _LOGGER.info("Start landingpage") | ||||||
|  |         try: | ||||||
|  |             await self._start() | ||||||
|  |         except HomeAssistantError: | ||||||
|  |             _LOGGER.warning("Can't start landingpage") | ||||||
|  |  | ||||||
|     @process_lock |     @process_lock | ||||||
|     async def install(self): |     async def install(self): | ||||||
| @@ -205,46 +226,57 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): | |||||||
|  |  | ||||||
|         # finishing |         # finishing | ||||||
|         _LOGGER.info("HomeAssistant docker now installed") |         _LOGGER.info("HomeAssistant docker now installed") | ||||||
|         if self.boot: |         try: | ||||||
|  |             if not self.boot: | ||||||
|  |                 return | ||||||
|  |             _LOGGER.info("Start HomeAssistant") | ||||||
|             await self._start() |             await self._start() | ||||||
|         await self.instance.cleanup() |         except HomeAssistantError: | ||||||
|  |             _LOGGER.error("Can't start HomeAssistant!") | ||||||
|  |         finally: | ||||||
|  |             await self.instance.cleanup() | ||||||
|  |  | ||||||
|     @process_lock |     @process_lock | ||||||
|     async def update(self, version=None): |     async def update(self, version=None): | ||||||
|         """Update HomeAssistant version.""" |         """Update HomeAssistant version.""" | ||||||
|         version = version or self.last_version |         version = version or self.last_version | ||||||
|         rollback = self.version |         rollback = self.version if not self.error_state else None | ||||||
|         running = await self.instance.is_running() |         running = await self.instance.is_running() | ||||||
|         exists = await self.instance.exists() |         exists = await self.instance.exists() | ||||||
|  |  | ||||||
|         if exists and version == self.instance.version: |         if exists and version == self.instance.version: | ||||||
|             _LOGGER.warning("Version %s is already installed", version) |             _LOGGER.warning("Version %s is already installed", version) | ||||||
|             return False |             return HomeAssistantUpdateError() | ||||||
|  |  | ||||||
|         # process a update |         # process a update | ||||||
|         async def _update(to_version): |         async def _update(to_version): | ||||||
|             """Run Home Assistant update.""" |             """Run Home Assistant update.""" | ||||||
|             try: |             try: | ||||||
|                 return await self.instance.update(to_version) |                 _LOGGER.info("Update HomeAssistant to version %s", to_version) | ||||||
|  |                 if not await self.instance.update(to_version): | ||||||
|  |                     raise HomeAssistantUpdateError() | ||||||
|             finally: |             finally: | ||||||
|                 if running: |                 if running: | ||||||
|                     await self._start() |                     await self._start() | ||||||
|  |                 _LOGGER.info("Successfull run HomeAssistant %s", to_version) | ||||||
|  |  | ||||||
|         # Update Home Assistant |         # Update Home Assistant | ||||||
|         ret = await _update(version) |         with suppress(HomeAssistantError): | ||||||
|  |             await _update(version) | ||||||
|  |             return | ||||||
|  |  | ||||||
|         # Update going wrong, revert it |         # Update going wrong, revert it | ||||||
|         if self.error_state and rollback: |         if self.error_state and rollback: | ||||||
|             _LOGGER.fatal("Home Assistant update fails -> rollback!") |             _LOGGER.fatal("HomeAssistant update fails -> rollback!") | ||||||
|             ret = await _update(rollback) |             await _update(rollback) | ||||||
|  |         else: | ||||||
|         return ret |             raise HomeAssistantUpdateError() | ||||||
|  |  | ||||||
|     async def _start(self): |     async def _start(self): | ||||||
|         """Start HomeAssistant docker & wait.""" |         """Start HomeAssistant docker & wait.""" | ||||||
|         if not await self.instance.run(): |         if not await self.instance.run(): | ||||||
|             return False |             raise HomeAssistantError() | ||||||
|         return await self._block_till_run() |         await self._block_till_run() | ||||||
|  |  | ||||||
|     @process_lock |     @process_lock | ||||||
|     def start(self): |     def start(self): | ||||||
| @@ -266,7 +298,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): | |||||||
|     async def restart(self): |     async def restart(self): | ||||||
|         """Restart HomeAssistant docker.""" |         """Restart HomeAssistant docker.""" | ||||||
|         await self.instance.stop() |         await self.instance.stop() | ||||||
|         return await self._start() |         await self._start() | ||||||
|  |  | ||||||
|     def logs(self): |     def logs(self): | ||||||
|         """Get HomeAssistant docker logs. |         """Get HomeAssistant docker logs. | ||||||
| @@ -309,7 +341,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): | |||||||
|  |  | ||||||
|         # if not valid |         # if not valid | ||||||
|         if result.exit_code is None: |         if result.exit_code is None: | ||||||
|             return ConfigResult(False, "") |             raise HomeAssistantError() | ||||||
|  |  | ||||||
|         # parse output |         # parse output | ||||||
|         log = convert_to_ascii(result.output) |         log = convert_to_ascii(result.output) | ||||||
| @@ -317,51 +349,86 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): | |||||||
|             return ConfigResult(False, log) |             return ConfigResult(False, log) | ||||||
|         return ConfigResult(True, log) |         return ConfigResult(True, log) | ||||||
|  |  | ||||||
|     async def check_api_state(self): |     async def ensure_access_token(self): | ||||||
|         """Check if Home-Assistant up and running.""" |         """Ensures there is an access token.""" | ||||||
|         url = f"{self.api_url}/api/" |         if self.access_token is not None: | ||||||
|         header = {CONTENT_TYPE: CONTENT_TYPE_JSON} |             return | ||||||
|  |  | ||||||
|         if self.api_password: |         with suppress(asyncio.TimeoutError, aiohttp.ClientError): | ||||||
|             header.update({HEADER_HA_ACCESS: self.api_password}) |  | ||||||
|  |  | ||||||
|         try: |  | ||||||
|             # pylint: disable=bad-continuation |  | ||||||
|             async with self.sys_websession_ssl.get( |             async with self.sys_websession_ssl.get( | ||||||
|                     url, headers=header, timeout=30) as request: |                     f"{self.api_url}/auth/token", | ||||||
|                 status = request.status |                     timeout=30, | ||||||
|  |                     data={ | ||||||
|  |                         "grant_type": "refresh_token", | ||||||
|  |                         "refresh_token": self.refresh_token | ||||||
|  |                     } | ||||||
|  |             ) as resp: | ||||||
|  |                 if resp.status != 200: | ||||||
|  |                     _LOGGER.error("Authenticate problem with HomeAssistant!") | ||||||
|  |                     raise HomeAssistantAuthError() | ||||||
|  |                 tokens = await resp.json() | ||||||
|  |                 self.access_token = tokens['access_token'] | ||||||
|  |                 return | ||||||
|  |  | ||||||
|         except (asyncio.TimeoutError, aiohttp.ClientError): |         _LOGGER.error("Can't update HomeAssistant access token!") | ||||||
|             return False |         raise HomeAssistantAPIError() | ||||||
|  |  | ||||||
|         if status not in (200, 201): |     @asynccontextmanager | ||||||
|             _LOGGER.warning("Home-Assistant API config missmatch") |     async def make_request(self, method, path, json=None, content_type=None, | ||||||
|         return True |                            data=None, timeout=30): | ||||||
|  |         """Async context manager to make a request with right auth.""" | ||||||
|  |         url = f"{self.api_url}/{path}" | ||||||
|  |         headers = {} | ||||||
|  |  | ||||||
|  |         # Passthrough content type | ||||||
|  |         if content_type is not None: | ||||||
|  |             headers[hdrs.CONTENT_TYPE] = content_type | ||||||
|  |  | ||||||
|  |         # Set old API Password | ||||||
|  |         if self.api_password: | ||||||
|  |             headers[HEADER_HA_ACCESS] = self.api_password | ||||||
|  |  | ||||||
|  |         for _ in (1, 2): | ||||||
|  |             # Prepare Access token | ||||||
|  |             if self.refresh_token: | ||||||
|  |                 await self.ensure_access_token() | ||||||
|  |                 headers[hdrs.AUTHORIZATION] = f'Bearer {self.access_token}' | ||||||
|  |  | ||||||
|  |             async with getattr(self.sys_websession_ssl, method)( | ||||||
|  |                     url, data=data, timeout=timeout, json=json, headers=headers | ||||||
|  |             ) as resp: | ||||||
|  |                 # Access token expired | ||||||
|  |                 if resp.status == 401 and self.refresh_token: | ||||||
|  |                     self.access_token = None | ||||||
|  |                     continue | ||||||
|  |                 yield resp | ||||||
|  |                 return | ||||||
|  |  | ||||||
|  |         raise HomeAssistantAPIError() | ||||||
|  |  | ||||||
|  |     async def check_api_state(self): | ||||||
|  |         """Return True if Home-Assistant up and running.""" | ||||||
|  |         with suppress(HomeAssistantAPIError): | ||||||
|  |             async with self.make_request('get', 'api/') as resp: | ||||||
|  |                 if resp.status in (200, 201): | ||||||
|  |                     return True | ||||||
|  |                 err = resp.status | ||||||
|  |  | ||||||
|  |         _LOGGER.warning("Home-Assistant API config missmatch: %d", err) | ||||||
|  |         return False | ||||||
|  |  | ||||||
|     async def send_event(self, event_type, event_data=None): |     async def send_event(self, event_type, event_data=None): | ||||||
|         """Send event to Home-Assistant.""" |         """Send event to Home-Assistant.""" | ||||||
|         url = f"{self.api_url}/api/events/{event_type}" |         with suppress(HomeAssistantAPIError): | ||||||
|         header = {CONTENT_TYPE: CONTENT_TYPE_JSON} |             async with self.make_request( | ||||||
|  |                     'get', f'api/events/{event_type}' | ||||||
|  |             ) as resp: | ||||||
|  |                 if resp.status in (200, 201): | ||||||
|  |                     return | ||||||
|  |                 err = resp.status | ||||||
|  |  | ||||||
|         if self.api_password: |         _LOGGER.warning("HomeAssistant event %s fails: %s", event_type, err) | ||||||
|             header.update({HEADER_HA_ACCESS: self.api_password}) |         return HomeAssistantError() | ||||||
|  |  | ||||||
|         try: |  | ||||||
|             # pylint: disable=bad-continuation |  | ||||||
|             async with self.sys_websession_ssl.post( |  | ||||||
|                     url, headers=header, timeout=30, |  | ||||||
|                     json=event_data) as request: |  | ||||||
|                 status = request.status |  | ||||||
|  |  | ||||||
|         except (asyncio.TimeoutError, aiohttp.ClientError) as err: |  | ||||||
|             _LOGGER.warning( |  | ||||||
|                 "Home-Assistant event %s fails: %s", event_type, err) |  | ||||||
|             return False |  | ||||||
|  |  | ||||||
|         if status not in (200, 201): |  | ||||||
|             _LOGGER.warning("Home-Assistant event %s fails", event_type) |  | ||||||
|             return False |  | ||||||
|         return True |  | ||||||
|  |  | ||||||
|     async def _block_till_run(self): |     async def _block_till_run(self): | ||||||
|         """Block until Home-Assistant is booting up or startup timeout.""" |         """Block until Home-Assistant is booting up or startup timeout.""" | ||||||
| @@ -374,27 +441,28 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): | |||||||
|                 result = sock.connect_ex((str(self.api_ip), self.api_port)) |                 result = sock.connect_ex((str(self.api_ip), self.api_port)) | ||||||
|                 sock.close() |                 sock.close() | ||||||
|  |  | ||||||
|  |                 # Check if the port is available | ||||||
|                 if result == 0: |                 if result == 0: | ||||||
|                     return True |                     return True | ||||||
|                 return False |  | ||||||
|             except OSError: |             except OSError: | ||||||
|                 pass |                 pass | ||||||
|  |             return False | ||||||
|  |  | ||||||
|         while time.monotonic() - start_time < self.wait_boot: |         while time.monotonic() - start_time < self.wait_boot: | ||||||
|             # Check if API response |             # Check if API response | ||||||
|             if await self.sys_run_in_executor(check_port): |             if await self.sys_run_in_executor(check_port): | ||||||
|                 _LOGGER.info("Detect a running Home-Assistant instance") |                 _LOGGER.info("Detect a running HomeAssistant instance") | ||||||
|                 self._error_state = False |                 self._error_state = False | ||||||
|                 return True |                 return | ||||||
|  |  | ||||||
|  |             # wait and don't hit the system | ||||||
|  |             await asyncio.sleep(10) | ||||||
|  |  | ||||||
|             # Check if Container is is_running |             # Check if Container is is_running | ||||||
|             if not await self.instance.is_running(): |             if not await self.instance.is_running(): | ||||||
|                 _LOGGER.error("Home Assistant is crashed!") |                 _LOGGER.error("Home Assistant is crashed!") | ||||||
|                 break |                 break | ||||||
|  |  | ||||||
|             # wait and don't hit the system |         _LOGGER.warning("Don't wait anymore of HomeAssistant startup!") | ||||||
|             await asyncio.sleep(10) |  | ||||||
|  |  | ||||||
|         _LOGGER.warning("Don't wait anymore of Home-Assistant startup!") |  | ||||||
|         self._error_state = True |         self._error_state = True | ||||||
|         return False |         raise HomeAssistantError() | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ ALL_FOLDERS = [FOLDER_HOMEASSISTANT, FOLDER_SHARE, FOLDER_ADDONS, FOLDER_SSL] | |||||||
|  |  | ||||||
| def unique_addons(addons_list): | def unique_addons(addons_list): | ||||||
|     """Validate that an add-on is unique.""" |     """Validate that an add-on is unique.""" | ||||||
|     single = set([addon[ATTR_SLUG] for addon in addons_list]) |     single = set(addon[ATTR_SLUG] for addon in addons_list) | ||||||
|  |  | ||||||
|     if len(single) != len(addons_list): |     if len(single) != len(addons_list): | ||||||
|         raise vol.Invalid("Invalid addon list on snapshot!") |         raise vol.Invalid("Invalid addon list on snapshot!") | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| """HomeAssistant control object.""" | """HomeAssistant control object.""" | ||||||
|  | import asyncio | ||||||
| import logging | import logging | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from tempfile import TemporaryDirectory | from tempfile import TemporaryDirectory | ||||||
| @@ -60,7 +61,7 @@ class Supervisor(CoreSysAttributes): | |||||||
|             async with self.sys_websession.get(url, timeout=10) as request: |             async with self.sys_websession.get(url, timeout=10) as request: | ||||||
|                 data = await request.text() |                 data = await request.text() | ||||||
|  |  | ||||||
|         except aiohttp.ClientError as err: |         except (aiohttp.ClientError, asyncio.TimeoutError) as err: | ||||||
|             _LOGGER.warning("Can't fetch AppArmor profile: %s", err) |             _LOGGER.warning("Can't fetch AppArmor profile: %s", err) | ||||||
|             return |             return | ||||||
|  |  | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ HASS_WATCHDOG_API = 'HASS_WATCHDOG_API' | |||||||
|  |  | ||||||
| RUN_UPDATE_SUPERVISOR = 29100 | RUN_UPDATE_SUPERVISOR = 29100 | ||||||
| RUN_UPDATE_ADDONS = 57600 | RUN_UPDATE_ADDONS = 57600 | ||||||
|  | RUN_UPDATE_HASSOSCLI = 29100 | ||||||
|  |  | ||||||
| RUN_RELOAD_ADDONS = 21600 | RUN_RELOAD_ADDONS = 21600 | ||||||
| RUN_RELOAD_SNAPSHOTS = 72000 | RUN_RELOAD_SNAPSHOTS = 72000 | ||||||
| @@ -35,6 +36,8 @@ class Tasks(CoreSysAttributes): | |||||||
|             self._update_addons, RUN_UPDATE_ADDONS)) |             self._update_addons, RUN_UPDATE_ADDONS)) | ||||||
|         self.jobs.add(self.sys_scheduler.register_task( |         self.jobs.add(self.sys_scheduler.register_task( | ||||||
|             self._update_supervisor, RUN_UPDATE_SUPERVISOR)) |             self._update_supervisor, RUN_UPDATE_SUPERVISOR)) | ||||||
|  |         self.jobs.add(self.sys_scheduler.register_task( | ||||||
|  |             self._update_hassos_cli, RUN_UPDATE_HASSOSCLI)) | ||||||
|  |  | ||||||
|         self.jobs.add(self.sys_scheduler.register_task( |         self.jobs.add(self.sys_scheduler.register_task( | ||||||
|             self.sys_addons.reload, RUN_RELOAD_ADDONS)) |             self.sys_addons.reload, RUN_RELOAD_ADDONS)) | ||||||
| @@ -79,7 +82,7 @@ class Tasks(CoreSysAttributes): | |||||||
|         if not self.sys_supervisor.need_update: |         if not self.sys_supervisor.need_update: | ||||||
|             return |             return | ||||||
|  |  | ||||||
|         # don't perform an update on beta/dev channel |         # don't perform an update on dev channel | ||||||
|         if self.sys_dev: |         if self.sys_dev: | ||||||
|             _LOGGER.warning("Ignore Hass.io update on dev channel!") |             _LOGGER.warning("Ignore Hass.io update on dev channel!") | ||||||
|             return |             return | ||||||
| @@ -131,5 +134,20 @@ class Tasks(CoreSysAttributes): | |||||||
|             return |             return | ||||||
|  |  | ||||||
|         _LOGGER.error("Watchdog found a problem with Home-Assistant API!") |         _LOGGER.error("Watchdog found a problem with Home-Assistant API!") | ||||||
|         await self.sys_homeassistant.restart() |         try: | ||||||
|         self._cache[HASS_WATCHDOG_API] = 0 |             await self.sys_homeassistant.restart() | ||||||
|  |         finally: | ||||||
|  |             self._cache[HASS_WATCHDOG_API] = 0 | ||||||
|  |  | ||||||
|  |     async def _update_hassos_cli(self): | ||||||
|  |         """Check and run update of HassOS CLI.""" | ||||||
|  |         if not self.sys_hassos.need_cli_update: | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         # don't perform an update on dev channel | ||||||
|  |         if self.sys_dev: | ||||||
|  |             _LOGGER.warning("Ignore HassOS CLI update on dev channel!") | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         _LOGGER.info("Found new HassOS CLI version") | ||||||
|  |         await self.sys_hassos.update_cli() | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| """Fetch last versions from webserver.""" | """Fetch last versions from webserver.""" | ||||||
|  | import asyncio | ||||||
| from contextlib import suppress | from contextlib import suppress | ||||||
| from datetime import timedelta | from datetime import timedelta | ||||||
| import json | import json | ||||||
| @@ -8,7 +9,7 @@ import aiohttp | |||||||
|  |  | ||||||
| from .const import ( | from .const import ( | ||||||
|     URL_HASSIO_VERSION, FILE_HASSIO_UPDATER, ATTR_HOMEASSISTANT, ATTR_HASSIO, |     URL_HASSIO_VERSION, FILE_HASSIO_UPDATER, ATTR_HOMEASSISTANT, ATTR_HASSIO, | ||||||
|     ATTR_CHANNEL, ATTR_HASSOS) |     ATTR_CHANNEL, ATTR_HASSOS, ATTR_HASSOS_CLI) | ||||||
| from .coresys import CoreSysAttributes | from .coresys import CoreSysAttributes | ||||||
| from .utils import AsyncThrottle | from .utils import AsyncThrottle | ||||||
| from .utils.json import JsonConfig | from .utils.json import JsonConfig | ||||||
| @@ -51,6 +52,11 @@ class Updater(JsonConfig, CoreSysAttributes): | |||||||
|         """Return last version of hassos.""" |         """Return last version of hassos.""" | ||||||
|         return self._data.get(ATTR_HASSOS) |         return self._data.get(ATTR_HASSOS) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def version_hassos_cli(self): | ||||||
|  |         """Return last version of hassos cli.""" | ||||||
|  |         return self._data.get(ATTR_HASSOS_CLI) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def channel(self): |     def channel(self): | ||||||
|         """Return upstream channel of hassio instance.""" |         """Return upstream channel of hassio instance.""" | ||||||
| @@ -76,7 +82,7 @@ class Updater(JsonConfig, CoreSysAttributes): | |||||||
|             async with self.sys_websession.get(url, timeout=10) as request: |             async with self.sys_websession.get(url, timeout=10) as request: | ||||||
|                 data = await request.json(content_type=None) |                 data = await request.json(content_type=None) | ||||||
|  |  | ||||||
|         except aiohttp.ClientError as err: |         except (aiohttp.ClientError, asyncio.TimeoutError) as err: | ||||||
|             _LOGGER.warning("Can't fetch versions from %s: %s", url, err) |             _LOGGER.warning("Can't fetch versions from %s: %s", url, err) | ||||||
|             raise HassioUpdaterError() from None |             raise HassioUpdaterError() from None | ||||||
|  |  | ||||||
| @@ -99,6 +105,7 @@ class Updater(JsonConfig, CoreSysAttributes): | |||||||
|             # update hassos version |             # update hassos version | ||||||
|             if self.sys_hassos.available and board: |             if self.sys_hassos.available and board: | ||||||
|                 self._data[ATTR_HASSOS] = data['hassos'][board] |                 self._data[ATTR_HASSOS] = data['hassos'][board] | ||||||
|  |                 self._data[ATTR_HASSOS_CLI] = data['hassos-cli'] | ||||||
|  |  | ||||||
|         except KeyError as err: |         except KeyError as err: | ||||||
|             _LOGGER.warning("Can't process version data: %s", err) |             _LOGGER.warning("Can't process version data: %s", err) | ||||||
|   | |||||||
| @@ -3,7 +3,6 @@ from datetime import datetime, timedelta, timezone | |||||||
| import logging | import logging | ||||||
| import re | import re | ||||||
|  |  | ||||||
| import aiohttp |  | ||||||
| import pytz | import pytz | ||||||
|  |  | ||||||
| UTC = pytz.utc | UTC = pytz.utc | ||||||
| @@ -23,22 +22,6 @@ DATETIME_RE = re.compile( | |||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| async def fetch_timezone(websession): |  | ||||||
|     """Read timezone from freegeoip.""" |  | ||||||
|     data = {} |  | ||||||
|     try: |  | ||||||
|         async with websession.get(FREEGEOIP_URL, timeout=10) as request: |  | ||||||
|             data = await request.json() |  | ||||||
|  |  | ||||||
|     except aiohttp.ClientError as err: |  | ||||||
|         _LOGGER.warning("Can't fetch freegeoip data: %s", err) |  | ||||||
|  |  | ||||||
|     except ValueError as err: |  | ||||||
|         _LOGGER.warning("Error on parse freegeoip data: %s", err) |  | ||||||
|  |  | ||||||
|     return data.get('time_zone', 'UTC') |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # Copyright (c) Django Software Foundation and individual contributors. | # Copyright (c) Django Software Foundation and individual contributors. | ||||||
| # All rights reserved. | # All rights reserved. | ||||||
| # https://github.com/django/django/blob/master/LICENSE | # https://github.com/django/django/blob/master/LICENSE | ||||||
|   | |||||||
| @@ -106,13 +106,29 @@ class DBus: | |||||||
|             _LOGGER.error("Can't parse '%s': %s", raw, err) |             _LOGGER.error("Can't parse '%s': %s", raw, err) | ||||||
|             raise DBusParseError() from None |             raise DBusParseError() from None | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def gvariant_args(args): | ||||||
|  |         """Convert args into gvariant.""" | ||||||
|  |         gvariant = "" | ||||||
|  |         for arg in args: | ||||||
|  |             if isinstance(arg, bool): | ||||||
|  |                 gvariant += " {}".format(str(arg).lower()) | ||||||
|  |             elif isinstance(arg, (int, float)): | ||||||
|  |                 gvariant += f" {arg}" | ||||||
|  |             elif isinstance(arg, str): | ||||||
|  |                 gvariant += f" \"{arg}\"" | ||||||
|  |             else: | ||||||
|  |                 gvariant += " {}".format(str(arg)) | ||||||
|  |  | ||||||
|  |         return gvariant.lstrip() | ||||||
|  |  | ||||||
|     async def call_dbus(self, method, *args): |     async def call_dbus(self, method, *args): | ||||||
|         """Call a dbus method.""" |         """Call a dbus method.""" | ||||||
|         command = shlex.split(CALL.format( |         command = shlex.split(CALL.format( | ||||||
|             bus=self.bus_name, |             bus=self.bus_name, | ||||||
|             object=self.object_path, |             object=self.object_path, | ||||||
|             method=method, |             method=method, | ||||||
|             args=" ".join(map(str, args)) |             args=self.gvariant_args(args) | ||||||
|         )) |         )) | ||||||
|  |  | ||||||
|         # Run command |         # Run command | ||||||
| @@ -231,7 +247,7 @@ class DBusSignalWrapper: | |||||||
|         self._proc.send_signal(SIGINT) |         self._proc.send_signal(SIGINT) | ||||||
|         await self._proc.communicate() |         await self._proc.communicate() | ||||||
|  |  | ||||||
|     async def __aiter__(self): |     def __aiter__(self): | ||||||
|         """Start Iteratation.""" |         """Start Iteratation.""" | ||||||
|         return self |         return self | ||||||
|  |  | ||||||
|   | |||||||
| @@ -9,7 +9,8 @@ from .const import ( | |||||||
|     ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_CHANNEL, ATTR_TIMEZONE, ATTR_HASSOS, |     ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_CHANNEL, ATTR_TIMEZONE, ATTR_HASSOS, | ||||||
|     ATTR_ADDONS_CUSTOM_LIST, ATTR_PASSWORD, ATTR_HOMEASSISTANT, ATTR_HASSIO, |     ATTR_ADDONS_CUSTOM_LIST, ATTR_PASSWORD, ATTR_HOMEASSISTANT, ATTR_HASSIO, | ||||||
|     ATTR_BOOT, ATTR_LAST_BOOT, ATTR_SSL, ATTR_PORT, ATTR_WATCHDOG, |     ATTR_BOOT, ATTR_LAST_BOOT, ATTR_SSL, ATTR_PORT, ATTR_WATCHDOG, | ||||||
|     ATTR_WAIT_BOOT, ATTR_UUID, CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV) |     ATTR_WAIT_BOOT, ATTR_UUID, ATTR_REFRESH_TOKEN, ATTR_HASSOS_CLI, | ||||||
|  |     CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV) | ||||||
|  |  | ||||||
|  |  | ||||||
| RE_REPOSITORY = re.compile(r"^(?P<url>[^#]+)(?:#(?P<branch>[\w\-]+))?$") | RE_REPOSITORY = re.compile(r"^(?P<url>[^#]+)(?:#(?P<branch>[\w\-]+))?$") | ||||||
| @@ -88,6 +89,7 @@ SCHEMA_HASS_CONFIG = vol.Schema({ | |||||||
|     vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str), |     vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str), | ||||||
|     vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT, |     vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT, | ||||||
|     vol.Optional(ATTR_PASSWORD): vol.Maybe(vol.Coerce(str)), |     vol.Optional(ATTR_PASSWORD): vol.Maybe(vol.Coerce(str)), | ||||||
|  |     vol.Optional(ATTR_REFRESH_TOKEN): vol.Maybe(vol.Coerce(str)), | ||||||
|     vol.Optional(ATTR_SSL, default=False): vol.Boolean(), |     vol.Optional(ATTR_SSL, default=False): vol.Boolean(), | ||||||
|     vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(), |     vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(), | ||||||
|     vol.Optional(ATTR_WAIT_BOOT, default=600): |     vol.Optional(ATTR_WAIT_BOOT, default=600): | ||||||
| @@ -100,6 +102,7 @@ SCHEMA_UPDATER_CONFIG = vol.Schema({ | |||||||
|     vol.Optional(ATTR_HOMEASSISTANT): vol.Coerce(str), |     vol.Optional(ATTR_HOMEASSISTANT): vol.Coerce(str), | ||||||
|     vol.Optional(ATTR_HASSIO): vol.Coerce(str), |     vol.Optional(ATTR_HASSIO): vol.Coerce(str), | ||||||
|     vol.Optional(ATTR_HASSOS): vol.Coerce(str), |     vol.Optional(ATTR_HASSOS): vol.Coerce(str), | ||||||
|  |     vol.Optional(ATTR_HASSOS_CLI): vol.Coerce(str), | ||||||
| }, extra=vol.REMOVE_EXTRA) | }, extra=vol.REMOVE_EXTRA) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
 Submodule home-assistant-polymer updated: 42026f096f...d71a80c4f8
									
								
							
							
								
								
									
										6
									
								
								pylintrc
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								pylintrc
									
									
									
									
									
								
							| @@ -21,7 +21,6 @@ disable= | |||||||
|   abstract-class-little-used, |   abstract-class-little-used, | ||||||
|   abstract-class-not-used, |   abstract-class-not-used, | ||||||
|   unused-argument, |   unused-argument, | ||||||
|   global-statement, |  | ||||||
|   redefined-variable-type, |   redefined-variable-type, | ||||||
|   too-many-arguments, |   too-many-arguments, | ||||||
|   too-many-branches, |   too-many-branches, | ||||||
| @@ -32,7 +31,10 @@ disable= | |||||||
|   too-many-statements, |   too-many-statements, | ||||||
|   too-many-lines, |   too-many-lines, | ||||||
|   too-few-public-methods, |   too-few-public-methods, | ||||||
|   abstract-method |   abstract-method, | ||||||
|  |   no-else-return, | ||||||
|  |   useless-return, | ||||||
|  |   not-async-context-manager | ||||||
|  |  | ||||||
| [EXCEPTIONS] | [EXCEPTIONS] | ||||||
| overgeneral-exceptions=Exception,HomeAssistantError | overgeneral-exceptions=Exception,HomeAssistantError | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								setup.py
									
									
									
									
									
								
							| @@ -43,13 +43,13 @@ setup( | |||||||
|         'attr==0.3.1', |         'attr==0.3.1', | ||||||
|         'async_timeout==3.0.0', |         'async_timeout==3.0.0', | ||||||
|         'aiohttp==3.3.2', |         'aiohttp==3.3.2', | ||||||
|         'docker==3.3.0', |         'docker==3.4.0', | ||||||
|         'colorlog==3.1.2', |         'colorlog==3.1.2', | ||||||
|         'voluptuous==0.11.1', |         'voluptuous==0.11.1', | ||||||
|         'gitpython==2.1.10', |         'gitpython==2.1.10', | ||||||
|         'pytz==2018.4', |         'pytz==2018.4', | ||||||
|         'pyudev==0.21.0', |         'pyudev==0.21.0', | ||||||
|         'pycryptodome==3.4.11', |         'pycryptodome==3.6.4', | ||||||
|         "cpe==1.2.1" |         "cpe==1.2.1" | ||||||
|     ] |     ] | ||||||
| ) | ) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user