diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index e409b346c6d..56542a0aadd 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -4,10 +4,14 @@ from __future__ import annotations import datetime import logging import os +import pathlib import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import DATA_MEGABYTES from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -23,7 +27,7 @@ _LOGGER = logging.getLogger(__name__) CONF_FILE_PATHS = "file_paths" ICON = "mdi:file" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( {vol.Required(CONF_FILE_PATHS): vol.All(cv.ensure_list, [cv.isfile])} ) @@ -39,11 +43,23 @@ def setup_platform( setup_reload_service(hass, DOMAIN, PLATFORMS) sensors = [] + paths = set() for path in config[CONF_FILE_PATHS]: + try: + fullpath = str(pathlib.Path(path).absolute()) + except OSError as error: + _LOGGER.error("Can not access file %s, error %s", path, error) + continue + + if fullpath in paths: + continue + paths.add(fullpath) + if not hass.config.is_allowed_path(path): _LOGGER.error("Filepath %s is not valid or allowed", path) continue - sensors.append(Filesize(path)) + + sensors.append(Filesize(fullpath)) if sensors: add_entities(sensors, True) @@ -52,48 +68,28 @@ def setup_platform( class Filesize(SensorEntity): """Encapsulates file size information.""" - def __init__(self, path): + _attr_native_unit_of_measurement = DATA_MEGABYTES + _attr_icon = ICON + + def __init__(self, path: str) -> None: """Initialize the data object.""" self._path = path # Need to check its a valid path - self._size = None - self._last_updated = None - self._name = path.split("/")[-1] - self._unit_of_measurement = DATA_MEGABYTES + self._attr_name = path.split("/")[-1] - def update(self): + def update(self) -> None: """Update the sensor.""" - statinfo = os.stat(self._path) - self._size = statinfo.st_size - last_updated = datetime.datetime.fromtimestamp(statinfo.st_mtime) - self._last_updated = last_updated.isoformat() + try: + statinfo = os.stat(self._path) + except OSError as error: + _LOGGER.error("Can not retrieve file statistics %s", error) + self._attr_native_value = None + return - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self): - """Return the size of the file in MB.""" - decimals = 2 - state_mb = round(self._size / 1e6, decimals) - return state_mb - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return ICON - - @property - def extra_state_attributes(self): - """Return other details about the sensor state.""" - return { + size = statinfo.st_size + last_updated = datetime.datetime.fromtimestamp(statinfo.st_mtime).isoformat() + self._attr_native_value = round(size / 1e6, 2) if size else None + self._attr_extra_state_attributes = { "path": self._path, - "last_updated": self._last_updated, - "bytes": self._size, + "last_updated": last_updated, + "bytes": size, } - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement diff --git a/tests/components/filesize/test_sensor.py b/tests/components/filesize/test_sensor.py index 72d0d112f17..fa85ce41437 100644 --- a/tests/components/filesize/test_sensor.py +++ b/tests/components/filesize/test_sensor.py @@ -7,7 +7,9 @@ import pytest from homeassistant import config as hass_config from homeassistant.components.filesize import DOMAIN from homeassistant.components.filesize.sensor import CONF_FILE_PATHS -from homeassistant.const import SERVICE_RELOAD +from homeassistant.const import SERVICE_RELOAD, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component from tests.common import get_fixture_path @@ -16,21 +18,21 @@ TEST_DIR = os.path.join(os.path.dirname(__file__)) TEST_FILE = os.path.join(TEST_DIR, "mock_file_test_filesize.txt") -def create_file(path): +def create_file(path) -> None: """Create a test file.""" with open(path, "w") as test_file: test_file.write("test") @pytest.fixture(autouse=True) -def remove_file(): +def remove_file() -> None: """Remove test file.""" yield if os.path.isfile(TEST_FILE): os.remove(TEST_FILE) -async def test_invalid_path(hass): +async def test_invalid_path(hass: HomeAssistant) -> None: """Test that an invalid path is caught.""" config = {"sensor": {"platform": "filesize", CONF_FILE_PATHS: ["invalid_path"]}} assert await async_setup_component(hass, "sensor", config) @@ -38,7 +40,21 @@ async def test_invalid_path(hass): assert len(hass.states.async_entity_ids("sensor")) == 0 -async def test_valid_path(hass): +async def test_cannot_access_file(hass: HomeAssistant) -> None: + """Test that an invalid path is caught.""" + config = {"sensor": {"platform": "filesize", CONF_FILE_PATHS: [TEST_FILE]}} + + with patch( + "homeassistant.components.filesize.sensor.pathlib", + side_effect=OSError("Can not access"), + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids("sensor")) == 0 + + +async def test_valid_path(hass: HomeAssistant) -> None: """Test for a valid path.""" create_file(TEST_FILE) config = {"sensor": {"platform": "filesize", CONF_FILE_PATHS: [TEST_FILE]}} @@ -51,7 +67,34 @@ async def test_valid_path(hass): assert state.attributes.get("bytes") == 4 -async def test_reload(hass, tmpdir): +async def test_state_unknown(hass: HomeAssistant, tmpdir: str) -> None: + """Verify we handle state unavailable.""" + create_file(TEST_FILE) + testfile = f"{tmpdir}/file" + await hass.async_add_executor_job(create_file, testfile) + with patch.object(hass.config, "is_allowed_path", return_value=True): + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "filesize", + "file_paths": [testfile], + } + }, + ) + await hass.async_block_till_done() + + assert hass.states.get("sensor.file") + + await hass.async_add_executor_job(os.remove, testfile) + await async_update_entity(hass, "sensor.file") + + state = hass.states.get("sensor.file") + assert state.state == STATE_UNKNOWN + + +async def test_reload(hass: HomeAssistant, tmpdir: str) -> None: """Verify we can reload filesize sensors.""" testfile = f"{tmpdir}/file" await hass.async_add_executor_job(create_file, testfile)