├── Example.png ├── custom_components └── volkswagen_we_connect_id │ ├── const.py │ ├── manifest.json │ ├── translations │ ├── sensor.nb.json │ ├── sensor.sv.json │ ├── sensor.da.json │ ├── sensor.en.json │ ├── sensor.fr.json │ ├── sensor.de.json │ ├── sensor.es.json │ ├── sensor.it.json │ ├── sensor.nl.json │ ├── sensor.pt.json │ └── en.json │ ├── strings.json │ ├── services.yaml │ ├── device_tracker.py │ ├── config_flow.py │ ├── number.py │ ├── button.py │ ├── binary_sensor.py │ ├── __init__.py │ └── sensor.py ├── .devcontainer ├── devcontainer.json └── init.py ├── .github ├── dependabot.yml ├── settings.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── issue.md └── stale.yml ├── CONTRIBUTING.md ├── .gitignore ├── README.md ├── info.md └── LICENSE /Example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitch-dc/volkswagen_we_connect_id/HEAD/Example.png -------------------------------------------------------------------------------- /custom_components/volkswagen_we_connect_id/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the Volkswagen We Connect ID integration.""" 2 | 3 | DOMAIN = "volkswagen_we_connect_id" 4 | 5 | DEFAULT_UPDATE_INTERVAL_SECONDS = 45 6 | MINIMUM_UPDATE_INTERVAL_SECONDS = 30 7 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/devcontainers/python:3", 3 | "postCreateCommand": "python .devcontainer/init.py", 4 | "customizations": { 5 | "vscode": { 6 | "extensions": [ 7 | "ms-python.python" 8 | ] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.devcontainer/init.py: -------------------------------------------------------------------------------- 1 | """ 2 | Initialize container with modules used by volkswage_we_connect_id component 3 | """ 4 | 5 | import json 6 | import pip 7 | 8 | PIP_ARGUMENTS = [ "install", "--user", "homeassistant" ] 9 | MANIFEST_PATH = "custom_components/volkswagen_we_connect_id/manifest.json" 10 | 11 | with open(MANIFEST_PATH, "r", encoding="utf8") as MANIFEST_FILE: 12 | MANIFEST = json.load(MANIFEST_FILE) 13 | REQUIREMENTS: list[str] = MANIFEST["requirements"] 14 | 15 | pip.main(PIP_ARGUMENTS + REQUIREMENTS) 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" # See documentation for possible values 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "daily" 7 | time: "06:00" 8 | timezone: "Europe/Stockholm" 9 | labels: 10 | - "dependencies" 11 | 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "daily" 16 | time: "04:00" 17 | timezone: "Europe/Stockholm" 18 | labels: 19 | - "dependencies" -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | repository: 2 | private: false 3 | has_issues: true 4 | has_projects: false 5 | has_wiki: false 6 | has_downloads: false 7 | default_branch: master 8 | allow_squash_merge: true 9 | allow_merge_commit: false 10 | allow_rebase_merge: false 11 | labels: 12 | - name: "Feature Request" 13 | color: "fbca04" 14 | - name: "Bug" 15 | color: "b60205" 16 | - name: "Wont Fix" 17 | color: "ffffff" 18 | - name: "Enhancement" 19 | color: a2eeef 20 | - name: "Documentation" 21 | color: "008672" 22 | - name: "Stale" 23 | color: "930191" -------------------------------------------------------------------------------- /custom_components/volkswagen_we_connect_id/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "volkswagen_we_connect_id", 3 | "name": "Volkswagen We Connect ID", 4 | "config_flow": true, 5 | "documentation": "https://github.com/mitch-dc/volkswagen_we_connect_id#readme", 6 | "requirements": ["weconnect==0.60.8", "ascii_magic>=2.0.0"], 7 | "ssdp": [], 8 | "zeroconf": [], 9 | "homekit": {}, 10 | "dependencies": [], 11 | "loggers": ["weconnect"], 12 | "issue_tracker": "https://github.com/mitch-dc/volkswagen_we_connect_id/issues", 13 | "codeowners": [ 14 | "@mitch-dc" 15 | ], 16 | "iot_class": "cloud_polling", 17 | "version": "1.0" 18 | } 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /custom_components/volkswagen_we_connect_id/translations/sensor.nb.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "_": { 4 | "locked": "Låst", 5 | "unlocked": "Ulåst", 6 | "unknown lock state": "Ukjent låsstatus", 7 | "closed": "Lukket", 8 | "open": "Åpen", 9 | "invalid": "Ugyldig", 10 | "manual": "Manuell", 11 | "notReadyForCharging": "Ikke klar for opplading", 12 | "charging": "Lader", 13 | "readyForCharging": "Klar for opplading", 14 | "connected": "Tilkoblet", 15 | "disconnected": "Frakoblet", 16 | "maximum": "Maksimum", 17 | "reduced": "Redusert", 18 | "heating": "Varmer", 19 | "default": "Standard", 20 | "safe": "Sikker", 21 | "unsafe": "Usikker" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /custom_components/volkswagen_we_connect_id/translations/sensor.sv.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "_": { 4 | "locked": "Låst", 5 | "unlocked": "Olåst", 6 | "unknown lock state": "Okänt låsläge", 7 | "closed": "Stängd", 8 | "open": "Öppen", 9 | "invalid": "Ogiltig", 10 | "manual": "Manuell", 11 | "notReadyForCharging": "Inte redo för laddning", 12 | "charging": "Laddar", 13 | "readyForCharging": "Redo för laddning", 14 | "connected": "Ansluten", 15 | "disconnected": "Frånkopplad", 16 | "maximum": "Maximal", 17 | "reduced": "Reducerad", 18 | "heating": "Värmer", 19 | "default": "Standard", 20 | "safe": "Säker", 21 | "unsafe": "Osäker" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 14 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | #exemptLabels: 7 | # - pinned 8 | # - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: Stale 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. 15 | # Comment to post when closing a stale issue. Set to `false` to disable 16 | closeComment: false -------------------------------------------------------------------------------- /custom_components/volkswagen_we_connect_id/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "data": { 6 | "host": "[%key:common::config_flow::data::host%]", 7 | "username": "[%key:common::config_flow::data::username%]", 8 | "password": "[%key:common::config_flow::data::password%]", 9 | "update_interval": "Update interval (seconds)" 10 | } 11 | } 12 | }, 13 | "error": { 14 | "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", 15 | "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", 16 | "unknown": "[%key:common::config_flow::error::unknown%]" 17 | }, 18 | "abort": { 19 | "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /custom_components/volkswagen_we_connect_id/translations/sensor.da.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "_": { 4 | "locked": "Låst", 5 | "unlocked": "Låst op", 6 | "unknown lock state": "Ukendt låsestatus", 7 | "closed": "Lukket", 8 | "open": "Åben", 9 | "invalid": "Ugyldig", 10 | "manual": "Manuel", 11 | "notReadyForCharging": "Ikke klar til opladning", 12 | "charging": "Oplader", 13 | "readyForCharging": "Klar til opladning", 14 | "connected": "Tilsluttet", 15 | "disconnected": "Afbrudt", 16 | "maximum": "Maksimum", 17 | "reduced": "Reduceret", 18 | "heating": "Opvarmer", 19 | "default": "Standard", 20 | "safe": "Sikker", 21 | "unsafe": "Usikker" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /custom_components/volkswagen_we_connect_id/translations/sensor.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "_": { 4 | "locked": "Locked", 5 | "unlocked": "Unlocked", 6 | "unknown lock state": "Unknown lock state", 7 | "closed": "Closed", 8 | "open": "Open", 9 | "invalid": "Invalid", 10 | "manual": "Manual", 11 | "notReadyForCharging": "Not Ready For Charging", 12 | "charging": "Charging", 13 | "readyForCharging": "Ready for charging", 14 | "connected": "Connected", 15 | "disconnected": "Disconnected", 16 | "maximum": "Maximum", 17 | "reduced": "Reduced", 18 | "heating": "Heating", 19 | "default": "Default", 20 | "safe": "Safe", 21 | "unsafe": "Unsafe" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /custom_components/volkswagen_we_connect_id/translations/sensor.fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "_": { 4 | "locked": "Verrouillé", 5 | "unlocked": "Déverrouillé", 6 | "unknown lock state": "Verrouillage inconnu", 7 | "closed": "Fermé", 8 | "open": "Ouvert", 9 | "invalid": "Invalide", 10 | "manual": "Manuel", 11 | "notReadyForCharging": "Pas prêt à charger", 12 | "charging": "Charge", 13 | "readyForCharging": "Prêt à charger", 14 | "connected": "Connecté", 15 | "disconnected": "Déconnecté", 16 | "maximum": "Maximal", 17 | "reduced": "Réduit", 18 | "heating": "Chauffage", 19 | "default": "Par défaut", 20 | "safe": "Sûr", 21 | "unsafe": "Pas sûr" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /custom_components/volkswagen_we_connect_id/translations/sensor.de.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "_": { 4 | "locked": "Abgeschlossen", 5 | "unlocked": "Offen", 6 | "unknown lock state": "Verriegelung unklar", 7 | "closed": "Geschlossen", 8 | "open": "Offen", 9 | "invalid": "Ungültig", 10 | "manual": "Manuell", 11 | "notReadyForCharging": "Nicht bereit zum Laden", 12 | "charging": "Lädt", 13 | "readyForCharging": "Bereit zum Laden", 14 | "connected": "Verbunden", 15 | "disconnected": "Getrennt", 16 | "maximum": "Maxmimal", 17 | "reduced": "Verringert", 18 | "heating": "Heizung", 19 | "default": "Standard", 20 | "safe": "Sicher", 21 | "unsafe": "Unsicher" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /custom_components/volkswagen_we_connect_id/translations/sensor.es.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "_": { 4 | "locked": "Bloqueado", 5 | "unlocked": "Desbloqueado", 6 | "unknown lock state": "Estado de bloqueo desconocido", 7 | "closed": "Cerrado", 8 | "open": "Abierto", 9 | "invalid": "Inválido", 10 | "manual": "Manual", 11 | "notReadyForCharging": "No está listo para cargar", 12 | "charging": "Cargando", 13 | "readyForCharging": "Listo para cargar", 14 | "connected": "Conectado", 15 | "disconnected": "Desconectado", 16 | "maximum": "Máximo", 17 | "reduced": "Reducido", 18 | "heating": "Calefacción", 19 | "default": "Por defecto", 20 | "safe": "Seguro", 21 | "unsafe": "Inseguro" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /custom_components/volkswagen_we_connect_id/translations/sensor.it.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "_": { 4 | "locked": "Bloccato", 5 | "unlocked": "Sbloccato", 6 | "unknown lock state": "Stato di blocco sconosciuto", 7 | "closed": "Chiuso", 8 | "open": "Aperto", 9 | "invalid": "Invalido", 10 | "manual": "Manuale", 11 | "notReadyForCharging": "Non pronto per la ricarica", 12 | "charging": "In carica", 13 | "readyForCharging": "Pronto per la ricarica", 14 | "connected": "Connesso", 15 | "disconnected": "Disconnesso", 16 | "maximum": "Massimo", 17 | "reduced": "Ridotto", 18 | "heating": "Riscaldamento", 19 | "default": "Default", 20 | "safe": "Sicuro", 21 | "unsafe": "Non sicuro" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /custom_components/volkswagen_we_connect_id/translations/sensor.nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "_": { 4 | "locked": "Vergrendeld", 5 | "unlocked": "Ontgrendeld", 6 | "unknown lock state": "Onbekende Vergrendelingsstatus", 7 | "closed": "Gesloten", 8 | "open": "Open", 9 | "invalid": "Ongeldig", 10 | "manual": "Handmatig", 11 | "notReadyForCharging": "Niet gereed om op te laden", 12 | "charging": "Opladen", 13 | "readyForCharging": "Gereed om op te laden", 14 | "connected": "Verbonden", 15 | "disconnected": "Losgekoppeld", 16 | "maximum": "Maximaal", 17 | "reduced": "Verminderd", 18 | "heating": "Verwarming", 19 | "default": "Standaard", 20 | "safe": "Veilig", 21 | "unsafe": "onveilig" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /custom_components/volkswagen_we_connect_id/translations/sensor.pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "_": { 4 | "locked": "Bloqueado", 5 | "unlocked": "Desbloqueado", 6 | "unknown lock state": "Estado de bloqueio desconhecido", 7 | "closed": "Fechado", 8 | "open": "Aberto", 9 | "invalid": "Inválido", 10 | "manual": "Manual", 11 | "notReadyForCharging": "Não está pronto para carregar", 12 | "charging": "A carregar", 13 | "readyForCharging": "Pronto para carregar", 14 | "connected": "Conectado", 15 | "disconnected": "Desconectado", 16 | "maximum": "Máximo", 17 | "reduced": "Reduzido", 18 | "heating": "Aquecimento", 19 | "default": "Por padrão", 20 | "safe": "Seguro", 21 | "unsafe": "Inseguro" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | 16 | 17 | 18 | ## Version of the custom_component 19 | 21 | 22 | 23 | ## Installation method (hacs / manual) 24 | 25 | 26 | 27 | ## Installation method of hass (venv, docker, hassio,...) 28 | 29 | 30 | 31 | ## Configuration 32 | 33 | ```yaml 34 | 35 | Add your configuration here. 36 | 37 | ``` 38 | 39 | ## Describe the bug 40 | 41 | A clear and concise description of what the bug is. 42 | 43 | ## Debug log 44 | 45 | 46 | 47 | ```text 48 | 49 | Add your logs here. 50 | 51 | ``` -------------------------------------------------------------------------------- /custom_components/volkswagen_we_connect_id/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Device is already configured" 5 | }, 6 | "error": { 7 | "cannot_connect": "Failed to connect", 8 | "invalid_auth": "Invalid authentication", 9 | "unknown": "Unexpected error" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "host": "Host", 15 | "password": "Password", 16 | "username": "Username", 17 | "update_interval": "Update interval (seconds)" 18 | } 19 | } 20 | } 21 | }, 22 | "options": { 23 | "error": { 24 | "cannot_connect": "Failed to connect", 25 | "invalid_auth": "Invalid authentication", 26 | "unknown": "Unexpected error" 27 | }, 28 | "step": { 29 | "init": { 30 | "data": { 31 | "host": "Host", 32 | "password": "Password", 33 | "username": "Username", 34 | "update_interval": "Update interval (seconds)" 35 | } 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guidelines 2 | 3 | Contributing to this project should be as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | 10 | ## Github is used for everything 11 | 12 | Github is used to host code, to track issues and feature requests, as well as accept pull requests. 13 | 14 | Pull requests are the best way to propose changes to the codebase. 15 | 16 | 1. Fork the repo and create your branch from `master`. 17 | 2. If you've changed something, update the documentation. 18 | 3. Make sure your code lints (using black). 19 | 4. Issue that pull request! 20 | 21 | ## Any contributions you make will be under the MIT Software License 22 | 23 | In short, when you submit code changes, your submissions are understood to be under the same [Apache License 2.0](https://choosealicense.com/licenses/apache-2.0/) that covers the project. Feel free to contact the maintainers if that's a concern. 24 | 25 | ## Report bugs using Github's [issues](../../issues) 26 | 27 | GitHub issues are used to track public bugs. 28 | Report a bug by [opening a new issue](../../issues/new/choose); it's that easy! 29 | 30 | ## Write bug reports with detail, background, and sample code 31 | 32 | **Great Bug Reports** tend to have: 33 | 34 | - A quick summary and/or background 35 | - Steps to reproduce 36 | - Be specific! 37 | - Give sample code if you can. 38 | - What you expected would happen 39 | - What actually happens 40 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 41 | 42 | People *love* thorough bug reports. I'm not even kidding. 43 | 44 | ## Use a Consistent Coding Style 45 | 46 | Follow the [official style guidelines](https://developers.home-assistant.io/docs/en/development_guidelines.html) of home-assistant 47 | 48 | ## License 49 | 50 | By contributing, you agree that your contributions will be licensed under its Apache License 2.0. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Workstation OS files 2 | .DS_Store 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | .vscode/settings.json 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Because I no longer have access to a Volkswagen ID vehicle, development on this integration has been discontinued. 2 | 3 | 4 | 5 | 6 | # Volkswagen We Connect ID [ONLY FOR EUROPE] 7 | _Volkswagen We Connect ID sensor provides statistics from the Volkswagen ID Api thru [WeConnect-python lib](https://pypi.org/project/weconnect/)._ 8 | 9 | **This component will set up the following platforms.** 10 | 11 | Platform | Description 12 | -- | -- 13 | `sensor` | Show information from your Volkswagen ID car. 14 | `button` | Start climatization in your Volkswagen ID car. 15 | 16 | ![image](https://user-images.githubusercontent.com/15835274/149675681-a0c6804c-3179-4fd3-ad74-ab489c8986dd.png) 17 | 18 | 19 | ## Installation 20 | 21 | ### HACS 22 | The easiest way to add the component to your Home Assistant installation is 23 | using [HACS](https://hacs.xyz). Add this GitHub repository as a [custom 24 | repository](https://hacs.xyz/docs/faq/custom_repositories), then follow the 25 | instructions under [Configuration](#configuration) below. 26 | 27 | ### Manual 28 | 29 | 1. Using the tool of choice open the directory (folder) for your HA configuration (where you find `configuration.yaml`). 30 | 2. If you do not have a `custom_components` directory (folder) there, you need to create it. 31 | 3. In the `custom_components` directory (folder) create a new folder called `volkswagen_we_connect_id`. 32 | 4. Download _all_ the files from the `custom_components/volkswagen_we_connect_id/` directory (folder) in this repository. 33 | 5. Place the files you downloaded in the new directory (folder) you created. 34 | 6. Follow the instructions under [Configuration](#configuration) below. 35 | 36 | Using your HA configuration directory (folder) as a starting point you should now also have this: 37 | 38 | ```text 39 | custom_components/volkswagen_we_connect_id/__init__.py 40 | custom_components/volkswagen_we_connect_id/manifest.json 41 | custom_components/volkswagen_we_connect_id/sensor.py 42 | .. etc 43 | ``` 44 | 45 | ## Configuration 46 | 47 | It's important that you first use the app, connect the app to the car and use it at least once. 48 | After that enable the integration on the integration page in Home Assistant with your e-mail and password that you use to login into the app. Wait a couple of seconds and 1 or more devices (your cars) with entities will show up. 49 | 50 | ## Tested Cars 51 | _This integration only works with cars sold in Europe and use the WeConnect ID app_ 52 | 53 | * Volkswagen ID.3 54 | * Volkswagen ID.4 55 | * Volkswagen ID.5 56 | * Volkswagen ID.7 57 | * Volkswagen ID Buzz 58 | * Volkswagen Tiguan (MKII) 59 | 60 | ## Requirements 61 | 62 | Home Assistant Core *2023.3.1* or higher 63 | 64 | -------------------------------------------------------------------------------- /custom_components/volkswagen_we_connect_id/services.yaml: -------------------------------------------------------------------------------- 1 | volkswagen_id_start_stop_charging: 2 | name: Volkswagen ID Start or Stop Charging 3 | description: Starts or stops charging of Volkswagen ID cars. 4 | fields: 5 | vin: 6 | name: VIN 7 | description: Vehicle identification number for the car. 8 | required: true 9 | example: WVGZZZA1ZMP001337 10 | selector: 11 | text: 12 | start_stop: 13 | name: Start or Stop 14 | description: Starts or stops charging. 15 | required: true 16 | selector: 17 | select: 18 | options: 19 | - "start" 20 | - "stop" 21 | 22 | volkswagen_id_set_climatisation: 23 | name: Volkswagen ID Set Climatisation 24 | description: Sets climatisation in Volkswagen ID cars. 25 | fields: 26 | vin: 27 | name: VIN 28 | description: Vehicle identification number for the car. 29 | required: true 30 | example: WVGZZZA1ZMP001337 31 | selector: 32 | text: 33 | start_stop: 34 | name: Start or Stop 35 | description: Starts or stops climatisation. 36 | required: true 37 | selector: 38 | select: 39 | options: 40 | - "start" 41 | - "stop" 42 | target_temp: 43 | name: Target Temperature. 44 | description: Sets target temperature in celsius. 45 | required: false 46 | selector: 47 | number: 48 | min: 10 49 | max: 30 50 | unit_of_measurement: "ºC" 51 | 52 | volkswagen_id_set_target_soc: 53 | name: Volkswagen ID Set Target SoC 54 | description: Sets the target SoC in Volkswagen ID cars. 55 | fields: 56 | vin: 57 | name: VIN 58 | description: Vehicle identification number for the car. 59 | required: true 60 | example: WVGZZZA1ZMP001337 61 | selector: 62 | text: 63 | target_soc: 64 | name: Target State of Charge. 65 | description: Sets state of charge in percentage. 66 | required: true 67 | selector: 68 | number: 69 | min: 10 70 | max: 100 71 | step: 10 72 | unit_of_measurement: "%" 73 | 74 | volkswagen_id_set_ac_charge_speed: 75 | name: Volkswagen ID Set AC Charge speed 76 | description: Sets the AC charging speed in Volkswagen ID cars. 77 | fields: 78 | vin: 79 | name: VIN 80 | description: Vehicle identification number for the car. 81 | required: true 82 | example: WVGZZZA1ZMP001337 83 | selector: 84 | text: 85 | maximum_reduced: 86 | name: Maximum or reduced 87 | description: Maximum (default) charging speed or reduced speed. Actual maximum/reduced speed depends on charging station. 88 | required: true 89 | selector: 90 | select: 91 | options: 92 | - "maximum" 93 | - "reduced" 94 | -------------------------------------------------------------------------------- /custom_components/volkswagen_we_connect_id/device_tracker.py: -------------------------------------------------------------------------------- 1 | """ 2 | Support for Volkswagen WeConnect Platform 3 | """ 4 | import logging 5 | 6 | from weconnect import weconnect 7 | 8 | from homeassistant.components.device_tracker import SourceType 9 | from homeassistant.components.device_tracker.config_entry import TrackerEntity 10 | from homeassistant.config_entries import ConfigEntry 11 | from homeassistant.core import HomeAssistant 12 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 13 | from homeassistant.helpers.dispatcher import async_dispatcher_connect 14 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator 15 | from homeassistant.util import slugify 16 | 17 | from . import DomainEntry, VolkswagenIDBaseEntity, get_object_value 18 | from .const import DOMAIN 19 | 20 | _LOGGER = logging.getLogger(__name__) 21 | 22 | 23 | async def async_setup_entry( 24 | hass: HomeAssistant, 25 | config_entry: ConfigEntry, 26 | async_add_entities: AddEntitiesCallback, 27 | ): 28 | """Add sensors for passed config_entry in HA.""" 29 | 30 | domain_entry: DomainEntry = hass.data[DOMAIN][config_entry.entry_id] 31 | we_connect = domain_entry.we_connect 32 | coordinator = domain_entry.coordinator 33 | 34 | # Fetch initial data so we have data when entities subscribe 35 | await coordinator.async_config_entry_first_refresh() 36 | 37 | entities = [] 38 | 39 | for index, vehicle in enumerate(coordinator.data): 40 | entities.append(VolkswagenIDSensor(we_connect, coordinator, index)) 41 | 42 | if entities: 43 | async_add_entities(entities) 44 | 45 | 46 | class VolkswagenIDSensor(VolkswagenIDBaseEntity, TrackerEntity): 47 | """Representation of a VolkswagenID vehicle sensor.""" 48 | 49 | def __init__( 50 | self, 51 | we_connect: weconnect.WeConnect, 52 | coordinator: DataUpdateCoordinator, 53 | index: int, 54 | ) -> None: 55 | """Initialize VolkswagenID vehicle sensor.""" 56 | super().__init__(we_connect, coordinator, index) 57 | 58 | self._coordinator = coordinator 59 | self._attr_name = f"{self.data.nickname} tracker" 60 | self._attr_unique_id = f"{self.data.vin}-tracker" 61 | 62 | @property 63 | def latitude(self) -> float: 64 | """Return latitude value of the device.""" 65 | try: 66 | return get_object_value( 67 | self.data.domains["parking"]["parkingPosition"].latitude.value 68 | ) 69 | except KeyError: 70 | return None 71 | 72 | @property 73 | def longitude(self) -> float: 74 | """Return longitude value of the device.""" 75 | try: 76 | return get_object_value( 77 | self.data.domains["parking"]["parkingPosition"].longitude.value 78 | ) 79 | except KeyError: 80 | return None 81 | 82 | @property 83 | def source_type(self): 84 | """Return the source type, eg gps or router, of the device.""" 85 | return SourceType.GPS 86 | 87 | @property 88 | def icon(self): 89 | """Return the icon.""" 90 | return "mdi:car" 91 | 92 | @property 93 | def extra_state_attributes(self): 94 | """Return timestamp of when the data was captured.""" 95 | try: 96 | return { 97 | "last_captured": get_object_value( 98 | self.data.domains["parking"][ 99 | "parkingPosition" 100 | ].carCapturedTimestamp.value 101 | ) 102 | } 103 | except KeyError: 104 | return None 105 | -------------------------------------------------------------------------------- /custom_components/volkswagen_we_connect_id/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for Volkswagen We Connect ID integration.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | from typing import Any 6 | 7 | import voluptuous as vol 8 | from weconnect.errors import AuthentificationError 9 | 10 | from homeassistant import config_entries 11 | from homeassistant.core import HomeAssistant, callback 12 | from homeassistant.data_entry_flow import FlowResult 13 | from homeassistant.exceptions import HomeAssistantError 14 | 15 | from . import get_parameter, get_we_connect_api, update 16 | from .const import DOMAIN, DEFAULT_UPDATE_INTERVAL_SECONDS, MINIMUM_UPDATE_INTERVAL_SECONDS 17 | 18 | _LOGGER = logging.getLogger(__name__) 19 | 20 | STEP_USER_DATA_SCHEMA = vol.Schema( 21 | { 22 | vol.Required("username"): str, 23 | vol.Required("password"): str, 24 | vol.Optional( 25 | "update_interval", default=DEFAULT_UPDATE_INTERVAL_SECONDS 26 | ): vol.All(vol.Coerce(int), vol.Range(min=MINIMUM_UPDATE_INTERVAL_SECONDS)), 27 | } 28 | ) 29 | 30 | 31 | async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: 32 | """Validate the user input allows us to connect.""" 33 | 34 | we_connect = get_we_connect_api( 35 | username=data["username"], 36 | password=data["password"], 37 | ) 38 | 39 | await hass.async_add_executor_job(we_connect.login) 40 | await hass.async_add_executor_job(update, we_connect) 41 | 42 | # vin = next(iter(we_connect.vehicles.items()))[0] 43 | 44 | return {"title": "Volkswagen We Connect ID"} 45 | 46 | 47 | class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 48 | """Handle a config flow for Volkswagen We Connect ID.""" 49 | 50 | VERSION = 1 51 | MINOR_VERSION = 2 52 | 53 | @staticmethod 54 | @callback 55 | def async_get_options_flow( 56 | config_entry: config_entries.ConfigEntry, 57 | ) -> OptionsFlowHandler: 58 | """Create the options flow.""" 59 | return OptionsFlowHandler() 60 | 61 | async def async_step_user( 62 | self, user_input: dict[str, Any] | None = None 63 | ) -> FlowResult: 64 | """Handle the initial step.""" 65 | if user_input is None: 66 | return self.async_show_form( 67 | step_id="user", data_schema=STEP_USER_DATA_SCHEMA 68 | ) 69 | 70 | errors = {} 71 | 72 | try: 73 | info = await validate_input(self.hass, user_input) 74 | except CannotConnect: 75 | errors["base"] = "cannot_connect" 76 | except AuthentificationError: 77 | errors["base"] = "invalid_auth" 78 | except Exception: # pylint: disable=broad-except 79 | _LOGGER.exception("Unexpected exception") 80 | errors["base"] = "unknown" 81 | else: 82 | return self.async_create_entry(title=info["title"], data=user_input) 83 | 84 | return self.async_show_form( 85 | step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors 86 | ) 87 | 88 | 89 | class OptionsFlowHandler(config_entries.OptionsFlow): 90 | """Options flow handler""" 91 | 92 | 93 | async def async_step_init(self, user_input: dict[str, Any] | None = None) -> FlowResult: 94 | """Manage the options.""" 95 | 96 | errors = {} 97 | 98 | if user_input is not None: 99 | try: 100 | info = await validate_input(self.hass, user_input) 101 | except CannotConnect: 102 | errors["base"] = "cannot_connect" 103 | except AuthentificationError: 104 | errors["base"] = "invalid_auth" 105 | except Exception: 106 | _LOGGER.exception("Unexpected exception") 107 | errors["base"] = "unknown" 108 | else: 109 | return self.async_create_entry(title=info["title"], data=user_input) 110 | 111 | return self.async_show_form( 112 | step_id="init", 113 | data_schema=vol.Schema( 114 | { 115 | vol.Required("username", default=get_parameter(self.config_entry, "username")): str, 116 | vol.Required("password", default=get_parameter(self.config_entry, "password")): str, 117 | vol.Optional( 118 | "update_interval", default=get_parameter(self.config_entry, "update_interval", DEFAULT_UPDATE_INTERVAL_SECONDS) 119 | ): vol.All(vol.Coerce(int), vol.Range(min=MINIMUM_UPDATE_INTERVAL_SECONDS)), 120 | } 121 | ), 122 | errors=errors, 123 | ) 124 | 125 | class CannotConnect(HomeAssistantError): 126 | """Error to indicate we cannot connect.""" 127 | -------------------------------------------------------------------------------- /custom_components/volkswagen_we_connect_id/number.py: -------------------------------------------------------------------------------- 1 | """Entity representing a Volkswagen number control.""" 2 | from __future__ import annotations 3 | 4 | from weconnect import weconnect 5 | 6 | from homeassistant.components.number import NumberEntity 7 | from homeassistant.config_entries import ConfigEntry 8 | from homeassistant.core import HomeAssistant 9 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 10 | from homeassistant.helpers.entity import EntityCategory 11 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator 12 | 13 | from . import ( 14 | DomainEntry, 15 | VolkswagenIDBaseEntity, 16 | get_object_value, 17 | set_climatisation, 18 | set_target_soc, 19 | ) 20 | from .const import DOMAIN 21 | 22 | from homeassistant.const import ( 23 | PERCENTAGE, 24 | UnitOfTemperature, 25 | ) 26 | 27 | 28 | async def async_setup_entry( 29 | hass: HomeAssistant, 30 | config_entry: ConfigEntry, 31 | async_add_entities: AddEntitiesCallback, 32 | ): 33 | """Add buttons for passed config_entry in HA.""" 34 | domain_entry: DomainEntry = hass.data[DOMAIN][config_entry.entry_id] 35 | we_connect = domain_entry.we_connect 36 | coordinator = domain_entry.coordinator 37 | 38 | # Fetch initial data so we have data when entities subscribe 39 | await coordinator.async_config_entry_first_refresh() 40 | 41 | entities = [] 42 | 43 | for index, vehicle in enumerate(coordinator.data): 44 | entities.append(TargetSoCNumber(we_connect, coordinator, index)) 45 | entities.append(TargetClimateNumber(we_connect, coordinator, index)) 46 | if entities: 47 | async_add_entities(entities) 48 | 49 | 50 | class TargetSoCNumber(VolkswagenIDBaseEntity, NumberEntity): 51 | """Representation of a Target SoC entity.""" 52 | 53 | _attr_entity_category = EntityCategory.CONFIG 54 | 55 | def __init__( 56 | self, 57 | we_connect: weconnect.WeConnect, 58 | coordinator: DataUpdateCoordinator, 59 | index: int, 60 | ) -> None: 61 | """Initialize VolkswagenID vehicle sensor.""" 62 | super().__init__(we_connect, coordinator, index) 63 | 64 | self._coordinator = coordinator 65 | self._attr_name = f"{self.data.nickname} Target State Of Charge" 66 | self._attr_unique_id = f"{self.data.vin}-target_state_of_charge" 67 | self._attr_icon = "mdi:battery" 68 | self._we_connect = we_connect 69 | self._attr_native_min_value = 10 70 | self._attr_native_max_value = 100 71 | self._attr_native_step = 10 72 | self._attr_native_unit_of_measurement = PERCENTAGE 73 | 74 | @property 75 | def native_value(self) -> float | None: 76 | """Return the value reported by the number.""" 77 | return int( 78 | get_object_value( 79 | self.data.domains["charging"]["chargingSettings"].targetSOC_pct.value 80 | ) 81 | ) 82 | 83 | async def async_set_native_value(self, value: float) -> None: 84 | """Update the current value.""" 85 | if value > 10: 86 | await self.hass.async_add_executor_job( 87 | set_target_soc, 88 | self.data.vin.value, 89 | self._we_connect, 90 | value, 91 | ) 92 | 93 | 94 | class TargetClimateNumber(VolkswagenIDBaseEntity, NumberEntity): 95 | """Representation of a Target Climate entity.""" 96 | 97 | _attr_entity_category = EntityCategory.CONFIG 98 | 99 | def __init__( 100 | self, 101 | we_connect: weconnect.WeConnect, 102 | coordinator: DataUpdateCoordinator, 103 | index: int, 104 | ) -> None: 105 | """Initialize VolkswagenID vehicle sensor.""" 106 | super().__init__(we_connect, coordinator, index) 107 | 108 | self._coordinator = coordinator 109 | self._attr_name = f"{self.data.nickname} Target Climate Temperature" 110 | self._attr_unique_id = f"{self.data.vin}-target_climate_temperature" 111 | self._attr_icon = "mdi:thermometer" 112 | self._we_connect = we_connect 113 | self._attr_native_min_value = 10 114 | self._attr_native_max_value = 30 115 | self._attr_native_step = 0.5 116 | self._attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS 117 | 118 | @property 119 | def native_value(self) -> float | None: 120 | """Return the value reported by the number.""" 121 | targetTemp = self.data.domains["climatisation"][ 122 | "climatisationSettings" 123 | ].targetTemperature_C.value 124 | 125 | return float(targetTemp) 126 | 127 | async def async_set_native_value(self, value: float) -> None: 128 | """Update the current value.""" 129 | if value > 10: 130 | self._attr_native_value = value 131 | await self.hass.async_add_executor_job( 132 | set_climatisation, self.data.vin.value, self._we_connect, "none", value 133 | ) 134 | -------------------------------------------------------------------------------- /custom_components/volkswagen_we_connect_id/button.py: -------------------------------------------------------------------------------- 1 | """Button integration.""" 2 | from weconnect import weconnect 3 | from weconnect.elements.vehicle import Vehicle 4 | 5 | from homeassistant.components.button import ButtonEntity 6 | from homeassistant.config_entries import ConfigEntry 7 | from homeassistant.core import HomeAssistant 8 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 9 | 10 | from . import ( 11 | DomainEntry, 12 | get_object_value, 13 | set_ac_charging_speed, 14 | set_climatisation, 15 | start_stop_charging, 16 | ) 17 | from .const import DOMAIN 18 | 19 | 20 | async def async_setup_entry( 21 | hass: HomeAssistant, 22 | config_entry: ConfigEntry, 23 | async_add_entities: AddEntitiesCallback, 24 | ) -> bool: 25 | """Add buttons for passed config_entry in HA.""" 26 | domain_entry: DomainEntry = hass.data[DOMAIN][config_entry.entry_id] 27 | we_connect = domain_entry.we_connect 28 | vehicles = domain_entry.vehicles 29 | 30 | entities = [] 31 | for vehicle in vehicles: # weConnect.vehicles.items(): 32 | entities.append(VolkswagenIDStartClimateButton(vehicle, we_connect)) 33 | entities.append(VolkswagenIDStopClimateButton(vehicle, we_connect)) 34 | entities.append(VolkswagenIDToggleACChargeSpeed(vehicle, we_connect)) 35 | entities.append(VolkswagenIDStartChargingButton(vehicle, we_connect)) 36 | entities.append(VolkswagenIDStopChargingButton(vehicle, we_connect)) 37 | 38 | async_add_entities(entities) 39 | 40 | return True 41 | 42 | 43 | class VolkswagenIDStartClimateButton(ButtonEntity): 44 | """Button for starting climate.""" 45 | 46 | def __init__(self, vehicle, we_connect) -> None: 47 | """Initialize VolkswagenID vehicle sensor.""" 48 | self._attr_name = f"{vehicle.nickname} Start Climate" 49 | self._attr_unique_id = f"{vehicle.vin}-start_climate" 50 | self._attr_icon = "mdi:fan-plus" 51 | self._we_connect = we_connect 52 | self._vehicle = vehicle 53 | 54 | def press(self) -> None: 55 | """Handle the button press.""" 56 | set_climatisation(self._vehicle.vin.value, self._we_connect, "start", 0) 57 | 58 | 59 | class VolkswagenIDStopClimateButton(ButtonEntity): 60 | """Button for stopping climate.""" 61 | 62 | def __init__(self, vehicle, we_connect) -> None: 63 | """Initialize VolkswagenID vehicle sensor.""" 64 | self._attr_name = f"{vehicle.nickname} Stop Climate" 65 | self._attr_unique_id = f"{vehicle.vin}-stop_climate" 66 | self._attr_icon = "mdi:fan-off" 67 | self._we_connect = we_connect 68 | self._vehicle = vehicle 69 | 70 | def press(self) -> None: 71 | """Handle the button press.""" 72 | set_climatisation(self._vehicle.vin.value, self._we_connect, "stop", 0) 73 | 74 | 75 | class VolkswagenIDToggleACChargeSpeed(ButtonEntity): 76 | """Button for toggling the charge speed.""" 77 | 78 | def __init__(self, vehicle: Vehicle, we_connect: weconnect.WeConnect) -> None: 79 | """Initialize VolkswagenID vehicle sensor.""" 80 | self._attr_name = f"{vehicle.nickname} Toggle AC Charge Speed" 81 | self._attr_unique_id = f"{vehicle.vin}-toggle_ac_charge_speed" 82 | self._attr_icon = "mdi:ev-station" 83 | self._we_connect = we_connect 84 | self._vehicle = vehicle 85 | 86 | def press(self) -> None: 87 | """Handle the button press.""" 88 | 89 | current_state = get_object_value( 90 | self._vehicle.domains["charging"]["chargingSettings"].maxChargeCurrentAC 91 | ) 92 | 93 | if current_state == "maximum": 94 | set_ac_charging_speed( 95 | self._vehicle.vin.value, 96 | self._we_connect, 97 | "reduced", 98 | ) 99 | else: 100 | set_ac_charging_speed( 101 | self._vehicle.vin.value, 102 | self._we_connect, 103 | "maximum", 104 | ) 105 | 106 | 107 | class VolkswagenIDStartChargingButton(ButtonEntity): 108 | """Button for start charging.""" 109 | 110 | def __init__(self, vehicle, we_connect) -> None: 111 | """Initialize VolkswagenID vehicle sensor.""" 112 | self._attr_name = f"{vehicle.nickname} Start Charging" 113 | self._attr_unique_id = f"{vehicle.vin}-start_charging" 114 | self._attr_icon = "mdi:play-circle-outline" 115 | self._we_connect = we_connect 116 | self._vehicle = vehicle 117 | 118 | def press(self) -> None: 119 | """Handle the button press.""" 120 | start_stop_charging(self._vehicle.vin.value, self._we_connect, "start") 121 | 122 | 123 | class VolkswagenIDStopChargingButton(ButtonEntity): 124 | """Button for stop charging.""" 125 | 126 | def __init__(self, vehicle, we_connect) -> None: 127 | """Initialize VolkswagenID vehicle sensor.""" 128 | self._attr_name = f"{vehicle.nickname} Stop Charging" 129 | self._attr_unique_id = f"{vehicle.vin}-stop_charging" 130 | self._attr_icon = "mdi:stop-circle-outline" 131 | self._we_connect = we_connect 132 | self._vehicle = vehicle 133 | 134 | def press(self) -> None: 135 | """Handle the button press.""" 136 | start_stop_charging(self._vehicle.vin.value, self._we_connect, "stop") 137 | -------------------------------------------------------------------------------- /custom_components/volkswagen_we_connect_id/binary_sensor.py: -------------------------------------------------------------------------------- 1 | """Binary_sensor integration.""" 2 | from __future__ import annotations 3 | 4 | from collections.abc import Callable 5 | from dataclasses import dataclass 6 | 7 | from weconnect import weconnect 8 | from weconnect.elements.plug_status import PlugStatus 9 | from weconnect.elements.lights_status import LightsStatus 10 | from weconnect.elements.window_heating_status import WindowHeatingStatus 11 | 12 | from homeassistant.config_entries import ConfigEntry 13 | from homeassistant.components.binary_sensor import ( 14 | BinarySensorDeviceClass, 15 | BinarySensorEntity, 16 | BinarySensorEntityDescription, 17 | ) 18 | from homeassistant.core import HomeAssistant 19 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 20 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator 21 | 22 | from . import DomainEntry, VolkswagenIDBaseEntity 23 | from .const import DOMAIN 24 | 25 | 26 | @dataclass 27 | class VolkswagenIdBinaryEntityDescription(BinarySensorEntityDescription): 28 | """Describes Volkswagen ID binary sensor entity.""" 29 | 30 | value: Callable = lambda x, y: x 31 | on_value: object | None = None 32 | enabled: Callable = lambda x, y: x 33 | 34 | 35 | SENSORS: tuple[VolkswagenIdBinaryEntityDescription, ...] = ( 36 | VolkswagenIdBinaryEntityDescription( 37 | key="climatisationWithoutExternalPower", 38 | name="Climatisation Without External Power", 39 | icon="mdi:fan", 40 | value=lambda data: data["climatisation"][ 41 | "climatisationSettings" 42 | ].climatisationWithoutExternalPower, 43 | ), 44 | VolkswagenIdBinaryEntityDescription( 45 | key="climatizationAtUnlock", 46 | name="Climatisation At Unlock", 47 | icon="mdi:fan", 48 | value=lambda data: data["climatisation"][ 49 | "climatisationSettings" 50 | ].climatizationAtUnlock, 51 | ), 52 | VolkswagenIdBinaryEntityDescription( 53 | key="zoneFrontLeftEnabled", 54 | name="Zone Front Left Enabled", 55 | icon="mdi:car-seat", 56 | value=lambda data: data["climatisation"][ 57 | "climatisationSettings" 58 | ].zoneFrontLeftEnabled, 59 | ), 60 | VolkswagenIdBinaryEntityDescription( 61 | key="zoneFrontRightEnabled", 62 | name="Zone Front Right Enabled", 63 | icon="mdi:car-seat", 64 | value=lambda data: data["climatisation"][ 65 | "climatisationSettings" 66 | ].zoneFrontRightEnabled, 67 | ), 68 | VolkswagenIdBinaryEntityDescription( 69 | key="windowHeatingEnabled", 70 | name="Window Heating Enabled", 71 | icon="mdi:car-defrost-front", 72 | value=lambda data: data["climatisation"][ 73 | "climatisationSettings" 74 | ].windowHeatingEnabled, 75 | ), 76 | VolkswagenIdBinaryEntityDescription( 77 | key="frontWindowHeatingState", 78 | name="Front Window Heating State", 79 | icon="mdi:car-defrost-front", 80 | value=lambda data: data["climatisation"]["windowHeatingStatus"] 81 | .windows["front"] 82 | .windowHeatingState, 83 | on_value=WindowHeatingStatus.Window.WindowHeatingState.ON, 84 | ), 85 | VolkswagenIdBinaryEntityDescription( 86 | key="rearWindowHeatingState", 87 | name="Rear Window Heating State", 88 | icon="mdi:car-defrost-rear", 89 | value=lambda data: data["climatisation"]["windowHeatingStatus"] 90 | .windows["rear"] 91 | .windowHeatingState, 92 | on_value=WindowHeatingStatus.Window.WindowHeatingState.ON, 93 | ), 94 | VolkswagenIdBinaryEntityDescription( 95 | key="insufficientBatteryLevelWarning", 96 | name="Insufficient Battery Level Warning", 97 | icon="mdi:battery-alert-variant-outline", 98 | value=lambda data: data["readiness"][ 99 | "readinessStatus" 100 | ].connectionWarning.insufficientBatteryLevelWarning, 101 | ), 102 | VolkswagenIdBinaryEntityDescription( 103 | name="Car Is Online", 104 | key="isOnline", 105 | value=lambda data: data["readiness"][ 106 | "readinessStatus" 107 | ].connectionState.isOnline, 108 | device_class=BinarySensorDeviceClass.CONNECTIVITY, 109 | ), 110 | VolkswagenIdBinaryEntityDescription( 111 | name="Car Is Active", 112 | key="isActive", 113 | icon="mdi:car-side", 114 | value=lambda data: data["readiness"][ 115 | "readinessStatus" 116 | ].connectionState.isActive, 117 | ), 118 | VolkswagenIdBinaryEntityDescription( 119 | name="Lights Right", 120 | key="lightsRight", 121 | icon="mdi:car-light-dimmed", 122 | value=lambda data: data["vehicleLights"]["lightsStatus"].lights["right"].status, 123 | on_value=LightsStatus.Light.LightState.ON, 124 | ), 125 | VolkswagenIdBinaryEntityDescription( 126 | name="Lights Left", 127 | key="lightsLeft", 128 | icon="mdi:car-light-dimmed", 129 | value=lambda data: data["vehicleLights"]["lightsStatus"].lights["left"].status, 130 | on_value=LightsStatus.Light.LightState.ON, 131 | ), 132 | ) 133 | 134 | 135 | async def async_setup_entry( 136 | hass: HomeAssistant, 137 | config_entry: ConfigEntry, 138 | async_add_entities: AddEntitiesCallback, 139 | ): 140 | """Add sensors for passed config_entry in HA.""" 141 | domain_entry: DomainEntry = hass.data[DOMAIN][config_entry.entry_id] 142 | we_connect = domain_entry.we_connect 143 | coordinator = domain_entry.coordinator 144 | 145 | # Fetch initial data so we have data when entities subscribe 146 | await coordinator.async_config_entry_first_refresh() 147 | 148 | entities: list[VolkswagenIDSensor] = [] 149 | 150 | for index, vehicle in enumerate(coordinator.data): 151 | for sensor in SENSORS: 152 | entities.append(VolkswagenIDSensor(sensor, we_connect, coordinator, index)) 153 | if entities: 154 | async_add_entities(entities) 155 | 156 | 157 | class VolkswagenIDSensor(VolkswagenIDBaseEntity, BinarySensorEntity): 158 | """Representation of a VolkswagenID vehicle sensor.""" 159 | 160 | entity_description: VolkswagenIdBinaryEntityDescription 161 | 162 | def __init__( 163 | self, 164 | sensor: VolkswagenIdBinaryEntityDescription, 165 | we_connect: weconnect.WeConnect, 166 | coordinator: DataUpdateCoordinator, 167 | index: int, 168 | ) -> None: 169 | """Initialize VolkswagenID vehicle sensor.""" 170 | super().__init__(we_connect, coordinator, index) 171 | 172 | self.entity_description = sensor 173 | self._coordinator = coordinator 174 | self._attr_name = f"{self.data.nickname} {sensor.name}" 175 | self._attr_unique_id = f"{self.data.vin}-{sensor.key}" 176 | 177 | @property 178 | def is_on(self) -> bool: 179 | """Return true if sensor is on.""" 180 | try: 181 | state = self.entity_description.value(self.data.domains) 182 | if state.enabled and isinstance(state.value, bool): 183 | return state.value 184 | 185 | return False 186 | 187 | except KeyError: 188 | return None 189 | -------------------------------------------------------------------------------- /info.md: -------------------------------------------------------------------------------- 1 | # Volkswagen We Connect ID 2 | 3 | This integration is only for the 'ID' cars from Volkswagen. 4 | 5 | ## Automation Examples 6 | 7 | My car is called Komo, and you will see that name in the examples. 8 | Replace the entities with the corresponding entities from your car. 9 | 10 | Start warming up the cabin when it's freezing outside 11 | ```yaml 12 | - alias: Car - Start preheating the cabin when it's freezing 13 | id: 4008ba82-bb35-4b2f-85b3-96dca152efd1 # Make this unique 14 | trigger: 15 | - platform: time 16 | at: 17 | - "07:00:00" 18 | condition: 19 | condition: numeric_state 20 | entity_id: weather.torentje 21 | attribute: temperature 22 | below: 1 23 | action: 24 | - service: volkswagen_we_connect_id.volkswagen_id_set_climatisation 25 | data: 26 | vin: WVGZZZE2ZMP4201337 27 | start_stop: start 28 | target_temp: 20 29 | ``` 30 | 31 | _My advice is to create a seperate automation for when the car actually starts heating up, the VW api is really slow and not really bullit proof._ 32 | 33 | Send messsage when car is heating up: 34 | ```yaml 35 | automation: 36 | - alias: Car - Notification when car started to heat up the cabin 37 | id: 919b5fbf-b1c7-41ec-a31f-3c9a14785bf0 # Make this unique 38 | trigger: 39 | - platform: state 40 | entity_id: sensor.volkswagen_id_komo_climatisation_state # Change entities 41 | to: "heating" 42 | action: 43 | - service: notify.mobile_app_mitchells_ifoon_app # Change entities 44 | data: 45 | message: "🔥 Komo is heating up the cabin to {{states.sensor.volkswagen_id_komo_target_temperature.state}}°C. It's done at {{ (now()|as_timestamp + (float(states('sensor.volkswagen_id_komo_remaining_climatisation_time')) * 60 ))|timestamp_custom('%H:%M', True) }}." # Change entities or change the message 46 | data: 47 | url: "/lovelace-car/car" 48 | push: 49 | thread-id: "car-group" 50 | ``` 51 | 52 | Send messsage when car is done charging: 53 | ```yaml 54 | - alias: Car - Notification when car is done charging 55 | id: 22a8347c-4001-4795-a3de-71a89a428806 # Make this unique 56 | trigger: 57 | - platform: template 58 | value_template: "{{ states.sensor.volkswagen_id_komo_state_of_charge.state == states.sensor.volkswagen_id_komo_target_state_of_charge.state }}" # Change entities 59 | action: 60 | - service: notify.mobile_app_mitchells_ifoon_app # Change entities 61 | data: 62 | message: "🔋 Komo is done charging, current range is now {{states.sensor.volkswagen_id_komo_range.state}}km." # Change entities 63 | data: 64 | url: "/lovelace-car/car" 65 | push: 66 | thread-id: "car-group" 67 | ``` 68 | 69 | Send messsage when car has an error while charging: 70 | ```yaml 71 | - alias: Car - Notification when car errored 72 | id: 791366e0-df10-465a-8706-30017d08ea91 # Make this unique 73 | trigger: 74 | - platform: state 75 | entity_id: sensor.volkswagen_id_komo_charging_state # Change entities 76 | from: "charging" 77 | to: "error" 78 | action: 79 | - service: notify.mobile_app_mitchells_ifoon_app # Change entities 80 | data: 81 | message: "🚨 Komo has an error while charging with {{states.sensor.volkswagen_id_komo_range.state}}km range." # Change entities 82 | data: 83 | url: "/lovelace-car/car" 84 | push: 85 | thread-id: "car-group" 86 | ``` 87 | 88 | Send messsage when car started charging: 89 | ```yaml 90 | - alias: Car - Notification when car started charging 91 | id: dadf86c7-fd05-4aff-9891-e3d9f1eaf4dc # Make this unique 92 | trigger: 93 | - platform: state 94 | entity_id: sensor.volkswagen_id_komo_charging_state # Change entities 95 | to: "charging" 96 | action: 97 | - service: notify.mobile_app_mitchells_ifoon_app # Change entities 98 | data: 99 | message: "⚡ Komo started charging. It's done at {{ (now()|as_timestamp + (float(states('sensor.volkswagen_id_komo_remaining_charging_time')) * 60 ))|timestamp_custom('%H:%M', True) }} and will have {{states.sensor.volkswagen_id_komo_target_state_of_charge.state}}% battery." # Change entities 100 | data: 101 | url: "/lovelace-car/car" 102 | push: 103 | thread-id: "car-group" 104 | ``` 105 | 106 | 107 | ## Lovelace Examples 108 | ![image](https://user-images.githubusercontent.com/15835274/152117284-f0f6cd6e-02aa-4745-bc8d-906b8da781e6.png) 109 | 110 | This example is by Puch-TDI (https://github.com/Puch-tdi) 111 | 112 | ```yaml 113 | type: vertical-stack 114 | cards: 115 | - type: entities 116 | entities: 117 | - entity: sensor.volkswagen_id_id_4_pro_performance_range 118 | name: ID4 Actieradius 119 | icon: mdi:speedometer 120 | - entity: binary_sensor.volkswagen_id_id_4_pro_performance_plug_connection_state 121 | name: Connected 122 | - entity: sensor.volkswagen_id_id_4_pro_performance_charge_rate 123 | name: Laadsnelheid Km/H 124 | - entity: sensor.volkswagen_id_id_4_pro_performance_remaining_charging_time 125 | name: Resterende laadtijd 126 | icon: mdi:clock-end 127 | title: Batterij info 128 | header: 129 | type: picture 130 | image: /local/id4-4.jpg 131 | tap_action: 132 | action: none 133 | hold_action: 134 | action: none 135 | - type: horizontal-stack 136 | cards: 137 | - type: gauge 138 | entity: sensor.volkswagen_id_id_4_pro_performance_state_of_charge 139 | min: 0 140 | max: 100 141 | name: Accu status 142 | unit: '%' 143 | severity: 144 | green: 60 145 | yellow: 40 146 | red: 20 147 | - type: gauge 148 | entity: sensor.volkswagen_id_id_4_pro_performance_charge_power 149 | min: 0 150 | max: 125 151 | name: Laadsnelheid 152 | severity: 153 | green: 0 154 | yellow: 80 155 | red: 110 156 | unit: kW/H 157 | needle: true 158 | header: 159 | ``` 160 | 161 | ```yaml 162 | type: entities 163 | entities: 164 | - entity: button.volkswagen_id_id_4_pro_performance_start_climate 165 | name: Climatisering aan/uit 166 | secondary_info: last-updated 167 | - entity: binary_sensor.volkswagen_id_id_4_pro_performance_rear_window_heating_state 168 | name: Achterruit verwarming 169 | icon: mdi:thermometer-lines 170 | - entity: >- 171 | binary_sensor.volkswagen_id_id_4_pro_performance_front_window_heating_state 172 | name: Voorruit verwarming 173 | icon: mdi:thermometer-chevron-up 174 | - entity: binary_sensor.volkswagen_id_id_4_pro_performance_zone_front_left_enabled 175 | name: Zone linksvoor 176 | icon: mdi:thermometer-low 177 | - entity: binary_sensor.volkswagen_id_id_4_pro_performance_zone_front_right_enabled 178 | icon: mdi:thermometer-low 179 | name: Zone rechtsvoor 180 | - entity: sensor.volkswagen_id_id_4_pro_performance_climatisation_state 181 | name: Climatisering status 182 | icon: mdi:air-conditioner 183 | - entity: sensor.volkswagen_id_id_4_pro_performance_remaining_climatisation_time 184 | icon: mdi:air-conditioner 185 | name: Resterende tijd clima 186 | - entity: sensor.volkswagen_id_id_4_pro_performance_target_temperature_c 187 | name: Ingestelde temperatuur 188 | title: Climatisering 189 | state_color: true 190 | header: 191 | type: picture 192 | image: /local/id4-11.jpg 193 | tap_action: 194 | action: none 195 | hold_action: 196 | action: none 197 | show_header_toggle: false 198 | ``` 199 | 200 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /custom_components/volkswagen_we_connect_id/__init__.py: -------------------------------------------------------------------------------- 1 | """The Volkswagen We Connect ID integration.""" 2 | from __future__ import annotations 3 | 4 | import asyncio 5 | from dataclasses import dataclass 6 | from datetime import timedelta 7 | import functools 8 | import logging 9 | import threading 10 | import time 11 | from typing import Any 12 | 13 | from weconnect import weconnect 14 | from weconnect.elements.vehicle import Vehicle 15 | from weconnect.elements.control_operation import ControlOperation 16 | 17 | from homeassistant.config_entries import ConfigEntry 18 | from homeassistant.const import Platform 19 | from homeassistant.core import HomeAssistant, ServiceCall, callback 20 | from homeassistant.helpers.entity import DeviceInfo 21 | from homeassistant.helpers.update_coordinator import ( 22 | CoordinatorEntity, 23 | DataUpdateCoordinator, 24 | ) 25 | 26 | from .const import DOMAIN, DEFAULT_UPDATE_INTERVAL_SECONDS 27 | 28 | PLATFORMS = [ 29 | Platform.BINARY_SENSOR, 30 | Platform.BUTTON, 31 | Platform.SENSOR, 32 | Platform.NUMBER, 33 | Platform.DEVICE_TRACKER, 34 | ] 35 | 36 | _LOGGER = logging.getLogger(__name__) 37 | 38 | SUPPORTED_VEHICLES = ["ID.3", "ID.4", "ID.5", "ID. Buzz", "ID.7 Limousine", "ID.7 Tourer"] 39 | 40 | 41 | @dataclass 42 | class DomainEntry: 43 | """References to objects shared through hass.data[DOMAIN][config_entry_id].""" 44 | 45 | coordinator: DataUpdateCoordinator[list[Vehicle]] 46 | we_connect: weconnect.WeConnect 47 | vehicles: list[Vehicle] 48 | 49 | def get_parameter(config_entry: ConfigEntry, parameter: str, default_val: Any = None): 50 | """Get parameter from OptionsFlow or ConfigFlow""" 51 | if parameter in config_entry.options.keys(): 52 | return config_entry.options.get(parameter) 53 | if parameter in config_entry.data.keys(): 54 | return config_entry.data.get(parameter) 55 | return default_val 56 | 57 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 58 | """Set up Volkswagen We Connect ID from a config entry.""" 59 | 60 | hass.data.setdefault(DOMAIN, {}) 61 | _we_connect = get_we_connect_api( 62 | username=get_parameter(entry, "username"), 63 | password=get_parameter(entry, "password"), 64 | ) 65 | 66 | await hass.async_add_executor_job(_we_connect.login) 67 | await hass.async_add_executor_job(update, _we_connect) 68 | 69 | async def async_update_data() -> list[Vehicle]: 70 | """Fetch data from Volkswagen API.""" 71 | 72 | await hass.async_add_executor_job(update, _we_connect) 73 | 74 | vehicles: list[Vehicle] = [] 75 | 76 | for vin, vehicle in _we_connect.vehicles.items(): 77 | if vehicle.model.value in SUPPORTED_VEHICLES: 78 | vehicles.append(vehicle) 79 | 80 | domain_entry: DomainEntry = hass.data[DOMAIN][entry.entry_id] 81 | domain_entry.vehicles = vehicles 82 | return vehicles 83 | 84 | coordinator = DataUpdateCoordinator[list[Vehicle]]( 85 | hass, 86 | _LOGGER, 87 | name=DOMAIN, 88 | update_method=async_update_data, 89 | update_interval=timedelta( 90 | seconds=get_parameter(entry, "update_interval", DEFAULT_UPDATE_INTERVAL_SECONDS), 91 | ), 92 | ) 93 | 94 | hass.data[DOMAIN][entry.entry_id] = DomainEntry(coordinator, _we_connect, []) 95 | 96 | # Fetch initial data so we have data when entities subscribe 97 | await coordinator.async_config_entry_first_refresh() 98 | 99 | # Setup components 100 | await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) 101 | 102 | @callback 103 | async def volkswagen_id_start_stop_charging(call: ServiceCall) -> None: 104 | 105 | vin = call.data["vin"] 106 | start_stop = call.data["start_stop"] 107 | 108 | if ( 109 | await hass.async_add_executor_job( 110 | start_stop_charging, 111 | vin, 112 | _we_connect, 113 | start_stop, 114 | ) 115 | is False 116 | ): 117 | _LOGGER.error("Cannot send charging request to car") 118 | 119 | @callback 120 | async def volkswagen_id_set_climatisation(call: ServiceCall) -> None: 121 | 122 | vin = call.data["vin"] 123 | start_stop = call.data["start_stop"] 124 | target_temperature = 0 125 | if "target_temp" in call.data: 126 | target_temperature = call.data["target_temp"] 127 | 128 | if ( 129 | await hass.async_add_executor_job( 130 | set_climatisation, 131 | vin, 132 | _we_connect, 133 | start_stop, 134 | target_temperature, 135 | ) 136 | is False 137 | ): 138 | _LOGGER.error("Cannot send climate request to car") 139 | 140 | @callback 141 | async def volkswagen_id_set_target_soc(call: ServiceCall) -> None: 142 | 143 | vin = call.data["vin"] 144 | target_soc = 0 145 | if "target_soc" in call.data: 146 | target_soc = call.data["target_soc"] 147 | 148 | if ( 149 | await hass.async_add_executor_job( 150 | set_target_soc, 151 | vin, 152 | _we_connect, 153 | target_soc, 154 | ) 155 | is False 156 | ): 157 | _LOGGER.error("Cannot send target soc request to car") 158 | 159 | @callback 160 | async def volkswagen_id_set_ac_charge_speed(call: ServiceCall) -> None: 161 | 162 | vin = call.data["vin"] 163 | if "maximum_reduced" in call.data: 164 | if ( 165 | await hass.async_add_executor_job( 166 | set_ac_charging_speed, 167 | vin, 168 | _we_connect, 169 | call.data["maximum_reduced"], 170 | ) 171 | is False 172 | ): 173 | _LOGGER.error("Cannot send ac speed request to car") 174 | 175 | # Register our services with Home Assistant. 176 | hass.services.async_register( 177 | DOMAIN, "volkswagen_id_start_stop_charging", volkswagen_id_start_stop_charging 178 | ) 179 | 180 | hass.services.async_register( 181 | DOMAIN, "volkswagen_id_set_climatisation", volkswagen_id_set_climatisation 182 | ) 183 | hass.services.async_register( 184 | DOMAIN, "volkswagen_id_set_target_soc", volkswagen_id_set_target_soc 185 | ) 186 | hass.services.async_register( 187 | DOMAIN, "volkswagen_id_set_ac_charge_speed", volkswagen_id_set_ac_charge_speed 188 | ) 189 | 190 | # Reload entry if configuration has changed 191 | entry.async_on_unload(entry.add_update_listener(async_reload_entry)) 192 | 193 | return True 194 | 195 | 196 | @functools.lru_cache(maxsize=1) 197 | def get_we_connect_api(username: str, password: str) -> weconnect.WeConnect: 198 | """Return a cached weconnect api object, shared with the config flow.""" 199 | return weconnect.WeConnect( 200 | username=username, 201 | password=password, 202 | updateAfterLogin=False, 203 | loginOnInit=False, 204 | timeout=10, 205 | updatePictures=False, 206 | ) 207 | 208 | 209 | _last_successful_api_update_timestamp: float = 0.0 210 | _last_we_connect_api: weconnect.WeConnect | None = None 211 | _update_lock = threading.Lock() 212 | 213 | 214 | def update( 215 | api: weconnect.WeConnect 216 | ) -> None: 217 | """API call to update vehicle information. 218 | 219 | This function is called on its own thread and it is possible for multiple 220 | threads to call it at the same time, before an earlier weconnect update() 221 | call has finished. When the integration is loaded, multiple platforms 222 | (binary_sensor, number...) may each call async_config_entry_first_refresh() 223 | that in turn calls hass.async_add_executor_job() that creates a thread. 224 | """ 225 | # pylint: disable=global-statement 226 | global _last_successful_api_update_timestamp, _last_we_connect_api 227 | 228 | # Acquire a lock so that only one thread can call api.update() at a time. 229 | with _update_lock: 230 | # Skip the update() call altogether if it was last succesfully called 231 | # in the past 24 seconds (80% of the minimum update interval of 30s). 232 | elapsed = time.monotonic() - _last_successful_api_update_timestamp 233 | if elapsed <= 24 and api is _last_we_connect_api: 234 | return 235 | api.update(updatePictures=False) 236 | _last_successful_api_update_timestamp = time.monotonic() 237 | _last_we_connect_api = api 238 | 239 | 240 | def start_stop_charging( 241 | call_data_vin, api: weconnect.WeConnect, operation: str 242 | ) -> bool: 243 | """Start of stop charging of your volkswagen.""" 244 | 245 | for vin, vehicle in api.vehicles.items(): 246 | if vin == call_data_vin: 247 | 248 | if operation == "start": 249 | try: 250 | if ( 251 | vehicle.controls.chargingControl is not None 252 | and vehicle.controls.chargingControl.enabled 253 | ): 254 | vehicle.controls.chargingControl.value = ControlOperation.START 255 | _LOGGER.info("Sended start charging call to the car") 256 | except Exception as exc: 257 | _LOGGER.error("Failed to send request to car - %s", exc) 258 | return False 259 | 260 | if operation == "stop": 261 | try: 262 | if ( 263 | vehicle.controls.chargingControl is not None 264 | and vehicle.controls.chargingControl.enabled 265 | ): 266 | vehicle.controls.chargingControl.value = ControlOperation.STOP 267 | _LOGGER.info("Sended stop charging call to the car") 268 | except Exception as exc: 269 | _LOGGER.error("Failed to send request to car - %s", exc) 270 | return False 271 | return True 272 | 273 | 274 | def set_ac_charging_speed( 275 | call_data_vin, api: weconnect.WeConnect, charging_speed 276 | ) -> bool: 277 | """Set charging speed in your volkswagen.""" 278 | 279 | for vin, vehicle in api.vehicles.items(): 280 | if vin == call_data_vin: 281 | if ( 282 | charging_speed 283 | != vehicle.domains["charging"][ 284 | "chargingSettings" 285 | ].maxChargeCurrentAC.value 286 | ): 287 | try: 288 | vehicle.domains["charging"][ 289 | "chargingSettings" 290 | ].maxChargeCurrentAC.value = charging_speed 291 | _LOGGER.info("Sended charging speed call to the car") 292 | except Exception as exc: 293 | _LOGGER.error("Failed to send request to car - %s", exc) 294 | return False 295 | 296 | return True 297 | 298 | 299 | def set_target_soc(call_data_vin, api: weconnect.WeConnect, target_soc: int) -> bool: 300 | """Set target SOC in your volkswagen.""" 301 | 302 | target_soc = int(target_soc) 303 | 304 | for vin, vehicle in api.vehicles.items(): 305 | if vin == call_data_vin: 306 | if ( 307 | target_soc > 10 308 | and target_soc 309 | != vehicle.domains["charging"]["chargingSettings"].targetSOC_pct.value 310 | ): 311 | try: 312 | vehicle.domains["charging"][ 313 | "chargingSettings" 314 | ].targetSOC_pct.value = target_soc 315 | _LOGGER.info("Sended target SoC call to the car") 316 | except Exception as exc: 317 | _LOGGER.error("Failed to send request to car - %s", exc) 318 | return False 319 | return True 320 | 321 | 322 | def set_climatisation( 323 | call_data_vin, api: weconnect.WeConnect, operation: str, target_temperature: float 324 | ) -> bool: 325 | """Set climate in your volkswagen.""" 326 | 327 | for vin, vehicle in api.vehicles.items(): 328 | if vin == call_data_vin: 329 | 330 | if ( 331 | target_temperature > 10 332 | and target_temperature 333 | != vehicle.domains["climatisation"][ 334 | "climatisationSettings" 335 | ].targetTemperature_C.value 336 | ): 337 | try: 338 | vehicle.domains["climatisation"][ 339 | "climatisationSettings" 340 | ].targetTemperature_C.value = float(target_temperature) 341 | _LOGGER.info("Sended target temperature call to the car") 342 | except Exception as exc: 343 | _LOGGER.error("Failed to send request to car - %s", exc) 344 | return False 345 | 346 | if operation == "start": 347 | try: 348 | if ( 349 | vehicle.controls.climatizationControl is not None 350 | and vehicle.controls.climatizationControl.enabled 351 | ): 352 | vehicle.controls.climatizationControl.value = ( 353 | ControlOperation.START 354 | ) 355 | _LOGGER.info("Sended start climate call to the car") 356 | except Exception as exc: 357 | _LOGGER.error("Failed to send request to car - %s", exc) 358 | return False 359 | 360 | if operation == "stop": 361 | try: 362 | if ( 363 | vehicle.controls.climatizationControl is not None 364 | and vehicle.controls.climatizationControl.enabled 365 | ): 366 | vehicle.controls.climatizationControl.value = ( 367 | ControlOperation.STOP 368 | ) 369 | _LOGGER.info("Sended stop climate call to the car") 370 | except Exception as exc: 371 | _LOGGER.error("Failed to send request to car - %s", exc) 372 | return False 373 | return True 374 | 375 | 376 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 377 | """Unload a config entry.""" 378 | 379 | unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) 380 | if unload_ok: 381 | hass.data[DOMAIN].pop(entry.entry_id) 382 | get_we_connect_api.cache_clear() 383 | global _last_we_connect_api # pylint: disable=global-statement 384 | _last_we_connect_api = None 385 | 386 | return unload_ok 387 | 388 | # Global lock 389 | volkswagen_we_connect_id_lock = asyncio.Lock() 390 | 391 | async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: 392 | """Reload config entry.""" 393 | # Make sure setup is completed before next unload can be started. 394 | async with volkswagen_we_connect_id_lock: 395 | await async_unload_entry(hass, entry) 396 | await async_setup_entry(hass, entry) 397 | 398 | def get_object_value(value) -> str: 399 | """Get value from object or enum.""" 400 | 401 | while hasattr(value, "value"): 402 | value = value.value 403 | 404 | return value 405 | 406 | 407 | class VolkswagenIDBaseEntity(CoordinatorEntity): 408 | """Common base for VolkswagenID entities.""" 409 | 410 | # _attr_should_poll = False 411 | _attr_attribution = "Data provided by Volkswagen Connect ID" 412 | 413 | def __init__( 414 | self, 415 | we_connect: weconnect.WeConnect, 416 | coordinator: DataUpdateCoordinator, 417 | index: int, 418 | ) -> None: 419 | """Initialize sensor.""" 420 | super().__init__(coordinator) 421 | self.we_connect = we_connect 422 | self.index = index 423 | 424 | self._attr_device_info = DeviceInfo( 425 | identifiers={(DOMAIN, f"vw{self.data.vin}")}, 426 | manufacturer="Volkswagen", 427 | model=f"{self.data.model}", # format because of the ID.3/ID.4 names. 428 | name=f"Volkswagen {self.data.nickname} ({self.data.vin})", 429 | ) 430 | 431 | @property 432 | def data(self): 433 | """Shortcut to access coordinator data for the entity.""" 434 | return self.coordinator.data[self.index] 435 | -------------------------------------------------------------------------------- /custom_components/volkswagen_we_connect_id/sensor.py: -------------------------------------------------------------------------------- 1 | """Sensor integration.""" 2 | from __future__ import annotations 3 | 4 | from collections.abc import Callable 5 | from dataclasses import dataclass 6 | from typing import cast 7 | 8 | from weconnect import weconnect 9 | 10 | from homeassistant.components.sensor import ( 11 | SensorEntity, 12 | SensorEntityDescription, 13 | SensorStateClass, 14 | SensorDeviceClass, 15 | ) 16 | from homeassistant.config_entries import ConfigEntry 17 | from homeassistant.const import ( 18 | PERCENTAGE, 19 | UnitOfPower, 20 | UnitOfTemperature, 21 | UnitOfEnergy, 22 | UnitOfLength, 23 | UnitOfSpeed, 24 | UnitOfTime, 25 | ) 26 | from homeassistant.core import HomeAssistant 27 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 28 | from homeassistant.helpers.typing import StateType 29 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator 30 | 31 | from . import DomainEntry, VolkswagenIDBaseEntity, get_object_value 32 | from .const import DOMAIN 33 | 34 | 35 | @dataclass 36 | class VolkswagenIdEntityDescription(SensorEntityDescription): 37 | """Describes Volkswagen ID sensor entity.""" 38 | 39 | value: Callable = lambda x, y: x 40 | 41 | 42 | SENSORS: tuple[VolkswagenIdEntityDescription, ...] = ( 43 | VolkswagenIdEntityDescription( 44 | key="carType", 45 | name="Car Type", 46 | icon="mdi:car", 47 | value=lambda data: data["fuelStatus"][ 48 | "rangeStatus" 49 | ].carType.value, 50 | ), 51 | VolkswagenIdEntityDescription( 52 | key="climatisationState", 53 | name="Climatisation State", 54 | icon="mdi:fan", 55 | value=lambda data: data["climatisation"][ 56 | "climatisationStatus" 57 | ].climatisationState.value, 58 | ), 59 | VolkswagenIdEntityDescription( 60 | key="remainingClimatisationTime_min", 61 | name="Remaining Climatisation Time", 62 | icon="mdi:fan-clock", 63 | native_unit_of_measurement=UnitOfTime.MINUTES, 64 | value=lambda data: data["climatisation"][ 65 | "climatisationStatus" 66 | ].remainingClimatisationTime_min.value, 67 | ), 68 | VolkswagenIdEntityDescription( 69 | key="targetTemperature", 70 | name="Target Temperature", 71 | device_class=SensorDeviceClass.TEMPERATURE, 72 | native_unit_of_measurement=UnitOfTemperature.CELSIUS, 73 | value=lambda data: data["climatisation"][ 74 | "climatisationSettings" 75 | ].targetTemperature_C.value, 76 | ), 77 | VolkswagenIdEntityDescription( 78 | key="unitInCar", 79 | name="Unit In car", 80 | value=lambda data: data["climatisation"][ 81 | "climatisationSettings" 82 | ].unitInCar.value, 83 | ), 84 | VolkswagenIdEntityDescription( 85 | key="chargingState", 86 | name="Charging State", 87 | icon="mdi:ev-station", 88 | value=lambda data: data["charging"]["chargingStatus"].chargingState.value, 89 | ), 90 | VolkswagenIdEntityDescription( 91 | key="remainingChargingTimeToComplete_min", 92 | name="Remaining Charging Time", 93 | icon="mdi:battery-clock", 94 | native_unit_of_measurement=UnitOfTime.MINUTES, 95 | value=lambda data: data["charging"][ 96 | "chargingStatus" 97 | ].remainingChargingTimeToComplete_min.value, 98 | ), 99 | VolkswagenIdEntityDescription( 100 | key="chargeMode", 101 | name="Charging Mode", 102 | icon="mdi:ev-station", 103 | value=lambda data: data["charging"]["chargingStatus"].chargeMode.value, 104 | ), 105 | VolkswagenIdEntityDescription( 106 | key="chargePower_kW", 107 | name="Charge Power", 108 | native_unit_of_measurement=UnitOfPower.KILO_WATT, 109 | device_class=SensorDeviceClass.POWER, 110 | value=lambda data: data["charging"]["chargingStatus"].chargePower_kW.value, 111 | ), 112 | VolkswagenIdEntityDescription( 113 | key="chargeRate_kmph", 114 | name="Charge Rate", 115 | native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, 116 | device_class=SensorDeviceClass.SPEED, 117 | value=lambda data: data["charging"]["chargingStatus"].chargeRate_kmph.value, 118 | ), 119 | VolkswagenIdEntityDescription( 120 | key="chargingSettings", 121 | name="Charging Settings", 122 | icon="mdi:ev-station", 123 | value=lambda data: data["charging"]["chargingStatus"].chargingSettings.value, 124 | ), 125 | VolkswagenIdEntityDescription( 126 | key="chargeType", 127 | name="Charge Type", 128 | icon="mdi:ev-station", 129 | value=lambda data: data["charging"]["chargingStatus"].chargeType.value, 130 | ), 131 | VolkswagenIdEntityDescription( 132 | key="maxChargeCurrentAC", 133 | name="Max Charge Current AC", 134 | icon="mdi:ev-station", 135 | value=lambda data: data["charging"][ 136 | "chargingSettings" 137 | ].maxChargeCurrentAC.value, 138 | ), 139 | VolkswagenIdEntityDescription( 140 | key="targetSOC_pct", 141 | name="Target State of Charge", 142 | device_class=SensorDeviceClass.BATTERY, 143 | native_unit_of_measurement=PERCENTAGE, 144 | value=lambda data: data["charging"]["chargingSettings"].targetSOC_pct.value, 145 | ), 146 | VolkswagenIdEntityDescription( 147 | key="currentSOC_pct", 148 | name="State of Charge", 149 | device_class=SensorDeviceClass.BATTERY, 150 | native_unit_of_measurement=PERCENTAGE, 151 | value=lambda data: data["charging"]["batteryStatus"].currentSOC_pct.value, 152 | ), 153 | VolkswagenIdEntityDescription( 154 | name="Range", 155 | key="cruisingRangeElectric", 156 | icon="mdi:car-arrow-right", 157 | device_class=SensorDeviceClass.DISTANCE, 158 | native_unit_of_measurement=UnitOfLength.KILOMETERS, 159 | value=lambda data: data["charging"][ 160 | "batteryStatus" 161 | ].cruisingRangeElectric_km.value, 162 | ), 163 | VolkswagenIdEntityDescription( 164 | name="Health Inspection", 165 | key="inspectionDue", 166 | icon="mdi:wrench-clock-outline", 167 | native_unit_of_measurement=UnitOfTime.DAYS, 168 | value=lambda data: data["vehicleHealthInspection"][ 169 | "maintenanceStatus" 170 | ].inspectionDue_days.value, 171 | ), 172 | VolkswagenIdEntityDescription( 173 | name="Health Inspection km", 174 | key="inspectionDuekm", 175 | icon="mdi:wrench-clock-outline", 176 | device_class=SensorDeviceClass.DISTANCE, 177 | native_unit_of_measurement=UnitOfLength.KILOMETERS, 178 | value=lambda data: data["vehicleHealthInspection"][ 179 | "maintenanceStatus" 180 | ].inspectionDue_km.value, 181 | ), 182 | VolkswagenIdEntityDescription( 183 | name="Odometer", 184 | key="odometer", 185 | icon="mdi:car-cruise-control", 186 | device_class=SensorDeviceClass.DISTANCE, 187 | native_unit_of_measurement=UnitOfLength.KILOMETERS, 188 | value=lambda data: data["measurements"]["odometerStatus"].odometer.value, 189 | ), 190 | VolkswagenIdEntityDescription( 191 | key="doorLockStatus", 192 | name="Door Lock Status", 193 | icon="mdi:car-door-lock", 194 | value=lambda data: data["access"]["accessStatus"].doorLockStatus.value, 195 | ), 196 | VolkswagenIdEntityDescription( 197 | key="bonnetLockStatus", 198 | name="Bonnet Lock Status", 199 | icon="mdi:lock-outline", 200 | value=lambda data: data["access"]["accessStatus"] 201 | .doors["bonnet"] 202 | .lockState.value, 203 | ), 204 | VolkswagenIdEntityDescription( 205 | key="trunkLockStatus", 206 | name="Trunk Lock Status", 207 | icon="mdi:lock-outline", 208 | value=lambda data: data["access"]["accessStatus"] 209 | .doors["trunk"] 210 | .lockState.value, 211 | ), 212 | VolkswagenIdEntityDescription( 213 | key="rearRightLockStatus", 214 | name="Door Rear Right Lock Status", 215 | icon="mdi:car-door-lock", 216 | value=lambda data: data["access"]["accessStatus"] 217 | .doors["rearRight"] 218 | .lockState.value, 219 | ), 220 | VolkswagenIdEntityDescription( 221 | key="rearLeftLockStatus", 222 | name="Door Rear Left Lock Status", 223 | icon="mdi:car-door-lock", 224 | value=lambda data: data["access"]["accessStatus"] 225 | .doors["rearLeft"] 226 | .lockState.value, 227 | ), 228 | VolkswagenIdEntityDescription( 229 | key="frontLeftLockStatus", 230 | name="Door Front Left Lock Status", 231 | icon="mdi:car-door-lock", 232 | value=lambda data: data["access"]["accessStatus"] 233 | .doors["frontLeft"] 234 | .lockState.value, 235 | ), 236 | VolkswagenIdEntityDescription( 237 | key="frontRightLockStatus", 238 | name="Door Front Right Lock Status", 239 | icon="mdi:car-door-lock", 240 | value=lambda data: data["access"]["accessStatus"] 241 | .doors["frontRight"] 242 | .lockState.value, 243 | ), 244 | VolkswagenIdEntityDescription( 245 | key="bonnetOpenStatus", 246 | name="Bonnet Open Status", 247 | value=lambda data: data["access"]["accessStatus"] 248 | .doors["bonnet"] 249 | .openState.value, 250 | ), 251 | VolkswagenIdEntityDescription( 252 | key="trunkOpenStatus", 253 | name="Trunk Open Status", 254 | value=lambda data: data["access"]["accessStatus"] 255 | .doors["trunk"] 256 | .openState.value, 257 | ), 258 | VolkswagenIdEntityDescription( 259 | key="rearRightOpenStatus", 260 | name="Door Rear Right Open Status", 261 | icon="mdi:car-door", 262 | value=lambda data: data["access"]["accessStatus"] 263 | .doors["rearRight"] 264 | .openState.value, 265 | ), 266 | VolkswagenIdEntityDescription( 267 | key="rearLeftOpenStatus", 268 | name="Door Rear Left Open Status", 269 | icon="mdi:car-door", 270 | value=lambda data: data["access"]["accessStatus"] 271 | .doors["rearLeft"] 272 | .openState.value, 273 | ), 274 | VolkswagenIdEntityDescription( 275 | key="frontLeftOpenStatus", 276 | name="Door Front Left Open Status", 277 | icon="mdi:car-door", 278 | value=lambda data: data["access"]["accessStatus"] 279 | .doors["frontLeft"] 280 | .openState.value, 281 | ), 282 | VolkswagenIdEntityDescription( 283 | key="frontRightOpenStatus", 284 | name="Door Front Right Open Status", 285 | icon="mdi:car-door", 286 | value=lambda data: data["access"]["accessStatus"] 287 | .doors["frontRight"] 288 | .openState.value, 289 | ), 290 | VolkswagenIdEntityDescription( 291 | key="sunRoofStatus", 292 | name="Sunroof Open Status", 293 | value=lambda data: data["access"]["accessStatus"] 294 | .windows["sunRoof"] 295 | .openState.value, 296 | ), 297 | VolkswagenIdEntityDescription( 298 | key="roofCoverStatus", 299 | name="Sunroof Cover Status", 300 | value=lambda data: data["access"]["accessStatus"] 301 | .windows["roofCover"] 302 | .openState.value, 303 | ), 304 | VolkswagenIdEntityDescription( 305 | key="windowRearRightOpenStatus", 306 | name="Window Rear Right Open Status", 307 | icon="mdi:window-closed", 308 | value=lambda data: data["access"]["accessStatus"] 309 | .windows["rearRight"] 310 | .openState.value, 311 | ), 312 | VolkswagenIdEntityDescription( 313 | key="windowRearLeftOpenStatus", 314 | name="Window Rear Left Open Status", 315 | icon="mdi:window-closed", 316 | value=lambda data: data["access"]["accessStatus"] 317 | .windows["rearLeft"] 318 | .openState.value, 319 | ), 320 | VolkswagenIdEntityDescription( 321 | key="windowFrontLeftOpenStatus", 322 | name="Window Front Left Open Status", 323 | icon="mdi:window-closed", 324 | value=lambda data: data["access"]["accessStatus"] 325 | .windows["frontLeft"] 326 | .openState.value, 327 | ), 328 | VolkswagenIdEntityDescription( 329 | key="windowfrontRightOpenStatus", 330 | name="Window Front Right Open Status", 331 | icon="mdi:window-closed", 332 | value=lambda data: data["access"]["accessStatus"] 333 | .windows["frontRight"] 334 | .openState.value, 335 | ), 336 | VolkswagenIdEntityDescription( 337 | key="overallStatus", 338 | name="Overall Status", 339 | icon="mdi:car-info", 340 | value=lambda data: data["access"]["accessStatus"].overallStatus.value, 341 | ), 342 | VolkswagenIdEntityDescription( 343 | key="autoUnlockPlugWhenCharged", 344 | name="Auto Unlock Plug When Charged", 345 | icon="mdi:ev-plug-type2", 346 | value=lambda data: data["charging"][ 347 | "chargingSettings" 348 | ].autoUnlockPlugWhenCharged.value, 349 | ), 350 | VolkswagenIdEntityDescription( 351 | key="autoUnlockPlugWhenChargedAC", 352 | name="Auto Unlock Plug When Charged AC", 353 | icon="mdi:ev-plug-type2", 354 | value=lambda data: data["charging"][ 355 | "chargingSettings" 356 | ].autoUnlockPlugWhenChargedAC.value, 357 | ), 358 | VolkswagenIdEntityDescription( 359 | key="plugConnectionState", 360 | name="Plug Connection State", 361 | icon="mdi:ev-plug-type2", 362 | value=lambda data: data["charging"]["plugStatus"].plugConnectionState, 363 | ), 364 | VolkswagenIdEntityDescription( 365 | key="plugLockState", 366 | name="Plug Lock State", 367 | icon="mdi:ev-plug-type2", 368 | value=lambda data: data["charging"]["plugStatus"].plugLockState, 369 | ), 370 | VolkswagenIdEntityDescription( 371 | name="Fuel Level", 372 | key="fuelLevel", 373 | icon="mdi:fuel", 374 | native_unit_of_measurement=PERCENTAGE, 375 | value=lambda data: data["fuelStatus"]["rangeStatus"].primaryEngine.currentFuelLevel_pct.value, 376 | ), 377 | VolkswagenIdEntityDescription( 378 | name="Gasoline Range", 379 | key="GasolineRange", 380 | icon="mdi:car-arrow-right", 381 | device_class=SensorDeviceClass.DISTANCE, 382 | native_unit_of_measurement=UnitOfLength.KILOMETERS, 383 | value=lambda data: data["measurements"][ 384 | "rangeStatus" 385 | ].gasolineRange.value, 386 | ), 387 | VolkswagenIdEntityDescription( 388 | name="Oil Inspection days", 389 | key="oilInspectionDue", 390 | icon="mdi:wrench-clock-outline", 391 | native_unit_of_measurement=UnitOfTime.DAYS, 392 | value=lambda data: data["vehicleHealthInspection"][ 393 | "maintenanceStatus" 394 | ].oilServiceDue_days.value, 395 | ), 396 | VolkswagenIdEntityDescription( 397 | name="Oil Inspection km", 398 | key="oilInspectionDuekm", 399 | icon="mdi:wrench-clock-outline", 400 | device_class=SensorDeviceClass.DISTANCE, 401 | native_unit_of_measurement=UnitOfLength.KILOMETERS, 402 | value=lambda data: data["vehicleHealthInspection"][ 403 | "maintenanceStatus" 404 | ].oilServiceDue_km.value, 405 | ), 406 | VolkswagenIdEntityDescription( 407 | name="HV Battery Temperature Min", 408 | key="hvBatteryTemperatureMin", 409 | icon="mdi:thermometer", 410 | device_class=SensorDeviceClass.TEMPERATURE, 411 | native_unit_of_measurement=UnitOfTemperature.CELSIUS, 412 | value=lambda data: data["measurements"][ 413 | "temperatureBatteryStatus" 414 | ].temperatureHvBatteryMin_K.value - 273.15, 415 | ), 416 | VolkswagenIdEntityDescription( 417 | name="HV Battery Temperature Max", 418 | key="hvBatteryTemperatureMax", 419 | icon="mdi:thermometer", 420 | device_class=SensorDeviceClass.TEMPERATURE, 421 | native_unit_of_measurement=UnitOfTemperature.CELSIUS, 422 | value=lambda data: data["measurements"][ 423 | "temperatureBatteryStatus" 424 | ].temperatureHvBatteryMax_K.value - 273.15, 425 | ), 426 | 427 | ) 428 | 429 | VEHICLE_SENSORS: tuple[VolkswagenIdEntityDescription, ...] = ( 430 | VolkswagenIdEntityDescription( 431 | key="lastTripAverageElectricConsumption", 432 | name="Last Trip Average Electric consumption", 433 | native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, 434 | value=lambda vehicle: vehicle.trips["shortTerm"].averageElectricConsumption.value, 435 | ), 436 | VolkswagenIdEntityDescription( 437 | key="lastTripAverageFuelConsumption", 438 | name="Last Trip Average Fuel consumption", 439 | native_unit_of_measurement="l/100km", 440 | value=lambda vehicle: vehicle.trips["shortTerm"].averageFuelConsumption.value, 441 | ), 442 | 443 | ) 444 | 445 | 446 | async def async_setup_entry( 447 | hass: HomeAssistant, 448 | config_entry: ConfigEntry, 449 | async_add_entities: AddEntitiesCallback, 450 | ): 451 | """Add sensors for passed config_entry in HA.""" 452 | domain_entry: DomainEntry = hass.data[DOMAIN][config_entry.entry_id] 453 | we_connect = domain_entry.we_connect 454 | coordinator = domain_entry.coordinator 455 | 456 | # Fetch initial data so we have data when entities subscribe 457 | await coordinator.async_config_entry_first_refresh() 458 | 459 | entities: list[VolkswagenIDSensor] = [] 460 | 461 | for index, vehicle in enumerate(coordinator.data): 462 | for sensor in SENSORS: 463 | entities.append(VolkswagenIDSensor(sensor, we_connect, coordinator, index)) 464 | for sensor in VEHICLE_SENSORS: 465 | entities.append(VolkswagenIDVehicleSensor(sensor, we_connect, coordinator, index)) 466 | 467 | if entities: 468 | async_add_entities(entities) 469 | 470 | 471 | class VolkswagenIDSensor(VolkswagenIDBaseEntity, SensorEntity): 472 | """Representation of a VolkswagenID vehicle sensor.""" 473 | 474 | entity_description: VolkswagenIdEntityDescription 475 | 476 | def __init__( 477 | self, 478 | sensor: VolkswagenIdEntityDescription, 479 | we_connect: weconnect.WeConnect, 480 | coordinator: DataUpdateCoordinator, 481 | index: int, 482 | ) -> None: 483 | """Initialize VolkswagenID vehicle sensor.""" 484 | super().__init__(we_connect, coordinator, index) 485 | 486 | self.entity_description = sensor 487 | self._coordinator = coordinator 488 | self._attr_name = f"{self.data.nickname} {sensor.name}" 489 | self._attr_unique_id = f"{self.data.vin}-{sensor.key}" 490 | if sensor.native_unit_of_measurement: 491 | self._attr_native_unit_of_measurement = sensor.native_unit_of_measurement 492 | self._attr_state_class = SensorStateClass.MEASUREMENT 493 | 494 | @property 495 | def native_value(self) -> StateType: 496 | """Return the state.""" 497 | 498 | try: 499 | state = get_object_value(self.entity_description.value(self.data.domains)) 500 | except (TypeError, KeyError, ValueError): 501 | return None 502 | 503 | return cast(StateType, state) 504 | 505 | class VolkswagenIDVehicleSensor(VolkswagenIDBaseEntity, SensorEntity): 506 | """Representation of a VolkswagenID vehicle sensor.""" 507 | 508 | entity_description: VolkswagenIdEntityDescription 509 | 510 | def __init__( 511 | self, 512 | sensor: VolkswagenIdEntityDescription, 513 | we_connect: weconnect.WeConnect, 514 | coordinator: DataUpdateCoordinator, 515 | index: int, 516 | ) -> None: 517 | """Initialize VolkswagenID vehicle sensor.""" 518 | super().__init__(we_connect, coordinator, index) 519 | 520 | self.entity_description = sensor 521 | self._coordinator = coordinator 522 | self._attr_name = f"{self.data.nickname} {sensor.name}" 523 | self._attr_unique_id = f"{self.data.vin}-{sensor.key}" 524 | if sensor.native_unit_of_measurement: 525 | self._attr_native_unit_of_measurement = sensor.native_unit_of_measurement 526 | self._attr_state_class = SensorStateClass.MEASUREMENT 527 | 528 | @property 529 | def native_value(self) -> StateType: 530 | """Return the state.""" 531 | 532 | try: 533 | state = get_object_value(self.entity_description.value(self.data)) 534 | except (KeyError, ValueError): 535 | return None 536 | 537 | return cast(StateType, state) 538 | --------------------------------------------------------------------------------