Allow user-defined sensors (#14613)

* Allow user-defined sensors

* Require element for resources

* Don't use .get()
This commit is contained in:
Fabian Affolter 2018-05-29 16:03:00 +02:00 committed by Paulus Schoutsen
parent eff1d1f14e
commit 3b38de63ea
2 changed files with 84 additions and 83 deletions

View file

@ -4,154 +4,152 @@ Support gathering system information of hosts which are running netdata.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.netdata/ https://home-assistant.io/components/sensor.netdata/
""" """
import logging
from datetime import timedelta from datetime import timedelta
from urllib.parse import urlsplit import logging
import requests
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_PORT, CONF_NAME, CONF_RESOURCES) CONF_HOST, CONF_ICON, CONF_NAME, CONF_PORT, CONF_RESOURCES)
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
REQUIREMENTS = ['netdata==0.1.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_RESOURCE = 'api/v1'
_REALTIME = 'before=0&after=-1&options=seconds' MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
CONF_ELEMENT = 'element'
DEFAULT_HOST = 'localhost' DEFAULT_HOST = 'localhost'
DEFAULT_NAME = 'Netdata' DEFAULT_NAME = 'Netdata'
DEFAULT_PORT = '19999' DEFAULT_PORT = 19999
SCAN_INTERVAL = timedelta(minutes=1) DEFAULT_ICON = 'mdi:desktop-classic'
SENSOR_TYPES = { RESOURCE_SCHEMA = vol.Any({
'memory_free': ['RAM Free', 'MiB', 'system.ram', 'free', 1], vol.Required(CONF_ELEMENT): cv.string,
'memory_used': ['RAM Used', 'MiB', 'system.ram', 'used', 1], vol.Optional(CONF_ICON, default=DEFAULT_ICON): cv.icon,
'memory_cached': ['RAM Cached', 'MiB', 'system.ram', 'cached', 1], vol.Optional(CONF_NAME): cv.string,
'memory_buffers': ['RAM Buffers', 'MiB', 'system.ram', 'buffers', 1], })
'swap_free': ['Swap Free', 'MiB', 'system.swap', 'free', 1],
'swap_used': ['Swap Used', 'MiB', 'system.swap', 'used', 1],
'processes_running': ['Processes Running', 'Count', 'system.processes',
'running', 0],
'processes_blocked': ['Processes Blocked', 'Count', 'system.processes',
'blocked', 0],
'system_load': ['System Load', '15 min', 'system.load', 'load15', 2],
'system_io_in': ['System IO In', 'Count', 'system.io', 'in', 0],
'system_io_out': ['System IO Out', 'Count', 'system.io', 'out', 0],
'ipv4_in': ['IPv4 In', 'kb/s', 'system.ipv4', 'received', 0],
'ipv4_out': ['IPv4 Out', 'kb/s', 'system.ipv4', 'sent', 0],
'disk_free': ['Disk Free', 'GiB', 'disk_space._', 'avail', 2],
'cpu_iowait': ['CPU IOWait', '%', 'system.cpu', 'iowait', 1],
'cpu_user': ['CPU User', '%', 'system.cpu', 'user', 1],
'cpu_system': ['CPU System', '%', 'system.cpu', 'system', 1],
'cpu_softirq': ['CPU SoftIRQ', '%', 'system.cpu', 'softirq', 1],
'cpu_guest': ['CPU Guest', '%', 'system.cpu', 'guest', 1],
'uptime': ['Uptime', 's', 'system.uptime', 'uptime', 0],
'packets_received': ['Packets Received', 'packets/s', 'ipv4.packets',
'received', 0],
'packets_sent': ['Packets Sent', 'packets/s', 'ipv4.packets',
'sent', 0],
'connections': ['Active Connections', 'Count',
'netfilter.conntrack_sockets', 'connections', 0]
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_RESOURCES, default=['memory_free']): vol.Required(CONF_RESOURCES): vol.Schema({cv.string: RESOURCE_SCHEMA}),
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
}) })
# pylint: disable=unused-variable async def async_setup_platform(
def setup_platform(hass, config, add_devices, discovery_info=None): hass, config, async_add_devices, discovery_info=None):
"""Set up the Netdata sensor.""" """Set up the Netdata sensor."""
from netdata import Netdata
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
host = config.get(CONF_HOST) host = config.get(CONF_HOST)
port = config.get(CONF_PORT) port = config.get(CONF_PORT)
url = 'http://{}:{}'.format(host, port)
data_url = '{}/{}/data?chart='.format(url, _RESOURCE)
resources = config.get(CONF_RESOURCES) resources = config.get(CONF_RESOURCES)
values = {} session = async_get_clientsession(hass)
for key, value in sorted(SENSOR_TYPES.items()): netdata = NetdataData(Netdata(host, hass.loop, session, port=port))
if key in resources: await netdata.async_update()
values.setdefault(value[2], []).append(key)
if netdata.api.metrics is None:
raise PlatformNotReady
dev = [] dev = []
for chart in values: for entry, data in resources.items():
rest_url = '{}{}&{}'.format(data_url, chart, _REALTIME) sensor = entry
rest = NetdataData(rest_url) element = data[CONF_ELEMENT]
rest.update() sensor_name = icon = None
for sensor_type in values[chart]: try:
dev.append(NetdataSensor(rest, name, sensor_type)) resource_data = netdata.api.metrics[sensor]
unit = '%' if resource_data['units'] == 'percentage' else \
resource_data['units']
if data is not None:
sensor_name = data.get(CONF_NAME)
icon = data.get(CONF_ICON)
except KeyError:
_LOGGER.error("Sensor is not available: %s", sensor)
continue
add_devices(dev, True) dev.append(NetdataSensor(
netdata, name, sensor, sensor_name, element, icon, unit))
async_add_devices(dev, True)
class NetdataSensor(Entity): class NetdataSensor(Entity):
"""Implementation of a Netdata sensor.""" """Implementation of a Netdata sensor."""
def __init__(self, rest, name, sensor_type): def __init__(
self, netdata, name, sensor, sensor_name, element, icon, unit):
"""Initialize the Netdata sensor.""" """Initialize the Netdata sensor."""
self.rest = rest self.netdata = netdata
self.type = sensor_type self._state = None
self._name = '{} {}'.format(name, SENSOR_TYPES[self.type][0]) self._sensor = sensor
self._precision = SENSOR_TYPES[self.type][4] self._element = element
self._unit_of_measurement = SENSOR_TYPES[self.type][1] self._sensor_name = self._sensor if sensor_name is None else \
sensor_name
self._name = name
self._icon = icon
self._unit_of_measurement = unit
@property @property
def name(self): def name(self):
"""Return the name of the sensor.""" """Return the name of the sensor."""
return self._name return '{} {}'.format(self._name, self._sensor_name)
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
"""Return the unit the value is expressed in.""" """Return the unit the value is expressed in."""
return self._unit_of_measurement return self._unit_of_measurement
@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
return self._icon
@property @property
def state(self): def state(self):
"""Return the state of the resources.""" """Return the state of the resources."""
value = self.rest.data return self._state
if value is not None:
netdata_id = SENSOR_TYPES[self.type][3]
if netdata_id in value:
return "{0:.{1}f}".format(value[netdata_id], self._precision)
return None
@property @property
def available(self): def available(self):
"""Could the resource be accessed during the last update call.""" """Could the resource be accessed during the last update call."""
return self.rest.available return self.netdata.available
def update(self): async def async_update(self):
"""Get the latest data from Netdata REST API.""" """Get the latest data from Netdata REST API."""
self.rest.update() await self.netdata.async_update()
resource_data = self.netdata.api.metrics.get(self._sensor)
self._state = round(
resource_data['dimensions'][self._element]['value'], 2)
class NetdataData(object): class NetdataData(object):
"""The class for handling the data retrieval.""" """The class for handling the data retrieval."""
def __init__(self, resource): def __init__(self, api):
"""Initialize the data object.""" """Initialize the data object."""
self._resource = resource self.api = api
self.data = None
self.available = True self.available = True
def update(self): @Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update(self):
"""Get the latest data from the Netdata REST API.""" """Get the latest data from the Netdata REST API."""
from netdata.exceptions import NetdataError
try: try:
response = requests.get(self._resource, timeout=5) await self.api.get_allmetrics()
det = response.json()
self.data = {k: v for k, v in zip(det['labels'], det['data'][0])}
self.available = True self.available = True
except requests.exceptions.ConnectionError: except NetdataError:
_LOGGER.error("Connection error: %s", urlsplit(self._resource)[1]) _LOGGER.error("Unable to retrieve data from Netdata")
self.data = None
self.available = False self.available = False

View file

@ -567,6 +567,9 @@ nad_receiver==0.0.9
# homeassistant.components.light.nanoleaf_aurora # homeassistant.components.light.nanoleaf_aurora
nanoleaf==0.4.1 nanoleaf==0.4.1
# homeassistant.components.sensor.netdata
netdata==0.1.2
# homeassistant.components.discovery # homeassistant.components.discovery
netdisco==1.4.1 netdisco==1.4.1