diff --git a/README.md b/README.md index a9aa02a..4472c20 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,12 @@ docker_monitor: | --------------------------------- | ------------------------------- | ----- | | version | Docker version | - | +| containers_total | Total number of containers | - | +| containers_paused | Number of paused containers | - | +| containers_running | Number of running containers | - | +| containers_stopped | Number of stopped containers | - | +| images_total | Total number of images | - | + | Container Conditions | Description | Unit | | --------------------------------- | ------------------------------- | ----- | | status | Container status | - | diff --git a/custom_components/docker_monitor/__init__.py b/custom_components/docker_monitor/__init__.py index 7cdf658..994d193 100644 --- a/custom_components/docker_monitor/__init__.py +++ b/custom_components/docker_monitor/__init__.py @@ -34,8 +34,6 @@ ) from .helpers import DockerMonitorApi -VERSION = '0.1.0-b0' - _LOGGER = logging.getLogger(__name__) CONF_MONITOR_UTILISATION_CONDITIONS_KEYS = list( diff --git a/custom_components/docker_monitor/const.py b/custom_components/docker_monitor/const.py index be3c169..97c2ad0 100644 --- a/custom_components/docker_monitor/const.py +++ b/custom_components/docker_monitor/const.py @@ -16,7 +16,13 @@ CONF_CONTAINER_SWITCH = 'switch' CONF_MONITOR_UTILISATION_VERSION = 'version' +CONF_MONITOR_UTILISATION_CONTAINERS_TOTAL = 'containers_total' +CONF_MONITOR_UTILISATION_CONTAINERS_PAUSED = 'containers_paused' +CONF_MONITOR_UTILISATION_CONTAINERS_RUNNING = 'containers_running' +CONF_MONITOR_UTILISATION_CONTAINERS_STOPPED = 'containers_stopped' +CONF_MONITOR_UTILISATION_IMAGES_TOTAL = 'images_total' +CONF_MONITOR_CONTAINER_STATE = 'state' CONF_MONITOR_CONTAINER_STATUS = 'status' CONF_MONITOR_CONTAINER_UPTIME = 'uptime' CONF_MONITOR_CONTAINER_IMAGE = 'image' @@ -29,9 +35,21 @@ CONF_MONITOR_UTILISATION_CONDITIONS = { CONF_MONITOR_UTILISATION_VERSION: ['Version', None, 'mdi:information-outline', None], + CONF_MONITOR_UTILISATION_CONTAINERS_TOTAL: + ['Containers Total', None, 'mdi:docker', None], + CONF_MONITOR_UTILISATION_CONTAINERS_PAUSED: + ['Containers Paused', None, 'mdi:docker', None], + CONF_MONITOR_UTILISATION_CONTAINERS_RUNNING: + ['Containers Running', None, 'mdi:docker', None], + CONF_MONITOR_UTILISATION_CONTAINERS_STOPPED: + ['Containers Stopped', None, 'mdi:docker', None], + CONF_MONITOR_UTILISATION_IMAGES_TOTAL: + ['Images Total', None, 'mdi:information-outline', None], } CONF_MONITOR_CONTAINER_CONDITIONS = { + CONF_MONITOR_CONTAINER_STATE: + ['State', None, 'mdi:checkbox-marked-circle-outline', None], CONF_MONITOR_CONTAINER_STATUS: ['Status', None, 'mdi:checkbox-marked-circle-outline', None], CONF_MONITOR_CONTAINER_UPTIME: @@ -67,14 +85,25 @@ VERSION_INFO_OS = 'os' VERSION_INFO_ARCHITECTURE = 'arch' VERSION_INFO_KERNEL = 'kernel' +VERSION_INFO_CONTAINERS_TOTAL = 'containers_total' +VERSION_INFO_CONTAINERS_PAUSED = 'containers_paused' +VERSION_INFO_CONTAINERS_RUNNING = 'containers_running' +VERSION_INFO_CONTAINERS_STOPPED = 'containers_stopped' +VERSION_INFO_IMAGES = 'images_total' +VERSION_INFO_MEMTOTAL = 'memory_total' CONTAINER_INFO = 'info' CONTAINER_INFO_ID = 'id' CONTAINER_INFO_IMAGE = 'image' +CONTAINER_INFO_STATE = 'state' CONTAINER_INFO_STATUS = 'status' CONTAINER_INFO_CREATED = 'created' CONTAINER_INFO_STARTED = 'started' +CONTAINER_INFO_ENDED = 'ended' +CONTAINER_INFO_EXITCODE = 'exitcode' CONTAINER_INFO_NETWORKMODE = 'networkmode' +CONTAINER_INFO_ENDED = 'ended' +CONTAINER_INFO_EXITCODE = 'exitcode' EVENT_INFO_CONTAINER = 'Container' EVENT_INFO_IMAGE = 'Image' diff --git a/custom_components/docker_monitor/helpers.py b/custom_components/docker_monitor/helpers.py index 9ec1f0e..721c640 100644 --- a/custom_components/docker_monitor/helpers.py +++ b/custom_components/docker_monitor/helpers.py @@ -6,16 +6,25 @@ CONTAINER_INFO_CREATED, CONTAINER_INFO_ID, CONTAINER_INFO_IMAGE, + CONTAINER_INFO_NETWORKMODE, CONTAINER_INFO_STARTED, + CONTAINER_INFO_ENDED, + CONTAINER_INFO_EXITCODE, + CONTAINER_INFO_STATE, CONTAINER_INFO_STATUS, - CONTAINER_INFO_NETWORKMODE, EVENT_INFO_CONTAINER, EVENT_INFO_ID, EVENT_INFO_IMAGE, EVENT_INFO_STATUS, VERSION_INFO_API_VERSION, VERSION_INFO_ARCHITECTURE, + VERSION_INFO_CONTAINERS_PAUSED, + VERSION_INFO_CONTAINERS_RUNNING, + VERSION_INFO_CONTAINERS_STOPPED, + VERSION_INFO_CONTAINERS_TOTAL, + VERSION_INFO_IMAGES, VERSION_INFO_KERNEL, + VERSION_INFO_MEMTOTAL, VERSION_INFO_OS, VERSION_INFO_VERSION ) @@ -72,22 +81,29 @@ def watch_container(self, name, callback): return self._containers[name] def get_info(self): - version = {} + result = {} try: raw_stats = self._client.version() - version = { + info = self._client.info() + result = { VERSION_INFO_VERSION: raw_stats.get('Version', None), VERSION_INFO_API_VERSION: raw_stats.get('ApiVersion', None), VERSION_INFO_OS: raw_stats.get('Os', None), VERSION_INFO_ARCHITECTURE: raw_stats.get('Arch', None), VERSION_INFO_KERNEL: raw_stats.get('KernelVersion', None), + VERSION_INFO_CONTAINERS_TOTAL : info.get('Containers', None), + VERSION_INFO_CONTAINERS_PAUSED : info.get('ContainersPaused', None), + VERSION_INFO_CONTAINERS_RUNNING : info.get('ContainersRunning', None), + VERSION_INFO_CONTAINERS_STOPPED : info.get('ContainersStopped', None), + VERSION_INFO_IMAGES : info.get('Images', None), + VERSION_INFO_MEMTOTAL : info.get('MemTotal', None), } except Exception as e: _LOGGER.info("Cannot get docker daemon info") _LOGGER.debug("Request exception: {}".format(e)) raise ConnectionError("Cannot request info") - return version + return result class DockerContainerEventListener(threading.Thread): @@ -185,10 +201,31 @@ def get_info(self): parser.parse(self._container.attrs['Created']), CONTAINER_INFO_STARTED: parser.parse(self._container.attrs['State']['StartedAt']), + CONTAINER_INFO_ENDED: + parser.parse(self._container.attrs['State']['FinishedAt']), + CONTAINER_INFO_EXITCODE: + self._container.attrs['State']['ExitCode'], CONTAINER_INFO_NETWORKMODE: - self._container.attrs['HostConfig']['NetworkMode'] == 'host' + self._container.attrs['HostConfig']['NetworkMode'] == 'host', + CONTAINER_INFO_ENDED: + parser.parse(self._container.attrs['State']['FinishedAt']), + CONTAINER_INFO_EXITCODE: + self._container.attrs['State']['ExitCode'], } + if info[CONTAINER_INFO_STATUS] == 'running': + info[CONTAINER_INFO_STATE] = 'Up {}'.format(self.__calcdockerformat(info[CONTAINER_INFO_STARTED])) + elif info[CONTAINER_INFO_STATUS] == 'exited': + info[CONTAINER_INFO_STATE] = 'Exited ({}) {} ago'.format(info[CONTAINER_INFO_EXITCODE], self.__calcdockerformat(info[CONTAINER_INFO_ENDED])) + elif info[CONTAINER_INFO_STATUS] == 'created': + info[CONTAINER_INFO_STATE] = 'Created {} ago'.format(self.__calcdockerformat(info[CONTAINER_INFO_CREATED])) + elif info[CONTAINER_INFO_STATUS] == 'restarting': + info[CONTAINER_INFO_STATE] = 'Restarting' + elif info[CONTAINER_INFO_STATUS] == 'paused': + info[CONTAINER_INFO_STATE] = 'Up {} (Paused)'.format(self.__calcdockerformat(info[CONTAINER_INFO_STARTED])) + else: + info[CONTAINER_INFO_STATE] = 'None ({})'.format(info[CONTAINER_INFO_STATUS]) + return info def start(self): @@ -231,6 +268,29 @@ def _notify(self): for callback in self._subscribers: callback() + def __calcdockerformat(self, dt): + """Calculate datetime to Docker format.""" + from datetime import datetime, timezone + + if dt is None: + return 'None' + + delta = round((datetime.now(timezone.utc) - dt).total_seconds()) + + if delta < (2 * 60): + return '{} seconds'.format(delta) + elif delta < (2 * 3600): + return '{} minutes'.format(round(delta / 60)) + elif delta < (2 * 86400): + return '{} hours'.format(round(delta / 3600)) + elif delta < (2 * 86400 * 7): + return '{} days'.format(round(delta / 86400)) + elif delta < (2 * 86400 * 30): + return '{} weeks'.format(round(delta / 86400 * 7)) + elif delta < (2 * 86400 * 365): + return '{} months'.format(round(delta / (86400 * 30))) + else: + return '{} years'.format(round(delta / (86400 * 365))) class DockerContainerStats(threading.Thread): """Docker monitor container stats listener thread.""" @@ -288,7 +348,7 @@ def run(self): container.set_stats(stats) except Exception as ex: - _LOGGER.info("Cannot get docker container") + _LOGGER.error("Cannot get docker container", exc_info=True) _LOGGER.debug("Request exception: {}".format(ex)) # Wait before read @@ -372,3 +432,4 @@ def __parse_stats(self, name, containerinfo, raw): self._old[name] = old return stats + diff --git a/custom_components/docker_monitor/sensor.py b/custom_components/docker_monitor/sensor.py index 951ee55..5dd3882 100644 --- a/custom_components/docker_monitor/sensor.py +++ b/custom_components/docker_monitor/sensor.py @@ -38,6 +38,7 @@ CONF_MONITOR_CONTAINER_MEMORY_USAGE, CONF_MONITOR_CONTAINER_NETWORK_TOTAL_DOWN, CONF_MONITOR_CONTAINER_NETWORK_TOTAL_UP, + CONF_MONITOR_CONTAINER_STATE, CONF_MONITOR_CONTAINER_STATUS, CONF_MONITOR_CONTAINER_UPTIME, CONF_MONITOR_UTILISATION_CONDITIONS, @@ -54,11 +55,19 @@ VERSION_INFO_ARCHITECTURE, VERSION_INFO_KERNEL, VERSION_INFO_OS, - VERSION_INFO_VERSION + VERSION_INFO_VERSION, + VERSION_INFO_CONTAINERS_TOTAL, + VERSION_INFO_CONTAINERS_PAUSED, + VERSION_INFO_CONTAINERS_RUNNING, + VERSION_INFO_CONTAINERS_STOPPED, + VERSION_INFO_IMAGES, + CONF_MONITOR_UTILISATION_CONTAINERS_TOTAL, + CONF_MONITOR_UTILISATION_CONTAINERS_PAUSED, + CONF_MONITOR_UTILISATION_CONTAINERS_RUNNING, + CONF_MONITOR_UTILISATION_CONTAINERS_STOPPED, + CONF_MONITOR_UTILISATION_IMAGES_TOTAL, ) -VERSION = '0.1.0-b0' - _LOGGER = logging.getLogger(__name__) @@ -161,6 +170,16 @@ def update(self): VERSION_INFO_OS, None) self._attributes[ATTR_VERSION_ARCH] = version.get( VERSION_INFO_ARCHITECTURE, None) + elif self._var_id == CONF_MONITOR_UTILISATION_CONTAINERS_TOTAL: + self._state = version.get(VERSION_INFO_CONTAINERS_TOTAL, None) + elif self._var_id == CONF_MONITOR_UTILISATION_CONTAINERS_PAUSED: + self._state = version.get(VERSION_INFO_CONTAINERS_PAUSED, None) + elif self._var_id == CONF_MONITOR_UTILISATION_CONTAINERS_RUNNING: + self._state = version.get(VERSION_INFO_CONTAINERS_RUNNING, None) + elif self._var_id == CONF_MONITOR_UTILISATION_CONTAINERS_STOPPED: + self._state = version.get(VERSION_INFO_CONTAINERS_STOPPED, None) + elif self._var_id == CONF_MONITOR_UTILISATION_IMAGES_TOTAL: + self._state = version.get(VERSION_INFO_IMAGES, None) else: self._state = None self._attributes = None @@ -189,6 +208,7 @@ def __init__(self, clientname, api, name, variable, interval): self._state = None self._available = False + self._running = None self._attributes = { ATTR_ATTRIBUTION: CONF_ATTRIBUTION } @@ -201,8 +221,8 @@ def name(self): @property def icon(self): """Icon to use in the frontend, if any.""" - if self._var_id == CONF_MONITOR_CONTAINER_STATUS: - if self._state == 'running': + if self._var_id in [CONF_MONITOR_CONTAINER_STATE, CONF_MONITOR_CONTAINER_STATUS]: + if self._running: return 'mdi:checkbox-marked-circle-outline' else: return 'mdi:checkbox-blank-circle-outline' @@ -247,7 +267,7 @@ async def async_added_to_hass(self): def event_callback(self): """Update callback.""" - _LOGGER.info("Sensor {} event".format(self._name)) + _LOGGER.info("Sensor %s event %s", self._name, self._var_id) info = None stats = None @@ -262,6 +282,10 @@ def event_callback(self): if self._var_id == CONF_MONITOR_CONTAINER_STATUS: state = info.get(CONTAINER_INFO_STATUS) + self._running = info.get(CONTAINER_INFO_STATUS) in ("running", "paused") + elif self._var_id == CONF_MONITOR_CONTAINER_STATE: + state = info.get(CONF_MONITOR_CONTAINER_STATE) + self._running = info.get(CONTAINER_INFO_STATUS) in ("running", "paused") else: if info[CONTAINER_INFO_STATUS] == 'running': # Info diff --git a/custom_components/docker_monitor/switch.py b/custom_components/docker_monitor/switch.py index 2f0f0f1..ed9704f 100644 --- a/custom_components/docker_monitor/switch.py +++ b/custom_components/docker_monitor/switch.py @@ -28,8 +28,6 @@ ICON_SWITCH ) -VERSION = '0.1.0-b0' - _LOGGER = logging.getLogger(__name__)