Add support to expose internal services (#339)

* Init services discovery

* extend it

* Add mqtt provider

* Service support

* More protocol stuff

* Update validate.py

* Update validate.py

* Update API.md

* Update API.md

* update api

* add API for services

* fix lint

* add security middleware

* Add discovery layout

* update

* Finish discovery

* improve discovery

* fix

* Update API

* Update api

* fix

* Fix lint

* Update API.md

* Update __init__.py

* Update API.md

* Update interface.py

* Update mqtt.py

* Update discovery.py

* Update const.py

* Update validate.py

* Update validate.py

* Update mqtt.py

* Update mqtt.py

* Update discovery.py

* Update discovery.py

* Update discovery.py

* Update interface.py

* Update mqtt.py

* Update mqtt.py

* Update services.py

* Update discovery.py

* Update discovery.py

* Update mqtt.py

* Update discovery.py

* Update services.py

* Update discovery.py

* Update discovery.py

* Update mqtt.py

* Update discovery.py

* fix aiohttp

* test

* Update const.py

* Update addon.py

* Update homeassistant.py

* Update const.py

* Update addon.py

* Update homeassistant.py

* Update addon.py

* Update security.py

* Update const.py

* Update validate.py

* Update const.py

* Update addon.py

* Update API.md

* Update addons.py

* Update addon.py

* Update validate.py

* Update security.py

* Update security.py

* Update const.py

* Update services.py

* Update discovery.py

* Update API.md

* Update services.py

* Update API.md

* Update services.py

* Update discovery.py

* Update discovery.py

* Update mqtt.py

* Update discovery.py

* Update discovery.py

* Update __init__.py

* Update mqtt.py

* Update security.py

* fix lint

* Update core.py

* Update API.md

* Update services.py
This commit is contained in:
Pascal Vizeli 2018-02-08 17:19:47 +01:00 committed by GitHub
parent b9538bdc67
commit b50756785e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 789 additions and 27 deletions

102
API.md
View File

@ -398,7 +398,9 @@ Get all available addons.
"gpio": "bool", "gpio": "bool",
"audio": "bool", "audio": "bool",
"audio_input": "null|0,0", "audio_input": "null|0,0",
"audio_output": "null|0,0" "audio_output": "null|0,0",
"services": "null|['mqtt']",
"discovery": "null|['component/platform']"
} }
``` ```
@ -462,6 +464,104 @@ Write data to add-on stdin
} }
``` ```
### Service discovery
- GET `/services/discovery`
```json
{
"discovery": [
{
"provider": "name",
"uuid": "uuid",
"component": "component",
"platform": "null|platform",
"config": {}
}
]
}
```
- GET `/services/discovery/{UUID}`
```json
{
"provider": "name",
"uuid": "uuid",
"component": "component",
"platform": "null|platform",
"config": {}
}
```
- POST `/services/discovery`
```json
{
"component": "component",
"platform": "null|platform",
"config": {}
}
```
return:
```json
{
"uuid": "uuid"
}
```
- DEL `/services/discovery/{UUID}`
- GET `/services`
```json
{
"services": [
{
"slug": "name",
"available": "bool",
"provider": "null|name|list"
}
]
}
```
- GET `/services/xy`
```json
{
"available": "bool",
"xy": {}
}
```
#### MQTT
This service perform a auto discovery to Home-Assistant.
- GET `/services/mqtt`
```json
{
"provider": "name",
"host": "xy",
"port": "8883",
"ssl": "bool",
"username": "optional",
"password": "optional",
"protocol": "3.1.1"
}
```
- POST `/services/mqtt`
```json
{
"host": "xy",
"port": "8883",
"ssl": "bool|optional",
"username": "optional",
"password": "optional",
"protocol": "3.1.1"
}
```
- DEL `/services/mqtt`
## Host Control ## Host Control
Communicate over UNIX socket with a host daemon. Communicate over UNIX socket with a host daemon.

View File

@ -4,7 +4,7 @@ import logging
from .addon import Addon from .addon import Addon
from .repository import Repository from .repository import Repository
from .data import Data from .data import AddonsData
from ..const import REPOSITORY_CORE, REPOSITORY_LOCAL, BOOT_AUTO from ..const import REPOSITORY_CORE, REPOSITORY_LOCAL, BOOT_AUTO
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
@ -19,7 +19,7 @@ class AddonManager(CoreSysAttributes):
def __init__(self, coresys): def __init__(self, coresys):
"""Initialize docker base wrapper.""" """Initialize docker base wrapper."""
self.coresys = coresys self.coresys = coresys
self.data = Data(coresys) self.data = AddonsData(coresys)
self.addons_obj = {} self.addons_obj = {}
self.repositories_obj = {} self.repositories_obj = {}

View File

