From f25347d98d6ae4cc976945b1299e39b6dfe38bff Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 15 May 2017 14:25:46 +0200 Subject: [PATCH] File sensor (#7569) * Add File sensor * Use None and return * Remove I/O * Use less memory * No traceback if file is empty --- homeassistant/components/sensor/file.py | 98 +++++++++++++++++++++++++ tests/components/sensor/test_file.py | 91 +++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 homeassistant/components/sensor/file.py create mode 100644 tests/components/sensor/test_file.py diff --git a/homeassistant/components/sensor/file.py b/homeassistant/components/sensor/file.py new file mode 100644 index 00000000000..afa305a0fb0 --- /dev/null +++ b/homeassistant/components/sensor/file.py @@ -0,0 +1,98 @@ +""" +Support for sensor value(s) stored in local files. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.file/ +""" +import os +import asyncio +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_VALUE_TEMPLATE, CONF_NAME, CONF_UNIT_OF_MEASUREMENT) +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +CONF_FILE_PATH = 'file_path' + +DEFAULT_NAME = 'File' + +ICON = 'mdi:file' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_FILE_PATH): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the file sensor.""" + file_path = config.get(CONF_FILE_PATH) + name = config.get(CONF_NAME) + unit = config.get(CONF_UNIT_OF_MEASUREMENT) + value_template = config.get(CONF_VALUE_TEMPLATE) + + if value_template is not None: + value_template.hass = hass + + async_add_devices( + [FileSensor(name, file_path, unit, value_template)], True) + + +class FileSensor(Entity): + """Implementation of a file sensor.""" + + def __init__(self, name, file_path, unit_of_measurement, value_template): + """Initialize the file sensor.""" + self._name = name + self._file_path = file_path + self._unit_of_measurement = unit_of_measurement + self._val_tpl = value_template + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit_of_measurement + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return ICON + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Get the latest entry from a file and updates the state.""" + try: + with open(self._file_path, 'r', encoding='utf-8') as file_data: + for line in file_data: + data = line + data = data.strip() + except (IndexError, FileNotFoundError, IsADirectoryError, + UnboundLocalError): + _LOGGER.warning("File or data not present at the moment: %s", + os.path.basename(self._file_path)) + return + + if self._val_tpl is not None: + self._state = self._val_tpl.async_render_with_possible_json_value( + data, None) + else: + self._state = data diff --git a/tests/components/sensor/test_file.py b/tests/components/sensor/test_file.py new file mode 100644 index 00000000000..00e8f2ba525 --- /dev/null +++ b/tests/components/sensor/test_file.py @@ -0,0 +1,91 @@ +"""The tests for local file sensor platform.""" +import unittest +from unittest.mock import Mock, patch + +# Using third party package because of a bug reading binary data in Python 3.4 +# https://bugs.python.org/issue23004 +from mock_open import MockOpen + +from homeassistant.setup import setup_component +from homeassistant.const import STATE_UNKNOWN + +from tests.common import get_test_home_assistant + + +class TestFileSensor(unittest.TestCase): + """Test the File sensor.""" + + def setup_method(self, method): + """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() + + @patch('os.path.isfile', Mock(return_value=True)) + @patch('os.access', Mock(return_value=True)) + def test_file_value(self): + """Test the File sensor.""" + config = { + 'sensor': { + 'platform': 'file', + 'name': 'file1', + 'file_path': 'mock.file1', + } + } + + m_open = MockOpen(read_data='43\n45\n21') + with patch('homeassistant.components.sensor.file.open', m_open, + create=True): + assert setup_component(self.hass, 'sensor', config) + self.hass.block_till_done() + + state = self.hass.states.get('sensor.file1') + self.assertEqual(state.state, '21') + + @patch('os.path.isfile', Mock(return_value=True)) + @patch('os.access', Mock(return_value=True)) + def test_file_value_template(self): + """Test the File sensor with JSON entries.""" + config = { + 'sensor': { + 'platform': 'file', + 'name': 'file2', + 'file_path': 'mock.file2', + 'value_template': '{{ value_json.temperature }}', + } + } + + data = '{"temperature": 29, "humidity": 31}\n' \ + '{"temperature": 26, "humidity": 36}' + + m_open = MockOpen(read_data=data) + with patch('homeassistant.components.sensor.file.open', m_open, + create=True): + assert setup_component(self.hass, 'sensor', config) + self.hass.block_till_done() + + state = self.hass.states.get('sensor.file2') + self.assertEqual(state.state, '26') + + @patch('os.path.isfile', Mock(return_value=True)) + @patch('os.access', Mock(return_value=True)) + def test_file_empty(self): + """Test the File sensor with an empty file.""" + config = { + 'sensor': { + 'platform': 'file', + 'name': 'file3', + 'file_path': 'mock.file', + } + } + + m_open = MockOpen(read_data='') + with patch('homeassistant.components.sensor.file.open', m_open, + create=True): + assert setup_component(self.hass, 'sensor', config) + self.hass.block_till_done() + + state = self.hass.states.get('sensor.file3') + self.assertEqual(state.state, STATE_UNKNOWN)