From b12175ab9a2b7560aed8e65d319386affc8f22cb Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 28 Mar 2019 14:11:18 +0100 Subject: [PATCH] Support for deconz discovery & cleanup (#974) * Support for deconz discovery & cleanup * Split discovery * Fix lint * Fix lint / import --- hassio/addons/validate.py | 4 +- hassio/api/discovery.py | 37 +++-- hassio/const.py | 2 - .../{discovery.py => discovery/__init__.py} | 88 +++++++----- hassio/discovery/const.py | 8 ++ hassio/discovery/services/__init__.py | 1 + hassio/discovery/services/deconz.py | 11 ++ hassio/discovery/services/mqtt.py | 27 ++++ hassio/discovery/validate.py | 47 +++++++ hassio/services/__init__.py | 30 ++-- hassio/services/const.py | 11 ++ hassio/services/data.py | 8 +- hassio/services/interface.py | 34 +++-- hassio/services/modules/__init__.py | 1 + hassio/services/modules/mqtt.py | 81 +++++++++++ hassio/services/mqtt.py | 50 ------- hassio/services/validate.py | 35 +---- hassio/validate.py | 133 ++++++++++-------- tests/discovery/__init__.py | 1 + tests/discovery/test_deconz.py | 19 +++ tests/discovery/test_mqtt.py | 21 +++ tests/discovery/test_validate.py | 21 +++ 22 files changed, 441 insertions(+), 229 deletions(-) rename hassio/{discovery.py => discovery/__init__.py} (61%) create mode 100644 hassio/discovery/const.py create mode 100644 hassio/discovery/services/__init__.py create mode 100644 hassio/discovery/services/deconz.py create mode 100644 hassio/discovery/services/mqtt.py create mode 100644 hassio/discovery/validate.py create mode 100644 hassio/services/const.py create mode 100644 hassio/services/modules/__init__.py create mode 100644 hassio/services/modules/mqtt.py delete mode 100644 hassio/services/mqtt.py create mode 100644 tests/discovery/__init__.py create mode 100644 tests/discovery/test_deconz.py create mode 100644 tests/discovery/test_mqtt.py create mode 100644 tests/discovery/test_validate.py diff --git a/hassio/addons/validate.py b/hassio/addons/validate.py index 8b02e7d0f..84e2ca627 100644 --- a/hassio/addons/validate.py +++ b/hassio/addons/validate.py @@ -21,7 +21,7 @@ from ..const import ( ATTR_UUID, ATTR_VERSION, ATTR_WEBUI, BOOT_AUTO, BOOT_MANUAL, PRIVILEGED_ALL, ROLE_ALL, ROLE_DEFAULT, STARTUP_ALL, STARTUP_APPLICATION, STARTUP_SERVICES, STATE_STARTED, STATE_STOPPED) -from ..services.validate import DISCOVERY_SERVICES +from ..discovery.validate import valid_discovery_service from ..validate import ( ALSA_DEVICE, DOCKER_PORTS, NETWORK_PORT, SHA256, UUID_MATCH) @@ -114,7 +114,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({ vol.Optional(ATTR_DOCKER_API, default=False): vol.Boolean(), vol.Optional(ATTR_AUTH_API, default=False): vol.Boolean(), vol.Optional(ATTR_SERVICES): [vol.Match(RE_SERVICE)], - vol.Optional(ATTR_DISCOVERY): [vol.In(DISCOVERY_SERVICES)], + vol.Optional(ATTR_DISCOVERY): [valid_discovery_service], vol.Required(ATTR_OPTIONS): dict, vol.Required(ATTR_SCHEMA): vol.Any(vol.Schema({ vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [ diff --git a/hassio/api/discovery.py b/hassio/api/discovery.py index b3ff6f99e..560f51fa7 100644 --- a/hassio/api/discovery.py +++ b/hassio/api/discovery.py @@ -3,17 +3,24 @@ import voluptuous as vol from .utils import api_process, api_validate from ..const import ( - ATTR_ADDON, ATTR_UUID, ATTR_CONFIG, ATTR_DISCOVERY, ATTR_SERVICE, - REQUEST_FROM) + ATTR_ADDON, + ATTR_UUID, + ATTR_CONFIG, + ATTR_DISCOVERY, + ATTR_SERVICE, + REQUEST_FROM, +) from ..coresys import CoreSysAttributes from ..exceptions import APIError, APIForbidden -from ..validate import SERVICE_ALL +from ..discovery.validate import valid_discovery_service -SCHEMA_DISCOVERY = vol.Schema({ - vol.Required(ATTR_SERVICE): SERVICE_ALL, - vol.Optional(ATTR_CONFIG): vol.Maybe(dict), -}) +SCHEMA_DISCOVERY = vol.Schema( + { + vol.Required(ATTR_SERVICE): valid_discovery_service, + vol.Optional(ATTR_CONFIG): vol.Maybe(dict), + } +) class APIDiscovery(CoreSysAttributes): @@ -21,7 +28,7 @@ class APIDiscovery(CoreSysAttributes): def _extract_message(self, request): """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: raise APIError("Discovery message not found") return message @@ -38,12 +45,14 @@ class APIDiscovery(CoreSysAttributes): discovery = [] for message in self.sys_discovery.list_messages: - discovery.append({ - ATTR_ADDON: message.addon, - ATTR_SERVICE: message.service, - ATTR_UUID: message.uuid, - ATTR_CONFIG: message.config, - }) + discovery.append( + { + ATTR_ADDON: message.addon, + ATTR_SERVICE: message.service, + ATTR_UUID: message.uuid, + ATTR_CONFIG: message.config, + } + ) return {ATTR_DISCOVERY: discovery} diff --git a/hassio/const.py b/hassio/const.py index ad5ebe3c3..a70966bab 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -159,7 +159,6 @@ ATTR_ADDON = "addon" ATTR_AVAILABLE = "available" ATTR_HOST = "host" ATTR_USERNAME = "username" -ATTR_PROTOCOL = "protocol" ATTR_DISCOVERY = "discovery" ATTR_CONFIG = "config" ATTR_SERVICES = "services" @@ -189,7 +188,6 @@ ATTR_AUTH_API = "auth_api" ATTR_KERNEL_MODULES = "kernel_modules" ATTR_SUPPORTED_ARCH = "supported_arch" -SERVICE_MQTT = "mqtt" PROVIDE_SERVICE = "provide" NEED_SERVICE = "need" WANT_SERVICE = "want" diff --git a/hassio/discovery.py b/hassio/discovery/__init__.py similarity index 61% rename from hassio/discovery.py rename to hassio/discovery/__init__.py index 116e4e4cd..46f3b1ff4 100644 --- a/hassio/discovery.py +++ b/hassio/discovery/__init__.py @@ -1,35 +1,50 @@ """Handle discover message for Home Assistant.""" -import logging +from __future__ import annotations + from contextlib import suppress -from uuid import uuid4 +import logging +from typing import Any, Dict, List, Optional, TYPE_CHECKING +from uuid import uuid4, UUID import attr import voluptuous as vol from voluptuous.humanize import humanize_error -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 +from ..const import ATTR_CONFIG, ATTR_DISCOVERY, FILE_HASSIO_DISCOVERY +from ..coresys import CoreSys, CoreSysAttributes +from ..exceptions import DiscoveryError, HomeAssistantAPIError +from ..utils.json import JsonConfig +from .validate import SCHEMA_DISCOVERY_CONFIG, valid_discovery_config + +if TYPE_CHECKING: + from ..addons.addon import Addon _LOGGER = logging.getLogger(__name__) -CMD_NEW = 'post' -CMD_DEL = 'delete' +CMD_NEW = "post" +CMD_DEL = "delete" + + +@attr.s +class Message: + """Represent a single Discovery message.""" + + addon: str = attr.ib() + service: str = attr.ib() + config: Dict[str, Any] = attr.ib(cmp=False) + uuid: UUID = attr.ib(factory=lambda: uuid4().hex, cmp=False) class Discovery(CoreSysAttributes, JsonConfig): """Home Assistant Discovery handler.""" - def __init__(self, coresys): + def __init__(self, coresys: CoreSys): """Initialize discovery handler.""" super().__init__(FILE_HASSIO_DISCOVERY, SCHEMA_DISCOVERY_CONFIG) - self.coresys = coresys - self.message_obj = {} + self.coresys: CoreSys = coresys + self.message_obj: Dict[str, Message] = {} - async def load(self): + async def load(self) -> None: """Load exists discovery message into storage.""" messages = {} for message in self._data[ATTR_DISCOVERY]: @@ -39,9 +54,9 @@ class Discovery(CoreSysAttributes, JsonConfig): _LOGGER.info("Load %d messages", len(messages)) self.message_obj = messages - def save(self): + def save(self) -> None: """Write discovery message into data file.""" - messages = [] + messages: List[Dict[str, Any]] = [] for message in self.list_messages: messages.append(attr.asdict(message)) @@ -49,22 +64,21 @@ class Discovery(CoreSysAttributes, JsonConfig): self._data[ATTR_DISCOVERY].extend(messages) self.save_data() - def get(self, uuid): + def get(self, uuid: str) -> Optional[Message]: """Return discovery message.""" return self.message_obj.get(uuid) @property - def list_messages(self): + def list_messages(self) -> List[Message]: """Return list of available discovery messages.""" return list(self.message_obj.values()) - def send(self, addon, service, config): + def send(self, addon: Addon, service: str, config: Dict[str, Any]) -> Message: """Send a discovery message to Home Assistant.""" try: - config = DISCOVERY_SERVICES[service](config) + config = valid_discovery_config(service, config) except vol.Invalid as err: - _LOGGER.error( - "Invalid discovery %s config", humanize_error(config, err)) + _LOGGER.error("Invalid discovery %s config", humanize_error(config, err)) raise DiscoveryError() from None # Create message @@ -77,24 +91,26 @@ class Discovery(CoreSysAttributes, JsonConfig): _LOGGER.info("Duplicate discovery message from %s", addon.slug) return old_message - _LOGGER.info("Send discovery to Home Assistant %s from %s", - service, addon.slug) + _LOGGER.info("Send discovery to Home Assistant %s from %s", service, addon.slug) self.message_obj[message.uuid] = message self.save() self.sys_create_task(self._push_discovery(message, CMD_NEW)) return message - def remove(self, message): + def remove(self, message: Message) -> None: """Remove a discovery message from Home Assistant.""" self.message_obj.pop(message.uuid, None) self.save() - _LOGGER.info("Delete discovery to Home Assistant %s from %s", - message.service, message.addon) + _LOGGER.info( + "Delete discovery to Home Assistant %s from %s", + message.service, + message.addon, + ) self.sys_create_task(self._push_discovery(message, CMD_DEL)) - async def _push_discovery(self, message, command): + async def _push_discovery(self, message: Message, command: str) -> None: """Send a discovery request.""" if not await self.sys_homeassistant.check_api_state(): _LOGGER.info("Discovery %s mesage ignore", message.uuid) @@ -105,18 +121,12 @@ class Discovery(CoreSysAttributes, JsonConfig): with suppress(HomeAssistantAPIError): async with self.sys_homeassistant.make_request( - command, f"api/hassio_push/discovery/{message.uuid}", - json=data, timeout=10): + 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", message.uuid) - - -@attr.s -class Message: - """Represent a single Discovery message.""" - addon = attr.ib() - service = attr.ib() - config = attr.ib(cmp=False) - uuid = attr.ib(factory=lambda: uuid4().hex, cmp=False) diff --git a/hassio/discovery/const.py b/hassio/discovery/const.py new file mode 100644 index 000000000..c85f248fd --- /dev/null +++ b/hassio/discovery/const.py @@ -0,0 +1,8 @@ +"""Discovery static data.""" + +ATTR_HOST = "host" +ATTR_PASSWORD = "password" +ATTR_PORT = "port" +ATTR_PROTOCOL = "protocol" +ATTR_SSL = "ssl" +ATTR_USERNAME = "username" diff --git a/hassio/discovery/services/__init__.py b/hassio/discovery/services/__init__.py new file mode 100644 index 000000000..5a423090d --- /dev/null +++ b/hassio/discovery/services/__init__.py @@ -0,0 +1 @@ +"""Discovery service modules.""" diff --git a/hassio/discovery/services/deconz.py b/hassio/discovery/services/deconz.py new file mode 100644 index 000000000..435640b2c --- /dev/null +++ b/hassio/discovery/services/deconz.py @@ -0,0 +1,11 @@ +"""Discovery service for MQTT.""" +import voluptuous as vol + +from hassio.validate import NETWORK_PORT + +from ..const import ATTR_HOST, ATTR_PORT + +# pylint: disable=no-value-for-parameter +SCHEMA = vol.Schema( + {vol.Required(ATTR_HOST): vol.Coerce(str), vol.Required(ATTR_PORT): NETWORK_PORT} +) diff --git a/hassio/discovery/services/mqtt.py b/hassio/discovery/services/mqtt.py new file mode 100644 index 000000000..abb673bf2 --- /dev/null +++ b/hassio/discovery/services/mqtt.py @@ -0,0 +1,27 @@ +"""Discovery service for MQTT.""" +import voluptuous as vol + +from hassio.validate import NETWORK_PORT + +from ..const import ( + ATTR_HOST, + ATTR_PASSWORD, + ATTR_PORT, + ATTR_PROTOCOL, + ATTR_SSL, + ATTR_USERNAME, +) + +# pylint: disable=no-value-for-parameter +SCHEMA = 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"]) + ), + } +) diff --git a/hassio/discovery/validate.py b/hassio/discovery/validate.py new file mode 100644 index 000000000..52d5db619 --- /dev/null +++ b/hassio/discovery/validate.py @@ -0,0 +1,47 @@ +"""Validate services schema.""" +from pathlib import Path +from importlib import import_module + +import voluptuous as vol + +from ..const import ATTR_ADDON, ATTR_CONFIG, ATTR_DISCOVERY, ATTR_SERVICE, ATTR_UUID +from ..utils.validate import schema_or +from ..validate import UUID_MATCH + + +def valid_discovery_service(service): + """Validate service name.""" + service_file = Path(__file__).parent.joinpath(f"services/{service}.py") + if not service_file.exists(): + raise vol.Invalid(f"Service {service} not found") + return service + + +def valid_discovery_config(service, config): + """Validate service name.""" + try: + service_mod = import_module(f".services.{service}", "hassio.discovery") + except ImportError: + raise vol.Invalid(f"Service {service} not found") + + return service_mod.SCHEMA(config) + + +SCHEMA_DISCOVERY = vol.Schema( + [ + vol.Schema( + { + vol.Required(ATTR_UUID): UUID_MATCH, + vol.Required(ATTR_ADDON): vol.Coerce(str), + vol.Required(ATTR_SERVICE): valid_discovery_service, + 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, +) diff --git a/hassio/services/__init__.py b/hassio/services/__init__.py index 1fec40187..bfe235cee 100644 --- a/hassio/services/__init__.py +++ b/hassio/services/__init__.py @@ -1,38 +1,38 @@ """Handle internal services discovery.""" -from .mqtt import MQTTService +from typing import Dict, List + +from ..coresys import CoreSys, CoreSysAttributes +from .const import SERVICE_MQTT from .data import ServicesData -from ..const import SERVICE_MQTT -from ..coresys import CoreSysAttributes +from .interface import ServiceInterface +from .modules.mqtt import MQTTService - -AVAILABLE_SERVICES = { - SERVICE_MQTT: MQTTService -} +AVAILABLE_SERVICES = {SERVICE_MQTT: MQTTService} class ServiceManager(CoreSysAttributes): """Handle internal services discovery.""" - def __init__(self, coresys): + def __init__(self, coresys: CoreSys): """Initialize Services handler.""" - self.coresys = coresys - self.data = ServicesData() - self.services_obj = {} + self.coresys: CoreSys = coresys + self.data: ServicesData = ServicesData() + self.services_obj: Dict[str, ServiceInterface] = {} @property - def list_services(self): + def list_services(self) -> List[ServiceInterface]: """Return a list of services.""" return list(self.services_obj.values()) - def get(self, slug): + def get(self, slug: str) -> ServiceInterface: """Return service object from slug.""" return self.services_obj.get(slug) - async def load(self): + async def load(self) -> None: """Load available services.""" for slug, service in AVAILABLE_SERVICES.items(): self.services_obj[slug] = service(self.coresys) - def reset(self): + def reset(self) -> None: """Reset available data.""" self.data.reset_data() diff --git a/hassio/services/const.py b/hassio/services/const.py new file mode 100644 index 000000000..f4a42e14c --- /dev/null +++ b/hassio/services/const.py @@ -0,0 +1,11 @@ +"""Service API static data.""" + +ATTR_ADDON = "addon" +ATTR_HOST = "host" +ATTR_PASSWORD = "password" +ATTR_PORT = "port" +ATTR_PROTOCOL = "protocol" +ATTR_SSL = "ssl" +ATTR_USERNAME = "username" + +SERVICE_MQTT = "mqtt" diff --git a/hassio/services/data.py b/hassio/services/data.py index 5df9df2d0..a77c81dcb 100644 --- a/hassio/services/data.py +++ b/hassio/services/data.py @@ -1,8 +1,10 @@ """Handle service data for persistent supervisor reboot.""" +from typing import Any, Dict -from .validate import SCHEMA_SERVICES_CONFIG -from ..const import FILE_HASSIO_SERVICES, SERVICE_MQTT +from ..const import FILE_HASSIO_SERVICES from ..utils.json import JsonConfig +from .const import SERVICE_MQTT +from .validate import SCHEMA_SERVICES_CONFIG class ServicesData(JsonConfig): @@ -13,6 +15,6 @@ class ServicesData(JsonConfig): super().__init__(FILE_HASSIO_SERVICES, SCHEMA_SERVICES_CONFIG) @property - def mqtt(self): + def mqtt(self) -> Dict[str, Any]: """Return settings for MQTT service.""" return self._data[SERVICE_MQTT] diff --git a/hassio/services/interface.py b/hassio/services/interface.py index 7215963e1..976f640a1 100644 --- a/hassio/services/interface.py +++ b/hassio/services/interface.py @@ -1,33 +1,37 @@ """Interface for single service.""" +from typing import Any, Dict, List, Optional -from ..coresys import CoreSysAttributes +import voluptuous as vol + +from ..addons.addon import Addon from ..const import PROVIDE_SERVICE +from ..coresys import CoreSys, CoreSysAttributes class ServiceInterface(CoreSysAttributes): """Interface class for service integration.""" - def __init__(self, coresys): + def __init__(self, coresys: CoreSys): """Initialize service interface.""" - self.coresys = coresys + self.coresys: CoreSys = coresys @property - def slug(self): + def slug(self) -> str: """Return slug of this service.""" - return None + raise NotImplementedError() @property - def _data(self): + def _data(self) -> Dict[str, Any]: """Return data of this service.""" - return None + raise NotImplementedError() @property - def schema(self): + def schema(self) -> vol.Schema: """Return data schema of this service.""" - return None + raise NotImplementedError() @property - def providers(self): + def providers(self) -> List[str]: """Return name of service providers addon.""" addons = [] for addon in self.sys_addons.list_installed: @@ -36,24 +40,24 @@ class ServiceInterface(CoreSysAttributes): return addons @property - def enabled(self): + def enabled(self) -> bool: """Return True if the service is in use.""" return bool(self._data) - def save(self): + def save(self) -> None: """Save changes.""" self.sys_services.data.save_data() - def get_service_data(self): + def get_service_data(self) -> Optional[Dict[str, Any]]: """Return the requested service data.""" if self.enabled: return self._data return None - def set_service_data(self, addon, data): + def set_service_data(self, addon: Addon, data: Dict[str, Any]) -> None: """Write the data into service object.""" raise NotImplementedError() - def del_service_data(self, addon): + def del_service_data(self, addon: Addon) -> None: """Remove the data from service object.""" raise NotImplementedError() diff --git a/hassio/services/modules/__init__.py b/hassio/services/modules/__init__.py new file mode 100644 index 000000000..d5ca020e1 --- /dev/null +++ b/hassio/services/modules/__init__.py @@ -0,0 +1 @@ +"""Services modules.""" diff --git a/hassio/services/modules/mqtt.py b/hassio/services/modules/mqtt.py new file mode 100644 index 000000000..9a85c80fd --- /dev/null +++ b/hassio/services/modules/mqtt.py @@ -0,0 +1,81 @@ +"""Provide the MQTT Service.""" +import logging +from typing import Any, Dict + +from hassio.addons.addon import Addon +from hassio.exceptions import ServicesError +from hassio.validate import NETWORK_PORT +import voluptuous as vol + +from ..const import ( + ATTR_ADDON, + ATTR_HOST, + ATTR_PASSWORD, + ATTR_PORT, + ATTR_PROTOCOL, + ATTR_SSL, + ATTR_USERNAME, + SERVICE_MQTT, +) +from ..interface import ServiceInterface + +_LOGGER = logging.getLogger(__name__) + + +# 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_ADDON): vol.Coerce(str)} +) + + +class MQTTService(ServiceInterface): + """Provide MQTT services.""" + + @property + def slug(self) -> str: + """Return slug of this service.""" + return SERVICE_MQTT + + @property + def _data(self) -> Dict[str, Any]: + """Return data of this service.""" + return self.sys_services.data.mqtt + + @property + def schema(self) -> vol.Schema: + """Return data schema of this service.""" + return SCHEMA_SERVICE_MQTT + + def set_service_data(self, addon: Addon, data: Dict[str, Any]) -> None: + """Write the data into service object.""" + if self.enabled: + _LOGGER.error("It is already a MQTT in use from %s", self._data[ATTR_ADDON]) + raise ServicesError() + + self._data.update(data) + self._data[ATTR_ADDON] = addon.slug + + _LOGGER.info("Set %s as service provider for mqtt", addon.slug) + self.save() + + def del_service_data(self, addon: Addon) -> None: + """Remove the data from service object.""" + if not self.enabled: + _LOGGER.warning("Can't remove not exists services") + raise ServicesError() + + self._data.clear() + self.save() diff --git a/hassio/services/mqtt.py b/hassio/services/mqtt.py deleted file mode 100644 index 05f98c292..000000000 --- a/hassio/services/mqtt.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Provide the MQTT Service.""" -import logging - -from .interface import ServiceInterface -from .validate import SCHEMA_SERVICE_MQTT -from ..const import ATTR_ADDON, SERVICE_MQTT -from ..exceptions import ServicesError - -_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.sys_services.data.mqtt - - @property - def schema(self): - """Return data schema of this service.""" - return SCHEMA_SERVICE_MQTT - - def set_service_data(self, addon, data): - """Write the data into service object.""" - if self.enabled: - _LOGGER.error( - "It is already a MQTT in use from %s", self._data[ATTR_ADDON]) - raise ServicesError() - - self._data.update(data) - self._data[ATTR_ADDON] = addon.slug - - _LOGGER.info("Set %s as service provider for mqtt", addon.slug) - self.save() - - def del_service_data(self, addon): - """Remove the data from service object.""" - if not self.enabled: - _LOGGER.warning("Can't remove not exists services") - raise ServicesError() - - self._data.clear() - self.save() diff --git a/hassio/services/validate.py b/hassio/services/validate.py index a7217be90..ff30217f1 100644 --- a/hassio/services/validate.py +++ b/hassio/services/validate.py @@ -1,35 +1,12 @@ """Validate services schema.""" import voluptuous as vol -from ..const import ( - SERVICE_MQTT, ATTR_HOST, ATTR_PORT, ATTR_PASSWORD, ATTR_USERNAME, ATTR_SSL, - ATTR_ADDON, ATTR_PROTOCOL) -from ..validate import NETWORK_PORT from ..utils.validate import schema_or +from .const import SERVICE_MQTT +from .modules.mqtt import SCHEMA_CONFIG_MQTT -# 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_ADDON): vol.Coerce(str), -}) - - -SCHEMA_SERVICES_CONFIG = vol.Schema({ - vol.Optional(SERVICE_MQTT, default=dict): schema_or(SCHEMA_CONFIG_MQTT), -}, extra=vol.REMOVE_EXTRA) - - -DISCOVERY_SERVICES = { - SERVICE_MQTT: SCHEMA_SERVICE_MQTT, -} +SCHEMA_SERVICES_CONFIG = vol.Schema( + {vol.Optional(SERVICE_MQTT, default=dict): schema_or(SCHEMA_CONFIG_MQTT)}, + extra=vol.REMOVE_EXTRA, +) diff --git a/hassio/validate.py b/hassio/validate.py index f5a9fdd50..c6189323f 100644 --- a/hassio/validate.py +++ b/hassio/validate.py @@ -5,14 +5,30 @@ import re import voluptuous as vol 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_CONFIG, - ATTR_WAIT_BOOT, ATTR_UUID, ATTR_REFRESH_TOKEN, ATTR_HASSOS_CLI, - ATTR_ACCESS_TOKEN, ATTR_DISCOVERY, ATTR_ADDON, ATTR_SERVICE, - SERVICE_MQTT, - CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV) -from .utils.validate import schema_or, validate_timezone + 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_WAIT_BOOT, + ATTR_UUID, + ATTR_REFRESH_TOKEN, + ATTR_HASSOS_CLI, + ATTR_ACCESS_TOKEN, + CHANNEL_STABLE, + CHANNEL_BETA, + CHANNEL_DEV, +) +from .utils.validate import validate_timezone RE_REPOSITORY = re.compile(r"^(?P[^#]+)(?:#(?P[\w\-]+))?$") @@ -24,7 +40,6 @@ 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}$") SHA256 = vol.Match(r"^[0-9a-f]{64}$") -SERVICE_ALL = vol.In([SERVICE_MQTT]) def validate_repository(repository): @@ -35,7 +50,7 @@ def validate_repository(repository): # Validate URL # pylint: disable=no-value-for-parameter - vol.Url()(data.group('url')) + vol.Url()(data.group("url")) return repository @@ -66,63 +81,61 @@ def convert_to_docker_ports(data): raise vol.Invalid("Can't validate Docker host settings") -DOCKER_PORTS = vol.Schema({ - vol.All(vol.Coerce(str), vol.Match(r"^\d+(?:/tcp|/udp)?$")): - convert_to_docker_ports, -}) +DOCKER_PORTS = vol.Schema( + { + vol.All( + vol.Coerce(str), vol.Match(r"^\d+(?:/tcp|/udp)?$") + ): convert_to_docker_ports + } +) # pylint: disable=no-value-for-parameter -SCHEMA_HASS_CONFIG = vol.Schema({ - vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): UUID_MATCH, - vol.Optional(ATTR_ACCESS_TOKEN): SHA256, - vol.Optional(ATTR_BOOT, default=True): vol.Boolean(), - vol.Inclusive(ATTR_IMAGE, 'custom_hass'): DOCKER_IMAGE, - vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str), - vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT, - vol.Optional(ATTR_PASSWORD): vol.Maybe(vol.Coerce(str)), - vol.Optional(ATTR_REFRESH_TOKEN): vol.Maybe(vol.Coerce(str)), - vol.Optional(ATTR_SSL, default=False): vol.Boolean(), - vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(), - vol.Optional(ATTR_WAIT_BOOT, default=600): - vol.All(vol.Coerce(int), vol.Range(min=60)), -}, extra=vol.REMOVE_EXTRA) +SCHEMA_HASS_CONFIG = vol.Schema( + { + vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): UUID_MATCH, + vol.Optional(ATTR_ACCESS_TOKEN): SHA256, + vol.Optional(ATTR_BOOT, default=True): vol.Boolean(), + vol.Inclusive(ATTR_IMAGE, "custom_hass"): DOCKER_IMAGE, + vol.Inclusive(ATTR_LAST_VERSION, "custom_hass"): vol.Coerce(str), + vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT, + vol.Optional(ATTR_PASSWORD): vol.Maybe(vol.Coerce(str)), + vol.Optional(ATTR_REFRESH_TOKEN): vol.Maybe(vol.Coerce(str)), + vol.Optional(ATTR_SSL, default=False): vol.Boolean(), + vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(), + vol.Optional(ATTR_WAIT_BOOT, default=600): vol.All( + vol.Coerce(int), vol.Range(min=60) + ), + }, + extra=vol.REMOVE_EXTRA, +) -SCHEMA_UPDATER_CONFIG = vol.Schema({ - vol.Optional(ATTR_CHANNEL, default=CHANNEL_STABLE): CHANNELS, - vol.Optional(ATTR_HOMEASSISTANT): vol.Coerce(str), - vol.Optional(ATTR_HASSIO): vol.Coerce(str), - vol.Optional(ATTR_HASSOS): vol.Coerce(str), - vol.Optional(ATTR_HASSOS_CLI): vol.Coerce(str), -}, extra=vol.REMOVE_EXTRA) +SCHEMA_UPDATER_CONFIG = vol.Schema( + { + vol.Optional(ATTR_CHANNEL, default=CHANNEL_STABLE): CHANNELS, + vol.Optional(ATTR_HOMEASSISTANT): vol.Coerce(str), + vol.Optional(ATTR_HASSIO): vol.Coerce(str), + vol.Optional(ATTR_HASSOS): vol.Coerce(str), + vol.Optional(ATTR_HASSOS_CLI): vol.Coerce(str), + }, + extra=vol.REMOVE_EXTRA, +) # pylint: disable=no-value-for-parameter -SCHEMA_HASSIO_CONFIG = vol.Schema({ - vol.Optional(ATTR_TIMEZONE, default='UTC'): validate_timezone, - vol.Optional(ATTR_LAST_BOOT): vol.Coerce(str), - vol.Optional(ATTR_ADDONS_CUSTOM_LIST, default=[ - "https://github.com/hassio-addons/repository", - ]): REPOSITORIES, - vol.Optional(ATTR_WAIT_BOOT, default=5): WAIT_BOOT, -}, extra=vol.REMOVE_EXTRA) +SCHEMA_HASSIO_CONFIG = vol.Schema( + { + vol.Optional(ATTR_TIMEZONE, default="UTC"): validate_timezone, + vol.Optional(ATTR_LAST_BOOT): vol.Coerce(str), + vol.Optional( + ATTR_ADDONS_CUSTOM_LIST, + default=["https://github.com/hassio-addons/repository"], + ): 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_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) - - -SCHEMA_AUTH_CONFIG = vol.Schema({ - SHA256: SHA256 -}) +SCHEMA_AUTH_CONFIG = vol.Schema({SHA256: SHA256}) diff --git a/tests/discovery/__init__.py b/tests/discovery/__init__.py new file mode 100644 index 000000000..ce066fb26 --- /dev/null +++ b/tests/discovery/__init__.py @@ -0,0 +1 @@ +"""Tests for discovery.""" diff --git a/tests/discovery/test_deconz.py b/tests/discovery/test_deconz.py new file mode 100644 index 000000000..092cd35cd --- /dev/null +++ b/tests/discovery/test_deconz.py @@ -0,0 +1,19 @@ +"""Test DeConz discovery.""" + +import voluptuous as vol +import pytest + +from hassio.discovery.validate import valid_discovery_config + + +def test_good_config(): + """Test good deconz config.""" + + valid_discovery_config("deconz", {"host": "test", "port": 3812}) + + +def test_bad_config(): + """Test good deconz config.""" + + with pytest.raises(vol.Invalid): + valid_discovery_config("deconz", {"host": "test"}) diff --git a/tests/discovery/test_mqtt.py b/tests/discovery/test_mqtt.py new file mode 100644 index 000000000..06fa9a39a --- /dev/null +++ b/tests/discovery/test_mqtt.py @@ -0,0 +1,21 @@ +"""Test MQTT discovery.""" + +import voluptuous as vol +import pytest + +from hassio.discovery.validate import valid_discovery_config + + +def test_good_config(): + """Test good mqtt config.""" + + valid_discovery_config( + "mqtt", {"host": "test", "port": 3812, "username": "bla", "ssl": True} + ) + + +def test_bad_config(): + """Test good mqtt config.""" + + with pytest.raises(vol.Invalid): + valid_discovery_config("mqtt", {"host": "test", "username": "bla", "ssl": True}) diff --git a/tests/discovery/test_validate.py b/tests/discovery/test_validate.py new file mode 100644 index 000000000..0aa6f7938 --- /dev/null +++ b/tests/discovery/test_validate.py @@ -0,0 +1,21 @@ +"""Test validate of discovery.""" + +import voluptuous as vol +import pytest + +from hassio.discovery import validate + + +def test_valid_services(): + """Validate that service is valid.""" + + for service in ("mqtt", "deconz"): + validate.valid_discovery_service(service) + + +def test_invalid_services(): + """Test that validate is invalid for a service.""" + + for service in ("fadsfasd", "203432"): + with pytest.raises(vol.Invalid): + validate.valid_discovery_service(service)