Merge pull request #754 from home-assistant/dev

Release 136
This commit is contained in:
Pascal Vizeli 2018-10-12 14:39:18 +02:00 committed by GitHub
commit 8ea123eb94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 257 additions and 32 deletions

15
API.md
View File

@ -663,14 +663,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_LOGIN_BACKEND,
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 with_login_backend(self):
"""Return True if the add-on access to login/auth backend."""
return self._mesh[ATTR_LOGIN_BACKEND]
@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."""

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_LOGIN_BACKEND,
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_LOGIN_BACKEND, 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):

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

@ -0,0 +1,58 @@
"""Init file for Hass.io auth/SSO RESTful API."""
import logging
from aiohttp import BasicAuth
from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION
from .utils import api_process
from ..const import REQUEST_FROM, CONTENT_TYPE_JSON, CONTENT_TYPE_URL
from ..coresys import CoreSysAttributes
from ..exceptions import APIError, 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.with_login_backend:
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 APIError("Auth method not detected!")

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

@ -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/.+"

91
hassio/auth.py Normal file
View File

@ -0,0 +1,91 @@
"""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):
"""Remove user from cache."""
username_h = _rehash(username)
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)
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 = '136'
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_LOGIN_BACKEND = 'login_backend'
SERVICE_MQTT = 'mqtt' SERVICE_MQTT = 'mqtt'
PROVIDE_SERVICE = 'provide' PROVIDE_SERVICE = 'provide'
@ -253,5 +256,6 @@ 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'

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

@ -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

@ -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

@ -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 {}
@ -97,6 +101,11 @@ class Hardware:
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

@ -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
})