mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 21:57:51 +00:00
Adds folder_watcher component (#12918)
* Create watchdog_file_watcher.py * Rename watchdog_file_watcher.py to folder_watcher.py * Address a number of issues * Adds filter * Adds pattern matching * Adds create_event_handler() * Update folder_watcher.py * Adds run_setup() * Remove stop_watching() * Adds shutdown() * Update config to allow patterns on each folder * Update to patterns from filters * Adds watchdog * Fix indents on schema * Update folder_watcher.py * Create test_file_watcher.py * Fix lints * Add test_invalid_path() * Adds folder_watcher * Update test_file_watcher.py * Update folder_watcher.py * Simplify config * Adapt for new config * Run observer.schedule() on EVENT_HOMEASSISTANT_START * Amend Watcher removing entity and tidying startup * Tidy config * Rename process to on_any_event for consistency * Rename on_any_event back to process Using `on_any_event` resulted in 2 events being fired * Update folder_watcher.py * Fix return False on setup * Update test_file_watcher.py * Update folder_watcher.py * Adds watchdog * Undo adding watchdog * Update test_file_watcher.py * Update test_file_watcher.py * Update test_file_watcher.py * Update test_file_watcher.py * Update test_file_watcher.py * Add event * Update test_file_watcher.py * Update .coveragerc * Update test_file_watcher.py * Update test_file_watcher.py * debug + join * test event * lint * lint * Rename test_file_watcher.py to test_folder_watcher.py * hound * Tidy test * Further refine test * Adds to test_all * Fix test for py35 * Change test again * Update test_folder_watcher.py * Fix test * Add watchdog to test * Update folder_watcher.py * add watchdog * Update folder_watcher.py
This commit is contained in:
parent
5908b55bba
commit
df78eecc1b
@ -402,6 +402,7 @@ omit =
|
|||||||
homeassistant/components/fan/mqtt.py
|
homeassistant/components/fan/mqtt.py
|
||||||
homeassistant/components/fan/xiaomi_miio.py
|
homeassistant/components/fan/xiaomi_miio.py
|
||||||
homeassistant/components/feedreader.py
|
homeassistant/components/feedreader.py
|
||||||
|
homeassistant/components/folder_watcher.py
|
||||||
homeassistant/components/foursquare.py
|
homeassistant/components/foursquare.py
|
||||||
homeassistant/components/goalfeed.py
|
homeassistant/components/goalfeed.py
|
||||||
homeassistant/components/ifttt.py
|
homeassistant/components/ifttt.py
|
||||||
|
111
homeassistant/components/folder_watcher.py
Normal file
111
homeassistant/components/folder_watcher.py
Normal file
@ -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()
|
@ -1279,6 +1279,9 @@ waqiasync==1.0.0
|
|||||||
# homeassistant.components.cloud
|
# homeassistant.components.cloud
|
||||||
warrant==0.6.1
|
warrant==0.6.1
|
||||||
|
|
||||||
|
# homeassistant.components.folder_watcher
|
||||||
|
watchdog==0.8.3
|
||||||
|
|
||||||
# homeassistant.components.waterfurnace
|
# homeassistant.components.waterfurnace
|
||||||
waterfurnace==0.4.0
|
waterfurnace==0.4.0
|
||||||
|
|
||||||
|
@ -199,5 +199,8 @@ wakeonlan==1.0.0
|
|||||||
# homeassistant.components.cloud
|
# homeassistant.components.cloud
|
||||||
warrant==0.6.1
|
warrant==0.6.1
|
||||||
|
|
||||||
|
# homeassistant.components.folder_watcher
|
||||||
|
watchdog==0.8.3
|
||||||
|
|
||||||
# homeassistant.components.sensor.yahoo_finance
|
# homeassistant.components.sensor.yahoo_finance
|
||||||
yahoo-finance==1.4.0
|
yahoo-finance==1.4.0
|
||||||
|
@ -90,6 +90,7 @@ TEST_REQUIREMENTS = (
|
|||||||
'yahoo-finance',
|
'yahoo-finance',
|
||||||
'pythonwhois',
|
'pythonwhois',
|
||||||
'wakeonlan',
|
'wakeonlan',
|
||||||
|
'watchdog',
|
||||||
'vultr'
|
'vultr'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
64
tests/components/test_folder_watcher.py
Normal file
64
tests/components/test_folder_watcher.py
Normal file
@ -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
|
Loading…
x
Reference in New Issue
Block a user