├── hacs.json ├── .github ├── FUNDING.yml └── workflows │ ├── hassfest.yaml │ ├── hacs.yaml │ ├── automerge.yaml │ └── release.yaml ├── custom_components └── saver │ ├── strings.json │ ├── translations │ ├── en.json │ └── pl.json │ ├── manifest.json │ ├── config_flow.py │ ├── const.py │ ├── services.yaml │ └── __init__.py ├── LICENSE └── README.md /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Saver", 3 | "render_readme": true, 4 | "zip_release": true, 5 | "filename": "saver.zip" 6 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: piotrmachowski 2 | custom: ["buycoffee.to/piotrmachowski", "paypal.me/PiMachowski", "revolut.me/314ma"] 3 | -------------------------------------------------------------------------------- /.github/workflows/hassfest.yaml: -------------------------------------------------------------------------------- 1 | name: Validate with hassfest 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | validate: 9 | runs-on: "ubuntu-latest" 10 | steps: 11 | - uses: "actions/checkout@v2" 12 | - uses: home-assistant/actions/hassfest@master 13 | -------------------------------------------------------------------------------- /custom_components/saver/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "The Saver is already configured." 5 | }, 6 | "step": { 7 | "user": { 8 | "description": "Do you want to configure the Saver?" 9 | } 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /custom_components/saver/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "The Saver is already configured." 5 | }, 6 | "step": { 7 | "user": { 8 | "description": "Do you want to configure the Saver?" 9 | } 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /custom_components/saver/translations/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Saver jest ju\u017c skonfigurowany." 5 | }, 6 | "step": { 7 | "user": { 8 | "description": "Czy chcesz skonfigurowa\u0107 Saver?" 9 | } 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /custom_components/saver/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "saver", 3 | "name": "Saver", 4 | "codeowners": ["@PiotrMachowski"], 5 | "config_flow": true, 6 | "dependencies": [], 7 | "documentation": "https://github.com/PiotrMachowski/Home-Assistant-custom-components-Saver", 8 | "iot_class": "calculated", 9 | "issue_tracker": "https://github.com/PiotrMachowski/Home-Assistant-custom-components-Saver/issues", 10 | "requirements": [], 11 | "version": "v0.0.0-dev" 12 | } -------------------------------------------------------------------------------- /.github/workflows/hacs.yaml: -------------------------------------------------------------------------------- 1 | name: Validate HACS 2 | on: 3 | push: 4 | pull_request: 5 | jobs: 6 | ci: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | name: Download repo 11 | with: 12 | fetch-depth: 0 13 | 14 | - uses: actions/setup-python@v2 15 | name: Setup Python 16 | with: 17 | python-version: '3.13.x' 18 | 19 | - name: HACS Action 20 | uses: hacs/action@main 21 | with: 22 | category: integration -------------------------------------------------------------------------------- /custom_components/saver/config_flow.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from homeassistant import config_entries 4 | from homeassistant.data_entry_flow import FlowResult 5 | 6 | from .const import DOMAIN 7 | 8 | 9 | class SaverFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 10 | 11 | VERSION = 1 12 | CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH 13 | 14 | async def async_step_user(self, user_input: dict[str, Any] | None = None) -> FlowResult: 15 | await self.async_set_unique_id(DOMAIN) 16 | self._async_abort_entries_match() 17 | if user_input is not None: 18 | return self.async_create_entry(title="Saver", data=user_input) 19 | return self.async_show_form(step_id="user", last_step=True) 20 | 21 | async_step_import = async_step_user 22 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yaml: -------------------------------------------------------------------------------- 1 | name: 'Automatically merge master -> dev' 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | name: Automatically merge master to dev 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | name: Git checkout 15 | with: 16 | fetch-depth: 0 17 | - name: Merge master -> dev 18 | run: | 19 | git config user.name "GitHub Actions" 20 | git config user.email "PiotrMachowski@users.noreply.github.com" 21 | if (git checkout dev) 22 | then 23 | git merge --ff-only master || git merge --no-commit master 24 | git commit -m "Automatically merge master -> dev" || echo "No commit needed" 25 | git push origin dev 26 | else 27 | echo "No dev branch" 28 | fi -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Piotr Machowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | env: 8 | COMPONENT_NAME: saver 9 | 10 | jobs: 11 | release: 12 | name: Prepare release 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | id-token: write 17 | steps: 18 | - name: Download repo 19 | uses: actions/checkout@v4.2.2 20 | 21 | - name: Adjust version number 22 | shell: bash 23 | run: | 24 | version="${{ github.event.release.tag_name }}" 25 | yq e -P -o=json \ 26 | -i ".version = \"${version}\"" \ 27 | "${{ github.workspace }}/custom_components/${{ env.COMPONENT_NAME }}/manifest.json" 28 | 29 | - name: Zip ${{ env.COMPONENT_NAME }} dir 30 | run: | 31 | cd "${{ github.workspace }}/custom_components/${{ env.COMPONENT_NAME }}" 32 | zip ${{ env.COMPONENT_NAME }}.zip -r ./ 33 | 34 | 35 | - name: Upload zip to release 36 | uses: softprops/action-gh-release@v2.1.0 37 | with: 38 | files: ${{ github.workspace }}/custom_components/${{ env.COMPONENT_NAME }}/${{ env.COMPONENT_NAME }}.zip 39 | -------------------------------------------------------------------------------- /custom_components/saver/const.py: -------------------------------------------------------------------------------- 1 | import homeassistant.helpers.config_validation as cv 2 | from homeassistant.const import CONF_ENTITY_ID, CONF_NAME 3 | from homeassistant.components.light import ATTR_TRANSITION, VALID_TRANSITION 4 | 5 | import voluptuous as vol 6 | 7 | DOMAIN = 'saver' 8 | NAME = 'Saver' 9 | 10 | SAVER_SCHEMA = vol.Schema( 11 | { 12 | DOMAIN: vol.Schema({}) 13 | }, 14 | extra=vol.ALLOW_EXTRA 15 | ) 16 | 17 | CONF_DELETE_AFTER_RUN = 'delete_after_run' 18 | CONF_RESTORE_SCRIPT = 'restore_script' 19 | CONF_SCRIPT = 'script' 20 | CONF_VALUE = 'value' 21 | CONF_REGEX = 'regex' 22 | 23 | SERVICE_CLEAR = 'clear' 24 | SERVICE_CLEAR_SCHEMA = vol.Schema({ 25 | }) 26 | 27 | SERVICE_DELETE = 'delete' 28 | SERVICE_DELETE_SCHEMA = vol.Schema({ 29 | vol.Required(CONF_ENTITY_ID): cv.entity_ids 30 | }) 31 | 32 | SERVICE_DELETE_VARIABLE = 'delete_variable' 33 | SERVICE_DELETE_VARIABLE_SCHEMA = vol.Schema({ 34 | vol.Required(CONF_NAME): cv.string 35 | }) 36 | 37 | SERVICE_DELETE_VARIABLE_REGEX = 'delete_variable_regex' 38 | SERVICE_DELETE_VARIABLE_REGEX_SCHEMA = vol.Schema({ 39 | vol.Required(CONF_REGEX): cv.string 40 | }) 41 | 42 | SERVICE_EXECUTE = 'execute' 43 | SERVICE_EXECUTE_SCHEMA = vol.Schema({ 44 | vol.Required(CONF_SCRIPT): cv.SCRIPT_SCHEMA 45 | }) 46 | 47 | SERVICE_RESTORE_STATE = 'restore_state' 48 | SERVICE_RESTORE_STATE_SCHEMA = vol.Schema({ 49 | vol.Required(CONF_ENTITY_ID): cv.entity_ids, 50 | vol.Optional(CONF_DELETE_AFTER_RUN, default=True): cv.boolean, 51 | vol.Optional(ATTR_TRANSITION, default=None): vol.Any(VALID_TRANSITION, None), 52 | }) 53 | 54 | SERVICE_SAVE_STATE = 'save_state' 55 | SERVICE_SAVE_STATE_SCHEMA = vol.Schema({ 56 | vol.Required(CONF_ENTITY_ID): cv.entity_ids 57 | }) 58 | 59 | SERVICE_SET_VARIABLE = 'set_variable' 60 | SERVICE_SET_VARIABLE_SCHEMA = vol.Schema({ 61 | vol.Required(CONF_NAME): cv.string, 62 | vol.Required(CONF_VALUE): cv.string 63 | }) 64 | -------------------------------------------------------------------------------- /custom_components/saver/services.yaml: -------------------------------------------------------------------------------- 1 | clear: 2 | name: Clear storage 3 | description: > 4 | Deletes all saved data. 5 | 6 | delete: 7 | name: Delete entity 8 | description: > 9 | Deletes a saved state for an entity. 10 | fields: 11 | entity_id: 12 | name: Entity 13 | description: ID of entity to delete from saver. 14 | example: sun.sun 15 | required: true 16 | selector: 17 | entity: 18 | multiple: true 19 | 20 | delete_variable: 21 | name: Delete variable 22 | description: > 23 | Deletes a saved variable. 24 | fields: 25 | name: 26 | name: Variable 27 | description: Name of the variable to delete from saver. 28 | example: counter 29 | required: true 30 | selector: 31 | text: 32 | 33 | delete_variable_regex: 34 | name: Delete variables by regex 35 | description: > 36 | Deletes saved variables by regex. 37 | fields: 38 | regex: 39 | name: Regex 40 | description: Regex that should be used to delete variables from saver. 41 | example: counter_.* 42 | required: true 43 | selector: 44 | text: 45 | 46 | set_variable: 47 | name: Set variable 48 | description: > 49 | Sets the value to the variable. 50 | fields: 51 | name: 52 | name: Variable 53 | description: Name of the variable to be set. 54 | example: counter 55 | required: true 56 | selector: 57 | text: 58 | value: 59 | name: Value 60 | description: The new value 61 | example: 3 62 | required: true 63 | selector: 64 | text: 65 | 66 | save_state: 67 | name: Save state 68 | description: > 69 | Saves the state and parameters of the entity. 70 | fields: 71 | entity_id: 72 | name: Entity 73 | description: ID of the entity that should be saved. 74 | example: sun.sun 75 | required: true 76 | selector: 77 | entity: 78 | multiple: true 79 | 80 | restore_state: 81 | name: Restore states 82 | description: > 83 | Restores saved states of entities. 84 | fields: 85 | entity_id: 86 | name: Entity 87 | description: ID of the entity that should be saved. 88 | example: sun.sun 89 | required: true 90 | selector: 91 | entity: 92 | multiple: true 93 | delete_after_run: 94 | name: Delete after run 95 | description: "Deletes the saved state after an execution of the script. Default: true [Optional]" 96 | example: false 97 | selector: 98 | boolean: 99 | transition: 100 | name: Transition 101 | description: "Transition that should be used during state restoring (only for light entities)" 102 | required: false 103 | selector: 104 | number: 105 | min: 0 106 | max: 300 107 | unit_of_measurement: seconds 108 | -------------------------------------------------------------------------------- /custom_components/saver/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import regex 4 | from typing import Any, Callable 5 | 6 | from homeassistant.config_entries import ConfigEntry 7 | from homeassistant.core import HomeAssistant, ServiceCall 8 | from homeassistant.helpers.restore_state import RestoreEntity 9 | from homeassistant.helpers.entity_component import EntityComponent 10 | from homeassistant.helpers.template import _get_state_if_valid, Template, TemplateEnvironment 11 | 12 | from .const import * 13 | 14 | _LOGGER = logging.getLogger(__name__) 15 | CONFIG_SCHEMA = SAVER_SCHEMA 16 | 17 | 18 | def setup(hass, config) -> bool: 19 | if DOMAIN not in config: 20 | return True 21 | return setup_entry(hass, config) 22 | 23 | 24 | async def async_setup_entry(hass, config_entry): 25 | result = await hass.async_add_executor_job(setup_entry, hass, config_entry) 26 | return result 27 | 28 | 29 | class SaverVariableTemplate: 30 | def __init__(self, hass: HomeAssistant, entity_id: str) -> None: 31 | self._hass = hass 32 | self._entity_id = entity_id 33 | 34 | def __call__(self, variable: str) -> Any: 35 | saver_state = _get_state_if_valid(self._hass, self._entity_id) 36 | if saver_state is None: 37 | return None 38 | variables = saver_state.attributes["variables"] 39 | if variable in variables: 40 | return variables[variable] 41 | return None 42 | 43 | def __repr__(self) -> str: 44 | return "