mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-28 03:26:32 +00:00
commit
8ea123eb94
15
API.md
15
API.md
@ -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
|
||||||
|
@ -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."""
|
||||||
|
@ -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):
|
||||||
|
@ -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
58
hassio/api/auth.py
Normal 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!")
|
@ -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,
|
@ -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
91
hassio/auth.py
Normal 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
|
@ -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)
|
||||||
|
@ -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'
|
||||||
|
@ -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."""
|
||||||
|
@ -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'
|
||||||
|
@ -57,6 +57,13 @@ class HassioUpdaterError(HassioError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Auth
|
||||||
|
|
||||||
|
class AuthError(HassioError):
|
||||||
|
"""Auth errors."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Host
|
# Host
|
||||||
|
|
||||||
class HostError(HassioError):
|
class HostError(HassioError):
|
||||||
|
@ -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):
|
||||||
|
@ -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."""
|
||||||
|
@ -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
|
||||||
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user