diff --git a/hassio/addons/addon.py b/hassio/addons/addon.py index 7bcef24b9..c9f81d547 100644 --- a/hassio/addons/addon.py +++ b/hassio/addons/addon.py @@ -584,6 +584,13 @@ class Addon(CoreSysAttributes): return False + def remove_discovery(self): + """Remove all discovery message from add-on.""" + for message in self.sys_discovery.list_messages: + if message.addon != self.slug: + continue + self.sys_discovery.remove(message) + def write_asound(self): """Write asound config to file and return True on success.""" asound_config = self.sys_host.alsa.asound( @@ -704,6 +711,9 @@ class Addon(CoreSysAttributes): with suppress(HostAppArmorError): await self.sys_host.apparmor.remove_profile(self.slug) + # Remove discovery messages + self.remove_discovery() + self._set_uninstall() return True diff --git a/hassio/addons/validate.py b/hassio/addons/validate.py index 9d2a2b705..0e25e365f 100644 --- a/hassio/addons/validate.py +++ b/hassio/addons/validate.py @@ -25,7 +25,7 @@ from ..const import ( PRIVILEGED_IPC_LOCK, PRIVILEGED_SYS_TIME, PRIVILEGED_SYS_NICE, PRIVILEGED_SYS_RESOURCE, PRIVILEGED_SYS_PTRACE, 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, UUID_MATCH from ..services.validate import DISCOVERY_SERVICES _LOGGER = logging.getLogger(__name__) @@ -185,8 +185,7 @@ SCHEMA_BUILD_CONFIG = vol.Schema({ # pylint: disable=no-value-for-parameter SCHEMA_ADDON_USER = vol.Schema({ vol.Required(ATTR_VERSION): vol.Coerce(str), - vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): - vol.Match(r"^[0-9a-f]{32}$"), + 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_OPTIONS, default=dict): dict, vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(), diff --git a/hassio/api/discovery.py b/hassio/api/discovery.py index f32b623c6..0acf972a9 100644 --- a/hassio/api/discovery.py +++ b/hassio/api/discovery.py @@ -7,11 +7,11 @@ from ..const import ( ATTR_DISCOVERY, ATTR_SERVICE, REQUEST_FROM) from ..coresys import CoreSysAttributes from ..exceptions import APIError, APIForbidden -from ..services.validate import SERVICE_ALL +from ..validate import SERVICE_ALL SCHEMA_DISCOVERY = vol.Schema({ - vol.Required(ATTR_SERVICE): vol.In(SERVICE_ALL), + vol.Required(ATTR_SERVICE): SERVICE_ALL, vol.Required(ATTR_COMPONENT): vol.Coerce(str), vol.Optional(ATTR_PLATFORM): vol.Maybe(vol.Coerce(str)), vol.Optional(ATTR_CONFIG): vol.Maybe(dict), diff --git a/hassio/api/supervisor.py b/hassio/api/supervisor.py index 5f1fa2e4b..33fe66c79 100644 --- a/hassio/api/supervisor.py +++ b/hassio/api/supervisor.py @@ -13,8 +13,9 @@ from ..const import ( ATTR_MEMORY_LIMIT, ATTR_NETWORK_RX, ATTR_NETWORK_TX, ATTR_BLK_READ, ATTR_BLK_WRITE, CONTENT_TYPE_BINARY, ATTR_ICON) from ..coresys import CoreSysAttributes -from ..validate import validate_timezone, WAIT_BOOT, REPOSITORIES, CHANNELS +from ..validate import WAIT_BOOT, REPOSITORIES, CHANNELS from ..exceptions import APIError +from ..utils.validate import validate_timezone _LOGGER = logging.getLogger(__name__) diff --git a/hassio/bootstrap.py b/hassio/bootstrap.py index 012205410..2aaa67b17 100644 --- a/hassio/bootstrap.py +++ b/hassio/bootstrap.py @@ -18,7 +18,7 @@ from .snapshots import SnapshotManager from .tasks import Tasks from .updater import Updater from .services import ServiceManager -from .services import Discovery +from .discovery import Discovery from .host import HostManager from .dbus import DBusManager from .hassos import HassOS diff --git a/hassio/const.py b/hassio/const.py index a88d58f98..c4d4b03b3 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -21,6 +21,7 @@ FILE_HASSIO_CONFIG = Path(HASSIO_DATA, "config.json") FILE_HASSIO_HOMEASSISTANT = Path(HASSIO_DATA, "homeassistant.json") FILE_HASSIO_UPDATER = Path(HASSIO_DATA, "updater.json") FILE_HASSIO_SERVICES = Path(HASSIO_DATA, "services.json") +FILE_HASSIO_DISCOVERY = Path(HASSIO_DATA, "discovery.json") SOCKET_DOCKER = Path("/var/run/docker.sock") diff --git a/hassio/core.py b/hassio/core.py index be32ee89f..0884549fe 100644 --- a/hassio/core.py +++ b/hassio/core.py @@ -52,6 +52,9 @@ class HassIO(CoreSysAttributes): # load services await self.sys_services.load() + # Load discovery + await self.sys_discovery.load() + # start dns forwarding self.sys_create_task(self.sys_dns.start()) diff --git a/hassio/services/discovery.py b/hassio/discovery.py similarity index 68% rename from hassio/services/discovery.py rename to hassio/discovery.py index f9b0b2467..98fb0e7e6 100644 --- a/hassio/services/discovery.py +++ b/hassio/discovery.py @@ -7,9 +7,12 @@ import attr import voluptuous as vol from voluptuous.humanize import humanize_error -from .validate import DISCOVERY_SERVICES -from ..coresys import CoreSysAttributes -from ..exceptions import DiscoveryError, HomeAssistantAPIError +from .const import FILE_HASSIO_DISCOVERY, ATTR_CONFIG, ATTR_DISCOVERY +from .coresys import CoreSysAttributes +from .exceptions import DiscoveryError, HomeAssistantAPIError +from .validate import SCHEMA_DISCOVERY_CONFIG +from .utils.json import JsonConfig +from .services.validate import DISCOVERY_SERVICES _LOGGER = logging.getLogger(__name__) @@ -17,18 +20,19 @@ CMD_NEW = 'post' CMD_DEL = 'delete' -class Discovery(CoreSysAttributes): +class Discovery(CoreSysAttributes, JsonConfig): """Home Assistant Discovery handler.""" def __init__(self, coresys): """Initialize discovery handler.""" + super().__init__(FILE_HASSIO_DISCOVERY, SCHEMA_DISCOVERY_CONFIG) self.coresys = coresys self.message_obj = {} - def load(self): + async def load(self): """Load exists discovery message into storage.""" messages = {} - for message in self._data: + for message in self._data[ATTR_DISCOVERY]: discovery = Message(**message) messages[discovery.uuid] = discovery @@ -40,19 +44,14 @@ class Discovery(CoreSysAttributes): for message in self.message_obj.values(): messages.append(attr.asdict(message)) - self._data.clear() - self._data.extend(messages) - self.sys_services.data.save_data() + self._data[ATTR_DISCOVERY].clear() + self._data[ATTR_DISCOVERY].extend(messages) + self.save_data() def get(self, uuid): """Return discovery message.""" return self.message_obj.get(uuid) - @property - def _data(self): - """Return discovery data.""" - return self.sys_services.data.discovery - @property def list_messages(self): """Return list of available discovery messages.""" @@ -71,7 +70,7 @@ class Discovery(CoreSysAttributes): message = Message(addon.slug, service, component, platform, config) # Already exists? - for old_message in self.message_obj: + for old_message in self.list_messages: if old_message != message: continue _LOGGER.warning("Duplicate discovery message from %s", addon.slug) @@ -82,7 +81,7 @@ class Discovery(CoreSysAttributes): self.message_obj[message.uuid] = message self.save() - self.sys_create_task(self._push_discovery(message.uuid, CMD_NEW)) + self.sys_create_task(self._push_discovery(message, CMD_NEW)) return message def remove(self, message): @@ -92,21 +91,25 @@ class Discovery(CoreSysAttributes): _LOGGER.info("Delete discovery to Home Assistant %s/%s from %s", message.component, message.platform, message.addon) - self.sys_create_task(self._push_discovery(message.uuid, CMD_DEL)) + self.sys_create_task(self._push_discovery(message, CMD_DEL)) - async def _push_discovery(self, uuid, command): + async def _push_discovery(self, message, command): """Send a discovery request.""" if not await self.sys_homeassistant.check_api_state(): - _LOGGER.info("Discovery %s mesage ignore", uuid) + _LOGGER.info("Discovery %s mesage ignore", message.uuid) return + data = attr.asdict(message) + data.pop(ATTR_CONFIG) + with suppress(HomeAssistantAPIError): async with self.sys_homeassistant.make_request( - command, f"api/hassio_push/discovery/{uuid}"): - _LOGGER.info("Discovery %s message send", uuid) + command, f"api/hassio_push/discovery/{message.uuid}", + json=data, timeout=10): + _LOGGER.info("Discovery %s message send", message.uuid) return - _LOGGER.warning("Discovery %s message fail", uuid) + _LOGGER.warning("Discovery %s message fail", message.uuid) @attr.s @@ -116,5 +119,5 @@ class Message: service = attr.ib() component = attr.ib() platform = attr.ib() - config = attr.ib() + config = attr.ib(cmp=False) uuid = attr.ib(factory=lambda: uuid4().hex, cmp=False) diff --git a/hassio/services/__init__.py b/hassio/services/__init__.py index 33c056725..1fec40187 100644 --- a/hassio/services/__init__.py +++ b/hassio/services/__init__.py @@ -1,5 +1,4 @@ """Handle internal services discovery.""" -from .discovery import Discovery # noqa from .mqtt import MQTTService from .data import ServicesData from ..const import SERVICE_MQTT @@ -34,10 +33,6 @@ class ServiceManager(CoreSysAttributes): for slug, service in AVAILABLE_SERVICES.items(): self.services_obj[slug] = service(self.coresys) - # Read exists discovery messages - self.sys_discovery.load() - def reset(self): """Reset available data.""" self.data.reset_data() - self.sys_discovery.load() diff --git a/hassio/services/data.py b/hassio/services/data.py index c2fe2d630..5df9df2d0 100644 --- a/hassio/services/data.py +++ b/hassio/services/data.py @@ -1,7 +1,7 @@ """Handle service data for persistent supervisor reboot.""" -from .validate import SCHEMA_SERVICES_FILE -from ..const import FILE_HASSIO_SERVICES, ATTR_DISCOVERY, SERVICE_MQTT +from .validate import SCHEMA_SERVICES_CONFIG +from ..const import FILE_HASSIO_SERVICES, SERVICE_MQTT from ..utils.json import JsonConfig @@ -10,12 +10,7 @@ class ServicesData(JsonConfig): 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] + super().__init__(FILE_HASSIO_SERVICES, SCHEMA_SERVICES_CONFIG) @property def mqtt(self): diff --git a/hassio/services/validate.py b/hassio/services/validate.py index 6e0a2a5bb..a7217be90 100644 --- a/hassio/services/validate.py +++ b/hassio/services/validate.py @@ -1,42 +1,11 @@ """Validate services schema.""" -import re - import voluptuous as vol from ..const import ( SERVICE_MQTT, ATTR_HOST, ATTR_PORT, ATTR_PASSWORD, ATTR_USERNAME, ATTR_SSL, - ATTR_ADDON, ATTR_PROTOCOL, ATTR_DISCOVERY, ATTR_COMPONENT, ATTR_UUID, - ATTR_PLATFORM, ATTR_CONFIG, ATTR_SERVICE) + ATTR_ADDON, ATTR_PROTOCOL) 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([ - vol.Schema({ - vol.Required(ATTR_UUID): vol.Match(UUID_MATCH), - 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_PLATFORM): vol.Maybe(vol.Coerce(str)), - vol.Required(ATTR_CONFIG): vol.Maybe(dict), - }, extra=vol.REMOVE_EXTRA) -]) +from ..utils.validate import schema_or # pylint: disable=no-value-for-parameter @@ -56,9 +25,8 @@ SCHEMA_CONFIG_MQTT = SCHEMA_SERVICE_MQTT.extend({ }) -SCHEMA_SERVICES_FILE = vol.Schema({ +SCHEMA_SERVICES_CONFIG = vol.Schema({ vol.Optional(SERVICE_MQTT, default=dict): schema_or(SCHEMA_CONFIG_MQTT), - vol.Optional(ATTR_DISCOVERY, default=list): schema_or(SCHEMA_DISCOVERY), }, extra=vol.REMOVE_EXTRA) diff --git a/hassio/utils/validate.py b/hassio/utils/validate.py new file mode 100644 index 000000000..147baa684 --- /dev/null +++ b/hassio/utils/validate.py @@ -0,0 +1,28 @@ +"""Validate utils.""" + +import pytz +import voluptuous as vol + + +def schema_or(schema): + """Allow schema or empty.""" + def _wrapper(value): + """Wrapper for validator.""" + if not value: + return value + return schema(value) + + return _wrapper + + +def validate_timezone(timezone): + """Validate voluptuous timezone.""" + try: + pytz.timezone(timezone) + except pytz.exceptions.UnknownTimeZoneError: + raise vol.Invalid( + "Invalid time zone passed in. Valid options can be found here: " + "http://en.wikipedia.org/wiki/List_of_tz_database_time_zones") \ + from None + + return timezone diff --git a/hassio/validate.py b/hassio/validate.py index 36dd2fbbe..6c3eaf39a 100644 --- a/hassio/validate.py +++ b/hassio/validate.py @@ -3,15 +3,17 @@ import uuid import re import voluptuous as vol -import pytz from .const import ( ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_CHANNEL, ATTR_TIMEZONE, ATTR_HASSOS, ATTR_ADDONS_CUSTOM_LIST, ATTR_PASSWORD, ATTR_HOMEASSISTANT, ATTR_HASSIO, - ATTR_BOOT, ATTR_LAST_BOOT, ATTR_SSL, ATTR_PORT, ATTR_WATCHDOG, + ATTR_BOOT, ATTR_LAST_BOOT, ATTR_SSL, ATTR_PORT, ATTR_WATCHDOG, ATTR_CONFIG, ATTR_WAIT_BOOT, ATTR_UUID, ATTR_REFRESH_TOKEN, ATTR_HASSOS_CLI, - ATTR_ACCESS_TOKEN, + ATTR_ACCESS_TOKEN, ATTR_DISCOVERY, ATTR_ADDON, ATTR_COMPONENT, + ATTR_PLATFORM, ATTR_SERVICE, + SERVICE_MQTT, CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV) +from .utils.validate import schema_or, validate_timezone RE_REPOSITORY = re.compile(r"^(?P[^#]+)(?:#(?P[\w\-]+))?$") @@ -21,6 +23,8 @@ WAIT_BOOT = vol.All(vol.Coerce(int), vol.Range(min=1, max=60)) DOCKER_IMAGE = vol.Match(r"^[\w{}]+/[\-\w{}]+$") ALSA_DEVICE = vol.Maybe(vol.Match(r"\d+,\d+")) CHANNELS = vol.In([CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV]) +UUID_MATCH = vol.Match(r"^[0-9a-f]{32}$") +SERVICE_ALL = vol.In([SERVICE_MQTT]) def validate_repository(repository): @@ -40,19 +44,6 @@ def validate_repository(repository): REPOSITORIES = vol.All([validate_repository], vol.Unique()) -def validate_timezone(timezone): - """Validate voluptuous timezone.""" - try: - pytz.timezone(timezone) - except pytz.exceptions.UnknownTimeZoneError: - raise vol.Invalid( - "Invalid time zone passed in. Valid options can be found here: " - "http://en.wikipedia.org/wiki/List_of_tz_database_time_zones") \ - from None - - return timezone - - # pylint: disable=inconsistent-return-statements def convert_to_docker_ports(data): """Convert data into Docker port list.""" @@ -83,8 +74,7 @@ DOCKER_PORTS = vol.Schema({ # pylint: disable=no-value-for-parameter SCHEMA_HASS_CONFIG = vol.Schema({ - vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): - vol.Match(r"^[0-9a-f]{32}$"), + 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_BOOT, default=True): vol.Boolean(), vol.Inclusive(ATTR_IMAGE, 'custom_hass'): DOCKER_IMAGE, @@ -117,3 +107,19 @@ SCHEMA_HASSIO_CONFIG = vol.Schema({ ]): REPOSITORIES, vol.Optional(ATTR_WAIT_BOOT, default=5): WAIT_BOOT, }, extra=vol.REMOVE_EXTRA) + + +SCHEMA_DISCOVERY = vol.Schema([ + vol.Schema({ + vol.Required(ATTR_UUID): UUID_MATCH, + vol.Required(ATTR_ADDON): vol.Coerce(str), + vol.Required(ATTR_SERVICE): SERVICE_ALL, + vol.Required(ATTR_COMPONENT): vol.Coerce(str), + vol.Required(ATTR_PLATFORM): vol.Maybe(vol.Coerce(str)), + vol.Required(ATTR_CONFIG): vol.Maybe(dict), + }, extra=vol.REMOVE_EXTRA) +]) + +SCHEMA_DISCOVERY_CONFIG = vol.Schema({ + vol.Optional(ATTR_DISCOVERY, default=list): schema_or(SCHEMA_DISCOVERY), +}, extra=vol.REMOVE_EXTRA)