├── requirements_test.txt ├── hacs.json ├── custom_components └── file_restore │ ├── services.yaml │ ├── manifest.json │ ├── utilities.py │ ├── const.py │ ├── translations │ ├── en.json │ └── it.json │ ├── __init__.py │ ├── config_schema.py │ ├── sensor.py │ └── config_flow.py ├── resources.json ├── .github └── workflows │ ├── validate-hacs.yml │ └── ci.yml ├── LICENSE ├── pyproject.toml ├── README.md ├── info.md └── .pre-commit-config.yaml /requirements_test.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "File restore" 3 | } 4 | -------------------------------------------------------------------------------- /custom_components/file_restore/services.yaml: -------------------------------------------------------------------------------- 1 | reload: 2 | description: Reload all file_restore entities. 3 | name: reload file restore 4 | -------------------------------------------------------------------------------- /resources.json: -------------------------------------------------------------------------------- 1 | [ 2 | "https://raw.githubusercontent.com/custom-components/sensor.file_restore/master/custom_components/file_restore/__init__.py", 3 | "https://raw.githubusercontent.com/custom-components/sensor.file_restore/master/custom_components/file_restore/const.py", 4 | "https://raw.githubusercontent.com/custom-components/sensor.file_restore/master/custom_components/file_restore/reproduce_state" 5 | ] 6 | -------------------------------------------------------------------------------- /custom_components/file_restore/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "file_restore", 3 | "name": "File Restore", 4 | "documentation": "https://github.com/custom-components/sensor.file_restore", 5 | "issue_tracker": "https://github.com/custom-components/sensor.file_restore/issues", 6 | "dependencies": [], 7 | "codeowners": [], 8 | "requirements": [], 9 | "config_flow": true, 10 | "iot_class": "local_push", 11 | "version": "4.2" 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/validate-hacs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Validate with hassfest 3 | 4 | on: 5 | push: 6 | pull_request: 7 | schedule: 8 | - cron: 0 0 * * * 9 | 10 | jobs: 11 | validate: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: home-assistant/actions/hassfest@master 16 | hacs: 17 | name: HACS Action 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: HACS Action 22 | uses: hacs/action@main 23 | with: 24 | category: integration 25 | -------------------------------------------------------------------------------- /custom_components/file_restore/utilities.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | _LOGGER = logging.getLogger(__name__) 5 | 6 | def create_file_and_directory(file_path, name) -> bool: 7 | data=f"File automatically generated with creation of file_restore entity {name}\n------------------------------------------------------------------------------\n" 8 | directory=file_path[:file_path.rfind("/"):] 9 | if not os.path.isdir(directory): 10 | os.mkdir(directory) 11 | with open(file_path, 'w', encoding='utf-8') as file_data: 12 | file_data.write(data) 13 | _LOGGER.info("File and/or directory was missing. Proceeded with creation.") 14 | return True 15 | 16 | def null_data_cleaner(original_data: dict, data: dict) -> dict: 17 | """ this is to remove all null parameters from data that are added during option flow """ 18 | for key in data.keys(): 19 | if data[key] == "null": 20 | original_data[key] = "" 21 | else: 22 | original_data[key]=data[key] 23 | return original_data 24 | -------------------------------------------------------------------------------- /custom_components/file_restore/const.py: -------------------------------------------------------------------------------- 1 | from homeassistant.const import Platform 2 | 3 | """ Useful constant for file_restore integration """ 4 | #Generic 5 | VERSION = "4.5" 6 | DOMAIN = "file_restore" 7 | PLATFORM = [Platform.SENSOR] 8 | ISSUE_URL = "https://github.com/custom-components/sensor.file_restore/issues" 9 | CONFIGFLOW_VERSION = 2 10 | 11 | #Defaults 12 | DEFAULT_NAME = 'file_restore' 13 | DEFAULT_LENGTH = 'week' 14 | DEFAULT_DETAIL = 'hour' 15 | ICON = 'mdi:file' 16 | 17 | #Attributes 18 | ATTR_TEMPERATURES = 'program_values' 19 | 20 | #Time constant 21 | CON_YEAR = "year" 22 | CON_MONTH = "month" 23 | CON_WEEK = "week" 24 | CON_DAY = "day" 25 | CON_HOUR = "hour" 26 | CON_MINUTE = "minute" 27 | 28 | #Options 29 | LENGTH_OPTIONS = [CON_YEAR, CON_MONTH, CON_WEEK, CON_DAY, CON_HOUR] 30 | DETAL_OPTIONS_FULL = [CON_MONTH, CON_WEEK, CON_DAY, CON_HOUR, CON_MINUTE] 31 | DETAIL_OPTIONS_YEAR = [CON_MONTH, CON_WEEK, CON_DAY] 32 | DETAIL_OPTIONS_MONTH = [CON_DAY, CON_HOUR] 33 | DETAIL_OPTIONS_WEEK = [CON_DAY, CON_HOUR] 34 | DETAIL_OPTIONS_DAY = [CON_HOUR, CON_MINUTE] 35 | DETAIL_OPTIONS_HOUR = [CON_MINUTE] 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /custom_components/file_restore/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "File Restore - definition of base data", 6 | "description": "Define the base data. Documentation on https://github.com/custom-components/sensor.file_restore", 7 | "data": { 8 | "name": "Name", 9 | "file_path": "File path (file name included) - use /", 10 | "unit_of_measurement": "Unit of measurement", 11 | "length": "Period length" 12 | } 13 | }, 14 | "final": { 15 | "title": "File Restore - Definition of period detail", 16 | "description": "Define the period detail. Documentation on https://github.com/custom-components/sensor.file_restore", 17 | "data": { 18 | "detail": "Period detail" 19 | } 20 | } 21 | }, 22 | "error": { 23 | "file_path_empty": "File path is empty" 24 | }, 25 | "abort": { 26 | "single_instance_allowed": "Issue during configuration" 27 | } 28 | }, 29 | "options": { 30 | "step": { 31 | "init": { 32 | "title": "File Restore - definition of base data", 33 | "description": "Define the base data. Documentation on https://github.com/custom-components/sensor.file_restore", 34 | "data": { 35 | "name": "Name", 36 | "file_path": "File path (file name included) - use /", 37 | "unit_of_measurement": "Unit of measurement - null to leave empty the field", 38 | "length": "Period length" 39 | } 40 | }, 41 | "final": { 42 | "title": "File Restore - Definition of period detail", 43 | "description": "Define the period detail. Documentation on https://github.com/custom-components/sensor.file_restore", 44 | "data": { 45 | "detail": "Period detail" 46 | } 47 | } 48 | }, 49 | "error": { 50 | "file_path_empty": "File path is empty" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /custom_components/file_restore/translations/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "File Restore - definizione dati base", 6 | "description": "Definisci le informazioni di base. Documentazione su https://github.com/custom-components/sensor.file_restore", 7 | "data": { 8 | "name": "Nome", 9 | "file_path": "Percorso file (includere nome file) - usa /", 10 | "unit_of_measurement": "Unità di misura", 11 | "length": "Lunghezza del periodo" 12 | } 13 | }, 14 | "final": { 15 | "title": "File Restore - definizione dettaglio del periodo", 16 | "description": "Definisci il dettaglio del periodo. Documentazione su https://github.com/custom-components/sensor.file_restore", 17 | "data": { 18 | "detail": "Dettaglio del periodo" 19 | } 20 | } 21 | }, 22 | "error": { 23 | "file_path_empty": "Il percorso del file è vuoto" 24 | }, 25 | "abort": { 26 | "single_instance_allowed": "Problema durante la configurazione" 27 | } 28 | }, 29 | "options": { 30 | "step": { 31 | "init": { 32 | "title": "File Restore - definizione dati base", 33 | "description": "Definisci le informazioni di base. Documentazione su https://github.com/custom-components/sensor.file_restore", 34 | "data": { 35 | "name": "Nome", 36 | "file_path": "Percorso file (includere nome file) - usa /", 37 | "unit_of_measurement": "Unità di misura - null per lasciare vuoto il campo", 38 | "length": "Lunghezza del periodo" 39 | } 40 | }, 41 | "final": { 42 | "title": "File Restore - definizione dettaglio del periodo", 43 | "description": "Definisci il dettaglio del periodo. Documentazione su https://github.com/custom-components/sensor.file_restore", 44 | "data": { 45 | "detail": "Dettaglio del periodo" 46 | } 47 | } 48 | }, 49 | "error": { 50 | "file_path_empty": "Il percorso del file è vuoto" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | # PEP 621 project metadata 3 | # See https://www.python.org/dev/peps/pep-0621/ 4 | dynamic = ["version"] 5 | #authors = [ {name = "TBD", email = "TBD"}, ] 6 | #license = {text = "TBD"} 7 | requires-python = ">=3.9.0" 8 | dependencies = [ 9 | ] 10 | name = "ha_programmable_thermostat" 11 | description = "TBD" 12 | readme = "README.md" 13 | classifiers=[ 14 | "Development Status :: 5 - Production/Stable", 15 | "Intended Audience :: Developers", 16 | #"License :: OSI Approved :: TBD", 17 | "Operating System :: Unix", 18 | "Operating System :: POSIX", 19 | "Programming Language :: Python", 20 | "Programming Language :: Python :: 3.9", 21 | "Programming Language :: Python :: 3.10", 22 | "Programming Language :: Python :: 3.11", 23 | "Topic :: Utilities", 24 | "Natural Language :: English", 25 | ] 26 | 27 | 28 | [tool.codespell] 29 | ignore-words-list = """ 30 | master, 31 | slave, 32 | hass""" 33 | skip = """ 34 | ./.*,./assets/*,./data/*,*.svg,*.css,*.json,*.js 35 | """ 36 | quiet-level=2 37 | ignore-regex = '\\[fnrstv]' 38 | builtin = "clear,rare,informal,usage,code,names" 39 | 40 | # --------- Pylint ------------- 41 | [tool.pylint.'TYPECHECK'] 42 | generated-members = "sh" 43 | 44 | [tool.pylint.'MESSAGES CONTROL'] 45 | extension-pkg-whitelist = "pydantic" 46 | disable = [ 47 | "broad-except", 48 | "invalid-name", 49 | "line-too-long", 50 | "missing-function-docstring", 51 | "missing-module-docstring", 52 | "too-few-public-methods", 53 | "too-many-arguments", 54 | "too-many-branches", 55 | "too-many-instance-attributes", 56 | "too-many-statements", 57 | "unused-import", 58 | "wrong-import-order", 59 | ] 60 | 61 | [tool.pylint.FORMAT] 62 | expected-line-ending-format = "LF" 63 | 64 | 65 | 66 | # --------- Mypy ------------- 67 | 68 | [tool.mypy] 69 | show_error_codes = true 70 | follow_imports = "silent" 71 | ignore_missing_imports = false 72 | strict_optional = true 73 | warn_redundant_casts = true 74 | warn_unused_ignores = true 75 | disallow_any_generics = true 76 | check_untyped_defs = true 77 | no_implicit_reexport = true 78 | warn_unused_configs = true 79 | disallow_subclassing_any = true 80 | disallow_incomplete_defs = true 81 | disallow_untyped_decorators = true 82 | disallow_untyped_calls = true 83 | disallow_untyped_defs = true 84 | plugins = [ 85 | "pydantic.mypy" 86 | ] 87 | 88 | [tool.pydantic-mypy] 89 | init_forbid_extra = true 90 | init_typed = true 91 | warn_required_dynamic_aliases = true 92 | warn_untyped_fields = true 93 | 94 | [[tool.mypy.overrides]] 95 | module = "tests.*" 96 | # Required to not have error: Untyped decorator makes function on fixtures and 97 | # parametrize decorators 98 | disallow_untyped_decorators = false 99 | 100 | [[tool.mypy.overrides]] 101 | #module = [ ] 102 | ignore_missing_imports = true 103 | -------------------------------------------------------------------------------- /custom_components/file_restore/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This component is an upgraded version of file sensor. 3 | It has the same characteristics but it: 4 | - expect a vecotr of data read from file in order to be able to interpret it. 5 | - vector lenght is dependant to information of setup. 6 | - It has an additional property that return the whole vector read. 7 | """ 8 | import os 9 | import logging 10 | import asyncio 11 | 12 | from homeassistant import config_entries 13 | from homeassistant.config_entries import ( 14 | SOURCE_IMPORT, 15 | ConfigEntry 16 | ) 17 | from homeassistant.helpers import discovery 18 | from homeassistant.util import Throttle 19 | from .sensor import FileSensor 20 | from .const import ( 21 | VERSION, 22 | DOMAIN, 23 | PLATFORM, 24 | ISSUE_URL, 25 | CONFIGFLOW_VERSION 26 | ) 27 | 28 | _LOGGER = logging.getLogger(__name__) 29 | 30 | async def async_setup(hass, config): 31 | _LOGGER.info("Set up of integration %s, version %s, in case of issue open ticket at %s", DOMAIN, VERSION, ISSUE_URL) 32 | return True 33 | 34 | async def async_setup_entry(hass, config_entry: ConfigEntry): 35 | """Set up this integration using UI.""" 36 | if config_entry.source == config_entries.SOURCE_IMPORT: 37 | # We get here if the integration is set up using YAML 38 | hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id)) 39 | return True 40 | undo_listener = config_entry.add_update_listener(update_listener) 41 | _LOGGER.info("Added new FileRestore entity, entry_id: %s", config_entry.entry_id) 42 | await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORM) 43 | 44 | return True 45 | 46 | async def async_unload_entry(hass, config_entry): 47 | """Unload a config entry.""" 48 | _LOGGER.debug("async_unload_entry: %s", config_entry) 49 | await asyncio.gather(hass.config_entries.async_forward_entry_unload(config_entry, PLATFORM)) 50 | return True 51 | 52 | async def update_listener(hass, config_entry): 53 | """Handle options update.""" 54 | _LOGGER.debug("update_listener: %s", config_entry) 55 | await hass.config_entries.async_reload(config_entry.entry_id) 56 | 57 | async def async_migrate_entry(hass, config_entry: ConfigEntry): 58 | """Migrate old entry.""" 59 | _LOGGER.debug("Migrating from version %s to version %s", config_entry.version, CONFIGFLOW_VERSION) 60 | 61 | new_data = {**config_entry.data} 62 | new_options = {**config_entry.options} 63 | 64 | if config_entry.version == 1: 65 | config_entry.unique_id = config_entry.data["unique_id"] 66 | del new_data["unique_id"] 67 | config_entry.version = CONFIGFLOW_VERSION 68 | config_entry.data = {**new_data} 69 | _LOGGER.info("Migration of entry %s done to version %s", config_entry.title, config_entry.version) 70 | return True 71 | 72 | _LOGGER.info("Migration not required") 73 | return True 74 | -------------------------------------------------------------------------------- /custom_components/file_restore/config_schema.py: -------------------------------------------------------------------------------- 1 | """ Configuration schema description for file_restore integration """ 2 | import voluptuous as vol 3 | import logging 4 | import homeassistant.helpers.config_validation as cv 5 | from homeassistant.const import CONF_NAME 6 | from .const import ( 7 | DEFAULT_NAME, 8 | DEFAULT_LENGTH, 9 | DEFAULT_DETAIL, 10 | CON_YEAR, 11 | CON_MONTH, 12 | CON_WEEK, 13 | CON_DAY, 14 | CON_HOUR, 15 | CON_MINUTE, 16 | LENGTH_OPTIONS, 17 | DETAL_OPTIONS_FULL, 18 | DETAIL_OPTIONS_YEAR, 19 | DETAIL_OPTIONS_MONTH, 20 | DETAIL_OPTIONS_WEEK, 21 | DETAIL_OPTIONS_DAY, 22 | DETAIL_OPTIONS_HOUR, 23 | ICON 24 | ) 25 | 26 | CONF_FILE_PATH = 'file_path' 27 | CONF_UNIT_OF_MEASUREMENT = 'unit_of_measurement' 28 | CONF_LENGTH = 'length' 29 | CONF_DETAIL = 'detail' 30 | 31 | SENSOR_SCHEMA = { 32 | vol.Required(CONF_FILE_PATH): cv.isfile, 33 | vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, 34 | vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, 35 | vol.Optional(CONF_DETAIL, default=DEFAULT_DETAIL): vol.In(DETAL_OPTIONS_FULL), 36 | vol.Optional(CONF_LENGTH, default=DEFAULT_LENGTH): vol.In(LENGTH_OPTIONS) 37 | } 38 | 39 | def get_config_flow_schema(config: dict = {}, config_flow_step: int = 0, length_value: str = DEFAULT_LENGTH) -> dict: 40 | if not config: 41 | config = { 42 | CONF_NAME: DEFAULT_NAME, 43 | CONF_FILE_PATH: "", 44 | CONF_UNIT_OF_MEASUREMENT: "", 45 | CONF_LENGTH: DEFAULT_LENGTH, 46 | CONF_DETAIL: DEFAULT_DETAIL 47 | } 48 | if config_flow_step==1: 49 | return { 50 | vol.Optional(CONF_NAME, default=config.get(CONF_NAME)): str, 51 | vol.Required(CONF_FILE_PATH, default=config.get(CONF_FILE_PATH)): str, 52 | vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=config.get(CONF_UNIT_OF_MEASUREMENT)): str, 53 | vol.Optional(CONF_LENGTH, default=config.get(CONF_LENGTH)): vol.In(LENGTH_OPTIONS) 54 | } 55 | elif config_flow_step==3: 56 | #identical to step 1 but without NAME (better to not change it since it will break configuration) 57 | #this is used for options flow only 58 | return { 59 | vol.Required(CONF_FILE_PATH, default=config.get(CONF_FILE_PATH)): str, 60 | vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=config.get(CONF_UNIT_OF_MEASUREMENT)): str, 61 | vol.Optional(CONF_LENGTH, default=config.get(CONF_LENGTH)): vol.In(LENGTH_OPTIONS) 62 | } 63 | elif config_flow_step==2 and length_value==CON_YEAR: 64 | return { 65 | vol.Required(CONF_DETAIL, default=config.get(CONF_DETAIL)): vol.In(DETAIL_OPTIONS_YEAR) 66 | } 67 | elif config_flow_step==2 and length_value==CON_MONTH: 68 | return { 69 | vol.Required(CONF_DETAIL, default=config.get(CONF_DETAIL)): vol.In(DETAIL_OPTIONS_MONTH) 70 | } 71 | elif config_flow_step==2 and length_value==CON_WEEK: 72 | return { 73 | vol.Optional(CONF_DETAIL, default=config.get(CONF_DETAIL)): vol.In(DETAIL_OPTIONS_WEEK) 74 | } 75 | elif config_flow_step==2 and length_value==CON_DAY: 76 | return { 77 | vol.Required(CONF_DETAIL, default=config.get(CONF_DETAIL)): vol.In(DETAIL_OPTIONS_DAY) 78 | } 79 | elif config_flow_step==2 and length_value==CON_HOUR: 80 | return { 81 | vol.Required(CONF_DETAIL, default=config.get(CONF_DETAIL)): vol.In(DETAIL_OPTIONS_HOUR) 82 | } 83 | 84 | return {} 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FILE RESTORE 2 | 3 | This component is a revision of the official Home Assistant component 'File Sensor' in order to have specifica function to program the value during the day. 4 | The component include also a special attribute that include the all the value read from the file configured in the setup. 5 | 6 | `file_restore` component has been developed to fulfill the need of a Home Assistant object that is able to return a different value according to time, for example you can get a different value each hour within a week. 7 | In other word this will allow you to define a program of value that will change with time and repeat when reached the last value. In this way you can use that value to do actions accordingly. 8 | 9 | ## HOW TO INSTALL 10 | Use HACS that is the easier and safer way to do it and to manage updates. 11 | Then setup the entities with user iterface. 12 | 13 | Note: old manual method still available. 14 | 15 | ## EXAMPLE OF SETUP 16 | Here below the example of setup of sensor and parameters to configure. 17 | 18 | ```yaml 19 | sensor: 20 | - platform: file_restore 21 | unit_of_measurement: '°C' 22 | file_path: {path}/file.txt 23 | name: File 24 | length: month 25 | detail: day 26 | ``` 27 | 28 | Field | Value | Necessity | Comments 29 | --- | --- | --- | --- 30 | platform | `file_restore` | *Required* | 31 | unit_of_measurement | | Optional | 32 | file_path | | *Required* | path of the file. My suggestion is to use WWW folder. Be sure that the URL is whitelisted, if needed. Directory and file will be created at setup if missing, but remember to add data! 33 | name | File_restore | Optional | 34 | length | week | Optional | this define the length of the period. Possibile combinantion of length and detail below. 35 | detail | hour | Optional | this define the detail of the period. Possibile combinantion of length and detail below. 36 | 37 | ## SPECIFICITIES 38 | ### DATA FILE 39 | File defined in `file_path` must have the structure of a CSV file with value separated by a comma. The list of data is composed by the number of elements in the table below. This table shows also the possibile combinantion of data. 40 | 41 | Length | Detail | Number of elements | Note 42 | --- | --- | --- | --- 43 | hour | minute | 60 | This will change the value each minute and will restart form the first value each hour. 44 | day | minute | 1440 | This will change the value each minute and will restart from the first value each day. 45 | day | hour | 24 | This will change the value each hour and will restart from the first value each day. 46 | week | hour | 168 | This will change the value each hour and will restart from the first value each week. 47 | week | day | 7 | This will change the value each day and will restart from the first value each week. 48 | month | hour | 744 | This will change the value each hour and will restart from the first value each month. 49 | month | day | 31 | This will change the value each day and will restart from the first value each month. 50 | year | day | 366 | This will change the value each day and will restart from the first value each year. 51 | year | week | 53 | This will change the value each week and will restart from the first value each year. 52 | year | month | 12 | This will change the value each month and will restart from the first value each year. 53 | 54 | NOTE: 55 | - Week is counted from Monday to Sunday (ISO week). 56 | - First day of the year is Jan-01 57 | - First week of the year is the one that include at least 4 days (ISO definition) 58 | - Only last line of file will be read. 59 | - Data in the CSV file has to be numbers. 60 | 61 | To give you an example: 62 | ```csv 63 | 10.0, 10.5, ...(165 other values)..., 11.0 64 | ``` 65 | ### ATTRIBUTE AND STATE 66 | Attribute `program_values` that include all values read from the file. 67 | State of the sensor will change automatically according the the data read from file. 68 | 69 | ## NOTE 70 | This component has been developed for the bigger project of building a smart thermostat using Home Assistant and way cheeper then the commercial ones. 71 | You can find more infomration [here][2]. 72 | 73 | *** 74 | Due to how `custom_components` are loaded, it could be possible to have a `ModuleNotFoundError` error on first boot after adding this; to resolve it, restart Home-Assistant. 75 | 76 | *** 77 | ![logo][1] 78 | 79 | [1]: https://github.com/MapoDan/home-assistant/blob/master/mapodanlogo.png 80 | [3]: https://github.com/MapoDan/home-assistant 81 | -------------------------------------------------------------------------------- /info.md: -------------------------------------------------------------------------------- 1 | # FILE RESTORE 2 | 3 | This component is a revision of the official Home Assistant component 'File Sensor' in order to have specifica function to program the value during the day. 4 | The component include also a special attribute that include the all the value read from the file configured in the setup. 5 | 6 | `file_restore` component has been developed to fulfill the need of a Home Assistant object that is able to return a different value according to time, for example you can get a different value each hour within a week. 7 | In other word this will allow you to define a program of value that will change with time and repeat when reached the last value. In this way you can use that value to do actions accordingly. 8 | 9 | ## HOW TO INSTALL 10 | Use HACS that is the easier and safer way to do it and to manage updates. 11 | Then setup the entities with user iterface. 12 | 13 | Note: old manual method still available. 14 | 15 | ## EXAMPLE OF SETUP 16 | Here below the example of setup of sensor and parameters to configure. 17 | 18 | ```yaml 19 | sensor: 20 | - platform: file_restore 21 | unit_of_measurement: '°C' 22 | file_path: {path}/file.txt 23 | name: File 24 | length: month 25 | detail: day 26 | ``` 27 | 28 | Field | Value | Necessity | Comments 29 | --- | --- | --- | --- 30 | platform | `file_restore` | *Required* | 31 | unit_of_measurement | | Optional | 32 | file_path | | *Required* | path of the file. My suggestion is to use WWW folder. Be sure that the URL is whitelisted, if needed. Directory and file will be created at setup if missing, but remember to add data! 33 | name | File_restore | Optional | 34 | length | week | Optional | this define the length of the period. Possibile combinantion of length and detail below. 35 | detail | hour | Optional | this define the detail of the period. Possibile combinantion of length and detail below. 36 | 37 | ## SPECIFICITIES 38 | ### DATA FILE 39 | File defined in `file_path` must have the structure of a CSV file with value separated by a comma. The list of data is composed by the number of elements in the table below. This table shows also the possibile combinantion of data. 40 | 41 | Length | Detail | Number of elements | Note 42 | --- | --- | --- | --- 43 | hour | minute | 60 | This will change the value each minute and will restart form the first value each hour. 44 | day | minute | 1440 | This will change the value each minute and will restart from the first value each day. 45 | day | hour | 24 | This will change the value each hour and will restart from the first value each day. 46 | week | hour | 168 | This will change the value each hour and will restart from the first value each week. 47 | week | day | 7 | This will change the value each day and will restart from the first value each week. 48 | month | hour | 744 | This will change the value each hour and will restart from the first value each month. 49 | month | day | 31 | This will change the value each day and will restart from the first value each month. 50 | year | day | 366 | This will change the value each day and will restart from the first value each year. 51 | year | week | 53 | This will change the value each week and will restart from the first value each year. 52 | year | month | 12 | This will change the value each month and will restart from the first value each year. 53 | 54 | NOTE: 55 | - Week is counted from Monday to Sunday (ISO week). 56 | - First day of the year is Jan-01 57 | - First week of the year is the one that include at least 4 days (ISO definition) 58 | - Only last line of file will be read. 59 | - Data in the CSV file has to be numbers. 60 | 61 | To give you an example: 62 | ```csv 63 | 10.0, 10.5, ...(165 other values)..., 11.0 64 | ``` 65 | ### ATTRIBUTE AND STATE 66 | Attribute `program_values` that include all values read from the file. 67 | State of the sensor will change automatically according the the data read from file. 68 | 69 | ## NOTE 70 | This component has been developed for the bigger project of building a smart thermostat using Home Assistant and way cheeper then the commercial ones. 71 | You can find more infomration [here][2]. 72 | 73 | *** 74 | Due to how `custom_components` are loaded, it could be possible to have a `ModuleNotFoundError` error on first boot after adding this; to resolve it, restart Home-Assistant. 75 | 76 | *** 77 | ![logo][1] 78 | 79 | [1]: https://github.com/MapoDan/home-assistant/blob/master/mapodanlogo.png 80 | [3]: https://github.com/MapoDan/home-assistant 81 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | files: ^(.*\.(py|json|md|sh|yaml|cfg|txt))$ 3 | exclude: ^(\.[^/]*cache/.*|.*/_user.py)$ 4 | repos: 5 | - repo: https://github.com/verhovsky/pyupgrade-docs 6 | rev: v0.3.0 7 | hooks: 8 | - id: pyupgrade-docs 9 | - repo: https://github.com/executablebooks/mdformat 10 | # Do this before other tools "fixing" the line endings 11 | rev: 0.7.16 12 | hooks: 13 | - id: mdformat 14 | name: Format Markdown 15 | entry: mdformat # Executable to run, with fixed options 16 | language: python 17 | types: [markdown] 18 | args: [--wrap, '75', --number] 19 | additional_dependencies: 20 | - mdformat-toc 21 | - mdformat-beautysh 22 | # -mdformat-shfmt 23 | # -mdformat-tables 24 | - mdformat-config 25 | - mdformat-black 26 | - mdformat-web 27 | - mdformat-gfm 28 | - repo: https://github.com/asottile/blacken-docs 29 | rev: 1.13.0 30 | hooks: 31 | - id: blacken-docs 32 | additional_dependencies: [black==22.6.0] 33 | stages: [manual] # Manual because already done by mdformat-black 34 | - repo: https://github.com/pre-commit/pre-commit-hooks 35 | rev: v4.4.0 36 | hooks: 37 | - id: no-commit-to-branch 38 | args: [--branch, main] 39 | - id: check-yaml 40 | args: [--unsafe] 41 | - id: debug-statements 42 | - id: end-of-file-fixer 43 | - id: trailing-whitespace 44 | - id: check-json 45 | - id: mixed-line-ending 46 | - id: check-builtin-literals 47 | - id: check-ast 48 | - id: check-merge-conflict 49 | - id: check-executables-have-shebangs 50 | - id: check-shebang-scripts-are-executable 51 | - id: check-docstring-first 52 | - id: fix-byte-order-marker 53 | - id: check-case-conflict 54 | # - id: check-toml 55 | - repo: https://github.com/adrienverge/yamllint.git 56 | rev: v1.29.0 57 | hooks: 58 | - id: yamllint 59 | args: 60 | - --no-warnings 61 | - -d 62 | - '{extends: relaxed, rules: {line-length: {max: 90}}}' 63 | - repo: https://github.com/lovesegfault/beautysh.git 64 | rev: v6.2.1 65 | hooks: 66 | - id: beautysh 67 | - repo: https://github.com/asottile/pyupgrade 68 | rev: v3.3.1 69 | hooks: 70 | - id: pyupgrade 71 | args: 72 | - --py310-plus 73 | - repo: https://github.com/psf/black 74 | rev: 22.12.0 75 | hooks: 76 | - id: black 77 | args: 78 | - --safe 79 | - --quiet 80 | - -l 79 81 | - repo: https://github.com/Lucas-C/pre-commit-hooks-bandit 82 | rev: v1.0.6 83 | hooks: 84 | - id: python-bandit-vulnerability-check 85 | - repo: https://github.com/fsouza/autoflake8 86 | rev: v0.4.0 87 | hooks: 88 | - id: autoflake8 89 | args: 90 | - -i 91 | - -r 92 | - --expand-star-imports 93 | - custom_components 94 | - repo: https://github.com/PyCQA/flake8 95 | rev: 6.0.0 96 | hooks: 97 | - id: flake8 98 | additional_dependencies: 99 | # - pyproject-flake8>=0.0.1a5 100 | - flake8-bugbear>=22.7.1 101 | - flake8-comprehensions>=3.10.1 102 | - flake8-2020>=1.7.0 103 | - mccabe>=0.7.0 104 | - pycodestyle>=2.9.1 105 | - pyflakes>=2.5.0 106 | - repo: https://github.com/PyCQA/isort 107 | rev: 5.12.0 108 | hooks: 109 | - id: isort 110 | - repo: https://github.com/codespell-project/codespell 111 | rev: v2.2.2 112 | hooks: 113 | - id: codespell 114 | args: [--toml, pyproject.toml] 115 | additional_dependencies: 116 | - tomli 117 | - repo: https://github.com/pre-commit/mirrors-pylint 118 | rev: v3.0.0a5 119 | hooks: 120 | - id: pylint 121 | additional_dependencies: 122 | #- voluptuous==0.13.1 123 | - homeassistant-stubs==2023.3.1 124 | #- sqlalchemy 125 | #- pyyaml 126 | - repo: https://github.com/pre-commit/mirrors-mypy 127 | rev: v0.991 128 | hooks: 129 | - id: mypy 130 | additional_dependencies: 131 | #- voluptuous==0.13.1 132 | - pydantic>=1.10.5 133 | - homeassistant-stubs==2023.3.1 134 | #- sqlalchemy 135 | #- pyyaml 136 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # yamllint disable-line rule:truthy 4 | on: 5 | #push: 6 | #pull_request: ~ 7 | workflow_dispatch: 8 | 9 | env: 10 | CACHE_VERSION: 1 11 | PYTHON_VERSION_DEFAULT: '3.10.8' 12 | PRE_COMMIT_HOME: ~/.cache/pre-commit 13 | 14 | jobs: 15 | # Separate job to pre-populate the base dependency cache 16 | # This prevent upcoming jobs to do the same individually 17 | prepare-base: 18 | name: Prepare base dependencies 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | #python-version: ['3.8.14', '3.9.15', '3.10.8', '3.11.0'] 23 | python-version: ['3.10.8', '3.11.0'] 24 | steps: 25 | - name: Check out code from GitHub 26 | uses: actions/checkout@v3 27 | - name: Set up Python ${{ matrix.python-version }} 28 | id: python 29 | uses: actions/setup-python@v4 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | - name: Restore base Python virtual environment 33 | id: cache-venv 34 | uses: actions/cache@v3 35 | with: 36 | path: venv 37 | key: >- 38 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ 39 | steps.python.outputs.python-version }}-${{ 40 | hashFiles('setup.py', 'requirements_test.txt') }} 41 | restore-keys: | 42 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}- 43 | - name: Create Python virtual environment 44 | if: steps.cache-venv.outputs.cache-hit != 'true' 45 | run: | 46 | python -m venv venv 47 | . venv/bin/activate 48 | pip install -U pip setuptools pre-commit 49 | pip install -r requirements_test.txt 50 | pip install -e . 51 | 52 | pre-commit: 53 | name: Prepare pre-commit environment 54 | runs-on: ubuntu-latest 55 | needs: prepare-base 56 | steps: 57 | - name: Check out code from GitHub 58 | uses: actions/checkout@v3 59 | - name: Set up Python ${{ env.PYTHON_VERSION_DEFAULT }} 60 | uses: actions/setup-python@v4 61 | id: python 62 | with: 63 | python-version: ${{ env.PYTHON_VERSION_DEFAULT }} 64 | - name: Restore base Python virtual environment 65 | id: cache-venv 66 | uses: actions/cache@v3 67 | with: 68 | path: venv 69 | key: >- 70 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ 71 | steps.python.outputs.python-version }}-${{ 72 | hashFiles('setup.py', 'requirements_test.txt') }} 73 | - name: Fail job if Python cache restore failed 74 | if: steps.cache-venv.outputs.cache-hit != 'true' 75 | run: | 76 | echo "Failed to restore Python virtual environment from cache" 77 | exit 1 78 | - name: Restore pre-commit environment from cache 79 | id: cache-precommit 80 | uses: actions/cache@v3 81 | with: 82 | path: ${{ env.PRE_COMMIT_HOME }} 83 | key: | 84 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} 85 | restore-keys: | 86 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit- 87 | - name: Install pre-commit dependencies 88 | if: steps.cache-precommit.outputs.cache-hit != 'true' 89 | run: | 90 | . venv/bin/activate 91 | pre-commit install-hooks 92 | 93 | pre-commit-run: 94 | name: Run all of pre-commit 95 | runs-on: ubuntu-latest 96 | needs: pre-commit 97 | steps: 98 | - name: Check out code from GitHub 99 | uses: actions/checkout@v3 100 | - name: Set up Python ${{ env.PYTHON_VERSION_DEFAULT }} 101 | uses: actions/setup-python@v4 102 | id: python 103 | with: 104 | python-version: ${{ env.PYTHON_VERSION_DEFAULT }} 105 | - name: Restore base Python virtual environment 106 | id: cache-venv 107 | uses: actions/cache@v3 108 | with: 109 | path: venv 110 | key: >- 111 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ 112 | steps.python.outputs.python-version }}-${{ 113 | hashFiles('setup.py', 'requirements_test.txt') }} 114 | - name: Fail job if Python cache restore failed 115 | if: steps.cache-venv.outputs.cache-hit != 'true' 116 | run: | 117 | echo "Failed to restore Python virtual environment from cache" 118 | exit 1 119 | - name: Restore pre-commit environment from cache 120 | id: cache-precommit 121 | uses: actions/cache@v3 122 | with: 123 | path: ${{ env.PRE_COMMIT_HOME }} 124 | key: | 125 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} 126 | - name: Fail job if cache restore failed 127 | if: steps.cache-venv.outputs.cache-hit != 'true' 128 | run: | 129 | echo "Failed to restore Python virtual environment from cache" 130 | exit 1 131 | - name: Run pre-commit 132 | run: | 133 | . venv/bin/activate 134 | pre-commit run -a 135 | -------------------------------------------------------------------------------- /custom_components/file_restore/sensor.py: -------------------------------------------------------------------------------- 1 | """ 2 | Support for sensor that get the value of a text value of a strinng of temperature value for the programmable thermostat. 3 | """ 4 | import os 5 | import logging 6 | import json 7 | 8 | from homeassistant.helpers.reload import async_setup_reload_service 9 | from homeassistant.config_entries import SOURCE_IMPORT 10 | from homeassistant.components.sensor import PLATFORM_SCHEMA 11 | from homeassistant.const import ( 12 | CONF_VALUE_TEMPLATE, 13 | CONF_NAME, 14 | CONF_UNIT_OF_MEASUREMENT 15 | ) 16 | from homeassistant.helpers.entity import Entity 17 | from homeassistant.util import slugify 18 | from datetime import datetime 19 | from .const import ( 20 | VERSION, 21 | DOMAIN, 22 | PLATFORM, 23 | ATTR_TEMPERATURES, 24 | CON_YEAR, 25 | CON_MONTH, 26 | CON_WEEK, 27 | CON_DAY, 28 | CON_HOUR, 29 | CON_MINUTE, 30 | ICON 31 | ) 32 | from .config_schema import ( 33 | SENSOR_SCHEMA, 34 | CONF_FILE_PATH, 35 | CONF_UNIT_OF_MEASUREMENT, 36 | CONF_LENGTH, 37 | CONF_DETAIL 38 | ) 39 | from .utilities import create_file_and_directory 40 | 41 | _LOGGER = logging.getLogger(__name__) 42 | 43 | __version__ = VERSION 44 | 45 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(SENSOR_SCHEMA) 46 | 47 | async def async_setup_platform(hass, config, async_add_entities, 48 | discovery_info=None): 49 | """Set up the file sensor.""" 50 | _LOGGER.debug("Setup entity coming from configuration.yaml named: %s", config.get(CONF_NAME)) 51 | await async_setup_reload_service(hass, DOMAIN, PLATFORM) 52 | async_add_entities([FileSensor(config)], True) 53 | 54 | async def async_setup_entry(hass, config_entry, async_add_devices): 55 | """Add file sensor entities from configuration flow.""" 56 | result = {} 57 | if config_entry.options != {}: 58 | result = config_entry.options 59 | else: 60 | result = config_entry.data 61 | _LOGGER.debug("setup entity-config_entry_data=%s",result) 62 | await async_setup_reload_service(hass, DOMAIN, PLATFORM) 63 | async_add_devices([FileSensor(result)], True) 64 | 65 | class FileSensor(Entity): 66 | """Implementation of a file sensor.""" 67 | 68 | def __init__(self, config): 69 | """Initialize the file sensor.""" 70 | self._name = config.get(CONF_NAME) 71 | self._attr_unique_id = f"programmable_thermostat_{slugify(self._name)}" 72 | self._file_path = config.get(CONF_FILE_PATH) 73 | self._state = None 74 | self._unit = config.get(CONF_UNIT_OF_MEASUREMENT) 75 | self._length = config.get(CONF_LENGTH) 76 | self._detail = config.get(CONF_DETAIL) 77 | 78 | """This is to define the amout of data that has to be managed""" 79 | time_data = self.get_value_according_to_length_detail() 80 | self._program = [0]*(time_data[1] * time_data[2] * time_data[3] * time_data[4] * time_data[5]) 81 | 82 | 83 | @property 84 | def name(self): 85 | """Return the name of the sensor.""" 86 | return self._name 87 | 88 | @property 89 | def unit_of_measurement(self): 90 | """Return the unit the value is expressed in.""" 91 | return self._unit 92 | 93 | @property 94 | def icon(self): 95 | """Return the icon to use in the frontend, if any.""" 96 | return ICON 97 | 98 | @property 99 | def state(self): 100 | """Return the state of the sensor.""" 101 | return self._state 102 | 103 | @property 104 | def device_state_attributes(self): 105 | """Return the state attributes of the sensor.""" 106 | return { 107 | ATTR_TEMPERATURES: self._program 108 | } 109 | 110 | def update(self): 111 | """Get the latest entry from a file and updates the state.""" 112 | try: 113 | with open(self._file_path, 'r', encoding='utf-8') as file_data: 114 | for line in file_data: 115 | data = line 116 | data = data.strip() 117 | except (IndexError, FileNotFoundError, IsADirectoryError, 118 | UnboundLocalError): 119 | create_file_and_directory(self._file_path, self._name) 120 | 121 | try: 122 | data_array = data.split(',') 123 | for index in range(len(data_array)): 124 | self._program[index] = float(data_array[index]) 125 | except: 126 | _LOGGER.warning("sensor.%s - File doesn't include valid data. Set to all 0.", self._name) 127 | 128 | self._state = self._program[self.get_value_according_to_length_detail()[0]] 129 | 130 | def get_value_according_to_length_detail(self): 131 | month = datetime.now().month 132 | hour = datetime.now().hour 133 | minute = datetime.now().minute 134 | if self._length == CON_YEAR and self._detail == CON_DAY: 135 | day = int(datetime.strftime(datetime.now(),'%j')) 136 | elif self._length == CON_WEEK: 137 | day = datetime.now().weekday() 138 | else: 139 | day = datetime.now().day 140 | week = datetime.now().isocalendar()[1] 141 | #Return array data are [position, month, week, day, hour, minute] 142 | if self._length == CON_HOUR and self._detail == CON_MINUTE: 143 | return [minute, 1, 1, 1, 1, 60] 144 | elif self._length == CON_DAY and self._detail == CON_MINUTE: 145 | return [60 * hour + minute, 1, 1, 1, 24, 60] 146 | elif self._length == CON_DAY and self._detail == CON_HOUR: 147 | return [hour, 1, 1, 1, 24, 1] 148 | elif self._length == CON_WEEK and self._detail == CON_HOUR: 149 | return [24 * day + hour, 1, 1, 7, 24, 1] 150 | elif self._length == CON_WEEK and self._detail == CON_DAY: 151 | return [day, 1, 1, 7, 1, 1] 152 | elif self._length == CON_MONTH and self._detail == CON_HOUR: 153 | return [24 * (day - 1) + hour, 1, 1, 31, 24, 1] 154 | # the -1 is required becasue in this case day range is between 1 and 31 (and not from 0) 155 | elif self._length == CON_MONTH and self._detail == CON_DAY: 156 | return [day, 1, 1, 31, 1, 1] 157 | elif self._length == CON_YEAR and self._detail == CON_DAY: 158 | return [day, 1, 1, 366, 1, 1] 159 | elif self._length == CON_YEAR and self._detail == CON_WEEK: 160 | return [week, 1, 53, 1, 1, 1] 161 | elif self._length == CON_YEAR and self._detail == CON_MONTH: 162 | return [month, 12, 1, 1, 1, 1] 163 | else: 164 | _LOGGER.error("Parameters has a not acceptable value, check possibile values of length and detail on documentation") 165 | return [] 166 | -------------------------------------------------------------------------------- /custom_components/file_restore/config_flow.py: -------------------------------------------------------------------------------- 1 | """ Configuration flow for the programmable_thermostat integration to allow user 2 | to define all file resotre entities from Lovelace UI.""" 3 | import logging 4 | import os 5 | from homeassistant.core import callback 6 | import voluptuous as vol 7 | import homeassistant.helpers.config_validation as cv 8 | from homeassistant import config_entries 9 | import uuid 10 | 11 | from homeassistant.const import EVENT_HOMEASSISTANT_START 12 | from .utilities import ( 13 | create_file_and_directory, 14 | null_data_cleaner 15 | ) 16 | from .const import ( 17 | DOMAIN, 18 | CONFIGFLOW_VERSION 19 | ) 20 | from homeassistant.const import CONF_NAME 21 | from .config_schema import ( 22 | get_config_flow_schema, 23 | SENSOR_SCHEMA, 24 | CONF_FILE_PATH, 25 | CONF_UNIT_OF_MEASUREMENT, 26 | CONF_LENGTH, 27 | CONF_DETAIL 28 | ) 29 | 30 | 31 | _LOGGER = logging.getLogger(__name__) 32 | 33 | ##################################################### 34 | #################### CONFIG FLOW #################### 35 | ##################################################### 36 | @config_entries.HANDLERS.register(DOMAIN) 37 | class FileRestoreConfigFlow(config_entries.ConfigFlow): 38 | """File Restore config flow.""" 39 | 40 | VERSION = CONFIGFLOW_VERSION 41 | CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL 42 | 43 | def __init__(self): 44 | """Initialize.""" 45 | self._errors = {} 46 | self._data = {} 47 | self._unique_id = str(uuid.uuid4()) 48 | 49 | """ INITIATE CONFIG FLOW """ 50 | async def async_step_user(self, user_input={}): 51 | """User initiated config flow.""" 52 | self._errors = {} 53 | if user_input is not None: 54 | if are_first_step_data_valid(self, user_input): 55 | self._data.update(user_input) 56 | _LOGGER.info("First input data are valid. Proceed with final step. %s", self._data) 57 | return await self.async_step_final() 58 | _LOGGER.warning("Wrong data have been input in the first form") 59 | return await self._show_config_form_first(user_input) 60 | return await self._show_config_form_first(user_input) 61 | 62 | """ SHOW FIRST FORM """ 63 | async def _show_config_form_first(self, user_input): 64 | """ Show form for config flow """ 65 | _LOGGER.info("Show first form") 66 | return self.async_show_form( 67 | step_id="user", 68 | data_schema=vol.Schema(get_config_flow_schema(user_input, 1, "")), 69 | errors=self._errors 70 | ) 71 | 72 | """ LAST CONFIG FLOW STEP """ 73 | async def async_step_final(self, user_input={}): 74 | """User initiated config flow.""" 75 | self._errors = {} 76 | if user_input is not None and user_input != {}: 77 | self._data.update(user_input) 78 | final_data = {} 79 | for key in self._data.keys(): 80 | if self._data[key] != "" and self._data[key] != []: 81 | final_data.update({key: self._data[key]}) 82 | _LOGGER.info("Data are valid. Proceed with entity creation. - %s", final_data) 83 | await self.async_set_unique_id(self._unique_id) 84 | self._abort_if_unique_id_configured() 85 | return self.async_create_entry(title=final_data["name"], data=final_data) 86 | return await self._show_config_form_final(user_input) 87 | 88 | """ SHOW LAST FORM """ 89 | async def _show_config_form_final(self, user_input): 90 | """ Show form for config flow """ 91 | _LOGGER.info("Show final form") 92 | return self.async_show_form( 93 | step_id="final", 94 | data_schema=vol.Schema(get_config_flow_schema(user_input, 2, self._data[CONF_LENGTH])), 95 | errors=self._errors 96 | ) 97 | 98 | """ SHOW CONFIGURATION.YAML ENTITIES """ 99 | async def async_step_import(self, user_input): 100 | """Import a config entry. 101 | Special type of import, we're not actually going to store any data. 102 | Instead, we're going to rely on the values that are in config file.""" 103 | 104 | if self._async_current_entries(): 105 | return self.async_abort(reason="single_instance_allowed") 106 | 107 | return self.async_create_entry(title="configuration.yaml", data={}) 108 | 109 | @staticmethod 110 | @callback 111 | def async_get_options_flow(config_entry): 112 | if config_entry.unique_id is not None: 113 | return OptionsFlowHandler(config_entry) 114 | else: 115 | return EmptyOptions(config_entry) 116 | 117 | ##################################################### 118 | #################### OPTION FLOW #################### 119 | ##################################################### 120 | class OptionsFlowHandler(config_entries.OptionsFlow): 121 | """File Restore config flow.""" 122 | 123 | def __init__(self, config_entry): 124 | """Initialize.""" 125 | self._errors = {} 126 | self._data = {} 127 | self.config_entry = config_entry 128 | if self.config_entry.options == {}: 129 | self._data.update(self.config_entry.data) 130 | else: 131 | self._data.update(self.config_entry.options) 132 | _LOGGER.debug("_data to start options flow: %s", self._data) 133 | 134 | """ INITIATE CONFIG FLOW """ 135 | async def async_step_init(self, user_input={}): 136 | """User initiated config flow.""" 137 | self._errors = {} 138 | if user_input is not None: 139 | if are_first_step_data_valid(self, user_input): 140 | self._data = null_data_cleaner(self._data, user_input) 141 | _LOGGER.info("First input data are valid. Proceed with final step. %s", self._data) 142 | return await self.async_step_final() 143 | _LOGGER.warning("Wrong data have been input in the first form") 144 | return await self._show_config_form_first(user_input) 145 | return await self._show_config_form_first(user_input) 146 | 147 | """ SHOW FIRST FORM """ 148 | async def _show_config_form_first(self, user_input): 149 | """ Show form for config flow """ 150 | _LOGGER.info("Show first form %s", user_input) 151 | if user_input is None or user_input == {}: 152 | user_input = self._data 153 | #3 is necessary for options. Check config_schema.py for explanations. 154 | return self.async_show_form( 155 | step_id="init", 156 | data_schema=vol.Schema(get_config_flow_schema(user_input, 3, "")), 157 | errors=self._errors 158 | ) 159 | 160 | """ LAST CONFIG FLOW STEP """ 161 | async def async_step_final(self, user_input={}): 162 | """User initiated config flow.""" 163 | self._errors = {} 164 | if user_input is not None and user_input != {}: 165 | self._data = null_data_cleaner(self._data, user_input) 166 | final_data = {} 167 | for key in self._data.keys(): 168 | if self._data[key] != "" and self._data[key] != []: 169 | final_data.update({key: self._data[key]}) 170 | _LOGGER.info("Data are valid. Proceed with entity creation. - %s", final_data) 171 | return self.async_create_entry(title="", data=final_data) 172 | return await self._show_config_form_final(user_input) 173 | 174 | """ SHOW LAST FORM """ 175 | async def _show_config_form_final(self, user_input): 176 | """ Show form for config flow """ 177 | _LOGGER.info("Show final form") 178 | if user_input is None or user_input == {}: 179 | user_input = self._data 180 | return self.async_show_form( 181 | step_id="final", 182 | data_schema=vol.Schema(get_config_flow_schema(user_input, 2, self._data[CONF_LENGTH])), 183 | errors=self._errors 184 | ) 185 | 186 | """ SHOW CONFIGURATION.YAML ENTITIES """ 187 | async def async_step_import(self, user_input): 188 | """Import a config entry. 189 | Special type of import, we're not actually going to store any data. 190 | Instead, we're going to rely on the values that are in config file.""" 191 | 192 | if self._async_current_entries(): 193 | return self.async_abort(reason="single_instance_allowed") 194 | 195 | return self.async_create_entry(title="configuration.yaml", data={}) 196 | 197 | ##################################################### 198 | #################### EMPTY FLOW #################### 199 | ##################################################### 200 | class EmptyOptions(config_entries.OptionsFlow): 201 | """A class for default options. Not sure why this is required.""" 202 | 203 | def __init__(self, config_entry): 204 | """Just set the config_entry parameter.""" 205 | self.config_entry = config_entry 206 | 207 | ##################################################### 208 | ############## DATA VALIDATION FUCTION ############## 209 | ##################################################### 210 | def are_first_step_data_valid(self, user_input) -> bool: 211 | if user_input[CONF_FILE_PATH] == "": 212 | self._errors["base"]="file_path_empty" 213 | return False 214 | else: 215 | user_input[CONF_FILE_PATH] = user_input[CONF_FILE_PATH].replace("\\", "/") 216 | if user_input[CONF_FILE_PATH][0]=="/": 217 | user_input[CONF_FILE_PATH] = user_input[CONF_FILE_PATH][1::] 218 | user_input[CONF_FILE_PATH] = user_input[CONF_FILE_PATH].replace("local/", "www/") 219 | try: 220 | with open(user_input[CONF_FILE_PATH], 'r', encoding='utf-8') as file_data: 221 | for line in file_data: 222 | data = line 223 | try: 224 | data = data.strip() 225 | except: 226 | _LOGGER.warning("File is empty, please add some data in it.") 227 | return True 228 | except (IndexError, FileNotFoundError, IsADirectoryError, 229 | UnboundLocalError): 230 | if CONF_NAME in user_input: 231 | create_file_and_directory(user_input[CONF_FILE_PATH], user_input[CONF_NAME]) 232 | else: 233 | create_file_and_directory(user_input[CONF_FILE_PATH], self._data[CONF_NAME]) 234 | return True 235 | --------------------------------------------------------------------------------