diff --git a/.coveragerc b/.coveragerc index a2c0dde77b1..d6cc126ef52 100644 --- a/.coveragerc +++ b/.coveragerc @@ -402,6 +402,7 @@ omit = homeassistant/components/fan/mqtt.py homeassistant/components/fan/xiaomi_miio.py homeassistant/components/feedreader.py + homeassistant/components/folder_watcher.py homeassistant/components/foursquare.py homeassistant/components/goalfeed.py homeassistant/components/ifttt.py diff --git a/homeassistant/components/folder_watcher.py b/homeassistant/components/folder_watcher.py new file mode 100644 index 00000000000..011ae892bc5 --- /dev/null +++ b/homeassistant/components/folder_watcher.py @@ -0,0 +1,111 @@ +""" +Component for monitoring activity on a folder. + +For more details about this platform, refer to the documentation at +https://home-assistant.io/components/folder_watcher/ +""" +import os +import logging +import voluptuous as vol +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['watchdog==0.8.3'] +_LOGGER = logging.getLogger(__name__) + +CONF_FOLDER = 'folder' +CONF_PATTERNS = 'patterns' +CONF_WATCHERS = 'watchers' +DEFAULT_PATTERN = '*' +DOMAIN = "folder_watcher" + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_FOLDER): cv.isdir, + vol.Optional(CONF_PATTERNS, default=[DEFAULT_PATTERN]): + vol.All(cv.ensure_list, [cv.string]), + })]) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the folder watcher.""" + conf = config[DOMAIN] + for watcher in conf: + path = watcher[CONF_FOLDER] + patterns = watcher[CONF_PATTERNS] + if not hass.config.is_allowed_path(path): + _LOGGER.error("folder %s is not valid or allowed", path) + return False + Watcher(path, patterns, hass) + + return True + + +def create_event_handler(patterns, hass): + """"Return the Watchdog EventHandler object.""" + from watchdog.events import PatternMatchingEventHandler + + class EventHandler(PatternMatchingEventHandler): + """Class for handling Watcher events.""" + + def __init__(self, patterns, hass): + """Initialise the EventHandler.""" + super().__init__(patterns) + self.hass = hass + + def process(self, event): + """On Watcher event, fire HA event.""" + _LOGGER.debug("process(%s)", event) + if not event.is_directory: + folder, file_name = os.path.split(event.src_path) + self.hass.bus.fire( + DOMAIN, { + "event_type": event.event_type, + 'path': event.src_path, + 'file': file_name, + 'folder': folder, + }) + + def on_modified(self, event): + """File modified.""" + self.process(event) + + def on_moved(self, event): + """File moved.""" + self.process(event) + + def on_created(self, event): + """File created.""" + self.process(event) + + def on_deleted(self, event): + """File deleted.""" + self.process(event) + + return EventHandler(patterns, hass) + + +class Watcher(): + """Class for starting Watchdog.""" + + def __init__(self, path, patterns, hass): + """Initialise the watchdog observer.""" + from watchdog.observers import Observer + self._observer = Observer() + self._observer.schedule( + create_event_handler(patterns, hass), + path, + recursive=True) + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, self.startup) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.shutdown) + + def startup(self, event): + """Start the watcher.""" + self._observer.start() + + def shutdown(self, event): + """Shutdown the watcher.""" + self._observer.stop() + self._observer.join() diff --git a/requirements_all.txt b/requirements_all.txt index e79249151b1..d983028cead 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1279,6 +1279,9 @@ waqiasync==1.0.0 # homeassistant.components.cloud warrant==0.6.1 +# homeassistant.components.folder_watcher +watchdog==0.8.3 + # homeassistant.components.waterfurnace waterfurnace==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8a57488d80..6630c09c1c4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -199,5 +199,8 @@ wakeonlan==1.0.0 # homeassistant.components.cloud warrant==0.6.1 +# homeassistant.components.folder_watcher +watchdog==0.8.3 + # homeassistant.components.sensor.yahoo_finance yahoo-finance==1.4.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index d8fc7b1ed60..1f5348136c6 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -90,6 +90,7 @@ TEST_REQUIREMENTS = ( 'yahoo-finance', 'pythonwhois', 'wakeonlan', + 'watchdog', 'vultr' ) diff --git a/tests/components/test_folder_watcher.py b/tests/components/test_folder_watcher.py new file mode 100644 index 00000000000..587d8b7ad6d --- /dev/null +++ b/tests/components/test_folder_watcher.py @@ -0,0 +1,64 @@ +"""The tests for the folder_watcher component.""" +import unittest +from unittest.mock import MagicMock +import os + +from homeassistant.components import folder_watcher +from homeassistant.setup import setup_component +from tests.common import get_test_home_assistant + +CWD = os.path.join(os.path.dirname(__file__)) +FILE = 'file.txt' + + +class TestFolderWatcher(unittest.TestCase): + """Test the file_watcher component.""" + + def setup_method(self, method): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.config.whitelist_external_dirs = set((CWD)) + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def test_invalid_path_setup(self): + """Test that a invalid path is not setup.""" + config = { + folder_watcher.DOMAIN: [{ + folder_watcher.CONF_FOLDER: 'invalid_path' + }] + } + self.assertFalse( + setup_component(self.hass, folder_watcher.DOMAIN, config)) + + def test_valid_path_setup(self): + """Test that a valid path is setup.""" + config = { + folder_watcher.DOMAIN: [{folder_watcher.CONF_FOLDER: CWD}] + } + + self.assertTrue(setup_component( + self.hass, folder_watcher.DOMAIN, config)) + + def test_event(self): + """Check that HASS events are fired correctly on watchdog event.""" + from watchdog.events import FileModifiedEvent + + # Cant use setup_component as need to retrieve Watcher object. + w = folder_watcher.Watcher(CWD, + folder_watcher.DEFAULT_PATTERN, + self.hass) + w.startup(None) + + self.hass.bus.fire = MagicMock() + + # Trigger a fake filesystem event through the Watcher Observer emitter. + (emitter,) = w._observer.emitters + emitter.queue_event(FileModifiedEvent(FILE)) + + # Wait for the event to propagate. + self.hass.block_till_done() + + assert self.hass.bus.fire.called