├── README.md ├── custom_components └── files │ ├── __init__.py │ ├── manifest.json │ └── sensor.py └── hacs.json /README.md: -------------------------------------------------------------------------------- 1 | # Files Component 2 | Custom Component for Home Assistant that reads a list of files from a directory into a sensor. 3 | 4 | This was developed for use alongside the [Gallery Card](https://github.com/TarheelGrad1998/gallery-card) but may have other uses. 5 | 6 | ## Installation 7 | [![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs) 8 | 9 | Files must be in the WWW folder, ideally in a subfolder. This component will periodically scan the folder for changes to the files, and is based on the built-in Folder component. 10 | 11 | The component can be installed from HACS (use the Custom Repository option), but follow the below instructions to install manually. 12 | 1. Create a folder in your `config` directory (normally where your configuration.yaml file lives) named `custom_components` 13 | 2. Create a folder in your `custom_components` named `files` 14 | 3. Copy the 3 files (_init_.py, manifest.json, and sensor.py) into the `files` folder 15 | 4. Restart Home Assistant 16 | 5. Create a folder in your `WWW` folder named `images` (or any other name, but be sure to use the proper name below) 17 | 6. Add your images/videos to this folder 18 | 7. Add the files sensor to your configuration.yaml file 19 | ```yaml 20 | - sensor 21 | - platform: files 22 | folder: /config/www/images 23 | filter: '**/*.jpg' 24 | name: gallery_images 25 | sort: date 26 | recursive: True 27 | ``` 28 | 8. Restart Home Assistant 29 | 9. Check the sensor.gallery_images entity to see if the `fileList` attribute lists your files 30 | 31 | ### Configuration Variables 32 | 33 | | Name | Type | Default | Description 34 | | ---- | ---- | ------- | ----------- 35 | | platform | string | **Required** | `files` 36 | | folder | string | **Required** | Folder to scan, must be /config/www/*** 37 | | name | string | **Required** | The entity ID for the sensor 38 | | sort | string | **Optional** | One of 'name', 'date', or 'size'; Determines how files are sorted in the Gallery, `Default: date` 39 | | recursive | boolean | **Optional** | True or False; If True, the pattern filter `**` will match any files and zero or more directories, subdirectories and symbolic links to directories. **Note:** Using the `**` pattern in large directory trees may consume an inordinate amount of time , `Default: False` 40 | 41 | ## Force Reloading 42 | 43 | The entity will automatically update on a schedule, however, if you need to refresh more often or at some event, you can use the [update_entity service call](https://www.home-assistant.io/integrations/homeassistant/#service-homeassistantupdate_entity). 44 | 45 | action: 46 | - service: homeassistant.update_entity 47 | target: 48 | entity_id: 49 | - sensor.gallery_images 50 | 51 | 52 | ## Credits 53 | 54 | This component largely created from work done by @zsarnett in [the slideshow card](https://github.com/zsarnett/slideshow-card), from which other inspiration was also taken. 55 | -------------------------------------------------------------------------------- /custom_components/files/__init__.py: -------------------------------------------------------------------------------- 1 | """Files in a Folder.""" 2 | -------------------------------------------------------------------------------- /custom_components/files/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "files", 3 | "name": "Files in a Folder", 4 | "documentation": "https://github.com/TarheelGrad1998/files", 5 | "issue_tracker": "https://github.com/TarheelGrad1998/files/issues", 6 | "dependencies": [], 7 | "requirements": [], 8 | "codeowners": ["@TarheelGrad1998"], 9 | "version": "1.5" 10 | } 11 | -------------------------------------------------------------------------------- /custom_components/files/sensor.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import logging 3 | import os 4 | from datetime import timedelta 5 | 6 | import homeassistant.helpers.config_validation as cv 7 | import voluptuous as vol 8 | from homeassistant.components.sensor import PLATFORM_SCHEMA 9 | from homeassistant.helpers.entity import Entity 10 | 11 | _LOGGER = logging.getLogger(__name__) 12 | 13 | CONF_FOLDER_PATHS = "folder" 14 | CONF_FILTER = "filter" 15 | CONF_NAME = "name" 16 | CONF_SORT = "sort" 17 | CONF_RECURSIVE = "recursive" 18 | DEFAULT_FILTER = "*" 19 | DEFAULT_SORT = "date" 20 | DEFAULT_RECURSIVE = False 21 | 22 | DOMAIN = "files" 23 | 24 | SCAN_INTERVAL = timedelta(minutes=1) 25 | 26 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 27 | { 28 | vol.Required(CONF_FOLDER_PATHS): cv.isdir, 29 | vol.Required(CONF_NAME): cv.string, 30 | vol.Optional(CONF_FILTER, default=DEFAULT_FILTER): cv.string, 31 | vol.Optional(CONF_SORT, default=DEFAULT_SORT): cv.string, 32 | vol.Optional(CONF_RECURSIVE, default=DEFAULT_RECURSIVE): cv.boolean, 33 | } 34 | ) 35 | 36 | 37 | def get_files_list(folder_path, filter_term, sort, recursive): 38 | """Return the list of files, applying filter.""" 39 | query = folder_path + filter_term 40 | """files_list = glob.glob(query)""" 41 | if sort == "name": 42 | files_list = sorted(glob.glob(query, recursive=recursive)) 43 | elif sort == "size": 44 | files_list = sorted(glob.glob(query, recursive=recursive), key=os.path.getsize) 45 | else: 46 | files_list = sorted(glob.glob(query, recursive=recursive), key=os.path.getmtime) 47 | return files_list 48 | 49 | 50 | def get_size(files_list): 51 | """Return the sum of the size in bytes of files in the list.""" 52 | size_list = [os.stat(f).st_size for f in files_list if os.path.isfile(f)] 53 | return sum(size_list) 54 | 55 | 56 | def setup_platform(hass, config, add_entities, discovery_info=None): 57 | """Set up the folder sensor.""" 58 | path = config.get(CONF_FOLDER_PATHS) 59 | name = config.get(CONF_NAME) 60 | 61 | if not hass.config.is_allowed_path(path): 62 | _LOGGER.error("folder %s is not valid or allowed", path) 63 | else: 64 | folder = FilesSensor( 65 | path, 66 | name, 67 | config.get(CONF_FILTER), 68 | config.get(CONF_SORT), 69 | config.get(CONF_RECURSIVE), 70 | ) 71 | add_entities([folder], True) 72 | 73 | 74 | class FilesSensor(Entity): 75 | """Representation of a folder.""" 76 | 77 | ICON = "mdi:folder" 78 | 79 | def __init__(self, folder_path, name, filter_term, sort, recursive): 80 | """Initialize the data object.""" 81 | folder_path = os.path.join(folder_path, "") # If no trailing / add it 82 | self._folder_path = folder_path # Need to check its a valid path 83 | self._filter_term = filter_term 84 | self._number_of_files = None 85 | self._size = None 86 | # self._name = os.path.split(os.path.split(folder_path)[0])[1] 87 | self._name = name 88 | self._unit_of_measurement = "MB" 89 | self._sort = sort 90 | self._recursive = recursive 91 | 92 | def update(self): 93 | """Update the sensor.""" 94 | files_list = get_files_list( 95 | self._folder_path, self._filter_term, self._sort, self._recursive 96 | ) 97 | self.fileList = files_list 98 | self._number_of_files = len(files_list) 99 | self._size = get_size(files_list) 100 | 101 | @property 102 | def name(self): 103 | """Return the name of the sensor.""" 104 | return self._name 105 | 106 | @property 107 | def state(self): 108 | """Return the state of the sensor.""" 109 | decimals = 2 110 | size_mb = round(self._size / 1e6, decimals) 111 | return size_mb 112 | 113 | @property 114 | def icon(self): 115 | """Icon to use in the frontend, if any.""" 116 | return self.ICON 117 | 118 | @property 119 | def extra_state_attributes(self): 120 | """Return other details about the sensor state.""" 121 | attr = { 122 | "path": self._folder_path, 123 | "filter": self._filter_term, 124 | "number_of_files": self._number_of_files, 125 | "bytes": self._size, 126 | "fileList": self.fileList, 127 | "sort": self._sort, 128 | "recursive": self._recursive, 129 | } 130 | return attr 131 | 132 | @property 133 | def unit_of_measurement(self): 134 | """Return the unit of measurement of this entity, if any.""" 135 | return self._unit_of_measurement 136 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Files Component", 3 | "domains": ["sensor"], 4 | "iot_class": "Local Polling", 5 | "render_readme": true 6 | } 7 | --------------------------------------------------------------------------------