Compare commits

...

25 Commits
135 ... 139

Author SHA1 Message Date
Pascal Vizeli
6e7cf5e4c9 Merge pull request #796 from home-assistant/dev
Release 139
2018-11-05 16:19:17 +01:00
Pascal Vizeli
11f8c97347 Fix discovery update (#795)
* Update discovery.py

* Update discovery.py

* Update discovery.py

* Update discovery.py

* Update discovery.py

* Update discovery.py

* Update discovery.py
2018-11-05 14:59:57 +01:00
Pascal Vizeli
a1461fd518 Update requirements.txt 2018-11-05 13:53:16 +01:00
Pascal Vizeli
fa5c2e37d3 Discovery default config (#793) 2018-11-05 07:45:28 +01:00
luca-simonetti
1f091b20ad fix: use a different convention to handle multiple devices on same card (#767)
* fix: use a different convention to handle multiple devices on same card

* fix: use a different convention to handle multiple devices on same card

* Update alsa.py

* Update alsa.py
2018-11-02 10:47:25 +01:00
Pascal Vizeli
d3b4a03851 Catch exception on watchdog for pretty log (#778)
* Catch exception on watchdog for pretty log

* Update tasks.py
2018-10-29 16:40:19 +01:00
Jorim Tielemans
fb12fee59b Expand add-on installation error message (#783)
* Expand error message

Since an add-on is only available for certain machine and architecture combination we should log both.

* Update addon.py
2018-10-27 15:24:56 +02:00
Pascal Vizeli
7a87d2334a flake8 update to 3.6.0 (#777)
* flake8 update to 3.6.0

* fix lint
2018-10-27 15:23:26 +02:00
Pascal Vizeli
9591e71138 Update auth.py (#771) 2018-10-24 14:02:16 +02:00
Ville Skyttä
cecad526a2 Grammar and spelling fixes (#772) 2018-10-24 14:01:28 +02:00
Pascal Vizeli
53dab4ee45 Bump version 139 2018-10-16 12:52:19 +02:00
Pascal Vizeli
8abbba46c7 Merge pull request #766 from home-assistant/dev
Release 138
2018-10-16 12:51:47 +02:00
Pascal Vizeli
0f01ac1b59 Fix syntax 2018-10-16 12:45:06 +02:00
Pascal Vizeli
aa8ab593c0 Rename login_backend to auth_api (#764)
* Update const.py

* Update validate.py

* Update addon.py

* Update auth.py

* Update addons.py

* Update API.md
2018-10-16 12:33:40 +02:00
Pascal Vizeli
84f791220e Don't clean cache on fake auth (#765)
* Don't clean cache on fake auth

* Update auth.py
2018-10-16 12:30:24 +02:00
Pascal Vizeli
cee2c5469f Bump version 138 2018-10-15 15:25:29 +02:00
Pascal Vizeli
6e75964a8b Merge pull request #761 from home-assistant/dev
Release 137
2018-10-15 15:25:05 +02:00
Pascal Vizeli
5ab5036504 Fix proxy handling with failing connection (#760)
* Fix proxy handling with failing connection

* fix lint

* Fix exception handling

* clenaup error handling

* Fix type error

* Fix event stream

* Fix stream handling

* Fix

* Fix lint

* Handle

* Update proxy.py

* fix lint
2018-10-15 13:01:52 +02:00
Pascal Vizeli
000a3c1f7e Bump to 137 2018-10-12 14:39:47 +02:00
Pascal Vizeli
8ea123eb94 Merge pull request #754 from home-assistant/dev
Release 136
2018-10-12 14:39:18 +02:00
Pascal Vizeli
571c42ef7d Create role for backup add-ons (#755)
* Create role for backup add-ons

* Update validate.py

* Update security.py
2018-10-12 12:48:12 +02:00
Pascal Vizeli
8443da0b9f Add-on SSO support with Home Assistant auth system (#752)
* Create auth.py

* Finish auth cache

* Add documentation

* Add valid schema

* Update auth.py

* Update auth.py

* Update security.py

* Create auth.py

* Update coresys.py

* Update bootstrap.py

* Update const.py

* Update validate.py

* Update const.py

* Update addon.py

* Update auth.py

* Update __init__.py

* Update auth.py

* Update auth.py

* Update auth.py

* Update const.py

* Update auth.py

* Update auth.py

* Update auth.py

* Update validate.py

* Update coresys.py

* Update auth.py

* Update auth.py

* more security

* Update API.md

* Update auth.py

* Update auth.py

* Update auth.py

* Update auth.py

* Update auth.py

* Update homeassistant.py

* Update homeassistant.py
2018-10-12 12:21:48 +02:00
Pascal Vizeli
7dbbcf24c8 Check exists hardware for audio/gpio devices (#753)
* Update hardware.py

* Update addon.py

* Update hardware.py

* Update addon.py
2018-10-12 10:22:58 +02:00
Pascal Vizeli
468cb0c36b Rename info (#750)
* Rename version to info

* fix security
2018-10-10 16:46:34 +02:00
Pascal Vizeli
78e093df96 Bump version 136 2018-10-09 17:10:25 +02:00
25 changed files with 330 additions and 80 deletions

18
API.md
View File

@@ -415,7 +415,7 @@ Proxy to real websocket instance.
### RESTful for API addons ### RESTful for API addons
If a add-on will call itself, you can use `/addons/self/...`. If an add-on will call itself, you can use `/addons/self/...`.
- GET `/addons` - GET `/addons`
@@ -488,6 +488,7 @@ Get all available addons.
"hassio_api": "bool", "hassio_api": "bool",
"hassio_role": "default|homeassistant|manager|admin", "hassio_role": "default|homeassistant|manager|admin",
"homeassistant_api": "bool", "homeassistant_api": "bool",
"auth_api": "bool",
"full_access": "bool", "full_access": "bool",
"protected": "bool", "protected": "bool",
"rating": "1-6", "rating": "1-6",
@@ -663,14 +664,27 @@ return:
### Misc ### Misc
- GET `/version` - GET `/info`
```json ```json
{ {
"supervisor": "version", "supervisor": "version",
"homeassistant": "version", "homeassistant": "version",
"hassos": "null|version", "hassos": "null|version",
"hostname": "name",
"machine": "type", "machine": "type",
"arch": "arch", "arch": "arch",
"channel": "stable|beta|dev" "channel": "stable|beta|dev"
} }
``` ```
### Auth / SSO API
You can use the user system on homeassistant. We handle this auth system on
supervisor.
You can call post `/auth`
We support:
- Json `{ "user|name": "...", "password": "..." }`
- application/x-www-form-urlencoded `user|name=...&password=...`
- BasicAuth

View File

@@ -28,7 +28,7 @@ from ..const import (
ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_DISCOVERY, ATTR_SERVICES, ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_DISCOVERY, ATTR_SERVICES,
ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_FULL_ACCESS, ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_FULL_ACCESS,
ATTR_PROTECTED, ATTR_ACCESS_TOKEN, ATTR_HOST_PID, ATTR_HASSIO_ROLE, ATTR_PROTECTED, ATTR_ACCESS_TOKEN, ATTR_HOST_PID, ATTR_HASSIO_ROLE,
ATTR_MACHINE, ATTR_MACHINE, ATTR_AUTH_API,
SECURITY_PROFILE, SECURITY_DISABLE, SECURITY_DEFAULT) SECURITY_PROFILE, SECURITY_DISABLE, SECURITY_DEFAULT)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..docker.addon import DockerAddon from ..docker.addon import DockerAddon
@@ -411,6 +411,11 @@ class Addon(CoreSysAttributes):
"""Return True if the add-on read access to devicetree.""" """Return True if the add-on read access to devicetree."""
return self._mesh[ATTR_DEVICETREE] return self._mesh[ATTR_DEVICETREE]
@property
def access_auth_api(self):
"""Return True if the add-on access to login/auth backend."""
return self._mesh[ATTR_AUTH_API]
@property @property
def with_audio(self): def with_audio(self):
"""Return True if the add-on access to audio.""" """Return True if the add-on access to audio."""
@@ -669,7 +674,8 @@ class Addon(CoreSysAttributes):
"""Install an add-on.""" """Install an add-on."""
if not self.available: if not self.available:
_LOGGER.error( _LOGGER.error(
"Add-on %s not supported on %s", self._id, self.sys_arch) "Add-on %s not supported on %s with %s architecture",
self._id, self.sys_machine, self.sys_arch)
return False return False
if self.is_installed: if self.is_installed:

View File

@@ -20,12 +20,13 @@ from ..const import (
ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_SERVICES, ATTR_DISCOVERY, ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_SERVICES, ATTR_DISCOVERY,
ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_PROTECTED, ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_PROTECTED,
ATTR_FULL_ACCESS, ATTR_ACCESS_TOKEN, ATTR_HOST_PID, ATTR_HASSIO_ROLE, ATTR_FULL_ACCESS, ATTR_ACCESS_TOKEN, ATTR_HOST_PID, ATTR_HASSIO_ROLE,
ATTR_MACHINE, ATTR_MACHINE, ATTR_AUTH_API,
PRIVILEGED_NET_ADMIN, PRIVILEGED_SYS_ADMIN, PRIVILEGED_SYS_RAWIO, PRIVILEGED_NET_ADMIN, PRIVILEGED_SYS_ADMIN, PRIVILEGED_SYS_RAWIO,
PRIVILEGED_IPC_LOCK, PRIVILEGED_SYS_TIME, PRIVILEGED_SYS_NICE, PRIVILEGED_IPC_LOCK, PRIVILEGED_SYS_TIME, PRIVILEGED_SYS_NICE,
PRIVILEGED_SYS_RESOURCE, PRIVILEGED_SYS_PTRACE, PRIVILEGED_DAC_READ_SEARCH, PRIVILEGED_SYS_RESOURCE, PRIVILEGED_SYS_PTRACE, PRIVILEGED_DAC_READ_SEARCH,
ROLE_DEFAULT, ROLE_HOMEASSISTANT, ROLE_MANAGER, ROLE_ADMIN) ROLE_DEFAULT, ROLE_HOMEASSISTANT, ROLE_MANAGER, ROLE_ADMIN, ROLE_BACKUP)
from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_DEVICE, UUID_MATCH from ..validate import (
NETWORK_PORT, DOCKER_PORTS, ALSA_DEVICE, UUID_MATCH, SHA256)
from ..services.validate import DISCOVERY_SERVICES from ..services.validate import DISCOVERY_SERVICES
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -84,6 +85,7 @@ PRIVILEGED_ALL = [
ROLE_ALL = [ ROLE_ALL = [
ROLE_DEFAULT, ROLE_DEFAULT,
ROLE_HOMEASSISTANT, ROLE_HOMEASSISTANT,
ROLE_BACKUP,
ROLE_MANAGER, ROLE_MANAGER,
ROLE_ADMIN, ROLE_ADMIN,
] ]
@@ -143,6 +145,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
vol.Optional(ATTR_STDIN, default=False): vol.Boolean(), vol.Optional(ATTR_STDIN, default=False): vol.Boolean(),
vol.Optional(ATTR_LEGACY, default=False): vol.Boolean(), vol.Optional(ATTR_LEGACY, default=False): vol.Boolean(),
vol.Optional(ATTR_DOCKER_API, default=False): vol.Boolean(), vol.Optional(ATTR_DOCKER_API, default=False): vol.Boolean(),
vol.Optional(ATTR_AUTH_API, default=False): vol.Boolean(),
vol.Optional(ATTR_SERVICES): [vol.Match(RE_SERVICE)], vol.Optional(ATTR_SERVICES): [vol.Match(RE_SERVICE)],
vol.Optional(ATTR_DISCOVERY): [vol.In(DISCOVERY_SERVICES)], vol.Optional(ATTR_DISCOVERY): [vol.In(DISCOVERY_SERVICES)],
vol.Required(ATTR_OPTIONS): dict, vol.Required(ATTR_OPTIONS): dict,
@@ -187,7 +190,7 @@ SCHEMA_BUILD_CONFIG = vol.Schema({
SCHEMA_ADDON_USER = vol.Schema({ SCHEMA_ADDON_USER = vol.Schema({
vol.Required(ATTR_VERSION): vol.Coerce(str), vol.Required(ATTR_VERSION): vol.Coerce(str),
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): UUID_MATCH, vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): UUID_MATCH,
vol.Optional(ATTR_ACCESS_TOKEN): vol.Match(r"^[0-9a-f]{64}$"), vol.Optional(ATTR_ACCESS_TOKEN): SHA256,
vol.Optional(ATTR_OPTIONS, default=dict): dict, vol.Optional(ATTR_OPTIONS, default=dict): dict,
vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(), vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(),
vol.Optional(ATTR_BOOT): vol.Optional(ATTR_BOOT):

View File

@@ -5,16 +5,17 @@ from pathlib import Path
from aiohttp import web from aiohttp import web
from .addons import APIAddons from .addons import APIAddons
from .auth import APIAuth
from .discovery import APIDiscovery from .discovery import APIDiscovery
from .homeassistant import APIHomeAssistant from .homeassistant import APIHomeAssistant
from .hardware import APIHardware from .hardware import APIHardware
from .host import APIHost from .host import APIHost
from .hassos import APIHassOS from .hassos import APIHassOS
from .info import APIInfo
from .proxy import APIProxy from .proxy import APIProxy
from .supervisor import APISupervisor from .supervisor import APISupervisor
from .snapshots import APISnapshots from .snapshots import APISnapshots
from .services import APIServices from .services import APIServices
from .version import APIVersion
from .security import SecurityMiddleware from .security import SecurityMiddleware
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
@@ -48,7 +49,8 @@ class RestAPI(CoreSysAttributes):
self._register_snapshots() self._register_snapshots()
self._register_discovery() self._register_discovery()
self._register_services() self._register_services()
self._register_version() self._register_info()
self._register_auth()
def _register_host(self): def _register_host(self):
"""Register hostcontrol functions.""" """Register hostcontrol functions."""
@@ -92,13 +94,22 @@ class RestAPI(CoreSysAttributes):
web.get('/hardware/audio', api_hardware.audio), web.get('/hardware/audio', api_hardware.audio),
]) ])
def _register_version(self): def _register_info(self):
"""Register version functions.""" """Register info functions."""
api_version = APIVersion() api_info = APIInfo()
api_version.coresys = self.coresys api_info.coresys = self.coresys
self.webapp.add_routes([ self.webapp.add_routes([
web.get('/version', api_version.info), web.get('/info', api_info.info),
])
def _register_auth(self):
"""Register auth functions."""
api_auth = APIAuth()
api_auth.coresys = self.coresys
self.webapp.add_routes([
web.post('/auth', api_auth.auth),
]) ])
def _register_supervisor(self): def _register_supervisor(self):

View File

@@ -20,7 +20,7 @@ from ..const import (
ATTR_NETWORK_RX, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_ICON, ATTR_SERVICES, ATTR_NETWORK_RX, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_ICON, ATTR_SERVICES,
ATTR_DISCOVERY, ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_DISCOVERY, ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API,
ATTR_FULL_ACCESS, ATTR_PROTECTED, ATTR_RATING, ATTR_HOST_PID, ATTR_FULL_ACCESS, ATTR_PROTECTED, ATTR_RATING, ATTR_HOST_PID,
ATTR_HASSIO_ROLE, ATTR_MACHINE, ATTR_AVAILABLE, ATTR_HASSIO_ROLE, ATTR_MACHINE, ATTR_AVAILABLE, ATTR_AUTH_API,
CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT, REQUEST_FROM) CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT, REQUEST_FROM)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..validate import DOCKER_PORTS, ALSA_DEVICE from ..validate import DOCKER_PORTS, ALSA_DEVICE
@@ -149,6 +149,7 @@ class APIAddons(CoreSysAttributes):
ATTR_STDIN: addon.with_stdin, ATTR_STDIN: addon.with_stdin,
ATTR_HASSIO_API: addon.access_hassio_api, ATTR_HASSIO_API: addon.access_hassio_api,
ATTR_HASSIO_ROLE: addon.hassio_role, ATTR_HASSIO_ROLE: addon.hassio_role,
ATTR_AUTH_API: addon.access_auth_api,
ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api, ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api,
ATTR_GPIO: addon.with_gpio, ATTR_GPIO: addon.with_gpio,
ATTR_DEVICETREE: addon.with_devicetree, ATTR_DEVICETREE: addon.with_devicetree,

61
hassio/api/auth.py Normal file
View File

@@ -0,0 +1,61 @@
"""Init file for Hass.io auth/SSO RESTful API."""
import logging
from aiohttp import BasicAuth
from aiohttp.web_exceptions import HTTPUnauthorized
from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION, WWW_AUTHENTICATE
from .utils import api_process
from ..const import REQUEST_FROM, CONTENT_TYPE_JSON, CONTENT_TYPE_URL
from ..coresys import CoreSysAttributes
from ..exceptions import APIForbidden
_LOGGER = logging.getLogger(__name__)
class APIAuth(CoreSysAttributes):
"""Handle RESTful API for auth functions."""
def _process_basic(self, request, addon):
"""Process login request with basic auth.
Return a coroutine.
"""
auth = BasicAuth.decode(request.headers[AUTHORIZATION])
return self.sys_auth.check_login(addon, auth.login, auth.password)
def _process_dict(self, request, addon, data):
"""Process login with dict data.
Return a coroutine.
"""
username = data.get('username') or data.get('user')
password = data.get('password')
return self.sys_auth.check_login(addon, username, password)
@api_process
async def auth(self, request):
"""Process login request."""
addon = request[REQUEST_FROM]
if not addon.access_auth_api:
raise APIForbidden("Can't use Home Assistant auth!")
# BasicAuth
if AUTHORIZATION in request.headers:
return await self._process_basic(request, addon)
# Json
if request.headers.get(CONTENT_TYPE) == CONTENT_TYPE_JSON:
data = await request.json()
return await self._process_dict(request, addon, data)
# URL encoded
if request.headers.get(CONTENT_TYPE) == CONTENT_TYPE_URL:
data = await request.post()
return await self._process_dict(request, addon, data)
raise HTTPUnauthorized(headers={
WWW_AUTHENTICATE: "Basic realm=\"Hass.io Authentication\""
})

View File

@@ -1,4 +1,4 @@
"""Init file for Hass.io version RESTful API.""" """Init file for Hass.io info RESTful API."""
import logging import logging
from .utils import api_process from .utils import api_process
@@ -10,12 +10,12 @@ from ..coresys import CoreSysAttributes
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class APIVersion(CoreSysAttributes): class APIInfo(CoreSysAttributes):
"""Handle RESTful API for version functions.""" """Handle RESTful API for info functions."""
@api_process @api_process
async def info(self, request): async def info(self, request):
"""Show version info.""" """Show system info."""
return { return {
ATTR_SUPERVISOR: self.sys_supervisor.version, ATTR_SUPERVISOR: self.sys_supervisor.version,
ATTR_HOMEASSISTANT: self.sys_homeassistant.version, ATTR_HOMEASSISTANT: self.sys_homeassistant.version,

View File

@@ -5,14 +5,15 @@ import logging
import aiohttp import aiohttp
from aiohttp import web from aiohttp import web
from aiohttp.web_exceptions import ( from aiohttp.web_exceptions import HTTPBadGateway, HTTPUnauthorized
HTTPBadGateway, HTTPInternalServerError, HTTPUnauthorized) from aiohttp.client_exceptions import ClientConnectorError
from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION
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 from ..exceptions import (
HomeAssistantAuthError, HomeAssistantAPIError, APIError)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -82,19 +83,13 @@ class APIProxy(CoreSysAttributes):
response.content_type = request.headers.get(CONTENT_TYPE) response.content_type = request.headers.get(CONTENT_TYPE)
try: try:
await response.prepare(request) await response.prepare(request)
while True: async for data in client.content:
data = await client.content.read(10)
if not data:
break
await response.write(data) await response.write(data)
except aiohttp.ClientError: except (aiohttp.ClientError, aiohttp.ClientPayloadError):
pass pass
finally: _LOGGER.info("Home Assistant EventStream close")
client.close()
_LOGGER.info("Home Assistant EventStream close")
return response return response
async def api(self, request): async def api(self, request):
@@ -117,7 +112,7 @@ class APIProxy(CoreSysAttributes):
try: try:
client = await self.sys_websession_ssl.ws_connect( client = await self.sys_websession_ssl.ws_connect(
url, heartbeat=60, verify_ssl=False) url, heartbeat=30, verify_ssl=False)
# Handle authentication # Handle authentication
data = await client.receive_json() data = await client.receive_json()
@@ -129,7 +124,7 @@ class APIProxy(CoreSysAttributes):
# Invalid protocol # Invalid protocol
_LOGGER.error( _LOGGER.error(
"Got unexpected response from HA WebSocket: %s", data) "Got unexpected response from HA WebSocket: %s", data)
raise HTTPBadGateway() raise APIError()
if self.sys_homeassistant.refresh_token: if self.sys_homeassistant.refresh_token:
await self.sys_homeassistant.ensure_access_token() await self.sys_homeassistant.ensure_access_token()
@@ -156,19 +151,19 @@ class APIProxy(CoreSysAttributes):
raise HomeAssistantAuthError() raise HomeAssistantAuthError()
except (RuntimeError, ValueError) as err: except (RuntimeError, ValueError, ClientConnectorError) as err:
_LOGGER.error("Client error on WebSocket API %s.", err) _LOGGER.error("Client error on WebSocket API %s.", err)
except HomeAssistantAuthError as err: except HomeAssistantAuthError:
_LOGGER.error("Failed authentication to Home Assistant WebSocket") _LOGGER.error("Failed authentication to Home Assistant WebSocket")
raise HTTPBadGateway() raise APIError()
async def websocket(self, request): async def websocket(self, request):
"""Initialize a WebSocket API connection.""" """Initialize a WebSocket API connection."""
_LOGGER.info("Home Assistant WebSocket API request initialize") _LOGGER.info("Home Assistant WebSocket API request initialize")
# init server # init server
server = web.WebSocketResponse(heartbeat=60) server = web.WebSocketResponse(heartbeat=30)
await server.prepare(request) await server.prepare(request)
# handle authentication # handle authentication
@@ -200,10 +195,13 @@ class APIProxy(CoreSysAttributes):
}) })
except (RuntimeError, ValueError) as err: except (RuntimeError, ValueError) as err:
_LOGGER.error("Can't initialize handshake: %s", err) _LOGGER.error("Can't initialize handshake: %s", err)
raise HTTPInternalServerError() from None return server
# init connection to hass # init connection to hass
client = await self._websocket_client() try:
client = await self._websocket_client()
except APIError:
return server
_LOGGER.info("Home Assistant WebSocket API request running") _LOGGER.info("Home Assistant WebSocket API request running")
try: try:
@@ -238,7 +236,7 @@ class APIProxy(CoreSysAttributes):
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
except RuntimeError as err: except (RuntimeError, ConnectionError, TypeError) as err:
_LOGGER.info("Home Assistant WebSocket API error: %s", err) _LOGGER.info("Home Assistant WebSocket API error: %s", err)
finally: finally:
@@ -248,8 +246,10 @@ class APIProxy(CoreSysAttributes):
server_read.cancel() server_read.cancel()
# close connections # close connections
await client.close() if not client.closed:
await server.close() await client.close()
if not server.closed:
await server.close()
_LOGGER.info("Home Assistant WebSocket API connection is closed") _LOGGER.info("Home Assistant WebSocket API connection is closed")
return server return server

View File

@@ -7,7 +7,7 @@ from aiohttp.web_exceptions import HTTPUnauthorized, HTTPForbidden
from ..const import ( from ..const import (
HEADER_TOKEN, REQUEST_FROM, ROLE_ADMIN, ROLE_DEFAULT, ROLE_HOMEASSISTANT, HEADER_TOKEN, REQUEST_FROM, ROLE_ADMIN, ROLE_DEFAULT, ROLE_HOMEASSISTANT,
ROLE_MANAGER) ROLE_MANAGER, ROLE_BACKUP)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -33,9 +33,10 @@ NO_SECURITY_CHECK = re.compile(
ADDONS_API_BYPASS = re.compile( ADDONS_API_BYPASS = re.compile(
r"^(?:" r"^(?:"
r"|/addons/self/(?!security|update)[^/]+" r"|/addons/self/(?!security|update)[^/]+"
r"|/version" r"|/info"
r"|/services.*" r"|/services.*"
r"|/discovery.*" r"|/discovery.*"
r"|/auth"
r")$" r")$"
) )
@@ -52,6 +53,11 @@ ADDONS_ROLE_ACCESS = {
r"|/homeassistant/.+" r"|/homeassistant/.+"
r")$" r")$"
), ),
ROLE_BACKUP: re.compile(
r"^(?:"
r"|/snapshots.*"
r")$"
),
ROLE_MANAGER: re.compile( ROLE_MANAGER: re.compile(
r"^(?:" r"^(?:"
r"|/homeassistant/.+" r"|/homeassistant/.+"

95
hassio/auth.py Normal file
View File

@@ -0,0 +1,95 @@
"""Manage SSO for Add-ons with Home Assistant user."""
import logging
import hashlib
from .const import (
FILE_HASSIO_AUTH, ATTR_PASSWORD, ATTR_USERNAME, ATTR_ADDON)
from .coresys import CoreSysAttributes
from .utils.json import JsonConfig
from .validate import SCHEMA_AUTH_CONFIG
from .exceptions import AuthError, HomeAssistantAPIError
_LOGGER = logging.getLogger(__name__)
class Auth(JsonConfig, CoreSysAttributes):
"""Manage SSO for Add-ons with Home Assistant user."""
def __init__(self, coresys):
"""Initialize updater."""
super().__init__(FILE_HASSIO_AUTH, SCHEMA_AUTH_CONFIG)
self.coresys = coresys
def _check_cache(self, username, password):
"""Check password in cache."""
username_h = _rehash(username)
password_h = _rehash(password, username)
if self._data.get(username_h) == password_h:
_LOGGER.info("Cache hit for %s", username)
return True
_LOGGER.warning("No cache hit for %s", username)
return False
def _update_cache(self, username, password):
"""Cache a username, password."""
username_h = _rehash(username)
password_h = _rehash(password, username)
if self._data.get(username_h) == password_h:
return
self._data[username_h] = password_h
self.save_data()
def _dismatch_cache(self, username, password):
"""Remove user from cache."""
username_h = _rehash(username)
password_h = _rehash(password, username)
if self._data.get(username_h) != password_h:
return
self._data.pop(username_h, None)
self.save_data()
async def check_login(self, addon, username, password):
"""Check username login."""
if password is None:
_LOGGER.error("None as password is not supported!")
raise AuthError()
_LOGGER.info("Auth request from %s for %s", addon.slug, username)
# Check API state
if not await self.sys_homeassistant.check_api_state():
_LOGGER.info("Home Assistant not running, check cache")
return self._check_cache(username, password)
try:
async with self.sys_homeassistant.make_request(
'post', 'api/hassio_auth', json={
ATTR_USERNAME: username,
ATTR_PASSWORD: password,
ATTR_ADDON: addon.slug,
}) as req:
if req.status == 200:
_LOGGER.info("Success login from %s", username)
self._update_cache(username, password)
return True
_LOGGER.warning("Wrong login from %s", username)
self._dismatch_cache(username, password)
return False
except HomeAssistantAPIError:
_LOGGER.error("Can't request auth on Home Assistant!")
raise AuthError()
def _rehash(value, salt2=""):
"""Rehash a value."""
for idx in range(1, 20):
value = hashlib.sha256(f"{value}{idx}{salt2}".encode()).hexdigest()
return value

View File

@@ -8,6 +8,7 @@ from pathlib import Path
from colorlog import ColoredFormatter from colorlog import ColoredFormatter
from .core import HassIO from .core import HassIO
from .auth import Auth
from .addons import AddonManager from .addons import AddonManager
from .api import RestAPI from .api import RestAPI
from .const import SOCKET_DOCKER from .const import SOCKET_DOCKER
@@ -38,6 +39,7 @@ def initialize_coresys(loop):
# Initialize core objects # Initialize core objects
coresys.core = HassIO(coresys) coresys.core = HassIO(coresys)
coresys.auth = Auth(coresys)
coresys.updater = Updater(coresys) coresys.updater = Updater(coresys)
coresys.api = RestAPI(coresys) coresys.api = RestAPI(coresys)
coresys.supervisor = Supervisor(coresys) coresys.supervisor = Supervisor(coresys)

View File

@@ -2,7 +2,7 @@
from pathlib import Path from pathlib import Path
from ipaddress import ip_network from ipaddress import ip_network
HASSIO_VERSION = '135' HASSIO_VERSION = '139'
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 = \
@@ -16,6 +16,7 @@ URL_HASSOS_OTA = (
HASSIO_DATA = Path("/data") HASSIO_DATA = Path("/data")
FILE_HASSIO_AUTH = Path(HASSIO_DATA, "auth.json")
FILE_HASSIO_ADDONS = Path(HASSIO_DATA, "addons.json") FILE_HASSIO_ADDONS = Path(HASSIO_DATA, "addons.json")
FILE_HASSIO_CONFIG = Path(HASSIO_DATA, "config.json") FILE_HASSIO_CONFIG = Path(HASSIO_DATA, "config.json")
FILE_HASSIO_HOMEASSISTANT = Path(HASSIO_DATA, "homeassistant.json") FILE_HASSIO_HOMEASSISTANT = Path(HASSIO_DATA, "homeassistant.json")
@@ -50,6 +51,7 @@ CONTENT_TYPE_PNG = 'image/png'
CONTENT_TYPE_JSON = 'application/json' 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'
CONTENT_TYPE_URL = 'application/x-www-form-urlencoded'
HEADER_HA_ACCESS = 'x-ha-access' HEADER_HA_ACCESS = 'x-ha-access'
HEADER_TOKEN = 'x-hassio-key' HEADER_TOKEN = 'x-hassio-key'
@@ -184,6 +186,7 @@ ATTR_PROTECTED = 'protected'
ATTR_RATING = 'rating' ATTR_RATING = 'rating'
ATTR_HASSIO_ROLE = 'hassio_role' ATTR_HASSIO_ROLE = 'hassio_role'
ATTR_SUPERVISOR = 'supervisor' ATTR_SUPERVISOR = 'supervisor'
ATTR_AUTH_API = 'auth_api'
SERVICE_MQTT = 'mqtt' SERVICE_MQTT = 'mqtt'
PROVIDE_SERVICE = 'provide' PROVIDE_SERVICE = 'provide'
@@ -253,5 +256,9 @@ FEATURES_SERVICES = 'services'
ROLE_DEFAULT = 'default' ROLE_DEFAULT = 'default'
ROLE_HOMEASSISTANT = 'homeassistant' ROLE_HOMEASSISTANT = 'homeassistant'
ROLE_BACKUP = 'backup'
ROLE_MANAGER = 'manager' ROLE_MANAGER = 'manager'
ROLE_ADMIN = 'admin' ROLE_ADMIN = 'admin'
CHAN_ID = 'chan_id'
CHAN_TYPE = 'chan_type'

View File

@@ -33,6 +33,7 @@ class CoreSys:
# Internal objects pointers # Internal objects pointers
self._core = None self._core = None
self._auth = None
self._homeassistant = None self._homeassistant = None
self._supervisor = None self._supervisor = None
self._addons = None self._addons = None
@@ -122,6 +123,18 @@ class CoreSys:
raise RuntimeError("Hass.io already set!") raise RuntimeError("Hass.io already set!")
self._core = value self._core = value
@property
def auth(self):
"""Return Auth object."""
return self._auth
@auth.setter
def auth(self, value):
"""Set a Auth object."""
if self._auth:
raise RuntimeError("Auth already set!")
self._auth = value
@property @property
def homeassistant(self): def homeassistant(self):
"""Return Home Assistant object.""" """Return Home Assistant object."""

View File

@@ -36,6 +36,7 @@ class Discovery(CoreSysAttributes, JsonConfig):
discovery = Message(**message) discovery = Message(**message)
messages[discovery.uuid] = discovery messages[discovery.uuid] = discovery
_LOGGER.info("Load %d messages", len(messages))
self.message_obj = messages self.message_obj = messages
def save(self): def save(self):
@@ -60,7 +61,7 @@ class Discovery(CoreSysAttributes, JsonConfig):
def send(self, addon, service, config): def send(self, addon, service, config):
"""Send a discovery message to Home Assistant.""" """Send a discovery message to Home Assistant."""
try: try:
DISCOVERY_SERVICES[service](config) config = DISCOVERY_SERVICES[service](config)
except vol.Invalid as err: except vol.Invalid as err:
_LOGGER.error( _LOGGER.error(
"Invalid discovery %s config", humanize_error(config, err)) "Invalid discovery %s config", humanize_error(config, err))
@@ -115,7 +116,7 @@ class Discovery(CoreSysAttributes, JsonConfig):
@attr.s @attr.s
class Message: class Message:
"""Represent a single Discovery message.""" """Represent a single Discovery message."""
uuid = attr.ib(factory=lambda: uuid4().hex, cmp=False, init=False)
addon = attr.ib() addon = attr.ib()
service = attr.ib() service = attr.ib()
config = attr.ib(cmp=False) config = attr.ib(cmp=False)
uuid = attr.ib(factory=lambda: uuid4().hex, cmp=False)

View File

@@ -1,7 +1,6 @@
"""Init file for Hass.io add-on Docker object.""" """Init file for Hass.io add-on Docker object."""
import logging import logging
import os import os
from pathlib import Path
import docker import docker
import requests import requests
@@ -101,7 +100,7 @@ class DockerAddon(DockerInterface):
devices = self.addon.devices or [] devices = self.addon.devices or []
# Use audio devices # Use audio devices
if self.addon.with_audio and AUDIO_DEVICE not in devices: if self.addon.with_audio and self.sys_hardware.support_audio:
devices.append(AUDIO_DEVICE) devices.append(AUDIO_DEVICE)
# Auto mapping UART devices # Auto mapping UART devices
@@ -216,10 +215,8 @@ class DockerAddon(DockerInterface):
# Init other hardware mappings # Init other hardware mappings
# GPIO support # GPIO support
if self.addon.with_gpio: if self.addon.with_gpio and self.sys_hardware.support_gpio:
for gpio_path in ("/sys/class/gpio", "/sys/devices/platform/soc"): for gpio_path in ("/sys/class/gpio", "/sys/devices/platform/soc"):
if not Path(gpio_path).exists():
continue
volumes.update({ volumes.update({
gpio_path: { gpio_path: {
'bind': gpio_path, 'mode': 'rw' 'bind': gpio_path, 'mode': 'rw'

View File

@@ -57,6 +57,13 @@ class HassioUpdaterError(HassioError):
pass pass
# Auth
class AuthError(HassioError):
"""Auth errors."""
pass
# Host # Host
class HostError(HassioError): class HostError(HassioError):

View File

@@ -66,7 +66,7 @@ class HassOS(CoreSysAttributes):
return self._board return self._board
def _check_host(self): def _check_host(self):
"""Check if HassOS is availabe.""" """Check if HassOS is available."""
if not self.available: if not self.available:
_LOGGER.error("No HassOS available") _LOGGER.error("No HassOS available")
raise HassOSNotSupportedError() raise HassOSNotSupportedError()

View File

@@ -191,7 +191,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
@property @property
def hassio_token(self): def hassio_token(self):
"""Return a access token for the Hass.io API.""" """Return an access token for the Hass.io API."""
return self._data.get(ATTR_ACCESS_TOKEN) return self._data.get(ATTR_ACCESS_TOKEN)
@property @property
@@ -260,7 +260,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
_LOGGER.warning("Version %s is already installed", version) _LOGGER.warning("Version %s is already installed", version)
return HomeAssistantUpdateError() return HomeAssistantUpdateError()
# process a update # process an update
async def _update(to_version): async def _update(to_version):
"""Run Home Assistant update.""" """Run Home Assistant update."""
try: try:
@@ -442,9 +442,9 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
async with self.make_request('get', 'api/') as resp: async with self.make_request('get', 'api/') as resp:
if resp.status in (200, 201): if resp.status in (200, 201):
return True return True
err = resp.status status = resp.status
_LOGGER.warning("Home Assistant API config mismatch: %s", status)
_LOGGER.warning("Home Assistant API config mismatch: %d", err)
return False return False
async def _block_till_run(self): async def _block_till_run(self):

View File

@@ -6,7 +6,8 @@ from string import Template
import attr import attr
from ..const import ATTR_INPUT, ATTR_OUTPUT, ATTR_DEVICES, ATTR_NAME from ..const import (
ATTR_INPUT, ATTR_OUTPUT, ATTR_DEVICES, ATTR_NAME, CHAN_ID, CHAN_TYPE)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -58,7 +59,9 @@ class AlsaAudio(CoreSysAttributes):
# Process devices # Process devices
for dev_id, dev_data in self.sys_hardware.audio_devices.items(): for dev_id, dev_data in self.sys_hardware.audio_devices.items():
for chan_id, chan_type in dev_data[ATTR_DEVICES].items(): for chan_info in dev_data[ATTR_DEVICES]:
chan_id = chan_info[CHAN_ID]
chan_type = chan_info[CHAN_TYPE]
alsa_id = f"{dev_id},{chan_id}" alsa_id = f"{dev_id},{chan_id}"
dev_name = dev_data[ATTR_NAME] dev_name = dev_data[ATTR_NAME]

View File

@@ -6,7 +6,7 @@ import re
import pyudev import pyudev
from ..const import ATTR_NAME, ATTR_TYPE, ATTR_DEVICES from ..const import ATTR_NAME, ATTR_TYPE, ATTR_DEVICES, CHAN_ID, CHAN_TYPE
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -20,6 +20,7 @@ PROC_STAT = Path("/proc/stat")
RE_BOOT_TIME = re.compile(r"btime (\d+)") RE_BOOT_TIME = re.compile(r"btime (\d+)")
GPIO_DEVICES = Path("/sys/class/gpio") GPIO_DEVICES = Path("/sys/class/gpio")
SOC_DEVICES = Path("/sys/devices/platform/soc")
RE_TTY = re.compile(r"tty[A-Z]+") RE_TTY = re.compile(r"tty[A-Z]+")
@@ -60,6 +61,11 @@ class Hardware:
return dev_list return dev_list
@property
def support_audio(self):
"""Return True if the system have audio support."""
return bool(self.audio_devices)
@property @property
def audio_devices(self): def audio_devices(self):
"""Return all available audio interfaces.""" """Return all available audio interfaces."""
@@ -68,10 +74,8 @@ class Hardware:
return {} return {}
try: try:
with ASOUND_CARDS.open('r') as cards_file: cards = ASOUND_CARDS.read_text()
cards = cards_file.read() devices = ASOUND_DEVICES.read_text()
with ASOUND_DEVICES.open('r') as devices_file:
devices = devices_file.read()
except OSError as err: except OSError as err:
_LOGGER.error("Can't read asound data: %s", err) _LOGGER.error("Can't read asound data: %s", err)
return {} return {}
@@ -83,20 +87,27 @@ class Hardware:
audio_list[match.group(1)] = { audio_list[match.group(1)] = {
ATTR_NAME: match.group(3), ATTR_NAME: match.group(3),
ATTR_TYPE: match.group(2), ATTR_TYPE: match.group(2),
ATTR_DEVICES: {}, ATTR_DEVICES: [],
} }
# parse devices # parse devices
for match in RE_DEVICES.finditer(devices): for match in RE_DEVICES.finditer(devices):
try: try:
audio_list[match.group(1)][ATTR_DEVICES][match.group(2)] = \ audio_list[match.group(1)][ATTR_DEVICES].append({
match.group(3) CHAN_ID: match.group(2),
CHAN_TYPE: match.group(3)
})
except KeyError: except KeyError:
_LOGGER.warning("Wrong audio device found %s", match.group(0)) _LOGGER.warning("Wrong audio device found %s", match.group(0))
continue continue
return audio_list return audio_list
@property
def support_gpio(self):
"""Return True if device support GPIOs."""
return SOC_DEVICES.exists() and GPIO_DEVICES.exists()
@property @property
def gpio_devices(self): def gpio_devices(self):
"""Return list of GPIO interface on device.""" """Return list of GPIO interface on device."""

View File

@@ -118,7 +118,7 @@ class SnapshotManager(CoreSysAttributes):
async def do_snapshot_full(self, name="", password=None): async def do_snapshot_full(self, name="", password=None):
"""Create a full snapshot.""" """Create a full snapshot."""
if self.lock.locked(): if self.lock.locked():
_LOGGER.error("It is already a snapshot/restore process running") _LOGGER.error("A snapshot/restore process is already running")
return None return None
snapshot = self._create_snapshot(name, SNAPSHOT_FULL, password) snapshot = self._create_snapshot(name, SNAPSHOT_FULL, password)
@@ -153,7 +153,7 @@ class SnapshotManager(CoreSysAttributes):
password=None): password=None):
"""Create a partial snapshot.""" """Create a partial snapshot."""
if self.lock.locked(): if self.lock.locked():
_LOGGER.error("It is already a snapshot/restore process running") _LOGGER.error("A snapshot/restore process is already running")
return None return None
addons = addons or [] addons = addons or []
@@ -201,7 +201,7 @@ class SnapshotManager(CoreSysAttributes):
async def do_restore_full(self, snapshot, password=None): async def do_restore_full(self, snapshot, password=None):
"""Restore a snapshot.""" """Restore a snapshot."""
if self.lock.locked(): if self.lock.locked():
_LOGGER.error("It is already a snapshot/restore process running") _LOGGER.error("A snapshot/restore process is already running")
return False return False
if snapshot.sys_type != SNAPSHOT_FULL: if snapshot.sys_type != SNAPSHOT_FULL:
@@ -274,7 +274,7 @@ class SnapshotManager(CoreSysAttributes):
addons=None, folders=None, password=None): addons=None, folders=None, password=None):
"""Restore a snapshot.""" """Restore a snapshot."""
if self.lock.locked(): if self.lock.locked():
_LOGGER.error("It is already a snapshot/restore process running") _LOGGER.error("A snapshot/restore process is already running")
return False return False
if snapshot.protected and not snapshot.set_password(password): if snapshot.protected and not snapshot.set_password(password):

View File

@@ -3,6 +3,7 @@ import asyncio
import logging import logging
from .coresys import CoreSysAttributes from .coresys import CoreSysAttributes
from .exceptions import HomeAssistantError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -104,7 +105,10 @@ class Tasks(CoreSysAttributes):
return return
_LOGGER.warning("Watchdog found a problem with Home Assistant Docker!") _LOGGER.warning("Watchdog found a problem with Home Assistant Docker!")
await self.sys_homeassistant.start() try:
await self.sys_homeassistant.start()
except HomeAssistantError:
_LOGGER.error("Watchdog Home Assistant reanimation fails!")
async def _watchdog_homeassistant_api(self): async def _watchdog_homeassistant_api(self):
"""Create scheduler task for monitoring running state of API. """Create scheduler task for monitoring running state of API.
@@ -136,6 +140,8 @@ class Tasks(CoreSysAttributes):
_LOGGER.error("Watchdog found a problem with Home Assistant API!") _LOGGER.error("Watchdog found a problem with Home Assistant API!")
try: try:
await self.sys_homeassistant.restart() await self.sys_homeassistant.restart()
except HomeAssistantError:
_LOGGER.error("Watchdog Home Assistant reanimation fails!")
finally: finally:
self._cache[HASS_WATCHDOG_API] = 0 self._cache[HASS_WATCHDOG_API] = 0

View File

@@ -23,6 +23,7 @@ DOCKER_IMAGE = vol.Match(r"^[\w{}]+/[\-\w{}]+$")
ALSA_DEVICE = vol.Maybe(vol.Match(r"\d+,\d+")) ALSA_DEVICE = vol.Maybe(vol.Match(r"\d+,\d+"))
CHANNELS = vol.In([CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV]) CHANNELS = vol.In([CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV])
UUID_MATCH = vol.Match(r"^[0-9a-f]{32}$") UUID_MATCH = vol.Match(r"^[0-9a-f]{32}$")
SHA256 = vol.Match(r"^[0-9a-f]{64}$")
SERVICE_ALL = vol.In([SERVICE_MQTT]) SERVICE_ALL = vol.In([SERVICE_MQTT])
@@ -74,7 +75,7 @@ DOCKER_PORTS = vol.Schema({
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
SCHEMA_HASS_CONFIG = vol.Schema({ SCHEMA_HASS_CONFIG = vol.Schema({
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): UUID_MATCH, vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): UUID_MATCH,
vol.Optional(ATTR_ACCESS_TOKEN): vol.Match(r"^[0-9a-f]{64}$"), vol.Optional(ATTR_ACCESS_TOKEN): SHA256,
vol.Optional(ATTR_BOOT, default=True): vol.Boolean(), vol.Optional(ATTR_BOOT, default=True): vol.Boolean(),
vol.Inclusive(ATTR_IMAGE, 'custom_hass'): DOCKER_IMAGE, vol.Inclusive(ATTR_IMAGE, 'custom_hass'): DOCKER_IMAGE,
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str), vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str),
@@ -120,3 +121,8 @@ SCHEMA_DISCOVERY = vol.Schema([
SCHEMA_DISCOVERY_CONFIG = vol.Schema({ SCHEMA_DISCOVERY_CONFIG = vol.Schema({
vol.Optional(ATTR_DISCOVERY, default=list): schema_or(SCHEMA_DISCOVERY), vol.Optional(ATTR_DISCOVERY, default=list): schema_or(SCHEMA_DISCOVERY),
}, extra=vol.REMOVE_EXTRA) }, extra=vol.REMOVE_EXTRA)
SCHEMA_AUTH_CONFIG = vol.Schema({
SHA256: SHA256
})

View File

@@ -1,11 +1,11 @@
attr==0.3.1 attrs==18.2.0
async_timeout==3.0.0 async_timeout==3.0.1
aiohttp==3.4.0 aiohttp==3.4.4
docker==3.5.0 docker==3.5.0
colorlog==3.1.2 colorlog==3.1.4
voluptuous==0.11.5 voluptuous==0.11.5
gitpython==2.1.10 gitpython==2.1.10
pytz==2018.4 pytz==2018.5
pyudev==0.21.0 pyudev==0.21.0
pycryptodome==3.6.6 pycryptodome==3.6.6
cpe==1.2.1 cpe==1.2.1

View File

@@ -3,7 +3,7 @@ envlist = lint
[testenv] [testenv]
deps = deps =
flake8==3.5.0 flake8==3.6.0
pylint==2.1.1 pylint==2.1.1
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt