Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,7 @@ omit =
homeassistant/components/sensor/fedex.py
homeassistant/components/sensor/fitbit.py
homeassistant/components/sensor/fixer.py
homeassistant/components/sensor/folder.py
homeassistant/components/sensor/fritzbox_callmonitor.py
homeassistant/components/sensor/fritzbox_netmonitor.py
homeassistant/components/sensor/gearbest.py
Expand Down
112 changes: 112 additions & 0 deletions homeassistant/components/sensor/folder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""
Sensor for monitoring the contents of a folder.

For more details about this platform, refer to the documentation at
https://home-assistant.io/components/sensor.folder/
"""
from datetime import datetime as dt
from datetime import timedelta
import glob
import logging
import os

import voluptuous as vol

from homeassistant.helpers.entity import Entity
from homeassistant.helpers.template import DATE_STR_FORMAT
import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA

_LOGGER = logging.getLogger(__name__)


CONF_FOLDER_PATHS = 'folder'
CONF_FILTER = 'filter'
DEFAULT_FILTER = '*'

SCAN_INTERVAL = timedelta(seconds=10)

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_FOLDER_PATHS): cv.isdir,
vol.Optional(CONF_FILTER, default=DEFAULT_FILTER): cv.string,
})


def get_sorted_files_list(folder_path, filter_term):
"""Return the sorted list of files, applying filter."""
query = folder_path + filter_term
files_list = glob.glob(query)
sorted_files_list = sorted(files_list, key=os.path.getmtime)
return sorted_files_list


def get_last_updated(recent_modified_file):
"""Return the time a file was last modified."""
modified_time = os.path.getmtime(recent_modified_file)
modified_time_datetime = dt.fromtimestamp(modified_time)
return modified_time_datetime.strftime(DATE_STR_FORMAT)


def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the folder sensor."""
path = config.get(CONF_FOLDER_PATHS)

if not hass.config.is_allowed_path(path):
_LOGGER.error("folder %s is not valid or allowed", path)
else:
folder = Folder(path, config.get(CONF_FILTER))
add_devices([folder], True)


class Folder(Entity):
"""Representation of a folder."""

ICON = 'mdi:folder'

def __init__(self, folder_path, filter_term):
"""Initialize the data object."""
folder_path = os.path.join(folder_path, '') # If no trailing / add it
self._folder_path = folder_path # Need to check its a valid path
self._filter_term = filter_term
self._sorted_files_list = []
self._number_of_files = None
self._recent_modified_file = None
self._last_updated = None
self._name = os.path.split(os.path.split(folder_path)[0])[1]

def update(self):
"""Update the sensor."""
self._sorted_files_list = get_sorted_files_list(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better if we don't keep this in memory but instead only set the instance variables that we want to derive from it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

self._folder_path, self._filter_term)

self._recent_modified_file = self._sorted_files_list[-1]

self._last_updated = get_last_updated(
self._recent_modified_file)

@property
def name(self):
"""Return the name of the sensor."""
return self._name

@property
def state(self):
"""Return the state of the sensor."""
return self._last_updated

@property
def icon(self):
"""Icon to use in the frontend, if any."""
return self.ICON

@property
def device_state_attributes(self):
"""Return other details about the sensor state."""
attr = {
'folder': self._folder_path,
'filter': self._filter_term,
'modified_file': os.path.split(self._recent_modified_file)[1],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's drop this attribute. If people want this info, we should get a watchdog component to monitor folders/files and fire events. If people want to automate on this, it will not work if 2 files get added between 2 updates. Then they want to have that fixed etc. I think that we should have people do the right thing right away by not including this, so they will look for the watchdog component instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

'number_of_files': len(self._sorted_files_list),
'files': [os.path.split(f)[1] for f in self._sorted_files_list]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not do this. This is way too much data for in the state machine.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

}
return attr
64 changes: 64 additions & 0 deletions tests/components/sensor/test_folder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""The tests for the folder sensor."""
import unittest
import os

from homeassistant.components.sensor.folder import CONF_FOLDER_PATHS
from homeassistant.setup import setup_component
from tests.common import get_test_home_assistant


CWD = os.path.join(os.path.dirname(__file__))
TEST_FOLDER = 'test_folder'
TEST_DIR = os.path.join(CWD, TEST_FOLDER)
TEST_TXT = 'mock_test_folder.txt'
TEST_FILE = os.path.join(TEST_DIR, TEST_TXT)


def create_file(path):
"""Create a test file."""
with open(path, 'w') as test_file:
test_file.write("test")


class TestFolderSensor(unittest.TestCase):
"""Test the filesize sensor."""

def setup_method(self, method):
"""Set up things to be run when tests are started."""
self.hass = get_test_home_assistant()
if not os.path.isdir(TEST_DIR):
os.mkdir(TEST_DIR)
self.hass.config.whitelist_external_dirs = set((TEST_DIR))

def teardown_method(self, method):
"""Stop everything that was started."""
if os.path.isfile(TEST_FILE):
os.remove(TEST_FILE)
os.rmdir(TEST_DIR)
self.hass.stop()

def test_invalid_path(self):
"""Test that an invalid path is caught."""
config = {
'sensor': {
'platform': 'folder',
CONF_FOLDER_PATHS: 'invalid_path'}
}
self.assertTrue(
setup_component(self.hass, 'sensor', config))
assert len(self.hass.states.entity_ids()) == 0

def test_valid_path(self):
"""Test for a valid path."""
create_file(TEST_FILE)
config = {
'sensor': {
'platform': 'folder',
CONF_FOLDER_PATHS: TEST_DIR}
}
self.assertTrue(
setup_component(self.hass, 'sensor', config))
assert len(self.hass.states.entity_ids()) == 1
state = self.hass.states.get('sensor.test_folder')
assert state.attributes.get('number_of_files') == 1
assert state.attributes.get('files') == [TEST_TXT]