├── .devcontainer
├── configuration.yaml
└── devcontainer.json
├── .gitattributes
├── .github
└── workflows
│ ├── lint_python.yml
│ └── validate.yaml
├── .gitignore
├── .gitmodules
├── .vscode
├── settings.json
└── tasks.json
├── README.md
├── custom_components
└── variable
│ ├── __init__.py
│ ├── binary_sensor.py
│ ├── config_flow.py
│ ├── const.py
│ ├── device.py
│ ├── device_tracker.py
│ ├── helpers.py
│ ├── manifest.json
│ ├── sensor.py
│ ├── services.yaml
│ ├── strings.json
│ └── translations
│ ├── en.json
│ └── sk.json
├── examples
├── counter.yaml
├── history.yaml
├── keypad.yaml
├── timer.yaml
└── value_and_data.yaml
├── hacs.json
├── logo
├── icon.png
└── icon@2x.png
├── requirements.txt
└── setup.cfg
/.devcontainer/configuration.yaml:
--------------------------------------------------------------------------------
1 | #Limited configuration instead of default_config
2 | #https://github.com/home-assistant/core/tree/dev/homeassistant/components/default_config
3 | automation:
4 | frontend:
5 | history:
6 | logbook:
7 |
8 | homeassistant:
9 | name: Home
10 |
11 | variable:
12 | test_sensor:
13 | value: 0
14 | restore: true
15 | domain: sensor
16 |
17 | test_counter:
18 | value: 0
19 | attributes:
20 | icon: mdi:alarm
21 |
22 | logger:
23 | default: info
24 | logs:
25 | custom_components.variable: debug
26 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Home Assistant Custom Component Dev",
3 | "context": "..",
4 | "image": "ghcr.io/ludeeus/devcontainer/integration:latest",
5 | "appPort": "9123:8123",
6 | "postCreateCommand": "container install && pip install --upgrade pip && pip install --ignore-installed -r requirements.txt",
7 | "extensions": [
8 | "ms-python.python",
9 | "github.vscode-pull-request-github",
10 | "ms-python.vscode-pylance",
11 | "spmeesseman.vscode-taskexplorer"
12 | ],
13 | "settings": {
14 | "files.eol": "\n",
15 | "editor.tabSize": 4,
16 | "terminal.integrated.shell.linux": "/bin/bash",
17 | "python.pythonPath": "/usr/local/python/bin/python",
18 | "python.analysis.autoSearchPaths": false,
19 | "python.linting.pylintEnabled": true,
20 | "python.linting.enabled": true,
21 | "python.linting.pylintArgs": [
22 | "--disable",
23 | "import-error"
24 | ],
25 | "python.formatting.provider": "black",
26 | "python.testing.pytestArgs": [
27 | "--no-cov"
28 | ],
29 | "editor.formatOnPaste": false,
30 | "editor.formatOnSave": true,
31 | "editor.formatOnType": true,
32 | "files.trimTrailingWhitespace": true
33 | }
34 | }
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text eol=lf
2 | *.py whitespace=error
3 |
4 | *.ico binary
5 | *.jpg binary
6 | *.png binary
7 | *.zip binary
8 | *.mp3 binary
--------------------------------------------------------------------------------
/.github/workflows/lint_python.yml:
--------------------------------------------------------------------------------
1 | name: lint_python
2 |
3 | on: [pull_request, push]
4 |
5 | jobs:
6 | lint_python:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v2
10 | - uses: actions/setup-python@v2
11 | - run: pip install --upgrade pip wheel
12 | - run: pip install bandit black codespell flake8 flake8-2020 flake8-bugbear
13 | flake8-comprehensions isort mypy pytest pyupgrade safety
14 | - run: bandit --recursive --skip B105,B108,B303,B311 .
15 | - run: black --check --diff . || true
16 | - run: codespell --ignore-words-list="hass,THIRDPARTY" --skip="./.git"
17 | - run: flake8 . --count --ignore=B001,E241,E265,E302,E722,E731,F403,F405,F841,W503,W504
18 | --max-complexity=21 --max-line-length=184 --show-source --statistics
19 | - run: isort --check-only --profile black . || true
20 | - run: pip install -r requirements.txt || pip install --editable . || true
21 | - run: mkdir --parents --verbose .mypy_cache
22 | - run: mypy --ignore-missing-imports --install-types --non-interactive . || true
23 | - run: pytest . || true
24 | - run: pytest --doctest-modules . || true
25 | - run: shopt -s globstar && pyupgrade --py36-plus **/*.py || true
26 |
--------------------------------------------------------------------------------
/.github/workflows/validate.yaml:
--------------------------------------------------------------------------------
1 | name: Validate
2 |
3 | on:
4 | push:
5 | pull_request:
6 | schedule:
7 | - cron: "0 0 * * *"
8 |
9 | jobs:
10 | validate:
11 | runs-on: "ubuntu-latest"
12 | steps:
13 | - name: Download repo
14 | uses: "actions/checkout@v2"
15 | - name: Hassfest validation
16 | uses: home-assistant/actions/hassfest@master
17 | - name: HACS validation
18 | uses: "hacs/action@main"
19 | with:
20 | category: "integration"
21 | ignore: brands
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 |
49 | # Translations
50 | *.mo
51 | *.pot
52 |
53 | # Django stuff:
54 | *.log
55 | local_settings.py
56 |
57 | # Flask stuff:
58 | instance/
59 | .webassets-cache
60 |
61 | # Scrapy stuff:
62 | .scrapy
63 |
64 | # Sphinx documentation
65 | docs/_build/
66 |
67 | # PyBuilder
68 | target/
69 |
70 | # Jupyter Notebook
71 | .ipynb_checkpoints
72 |
73 | # pyenv
74 | .python-version
75 |
76 | # celery beat schedule file
77 | celerybeat-schedule
78 |
79 | # SageMath parsed files
80 | *.sage.py
81 |
82 | # dotenv
83 | .env
84 |
85 | # virtualenv
86 | .venv
87 | venv/
88 | ENV/
89 |
90 | # Spyder project settings
91 | .spyderproject
92 | .spyproject
93 |
94 | # Rope project settings
95 | .ropeproject
96 |
97 | # mkdocs documentation
98 | /site
99 |
100 | .dccache
101 |
102 | # mypy
103 | .mypy_cache/
104 | .idea
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "custom_components/variable/recorder_history_prefilter"]
2 | path = custom_components/variable/recorder_history_prefilter
3 | url = https://github.com/gcobb321/recorder_history_prefilter
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.formatting.provider": "black",
3 | "editor.formatOnPaste": true,
4 | "editor.formatOnSave": true,
5 | "files.trimTrailingWhitespace": true,
6 | "git.ignoreLimitWarning": true,
7 | "files.associations": {
8 | "*.yaml": "home-assistant"
9 | }
10 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "Run Home Assistant on port 9123",
6 | "type": "shell",
7 | "command": "container start",
8 | "problemMatcher": []
9 | },
10 | {
11 | "label": "Run Home Assistant configuration against /config",
12 | "type": "shell",
13 | "command": "container check",
14 | "problemMatcher": []
15 | },
16 | {
17 | "label": "Upgrade Home Assistant to latest dev",
18 | "type": "shell",
19 | "command": "container install",
20 | "problemMatcher": []
21 | },
22 | {
23 | "label": "Install a specific version of Home Assistant",
24 | "type": "shell",
25 | "command": "container set-version",
26 | "problemMatcher": []
27 | }
28 | ]
29 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Variables+History
2 | ### aka. `variable`
3 |
4 |
5 |
6 | A Home Assistant Integration to declare and set/update variables.
7 |
8 | Forked and updated from initial integration developed by [rogro82](https://github.com/rogro82)
9 |
10 | # Home Assistant versions starting with v2024.7.2 will prevent Variables+History versions earlier than v3.4.5 from working. Be sure to upgrade.
11 |
12 | ## Upgrading from v2 to v3
13 | **Existing variables will remain as yaml variables but instead of starting with `variable.`, they will now start with `sensor.` If you would like to manage the variable using the UI configuration, you will need to delete the entity from your yaml and recreate it in the UI. This is also the only change needed when migrating from rogro82's version to this one**
14 |
15 | ## Installation
16 |
17 | ### HACS *(recommended)*
18 | 1. Ensure that [HACS](https://hacs.xyz/) is installed
19 | 2. [Click Here](https://my.home-assistant.io/redirect/hacs_repository/?owner=enkama&repository=hass-variables) to directly open `Variables+History` in HACS **or**
20 | a. Navigate to HACS
21 | b. Click `+ Explore & Download Repositories`
22 | c. Find the `Variables+History` integration
23 | 3. Click `Download`
24 | 4. Restart Home Assistant
25 | 5. See [Configuration](#configuration) below
26 |
27 |
28 | Manual
29 |
30 | You probably **do not** want to do this! Use the HACS method above unless you know what you are doing and have a good reason as to why you are installing manually
31 |
32 | 1. Using the tool of choice open the directory (folder) for your HA configuration (where you find `configuration.yaml`)
33 | 2. If you do not have a `custom_components` directory there, you need to create it
34 | 3. In the `custom_components` directory create a new folder called `variable`
35 | 4. Download _all_ the files from the `custom_components/variable/` directory in this repository
36 | 5. Place the files you downloaded in the new directory you created
37 | 6. Restart Home Assistant
38 | 7. See [Configuration](#configuration) below
39 |
40 |
41 | ## Preferred Configuration
42 | 1. [Click Here](https://my.home-assistant.io/redirect/config_flow_start/?domain=variable) to directly add a `Variables+History` sensor **or**
43 | a. In Home Assistant, go to Settings -> [Integrations](https://my.home-assistant.io/redirect/integrations/)
44 | b. Click `+ Add Integrations` and select `Variables+History`
45 | 2. Add your configuration ([see Configuration Options below](#configuration-options))
46 | 3. Click `Submit`
47 | * Repeat as needed to create additional `Variables+History` sensors
48 | * Options can be changed for existing `Variables+History` sensors in Home Assistant Integrations by selecting `Configure` under the desired `Variables+History` sensor.
49 |
50 | ## Configuration Options
51 |
52 | ### First choose the `variable` type.
53 |
54 |
55 | Sensor
56 |
57 | | Name | Required | Default | Description |
58 | |-------------------------|----------|----------------|---------------------------------------------------------------------------------------------------------------------------------|
59 | | `Variable ID` | `Yes` | | The desired id of the new sensor (ex. `test_variable` would create an entity_id of `sensor.test_variable`) |
60 | | `Name` | `No` | | Friendly name of the variable sensor |
61 | | `Icon` | `No` | `mdi:variable` | Icon of the Variable |
62 | | `Initial Value` | `No` | | Initial value/state of the variable. If `Restore on Restart` is `False`, the variable will reset to this value on every restart |
63 | | `Initial Attributes` | `No` | | Initial attributes of the variable. If `Restore on Restart` is `False`, the variable will reset to this value on every restart |
64 | | `Restore on Restart` | `No` | `True` | If `True` will restore previous value on restart. If `False`, will reset to `Initial Value` and `Initial Attributes` on restart |
65 | | `Force Update` | `No` | `False` | Variable's `last_updated` time will change with any service calls to update the variable even if the value does not change |
66 | | `Exclude from Recorder` | `No` | `False` | For Variables with large attributes (>16 kB), enable this to prevent Recorder Errors. |
67 |
68 |
69 |
70 |
71 | Binary Sensor
72 |
73 | | Name | Required | Default | Description |
74 | |-------------------------|----------|----------------|------------------------------------------------------------------------------------------------------------------------------------------------|
75 | | `Variable ID` | `Yes` | | The desired id of the new binary sensor (ex. `test_variable` would create an entity_id of `binary_sensor.test_variable`) |
76 | | `Name` | `No` | | Friendly name of the variable binary sensor |
77 | | `Icon` | `No` | `mdi:variable` | Icon of the Variable |
78 | | `Initial Value` | `No` | `False` | Initial `True`/`False` value/state of the variable. If `Restore on Restart` is `False`, the variable will reset to this value on every restart |
79 | | `Initial Attributes` | `No` | | Initial attributes of the variable. If `Restore on Restart` is `False`, the variable will reset to this value on every restart |
80 | | `Restore on Restart` | `No` | `True` | If `True` will restore previous value on restart. If `False`, will reset to `Initial Value` and `Initial Attributes` on restart |
81 | | `Force Update` | `No` | `False` | Variable's `last_updated` time will change with any service calls to update the variable even if the value does not change |
82 | | `Exclude from Recorder` | `No` | `False` | For Variables with large attributes (>16 kB), enable this to prevent Recorder Errors. |
83 |
84 |
85 |
86 |
87 | Device Tracker
88 |
89 | | Name | Required | Default | Description |
90 | |-------------------------|----------|----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
91 | | `Variable ID` | `Yes` | | The desired id of the new device tracker (ex. `test_variable` would create an entity_id of `device_tracker.test_variable`) |
92 | | `Name` | `No` | | Friendly name of the variable device tracker |
93 | | `Icon` | `No` | `mdi:variable` | Icon of the Variable |
94 | | `Initial Latitude` | `Yes` | | Latitude |
95 | | `Initial Longitude` | `Yes` | | Longitude |
96 | | `Initial Location Name` | `No` | | If set, will show this as the state |
97 | | `Initial GPS Accuracy` | `No` | | Accuracy in meters |
98 | | `Initial Battery Level` | `No` | | Battery level from 0-100% |
99 | | `Initial Attributes` | `No` | | Initial attributes of the variable |
100 | | `Restore on Restart` | `No` | `True` | If `True` will restore previous value on restart. If `False`, will reset to `Initial Latitude`, `Initial Longitude`, `Initial Location Name`, `Initial GPS Accuracy`, `Initial Battery Level`, and `Initial Attributes` on restart |
101 | | `Force Update` | `No` | `False` | Variable's `last_updated` time will change with any service calls to update the variable even if the value does not change |
102 | | `Exclude from Recorder` | `No` | `False` | For Variables with large attributes (>16 kB), enable this to prevent Recorder Errors. |
103 |
104 |
105 |
106 |
107 | Alternate YAML Configuration
108 |
109 | **Variables created via YAML will all start with `sensor.` and cannot be edited in the UI.**
110 |
111 | _You can have a combination of Variables created via the UI and via YAML._
112 |
113 | Add the component `variable` to your configuration and declare the variables you want.
114 |
115 | | Name | yaml | Required | Default | Description |
116 | |-----------------------|-------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------|
117 | | Variable ID | `:` | `Yes` | | The desired id of the new sensor (ex. `test_variable` would create an entity_id of `sensor.test_variable`) |
118 | | Name | `name` | `No` | | Friendly name of the variable sensor |
119 | | Initial Value | `value` | `No` | | Initial value/state of the variable. If `Restore on Restart` is `False`, the variable will reset to this value on every restart |
120 | | Initial Attributes | `attributes` | `No` | | Initial attributes of the variable. If `Restore on Restart` is `False`, the variable will reset to this value on every restart |
121 | | Restore on Restart | `restore` | `No` | `True` | If `True` will restore previous value on restart. If `False`, will reset to `Initial Value` and `Initial Attributes` on restart |
122 | | Force Update | `force_update` | `No` | `False` | Variable's `last_updated` time will change with any service calls to update the variable even if the value does not change |
123 | | Exclude from Recorder | `exclude_from_recorder` | `No` | `False` | For Variables with large attributes (>16 kB), set to `True` to prevent Recorder Errors. |
124 |
125 | #### Example:
126 |
127 | ```yaml
128 | variable:
129 | countdown_timer:
130 | value: 30
131 | attributes:
132 | friendly_name: 'Countdown'
133 | icon: mdi:alarm
134 | countdown_trigger:
135 | name: Countdown
136 | value: False
137 | light_scene:
138 | value: 'normal'
139 | attributes:
140 | previous: ''
141 | restore: true
142 | current_power_usage:
143 | force_update: true
144 |
145 | daily_download:
146 | value: 0
147 | restore: true
148 | attributes:
149 | state_class: measurement
150 | unit_of_measurement: GB
151 | icon: mdi:download
152 | ```
153 |
154 |
155 |
156 | ## Services
157 |
158 | There are instructions and selectors when the service is called from the Developer Tools or within a Script or Automation.
159 |
160 | ### `variable.update_sensor`
161 |
162 | Used to update the value or attributes of a Sensor Variable
163 |
164 | | Name | Key | Required | Default | Description |
165 | |----------------------|-----------------------------------------|----------|---------|---------------------------------------------------------------------------------------|
166 | | `Targets` | `target:`
`entity_id:` | `Yes` | | The entity_ids of one or more sensor variables to update (ex. `sensor.test_variable`) |
167 | | `New Value` | `value` | `No` | | Value/state to change the variable to |
168 | | `New Attributes` | `attributes` | `No` | | What to update the attributes to |
169 | | `Replace Attributes` | `replace_attributes` | `No` | `False` | Replace or merge current attributes (`False` = merge) |
170 |
171 | ### `variable.update_binary_sensor`
172 |
173 | Used to update the value or attributes of a Binary Sensor Variable
174 |
175 | | Name | Key | Required | Default | Description |
176 | |----------------------|-----------------------------------------|----------|---------|-----------------------------------------------------------------------------------------------------|
177 | | `Targets` | `target:`
`entity_id:` | `Yes` | | The entity_ids of one or more binary sensor variables to update (ex. `binary_sensor.test_variable`) |
178 | | `New Value` | `value` | `No` | | Value/state to change the variable to |
179 | | `New Attributes` | `attributes` | `No` | | What to update the attributes to |
180 | | `Replace Attributes` | `replace_attributes` | `No` | `False` | Replace or merge current attributes (`False` = merge) |
181 |
182 | ### `variable.update_device_tracker`
183 |
184 | Used to update the value or attributes of a Device Tracker Variable
185 |
186 | | Name | Key | Required | Default | Description |
187 | |------------------------|-----------------------------------------|----------|---------|-------------------------------------------------------------------------------------------------------|
188 | | `Targets` | `target:`
`entity_id:` | `Yes` | | The entity_ids of one or more device tracker variables to update (ex. `device_tracker.test_variable`) |
189 | | `Latitude` | `latitude` | `No` | | Latitude |
190 | | `Longitude` | `longitude` | `No` | | Longitude |
191 | | `Location Name` | `location_name` | `No` | | If set, will show this as the state |
192 | | `Delete Location Name` | `delete_location_name` | `No` | | Remove the Location Name so state will be based on Lat/Long (`boolean`) |
193 | | `GPS Accuracy` | `gps_accuracy` | `No` | | Accuracy in meters |
194 | | `Battery Level` | `battery_level` | `No` | | Battery level from 0-100% |
195 |
196 | ### `variable.toggle_binary_sensor`
197 |
198 | Used to toggle the state or update attributes of a Binary Sensor Variable. If the binary_sensor state is None, the toggle service will not change the state.
199 |
200 | | Name | Key | Required | Default | Description |
201 | |----------------------|-----------------------------------------|----------|---------|-----------------------------------------------------------------------------------------------------|
202 | | `Targets` | `target:`
`entity_id:` | `Yes` | | The entity_ids of one or more binary sensor variables to toggle (ex. `binary_sensor.test_variable`) |
203 | | `New Attributes` | `attributes` | `No` | | What to update the attributes to |
204 | | `Replace Attributes` | `replace_attributes` | `No` | `False` | Replace or merge current attributes (`False` = merge) |
205 |
206 |
207 | Legacy Services
208 |
209 | #### These will only work for Sensor Variables
210 | _These services are from the previous version of the integration and are being kept for pre-existing automations and scripts. In general, the new `variable.update_` and `variable.toggle_` services above should be used going forward._
211 |
212 | Both services are similar and used to update the value or attributes of a Sensor Variable. `variable.set_variable` uses just the `variable_id` and `variable.set_entity` uses the full `entity_id`. There are instructions and selectors when the service is called from the Developer Tools or within a Script or Automation.
213 |
214 | ### `variable.set_variable`
215 |
216 | | Name | Key | Required | Default | Description |
217 | |----------------------|----------------------|----------|---------|---------------------------------------------------------------------------------------------------------------|
218 | | `Variable ID` | `variable` | `Yes` | | The id of the sensor variable to update (ex. `test_variable` for a sensor variable of `sensor.test_variable`) |
219 | | `Value` | `value` | `No` | | Value/state to change the variable to |
220 | | `Attributes` | `attributes` | `No` | | What to update the attributes to |
221 | | `Replace Attributes` | `replace_attributes` | `No` | `False` | Replace or merge current attributes (`False` = merge) |
222 |
223 | ### `variable.set_entity`
224 |
225 | | Name | Key | Required | Default | Description |
226 | |----------------------|----------------------|----------|---------|-----------------------------------------------------------------------------|
227 | | `Entity ID` | `entity` | `Yes` | | The entity_id of the sensor variable to update (ex. `sensor.test_variable`) |
228 | | `Value` | `value` | `No` | | Value/state to change the variable to |
229 | | `Attributes` | `attributes` | `No` | | What to update the attributes to |
230 | | `Replace Attributes` | `replace_attributes` | `No` | `False` | Replace or merge current attributes (`False` = merge) |
231 |
232 |
233 |
234 | ## Example service calls
235 |
236 | ```yaml
237 | action:
238 | - service: variable.update_sensor
239 | data:
240 | value: 30
241 | target:
242 | entity_id: sensor.test_timer
243 | ```
244 | ```yaml
245 | action:
246 | - service: variable.update_sensor
247 | data:
248 | value: >-
249 | {{trigger.to_state.name|replace('Motion Sensor','')}}
250 | attributes:
251 | history_1: "{{states('sensor.last_motion')}}"
252 | history_2: "{{state_attr('sensor.last_motion','history_1')}}"
253 | history_3: "{{state_attr('sensor.last_motion','history_2')}}"
254 | target:
255 | entity_id: sensor.last_motion
256 | ```
257 | ```yaml
258 | action:
259 | - service: variable.update_binary_sensor
260 | data:
261 | value: true
262 | replace_attributes: true
263 | attributes:
264 | country: USA
265 | target:
266 | entity_id: binary_sensor.test_binary_var
267 | ```
268 |
269 | ## Example timer automation
270 |
271 | * Create a sensor variable with the Variable ID of `test_timer` and Initial Value of `0`
272 |
273 | ```yaml
274 | script:
275 | schedule_test_timer:
276 | sequence:
277 | - service: variable.update_sensor
278 | data:
279 | value: 30
280 | target:
281 | entity_id: sensor.test_timer
282 | - service: automation.turn_on
283 | data:
284 | entity_id: automation.test_timer_countdown
285 |
286 | automation:
287 | - alias: test_timer_countdown
288 | initial_state: 'off'
289 | trigger:
290 | - platform: time_pattern
291 | seconds: '/1'
292 | action:
293 | - service: variable.update_sensor
294 | data:
295 | value: >
296 | {{ [((states('sensor.test_timer') | int(default=0)) - 1), 0] | max }}
297 | target:
298 | entity_id: sensor.test_timer
299 | - alias: test_timer_trigger
300 | trigger:
301 | platform: state
302 | entity_id: sensor.test_timer
303 | to: '0'
304 | action:
305 | - service: automation.turn_off
306 | data:
307 | entity_id: automation.test_timer_countdown
308 | ```
309 |
310 | ## Examples
311 |
312 |
313 | Play and Save TTS Messages + Message History - Made by jazzyisj
314 |
315 | #### https://github.com/jazzyisj/save-tts-messages
316 |
317 | This is more or less an answering machine (remember those?) for your TTS messages. When you play a TTS message that you want saved under certain conditions (i.e. nobody is home), you will call the script Play or Save TTS Message script.play_or_save_message instead of calling your tts service (or Alexa notify) directly. The script will decide whether to play the message immediately, or save it based on the conditions you specify. If a saved tts message is repeated another message is not saved, only the timestamp is updated to the most recent instance.
318 |
319 | Messages are played back using the Play Saved TTS Messages script "script.play_saved_tts_messages". Set an appropriate trigger (for example when you arrive home) in the automation Play Saved Messages automation.play_saved_messages automation to call this script automatically.
320 |
321 | Saved messages will survive restarts.
322 |
323 | BONUS - OPTIONAL TTS MESSAGE HISTORY
324 |
325 | You can find the full documentation on how to do this and adjust this to your needs in [here](https://github.com/enkama/hass-variables/tree/master/examples/save-tts-message/tts.md).
326 |
327 |
328 | #### More examples can be found in the [examples](https://github.com/enkama/hass-variables/tree/master/examples) folder.
329 |
--------------------------------------------------------------------------------
/custom_components/variable/__init__.py:
--------------------------------------------------------------------------------
1 | """Variable implementation for Home Assistant."""
2 |
3 | import contextlib
4 | import copy
5 | import json
6 | import logging
7 |
8 | import voluptuous as vol
9 | from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
10 | from homeassistant.const import (
11 | CONF_DEVICE,
12 | CONF_DEVICE_ID,
13 | CONF_ENTITY_ID,
14 | CONF_FRIENDLY_NAME,
15 | CONF_ICON,
16 | CONF_NAME,
17 | SERVICE_RELOAD,
18 | Platform,
19 | )
20 | from homeassistant.core import HomeAssistant, ServiceCall
21 | from homeassistant.exceptions import HomeAssistantError
22 | from homeassistant.helpers import config_validation as cv
23 | from homeassistant.helpers.device import (
24 | async_remove_stale_devices_links_keep_current_device,
25 | )
26 | from homeassistant.helpers.reload import async_integration_yaml_config
27 | from homeassistant.helpers.typing import ConfigType
28 |
29 | from .const import (
30 | ATTR_ATTRIBUTES,
31 | ATTR_ENTITY,
32 | ATTR_REPLACE_ATTRIBUTES,
33 | ATTR_VALUE,
34 | ATTR_VARIABLE,
35 | CONF_ATTRIBUTES,
36 | CONF_ENTITY_PLATFORM,
37 | CONF_FORCE_UPDATE,
38 | CONF_RESTORE,
39 | CONF_VALUE,
40 | CONF_VARIABLE_ID,
41 | CONF_YAML_PRESENT,
42 | CONF_YAML_VARIABLE,
43 | DEFAULT_REPLACE_ATTRIBUTES,
44 | DOMAIN,
45 | PLATFORMS,
46 | SERVICE_UPDATE_SENSOR,
47 | )
48 | from .device import create_device, remove_device
49 |
50 | _LOGGER = logging.getLogger(__name__)
51 |
52 | SERVICE_SET_VARIABLE_LEGACY = "set_variable"
53 | SERVICE_SET_ENTITY_LEGACY = "set_entity"
54 |
55 | SERVICE_SET_VARIABLE_LEGACY_SCHEMA = vol.Schema(
56 | {
57 | vol.Required(ATTR_VARIABLE): cv.string,
58 | vol.Optional(ATTR_VALUE): cv.match_all,
59 | vol.Optional(ATTR_ATTRIBUTES): dict,
60 | vol.Optional(
61 | ATTR_REPLACE_ATTRIBUTES, default=DEFAULT_REPLACE_ATTRIBUTES
62 | ): cv.boolean,
63 | }
64 | )
65 |
66 | SERVICE_SET_ENTITY_LEGACY_SCHEMA = vol.Schema(
67 | {
68 | vol.Required(ATTR_ENTITY): cv.string,
69 | vol.Optional(ATTR_VALUE): cv.match_all,
70 | vol.Optional(ATTR_ATTRIBUTES): dict,
71 | vol.Optional(
72 | ATTR_REPLACE_ATTRIBUTES, default=DEFAULT_REPLACE_ATTRIBUTES
73 | ): cv.boolean,
74 | }
75 | )
76 |
77 |
78 | async def async_setup(hass: HomeAssistant, config: ConfigType):
79 | """Set up the Variable services."""
80 |
81 | async def async_set_variable_legacy_service(call: ServiceCall) -> None:
82 | """Handle calls to the set_variable legacy service."""
83 |
84 | # _LOGGER.debug(f"[async_set_variable_legacy_service] Pre call data: {call.data}")
85 | ENTITY_ID_FORMAT = Platform.SENSOR + ".{}"
86 | var_ent = ENTITY_ID_FORMAT.format(call.data.get(ATTR_VARIABLE))
87 | # _LOGGER.debug(f"[async_set_variable_legacy_service] Post call data: {call.data}")
88 | await _async_set_legacy_service(call, var_ent)
89 |
90 | async def async_set_entity_legacy_service(call: ServiceCall) -> None:
91 | """Handle calls to the set_entity legacy service."""
92 |
93 | # _LOGGER.debug(f"[async_set_entity_legacy_service] call data: {call.data}")
94 | await _async_set_legacy_service(call, call.data.get(ATTR_ENTITY))
95 |
96 | async def _async_set_legacy_service(call: ServiceCall, var_ent: str):
97 | """Shared function for both set_entity and set_variable legacy services."""
98 |
99 | # _LOGGER.debug(f"[async_set_legacy_service] call data: {call.data}")
100 | update_sensor_data = {
101 | CONF_ENTITY_ID: [var_ent],
102 | ATTR_REPLACE_ATTRIBUTES: call.data.get(ATTR_REPLACE_ATTRIBUTES, False),
103 | }
104 | if call.data.get(ATTR_VALUE):
105 | update_sensor_data.update({ATTR_VALUE: call.data.get(ATTR_VALUE)})
106 | if call.data.get(ATTR_ATTRIBUTES):
107 | update_sensor_data.update({ATTR_ATTRIBUTES: call.data.get(ATTR_ATTRIBUTES)})
108 | _LOGGER.debug(
109 | f"[async_set_legacy_service] update_sensor_data: {update_sensor_data}"
110 | )
111 | await hass.services.async_call(
112 | DOMAIN, SERVICE_UPDATE_SENSOR, service_data=update_sensor_data
113 | )
114 |
115 | async def _async_reload_service_handler(service: ServiceCall) -> None:
116 | """Handle reload service call."""
117 | _LOGGER.info("Service %s.reload called: reloading YAML integration", DOMAIN)
118 | reload_config = None
119 | with contextlib.suppress(HomeAssistantError):
120 | reload_config = await async_integration_yaml_config(hass, DOMAIN)
121 | if reload_config is None:
122 | return
123 | _LOGGER.debug(f" reload_config: {reload_config}")
124 | await _async_process_yaml(hass, reload_config)
125 |
126 | hass.services.async_register(
127 | DOMAIN,
128 | SERVICE_SET_VARIABLE_LEGACY,
129 | async_set_variable_legacy_service,
130 | schema=SERVICE_SET_VARIABLE_LEGACY_SCHEMA,
131 | )
132 |
133 | hass.services.async_register(
134 | DOMAIN,
135 | SERVICE_SET_ENTITY_LEGACY,
136 | async_set_entity_legacy_service,
137 | schema=SERVICE_SET_ENTITY_LEGACY_SCHEMA,
138 | )
139 | hass.services.async_register(DOMAIN, SERVICE_RELOAD, _async_reload_service_handler)
140 |
141 | return await _async_process_yaml(hass, config)
142 |
143 |
144 | async def _async_process_yaml(hass: HomeAssistant, config: ConfigType) -> bool:
145 | variables = json.loads(json.dumps(config.get(DOMAIN, {})))
146 |
147 | for var, var_fields in variables.items():
148 | if var is not None:
149 | _LOGGER.debug(f"[YAML] variable_id: {var}")
150 | _LOGGER.debug(f"[YAML] var_fields: {var_fields}")
151 |
152 | for key_empty, var_empty in var_fields.copy().items():
153 | if var_empty is None:
154 | var_fields.pop(key_empty)
155 |
156 | attr = var_fields.get(CONF_ATTRIBUTES, {})
157 | icon = attr.pop(CONF_ICON, None)
158 | name = var_fields.get(CONF_NAME, attr.pop(CONF_FRIENDLY_NAME, None))
159 | attr.pop(CONF_FRIENDLY_NAME, None)
160 |
161 | if var not in {
162 | entry.data.get(CONF_VARIABLE_ID)
163 | for entry in hass.config_entries.async_entries(DOMAIN)
164 | }:
165 | _LOGGER.warning(f"[YAML] Creating New Sensor Variable: {var}")
166 | hass.async_create_task(
167 | hass.config_entries.flow.async_init(
168 | DOMAIN,
169 | context={"source": SOURCE_IMPORT},
170 | data={
171 | CONF_ENTITY_PLATFORM: Platform.SENSOR,
172 | CONF_VARIABLE_ID: var,
173 | CONF_NAME: name,
174 | CONF_VALUE: var_fields.get(CONF_VALUE),
175 | CONF_RESTORE: var_fields.get(CONF_RESTORE),
176 | CONF_FORCE_UPDATE: var_fields.get(CONF_FORCE_UPDATE),
177 | CONF_ATTRIBUTES: attr,
178 | CONF_ICON: icon,
179 | },
180 | )
181 | )
182 | else:
183 | _LOGGER.info(f"[YAML] Updating Existing Sensor Variable: {var}")
184 |
185 | entry_id = None
186 | for ent in hass.config_entries.async_entries(DOMAIN):
187 | if var == ent.data.get(CONF_VARIABLE_ID):
188 | entry_id = ent.entry_id
189 | break
190 | # _LOGGER.debug(f"[YAML] entry_id: {entry_id}")
191 | if entry_id:
192 | entry = ent
193 | # _LOGGER.debug(f"[YAML] entry before: {entry.as_dict()}")
194 |
195 | for m in dict(entry.data).keys():
196 | var_fields.setdefault(m, entry.data[m])
197 | var_fields.update({CONF_YAML_PRESENT: True})
198 | # _LOGGER.debug(f"[YAML] Updated var_fields: {var_fields}")
199 | hass.config_entries.async_update_entry(
200 | entry, data=var_fields, options={}
201 | )
202 |
203 | hass.async_create_task(hass.config_entries.async_reload(entry_id))
204 |
205 | else:
206 | _LOGGER.error(
207 | f"[YAML] Update Error. Could not find entry_id for: {var}"
208 | )
209 |
210 | return True
211 |
212 |
213 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
214 | """Set up from a config entry."""
215 |
216 | # _LOGGER.debug(f"[init async_setup_entry] entry: {entry.data}")
217 | if entry.data.get(CONF_YAML_VARIABLE, False) is True:
218 | if entry.data.get(CONF_YAML_PRESENT, False) is False:
219 | _LOGGER.warning(
220 | f"[YAML] YAML Entry no longer exists, deleting entry in HA: {entry.data.get(CONF_VARIABLE_ID)}"
221 | )
222 | # _LOGGER.debug(f"[YAML] entry_id: {entry.entry_id}")
223 | hass.async_create_task(hass.config_entries.async_remove(entry.entry_id))
224 | return False
225 | else:
226 | yaml_data = copy.deepcopy(dict(entry.data))
227 | yaml_data.pop(CONF_YAML_PRESENT, None)
228 | hass.config_entries.async_update_entry(entry, data=yaml_data, options={})
229 | async_remove_stale_devices_links_keep_current_device(
230 | hass,
231 | entry.entry_id,
232 | entry.data.get(CONF_DEVICE_ID),
233 | )
234 | hass.data.setdefault(DOMAIN, {})
235 | hass_data = dict(entry.data)
236 | hass.data[DOMAIN][entry.entry_id] = hass_data
237 | if hass_data.get(CONF_ENTITY_PLATFORM) in PLATFORMS:
238 | await hass.config_entries.async_forward_entry_setups(
239 | entry, [hass_data.get(CONF_ENTITY_PLATFORM)]
240 | )
241 | elif hass_data.get(CONF_ENTITY_PLATFORM) == CONF_DEVICE:
242 | await create_device(hass, entry)
243 | return True
244 |
245 |
246 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
247 | """Unload a config entry."""
248 |
249 | _LOGGER.info(f"Unloading: {entry.data}")
250 | # _LOGGER.debug(f"[init async_unload_entry] entry: {entry}")
251 | hass_data = dict(entry.data)
252 | unload_ok = False
253 | if hass_data.get(CONF_ENTITY_PLATFORM) in PLATFORMS:
254 | unload_ok = await hass.config_entries.async_unload_platforms(
255 | entry, [hass_data.get(CONF_ENTITY_PLATFORM)]
256 | )
257 | elif hass_data.get(CONF_ENTITY_PLATFORM) == CONF_DEVICE:
258 | unload_ok = await remove_device(hass, entry)
259 | if unload_ok:
260 | hass.data[DOMAIN].pop(entry.entry_id)
261 |
262 | return unload_ok
263 |
--------------------------------------------------------------------------------
/custom_components/variable/binary_sensor.py:
--------------------------------------------------------------------------------
1 | import copy
2 | import logging
3 | from collections.abc import MutableMapping
4 |
5 | import homeassistant.helpers.entity_registry as er
6 | import voluptuous as vol
7 | from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity
8 | from homeassistant.config_entries import ConfigEntry
9 | from homeassistant.const import (
10 | ATTR_FRIENDLY_NAME,
11 | ATTR_ICON,
12 | CONF_DEVICE_CLASS,
13 | CONF_DEVICE_ID,
14 | CONF_ICON,
15 | CONF_NAME,
16 | MATCH_ALL,
17 | STATE_OFF,
18 | STATE_ON,
19 | Platform,
20 | )
21 | from homeassistant.core import HomeAssistant
22 | from homeassistant.helpers import config_validation as cv
23 | from homeassistant.helpers import entity_platform, selector
24 | from homeassistant.helpers.device import async_device_info_to_link_from_device_id
25 | from homeassistant.helpers.entity import generate_entity_id
26 | from homeassistant.helpers.restore_state import RestoreEntity
27 | from homeassistant.util import slugify
28 |
29 | from .const import (
30 | ATTR_ATTRIBUTES,
31 | ATTR_REPLACE_ATTRIBUTES,
32 | ATTR_VALUE,
33 | CONF_ATTRIBUTES,
34 | CONF_EXCLUDE_FROM_RECORDER,
35 | CONF_FORCE_UPDATE,
36 | CONF_RESTORE,
37 | CONF_UPDATED,
38 | CONF_VALUE,
39 | CONF_VARIABLE_ID,
40 | CONF_YAML_VARIABLE,
41 | DEFAULT_EXCLUDE_FROM_RECORDER,
42 | DEFAULT_REPLACE_ATTRIBUTES,
43 | DOMAIN,
44 | )
45 |
46 | _LOGGER = logging.getLogger(__name__)
47 |
48 | PLATFORM = Platform.BINARY_SENSOR
49 | ENTITY_ID_FORMAT = PLATFORM + ".{}"
50 |
51 | SERVICE_UPDATE_VARIABLE = "update_" + PLATFORM
52 | SERVICE_TOGGLE_VARIABLE = "toggle_" + PLATFORM
53 |
54 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({})
55 |
56 | VARIABLE_ATTR_SETTINGS = {
57 | ATTR_FRIENDLY_NAME: "_attr_name",
58 | ATTR_ICON: "_attr_icon",
59 | CONF_DEVICE_CLASS: "_attr_device_class",
60 | }
61 |
62 |
63 | async def async_setup_entry(
64 | hass: HomeAssistant,
65 | config_entry: ConfigEntry,
66 | async_add_entities,
67 | ) -> None:
68 | """Setup the Binary Sensor Variable entity with a config_entry (config_flow)."""
69 |
70 | platform = entity_platform.async_get_current_platform()
71 |
72 | platform.async_register_entity_service(
73 | SERVICE_UPDATE_VARIABLE,
74 | {
75 | vol.Optional(CONF_VALUE): selector.SelectSelector(
76 | selector.SelectSelectorConfig(
77 | options=["None", "true", "false"],
78 | translation_key="boolean_options",
79 | multiple=False,
80 | custom_value=False,
81 | mode=selector.SelectSelectorMode.LIST,
82 | )
83 | ),
84 | vol.Optional(ATTR_ATTRIBUTES): dict,
85 | vol.Optional(
86 | ATTR_REPLACE_ATTRIBUTES, default=DEFAULT_REPLACE_ATTRIBUTES
87 | ): cv.boolean,
88 | },
89 | "async_update_variable",
90 | )
91 |
92 | platform.async_register_entity_service(
93 | SERVICE_TOGGLE_VARIABLE,
94 | {
95 | vol.Optional(ATTR_ATTRIBUTES): dict,
96 | vol.Optional(
97 | ATTR_REPLACE_ATTRIBUTES, default=DEFAULT_REPLACE_ATTRIBUTES
98 | ): cv.boolean,
99 | },
100 | "async_toggle_variable",
101 | )
102 |
103 | config = hass.data.get(DOMAIN).get(config_entry.entry_id)
104 | unique_id = config_entry.entry_id
105 | # _LOGGER.debug(f"[async_setup_entry] config_entry: {config_entry.as_dict()}")
106 | # _LOGGER.debug(f"[async_setup_entry] config: {config}")
107 | # _LOGGER.debug(f"[async_setup_entry] unique_id: {unique_id}")
108 |
109 | if config.get(CONF_EXCLUDE_FROM_RECORDER, DEFAULT_EXCLUDE_FROM_RECORDER):
110 | _LOGGER.debug(
111 | f"({config.get(CONF_NAME, config.get(CONF_VARIABLE_ID, None))}) "
112 | "Excluding from Recorder"
113 | )
114 | async_add_entities([VariableNoRecorder(hass, config, config_entry, unique_id)])
115 | else:
116 | async_add_entities([Variable(hass, config, config_entry, unique_id)])
117 |
118 | return True
119 |
120 |
121 | class Variable(BinarySensorEntity, RestoreEntity):
122 | """Representation of a Binary Sensor Variable."""
123 |
124 | def __init__(
125 | self,
126 | hass,
127 | config,
128 | config_entry,
129 | unique_id,
130 | ):
131 | """Initialize a Binary Sensor Variable."""
132 | # _LOGGER.debug(f"({config.get(CONF_NAME, config.get(CONF_VARIABLE_ID))}) [init] config: {config}")
133 | if config.get(CONF_VALUE) is None or (
134 | isinstance(config.get(CONF_VALUE), str)
135 | and config.get(CONF_VALUE).lower() in ["", "none", "unknown", "unavailable"]
136 | ):
137 | self._attr_is_on = None
138 | elif isinstance(config.get(CONF_VALUE), str):
139 | if config.get(CONF_VALUE).lower() in ["true", "1", "t", "y", "yes", "on"]:
140 | self._attr_is_on = True
141 | else:
142 | self._attr_is_on = False
143 | else:
144 | self._attr_is_on = config.get(CONF_VALUE)
145 | self._hass = hass
146 | self._config = config
147 | self._config_entry = config_entry
148 | self._attr_has_entity_name = True
149 | self._variable_id = slugify(config.get(CONF_VARIABLE_ID).lower())
150 | self._attr_unique_id = unique_id
151 | self._attr_name = config.get(CONF_NAME, config.get(CONF_VARIABLE_ID, None))
152 | self._attr_icon = config.get(CONF_ICON)
153 | self._attr_device_class = config.get(CONF_DEVICE_CLASS)
154 | self._restore = config.get(CONF_RESTORE)
155 | self._force_update = config.get(CONF_FORCE_UPDATE)
156 | self._yaml_variable = config.get(CONF_YAML_VARIABLE)
157 | self._exclude_from_recorder = config.get(CONF_EXCLUDE_FROM_RECORDER)
158 | self._attr_device_info = async_device_info_to_link_from_device_id(
159 | hass,
160 | config.get(CONF_DEVICE_ID),
161 | )
162 | if (
163 | config.get(CONF_ATTRIBUTES) is not None
164 | and config.get(CONF_ATTRIBUTES)
165 | and isinstance(config.get(CONF_ATTRIBUTES), MutableMapping)
166 | ):
167 | self._attr_extra_state_attributes = self._update_attr_settings(
168 | config.get(CONF_ATTRIBUTES)
169 | )
170 | else:
171 | self._attr_extra_state_attributes = None
172 | registry = er.async_get(self._hass)
173 | current_entity_id = registry.async_get_entity_id(
174 | PLATFORM, DOMAIN, self._attr_unique_id
175 | )
176 | if current_entity_id is not None:
177 | self.entity_id = current_entity_id
178 | else:
179 | self.entity_id = generate_entity_id(
180 | ENTITY_ID_FORMAT, self._variable_id, hass=self._hass
181 | )
182 | _LOGGER.debug(f"({self._attr_name}) [init] entity_id: {self.entity_id}")
183 |
184 | async def async_added_to_hass(self):
185 | """Run when entity about to be added."""
186 | await super().async_added_to_hass()
187 | if self._restore is True:
188 | _LOGGER.info(f"({self._attr_name}) Restoring after Reboot")
189 | state = await self.async_get_last_state()
190 | if state:
191 | # _LOGGER.debug(f"({self._attr_name}) Restored last state: {state.as_dict()}")
192 | if (
193 | hasattr(state, "attributes")
194 | and state.attributes
195 | and isinstance(state.attributes, MutableMapping)
196 | ):
197 | self._attr_extra_state_attributes = self._update_attr_settings(
198 | state.attributes.copy(),
199 | just_pop=self._config.get(CONF_UPDATED, False),
200 | )
201 | if hasattr(state, "state"):
202 | if state.state is None or (
203 | isinstance(state.state, str)
204 | and state.state.lower()
205 | in ["", "none", "unknown", "unavailable"]
206 | ):
207 | self._attr_is_on = None
208 | elif state.state == STATE_OFF:
209 | self._attr_is_on = False
210 | elif state.state == STATE_ON:
211 | self._attr_is_on = True
212 | else:
213 | self._attr_is_on = state.state
214 | else:
215 | self._attr_is_on = None
216 | _LOGGER.debug(
217 | f"({self._attr_name}) [restored] _attr_is_on: {self._attr_is_on}"
218 | )
219 | _LOGGER.debug(
220 | f"({self._attr_name}) [restored] attributes: {self._attr_extra_state_attributes}"
221 | )
222 | if self._config.get(CONF_UPDATED, True):
223 | self._config.update({CONF_UPDATED: False})
224 | self._hass.config_entries.async_update_entry(
225 | self._config_entry,
226 | data=self._config,
227 | options={},
228 | )
229 | _LOGGER.debug(
230 | f"({self._attr_name}) Updated config_updated: "
231 | + f"{self._config_entry.data.get(CONF_UPDATED)}"
232 | )
233 |
234 | @property
235 | def should_poll(self):
236 | """If entity should be polled."""
237 | return False
238 |
239 | @property
240 | def force_update(self) -> bool:
241 | """Force update status of the entity."""
242 | return self._force_update
243 |
244 | def _update_attr_settings(self, new_attributes=None, just_pop=False):
245 | if new_attributes is not None:
246 | _LOGGER.debug(
247 | f"({self._attr_name}) [update_attr_settings] Updating Special Attributes"
248 | )
249 | if isinstance(new_attributes, MutableMapping):
250 | attributes = copy.deepcopy(new_attributes)
251 | for attrib, setting in VARIABLE_ATTR_SETTINGS.items():
252 | if attrib in attributes.keys():
253 | if just_pop:
254 | # _LOGGER.debug(f"({self._attr_name}) [update_attr_settings] just_pop / attrib: {attrib} / value: {attributes.get(attrib)}")
255 | attributes.pop(attrib, None)
256 | else:
257 | # _LOGGER.debug(f"({self._attr_name}) [update_attr_settings] attrib: {attrib} / setting: {setting} / value: {attributes.get(attrib)}")
258 | setattr(self, setting, attributes.pop(attrib, None))
259 | return copy.deepcopy(attributes)
260 | else:
261 | _LOGGER.error(
262 | f"({self._attr_name}) AttributeError: Attributes must be a dictionary: {new_attributes}"
263 | )
264 | return new_attributes
265 | else:
266 | return None
267 |
268 | async def async_update_variable(self, **kwargs) -> None:
269 | """Update Binary Sensor Variable."""
270 |
271 | updated_attributes = None
272 |
273 | replace_attributes = kwargs.get(ATTR_REPLACE_ATTRIBUTES, False)
274 | _LOGGER.debug(
275 | f"({self._attr_name}) [async_update_variable] Replace Attributes: {replace_attributes}"
276 | )
277 |
278 | if (
279 | not replace_attributes
280 | and hasattr(self, "_attr_extra_state_attributes")
281 | and self._attr_extra_state_attributes is not None
282 | ):
283 | updated_attributes = copy.deepcopy(self._attr_extra_state_attributes)
284 |
285 | attributes = kwargs.get(ATTR_ATTRIBUTES)
286 | if attributes is not None:
287 | if isinstance(attributes, MutableMapping):
288 | _LOGGER.debug(
289 | f"({self._attr_name}) [async_update_variable] New Attributes: {attributes}"
290 | )
291 | extra_attributes = self._update_attr_settings(attributes)
292 | if updated_attributes is not None:
293 | updated_attributes.update(extra_attributes)
294 | else:
295 | updated_attributes = extra_attributes
296 | else:
297 | _LOGGER.error(
298 | f"({self._attr_name}) AttributeError: Attributes must be a dictionary: {attributes}"
299 | )
300 |
301 | if updated_attributes is not None:
302 | self._attr_extra_state_attributes = copy.deepcopy(updated_attributes)
303 | _LOGGER.debug(
304 | f"({self._attr_name}) [async_update_variable] Final Attributes: {updated_attributes}"
305 | )
306 | else:
307 | self._attr_extra_state_attributes = None
308 |
309 | if ATTR_VALUE in kwargs:
310 | if kwargs.get(ATTR_VALUE) is None or (
311 | isinstance(kwargs.get(ATTR_VALUE), str)
312 | and kwargs.get(ATTR_VALUE).lower()
313 | in ["", "none", "unknown", "unavailable"]
314 | ):
315 | self._attr_is_on = None
316 | elif isinstance(kwargs.get(ATTR_VALUE), str):
317 | if kwargs.get(ATTR_VALUE).lower() in [
318 | "true",
319 | "1",
320 | "t",
321 | "y",
322 | "yes",
323 | "on",
324 | ]:
325 | self._attr_is_on = True
326 | else:
327 | self._attr_is_on = False
328 | else:
329 | self._attr_is_on = kwargs.get(ATTR_VALUE)
330 | _LOGGER.debug(
331 | f"({self._attr_name}) [async_update_variable] New Value: {self._attr_is_on}"
332 | )
333 |
334 | self.async_write_ha_state()
335 |
336 | async def async_toggle_variable(self, **kwargs) -> None:
337 | """Toggle Binary Sensor Variable."""
338 |
339 | updated_attributes = None
340 |
341 | replace_attributes = kwargs.get(ATTR_REPLACE_ATTRIBUTES, False)
342 | _LOGGER.debug(
343 | f"({self._attr_name}) [async_toggle_variable] Replace Attributes: {replace_attributes}"
344 | )
345 |
346 | if (
347 | not replace_attributes
348 | and hasattr(self, "_attr_extra_state_attributes")
349 | and self._attr_extra_state_attributes is not None
350 | ):
351 | updated_attributes = copy.deepcopy(self._attr_extra_state_attributes)
352 |
353 | attributes = kwargs.get(ATTR_ATTRIBUTES)
354 | if attributes is not None:
355 | if isinstance(attributes, MutableMapping):
356 | _LOGGER.debug(
357 | f"({self._attr_name}) [async_toggle_variable] New Attributes: {attributes}"
358 | )
359 | extra_attributes = self._update_attr_settings(attributes)
360 | if updated_attributes is not None:
361 | updated_attributes.update(extra_attributes)
362 | else:
363 | updated_attributes = extra_attributes
364 | else:
365 | _LOGGER.error(
366 | f"({self._attr_name}) AttributeError: Attributes must be a dictionary: {attributes}"
367 | )
368 |
369 | if updated_attributes is not None:
370 | self._attr_extra_state_attributes = copy.deepcopy(updated_attributes)
371 | _LOGGER.debug(
372 | f"({self._attr_name}) [async_toggle_variable] Final Attributes: {updated_attributes}"
373 | )
374 | else:
375 | self._attr_extra_state_attributes = None
376 |
377 | if self._attr_is_on is not None:
378 | self._attr_is_on = not self._attr_is_on
379 | _LOGGER.debug(
380 | f"({self._attr_name}) [async_toggle_variable] New Value: {self._attr_is_on}"
381 | )
382 |
383 | self.async_write_ha_state()
384 |
385 |
386 | class VariableNoRecorder(Variable):
387 | _unrecorded_attributes = frozenset({MATCH_ALL})
388 |
--------------------------------------------------------------------------------
/custom_components/variable/const.py:
--------------------------------------------------------------------------------
1 | from homeassistant.const import Platform
2 |
3 | PLATFORM_NAME = "Variables+History"
4 | DOMAIN = "variable"
5 |
6 | PLATFORMS: list[str] = [
7 | Platform.SENSOR,
8 | Platform.BINARY_SENSOR,
9 | Platform.DEVICE_TRACKER,
10 | ]
11 |
12 | # Defaults
13 | DEFAULT_FORCE_UPDATE = False
14 | DEFAULT_ICON = "mdi:variable"
15 | DEFAULT_REPLACE_ATTRIBUTES = False
16 | DEFAULT_RESTORE = True
17 | DEFAULT_EXCLUDE_FROM_RECORDER = False
18 |
19 | CONF_ATTRIBUTES = "attributes"
20 | CONF_ENTITY_PLATFORM = "entity_platform"
21 | CONF_FORCE_UPDATE = "force_update"
22 | CONF_RESTORE = "restore"
23 | CONF_TZOFFSET = "tz_offset"
24 | CONF_VALUE = "value"
25 | CONF_VALUE_TYPE = "value_type"
26 | CONF_VARIABLE_ID = "variable_id"
27 | CONF_YAML_PRESENT = "yaml_present"
28 | CONF_YAML_VARIABLE = "yaml_variable"
29 | CONF_EXCLUDE_FROM_RECORDER = "exclude_from_recorder"
30 | CONF_UPDATED = "config_updated"
31 | CONF_CLEAR_DEVICE_ID = "clear_device_id"
32 |
33 | ATTR_ATTRIBUTES = "attributes"
34 | ATTR_DELETE_LOCATION_NAME = "delete_location_name"
35 | ATTR_ENTITY = "entity"
36 | ATTR_NATIVE_UNIT_OF_MEASUREMENT = "native_unit_of_measurement"
37 | ATTR_SUGGESTED_UNIT_OF_MEASUREMENT = "suggested_unit_of_measurement"
38 | ATTR_REPLACE_ATTRIBUTES = "replace_attributes"
39 | ATTR_VALUE = "value"
40 | ATTR_VARIABLE = "variable"
41 |
42 | SERVICE_UPDATE_SENSOR = "update_sensor"
43 | SERVICE_UPDATE_BINARY_SENSOR = "update_binary_sensor"
44 | SERVICE_UPDATE_DEVICE_TRACKER = "update_device_tracker"
45 |
--------------------------------------------------------------------------------
/custom_components/variable/device.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from homeassistant.config_entries import ConfigEntry
4 | from homeassistant.const import (
5 | ATTR_CONFIGURATION_URL,
6 | ATTR_HW_VERSION,
7 | ATTR_MANUFACTURER,
8 | ATTR_MODEL,
9 | ATTR_MODEL_ID,
10 | ATTR_SERIAL_NUMBER,
11 | ATTR_SW_VERSION,
12 | CONF_NAME,
13 | )
14 | from homeassistant.core import HomeAssistant
15 | from homeassistant.helpers import device_registry as dr
16 | from homeassistant.helpers import entity_registry as er
17 |
18 | from .const import CONF_YAML_VARIABLE, DOMAIN
19 |
20 | _LOGGER = logging.getLogger(__name__)
21 |
22 |
23 | async def create_device(hass: HomeAssistant, entry: ConfigEntry):
24 | # _LOGGER.debug(f"({entry.title}) [create_device] entry: {entry}")
25 |
26 | device_registry = dr.async_get(hass)
27 | entity_registry = er.async_get(hass)
28 |
29 | device = device_registry.async_get_or_create(
30 | config_entry_id=entry.entry_id,
31 | identifiers={(DOMAIN, entry.entry_id)},
32 | manufacturer=entry.data.get(ATTR_MANUFACTURER),
33 | name=entry.data.get(CONF_NAME),
34 | model=entry.data.get(ATTR_MODEL),
35 | model_id=entry.data.get(ATTR_MODEL_ID),
36 | sw_version=entry.data.get(ATTR_SW_VERSION),
37 | hw_version=entry.data.get(ATTR_HW_VERSION),
38 | serial_number=entry.data.get(ATTR_SERIAL_NUMBER),
39 | configuration_url=entry.data.get(ATTR_CONFIGURATION_URL),
40 | )
41 | _LOGGER.debug(f"({device.name}) [create_device] device: {device}")
42 | device_entities = er.async_entries_for_device(
43 | registry=entity_registry, device_id=device.id, include_disabled_entities=True
44 | )
45 | # _LOGGER.debug(f"({device.name}) [create_device] device entities: {device_entities}")
46 |
47 | domain_entries = hass.config_entries.async_loaded_entries(DOMAIN)
48 | # _LOGGER.debug(f"({device.name}) [create_device] domain_entries: {domain_entries}")
49 | domain_entities = []
50 | for entry in domain_entries:
51 | # _LOGGER.debug(f"({device.name}) [create_device] domain_entry: {entry}")
52 | # _LOGGER.debug(f"({device.name}) [create_device] domain_entry data: {entry.data}")
53 | if not entry.data.get(CONF_YAML_VARIABLE, False):
54 | domain_entities = domain_entities + er.async_entries_for_config_entry(
55 | registry=entity_registry, config_entry_id=entry.entry_id
56 | )
57 | # _LOGGER.debug(f"({device.name}) [create_device] domain entities: {domain_entities}")
58 | domain_reload_entities = []
59 | for entity in domain_entities:
60 | if entity.device_id == device.id:
61 | domain_reload_entities.append(entity)
62 | reload_entities = device_entities + domain_reload_entities
63 | if len(reload_entities) > 0:
64 | _LOGGER.debug(
65 | f"({device.name}) [create_device] Reloading {len(reload_entities)} entities"
66 | )
67 | else:
68 | _LOGGER.debug(
69 | f"({device.name}) [create_device] Reloading all Variable entities"
70 | )
71 | reload_entities = domain_entities
72 |
73 | for entity in reload_entities:
74 | # May actually want to do this for all entities, will see
75 | if entity.platform != DOMAIN:
76 | continue
77 | _LOGGER.debug(
78 | f"({device.name}) [create_device] Reloading entity_id: {entity.entity_id}"
79 | )
80 | hass.config_entries.async_schedule_reload(entity.config_entry_id)
81 |
82 |
83 | async def update_device(hass: HomeAssistant, entry: ConfigEntry, user_input) -> bool:
84 | # _LOGGER.debug(f"({entry.title}) [update_device] entry: {entry}")
85 | # _LOGGER.debug(f"({entry.title}) [update_device] user_input: {user_input}")
86 | device_registry = dr.async_get(hass)
87 | device = device_registry.async_get_device(identifiers={(DOMAIN, entry.entry_id)})
88 | # _LOGGER.debug(f"({device.name}) [update_device] device: {device}")
89 |
90 | device_registry.async_update_device(
91 | device_id=device.id,
92 | manufacturer=user_input.get(ATTR_MANUFACTURER),
93 | model=user_input.get(ATTR_MODEL),
94 | model_id=user_input.get(ATTR_MODEL_ID),
95 | sw_version=user_input.get(ATTR_SW_VERSION),
96 | hw_version=user_input.get(ATTR_HW_VERSION),
97 | serial_number=user_input.get(ATTR_SERIAL_NUMBER),
98 | configuration_url=user_input.get(ATTR_CONFIGURATION_URL),
99 | )
100 | _LOGGER.debug(f"({device.name}) [update_device] updated device: {device}")
101 |
102 |
103 | async def remove_device(hass: HomeAssistant, entry: ConfigEntry) -> bool:
104 | # _LOGGER.debug(f"({entry.title}) [remove_device] entry: {entry}")
105 |
106 | device_registry = dr.async_get(hass)
107 | entity_registry = er.async_get(hass)
108 |
109 | device = device_registry.async_get_device(identifiers={(DOMAIN, entry.entry_id)})
110 | _LOGGER.debug(f"({device.name}) [remove_device] device: {device}")
111 | if not device:
112 | return True
113 | entities = er.async_entries_for_device(
114 | registry=entity_registry, device_id=device.id, include_disabled_entities=True
115 | )
116 | _LOGGER.debug(f"({device.name}) [remove_device] Reloading {len(entities)} entities")
117 | device_registry.async_remove_device(device.id)
118 |
119 | for entity in entities:
120 | # May actually want to do this for all entities, will see
121 | if entity.platform != DOMAIN:
122 | continue
123 | _LOGGER.debug(
124 | f"({device.name}) [remove_device] Reloading entity_id: {entity.entity_id}"
125 | )
126 | hass.config_entries.async_schedule_reload(entity.config_entry_id)
127 |
128 | return True
129 |
--------------------------------------------------------------------------------
/custom_components/variable/device_tracker.py:
--------------------------------------------------------------------------------
1 | import copy
2 | import logging
3 | from collections.abc import MutableMapping
4 | from typing import final
5 |
6 | import homeassistant.helpers.entity_registry as er
7 | import voluptuous as vol
8 | from homeassistant.components.device_tracker import (
9 | ATTR_LOCATION_NAME,
10 | ATTR_SOURCE_TYPE,
11 | PLATFORM_SCHEMA,
12 | SourceType,
13 | TrackerEntity,
14 | )
15 | from homeassistant.config_entries import ConfigEntry
16 | from homeassistant.const import (
17 | ATTR_BATTERY_LEVEL,
18 | ATTR_FRIENDLY_NAME,
19 | ATTR_GPS_ACCURACY,
20 | ATTR_ICON,
21 | ATTR_LATITUDE,
22 | ATTR_LONGITUDE,
23 | CONF_DEVICE_ID,
24 | CONF_ICON,
25 | CONF_NAME,
26 | MATCH_ALL,
27 | Platform,
28 | )
29 | from homeassistant.core import HomeAssistant
30 | from homeassistant.helpers import config_validation as cv
31 | from homeassistant.helpers import entity_platform
32 | from homeassistant.helpers.device import async_device_info_to_link_from_device_id
33 | from homeassistant.helpers.device_registry import DeviceInfo
34 | from homeassistant.helpers.entity import generate_entity_id
35 | from homeassistant.helpers.entity_platform import AddEntitiesCallback
36 | from homeassistant.helpers.restore_state import RestoreEntity
37 | from homeassistant.helpers.typing import StateType
38 | from homeassistant.util import slugify
39 |
40 | from .const import (
41 | ATTR_ATTRIBUTES,
42 | ATTR_DELETE_LOCATION_NAME,
43 | ATTR_REPLACE_ATTRIBUTES,
44 | CONF_ATTRIBUTES,
45 | CONF_EXCLUDE_FROM_RECORDER,
46 | CONF_FORCE_UPDATE,
47 | CONF_RESTORE,
48 | CONF_UPDATED,
49 | CONF_VARIABLE_ID,
50 | CONF_YAML_VARIABLE,
51 | DEFAULT_EXCLUDE_FROM_RECORDER,
52 | DEFAULT_REPLACE_ATTRIBUTES,
53 | DOMAIN,
54 | )
55 |
56 | _LOGGER = logging.getLogger(__name__)
57 |
58 | PLATFORM = Platform.DEVICE_TRACKER
59 | ENTITY_ID_FORMAT = PLATFORM + ".{}"
60 | SERVICE_UPDATE_VARIABLE = "update_" + PLATFORM
61 |
62 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({})
63 |
64 | VARIABLE_ATTR_SETTINGS = {
65 | ATTR_FRIENDLY_NAME: "_attr_name",
66 | ATTR_ICON: "_attr_icon",
67 | ATTR_SOURCE_TYPE: "_attr_source_type",
68 | ATTR_LATITUDE: "_attr_latitude",
69 | ATTR_LONGITUDE: "_attr_longitude",
70 | ATTR_BATTERY_LEVEL: "_attr_battery_level",
71 | ATTR_LOCATION_NAME: "_attr_location_name",
72 | ATTR_GPS_ACCURACY: "_attr_gps_accuracy",
73 | }
74 |
75 |
76 | async def async_setup_entry(
77 | hass: HomeAssistant,
78 | config_entry: ConfigEntry,
79 | async_add_entities: AddEntitiesCallback,
80 | ) -> None:
81 | """Setup the Device Tracker Variable entity with a config_entry (config_flow)."""
82 |
83 | platform = entity_platform.async_get_current_platform()
84 |
85 | platform.async_register_entity_service(
86 | SERVICE_UPDATE_VARIABLE,
87 | {
88 | vol.Optional(ATTR_LATITUDE): cv.latitude,
89 | vol.Optional(ATTR_LONGITUDE): cv.longitude,
90 | vol.Optional(ATTR_LOCATION_NAME): cv.string,
91 | vol.Optional(ATTR_DELETE_LOCATION_NAME): cv.boolean,
92 | vol.Optional(ATTR_GPS_ACCURACY): cv.positive_int,
93 | vol.Optional(ATTR_BATTERY_LEVEL): vol.All(
94 | vol.Coerce(int), vol.Range(min=0, max=100)
95 | ),
96 | vol.Optional(ATTR_ATTRIBUTES): dict,
97 | vol.Optional(
98 | ATTR_REPLACE_ATTRIBUTES, default=DEFAULT_REPLACE_ATTRIBUTES
99 | ): cv.boolean,
100 | },
101 | "async_update_variable",
102 | )
103 |
104 | config = hass.data.get(DOMAIN).get(config_entry.entry_id)
105 | unique_id = config_entry.entry_id
106 | # _LOGGER.debug(f"[async_setup_entry] config_entry: {config_entry.as_dict()}")
107 | # _LOGGER.debug(f"[async_setup_entry] config: {config}")
108 | # _LOGGER.debug(f"[async_setup_entry] unique_id: {unique_id}")
109 |
110 | if config.get(CONF_EXCLUDE_FROM_RECORDER, DEFAULT_EXCLUDE_FROM_RECORDER):
111 | _LOGGER.debug(
112 | f"({config.get(CONF_NAME, config.get(CONF_VARIABLE_ID, None))}) "
113 | "Excluding from Recorder."
114 | )
115 | async_add_entities([VariableNoRecorder(hass, config, config_entry, unique_id)])
116 | else:
117 | async_add_entities([Variable(hass, config, config_entry, unique_id)])
118 |
119 | return True
120 |
121 |
122 | class Variable(RestoreEntity, TrackerEntity):
123 | """Class for the device tracker."""
124 |
125 | def __init__(
126 | self,
127 | hass,
128 | config,
129 | config_entry,
130 | unique_id,
131 | ):
132 | """Initialize a Device Tracker Variable."""
133 | # _LOGGER.debug(f"({config.get(CONF_NAME, config.get(CONF_VARIABLE_ID))}) [init] config: {config}")
134 | self._hass = hass
135 | self._config = config
136 | self._config_entry = config_entry
137 | self._attr_has_entity_name = True
138 | self._variable_id = slugify(config.get(CONF_VARIABLE_ID).lower())
139 | self._attr_unique_id = unique_id
140 | self._attr_name = config.get(CONF_NAME, config.get(CONF_VARIABLE_ID, None))
141 | self._attr_icon = config.get(CONF_ICON)
142 | self._restore = config.get(CONF_RESTORE)
143 | self._force_update = config.get(CONF_FORCE_UPDATE)
144 | self._yaml_variable = config.get(CONF_YAML_VARIABLE)
145 | self._exclude_from_recorder = config.get(CONF_EXCLUDE_FROM_RECORDER)
146 | self._attr_device_info = async_device_info_to_link_from_device_id(
147 | hass,
148 | config.get(CONF_DEVICE_ID),
149 | )
150 | if (
151 | config.get(CONF_ATTRIBUTES) is not None
152 | and config.get(CONF_ATTRIBUTES)
153 | and isinstance(config.get(CONF_ATTRIBUTES), MutableMapping)
154 | ):
155 | self._attr_extra_state_attributes = self._update_attr_settings(
156 | config.get(CONF_ATTRIBUTES)
157 | )
158 | else:
159 | self._attr_extra_state_attributes = None
160 | registry = er.async_get(self._hass)
161 | current_entity_id = registry.async_get_entity_id(
162 | PLATFORM, DOMAIN, self._attr_unique_id
163 | )
164 | if current_entity_id is not None:
165 | self.entity_id = current_entity_id
166 | else:
167 | self.entity_id = generate_entity_id(
168 | ENTITY_ID_FORMAT, self._variable_id, hass=self._hass
169 | )
170 | _LOGGER.debug(f"({self._attr_name}) [init] entity_id: {self.entity_id}")
171 | self._attr_source_type = config.get(ATTR_SOURCE_TYPE, SourceType.GPS)
172 | self._attr_latitude = config.get(ATTR_LATITUDE)
173 | self._attr_longitude = config.get(ATTR_LONGITUDE)
174 | self._attr_battery_level = config.get(ATTR_BATTERY_LEVEL)
175 | self._attr_location_name = config.get(ATTR_LOCATION_NAME)
176 | self._attr_gps_accuracy = config.get(ATTR_GPS_ACCURACY)
177 |
178 | async def async_added_to_hass(self):
179 | """Run when entity about to be added."""
180 | await super().async_added_to_hass()
181 | if self._restore is True:
182 | _LOGGER.info(f"({self._attr_name}) Restoring after Reboot")
183 | state = await self.async_get_last_state()
184 | if state:
185 | _LOGGER.debug(
186 | f"({self._attr_name}) Restored last state: {state.as_dict()}"
187 | )
188 | if (
189 | hasattr(state, "attributes")
190 | and state.attributes
191 | and isinstance(state.attributes, MutableMapping)
192 | ):
193 | self._attr_extra_state_attributes = self._update_attr_settings(
194 | state.attributes.copy(),
195 | just_pop=self._config.get(CONF_UPDATED, False),
196 | )
197 | _LOGGER.debug(
198 | f"({self._attr_name}) [restored] attributes: {self._attr_extra_state_attributes}"
199 | )
200 | if self._config.get(CONF_UPDATED, True):
201 | self._config.update({CONF_UPDATED: False})
202 | self._hass.config_entries.async_update_entry(
203 | self._config_entry,
204 | data=self._config,
205 | options={},
206 | )
207 | _LOGGER.debug(
208 | f"({self._attr_name}) Updated config_updated: "
209 | + f"{self._config_entry.data.get(CONF_UPDATED)}"
210 | )
211 |
212 | def _update_attr_settings(self, new_attributes=None, just_pop=False):
213 | if new_attributes is not None:
214 | _LOGGER.debug(
215 | f"({self._attr_name}) [update_attr_settings] Updating Special Attributes"
216 | )
217 | if isinstance(new_attributes, MutableMapping):
218 | attributes = copy.deepcopy(new_attributes)
219 | for attrib, setting in VARIABLE_ATTR_SETTINGS.items():
220 | if attrib in attributes.keys():
221 | if just_pop:
222 | # _LOGGER.debug(f"({self._attr_name}) [update_attr_settings] just_pop / attrib: {attrib} / value: {attributes.get(attrib)}")
223 | attributes.pop(attrib, None)
224 | else:
225 | # _LOGGER.debug(f"({self._attr_name}) [update_attr_settings] attrib: {attrib} / setting: {setting} / value: {attributes.get(attrib)}")
226 | setattr(self, setting, attributes.pop(attrib, None))
227 | return copy.deepcopy(attributes)
228 | else:
229 | _LOGGER.error(
230 | f"({self._attr_name}) AttributeError: Attributes must be a dictionary: {new_attributes}"
231 | )
232 | return new_attributes
233 | else:
234 | return None
235 |
236 | async def async_update_variable(self, **kwargs) -> None:
237 | """Update Device Tracker Variable."""
238 |
239 | updated_attributes = None
240 |
241 | replace_attributes = kwargs.get(ATTR_REPLACE_ATTRIBUTES, False)
242 | _LOGGER.debug(
243 | f"({self._attr_name}) [async_update_variable] Replace Attributes: {replace_attributes}"
244 | )
245 |
246 | if (
247 | not replace_attributes
248 | and hasattr(self, "_attr_extra_state_attributes")
249 | and self._attr_extra_state_attributes is not None
250 | ):
251 | updated_attributes = copy.deepcopy(self._attr_extra_state_attributes)
252 |
253 | attributes = kwargs.get(ATTR_ATTRIBUTES)
254 | if attributes is not None:
255 | if isinstance(attributes, MutableMapping):
256 | _LOGGER.debug(
257 | f"({self._attr_name}) [async_update_variable] New Attributes: {attributes}"
258 | )
259 | extra_attributes = self._update_attr_settings(attributes)
260 | if updated_attributes is not None:
261 | updated_attributes.update(extra_attributes)
262 | else:
263 | updated_attributes = extra_attributes
264 | else:
265 | _LOGGER.error(
266 | f"({self._attr_name}) AttributeError: Attributes must be a dictionary: {attributes}"
267 | )
268 |
269 | if updated_attributes is not None:
270 | self._attr_extra_state_attributes = copy.deepcopy(updated_attributes)
271 | _LOGGER.debug(
272 | f"({self._attr_name}) [async_update_variable] Final Attributes: {updated_attributes}"
273 | )
274 | else:
275 | self._attr_extra_state_attributes = None
276 |
277 | if ATTR_LATITUDE in kwargs:
278 | self._attr_latitude = kwargs.get(ATTR_LATITUDE)
279 | if ATTR_LONGITUDE in kwargs:
280 | self._attr_longitude = kwargs.get(ATTR_LONGITUDE)
281 | if ATTR_LOCATION_NAME in kwargs:
282 | self._attr_location_name = kwargs.get(ATTR_LOCATION_NAME)
283 | if ATTR_BATTERY_LEVEL in kwargs:
284 | self._attr_battery_level = kwargs.get(ATTR_BATTERY_LEVEL)
285 | if ATTR_GPS_ACCURACY in kwargs:
286 | self._attr_gps_accuracy = kwargs.get(ATTR_GPS_ACCURACY)
287 | if (
288 | ATTR_DELETE_LOCATION_NAME in kwargs
289 | and kwargs.get(ATTR_DELETE_LOCATION_NAME) is True
290 | ):
291 | self._attr_location_name = None
292 | self.async_write_ha_state()
293 |
294 | @property
295 | def should_poll(self):
296 | """If entity should be polled."""
297 | return False
298 |
299 | @property
300 | def force_update(self) -> bool:
301 | """Force update status of the entity."""
302 | return self._force_update
303 |
304 | @property
305 | def source_type(self) -> SourceType:
306 | """Return the source type, e.g. gps or router, of the device."""
307 | return self._attr_source_type
308 |
309 | @property
310 | def latitude(self):
311 | """Return latitude value of the device."""
312 | return self._attr_latitude
313 |
314 | @property
315 | def longitude(self):
316 | """Return longitude value of the device."""
317 | return self._attr_longitude
318 |
319 | @property
320 | def location_accuracy(self) -> int:
321 | """Return the location accuracy of the device.
322 |
323 | Value in meters.
324 | """
325 | return self._attr_gps_accuracy if self._attr_gps_accuracy is not None else 0
326 |
327 | @property
328 | def location_name(self) -> str | None:
329 | """Return a location name for the current location of the device."""
330 | return self._attr_location_name
331 |
332 | @final
333 | @property
334 | def state_attributes(self) -> dict[str, StateType]:
335 | """Return the device state attributes."""
336 | attr: dict[str, StateType] = {}
337 | attr.update(super().state_attributes)
338 | if self._attr_extra_state_attributes is not None:
339 | attr.update(self._attr_extra_state_attributes)
340 | if self._attr_source_type is not None:
341 | attr[ATTR_SOURCE_TYPE] = self._attr_source_type
342 | if self._attr_latitude is not None and self._attr_longitude is not None:
343 | attr[ATTR_LATITUDE] = self._attr_latitude
344 | attr[ATTR_LONGITUDE] = self._attr_longitude
345 | if self._attr_gps_accuracy is not None:
346 | attr[ATTR_GPS_ACCURACY] = self._attr_gps_accuracy
347 | if self._attr_battery_level is not None:
348 | attr[ATTR_BATTERY_LEVEL] = self._attr_battery_level
349 | if self._attr_location_name is not None:
350 | attr[ATTR_LOCATION_NAME] = self._attr_location_name
351 | return attr
352 |
353 | @property
354 | def device_info(self) -> DeviceInfo:
355 | """Return device info."""
356 | return self._attr_device_info
357 |
358 |
359 | class VariableNoRecorder(Variable):
360 | _unrecorded_attributes = frozenset({MATCH_ALL})
361 |
--------------------------------------------------------------------------------
/custom_components/variable/helpers.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import datetime
4 | import logging
5 |
6 | import homeassistant.util.dt as dt_util
7 |
8 | _LOGGER = logging.getLogger(__name__)
9 |
10 |
11 | def to_num(s):
12 | try:
13 | return int(s)
14 | except ValueError:
15 | try:
16 | return float(s)
17 | except ValueError:
18 | return None
19 |
20 |
21 | def value_to_type(init_val, dest_type): # noqa: C901
22 | if init_val is None or (
23 | isinstance(init_val, str)
24 | and init_val.lower() in ["", "none", "unknown", "unavailable"]
25 | ):
26 | _LOGGER.debug(f"[value_to_type] return value: {init_val}, returning None")
27 | return None
28 |
29 | # _LOGGER.debug(f"[value_to_type] initial value: {init_val}, initial type: {type(init_val)}, dest type: {dest_type}")
30 | if isinstance(init_val, str):
31 | # _LOGGER.debug("[value_to_type] Processing as string")
32 | if dest_type is None or dest_type == "string":
33 | _LOGGER.debug(
34 | f"[value_to_type] return value: {init_val}, type: {type(init_val)}"
35 | )
36 | return init_val
37 |
38 | elif dest_type == "date":
39 | try:
40 | value_date = datetime.date.fromisoformat(init_val)
41 | except ValueError:
42 | _LOGGER.debug(
43 | f"Cannot convert string to {dest_type}: {init_val}, returning None"
44 | )
45 | raise ValueError(f"Cannot convert string to {dest_type}: {init_val}")
46 | return None
47 | else:
48 | _LOGGER.debug(
49 | f"[value_to_type] return value: {value_date}, type: {type(value_date)}"
50 | )
51 | return value_date
52 |
53 | elif dest_type == "datetime":
54 | try:
55 | value_datetime = datetime.datetime.fromisoformat(init_val)
56 | except ValueError:
57 | _LOGGER.debug(
58 | f"Cannot convert string to {dest_type}: {init_val}, returning None"
59 | )
60 | raise ValueError(f"Cannot convert string to {dest_type}: {init_val}")
61 | return None
62 | else:
63 | _LOGGER.debug(
64 | f"[value_to_type] return value: {value_datetime}, type: {type(value_datetime)}"
65 | )
66 | if (
67 | value_datetime.tzinfo is None
68 | or value_datetime.tzinfo.utcoffset(value_datetime) is None
69 | ):
70 | return value_datetime.replace(tzinfo=dt_util.UTC)
71 | return value_datetime
72 |
73 | elif dest_type == "number":
74 | if (value_num := to_num(init_val)) is not None:
75 | _LOGGER.debug(
76 | f"[value_to_type] return value: {value_num}, type: {type(value_num)}"
77 | )
78 | return value_num
79 | else:
80 | _LOGGER.debug(
81 | f"Cannot convert string to {dest_type}: {init_val}, returning None"
82 | )
83 | raise ValueError(f"Cannot convert string to {dest_type}: {init_val}")
84 | else:
85 | _LOGGER.debug(f"Invalid dest_type: {dest_type}, returning None")
86 | raise ValueError(f"Invalid dest_type: {dest_type}")
87 | return None
88 | elif isinstance(init_val, int) or isinstance(init_val, float):
89 | # _LOGGER.debug("[value_to_type] Processing as number")
90 | if dest_type is None or dest_type == "string":
91 | _LOGGER.debug(
92 | f"[value_to_type] return value: {str(init_val)}, type: {type(str(init_val))}"
93 | )
94 | return str(init_val)
95 | elif dest_type == "date":
96 | try:
97 | value_date = datetime.date.fromisoformat(str(init_val))
98 | except ValueError:
99 | _LOGGER.debug(
100 | f"Cannot convert number to {dest_type}: {init_val}, returning None"
101 | )
102 | raise ValueError(f"Cannot convert number to {dest_type}: {init_val}")
103 | return None
104 | else:
105 | _LOGGER.debug(
106 | f"[value_to_type] return value: {value_date}, type: {type(value_date)}"
107 | )
108 | return value_date
109 |
110 | elif dest_type == "datetime":
111 | try:
112 | value_datetime = datetime.datetime.fromisoformat(str(init_val))
113 | except ValueError:
114 | _LOGGER.debug(
115 | f"Cannot convert number to {dest_type}: {init_val}, returning None"
116 | )
117 | raise ValueError(f"Cannot convert number to {dest_type}: {init_val}")
118 | return None
119 | else:
120 | _LOGGER.debug(
121 | f"[value_to_type] return value: {value_datetime}, type: {type(value_datetime)}"
122 | )
123 | if (
124 | value_datetime.tzinfo is None
125 | or value_datetime.tzinfo.utcoffset(value_datetime) is None
126 | ):
127 | return value_datetime.replace(tzinfo=dt_util.UTC)
128 | return value_datetime
129 | elif dest_type == "number":
130 | _LOGGER.debug(
131 | f"[value_to_type] return value: {init_val}, type: {type(init_val)}"
132 | )
133 | return init_val
134 | else:
135 | _LOGGER.debug(f"Invalid dest_type: {dest_type}, returning None")
136 | raise ValueError(f"Invalid dest_type: {dest_type}")
137 | return None
138 | elif isinstance(init_val, datetime.date) and type(init_val) is datetime.date:
139 | # _LOGGER.debug("[value_to_type] Processing as date")
140 | if dest_type is None or dest_type == "string":
141 | _LOGGER.debug(
142 | f"[value_to_type] return value: {init_val.isoformat()}, type: {type(init_val.isoformat())}"
143 | )
144 | return init_val.isoformat()
145 | elif dest_type == "date":
146 | _LOGGER.debug(
147 | f"[value_to_type] return value: {init_val}, type: {type(init_val)}"
148 | )
149 | return init_val
150 | elif dest_type == "datetime":
151 | _LOGGER.debug(
152 | f"[value_to_type] return value: {datetime.datetime.combine(init_val, datetime.time.min)}, "
153 | + f"type: {type(datetime.datetime.combine(init_val, datetime.time.min))}"
154 | )
155 | return datetime.datetime.combine(init_val, datetime.time.min)
156 | elif dest_type == "number":
157 | _LOGGER.debug(
158 | f"[value_to_type] return value: {datetime.datetime.combine(init_val, datetime.time.min).timestamp()}, "
159 | + f"type: {type(datetime.datetime.combine(init_val, datetime.time.min).timestamp())}"
160 | )
161 | return datetime.datetime.combine(init_val, datetime.time.min).timestamp()
162 | else:
163 | _LOGGER.debug(f"Invalid dest_type: {dest_type}, returning None")
164 | raise ValueError(f"Invalid dest_type: {dest_type}")
165 | return None
166 | elif (
167 | isinstance(init_val, datetime.datetime) and type(init_val) is datetime.datetime
168 | ):
169 | # _LOGGER.debug("[value_to_type] Processing as datetime")
170 | if dest_type is None or dest_type == "string":
171 | _LOGGER.debug(
172 | f"[value_to_type] return value: {init_val.isoformat()}, type: {type(init_val.isoformat())}"
173 | )
174 | return init_val.isoformat()
175 | elif dest_type == "date":
176 | _LOGGER.debug(
177 | f"[value_to_type] return value: {init_val.date()}, type: {type(init_val.date())}"
178 | )
179 | return init_val.date()
180 | elif dest_type == "datetime":
181 | _LOGGER.debug(
182 | f"[value_to_type] return value: {init_val}, type: {type(init_val)}"
183 | )
184 | return init_val
185 | elif dest_type == "number":
186 | _LOGGER.debug(
187 | f"[value_to_type] return value: {init_val.timestamp()}, type: {type(init_val.timestamp())}"
188 | )
189 | return init_val.timestamp()
190 | else:
191 | _LOGGER.debug(f"Invalid dest_type: {dest_type}, returning None")
192 | raise ValueError(f"Invalid dest_type: {dest_type}")
193 | return None
194 | else:
195 | _LOGGER.debug(f"Invalid initial type: {type(init_val)}, returning None")
196 | raise ValueError(f"Invalid initial type: {type(init_val)}")
197 | return None
198 |
--------------------------------------------------------------------------------
/custom_components/variable/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "domain": "variable",
3 | "name": "Variables+History",
4 | "after_dependencies": ["recorder"],
5 | "codeowners": [
6 | "@rogro82",
7 | "@enkama",
8 | "@Snuffy2"
9 | ],
10 | "config_flow": true,
11 | "dependencies": [],
12 | "documentation": "https://github.com/enkama/hass-variables",
13 | "iot_class": "local_push",
14 | "issue_tracker": "https://github.com/enkama/hass-variables/issues",
15 | "requirements": ["iso4217>=1.11.20220401"],
16 | "version": "3.4.9"
17 | }
18 |
--------------------------------------------------------------------------------
/custom_components/variable/sensor.py:
--------------------------------------------------------------------------------
1 | import copy
2 | import logging
3 | from collections.abc import MutableMapping
4 |
5 | import homeassistant.helpers.entity_registry as er
6 | import voluptuous as vol
7 | from homeassistant.components.sensor import (
8 | CONF_STATE_CLASS,
9 | PLATFORM_SCHEMA,
10 | UNIT_CONVERTERS,
11 | RestoreSensor,
12 | )
13 | from homeassistant.config_entries import ConfigEntry
14 | from homeassistant.const import (
15 | ATTR_FRIENDLY_NAME,
16 | ATTR_ICON,
17 | CONF_DEVICE_CLASS,
18 | CONF_DEVICE_ID,
19 | CONF_ICON,
20 | CONF_NAME,
21 | CONF_UNIT_OF_MEASUREMENT,
22 | MATCH_ALL,
23 | Platform,
24 | )
25 | from homeassistant.core import HomeAssistant
26 | from homeassistant.helpers import (
27 | config_validation as cv,
28 | )
29 | from homeassistant.helpers import (
30 | device_registry as dr,
31 | )
32 | from homeassistant.helpers import (
33 | entity_platform,
34 | )
35 | from homeassistant.helpers.device import async_device_info_to_link_from_device_id
36 | from homeassistant.helpers.entity import generate_entity_id
37 | from homeassistant.util import slugify
38 |
39 | from .const import (
40 | ATTR_ATTRIBUTES,
41 | ATTR_NATIVE_UNIT_OF_MEASUREMENT,
42 | ATTR_REPLACE_ATTRIBUTES,
43 | ATTR_SUGGESTED_UNIT_OF_MEASUREMENT,
44 | ATTR_VALUE,
45 | CONF_ATTRIBUTES,
46 | CONF_EXCLUDE_FROM_RECORDER,
47 | CONF_FORCE_UPDATE,
48 | CONF_RESTORE,
49 | CONF_UPDATED,
50 | CONF_VALUE,
51 | CONF_VALUE_TYPE,
52 | CONF_VARIABLE_ID,
53 | CONF_YAML_VARIABLE,
54 | DEFAULT_EXCLUDE_FROM_RECORDER,
55 | DEFAULT_FORCE_UPDATE,
56 | DEFAULT_ICON,
57 | DEFAULT_REPLACE_ATTRIBUTES,
58 | DEFAULT_RESTORE,
59 | DOMAIN,
60 | )
61 | from .helpers import value_to_type
62 |
63 | _LOGGER = logging.getLogger(__name__)
64 |
65 | PLATFORM = Platform.SENSOR
66 | ENTITY_ID_FORMAT = PLATFORM + ".{}"
67 |
68 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
69 | {
70 | vol.Required(CONF_VARIABLE_ID): cv.string,
71 | vol.Optional(CONF_NAME): cv.string,
72 | vol.Optional(CONF_ICON, default=DEFAULT_ICON): cv.string,
73 | vol.Optional(CONF_VALUE): cv.match_all,
74 | vol.Optional(CONF_ATTRIBUTES): dict,
75 | vol.Optional(CONF_RESTORE, default=DEFAULT_RESTORE): cv.boolean,
76 | vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
77 | vol.Optional(
78 | CONF_EXCLUDE_FROM_RECORDER, default=DEFAULT_EXCLUDE_FROM_RECORDER
79 | ): cv.boolean,
80 | }
81 | )
82 |
83 | SERVICE_UPDATE_VARIABLE = "update_" + PLATFORM
84 |
85 | VARIABLE_ATTR_SETTINGS = {
86 | ATTR_FRIENDLY_NAME: "_attr_name",
87 | ATTR_ICON: "_attr_icon",
88 | CONF_DEVICE_CLASS: "_attr_device_class",
89 | CONF_STATE_CLASS: "_attr_state_class",
90 | ATTR_NATIVE_UNIT_OF_MEASUREMENT: "_attr_native_unit_of_measurement",
91 | ATTR_SUGGESTED_UNIT_OF_MEASUREMENT: "_attr_suggested_unit_of_measurement",
92 | }
93 |
94 |
95 | async def async_setup_entry(
96 | hass: HomeAssistant,
97 | config_entry: ConfigEntry,
98 | async_add_entities,
99 | ) -> None:
100 | """Setup the Sensor Variable entity with a config_entry (config_flow)."""
101 |
102 | platform = entity_platform.async_get_current_platform()
103 |
104 | platform.async_register_entity_service(
105 | SERVICE_UPDATE_VARIABLE,
106 | {
107 | vol.Optional(ATTR_VALUE): cv.string,
108 | vol.Optional(ATTR_ATTRIBUTES): dict,
109 | vol.Optional(
110 | ATTR_REPLACE_ATTRIBUTES, default=DEFAULT_REPLACE_ATTRIBUTES
111 | ): cv.boolean,
112 | },
113 | "async_update_variable",
114 | )
115 |
116 | config = hass.data.get(DOMAIN).get(config_entry.entry_id)
117 | unique_id = config_entry.entry_id
118 |
119 | if config.get(CONF_EXCLUDE_FROM_RECORDER, DEFAULT_EXCLUDE_FROM_RECORDER):
120 | _LOGGER.debug(
121 | f"({config.get(CONF_NAME, config.get(CONF_VARIABLE_ID, None))}) "
122 | "Excluding from Recorder"
123 | )
124 | async_add_entities([VariableNoRecorder(hass, config, config_entry, unique_id)])
125 | else:
126 | async_add_entities([Variable(hass, config, config_entry, unique_id)])
127 |
128 | return True
129 |
130 |
131 | class Variable(RestoreSensor):
132 | """Representation of a Sensor Variable."""
133 |
134 | def __init__(
135 | self,
136 | hass,
137 | config,
138 | config_entry,
139 | unique_id,
140 | ):
141 | """Initialize a Sensor Variable."""
142 | # _LOGGER.debug(f"({config.get(CONF_NAME, config.get(CONF_VARIABLE_ID))}) [init] config: {config}")
143 | self._hass = hass
144 | self._config = config
145 | self._config_entry = config_entry
146 | self._attr_has_entity_name = True
147 | self._variable_id = slugify(config.get(CONF_VARIABLE_ID).lower())
148 | self._attr_unique_id = unique_id
149 | self._attr_name = config.get(CONF_NAME, config.get(CONF_VARIABLE_ID, None))
150 | registry = er.async_get(self._hass)
151 | current_entity_id = registry.async_get_entity_id(
152 | PLATFORM, DOMAIN, self._attr_unique_id
153 | )
154 | if current_entity_id is not None:
155 | self.entity_id = current_entity_id
156 | else:
157 | self.entity_id = generate_entity_id(
158 | ENTITY_ID_FORMAT, self._variable_id, hass=self._hass
159 | )
160 | _LOGGER.debug(f"({self._attr_name}) [init] entity_id: {self.entity_id}")
161 |
162 | self._attr_icon = config.get(CONF_ICON)
163 | self._restore = config.get(CONF_RESTORE)
164 | self._force_update = config.get(CONF_FORCE_UPDATE)
165 | self._yaml_variable = config.get(CONF_YAML_VARIABLE)
166 | self._exclude_from_recorder = config.get(CONF_EXCLUDE_FROM_RECORDER)
167 | self._value_type = config.get(CONF_VALUE_TYPE)
168 | self._attr_device_class = config.get(CONF_DEVICE_CLASS)
169 | self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
170 | self._attr_suggested_unit_of_measurement = None
171 | self._attr_state_class = config.get(CONF_STATE_CLASS)
172 | self._attr_device_info = async_device_info_to_link_from_device_id(
173 | hass,
174 | config.get(CONF_DEVICE_ID),
175 | )
176 | # _LOGGER.debug(f"({self._attr_name}) [init] device_id: {config.get(CONF_DEVICE_ID)}, device_info: {self.device_info}")
177 | if (
178 | config.get(CONF_ATTRIBUTES) is not None
179 | and config.get(CONF_ATTRIBUTES)
180 | and isinstance(config.get(CONF_ATTRIBUTES), MutableMapping)
181 | ):
182 | self._attr_extra_state_attributes = self._update_attr_settings(
183 | config.get(CONF_ATTRIBUTES)
184 | )
185 | else:
186 | self._attr_extra_state_attributes = None
187 | if config.get(CONF_VALUE) is None or (
188 | isinstance(config.get(CONF_VALUE), str)
189 | and config.get(CONF_VALUE).lower() in ["", "none", "unknown", "unavailable"]
190 | ):
191 | self._attr_native_value = None
192 | else:
193 | try:
194 | self._attr_native_value = value_to_type(
195 | config.get(CONF_VALUE), self._value_type
196 | )
197 | except ValueError:
198 | self._attr_native_value = None
199 | if config.get(CONF_DEVICE_CLASS) in UNIT_CONVERTERS:
200 | self._attr_suggested_unit_of_measurement = config.get(
201 | CONF_UNIT_OF_MEASUREMENT
202 | )
203 |
204 | # _LOGGER.debug(f"({self._attr_name}) [init] unrecorded_attributes: {self._unrecorded_attributes}")
205 |
206 | async def async_added_to_hass(self):
207 | """Run when entity about to be added."""
208 | await super().async_added_to_hass()
209 | if self._restore is True:
210 | _LOGGER.info(f"({self._attr_name}) Restoring after Reboot")
211 | sensor = await self.async_get_last_sensor_data()
212 | if sensor and hasattr(sensor, "native_value"):
213 | # _LOGGER.debug(f"({self._attr_name}) Restored last sensor data: {sensor.as_dict()}")
214 | if sensor.native_value is None or (
215 | isinstance(sensor.native_value, str)
216 | and sensor.native_value.lower()
217 | in [
218 | "",
219 | "none",
220 | "unknown",
221 | "unavailable",
222 | ]
223 | ):
224 | self._attr_native_value = None
225 | else:
226 | try:
227 | self._attr_native_value = value_to_type(
228 | sensor.native_value, self._value_type
229 | )
230 | except ValueError:
231 | self._attr_native_value = None
232 |
233 | state = await self.async_get_last_state()
234 | if state:
235 | # _LOGGER.debug(f"({self._attr_name}) Restored last state: {state.as_dict()}")
236 | if (
237 | hasattr(state, CONF_ATTRIBUTES)
238 | and state.attributes
239 | and isinstance(state.attributes, MutableMapping)
240 | ):
241 | self._attr_extra_state_attributes = self._update_attr_settings(
242 | state.attributes.copy(),
243 | just_pop=self._config.get(CONF_UPDATED, False),
244 | )
245 | if self._config.get(CONF_UPDATED, True):
246 | self._attr_extra_state_attributes.pop(
247 | CONF_UNIT_OF_MEASUREMENT, None
248 | )
249 | if self._attr_device_info:
250 | device_registry = dr.async_get(self._hass)
251 | device = device_registry.async_get_device(
252 | identifiers=self._attr_device_info.get(
253 | "identifiers",
254 | )
255 | )
256 | # _LOGGER.debug(f"({self._attr_name}) [restored] device: {device}")
257 | if (
258 | hasattr(device, "name")
259 | and isinstance(device.name, str)
260 | and self._attr_name.lower().strip()
261 | != device.name.lower().strip()
262 | and self._attr_name.lower().startswith(device.name.lower())
263 | ):
264 | old_name = self._attr_name
265 | self._attr_name = self._attr_name.replace(
266 | device.name, "", 1
267 | ).strip()
268 | _LOGGER.debug(
269 | f"({self._attr_name}) [restored] Truncated: {old_name}"
270 | )
271 | elif (
272 | hasattr(device, "name_by_user")
273 | and isinstance(device.name_by_user, str)
274 | and self._attr_name.lower().strip()
275 | != device.name_by_user.lower().strip()
276 | and self._attr_name.lower().startswith(
277 | device.name_by_user.lower()
278 | )
279 | ):
280 | old_name = self._attr_name
281 | self._attr_name = self._attr_name.replace(
282 | device.name_by_user, "", 1
283 | ).strip()
284 | _LOGGER.debug(
285 | f"({self._attr_name}) [restored] Truncated: {old_name}"
286 | )
287 | _LOGGER.debug(
288 | f"({self._attr_name}) [restored] _attr_native_value: {self._attr_native_value}"
289 | )
290 | _LOGGER.debug(
291 | f"({self._attr_name}) [restored] attributes: {self._attr_extra_state_attributes}"
292 | )
293 | if self._config.get(CONF_UPDATED, True):
294 | self._config.update({CONF_UPDATED: False})
295 | self._hass.config_entries.async_update_entry(
296 | self._config_entry,
297 | data=self._config,
298 | options={},
299 | )
300 | _LOGGER.debug(
301 | f"({self._attr_name}) Updated config_updated: "
302 | + f"{self._config_entry.data.get(CONF_UPDATED)}"
303 | )
304 |
305 | @property
306 | def should_poll(self):
307 | """If entity should be polled."""
308 | return False
309 |
310 | @property
311 | def force_update(self) -> bool:
312 | """Force update status of the entity."""
313 | return self._force_update
314 |
315 | def _update_attr_settings(self, new_attributes=None, just_pop=False):
316 | if new_attributes is not None:
317 | _LOGGER.debug(
318 | f"({self._attr_name}) [update_attr_settings] Updating Special Attributes"
319 | )
320 | if isinstance(new_attributes, MutableMapping):
321 | attributes = copy.deepcopy(new_attributes)
322 | for attrib, setting in VARIABLE_ATTR_SETTINGS.items():
323 | if attrib in attributes.keys():
324 | if just_pop:
325 | # _LOGGER.debug(f"({self._attr_name}) [update_attr_settings] just_pop / attrib: {attrib} / value: {attributes.get(attrib)}")
326 | attributes.pop(attrib, None)
327 | else:
328 | # _LOGGER.debug(f"({self._attr_name}) [update_attr_settings] attrib: {attrib} / setting: {setting} / value: {attributes.get(attrib)}")
329 | setattr(self, setting, attributes.pop(attrib, None))
330 | return copy.deepcopy(attributes)
331 | else:
332 | _LOGGER.error(
333 | f"({self._attr_name}) AttributeError: Attributes must be a dictionary: {new_attributes}"
334 | )
335 | return new_attributes
336 | else:
337 | return None
338 |
339 | async def async_update_variable(self, **kwargs) -> None:
340 | """Update Sensor Variable."""
341 |
342 | updated_attributes = None
343 |
344 | replace_attributes = kwargs.get(ATTR_REPLACE_ATTRIBUTES, False)
345 | _LOGGER.debug(
346 | f"({self._attr_name}) [async_update_variable] Replace Attributes: {replace_attributes}"
347 | )
348 |
349 | if (
350 | not replace_attributes
351 | and hasattr(self, "_attr_extra_state_attributes")
352 | and self._attr_extra_state_attributes is not None
353 | ):
354 | updated_attributes = copy.deepcopy(self._attr_extra_state_attributes)
355 |
356 | attributes = kwargs.get(ATTR_ATTRIBUTES)
357 | if attributes is not None:
358 | if isinstance(attributes, MutableMapping):
359 | _LOGGER.debug(
360 | f"({self._attr_name}) [async_update_variable] New Attributes: {attributes}"
361 | )
362 | extra_attributes = self._update_attr_settings(attributes)
363 | if updated_attributes is not None:
364 | updated_attributes.update(extra_attributes)
365 | else:
366 | updated_attributes = extra_attributes
367 | else:
368 | _LOGGER.error(
369 | f"({self._attr_name}) AttributeError: Attributes must be a dictionary: {attributes}"
370 | )
371 |
372 | if ATTR_VALUE in kwargs:
373 | try:
374 | newval = value_to_type(kwargs.get(ATTR_VALUE), self._value_type)
375 | except ValueError:
376 | ERROR = f"The value entered is not compatible with the selected device_class: {self._attr_device_class}. Expected: {self._value_type}. Value: {kwargs.get(ATTR_VALUE)}"
377 | raise ValueError(ERROR)
378 | return
379 | else:
380 | _LOGGER.debug(
381 | f"({self._attr_name}) [async_update_variable] New Value: {newval}"
382 | )
383 | self._attr_native_value = newval
384 |
385 | if updated_attributes is not None:
386 | self._attr_extra_state_attributes = copy.deepcopy(updated_attributes)
387 | _LOGGER.debug(
388 | f"({self._attr_name}) [async_update_variable] Final Attributes: {updated_attributes}"
389 | )
390 | else:
391 | self._attr_extra_state_attributes = None
392 |
393 | _LOGGER.debug(
394 | f"({self._attr_name}) [updated] _attr_native_value: {self._attr_native_value}"
395 | )
396 | _LOGGER.debug(
397 | f"({self._attr_name}) [updated] attributes: {self._attr_extra_state_attributes}"
398 | )
399 | self.async_write_ha_state()
400 |
401 |
402 | class VariableNoRecorder(Variable):
403 | _unrecorded_attributes = frozenset({MATCH_ALL})
404 |
--------------------------------------------------------------------------------
/custom_components/variable/services.yaml:
--------------------------------------------------------------------------------
1 | update_sensor:
2 | name: Update Sensor Variable
3 | description: Update a Sensor Variable value and/or its attributes.
4 | target:
5 | entity:
6 | integration: variable
7 | domain: sensor
8 | fields:
9 | value:
10 | name: New Value
11 | description: "New value to set. If a device class and native unit of measurement is set: 1. This will update the value in the native unit of measurement not necessarily the displayed unit of measurement if they are different. 2. It will give an error if the value is not a supported type for the device class (ex. setting a string for a temperature device class) (optional)"
12 | example: 9
13 | selector:
14 | text:
15 | attributes:
16 | name: New Attributes
17 | description: Attributes to set or update [dictionary] (optional)
18 | example: "{'key': 'value'}"
19 | selector:
20 | object:
21 | replace_attributes:
22 | name: Replace Attributes
23 | description: Replace or merge current attributes [boolean] (optional) (default false = merge)
24 | required: false
25 | default: false
26 | example: "false"
27 | selector:
28 | boolean:
29 |
30 | update_binary_sensor:
31 | name: Update Binary Sensor Variable
32 | description: Update a Binary Sensor Variable value and/or its attributes.
33 | target:
34 | entity:
35 | integration: variable
36 | domain: binary_sensor
37 | fields:
38 | value:
39 | name: New Value
40 | description: New value to set [boolean] (optional)
41 | required: false
42 | example: "false"
43 | selector:
44 | select:
45 | mode: list
46 | translation_key: "boolean_options"
47 | options:
48 | - "None"
49 | - "true"
50 | - "false"
51 | attributes:
52 | name: New Attributes
53 | description: Attributes to set or update [dictionary] (optional)
54 | example: "{'key': 'value'}"
55 | selector:
56 | object:
57 | replace_attributes:
58 | name: Replace Attributes
59 | description: Replace or merge current attributes [boolean] (optional) (default false = merge)
60 | required: false
61 | default: false
62 | example: "false"
63 | selector:
64 | boolean:
65 |
66 | update_device_tracker:
67 | name: Update Device Tracker (GPS) Variable
68 | description: Update a Device Tracker (GPS) Variable.
69 | target:
70 | entity:
71 | integration: variable
72 | domain: device_tracker
73 | fields:
74 | latitude:
75 | name: Latitude
76 | description: New Latitude
77 | example: 38.889466
78 | selector:
79 | number:
80 | min: -90
81 | max: 90
82 | step: "any"
83 | unit_of_measurement: "°"
84 | mode: box
85 | longitude:
86 | name: Longitude
87 | description: New Longitude
88 | example: -77.035235
89 | selector:
90 | number:
91 | min: -180
92 | max: 180
93 | step: "any"
94 | unit_of_measurement: "°"
95 | mode: box
96 | location_name:
97 | name: Location Name
98 | description: New Location Name
99 | example: "School"
100 | selector:
101 | text:
102 | delete_location_name:
103 | name: Delete Location Name
104 | description: Remove the Location Name so state will be based on Lat/Long
105 | selector:
106 | constant:
107 | label: Will Delete
108 | value: true
109 | gps_accuracy:
110 | name: GPS Accuracy
111 | description: New GPS Accuracy
112 | example: 5
113 | selector:
114 | number:
115 | min: 0
116 | max: 1000000
117 | step: 1
118 | unit_of_measurement: "m"
119 | mode: box
120 | battery_level:
121 | name: Battery Level
122 | description: New Battery Level
123 | example: 99
124 | selector:
125 | number:
126 | min: 0
127 | max: 100
128 | step: 1
129 | unit_of_measurement: "%"
130 | mode: box
131 | attributes:
132 | name: New Attributes
133 | description: Attributes to set or update [dictionary] (optional)
134 | example: "{'key': 'value'}"
135 | selector:
136 | object:
137 | replace_attributes:
138 | name: Replace Attributes
139 | description: Replace or merge current attributes [boolean] (optional) (default false = merge)
140 | required: false
141 | default: false
142 | example: "false"
143 | selector:
144 | boolean:
145 |
146 | toggle_binary_sensor:
147 | name: Toggle Binary Sensor Variable
148 | description: Toggle a Binary Sensor Variable value and optionally Update its attributes.
149 | target:
150 | entity:
151 | integration: variable
152 | domain: binary_sensor
153 | fields:
154 | attributes:
155 | name: New Attributes
156 | description: Attributes to set or update [dictionary] (optional)
157 | example: "{'key': 'value'}"
158 | selector:
159 | object:
160 | replace_attributes:
161 | name: Replace Attributes
162 | description: Replace or merge current attributes [boolean] (optional) (default false = merge)
163 | required: false
164 | default: false
165 | example: "false"
166 | selector:
167 | boolean:
168 |
169 | set_variable:
170 | # Description of the service
171 | name: Set Variable (Legacy)
172 | description: "Legacy service: Update a Sensor Variable value and/or its attributes. Will only work on Sensor Variables. Use one of the variable.update_ services for additional options."
173 | # Different fields that your service accepts
174 | fields:
175 | # Key of the field
176 | variable:
177 | name: Variable ID
178 | description: The name of the Sensor Variable to update [string] (required)
179 | required: true
180 | example: test_counter
181 | selector:
182 | text:
183 | value:
184 | name: New Value
185 | description: "New value to set. If a device class and native unit of measurement is set: 1. This will update the value in the native unit of measurement not necessarily the displayed unit of measurement if they are different. 2. It will give an error if the value is not a supported type for the device class (ex. setting a string for a temperature device class) (optional)"
186 | example: 9
187 | selector:
188 | text:
189 | attributes:
190 | name: New Attributes
191 | description: Attributes to set or update [dictionary] (optional)
192 | example: "{'key': 'value'}"
193 | selector:
194 | object:
195 | replace_attributes:
196 | name: Replace Attributes
197 | description: Replace or merge current attributes [boolean] (optional) (default false = merge)
198 | required: false
199 | default: false
200 | example: "false"
201 | selector:
202 | boolean:
203 |
204 | set_entity:
205 | name: Set Entity (Legacy)
206 | description: "Legacy service: Update a Sensor Variable value and/or its attributes. Will only work on Sensor Variables. Use one of the variable.update_ services for additional options."
207 | fields:
208 | entity:
209 | name: Entity ID
210 | description: The entity_id of the Sensor Variable to update [string] (required)
211 | example: sensor.test_sensor
212 | required: true
213 | selector:
214 | entity:
215 | integration: variable
216 | domain: sensor
217 | value:
218 | name: New Value
219 | description: "New value to set. If a device class and native unit of measurement is set: 1. This will update the value in the native unit of measurement not necessarily the displayed unit of measurement if they are different. 2. It will give an error if the value is not a supported type for the device class (ex. setting a string for a temperature device class) (optional)"
220 | example: 9
221 | selector:
222 | text:
223 | attributes:
224 | name: New Attributes
225 | description: Attributes to set or update [dictionary] (optional)
226 | example: "{'key': 'value'}"
227 | selector:
228 | object:
229 | replace_attributes:
230 | name: Replace Attributes
231 | description: Replace or merge current attributes [boolean] (optional) (default false = merge)
232 | required: false
233 | default: false
234 | example: "false"
235 | selector:
236 | boolean:
237 |
--------------------------------------------------------------------------------
/custom_components/variable/strings.json:
--------------------------------------------------------------------------------
1 | {
2 | "config": {
3 | "step": {
4 | "user": {
5 | "menu_options": {
6 | "add_sensor": "Create a Sensor Variable",
7 | "add_binary_sensor": "Create a Binary Sensor Variable",
8 | "add_device_tracker": "Create a Device Tracker (GPS) Variable",
9 | "add_device": "Create a Device"
10 | }
11 | },
12 | "add_sensor": {
13 | "title": "Variables+History - Sensor",
14 | "data": {
15 | "name": "[%key:common::config_flow::data::name%]",
16 | "variable_id": "Variable ID",
17 | "icon": "Icon",
18 | "device_class": "Device Class",
19 | "device_id": "Associate Variable with a Device",
20 | "restore": "Restore on Restart",
21 | "force_update": "Force Update",
22 | "exclude_from_recorder": "Exclude from Recorder"
23 | },
24 | "description": "Create a new Sensor Variable"
25 | },
26 | "sensor_page_2": {
27 | "title": "Variables+History - Sensor Page 2",
28 | "data": {
29 | "value": "Initial Value",
30 | "tz_offset": "Initial Time Zone Offset",
31 | "attributes": "Initial Attributes",
32 | "state_class": "State Class",
33 | "unit_of_measurement": "Unit of Measurement"
34 | },
35 | "description": "Create a new Sensor Variable Page 2"
36 | },
37 | "add_binary_sensor": {
38 | "title": "Variables+History - Binary Sensor",
39 | "data": {
40 | "name": "[%key:common::config_flow::data::name%]",
41 | "variable_id": "Variable ID",
42 | "icon": "Icon",
43 | "value": "Initial Value",
44 | "attributes": "Initial Attributes",
45 | "device_class": "Device Class",
46 | "device_id": "Associate Variable with a Device",
47 | "restore": "Restore on Restart",
48 | "force_update": "Force Update",
49 | "exclude_from_recorder": "Exclude from Recorder"
50 | },
51 | "description": "Create a new Binary Sensor Variable"
52 | },
53 | "add_device_tracker": {
54 | "title": "Variables+History - Device Tracker (GPS)",
55 | "data": {
56 | "name": "[%key:common::config_flow::data::name%]",
57 | "variable_id": "Variable ID",
58 | "icon": "Icon",
59 | "latitude": "Initial Latitude",
60 | "longitude": "Initial Longitude",
61 | "location_name": "Initial Location Name",
62 | "gps_accuracy": "Initial GPS Accuracy",
63 | "battery_level": "Initial Battery Level",
64 | "attributes": "Initial Attributes",
65 | "device_id": "Associate Variable with a Device",
66 | "restore": "Restore on Restart",
67 | "force_update": "Force Update",
68 | "exclude_from_recorder": "Exclude from Recorder"
69 | },
70 | "description": "Create a new Device Tracker (GPS) Variable"
71 | },
72 | "add_device": {
73 | "title": "Variables+History - Device",
74 | "data": {
75 | "name": "[%key:common::config_flow::data::name%]",
76 | "configuration_url": "Configuration URL",
77 | "manufacturer": "Manufacturer",
78 | "hw_version": "Hardware Version",
79 | "model": "Model",
80 | "model_id": "Model ID",
81 | "serial_number": "Serial Number",
82 | "sw_version": "Software Version"
83 | },
84 | "description": "Create a new Device"
85 | }
86 | },
87 | "error": {
88 | "invalid_value_type": "The value entered is not compatible with the selected device_class",
89 | "invalid_url": "Invalid URL",
90 | "unknown": "[%key:common::config_flow::error::unknown%]"
91 | }
92 | },
93 | "options": {
94 | "step": {
95 | "init": {
96 | "menu_options": {
97 | "change_sensor_value": "Change Value and Attributes",
98 | "change_binary_sensor_value": "Change Value and Attributes",
99 | "change_device_tracker_value": "Change Value and Attributes",
100 | "sensor_options": "Change Options",
101 | "binary_sensor_options": "Change Options",
102 | "device_tracker_options": "Change Options"
103 | }
104 | },
105 | "sensor_options": {
106 | "title": "Variables+History - Sensor",
107 | "data": {
108 | "device_class": "Device Class",
109 | "device_id": "Associate Variable with a Device",
110 | "clear_device_id": "Clear Device Association",
111 | "restore": "Restore on Restart",
112 | "force_update": "Force Update",
113 | "exclude_from_recorder": "Exclude from Recorder"
114 | },
115 | "description": "Update existing Sensor Variable"
116 | },
117 | "sensor_options_page_2": {
118 | "title": "Variables+History - Sensor Page 2",
119 | "data": {
120 | "value": "Value (typically only useful if Restore on Restart is False)",
121 | "tz_offset": "Time Zone Offset (typically only useful if Restore on Restart is False)",
122 | "attributes": "Attributes (typically only useful if Restore on Restart is False)",
123 | "state_class": "State Class",
124 | "unit_of_measurement": "Unit of Measurement"
125 | },
126 | "description": "Update existing Sensor Variable"
127 | },
128 | "change_sensor_value": {
129 | "title": "Variables+History - Change Sensor Value",
130 | "data": {
131 | "value": "Value",
132 | "tz_offset": "Time Zone Offset",
133 | "attributes": "Attributes"
134 | },
135 | "description": "Update existing Sensor Variable"
136 | },
137 | "change_binary_sensor_value": {
138 | "title": "Variables+History - Change Binary Sensor Value",
139 | "data": {
140 | "value": "Value",
141 | "attributes": "Attributes"
142 | },
143 | "description": "Update existing Binary Sensor Variable"
144 | },
145 | "change_device_tracker_value": {
146 | "title": "Variables+History - Device Tracker (GPS)",
147 | "data": {
148 | "latitude": "Latitude",
149 | "longitude": "Longitude",
150 | "location_name": "Location Name",
151 | "delete_location_name": "Delete Location Name",
152 | "gps_accuracy": "GPS Accuracy",
153 | "battery_level": "Battery Level",
154 | "attributes": "Attributes"
155 | },
156 | "description": "Update existing Device Tracker (GPS) Variable"
157 | },
158 | "binary_sensor_options": {
159 | "title": "Variables+History - Binary Sensor",
160 | "data": {
161 | "value": "Value (typically only useful if Restore on Restart is False)",
162 | "attributes": "Attributes (typically only useful if Restore on Restart is False)",
163 | "device_class": "Device Class",
164 | "device_id": "Associate Variable with a Device",
165 | "clear_device_id": "Clear Device Association",
166 | "restore": "Restore on Restart",
167 | "force_update": "Force Update",
168 | "exclude_from_recorder": "Exclude from Recorder"
169 | },
170 | "description": "Update existing Binary Sensor Variable"
171 | },
172 | "device_tracker_options": {
173 | "title": "Variables+History - Device Tracker (GPS)",
174 | "data": {
175 | "latitude": "Latitude (typically only useful if Restore on Restart is False)",
176 | "longitude": "Longitude (typically only useful if Restore on Restart is False)",
177 | "location_name": "Location Name (typically only useful if Restore on Restart is False)",
178 | "gps_accuracy": "GPS Accuracy (typically only useful if Restore on Restart is False)",
179 | "battery_level": "Battery Level (typically only useful if Restore on Restart is False)",
180 | "attributes": "Attributes (typically only useful if Restore on Restart is False)",
181 | "device_id": "Associate Variable with a Device",
182 | "clear_device_id": "Clear Device Association",
183 | "restore": "Restore on Restart",
184 | "force_update": "Force Update",
185 | "exclude_from_recorder": "Exclude from Recorder"
186 | },
187 | "description": "Update existing Device Tracker (GPS) Variable"
188 | }
189 | },
190 | "abort": {
191 | "yaml_variable": "Cannot change options here for Variables created by YAML.\n\nTo use the User Interface to manage this Variable, you will need to:\n1. Remove it from YAML\n2. Restart Home Assistant\n3. Manually recreate it in Home Assistant, Integrations, +Add Integration.",
192 | "yaml_update_error": "Unable to update YAML Sensor Variable",
193 | "value_changed": "Variable Changed"
194 | },
195 | "error": {
196 | "invalid_value_type": "The value entered is not compatible with the selected device_class",
197 | "invalid_url": "Invalid URL",
198 | "unknown": "[%key:common::config_flow::error::unknown%]"
199 | }
200 | },
201 | "selector": {
202 | "boolean_options": {
203 | "options": {
204 | "true": "true",
205 | "false": "false"
206 | }
207 | }
208 | }
209 | }
--------------------------------------------------------------------------------
/custom_components/variable/translations/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "config": {
3 | "step": {
4 | "user": {
5 | "menu_options": {
6 | "add_sensor": "Create a Sensor Variable",
7 | "add_binary_sensor": "Create a Binary Sensor Variable",
8 | "add_device_tracker": "Create a Device Tracker (GPS) Variable",
9 | "add_device": "Create a Device"
10 | }
11 | },
12 | "add_sensor": {
13 | "title": "Variables+History - Sensor",
14 | "data": {
15 | "name": "Variable Name",
16 | "variable_id": "Variable ID",
17 | "icon": "Icon",
18 | "device_class": "Device Class",
19 | "device_id": "Associate Variable with a Device",
20 | "restore": "Restore on Restart",
21 | "force_update": "Force Update",
22 | "exclude_from_recorder": "Exclude from Recorder"
23 | },
24 | "description": "Create a new Sensor Variable\nSee [Configuration Options]({component_config_url}) on GitHub for details"
25 | },
26 | "sensor_page_2": {
27 | "title": "Variables+History - Sensor Page 2",
28 | "data": {
29 | "value": "Initial Value",
30 | "tz_offset": "Initial Time Zone Offset",
31 | "attributes": "Initial Attributes",
32 | "state_class": "State Class",
33 | "unit_of_measurement": "Unit of Measurement"
34 | },
35 | "description": "Create a new Sensor Variable Page 2\n\n**Variable: {disp_name}**\n**Device Class: {device_class}**\n**Value Type: {value_type}**"
36 | },
37 | "add_binary_sensor": {
38 | "title": "Variables+History - Binary Sensor",
39 | "data": {
40 | "name": "Variable Name",
41 | "variable_id": "Variable ID",
42 | "icon": "Icon",
43 | "value": "Initial Value",
44 | "attributes": "Initial Attributes",
45 | "device_class": "Device Class",
46 | "device_id": "Associate Variable with a Device",
47 | "restore": "Restore on Restart",
48 | "force_update": "Force Update",
49 | "exclude_from_recorder": "Exclude from Recorder"
50 | },
51 | "description": "Create a new Binary Sensor Variable\nSee [Configuration Options]({component_config_url}) on GitHub for details"
52 | },
53 | "add_device_tracker": {
54 | "title": "Variables+History - Device Tracker (GPS)",
55 | "data": {
56 | "name": "Variable Name",
57 | "variable_id": "Variable ID",
58 | "icon": "Icon",
59 | "latitude": "Initial Latitude",
60 | "longitude": "Initial Longitude",
61 | "location_name": "Initial Location Name",
62 | "gps_accuracy": "Initial GPS Accuracy",
63 | "battery_level": "Initial Battery Level",
64 | "attributes": "Initial Attributes",
65 | "device_id": "Associate Variable with a Device",
66 | "restore": "Restore on Restart",
67 | "force_update": "Force Update",
68 | "exclude_from_recorder": "Exclude from Recorder"
69 | },
70 | "description": "Create a new Device Tracker (GPS) Variable\nSee [Configuration Options]({component_config_url}) on GitHub for details"
71 | },
72 | "add_device": {
73 | "title": "Variables+History - Device",
74 | "data": {
75 | "name": "Device Name",
76 | "configuration_url": "Configuration URL",
77 | "manufacturer": "Manufacturer",
78 | "hw_version": "Hardware Version",
79 | "model": "Model",
80 | "model_id": "Model ID",
81 | "serial_number": "Serial Number",
82 | "sw_version": "Software Version"
83 | },
84 | "description": "Create a new Device\nSee [Configuration Options]({component_config_url}) on GitHub for details"
85 | }
86 | },
87 | "error": {
88 | "invalid_value_type": "The value entered is not compatible with the selected device_class: {device_class}. Expected {value_type}.",
89 | "invalid_url": "Invalid URL",
90 | "unknown": "[%key:common::config_flow::error::unknown%]"
91 | }
92 | },
93 | "options": {
94 | "step": {
95 | "init": {
96 | "menu_options": {
97 | "change_sensor_value": "Change Value and Attributes",
98 | "change_binary_sensor_value": "Change Value and Attributes",
99 | "change_device_tracker_value": "Change Value and Attributes",
100 | "sensor_options": "Change Options",
101 | "binary_sensor_options": "Change Options",
102 | "device_tracker_options": "Change Options",
103 | "device_options": "Change Options"
104 | }
105 | },
106 | "sensor_options": {
107 | "title": "Variables+History - Sensor",
108 | "data": {
109 | "device_class": "Device Class",
110 | "device_id": "Associate Variable with a Device",
111 | "clear_device_id": "Clear Device Association",
112 | "restore": "Restore on Restart",
113 | "force_update": "Force Update",
114 | "exclude_from_recorder": "Exclude from Recorder"
115 | },
116 | "description": "**Updating Sensor: {disp_name}**\nSee [Configuration Options]({component_config_url}) on GitHub for details"
117 | },
118 | "sensor_options_page_2": {
119 | "title": "Variables+History - Sensor Page 2",
120 | "data": {
121 | "value": "Value (typically only useful if Restore on Restart is False)",
122 | "tz_offset": "Time Zone Offset (typically only useful if Restore on Restart is False)",
123 | "attributes": "Attributes (typically only useful if Restore on Restart is False)",
124 | "state_class": "State Class",
125 | "unit_of_measurement": "Unit of Measurement"
126 | },
127 | "description": "Updating Sensor Variable Page 2\n\n**Variable: {disp_name}**\n**Device Class: {device_class}**\n**Value Type: {value_type}**"
128 | },
129 | "change_sensor_value": {
130 | "title": "Variables+History - Change Sensor Value",
131 | "data": {
132 | "value": "Value",
133 | "tz_offset": "Time Zone Offset",
134 | "attributes": "Attributes"
135 | },
136 | "description": "**Updating Sensor: {disp_name}**"
137 | },
138 | "change_binary_sensor_value": {
139 | "title": "Variables+History - Change Binary Sensor Value",
140 | "data": {
141 | "value": "Value",
142 | "attributes": "Attributes"
143 | },
144 | "description": "**Updating Binary Sensor: {disp_name}**"
145 | },
146 | "change_device_tracker_value": {
147 | "title": "Variables+History - Device Tracker (GPS)",
148 | "data": {
149 | "latitude": "Latitude",
150 | "longitude": "Longitude",
151 | "location_name": "Location Name",
152 | "delete_location_name": "Delete Location Name",
153 | "gps_accuracy": "GPS Accuracy",
154 | "battery_level": "Battery Level",
155 | "attributes": "Attributes"
156 | },
157 | "description": "**Updating Device Tracker: {disp_name}**\n**Current State: {dt_state}**"
158 | },
159 | "binary_sensor_options": {
160 | "title": "Variables+History - Binary Sensor",
161 | "data": {
162 | "value": "Value (typically only useful if Restore on Restart is False)",
163 | "attributes": "Attributes (typically only useful if Restore on Restart is False)",
164 | "device_class": "Device Class",
165 | "device_id": "Associate Variable with a Device",
166 | "clear_device_id": "Clear Device Association",
167 | "restore": "Restore on Restart",
168 | "force_update": "Force Update",
169 | "exclude_from_recorder": "Exclude from Recorder"
170 | },
171 | "description": "**Updating Binary Sensor: {disp_name}**\nSee [Configuration Options]({component_config_url}) on GitHub for details"
172 | },
173 | "device_tracker_options": {
174 | "title": "Variables+History - Device Tracker (GPS)",
175 | "data": {
176 | "latitude": "Latitude (typically only useful if Restore on Restart is False)",
177 | "longitude": "Longitude (typically only useful if Restore on Restart is False)",
178 | "location_name": "Location Name (typically only useful if Restore on Restart is False)",
179 | "gps_accuracy": "GPS Accuracy (typically only useful if Restore on Restart is False)",
180 | "battery_level": "Battery Level (typically only useful if Restore on Restart is False)",
181 | "attributes": "Attributes (typically only useful if Restore on Restart is False)",
182 | "device_id": "Associate Variable with a Device",
183 | "clear_device_id": "Clear Device Association",
184 | "restore": "Restore on Restart",
185 | "force_update": "Force Update",
186 | "exclude_from_recorder": "Exclude from Recorder"
187 | },
188 | "description": "**Updating Device Tracker (GPS): {disp_name}**\nSee [Configuration Options]({component_config_url}) on GitHub for details"
189 | },
190 | "device_options": {
191 | "title": "Variables+History - Device",
192 | "data": {
193 | "name": "Device Name",
194 | "configuration_url": "Configuration URL",
195 | "manufacturer": "Manufacturer",
196 | "hw_version": "Hardware Version",
197 | "model": "Model",
198 | "model_id": "Model ID",
199 | "serial_number": "Serial Number",
200 | "sw_version": "Software Version"
201 | },
202 | "description": "**Updating Device: {disp_name}**\nSee [Configuration Options]({component_config_url}) on GitHub for details"
203 | }
204 | },
205 | "abort": {
206 | "yaml_variable": "Cannot change options here for Variables created by YAML.\n\nTo use the User Interface to manage this Variable, you will need to:\n1. Remove it from YAML\n2. Restart Home Assistant\n3. Manually recreate it in Home Assistant, Integrations, +Add Integration.",
207 | "yaml_update_error": "Unable to update YAML Sensor Variable",
208 | "value_changed": "Variable Changed"
209 | },
210 | "error": {
211 | "invalid_value_type": "The value entered is not compatible with the selected device_class: {device_class}. Expected {value_type}.",
212 | "invalid_url": "Invalid URL",
213 | "unknown": "[%key:common::config_flow::error::unknown%]"
214 | }
215 | },
216 | "selector": {
217 | "boolean_options": {
218 | "options": {
219 | "true": "true",
220 | "false": "false"
221 | }
222 | }
223 | }
224 | }
--------------------------------------------------------------------------------
/custom_components/variable/translations/sk.json:
--------------------------------------------------------------------------------
1 | {
2 | "config": {
3 | "step": {
4 | "user": {
5 | "menu_options": {
6 | "add_sensor": "Vytvorte premennú snímača",
7 | "add_binary_sensor": "Vytvorte binárnu premennú snímača",
8 | "add_device_tracker": "Vytvorte premennú sledovania zariadenia (GPS)."
9 | }
10 | },
11 | "add_sensor": {
12 | "title": "Premenné+história - Snímač",
13 | "data": {
14 | "name": "Názov premennej",
15 | "variable_id": "ID premennej",
16 | "icon": "Ikona",
17 | "device_class": "Trieda zariadenia",
18 | "restore": "Obnoviť pri reštarte",
19 | "force_update": "Vynútiť aktualizáciu",
20 | "exclude_from_recorder": "Vylúčiť zo záznamu"
21 | },
22 | "description": "Vytvorte novú premennú snímača\nPozri [Configuration Options]({component_config_url}) na GitHube pre detaily"
23 | },
24 | "sensor_page_2": {
25 | "title": "Premenné+história - Snímač Strana 2",
26 | "data": {
27 | "value": "Počiatočná hodnota",
28 | "tz_offset": "Posun počiatočného časového pásma",
29 | "attributes": "Počiatočné atribúty",
30 | "state_class": "Trieda stavu",
31 | "unit_of_measurement": "Jednotky merania"
32 | },
33 | "description": "Vytvorte novú stránku premennej snímača 2\n\n**Premenná: {disp_name}**\n**Trieda zariadenia: {device_class}**\n**Typ hodnoty: {value_type}**"
34 | },
35 | "add_binary_sensor": {
36 | "title": "Premenné+história - Binárny snímač",
37 | "data": {
38 | "name": "Názov premennej",
39 | "variable_id": "ID premennej",
40 | "icon": "Ikona",
41 | "value": "Počiatočná hodnota",
42 | "attributes": "Počiatočné atribúty",
43 | "device_class": "Trieda zariadenia",
44 | "restore": "Obnoviť pri reštarte",
45 | "force_update": "Vynútiť aktualizáciu",
46 | "exclude_from_recorder": "Vylúčiť zo záznamu"
47 | },
48 | "description": "Vytvorte novú premennú binárneho snímača\nPozri [Configuration Options]({component_config_url}) na GitHube pre detaily"
49 | },
50 | "add_device_tracker": {
51 | "title": "Premenné+história - Sledovanie zariadení (GPS)",
52 | "data": {
53 | "name": "Názov premennej",
54 | "variable_id": "ID premennej",
55 | "icon": "Ikona",
56 | "latitude": "Počiatočná zemepisná šírka",
57 | "longitude": "Počiatočná zemepisná dĺžka",
58 | "location_name": "Názov počiatočného miesta",
59 | "gps_accuracy": "Počiatočná presnosť GPS",
60 | "battery_level": "Počiatočná úroveň nabitia batérie",
61 | "attributes": "Počiatočné atribúty",
62 | "restore": "Obnoviť pri reštarte",
63 | "force_update": "Vynútiť aktualizáciu",
64 | "exclude_from_recorder": "Vylúčiť zo záznamu"
65 | },
66 | "description": "Vytvorte novú premennú sledovania zariadenia (GPS)\nPozri [Configuration Options]({component_config_url}) na GitHube pre detaily"
67 | }
68 | },
69 | "error": {
70 | "invalid_value_type": "Zadaná hodnota nie je kompatibilná s vybratou hodnotou device_class: {device_class}. Očakávané {value_type}.",
71 | "unknown": "[%key:common::config_flow::error::unknown%]"
72 | }
73 | },
74 | "options": {
75 | "step": {
76 | "init": {
77 | "menu_options": {
78 | "change_sensor_value": "Zmeňte hodnotu a atribúty",
79 | "change_binary_sensor_value": "Zmeňte hodnotu a atribúty",
80 | "change_device_tracker_value": "Zmeňte hodnotu a atribúty",
81 | "sensor_options": "Zmeniť možnosti",
82 | "binary_sensor_options": "Zmeniť možnosti",
83 | "device_tracker_options": "Zmeniť možnosti"
84 | }
85 | },
86 | "sensor_options": {
87 | "title": "Premenné+história - snímač",
88 | "data": {
89 | "device_class": "Trieda zariadenia",
90 | "restore": "Obnoviť pri reštarte",
91 | "force_update": "Vynútiť aktualizáciu",
92 | "exclude_from_recorder": "Vylúčiť zo záznamu"
93 | },
94 | "description": "**Aktualizácia snímača: {disp_name}**\nPozri [Configuration Options]({component_config_url}) na GitHube pre detaily"
95 | },
96 | "sensor_options_page_2": {
97 | "title": "Premenné+história - snímač Strana 2",
98 | "data": {
99 | "value": "Hodnota (zvyčajne užitočná iba vtedy, ak je možnosť Obnoviť pri reštarte nastavená na hodnotu False)",
100 | "tz_offset": "Posun časového pásma",
101 | "attributes": "Atribúty (zvyčajne užitočné iba vtedy, ak je možnosť Obnoviť pri reštarte nastavená na hodnotu False)",
102 | "state_class": "Trieda stavu",
103 | "unit_of_measurement": "Jednotky merania"
104 | },
105 | "description": "Aktualizácia premennej snímača Strana 2\n\n**Premenná: {disp_name}**\n**Trieda zariadenia: {device_class}**\n**Typ hodnoty: {value_type}**"
106 | },
107 | "change_sensor_value": {
108 | "title": "Premenné+História - Zmena hodnoty senzora",
109 | "data": {
110 | "value": "Hodnota",
111 | "tz_offset": "Posun časového pásma",
112 | "attributes": "Atribúty"
113 | },
114 | "description": "**Aktualizácia senzora: {disp_name}**"
115 | },
116 | "change_binary_sensor_value": {
117 | "title": "Premenné+História – Zmeňte hodnotu binárneho senzora",
118 | "data": {
119 | "value": "Hodnota",
120 | "attributes": "Atribúty"
121 | },
122 | "description": "**Aktualizácia binárneho senzora: {disp_name}**"
123 | },
124 | "change_device_tracker_value": {
125 | "title": "Premenné+História – Sledovanie zariadení (GPS)",
126 | "data": {
127 | "latitude": "Zemepisná šírka",
128 | "longitude": "Zemepisná dĺžka",
129 | "location_name": "Názov miesta",
130 | "delete_location_name": "Odstrániť názov miesta",
131 | "gps_accuracy": "Presnosť GPS",
132 | "battery_level": "Úroveň batérie",
133 | "attributes": "Atribúty"
134 | },
135 | "description": "**Aktualizuje sa nástroj na sledovanie zariadenia: {disp_name}**\n**Aktuálny stav: {dt_state}**"
136 | },
137 | "binary_sensor_options": {
138 | "title": "Premenné+história - binárny snímač",
139 | "data": {
140 | "value": "Hodnota (zvyčajne užitočná iba vtedy, ak je možnosť Obnoviť pri reštarte nastavená na hodnotu False)",
141 | "attributes": "Atribúty (zvyčajne užitočné iba vtedy, ak je možnosť Obnoviť pri reštarte nastavená na hodnotu False)",
142 | "device_class": "Trieda zariadenia",
143 | "restore": "Obnoviť pri reštarte",
144 | "force_update": "Vynútiť aktualizáciu",
145 | "exclude_from_recorder": "Vylúčiť zo záznamu"
146 | },
147 | "description": "**Aktualizácia binárneho snímača: {disp_name}**\nPozri [Configuration Options]({component_config_url}) na GitHube pre detaily"
148 | },
149 | "device_tracker_options": {
150 | "title": "Premenné+história - Sledovanie zariadení (GPS)",
151 | "data": {
152 | "latitude": "Zemepisná šírka (zvyčajne užitočné iba vtedy, ak je možnosť Obnoviť pri reštarte nastavená na hodnotu False)",
153 | "longitude": "Zemepisná dĺžka (zvyčajne užitočné iba vtedy, ak je možnosť Obnoviť pri reštarte nastavená na hodnotu False)",
154 | "location_name": "Názov miesta (zvyčajne užitočné iba vtedy, ak je možnosť Obnoviť pri reštarte nastavená na hodnotu False)",
155 | "gps_accuracy": "Presnosť GPS (zvyčajne užitočné iba vtedy, ak je možnosť Obnoviť pri reštarte nastavená na hodnotu False)",
156 | "battery_level": "Úroveň nabitia batérie (zvyčajne užitočné iba vtedy, ak je možnosť Obnoviť pri reštarte nastavená na hodnotu False)",
157 | "attributes": "Atribúty (zvyčajne užitočné iba vtedy, ak je možnosť Obnoviť pri reštarte nastavená na hodnotu False)",
158 | "restore": "Obnoviť pri reštarte",
159 | "force_update": "Vynútiť aktualizáciu",
160 | "exclude_from_recorder": "Vylúčiť zo záznamu"
161 | },
162 | "description": "**Aktualizácia nástroja na sledovanie zariadenia (GPS): {disp_name}**\nPozri [Configuration Options]({component_config_url}) na GitHube pre detaily"
163 | }
164 | },
165 | "abort": {
166 | "yaml_variable": "Tu nie je možné zmeniť možnosti pre premenné vytvorené pomocou YAML.\n\nAk chcete na správu tejto premennej použiť používateľské rozhranie, budete musieť:\n1. Odstráňte ho z YAML\n2. Reštartujte domáceho asistenta\n3. Manuálne ho znova vytvorte v Home Assistant, Integrácie, + Pridať integráciu.",
167 | "yaml_update_error": "Nie je možné aktualizovať premennú snímača YAML",
168 | "value_changed": "Hodnota zmenená"
169 | },
170 | "error": {
171 | "invalid_value_type": "Zadaná hodnota nie je kompatibilná s vybratou triedou zariadenia: {device_class}. Očakávané {value_type}.",
172 | "unknown": "[%key:common::config_flow::error::unknown%]"
173 | }
174 | },
175 | "selector": {
176 | "boolean_options": {
177 | "options": {
178 | "true": "true",
179 | "false": "false"
180 | }
181 | }
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/examples/counter.yaml:
--------------------------------------------------------------------------------
1 | # Create a sensor variable with the Variable ID of `test_counter` and Initial Value of `0`
2 |
3 | automation:
4 | - alias: test_counter
5 | initial_state: 'on'
6 | trigger:
7 | - platform: time
8 | seconds: '/1'
9 | action:
10 | - service: variable.update_sensor
11 | data:
12 | value: >
13 | {{ (states('sensor.test_counter') | int(default=0)) + 1 }}
14 | target:
15 | entity_id: sensor.test_counter
16 |
--------------------------------------------------------------------------------
/examples/history.yaml:
--------------------------------------------------------------------------------
1 | # Create a sensor variable with the Variable ID of `last_motion`, and Initial Value of `unknown`, and Restore on Restart of `True`
2 |
3 | script:
4 | update_last_motion:
5 | sequence:
6 | - service: variable.update_sensor
7 | data:
8 | value: >
9 | {{ location }}
10 | attributes:
11 | history_1: "{{states('sensor.last_motion')}}"
12 | history_2: "{{state_attr('sensor.last_motion','history_1')}}"
13 | history_3: "{{state_attr('sensor.last_motion','history_2')}}"
14 | target:
15 | entity_id: sensor.last_motion
16 |
17 | update_motion_hall:
18 | sequence:
19 | - service: script.update_last_motion
20 | data:
21 | location: 'hall'
22 |
23 | update_motion_livingroom:
24 | sequence:
25 | - service: script.update_last_motion
26 | data:
27 | location: 'livingroom'
28 |
29 | update_motion_toilet:
30 | sequence:
31 | - service: script.update_last_motion
32 | data:
33 | location: 'toilet'
34 |
35 | update_motion_bedroom:
36 | sequence:
37 | - service: script.update_last_motion
38 | data:
39 | location: 'bedroom'
40 |
--------------------------------------------------------------------------------
/examples/keypad.yaml:
--------------------------------------------------------------------------------
1 | # Create a sensor variable with the Variable ID of `keypad` and Initial Value of ``
2 | # Create a sensor variable with the Variable ID of `keypad_timer` and Initial Value of `0`
3 |
4 | input_boolean:
5 | keypad_toggle:
6 |
7 | script:
8 | update_keypad:
9 | sequence:
10 | - service: variable.update_sensor
11 | data:
12 | attributes:
13 | last: "{{ number }}"
14 | target:
15 | entity_id: sensor.keypad
16 | - service: variable.update_sensor
17 | data:
18 | value: >
19 | {{ states('sensor.keypad') }}{{ state_attr('sensor.keypad','last') }}
20 | target:
21 | entity_id: sensor.keypad
22 | - service: variable.update_sensor
23 | data:
24 | value: "10"
25 | target:
26 | entity_id: sensor.keypad_timer
27 | - service: automation.turn_on
28 | data:
29 | entity_id: automation.keypad_timer
30 |
31 | clear_keypad:
32 | sequence:
33 | - service: variable.update_sensor
34 | data:
35 | value: ''
36 | target:
37 | entity_id: sensor.keypad
38 |
39 | enter_keypad_1:
40 | sequence:
41 | - service: script.update_keypad
42 | data:
43 | number: 1
44 |
45 | enter_keypad_2:
46 | sequence:
47 | - service: script.update_keypad
48 | data:
49 | number: 2
50 |
51 | enter_keypad_3:
52 | sequence:
53 | - service: script.update_keypad
54 | data:
55 | number: 3
56 |
57 | enter_keypad_4:
58 | sequence:
59 | - service: script.update_keypad
60 | data:
61 | number: 4
62 |
63 | automation:
64 | - alias: keypad_timer
65 | initial_state: 'off'
66 | trigger:
67 | - platform: time
68 | seconds: '/1'
69 | action:
70 | - service: variable.update_sensor
71 | data:
72 | value: >
73 | {{ [(states('sensor.keypad_timer') | int(default=0)) - 1, 0] | max }}
74 | target:
75 | entity_id: sensor.keypad_timer
76 |
77 |
78 | - alias: keypad_timeout
79 | trigger:
80 | platform: state
81 | entity_id: sensor.keypad_timer
82 | to: '0'
83 | action:
84 | - service: script.clear_keypad
85 | - service: automation.turn_off
86 | data:
87 | entity_id: automation.keypad_timer
88 |
89 | - alias: keypad_validate
90 | trigger:
91 | platform: state
92 | entity_id: sensor.keypad
93 | to: '1234'
94 | action:
95 | - service: input_boolean.toggle
96 | data:
97 | entity_id: input_boolean.keypad_toggle
98 | - service: script.clear_keypad
99 |
--------------------------------------------------------------------------------
/examples/timer.yaml:
--------------------------------------------------------------------------------
1 | # Create a sensor variable with the Variable ID of `test_timer` and Initial Value of `0`
2 |
3 | script:
4 | schedule_test_timer:
5 | sequence:
6 | - service: variable.update_sensor
7 | data:
8 | value: 30
9 | target:
10 | entity_id: sensor.test_timer
11 | - service: automation.turn_on
12 | data:
13 | entity_id: automation.test_timer_countdown
14 |
15 | automation:
16 | - alias: test_timer_countdown
17 | initial_state: 'off'
18 | trigger:
19 | - platform: time_pattern
20 | seconds: '/1'
21 | action:
22 | - service: variable.update_sensor
23 | data:
24 | value: >
25 | {{ [((states('sensor.test_timer') | int(default=0)) - 1), 0] | max }}
26 | target:
27 | entity_id: sensor.test_timer
28 |
29 | - alias: test_timer_trigger
30 | trigger:
31 | platform: state
32 | entity_id: variable.test_timer
33 | to: '0'
34 | action:
35 | - service: automation.turn_off
36 | data:
37 | entity_id: automation.test_timer_countdown
38 |
--------------------------------------------------------------------------------
/examples/value_and_data.yaml:
--------------------------------------------------------------------------------
1 | - id: posta_state_change
2 | alias: 'Posta state change'
3 | hide_entity: true
4 | initial_state: 'true'
5 | trigger:
6 | platform: state
7 | entity_id: sensor.cp_packages_coming_today
8 | condition:
9 | condition: state
10 | entity_id: 'sensor.cp_packages_coming_today'
11 | state: 'Delivery'
12 | action:
13 | service: variable.update_sensor
14 | data:
15 | attributes:
16 | from: "{{ state_attr('sensor.cp_packages_coming_today', 'from') }}"
17 | date: "{{ state_attr('sensor.cp_packages_coming_today', 'date') }}"
18 | subject: "{{ state_attr('sensor.cp_packages_coming_today', 'subject') }}"
19 | value: >
20 | {{ states('sensor.cp_packages_coming_today')}}
21 | target:
22 | entity_id: sensor.posta_variable
23 |
--------------------------------------------------------------------------------
/hacs.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Variables+History",
3 | "render_readme": true,
4 | "homeassistant": "2024.7.2"
5 | }
6 |
--------------------------------------------------------------------------------
/logo/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/enkama/hass-variables/acb72a01bccd57e6a0e1bd370c470cf0ca661e7e/logo/icon.png
--------------------------------------------------------------------------------
/logo/icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/enkama/hass-variables/acb72a01bccd57e6a0e1bd370c470cf0ca661e7e/logo/icon@2x.png
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # Custom requirements
2 |
3 | # Pre-commit requirements
4 | codespell==2.2.6
5 | flake8-comprehensions==3.14.0
6 | flake8-docstrings==1.7.0
7 | flake8-noqa==1.3.2
8 | flake8==6.1.0
9 | isort==6.0.0b2
10 |
11 | # Unit tests requirements
12 | pytest==7.4.2
13 | pytest-cov==4.1.0
14 | pytest-homeassistant-custom-component
15 | homeassistant~=2023.4.6
16 | voluptuous~=0.13.1
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [tool:pytest]
2 | testpaths = tests
3 | norecursedirs = .git
4 | addopts =
5 | --strict
6 | --cov=custom_components
7 |
8 | [flake8]
9 | exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build
10 | doctests = True
11 | # To work with Black
12 | # E501: line too long
13 | # W503: Line break occurred before a binary operator
14 | # E203: Whitespace before ':'
15 | # D202 No blank lines allowed after function docstring
16 | # W504 line break after binary operator
17 | ignore =
18 | E501,
19 | W503,
20 | E203,
21 | D202,
22 | W504
23 | noqa-require-code = True
24 |
25 | [isort]
26 | # https://github.com/timothycrosley/isort
27 | # https://github.com/timothycrosley/isort/wiki/isort-Settings
28 | # splits long import on multiple lines indented by 4 spaces
29 | multi_line_output = 3
30 | include_trailing_comma=True
31 | force_grid_wrap=0
32 | use_parentheses=True
33 | line_length=88
34 | indent = " "
35 | # will group `import x` and `from x import` of the same module.
36 | force_sort_within_sections = true
37 | sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
38 | default_section = THIRDPARTY
39 | known_first_party = custom_components,tests
40 | forced_separate = tests
41 | combine_as_imports = true
--------------------------------------------------------------------------------