├── .github
├── FUNDING.yml
└── workflows
│ ├── automerge.yaml
│ ├── hacs.yaml
│ ├── hassfest.yaml
│ └── release.yaml
├── LICENSE
├── README.md
├── custom_components
└── saver
│ ├── __init__.py
│ ├── config_flow.py
│ ├── const.py
│ ├── manifest.json
│ ├── services.yaml
│ ├── strings.json
│ └── translations
│ ├── en.json
│ └── pl.json
└── hacs.json
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | ko_fi: piotrmachowski
2 | custom: ["buycoffee.to/piotrmachowski", "paypal.me/PiMachowski", "revolut.me/314ma"]
3 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/.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.8.x'
18 |
19 | - uses: actions/cache@v2
20 | name: Cache
21 | with:
22 | path: |
23 | ~/.cache/pip
24 | key: custom-component-ci
25 |
26 | - name: HACS Action
27 | uses: hacs/action@main
28 | with:
29 | CATEGORY: integration
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | release:
9 | name: Prepare release
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Download repo
13 | uses: actions/checkout@v1
14 |
15 | - name: Zip saver dir
16 | run: |
17 | cd /home/runner/work/Home-Assistant-custom-components-Saver/Home-Assistant-custom-components-Saver/custom_components/saver
18 | zip saver.zip -r ./
19 |
20 | - name: Upload zip to release
21 | uses: svenstaro/upload-release-action@v1-release
22 | with:
23 | repo_token: ${{ secrets.GITHUB_TOKEN }}
24 | file: /home/runner/work/Home-Assistant-custom-components-Saver/Home-Assistant-custom-components-Saver/custom_components/saver/saver.zip
25 | asset_name: saver.zip
26 | tag: ${{ github.ref }}
27 | overwrite: true
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [![HACS Default][hacs_shield]][hacs]
2 | [![GitHub Latest Release][releases_shield]][latest_release]
3 | [![GitHub All Releases][downloads_total_shield]][releases]
4 | [![Community Forum][community_forum_shield]][community_forum]
5 | [![Ko-Fi][ko_fi_shield]][ko_fi]
6 | [![buycoffee.to][buycoffee_to_shield]][buycoffee_to]
7 | [![PayPal.Me][paypal_me_shield]][paypal_me]
8 | [![Revolut.Me][revolut_me_shield]][revolut_me]
9 |
10 |
11 |
12 | [hacs_shield]: https://img.shields.io/static/v1.svg?label=HACS&message=Default&style=popout&color=green&labelColor=41bdf5&logo=HomeAssistantCommunityStore&logoColor=white
13 | [hacs]: https://hacs.xyz/docs/default_repositories
14 |
15 | [latest_release]: https://github.com/PiotrMachowski/Home-Assistant-custom-components-Saver/releases/latest
16 | [releases_shield]: https://img.shields.io/github/release/PiotrMachowski/Home-Assistant-custom-components-Saver.svg?style=popout
17 |
18 | [releases]: https://github.com/PiotrMachowski/Home-Assistant-custom-components-Saver/releases
19 | [downloads_total_shield]: https://img.shields.io/github/downloads/PiotrMachowski/Home-Assistant-custom-components-Saver/total
20 |
21 | [community_forum_shield]: https://img.shields.io/static/v1.svg?label=%20&message=Forum&style=popout&color=41bdf5&logo=HomeAssistant&logoColor=white
22 | [community_forum]: https://community.home-assistant.io/t/custom-component-saver/204249
23 |
24 |
25 | # Saver
26 |
27 | This custom component allows you to save current state of any entity and use its data later to restore it.
28 |
29 | Additionally, you can create simple variables and use their values in scripts.
30 |
31 | ## Installation
32 |
33 | ### Using [HACS](https://hacs.xyz/) (recommended)
34 |
35 | [](https://my.home-assistant.io/redirect/hacs_repository/?owner=PiotrMachowski&repository=Home-Assistant-custom-components-Saver&category=Integration)
36 |
37 | This integration can be installed using HACS.
38 | To do it search for `Saver` in *Integrations* section.
39 |
40 | ### Manual
41 |
42 | To install this integration manually you have to download [*saver.zip*](https://github.com/PiotrMachowski/Home-Assistant-custom-components-Saver/releases/latest/download/saver.zip) and extract its contents to `config/custom_components/saver` directory:
43 | ```bash
44 | mkdir -p custom_components/saver
45 | cd custom_components/saver
46 | wget https://github.com/PiotrMachowski/Home-Assistant-custom-components-Saver/releases/latest/download/saver.zip
47 | unzip saver.zip
48 | rm saver.zip
49 | ```
50 |
51 | ## Configuration
52 |
53 | ### Using UI
54 |
55 | [](https://my.home-assistant.io/redirect/config_flow_start/?domain=saver)
56 |
57 | From the Home Assistant front page go to **Configuration** and then select **Integrations** from the list.
58 |
59 | Use the "plus" button in the bottom right to add a new integration called **Saver**.
60 |
61 | The success dialog will appear or an error will be displayed in the popup.
62 |
63 | ### YAML
64 |
65 | Add following code in configuration.yaml:
66 | ```yaml
67 | saver:
68 | ```
69 |
70 | ## Available services
71 |
72 | ### Save state
73 | Saves the state and parameters of the entity.
74 | ```yaml
75 | service: saver.save_state
76 | data:
77 | entity_id: cover.living_room
78 | ```
79 |
80 | ### Delete saved state
81 | Deletes a saved state for an entity.
82 | ```yaml
83 | service: saver.delete
84 | data:
85 | entity_id: cover.living_room
86 | ```
87 |
88 | ### Set variable
89 | Sets the value to the variable.
90 | ```yaml
91 | service: saver.set_variable
92 | data:
93 | name: counter
94 | value: 3
95 | ```
96 |
97 | ### Delete variable
98 | Deletes a saved variable.
99 | ```yaml
100 | service: saver.delete_variable
101 | data:
102 | name: counter
103 | ```
104 |
105 | ### Clear
106 | Deletes all saved data.
107 | ```yaml
108 | service: saver.clear
109 | ```
110 |
111 | ### Deprecated services
112 |
113 |
114 | Show
115 |
116 |
117 | #### Restore state
118 | Executes the script using saved state of the entity.
119 |
120 | To use state of an entity you have to use a following value in a template: `state`.
121 |
122 | To use state attribute (in this case `current_position`) of an entity you have to use a following value in a template: `attr_current_position`.
123 |
124 | ```yaml
125 | service: saver.restore_state
126 | data:
127 | entity_id: cover.living_room
128 | restore_script:
129 | - service: cover.set_cover_position
130 | data_template:
131 | entity_id: cover.living_room
132 | position: "{{ '{{ attr_current_position | int }}' }}"
133 | - service: input_text.set_value
134 | data_template:
135 | entity_id: input_text.cover_description
136 | value: "Cover is now {{ '{{ state }}' }}"
137 | ```
138 |
139 | #### Execute script
140 | Executes a script using all saved entities and variables.
141 |
142 | To use state of an entity (in this case `cover.living_room`) you have to use a following value in a template: `cover_living_room_state`.
143 |
144 | To use state attribute (in this case `current_position`) of an entity you have to use a following value in a template: `cover_living_room_attr_current_position`.
145 |
146 | ```yaml
147 | service: saver.execute
148 | data:
149 | script:
150 | - service: cover.set_cover_position
151 | data_template:
152 | entity_id: cover.living_room
153 | position: "{{ '{{ cover_living_room_attr_current_position | int }}' }}"
154 | - service: input_text.set_value
155 | data_template:
156 | entity_id: input_text.cover_description
157 | value: "Cover is now {{ '{{ cover_living_room_state }}' }}"
158 | - service: input_text.set_value
159 | data_template:
160 | entity_id: input_text.counter_description
161 | value: "Counter has value {{ '{{ counter }}' }}"
162 | ```
163 |
164 |
165 |
166 |
167 |
168 | ## Using saved values in templates
169 | It is possible to use saved data in templates using `saver_entity` and `saver_variable` functions:
170 | ```yaml
171 | {{ saver_entity("sun.sun") }} # returns saved state of "sun.sun" entity
172 | {{ saver_entity("sun.sun", "azimuth") }} # returns "azimuth" attribute of saved "sun.sun" entity
173 | {{ saver_variable("counter") }} # returns saved variable "counter"
174 | ```
175 |
176 | ## Events
177 |
178 | After the completion of the services mentioned before, the following events are fired:
179 |
180 | | **Service Function** | **Event ID** | **Provided Arguments** |
181 | |----------------------|------------------------------|------------------------|
182 | | **execute** | event_saver_executed | script |
183 | | **save_state** | event_saver_saved_entity | entity_id |
184 | | **restore_state** | event_saver_restored | entity_id |
185 | | **delete** | event_saver_deleted_entity | entity_id |
186 | | **clear** | event_saver_cleared | |
187 | | **set_variable** | event_saver_saved_variable | variable, value |
188 | | **delete_variable** | event_saver_deleted_variable | variable |
189 |
190 | The events can be used to trigger further automations that depend on the completion of the services. The documentation is provided [here](https://www.home-assistant.io/docs/automation/trigger/#event-trigger).
191 |
192 |
193 |
194 |
195 | ## Support
196 |
197 | If you want to support my work with a donation you can use one of the following platforms:
198 |
199 |
200 |
201 | Platform |
202 | Payment methods |
203 | Link |
204 | Comment |
205 |
206 |
207 | Ko-fi |
208 |
209 | PayPal
210 | Credit card
211 | |
212 |
213 |
214 | |
215 |
216 | No fees
217 | Single or monthly payment
218 | |
219 |
220 |
221 | buycoffee.to |
222 |
223 | BLIK
224 | Bank transfer
225 | |
226 |
227 |
228 | |
229 | |
230 |
231 |
232 | PayPal |
233 |
234 | PayPal
235 | |
236 |
237 |
238 | |
239 |
240 | No fees
241 | |
242 |
243 |
244 | Revolut |
245 |
246 | Revolut
247 | Credit Card
248 | |
249 |
250 |
251 | |
252 |
253 | No fees
254 | |
255 |
256 |
257 |
258 | ### Powered by
259 | [](https://jb.gg/OpenSourceSupport)
260 |
261 |
262 | [ko_fi_shield]: https://img.shields.io/static/v1.svg?label=%20&message=Ko-Fi&color=F16061&logo=ko-fi&logoColor=white
263 |
264 | [ko_fi]: https://ko-fi.com/piotrmachowski
265 |
266 | [buycoffee_to_shield]: https://shields.io/badge/buycoffee.to-white?style=flat&labelColor=white&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABhmlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw1AUhU9TpaIVh1YQcchQnayIijhKFYtgobQVWnUweemP0KQhSXFxFFwLDv4sVh1cnHV1cBUEwR8QVxcnRRcp8b6k0CLGC4/3cd49h/fuA4R6malmxzigapaRisfEbG5FDLzChxB6MIZ+iZl6Ir2QgWd93VM31V2UZ3n3/Vm9St5kgE8knmW6YRGvE09vWjrnfeIwK0kK8TnxqEEXJH7kuuzyG+eiwwLPDBuZ1BxxmFgstrHcxqxkqMRTxBFF1ShfyLqscN7irJarrHlP/sJgXltOc53WEOJYRAJJiJBRxQbKsBClXSPFRIrOYx7+QcefJJdMrg0wcsyjAhWS4wf/g9+zNQuTE25SMAZ0vtj2xzAQ2AUaNdv+PrbtxgngfwautJa/UgdmPkmvtbTIEdC3DVxctzR5D7jcAQaedMmQHMlPSygUgPcz+qYcELoFulfduTXPcfoAZGhWSzfAwSEwUqTsNY93d7XP7d+e5vx+AIahcq//o+yoAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH5wETCy4vFNqLzwAAAVpJREFUOMvd0rFLVXEYxvHPOedKJnKJhrDLuUFREULE7YDCMYj+AydpsCWiaKu29hZxiP4Al4aWwC1EdFI4Q3hqEmkIBI8ZChWXKNLLvS0/Qcza84V3enm/7/s878t/HxGkeTaIGziP+EB918nawu7Dq1d0e1+2J2bepnk2jFEUVVF+qKV51o9neBCaugfge70keoxxUbSWjrQ+4SUyzKZ5NlnDZdzGG7w4DIh+dtZEFntDA98l8S0MYwctNGrYz9WqKJePFLq80g5Sr+EHlnATp+NA+4qLaZ7FfzMrzbMBjGEdq8GrJMZnvAvFC/8wfAwjWMQ8XmMzaW9sdevNRgd3MFhvNpbaG1u/Dk2/hOc4gadVUa7Um425qii/7Z+xH9O4jwW8Cqv24Tru4hyeVEU588cfBMgpPMI9nMFe0BkFzVOYrYqycyQgQJLwTC2cDZCPeF8V5Y7jGb8BUpRicy7OU5MAAAAASUVORK5CYII=
267 |
268 | [buycoffee_to]: https://buycoffee.to/piotrmachowski
269 |
270 | [buy_me_a_coffee_shield]: https://img.shields.io/static/v1.svg?label=%20&message=Buy%20me%20a%20coffee&color=6f4e37&logo=buy%20me%20a%20coffee&logoColor=white
271 |
272 | [buy_me_a_coffee]: https://www.buymeacoffee.com/PiotrMachowski
273 |
274 | [paypal_me_shield]: https://img.shields.io/static/v1.svg?label=%20&message=PayPal.Me&logo=paypal
275 |
276 | [paypal_me]: https://paypal.me/PiMachowski
277 |
278 | [revolut_me_shield]: https://img.shields.io/static/v1.svg?label=%20&message=Revolut&logo=revolut
279 |
280 | [revolut_me]: https://revolut.me/314ma
281 |
--------------------------------------------------------------------------------
/custom_components/saver/__init__.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from typing import Any, Callable, Sequence
3 |
4 | from homeassistant.config_entries import ConfigEntry
5 | from homeassistant.core import Context, HomeAssistant, ServiceCall
6 | from homeassistant.helpers.restore_state import RestoreEntity
7 | from homeassistant.helpers.script import Script, SCRIPT_MODE_PARALLEL
8 | from homeassistant.helpers.entity_component import EntityComponent
9 | from homeassistant.helpers.template import _get_state_if_valid, Template, TemplateEnvironment
10 |
11 | from .const import *
12 |
13 | _LOGGER = logging.getLogger(__name__)
14 | CONFIG_SCHEMA = SAVER_SCHEMA
15 |
16 |
17 | def setup(hass, config) -> bool:
18 | if DOMAIN not in config:
19 | return True
20 | return setup_entry(hass, config)
21 |
22 |
23 | async def async_setup_entry(hass, config_entry):
24 | result = await hass.async_add_executor_job(setup_entry, hass, config_entry)
25 | return result
26 |
27 |
28 | class SaverVariableTemplate:
29 | def __init__(self, hass: HomeAssistant, entity_id: str) -> None:
30 | self._hass = hass
31 | self._entity_id = entity_id
32 |
33 | def __call__(self, variable: str) -> Any:
34 | saver_state = _get_state_if_valid(self._hass, self._entity_id)
35 | if saver_state is None:
36 | return None
37 | variables = saver_state.attributes["variables"]
38 | if variable in variables:
39 | return variables[variable]
40 | return None
41 |
42 | def __repr__(self) -> str:
43 | return ""
44 |
45 |
46 | class SaverEntityTemplate:
47 | def __init__(self, hass: HomeAssistant, entity_id: str) -> None:
48 | self._hass = hass
49 | self._entity_id = entity_id
50 |
51 | def __call__(self, entity_id: str, attribute: str | None = None) -> Any:
52 | saver_state = _get_state_if_valid(self._hass, self._entity_id)
53 | if saver_state is None:
54 | return None
55 | entities = saver_state.attributes["entities"]
56 | if entity_id not in entities:
57 | return None
58 | state = entities[entity_id]
59 | state_val = state["state"] if isinstance(state, dict) else state.state
60 | attrs = state["attributes"] if isinstance(state, dict) else state.attributes
61 | if attribute is None:
62 | return state_val
63 | if attribute not in attrs:
64 | return None
65 | return attrs[attribute]
66 |
67 | def __repr__(self) -> str:
68 | return ""
69 |
70 |
71 | def setup_templates(hass: HomeAssistant) -> None:
72 | def is_safe_callable(self: TemplateEnvironment, obj) -> bool:
73 | # noinspection PyUnresolvedReferences
74 | return (isinstance(obj, (SaverVariableTemplate, SaverEntityTemplate))
75 | or self.saver_original_is_safe_callable(obj))
76 |
77 | def patch_environment(env: TemplateEnvironment) -> None:
78 | env.globals["saver_variable"] = SaverVariableTemplate(hass, f"{DOMAIN}.{DOMAIN}")
79 | env.globals["saver_entity"] = SaverEntityTemplate(hass, f"{DOMAIN}.{DOMAIN}")
80 |
81 | def patched_init(
82 | self: TemplateEnvironment,
83 | hass_param: HomeAssistant | None,
84 | limited: bool | None = False,
85 | strict: bool | None = False,
86 | log_fn: Callable[[int, str], None] | None = None,
87 | ) -> None:
88 | # noinspection PyUnresolvedReferences
89 | self.saver_original__init__(hass_param, limited, strict, log_fn)
90 | patch_environment(self)
91 |
92 | if not hasattr(TemplateEnvironment, 'saver_original__init__'):
93 | TemplateEnvironment.saver_original__init__ = TemplateEnvironment.__init__
94 | TemplateEnvironment.__init__ = patched_init
95 |
96 | if not hasattr(TemplateEnvironment, 'saver_original_is_safe_callable'):
97 | TemplateEnvironment.saver_original_is_safe_callable = TemplateEnvironment.is_safe_callable
98 | TemplateEnvironment.is_safe_callable = is_safe_callable
99 |
100 | tpl = Template("", hass)
101 | tpl._strict = False
102 | tpl._limited = False
103 | patch_environment(tpl._env)
104 | tpl._strict = True
105 | tpl._limited = False
106 | patch_environment(tpl._env)
107 |
108 |
109 | def setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
110 | component = EntityComponent(_LOGGER, DOMAIN, hass)
111 | saver_entity = SaverEntity()
112 | component.add_entities([saver_entity])
113 | setup_templates(hass)
114 |
115 | def clear(_call: ServiceCall) -> None:
116 | saver_entity.clear()
117 | hass.bus.fire('event_saver_cleared')
118 |
119 | def delete(call: ServiceCall) -> None:
120 | data = call.data
121 | entity_id = data[CONF_ENTITY_ID]
122 | saver_entity.delete(entity_id)
123 | hass.bus.fire('event_saver_deleted_entity', {'entity_id': entity_id})
124 |
125 | def delete_variable(call: ServiceCall) -> None:
126 | data = call.data
127 | variable = data[CONF_NAME]
128 | saver_entity.delete_variable(variable)
129 | hass.bus.fire('event_saver_deleted_variable', {'variable': variable})
130 |
131 | def execute(call: ServiceCall) -> None:
132 | data = call.data
133 | script = data[CONF_SCRIPT]
134 | saver_entity.execute(script)
135 | hass.bus.fire('event_saver_executed', {'script': script})
136 |
137 | def restore_state(call: ServiceCall) -> None:
138 | data = call.data
139 | entity_id = data[CONF_ENTITY_ID]
140 | restore_script = data[CONF_RESTORE_SCRIPT]
141 | should_delete = data[CONF_DELETE_AFTER_RUN]
142 | saver_entity.restore(entity_id, restore_script, should_delete)
143 | hass.bus.fire('event_saver_restored', {'entity_id': entity_id})
144 |
145 | def save_state(call: ServiceCall) -> None:
146 | data = call.data
147 | entity_id = data[CONF_ENTITY_ID]
148 | saver_entity.save(entity_id)
149 | hass.bus.fire('event_saver_saved_entity', {'entity_id': entity_id})
150 |
151 | def set_variable(call) -> None:
152 | data = call.data
153 | name = data[CONF_NAME]
154 | value = data[CONF_VALUE]
155 | saver_entity.set_variable(name, value)
156 | hass.bus.fire('event_saver_saved_variable', {'variable': name, 'value': value})
157 |
158 | hass.services.register(DOMAIN, SERVICE_CLEAR, clear, SERVICE_CLEAR_SCHEMA)
159 | hass.services.register(DOMAIN, SERVICE_DELETE, delete, SERVICE_DELETE_SCHEMA)
160 | hass.services.register(DOMAIN, SERVICE_DELETE_VARIABLE, delete_variable, SERVICE_DELETE_VARIABLE_SCHEMA)
161 | hass.services.register(DOMAIN, SERVICE_EXECUTE, execute, SERVICE_EXECUTE_SCHEMA)
162 | hass.services.register(DOMAIN, SERVICE_RESTORE_STATE, restore_state, SERVICE_RESTORE_STATE_SCHEMA)
163 | hass.services.register(DOMAIN, SERVICE_SAVE_STATE, save_state, SERVICE_SAVE_STATE_SCHEMA)
164 | hass.services.register(DOMAIN, SERVICE_SET_VARIABLE, set_variable, SERVICE_SET_VARIABLE_SCHEMA)
165 |
166 | return True
167 |
168 |
169 | class SaverEntity(RestoreEntity):
170 |
171 | def __init__(self) -> None:
172 | self._entities_db = {}
173 | self._variables_db = {}
174 |
175 | @property
176 | def name(self) -> str:
177 | return DOMAIN
178 |
179 | def clear(self) -> None:
180 | self._entities_db = {}
181 | self._variables_db = {}
182 | self.schedule_update_ha_state()
183 |
184 | def delete(self, entity_ids: list[str]) -> None:
185 | tmp = {**self._entities_db}
186 | for entity_id in entity_ids:
187 | tmp.pop(entity_id)
188 | self._entities_db = tmp
189 | self.schedule_update_ha_state()
190 |
191 | def delete_variable(self, variable: str) -> None:
192 | tmp = {**self._variables_db}
193 | tmp.pop(variable)
194 | self._variables_db = tmp
195 | self.schedule_update_ha_state()
196 |
197 | def execute(self, script: Sequence[dict[str, Any]]) -> None:
198 | script = Script(self.hass, script, self.name, DOMAIN, script_mode=SCRIPT_MODE_PARALLEL)
199 | variables = {}
200 | variables.update(self._variables_db)
201 | for entity_id in self._entities_db:
202 | variables.update(SaverEntity.convert_to_variables(self._entities_db[entity_id], entity_id))
203 | script.run(variables=variables, context=Context())
204 | self.schedule_update_ha_state()
205 |
206 | def restore(self, entity_id: str, restore_script: Sequence[dict[str, Any]], delete: bool) -> None:
207 | if entity_id not in self._entities_db:
208 | return
209 | old = self._entities_db[entity_id]
210 | variables = SaverEntity.convert_to_variables(old)
211 | if delete:
212 | tmp = {**self._entities_db}
213 | tmp.pop(entity_id)
214 | self._entities_db = tmp
215 | script = Script(self.hass, restore_script, self.name, DOMAIN, script_mode=SCRIPT_MODE_PARALLEL)
216 | script.run(variables=variables, context=Context())
217 | self.schedule_update_ha_state()
218 |
219 | def save(self, entity_ids: list[str]) -> None:
220 | self._entities_db = {**self._entities_db}
221 | for entity_id in entity_ids:
222 | self._entities_db[entity_id] = self.hass.states.get(entity_id)
223 | self.schedule_update_ha_state()
224 |
225 | def set_variable(self, variable: str, value: Any) -> None:
226 | self._variables_db = {**self._variables_db, variable: value}
227 | self.schedule_update_ha_state()
228 |
229 | @property
230 | def state_attributes(self) -> dict[str, Any]:
231 | return {
232 | "entities": self._entities_db,
233 | "variables": self._variables_db
234 | }
235 |
236 | @property
237 | def state(self) -> int:
238 | return len(self._entities_db) + len(self._variables_db)
239 |
240 | async def async_added_to_hass(self) -> None:
241 | state = await self.async_get_last_state()
242 | if (
243 | state is not None
244 | and state.attributes is not None
245 | and "variables" in state.attributes and not isinstance(state.attributes["entities"], list)
246 | and "entities" in state.attributes and not isinstance(state.attributes["variables"], list)
247 | ):
248 | self._variables_db = state.attributes["variables"]
249 | self._entities_db = state.attributes["entities"]
250 |
251 | @staticmethod
252 | def convert_to_variables(state: Any, entity_id: str | None = None) -> dict:
253 | prefix = ""
254 | state_val = state["state"] if isinstance(state, dict) else state.state
255 | attrs = state["attributes"] if isinstance(state, dict) else state.attributes
256 | if entity_id is not None:
257 | prefix = f"{entity_id}_".replace(".", "_")
258 | variables = {f"{prefix}state": state_val}
259 | for attr in attrs:
260 | variables[f"{prefix}attr_{attr}"] = attrs[attr]
261 | return variables
262 |
--------------------------------------------------------------------------------
/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._abort_if_unique_id_configured()
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")
20 |
21 | async_step_import = async_step_user
22 |
--------------------------------------------------------------------------------
/custom_components/saver/const.py:
--------------------------------------------------------------------------------
1 | import homeassistant.helpers.config_validation as cv
2 | from homeassistant.const import CONF_ENTITY_ID, CONF_NAME
3 |
4 | import voluptuous as vol
5 |
6 | DOMAIN = 'saver'
7 |
8 | SAVER_SCHEMA = vol.Schema(
9 | {
10 | DOMAIN: vol.Schema({})
11 | },
12 | extra=vol.ALLOW_EXTRA
13 | )
14 |
15 | CONF_DELETE_AFTER_RUN = 'delete_after_run'
16 | CONF_RESTORE_SCRIPT = 'restore_script'
17 | CONF_SCRIPT = 'script'
18 | CONF_VALUE = 'value'
19 |
20 | SERVICE_CLEAR = 'clear'
21 | SERVICE_CLEAR_SCHEMA = vol.Schema({
22 | })
23 |
24 | SERVICE_DELETE = 'delete'
25 | SERVICE_DELETE_SCHEMA = vol.Schema({
26 | vol.Required(CONF_ENTITY_ID): cv.entity_ids
27 | })
28 |
29 | SERVICE_DELETE_VARIABLE = 'delete_variable'
30 | SERVICE_DELETE_VARIABLE_SCHEMA = vol.Schema({
31 | vol.Required(CONF_NAME): cv.string
32 | })
33 |
34 | SERVICE_EXECUTE = 'execute'
35 | SERVICE_EXECUTE_SCHEMA = vol.Schema({
36 | vol.Required(CONF_SCRIPT): cv.SCRIPT_SCHEMA
37 | })
38 |
39 | SERVICE_RESTORE_STATE = 'restore_state'
40 | SERVICE_RESTORE_STATE_SCHEMA = vol.Schema({
41 | vol.Required(CONF_ENTITY_ID): cv.entity_id,
42 | vol.Required(CONF_RESTORE_SCRIPT): cv.SCRIPT_SCHEMA,
43 | vol.Optional(CONF_DELETE_AFTER_RUN, default=True): cv.boolean
44 | })
45 |
46 | SERVICE_SAVE_STATE = 'save_state'
47 | SERVICE_SAVE_STATE_SCHEMA = vol.Schema({
48 | vol.Required(CONF_ENTITY_ID): cv.entity_ids
49 | })
50 |
51 | SERVICE_SET_VARIABLE = 'set_variable'
52 | SERVICE_SET_VARIABLE_SCHEMA = vol.Schema({
53 | vol.Required(CONF_NAME): cv.string,
54 | vol.Required(CONF_VALUE): cv.string
55 | })
56 |
--------------------------------------------------------------------------------
/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": "v1.2.1"
12 | }
--------------------------------------------------------------------------------
/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 | execute:
34 | name: Execute
35 | description: >
36 | (Deprecated) Executes a script using all saved entities and variables.
37 | fields:
38 | script:
39 | name: Script
40 | description: The script to execute using saved data.
41 | required: true
42 |
43 | set_variable:
44 | name: Set variable
45 | description: >
46 | Sets the value to the variable.
47 | fields:
48 | name:
49 | name: Variable
50 | description: Name of the variable to save.
51 | example: counter
52 | required: true
53 | selector:
54 | text:
55 | value:
56 | name: Value
57 | description: The new value
58 | example: 3
59 | required: true
60 | selector:
61 | text:
62 |
63 | save_state:
64 | name: Save state
65 | description: >
66 | Saves the state and parameters of the entity.
67 | fields:
68 | entity_id:
69 | name: Entity
70 | description: ID of the entity that should be saved.
71 | example: sun.sun
72 | required: true
73 | selector:
74 | entity:
75 | multiple: true
76 |
77 | restore_state:
78 | name: Restore state
79 | description: >
80 | (Deprecated) Executes the script using saved state of the entity.
81 | fields:
82 | entity_id:
83 | name: Entity
84 | description: ID of the entity to restore.
85 | example: sun.sun
86 | required: true
87 | selector:
88 | entity:
89 | restore_script:
90 | name: Restore script
91 | description: Script used to restore values.
92 | required: 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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/hacs.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Saver",
3 | "render_readme": true,
4 | "zip_release": true,
5 | "filename": "saver.zip"
6 | }
--------------------------------------------------------------------------------