├── .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 | Variable Logo 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 --------------------------------------------------------------------------------