diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py new file mode 100644 index 0000000000000000000000000000000000000000..a0648d4851a6876b6e2279d591acd194253049cd --- /dev/null +++ b/homeassistant/components/sensor/sql.py @@ -0,0 +1,145 @@ +""" +Sensor from an SQL Query. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.sql/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE) +from homeassistant.components.recorder import ( + CONF_DB_URL, DEFAULT_URL, DEFAULT_DB_FILE) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['sqlalchemy==1.2.2'] + +CONF_QUERIES = 'queries' +CONF_QUERY = 'query' +CONF_COLUMN_NAME = 'column' + +_QUERY_SCHEME = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_QUERY): cv.string, + vol.Required(CONF_COLUMN_NAME): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_QUERIES): [_QUERY_SCHEME], + vol.Optional(CONF_DB_URL): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the sensor platform.""" + db_url = config.get(CONF_DB_URL, None) + if not db_url: + db_url = DEFAULT_URL.format( + hass_config_path=hass.config.path(DEFAULT_DB_FILE)) + + import sqlalchemy + from sqlalchemy.orm import sessionmaker, scoped_session + + try: + engine = sqlalchemy.create_engine(db_url) + sessionmaker = scoped_session(sessionmaker(bind=engine)) + + # run a dummy query just to test the db_url + sess = sessionmaker() + sess.execute("SELECT 1;") + + except sqlalchemy.exc.SQLAlchemyError as err: + _LOGGER.error("Couldn't connect using %s DB_URL: %s", db_url, err) + return + + queries = [] + + for query in config.get(CONF_QUERIES): + name = query.get(CONF_NAME) + query_str = query.get(CONF_QUERY) + unit = query.get(CONF_UNIT_OF_MEASUREMENT) + value_template = query.get(CONF_VALUE_TEMPLATE) + column_name = query.get(CONF_COLUMN_NAME) + + if value_template is not None: + value_template.hass = hass + + sensor = SQLSensor( + name, sessionmaker, query_str, column_name, unit, value_template + ) + queries.append(sensor) + + add_devices(queries, True) + + +class SQLSensor(Entity): + """An SQL sensor.""" + + def __init__(self, name, sessmaker, query, column, unit, value_template): + """Initialize SQL sensor.""" + self._name = name + if "LIMIT" in query: + self._query = query + else: + self._query = query.replace(";", " LIMIT 1;") + self._unit_of_measurement = unit + self._template = value_template + self._column_name = column + self.sessionmaker = sessmaker + self._state = None + self._attributes = None + + @property + def name(self): + """Return the name of the query.""" + return self._name + + @property + def state(self): + """Return the query's current state.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + + def update(self): + """Retrieve sensor data from the query.""" + import sqlalchemy + try: + sess = self.sessionmaker() + result = sess.execute(self._query) + except sqlalchemy.exc.SQLAlchemyError as err: + _LOGGER.error("Error executing query %s: %s", self._query, err) + return + + for res in result: + _LOGGER.debug(res.items()) + data = res[self._column_name] + self._attributes = {k: str(v) for k, v in res.items()} + + if data is None: + _LOGGER.error("%s returned no results", self._query) + return False + + if self._template is not None: + self._state = self._template.async_render_with_possible_json_value( + data, None) + else: + self._state = data + + sess.close() diff --git a/requirements_all.txt b/requirements_all.txt index 79d87b2d9e5cd67e642a2854f283da54f25e20e4..149bf6d7154e52f7e12d337939c6593638fde566 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1127,6 +1127,7 @@ speedtest-cli==1.0.7 # homeassistant.components.recorder # homeassistant.scripts.db_migrator +# homeassistant.components.sensor.sql sqlalchemy==1.2.2 # homeassistant.components.statsd diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f4a4e09b1248d739916594a48c722cac8f6dd036..acddad7e9426b27e763e27f4bb10c18da8232db9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -169,6 +169,7 @@ somecomfort==0.5.0 # homeassistant.components.recorder # homeassistant.scripts.db_migrator +# homeassistant.components.sensor.sql sqlalchemy==1.2.2 # homeassistant.components.statsd diff --git a/tests/components/sensor/test_sql.py b/tests/components/sensor/test_sql.py new file mode 100644 index 0000000000000000000000000000000000000000..ebf2d749e67b53c7daccfe85452ec1cdaa4f7aee --- /dev/null +++ b/tests/components/sensor/test_sql.py @@ -0,0 +1,37 @@ +"""The test for the sql sensor platform.""" +import unittest + +from homeassistant.setup import setup_component + +from tests.common import get_test_home_assistant + + +class TestSQLSensor(unittest.TestCase): + """Test the SQL sensor.""" + + def setUp(self): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def test_query(self): + """Test the SQL sensor.""" + config = { + 'sensor': { + 'platform': 'sql', + 'db_url': 'sqlite://', + 'queries': [{ + 'name': 'count_tables', + 'query': 'SELECT count(*) value FROM sqlite_master;', + 'column': 'value', + }] + } + } + + assert setup_component(self.hass, 'sensor', config) + + state = self.hass.states.get('sensor.count_tables') + self.assertEqual(state.state, '0')