diff --git a/API.md b/API.md index 65e619a29..5123745bf 100644 --- a/API.md +++ b/API.md @@ -86,6 +86,19 @@ Reload addons/version. Output is the raw docker log. +- GET `/supervisor/stats` +```json +{ + "cpu_percent": 0.0, + "memory_usage": 283123, + "memory_limit": 329392, + "network_tx": 0, + "network_rx": 0, + "blk_read": 0, + "blk_write": 0 +} +``` + ### Security - GET `/security/info` @@ -334,6 +347,19 @@ Proxy to real home-assistant instance. Proxy to real websocket instance. +- GET `/homeassistant/stats` +```json +{ + "cpu_percent": 0.0, + "memory_usage": 283123, + "memory_limit": 329392, + "network_tx": 0, + "network_rx": 0, + "blk_read": 0, + "blk_write": 0 +} +``` + ### RESTful for API addons - GET `/addons` @@ -452,6 +478,19 @@ Only supported for local build addons Write data to add-on stdin +- GET `/addons/{addon}/stats` +```json +{ + "cpu_percent": 0.0, + "memory_usage": 283123, + "memory_limit": 329392, + "network_tx": 0, + "network_rx": 0, + "blk_read": 0, + "blk_write": 0 +} +``` + ## Host Control Communicate over UNIX socket with a host daemon. diff --git a/hassio/addons/addon.py b/hassio/addons/addon.py index 9dabb94a9..032a2ec56 100644 --- a/hassio/addons/addon.py +++ b/hassio/addons/addon.py @@ -568,7 +568,7 @@ class Addon(CoreSysAttributes): last_state = await self.state() if self.last_version == self.version_installed: - _LOGGER.info("No update available for Addon %s", self._id) + _LOGGER.warning("No update available for Addon %s", self._id) return False if not await self.instance.update(self.last_version): @@ -596,6 +596,14 @@ class Addon(CoreSysAttributes): """ return self.instance.logs() + @check_installed + def stats(self): + """Return stats of container. + + Return a coroutine. + """ + return self.instance.stats() + @check_installed async def rebuild(self): """Performe a rebuild of local build addon.""" diff --git a/hassio/api/__init__.py b/hassio/api/__init__.py index a9aa6dd76..5601d469d 100644 --- a/hassio/api/__init__.py +++ b/hassio/api/__init__.py @@ -69,6 +69,7 @@ class RestAPI(CoreSysAttributes): self.webapp.router.add_get('/supervisor/ping', api_supervisor.ping) self.webapp.router.add_get('/supervisor/info', api_supervisor.info) + self.webapp.router.add_get('/supervisor/stats', api_supervisor.stats) self.webapp.router.add_post( '/supervisor/update', api_supervisor.update) self.webapp.router.add_post( @@ -84,6 +85,7 @@ class RestAPI(CoreSysAttributes): self.webapp.router.add_get('/homeassistant/info', api_hass.info) self.webapp.router.add_get('/homeassistant/logs', api_hass.logs) + self.webapp.router.add_get('/homeassistant/stats', api_hass.stats) self.webapp.router.add_post('/homeassistant/options', api_hass.options) self.webapp.router.add_post('/homeassistant/update', api_hass.update) self.webapp.router.add_post('/homeassistant/restart', api_hass.restart) @@ -114,7 +116,6 @@ class RestAPI(CoreSysAttributes): self.webapp.router.add_get('/addons', api_addons.list) self.webapp.router.add_post('/addons/reload', api_addons.reload) - self.webapp.router.add_get('/addons/{addon}/info', api_addons.info) self.webapp.router.add_post( '/addons/{addon}/install', api_addons.install) @@ -135,6 +136,7 @@ class RestAPI(CoreSysAttributes): self.webapp.router.add_get( '/addons/{addon}/changelog', api_addons.changelog) self.webapp.router.add_post('/addons/{addon}/stdin', api_addons.stdin) + self.webapp.router.add_get('/addons/{addon}/stats', api_addons.stats) def _register_security(self): """Register security function.""" diff --git a/hassio/api/addons.py b/hassio/api/addons.py index 73d3a5ccf..bf670dd74 100644 --- a/hassio/api/addons.py +++ b/hassio/api/addons.py @@ -15,6 +15,8 @@ from ..const import ( ATTR_AUDIO, ATTR_AUDIO_INPUT, ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, BOOT_AUTO, BOOT_MANUAL, ATTR_CHANGELOG, ATTR_HOST_IPC, ATTR_HOST_DBUS, ATTR_LONG_DESCRIPTION, + ATTR_CPU_PERCENT, ATTR_MEMORY_LIMIT, ATTR_MEMORY_USAGE, ATTR_NETWORK_TX, + ATTR_NETWORK_RX, ATTR_BLK_READ, ATTR_BLK_WRITE, CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT) from ..coresys import CoreSysAttributes from ..validate import DOCKER_PORTS @@ -158,6 +160,25 @@ class APIAddons(CoreSysAttributes): return True + @api_process + async def stats(self, request): + """Return resource information.""" + addon = self._extract_addon(request) + stats = await addon.stats() + + if not stats: + raise RuntimeError("No stats available") + + return { + ATTR_CPU_PERCENT: stats.cpu_percent, + ATTR_MEMORY_USAGE: stats.memory_usage, + ATTR_MEMORY_LIMIT: stats.memory_limit, + ATTR_NETWORK_RX: stats.network_rx, + ATTR_NETWORK_TX: stats.network_tx, + ATTR_BLK_READ: stats.blk_read, + ATTR_BLK_WRITE: stats.blk_write, + } + @api_process def install(self, request): """Install addon.""" diff --git a/hassio/api/homeassistant.py b/hassio/api/homeassistant.py index d12a108bc..2fb8216bf 100644 --- a/hassio/api/homeassistant.py +++ b/hassio/api/homeassistant.py @@ -7,7 +7,9 @@ import voluptuous as vol from .utils import api_process, api_process_raw, api_validate from ..const import ( ATTR_VERSION, ATTR_LAST_VERSION, ATTR_IMAGE, ATTR_CUSTOM, ATTR_BOOT, - ATTR_PORT, ATTR_PASSWORD, ATTR_SSL, ATTR_WATCHDOG, CONTENT_TYPE_BINARY) + ATTR_PORT, ATTR_PASSWORD, ATTR_SSL, ATTR_WATCHDOG, ATTR_CPU_PERCENT, + ATTR_MEMORY_USAGE, ATTR_MEMORY_LIMIT, ATTR_NETWORK_RX, ATTR_NETWORK_TX, + ATTR_BLK_READ, ATTR_BLK_WRITE, CONTENT_TYPE_BINARY) from ..coresys import CoreSysAttributes from ..validate import NETWORK_PORT @@ -76,6 +78,23 @@ class APIHomeAssistant(CoreSysAttributes): self._homeassistant.save() return True + @api_process + async def stats(self, request): + """Return resource information.""" + stats = await self._homeassistant.stats() + if not stats: + raise RuntimeError("No stats available") + + return { + ATTR_CPU_PERCENT: stats.cpu_percent, + ATTR_MEMORY_USAGE: stats.memory_usage, + ATTR_MEMORY_LIMIT: stats.memory_limit, + ATTR_NETWORK_RX: stats.network_rx, + ATTR_NETWORK_TX: stats.network_tx, + ATTR_BLK_READ: stats.blk_read, + ATTR_BLK_WRITE: stats.blk_write, + } + @api_process async def update(self, request): """Update homeassistant.""" diff --git a/hassio/api/panel/hassio-main-es5.html b/hassio/api/panel/hassio-main-es5.html index 8668928e7..54fc052e5 100644 --- a/hassio/api/panel/hassio-main-es5.html +++ b/hassio/api/panel/hassio-main-es5.html @@ -34,7 +34,7 @@ 0 6px 30px 5px rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(0, 0, 0, 0.4);};--shadow-elevation-24dp:{box-shadow:0 24px 38px 3px rgba(0, 0, 0, 0.14), 0 9px 46px 8px rgba(0, 0, 0, 0.12), - 0 11px 15px -7px rgba(0, 0, 0, 0.4);};} \ No newline at end of file diff --git a/hassio/api/panel/hassio-main-es5.html.gz b/hassio/api/panel/hassio-main-es5.html.gz index 6b833732f..5b1deb7ed 100644 Binary files a/hassio/api/panel/hassio-main-es5.html.gz and b/hassio/api/panel/hassio-main-es5.html.gz differ diff --git a/hassio/api/panel/hassio-main-latest.html b/hassio/api/panel/hassio-main-latest.html index 37e56120e..997972c22 100644 --- a/hassio/api/panel/hassio-main-latest.html +++ b/hassio/api/panel/hassio-main-latest.html @@ -34,7 +34,7 @@ 0 6px 30px 5px rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(0, 0, 0, 0.4);};--shadow-elevation-24dp:{box-shadow:0 24px 38px 3px rgba(0, 0, 0, 0.14), 0 9px 46px 8px rgba(0, 0, 0, 0.12), - 0 11px 15px -7px rgba(0, 0, 0, 0.4);};} \ No newline at end of file diff --git a/hassio/api/panel/hassio-main-latest.html.gz b/hassio/api/panel/hassio-main-latest.html.gz index 3e0c358b9..da3dfe81a 100644 Binary files a/hassio/api/panel/hassio-main-latest.html.gz and b/hassio/api/panel/hassio-main-latest.html.gz differ diff --git a/hassio/api/supervisor.py b/hassio/api/supervisor.py index bd32482ac..efa0e25ef 100644 --- a/hassio/api/supervisor.py +++ b/hassio/api/supervisor.py @@ -9,7 +9,9 @@ from ..const import ( ATTR_ADDONS, ATTR_VERSION, ATTR_LAST_VERSION, ATTR_BETA_CHANNEL, ATTR_ARCH, HASSIO_VERSION, ATTR_ADDONS_REPOSITORIES, ATTR_LOGO, ATTR_REPOSITORY, ATTR_DESCRIPTON, ATTR_NAME, ATTR_SLUG, ATTR_INSTALLED, ATTR_TIMEZONE, - ATTR_STATE, ATTR_WAIT_BOOT, CONTENT_TYPE_BINARY) + ATTR_STATE, ATTR_WAIT_BOOT, ATTR_CPU_PERCENT, ATTR_MEMORY_USAGE, + ATTR_MEMORY_LIMIT, ATTR_NETWORK_RX, ATTR_NETWORK_TX, ATTR_BLK_READ, + ATTR_BLK_WRITE, CONTENT_TYPE_BINARY) from ..coresys import CoreSysAttributes from ..validate import validate_timezone, WAIT_BOOT @@ -86,6 +88,23 @@ class APISupervisor(CoreSysAttributes): self._config.save() return True + @api_process + async def stats(self, request): + """Return resource information.""" + stats = await self._supervisor.stats() + if not stats: + raise RuntimeError("No stats available") + + return { + ATTR_CPU_PERCENT: stats.cpu_percent, + ATTR_MEMORY_USAGE: stats.memory_usage, + ATTR_MEMORY_LIMIT: stats.memory_limit, + ATTR_NETWORK_RX: stats.network_rx, + ATTR_NETWORK_TX: stats.network_tx, + ATTR_BLK_READ: stats.blk_read, + ATTR_BLK_WRITE: stats.blk_write, + } + @api_process async def update(self, request): """Update supervisor OS.""" diff --git a/hassio/const.py b/hassio/const.py index 0b50bb675..c26ecdf2e 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -2,7 +2,7 @@ from pathlib import Path from ipaddress import ip_network -HASSIO_VERSION = '0.79' +HASSIO_VERSION = '0.80' URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/' 'hassio/{}/version.json') @@ -128,6 +128,13 @@ ATTR_SQUASH = 'squash' ATTR_GPIO = 'gpio' ATTR_LEGACY = 'legacy' ATTR_ADDONS_CUSTOM_LIST = 'addons_custom_list' +ATTR_CPU_PERCENT = 'cpu_percent' +ATTR_NETWORK_RX = 'network_rx' +ATTR_NETWORK_TX = 'network_tx' +ATTR_MEMORY_LIMIT = 'memory_limit' +ATTR_MEMORY_USAGE = 'memory_usage' +ATTR_BLK_READ = 'blk_read' +ATTR_BLK_WRITE = 'blk_write' STARTUP_INITIALIZE = 'initialize' STARTUP_SYSTEM = 'system' diff --git a/hassio/coresys.py b/hassio/coresys.py index 522b1305e..782fa3480 100644 --- a/hassio/coresys.py +++ b/hassio/coresys.py @@ -4,10 +4,10 @@ import aiohttp from .config import CoreConfig from .docker import DockerAPI -from .dns import DNSForward -from .hardware import Hardware -from .host_control import HostControl -from .scheduler import Scheduler +from .misc.dns import DNSForward +from .misc.hardware import Hardware +from .misc.host_control import HostControl +from .misc.scheduler import Scheduler class CoreSys(object): diff --git a/hassio/docker/interface.py b/hassio/docker/interface.py index fd44e943a..99596de37 100644 --- a/hassio/docker/interface.py +++ b/hassio/docker/interface.py @@ -6,6 +6,7 @@ import logging import docker from .utils import docker_process +from .stats import DockerStats from ..const import LABEL_VERSION, LABEL_ARCH from ..coresys import CoreSysAttributes @@ -325,3 +326,24 @@ class DockerInterface(CoreSysAttributes): Need run inside executor. """ raise NotImplementedError() + + def stats(self): + """Read and return stats from container.""" + return self._loop.run_in_executor(None, self._stats) + + def _stats(self): + """Create a temporary container and run command. + + Need run inside executor. + """ + try: + container = self._docker.containers.get(self.name) + except docker.errors.DockerException: + return None + + try: + stats = container.stats(stream=False) + return DockerStats(stats) + except docker.errors.DockerException as err: + _LOGGER.error("Can't read stats from %s: %s", self.name, err) + return None diff --git a/hassio/docker/stats.py b/hassio/docker/stats.py new file mode 100644 index 000000000..be6bb4b7a --- /dev/null +++ b/hassio/docker/stats.py @@ -0,0 +1,90 @@ +"""Calc & represent docker stats data.""" +from contextlib import suppress + + +class DockerStats(object): + """Hold stats data from container inside.""" + + def __init__(self, stats): + """Initialize docker stats.""" + self._cpu = 0.0 + self._network_rx = 0 + self._network_tx = 0 + self._blk_read = 0 + self._blk_write = 0 + + try: + self._memory_usage = stats['memory_stats']['usage'] + self._memory_limit = stats['memory_stats']['limit'] + except KeyError: + self._memory_usage = 0 + self._memory_limit = 0 + + with suppress(KeyError): + self._calc_cpu_percent(stats) + + with suppress(KeyError): + self._calc_network(stats['networks']) + + with suppress(KeyError): + self._calc_block_io(stats['blkio_stats']) + + def _calc_cpu_percent(self, stats): + """Calculate CPU percent.""" + cpu_delta = stats['cpu_stats']['cpu_usage']['total_usage'] - \ + stats['precpu_stats']['cpu_usage']['total_usage'] + system_delta = stats['cpu_stats']['system_cpu_usage'] - \ + stats['precpu_stats']['system_cpu_usage'] + + if system_delta > 0.0 and cpu_delta > 0.0: + self._cpu = (cpu_delta / system_delta) * \ + len(stats['cpu_stats']['cpu_usage']['percpu_usage']) * 100.0 + + def _calc_network(self, networks): + """Calculate Network IO stats.""" + for _, stats in networks.items(): + self._network_rx += stats['rx_bytes'] + self._network_tx += stats['tx_bytes'] + + def _calc_block_io(self, blkio): + """Calculate block IO stats.""" + for stats in blkio['io_service_bytes_recursive']: + if stats['op'] == 'Read': + self._blk_read += stats['value'] + elif stats['op'] == 'Write': + self._blk_write += stats['value'] + + @property + def cpu_percent(self): + """Return CPU percent.""" + return self._cpu + + @property + def memory_usage(self): + """Return memory usage.""" + return self._memory_usage + + @property + def memory_limit(self): + """Return memory limit.""" + return self._memory_limit + + @property + def network_rx(self): + """Return network rx stats.""" + return self._network_rx + + @property + def network_tx(self): + """Return network rx stats.""" + return self._network_tx + + @property + def blk_read(self): + """Return block IO read stats.""" + return self._blk_read + + @property + def blk_write(self): + """Return block IO write stats.""" + return self._blk_write diff --git a/hassio/homeassistant.py b/hassio/homeassistant.py index 29ec28001..6ee12289d 100644 --- a/hassio/homeassistant.py +++ b/hassio/homeassistant.py @@ -219,6 +219,13 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): """ return self.instance.logs() + def stats(self): + """Return stats of HomeAssistant. + + Return a coroutine. + """ + return self.instance.stats() + def is_running(self): """Return True if docker container is running. diff --git a/hassio/misc/__init__.py b/hassio/misc/__init__.py new file mode 100644 index 000000000..0b6c51de2 --- /dev/null +++ b/hassio/misc/__init__.py @@ -0,0 +1 @@ +"""Special object and tools for Hass.io.""" diff --git a/hassio/dns.py b/hassio/misc/dns.py similarity index 100% rename from hassio/dns.py rename to hassio/misc/dns.py diff --git a/hassio/hardware.py b/hassio/misc/hardware.py similarity index 98% rename from hassio/hardware.py rename to hassio/misc/hardware.py index 4f3ea8961..66060d0c4 100644 --- a/hassio/hardware.py +++ b/hassio/misc/hardware.py @@ -6,7 +6,7 @@ import re import pyudev -from .const import ATTR_NAME, ATTR_TYPE, ATTR_DEVICES +from ..const import ATTR_NAME, ATTR_TYPE, ATTR_DEVICES _LOGGER = logging.getLogger(__name__) diff --git a/hassio/host_control.py b/hassio/misc/host_control.py similarity index 99% rename from hassio/host_control.py rename to hassio/misc/host_control.py index 54290e406..848a7e0aa 100644 --- a/hassio/host_control.py +++ b/hassio/misc/host_control.py @@ -5,7 +5,7 @@ import logging import async_timeout -from .const import ( +from ..const import ( SOCKET_HC, ATTR_LAST_VERSION, ATTR_VERSION, ATTR_TYPE, ATTR_FEATURES, ATTR_HOSTNAME, ATTR_OS) diff --git a/hassio/scheduler.py b/hassio/misc/scheduler.py similarity index 100% rename from hassio/scheduler.py rename to hassio/misc/scheduler.py diff --git a/hassio/supervisor.py b/hassio/supervisor.py index 567f1ba50..e8556a95e 100644 --- a/hassio/supervisor.py +++ b/hassio/supervisor.py @@ -46,14 +46,16 @@ class Supervisor(CoreSysAttributes): version = version or self.last_version if version == self._supervisor.version: - _LOGGER.info("Version %s is already installed", version) + _LOGGER.warning("Version %s is already installed", version) return _LOGGER.info("Update supervisor to version %s", version) if await self.instance.install(version): self._loop.call_later(1, self._loop.stop) - else: - _LOGGER.error("Update of hass.io fails!") + return True + + _LOGGER.error("Update of hass.io fails!") + return False @property def in_progress(self): @@ -66,3 +68,10 @@ class Supervisor(CoreSysAttributes): Return a coroutine. """ return self.instance.logs() + + def stats(self): + """Return stats of Supervisor. + + Return a coroutine. + """ + return self.instance.stats() diff --git a/home-assistant-polymer b/home-assistant-polymer index 9b9cba86c..ea16ebd4f 160000 --- a/home-assistant-polymer +++ b/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 9b9cba86c28bf7710a84ac00e065e0165c61754f +Subproject commit ea16ebd4f0fe90ef3c4623875ddc0b53f79ce3ae diff --git a/setup.py b/setup.py index fda2152c0..7b17c82c1 100644 --- a/setup.py +++ b/setup.py @@ -31,10 +31,11 @@ setup( platforms='any', packages=[ 'hassio', - 'hassio.utils', 'hassio.docker', - 'hassio.api', 'hassio.addons', + 'hassio.api', + 'hassio.misc', + 'hassio.utils', 'hassio.snapshots' ], include_package_data=True, diff --git a/tox.ini b/tox.ini index 050010d69..14c887d08 100644 --- a/tox.ini +++ b/tox.ini @@ -10,5 +10,5 @@ deps = basepython = python3 ignore_errors = True commands = - flake8 + flake8 hassio pylint --rcfile pylintrc hassio diff --git a/version.json b/version.json index f276bd266..ccfbf21e2 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "hassio": "0.79", + "hassio": "0.80", "homeassistant": "0.60.1", "resinos": "1.1", "resinhup": "0.3",