@ -12,7 +12,7 @@ import voluptuous as vol
from voluptuous.humanize import humanize_error from voluptuous.humanize import humanize_error
from .validate import ( from .validate import (
validate_options, SCHEMA_ADDON_SNAPSHOT, RE_VOLUME) validate_options, SCHEMA_ADDON_SNAPSHOT, RE_VOLUME, RE_SERVICE)
from .utils import check_installed from .utils import check_installed
from ..const import ( from ..const import (
ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_BOOT, ATTR_MAP, ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_BOOT, ATTR_MAP,
@ -23,7 +23,7 @@ from ..const import (
ATTR_STATE, ATTR_TIMEOUT, ATTR_AUTO_UPDATE, ATTR_NETWORK, ATTR_WEBUI, ATTR_STATE, ATTR_TIMEOUT, ATTR_AUTO_UPDATE, ATTR_NETWORK, ATTR_WEBUI,
ATTR_HASSIO_API, ATTR_AUDIO, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT, ATTR_HASSIO_API, ATTR_AUDIO, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT,
ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY, ATTR_HOST_IPC, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY, ATTR_HOST_IPC,
ATTR_HOST_DBUS, ATTR_AUTO_UART) ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_DISCOVERY, ATTR_SERVICES)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..docker.addon import DockerAddon from ..docker.addon import DockerAddon
from ..utils.json import write_json_file, read_json_file from ..utils.json import write_json_file, read_json_file
@ -201,6 +201,26 @@ class Addon(CoreSysAttributes):
"""Return startup type of addon.""" """Return startup type of addon."""
return self._mesh.get(ATTR_STARTUP) return self._mesh.get(ATTR_STARTUP)
@property
def services(self):
"""Return dict of services with rights."""
raw_services = self._mesh.get(ATTR_SERVICES)
if not raw_services:
return None
formated_services = {}
for data in raw_services:
service = RE_SERVICE.match(data)
formated_services[service.group('service')] = \
service.group('rights') or 'ro'
return formated_services
@property
def discovery(self):
"""Return list of discoverable components/platforms."""
return self._mesh.get(ATTR_DISCOVERY)
@property @property
def ports(self): def ports(self):
"""Return ports of addon.""" """Return ports of addon."""

View File

@ -9,7 +9,7 @@ from voluptuous.humanize import humanize_error
from .utils import extract_hash_from_path from .utils import extract_hash_from_path
from .validate import ( from .validate import (
SCHEMA_ADDON_CONFIG, SCHEMA_ADDON_FILE, SCHEMA_REPOSITORY_CONFIG) SCHEMA_ADDON_CONFIG, SCHEMA_ADDONS_FILE, SCHEMA_REPOSITORY_CONFIG)
from ..const import ( from ..const import (
FILE_HASSIO_ADDONS, ATTR_VERSION, ATTR_SLUG, ATTR_REPOSITORY, ATTR_LOCATON, FILE_HASSIO_ADDONS, ATTR_VERSION, ATTR_SLUG, ATTR_REPOSITORY, ATTR_LOCATON,
REPOSITORY_CORE, REPOSITORY_LOCAL, ATTR_USER, ATTR_SYSTEM) REPOSITORY_CORE, REPOSITORY_LOCAL, ATTR_USER, ATTR_SYSTEM)
@ -19,12 +19,12 @@ from ..utils.json import JsonConfig, read_json_file
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class Data(JsonConfig, CoreSysAttributes): class AddonsData(JsonConfig, CoreSysAttributes):
"""Hold data for addons inside HassIO.""" """Hold data for addons inside HassIO."""
def __init__(self, coresys): def __init__(self, coresys):
"""Initialize data holder.""" """Initialize data holder."""
super().__init__(FILE_HASSIO_ADDONS, SCHEMA_ADDON_FILE) super().__init__(FILE_HASSIO_ADDONS, SCHEMA_ADDONS_FILE)
self.coresys = coresys self.coresys = coresys
self._repositories = {} self._repositories = {}
self._cache = {} self._cache = {}

View File

@ -17,13 +17,15 @@ from ..const import (
ATTR_AUTO_UPDATE, ATTR_WEBUI, ATTR_AUDIO, ATTR_AUDIO_INPUT, ATTR_HOST_IPC, ATTR_AUTO_UPDATE, ATTR_WEBUI, ATTR_AUDIO, ATTR_AUDIO_INPUT, ATTR_HOST_IPC,
ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API, ATTR_BUILD_FROM, ATTR_SQUASH, ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API, ATTR_BUILD_FROM, ATTR_SQUASH,
ATTR_ARGS, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY, ATTR_ARGS, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY,
ATTR_HOST_DBUS, ATTR_AUTO_UART) ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_SERVICES, ATTR_DISCOVERY)
from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_CHANNEL from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_CHANNEL
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
RE_VOLUME = re.compile(r"^(config|ssl|addons|backup|share)(?::(rw|:ro))?$") RE_VOLUME = re.compile(r"^(config|ssl|addons|backup|share)(?::(rw|:ro))?$")
RE_SERVICE = re.compile(r"^(?P<service>mqtt)(?::(?P<rights>rw|:ro))?$")
RE_DISCOVERY = re.compile(r"^(?P<component>\w*)(?:/(?P<platform>\w*>))?$")
V_STR = 'str' V_STR = 'str'
V_INT = 'int' V_INT = 'int'
@ -110,6 +112,8 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
vol.Optional(ATTR_HOMEASSISTANT_API, default=False): vol.Boolean(), vol.Optional(ATTR_HOMEASSISTANT_API, default=False): vol.Boolean(),
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_SERVICES): [vol.Match(RE_SERVICE)],
vol.Optional(ATTR_DISCOVERY): [vol.Match(RE_DISCOVERY)],
vol.Required(ATTR_OPTIONS): dict, vol.Required(ATTR_OPTIONS): dict,
vol.Required(ATTR_SCHEMA): vol.Any(vol.Schema({ vol.Required(ATTR_SCHEMA): vol.Any(vol.Schema({
vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [ vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [
@ -168,7 +172,7 @@ SCHEMA_ADDON_SYSTEM = SCHEMA_ADDON_CONFIG.extend({
}) })
SCHEMA_ADDON_FILE = vol.Schema({ SCHEMA_ADDONS_FILE = vol.Schema({
vol.Optional(ATTR_USER, default=dict): { vol.Optional(ATTR_USER, default=dict): {
vol.Coerce(str): SCHEMA_ADDON_USER, vol.Coerce(str): SCHEMA_ADDON_USER,
}, },

View File

@ -5,12 +5,15 @@ from pathlib import Path
from aiohttp import web from aiohttp import web
from .addons import APIAddons from .addons import APIAddons
from .discovery import APIDiscovery
from .homeassistant import APIHomeAssistant from .homeassistant import APIHomeAssistant
from .host import APIHost from .host import APIHost
from .network import APINetwork from .network import APINetwork
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 .security import security_layer
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -22,12 +25,16 @@ class RestAPI(CoreSysAttributes):
def __init__(self, coresys): def __init__(self, coresys):
"""Initialize docker base wrapper.""" """Initialize docker base wrapper."""
self.coresys = coresys self.coresys = coresys
self.webapp = web.Application(loop=self._loop) self.webapp = web.Application(
middlewares=[security_layer], loop=self._loop)
# service stuff # service stuff
self._handler = None self._handler = None
self.server = None self.server = None
# middleware
self.webapp['coresys'] = coresys
async def load(self): async def load(self):
"""Register REST API Calls.""" """Register REST API Calls."""
self._register_supervisor() self._register_supervisor()
@ -38,6 +45,8 @@ class RestAPI(CoreSysAttributes):
self._register_addons() self._register_addons()
self._register_snapshots() self._register_snapshots()
self._register_network() self._register_network()
self._register_discovery()
self._register_services()
def _register_host(self): def _register_host(self):
"""Register hostcontrol function.""" """Register hostcontrol function."""
@ -162,6 +171,32 @@ class RestAPI(CoreSysAttributes):
'/snapshots/{snapshot}/restore/partial', '/snapshots/{snapshot}/restore/partial',
api_snapshots.restore_partial) api_snapshots.restore_partial)
def _register_services(self):
api_services = APIServices()
api_services.coresys = self.coresys
self.webapp.router.add_get('/services', api_services.list)
self.webapp.router.add_get(
'/services/{service}', api_services.get_service)
self.webapp.router.add_post(
'/services/{service}', api_services.set_service)
self.webapp.router.add_delete(
'/services/{service}', api_services.del_service)
def _register_discovery(self):
api_discovery = APIDiscovery()
api_discovery.coresys = self.coresys
self.webapp.router.add_get(
'/services/discovery', api_discovery.list)
self.webapp.router.add_get(
'/services/discovery/{uuid}', api_discovery.get_discovery)
self.webapp.router.add_delete(
'/services/discovery/{uuid}', api_discovery.del_discovery)
self.webapp.router.add_post(
'/services/discovery', api_discovery.set_discovery)
def _register_panel(self): def _register_panel(self):
"""Register panel for homeassistant.""" """Register panel for homeassistant."""
def create_panel_response(build_type): def create_panel_response(build_type):

View File

@ -16,7 +16,8 @@ from ..const import (
ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, BOOT_AUTO, BOOT_MANUAL, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, BOOT_AUTO, BOOT_MANUAL,
ATTR_CHANGELOG, ATTR_HOST_IPC, ATTR_HOST_DBUS, ATTR_LONG_DESCRIPTION, ATTR_CHANGELOG, ATTR_HOST_IPC, ATTR_HOST_DBUS, ATTR_LONG_DESCRIPTION,
ATTR_CPU_PERCENT, ATTR_MEMORY_LIMIT, ATTR_MEMORY_USAGE, ATTR_NETWORK_TX, ATTR_CPU_PERCENT, ATTR_MEMORY_LIMIT, ATTR_MEMORY_USAGE, ATTR_NETWORK_TX,
ATTR_NETWORK_RX, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_ICON, ATTR_NETWORK_RX, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_ICON, ATTR_SERVICES,
ATTR_DISCOVERY,
CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT) CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..validate import DOCKER_PORTS from ..validate import DOCKER_PORTS
@ -134,6 +135,8 @@ class APIAddons(CoreSysAttributes):
ATTR_AUDIO: addon.with_audio, ATTR_AUDIO: addon.with_audio,
ATTR_AUDIO_INPUT: addon.audio_input, ATTR_AUDIO_INPUT: addon.audio_input,
ATTR_AUDIO_OUTPUT: addon.audio_output, ATTR_AUDIO_OUTPUT: addon.audio_output,
ATTR_SERVICES: addon.services,
ATTR_DISCOVERY: addon.discovery,
} }
@api_process @api_process

72
hassio/api/discovery.py Normal file
View File

@ -0,0 +1,72 @@
"""Init file for HassIO network rest api."""
import voluptuous as vol
from .utils import api_process, api_validate
from ..const import (
ATTR_PROVIDER, ATTR_UUID, ATTR_COMPONENT, ATTR_PLATFORM, ATTR_CONFIG,
ATTR_DISCOVERY, REQUEST_FROM)
from ..coresys import CoreSysAttributes
SCHEMA_DISCOVERY = vol.Schema({
vol.Required(ATTR_COMPONENT): vol.Coerce(str),
vol.Optional(ATTR_PLATFORM): vol.Any(None, vol.Coerce(str)),
vol.Optional(ATTR_CONFIG): vol.Any(None, dict),
})
class APIDiscovery(CoreSysAttributes):
"""Handle rest api for discovery functions."""
def _extract_message(self, request):
"""Extract discovery message from URL."""
message = self._services.discovery.get(request.match_info.get('uuid'))
if not message:
raise RuntimeError("Discovery message not found")
return message
@api_process
async def list(self, request):
"""Show register services."""
discovery = []
for message in self._services.discovery.list_messages:
discovery.append({
ATTR_PROVIDER: message.provider,
ATTR_UUID: message.uuid,
ATTR_COMPONENT: message.component,
ATTR_PLATFORM: message.platform,
ATTR_CONFIG: message.config,
})
return {ATTR_DISCOVERY: discovery}
@api_process
async def set_discovery(self, request):
"""Write data into a discovery pipeline."""
body = await api_validate(SCHEMA_DISCOVERY, request)
message = self._services.discovery.send(
provider=request[REQUEST_FROM], **body)
return {ATTR_UUID: message.uuid}
@api_process
async def get_discovery(self, request):
"""Read data into a discovery message."""
message = self._extract_message(request)
return {
ATTR_PROVIDER: message.provider,
ATTR_UUID: message.uuid,
ATTR_COMPONENT: message.component,
ATTR_PLATFORM: message.platform,
ATTR_CONFIG: message.config,
}
@api_process
async def del_discovery(self, request):
"""Delete data into a discovery message."""
message = self._extract_message(request)
self._services.discovery.remove(message)
return True

34
hassio/api/security.py Normal file
View File

@ -0,0 +1,34 @@
"""Handle security part of this API."""
import logging
from aiohttp.web import middleware
from ..const import HEADER_TOKEN, REQUEST_FROM
_LOGGER = logging.getLogger(__name__)
@middleware
async def security_layer(request, handler):
"""Check security access of this layer."""
coresys = request.app['coresys']
hassio_token = request.headers.get(HEADER_TOKEN)
# Need to be removed later
if not hassio_token:
_LOGGER.warning("No valid hassio token for API access!")
request[REQUEST_FROM] = 'UNKNOWN'
# From Home-Assistant
elif hassio_token == coresys.homeassistant.uuid:
request[REQUEST_FROM] = 'homeassistant'
# From Add-on
else:
for addon in coresys.addons.list_addons:
if hassio_token != addon.uuid:
continue
request[REQUEST_FROM] = addon.slug
break
return await handler(request)

55
hassio/api/services.py Normal file
View File

@ -0,0 +1,55 @@
"""Init file for HassIO network rest api."""
from .utils import api_process, api_validate
from ..const import (
ATTR_AVAILABLE, ATTR_PROVIDER, ATTR_SLUG, ATTR_SERVICES, REQUEST_FROM)
from ..coresys import CoreSysAttributes
class APIServices(CoreSysAttributes):
"""Handle rest api for services functions."""
def _extract_service(self, request):
"""Return service and if not exists trow a exception."""
service = self._services.get(request.match_info.get('service'))
if not service:
raise RuntimeError("Service not exists")
return service
@api_process
async def list(self, request):
"""Show register services."""
services = []
for service in self._services.list_services:
services.append({
ATTR_SLUG: service.slug,
ATTR_AVAILABLE: service.enabled,
ATTR_PROVIDER: service.provider,
})
return {ATTR_SERVICES: services}
@api_process
async def set_service(self, request):
"""Write data into a service."""
service = self._extract_service(request)
body = await api_validate(service.schema, request)
return service.set_service_data(request[REQUEST_FROM], body)
@api_process
async def get_service(self, request):
"""Read data into a service."""
service = self._extract_service(request)
return {
ATTR_AVAILABLE: service.enabled,
service.slug: service.get_service_data(),
}
@api_process
async def del_service(self, request):
"""Delete data into a service."""
service = self._extract_service(request)
return service.del_service_data(request[REQUEST_FROM])

View File

@ -13,9 +13,10 @@ from .const import SOCKET_DOCKER
from .coresys import CoreSys from .coresys import CoreSys
from .supervisor import Supervisor from .supervisor import Supervisor
from .homeassistant import HomeAssistant from .homeassistant import HomeAssistant
from .snapshots import SnapshotsManager from .snapshots import SnapshotManager
from .tasks import Tasks from .tasks import Tasks
from .updater import Updater from .updater import Updater
from .services import ServiceManager
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -30,8 +31,9 @@ def initialize_coresys(loop):
coresys.supervisor = Supervisor(coresys) coresys.supervisor = Supervisor(coresys)
coresys.homeassistant = HomeAssistant(coresys) coresys.homeassistant = HomeAssistant(coresys)
coresys.addons = AddonManager(coresys) coresys.addons = AddonManager(coresys)
coresys.snapshots = SnapshotsManager(coresys) coresys.snapshots = SnapshotManager(coresys)
coresys.tasks = Tasks(coresys) coresys.tasks = Tasks(coresys)
coresys.services = ServiceManager(coresys)
# bootstrap config # bootstrap config
initialize_system_data(coresys) initialize_system_data(coresys)

View File

@ -15,6 +15,7 @@ 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")
FILE_HASSIO_UPDATER = Path(HASSIO_DATA, "updater.json") FILE_HASSIO_UPDATER = Path(HASSIO_DATA, "updater.json")
FILE_HASSIO_SERVICES = Path(HASSIO_DATA, "services.json")
SOCKET_DOCKER = Path("/var/run/docker.sock") SOCKET_DOCKER = Path("/var/run/docker.sock")
SOCKET_HC = Path("/var/run/hassio-hc.sock") SOCKET_HC = Path("/var/run/hassio-hc.sock")
@ -43,6 +44,12 @@ 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'
HEADER_HA_ACCESS = 'x-ha-access' HEADER_HA_ACCESS = 'x-ha-access'
HEADER_TOKEN = 'X-HASSIO-KEY'
ENV_TOKEN = 'HASSIO_TOKEN'
ENV_TIME = 'TZ'
REQUEST_FROM = 'HASSIO_FROM'
ATTR_WAIT_BOOT = 'wait_boot' ATTR_WAIT_BOOT = 'wait_boot'
ATTR_WATCHDOG = 'watchdog' ATTR_WATCHDOG = 'watchdog'
@ -136,6 +143,20 @@ ATTR_MEMORY_LIMIT = 'memory_limit'
ATTR_MEMORY_USAGE = 'memory_usage' ATTR_MEMORY_USAGE = 'memory_usage'
ATTR_BLK_READ = 'blk_read' ATTR_BLK_READ = 'blk_read'
ATTR_BLK_WRITE = 'blk_write' ATTR_BLK_WRITE = 'blk_write'
ATTR_PROVIDER = 'provider'
ATTR_AVAILABLE = 'available'
ATTR_HOST = 'host'
ATTR_USERNAME = 'username'
ATTR_PROTOCOL = 'protocol'
ATTR_DISCOVERY = 'discovery'
ATTR_PLATFORM = 'platform'
ATTR_COMPONENT = 'component'
ATTR_CONFIG = 'config'
ATTR_DISCOVERY_ID = 'discovery_id'
ATTR_SERVICES = 'services'
ATTR_DISCOVERY = 'discovery'
SERVICE_MQTT = 'mqtt'
STARTUP_INITIALIZE = 'initialize' STARTUP_INITIALIZE = 'initialize'
STARTUP_SYSTEM = 'system' STARTUP_SYSTEM = 'system'

View File

@ -44,6 +44,9 @@ class HassIO(CoreSysAttributes):
# load last available data # load last available data
await self._snapshots.load() await self._snapshots.load()
# load services
await self._services.load()
# start dns forwarding # start dns forwarding
self._loop.create_task(self._dns.start()) self._loop.create_task(self._dns.start())
@ -70,6 +73,9 @@ class HassIO(CoreSysAttributes):
_LOGGER.info("Hass.io reboot detected") _LOGGER.info("Hass.io reboot detected")
return return
# reset register services / discovery
self._services.reset()
# start addon mark as system # start addon mark as system
await self._addons.auto_boot(STARTUP_SYSTEM) await self._addons.auto_boot(STARTUP_SYSTEM)
@ -85,6 +91,7 @@ class HassIO(CoreSysAttributes):
# store new last boot # store new last boot
self._config.last_boot = self._hardware.last_boot self._config.last_boot = self._hardware.last_boot
self._config.save_data()
finally: finally:
# Add core tasks into scheduler # Add core tasks into scheduler

View File

@ -40,6 +40,7 @@ class CoreSys(object):
self._updater = None self._updater = None
self._snapshots = None self._snapshots = None
self._tasks = None self._tasks = None
self._services = None
@property @property
def arch(self): def arch(self):
@ -155,19 +156,19 @@ class CoreSys(object):
@property @property
def snapshots(self): def snapshots(self):
"""Return SnapshotsManager object.""" """Return SnapshotManager object."""
return self._snapshots return self._snapshots
@snapshots.setter @snapshots.setter
def snapshots(self, value): def snapshots(self, value):
"""Set a SnapshotsManager object.""" """Set a SnapshotManager object."""
if self._snapshots: if self._snapshots:
raise RuntimeError("SnapshotsManager already set!") raise RuntimeError("SnapshotsManager already set!")
self._snapshots = value self._snapshots = value
@property @property
def tasks(self): def tasks(self):
"""Return SnapshotsManager object.""" """Return Tasks object."""
return self._tasks return self._tasks
@tasks.setter @tasks.setter
@ -177,6 +178,18 @@ class CoreSys(object):
raise RuntimeError("Tasks already set!") raise RuntimeError("Tasks already set!")
self._tasks = value self._tasks = value
@property
def services(self):
"""Return ServiceManager object."""
return self._services
@services.setter
def services(self, value):
"""Set a ServiceManager object."""
if self._services:
raise RuntimeError("Services already set!")
self._services = value
class CoreSysAttributes(object): class CoreSysAttributes(object):
"""Inheret basic CoreSysAttributes.""" """Inheret basic CoreSysAttributes."""

View File

@ -9,7 +9,8 @@ from .interface import DockerInterface
from .utils import docker_process from .utils import docker_process
from ..addons.build import AddonBuild from ..addons.build import AddonBuild
from ..const import ( from ..const import (
MAP_CONFIG, MAP_SSL, MAP_ADDONS, MAP_BACKUP, MAP_SHARE) MAP_CONFIG, MAP_SSL, MAP_ADDONS, MAP_BACKUP, MAP_SHARE, ENV_TOKEN,
ENV_TIME)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -74,19 +75,18 @@ class DockerAddon(DockerInterface):
def environment(self): def environment(self):
"""Return environment for docker add-on.""" """Return environment for docker add-on."""
addon_env = self.addon.environment or {} addon_env = self.addon.environment or {}
# Need audio settings
if self.addon.with_audio: if self.addon.with_audio:
addon_env.update({ addon_env.update({
'ALSA_OUTPUT': self.addon.audio_output, 'ALSA_OUTPUT': self.addon.audio_output,
'ALSA_INPUT': self.addon.audio_input, 'ALSA_INPUT': self.addon.audio_input,
}) })
# Set api token if any API access is needed
if self.addon.access_hassio_api or self.addon.access_homeassistant_api:
addon_env['HASSIO_TOKEN'] = self.addon.uuid
return { return {
**addon_env, **addon_env,
'TZ': self._config.timezone, ENV_TIME: self._config.timezone,
ENV_TOKEN: self.addon.uuid,
} }
@property @property

View File

@ -4,6 +4,7 @@ import logging
import docker import docker
from .interface import DockerInterface from .interface import DockerInterface
from ..const import ENV_TOKEN, ENV_TIME
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -53,8 +54,8 @@ class DockerHomeAssistant(DockerInterface):
network_mode='host', network_mode='host',
environment={ environment={
'HASSIO': self._docker.network.supervisor, 'HASSIO': self._docker.network.supervisor,
'TZ': self._config.timezone, ENV_TIME: self._config.timezone,
'HASSIO_TOKEN': self._homeassistant.uuid, ENV_TOKEN: self._homeassistant.uuid,
}, },
volumes={ volumes={
str(self._config.path_extern_config): str(self._config.path_extern_config):

View File

@ -285,3 +285,28 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
if status not in (200, 201): if status not in (200, 201):
_LOGGER.warning("Home-Assistant API config missmatch") _LOGGER.warning("Home-Assistant API config missmatch")
return True return True
async def send_event(self, event_type, event_data=None):
"""Send event to Home-Assistant."""
url = f"{self.api_url}/api/events/{event_type}"
header = {CONTENT_TYPE: CONTENT_TYPE_JSON}
if self.api_password:
header.update({HEADER_HA_ACCESS: self.api_password})
try:
# pylint: disable=bad-continuation
async with self._websession_ssl.post(
url, headers=header, timeout=30,
json=event_data) as request:
status = request.status
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
_LOGGER.warning(
"Home-Assistant event %s fails: %s", event_type, err)
return False
if status not in (200, 201):
_LOGGER.warning("Home-Assistant event %s fails", event_type)
return False
return True

View File

@ -0,0 +1,45 @@
"""Handle internal services discovery."""
from .mqtt import MQTTService
from .data import ServicesData
from .discovery import Discovery
from ..const import SERVICE_MQTT
from ..coresys import CoreSysAttributes
AVAILABLE_SERVICES = {
SERVICE_MQTT: MQTTService
}
class ServiceManager(CoreSysAttributes):
"""Handle internal services discovery."""
def __init__(self, coresys):
"""Initialize Services handler."""
self.coresys = coresys
self.data = ServicesData()
self.discovery = Discovery(coresys)
self.services_obj = {}
@property
def list_services(self):
"""Return a list of services."""
return list(self.services_obj.values())
def get(self, slug):
"""Return service object from slug."""
return self.services_obj.get(slug)
async def load(self):
"""Load available services."""
for slug, service in AVAILABLE_SERVICES.items():
self.services_obj[slug] = service(self.coresys)
# Read exists discovery messages
self.discovery.load()
def reset(self):
"""Reset available data."""
self.data.reset_data()
self.discovery.load()

23
hassio/services/data.py Normal file
View File

@ -0,0 +1,23 @@
"""Handle service data for persistent supervisor reboot."""
from .validate import SCHEMA_SERVICES_FILE
from ..const import FILE_HASSIO_SERVICES, ATTR_DISCOVERY, SERVICE_MQTT
from ..utils.json import JsonConfig
class ServicesData(JsonConfig):
"""Class to handle services data."""
def __init__(self):
"""Initialize services data."""
super().__init__(FILE_HASSIO_SERVICES, SCHEMA_SERVICES_FILE)
@property
def discovery(self):
"""Return discovery data for home-assistant."""
return self._data[ATTR_DISCOVERY]
@property
def mqtt(self):
"""Return settings for mqtt service."""
return self._data[SERVICE_MQTT]

View File

@ -0,0 +1,107 @@
"""Handle discover message for Home-Assistant."""
import logging
from uuid import uuid4
from ..const import ATTR_UUID
from ..coresys import CoreSysAttributes
_LOGGER = logging.getLogger(__name__)
EVENT_DISCOVERY_ADD = 'hassio_discovery_add'
EVENT_DISCOVERY_DEL = 'hassio_discovery_del'
class Discovery(CoreSysAttributes):
"""Home-Assistant Discovery handler."""
def __init__(self, coresys):
"""Initialize discovery handler."""
self.coresys = coresys
self.message_obj = {}
def load(self):
"""Load exists discovery message into storage."""
messages = {}
for message in self._data:
discovery = Message(**message)
messages[discovery.uuid] = discovery
self.message_obj = messages
def save(self):
"""Write discovery message into data file."""
messages = []
for message in self.message_obj.values():
messages.append(message.raw())
self._data.clear()
self._data.extend(messages)
self._services.data.save_data()
def get(self, uuid):
"""Return discovery message."""
return self.message_obj.get(uuid)
@property
def _data(self):
"""Return discovery data."""
return self._services.data.discovery
@property
def list_messages(self):
"""Return list of available discovery messages."""
return self.message_obj.values()
def send(self, provider, component, platform=None, config=None):
"""Send a discovery message to Home-Assistant."""
message = Message(provider, component, platform, config)
# Allready exists?
for exists_message in self.message_obj:
if exists_message == message:
_LOGGER.warning("Found douplicate discovery message from %s",
provider)
return exists_message
_LOGGER.info("Send discovery to Home-Assistant %s/%s from %s",
component, platform, provider)
self.message_obj[message.uuid] = message
self.save()
# send event to Home-Assistant
self._loop.create_task(self._homeassistant.send_event(
EVENT_DISCOVERY_ADD, {ATTR_UUID: message.uuid}))
return message
def remove(self, message):
"""Remove a discovery message from Home-Assistant."""
self.message_obj.pop(message.uuid, None)
self.save()
# send event to Home-Assistant
self._loop.create_task(self._homeassistant.send_event(
EVENT_DISCOVERY_DEL, {ATTR_UUID: message.uuid}))
class Message(object):
"""Represent a single Discovery message."""
def __init__(self, provider, component, platform, config, uuid=None):
"""Initialize discovery message."""
self.provider = provider
self.component = component
self.platform = platform
self.config = config
self.uuid = uuid or uuid4().hex
def raw(self):
"""Return raw discovery message."""
return self.__dict__
def __eq__(self, other):
"""Compare with other message."""
for attribute in ('provider', 'component', 'platform', 'config'):
if getattr(self, attribute) != getattr(other, attribute):
return False
return True

View File

@ -0,0 +1,54 @@
"""Interface for single service."""
from ..coresys import CoreSysAttributes
class ServiceInterface(CoreSysAttributes):
"""Interface class for service integration."""
def __init__(self, coresys):
"""Initialize service interface."""
self.coresys = coresys
@property
def slug(self):
"""Return slug of this service."""
return None
@property
def _data(self):
"""Return data of this service."""
return None
@property
def schema(self):
"""Return data schema of this service."""
return None
@property
def provider(self):
"""Return name of service provider."""
return None
@property
def enabled(self):
"""Return True if the service is in use."""
return bool(self._data)
def save(self):
"""Save changes."""
self._services.data.save_data()
def get_service_data(self):
"""Return the requested service data."""
if self.enabled:
return self._data
return None
def set_service_data(self, provider, data):
"""Write the data into service object."""
raise NotImplementedError()
def del_service_data(self, provider):
"""Remove the data from service object."""
raise NotImplementedError()

89
hassio/services/mqtt.py Normal file
View File

@ -0,0 +1,89 @@
"""Provide MQTT Service."""
import logging
from .interface import ServiceInterface
from .validate import SCHEMA_SERVICE_MQTT
from ..const import (
ATTR_PROVIDER, SERVICE_MQTT, ATTR_HOST, ATTR_PORT, ATTR_USERNAME,
ATTR_PASSWORD, ATTR_PROTOCOL, ATTR_DISCOVERY_ID)
_LOGGER = logging.getLogger(__name__)
class MQTTService(ServiceInterface):
"""Provide mqtt services."""
@property
def slug(self):
"""Return slug of this service."""
return SERVICE_MQTT
@property
def _data(self):
"""Return data of this service."""
return self._services.data.mqtt
@property
def schema(self):
"""Return data schema of this service."""
return SCHEMA_SERVICE_MQTT
@property
def provider(self):
"""Return name of service provider."""
return self._data.get(ATTR_PROVIDER)
@property
def hass_config(self):
"""Return Home-Assistant mqtt config."""
if not self.enabled:
return None
hass_config = {
'host': self._data[ATTR_HOST],
'port': self._data[ATTR_PORT],
'protocol': self._data[ATTR_PROTOCOL]
}
if ATTR_USERNAME in self._data:
hass_config['user']: self._data[ATTR_USERNAME]
if ATTR_PASSWORD in self._data:
hass_config['password']: self._data[ATTR_PASSWORD]
return hass_config
def set_service_data(self, provider, data):
"""Write the data into service object."""
if self.enabled:
_LOGGER.error("It is already a mqtt in use from %s", self.provider)
return False
self._data.update(data)
self._data[ATTR_PROVIDER] = provider
if provider == 'homeassistant':
_LOGGER.info("Use mqtt settings from Home-Assistant")
self.save()
return True
# discover mqtt to homeassistant
message = self._services.discovery.send(
provider, SERVICE_MQTT, None, self.hass_config)
self._data[ATTR_DISCOVERY_ID] = message.uuid
self.save()
return True
def del_service_data(self, provider):
"""Remove the data from service object."""
if not self.enabled:
_LOGGER.warning("Can't remove not exists services.")
return False
discovery_id = self._data.get(ATTR_DISCOVERY_ID)
if discovery_id:
self._services.discovery.remove(
self._services.discovery.get(discovery_id))
self._data.clear()
self.save()
return True

View File

@ -0,0 +1,44 @@
"""Validate services schema."""
import voluptuous as vol
from ..const import (
SERVICE_MQTT, ATTR_HOST, ATTR_PORT, ATTR_PASSWORD, ATTR_USERNAME, ATTR_SSL,
ATTR_PROVIDER, ATTR_PROTOCOL, ATTR_DISCOVERY, ATTR_COMPONENT, ATTR_UUID,
ATTR_PLATFORM, ATTR_CONFIG, ATTR_DISCOVERY_ID)
from ..validate import NETWORK_PORT
SCHEMA_DISCOVERY = vol.Schema([
vol.Schema({
vol.Required(ATTR_UUID): vol.Match(r"^[0-9a-f]{32}$"),
vol.Required(ATTR_PROVIDER): vol.Coerce(str),
vol.Required(ATTR_COMPONENT): vol.Coerce(str),
vol.Required(ATTR_PLATFORM): vol.Any(None, vol.Coerce(str)),
vol.Required(ATTR_CONFIG): vol.Any(None, dict),
}, extra=vol.REMOVE_EXTRA)
])
# pylint: disable=no-value-for-parameter
SCHEMA_SERVICE_MQTT = vol.Schema({
vol.Required(ATTR_HOST): vol.Coerce(str),
vol.Required(ATTR_PORT): NETWORK_PORT,
vol.Optional(ATTR_USERNAME): vol.Coerce(str),
vol.Optional(ATTR_PASSWORD): vol.Coerce(str),
vol.Optional(ATTR_SSL, default=False): vol.Boolean(),
vol.Optional(ATTR_PROTOCOL, default='3.1.1'):
vol.All(vol.Coerce(str), vol.In(['3.1', '3.1.1'])),
})
SCHEMA_CONFIG_MQTT = SCHEMA_SERVICE_MQTT.extend({
vol.Required(ATTR_PROVIDER): vol.Coerce(str),
vol.Optional(ATTR_DISCOVERY_ID): vol.Match(r"^[0-9a-f]{32}$"),
})
SCHEMA_SERVICES_FILE = vol.Schema({
vol.Optional(SERVICE_MQTT, default=dict): vol.Any({}, SCHEMA_CONFIG_MQTT),
vol.Optional(ATTR_DISCOVERY, default=list): vol.Any([], SCHEMA_DISCOVERY),
}, extra=vol.REMOVE_EXTRA)

View File

@ -14,7 +14,7 @@ from ..coresys import CoreSysAttributes
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class SnapshotsManager(CoreSysAttributes): class SnapshotManager(CoreSysAttributes):
"""Manage snapshots.""" """Manage snapshots."""
def __init__(self, coresys): def __init__(self, coresys):

View File

@ -32,6 +32,14 @@ class JsonConfig(object):
self.read_data() self.read_data()
def reset_data(self):
"""Reset json file to default."""
try:
self._data = self._schema({})
except vol.Invalid as ex:
_LOGGER.error("Can't reset %s: %s",
self._file, humanize_error(self._data, ex))
def read_data(self): def read_data(self):
"""Read json file & validate.""" """Read json file & validate."""
if self._file.is_file(): if self._file.is_file():
@ -63,7 +71,7 @@ class JsonConfig(object):
# Load last valid data # Load last valid data
_LOGGER.warning("Reset %s to last version", self._file) _LOGGER.warning("Reset %s to last version", self._file)
self.save_data() self.read_data()
return return
# write # write