1
mirror of https://github.com/home-assistant/core synced 2024-09-18 19:55:20 +02:00

Break out sensors for filesize (#68702)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
G Johansson 2022-03-26 20:43:15 +01:00 committed by GitHub
parent 0c2b5b6c12
commit 00b53502fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 138 additions and 32 deletions

View File

@ -1,7 +1,7 @@
"""Sensor for monitoring the size of a file."""
from __future__ import annotations
import datetime
from datetime import datetime, timedelta
import logging
import os
import pathlib
@ -10,14 +10,25 @@ import voluptuous as vol
from homeassistant.components.sensor import (
PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA,
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_FILE_PATH, DATA_MEGABYTES
from homeassistant.const import CONF_FILE_PATH, DATA_BYTES, DATA_MEGABYTES
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
import homeassistant.util.dt as dt_util
from .const import CONF_FILE_PATHS, DOMAIN
@ -25,6 +36,34 @@ _LOGGER = logging.getLogger(__name__)
ICON = "mdi:file"
SENSOR_TYPES = (
SensorEntityDescription(
key="file",
icon=ICON,
name="Size",
native_unit_of_measurement=DATA_MEGABYTES,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="bytes",
entity_registry_enabled_default=False,
icon=ICON,
name="Size bytes",
native_unit_of_measurement=DATA_BYTES,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="last_updated",
entity_registry_enabled_default=False,
icon=ICON,
name="Last Updated",
device_class=SensorDeviceClass.TIMESTAMP,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
)
PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(
{vol.Required(CONF_FILE_PATHS): vol.All(cv.ensure_list, [cv.isfile])}
)
@ -65,36 +104,82 @@ async def async_setup_entry(
get_path = await hass.async_add_executor_job(pathlib.Path, path)
fullpath = str(get_path.absolute())
coordinator = FileSizeCoordinator(hass, fullpath)
await coordinator.async_config_entry_first_refresh()
if get_path.exists() and get_path.is_file():
async_add_entities([FilesizeEntity(fullpath, entry.entry_id)], True)
async_add_entities(
[
FilesizeEntity(description, fullpath, entry.entry_id, coordinator)
for description in SENSOR_TYPES
]
)
class FilesizeEntity(SensorEntity):
"""Encapsulates file size information."""
class FileSizeCoordinator(DataUpdateCoordinator):
"""Filesize coordinator."""
_attr_native_unit_of_measurement = DATA_MEGABYTES
_attr_icon = ICON
def __init__(self, hass: HomeAssistant, path: str) -> None:
"""Initialize filesize coordinator."""
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=60),
)
self._path = path
def __init__(self, path: str, entry_id: str) -> None:
"""Initialize the data object."""
self._path = path # Need to check its a valid path
self._attr_name = path.split("/")[-1]
self._attr_unique_id = entry_id
def update(self) -> None:
"""Update the sensor."""
async def _async_update_data(self) -> dict[str, float | int | datetime]:
"""Fetch file information."""
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
raise UpdateFailed(f"Can not retrieve file statistics {error}") from error
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": last_updated,
last_updated = datetime.fromtimestamp(statinfo.st_mtime).replace(
tzinfo=dt_util.UTC
)
_LOGGER.debug("size %s, last updated %s", size, last_updated)
data: dict[str, int | float | datetime] = {
"file": round(size / 1e6, 2),
"bytes": size,
"last_updated": last_updated,
}
return data
class FilesizeEntity(CoordinatorEntity[FileSizeCoordinator], SensorEntity):
"""Encapsulates file size information."""
entity_description: SensorEntityDescription
def __init__(
self,
description: SensorEntityDescription,
path: str,
entry_id: str,
coordinator: FileSizeCoordinator,
) -> None:
"""Initialize the data object."""
super().__init__(coordinator)
base_name = path.split("/")[-1]
self._attr_name = f"{base_name} {description.name}"
self._attr_unique_id = (
entry_id if description.key == "file" else f"{entry_id}-{description.key}"
)
self.entity_description = description
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, entry_id)},
name=base_name,
)
@property
def native_value(self) -> float | int | datetime:
"""Return the value of the sensor."""
value: float | int | datetime = self.coordinator.data[
self.entity_description.key
]
return value

View File

@ -1,9 +1,11 @@
"""The tests for the filesize sensor."""
import os
from homeassistant.const import CONF_FILE_PATH, STATE_UNKNOWN
from homeassistant.components.filesize.const import DOMAIN
from homeassistant.const import CONF_FILE_PATH, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_component import async_update_entity
from homeassistant.setup import async_setup_component
from . import TEST_FILE, TEST_FILE_NAME, create_file
@ -38,19 +40,18 @@ async def test_valid_path(
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.file_txt")
state = hass.states.get("sensor.file_txt_size")
assert state
assert state.state == "0.0"
assert state.attributes.get("bytes") == 4
await hass.async_add_executor_job(os.remove, testfile)
async def test_state_unknown(
async def test_state_unavailable(
hass: HomeAssistant, tmpdir: str, mock_config_entry: MockConfigEntry
) -> None:
"""Verify we handle state unavailable."""
testfile = f"{tmpdir}/file"
testfile = f"{tmpdir}/file.txt"
create_file(testfile)
hass.config.allowlist_external_dirs = {tmpdir}
mock_config_entry.add_to_hass(hass)
@ -61,12 +62,32 @@ async def test_state_unknown(
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.file")
state = hass.states.get("sensor.file_txt_size")
assert state
assert state.state == "0.0"
await hass.async_add_executor_job(os.remove, testfile)
await async_update_entity(hass, "sensor.file")
await async_update_entity(hass, "sensor.file_txt_size")
state = hass.states.get("sensor.file")
assert state.state == STATE_UNKNOWN
state = hass.states.get("sensor.file_txt_size")
assert state.state == STATE_UNAVAILABLE
async def test_import_query(hass: HomeAssistant, tmpdir: str) -> None:
"""Test import from yaml."""
testfile = f"{tmpdir}/file.txt"
create_file(testfile)
hass.config.allowlist_external_dirs = {tmpdir}
config = {
"sensor": {
"platform": "filesize",
"file_paths": [testfile],
}
}
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
assert hass.config_entries.async_entries(DOMAIN)
data = hass.config_entries.async_entries(DOMAIN)[0].data
assert data[CONF_FILE_PATH] == testfile