-
-
Save Sanderhuisman/e609a99682854d9f880f8334b7194558 to your computer and use it in GitHub Desktop.
| import logging | |
| from datetime import timedelta | |
| import os | |
| import threading | |
| import time | |
| import voluptuous as vol | |
| import homeassistant.helpers.config_validation as cv | |
| from homeassistant.components.sensor import PLATFORM_SCHEMA | |
| from homeassistant.const import ( | |
| ATTR_ATTRIBUTION, | |
| CONF_HOST, | |
| CONF_MONITORED_CONDITIONS, | |
| EVENT_HOMEASSISTANT_STOP | |
| ) | |
| from homeassistant.helpers.entity import Entity | |
| from homeassistant.util import Throttle | |
| REQUIREMENTS = ['docker==3.7.0'] | |
| _LOGGER = logging.getLogger(__name__) | |
| DEFAULT_HOST = 'unix://var/run/docker.sock' | |
| CONF_CONTAINERS = 'containers' | |
| CONF_ATTRIBUTION = 'Data provided by Docker' | |
| ATTR_ONLINE_CPUS = 'online_cpus' | |
| PRECISION = 2 | |
| UTILISATION_MONITOR_VERSION = 'utilization_version' | |
| CONTAINER_MONITOR_STATUS = 'container_status' | |
| CONTAINER_MONITOR_MEMORY_USAGE = 'container_memory_usage' | |
| CONTAINER_MONITOR_MEMORY_PERCENTAGE = 'container_memory_percentage_usage' | |
| CONTAINER_MONITOR_CPU_PERCENTAGE = 'container_cpu_percentage_usage' | |
| CONTAINER_MONITOR_NETWORK_UP = 'container_network_up' | |
| CONTAINER_MONITOR_NETWORK_DOWN = 'container_network_down' | |
| _UTILISATION_MON_COND = { | |
| UTILISATION_MONITOR_VERSION : ['Version' , None , 'mdi:memory'], | |
| } | |
| _CONTAINER_MON_COND = { | |
| CONTAINER_MONITOR_STATUS : ['Status' , None , 'mdi:checkbox-marked-circle-outline' ], | |
| CONTAINER_MONITOR_MEMORY_USAGE : ['Memory use' , 'bytes' , 'mdi:memory' ], | |
| CONTAINER_MONITOR_MEMORY_PERCENTAGE : ['Memory use (percent)' , '%' , 'mdi:memory' ], | |
| CONTAINER_MONITOR_CPU_PERCENTAGE : ['CPU use' , '%' , 'mdi:chip' ], | |
| CONTAINER_MONITOR_NETWORK_UP : ['Network Up' , 'Bytes' , 'mdi:upload' ], | |
| CONTAINER_MONITOR_NETWORK_DOWN : ['Network Down' , 'Bytes' , 'mdi:download' ], | |
| } | |
| _MONITORED_CONDITIONS = list(_UTILISATION_MON_COND.keys()) + \ | |
| list(_CONTAINER_MON_COND.keys()) | |
| PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ | |
| vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, | |
| vol.Optional(CONF_MONITORED_CONDITIONS): | |
| vol.All(cv.ensure_list, [vol.In(_MONITORED_CONDITIONS)]), | |
| vol.Optional(CONF_CONTAINERS): cv.ensure_list, | |
| }) | |
| def setup_platform(hass, config, add_entities, discovery_info=None): | |
| """Set up the Synology NAS Sensor.""" | |
| import docker | |
| host = config.get(CONF_HOST) | |
| monitored_conditions = config.get(CONF_MONITORED_CONDITIONS) | |
| try: | |
| api = docker.DockerClient(base_url=host) | |
| except: | |
| _LOGGER.info("Error setting up Docker sensor") | |
| return | |
| version = dockerVersion(api) | |
| _LOGGER.info("Docker version: {}".format(version.get('version', None))) | |
| threads = {} | |
| sensors = [DockerUtilSensor(api, variable) for variable in monitored_conditions if variable in _UTILISATION_MON_COND] | |
| containers = api.containers.list(all=True) or [] | |
| container_names = [x.name for x in containers] | |
| for container in containers: | |
| _LOGGER.debug("Found container: {}".format(container.name)) | |
| for container_name in config.get(CONF_CONTAINERS, container_names): | |
| thread = DockerContainerApi(container_name, api) | |
| threads[container_name] = thread | |
| thread.start() | |
| sensors += [DockerContainerSensor(api, thread, variable) for variable in monitored_conditions if variable in _CONTAINER_MON_COND] | |
| if sensors: | |
| def monitor_stop(_service_or_event): | |
| """Stop the monitor thread.""" | |
| _LOGGER.info("Stopping threads for Docker monitor") | |
| for t in threads.values(): | |
| t.stop() | |
| add_entities(sensors, True) | |
| hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, monitor_stop) | |
| def dockerVersion(api): | |
| raw_stats = api.version() | |
| return { | |
| 'version' : raw_stats.get('Version' , None), | |
| 'api_version' : raw_stats.get('ApiVersion', None), | |
| 'os' : raw_stats.get('Os' , None), | |
| 'arch' : raw_stats.get('Arch' , None), | |
| } | |
| class DockerContainerApi(threading.Thread): | |
| def __init__(self, container_name, api): | |
| self._container_name = container_name | |
| self._api = api | |
| self._container = self._api.containers.get(self._container_name) | |
| super(DockerContainerApi, self).__init__() | |
| self._stopper = threading.Event() | |
| self._stats = {} | |
| self._stats_stream = self._container.stats(stream=True, decode=True) | |
| _LOGGER.debug("Create thread for container {}".format(self._container.name)) | |
| def run(self): | |
| for i in self._stats_stream: | |
| self._setStats(i) | |
| time.sleep(1) | |
| if self.stopped(): | |
| break | |
| def stats(self): | |
| """Stats getter.""" | |
| return self._stats | |
| def getContainerName(self): | |
| """Container name getter.""" | |
| return self._container_name | |
| def stop(self, timeout=None): | |
| """Stop the thread.""" | |
| _LOGGER.debug("Close thread for container {}".format(self._container.name)) | |
| self._stopper.set() | |
| def stopped(self): | |
| """Return True is the thread is stopped.""" | |
| return self._stopper.isSet() | |
| def _setStats(self, raw_stats): | |
| stats = {} | |
| stats['id'] = self._container.id | |
| stats['image'] = self._container.image.tags | |
| stats['status'] = self._container.attrs['State']['Status'] | |
| if stats['status'] in ('running', 'paused'): | |
| stats['cpu'] = self._get_docker_cpu(raw_stats) | |
| stats['memory'] = self._get_docker_memory(raw_stats) | |
| stats['io'] = self._get_docker_io(raw_stats) | |
| stats['network'] = self._get_docker_network(raw_stats) | |
| stats['cpu_percent'] = stats['cpu'].get('total', None) | |
| stats['memory_usage'] = stats['memory'].get('usage', None) | |
| stats['memory_percent'] = stats['memory'].get('usage_percent', None) | |
| stats['io_r'] = stats['io'].get('ior', None) | |
| stats['io_w'] = stats['io'].get('iow', None) | |
| stats['network_up'] = stats['network'].get('tx', None) | |
| stats['network_down'] = stats['network'].get('rx', None) | |
| else: | |
| stats['cpu'] = {} | |
| stats['memory'] = {} | |
| stats['io'] = {} | |
| stats['network'] = {} | |
| stats['cpu_percent'] = None | |
| stats['memory_usage'] = None | |
| stats['memory_percent'] = None | |
| stats['io_r'] = None | |
| stats['io_w'] = None | |
| stats['network_up'] = None | |
| stats['network_down'] = None | |
| self._stats = stats | |
| def _get_docker_cpu(self, raw_stats): | |
| ret = {} | |
| cpu_new = {} | |
| try: | |
| cpu_new['total'] = raw_stats['cpu_stats']['cpu_usage']['total_usage'] | |
| cpu_new['system'] = raw_stats['cpu_stats']['system_cpu_usage'] | |
| if 'online_cpus' in raw_stats['cpu_stats']: | |
| ret['online_cpus'] = raw_stats['cpu_stats']['online_cpus'] | |
| else: | |
| ret['online_cpus'] = len(raw_stats['cpu_stats']['cpu_usage']['percpu_usage'] or []) | |
| except KeyError as e: | |
| # raw_stats do not have CPU information | |
| _LOGGER.error("Cannot grab CPU usage for container {} ({})".format(self._container.id, e)) | |
| _LOGGER.debug(raw_stats) | |
| else: | |
| if not hasattr(self, 'cpu_old'): | |
| # First call, we init the cpu_old variable | |
| try: | |
| self.cpu_old = cpu_new | |
| except (IOError, UnboundLocalError): | |
| pass | |
| cpu_delta = float(cpu_new['total'] - self.cpu_old['total']) | |
| system_delta = float(cpu_new['system'] - self.cpu_old['system']) | |
| if cpu_delta > 0.0 and system_delta > 0.0: | |
| ret['total'] = round((cpu_delta / system_delta) * float(ret['online_cpus']) * 100.0, PRECISION) | |
| else: | |
| ret['total'] = round(0.0, PRECISION) | |
| self.cpu_old = cpu_new | |
| return ret | |
| def _get_docker_memory(self, raw_stats): | |
| ret = {} | |
| try: | |
| ret['usage'] = raw_stats['memory_stats']['usage'] | |
| ret['limit'] = raw_stats['memory_stats']['limit'] | |
| ret['max_usage'] = raw_stats['memory_stats']['max_usage'] | |
| except (KeyError, TypeError) as e: | |
| # raw_stats do not have MEM information | |
| _LOGGER.error("Cannot grab MEM usage for container {} ({})".format(self._container.id, e)) | |
| _LOGGER.debug(raw_stats) | |
| else: | |
| ret['usage_percent'] = round(float(ret['usage']) / float(ret['limit']) * 100.0, PRECISION) | |
| return ret | |
| def _get_docker_network(self, raw_stats): | |
| network_new = {} | |
| try: | |
| netcounters = raw_stats["networks"] | |
| except KeyError as e: | |
| # raw_stats do not have NETWORK information | |
| _LOGGER.error("Cannot grab NET usage for container {} ({})".format(self._container.id, e)) | |
| _LOGGER.debug(raw_stats) | |
| else: | |
| if not hasattr(self, 'inetcounters_old'): | |
| # First call, we init the network_old var | |
| try: | |
| self.netcounters_old = netcounters | |
| except (IOError, UnboundLocalError): | |
| pass | |
| try: | |
| network_new['rx'] = netcounters['eth0']['rx_bytes'] - self.netcounters_old['eth0']['rx_bytes'] | |
| network_new['tx'] = netcounters['eth0']['tx_bytes'] - self.netcounters_old['eth0']['tx_bytes'] | |
| network_new['cumulative_rx'] = netcounters['eth0']['rx_bytes'] | |
| network_new['cumulative_tx'] = netcounters['eth0']['tx_bytes'] | |
| except KeyError as e: | |
| _LOGGER.debug("Cannot grab network interface usage for container {} ({})".format(self._container.id, e)) | |
| _LOGGER.debug(raw_stats) | |
| self.netcounters_old = netcounters | |
| return network_new | |
| def _get_docker_io(self, raw_stats): | |
| io_new = {} | |
| try: | |
| iocounters = raw_stats['blkio_stats'] | |
| except KeyError as e: | |
| # raw_stats do not have io information | |
| _LOGGER.error("Cannot grab block IO usage for container {} ({})".format(self._container.id, e)) | |
| _LOGGER.debug(raw_stats) | |
| return io_new | |
| else: | |
| if not hasattr(self, 'iocounters_old'): | |
| # First call, we init the io_old var | |
| try: | |
| self.iocounters_old = iocounters | |
| except (IOError, UnboundLocalError): | |
| pass | |
| try: | |
| # Read IOR and IOW value in the structure list of dict | |
| ior = [i for i in iocounters['io_service_bytes_recursive'] if i['op'] == 'Read'][0]['value'] | |
| iow = [i for i in iocounters['io_service_bytes_recursive'] if i['op'] == 'Write'][0]['value'] | |
| ior_old = [i for i in self.iocounters_old['io_service_bytes_recursive'] if i['op'] == 'Read'][0]['value'] | |
| iow_old = [i for i in self.iocounters_old['io_service_bytes_recursive'] if i['op'] == 'Write'][0]['value'] | |
| except (TypeError, IndexError, KeyError) as e: | |
| _LOGGER.debug("Cannot grab block IO usage for container {} ({})".format(self._container.id, e)) | |
| else: | |
| io_new['ior'] = ior - ior_old | |
| io_new['iow'] = iow - iow_old | |
| io_new['cumulative_ior'] = ior | |
| io_new['cumulative_iow'] = iow | |
| self.iocounters_old = iocounters | |
| return io_new | |
| class DockerUtilSensor(Entity): | |
| """Representation of a Docker Sensor.""" | |
| def __init__(self, api, variable): | |
| """Initialize the sensor.""" | |
| self._api = api | |
| self._var_id = variable | |
| self._var_name = _UTILISATION_MON_COND[variable][0] | |
| self._var_unit = _UTILISATION_MON_COND[variable][1] | |
| self._var_icon = _UTILISATION_MON_COND[variable][2] | |
| self._state = None | |
| self._attributes = { | |
| ATTR_ATTRIBUTION: CONF_ATTRIBUTION | |
| } | |
| _LOGGER.info("Initializing utilization sensor \"{}\"".format(self._var_id)) | |
| @property | |
| def name(self): | |
| """Return the name of the sensor, if any.""" | |
| return "docker_{}".format(self._var_name.lower()) | |
| @property | |
| def icon(self): | |
| """Icon to use in the frontend, if any.""" | |
| return self._var_icon | |
| @property | |
| def state(self): | |
| """Return the state of the sensor.""" | |
| return self._state | |
| @property | |
| def unit_of_measurement(self): | |
| """Return the unit the value is expressed in.""" | |
| return self._var_unit | |
| def update(self): | |
| """Get the latest data for the states.""" | |
| if self._var_id == UTILISATION_MONITOR_VERSION: | |
| version = dockerVersion(self._api) | |
| self._state = version.get('version', None) | |
| self._attributes['api_version'] = version.get('api_version', None) | |
| self._attributes['os'] = version.get('os', None) | |
| self._attributes['arch'] = version.get('arch', None) | |
| @property | |
| def device_state_attributes(self): | |
| """Return the state attributes.""" | |
| return self._attributes | |
| class DockerContainerSensor(Entity): | |
| """Representation of a Docker Sensor.""" | |
| def __init__(self, api, container_thread, variable): | |
| """Initialize the sensor.""" | |
| self._api = api | |
| self._thread = container_thread | |
| self._var_id = variable | |
| self._var_name = _CONTAINER_MON_COND[variable][0] | |
| self._var_unit = _CONTAINER_MON_COND[variable][1] | |
| self._var_icon = _CONTAINER_MON_COND[variable][2] | |
| self._state = None | |
| self._attributes = { | |
| ATTR_ATTRIBUTION: CONF_ATTRIBUTION | |
| } | |
| self._name = self._thread.getContainerName() | |
| _LOGGER.info("Initializing Docker sensor \"{}\" with parameter: {}".format(self._name, self._var_name)) | |
| @property | |
| def name(self): | |
| """Return the name of the sensor, if any.""" | |
| return "docker_{}_{}".format(self._name, self._var_name) | |
| @property | |
| def icon(self): | |
| """Icon to use in the frontend, if any.""" | |
| return self._var_icon | |
| @property | |
| def state(self): | |
| """Return the state of the sensor.""" | |
| return self._state | |
| @property | |
| def unit_of_measurement(self): | |
| """Return the unit the value is expressed in.""" | |
| return self._var_unit | |
| def update(self): | |
| """Get the latest data for the states.""" | |
| stats = self._thread.stats() | |
| if self._var_id == CONTAINER_MONITOR_STATUS: | |
| self._state = stats.get('status', None) | |
| elif self._var_id == CONTAINER_MONITOR_MEMORY_USAGE: | |
| self._state = stats.get('memory_usage', None) | |
| elif self._var_id == CONTAINER_MONITOR_CPU_PERCENTAGE: | |
| self._state = stats.get('cpu_percent', None) | |
| if 'cpu' in stats: | |
| self._attributes[ATTR_ONLINE_CPUS] = stats['cpu'].get('online_cpus', None) | |
| elif self._var_id == CONTAINER_MONITOR_MEMORY_PERCENTAGE: | |
| self._state = stats.get('memory_percent', None) | |
| # Network | |
| elif self._var_id == CONTAINER_MONITOR_NETWORK_UP: | |
| self._state = round(stats.get('network_up', None) / 1024.0, PRECISION) | |
| elif self._var_id == CONTAINER_MONITOR_NETWORK_DOWN: | |
| self._state = round(stats.get('network_down', None) / 1024.0, PRECISION) | |
| @property | |
| def device_state_attributes(self): | |
| """Return the state attributes.""" | |
| return self._attributes |
Hi Sanderhuisman , sorry but after last HASSIO update, your custom component has stopped working.
I checked if you released updates, but I saw that the last changes date back to 4 months ago.
Do you know how I can solve the problem?
thanks in advance as always
At my server it is still working (0.96.1) maybe you can have a look if there are errors.
If so, please make a bug report in my repository (than it is logged with the components source). I haven't been active recently as I am busy finishing my study, but I can have a quick look
Hi,
Just wanted to know if you still maintain this component ?
Thanks anyway.
@guillaumelamirand this component got its own repository, but I haven't actively been working on the last months since I was quite busy.
@Sanderhuisman Thank you.
fix my issue, thank you mate.

for the other users, i fix the issue in this way:
and now, on hassio:
Thank you for the help, and sorry for the noob question.
Your custom component is awesome!!!