diff --git a/API.md b/API.md index 58e635a72..a6fc1b6da 100644 --- a/API.md +++ b/API.md @@ -502,6 +502,7 @@ Get all available addons. "privileged": ["NET_ADMIN", "SYS_ADMIN"], "apparmor": "disable|default|profile", "devices": ["/dev/xy"], + "udev": "bool", "auto_uart": "bool", "icon": "bool", "logo": "bool", diff --git a/hassio/addons/model.py b/hassio/addons/model.py index 8c185f24f..5e4bb20a3 100644 --- a/hassio/addons/model.py +++ b/hassio/addons/model.py @@ -51,6 +51,7 @@ from ..const import ( ATTR_STDIN, ATTR_TIMEOUT, ATTR_TMPFS, + ATTR_UDEV, ATTR_URL, ATTR_VERSION, ATTR_WEBUI, @@ -343,6 +344,11 @@ class AddonModel(CoreSysAttributes): """Return True if the add-on access to GPIO interface.""" return self.data[ATTR_GPIO] + @property + def with_udev(self) -> bool: + """Return True if the add-on have his own udev.""" + return self.data[ATTR_UDEV] + @property def with_kernel_modules(self) -> bool: """Return True if the add-on access to kernel modules.""" diff --git a/hassio/addons/validate.py b/hassio/addons/validate.py index fe695c793..6ea3911c1 100644 --- a/hassio/addons/validate.py +++ b/hassio/addons/validate.py @@ -68,6 +68,7 @@ from ..const import ( ATTR_SYSTEM, ATTR_TIMEOUT, ATTR_TMPFS, + ATTR_UDEV, ATTR_URL, ATTR_USER, ATTR_UUID, @@ -186,6 +187,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema( vol.Optional(ATTR_HOST_DBUS, default=False): vol.Boolean(), vol.Optional(ATTR_DEVICES): [vol.Match(r"^(.*):(.*):([rwm]{1,3})$")], vol.Optional(ATTR_AUTO_UART, default=False): vol.Boolean(), + vol.Optional(ATTR_UDEV, default=False): vol.Boolean(), vol.Optional(ATTR_TMPFS): vol.Match(r"^size=(\d)*[kmg](,uid=\d{1,4})?(,rw)?$"), vol.Optional(ATTR_MAP, default=list): [vol.Match(RE_VOLUME)], vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): vol.Coerce(str)}, diff --git a/hassio/api/addons.py b/hassio/api/addons.py index c2a3feec3..978034a93 100644 --- a/hassio/api/addons.py +++ b/hassio/api/addons.py @@ -8,6 +8,7 @@ import voluptuous as vol from voluptuous.humanize import humanize_error from ..addons import AnyAddon +from ..docker.stats import DockerStats from ..addons.utils import rating_security from ..const import ( ATTR_ADDONS, @@ -58,8 +59,8 @@ from ..const import ( ATTR_MACHINE, ATTR_MAINTAINER, ATTR_MEMORY_LIMIT, - ATTR_MEMORY_USAGE, ATTR_MEMORY_PERCENT, + ATTR_MEMORY_USAGE, ATTR_NAME, ATTR_NETWORK, ATTR_NETWORK_DESCRIPTION, @@ -76,6 +77,7 @@ from ..const import ( ATTR_SOURCE, ATTR_STATE, ATTR_STDIN, + ATTR_UDEV, ATTR_URL, ATTR_VERSION, ATTR_WEBUI, @@ -119,7 +121,7 @@ class APIAddons(CoreSysAttributes): self, request: web.Request, check_installed: bool = True ) -> AnyAddon: """Return addon, throw an exception it it doesn't exist.""" - addon_slug = request.match_info.get("addon") + addon_slug: str = request.match_info.get("addon") # Lookup itself if addon_slug == "self": @@ -178,7 +180,7 @@ class APIAddons(CoreSysAttributes): @api_process async def info(self, request: web.Request) -> Dict[str, Any]: """Return add-on information.""" - addon = self._extract_addon(request, check_installed=False) + addon: AnyAddon = self._extract_addon(request, check_installed=False) data = { ATTR_NAME: addon.name, @@ -225,6 +227,7 @@ class APIAddons(CoreSysAttributes): ATTR_GPIO: addon.with_gpio, ATTR_KERNEL_MODULES: addon.with_kernel_modules, ATTR_DEVICETREE: addon.with_devicetree, + ATTR_UDEV: addon.with_udev, ATTR_DOCKER_API: addon.access_docker_api, ATTR_AUDIO: addon.with_audio, ATTR_AUDIO_INPUT: None, @@ -261,12 +264,12 @@ class APIAddons(CoreSysAttributes): @api_process async def options(self, request: web.Request) -> None: """Store user options for add-on.""" - addon = self._extract_addon(request) + addon: AnyAddon = self._extract_addon(request) addon_schema = SCHEMA_OPTIONS.extend( {vol.Optional(ATTR_OPTIONS): vol.Any(None, addon.schema)} ) - body = await api_validate(addon_schema, request) + body: Dict[str, Any] = await api_validate(addon_schema, request) if ATTR_OPTIONS in body: addon.options = body[ATTR_OPTIONS] @@ -289,8 +292,8 @@ class APIAddons(CoreSysAttributes): @api_process async def security(self, request: web.Request) -> None: """Store security options for add-on.""" - addon = self._extract_addon(request) - body = await api_validate(SCHEMA_SECURITY, request) + addon: AnyAddon = self._extract_addon(request) + body: Dict[str, Any] = await api_validate(SCHEMA_SECURITY, request) if ATTR_PROTECTED in body: _LOGGER.warning("Protected flag changing for %s!", addon.slug) @@ -301,8 +304,8 @@ class APIAddons(CoreSysAttributes): @api_process async def stats(self, request: web.Request) -> Dict[str, Any]: """Return resource information.""" - addon = self._extract_addon(request) - stats = await addon.stats() + addon: AnyAddon = self._extract_addon(request) + stats: DockerStats = await addon.stats() return { ATTR_CPU_PERCENT: stats.cpu_percent, @@ -318,19 +321,19 @@ class APIAddons(CoreSysAttributes): @api_process def install(self, request: web.Request) -> Awaitable[None]: """Install add-on.""" - addon = self._extract_addon(request, check_installed=False) + addon: AnyAddon = self._extract_addon(request, check_installed=False) return asyncio.shield(addon.install()) @api_process def uninstall(self, request: web.Request) -> Awaitable[None]: """Uninstall add-on.""" - addon = self._extract_addon(request) + addon: AnyAddon = self._extract_addon(request) return asyncio.shield(addon.uninstall()) @api_process def start(self, request: web.Request) -> Awaitable[None]: """Start add-on.""" - addon = self._extract_addon(request) + addon: AnyAddon = self._extract_addon(request) # check options options = addon.options @@ -344,13 +347,13 @@ class APIAddons(CoreSysAttributes): @api_process def stop(self, request: web.Request) -> Awaitable[None]: """Stop add-on.""" - addon = self._extract_addon(request) + addon: AnyAddon = self._extract_addon(request) return asyncio.shield(addon.stop()) @api_process def update(self, request: web.Request) -> Awaitable[None]: """Update add-on.""" - addon = self._extract_addon(request) + addon: AnyAddon = self._extract_addon(request) if addon.latest_version == addon.version: raise APIError("No update available!") @@ -360,13 +363,13 @@ class APIAddons(CoreSysAttributes): @api_process def restart(self, request: web.Request) -> Awaitable[None]: """Restart add-on.""" - addon = self._extract_addon(request) + addon: AnyAddon = self._extract_addon(request) return asyncio.shield(addon.restart()) @api_process def rebuild(self, request: web.Request) -> Awaitable[None]: """Rebuild local build add-on.""" - addon = self._extract_addon(request) + addon: AnyAddon = self._extract_addon(request) if not addon.need_build: raise APIError("Only local build addons are supported") @@ -375,13 +378,13 @@ class APIAddons(CoreSysAttributes): @api_process_raw(CONTENT_TYPE_BINARY) def logs(self, request: web.Request) -> Awaitable[bytes]: """Return logs from add-on.""" - addon = self._extract_addon(request) + addon: AnyAddon = self._extract_addon(request) return addon.logs() @api_process_raw(CONTENT_TYPE_PNG) async def icon(self, request: web.Request) -> bytes: """Return icon from add-on.""" - addon = self._extract_addon(request, check_installed=False) + addon: AnyAddon = self._extract_addon(request, check_installed=False) if not addon.with_icon: raise APIError("No icon found!") @@ -391,7 +394,7 @@ class APIAddons(CoreSysAttributes): @api_process_raw(CONTENT_TYPE_PNG) async def logo(self, request: web.Request) -> bytes: """Return logo from add-on.""" - addon = self._extract_addon(request, check_installed=False) + addon: AnyAddon = self._extract_addon(request, check_installed=False) if not addon.with_logo: raise APIError("No logo found!") @@ -401,7 +404,7 @@ class APIAddons(CoreSysAttributes): @api_process_raw(CONTENT_TYPE_TEXT) async def changelog(self, request: web.Request) -> str: """Return changelog from add-on.""" - addon = self._extract_addon(request, check_installed=False) + addon: AnyAddon = self._extract_addon(request, check_installed=False) if not addon.with_changelog: raise APIError("No changelog found!") @@ -411,7 +414,7 @@ class APIAddons(CoreSysAttributes): @api_process async def stdin(self, request: web.Request) -> None: """Write to stdin of add-on.""" - addon = self._extract_addon(request) + addon: AnyAddon = self._extract_addon(request) if not addon.with_stdin: raise APIError("STDIN not supported by add-on") diff --git a/hassio/api/hardware.py b/hassio/api/hardware.py index afb6fae8c..80bcc35bc 100644 --- a/hassio/api/hardware.py +++ b/hassio/api/hardware.py @@ -22,7 +22,9 @@ class APIHardware(CoreSysAttributes): async def info(self, request): """Show hardware info.""" return { - ATTR_SERIAL: list(self.sys_hardware.serial_devices), + ATTR_SERIAL: list( + self.sys_hardware.serial_devices | self.sys_hardware.serial_by_id + ), ATTR_INPUT: list(self.sys_hardware.input_devices), ATTR_DISK: list(self.sys_hardware.disk_devices), ATTR_GPIO: list(self.sys_hardware.gpio_devices), diff --git a/hassio/const.py b/hassio/const.py index db6984ac9..a6447cea4 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -218,6 +218,7 @@ ATTR_DEBUG = "debug" ATTR_DEBUG_BLOCK = "debug_block" ATTR_DNS = "dns" ATTR_SERVERS = "servers" +ATTR_UDEV = "udev" PROVIDE_SERVICE = "provide" NEED_SERVICE = "need" diff --git a/hassio/docker/addon.py b/hassio/docker/addon.py index 40608ff23..36aa7a55e 100644 --- a/hassio/docker/addon.py +++ b/hassio/docker/addon.py @@ -135,7 +135,14 @@ class DockerAddon(DockerInterface): # Auto mapping UART devices if self.addon.auto_uart: - for device in self.sys_hardware.serial_devices: + if self.addon.with_udev: + serial_devs = self.sys_hardware.serial_devices + else: + serial_devs = ( + self.sys_hardware.serial_devices | self.sys_hardware.serial_by_id + ) + + for device in serial_devs: devices.append(f"{device}:{device}:rwm") # Return None if no devices is present diff --git a/hassio/misc/hardware.py b/hassio/misc/hardware.py index eb79708e4..f3038bc1b 100644 --- a/hassio/misc/hardware.py +++ b/hassio/misc/hardware.py @@ -3,25 +3,26 @@ from datetime import datetime import logging from pathlib import Path import re +from typing import Any, Dict, Optional, Set import pyudev -from ..const import ATTR_NAME, ATTR_TYPE, ATTR_DEVICES, CHAN_ID, CHAN_TYPE +from ..const import ATTR_DEVICES, ATTR_NAME, ATTR_TYPE, CHAN_ID, CHAN_TYPE _LOGGER = logging.getLogger(__name__) -ASOUND_CARDS = Path("/proc/asound/cards") -RE_CARDS = re.compile(r"(\d+) \[(\w*) *\]: (.*\w)") +ASOUND_CARDS: Path = Path("/proc/asound/cards") +RE_CARDS: re.Pattern = re.compile(r"(\d+) \[(\w*) *\]: (.*\w)") -ASOUND_DEVICES = Path("/proc/asound/devices") -RE_DEVICES = re.compile(r"\[.*(\d+)- (\d+).*\]: ([\w ]*)") +ASOUND_DEVICES: Path = Path("/proc/asound/devices") +RE_DEVICES: re.Pattern = re.compile(r"\[.*(\d+)- (\d+).*\]: ([\w ]*)") -PROC_STAT = Path("/proc/stat") -RE_BOOT_TIME = re.compile(r"btime (\d+)") +PROC_STAT: Path = Path("/proc/stat") +RE_BOOT_TIME: re.Pattern = re.compile(r"btime (\d+)") -GPIO_DEVICES = Path("/sys/class/gpio") -SOC_DEVICES = Path("/sys/devices/platform/soc") -RE_TTY = re.compile(r"tty[A-Z]+") +GPIO_DEVICES: Path = Path("/sys/class/gpio") +SOC_DEVICES: Path = Path("/sys/devices/platform/soc") +RE_TTY: re.Pattern = re.compile(r"tty[A-Z]+") class Hardware: @@ -32,13 +33,21 @@ class Hardware: self.context = pyudev.Context() @property - def serial_devices(self): + def serial_devices(self) -> Set[str]: """Return all serial and connected devices.""" - dev_list = set() + dev_list: Set[str] = set() for device in self.context.list_devices(subsystem="tty"): if "ID_VENDOR" in device.properties or RE_TTY.search(device.device_node): dev_list.add(device.device_node) + return dev_list + + @property + def serial_by_id(self) -> Set[str]: + """Return all /dev/serial/by-id for serial devices.""" + dev_list: Set[str] = set() + for device in self.context.list_devices(subsystem="tty"): + if "ID_VENDOR" in device.properties or RE_TTY.search(device.device_node): # Add /dev/serial/by-id devlink for current device for dev_link in device.device_links: if not dev_link.startswith("/dev/serial/by-id"): @@ -48,9 +57,9 @@ class Hardware: return dev_list @property - def input_devices(self): + def input_devices(self) -> Set[str]: """Return all input devices.""" - dev_list = set() + dev_list: Set[str] = set() for device in self.context.list_devices(subsystem="input"): if "NAME" in device.properties: dev_list.add(device.properties["NAME"].replace('"', "")) @@ -58,9 +67,9 @@ class Hardware: return dev_list @property - def disk_devices(self): + def disk_devices(self) -> Set[str]: """Return all disk devices.""" - dev_list = set() + dev_list: Set[str] = set() for device in self.context.list_devices(subsystem="block"): if "ID_NAME" in device.properties: dev_list.add(device.device_node) @@ -68,15 +77,15 @@ class Hardware: return dev_list @property - def support_audio(self): + def support_audio(self) -> bool: """Return True if the system have audio support.""" return bool(self.audio_devices) @property - def audio_devices(self): + def audio_devices(self) -> Dict[str, Any]: """Return all available audio interfaces.""" if not ASOUND_CARDS.exists(): - _LOGGER.debug("No audio devices found") + _LOGGER.info("No audio devices found") return {} try: @@ -86,7 +95,7 @@ class Hardware: _LOGGER.error("Can't read asound data: %s", err) return {} - audio_list = {} + audio_list: Dict[str, Any] = {} # parse cards for match in RE_CARDS.finditer(cards): @@ -109,31 +118,31 @@ class Hardware: return audio_list @property - def support_gpio(self): + def support_gpio(self) -> bool: """Return True if device support GPIOs.""" return SOC_DEVICES.exists() and GPIO_DEVICES.exists() @property - def gpio_devices(self): + def gpio_devices(self) -> Set[str]: """Return list of GPIO interface on device.""" - dev_list = set() + dev_list: Set[str] = set() for interface in GPIO_DEVICES.glob("gpio*"): dev_list.add(interface.name) return dev_list @property - def last_boot(self): + def last_boot(self) -> Optional[str]: """Return last boot time.""" try: with PROC_STAT.open("r") as stat_file: - stats = stat_file.read() + stats: str = stat_file.read() except OSError as err: _LOGGER.error("Can't read stat data: %s", err) return None # parse stat file - found = RE_BOOT_TIME.search(stats) + found: Optional[re.Match] = RE_BOOT_TIME.search(stats) if not found: _LOGGER.error("Can't found last boot time!") return None