mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-08 09:46:29 +00:00
Overwork Services/Discovery (#725)
* Update homeassistant.py * Update validate.py * Update exceptions.py * Update services.py * Update discovery.py * fix gitignore * Fix handling for discovery * use object in ref * lock down discovery API * fix api * Design * Fix API * fix lint * fix * Fix security layer * add provide layer * fix access * change rating * fix rights * Fix API error handling * raise error * fix rights * api * fix handling * fix * debug * debug json * Fix validator * fix error * new url * fix schema
This commit is contained in:
parent
4ef8c9d633
commit
e5451973bd
3
.gitignore
vendored
3
.gitignore
vendored
@ -90,3 +90,6 @@ ENV/
|
|||||||
|
|
||||||
# pylint
|
# pylint
|
||||||
.pylint.d/
|
.pylint.d/
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/
|
||||||
|
25
API.md
25
API.md
@ -499,8 +499,8 @@ Get all available addons.
|
|||||||
"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']",
|
"services_role": "['service:access']",
|
||||||
"discovery": "null|['component/platform']"
|
"discovery": "['service']"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -576,12 +576,13 @@ Write data to add-on stdin
|
|||||||
|
|
||||||
### Service discovery
|
### Service discovery
|
||||||
|
|
||||||
- GET `/services/discovery`
|
- GET `/discovery`
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"discovery": [
|
"discovery": [
|
||||||
{
|
{
|
||||||
"provider": "name",
|
"addon": "slug",
|
||||||
|
"service": "name",
|
||||||
"uuid": "uuid",
|
"uuid": "uuid",
|
||||||
"component": "component",
|
"component": "component",
|
||||||
"platform": "null|platform",
|
"platform": "null|platform",
|
||||||
@ -591,10 +592,11 @@ Write data to add-on stdin
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- GET `/services/discovery/{UUID}`
|
- GET `/discovery/{UUID}`
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"provider": "name",
|
"addon": "slug",
|
||||||
|
"service": "name",
|
||||||
"uuid": "uuid",
|
"uuid": "uuid",
|
||||||
"component": "component",
|
"component": "component",
|
||||||
"platform": "null|platform",
|
"platform": "null|platform",
|
||||||
@ -602,9 +604,10 @@ Write data to add-on stdin
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- POST `/services/discovery`
|
- POST `/discovery`
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"service": "name",
|
||||||
"component": "component",
|
"component": "component",
|
||||||
"platform": "null|platform",
|
"platform": "null|platform",
|
||||||
"config": {}
|
"config": {}
|
||||||
@ -618,7 +621,7 @@ return:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- DEL `/services/discovery/{UUID}`
|
- DEL `/discovery/{UUID}`
|
||||||
|
|
||||||
- GET `/services`
|
- GET `/services`
|
||||||
```json
|
```json
|
||||||
@ -627,7 +630,7 @@ return:
|
|||||||
{
|
{
|
||||||
"slug": "name",
|
"slug": "name",
|
||||||
"available": "bool",
|
"available": "bool",
|
||||||
"provider": "null|name|list"
|
"providers": "list"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -635,12 +638,10 @@ return:
|
|||||||
|
|
||||||
#### MQTT
|
#### MQTT
|
||||||
|
|
||||||
This service performs an auto discovery to Home-Assistant.
|
|
||||||
|
|
||||||
- GET `/services/mqtt`
|
- GET `/services/mqtt`
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"provider": "name",
|
"addon": "name",
|
||||||
"host": "xy",
|
"host": "xy",
|
||||||
"port": "8883",
|
"port": "8883",
|
||||||
"ssl": "bool",
|
"ssl": "bool",
|
||||||
|
@ -239,24 +239,23 @@ class Addon(CoreSysAttributes):
|
|||||||
return self._mesh.get(ATTR_STARTUP)
|
return self._mesh.get(ATTR_STARTUP)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def services(self):
|
def services_role(self):
|
||||||
"""Return dict of services with rights."""
|
"""Return dict of services with rights."""
|
||||||
raw_services = self._mesh.get(ATTR_SERVICES)
|
raw_services = self._mesh.get(ATTR_SERVICES)
|
||||||
if not raw_services:
|
if not raw_services:
|
||||||
return None
|
return {}
|
||||||
|
|
||||||
formated_services = {}
|
services = {}
|
||||||
for data in raw_services:
|
for data in raw_services:
|
||||||
service = RE_SERVICE.match(data)
|
service = RE_SERVICE.match(data)
|
||||||
formated_services[service.group('service')] = \
|
services[service.group('service')] = service.group('rights')
|
||||||
service.group('rights') or 'ro'
|
|
||||||
|
|
||||||
return formated_services
|
return services
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def discovery(self):
|
def discovery(self):
|
||||||
"""Return list of discoverable components/platforms."""
|
"""Return list of discoverable components/platforms."""
|
||||||
return self._mesh.get(ATTR_DISCOVERY)
|
return self._mesh.get(ATTR_DISCOVERY, [])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ports(self):
|
def ports(self):
|
||||||
|
@ -4,6 +4,7 @@ from .utils import get_hash_from_repository
|
|||||||
from ..const import (
|
from ..const import (
|
||||||
REPOSITORY_CORE, REPOSITORY_LOCAL, ATTR_NAME, ATTR_URL, ATTR_MAINTAINER)
|
REPOSITORY_CORE, REPOSITORY_LOCAL, ATTR_NAME, ATTR_URL, ATTR_MAINTAINER)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..exceptions import APIError
|
||||||
|
|
||||||
UNKNOWN = 'unknown'
|
UNKNOWN = 'unknown'
|
||||||
|
|
||||||
@ -67,6 +68,6 @@ class Repository(CoreSysAttributes):
|
|||||||
def remove(self):
|
def remove(self):
|
||||||
"""Remove add-on repository."""
|
"""Remove add-on repository."""
|
||||||
if self._id in (REPOSITORY_CORE, REPOSITORY_LOCAL):
|
if self._id in (REPOSITORY_CORE, REPOSITORY_LOCAL):
|
||||||
raise RuntimeError("Can't remove built-in repositories!")
|
raise APIError("Can't remove built-in repositories!")
|
||||||
|
|
||||||
self.git.remove()
|
self.git.remove()
|
||||||
|
@ -28,10 +28,6 @@ def rating_security(addon):
|
|||||||
elif addon.apparmor == SECURITY_PROFILE:
|
elif addon.apparmor == SECURITY_PROFILE:
|
||||||
rating += 1
|
rating += 1
|
||||||
|
|
||||||
# API Access
|
|
||||||
if addon.access_hassio_api or addon.access_homeassistant_api:
|
|
||||||
rating += -1
|
|
||||||
|
|
||||||
# Privileged options
|
# Privileged options
|
||||||
if addon.privileged in (PRIVILEGED_NET_ADMIN, PRIVILEGED_SYS_ADMIN,
|
if addon.privileged in (PRIVILEGED_NET_ADMIN, PRIVILEGED_SYS_ADMIN,
|
||||||
PRIVILEGED_SYS_RAWIO, PRIVILEGED_SYS_PTRACE):
|
PRIVILEGED_SYS_RAWIO, PRIVILEGED_SYS_PTRACE):
|
||||||
|
@ -26,13 +26,13 @@ from ..const import (
|
|||||||
PRIVILEGED_SYS_RESOURCE, PRIVILEGED_SYS_PTRACE,
|
PRIVILEGED_SYS_RESOURCE, PRIVILEGED_SYS_PTRACE,
|
||||||
ROLE_DEFAULT, ROLE_HOMEASSISTANT, ROLE_MANAGER, ROLE_ADMIN)
|
ROLE_DEFAULT, ROLE_HOMEASSISTANT, ROLE_MANAGER, ROLE_ADMIN)
|
||||||
from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_DEVICE
|
from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_DEVICE
|
||||||
|
from ..services.validate import DISCOVERY_SERVICES
|
||||||
|
|
||||||
_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_SERVICE = re.compile(r"^(?P<service>mqtt):(?P<rights>provide|want|need)$")
|
||||||
RE_DISCOVERY = re.compile(r"^(?P<component>\w*)(?:/(?P<platform>\w*>))?$")
|
|
||||||
|
|
||||||
V_STR = 'str'
|
V_STR = 'str'
|
||||||
V_INT = 'int'
|
V_INT = 'int'
|
||||||
@ -143,7 +143,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
|
|||||||
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_SERVICES): [vol.Match(RE_SERVICE)],
|
vol.Optional(ATTR_SERVICES): [vol.Match(RE_SERVICE)],
|
||||||
vol.Optional(ATTR_DISCOVERY): [vol.Match(RE_DISCOVERY)],
|
vol.Optional(ATTR_DISCOVERY): [vol.In(DISCOVERY_SERVICES)],
|
||||||
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, [
|
||||||
|
@ -211,11 +211,11 @@ class RestAPI(CoreSysAttributes):
|
|||||||
api_discovery.coresys = self.coresys
|
api_discovery.coresys = self.coresys
|
||||||
|
|
||||||
self.webapp.add_routes([
|
self.webapp.add_routes([
|
||||||
web.get('/services/discovery', api_discovery.list),
|
web.get('/discovery', api_discovery.list),
|
||||||
web.get('/services/discovery/{uuid}', api_discovery.get_discovery),
|
web.get('/discovery/{uuid}', api_discovery.get_discovery),
|
||||||
web.delete('/services/discovery/{uuid}',
|
web.delete('/discovery/{uuid}',
|
||||||
api_discovery.del_discovery),
|
api_discovery.del_discovery),
|
||||||
web.post('/services/discovery', api_discovery.set_discovery),
|
web.post('/discovery', api_discovery.set_discovery),
|
||||||
])
|
])
|
||||||
|
|
||||||
def _register_panel(self):
|
def _register_panel(self):
|
||||||
|
@ -55,7 +55,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
|
|
||||||
# Lookup itself
|
# Lookup itself
|
||||||
if addon_slug == 'self':
|
if addon_slug == 'self':
|
||||||
addon_slug = request.get(REQUEST_FROM)
|
return request.get(REQUEST_FROM)
|
||||||
|
|
||||||
addon = self.sys_addons.get(addon_slug)
|
addon = self.sys_addons.get(addon_slug)
|
||||||
if not addon:
|
if not addon:
|
||||||
@ -66,14 +66,6 @@ class APIAddons(CoreSysAttributes):
|
|||||||
|
|
||||||
return addon
|
return addon
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _pretty_devices(addon):
|
|
||||||
"""Return a simplified device list."""
|
|
||||||
dev_list = addon.devices
|
|
||||||
if not dev_list:
|
|
||||||
return None
|
|
||||||
return [row.split(':')[0] for row in dev_list]
|
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def list(self, request):
|
async def list(self, request):
|
||||||
"""Return all add-ons or repositories."""
|
"""Return all add-ons or repositories."""
|
||||||
@ -148,7 +140,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_PRIVILEGED: addon.privileged,
|
ATTR_PRIVILEGED: addon.privileged,
|
||||||
ATTR_FULL_ACCESS: addon.with_full_access,
|
ATTR_FULL_ACCESS: addon.with_full_access,
|
||||||
ATTR_APPARMOR: addon.apparmor,
|
ATTR_APPARMOR: addon.apparmor,
|
||||||
ATTR_DEVICES: self._pretty_devices(addon),
|
ATTR_DEVICES: _pretty_devices(addon),
|
||||||
ATTR_ICON: addon.with_icon,
|
ATTR_ICON: addon.with_icon,
|
||||||
ATTR_LOGO: addon.with_logo,
|
ATTR_LOGO: addon.with_logo,
|
||||||
ATTR_CHANGELOG: addon.with_changelog,
|
ATTR_CHANGELOG: addon.with_changelog,
|
||||||
@ -163,7 +155,7 @@ 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_SERVICES: _pretty_services(addon),
|
||||||
ATTR_DISCOVERY: addon.discovery,
|
ATTR_DISCOVERY: addon.discovery,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,3 +320,19 @@ class APIAddons(CoreSysAttributes):
|
|||||||
|
|
||||||
data = await request.read()
|
data = await request.read()
|
||||||
return await asyncio.shield(addon.write_stdin(data))
|
return await asyncio.shield(addon.write_stdin(data))
|
||||||
|
|
||||||
|
|
||||||
|
def _pretty_devices(addon):
|
||||||
|
"""Return a simplified device list."""
|
||||||
|
dev_list = addon.devices
|
||||||
|
if not dev_list:
|
||||||
|
return None
|
||||||
|
return [row.split(':')[0] for row in dev_list]
|
||||||
|
|
||||||
|
|
||||||
|
def _pretty_services(addon):
|
||||||
|
"""Return a simplified services role list."""
|
||||||
|
services = []
|
||||||
|
for name, access in addon.services_role.items():
|
||||||
|
services.append(f"{name}:{access}")
|
||||||
|
return services
|
||||||
|
@ -3,15 +3,18 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from .utils import api_process, api_validate
|
from .utils import api_process, api_validate
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_PROVIDER, ATTR_UUID, ATTR_COMPONENT, ATTR_PLATFORM, ATTR_CONFIG,
|
ATTR_ADDON, ATTR_UUID, ATTR_COMPONENT, ATTR_PLATFORM, ATTR_CONFIG,
|
||||||
ATTR_DISCOVERY, REQUEST_FROM)
|
ATTR_DISCOVERY, ATTR_SERVICE, REQUEST_FROM)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..exceptions import APIError, APIForbidden
|
||||||
|
from ..services.validate import SERVICE_ALL
|
||||||
|
|
||||||
|
|
||||||
SCHEMA_DISCOVERY = vol.Schema({
|
SCHEMA_DISCOVERY = vol.Schema({
|
||||||
|
vol.Required(ATTR_SERVICE): vol.In(SERVICE_ALL),
|
||||||
vol.Required(ATTR_COMPONENT): vol.Coerce(str),
|
vol.Required(ATTR_COMPONENT): vol.Coerce(str),
|
||||||
vol.Optional(ATTR_PLATFORM): vol.Any(None, vol.Coerce(str)),
|
vol.Optional(ATTR_PLATFORM): vol.Maybe(vol.Coerce(str)),
|
||||||
vol.Optional(ATTR_CONFIG): vol.Any(None, dict),
|
vol.Optional(ATTR_CONFIG): vol.Maybe(dict),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -22,16 +25,24 @@ class APIDiscovery(CoreSysAttributes):
|
|||||||
"""Extract discovery message from URL."""
|
"""Extract discovery message from URL."""
|
||||||
message = self.sys_discovery.get(request.match_info.get('uuid'))
|
message = self.sys_discovery.get(request.match_info.get('uuid'))
|
||||||
if not message:
|
if not message:
|
||||||
raise RuntimeError("Discovery message not found")
|
raise APIError("Discovery message not found")
|
||||||
return message
|
return message
|
||||||
|
|
||||||
|
def _check_permission_ha(self, request):
|
||||||
|
"""Check permission for API call / Home Assistant."""
|
||||||
|
if request[REQUEST_FROM] != self.sys_homeassistant:
|
||||||
|
raise APIForbidden("Only HomeAssistant can use this API!")
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def list(self, request):
|
async def list(self, request):
|
||||||
"""Show register services."""
|
"""Show register services."""
|
||||||
|
self._check_permission_ha(request)
|
||||||
|
|
||||||
discovery = []
|
discovery = []
|
||||||
for message in self.sys_discovery.list_messages:
|
for message in self.sys_discovery.list_messages:
|
||||||
discovery.append({
|
discovery.append({
|
||||||
ATTR_PROVIDER: message.provider,
|
ATTR_ADDON: message.addon,
|
||||||
|
ATTR_SERVICE: message.service,
|
||||||
ATTR_UUID: message.uuid,
|
ATTR_UUID: message.uuid,
|
||||||
ATTR_COMPONENT: message.component,
|
ATTR_COMPONENT: message.component,
|
||||||
ATTR_PLATFORM: message.platform,
|
ATTR_PLATFORM: message.platform,
|
||||||
@ -44,8 +55,14 @@ class APIDiscovery(CoreSysAttributes):
|
|||||||
async def set_discovery(self, request):
|
async def set_discovery(self, request):
|
||||||
"""Write data into a discovery pipeline."""
|
"""Write data into a discovery pipeline."""
|
||||||
body = await api_validate(SCHEMA_DISCOVERY, request)
|
body = await api_validate(SCHEMA_DISCOVERY, request)
|
||||||
message = self.sys_discovery.send(
|
addon = request[REQUEST_FROM]
|
||||||
provider=request[REQUEST_FROM], **body)
|
|
||||||
|
# Access?
|
||||||
|
if body[ATTR_SERVICE] not in addon.discovery:
|
||||||
|
raise APIForbidden(f"Can't use discovery!")
|
||||||
|
|
||||||
|
# Process discovery message
|
||||||
|
message = self.sys_discovery.send(addon, **body)
|
||||||
|
|
||||||
return {ATTR_UUID: message.uuid}
|
return {ATTR_UUID: message.uuid}
|
||||||
|
|
||||||
@ -54,8 +71,12 @@ class APIDiscovery(CoreSysAttributes):
|
|||||||
"""Read data into a discovery message."""
|
"""Read data into a discovery message."""
|
||||||
message = self._extract_message(request)
|
message = self._extract_message(request)
|
||||||
|
|
||||||
|
# HomeAssistant?
|
||||||
|
self._check_permission_ha(request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ATTR_PROVIDER: message.provider,
|
ATTR_ADDON: message.addon,
|
||||||
|
ATTR_SERVICE: message.service,
|
||||||
ATTR_UUID: message.uuid,
|
ATTR_UUID: message.uuid,
|
||||||
ATTR_COMPONENT: message.component,
|
ATTR_COMPONENT: message.component,
|
||||||
ATTR_PLATFORM: message.platform,
|
ATTR_PLATFORM: message.platform,
|
||||||
@ -66,6 +87,11 @@ class APIDiscovery(CoreSysAttributes):
|
|||||||
async def del_discovery(self, request):
|
async def del_discovery(self, request):
|
||||||
"""Delete data into a discovery message."""
|
"""Delete data into a discovery message."""
|
||||||
message = self._extract_message(request)
|
message = self._extract_message(request)
|
||||||
|
addon = request[REQUEST_FROM]
|
||||||
|
|
||||||
|
# Permission
|
||||||
|
if message.addon != addon.slug:
|
||||||
|
raise APIForbidden(f"Can't remove discovery message")
|
||||||
|
|
||||||
self.sys_discovery.remove(message)
|
self.sys_discovery.remove(message)
|
||||||
return True
|
return True
|
||||||
|
@ -13,6 +13,7 @@ from ..const import (
|
|||||||
ATTR_REFRESH_TOKEN, CONTENT_TYPE_BINARY)
|
ATTR_REFRESH_TOKEN, CONTENT_TYPE_BINARY)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..validate import NETWORK_PORT, DOCKER_IMAGE
|
from ..validate import NETWORK_PORT, DOCKER_IMAGE
|
||||||
|
from ..exceptions import APIError
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -94,7 +95,7 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
"""Return resource information."""
|
"""Return resource information."""
|
||||||
stats = await self.sys_homeassistant.stats()
|
stats = await self.sys_homeassistant.stats()
|
||||||
if not stats:
|
if not stats:
|
||||||
raise RuntimeError("No stats available")
|
raise APIError("No stats available")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ATTR_CPU_PERCENT: stats.cpu_percent,
|
ATTR_CPU_PERCENT: stats.cpu_percent,
|
||||||
@ -139,6 +140,6 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
"""Check configuration of Home Assistant."""
|
"""Check configuration of Home Assistant."""
|
||||||
result = await self.sys_homeassistant.check_config()
|
result = await self.sys_homeassistant.check_config()
|
||||||
if not result.valid:
|
if not result.valid:
|
||||||
raise RuntimeError(result.log)
|
raise APIError(result.log)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -26,7 +26,6 @@ NO_SECURITY_CHECK = re.compile(
|
|||||||
r"|/homeassistant/api/.*"
|
r"|/homeassistant/api/.*"
|
||||||
r"|/homeassistant/websocket"
|
r"|/homeassistant/websocket"
|
||||||
r"|/supervisor/ping"
|
r"|/supervisor/ping"
|
||||||
r"|/services.*"
|
|
||||||
r")$"
|
r")$"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -35,6 +34,8 @@ ADDONS_API_BYPASS = re.compile(
|
|||||||
r"^(?:"
|
r"^(?:"
|
||||||
r"|/addons/self/(?!security)[^/]+"
|
r"|/addons/self/(?!security)[^/]+"
|
||||||
r"|/version"
|
r"|/version"
|
||||||
|
r"|/services.*"
|
||||||
|
r"|/discovery.*"
|
||||||
r")$"
|
r")$"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -58,8 +59,7 @@ ADDONS_ROLE_ACCESS = {
|
|||||||
r"|/hardware/.+"
|
r"|/hardware/.+"
|
||||||
r"|/hassos/.+"
|
r"|/hassos/.+"
|
||||||
r"|/supervisor/.+"
|
r"|/supervisor/.+"
|
||||||
r"|/addons/[^/]+/(?!security|options).+"
|
r"|/addons/[^/]+/(?!security).+"
|
||||||
r"|/addons(?:/self/(?!security).+)?"
|
|
||||||
r"|/snapshots.*"
|
r"|/snapshots.*"
|
||||||
r")$"
|
r")$"
|
||||||
),
|
),
|
||||||
@ -102,12 +102,12 @@ class SecurityMiddleware(CoreSysAttributes):
|
|||||||
if hassio_token in (self.sys_homeassistant.uuid,
|
if hassio_token in (self.sys_homeassistant.uuid,
|
||||||
self.sys_homeassistant.hassio_token):
|
self.sys_homeassistant.hassio_token):
|
||||||
_LOGGER.debug("%s access from Home Assistant", request.path)
|
_LOGGER.debug("%s access from Home Assistant", request.path)
|
||||||
request_from = 'homeassistant'
|
request_from = self.sys_homeassistant
|
||||||
|
|
||||||
# Host
|
# Host
|
||||||
if hassio_token == self.sys_machine_id:
|
if hassio_token == self.sys_machine_id:
|
||||||
_LOGGER.debug("%s access from Host", request.path)
|
_LOGGER.debug("%s access from Host", request.path)
|
||||||
request_from = 'host'
|
request_from = self.sys_host
|
||||||
|
|
||||||
# Add-on
|
# Add-on
|
||||||
addon = None
|
addon = None
|
||||||
@ -117,12 +117,12 @@ class SecurityMiddleware(CoreSysAttributes):
|
|||||||
# Check Add-on API access
|
# Check Add-on API access
|
||||||
if addon and ADDONS_API_BYPASS.match(request.path):
|
if addon and ADDONS_API_BYPASS.match(request.path):
|
||||||
_LOGGER.debug("Passthrough %s from %s", request.path, addon.slug)
|
_LOGGER.debug("Passthrough %s from %s", request.path, addon.slug)
|
||||||
request_from = addon.slug
|
request_from = addon
|
||||||
elif addon and addon.access_hassio_api:
|
elif addon and addon.access_hassio_api:
|
||||||
# Check Role
|
# Check Role
|
||||||
if ADDONS_ROLE_ACCESS[addon.hassio_role].match(request.path):
|
if ADDONS_ROLE_ACCESS[addon.hassio_role].match(request.path):
|
||||||
_LOGGER.info("%s access from %s", request.path, addon.slug)
|
_LOGGER.info("%s access from %s", request.path, addon.slug)
|
||||||
request_from = addon.slug
|
request_from = addon
|
||||||
else:
|
else:
|
||||||
_LOGGER.warning("%s no role for %s", request.path, addon.slug)
|
_LOGGER.warning("%s no role for %s", request.path, addon.slug)
|
||||||
|
|
||||||
|
@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
from .utils import api_process, api_validate
|
from .utils import api_process, api_validate
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_AVAILABLE, ATTR_PROVIDER, ATTR_SLUG, ATTR_SERVICES, REQUEST_FROM)
|
ATTR_AVAILABLE, ATTR_PROVIDERS, ATTR_SLUG, ATTR_SERVICES, REQUEST_FROM,
|
||||||
|
PROVIDE_SERVICE)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..exceptions import APIError, APIForbidden
|
||||||
|
|
||||||
|
|
||||||
class APIServices(CoreSysAttributes):
|
class APIServices(CoreSysAttributes):
|
||||||
@ -13,7 +15,7 @@ class APIServices(CoreSysAttributes):
|
|||||||
"""Return service, throw an exception if it doesn't exist."""
|
"""Return service, throw an exception if it doesn't exist."""
|
||||||
service = self.sys_services.get(request.match_info.get('service'))
|
service = self.sys_services.get(request.match_info.get('service'))
|
||||||
if not service:
|
if not service:
|
||||||
raise RuntimeError("Service does not exist")
|
raise APIError("Service does not exist")
|
||||||
|
|
||||||
return service
|
return service
|
||||||
|
|
||||||
@ -25,7 +27,7 @@ class APIServices(CoreSysAttributes):
|
|||||||
services.append({
|
services.append({
|
||||||
ATTR_SLUG: service.slug,
|
ATTR_SLUG: service.slug,
|
||||||
ATTR_AVAILABLE: service.enabled,
|
ATTR_AVAILABLE: service.enabled,
|
||||||
ATTR_PROVIDER: service.provider,
|
ATTR_PROVIDERS: service.providers,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {ATTR_SERVICES: services}
|
return {ATTR_SERVICES: services}
|
||||||
@ -35,21 +37,39 @@ class APIServices(CoreSysAttributes):
|
|||||||
"""Write data into a service."""
|
"""Write data into a service."""
|
||||||
service = self._extract_service(request)
|
service = self._extract_service(request)
|
||||||
body = await api_validate(service.schema, request)
|
body = await api_validate(service.schema, request)
|
||||||
|
addon = request[REQUEST_FROM]
|
||||||
|
|
||||||
return service.set_service_data(request[REQUEST_FROM], body)
|
_check_access(request, service.slug)
|
||||||
|
service.set_service_data(addon, body)
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def get_service(self, request):
|
async def get_service(self, request):
|
||||||
"""Read data into a service."""
|
"""Read data into a service."""
|
||||||
service = self._extract_service(request)
|
service = self._extract_service(request)
|
||||||
|
|
||||||
return {
|
# Access
|
||||||
ATTR_AVAILABLE: service.enabled,
|
_check_access(request, service.slug)
|
||||||
service.slug: service.get_service_data(),
|
|
||||||
}
|
if not service.enabled:
|
||||||
|
raise APIError("Service not enabled")
|
||||||
|
return service.get_service_data()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def del_service(self, request):
|
async def del_service(self, request):
|
||||||
"""Delete data into a service."""
|
"""Delete data into a service."""
|
||||||
service = self._extract_service(request)
|
service = self._extract_service(request)
|
||||||
return service.del_service_data(request[REQUEST_FROM])
|
addon = request[REQUEST_FROM]
|
||||||
|
|
||||||
|
# Access
|
||||||
|
_check_access(request, service.slug, True)
|
||||||
|
service.del_service_data(addon)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_access(request, service, provide=False):
|
||||||
|
"""Raise error if the rights are wrong."""
|
||||||
|
addon = request[REQUEST_FROM]
|
||||||
|
if not addon.services_role.get(service):
|
||||||
|
raise APIForbidden(f"No access to {service} service!")
|
||||||
|
|
||||||
|
if provide and addon.services_role.get(service) != PROVIDE_SERVICE:
|
||||||
|
raise APIForbidden(f"No access to write {service} service!")
|
||||||
|
@ -14,6 +14,7 @@ from ..const import (
|
|||||||
ATTR_HOMEASSISTANT, ATTR_VERSION, ATTR_SIZE, ATTR_FOLDERS, ATTR_TYPE,
|
ATTR_HOMEASSISTANT, ATTR_VERSION, ATTR_SIZE, ATTR_FOLDERS, ATTR_TYPE,
|
||||||
ATTR_SNAPSHOTS, ATTR_PASSWORD, ATTR_PROTECTED, CONTENT_TYPE_TAR)
|
ATTR_SNAPSHOTS, ATTR_PASSWORD, ATTR_PROTECTED, CONTENT_TYPE_TAR)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..exceptions import APIError
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -52,7 +53,7 @@ class APISnapshots(CoreSysAttributes):
|
|||||||
"""Return snapshot, throw an exception if it doesn't exist."""
|
"""Return snapshot, throw an exception if it doesn't exist."""
|
||||||
snapshot = self.sys_snapshots.get(request.match_info.get('snapshot'))
|
snapshot = self.sys_snapshots.get(request.match_info.get('snapshot'))
|
||||||
if not snapshot:
|
if not snapshot:
|
||||||
raise RuntimeError("Snapshot does not exist")
|
raise APIError("Snapshot does not exist")
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
|
@ -14,6 +14,7 @@ from ..const import (
|
|||||||
ATTR_BLK_WRITE, CONTENT_TYPE_BINARY, ATTR_ICON)
|
ATTR_BLK_WRITE, CONTENT_TYPE_BINARY, ATTR_ICON)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..validate import validate_timezone, WAIT_BOOT, REPOSITORIES, CHANNELS
|
from ..validate import validate_timezone, WAIT_BOOT, REPOSITORIES, CHANNELS
|
||||||
|
from ..exceptions import APIError
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -93,7 +94,7 @@ class APISupervisor(CoreSysAttributes):
|
|||||||
"""Return resource information."""
|
"""Return resource information."""
|
||||||
stats = await self.sys_supervisor.stats()
|
stats = await self.sys_supervisor.stats()
|
||||||
if not stats:
|
if not stats:
|
||||||
raise RuntimeError("No stats available")
|
raise APIError("No stats available")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ATTR_CPU_PERCENT: stats.cpu_percent,
|
ATTR_CPU_PERCENT: stats.cpu_percent,
|
||||||
@ -112,7 +113,7 @@ class APISupervisor(CoreSysAttributes):
|
|||||||
version = body.get(ATTR_VERSION, self.sys_updater.version_hassio)
|
version = body.get(ATTR_VERSION, self.sys_updater.version_hassio)
|
||||||
|
|
||||||
if version == self.sys_supervisor.version:
|
if version == self.sys_supervisor.version:
|
||||||
raise RuntimeError("Version {} is already in use".format(version))
|
raise APIError("Version {} is already in use".format(version))
|
||||||
|
|
||||||
return await asyncio.shield(
|
return await asyncio.shield(
|
||||||
self.sys_supervisor.update(version))
|
self.sys_supervisor.update(version))
|
||||||
@ -128,7 +129,7 @@ class APISupervisor(CoreSysAttributes):
|
|||||||
|
|
||||||
for result in results:
|
for result in results:
|
||||||
if result.exception() is not None:
|
if result.exception() is not None:
|
||||||
raise RuntimeError("Some reload task fails!")
|
raise APIError("Some reload task fails!")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ from voluptuous.humanize import humanize_error
|
|||||||
from ..const import (
|
from ..const import (
|
||||||
JSON_RESULT, JSON_DATA, JSON_MESSAGE, RESULT_OK, RESULT_ERROR,
|
JSON_RESULT, JSON_DATA, JSON_MESSAGE, RESULT_OK, RESULT_ERROR,
|
||||||
CONTENT_TYPE_BINARY)
|
CONTENT_TYPE_BINARY)
|
||||||
from ..exceptions import HassioError
|
from ..exceptions import HassioError, APIError, APIForbidden
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ def json_loads(data):
|
|||||||
try:
|
try:
|
||||||
return json.loads(data)
|
return json.loads(data)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
raise RuntimeError("Invalid json")
|
raise APIError("Invalid json")
|
||||||
|
|
||||||
|
|
||||||
def api_process(method):
|
def api_process(method):
|
||||||
@ -30,10 +30,10 @@ def api_process(method):
|
|||||||
"""Return API information."""
|
"""Return API information."""
|
||||||
try:
|
try:
|
||||||
answer = await method(api, *args, **kwargs)
|
answer = await method(api, *args, **kwargs)
|
||||||
except HassioError:
|
except (APIError, APIForbidden) as err:
|
||||||
return api_return_error()
|
|
||||||
except RuntimeError as err:
|
|
||||||
return api_return_error(message=str(err))
|
return api_return_error(message=str(err))
|
||||||
|
except HassioError:
|
||||||
|
return api_return_error(message="Unknown Error, see logs")
|
||||||
|
|
||||||
if isinstance(answer, dict):
|
if isinstance(answer, dict):
|
||||||
return api_return_ok(data=answer)
|
return api_return_ok(data=answer)
|
||||||
@ -55,7 +55,7 @@ def api_process_raw(content):
|
|||||||
try:
|
try:
|
||||||
msg_data = await method(api, *args, **kwargs)
|
msg_data = await method(api, *args, **kwargs)
|
||||||
msg_type = content
|
msg_type = content
|
||||||
except RuntimeError as err:
|
except (APIError, APIForbidden) as err:
|
||||||
msg_data = str(err).encode()
|
msg_data = str(err).encode()
|
||||||
msg_type = CONTENT_TYPE_BINARY
|
msg_type = CONTENT_TYPE_BINARY
|
||||||
except HassioError:
|
except HassioError:
|
||||||
@ -90,6 +90,6 @@ async def api_validate(schema, request):
|
|||||||
try:
|
try:
|
||||||
data = schema(data)
|
data = schema(data)
|
||||||
except vol.Invalid as ex:
|
except vol.Invalid as ex:
|
||||||
raise RuntimeError(humanize_error(data, ex)) from None
|
raise APIError(humanize_error(data, ex)) from None
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
@ -74,6 +74,7 @@ ATTR_TYPE = 'type'
|
|||||||
ATTR_SOURCE = 'source'
|
ATTR_SOURCE = 'source'
|
||||||
ATTR_FEATURES = 'features'
|
ATTR_FEATURES = 'features'
|
||||||
ATTR_ADDONS = 'addons'
|
ATTR_ADDONS = 'addons'
|
||||||
|
ATTR_PROVIDERS = 'providers'
|
||||||
ATTR_VERSION = 'version'
|
ATTR_VERSION = 'version'
|
||||||
ATTR_VERSION_LATEST = 'version_latest'
|
ATTR_VERSION_LATEST = 'version_latest'
|
||||||
ATTR_AUTO_UART = 'auto_uart'
|
ATTR_AUTO_UART = 'auto_uart'
|
||||||
@ -107,8 +108,6 @@ ATTR_MAINTAINER = 'maintainer'
|
|||||||
ATTR_PASSWORD = 'password'
|
ATTR_PASSWORD = 'password'
|
||||||
ATTR_TOTP = 'totp'
|
ATTR_TOTP = 'totp'
|
||||||
ATTR_INITIALIZE = 'initialize'
|
ATTR_INITIALIZE = 'initialize'
|
||||||
ATTR_SESSION = 'session'
|
|
||||||
ATTR_SESSIONS = 'sessions'
|
|
||||||
ATTR_LOCATON = 'location'
|
ATTR_LOCATON = 'location'
|
||||||
ATTR_BUILD = 'build'
|
ATTR_BUILD = 'build'
|
||||||
ATTR_DEVICES = 'devices'
|
ATTR_DEVICES = 'devices'
|
||||||
@ -154,7 +153,7 @@ 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_ADDON = 'addon'
|
||||||
ATTR_AVAILABLE = 'available'
|
ATTR_AVAILABLE = 'available'
|
||||||
ATTR_HOST = 'host'
|
ATTR_HOST = 'host'
|
||||||
ATTR_USERNAME = 'username'
|
ATTR_USERNAME = 'username'
|
||||||
@ -163,8 +162,8 @@ ATTR_DISCOVERY = 'discovery'
|
|||||||
ATTR_PLATFORM = 'platform'
|
ATTR_PLATFORM = 'platform'
|
||||||
ATTR_COMPONENT = 'component'
|
ATTR_COMPONENT = 'component'
|
||||||
ATTR_CONFIG = 'config'
|
ATTR_CONFIG = 'config'
|
||||||
ATTR_DISCOVERY_ID = 'discovery_id'
|
|
||||||
ATTR_SERVICES = 'services'
|
ATTR_SERVICES = 'services'
|
||||||
|
ATTR_SERVICE = 'service'
|
||||||
ATTR_DISCOVERY = 'discovery'
|
ATTR_DISCOVERY = 'discovery'
|
||||||
ATTR_PROTECTED = 'protected'
|
ATTR_PROTECTED = 'protected'
|
||||||
ATTR_CRYPTO = 'crypto'
|
ATTR_CRYPTO = 'crypto'
|
||||||
@ -188,6 +187,9 @@ ATTR_HASSIO_ROLE = 'hassio_role'
|
|||||||
ATTR_SUPERVISOR = 'supervisor'
|
ATTR_SUPERVISOR = 'supervisor'
|
||||||
|
|
||||||
SERVICE_MQTT = 'mqtt'
|
SERVICE_MQTT = 'mqtt'
|
||||||
|
PROVIDE_SERVICE = 'provide'
|
||||||
|
NEED_SERVICE = 'need'
|
||||||
|
WANT_SERVICE = 'want'
|
||||||
|
|
||||||
STARTUP_INITIALIZE = 'initialize'
|
STARTUP_INITIALIZE = 'initialize'
|
||||||
STARTUP_SYSTEM = 'system'
|
STARTUP_SYSTEM = 'system'
|
||||||
|
@ -81,13 +81,25 @@ class HostAppArmorError(HostError):
|
|||||||
|
|
||||||
# API
|
# API
|
||||||
|
|
||||||
class APIError(HassioError):
|
class APIError(HassioError, RuntimeError):
|
||||||
"""API errors."""
|
"""API errors."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class APINotSupportedError(HassioNotSupportedError):
|
class APIForbidden(APIError):
|
||||||
"""API not supported error."""
|
"""API forbidden error."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Service / Discovery
|
||||||
|
|
||||||
|
class DiscoveryError(HassioError):
|
||||||
|
"""Discovery Errors."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ServicesError(HassioError):
|
||||||
|
"""Services Errors."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -439,19 +439,6 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
|||||||
_LOGGER.warning("Home Assistant API config mismatch: %d", err)
|
_LOGGER.warning("Home Assistant API config mismatch: %d", err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def send_event(self, event_type, event_data=None):
|
|
||||||
"""Send event to Home-Assistant."""
|
|
||||||
with suppress(HomeAssistantAPIError):
|
|
||||||
async with self.make_request(
|
|
||||||
'get', f'api/events/{event_type}'
|
|
||||||
) as resp:
|
|
||||||
if resp.status in (200, 201):
|
|
||||||
return
|
|
||||||
err = resp.status
|
|
||||||
|
|
||||||
_LOGGER.warning("Home Assistant event %s fails: %s", event_type, err)
|
|
||||||
return HomeAssistantError()
|
|
||||||
|
|
||||||
async def _block_till_run(self):
|
async def _block_till_run(self):
|
||||||
"""Block until Home-Assistant is booting up or startup timeout."""
|
"""Block until Home-Assistant is booting up or startup timeout."""
|
||||||
start_time = time.monotonic()
|
start_time = time.monotonic()
|
||||||
|
@ -1,14 +1,20 @@
|
|||||||
"""Handle discover message for Home Assistant."""
|
"""Handle discover message for Home Assistant."""
|
||||||
import logging
|
import logging
|
||||||
|
from contextlib import suppress
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from ..const import ATTR_UUID
|
import attr
|
||||||
|
import voluptuous as vol
|
||||||
|
from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
|
from .validate import DISCOVERY_SERVICES
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..exceptions import DiscoveryError, HomeAssistantAPIError
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
EVENT_DISCOVERY_ADD = 'hassio_discovery_add'
|
CMD_NEW = 'post'
|
||||||
EVENT_DISCOVERY_DEL = 'hassio_discovery_del'
|
CMD_DEL = 'delete'
|
||||||
|
|
||||||
|
|
||||||
class Discovery(CoreSysAttributes):
|
class Discovery(CoreSysAttributes):
|
||||||
@ -32,7 +38,7 @@ class Discovery(CoreSysAttributes):
|
|||||||
"""Write discovery message into data file."""
|
"""Write discovery message into data file."""
|
||||||
messages = []
|
messages = []
|
||||||
for message in self.message_obj.values():
|
for message in self.message_obj.values():
|
||||||
messages.append(message.raw())
|
messages.append(attr.asdict(message))
|
||||||
|
|
||||||
self._data.clear()
|
self._data.clear()
|
||||||
self._data.extend(messages)
|
self._data.extend(messages)
|
||||||
@ -52,26 +58,31 @@ class Discovery(CoreSysAttributes):
|
|||||||
"""Return list of available discovery messages."""
|
"""Return list of available discovery messages."""
|
||||||
return self.message_obj.values()
|
return self.message_obj.values()
|
||||||
|
|
||||||
def send(self, provider, component, platform=None, config=None):
|
def send(self, addon, service, component, platform, config):
|
||||||
"""Send a discovery message to Home Assistant."""
|
"""Send a discovery message to Home Assistant."""
|
||||||
message = Message(provider, component, platform, config)
|
try:
|
||||||
|
DISCOVERY_SERVICES[service](config)
|
||||||
|
except vol.Invalid as err:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Invalid discovery %s config", humanize_error(config, err))
|
||||||
|
raise DiscoveryError() from None
|
||||||
|
|
||||||
|
# Create message
|
||||||
|
message = Message(addon.slug, service, component, platform, config)
|
||||||
|
|
||||||
# Already exists?
|
# Already exists?
|
||||||
for exists_message in self.message_obj:
|
for old_message in self.message_obj:
|
||||||
if exists_message == message:
|
if old_message != message:
|
||||||
_LOGGER.warning("Found duplicate discovery message from %s",
|
continue
|
||||||
provider)
|
_LOGGER.warning("Duplicate discovery message from %s", addon.slug)
|
||||||
return exists_message
|
return old_message
|
||||||
|
|
||||||
_LOGGER.info("Send discovery to Home Assistant %s/%s from %s",
|
_LOGGER.info("Send discovery to Home Assistant %s/%s from %s",
|
||||||
component, platform, provider)
|
component, platform, addon.slug)
|
||||||
self.message_obj[message.uuid] = message
|
self.message_obj[message.uuid] = message
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
# Send event to Home Assistant
|
self.sys_create_task(self._push_discovery(message.uuid, CMD_NEW))
|
||||||
self.sys_create_task(self.sys_homeassistant.send_event(
|
|
||||||
EVENT_DISCOVERY_ADD, {ATTR_UUID: message.uuid}))
|
|
||||||
|
|
||||||
return message
|
return message
|
||||||
|
|
||||||
def remove(self, message):
|
def remove(self, message):
|
||||||
@ -79,29 +90,31 @@ class Discovery(CoreSysAttributes):
|
|||||||
self.message_obj.pop(message.uuid, None)
|
self.message_obj.pop(message.uuid, None)
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
# send event to Home-Assistant
|
_LOGGER.info("Delete discovery to Home Assistant %s/%s from %s",
|
||||||
self.sys_create_task(self.sys_homeassistant.send_event(
|
message.component, message.platform, message.addon)
|
||||||
EVENT_DISCOVERY_DEL, {ATTR_UUID: message.uuid}))
|
self.sys_create_task(self._push_discovery(message.uuid, CMD_DEL))
|
||||||
|
|
||||||
|
async def _push_discovery(self, uuid, command):
|
||||||
|
"""Send a discovery request."""
|
||||||
|
if not await self.sys_homeassistant.check_api_state():
|
||||||
|
_LOGGER.info("Discovery %s mesage ignore", uuid)
|
||||||
|
return
|
||||||
|
|
||||||
|
with suppress(HomeAssistantAPIError):
|
||||||
|
async with self.sys_homeassistant.make_request(
|
||||||
|
command, f"api/hassio_push/discovery/{uuid}"):
|
||||||
|
_LOGGER.info("Discovery %s message send", uuid)
|
||||||
|
return
|
||||||
|
|
||||||
|
_LOGGER.warning("Discovery %s message fail", uuid)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
class Message:
|
class Message:
|
||||||
"""Represent a single Discovery message."""
|
"""Represent a single Discovery message."""
|
||||||
|
addon = attr.ib()
|
||||||
def __init__(self, provider, component, platform, config, uuid=None):
|
service = attr.ib()
|
||||||
"""Initialize discovery message."""
|
component = attr.ib()
|
||||||
self.provider = provider
|
platform = attr.ib()
|
||||||
self.component = component
|
config = attr.ib()
|
||||||
self.platform = platform
|
uuid = attr.ib(factory=lambda: uuid4().hex, cmp=False)
|
||||||
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
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Interface for single service."""
|
"""Interface for single service."""
|
||||||
|
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..const import PROVIDE_SERVICE
|
||||||
|
|
||||||
|
|
||||||
class ServiceInterface(CoreSysAttributes):
|
class ServiceInterface(CoreSysAttributes):
|
||||||
@ -26,9 +27,13 @@ class ServiceInterface(CoreSysAttributes):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def provider(self):
|
def providers(self):
|
||||||
"""Return name of service provider."""
|
"""Return name of service providers addon."""
|
||||||
return None
|
addons = []
|
||||||
|
for addon in self.sys_addons.list_installed:
|
||||||
|
if addon.services_role.get(self.slug) == PROVIDE_SERVICE:
|
||||||
|
addons.append(addon.slug)
|
||||||
|
return addons
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def enabled(self):
|
def enabled(self):
|
||||||
@ -45,10 +50,10 @@ class ServiceInterface(CoreSysAttributes):
|
|||||||
return self._data
|
return self._data
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def set_service_data(self, provider, data):
|
def set_service_data(self, addon, data):
|
||||||
"""Write the data into service object."""
|
"""Write the data into service object."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def del_service_data(self, provider):
|
def del_service_data(self, addon):
|
||||||
"""Remove the data from service object."""
|
"""Remove the data from service object."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
@ -3,9 +3,8 @@ import logging
|
|||||||
|
|
||||||
from .interface import ServiceInterface
|
from .interface import ServiceInterface
|
||||||
from .validate import SCHEMA_SERVICE_MQTT
|
from .validate import SCHEMA_SERVICE_MQTT
|
||||||
from ..const import (
|
from ..const import ATTR_ADDON, SERVICE_MQTT
|
||||||
ATTR_PROVIDER, SERVICE_MQTT, ATTR_HOST, ATTR_PORT, ATTR_USERNAME,
|
from ..exceptions import ServicesError
|
||||||
ATTR_PASSWORD, ATTR_PROTOCOL, ATTR_DISCOVERY_ID)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -28,62 +27,24 @@ class MQTTService(ServiceInterface):
|
|||||||
"""Return data schema of this service."""
|
"""Return data schema of this service."""
|
||||||
return SCHEMA_SERVICE_MQTT
|
return SCHEMA_SERVICE_MQTT
|
||||||
|
|
||||||
@property
|
def set_service_data(self, addon, data):
|
||||||
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."""
|
"""Write the data into service object."""
|
||||||
if self.enabled:
|
if self.enabled:
|
||||||
_LOGGER.error("It is already a MQTT in use from %s", self.provider)
|
_LOGGER.error(
|
||||||
return False
|
"It is already a MQTT in use from %s", self._data[ATTR_ADDON])
|
||||||
|
raise ServicesError()
|
||||||
|
|
||||||
self._data.update(data)
|
self._data.update(data)
|
||||||
self._data[ATTR_PROVIDER] = provider
|
self._data[ATTR_ADDON] = addon.slug
|
||||||
|
|
||||||
if provider == 'homeassistant':
|
_LOGGER.info("Set %s as service provider for mqtt", addon.slug)
|
||||||
_LOGGER.info("Use MQTT settings from Home Assistant")
|
|
||||||
self.save()
|
self.save()
|
||||||
return True
|
|
||||||
|
|
||||||
# Discover MQTT to Home Assistant
|
def del_service_data(self, addon):
|
||||||
message = self.sys_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."""
|
"""Remove the data from service object."""
|
||||||
if not self.enabled:
|
if not self.enabled:
|
||||||
_LOGGER.warning("Can't remove not exists services")
|
_LOGGER.warning("Can't remove not exists services")
|
||||||
return False
|
raise ServicesError()
|
||||||
|
|
||||||
discovery_id = self._data.get(ATTR_DISCOVERY_ID)
|
|
||||||
if discovery_id:
|
|
||||||
self.sys_discovery.remove(
|
|
||||||
self.sys_discovery.get(discovery_id))
|
|
||||||
|
|
||||||
self._data.clear()
|
self._data.clear()
|
||||||
self.save()
|
self.save()
|
||||||
return True
|
|
||||||
|
@ -1,20 +1,40 @@
|
|||||||
"""Validate services schema."""
|
"""Validate services schema."""
|
||||||
|
import re
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
SERVICE_MQTT, ATTR_HOST, ATTR_PORT, ATTR_PASSWORD, ATTR_USERNAME, ATTR_SSL,
|
SERVICE_MQTT, ATTR_HOST, ATTR_PORT, ATTR_PASSWORD, ATTR_USERNAME, ATTR_SSL,
|
||||||
ATTR_PROVIDER, ATTR_PROTOCOL, ATTR_DISCOVERY, ATTR_COMPONENT, ATTR_UUID,
|
ATTR_ADDON, ATTR_PROTOCOL, ATTR_DISCOVERY, ATTR_COMPONENT, ATTR_UUID,
|
||||||
ATTR_PLATFORM, ATTR_CONFIG, ATTR_DISCOVERY_ID)
|
ATTR_PLATFORM, ATTR_CONFIG, ATTR_SERVICE)
|
||||||
from ..validate import NETWORK_PORT
|
from ..validate import NETWORK_PORT
|
||||||
|
|
||||||
|
UUID_MATCH = re.compile(r"^[0-9a-f]{32}$")
|
||||||
|
|
||||||
|
SERVICE_ALL = [
|
||||||
|
SERVICE_MQTT
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def schema_or(schema):
|
||||||
|
"""Allow schema or empty."""
|
||||||
|
def _wrapper(value):
|
||||||
|
"""Wrapper for validator."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
return schema(value)
|
||||||
|
|
||||||
|
return _wrapper
|
||||||
|
|
||||||
|
|
||||||
SCHEMA_DISCOVERY = vol.Schema([
|
SCHEMA_DISCOVERY = vol.Schema([
|
||||||
vol.Schema({
|
vol.Schema({
|
||||||
vol.Required(ATTR_UUID): vol.Match(r"^[0-9a-f]{32}$"),
|
vol.Required(ATTR_UUID): vol.Match(UUID_MATCH),
|
||||||
vol.Required(ATTR_PROVIDER): vol.Coerce(str),
|
vol.Required(ATTR_ADDON): vol.Coerce(str),
|
||||||
|
vol.Required(ATTR_SERVICE): vol.In(SERVICE_ALL),
|
||||||
vol.Required(ATTR_COMPONENT): vol.Coerce(str),
|
vol.Required(ATTR_COMPONENT): vol.Coerce(str),
|
||||||
vol.Required(ATTR_PLATFORM): vol.Any(None, vol.Coerce(str)),
|
vol.Required(ATTR_PLATFORM): vol.Maybe(vol.Coerce(str)),
|
||||||
vol.Required(ATTR_CONFIG): vol.Any(None, dict),
|
vol.Required(ATTR_CONFIG): vol.Maybe(dict),
|
||||||
}, extra=vol.REMOVE_EXTRA)
|
}, extra=vol.REMOVE_EXTRA)
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -32,12 +52,16 @@ SCHEMA_SERVICE_MQTT = vol.Schema({
|
|||||||
|
|
||||||
|
|
||||||
SCHEMA_CONFIG_MQTT = SCHEMA_SERVICE_MQTT.extend({
|
SCHEMA_CONFIG_MQTT = SCHEMA_SERVICE_MQTT.extend({
|
||||||
vol.Required(ATTR_PROVIDER): vol.Coerce(str),
|
vol.Required(ATTR_ADDON): vol.Coerce(str),
|
||||||
vol.Optional(ATTR_DISCOVERY_ID): vol.Match(r"^[0-9a-f]{32}$"),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
SCHEMA_SERVICES_FILE = vol.Schema({
|
SCHEMA_SERVICES_FILE = vol.Schema({
|
||||||
vol.Optional(SERVICE_MQTT, default=dict): vol.Any({}, SCHEMA_CONFIG_MQTT),
|
vol.Optional(SERVICE_MQTT, default=dict): schema_or(SCHEMA_CONFIG_MQTT),
|
||||||
vol.Optional(ATTR_DISCOVERY, default=list): vol.Any([], SCHEMA_DISCOVERY),
|
vol.Optional(ATTR_DISCOVERY, default=list): schema_or(SCHEMA_DISCOVERY),
|
||||||
}, extra=vol.REMOVE_EXTRA)
|
}, extra=vol.REMOVE_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
DISCOVERY_SERVICES = {
|
||||||
|
SERVICE_MQTT: SCHEMA_SERVICE_MQTT,
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user