[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: \"\"\nlabels: \"\"\nassignees: \"\"\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Logfile**\nHow to enable audiconnect debugging?\nSettings > Integrations > Audi Connect > Enable Debug Logging\nRun service refresh_cloud_data\nDisable Debug Logging\n\n**Your Vehicle Details**\nModel:\nYear:\nType (Gas/Hybrid/Electric):\nRegion (EU/US/CA/CN):\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: \"\"\nlabels: \"\"\nassignees: \"\"\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n\n**Logfile**\nHow to enable audiconnect debugging?\nSettings > Integrations > Audi Connect > Enable Debug Logging\nRun service refresh_cloud_data\nDisable Debug Logging\n\n**Your Vehicle Details**\nModel:\nYear:\nType (ICE/PHEV/BEV):\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  # Enable version updates for Python\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    # Check for updates once a week\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/workflows/inactiveIssues.yml",
    "content": "name: Close inactive issues\non:\n  schedule:\n    - cron: \"30 1 * * *\"\n\njobs:\n  close-issues:\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n      actions: write\n      pull-requests: write\n    steps:\n      - uses: actions/stale@v10\n        with:\n          days-before-issue-stale: 45\n          days-before-issue-close: 15\n          stale-issue-label: \"stale\"\n          stale-issue-message: \"This issue is stale because it has been open for 45 days with no activity. Are you still experiencing this issue? \"\n          close-issue-message: \"This issue was closed because it has been inactive for 15 days since being marked as stale.\"\n          days-before-pr-stale: -1\n          days-before-pr-close: -1\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\non:\n  workflow_dispatch:\n  schedule:\n    - cron: \"0 8 * * Wed,Sun\"\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Gets semantic release info\n        id: semantic_release_info\n        uses: jossef/action-semantic-release-info@v3.0.0\n        env:\n          GITHUB_TOKEN: ${{ github.token }}\n      - name: Update Version and Commit\n        if: ${{steps.semantic_release_info.outputs.version != ''}}\n        run: |\n          echo \"Version: ${{steps.semantic_release_info.outputs.version}}\"\n          sed -i \"s/\\\"version\\\": \\\".*\\\"/\\\"version\\\": \\\"${{steps.semantic_release_info.outputs.version}}\\\"/g\" custom_components/audiconnect/manifest.json\n          git config --local user.email \"action@github.com\"\n          git config --local user.name \"GitHub Action\"\n          git add -A\n          git commit -m \"chore: bumping version to ${{steps.semantic_release_info.outputs.version}}\"\n          git tag ${{ steps.semantic_release_info.outputs.git_tag }}\n\n      - name: Push changes\n        if: ${{steps.semantic_release_info.outputs.version != ''}}\n        uses: ad-m/github-push-action@v1.1.0\n        with:\n          github_token: ${{ github.token }}\n          tags: true\n\n      - name: Create GitHub Release\n        if: ${{steps.semantic_release_info.outputs.version != ''}}\n        uses: ncipollo/release-action@v1\n        env:\n          GITHUB_TOKEN: ${{ github.token }}\n        with:\n          tag: ${{ steps.semantic_release_info.outputs.git_tag }}\n          name: ${{ steps.semantic_release_info.outputs.git_tag }}\n          body: ${{ steps.semantic_release_info.outputs.notes }}\n          draft: false\n          prerelease: false\n"
  },
  {
    "path": ".github/workflows/semanticTitle.yaml",
    "content": "name: \"Semantic Title\"\n\non:\n  pull_request_target:\n    types:\n      - opened\n      - edited\n      - synchronize\n\njobs:\n  main:\n    runs-on: ubuntu-latest\n    steps:\n      # Please look up the latest version from\n      # https://github.com/amannn/action-semantic-pull-request/releases\n      - uses: amannn/action-semantic-pull-request@v6.1.1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/validate.yml",
    "content": "name: Validate\n\non:\n  push:\n  pull_request:\n  schedule:\n    - cron: \"0 0 * * *\"\n  workflow_dispatch:\n\njobs:\n  validate-hassfest:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - name: Hassfest validation\n        uses: home-assistant/actions/hassfest@master\n\n  validate-hacs:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - name: HACS validation\n        uses: hacs/action@main\n        with:\n          category: integration\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.idea\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "---\nci:\n  autoupdate_commit_msg: \"chore: pre-commit autoupdate\"\nrepos:\n  - repo: https://github.com/astral-sh/ruff-pre-commit\n    rev: v0.15.10\n    hooks:\n      - id: ruff\n        args:\n          - --fix\n      - id: ruff-format\n  - repo: https://github.com/codespell-project/codespell\n    rev: v2.4.2\n    hooks:\n      - id: codespell\n        args:\n          - --ignore-words-list=fro,hass\n          - --skip=\"./.*,*.csv,*.json,*.ambr\"\n          - --quiet-level=2\n        exclude_types: [csv, json]\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v6.0.0\n    hooks:\n      - id: trailing-whitespace\n      - id: end-of-file-fixer\n      - id: check-executables-have-shebangs\n        stages: [manual]\n      - id: check-json\n        exclude: (.vscode|.devcontainer)\n  - repo: https://github.com/asottile/pyupgrade\n    rev: v3.21.2\n    hooks:\n      - id: pyupgrade\n  - repo: https://github.com/adrienverge/yamllint.git\n    rev: v1.38.0\n    hooks:\n      - id: yamllint\n        exclude: (.github|.vscode|.devcontainer)\n  - repo: https://github.com/pre-commit/mirrors-prettier\n    rev: v4.0.0-alpha.8\n    hooks:\n      - id: prettier\n  - repo: https://github.com/cdce8p/python-typing-update\n    rev: v0.8.1\n    hooks:\n      # Run `python-typing-update` hook manually from time to time\n      # to update python typing syntax.\n      # Will require manual work, before submitting changes!\n      # pre-commit run --hook-stage manual python-typing-update --all-files\n      - id: python-typing-update\n        stages: [manual]\n        args:\n          - --py311-plus\n          - --force\n          - --keep-updates\n        files: ^(/.+)?[^/]+\\.py$\n  - repo: https://github.com/pre-commit/mirrors-mypy\n    rev: v1.20.1\n    hooks:\n      - id: mypy\n        args: [--strict, --ignore-missing-imports]\n        files: ^(/.+)?[^/]+\\.py$\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contribution guidelines\n\nContributing to this project should be as easy and transparent as possible, whether it's:\n\n- Reporting a bug\n- Discussing the current state of the code\n- Submitting a fix\n- Proposing new features\n\n## Contributing Code\n\nGithub is used to host code, to track issues and feature requests, as well as accept pull requests.\n\nPull requests are the best way to propose changes to the codebase.\n\n1. Fork the repo and create your branch from `master`.\n2. If you've changed something, update the documentation.\n3. Issue a pull request\n\nBy contributing, you agree that your contributions will be licensed under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project.\nFeel free to contact the maintainers if that's a concern.\n\n## Coding Style\n\nThis project uses [black](https://github.com/ambv/black) to ensure the code follows a consistent style.\n\n## Report bugs using Github's issues\n\nGitHub issues are used to track public bugs. Report a bug by [opening a new issue](../../issues/new/choose)\n\n## Write bug reports with details\n\n**Great Bug Reports** tend to have:\n\n- A quick summary and/or background\n- Steps to reproduce\n- What you expected would happen\n- What actually happens\n- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 Arjen van Rhijn @arjenvrh\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "custom_components/audiconnect/__init__.py",
    "content": "\"\"\"Support for Audi Connect.\"\"\"\n\nfrom datetime import timedelta\nimport voluptuous as vol\nimport logging\n\nimport homeassistant.helpers.config_validation as cv\nfrom homeassistant.helpers.event import async_track_time_interval\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.util.dt import utcnow\nfrom homeassistant import config_entries\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.const import (\n    CONF_NAME,\n    CONF_PASSWORD,\n    CONF_RESOURCES,\n    CONF_SCAN_INTERVAL,\n    CONF_USERNAME,\n)\nfrom homeassistant.helpers.device_registry import DeviceEntry\n\n\nfrom .audi_account import AudiAccount\n\nfrom .const import (\n    DOMAIN,\n    CONF_REGION,\n    CONF_MUTABLE,\n    CONF_SCAN_INITIAL,\n    CONF_SCAN_ACTIVE,\n    DEFAULT_UPDATE_INTERVAL,\n    MIN_UPDATE_INTERVAL,\n    RESOURCES,\n    COMPONENTS,\n    CONF_API_LEVEL,\n    DEFAULT_API_LEVEL,\n    API_LEVELS,\n)\n\n_LOGGER = logging.getLogger(__name__)\n\nCONFIG_SCHEMA = vol.Schema(\n    {\n        DOMAIN: vol.Schema(\n            {\n                vol.Required(CONF_USERNAME): cv.string,\n                vol.Required(CONF_PASSWORD): cv.string,\n                vol.Optional(\n                    CONF_SCAN_INTERVAL,\n                    default=timedelta(minutes=DEFAULT_UPDATE_INTERVAL),\n                ): vol.All(\n                    cv.time_period,\n                    vol.Clamp(min=timedelta(minutes=MIN_UPDATE_INTERVAL)),\n                ),\n                vol.Optional(CONF_NAME, default={}): cv.schema_with_slug_keys(\n                    cv.string\n                ),\n                vol.Optional(CONF_RESOURCES): vol.All(\n                    cv.ensure_list, [vol.In(RESOURCES)]\n                ),\n                vol.Optional(CONF_REGION): cv.string,\n                vol.Optional(CONF_MUTABLE, default=True): cv.boolean,\n                vol.Optional(\n                    CONF_API_LEVEL, default=API_LEVELS[DEFAULT_API_LEVEL]\n                ): vol.All(vol.Coerce(int), vol.In(API_LEVELS)),\n            }\n        )\n    },\n    extra=vol.ALLOW_EXTRA,\n)\n\n\nasync def async_setup(hass, config):\n    if hass.config_entries.async_entries(DOMAIN):\n        return True\n\n    if DOMAIN not in config:\n        return True\n\n    names = config[DOMAIN].get(CONF_NAME)\n    if len(names) == 0:\n        return True\n\n    data = {}\n    data[CONF_USERNAME] = config[DOMAIN].get(CONF_USERNAME)\n    data[CONF_PASSWORD] = config[DOMAIN].get(CONF_PASSWORD)\n    data[CONF_SCAN_INTERVAL] = config[DOMAIN].get(CONF_SCAN_INTERVAL).seconds / 60\n    data[CONF_REGION] = config[DOMAIN].get(CONF_REGION)\n    data[CONF_API_LEVEL] = config[DOMAIN].get(CONF_API_LEVEL)\n\n    hass.async_create_task(\n        hass.config_entries.flow.async_init(\n            DOMAIN, context={\"source\": config_entries.SOURCE_IMPORT}, data=data\n        )\n    )\n\n    return True\n\n\nasync def async_update_listener(hass, config_entry):\n    _LOGGER.debug(\"Updates detected, reloading configuration...\")\n    await hass.config_entries.async_reload(config_entry.entry_id)\n\n\nasync def async_setup_entry(hass, config_entry):\n    \"\"\"Set up this integration using UI.\"\"\"\n    _LOGGER.debug(\"Audi Connect starting...\")\n\n    # Register the update listener so that changes to configuration options are applied immediately.\n    config_entry.async_on_unload(\n        config_entry.add_update_listener(async_update_listener)\n    )\n\n    if DOMAIN not in hass.data:\n        hass.data[DOMAIN] = {}\n\n    \"\"\"Set up the Audi Connect component.\"\"\"\n    hass.data[DOMAIN][\"devices\"] = set()\n\n    # Attempt to retrieve the scan interval from options, then fall back to data, or use default\n    scan_interval = timedelta(\n        minutes=config_entry.options.get(\n            CONF_SCAN_INTERVAL,\n            config_entry.data.get(CONF_SCAN_INTERVAL, DEFAULT_UPDATE_INTERVAL),\n        )\n    )\n    _LOGGER.debug(\"User option for CONF_SCAN_INTERVAL is %s\", scan_interval)\n\n    # Get Initial Scan Option - Default to True\n    _scan_initial = config_entry.options.get(CONF_SCAN_INITIAL, True)\n    _LOGGER.debug(\"User option for CONF_SCAN_INITIAL is %s.\", _scan_initial)\n\n    # Get Active Scan Option - Default to True\n    _scan_active = config_entry.options.get(CONF_SCAN_ACTIVE, True)\n    _LOGGER.debug(\"User option for CONF_SCAN_ACTIVE is %s.\", _scan_active)\n\n    account = config_entry.data.get(CONF_USERNAME)\n\n    if account not in hass.data[DOMAIN]:\n        data = hass.data[DOMAIN][account] = AudiAccount(hass, config_entry)\n        data.init_connection()\n    else:\n        data = hass.data[DOMAIN][account]\n\n    # Define a callback function for the timer to update data\n    async def update_data(now):\n        \"\"\"Update the data with the latest information.\"\"\"\n        _LOGGER.debug(\"ACTIVE POLLING: Requesting scheduled cloud data refresh...\")\n        await data.update(utcnow())\n\n    # Schedule the update_data function if option is true\n    if _scan_active:\n        _LOGGER.debug(\n            \"ACTIVE POLLING: Scheduling cloud data refresh every %d minutes.\",\n            scan_interval.seconds / 60,\n        )\n        async_track_time_interval(hass, update_data, scan_interval)\n    else:\n        _LOGGER.debug(\n            \"ACTIVE POLLING: Active Polling at Scan Interval is turned off in user options. Skipping scheduling...\"\n        )\n\n    # Initially update the data if option is true\n    if _scan_initial:\n        _LOGGER.debug(\"Requesting initial cloud data update...\")\n        return await data.update(utcnow())\n    else:\n        _LOGGER.debug(\n            \"Cloud Update at Start is turned off in user options. Skipping initial update...\"\n        )\n\n    _LOGGER.debug(\"Audi Connect Setup Complete\")\n    return True\n\n\nasync def async_unload_entry(hass, config_entry):\n    account = config_entry.data.get(CONF_USERNAME)\n\n    data = hass.data[DOMAIN][account]\n\n    for component in COMPONENTS:\n        await hass.config_entries.async_forward_entry_unload(\n            data.config_entry, component\n        )\n\n    del hass.data[DOMAIN][account]\n\n    return True\n\n\nasync def async_remove_config_entry_device(\n    hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry\n) -> bool:\n    \"\"\"Remove a config entry from a device.\"\"\"\n    return True\n"
  },
  {
    "path": "custom_components/audiconnect/audi_account.py",
    "content": "import asyncio\nimport logging\n\nimport voluptuous as vol\n\nfrom homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform\nfrom homeassistant.helpers.aiohttp_client import async_get_clientsession\nimport homeassistant.helpers.config_validation as cv\nfrom homeassistant.helpers.dispatcher import async_dispatcher_send\nfrom homeassistant.util.dt import utcnow\n\nfrom .audi_connect_account import AudiConnectAccount, AudiConnectObserver\nfrom .audi_models import VehicleData\nfrom .const import (\n    COMPONENTS,\n    CONF_ACTION,\n    CONF_CLIMATE_GLASS,\n    CONF_CLIMATE_SEAT_FL,\n    CONF_CLIMATE_SEAT_FR,\n    CONF_CLIMATE_SEAT_RL,\n    CONF_CLIMATE_SEAT_RR,\n    CONF_CLIMATE_AT_UNLOCK,\n    CONF_CLIMATE_MODE,\n    CONF_CLIMATE_TEMP_C,\n    CONF_CLIMATE_TEMP_F,\n    CONF_REGION,\n    CONF_SPIN,\n    CONF_VIN,\n    DOMAIN,\n    SIGNAL_STATE_UPDATED,\n    TRACKER_UPDATE,\n    UPDATE_SLEEP,\n    CONF_API_LEVEL,\n    DEFAULT_API_LEVEL,\n    API_LEVELS,\n    CONF_DURATION,\n    CONF_TARGET_SOC,\n    CONF_FILTER_VINS,\n)\nfrom .dashboard import Dashboard\n\nREFRESH_VEHICLE_DATA_FAILED_EVENT = \"refresh_failed\"\nREFRESH_VEHICLE_DATA_COMPLETED_EVENT = \"refresh_completed\"\n\nSERVICE_REFRESH_VEHICLE_DATA = \"refresh_vehicle_data\"\nSERVICE_REFRESH_VEHICLE_DATA_SCHEMA = vol.Schema(\n    {\n        vol.Required(CONF_VIN): cv.string,\n    }\n)\n\nSERVICE_EXECUTE_VEHICLE_ACTION = \"execute_vehicle_action\"\nSERVICE_EXECUTE_VEHICLE_ACTION_SCHEMA = vol.Schema(\n    {vol.Required(CONF_VIN): cv.string, vol.Required(CONF_ACTION): cv.string}\n)\n\nSERVICE_START_CLIMATE_CONTROL = \"start_climate_control\"\nSERVICE_START_CLIMATE_CONTROL_SCHEMA = vol.Schema(\n    {\n        vol.Required(CONF_VIN): cv.string,\n        vol.Optional(CONF_CLIMATE_TEMP_F): cv.positive_int,\n        vol.Optional(CONF_CLIMATE_TEMP_C): cv.positive_int,\n        vol.Optional(CONF_CLIMATE_GLASS): cv.boolean,\n        vol.Optional(CONF_CLIMATE_SEAT_FL): cv.boolean,\n        vol.Optional(CONF_CLIMATE_SEAT_FR): cv.boolean,\n        vol.Optional(CONF_CLIMATE_SEAT_RL): cv.boolean,\n        vol.Optional(CONF_CLIMATE_SEAT_RR): cv.boolean,\n        vol.Optional(CONF_CLIMATE_AT_UNLOCK): cv.boolean,\n        vol.Optional(CONF_CLIMATE_MODE): cv.string,\n    }\n)\n\nSERVICE_START_AUXILIARY_HEATING = \"start_auxiliary_heating\"\nSERVICE_START_AUXILIARY_HEATING_SCHEMA = vol.Schema(\n    {\n        vol.Required(CONF_VIN): cv.string,\n        vol.Optional(CONF_DURATION): cv.positive_int,\n    }\n)\n\nSERVICE_SET_TARGET_SOC = \"set_target_soc\"\nSERVICE_SET_TARGET_SOC_SCHEMA = vol.Schema(\n    {\n        vol.Required(CONF_VIN): cv.string,\n        vol.Required(CONF_TARGET_SOC): vol.All(\n            cv.positive_int, vol.Range(min=20, max=100)\n        ),\n    }\n)\n\nPLATFORMS: list[str] = [\n    Platform.BINARY_SENSOR,\n    Platform.SENSOR,\n    Platform.DEVICE_TRACKER,\n    Platform.LOCK,\n    Platform.SWITCH,\n]\n\nSERVICE_REFRESH_CLOUD_DATA = \"refresh_cloud_data\"\n\n_LOGGER = logging.getLogger(__name__)\n\n\nclass AudiAccount(AudiConnectObserver):\n    def __init__(self, hass, config_entry):\n        \"\"\"Initialize the component state.\"\"\"\n        self.hass = hass\n        self.config_entry = config_entry\n        self.config_vehicles = set()\n        self.vehicles = set()\n\n    def init_connection(self):\n        session = async_get_clientsession(self.hass)\n        excluded_vins = [\n            x.strip()\n            for x in self.config_entry.options.get(\n                CONF_FILTER_VINS, self.config_entry.data.get(CONF_FILTER_VINS, \"\")\n            ).split(\",\")\n            if x.strip()\n        ]\n\n        self.connection = AudiConnectAccount(\n            session=session,\n            username=self.config_entry.data.get(CONF_USERNAME),\n            password=self.config_entry.data.get(CONF_PASSWORD),\n            country=self.config_entry.data.get(CONF_REGION),\n            spin=self.config_entry.data.get(CONF_SPIN),\n            api_level=self.config_entry.options.get(\n                CONF_API_LEVEL,\n                self.config_entry.data.get(\n                    CONF_API_LEVEL, API_LEVELS[DEFAULT_API_LEVEL]\n                ),\n            ),\n            excluded_vins=excluded_vins,\n        )\n\n        self.hass.services.async_register(\n            DOMAIN,\n            SERVICE_REFRESH_VEHICLE_DATA,\n            self.refresh_vehicle_data,\n            schema=SERVICE_REFRESH_VEHICLE_DATA_SCHEMA,\n        )\n        self.hass.services.async_register(\n            DOMAIN,\n            SERVICE_EXECUTE_VEHICLE_ACTION,\n            self.execute_vehicle_action,\n            schema=SERVICE_EXECUTE_VEHICLE_ACTION_SCHEMA,\n        )\n        self.hass.services.async_register(\n            DOMAIN,\n            SERVICE_START_CLIMATE_CONTROL,\n            self.start_climate_control,\n            schema=SERVICE_START_CLIMATE_CONTROL_SCHEMA,\n        )\n        self.hass.services.async_register(\n            DOMAIN,\n            SERVICE_REFRESH_CLOUD_DATA,\n            self.update,\n        )\n        self.hass.services.async_register(\n            DOMAIN,\n            SERVICE_START_AUXILIARY_HEATING,\n            self.start_auxiliary_heating,\n            schema=SERVICE_START_AUXILIARY_HEATING_SCHEMA,\n        )\n        self.hass.services.async_register(\n            DOMAIN,\n            SERVICE_SET_TARGET_SOC,\n            self.set_target_soc,\n            schema=SERVICE_SET_TARGET_SOC_SCHEMA,\n        )\n\n        self.connection.add_observer(self)\n\n    def is_enabled(self, attr):\n        return True\n        # \"\"\"Return true if the user has enabled the resource.\"\"\"\n        # return attr in config[DOMAIN].get(CONF_RESOURCES, [attr])\n\n    async def discover_vehicles(self, vehicles):\n        if len(vehicles) > 0:\n            for vehicle in vehicles:\n                vin = vehicle.vin.lower()\n\n                self.vehicles.add(vin)\n\n                cfg_vehicle = VehicleData(self.config_entry)\n                cfg_vehicle.vehicle = vehicle\n                self.config_vehicles.add(cfg_vehicle)\n\n                dashboard = Dashboard(self.connection, vehicle)\n\n                for instrument in (\n                    instrument\n                    for instrument in dashboard.instruments\n                    if instrument._component in COMPONENTS\n                    and self.is_enabled(instrument.slug_attr)\n                ):\n                    if instrument._component == \"sensor\":\n                        cfg_vehicle.sensors.add(instrument)\n                    if instrument._component == \"binary_sensor\":\n                        cfg_vehicle.binary_sensors.add(instrument)\n                    if instrument._component == \"switch\":\n                        cfg_vehicle.switches.add(instrument)\n                    if instrument._component == \"device_tracker\":\n                        cfg_vehicle.device_trackers.add(instrument)\n                    if instrument._component == \"lock\":\n                        cfg_vehicle.locks.add(instrument)\n\n            await self.hass.config_entries.async_forward_entry_setups(\n                self.config_entry, PLATFORMS\n            )\n\n    async def update(self, now):\n        \"\"\"Update status from the cloud.\"\"\"\n        _LOGGER.debug(\"Starting refresh cloud data...\")\n        if not await self.connection.update(None):\n            _LOGGER.warning(\"Failed refresh cloud data\")\n            return False\n\n        # Discover new vehicles that have not been added yet\n        new_vehicles = [\n            x for x in self.connection._vehicles if x.vin not in self.vehicles\n        ]\n        if new_vehicles:\n            _LOGGER.debug(\"Retrieved %d vehicle(s)\", len(new_vehicles))\n        await self.discover_vehicles(new_vehicles)\n\n        async_dispatcher_send(self.hass, SIGNAL_STATE_UPDATED)\n\n        for config_vehicle in self.config_vehicles:\n            for instrument in config_vehicle.device_trackers:\n                async_dispatcher_send(self.hass, TRACKER_UPDATE, instrument)\n\n        _LOGGER.debug(\"Successfully refreshed cloud data\")\n        return True\n\n    async def execute_vehicle_action(self, service):\n        vin = service.data.get(CONF_VIN).lower()\n        action = service.data.get(CONF_ACTION).lower()\n\n        if action == \"lock\":\n            await self.connection.set_vehicle_lock(vin, True)\n        if action == \"unlock\":\n            await self.connection.set_vehicle_lock(vin, False)\n        if action == \"start_climatisation\":\n            await self.connection.set_vehicle_climatisation(vin, True)\n        if action == \"stop_climatisation\":\n            await self.connection.set_vehicle_climatisation(vin, False)\n        if action == \"start_charger\":\n            await self.connection.set_battery_charger(vin, True, False)\n        if action == \"start_timed_charger\":\n            await self.connection.set_battery_charger(vin, True, True)\n        if action == \"stop_charger\":\n            await self.connection.set_battery_charger(vin, False, False)\n        if action == \"start_preheater\":\n            _LOGGER.warning(\n                'The \"Start Preheater (Legacy)\" action is deprecated and will be removed in a future release.'\n                'Please use the \"Start Auxiliary Heating\" service instead.'\n            )\n            await self.connection.set_vehicle_pre_heater(vin, True)\n        if action == \"stop_preheater\":\n            await self.connection.set_vehicle_pre_heater(vin, False)\n        if action == \"start_window_heating\":\n            await self.connection.set_vehicle_window_heating(vin, True)\n        if action == \"stop_window_heating\":\n            await self.connection.set_vehicle_window_heating(vin, False)\n\n    async def start_climate_control(self, service):\n        _LOGGER.debug(\"Initiating Start Climate Control Service...\")\n        vin = service.data.get(CONF_VIN).lower()\n        # Optional Parameters\n        temp_f = service.data.get(CONF_CLIMATE_TEMP_F, None)\n        temp_c = service.data.get(CONF_CLIMATE_TEMP_C, None)\n        glass_heating = service.data.get(CONF_CLIMATE_GLASS, False)\n        seat_fl = service.data.get(CONF_CLIMATE_SEAT_FL, False)\n        seat_fr = service.data.get(CONF_CLIMATE_SEAT_FR, False)\n        seat_rl = service.data.get(CONF_CLIMATE_SEAT_RL, False)\n        seat_rr = service.data.get(CONF_CLIMATE_SEAT_RR, False)\n        climatisation_at_unlock = service.data.get(CONF_CLIMATE_AT_UNLOCK, False)\n        climatisation_mode = service.data.get(CONF_CLIMATE_MODE)\n\n        await self.connection.start_climate_control(\n            vin,\n            temp_f,\n            temp_c,\n            glass_heating,\n            seat_fl,\n            seat_fr,\n            seat_rl,\n            seat_rr,\n            climatisation_at_unlock,\n            climatisation_mode,\n        )\n\n    async def start_auxiliary_heating(self, service):\n        vin = service.data.get(CONF_VIN)\n\n        # Optional Parameters\n        duration = service.data.get(CONF_DURATION, None)\n\n        if duration is None:\n            _LOGGER.debug('Initiating \"Start Auxiliary Heating\" action...')\n        else:\n            _LOGGER.debug(\n                f'Initiating \"Start Auxiliary Heating\" action for {duration} minutes...'\n            )\n\n        await self.connection.set_vehicle_pre_heater(\n            vin=vin,\n            activate=True,\n            duration=duration,\n        )\n\n    async def set_target_soc(self, service):\n        \"\"\"Set the target state of charge for the vehicle battery.\"\"\"\n        vin = service.data.get(CONF_VIN).lower()\n        target_soc = service.data.get(CONF_TARGET_SOC)\n\n        _LOGGER.debug(\n            f\"Initiating 'Set Target SOC' action to {target_soc}% for VIN {vin}...\"\n        )\n\n        await self.connection.set_target_state_of_charge(vin, target_soc)\n\n    async def handle_notification(self, vin: str, action: str) -> None:\n        await self._refresh_vehicle_data(vin)\n\n    async def refresh_vehicle_data(self, service):\n        vin = service.data.get(CONF_VIN).lower()\n        await self._refresh_vehicle_data(vin)\n\n    async def _refresh_vehicle_data(self, vin):\n        redacted_vin = \"*\" * (len(vin) - 4) + vin[-4:]\n        res = await self.connection.refresh_vehicle_data(vin)\n\n        if res is True:\n            _LOGGER.debug(\"Refresh vehicle data successful for VIN: %s\", redacted_vin)\n            self.hass.bus.fire(\n                \"{}_{}\".format(DOMAIN, REFRESH_VEHICLE_DATA_COMPLETED_EVENT),\n                {\"vin\": redacted_vin},\n            )\n        elif res == \"disabled\":\n            _LOGGER.debug(\"Refresh vehicle data is disabled for VIN: %s\", redacted_vin)\n        else:\n            _LOGGER.debug(\"Refresh vehicle data failed for VIN: %s\", redacted_vin)\n            self.hass.bus.fire(\n                \"{}_{}\".format(DOMAIN, REFRESH_VEHICLE_DATA_FAILED_EVENT),\n                {\"vin\": redacted_vin},\n            )\n\n        _LOGGER.debug(\"Requesting to refresh cloud data in %d seconds...\", UPDATE_SLEEP)\n        await asyncio.sleep(UPDATE_SLEEP)\n\n        try:\n            _LOGGER.debug(\"Requesting to refresh cloud data now...\")\n            await self.update(utcnow())\n        except Exception as e:\n            _LOGGER.exception(\"Refresh cloud data failed: %s\", str(e))\n"
  },
  {
    "path": "custom_components/audiconnect/audi_api.py",
    "content": "import json\nimport logging\nfrom datetime import datetime\nimport asyncio\nfrom asyncio import TimeoutError, CancelledError\nfrom aiohttp import ClientResponseError\nfrom aiohttp.hdrs import METH_GET, METH_POST, METH_PUT\nfrom typing import Dict\n\n# ===========================================\n# VERBOSE DEBUG TOGGLE\n# Set to True to log EVERYTHING (full headers, full body, raw JSON, etc.)\n# Set to False for normal operation (minimal debug output)\nDEBUG_VERBOSE = False\n# ===========================================\n\nTIMEOUT = 30\n_LOGGER = logging.getLogger(__name__)\n\n\nclass AudiAPI:\n    HDR_XAPP_VERSION = \"4.31.0\"\n    HDR_USER_AGENT = \"Android/4.31.0 (Build 800341641.root project 'myaudi_android'.ext.buildTime) Android/13\"\n\n    def __init__(self, session, proxy=None):\n        self.__token = None\n        self.__xclientid = None\n        self._session = session\n        self.__proxy = {\"http\": proxy, \"https\": proxy} if proxy else None\n\n    def use_token(self, token):\n        self.__token = token\n        if DEBUG_VERBOSE:\n            _LOGGER.debug(\"[use_token] Token set: %s\", token)\n\n    def set_xclient_id(self, xclientid):\n        self.__xclientid = xclientid\n        if DEBUG_VERBOSE:\n            _LOGGER.debug(\"[set_xclient_id] X-Client-ID set: %s\", xclientid)\n\n    async def request(\n        self,\n        method,\n        url,\n        data,\n        headers: Dict[str, str] = None,\n        raw_reply: bool = False,\n        raw_contents: bool = False,\n        rsp_wtxt: bool = False,\n        **kwargs,\n    ):\n        if DEBUG_VERBOSE:\n            _LOGGER.debug(\"[REQUEST INITIATED]\")\n            _LOGGER.debug(\"Method: %s\", method)\n            _LOGGER.debug(\"URL: %s\", url)\n            _LOGGER.debug(\"Data: %s\", data)\n            _LOGGER.debug(\"Headers: %s\", headers)\n            _LOGGER.debug(\"Kwargs: %s\", kwargs)\n            _LOGGER.debug(\"Proxy: %s\", self.__proxy)\n\n        try:\n            async with asyncio.timeout(TIMEOUT):\n                async with self._session.request(\n                    method, url, headers=headers, data=data, **kwargs\n                ) as response:\n                    if DEBUG_VERBOSE:\n                        _LOGGER.debug(\"[RESPONSE RECEIVED]\")\n                        _LOGGER.debug(\"Status: %s\", response.status)\n                        _LOGGER.debug(\"Reason: %s\", response.reason)\n                        _LOGGER.debug(\"Headers: %s\", dict(response.headers))\n\n                    if raw_reply:\n                        if DEBUG_VERBOSE:\n                            _LOGGER.debug(\"Returning raw reply object.\")\n                        return response\n\n                    if rsp_wtxt:\n                        txt = await response.text()\n                        if DEBUG_VERBOSE:\n                            _LOGGER.debug(\"Response text (full): %s\", txt)\n                        else:\n                            _LOGGER.debug(\n                                \"Returning response text; length=%d\", len(txt)\n                            )\n                        return response, txt\n\n                    elif raw_contents:\n                        contents = await response.read()\n                        if DEBUG_VERBOSE:\n                            _LOGGER.debug(\"Raw contents (bytes): %s\", contents)\n                        else:\n                            _LOGGER.debug(\n                                \"Returning raw contents; length=%d\", len(contents)\n                            )\n                        return contents\n\n                    elif response.status in (200, 202, 207):\n                        raw_body = await response.text()\n                        if DEBUG_VERBOSE:\n                            _LOGGER.debug(\n                                \"Raw JSON text (before parsing): %s\", raw_body\n                            )\n                        json_data = json_loads(raw_body)\n                        if DEBUG_VERBOSE:\n                            _LOGGER.debug(\"Parsed JSON data (full): %s\", json_data)\n                        else:\n                            _LOGGER.debug(\"Returning JSON data: %s\", json_data)\n                        return json_data\n\n                    else:\n                        # this should be refactored:\n                        # 204 is a valid response for some requests (e.g. update_vehicle_position)\n                        # and should not raise an error.\n                        # request should return a tuple indicating the response itself and the\n                        # http-status\n                        if response.status != 204:\n                            _LOGGER.debug(\n                                \"Non-success response: status=%s, reason=%s — will be handled by caller\",\n                                response.status,\n                                response.reason,\n                            )\n                            if DEBUG_VERBOSE:\n                                _LOGGER.debug(\n                                    \"Response url: %s, body: %s\",\n                                    url,\n                                    await response.text(),\n                                )\n                        raise ClientResponseError(\n                            response.request_info,\n                            response.history,\n                            status=response.status,\n                            message=response.reason,\n                        )\n\n        except CancelledError:\n            if DEBUG_VERBOSE:\n                _LOGGER.debug(\"Request cancelled (CancelledError).\")\n            raise TimeoutError(\"Timeout error\")\n\n        except TimeoutError:\n            if DEBUG_VERBOSE:\n                _LOGGER.debug(\"Request timed out after %s seconds.\", TIMEOUT)\n            raise TimeoutError(\"Timeout error\")\n\n        except Exception as e:\n            if DEBUG_VERBOSE:\n                _LOGGER.exception(\"Unexpected exception during request: %s\", e)\n            raise\n\n    async def get(\n        self, url, raw_reply: bool = False, raw_contents: bool = False, **kwargs\n    ):\n        full_headers = self.__get_headers()\n        if DEBUG_VERBOSE:\n            _LOGGER.debug(\"[GET] URL: %s | Headers: %s\", url, full_headers)\n        return await self.request(\n            METH_GET,\n            url,\n            data=None,\n            headers=full_headers,\n            raw_reply=raw_reply,\n            raw_contents=raw_contents,\n            **kwargs,\n        )\n\n    async def put(self, url, data=None, headers: Dict[str, str] = None):\n        full_headers = self.__get_headers()\n        if headers:\n            full_headers.update(headers)\n        if DEBUG_VERBOSE:\n            _LOGGER.debug(\n                \"[PUT] URL: %s | Data: %s | Headers: %s\", url, data, full_headers\n            )\n        return await self.request(METH_PUT, url, headers=full_headers, data=data)\n\n    async def post(\n        self,\n        url,\n        data=None,\n        headers: Dict[str, str] = None,\n        use_json: bool = True,\n        raw_reply: bool = False,\n        raw_contents: bool = False,\n        **kwargs,\n    ):\n        full_headers = self.__get_headers()\n        if headers:\n            full_headers.update(headers)\n        if use_json and data is not None:\n            data = json.dumps(data)\n        if DEBUG_VERBOSE:\n            _LOGGER.debug(\n                \"[POST] URL: %s | Data: %s | Headers: %s\", url, data, full_headers\n            )\n        return await self.request(\n            METH_POST,\n            url,\n            headers=full_headers,\n            data=data,\n            raw_reply=raw_reply,\n            raw_contents=raw_contents,\n            **kwargs,\n        )\n\n    def __get_headers(self):\n        data = {\n            \"Accept\": \"application/json\",\n            \"Accept-Charset\": \"utf-8\",\n            \"X-App-Version\": self.HDR_XAPP_VERSION,\n            \"X-App-Name\": \"myAudi\",\n            \"User-Agent\": self.HDR_USER_AGENT,\n        }\n        if self.__token is not None:\n            data[\"Authorization\"] = \"Bearer \" + self.__token.get(\"access_token\")\n        if self.__xclientid is not None:\n            data[\"X-Client-ID\"] = self.__xclientid\n        if DEBUG_VERBOSE:\n            _LOGGER.debug(\"[HEADERS BUILT]: %s\", data)\n        return data\n\n\ndef obj_parser(obj):\n    \"\"\"Parse datetime.\"\"\"\n    for key, val in obj.items():\n        try:\n            obj[key] = datetime.strptime(val, \"%Y-%m-%dT%H:%M:%S%z\")\n        except (TypeError, ValueError):\n            pass\n    return obj\n\n\ndef json_loads(s):\n    return json.loads(s, object_hook=obj_parser)\n"
  },
  {
    "path": "custom_components/audiconnect/audi_connect_account.py",
    "content": "import time\r\nfrom datetime import datetime, timezone, timedelta\r\nimport logging\r\nimport asyncio\r\nfrom typing import List\r\nimport re\r\n\r\nfrom asyncio import TimeoutError\r\nfrom aiohttp import ClientResponseError\r\n\r\nfrom abc import ABC, abstractmethod\r\n\r\nfrom .audi_services import AudiService\r\nfrom .audi_api import AudiAPI\r\nfrom .util import log_exception, get_attr, parse_int, parse_float, parse_datetime\r\n\r\n_LOGGER = logging.getLogger(__name__)\r\n\r\nMAX_RESPONSE_ATTEMPTS = 10\r\nREQUEST_STATUS_SLEEP = 5\r\n\r\nACTION_LOCK = \"lock\"\r\nACTION_CLIMATISATION = \"climatisation\"\r\nACTION_CHARGER = \"charger\"\r\nACTION_WINDOW_HEATING = \"window_heating\"\r\nACTION_PRE_HEATER = \"pre_heater\"\r\n\r\n\r\nclass AudiConnectObserver(ABC):\r\n    @abstractmethod\r\n    async def handle_notification(self, vin: str, action: str) -> None:\r\n        pass\r\n\r\n\r\nclass AudiConnectAccount:\r\n    \"\"\"Representation of an Audi Connect Account.\"\"\"\r\n\r\n    def __init__(\r\n        self,\r\n        session,\r\n        username: str,\r\n        password: str,\r\n        country: str,\r\n        spin: str,\r\n        api_level: int,\r\n        excluded_vins: List[str] = None,\r\n    ) -> None:\r\n        self._api = AudiAPI(session)\r\n        self._audi_service = AudiService(self._api, country, spin, api_level)\r\n\r\n        self._username = username\r\n        self._password = password\r\n        self._loggedin = False\r\n        self._support_vehicle_refresh = True\r\n        self._logintime = 0\r\n\r\n        self._connect_retries = 3\r\n        self._connect_delay = 10\r\n\r\n        self._update_listeners = []\r\n\r\n        self._vehicles = []\r\n        self._audi_vehicles = []\r\n        self._excluded_vins = [v.lower() for v in (excluded_vins or [])]\r\n\r\n        self._observers: List[AudiConnectObserver] = []\r\n\r\n    def add_observer(self, observer: AudiConnectObserver) -> None:\r\n        self._observers.append(observer)\r\n\r\n    async def notify(self, vin: str, action: str) -> None:\r\n        for observer in self._observers:\r\n            await observer.handle_notification(vin, action)\r\n\r\n    async def login(self):\r\n        for i in range(self._connect_retries):\r\n            self._loggedin = await self.try_login(i == self._connect_retries - 1)\r\n            if self._loggedin is True:\r\n                self._logintime = time.time()\r\n                break\r\n\r\n            if i < self._connect_retries - 1:\r\n                _LOGGER.warning(\r\n                    \"LOGIN: Login to Audi service failed, retrying in %s seconds\",\r\n                    self._connect_delay,\r\n                )\r\n                await asyncio.sleep(self._connect_delay)\r\n\r\n    async def try_login(self, logError):\r\n        try:\r\n            _LOGGER.debug(\"LOGIN: Requesting login to Audi service...\")\r\n            await self._audi_service.login(self._username, self._password, False)\r\n            _LOGGER.debug(\"LOGIN: Login to Audi service successful\")\r\n            return True\r\n        except Exception as exception:\r\n            if logError is True:\r\n                _LOGGER.error(\r\n                    \"LOGIN: Failed to log in to the Audi service: %s.\"\r\n                    \"You may need to open the myAudi app, or log in via a web browser, to accept updated terms and conditions.\",\r\n                    str(exception),\r\n                )\r\n            return False\r\n\r\n    async def update(self, vinlist):\r\n        if not self._loggedin:\r\n            await self.login()\r\n\r\n        if not self._loggedin:\r\n            return False\r\n\r\n        #\r\n        elapsed_sec = time.time() - self._logintime\r\n        if await self._audi_service.refresh_token_if_necessary(elapsed_sec):\r\n            # Store current timestamp when refresh was performed and successful\r\n            self._logintime = time.time()\r\n\r\n        \"\"\"Update the state of all vehicles.\"\"\"\r\n        try:\r\n            if len(self._audi_vehicles) > 0:\r\n                for vehicle in self._audi_vehicles:\r\n                    if vehicle.vin and vehicle.vin.lower() in self._excluded_vins:\r\n                        continue\r\n                    await self.add_or_update_vehicle(vehicle, vinlist)\r\n\r\n            else:\r\n                vehicles_response = await self._audi_service.get_vehicle_information()\r\n                self._audi_vehicles = vehicles_response.vehicles\r\n                self._vehicles = []\r\n                for vehicle in self._audi_vehicles:\r\n                    if vehicle.vin and vehicle.vin.lower() in self._excluded_vins:\r\n                        _LOGGER.debug(\"Skipping excluded vehicle VIN: %s\", vehicle.vin)\r\n                        continue\r\n                    await self.add_or_update_vehicle(vehicle, vinlist)\r\n\r\n            for listener in self._update_listeners:\r\n                listener()\r\n\r\n            # TR/2021-12-01: do not set to False as refresh_token is used\r\n            # self._loggedin = False\r\n\r\n            return True\r\n\r\n        except OSError as exception:\r\n            # Force a re-login in case of failure/exception\r\n            self._loggedin = False\r\n            _LOGGER.exception(exception)\r\n            return False\r\n\r\n    async def add_or_update_vehicle(self, vehicle, vinlist):\r\n        if vehicle.vin is not None:\r\n            if vinlist is None or vehicle.vin.lower() in vinlist:\r\n                vupd = [x for x in self._vehicles if x.vin == vehicle.vin.lower()]\r\n                if len(vupd) > 0:\r\n                    if await vupd[0].update() is False:\r\n                        self._loggedin = False\r\n                else:\r\n                    try:\r\n                        audiVehicle = AudiConnectVehicle(self._audi_service, vehicle)\r\n                        if await audiVehicle.update() is False:\r\n                            self._loggedin = False\r\n                        self._vehicles.append(audiVehicle)\r\n                    except Exception:\r\n                        pass\r\n\r\n    async def refresh_vehicle_data(self, vin: str):\r\n        redacted_vin = \"*\" * (len(vin) - 4) + vin[-4:]\r\n        if not self._loggedin:\r\n            await self.login()\r\n\r\n        if not self._loggedin:\r\n            return False\r\n\r\n        if not self._support_vehicle_refresh:\r\n            _LOGGER.debug(\r\n                \"Vehicle refresh support is disabled for VIN: %s. Exiting update process.\",\r\n                redacted_vin,\r\n            )\r\n            return \"disabled\"\r\n\r\n        try:\r\n            _LOGGER.debug(\r\n                \"Sending command to refresh vehicle data for VIN: %s\",\r\n                redacted_vin,\r\n            )\r\n\r\n            await self._audi_service.refresh_vehicle_data(vin)\r\n\r\n            _LOGGER.debug(\r\n                \"Successfully refreshed vehicle data for VIN: %s\",\r\n                redacted_vin,\r\n            )\r\n\r\n            return True\r\n\r\n        except TimeoutError:\r\n            _LOGGER.debug(\r\n                \"TimeoutError encountered while refreshing vehicle data for VIN: %s.\",\r\n                redacted_vin,\r\n            )\r\n            return False\r\n        except ClientResponseError as cre:\r\n            if cre.status in (403, 404):\r\n                _LOGGER.debug(\r\n                    \"VEHICLE REFRESH: ClientResponseError with status %s for VIN: %s. Vehicle does not support vehicle refresh — disabling.\",\r\n                    cre.status,\r\n                    redacted_vin,\r\n                )\r\n                self._support_vehicle_refresh = False\r\n                return \"disabled\"\r\n            elif cre.status == 502:\r\n                _LOGGER.debug(\r\n                    \"VEHICLE REFRESH: Received status %s while refreshing vehicle data for VIN: %s. This is typically transient and may resolve on its own.\",\r\n                    cre.status,\r\n                    redacted_vin,\r\n                )\r\n                return False\r\n            elif cre.status != 204:\r\n                _LOGGER.debug(\r\n                    \"VEHICLE REFRESH: ClientResponseError with status %s while refreshing vehicle data for VIN: %s. Error: %s\",\r\n                    cre.status,\r\n                    redacted_vin,\r\n                    cre,\r\n                )\r\n                return False\r\n            else:\r\n                _LOGGER.debug(\r\n                    \"VEHICLE REFRESH: Refresh vehicle data currently not available for VIN: %s. Received 204 status.\",\r\n                    redacted_vin,\r\n                )\r\n                return False\r\n\r\n        except Exception as e:\r\n            _LOGGER.error(\r\n                \"VEHICLE REFRESH: An unexpected error occurred while refreshing vehicle data for VIN: %s. Error: %s\",\r\n                redacted_vin,\r\n                e,\r\n            )\r\n            return False\r\n\r\n    async def set_vehicle_lock(self, vin: str, lock: bool):\r\n        if not self._loggedin:\r\n            await self.login()\r\n\r\n        if not self._loggedin:\r\n            return False\r\n\r\n        try:\r\n            _LOGGER.debug(\r\n                \"Sending command to {action} to vehicle {vin}\".format(\r\n                    action=\"lock\" if lock else \"unlock\", vin=vin\r\n                ),\r\n            )\r\n\r\n            await self._audi_service.set_vehicle_lock(vin, lock)\r\n\r\n            _LOGGER.debug(\r\n                \"Successfully {action} vehicle {vin}\".format(\r\n                    action=\"locked\" if lock else \"unlocked\", vin=vin\r\n                ),\r\n            )\r\n\r\n            await self.notify(vin, ACTION_LOCK)\r\n\r\n            return True\r\n\r\n        except Exception as exception:\r\n            log_exception(\r\n                exception,\r\n                \"Unable to {action} {vin}\".format(\r\n                    action=\"lock\" if lock else \"unlock\", vin=vin\r\n                ),\r\n            )\r\n\r\n    async def set_target_state_of_charge(self, vin: str, target_soc: int):\r\n        \"\"\"Set the target state of charge for the vehicle battery.\"\"\"\r\n        if not self._loggedin:\r\n            await self.login()\r\n\r\n        if not self._loggedin:\r\n            return False\r\n\r\n        try:\r\n            _LOGGER.debug(\r\n                \"Setting target state of charge to %d%% for vehicle %s\",\r\n                target_soc,\r\n                vin,\r\n            )\r\n\r\n            await self._audi_service.set_target_state_of_charge(vin, target_soc)\r\n\r\n            _LOGGER.debug(\r\n                \"Successfully set target state of charge to %d%% for vehicle %s\",\r\n                target_soc,\r\n                vin,\r\n            )\r\n\r\n            return True\r\n\r\n        except Exception as exception:\r\n            log_exception(\r\n                exception,\r\n                \"Unable to set target state of charge for vehicle {}\".format(vin),\r\n            )\r\n            return False\r\n\r\n    async def set_vehicle_climatisation(self, vin: str, activate: bool):\r\n        if not self._loggedin:\r\n            await self.login()\r\n\r\n        if not self._loggedin:\r\n            return False\r\n\r\n        try:\r\n            _LOGGER.debug(\r\n                \"Sending command to {action} climatisation to vehicle {vin}\".format(\r\n                    action=\"start\" if activate else \"stop\", vin=vin\r\n                ),\r\n            )\r\n\r\n            await self._audi_service.set_climatisation(vin, activate)\r\n\r\n            _LOGGER.debug(\r\n                \"Successfully {action} climatisation of vehicle {vin}\".format(\r\n                    action=\"started\" if activate else \"stopped\", vin=vin\r\n                ),\r\n            )\r\n\r\n            await self.notify(vin, ACTION_CLIMATISATION)\r\n\r\n            return True\r\n\r\n        except Exception as exception:\r\n            log_exception(\r\n                exception,\r\n                \"Unable to {action} climatisation of vehicle {vin}\".format(\r\n                    action=\"start\" if activate else \"stop\", vin=vin\r\n                ),\r\n            )\r\n\r\n    async def start_climate_control(\r\n        self,\r\n        vin: str,\r\n        temp_f: int,\r\n        temp_c: int,\r\n        glass_heating: bool,\r\n        seat_fl: bool,\r\n        seat_fr: bool,\r\n        seat_rl: bool,\r\n        seat_rr: bool,\r\n        climatisation_at_unlock: bool,\r\n        climatisation_mode: str,\r\n    ):\r\n        if not self._loggedin:\r\n            await self.login()\r\n\r\n        if not self._loggedin:\r\n            return False\r\n\r\n        try:\r\n            _LOGGER.debug(\r\n                f\"Sending command to start climate control for vehicle {vin} with settings - Temp(F): {temp_f}, Temp(C): {temp_c}, Glass Heating: {glass_heating}, Seat FL: {seat_fl}, Seat FR: {seat_fr}, Seat RL: {seat_rl}, Seat RR: {seat_rr}, Climatisation at Unlock: {climatisation_at_unlock}, Climatisation Mode: {climatisation_mode}\"\r\n            )\r\n\r\n            await self._audi_service.start_climate_control(\r\n                vin,\r\n                temp_f,\r\n                temp_c,\r\n                glass_heating,\r\n                seat_fl,\r\n                seat_fr,\r\n                seat_rl,\r\n                seat_rr,\r\n                climatisation_at_unlock,\r\n                climatisation_mode,\r\n            )\r\n\r\n            _LOGGER.debug(f\"Successfully started climate control of vehicle {vin}\")\r\n\r\n            await self.notify(vin, ACTION_CLIMATISATION)\r\n\r\n            return True\r\n\r\n        except Exception as exception:\r\n            _LOGGER.error(\r\n                f\"Unable to start climate control of vehicle {vin}. Error: {exception}\",\r\n                exc_info=True,\r\n            )\r\n            return False\r\n\r\n    async def set_battery_charger(self, vin: str, activate: bool, timer: bool):\r\n        if not self._loggedin:\r\n            await self.login()\r\n\r\n        if not self._loggedin:\r\n            return False\r\n\r\n        try:\r\n            _LOGGER.debug(\r\n                \"Sending command to {action}{timer} charger to vehicle {vin}\".format(\r\n                    action=\"start\" if activate else \"stop\",\r\n                    vin=vin,\r\n                    timer=\" timed\" if timer else \"\",\r\n                ),\r\n            )\r\n\r\n            await self._audi_service.set_battery_charger(vin, activate, timer)\r\n\r\n            _LOGGER.debug(\r\n                \"Successfully {action}{timer} charger of vehicle {vin}\".format(\r\n                    action=\"started\" if activate else \"stopped\",\r\n                    vin=vin,\r\n                    timer=\" timed\" if timer else \"\",\r\n                ),\r\n            )\r\n\r\n            await self.notify(vin, ACTION_CHARGER)\r\n\r\n            return True\r\n\r\n        except Exception as exception:\r\n            log_exception(\r\n                exception,\r\n                \"Unable to {action} charger of vehicle {vin}\".format(\r\n                    action=\"start\" if activate else \"stop\", vin=vin\r\n                ),\r\n            )\r\n\r\n    async def set_vehicle_window_heating(self, vin: str, activate: bool):\r\n        if not self._loggedin:\r\n            await self.login()\r\n\r\n        if not self._loggedin:\r\n            return False\r\n\r\n        try:\r\n            _LOGGER.debug(\r\n                \"Sending command to {action} window heating to vehicle {vin}\".format(\r\n                    action=\"start\" if activate else \"stop\", vin=vin\r\n                ),\r\n            )\r\n\r\n            await self._audi_service.set_window_heating(vin, activate)\r\n\r\n            _LOGGER.debug(\r\n                \"Successfully {action} window heating of vehicle {vin}\".format(\r\n                    action=\"started\" if activate else \"stopped\", vin=vin\r\n                ),\r\n            )\r\n\r\n            await self.notify(vin, ACTION_WINDOW_HEATING)\r\n\r\n            return True\r\n\r\n        except Exception as exception:\r\n            log_exception(\r\n                exception,\r\n                \"Unable to {action} window heating of vehicle {vin}\".format(\r\n                    action=\"start\" if activate else \"stop\", vin=vin\r\n                ),\r\n            )\r\n\r\n    async def set_vehicle_pre_heater(self, vin: str, activate: bool, **kwargs):\r\n        if not self._loggedin:\r\n            await self.login()\r\n\r\n        if not self._loggedin:\r\n            return False\r\n\r\n        try:\r\n            _LOGGER.debug(\r\n                \"Sending command to {action} pre-heater to vehicle {vin}\".format(\r\n                    action=\"start\" if activate else \"stop\", vin=vin\r\n                ),\r\n            )\r\n\r\n            # Pass **kwargs down\r\n            await self._audi_service.set_pre_heater(vin, activate, **kwargs)\r\n\r\n            _LOGGER.debug(\r\n                \"Successfully {action} pre-heater of vehicle {vin}\".format(\r\n                    action=\"started\" if activate else \"stopped\", vin=vin\r\n                ),\r\n            )\r\n\r\n            await self.notify(vin, ACTION_PRE_HEATER)\r\n\r\n            return True\r\n\r\n        except Exception as exception:\r\n            log_exception(\r\n                exception,\r\n                \"Unable to {action} pre-heater of vehicle {vin}\".format(\r\n                    action=\"start\" if activate else \"stop\", vin=vin\r\n                ),\r\n            )\r\n\r\n\r\nclass AudiConnectVehicle:\r\n    def __init__(self, audi_service: AudiService, vehicle) -> None:\r\n        self._audi_service = audi_service\r\n        self._vehicle = vehicle\r\n        self._vin = vehicle.vin.lower()\r\n        self._vehicle.state = {}\r\n        self._vehicle.fields = {}\r\n        self._logged_errors = set()\r\n        self._no_error = False\r\n\r\n        self.support_status_report = True\r\n        self.support_position = True\r\n        self.support_climater = True\r\n        self.support_preheater = True\r\n        self.support_charger = True\r\n        self.support_trip_data = True\r\n\r\n        self.charging_complete_time_frozen = None\r\n\r\n    @property\r\n    def vin(self):\r\n        return self._vin\r\n\r\n    @property\r\n    def csid(self):\r\n        return self._vehicle.csid\r\n\r\n    @property\r\n    def title(self):\r\n        return self._vehicle.title\r\n\r\n    @property\r\n    def model(self):\r\n        return self._vehicle.model\r\n\r\n    @property\r\n    def model_year(self):\r\n        return self._vehicle.model_year\r\n\r\n    @property\r\n    def model_family(self):\r\n        return self._vehicle.model_family\r\n\r\n    async def call_update(self, func, ntries: int):\r\n        try:\r\n            await func()\r\n        except TimeoutError:\r\n            if ntries > 1:\r\n                await asyncio.sleep(2)\r\n                await self.call_update(func, ntries - 1)\r\n            else:\r\n                raise\r\n\r\n    async def update(self):\r\n        info = \"\"\r\n        try:\r\n            self._no_error = True\r\n            info = \"statusreport\"\r\n            await self.call_update(self.update_vehicle_statusreport, 3)\r\n            info = \"shortterm\"\r\n            await self.call_update(self.update_vehicle_shortterm, 3)\r\n            info = \"longterm\"\r\n            await self.call_update(self.update_vehicle_longterm, 3)\r\n            info = \"position\"\r\n            await self.call_update(self.update_vehicle_position, 3)\r\n            info = \"climater\"\r\n            await self.call_update(self.update_vehicle_climater, 3)\r\n            # info = \"charger\"\r\n            # await self.call_update(self.update_vehicle_charger, 3)\r\n            info = \"preheater\"\r\n            await self.call_update(self.update_vehicle_preheater, 3)\r\n            # Return True on success, False on error\r\n            return self._no_error\r\n        except Exception as exception:\r\n            log_exception(\r\n                exception,\r\n                \"Unable to update vehicle data {} of {}\".format(\r\n                    info, self._vehicle.vin\r\n                ),\r\n            )\r\n\r\n    def log_exception_once(self, exception, message):\r\n        self._no_error = False\r\n        err = message + \": \" + str(exception).rstrip(\"\\n\")\r\n        if err not in self._logged_errors:\r\n            self._logged_errors.add(err)\r\n            _LOGGER.error(err, exc_info=True)\r\n\r\n    async def update_vehicle_statusreport(self):\r\n        if not self.support_status_report:\r\n            return\r\n\r\n        try:\r\n            status = await self._audi_service.get_stored_vehicle_data(self._vehicle.vin)\r\n            self._vehicle.fields = {\r\n                status.data_fields[i].name: status.data_fields[i].value\r\n                for i in range(len(status.data_fields))\r\n            }\r\n\r\n            # Initialize with a default very old datetime\r\n            self._vehicle.state[\"last_update_time\"] = datetime(\r\n                1970, 1, 1, tzinfo=timezone.utc\r\n            )\r\n\r\n            # Update with the newest carCapturedTimestamp from data_fields\r\n            for f in status.data_fields:\r\n                new_time = parse_datetime(f.measure_time)\r\n                if new_time:\r\n                    self._vehicle.state[\"last_update_time\"] = max(\r\n                        self._vehicle.state[\"last_update_time\"], new_time\r\n                    )\r\n\r\n            # Update with the newest carCapturedTimestamp from states\r\n            for state in status.states:\r\n                new_time = parse_datetime(state.get(\"measure_time\"))\r\n                if new_time:\r\n                    self._vehicle.state[\"last_update_time\"] = max(\r\n                        self._vehicle.state[\"last_update_time\"], new_time\r\n                    )\r\n\r\n            # Update other states\r\n            for state in status.states:\r\n                self._vehicle.state[state[\"name\"]] = state[\"value\"]\r\n\r\n        except TimeoutError:\r\n            raise\r\n        except ClientResponseError as resp_exception:\r\n            if resp_exception.status in (403, 404):\r\n                self.support_status_report = False\r\n            else:\r\n                self.log_exception_once(\r\n                    resp_exception,\r\n                    \"Unable to obtain the vehicle status report of {}\".format(\r\n                        self._vehicle.vin\r\n                    ),\r\n                )\r\n        except Exception as exception:\r\n            self.log_exception_once(\r\n                exception,\r\n                \"Unable to obtain the vehicle status report of {}\".format(\r\n                    self._vehicle.vin\r\n                ),\r\n            )\r\n\r\n    async def update_vehicle_position(self):\r\n        # Redact all but the last 4 characters of the VIN\r\n        redacted_vin = \"*\" * (len(self._vehicle.vin) - 4) + self._vehicle.vin[-4:]\r\n        _LOGGER.debug(\r\n            \"POSITION: Starting update_vehicle_position for VIN: %s\", redacted_vin\r\n        )\r\n\r\n        if not self.support_position:\r\n            _LOGGER.debug(\r\n                \"POSITION: Vehicle position support is disabled for VIN: %s. Exiting update process.\",\r\n                redacted_vin,\r\n            )\r\n            return\r\n\r\n        try:\r\n            _LOGGER.debug(\r\n                \"POSITION: Attempting to retrieve stored vehicle position for VIN: %s\",\r\n                redacted_vin,\r\n            )\r\n            resp = await self._audi_service.get_stored_position(self._vehicle.vin)\r\n            # To enable detailed logging of raw vehicle position data for debugging purposes:\r\n            # 1. Remove the '#' from the start of the _LOGGER.debug line below.\r\n            # 2. Save the file.\r\n            # 3. Restart Home Assistant to apply the changes.\r\n            # Note: This will log sensitive data. To stop logging this data:\r\n            # 1. Add the '#' back at the start of the _LOGGER.debug line.\r\n            # 2. Save the file and restart Home Assistant again.\r\n            # _LOGGER.debug(\"POSITION - UNREDACTED SENSITIVE DATA: Raw vehicle position data: %s\", resp)\r\n            if resp is not None:\r\n                redacted_lat = re.sub(r\"\\d\", \"#\", str(resp[\"data\"][\"lat\"]))\r\n                redacted_lon = re.sub(r\"\\d\", \"#\", str(resp[\"data\"][\"lon\"]))\r\n\r\n                # Check if 'carCapturedTimestamp' is available in the data\r\n                if \"carCapturedTimestamp\" in resp[\"data\"]:\r\n                    timestamp = parse_datetime(resp[\"data\"][\"carCapturedTimestamp\"])\r\n                    parktime = parse_datetime(resp[\"data\"][\"carCapturedTimestamp\"])\r\n                else:\r\n                    # Log and use None timestamp and parktime\r\n                    timestamp = None\r\n                    parktime = None\r\n                    _LOGGER.debug(\r\n                        \"POSITION: Timestamp not available for vehicle position data of VIN: %s.\",\r\n                        redacted_vin,\r\n                    )\r\n                _LOGGER.debug(\r\n                    \"POSITION: Vehicle position data received for VIN: %s, lat: %s, lon: %s, timestamp: %s, parktime: %s\",\r\n                    redacted_vin,\r\n                    redacted_lat,\r\n                    redacted_lon,\r\n                    timestamp,\r\n                    parktime,\r\n                )\r\n\r\n                self._vehicle.state[\"position\"] = {\r\n                    \"latitude\": resp[\"data\"][\"lat\"],\r\n                    \"longitude\": resp[\"data\"][\"lon\"],\r\n                    \"timestamp\": timestamp,\r\n                    \"parktime\": parktime,\r\n                }\r\n\r\n                self._vehicle.state[\"is_moving\"] = False\r\n\r\n                _LOGGER.debug(\r\n                    \"POSITION: Vehicle position updated successfully for VIN: %s\",\r\n                    redacted_vin,\r\n                )\r\n            else:\r\n                _LOGGER.warning(\r\n                    \"POSITION: No vehicle position data received for VIN: %s. Response was None.\",\r\n                    redacted_vin,\r\n                )\r\n\r\n        except TimeoutError:\r\n            _LOGGER.warning(\r\n                \"POSITION: TimeoutError encountered while updating vehicle position for VIN: %s.\",\r\n                redacted_vin,\r\n            )\r\n            raise\r\n        except ClientResponseError as cre:\r\n            if cre.status in (403, 404):\r\n                _LOGGER.debug(\r\n                    \"POSITION: ClientResponseError with status %s for VIN: %s. Vehicle does not support position — disabling.\",\r\n                    cre.status,\r\n                    redacted_vin,\r\n                )\r\n                self.support_position = False\r\n            elif cre.status == 502:\r\n                _LOGGER.debug(\r\n                    \"POSITION: Received status %s while updating vehicle position for VIN: %s. This is typically transient and may resolve on its own.\",\r\n                    cre.status,\r\n                    redacted_vin,\r\n                )\r\n            elif cre.status != 204:\r\n                _LOGGER.warning(\r\n                    \"POSITION: ClientResponseError with status %s for VIN: %s. Error: %s\",\r\n                    cre.status,\r\n                    redacted_vin,\r\n                    cre,\r\n                )\r\n            else:\r\n                _LOGGER.debug(\r\n                    \"POSITION: Vehicle position currently not available for VIN: %s (Is moving?!). Received 204 status.\",\r\n                    redacted_vin,\r\n                )\r\n                # we receive a 204 when the vehicle is moving.\r\n                self._vehicle.state[\"is_moving\"] = True\r\n\r\n        except Exception as e:\r\n            _LOGGER.error(\r\n                \"POSITION: An unexpected error occurred while updating vehicle position for VIN: %s. Error: %s\",\r\n                redacted_vin,\r\n                e,\r\n            )\r\n\r\n    async def update_vehicle_climater(self):\r\n        redacted_vin = \"*\" * (len(self._vehicle.vin) - 4) + self._vehicle.vin[-4:]\r\n        if not self.support_climater:\r\n            return\r\n\r\n        try:\r\n            result = await self._audi_service.get_climater(self._vehicle.vin)\r\n            if result:\r\n                self._vehicle.state[\"climatisationState\"] = get_attr(\r\n                    result,\r\n                    \"climater.status.climatisationStatusData.climatisationState.content\",\r\n                )\r\n                tmp = get_attr(\r\n                    result,\r\n                    \"climater.status.temperatureStatusData.outdoorTemperature.content\",\r\n                )\r\n                if tmp is not None:\r\n                    self._vehicle.state[\"outdoorTemperature\"] = round(\r\n                        float(tmp) / 10 - 273, 1\r\n                    )\r\n                else:\r\n                    self._vehicle.state[\"outdoorTemperature\"] = None\r\n\r\n                remainingClimatisationTime = get_attr(\r\n                    result,\r\n                    \"climater.status.climatisationStatusData.remainingClimatisationTime.content\",\r\n                )\r\n                self._vehicle.state[\"remainingClimatisationTime\"] = (\r\n                    remainingClimatisationTime\r\n                )\r\n                _LOGGER.debug(\r\n                    \"CLIMATER: remainingClimatisationTime: %s\",\r\n                    remainingClimatisationTime,\r\n                )\r\n\r\n                vehicleParkingClock = get_attr(\r\n                    result,\r\n                    \"climater.status.vehicleParkingClockStatusData.vehicleParkingClock.content\",\r\n                )\r\n                self._vehicle.state[\"vehicleParkingClock\"] = parse_datetime(\r\n                    vehicleParkingClock\r\n                )\r\n                _LOGGER.debug(\"CLIMATER: vehicleParkingClock: %s\", vehicleParkingClock)\r\n\r\n                isMirrorHeatingActive = get_attr(\r\n                    result,\r\n                    \"climater.status.climatisationStatusData.climatisationElementStates.isMirrorHeatingActive.content\",\r\n                )\r\n                self._vehicle.state[\"isMirrorHeatingActive\"] = isMirrorHeatingActive\r\n                _LOGGER.debug(\r\n                    \"CLIMATER: isMirrorHeatingActive: %s\", isMirrorHeatingActive\r\n                )\r\n\r\n            else:\r\n                _LOGGER.debug(\r\n                    \"No climater data received for VIN: %s. Response was None.\",\r\n                    redacted_vin,\r\n                )\r\n\r\n        except TimeoutError:\r\n            _LOGGER.debug(\r\n                \"TimeoutError encountered while updating climater for VIN: %s.\",\r\n                redacted_vin,\r\n            )\r\n            raise\r\n        except ClientResponseError as cre:\r\n            if cre.status in (403, 404):\r\n                _LOGGER.debug(\r\n                    \"CLIMATER: ClientResponseError with status %s for VIN: %s. Vehicle does not support climater — disabling.\",\r\n                    cre.status,\r\n                    redacted_vin,\r\n                )\r\n                self.support_climater = False\r\n            elif cre.status == 502:\r\n                _LOGGER.debug(\r\n                    \"CLIMATER: Received status %s while updating climater for VIN: %s. This is typically transient and may resolve on its own.\",\r\n                    cre.status,\r\n                    redacted_vin,\r\n                )\r\n            elif cre.status != 204:\r\n                _LOGGER.debug(\r\n                    \"ClientResponseError with status %s while updating climater for VIN: %s. Error: %s\",\r\n                    cre.status,\r\n                    redacted_vin,\r\n                    cre,\r\n                )\r\n            else:\r\n                _LOGGER.debug(\r\n                    \"Climater currently not available for VIN: %s. Received 204 status.\",\r\n                    redacted_vin,\r\n                )\r\n\r\n        except Exception as e:\r\n            _LOGGER.error(\r\n                \"An unexpected error occurred while updating climater for VIN: %s. Error: %s\",\r\n                redacted_vin,\r\n                e,\r\n            )\r\n\r\n    async def update_vehicle_preheater(self):\r\n        redacted_vin = \"*\" * (len(self._vehicle.vin) - 4) + self._vehicle.vin[-4:]\r\n        if not self.support_preheater:\r\n            return\r\n\r\n        try:\r\n            result = await self._audi_service.get_preheater(self._vehicle.vin)\r\n            if result:\r\n                self._vehicle.state[\"preheaterState\"] = get_attr(\r\n                    result,\r\n                    \"statusResponse\",\r\n                )\r\n\r\n        except TimeoutError:\r\n            raise\r\n        except ClientResponseError as cre:\r\n            if cre.status in (403, 404, 502):\r\n                _LOGGER.debug(\r\n                    \"PREHEATER: ClientResponseError with status %s for VIN: %s. Vehicle does not support preheater — disabling.\",\r\n                    cre.status,\r\n                    redacted_vin,\r\n                )\r\n                self.support_preheater = False\r\n            # elif cre.status == 502:\r\n            #    _LOGGER.warning(\r\n            #        \"PREHEATER: ClientResponseError with status %s while updating preheater for VIN: %s. This issue may resolve in time. If it persists, please open an issue.\",\r\n            #        cre.status,\r\n            #        redacted_vin,\r\n            #    )\r\n            else:\r\n                self.log_exception_once(\r\n                    cre,\r\n                    \"Unable to obtain the vehicle preheater state for {}\".format(\r\n                        self._vehicle.vin\r\n                    ),\r\n                )\r\n        except Exception as exception:\r\n            self.log_exception_once(\r\n                exception,\r\n                \"Unable to obtain the vehicle preheater state for {}\".format(\r\n                    self._vehicle.vin\r\n                ),\r\n            )\r\n\r\n    async def update_vehicle_charger(self):\r\n        redacted_vin = \"*\" * (len(self._vehicle.vin) - 4) + self._vehicle.vin[-4:]\r\n        if not self.support_charger:\r\n            return\r\n\r\n        try:\r\n            result = await self._audi_service.get_charger(self._vehicle.vin)\r\n            if result:\r\n                self._vehicle.state[\"maxChargeCurrent\"] = get_attr(\r\n                    result, \"charger.settings.maxChargeCurrent.content\"\r\n                )\r\n\r\n                self._vehicle.state[\"chargingState\"] = get_attr(\r\n                    result, \"charger.status.chargingStatusData.chargingState.content\"\r\n                )\r\n                self._vehicle.state[\"actualChargeRate\"] = get_attr(\r\n                    result, \"charger.status.chargingStatusData.actualChargeRate.content\"\r\n                )\r\n                if self._vehicle.state[\"actualChargeRate\"] is not None:\r\n                    self._vehicle.state[\"actualChargeRate\"] = float(\r\n                        self._vehicle.state[\"actualChargeRate\"]\r\n                    )\r\n                self._vehicle.state[\"actualChargeRateUnit\"] = get_attr(\r\n                    result, \"charger.status.chargingStatusData.chargeRateUnit.content\"\r\n                )\r\n                self._vehicle.state[\"chargingPower\"] = get_attr(\r\n                    result, \"charger.status.chargingStatusData.chargingPower.content\"\r\n                )\r\n                self._vehicle.state[\"chargingMode\"] = get_attr(\r\n                    result, \"charger.status.chargingStatusData.chargingMode.content\"\r\n                )\r\n\r\n                self._vehicle.state[\"energyFlow\"] = get_attr(\r\n                    result, \"charger.status.chargingStatusData.energyFlow.content\"\r\n                )\r\n\r\n                self._vehicle.state[\"engineTypeFirstEngine\"] = get_attr(\r\n                    result,\r\n                    \"charger.status.cruisingRangeStatusData.engineTypeFirstEngine.content\",\r\n                )\r\n                self._vehicle.state[\"engineTypeSecondEngine\"] = get_attr(\r\n                    result,\r\n                    \"charger.status.cruisingRangeStatusData.engineTypeSecondEngine.content\",\r\n                )\r\n                self._vehicle.state[\"hybridRange\"] = get_attr(\r\n                    result, \"charger.status.cruisingRangeStatusData.hybridRange.content\"\r\n                )\r\n                self._vehicle.state[\"primaryEngineRange\"] = get_attr(\r\n                    result,\r\n                    \"charger.status.cruisingRangeStatusData.primaryEngineRange.content\",\r\n                )\r\n                self._vehicle.state[\"secondaryEngineRange\"] = get_attr(\r\n                    result,\r\n                    \"charger.status.cruisingRangeStatusData.secondaryEngineRange.content\",\r\n                )\r\n\r\n                self._vehicle.state[\"stateOfCharge\"] = get_attr(\r\n                    result, \"charger.status.batteryStatusData.stateOfCharge.content\"\r\n                )\r\n                self._vehicle.state[\"remainingChargingTime\"] = get_attr(\r\n                    result,\r\n                    \"charger.status.batteryStatusData.remainingChargingTime.content\",\r\n                )\r\n                self._vehicle.state[\"plugState\"] = get_attr(\r\n                    result, \"charger.status.plugStatusData.plugState.content\"\r\n                )\r\n                self._vehicle.state[\"plugLockState\"] = get_attr(\r\n                    result, \"charger.status.plugStatusData.plugLockState.content\"\r\n                )\r\n                self._vehicle.state[\"externalPower\"] = get_attr(\r\n                    result, \"charger.status.plugStatusData.externalPower.content\"\r\n                )\r\n                self._vehicle.state[\"plugledColor\"] = get_attr(\r\n                    result, \"charger.status.plugStatusData.plugledColor.content\"\r\n                )\r\n\r\n        except TimeoutError:\r\n            raise\r\n        except ClientResponseError as cre:\r\n            if cre.status in (403, 404):\r\n                _LOGGER.debug(\r\n                    \"CHARGER: ClientResponseError with status %s for VIN: %s. Vehicle does not support charger — disabling.\",\r\n                    cre.status,\r\n                    redacted_vin,\r\n                )\r\n                self.support_charger = False\r\n            elif cre.status == 502:\r\n                _LOGGER.debug(\r\n                    \"CHARGER: Received status %s while updating charger for VIN: %s. This is typically transient and may resolve on its own.\",\r\n                    cre.status,\r\n                    redacted_vin,\r\n                )\r\n            else:\r\n                self.log_exception_once(\r\n                    cre,\r\n                    \"Unable to obtain the vehicle charger state for {}\".format(\r\n                        self._vehicle.vin\r\n                    ),\r\n                )\r\n        except Exception as exception:\r\n            self.log_exception_once(\r\n                exception,\r\n                \"Unable to obtain the vehicle charger state for {}\".format(\r\n                    self._vehicle.vin\r\n                ),\r\n            )\r\n\r\n    async def update_vehicle_longterm(self):\r\n        await self.update_vehicle_tripdata(\"longTerm\")\r\n\r\n    async def update_vehicle_shortterm(self):\r\n        await self.update_vehicle_tripdata(\"shortTerm\")\r\n\r\n    async def update_vehicle_tripdata(self, kind: str):\r\n        redacted_vin = \"*\" * (len(self._vehicle.vin) - 4) + self._vehicle.vin[-4:]\r\n        if not self.support_trip_data:\r\n            _LOGGER.debug(\r\n                \"TRIP DATA: Trip data support is disabled for VIN: %s. Exiting update process.\",\r\n                redacted_vin,\r\n            )\r\n            return\r\n        try:\r\n            td_cur, td_rst = await self._audi_service.get_tripdata(\r\n                self._vehicle.vin, kind\r\n            )\r\n            self._vehicle.state[kind.lower() + \"_current\"] = {\r\n                \"tripID\": td_cur.tripID,\r\n                \"averageElectricEngineConsumption\": td_cur.averageElectricEngineConsumption,\r\n                \"averageFuelConsumption\": td_cur.averageFuelConsumption,\r\n                \"averageSpeed\": td_cur.averageSpeed,\r\n                \"mileage\": td_cur.mileage,\r\n                \"startMileage\": td_cur.startMileage,\r\n                \"traveltime\": td_cur.traveltime,\r\n                \"timestamp\": td_cur.timestamp,\r\n                \"overallMileage\": td_cur.overallMileage,\r\n                \"zeroEmissionDistance\": td_cur.zeroEmissionDistance,\r\n            }\r\n            self._vehicle.state[kind.lower() + \"_reset\"] = {\r\n                \"tripID\": td_rst.tripID,\r\n                \"averageElectricEngineConsumption\": td_rst.averageElectricEngineConsumption,\r\n                \"averageFuelConsumption\": td_rst.averageFuelConsumption,\r\n                \"averageSpeed\": td_rst.averageSpeed,\r\n                \"mileage\": td_rst.mileage,\r\n                \"startMileage\": td_rst.startMileage,\r\n                \"traveltime\": td_rst.traveltime,\r\n                \"timestamp\": td_rst.timestamp,\r\n                \"overallMileage\": td_rst.overallMileage,\r\n                \"zeroEmissionDistance\": td_rst.zeroEmissionDistance,\r\n            }\r\n\r\n        except TimeoutError:\r\n            _LOGGER.debug(\r\n                \"TRIP DATA: TimeoutError encountered while updating trip data for VIN: %s.\",\r\n                redacted_vin,\r\n            )\r\n            raise\r\n        except ClientResponseError as cre:\r\n            if cre.status in (403, 404):\r\n                _LOGGER.debug(\r\n                    \"TRIP DATA: ClientResponseError with status %s for VIN: %s. Vehicle does not support trip data — disabling.\",\r\n                    cre.status,\r\n                    redacted_vin,\r\n                )\r\n                self.support_trip_data = False\r\n            elif cre.status == 502:\r\n                _LOGGER.debug(\r\n                    \"TRIP DATA: Received status %s while updating trip data for VIN: %s. This is typically transient and may resolve on its own.\",\r\n                    cre.status,\r\n                    redacted_vin,\r\n                )\r\n            elif cre.status != 204:\r\n                _LOGGER.debug(\r\n                    \"TRIP DATA: ClientResponseError with status %s while updating trip data for VIN: %s. Error: %s\",\r\n                    cre.status,\r\n                    redacted_vin,\r\n                    cre,\r\n                )\r\n            else:\r\n                _LOGGER.debug(\r\n                    \"TRIP DATA: Trip data currently not available for VIN: %s. Received 204 status.\",\r\n                    redacted_vin,\r\n                )\r\n\r\n        except Exception as e:\r\n            _LOGGER.error(\r\n                \"TRIP DATA: An unexpected error occurred while updating trip data for VIN: %s. Error: %s\",\r\n                redacted_vin,\r\n                e,\r\n            )\r\n\r\n    @property\r\n    def last_update_time(self):\r\n        if self.last_update_time_supported:\r\n            return self._vehicle.state.get(\"last_update_time\")\r\n\r\n    @property\r\n    def last_update_time_supported(self):\r\n        check = self._vehicle.state.get(\"last_update_time\")\r\n        if check:\r\n            return True\r\n\r\n    @property\r\n    def service_inspection_time(self):\r\n        \"\"\"Return time left for service inspection\"\"\"\r\n        if self.service_inspection_time_supported:\r\n            return int(\r\n                self._vehicle.fields.get(\"MAINTENANCE_INTERVAL_TIME_TO_INSPECTION\")\r\n            )\r\n\r\n    @property\r\n    def service_inspection_time_supported(self):\r\n        check = self._vehicle.fields.get(\"MAINTENANCE_INTERVAL_TIME_TO_INSPECTION\")\r\n        if check and parse_int(check):\r\n            return True\r\n\r\n    @property\r\n    def service_inspection_distance(self):\r\n        \"\"\"Return distance left for service inspection\"\"\"\r\n        if self.service_inspection_distance_supported:\r\n            return int(\r\n                self._vehicle.fields.get(\"MAINTENANCE_INTERVAL_DISTANCE_TO_INSPECTION\")\r\n            )\r\n\r\n    @property\r\n    def service_inspection_distance_supported(self):\r\n        check = self._vehicle.fields.get(\"MAINTENANCE_INTERVAL_DISTANCE_TO_INSPECTION\")\r\n        if check and parse_int(check):\r\n            return True\r\n\r\n    @property\r\n    def service_adblue_distance(self):\r\n        \"\"\"Return distance left for service inspection\"\"\"\r\n        if self.service_adblue_distance_supported:\r\n            return int(self._vehicle.fields.get(\"ADBLUE_RANGE\"))\r\n\r\n    @property\r\n    def service_adblue_distance_supported(self):\r\n        check = self._vehicle.fields.get(\"ADBLUE_RANGE\")\r\n        if check and parse_int(check):\r\n            return True\r\n\r\n    @property\r\n    def oil_change_time(self):\r\n        \"\"\"Return time left for oil change\"\"\"\r\n        if self.oil_change_time_supported:\r\n            return int(\r\n                self._vehicle.fields.get(\"MAINTENANCE_INTERVAL_TIME_TO_OIL_CHANGE\")\r\n            )\r\n\r\n    @property\r\n    def oil_change_time_supported(self):\r\n        check = self._vehicle.fields.get(\"MAINTENANCE_INTERVAL_TIME_TO_OIL_CHANGE\")\r\n        if check and parse_int(check):\r\n            return True\r\n\r\n    @property\r\n    def oil_change_distance(self):\r\n        \"\"\"Return distance left for oil change\"\"\"\r\n        if self.oil_change_distance_supported:\r\n            return int(\r\n                self._vehicle.fields.get(\"MAINTENANCE_INTERVAL_DISTANCE_TO_OIL_CHANGE\")\r\n            )\r\n\r\n    @property\r\n    def oil_change_distance_supported(self):\r\n        check = self._vehicle.fields.get(\"MAINTENANCE_INTERVAL_DISTANCE_TO_OIL_CHANGE\")\r\n        if check and parse_int(check):\r\n            return True\r\n\r\n    @property\r\n    def oil_level(self):\r\n        \"\"\"Return oil level percentage\"\"\"\r\n        if self.oil_level_supported:\r\n            return parse_float(\r\n                self._vehicle.fields.get(\"OIL_LEVEL_DIPSTICKS_PERCENTAGE\")\r\n            )\r\n\r\n    @property\r\n    def oil_level_supported(self):\r\n        \"\"\"Check if oil level is supported.\"\"\"\r\n        check = self._vehicle.fields.get(\"OIL_LEVEL_DIPSTICKS_PERCENTAGE\")\r\n        return not isinstance(check, bool) and check is not None\r\n\r\n    @property\r\n    def oil_level_binary(self):\r\n        \"\"\"Return oil level binary.\"\"\"\r\n        if self.oil_level_binary_supported:\r\n            return not self._vehicle.fields.get(\"OIL_LEVEL_DIPSTICKS_PERCENTAGE\")\r\n\r\n    @property\r\n    def oil_level_binary_supported(self):\r\n        \"\"\"Check if oil level binary is supported.\"\"\"\r\n        return isinstance(\r\n            self._vehicle.fields.get(\"OIL_LEVEL_DIPSTICKS_PERCENTAGE\"), bool\r\n        )\r\n\r\n    @property\r\n    def preheater_active(self):\r\n        if self.preheater_active_supported:\r\n            res = (\r\n                self._vehicle.state[\"preheaterState\"]\r\n                .get(\"climatisationStateReport\")\r\n                .get(\"climatisationState\")\r\n            )\r\n            return res != \"off\"\r\n\r\n    @property\r\n    def preheater_active_supported(self):\r\n        return self.preheater_state_supported\r\n\r\n    @property\r\n    def preheater_duration(self):\r\n        if self.preheater_duration_supported:\r\n            res = (\r\n                self._vehicle.state[\"preheaterState\"]\r\n                .get(\"climatisationStateReport\")\r\n                .get(\"climatisationDuration\")\r\n            )\r\n            return parse_int(res)\r\n\r\n    @property\r\n    def preheater_duration_supported(self):\r\n        return self.preheater_state_supported\r\n\r\n    @property\r\n    def preheater_remaining_supported(self):\r\n        return self.preheater_state_supported\r\n\r\n    @property\r\n    def preheater_remaining(self):\r\n        if self.preheater_remaining_supported:\r\n            res = (\r\n                self._vehicle.state[\"preheaterState\"]\r\n                .get(\"climatisationStateReport\")\r\n                .get(\"remainingClimateTime\")\r\n            )\r\n            return parse_int(res)\r\n\r\n    @property\r\n    def parking_light(self):\r\n        \"\"\"Return true if parking light is on\"\"\"\r\n        if self.parking_light_supported:\r\n            try:\r\n                check = self._vehicle.fields.get(\"LIGHT_STATUS\")\r\n                return check[0][\"status\"] != \"off\" or check[1][\"status\"] != \"off\"\r\n            except KeyError:\r\n                return False\r\n\r\n    @property\r\n    def parking_light_supported(self):\r\n        \"\"\"Return true if parking light is supported\"\"\"\r\n        check = self._vehicle.fields.get(\"LIGHT_STATUS\")\r\n        if check:\r\n            return True\r\n\r\n    @property\r\n    def braking_status(self):\r\n        \"\"\"Return true if braking status is on\"\"\"\r\n        if self.braking_status_supported:\r\n            check = self._vehicle.fields.get(\"BRAKING_STATUS\")\r\n            return check != \"2\"\r\n\r\n    @property\r\n    def braking_status_supported(self):\r\n        \"\"\"Return true if braking status is supported\"\"\"\r\n        check = self._vehicle.fields.get(\"BRAKING_STATUS\")\r\n        if check:\r\n            return True\r\n\r\n    @property\r\n    def mileage(self):\r\n        if self.mileage_supported:\r\n            check = self._vehicle.fields.get(\"UTC_TIME_AND_KILOMETER_STATUS\")\r\n            return parse_int(check)\r\n\r\n    @property\r\n    def mileage_supported(self):\r\n        \"\"\"Return true if mileage is supported\"\"\"\r\n        check = self._vehicle.fields.get(\"UTC_TIME_AND_KILOMETER_STATUS\")\r\n        if check and parse_int(check):\r\n            return True\r\n\r\n    @property\r\n    def range(self):\r\n        if self.range_supported:\r\n            check = self._vehicle.fields.get(\"TOTAL_RANGE\")\r\n            return parse_int(check)\r\n\r\n    @property\r\n    def range_supported(self):\r\n        \"\"\"Return true if range is supported\"\"\"\r\n        check = self._vehicle.fields.get(\"TOTAL_RANGE\")\r\n        if check and parse_int(check):\r\n            return True\r\n\r\n    @property\r\n    def tank_level(self):\r\n        if self.tank_level_supported:\r\n            check = self._vehicle.fields.get(\"TANK_LEVEL_IN_PERCENTAGE\")\r\n            return parse_int(check)\r\n\r\n    @property\r\n    def tank_level_supported(self):\r\n        \"\"\"Return true if tank_level is supported\"\"\"\r\n        check = self._vehicle.fields.get(\"TANK_LEVEL_IN_PERCENTAGE\")\r\n        if check and parse_int(check):\r\n            return True\r\n\r\n    @property\r\n    def position(self):\r\n        \"\"\"Return position.\"\"\"\r\n        if self.position_supported:\r\n            return self._vehicle.state.get(\"position\")\r\n\r\n    @property\r\n    def position_supported(self):\r\n        \"\"\"Return true if vehicle has position.\"\"\"\r\n        check = self._vehicle.state.get(\"position\")\r\n        if check:\r\n            return True\r\n\r\n    @property\r\n    def any_window_open_supported(self):\r\n        \"\"\"Return true if window state is supported\"\"\"\r\n        checkLeftFront = self._vehicle.fields.get(\"STATE_LEFT_FRONT_WINDOW\")\r\n        checkLeftRear = self._vehicle.fields.get(\"STATE_LEFT_REAR_WINDOW\")\r\n        checkRightFront = self._vehicle.fields.get(\"STATE_RIGHT_FRONT_WINDOW\")\r\n        checkRightRear = self._vehicle.fields.get(\"STATE_RIGHT_REAR_WINDOW\")\r\n        checkSunRoof = self._vehicle.fields.get(\"STATE_SUN_ROOF_MOTOR_COVER\", None)\r\n        checkRoofCover = self._vehicle.fields.get(\"STATE_ROOF_COVER_WINDOW\", None)\r\n        acceptable_window_states = [\"3\", \"0\", None]\r\n        if (\r\n            checkLeftFront\r\n            and checkLeftRear\r\n            and checkRightFront\r\n            and checkRightRear\r\n            and (checkSunRoof in acceptable_window_states)\r\n            and (checkRoofCover in acceptable_window_states)\r\n        ):\r\n            return True\r\n\r\n    @property\r\n    def any_window_open(self):\r\n        if self.any_window_open_supported:\r\n            checkLeftFront = self._vehicle.fields.get(\"STATE_LEFT_FRONT_WINDOW\")\r\n            checkLeftRear = self._vehicle.fields.get(\"STATE_LEFT_REAR_WINDOW\")\r\n            checkRightFront = self._vehicle.fields.get(\"STATE_RIGHT_FRONT_WINDOW\")\r\n            checkRightRear = self._vehicle.fields.get(\"STATE_RIGHT_REAR_WINDOW\")\r\n            checkSunRoof = self._vehicle.fields.get(\"STATE_SUN_ROOF_MOTOR_COVER\", None)\r\n            checkRoofCover = self._vehicle.fields.get(\"STATE_ROOF_COVER_WINDOW\", None)\r\n            acceptable_window_states = [\"3\", None]\r\n            return not (\r\n                checkLeftFront == \"3\"\r\n                and checkLeftRear == \"3\"\r\n                and checkRightFront == \"3\"\r\n                and checkRightRear == \"3\"\r\n                and (checkSunRoof in acceptable_window_states)\r\n                and (checkRoofCover in acceptable_window_states)\r\n            )\r\n\r\n    @property\r\n    def left_front_window_open_supported(self):\r\n        return self._vehicle.fields.get(\"STATE_LEFT_FRONT_WINDOW\")\r\n\r\n    @property\r\n    def left_front_window_open(self):\r\n        if self.left_front_window_open_supported:\r\n            return self._vehicle.fields.get(\"STATE_LEFT_FRONT_WINDOW\") != \"3\"\r\n\r\n    @property\r\n    def right_front_window_open_supported(self):\r\n        return self._vehicle.fields.get(\"STATE_RIGHT_FRONT_WINDOW\")\r\n\r\n    @property\r\n    def right_front_window_open(self):\r\n        if self.right_front_window_open_supported:\r\n            return self._vehicle.fields.get(\"STATE_RIGHT_FRONT_WINDOW\") != \"3\"\r\n\r\n    @property\r\n    def left_rear_window_open_supported(self):\r\n        return self._vehicle.fields.get(\"STATE_LEFT_REAR_WINDOW\")\r\n\r\n    @property\r\n    def left_rear_window_open(self):\r\n        if self.left_rear_window_open_supported:\r\n            return self._vehicle.fields.get(\"STATE_LEFT_REAR_WINDOW\") != \"3\"\r\n\r\n    @property\r\n    def right_rear_window_open_supported(self):\r\n        return self._vehicle.fields.get(\"STATE_RIGHT_REAR_WINDOW\")\r\n\r\n    @property\r\n    def right_rear_window_open(self):\r\n        if self.right_rear_window_open_supported:\r\n            return self._vehicle.fields.get(\"STATE_RIGHT_REAR_WINDOW\") != \"3\"\r\n\r\n    @property\r\n    def sun_roof_supported(self):\r\n        return self._vehicle.fields.get(\"STATE_SUN_ROOF_MOTOR_COVER\")\r\n\r\n    @property\r\n    def sun_roof(self):\r\n        if self.sun_roof_supported:\r\n            return self._vehicle.fields.get(\"STATE_SUN_ROOF_MOTOR_COVER\") != \"3\"\r\n\r\n    @property\r\n    def roof_cover_supported(self):\r\n        return self._vehicle.fields.get(\"STATE_ROOF_COVER_WINDOW\")\r\n\r\n    @property\r\n    def roof_cover(self):\r\n        if self.roof_cover_supported:\r\n            return self._vehicle.fields.get(\"STATE_ROOF_COVER_WINDOW\") != \"3\"\r\n\r\n    @property\r\n    def any_door_unlocked_supported(self):\r\n        checkLeftFront = self._vehicle.fields.get(\"LOCK_STATE_LEFT_FRONT_DOOR\")\r\n        checkLeftRear = self._vehicle.fields.get(\"LOCK_STATE_LEFT_REAR_DOOR\")\r\n        checkRightFront = self._vehicle.fields.get(\"LOCK_STATE_RIGHT_FRONT_DOOR\")\r\n        checkRightRear = self._vehicle.fields.get(\"LOCK_STATE_RIGHT_REAR_DOOR\")\r\n        if checkLeftFront and checkLeftRear and checkRightFront and checkRightRear:\r\n            return True\r\n\r\n    @property\r\n    def any_door_unlocked(self):\r\n        if self.any_door_unlocked_supported:\r\n            checkLeftFront = self._vehicle.fields.get(\"LOCK_STATE_LEFT_FRONT_DOOR\")\r\n            checkLeftRear = self._vehicle.fields.get(\"LOCK_STATE_LEFT_REAR_DOOR\")\r\n            checkRightFront = self._vehicle.fields.get(\"LOCK_STATE_RIGHT_FRONT_DOOR\")\r\n            checkRightRear = self._vehicle.fields.get(\"LOCK_STATE_RIGHT_REAR_DOOR\")\r\n            return not (\r\n                checkLeftFront == \"2\"\r\n                and checkLeftRear == \"2\"\r\n                and checkRightFront == \"2\"\r\n                and checkRightRear == \"2\"\r\n            )\r\n\r\n    @property\r\n    def any_door_open_supported(self):\r\n        checkLeftFront = self._vehicle.fields.get(\"OPEN_STATE_LEFT_FRONT_DOOR\")\r\n        checkLeftRear = self._vehicle.fields.get(\"OPEN_STATE_LEFT_REAR_DOOR\")\r\n        checkRightFront = self._vehicle.fields.get(\"OPEN_STATE_RIGHT_FRONT_DOOR\")\r\n        checkRightRear = self._vehicle.fields.get(\"OPEN_STATE_RIGHT_REAR_DOOR\")\r\n        if checkLeftFront and checkLeftRear and checkRightFront and checkRightRear:\r\n            return True\r\n\r\n    @property\r\n    def any_door_open(self):\r\n        if self.any_door_open_supported:\r\n            checkLeftFront = self._vehicle.fields.get(\"OPEN_STATE_LEFT_FRONT_DOOR\")\r\n            checkLeftRear = self._vehicle.fields.get(\"OPEN_STATE_LEFT_REAR_DOOR\")\r\n            checkRightFront = self._vehicle.fields.get(\"OPEN_STATE_RIGHT_FRONT_DOOR\")\r\n            checkRightRear = self._vehicle.fields.get(\"OPEN_STATE_RIGHT_REAR_DOOR\")\r\n            return not (\r\n                checkLeftFront == \"3\"\r\n                and checkLeftRear == \"3\"\r\n                and checkRightFront == \"3\"\r\n                and checkRightRear == \"3\"\r\n            )\r\n\r\n    @property\r\n    def left_front_door_open_supported(self):\r\n        return self._vehicle.fields.get(\"OPEN_STATE_LEFT_FRONT_DOOR\")\r\n\r\n    @property\r\n    def left_front_door_open(self):\r\n        if self.left_front_door_open_supported:\r\n            return self._vehicle.fields.get(\"OPEN_STATE_LEFT_FRONT_DOOR\") != \"3\"\r\n\r\n    @property\r\n    def right_front_door_open_supported(self):\r\n        return self._vehicle.fields.get(\"OPEN_STATE_RIGHT_FRONT_DOOR\")\r\n\r\n    @property\r\n    def right_front_door_open(self):\r\n        if self.right_front_door_open_supported:\r\n            return self._vehicle.fields.get(\"OPEN_STATE_RIGHT_FRONT_DOOR\") != \"3\"\r\n\r\n    @property\r\n    def left_rear_door_open_supported(self):\r\n        return self._vehicle.fields.get(\"OPEN_STATE_LEFT_REAR_DOOR\")\r\n\r\n    @property\r\n    def left_rear_door_open(self):\r\n        if self.left_rear_door_open_supported:\r\n            return self._vehicle.fields.get(\"OPEN_STATE_LEFT_REAR_DOOR\") != \"3\"\r\n\r\n    @property\r\n    def right_rear_door_open_supported(self):\r\n        return self._vehicle.fields.get(\"OPEN_STATE_RIGHT_REAR_DOOR\")\r\n\r\n    @property\r\n    def right_rear_door_open(self):\r\n        if self.right_rear_door_open_supported:\r\n            return self._vehicle.fields.get(\"OPEN_STATE_RIGHT_REAR_DOOR\") != \"3\"\r\n\r\n    @property\r\n    def doors_trunk_status_supported(self):\r\n        return (\r\n            self.any_door_open_supported\r\n            and self.any_door_unlocked_supported\r\n            and self.trunk_open_supported\r\n            and self.trunk_unlocked_supported\r\n        )\r\n\r\n    @property\r\n    def doors_trunk_status(self):\r\n        if (\r\n            self.any_door_open_supported\r\n            and self.any_door_unlocked_supported\r\n            and self.trunk_open_supported\r\n            and self.trunk_unlocked_supported\r\n        ):\r\n            if self.any_door_open or self.trunk_open:\r\n                return \"Open\"\r\n            elif self.any_door_unlocked or self.trunk_unlocked:\r\n                return \"Closed\"\r\n            else:\r\n                return \"Locked\"\r\n\r\n    @property\r\n    def trunk_unlocked(self):\r\n        if self.trunk_unlocked_supported:\r\n            check = self._vehicle.fields.get(\"LOCK_STATE_TRUNK_LID\")\r\n            return check != \"2\"\r\n\r\n    @property\r\n    def trunk_unlocked_supported(self):\r\n        check = self._vehicle.fields.get(\"LOCK_STATE_TRUNK_LID\")\r\n        if check:\r\n            return True\r\n\r\n    @property\r\n    def trunk_open(self):\r\n        if self.trunk_open_supported:\r\n            check = self._vehicle.fields.get(\"OPEN_STATE_TRUNK_LID\")\r\n            return check != \"3\"\r\n\r\n    @property\r\n    def trunk_open_supported(self):\r\n        check = self._vehicle.fields.get(\"OPEN_STATE_TRUNK_LID\")\r\n        if check:\r\n            return True\r\n\r\n    @property\r\n    def hood_open(self):\r\n        if self.hood_open_supported:\r\n            check = self._vehicle.fields.get(\"OPEN_STATE_HOOD\")\r\n            return check != \"3\"\r\n\r\n    @property\r\n    def hood_open_supported(self):\r\n        check = self._vehicle.fields.get(\"OPEN_STATE_HOOD\")\r\n        if check:\r\n            return True\r\n\r\n    @property\r\n    def charging_state(self):\r\n        \"\"\"Return charging state\"\"\"\r\n        if self.charging_state_supported:\r\n            return self._vehicle.state.get(\"chargingState\")\r\n\r\n    @property\r\n    def charging_state_supported(self):\r\n        check = self._vehicle.state.get(\"chargingState\")\r\n        if check:\r\n            return True\r\n\r\n    @property\r\n    def charging_mode(self):\r\n        \"\"\"Return charging mode\"\"\"\r\n        if self.charging_mode_supported:\r\n            return self._vehicle.state.get(\"chargeMode\")\r\n\r\n    @property\r\n    def charging_mode_supported(self):\r\n        check = self._vehicle.state.get(\"chargeMode\")\r\n        return check is not None and check != \"unsupported\"\r\n\r\n    @property\r\n    def energy_flow(self):\r\n        \"\"\"Return charging mode\"\"\"\r\n        if self.energy_flow_supported:\r\n            return self._vehicle.state.get(\"energyFlow\")\r\n\r\n    @property\r\n    def energy_flow_supported(self):\r\n        check = self._vehicle.state.get(\"energyFlow\")\r\n        if check is not None:\r\n            return True\r\n\r\n    @property\r\n    def charging_type(self):\r\n        \"\"\"Return charging type\"\"\"\r\n        if self.charging_type_supported:\r\n            return self._vehicle.state.get(\"chargeType\")\r\n\r\n    @property\r\n    def charging_type_supported(self):\r\n        check = self._vehicle.state.get(\"chargeType\")\r\n        if check and check != \"unsupported\":\r\n            return True\r\n\r\n    @property\r\n    def max_charge_current(self):\r\n        \"\"\"Return max charge current\"\"\"\r\n        if self.max_charge_current_supported:\r\n            try:\r\n                return parse_float(self._vehicle.state.get(\"maxChargeCurrent\"))\r\n            except ValueError:\r\n                return -1\r\n\r\n    @property\r\n    def max_charge_current_supported(self):\r\n        check = self._vehicle.state.get(\"maxChargeCurrent\")\r\n        if check is not None:\r\n            return True\r\n\r\n    @property\r\n    def actual_charge_rate(self):\r\n        \"\"\"Return actual charge rate\"\"\"\r\n        if self.actual_charge_rate_supported:\r\n            try:\r\n                return parse_float(self._vehicle.state.get(\"actualChargeRate\"))\r\n            except ValueError:\r\n                return -1\r\n\r\n    @property\r\n    def actual_charge_rate_supported(self):\r\n        check = self._vehicle.state.get(\"actualChargeRate\")\r\n        if check is not None:\r\n            return True\r\n\r\n    @property\r\n    def actual_charge_rate_unit(self):\r\n        return \"km/h\"\r\n\r\n    @property\r\n    def charging_power(self):\r\n        \"\"\"Return charging power\"\"\"\r\n        if self.charging_power_supported:\r\n            try:\r\n                return parse_float(self._vehicle.state.get(\"chargingPower\"))\r\n            except ValueError:\r\n                return -1\r\n\r\n    @property\r\n    def charging_power_supported(self):\r\n        check = self._vehicle.state.get(\"chargingPower\")\r\n        if check is not None:\r\n            return True\r\n\r\n    @property\r\n    def primary_engine_type(self):\r\n        \"\"\"Return primary engine type\"\"\"\r\n        if self.primary_engine_type_supported:\r\n            return self._vehicle.state.get(\"engineTypeFirstEngine\")\r\n\r\n    @property\r\n    def primary_engine_type_supported(self):\r\n        check = self._vehicle.state.get(\"engineTypeFirstEngine\")\r\n        if check and check != \"unsupported\":\r\n            return True\r\n\r\n    @property\r\n    def secondary_engine_type(self):\r\n        \"\"\"Return secondary engine type\"\"\"\r\n        if self.secondary_engine_type_supported:\r\n            return self._vehicle.state.get(\"engineTypeSecondEngine\")\r\n\r\n    @property\r\n    def secondary_engine_type_supported(self):\r\n        check = self._vehicle.state.get(\"engineTypeSecondEngine\")\r\n        if check and check != \"unsupported\":\r\n            return True\r\n\r\n    @property\r\n    def primary_engine_range(self):\r\n        \"\"\"Return primary engine range\"\"\"\r\n        if self.primary_engine_range_supported:\r\n            return self._vehicle.state.get(\"primaryEngineRange\")\r\n\r\n    @property\r\n    def primary_engine_range_supported(self):\r\n        check = self._vehicle.state.get(\"primaryEngineRange\")\r\n        if check and check != \"unsupported\":\r\n            return True\r\n\r\n    @property\r\n    def primary_engine_range_percent(self):\r\n        \"\"\"Return primary engine range\"\"\"\r\n        if self.primary_engine_range_percent_supported:\r\n            return self._vehicle.state.get(\"primaryEngineRangePercent\")\r\n\r\n    @property\r\n    def primary_engine_range_percent_supported(self):\r\n        check = self._vehicle.state.get(\"primaryEngineRangePercent\")\r\n        if check and check != \"unsupported\":\r\n            return True\r\n\r\n    @property\r\n    def secondary_engine_range(self):\r\n        \"\"\"Return secondary engine range\"\"\"\r\n        if self.secondary_engine_range_supported:\r\n            return self._vehicle.state.get(\"secondaryEngineRange\")\r\n\r\n    @property\r\n    def secondary_engine_range_supported(self):\r\n        check = self._vehicle.state.get(\"secondaryEngineRange\")\r\n        if check is not None and check != \"unsupported\":\r\n            return True\r\n\r\n    @property\r\n    def car_type(self):\r\n        \"\"\"Return secondary engine range\"\"\"\r\n        if self.car_type_supported:\r\n            return self._vehicle.state.get(\"carType\")\r\n\r\n    @property\r\n    def car_type_supported(self):\r\n        check = self._vehicle.state.get(\"carType\")\r\n        if check and check != \"unsupported\":\r\n            return True\r\n\r\n    @property\r\n    def secondary_engine_range_percent(self):\r\n        \"\"\"Return secondary engine range\"\"\"\r\n        if self.secondary_engine_range_percent_supported:\r\n            return self._vehicle.state.get(\"secondaryEngineRangePercent\")\r\n\r\n    @property\r\n    def secondary_engine_range_percent_supported(self):\r\n        check = self._vehicle.state.get(\"secondaryEngineRangePercent\")\r\n        if check and check != \"unsupported\":\r\n            return True\r\n\r\n    @property\r\n    def hybrid_range(self):\r\n        \"\"\"Return hybrid range\"\"\"\r\n        if self.hybrid_range_supported:\r\n            return self._vehicle.state.get(\"hybridRange\")\r\n\r\n    @property\r\n    def hybrid_range_supported(self):\r\n        check = self._vehicle.state.get(\"hybridRange\")\r\n        if check and check != \"unsupported\":\r\n            return True\r\n\r\n    @property\r\n    def state_of_charge(self):\r\n        \"\"\"Return state of charge\"\"\"\r\n        if self.state_of_charge_supported:\r\n            return parse_int(self._vehicle.state.get(\"stateOfCharge\"))\r\n\r\n    @property\r\n    def state_of_charge_supported(self):\r\n        return parse_int(self._vehicle.state.get(\"stateOfCharge\")) is not None\r\n\r\n    @property\r\n    def remaining_charging_time(self):\r\n        \"\"\"Return remaining charging time\"\"\"\r\n        if self.remaining_charging_time_supported:\r\n            return self._vehicle.state.get(\"remainingChargingTime\", 0)\r\n\r\n    @property\r\n    def remaining_charging_time_unit(self):\r\n        return \"min\"\r\n\r\n    @property\r\n    def remaining_charging_time_supported(self):\r\n        return self.car_type in [\"hybrid\", \"electric\"]\r\n\r\n    @property\r\n    def charging_complete_time(self):\r\n        \"\"\"Return the datetime when charging is or was expected to be complete.\"\"\"\r\n        # Check if remaining charging time is not supported\r\n        if not self.remaining_charging_time_supported:\r\n            return None\r\n        # If there's no last update or remaining time, we can't calculate\r\n        if self.last_update_time is None or self.remaining_charging_time is None:\r\n            return None\r\n        # Calculate the complete time whenever there is a positive remaining time\r\n        if self.remaining_charging_time > 0:\r\n            calculated_time = self.last_update_time + timedelta(\r\n                minutes=self.remaining_charging_time\r\n            )\r\n            self.charging_complete_time_frozen = (\r\n                calculated_time  # Always update the frozen time\r\n            )\r\n            return calculated_time\r\n        # If the remaining time is zero or negative, and no frozen time is set,\r\n        # we have no knowledge of the last completion time, so return None\r\n        if self.charging_complete_time_frozen is None:\r\n            return None\r\n        # Otherwise, return the frozen complete time\r\n        return self.charging_complete_time_frozen\r\n\r\n    @property\r\n    def target_state_of_charge(self):\r\n        \"\"\"Return state of charge\"\"\"\r\n        if self.target_state_of_charge_supported:\r\n            return parse_int(self._vehicle.state.get(\"targetstateOfCharge\"))\r\n\r\n    @property\r\n    def target_state_of_charge_supported(self):\r\n        return parse_int(self._vehicle.state.get(\"targetstateOfCharge\")) is not None\r\n\r\n    @property\r\n    def plug_state(self):\r\n        \"\"\"Return plug state\"\"\"\r\n        if self.plug_state_supported:\r\n            check = self._vehicle.state.get(\"plugState\")\r\n            return check != \"disconnected\"\r\n\r\n    @property\r\n    def plug_state_supported(self):\r\n        check = self._vehicle.state.get(\"plugState\")\r\n        if check:\r\n            return True\r\n\r\n    @property\r\n    def plug_lock_state(self):\r\n        \"\"\"Return plug lock state\"\"\"\r\n        if self.plug_lock_state_supported:\r\n            check = self._vehicle.state.get(\"plugLockState\")\r\n            return check != \"locked\"\r\n\r\n    @property\r\n    def plug_lock_state_supported(self):\r\n        check = self._vehicle.state.get(\"plugLockState\")\r\n        if check:\r\n            return True\r\n\r\n    @property\r\n    def external_power(self):\r\n        \"\"\"Return external Power\"\"\"\r\n        if self.external_power_supported:\r\n            external_power_status = self._vehicle.state.get(\"externalPower\")\r\n            if external_power_status == \"unavailable\":\r\n                return \"Not Ready\"\r\n            elif external_power_status == \"ready\":\r\n                return \"Ready\"\r\n            else:\r\n                return external_power_status\r\n\r\n    @property\r\n    def external_power_supported(self):\r\n        return self._vehicle.state.get(\"externalPower\") is not None\r\n\r\n    @property\r\n    def plug_led_color(self):\r\n        \"\"\"Return plug LED Color\"\"\"\r\n        if self.plug_led_color_supported:\r\n            return self._vehicle.state.get(\"plugledColor\")\r\n\r\n    @property\r\n    def plug_led_color_supported(self):\r\n        check = self._vehicle.state.get(\"plugledColor\")\r\n        if check:\r\n            return True\r\n\r\n    @property\r\n    def climatisation_state(self):\r\n        if self.climatisation_state_supported:\r\n            return self._vehicle.state.get(\"climatisationState\")\r\n\r\n    @property\r\n    def climatisation_state_supported(self):\r\n        check = self._vehicle.state.get(\"climatisationState\")\r\n        if check:\r\n            return True\r\n\r\n    @property\r\n    def outdoor_temperature(self):\r\n        if self.outdoor_temperature_supported:\r\n            return self._vehicle.state.get(\"outdoorTemperature\")\r\n\r\n    @property\r\n    def outdoor_temperature_supported(self):\r\n        check = self._vehicle.state.get(\"outdoorTemperature\")\r\n        if check:\r\n            return True\r\n\r\n    @property\r\n    def glass_surface_heating(self):\r\n        if self.glass_surface_heating_supported:\r\n            return self._vehicle.state.get(\"isMirrorHeatingActive\")\r\n\r\n    @property\r\n    def glass_surface_heating_supported(self):\r\n        return self._vehicle.state.get(\"isMirrorHeatingActive\") is not None\r\n\r\n    @property\r\n    def park_time(self):\r\n        if self.park_time_supported:\r\n            return self._vehicle.state.get(\"vehicleParkingClock\")\r\n\r\n    @property\r\n    def park_time_supported(self):\r\n        return self._vehicle.state.get(\"vehicleParkingClock\") is not None\r\n\r\n    @property\r\n    def remaining_climatisation_time(self):\r\n        if self.remaining_climatisation_time_supported:\r\n            remaining_time = self._vehicle.state.get(\"remainingClimatisationTime\")\r\n            if remaining_time is not None and remaining_time < 0:\r\n                return 0\r\n            elif remaining_time is not None:\r\n                return remaining_time\r\n        return None\r\n\r\n    @property\r\n    def remaining_climatisation_time_supported(self):\r\n        return self._vehicle.state.get(\"remainingClimatisationTime\") is not None\r\n\r\n    @property\r\n    def preheater_state(self):\r\n        check = self._vehicle.state.get(\"preheaterState\")\r\n        if check:\r\n            return True\r\n\r\n    @property\r\n    def preheater_state_supported(self):\r\n        check = self._vehicle.state.get(\"preheaterState\")\r\n        if check:\r\n            return True\r\n\r\n    def lock_supported(self):\r\n        return (\r\n            self.doors_trunk_status_supported and self._audi_service._spin is not None\r\n        )\r\n\r\n    @property\r\n    def shortterm_current(self):\r\n        \"\"\"Return shortterm.\"\"\"\r\n        if self.shortterm_current_supported:\r\n            return self._vehicle.state.get(\"shortterm_current\")\r\n\r\n    @property\r\n    def shortterm_current_supported(self):\r\n        \"\"\"Return true if vehicle has shortterm_current.\"\"\"\r\n        check = self._vehicle.state.get(\"shortterm_current\")\r\n        if check:\r\n            return True\r\n\r\n    @property\r\n    def shortterm_reset(self):\r\n        \"\"\"Return shortterm.\"\"\"\r\n        if self.shortterm_reset_supported:\r\n            return self._vehicle.state.get(\"shortterm_reset\")\r\n\r\n    @property\r\n    def shortterm_reset_supported(self):\r\n        \"\"\"Return true if vehicle has shortterm_reset.\"\"\"\r\n        check = self._vehicle.state.get(\"shortterm_reset\")\r\n        if check:\r\n            return True\r\n\r\n    @property\r\n    def longterm_current(self):\r\n        \"\"\"Return longterm.\"\"\"\r\n        if self.longterm_current_supported:\r\n            return self._vehicle.state.get(\"longterm_current\")\r\n\r\n    @property\r\n    def longterm_current_supported(self):\r\n        \"\"\"Return true if vehicle has longterm_current.\"\"\"\r\n        check = self._vehicle.state.get(\"longterm_current\")\r\n        if check:\r\n            return True\r\n\r\n    @property\r\n    def longterm_reset(self):\r\n        \"\"\"Return longterm.\"\"\"\r\n        if self.longterm_reset_supported:\r\n            return self._vehicle.state.get(\"longterm_reset\")\r\n\r\n    @property\r\n    def longterm_reset_supported(self):\r\n        \"\"\"Return true if vehicle has longterm_reset.\"\"\"\r\n        check = self._vehicle.state.get(\"longterm_reset\")\r\n        if check:\r\n            return True\r\n\r\n    @property\r\n    def is_moving(self):\r\n        \"\"\"Return true if the vehicle is moving.\"\"\"\r\n        if self.is_moving_supported:\r\n            return self._vehicle.state.get(\"is_moving\")\r\n\r\n    @property\r\n    def is_moving_supported(self):\r\n        \"\"\"Return true if vehicle can move.\"\"\"\r\n        return True\r\n"
  },
  {
    "path": "custom_components/audiconnect/audi_entity.py",
    "content": "from homeassistant.helpers.entity import Entity\nfrom homeassistant.helpers.dispatcher import (\n    async_dispatcher_connect,\n)\nfrom homeassistant.helpers.entity import DeviceInfo\n\nfrom .const import DOMAIN, SIGNAL_STATE_UPDATED\n\n\nclass AudiEntity(Entity):\n    \"\"\"Base class for all Audi entities.\"\"\"\n\n    def __init__(self, data, instrument):\n        \"\"\"Initialize the entity.\"\"\"\n        self._data = data\n        self._instrument = instrument\n        self._vin = self._instrument.vehicle_name\n        self._component = self._instrument.component\n        self._attribute = self._instrument.attr\n\n    async def async_added_to_hass(self):\n        \"\"\"Register update dispatcher.\"\"\"\n        async_dispatcher_connect(\n            self.hass, SIGNAL_STATE_UPDATED, self.async_schedule_update_ha_state\n        )\n\n    @property\n    def icon(self):\n        \"\"\"Return the icon.\"\"\"\n        return self._instrument.icon\n\n    @property\n    def _entity_name(self):\n        return self._instrument.name\n\n    @property\n    def _vehicle_name(self):\n        return self._instrument.vehicle_name\n\n    @property\n    def name(self):\n        \"\"\"Return full name of the entity.\"\"\"\n        return \"{} {}\".format(self._vehicle_name, self._entity_name)\n\n    @property\n    def should_poll(self):\n        \"\"\"Return the polling state.\"\"\"\n        return False\n\n    @property\n    def assumed_state(self):\n        \"\"\"Return true if unable to access real state of entity.\"\"\"\n        return True\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Return device specific state attributes.\"\"\"\n        return dict(\n            self._instrument.attributes,\n            model=\"{}/{}\".format(\n                self._instrument.vehicle_model, self._instrument.vehicle_name\n            ),\n            model_year=self._instrument.vehicle_model_year,\n            model_family=self._instrument.vehicle_model_family,\n            title=self._instrument.vehicle_name,\n            csid=self._instrument.vehicle_csid,\n            vin=self._instrument.vehicle_vin,\n        )\n\n    @property\n    def unique_id(self):\n        \"\"\"Return a unique ID.\"\"\"\n        return self._instrument.full_name\n\n    @property\n    def device_info(self):\n        \"\"\"Return device information.\"\"\"\n        if self._instrument.vehicle_model:\n            model_info = self._instrument.vehicle_model.replace(\"Audi \", \"\")\n        elif self._instrument.vehicle_name:\n            model_info = self._instrument.vehicle_name\n        else:\n            model_info = \"Unknown\"\n        return DeviceInfo(\n            identifiers={(DOMAIN, self._instrument.vehicle_name)},\n            manufacturer=\"Audi\",\n            name=self._instrument.vehicle_name,\n            model=\"{} ({})\".format(model_info, self._instrument.vehicle_model_year),\n        )\n"
  },
  {
    "path": "custom_components/audiconnect/audi_models.py",
    "content": "import logging\nfrom .util import get_attr\n\n_LOGGER = logging.getLogger(__name__)\n\n\nclass VehicleData:\n    def __init__(self, config_entry):\n        self.sensors = set()\n        self.binary_sensors = set()\n        self.switches = set()\n        self.device_trackers = set()\n        self.locks = set()\n        self.config_entry = config_entry\n        self.vehicle = None\n\n\nclass CurrentVehicleDataResponse:\n    def __init__(self, data):\n        data = data[\"CurrentVehicleDataResponse\"]\n        self.request_id = data[\"requestId\"]\n        self.vin = data[\"vin\"]\n\n\nclass VehicleDataResponse:\n    OLDAPI_MAPPING = {\n        \"frontRightLock\": \"LOCK_STATE_RIGHT_FRONT_DOOR\",\n        \"frontRightOpen\": \"OPEN_STATE_RIGHT_FRONT_DOOR\",\n        \"frontLeftLock\": \"LOCK_STATE_LEFT_FRONT_DOOR\",\n        \"frontLeftOpen\": \"OPEN_STATE_LEFT_FRONT_DOOR\",\n        \"rearRightLock\": \"LOCK_STATE_RIGHT_REAR_DOOR\",\n        \"rearRightOpen\": \"OPEN_STATE_RIGHT_REAR_DOOR\",\n        \"rearLeftLock\": \"LOCK_STATE_LEFT_REAR_DOOR\",\n        \"rearLeftOpen\": \"OPEN_STATE_LEFT_REAR_DOOR\",\n        \"trunkLock\": \"LOCK_STATE_TRUNK_LID\",\n        \"trunkOpen\": \"OPEN_STATE_TRUNK_LID\",\n        \"bonnetLock\": \"LOCK_STATE_HOOD\",\n        \"bonnetOpen\": \"OPEN_STATE_HOOD\",\n        \"sunRoofWindow\": \"STATE_SUN_ROOF_MOTOR_COVER\",\n        \"frontLeftWindow\": \"STATE_LEFT_FRONT_WINDOW\",\n        \"frontRightWindow\": \"STATE_RIGHT_FRONT_WINDOW\",\n        \"rearLeftWindow\": \"STATE_LEFT_REAR_WINDOW\",\n        \"rearRightWindow\": \"STATE_RIGHT_REAR_WINDOW\",\n        \"roofCoverWindow\": \"STATE_ROOF_COVER_WINDOW\",\n    }\n\n    def __init__(self, data):\n        self.data_fields = []\n        self.states = []\n\n        self._tryAppendFieldWithTs(\n            data, \"TOTAL_RANGE\", [\"fuelStatus\", \"rangeStatus\", \"value\", \"totalRange_km\"]\n        )\n        self._tryAppendFieldWithTs(\n            data,\n            \"TANK_LEVEL_IN_PERCENTAGE\",\n            [\"measurements\", \"fuelLevelStatus\", \"value\", \"currentFuelLevel_pct\"],\n        )\n        self._tryAppendFieldWithTs(\n            data,\n            \"UTC_TIME_AND_KILOMETER_STATUS\",\n            [\"measurements\", \"odometerStatus\", \"value\", \"odometer\"],\n        )\n        self._tryAppendFieldWithTs(\n            data,\n            \"MAINTENANCE_INTERVAL_TIME_TO_INSPECTION\",\n            [\n                \"vehicleHealthInspection\",\n                \"maintenanceStatus\",\n                \"value\",\n                \"inspectionDue_days\",\n            ],\n        )\n        self._tryAppendFieldWithTs(\n            data,\n            \"MAINTENANCE_INTERVAL_DISTANCE_TO_INSPECTION\",\n            [\n                \"vehicleHealthInspection\",\n                \"maintenanceStatus\",\n                \"value\",\n                \"inspectionDue_km\",\n            ],\n        )\n\n        self._tryAppendFieldWithTs(\n            data,\n            \"MAINTENANCE_INTERVAL_TIME_TO_OIL_CHANGE\",\n            [\n                \"vehicleHealthInspection\",\n                \"maintenanceStatus\",\n                \"value\",\n                \"oilServiceDue_days\",\n            ],\n        )\n        self._tryAppendFieldWithTs(\n            data,\n            \"MAINTENANCE_INTERVAL_DISTANCE_TO_OIL_CHANGE\",\n            [\n                \"vehicleHealthInspection\",\n                \"maintenanceStatus\",\n                \"value\",\n                \"oilServiceDue_km\",\n            ],\n        )\n\n        self._tryAppendFieldWithTs(\n            data,\n            \"OIL_LEVEL_DIPSTICKS_PERCENTAGE\",\n            [\"oilLevel\", \"oilLevelStatus\", \"value\", \"value\"],\n        )\n        self._tryAppendFieldWithTs(\n            data,\n            \"ADBLUE_RANGE\",\n            [\"measurements\", \"rangeStatus\", \"value\", \"adBlueRange\"],\n        )\n\n        self._tryAppendFieldWithTs(\n            data, \"LIGHT_STATUS\", [\"vehicleLights\", \"lightsStatus\", \"value\", \"lights\"]\n        )\n\n        self.appendWindowState(data)\n        self.appendDoorState(data)\n\n        self._tryAppendStateWithTs(\n            data, \"carType\", -1, [\"fuelStatus\", \"rangeStatus\", \"value\", \"carType\"]\n        )\n\n        self._tryAppendStateWithTs(\n            data,\n            \"engineTypeFirstEngine\",\n            -2,\n            [\"fuelStatus\", \"rangeStatus\", \"value\", \"primaryEngine\", \"type\"],\n        )\n        self._tryAppendStateWithTs(\n            data,\n            \"primaryEngineRange\",\n            -2,\n            [\n                \"fuelStatus\",\n                \"rangeStatus\",\n                \"value\",\n                \"primaryEngine\",\n                \"remainingRange_km\",\n            ],\n        )\n        self._tryAppendStateWithTs(\n            data,\n            \"primaryEngineRangePercent\",\n            -2,\n            [\"fuelStatus\", \"rangeStatus\", \"value\", \"primaryEngine\", \"currentSOC_pct\"],\n        )\n        self._tryAppendStateWithTs(\n            data,\n            \"engineTypeSecondEngine\",\n            -2,\n            [\"fuelStatus\", \"rangeStatus\", \"value\", \"secondaryEngine\", \"type\"],\n        )\n        self._tryAppendStateWithTs(\n            data,\n            \"secondaryEngineRange\",\n            -2,\n            [\n                \"fuelStatus\",\n                \"rangeStatus\",\n                \"value\",\n                \"secondaryEngine\",\n                \"remainingRange_km\",\n            ],\n        )\n        self._tryAppendStateWithTs(\n            data,\n            \"secondaryEngineRangePercent\",\n            -2,\n            [\"fuelStatus\", \"rangeStatus\", \"value\", \"secondaryEngine\", \"currentSOC_pct\"],\n        )\n        self._tryAppendStateWithTs(\n            data,\n            \"hybridRange\",\n            -1,\n            [\"fuelStatus\", \"rangeStatus\", \"value\", \"totalRange_km\"],\n        )\n        self._tryAppendStateWithTs(\n            data,\n            \"stateOfCharge\",\n            -1,\n            [\"charging\", \"batteryStatus\", \"value\", \"currentSOC_pct\"],\n        )\n        self._tryAppendStateWithTs(\n            data,\n            \"chargingState\",\n            -1,\n            [\"charging\", \"chargingStatus\", \"value\", \"chargingState\"],\n        )\n        self._tryAppendStateWithTs(\n            data,\n            \"chargeMode\",\n            -1,\n            [\"charging\", \"chargingStatus\", \"value\", \"chargeMode\"],\n        )\n        self._tryAppendStateWithTs(\n            data,\n            \"chargingPower\",\n            -1,\n            [\"charging\", \"chargingStatus\", \"value\", \"chargePower_kW\"],\n        )\n        self._tryAppendStateWithTs(\n            data,\n            \"actualChargeRate\",\n            -1,\n            [\"charging\", \"chargingStatus\", \"value\", \"chargeRate_kmph\"],\n        )\n        self._tryAppendStateWithTs(\n            data,\n            \"chargeType\",\n            -1,\n            [\"charging\", \"chargingStatus\", \"value\", \"chargeType\"],\n        )\n        self._tryAppendStateWithTs(\n            data,\n            \"targetstateOfCharge\",\n            -1,\n            [\"charging\", \"chargingSettings\", \"value\", \"targetSOC_pct\"],\n        )\n        self._tryAppendStateWithTs(\n            data,\n            \"plugState\",\n            -1,\n            [\"charging\", \"plugStatus\", \"value\", \"plugConnectionState\"],\n        )\n        self._tryAppendStateWithTs(\n            data,\n            \"remainingChargingTime\",\n            -1,\n            [\n                \"charging\",\n                \"chargingStatus\",\n                \"value\",\n                \"remainingChargingTimeToComplete_min\",\n            ],\n        )\n        self._tryAppendStateWithTs(\n            data,\n            \"plugLockState\",\n            -1,\n            [\"charging\", \"plugStatus\", \"value\", \"plugLockState\"],\n        )\n        self._tryAppendStateWithTs(\n            data,\n            \"externalPower\",\n            -1,\n            [\"charging\", \"plugStatus\", \"value\", \"externalPower\"],\n        )\n        self._tryAppendStateWithTs(\n            data,\n            \"plugledColor\",\n            -1,\n            [\"charging\", \"plugStatus\", \"value\", \"ledColor\"],\n        )\n        self._tryAppendStateWithTs(\n            data,\n            \"climatisationState\",\n            -1,\n            [\"climatisation\", \"auxiliaryHeatingStatus\", \"value\", \"climatisationState\"],\n        )\n        # 2024 Q4 updated data structure for climate data\n        self._tryAppendStateWithTs(\n            data,\n            \"climatisationState\",\n            -1,\n            [\"climatisation\", \"climatisationStatus\", \"value\", \"climatisationState\"],\n        )\n        self._tryAppendStateWithTs(\n            data,\n            \"remainingClimatisationTime\",\n            -1,\n            [\n                \"climatisation\",\n                \"climatisationStatus\",\n                \"value\",\n                \"remainingClimatisationTime_min\",\n            ],\n        )\n\n    def _tryAppendStateWithTs(self, json, name, tsoff, loc):\n        _LOGGER.debug(\n            \"TRY APPEND STATE: Searching for '%s' at location=%s, tsoff=%s\",\n            name,\n            loc,\n            tsoff,\n        )\n\n        ts = None\n        val = self._getFromJson(json, loc)\n        # _LOGGER.debug(\"Initial value retrieved for '%s': %s\", name, val)\n\n        if val is not None:\n            loc[tsoff:] = [\"carCapturedTimestamp\"]\n            # _LOGGER.debug(\"Updated loc for timestamp retrieval: %s\", loc)\n            ts = self._getFromJson(json, loc)\n            # _LOGGER.debug(\"Timestamp retrieved for '%s': %s\", name, ts)\n\n        if val is not None and ts:\n            self.states.append({\"name\": name, \"value\": val, \"measure_time\": ts})\n            _LOGGER.debug(\n                \"TRY APPEND STATE: Found '%s' with value=%s, tsoff=%s, loc=%s, ts=%s\",\n                name,\n                val,\n                tsoff,\n                loc,\n                ts,\n            )\n        else:\n            if val is None:\n                _LOGGER.debug(\n                    \"TRY APPEND STATE: Value for '%s' is None; not appending state.\",\n                    name,\n                )\n            elif not ts:\n                _LOGGER.debug(\n                    \"TRY APPEND STATE: Timestamp for '%s' is None or missing; not appending state.\",\n                    name,\n                )\n\n    def _tryAppendFieldWithTs(self, json, textId, loc):\n        _LOGGER.debug(\n            \"TRY APPEND FIELD: Searching for '%s' at location=%s\",\n            textId,\n            loc,\n        )\n\n        ts = None\n        val = self._getFromJson(json, loc)\n        # _LOGGER.debug(\"Initial value retrieved for '%s': %s\", textId, val)\n\n        if val is not None:\n            loc[-1:] = [\"carCapturedTimestamp\"]\n            # _LOGGER.debug(\"Updated loc for timestamp retrieval: %s\", loc)\n            ts = self._getFromJson(json, loc)\n            # _LOGGER.debug(\"Timestamp retrieved for '%s': %s\", textId, ts)\n\n        if val is not None and ts:\n            self.data_fields.append(\n                Field(\n                    {\n                        \"textId\": textId,\n                        \"value\": val,\n                        \"tsCarCaptured\": ts,\n                    }\n                )\n            )\n            _LOGGER.debug(\n                \"TRY APPEND FIELD: Found '%s' with value=%s, loc=%s, ts=%s\",\n                textId,\n                val,\n                loc,\n                ts,\n            )\n        else:\n            if val is None:\n                _LOGGER.debug(\n                    \"TRY APPEND FIELD: Value for '%s' is None or missing; not appending field.\",\n                    textId,\n                )\n            elif not ts:\n                _LOGGER.debug(\n                    \"TRY APPEND FIELD: Timestamp for '%s' is None or missing; not appending field.\",\n                    textId,\n                )\n\n    def _getFromJson(self, json, loc):\n        child = json\n        for i in loc:\n            if i not in child:\n                return None\n            child = child[i]\n        return child\n\n    def appendDoorState(self, data):\n        _LOGGER.debug(\"APPEND DOOR: Starting to append doors...\")\n        doors = get_attr(data, \"access.accessStatus.value.doors\", [])\n        tsCarCapturedAccess = get_attr(\n            data, \"access.accessStatus.value.carCapturedTimestamp\"\n        )\n        _LOGGER.debug(\n            \"APPEND DOOR: Timestamp captured from car: %s\", tsCarCapturedAccess\n        )\n        for door in doors:\n            status = door[\"status\"]\n            name = door[\"name\"]\n            _LOGGER.debug(\n                \"APPEND DOOR: Processing door: %s with status: %s\", name, status\n            )\n            if name + \"Lock\" not in self.OLDAPI_MAPPING:\n                _LOGGER.debug(\n                    \"APPEND DOOR: Skipping door not mapped in OLDAPI_MAPPING: %s\", name\n                )\n                continue\n            lock = \"0\"\n            open = \"0\"\n            unsupported = False\n            for state in status:\n                if state == \"unsupported\":\n                    unsupported = True\n                    _LOGGER.debug(\"APPEND DOOR: Unsupported state for door: %s\", name)\n                if state == \"locked\":\n                    lock = \"2\"\n                if state == \"closed\":\n                    open = \"3\"\n            if not unsupported:\n                doorFieldLock = {\n                    \"textId\": self.OLDAPI_MAPPING[name + \"Lock\"],\n                    \"value\": lock,\n                    \"tsCarCaptured\": tsCarCapturedAccess,\n                }\n                _LOGGER.debug(\n                    \"APPEND DOOR: Appended door lock field: %s\", doorFieldLock\n                )\n                self.data_fields.append(Field(doorFieldLock))\n\n                doorFieldOpen = {\n                    \"textId\": self.OLDAPI_MAPPING[name + \"Open\"],\n                    \"value\": open,\n                    \"tsCarCaptured\": tsCarCapturedAccess,\n                }\n                _LOGGER.debug(\n                    \"APPEND DOOR: Appended door open field: %s\", doorFieldOpen\n                )\n                self.data_fields.append(Field(doorFieldOpen))\n        _LOGGER.debug(\"APPEND DOOR: Finished appending doors\")\n\n    def appendWindowState(self, data):\n        _LOGGER.debug(\"APPEND WINDOW: Starting to append windows...\")\n        windows = get_attr(data, \"access.accessStatus.value.windows\", [])\n        tsCarCapturedAccess = get_attr(\n            data, \"access.accessStatus.value.carCapturedTimestamp\"\n        )\n        _LOGGER.debug(\n            \"APPEND WINDOW: Timestamp captured from car: %s\", tsCarCapturedAccess\n        )\n        for window in windows:\n            name = window[\"name\"]\n            status = window[\"status\"]\n            _LOGGER.debug(\n                \"APPEND WINDOW: Processing window: %s with status: %s\", name, status\n            )\n            if (\n                status[0] == \"unsupported\"\n            ) or name + \"Window\" not in self.OLDAPI_MAPPING:\n                _LOGGER.debug(\n                    \"APPEND WINDOW: Skipping unsupported window or not mapped in OLDAPI_MAPPING: %s\",\n                    name,\n                )\n                continue\n            windowField = {\n                \"textId\": self.OLDAPI_MAPPING[name + \"Window\"],\n                \"value\": \"3\" if status[0] == \"closed\" else \"0\",\n                \"tsCarCaptured\": tsCarCapturedAccess,\n            }\n            _LOGGER.debug(\"APPEND WINDOW: Appended window field: %s\", windowField)\n            self.data_fields.append(Field(windowField))\n        _LOGGER.debug(\"APPEND WINDOW: Finished appending windows\")\n\n\nclass TripDataResponse:\n    def __init__(self, data):\n        self.data_fields = []\n\n        self.tripID = data[\"tripID\"]\n\n        self.averageElectricEngineConsumption = None\n        if \"averageElectricEngineConsumption\" in data:\n            self.averageElectricEngineConsumption = (\n                float(data[\"averageElectricEngineConsumption\"]) / 10\n            )\n\n        self.averageFuelConsumption = None\n        if \"averageFuelConsumption\" in data:\n            self.averageFuelConsumption = float(data[\"averageFuelConsumption\"]) / 10\n\n        self.averageSpeed = None\n        if \"averageSpeed\" in data:\n            self.averageSpeed = int(data[\"averageSpeed\"])\n\n        self.mileage = None\n        if \"mileage\" in data:\n            self.mileage = int(data[\"mileage\"])\n\n        self.startMileage = None\n        if \"startMileage\" in data:\n            self.startMileage = int(data[\"startMileage\"])\n\n        self.traveltime = None\n        if \"traveltime\" in data:\n            self.traveltime = int(data[\"traveltime\"])\n\n        self.timestamp = None\n        if \"timestamp\" in data:\n            self.timestamp = data[\"timestamp\"]\n\n        self.overallMileage = None\n        if \"overallMileage\" in data:\n            self.overallMileage = int(data[\"overallMileage\"])\n\n        self.zeroEmissionDistance = None\n        if \"zeroEmissionDistance\" in data:\n            self.zeroEmissionDistance = int(data[\"zeroEmissionDistance\"])\n\n\nclass Field:\n    IDS = {\n        \"0x0\": \"UNKNOWN\",\n        \"0x0101010002\": \"UTC_TIME_AND_KILOMETER_STATUS\",\n        \"0x0203010001\": \"MAINTENANCE_INTERVAL_DISTANCE_TO_OIL_CHANGE\",\n        \"0x0203010002\": \"MAINTENANCE_INTERVAL_TIME_TO_OIL_CHANGE\",\n        \"0x0203010003\": \"MAINTENANCE_INTERVAL_DISTANCE_TO_INSPECTION\",\n        \"0x0203010004\": \"MAINTENANCE_INTERVAL_TIME_TO_INSPECTION\",\n        \"0x0203010006\": \"MAINTENANCE_INTERVAL_ALARM_INSPECTION\",\n        \"0x0203010007\": \"MAINTENANCE_INTERVAL_MONTHLY_MILEAGE\",\n        \"0x0203010005\": \"WARNING_OIL_CHANGE\",\n        \"0x0204040001\": \"OIL_LEVEL_AMOUNT_IN_LITERS\",\n        \"0x0204040002\": \"OIL_LEVEL_MINIMUM_WARNING\",\n        \"0x0204040003\": \"OIL_LEVEL_DIPSTICKS_PERCENTAGE\",\n        \"0x02040C0001\": \"ADBLUE_RANGE\",\n        \"0x0301010001\": \"LIGHT_STATUS\",\n        \"0x0301030001\": \"BRAKING_STATUS\",\n        \"0x0301030005\": \"TOTAL_RANGE\",\n        \"0x030103000A\": \"TANK_LEVEL_IN_PERCENTAGE\",\n        \"0x0301040001\": \"LOCK_STATE_LEFT_FRONT_DOOR\",\n        \"0x0301040002\": \"OPEN_STATE_LEFT_FRONT_DOOR\",\n        \"0x0301040003\": \"SAFETY_STATE_LEFT_FRONT_DOOR\",\n        \"0x0301040004\": \"LOCK_STATE_LEFT_REAR_DOOR\",\n        \"0x0301040005\": \"OPEN_STATE_LEFT_REAR_DOOR\",\n        \"0x0301040006\": \"SAFETY_STATE_LEFT_REAR_DOOR\",\n        \"0x0301040007\": \"LOCK_STATE_RIGHT_FRONT_DOOR\",\n        \"0x0301040008\": \"OPEN_STATE_RIGHT_FRONT_DOOR\",\n        \"0x0301040009\": \"SAFETY_STATE_RIGHT_FRONT_DOOR\",\n        \"0x030104000A\": \"LOCK_STATE_RIGHT_REAR_DOOR\",\n        \"0x030104000B\": \"OPEN_STATE_RIGHT_REAR_DOOR\",\n        \"0x030104000C\": \"SAFETY_STATE_RIGHT_REAR_DOOR\",\n        \"0x030104000D\": \"LOCK_STATE_TRUNK_LID\",\n        \"0x030104000E\": \"OPEN_STATE_TRUNK_LID\",\n        \"0x030104000F\": \"SAFETY_STATE_TRUNK_LID\",\n        \"0x0301040010\": \"LOCK_STATE_HOOD\",\n        \"0x0301040011\": \"OPEN_STATE_HOOD\",\n        \"0x0301040012\": \"SAFETY_STATE_HOOD\",\n        \"0x0301050001\": \"STATE_LEFT_FRONT_WINDOW\",\n        \"0x0301050003\": \"STATE_LEFT_REAR_WINDOW\",\n        \"0x0301050005\": \"STATE_RIGHT_FRONT_WINDOW\",\n        \"0x0301050007\": \"STATE_RIGHT_REAR_WINDOW\",\n        \"0x0301050009\": \"STATE_DECK\",\n        \"0x030105000B\": \"STATE_SUN_ROOF_MOTOR_COVER\",\n        \"0x0301030006\": \"PRIMARY_RANGE\",\n        \"0x0301030007\": \"PRIMARY_DRIVE\",\n        \"0x0301030008\": \"SECONDARY_RANGE\",\n        \"0x0301030009\": \"SECONDARY_DRIVE\",\n        \"0x0301030002\": \"STATE_OF_CHARGE\",\n        \"0x0301020001\": \"TEMPERATURE_OUTSIDE\",\n        \"0x0202\": \"ACTIVE_INSTRUMENT_CLUSTER_WARNING\",\n    }\n\n    def __init__(self, data):\n        self.name = None\n        self.id = data.get(\"id\")\n        self.unit = data.get(\"unit\")\n        self.value = data.get(\"value\")\n        self.measure_time = data.get(\"tsTssReceivedUtc\")\n        if self.measure_time is None:\n            self.measure_time = data.get(\"tsCarCaptured\")\n        self.send_time = data.get(\"tsCarSentUtc\")\n        self.measure_mileage = data.get(\"milCarCaptured\")\n        self.send_mileage = data.get(\"milCarSent\")\n\n        for field_id, name in self.IDS.items():\n            if field_id == self.id:\n                self.name = name\n                break\n        if self.name is None:\n            # No direct mapping found - maybe we've at least got a text id\n            self.name = data.get(\"textId\")\n\n    def __str__(self):\n        str_rep = str(self.name) + \" \" + str(self.value)\n        if self.unit is not None:\n            str_rep += self.unit\n        return str_rep\n\n\nclass Vehicle:\n    def __init__(self):\n        self.vin = \"\"\n        self.csid = \"\"\n        self.model = \"\"\n        self.model_year = \"\"\n        self.model_family = \"\"\n        self.title = \"\"\n\n    def parse(self, data):\n        self.vin = data.get(\"vin\")\n        self.csid = data.get(\"csid\")\n        if (\n            data.get(\"vehicle\") is not None\n            and data.get(\"vehicle\").get(\"media\") is not None\n        ):\n            self.model = data.get(\"vehicle\").get(\"media\").get(\"longName\")\n        if (\n            data.get(\"vehicle\") is not None\n            and data.get(\"vehicle\").get(\"core\") is not None\n        ):\n            self.model_year = data.get(\"vehicle\").get(\"core\").get(\"modelYear\")\n        if data.get(\"nickname\") is not None and len(data.get(\"nickname\")) > 0:\n            self.title = data.get(\"nickname\")\n        elif (\n            data.get(\"vehicle\") is not None\n            and data.get(\"vehicle\").get(\"media\") is not None\n        ):\n            self.title = data.get(\"vehicle\").get(\"media\").get(\"shortName\")\n\n    def __str__(self):\n        return str(self.__dict__)\n\n\nclass VehiclesResponse:\n    def __init__(self):\n        self.vehicles = []\n        self.blacklisted_vins = 0\n\n    def parse(self, data):\n        user_vehicles = data.get(\"userVehicles\")\n        if user_vehicles is None:\n            _LOGGER.warning(\"No vehicle data received from API. Check authentication.\")\n            return\n\n        for item in user_vehicles:\n            vehicle = Vehicle()\n            vehicle.parse(item)\n            self.vehicles.append(vehicle)\n"
  },
  {
    "path": "custom_components/audiconnect/audi_services.py",
    "content": "import json\nimport uuid\nimport base64\nimport os\nimport re\nimport logging\nfrom datetime import timedelta, datetime\nfrom typing import Optional\n\nfrom .audi_models import (\n    TripDataResponse,\n    CurrentVehicleDataResponse,\n    VehicleDataResponse,\n    VehiclesResponse,\n)\nfrom .audi_api import AudiAPI\nfrom .const import DEFAULT_API_LEVEL\nfrom .util import to_byte_array, get_attr\n\nfrom hashlib import sha256, sha512\nimport hmac\nimport asyncio\n\nfrom urllib.parse import urlparse, parse_qs, urlencode\n\nimport requests\nfrom bs4 import BeautifulSoup\nfrom requests import RequestException\n\nfrom typing import Dict\n\n\nMAX_RESPONSE_ATTEMPTS = 10\nREQUEST_STATUS_SLEEP = 10\n\nSUCCEEDED = \"succeeded\"\nFAILED = \"failed\"\nREQUEST_SUCCESSFUL = \"request_successful\"\nREQUEST_FAILED = \"request_failed\"\n\n_LOGGER = logging.getLogger(__name__)\n\n\nclass BrowserLoginResponse:\n    def __init__(self, response: requests.Response, url: str):\n        self.response = response  # type: requests.Response\n        self.url = url  # type : str\n\n    def get_location(self) -> str:\n        \"\"\"\n        Returns the location the previous request redirected to\n        \"\"\"\n        location = self.response.headers[\"Location\"]\n        if location.startswith(\"/\"):\n            # Relative URL\n            return BrowserLoginResponse.to_absolute(self.url, location)\n        return location\n\n    @classmethod\n    def to_absolute(cls, absolute_url, relative_url) -> str:\n        \"\"\"\n        Converts a relative url to an absolute url\n        :param absolute_url: Absolute url used as baseline\n        :param relative_url: Relative url (must start with /)\n        :return: New absolute url\n        \"\"\"\n        url_parts = urlparse(absolute_url)\n        return url_parts.scheme + \"://\" + url_parts.netloc + relative_url\n\n\nclass AudiService:\n    def __init__(self, api: AudiAPI, country: str, spin: str, api_level: int):\n        self._api = api\n        self._country = country\n        self._language = None\n        self._type = \"Audi\"\n        self._spin = spin\n        self._homeRegion = {}\n        self._homeRegionSetter = {}\n        self.mbbOAuthBaseURL = None\n        self.mbboauthToken = None\n        self.xclientId = None\n        self._tokenEndpoint = \"\"\n        self._bearer_token_json = None\n        self._client_id = \"\"\n        self._authorizationServerBaseURLLive = \"\"\n        self._api_level = api_level\n\n        if self._api_level is None:\n            self._api_level = DEFAULT_API_LEVEL\n\n        if self._country is None:\n            self._country = \"DE\"\n\n    def get_hidden_html_input_form_data(self, response, form_data: Dict[str, str]):\n        # Now parse the html body and extract the target url, csrf token and other required parameters\n        html = BeautifulSoup(response, \"html.parser\")\n        form_inputs = html.find_all(\"input\", attrs={\"type\": \"hidden\"})\n        for form_input in form_inputs:\n            name = form_input.get(\"name\")\n            form_data[name] = form_input.get(\"value\")\n\n        return form_data\n\n    def get_post_url(self, response, url):\n        # Now parse the html body and extract the target url, csrf token and other required parameters\n        html = BeautifulSoup(response, \"html.parser\")\n        form_tag = html.find(\"form\")\n\n        # Extract the target url\n        action = form_tag.get(\"action\")\n        if action.startswith(\"http\"):\n            # Absolute url\n            username_post_url = action\n        elif action.startswith(\"/\"):\n            # Relative to domain\n            username_post_url = BrowserLoginResponse.to_absolute(url, action)\n        else:\n            raise RequestException(\"Unknown form action: \" + action)\n        return username_post_url\n\n    async def login(self, user: str, password: str, persist_token: bool = True):\n        _LOGGER.debug(\"LOGIN: Starting login to Audi service...\")\n        await self.login_request(user, password)\n\n    async def refresh_vehicle_data(self, vin: str):\n        res = await self.request_current_vehicle_data(vin.upper())\n        request_id = res.request_id\n\n        checkUrl = \"{homeRegion}/fs-car/bs/vsr/v1/{type}/{country}/vehicles/{vin}/requests/{requestId}/jobstatus\".format(\n            homeRegion=await self._get_home_region(vin.upper()),\n            type=self._type,\n            country=self._country,\n            vin=vin.upper(),\n            requestId=request_id,\n        )\n\n        await self.check_request_succeeded(\n            checkUrl,\n            \"refresh vehicle data\",\n            REQUEST_SUCCESSFUL,\n            REQUEST_FAILED,\n            \"requestStatusResponse.status\",\n        )\n\n    async def request_current_vehicle_data(self, vin: str):\n        self._api.use_token(self.vwToken)\n        data = await self._api.post(\n            \"{homeRegion}/fs-car/bs/vsr/v1/{type}/{country}/vehicles/{vin}/requests\".format(\n                homeRegion=await self._get_home_region(vin.upper()),\n                type=self._type,\n                country=self._country,\n                vin=vin.upper(),\n            )\n        )\n        return CurrentVehicleDataResponse(data)\n\n    async def get_preheater(self, vin: str):\n        self._api.use_token(self.vwToken)\n        return await self._api.get(\n            \"{homeRegion}/fs-car/bs/rs/v1/{type}/{country}/vehicles/{vin}/status\".format(\n                homeRegion=await self._get_home_region(vin.upper()),\n                type=self._type,\n                country=self._country,\n                vin=vin.upper(),\n            )\n        )\n\n    async def get_stored_vehicle_data(self, vin: str):\n        redacted_vin = \"*\" * (len(vin) - 4) + vin[-4:]\n        JOBS2QUERY = {\n            \"access\",\n            \"activeVentilation\",\n            \"auxiliaryHeating\",\n            \"batteryChargingCare\",\n            \"batterySupport\",\n            \"charging\",\n            \"chargingProfiles\",\n            \"chargingTimers\",\n            \"climatisation\",\n            \"climatisationTimers\",\n            \"departureProfiles\",\n            \"departureTimers\",\n            \"fuelStatus\",\n            \"honkAndFlash\",\n            \"hybridCarAuxiliaryHeating\",\n            \"lvBattery\",\n            \"measurements\",\n            \"oilLevel\",\n            \"readiness\",\n            # \"userCapabilities\",\n            \"vehicleHealthInspection\",\n            \"vehicleHealthWarnings\",\n            \"vehicleLights\",\n        }\n        self._api.use_token(self._bearer_token_json)\n        data = await self._api.get(\n            self.__get_cariad_url_for_vin(\n                vin, \"selectivestatus?jobs={jobs}\", jobs=\",\".join(JOBS2QUERY)\n            )\n        )\n\n        _LOGGER.debug(\"Vehicle data returned for VIN: %s: %s\", redacted_vin, data)\n        return VehicleDataResponse(data)\n\n    async def get_charger(self, vin: str):\n        self._api.use_token(self.vwToken)\n        return await self._api.get(\n            \"{homeRegion}/fs-car/bs/batterycharge/v1/{type}/{country}/vehicles/{vin}/charger\".format(\n                homeRegion=await self._get_home_region(vin.upper()),\n                type=self._type,\n                country=self._country,\n                vin=vin.upper(),\n            )\n        )\n\n    async def get_climater(self, vin: str):\n        self._api.use_token(self.vwToken)\n        return await self._api.get(\n            \"{homeRegion}/fs-car/bs/climatisation/v1/{type}/{country}/vehicles/{vin}/climater\".format(\n                homeRegion=await self._get_home_region(vin.upper()),\n                type=self._type,\n                country=self._country,\n                vin=vin.upper(),\n            )\n        )\n\n    async def get_stored_position(self, vin: str):\n        self._api.use_token(self._bearer_token_json)\n        return await self._api.get(\n            self.__get_cariad_url_for_vin(vin, \"parkingposition\")\n        )\n\n    async def get_operations_list(self, vin: str):\n        self._api.use_token(self.vwToken)\n        return await self._api.get(\n            \"https://mal-1a.prd.ece.vwg-connect.com/api/rolesrights/operationlist/v3/vehicles/\"\n            + vin.upper()\n        )\n\n    async def get_timer(self, vin: str):\n        self._api.use_token(self.vwToken)\n        return await self._api.get(\n            \"{homeRegion}/fs-car/bs/departuretimer/v1/{type}/{country}/vehicles/{vin}/timer\".format(\n                homeRegion=await self._get_home_region(vin.upper()),\n                type=self._type,\n                country=self._country,\n                vin=vin.upper(),\n            )\n        )\n\n    async def get_vehicles(self):\n        self._api.use_token(self.vwToken)\n        return await self._api.get(\n            \"https://msg.volkswagen.de/fs-car/usermanagement/users/v1/{type}/{country}/vehicles\".format(\n                type=self._type, country=self._country\n            )\n        )\n\n    async def get_vehicle_information(self):\n        headers = {\n            \"Accept\": \"application/json\",\n            \"Accept-Charset\": \"utf-8\",\n            \"X-App-Name\": \"myAudi\",\n            \"X-App-Version\": AudiAPI.HDR_XAPP_VERSION,\n            \"Accept-Language\": \"{l}-{c}\".format(\n                l=self._language, c=self._country.upper()\n            ),\n            \"X-User-Country\": self._country.upper(),\n            \"User-Agent\": AudiAPI.HDR_USER_AGENT,\n            \"Authorization\": \"Bearer \" + self.audiToken[\"access_token\"],\n            \"Content-Type\": \"application/json; charset=utf-8\",\n        }\n        req_data = {\n            \"query\": \"query vehicleList {\\n userVehicles {\\n vin\\n mappingVin\\n vehicle { core { modelYear\\n }\\n media { shortName\\n longName }\\n }\\n csid\\n commissionNumber\\n type\\n devicePlatform\\n mbbConnect\\n userRole {\\n role\\n }\\n vehicle {\\n classification {\\n driveTrain\\n }\\n }\\n nickname\\n }\\n}\"\n        }\n        req_rsp, rep_rsptxt = await self._api.request(\n            \"POST\",\n            \"https://app-api.my.aoa.audi.com/vgql/v1/graphql\"\n            if self._country.upper() == \"US\"\n            else \"https://app-api.live-my.audi.com/vgql/v1/graphql\",  # Starting in 2023, US users need to point at the aoa (Audi of America) URL.\n            json.dumps(req_data),\n            headers=headers,\n            allow_redirects=False,\n            rsp_wtxt=True,\n        )\n        vins = json.loads(rep_rsptxt)\n        if \"errors\" in vins:\n            raise Exception(f\"API returned errors: {vins['errors']}\")\n\n        if \"data\" not in vins or vins[\"data\"] is None:\n            raise Exception(\"No data in API response\")\n\n        if vins[\"data\"].get(\"userVehicles\") is None:\n            raise Exception(\n                \"No vehicle data in API response - possible authentication issue\"\n            )\n\n        response = VehiclesResponse()\n        response.parse(vins[\"data\"])\n        return response\n\n    async def get_vehicle_data(self, vin: str):\n        self._api.use_token(self.vwToken)\n        return await self._api.get(\n            \"{homeRegion}/fs-car/vehicleMgmt/vehicledata/v2/{type}/{country}/vehicles/{vin}/\".format(\n                homeRegion=await self._get_home_region(vin.upper()),\n                type=self._type,\n                country=self._country,\n                vin=vin.upper(),\n            )\n        )\n\n    async def get_tripdata(self, vin: str, kind: str):\n        self._api.use_token(self.vwToken)\n\n        # read tripdata\n        headers = {\n            \"Accept\": \"application/json\",\n            \"Accept-Charset\": \"utf-8\",\n            \"X-App-Name\": \"myAudi\",\n            \"X-App-Version\": AudiAPI.HDR_XAPP_VERSION,\n            \"X-Client-ID\": self.xclientId,\n            \"User-Agent\": AudiAPI.HDR_USER_AGENT,\n            \"Authorization\": \"Bearer \" + self.vwToken[\"access_token\"],\n        }\n        td_reqdata = {\n            \"type\": \"list\",\n            \"from\": \"1970-01-01T00:00:00Z\",\n            # \"from\":(datetime.utcnow() - timedelta(days=365)).strftime(\"%Y-%m-%dT%H:%M:%SZ\"),\n            \"to\": (datetime.utcnow() + timedelta(minutes=90)).strftime(\n                \"%Y-%m-%dT%H:%M:%SZ\"\n            ),\n        }\n        data = await self._api.request(\n            \"GET\",\n            \"{homeRegion}/api/bs/tripstatistics/v1/vehicles/{vin}/tripdata/{kind}\".format(\n                homeRegion=await self._get_home_region_setter(vin.upper()),\n                vin=vin.upper(),\n                kind=kind,\n            ),\n            None,\n            params=td_reqdata,\n            headers=headers,\n        )\n        td_sorted = sorted(\n            data[\"tripDataList\"][\"tripData\"],\n            key=lambda k: k[\"overallMileage\"],\n            reverse=True,\n        )\n        # _LOGGER.debug(\"get_tripdata: td_sorted: %s\", td_sorted)\n        td_current = td_sorted[0]\n        # FIX, TR/2023-03-25: Assign just in case td_sorted contains only one item\n        td_reset_trip = td_sorted[0]\n\n        for trip in td_sorted:\n            if (td_current[\"startMileage\"] - trip[\"startMileage\"]) > 2:\n                td_reset_trip = trip\n                break\n            else:\n                td_current[\"tripID\"] = trip[\"tripID\"]\n                td_current[\"startMileage\"] = trip[\"startMileage\"]\n        _LOGGER.debug(\"TRIP DATA: td_current: %s\", td_current)\n        _LOGGER.debug(\"TRIP DATA: td_reset_trip: %s\", td_reset_trip)\n\n        return TripDataResponse(td_current), TripDataResponse(td_reset_trip)\n\n    async def _fill_home_region(self, vin: str):\n        # the home-region endpoint returns\n        # https://ha-5a.prd.eu.vwg.vwautocloud.net which is no valid endpoint\n        # (at least not in DE region). set it statically.\n        if self._country.upper() != \"US\" and self._api_level == 1:\n            self._homeRegion[vin] = \"https://mal-3a.prd.eu.dp.vwg-connect.com\"\n            self._homeRegionSetter[vin] = \"https://mal-3a.prd.eu.dp.vwg-connect.com\"\n            return\n        self._homeRegion[vin] = \"https://msg.volkswagen.de\"\n        self._homeRegionSetter[vin] = \"https://mal-1a.prd.ece.vwg-connect.com\"\n\n        try:\n            self._api.use_token(self.vwToken)\n            res = await self._api.get(\n                \"https://mal-1a.prd.ece.vwg-connect.com/api/cs/vds/v1/vehicles/{vin}/homeRegion\".format(\n                    vin=vin\n                )\n            )\n            if (\n                res is not None\n                and res.get(\"homeRegion\") is not None\n                and res[\"homeRegion\"].get(\"baseUri\") is not None\n                and res[\"homeRegion\"][\"baseUri\"].get(\"content\") is not None\n            ):\n                uri = res[\"homeRegion\"][\"baseUri\"][\"content\"]\n                if uri != \"https://mal-1a.prd.ece.vwg-connect.com/api\":\n                    self._homeRegionSetter[vin] = uri.split(\"/api\")[0]\n                    self._homeRegion[vin] = self._homeRegionSetter[vin].replace(\n                        \"mal-\", \"fal-\"\n                    )\n        except Exception:\n            pass\n\n    async def _get_home_region(self, vin: str):\n        if self._homeRegion.get(vin) is not None:\n            return self._homeRegion[vin]\n\n        await self._fill_home_region(vin)\n\n        return self._homeRegion[vin]\n\n    async def _get_home_region_setter(self, vin: str):\n        if self._homeRegionSetter.get(vin) is not None:\n            return self._homeRegionSetter[vin]\n\n        await self._fill_home_region(vin)\n\n        return self._homeRegionSetter[vin]\n\n    async def _get_security_token(self, vin: str, action: str):\n        # Challenge\n        headers = {\n            \"User-Agent\": \"okhttp/3.7.0\",\n            \"X-App-Version\": \"3.14.0\",\n            \"X-App-Name\": \"myAudi\",\n            \"Accept\": \"application/json\",\n            \"Authorization\": \"Bearer \" + self.vwToken.get(\"access_token\"),\n        }\n\n        body = await self._api.request(\n            \"GET\",\n            \"{homeRegionSetter}/api/rolesrights/authorization/v2/vehicles/\".format(\n                homeRegionSetter=await self._get_home_region_setter(vin.upper())\n            )\n            + vin.upper()\n            + \"/services/\"\n            + action\n            + \"/security-pin-auth-requested\",\n            headers=headers,\n            data=None,\n        )\n        secToken = body[\"securityPinAuthInfo\"][\"securityToken\"]\n        challenge = body[\"securityPinAuthInfo\"][\"securityPinTransmission\"][\"challenge\"]\n\n        # Response\n        securityPinHash = self._generate_security_pin_hash(challenge)\n        data = {\n            \"securityPinAuthentication\": {\n                \"securityPin\": {\n                    \"challenge\": challenge,\n                    \"securityPinHash\": securityPinHash,\n                },\n                \"securityToken\": secToken,\n            }\n        }\n\n        headers = {\n            \"User-Agent\": \"okhttp/3.7.0\",\n            \"Content-Type\": \"application/json\",\n            \"X-App-Version\": \"3.14.0\",\n            \"X-App-Name\": \"myAudi\",\n            \"Accept\": \"application/json\",\n            \"Authorization\": \"Bearer \" + self.vwToken.get(\"access_token\"),\n        }\n\n        body = await self._api.request(\n            \"POST\",\n            \"{homeRegionSetter}/api/rolesrights/authorization/v2/security-pin-auth-completed\".format(\n                homeRegionSetter=await self._get_home_region_setter(vin.upper())\n            ),\n            headers=headers,\n            data=json.dumps(data),\n        )\n        return body[\"securityToken\"]\n\n    def _get_vehicle_action_header(\n        self, content_type: str, security_token: str, host: Optional[str] = None\n    ):\n        if not host:\n            host = (\n                \"mal-3a.prd.eu.dp.vwg-connect.com\"\n                if self._country in {\"DE\", \"US\"}\n                else \"msg.volkswagen.de\"\n            )\n\n        headers = {\n            \"User-Agent\": AudiAPI.HDR_USER_AGENT,\n            \"Host\": host,\n            \"X-App-Version\": AudiAPI.HDR_XAPP_VERSION,\n            \"X-App-Name\": \"myAudi\",\n            \"Authorization\": \"Bearer \" + self.vwToken.get(\"access_token\"),\n            \"Accept-charset\": \"UTF-8\",\n            \"Content-Type\": content_type,\n            \"Accept\": \"application/json, application/vnd.vwg.mbb.ChargerAction_v1_0_0+xml,application/vnd.volkswagenag.com-error-v1+xml,application/vnd.vwg.mbb.genericError_v1_0_2+xml, application/vnd.vwg.mbb.RemoteStandheizung_v2_0_0+xml, application/vnd.vwg.mbb.genericError_v1_0_2+xml,application/vnd.vwg.mbb.RemoteLockUnlock_v1_0_0+xml,*/*\",\n        }\n\n        if security_token:\n            headers[\"x-securityToken\"] = security_token\n\n        return headers\n\n    def __build_url(\n        self, base_url: str, path_and_query: str, **path_and_query_kwargs: dict\n    ):\n        action_path = path_and_query.format(**path_and_query_kwargs)\n\n        return base_url.rstrip(\"/\") + \"/\" + action_path.lstrip(\"/\")\n\n    def __get_cariad_url(self, path_and_query: str, **path_and_query_kwargs: dict):\n        base_url = \"https://{region}.bff.cariad.digital\".format(\n            region=\"emea\" if self._country.upper() != \"US\" else \"na\"\n        )\n\n        return self.__build_url(base_url, path_and_query, **path_and_query_kwargs)\n\n    def __get_cariad_url_for_vin(\n        self, vin: str, path_and_query: str, **path_and_query_kwargs: dict\n    ):\n        base_url = self.__get_cariad_url(\"/vehicle/v1/vehicles/{vin}\", vin=vin.upper())\n\n        return self.__build_url(base_url, path_and_query, **path_and_query_kwargs)\n\n    async def set_vehicle_lock(self, vin: str, lock: bool):\n        security_token = await self._get_security_token(\n            vin, \"rlu_v1/operations/\" + (\"LOCK\" if lock else \"UNLOCK\")\n        )\n        # deprecated data removed on 24Mar2025\n        # data = '<?xml version=\"1.0\" encoding= \"UTF-8\" ?><rluAction xmlns=\"http://audi.de/connect/rlu\"><action>{action}</action></rluAction>'.format(\n        #     action=\"lock\" if lock else \"unlock\"\n        # )\n        data = None\n\n        headers = self._get_vehicle_action_header(\n            \"application/vnd.vwg.mbb.RemoteLockUnlock_v1_0_0+xml\", security_token\n        )\n        res = await self._api.request(\n            \"POST\",\n            \"{homeRegionSetter}/api/bs/rlu/v1/vehicles/{vin}/{action}\".format(\n                homeRegionSetter=await self._get_home_region_setter(vin.upper()),\n                vin=vin.upper(),\n                action=\"lock\" if lock else \"unlock\",\n            ),\n            headers=headers,\n            data=data,\n        )\n\n        checkUrl = \"{homeRegionSetter}/api/bs/rlu/v1/vehicles/{vin}/requests/{requestId}/status\".format(\n            homeRegionSetter=await self._get_home_region_setter(vin.upper()),\n            vin=vin.upper(),\n            requestId=res[\"rluActionResponse\"][\"requestId\"],\n        )\n\n        await self.check_request_succeeded(\n            checkUrl,\n            \"lock vehicle\" if lock else \"unlock vehicle\",\n            REQUEST_SUCCESSFUL,\n            REQUEST_FAILED,\n            \"requestStatusResponse.status\",\n        )\n\n    async def set_battery_charger(self, vin: str, start: bool, timer: bool):\n        if start and timer:\n            data = {\"preferredChargeMode\": \"timer\"}\n        elif start:\n            data = {\"preferredChargeMode\": \"manual\"}\n        else:\n            raise NotImplementedError(\n                \"The 'Stop Charger' service is deprecated and will be removed in a future release.\"\n            )\n\n        data = json.dumps(data)\n        headers = {\"Authorization\": \"Bearer \" + self._bearer_token_json[\"access_token\"]}\n\n        await self._api.request(\n            \"PUT\",\n            self.__get_cariad_url_for_vin(vin, \"charging/mode\"),\n            headers=headers,\n            data=data,\n        )\n\n        # checkUrl = \"{homeRegion}/fs-car/bs/batterycharge/v1/{type}/{country}/vehicles/{vin}/charger/actions/{actionid}\".format(\n        #     homeRegion=await self._get_home_region(vin.upper()),\n        #     type=self._type,\n        #     country=self._country,\n        #     vin=vin.upper(),\n        #     actionid=res[\"action\"][\"actionId\"],\n        # )\n\n        # await self.check_request_succeeded(\n        #     checkUrl,\n        #     \"start charger\" if start else \"stop charger\",\n        #     SUCCEEDED,\n        #     FAILED,\n        #     \"action.actionState\",\n        # )\n\n    async def set_target_state_of_charge(self, vin: str, target_soc: int):\n        \"\"\"Set the target state of charge (battery percentage).\"\"\"\n        if not (20 <= target_soc <= 100):\n            raise ValueError(\n                \"Target state of charge must be between 20 and 100 percent\"\n            )\n\n        # Use Cariad BFF API (requires API level 1)\n        headers = {\"Authorization\": \"Bearer \" + self._bearer_token_json[\"access_token\"]}\n\n        data = {\"targetSOC_pct\": target_soc}\n\n        await self._api.request(\n            \"PUT\",\n            self.__get_cariad_url_for_vin(vin, \"charging/settings\"),\n            headers=headers,\n            data=json.dumps(data),\n        )\n\n    async def set_climatisation(self, vin: str, start: bool):\n        api_level = self._api_level\n        country = self._country\n\n        if start:\n            raise NotImplementedError(\n                \"The 'Start Climatisation (Legacy)' service is deprecated and no longer functional. \"\n                \"Please use the 'Start Climate Control' service instead.\"\n            )\n            # data = '{\"action\":{\"type\": \"startClimatisation\",\"settings\": {\"targetTemperature\": 2940,\"climatisationWithoutHVpower\": true,\"heaterSource\": \"electric\",\"climaterElementSettings\": {\"isClimatisationAtUnlock\": false, \"isMirrorHeatingEnabled\": true,}}}}'\n        else:\n            if api_level == 0:\n                data = '{\"action\":{\"type\": \"stopClimatisation\"}}'\n\n                if country == \"US\":\n                    headers = self._get_vehicle_action_header(\"application/json\", None)\n                    res = await self._api.request(\n                        \"POST\",\n                        \"https://mal-3a.prd.eu.dp.vwg-connect.com/api/bs/climatisation/v1/vehicles/{vin}/climater/actions\".format(\n                            vin=vin.upper(),\n                        ),\n                        headers=headers,\n                        data=data,\n                    )\n                    checkUrl = \"https://mal-3a.prd.eu.dp.vwg-connect.com/api/bs/climatisation/v1/vehicles/{vin}/climater/actions/{actionid}\".format(\n                        vin=vin.upper(),\n                        actionid=res[\"action\"][\"actionId\"],\n                    )\n\n                else:\n                    headers = self._get_vehicle_action_header(\n                        \"application/json\", None, \"msg.volkswagen.de\"\n                    )\n                    res = await self._api.request(\n                        \"POST\",\n                        \"{homeRegion}/fs-car/bs/climatisation/v1/{type}/{country}/vehicles/{vin}/climater/actions\".format(\n                            homeRegion=await self._get_home_region(vin.upper()),\n                            type=self._type,\n                            country=self._country,\n                            vin=vin.upper(),\n                        ),\n                        headers=headers,\n                        data=data,\n                    )\n\n                    checkUrl = \"{homeRegion}/fs-car/bs/climatisation/v1/{type}/{country}/vehicles/{vin}/climater/actions/{actionid}\".format(\n                        homeRegion=await self._get_home_region(vin.upper()),\n                        type=self._type,\n                        country=self._country,\n                        vin=vin.upper(),\n                        actionid=res[\"action\"][\"actionId\"],\n                    )\n\n                await self.check_request_succeeded(\n                    checkUrl,\n                    \"stop climatisation\",\n                    SUCCEEDED,\n                    FAILED,\n                    \"action.actionState\",\n                )\n\n            elif api_level == 1:\n                data = None\n                headers = {\n                    \"Authorization\": \"Bearer \" + self._bearer_token_json[\"access_token\"]\n                }\n                res = await self._api.request(\n                    \"POST\",\n                    self.__get_cariad_url_for_vin(vin, \"climatisation/stop\"),\n                    headers=headers,\n                    data=data,\n                )\n\n                # checkUrl = \"https://emea.bff.cariad.digital/vehicle/v1/vehicles/{vin}/pendingrequests\".format(\n                #     vin=vin.upper(),\n                #     actionid=res[\"action\"][\"actionId\"],\n                # )\n\n                # await self.check_request_succeeded(\n                #     checkUrl,\n                #     \"startClimatisation\",\n                #     SUCCEEDED,\n                #     FAILED,\n                #     \"action.actionState\",\n                # )\n\n    async def start_climate_control(\n        self,\n        vin: str,\n        temp_f: int,\n        temp_c: int,\n        glass_heating: bool,\n        seat_fl: bool,\n        seat_fr: bool,\n        seat_rl: bool,\n        seat_rr: bool,\n        climatisation_at_unlock: bool = False,\n        climatisation_mode: str = \"comfort\",\n    ):\n        api_level = self._api_level\n        country = self._country\n        target_temperature = None\n\n        _LOGGER.debug(\n            f\"Attempting to start climate control with API Level {api_level} and country {country}.\"\n        )\n\n        if api_level == 0:\n            target_temperature = None\n            if temp_f is not None:\n                target_temperature = int(((temp_f - 32) * (5 / 9)) * 10 + 2731)\n            elif temp_c is not None:\n                target_temperature = int(temp_c * 10 + 2731)\n\n            # Default Temp\n            target_temperature = target_temperature or 2941\n\n            # Construct Zone Settings\n            zone_settings = [\n                {\"value\": {\"isEnabled\": seat_fl, \"position\": \"frontLeft\"}},\n                {\"value\": {\"isEnabled\": seat_fr, \"position\": \"frontRight\"}},\n                {\"value\": {\"isEnabled\": seat_rl, \"position\": \"rearLeft\"}},\n                {\"value\": {\"isEnabled\": seat_rr, \"position\": \"rearRight\"}},\n            ]\n\n            data = {\n                \"action\": {\n                    \"type\": \"startClimatisation\",\n                    \"settings\": {\n                        \"targetTemperature\": target_temperature,\n                        \"climatisationWithoutHVpower\": True,\n                        \"heaterSource\": \"electric\",\n                        \"climaterElementSettings\": {\n                            \"isClimatisationAtUnlock\": climatisation_at_unlock,\n                            \"isMirrorHeatingEnabled\": glass_heating,\n                            \"zoneSettings\": {\"zoneSetting\": zone_settings},\n                        },\n                    },\n                }\n            }\n\n            data = json.dumps(data)\n\n            if country == \"US\":\n                headers = self._get_vehicle_action_header(\"application/json\", None)\n                res = await self._api.request(\n                    \"POST\",\n                    \"https://mal-3a.prd.eu.dp.vwg-connect.com/api/bs/climatisation/v1/vehicles/{vin}/climater/actions\".format(\n                        vin=vin.upper(),\n                    ),\n                    headers=headers,\n                    data=data,\n                )\n\n                checkUrl = \"https://mal-3a.prd.eu.dp.vwg-connect.com/api/bs/climatisation/v1/vehicles/{vin}/climater/actions/{actionid}\".format(\n                    vin=vin.upper(),\n                    actionid=res[\"action\"][\"actionId\"],\n                )\n            else:\n                headers = self._get_vehicle_action_header(\n                    \"application/json\", None, \"msg.volkswagen.de\"\n                )\n                res = await self._api.request(\n                    \"POST\",\n                    \"{homeRegion}/fs-car/bs/climatisation/v1/{type}/{country}/vehicles/{vin}/climater/actions\".format(\n                        homeRegion=await self._get_home_region(vin.upper()),\n                        type=self._type,\n                        country=self._country,\n                        vin=vin.upper(),\n                    ),\n                    headers=headers,\n                    data=data,\n                )\n\n                checkUrl = \"{homeRegion}/fs-car/bs/climatisation/v1/{type}/{country}/vehicles/{vin}/climater/actions/{actionid}\".format(\n                    homeRegion=await self._get_home_region(vin.upper()),\n                    type=self._type,\n                    country=self._country,\n                    vin=vin.upper(),\n                    actionid=res[\"action\"][\"actionId\"],\n                )\n\n            await self.check_request_succeeded(\n                checkUrl,\n                \"startClimatisation\",\n                SUCCEEDED,\n                FAILED,\n                \"action.actionState\",\n            )\n\n        elif api_level == 1:\n            if temp_f is not None:\n                target_temperature = int((temp_f - 32) * (5 / 9))\n            elif temp_c is not None:\n                target_temperature = int(temp_c)\n\n            target_temperature = target_temperature or 21\n\n            data = {\n                \"climatisationMode\": climatisation_mode,\n                \"targetTemperature\": target_temperature,\n                \"targetTemperatureUnit\": \"celsius\",\n                \"climatisationWithoutExternalPower\": True,\n                \"climatizationAtUnlock\": climatisation_at_unlock,\n                \"windowHeatingEnabled\": glass_heating,\n                \"zoneFrontLeftEnabled\": seat_fl,\n                \"zoneFrontRightEnabled\": seat_fr,\n                \"zoneRearLeftEnabled\": seat_rl,\n                \"zoneRearRightEnabled\": seat_rr,\n            }\n\n            data = json.dumps(data)\n            headers = {\n                \"Authorization\": \"Bearer \" + self._bearer_token_json[\"access_token\"]\n            }\n            res = await self._api.request(\n                \"POST\",\n                self.__get_cariad_url_for_vin(vin, \"climatisation/start\"),\n                headers=headers,\n                data=data,\n            )\n\n            # checkUrl = \"https://emea.bff.cariad.digital/vehicle/v1/vehicles/{vin}/pendingrequests\".format(\n            #     vin=vin.upper(),\n            #     actionid=res[\"action\"][\"actionId\"],\n            # )\n\n            # await self.check_request_succeeded(\n            #     checkUrl,\n            #     \"startClimatisation\",\n            #     SUCCEEDED,\n            #     FAILED,\n            #     \"action.actionState\",\n            # )\n\n    async def set_window_heating(self, vin: str, start: bool):\n        data = '<?xml version=\"1.0\" encoding= \"UTF-8\" ?><action><type>{action}</type></action>'.format(\n            action=\"startWindowHeating\" if start else \"stopWindowHeating\"\n        )\n\n        headers = self._get_vehicle_action_header(\n            \"application/vnd.vwg.mbb.ClimaterAction_v1_0_0+xml\", None\n        )\n        res = await self._api.request(\n            \"POST\",\n            \"{homeRegion}/fs-car/bs/climatisation/v1/{type}/{country}/vehicles/{vin}/climater/actions\".format(\n                homeRegion=await self._get_home_region(vin.upper()),\n                type=self._type,\n                country=self._country,\n                vin=vin.upper(),\n            ),\n            headers=headers,\n            data=data,\n        )\n\n        checkUrl = \"{homeRegion}/fs-car/bs/climatisation/v1/{type}/{country}/vehicles/{vin}/climater/actions/{actionid}\".format(\n            homeRegion=await self._get_home_region(vin.upper()),\n            type=self._type,\n            country=self._country,\n            vin=vin.upper(),\n            actionid=res[\"action\"][\"actionId\"],\n        )\n\n        await self.check_request_succeeded(\n            checkUrl,\n            \"start window heating\" if start else \"stop window heating\",\n            SUCCEEDED,\n            FAILED,\n            \"action.actionState\",\n        )\n\n    async def set_pre_heater(\n        self, vin: str, activate: bool, duration: Optional[int] = None\n    ):\n        if activate:\n            if not duration:\n                duration = 30\n            data = {\n                \"duration_min\": int(duration),\n                \"spin\": self._spin,\n            }\n\n            data = json.dumps(data)\n        else:\n            data = None\n\n        headers = {\n            \"Accept\": \"application/json\",\n            \"Accept-charset\": \"utf-8\",\n            \"Authorization\": \"Bearer \" + self._bearer_token_json[\"access_token\"],\n            \"User-Agent\": AudiAPI.HDR_USER_AGENT,\n            \"Content-Type\": \"application/json; charset=utf-8\",\n            \"Accept-encoding\": \"gzip\",\n        }\n        res = await self._api.request(\n            \"POST\",\n            self.__get_cariad_url_for_vin(\n                vin, \"auxiliaryheating/{action}\", action=\"start\" if activate else \"stop\"\n            ),\n            headers=headers,\n            data=data,\n        )\n\n        await self.check_bff_request_succeeded(vin, res[\"data\"][\"requestID\"])\n\n    async def check_bff_request_succeeded(self, vin: str, request_id: str):\n        headers = {\n            \"Accept\": \"application/json\",\n            \"Accept-charset\": \"utf-8\",\n            \"Authorization\": \"Bearer \" + self._bearer_token_json[\"access_token\"],\n            \"User-Agent\": AudiAPI.HDR_USER_AGENT,\n            \"Content-Type\": \"application/json; charset=utf-8\",\n            \"Accept-encoding\": \"gzip\",\n        }\n\n        for _ in range(MAX_RESPONSE_ATTEMPTS):\n            await asyncio.sleep(REQUEST_STATUS_SLEEP)\n            res = await self._api.request(\n                \"GET\",\n                \"https://{homeRegion}.bff.cariad.digital/vehicle/v1/vehicles/{vin}/pendingrequests\".format(\n                    homeRegion=\"na\" if self._country.upper() == \"US\" else \"emea\",\n                    vin=vin.upper(),\n                ),\n                headers=headers,\n                data=None,\n            )\n\n            for pending_request in res[\"data\"]:\n                if pending_request[\"id\"] == request_id:\n                    if pending_request[\"status\"] == \"in_progress\":\n                        break  # continue waiting\n\n                    if pending_request[\"status\"] == \"successful\":\n                        return\n\n                    raise Exception(\n                        \"Request {} reached unexpected status {}\".format(\n                            request_id, pending_request[\"status\"]\n                        )\n                    )\n\n        raise Exception(\"Request {} timed out\".format(request_id))\n\n    async def check_request_succeeded(\n        self, url: str, action: str, successCode: str, failedCode: str, path: str\n    ):\n        for _ in range(MAX_RESPONSE_ATTEMPTS):\n            await asyncio.sleep(REQUEST_STATUS_SLEEP)\n\n            self._api.use_token(self.vwToken)\n            res = await self._api.get(url)\n\n            status = get_attr(res, path)\n\n            if status is None or (failedCode is not None and status == failedCode):\n                raise Exception(\n                    \"Cannot {action}, return code '{code}'\".format(\n                        action=action, code=status\n                    )\n                )\n\n            if status == successCode:\n                return\n\n        raise Exception(\"Cannot {action}, operation timed out\".format(action=action))\n\n    # TR/2022-12-20: New secret for X_QMAuth\n    def _calculate_X_QMAuth(self):\n        # Calculate X-QMAuth value\n        gmtime_100sec = int(\n            (datetime.utcnow() - datetime(1970, 1, 1)).total_seconds() / 100\n        )\n        xqmauth_secret = bytes(\n            [\n                26,\n                256 - 74,\n                256 - 103,\n                37,\n                256 - 84,\n                23,\n                256 - 102,\n                256 - 86,\n                78,\n                256 - 125,\n                256 - 85,\n                256 - 26,\n                113,\n                256 - 87,\n                71,\n                109,\n                23,\n                100,\n                24,\n                256 - 72,\n                91,\n                256 - 41,\n                6,\n                256 - 15,\n                67,\n                108,\n                256 - 95,\n                91,\n                256 - 26,\n                71,\n                256 - 104,\n                256 - 100,\n            ]\n        )\n        xqmauth_val = hmac.new(\n            xqmauth_secret,\n            str(gmtime_100sec).encode(\"ascii\", \"ignore\"),\n            digestmod=\"sha256\",\n        ).hexdigest()\n\n        # v1:01da27b0:fbdb6e4ba3109bc68040cb83f380796f4d3bb178a626c4cc7e166815b806e4b5\n        return \"v1:01da27b0:\" + xqmauth_val\n\n    # TR/2021-12-01: Refresh token before it expires\n    # returns True when refresh was required and successful, otherwise False\n    async def refresh_token_if_necessary(self, elapsed_sec: int) -> bool:\n        if self.mbboauthToken is None:\n            return False\n        if \"refresh_token\" not in self.mbboauthToken:\n            return False\n        if \"expires_in\" not in self.mbboauthToken:\n            return False\n\n        if (elapsed_sec + 5 * 60) < self.mbboauthToken[\"expires_in\"]:\n            # refresh not needed now\n            return False\n\n        try:\n            headers = {\n                \"Accept\": \"application/json\",\n                \"Accept-Charset\": \"utf-8\",\n                \"User-Agent\": AudiAPI.HDR_USER_AGENT,\n                \"Content-Type\": \"application/x-www-form-urlencoded\",\n                \"X-Client-ID\": self.xclientId,\n            }\n            mbboauth_refresh_data = {\n                \"grant_type\": \"refresh_token\",\n                \"token\": self.mbboauthToken[\"refresh_token\"],\n                \"scope\": \"sc2:fal\",\n                # \"vin\": vin,  << App uses a dedicated VIN here, but it works without, don't know\n            }\n            encoded_mbboauth_refresh_data = urlencode(\n                mbboauth_refresh_data, encoding=\"utf-8\"\n            ).replace(\"+\", \"%20\")\n            mbboauth_refresh_rsp, mbboauth_refresh_rsptxt = await self._api.request(\n                \"POST\",\n                self.mbbOAuthBaseURL + \"/mobile/oauth2/v1/token\",\n                encoded_mbboauth_refresh_data,\n                headers=headers,\n                allow_redirects=False,\n                rsp_wtxt=True,\n            )\n\n            # this code is the old \"vwToken\"\n            self.vwToken = json.loads(mbboauth_refresh_rsptxt)\n\n            # TR/2022-02-10: If a new refresh_token is provided, save it for further refreshes\n            if \"refresh_token\" in self.vwToken:\n                self.mbboauthToken[\"refresh_token\"] = self.vwToken[\"refresh_token\"]\n\n            # hdr\n            headers = {\n                \"Accept\": \"application/json\",\n                \"Accept-Charset\": \"utf-8\",\n                \"X-QMAuth\": self._calculate_X_QMAuth(),\n                \"User-Agent\": AudiAPI.HDR_USER_AGENT,\n                \"Content-Type\": \"application/x-www-form-urlencoded\",\n            }\n            # IDK token request data\n            tokenreq_data = {\n                \"client_id\": self._client_id,\n                \"grant_type\": \"refresh_token\",\n                \"refresh_token\": self._bearer_token_json.get(\"refresh_token\"),\n                \"response_type\": \"token id_token\",\n            }\n            # IDK token request\n            encoded_tokenreq_data = urlencode(tokenreq_data, encoding=\"utf-8\").replace(\n                \"+\", \"%20\"\n            )\n            bearer_token_rsp, bearer_token_rsptxt = await self._api.request(\n                \"POST\",\n                self._tokenEndpoint,\n                encoded_tokenreq_data,\n                headers=headers,\n                allow_redirects=False,\n                rsp_wtxt=True,\n            )\n            self._bearer_token_json = json.loads(bearer_token_rsptxt)\n\n            # AZS token\n            headers = {\n                \"Accept\": \"application/json\",\n                \"Accept-Charset\": \"utf-8\",\n                \"X-App-Version\": AudiAPI.HDR_XAPP_VERSION,\n                \"X-App-Name\": \"myAudi\",\n                \"User-Agent\": AudiAPI.HDR_USER_AGENT,\n                \"Content-Type\": \"application/json; charset=utf-8\",\n            }\n            asz_req_data = {\n                \"token\": self._bearer_token_json[\"access_token\"],\n                \"grant_type\": \"id_token\",\n                \"stage\": \"live\",\n                \"config\": \"myaudi\",\n            }\n            azs_token_rsp, azs_token_rsptxt = await self._api.request(\n                \"POST\",\n                self._authorizationServerBaseURLLive + \"/token\",\n                json.dumps(asz_req_data),\n                headers=headers,\n                allow_redirects=False,\n                rsp_wtxt=True,\n            )\n            azs_token_json = json.loads(azs_token_rsptxt)\n            self.audiToken = azs_token_json\n\n            return True\n\n        except Exception as exception:\n            _LOGGER.error(\"Refresh token failed: \" + str(exception))\n            return False\n\n    # TR/2021-12-01 updated to match behaviour of Android myAudi 4.5.0\n    async def login_request(self, user: str, password: str):\n        self._api.use_token(None)\n        self._api.set_xclient_id(None)\n        self.xclientId = None\n\n        # get markets\n        markets_json = await self._api.request(\n            \"GET\",\n            \"https://content.app.my.audi.com/service/mobileapp/configurations/markets\",\n            None,\n        )\n        if (\n            self._country.upper()\n            not in markets_json[\"countries\"][\"countrySpecifications\"]\n        ):\n            raise Exception(\"Country not found\")\n        self._language = markets_json[\"countries\"][\"countrySpecifications\"][\n            self._country.upper()\n        ][\"defaultLanguage\"]\n\n        # Dynamic configuration URLs\n        marketcfg_url = \"https://content.app.my.audi.com/service/mobileapp/configurations/market/{c}/{l}?v=4.23.1\".format(\n            c=self._country, l=self._language\n        )\n        openidcfg_url = self.__get_cariad_url(\"/login/v1/idk/openid-configuration\")\n\n        # get market config\n        marketcfg_json = await self._api.request(\"GET\", marketcfg_url, None)\n\n        # use dynamic config from marketcfg\n        self._client_id = \"09b6cbec-cd19-4589-82fd-363dfa8c24da@apps_vw-dilab_com\"\n        if \"idkClientIDAndroidLive\" in marketcfg_json:\n            self._client_id = marketcfg_json[\"idkClientIDAndroidLive\"]\n\n        self._authorizationServerBaseURLLive = self.__get_cariad_url(\"/login/v1/audi\")\n\n        if \"authorizationServerBaseURLLive\" in marketcfg_json:\n            self._authorizationServerBaseURLLive = marketcfg_json[\n                \"myAudiAuthorizationServerProxyServiceURLProduction\"\n            ]\n        self.mbbOAuthBaseURL = \"https://mbboauth-1d.prd.ece.vwg-connect.com/mbbcoauth\"\n        if \"mbbOAuthBaseURLLive\" in marketcfg_json:\n            self.mbbOAuthBaseURL = marketcfg_json[\"mbbOAuthBaseURLLive\"]\n\n        # get openId config\n        openidcfg_json = await self._api.request(\"GET\", openidcfg_url, None)\n\n        # use dynamic config from openId config\n        authorization_endpoint = \"https://identity.vwgroup.io/oidc/v1/authorize\"\n        if \"authorization_endpoint\" in openidcfg_json:\n            authorization_endpoint = openidcfg_json[\"authorization_endpoint\"]\n\n        self._tokenEndpoint = self.__get_cariad_url(\"/login/v1/idk/token\")\n\n        if \"token_endpoint\" in openidcfg_json:\n            self._tokenEndpoint = openidcfg_json[\"token_endpoint\"]\n        # revocation_endpoint = self.__get_cariad_base_url(\"/login/v1/idk/revoke\")\n        # if \"revocation_endpoint\" in openidcfg_json:\n        # revocation_endpoint = openidcfg_json[\"revocation_endpoint\"]\n\n        # generate code_challenge\n        code_verifier = str(base64.urlsafe_b64encode(os.urandom(32)), \"utf-8\").strip(\n            \"=\"\n        )\n        code_challenge = str(\n            base64.urlsafe_b64encode(\n                sha256(code_verifier.encode(\"ascii\", \"ignore\")).digest()\n            ),\n            \"utf-8\",\n        ).strip(\"=\")\n        code_challenge_method = \"S256\"\n\n        #\n        state = str(uuid.uuid4())\n        nonce = str(uuid.uuid4())\n\n        # login page\n        headers = {\n            \"Accept\": \"application/json\",\n            \"Accept-Charset\": \"utf-8\",\n            \"X-App-Version\": AudiAPI.HDR_XAPP_VERSION,\n            \"X-App-Name\": \"myAudi\",\n            \"User-Agent\": AudiAPI.HDR_USER_AGENT,\n        }\n        idk_data = {\n            \"response_type\": \"code\",\n            \"client_id\": self._client_id,\n            \"redirect_uri\": \"myaudi:///\",\n            \"scope\": \"address profile badge birthdate birthplace nationalIdentifier nationality profession email vin phone nickname name picture mbb gallery openid\",\n            \"state\": state,\n            \"nonce\": nonce,\n            \"prompt\": \"login\",\n            \"code_challenge\": code_challenge,\n            \"code_challenge_method\": code_challenge_method,\n            \"ui_locales\": \"de-de de\",\n        }\n        idk_rsp, idk_rsptxt = await self._api.request(\n            \"GET\",\n            authorization_endpoint,\n            None,\n            headers=headers,\n            params=idk_data,\n            rsp_wtxt=True,\n        )\n\n        # form_data with email\n        submit_data = self.get_hidden_html_input_form_data(idk_rsptxt, {\"email\": user})\n        submit_url = self.get_post_url(idk_rsptxt, authorization_endpoint)\n        # send email\n        email_rsp, email_rsptxt = await self._api.request(\n            \"POST\",\n            submit_url,\n            submit_data,\n            headers=headers,\n            cookies=idk_rsp.cookies,\n            allow_redirects=True,\n            rsp_wtxt=True,\n        )\n\n        # form_data with password\n        # 2022-01-29: new HTML response uses a js two build the html form data + button.\n        #             Therefore it's not possible to extract hmac and other form data.\n        #             --> extract hmac from embedded js snippet.\n        regex_res = re.findall('\"hmac\"\\\\s*:\\\\s*\"[0-9a-fA-F]+\"', email_rsptxt)\n        if regex_res:\n            submit_url = submit_url.replace(\"identifier\", \"authenticate\")\n            submit_data[\"hmac\"] = regex_res[0].split(\":\")[1].strip('\"')\n            submit_data[\"password\"] = password\n        else:\n            submit_data = self.get_hidden_html_input_form_data(\n                email_rsptxt, {\"password\": password}\n            )\n            submit_url = self.get_post_url(email_rsptxt, submit_url)\n\n        # send password\n        pw_rsp, pw_rsptxt = await self._api.request(\n            \"POST\",\n            submit_url,\n            submit_data,\n            headers=headers,\n            cookies=idk_rsp.cookies,\n            allow_redirects=False,\n            rsp_wtxt=True,\n        )\n\n        # forward1 after pwd\n        fwd1_rsp, fwd1_rsptxt = await self._api.request(\n            \"GET\",\n            pw_rsp.headers[\"Location\"],\n            None,\n            headers=headers,\n            cookies=idk_rsp.cookies,\n            allow_redirects=False,\n            rsp_wtxt=True,\n        )\n        # forward2 after pwd\n        fwd2_rsp, fwd2_rsptxt = await self._api.request(\n            \"GET\",\n            fwd1_rsp.headers[\"Location\"],\n            None,\n            headers=headers,\n            cookies=idk_rsp.cookies,\n            allow_redirects=False,\n            rsp_wtxt=True,\n        )\n        # get tokens\n        codeauth_rsp, codeauth_rsptxt = await self._api.request(\n            \"GET\",\n            fwd2_rsp.headers[\"Location\"],\n            None,\n            headers=headers,\n            cookies=fwd2_rsp.cookies,\n            allow_redirects=False,\n            rsp_wtxt=True,\n        )\n        authcode_parsed = urlparse(\n            codeauth_rsp.headers[\"Location\"][len(\"myaudi:///?\") :]\n        )\n        authcode_strings = parse_qs(authcode_parsed.path)\n\n        # hdr\n        headers = {\n            \"Accept\": \"application/json\",\n            \"Accept-Charset\": \"utf-8\",\n            \"X-QMAuth\": self._calculate_X_QMAuth(),\n            \"User-Agent\": AudiAPI.HDR_USER_AGENT,\n            \"Content-Type\": \"application/x-www-form-urlencoded\",\n        }\n        # IDK token request data\n        tokenreq_data = {\n            \"client_id\": self._client_id,\n            \"grant_type\": \"authorization_code\",\n            \"code\": authcode_strings[\"code\"][0],\n            \"redirect_uri\": \"myaudi:///\",\n            \"response_type\": \"token id_token\",\n            \"code_verifier\": code_verifier,\n        }\n        # IDK token request\n        encoded_tokenreq_data = urlencode(tokenreq_data, encoding=\"utf-8\").replace(\n            \"+\", \"%20\"\n        )\n        bearer_token_rsp, bearer_token_rsptxt = await self._api.request(\n            \"POST\",\n            self._tokenEndpoint,\n            encoded_tokenreq_data,\n            headers=headers,\n            allow_redirects=False,\n            rsp_wtxt=True,\n        )\n        self._bearer_token_json = json.loads(bearer_token_rsptxt)\n\n        # AZS token\n        headers = {\n            \"Accept\": \"application/json\",\n            \"Accept-Charset\": \"utf-8\",\n            \"X-App-Version\": AudiAPI.HDR_XAPP_VERSION,\n            \"X-App-Name\": \"myAudi\",\n            \"User-Agent\": AudiAPI.HDR_USER_AGENT,\n            \"Content-Type\": \"application/json; charset=utf-8\",\n        }\n        asz_req_data = {\n            \"token\": self._bearer_token_json[\"access_token\"],\n            \"grant_type\": \"id_token\",\n            \"stage\": \"live\",\n            \"config\": \"myaudi\",\n        }\n        azs_token_rsp, azs_token_rsptxt = await self._api.request(\n            \"POST\",\n            self._authorizationServerBaseURLLive + \"/token\",\n            json.dumps(asz_req_data),\n            headers=headers,\n            allow_redirects=False,\n            rsp_wtxt=True,\n        )\n        azs_token_json = json.loads(azs_token_rsptxt)\n        self.audiToken = azs_token_json\n\n        # mbboauth client register\n        headers = {\n            \"Accept\": \"application/json\",\n            \"Accept-Charset\": \"utf-8\",\n            \"User-Agent\": AudiAPI.HDR_USER_AGENT,\n            \"Content-Type\": \"application/json; charset=utf-8\",\n        }\n        mbboauth_reg_data = {\n            \"client_name\": \"SM-A405FN\",\n            \"platform\": \"google\",\n            \"client_brand\": \"Audi\",\n            \"appName\": \"myAudi\",\n            \"appVersion\": AudiAPI.HDR_XAPP_VERSION,\n            \"appId\": \"de.myaudi.mobile.assistant\",\n        }\n        mbboauth_client_reg_rsp, mbboauth_client_reg_rsptxt = await self._api.request(\n            \"POST\",\n            self.mbbOAuthBaseURL + \"/mobile/register/v1\",\n            json.dumps(mbboauth_reg_data),\n            headers=headers,\n            allow_redirects=False,\n            rsp_wtxt=True,\n        )\n        mbboauth_client_reg_json = json.loads(mbboauth_client_reg_rsptxt)\n        self.xclientId = mbboauth_client_reg_json[\"client_id\"]\n        self._api.set_xclient_id(self.xclientId)\n\n        # mbboauth auth\n        headers = {\n            \"Accept\": \"application/json\",\n            \"Accept-Charset\": \"utf-8\",\n            \"User-Agent\": AudiAPI.HDR_USER_AGENT,\n            \"Content-Type\": \"application/x-www-form-urlencoded\",\n            \"X-Client-ID\": self.xclientId,\n        }\n        mbboauth_auth_data = {\n            \"grant_type\": \"id_token\",\n            \"token\": self._bearer_token_json[\"id_token\"],\n            \"scope\": \"sc2:fal\",\n        }\n        encoded_mbboauth_auth_data = urlencode(\n            mbboauth_auth_data, encoding=\"utf-8\"\n        ).replace(\"+\", \"%20\")\n        mbboauth_auth_rsp, mbboauth_auth_rsptxt = await self._api.request(\n            \"POST\",\n            self.mbbOAuthBaseURL + \"/mobile/oauth2/v1/token\",\n            encoded_mbboauth_auth_data,\n            headers=headers,\n            allow_redirects=False,\n            rsp_wtxt=True,\n        )\n        mbboauth_auth_json = json.loads(mbboauth_auth_rsptxt)\n        # store token and expiration time\n        self.mbboauthToken = mbboauth_auth_json\n\n        # mbboauth refresh (app immediately refreshes the token)\n        headers = {\n            \"Accept\": \"application/json\",\n            \"Accept-Charset\": \"utf-8\",\n            \"User-Agent\": AudiAPI.HDR_USER_AGENT,\n            \"Content-Type\": \"application/x-www-form-urlencoded\",\n            \"X-Client-ID\": self.xclientId,\n        }\n        mbboauth_refresh_data = {\n            \"grant_type\": \"refresh_token\",\n            \"token\": mbboauth_auth_json[\"refresh_token\"],\n            \"scope\": \"sc2:fal\",\n            # \"vin\": vin,  << App uses a dedicated VIN here, but it works without, don't know\n        }\n        encoded_mbboauth_refresh_data = urlencode(\n            mbboauth_refresh_data, encoding=\"utf-8\"\n        ).replace(\"+\", \"%20\")\n        mbboauth_refresh_rsp, mbboauth_refresh_rsptxt = await self._api.request(\n            \"POST\",\n            self.mbbOAuthBaseURL + \"/mobile/oauth2/v1/token\",\n            encoded_mbboauth_refresh_data,\n            headers=headers,\n            allow_redirects=False,\n            cookies=mbboauth_client_reg_rsp.cookies,\n            rsp_wtxt=True,\n        )\n        # this code is the old \"vwToken\"\n        self.vwToken = json.loads(mbboauth_refresh_rsptxt)\n\n    def _generate_security_pin_hash(self, challenge):\n        if self._spin is None:\n            raise Exception(\"sPin is required to perform this action\")\n\n        pin = to_byte_array(self._spin)\n        byteChallenge = to_byte_array(challenge)\n        b = bytes(pin + byteChallenge)\n        return sha512(b).hexdigest().upper()\n\n    async def _emulate_browser(\n        self, reply: BrowserLoginResponse, form_data: Dict[str, str]\n    ) -> BrowserLoginResponse:\n        # The reply redirects to the login page\n        login_location = reply.get_location()\n        page_reply = await self._api.get(login_location, raw_contents=True)\n\n        # Now parse the html body and extract the target url, csrf token and other required parameters\n        html = BeautifulSoup(page_reply, \"html.parser\")\n        form_tag = html.find(\"form\")\n\n        form_inputs = html.find_all(\"input\", attrs={\"type\": \"hidden\"})\n        for form_input in form_inputs:\n            name = form_input.get(\"name\")\n            form_data[name] = form_input.get(\"value\")\n\n        # Extract the target url\n        action = form_tag.get(\"action\")\n        if action.startswith(\"http\"):\n            # Absolute url\n            username_post_url = action\n        elif action.startswith(\"/\"):\n            # Relative to domain\n            username_post_url = BrowserLoginResponse.to_absolute(login_location, action)\n        else:\n            raise RequestException(\"Unknown form action: \" + action)\n\n        headers = {\"referer\": login_location}\n        reply = await self._api.post(\n            username_post_url,\n            form_data,\n            headers=headers,\n            use_json=False,\n            raw_reply=True,\n            allow_redirects=False,\n        )\n        return BrowserLoginResponse(reply, username_post_url)\n"
  },
  {
    "path": "custom_components/audiconnect/binary_sensor.py",
    "content": "\"\"\"Support for Audi Connect sensors.\"\"\"\n\nimport logging\n\nfrom homeassistant.components.binary_sensor import BinarySensorEntity\nfrom homeassistant.const import CONF_USERNAME\n\nfrom .audi_entity import AudiEntity\nfrom .const import DOMAIN\n\n_LOGGER = logging.getLogger(__name__)\n\n\nasync def async_setup_platform(hass, config, async_add_entities, discovery_info=None):\n    \"\"\"Old way.\"\"\"\n\n\nasync def async_setup_entry(hass, config_entry, async_add_entities):\n    sensors = []\n    account = config_entry.data.get(CONF_USERNAME)\n    audiData = hass.data[DOMAIN][account]\n\n    for config_vehicle in audiData.config_vehicles:\n        for binary_sensor in config_vehicle.binary_sensors:\n            sensors.append(AudiSensor(config_vehicle, binary_sensor))\n\n    async_add_entities(sensors)\n\n\nclass AudiSensor(AudiEntity, BinarySensorEntity):\n    \"\"\"Representation of an Audi sensor.\"\"\"\n\n    @property\n    def is_on(self):\n        \"\"\"Return True if the binary sensor is on.\"\"\"\n        return self._instrument.is_on\n\n    @property\n    def device_class(self):\n        \"\"\"Return the device_class of this sensor.\"\"\"\n        return self._instrument.device_class\n\n    @property\n    def entity_category(self):\n        \"\"\"Return the entity_category.\"\"\"\n        return self._instrument.entity_category\n"
  },
  {
    "path": "custom_components/audiconnect/config_flow.py",
    "content": "from collections import OrderedDict\nimport logging\nimport voluptuous as vol\n\nfrom homeassistant import config_entries\nfrom homeassistant.const import (\n    CONF_PASSWORD,\n    CONF_USERNAME,\n    CONF_REGION,\n    CONF_SCAN_INTERVAL,\n)\nfrom homeassistant.helpers.aiohttp_client import async_get_clientsession\nfrom homeassistant.core import callback\n\nfrom .audi_connect_account import AudiConnectAccount\nfrom .const import (\n    DOMAIN,\n    CONF_SPIN,\n    DEFAULT_UPDATE_INTERVAL,\n    MIN_UPDATE_INTERVAL,\n    CONF_SCAN_INITIAL,\n    CONF_SCAN_ACTIVE,\n    REGIONS,\n    CONF_API_LEVEL,\n    DEFAULT_API_LEVEL,\n    API_LEVELS,\n    CONF_FILTER_VINS,\n)\n\n_LOGGER = logging.getLogger(__name__)\n\n\n@callback\ndef configured_accounts(hass):\n    \"\"\"Return tuple of configured usernames.\"\"\"\n    entries = hass.config_entries.async_entries(DOMAIN)\n    if entries:\n        return (entry.data[CONF_USERNAME] for entry in entries)\n    return ()\n\n\n@config_entries.HANDLERS.register(DOMAIN)\nclass AudiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):\n    def __init__(self):\n        \"\"\"Initialize.\"\"\"\n        self._username = vol.UNDEFINED\n        self._password = vol.UNDEFINED\n        self._spin = vol.UNDEFINED\n        self._region = vol.UNDEFINED\n        self._scan_interval = DEFAULT_UPDATE_INTERVAL\n        self._api_level = DEFAULT_API_LEVEL\n\n    async def async_step_user(self, user_input=None):\n        \"\"\"Handle a user initiated config flow.\"\"\"\n        errors = {}\n\n        if user_input is not None:\n            self._username = user_input[CONF_USERNAME]\n            self._password = user_input[CONF_PASSWORD]\n            self._spin = user_input.get(CONF_SPIN)\n            self._region = REGIONS[user_input.get(CONF_REGION)]\n            self._scan_interval = user_input[CONF_SCAN_INTERVAL]\n            self._api_level = user_input[CONF_API_LEVEL]\n\n            try:\n                # pylint: disable=no-value-for-parameter\n                session = async_get_clientsession(self.hass)\n                connection = AudiConnectAccount(\n                    session=session,\n                    username=vol.Email()(self._username),\n                    password=self._password,\n                    country=self._region,\n                    spin=self._spin,\n                    api_level=self._api_level,\n                )\n\n                if await connection.try_login(False) is False:\n                    raise Exception(\n                        \"Unexpected error communicating with the Audi server\"\n                    )\n\n            except vol.Invalid:\n                errors[CONF_USERNAME] = \"invalid_username\"\n            except Exception:\n                errors[\"base\"] = \"invalid_credentials\"\n            else:\n                if self._username in configured_accounts(self.hass):\n                    errors[\"base\"] = \"user_already_configured\"\n                else:\n                    return self.async_create_entry(\n                        title=f\"{self._username}\",\n                        data={\n                            CONF_USERNAME: self._username,\n                            CONF_PASSWORD: self._password,\n                            CONF_SPIN: self._spin,\n                            CONF_REGION: self._region,\n                            CONF_SCAN_INTERVAL: self._scan_interval,\n                            CONF_API_LEVEL: self._api_level,\n                        },\n                    )\n\n        data_schema = OrderedDict()\n        data_schema[vol.Required(CONF_USERNAME, default=self._username)] = str\n        data_schema[vol.Required(CONF_PASSWORD, default=self._password)] = str\n        data_schema[vol.Optional(CONF_SPIN, default=self._spin)] = str\n        data_schema[vol.Required(CONF_REGION, default=self._region)] = vol.In(REGIONS)\n        data_schema[\n            vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_UPDATE_INTERVAL)\n        ] = int\n        data_schema[\n            vol.Optional(CONF_API_LEVEL, default=API_LEVELS[DEFAULT_API_LEVEL])\n        ] = vol.All(vol.Coerce(int), vol.In(API_LEVELS))\n\n        return self.async_show_form(\n            step_id=\"user\",\n            data_schema=vol.Schema(data_schema),\n            errors=errors,\n        )\n\n    async def async_step_import(self, user_input):\n        \"\"\"Import a config flow from configuration.\"\"\"\n        username = user_input[CONF_USERNAME]\n        password = user_input[CONF_PASSWORD]\n        api_level = user_input[CONF_API_LEVEL]\n\n        spin = None\n        if user_input.get(CONF_SPIN):\n            spin = user_input[CONF_SPIN]\n\n        region = \"DE\"\n        if user_input.get(CONF_REGION):\n            region = REGIONS[user_input.get(CONF_REGION)]\n\n        scan_interval = DEFAULT_UPDATE_INTERVAL\n\n        if user_input.get(CONF_SCAN_INTERVAL):\n            scan_interval = user_input[CONF_SCAN_INTERVAL]\n\n        if scan_interval < MIN_UPDATE_INTERVAL:\n            scan_interval = MIN_UPDATE_INTERVAL\n\n        try:\n            session = async_get_clientsession(self.hass)\n            connection = AudiConnectAccount(\n                session=session,\n                username=username,\n                password=password,\n                country=region,\n                spin=spin,\n                api_level=api_level,\n            )\n\n            if await connection.try_login(False) is False:\n                raise Exception(\"Unexpected error communicating with the Audi server\")\n\n        except Exception:\n            _LOGGER.error(\"Invalid credentials for %s\", username)\n            return self.async_abort(reason=\"invalid_credentials\")\n\n        return self.async_create_entry(\n            title=f\"{username} (from configuration)\",\n            data={\n                CONF_USERNAME: username,\n                CONF_PASSWORD: password,\n                CONF_SPIN: spin,\n                CONF_REGION: region,\n                CONF_SCAN_INTERVAL: scan_interval,\n                CONF_API_LEVEL: api_level,\n            },\n        )\n\n    @staticmethod\n    @callback\n    def async_get_options_flow(config_entry):\n        \"\"\"Get the options flow for this handler.\"\"\"\n        return OptionsFlowHandler(config_entry)\n\n\nclass OptionsFlowHandler(config_entries.OptionsFlow):\n    def __init__(self, config_entry):\n        self._config_entry: config_entries.ConfigEntry = config_entry\n        _LOGGER.debug(\n            \"Initializing options flow for audiconnect: %s\", config_entry.title\n        )\n\n    async def async_step_init(self, user_input=None):\n        _LOGGER.debug(\n            \"Options flow initiated for audiconnect: %s\", self._config_entry.title\n        )\n        if user_input is not None:\n            _LOGGER.debug(\"Received user input for options: %s\", user_input)\n            return self.async_create_entry(title=\"\", data=user_input)\n\n        current_scan_interval = self._config_entry.options.get(\n            CONF_SCAN_INTERVAL,\n            self._config_entry.data.get(CONF_SCAN_INTERVAL, DEFAULT_UPDATE_INTERVAL),\n        )\n\n        current_api_level = self._config_entry.options.get(\n            CONF_API_LEVEL,\n            self._config_entry.data.get(CONF_API_LEVEL, API_LEVELS[DEFAULT_API_LEVEL]),\n        )\n\n        current_filter_vins = self._config_entry.options.get(\n            CONF_FILTER_VINS,\n            self._config_entry.data.get(CONF_FILTER_VINS, \"\"),\n        )\n\n        _LOGGER.debug(\n            \"Retrieved current scan interval for audiconnect %s: %s minutes\",\n            self._config_entry.title,\n            current_scan_interval,\n        )\n\n        _LOGGER.debug(\n            \"Preparing options form for %s with default scan interval: %s minutes, initial scan: %s, active scan: %s\",\n            self._config_entry.title,\n            current_scan_interval,\n            self._config_entry.options.get(CONF_SCAN_INITIAL, True),\n            self._config_entry.options.get(CONF_SCAN_ACTIVE, True),\n        )\n\n        return self.async_show_form(\n            step_id=\"init\",\n            data_schema=vol.Schema(\n                {\n                    vol.Required(\n                        CONF_SCAN_INITIAL,\n                        default=self._config_entry.options.get(CONF_SCAN_INITIAL, True),\n                    ): bool,\n                    vol.Required(\n                        CONF_SCAN_ACTIVE,\n                        default=self._config_entry.options.get(CONF_SCAN_ACTIVE, True),\n                    ): bool,\n                    vol.Optional(\n                        CONF_SCAN_INTERVAL, default=current_scan_interval\n                    ): vol.All(vol.Coerce(int), vol.Clamp(min=MIN_UPDATE_INTERVAL)),\n                    vol.Optional(CONF_API_LEVEL, default=current_api_level): vol.All(\n                        vol.Coerce(int), vol.In(API_LEVELS)\n                    ),\n                    vol.Optional(CONF_FILTER_VINS, default=current_filter_vins): str,\n                }\n            ),\n        )\n"
  },
  {
    "path": "custom_components/audiconnect/const.py",
    "content": "DOMAIN = \"audiconnect\"\n\nCONF_VIN = \"vin\"\nCONF_CARNAME = \"carname\"\nCONF_ACTION = \"action\"\nCONF_CLIMATE_TEMP_F = \"temp_f\"\nCONF_CLIMATE_TEMP_C = \"temp_c\"\nCONF_CLIMATE_GLASS = \"glass_heating\"\nCONF_CLIMATE_SEAT_FL = \"seat_fl\"\nCONF_CLIMATE_SEAT_FR = \"seat_fr\"\nCONF_CLIMATE_SEAT_RL = \"seat_rl\"\nCONF_CLIMATE_SEAT_RR = \"seat_rr\"\nCONF_CLIMATE_AT_UNLOCK = \"climatisation_at_unlock\"\nCONF_CLIMATE_MODE = \"climatisation_mode\"\nCONF_SCAN_INITIAL = \"scan_initial\"\nCONF_SCAN_ACTIVE = \"scan_active\"\nCONF_API_LEVEL = \"api_level\"\nCONF_DURATION = \"duration\"\nCONF_TARGET_SOC = \"target_soc\"\n\nMIN_UPDATE_INTERVAL = 15\nDEFAULT_UPDATE_INTERVAL = 15\nUPDATE_SLEEP = 5\nDEFAULT_API_LEVEL = 0\n\nCONF_SPIN = \"spin\"\nCONF_REGION = \"region\"\nCONF_SERVICE_URL = \"service_url\"\nCONF_MUTABLE = \"mutable\"\nCONF_FILTER_VINS = \"filter_vins\"\n\nSIGNAL_STATE_UPDATED = \"{}.updated\".format(DOMAIN)\nTRACKER_UPDATE = f\"{DOMAIN}_tracker_update\"\n\nRESOURCES = [\n    \"position\",\n    \"last_update_time\",\n    \"shortterm_current\",\n    \"shortterm_reset\",\n    \"longterm_current\",\n    \"longterm_reset\",\n    \"mileage\",\n    \"range\",\n    \"service_inspection_time\",\n    \"service_inspection_distance\",\n    \"service_adblue_distance\",\n    \"oil_change_time\",\n    \"oil_change_distance\",\n    \"oil_level\",\n    \"charging_state\",\n    \"charging_mode\",\n    \"charging_type\",\n    \"energy_flow\",\n    \"max_charge_current\",\n    \"engine_type1\",\n    \"engine_type2\",\n    \"parking_light\",\n    \"any_window_open\",\n    \"any_door_unlocked\",\n    \"any_door_open\",\n    \"trunk_unlocked\",\n    \"trunk_open\",\n    \"hood_open\",\n    \"tank_level\",\n    \"state_of_charge\",\n    \"remaining_charging_time\",\n    \"plug_state\",\n    \"sun_roof\",\n    \"doors_trunk_status\",\n    \"left_front_door_open\",\n    \"right_front_door_open\",\n    \"left_rear_door_open\",\n    \"right_rear_door_open\",\n    \"left_front_window_open\",\n    \"right_front_window_open\",\n    \"left_rear_window_open\",\n    \"right_rear_window_open\",\n    \"braking_status\",\n    \"is_moving\",\n]\n\nCOMPONENTS = {\n    \"sensor\": \"sensor\",\n    \"binary_sensor\": \"binary_sensor\",\n    \"lock\": \"lock\",\n    \"device_tracker\": \"device_tracker\",\n    \"switch\": \"switch\",\n}\n\nREGION_EUROPE: str = \"DE\"\nREGION_CANADA: str = \"CA\"\nREGION_USA: str = \"US\"\nREGION_CHINA: str = \"CN\"\n\nREGIONS = {\n    1: REGION_EUROPE,\n    2: REGION_CANADA,\n    3: REGION_USA,\n    4: REGION_CHINA,\n}\n\nAPI_LEVELS = [0, 1]\n"
  },
  {
    "path": "custom_components/audiconnect/dashboard.py",
    "content": "#  Utilities for integration with Home Assistant (directly or via MQTT)\r\n\r\nimport logging\r\nimport re\r\n\r\nfrom homeassistant.components.sensor import (\r\n    SensorDeviceClass,\r\n    SensorStateClass,\r\n)\r\nfrom homeassistant.components.binary_sensor import BinarySensorDeviceClass\r\nfrom homeassistant.const import (\r\n    PERCENTAGE,\r\n    UnitOfTime,\r\n    UnitOfLength,\r\n    UnitOfTemperature,\r\n    UnitOfPower,\r\n    UnitOfElectricCurrent,\r\n    EntityCategory,\r\n)\r\nfrom .util import parse_datetime\r\n\r\n_LOGGER = logging.getLogger(__name__)\r\n\r\n\r\nclass Instrument:\r\n    def __init__(\r\n        self, component, attr, name, icon=None, suggested_display_precision=None\r\n    ):\r\n        self._attr = attr\r\n        self._component = component\r\n        self._name = name\r\n        self._connection = None\r\n        self._vehicle = None\r\n        self._icon = icon\r\n        self._suggested_display_precision = suggested_display_precision\r\n\r\n    def __repr__(self):\r\n        return self.full_name\r\n\r\n    def camel2slug(self, s):\r\n        \"\"\"Convert camelCase to camel_case.\r\n            >>> camel2slug('fooBar')\r\n        'foo_bar'\r\n        \"\"\"\r\n        return re.sub(\"([A-Z])\", \"_\\\\1\", s).lower().lstrip(\"_\")\r\n\r\n    @property\r\n    def slug_attr(self):\r\n        return self.camel2slug(self._attr.replace(\".\", \"_\"))\r\n\r\n    def setup(self, connection, vehicle, mutable=True, **config):\r\n        self._connection = connection\r\n        self._vehicle = vehicle\r\n\r\n        if not mutable and self.is_mutable:\r\n            _LOGGER.debug(\"Skipping %s because mutable\", self)\r\n            return False\r\n\r\n        if not self.is_supported:\r\n            # _LOGGER.debug(\r\n            #     \"%s (%s:%s) is not supported\", self, type(self).__name__, self._attr,\r\n            # )\r\n            return False\r\n\r\n        # _LOGGER.debug(\"%s is supported\", self)\r\n\r\n        return True\r\n\r\n    @property\r\n    def component(self):\r\n        return self._component\r\n\r\n    @property\r\n    def icon(self):\r\n        return self._icon\r\n\r\n    @property\r\n    def name(self):\r\n        return self._name\r\n\r\n    @property\r\n    def attr(self):\r\n        return self._attr\r\n\r\n    @property\r\n    def suggested_display_precision(self):\r\n        return self._suggested_display_precision\r\n\r\n    @property\r\n    def vehicle_name(self):\r\n        return self._vehicle.title\r\n\r\n    @property\r\n    def full_name(self):\r\n        return \"{} {}\".format(self.vehicle_name, self._name)\r\n\r\n    @property\r\n    def vehicle_model(self):\r\n        return self._vehicle.model\r\n\r\n    @property\r\n    def vehicle_model_year(self):\r\n        return self._vehicle.model_year\r\n\r\n    @property\r\n    def vehicle_model_family(self):\r\n        return self._vehicle.model_family\r\n\r\n    @property\r\n    def vehicle_vin(self):\r\n        return self._vehicle.vin\r\n\r\n    @property\r\n    def vehicle_csid(self):\r\n        return self._vehicle.csid\r\n\r\n    @property\r\n    def is_mutable(self):\r\n        raise NotImplementedError(\"Must be set\")\r\n\r\n    @property\r\n    def is_supported(self):\r\n        supported = self._attr + \"_supported\"\r\n        if hasattr(self._vehicle, supported):\r\n            return getattr(self._vehicle, supported)\r\n        if hasattr(self._vehicle, self._attr):\r\n            return True\r\n        return False\r\n\r\n    @property\r\n    def str_state(self):\r\n        return self.state\r\n\r\n    @property\r\n    def state(self):\r\n        if hasattr(self._vehicle, self._attr):\r\n            return getattr(self._vehicle, self._attr)\r\n        return self._vehicle.get_attr(self._attr)\r\n\r\n    @property\r\n    def attributes(self):\r\n        return {}\r\n\r\n\r\nclass Sensor(Instrument):\r\n    def __init__(\r\n        self,\r\n        attr,\r\n        name,\r\n        icon=None,\r\n        unit=None,\r\n        state_class=None,\r\n        device_class=None,\r\n        entity_category=None,\r\n        extra_state_attributes=None,\r\n        suggested_display_precision=None,\r\n    ):\r\n        super().__init__(\r\n            component=\"sensor\",\r\n            attr=attr,\r\n            name=name,\r\n            icon=icon,\r\n            suggested_display_precision=suggested_display_precision,\r\n        )\r\n        self.device_class = device_class\r\n        self._unit = unit\r\n        self.state_class = state_class\r\n        self.entity_category = entity_category\r\n        self.extra_state_attributes = extra_state_attributes\r\n        self._convert = False\r\n\r\n    @property\r\n    def is_mutable(self):\r\n        return False\r\n\r\n    @property\r\n    def str_state(self):\r\n        if self.unit:\r\n            return \"{} {}\".format(self.state, self.unit)\r\n        else:\r\n            return \"%s\" % self.state\r\n\r\n    @property\r\n    def state(self):\r\n        return super().state\r\n\r\n    @property\r\n    def unit(self):\r\n        supported = self._attr + \"_unit\"\r\n        if hasattr(self._vehicle, supported):\r\n            return getattr(self._vehicle, supported)\r\n\r\n        return self._unit\r\n\r\n\r\nclass BinarySensor(Instrument):\r\n    def __init__(self, attr, name, device_class=None, icon=None, entity_category=None):\r\n        super().__init__(component=\"binary_sensor\", attr=attr, name=name, icon=icon)\r\n        self.device_class = device_class\r\n        self.entity_category = entity_category\r\n\r\n    @property\r\n    def is_mutable(self):\r\n        return False\r\n\r\n    @property\r\n    def str_state(self):\r\n        if self.device_class in [\"door\", \"window\"]:\r\n            return \"Open\" if self.state else \"Closed\"\r\n        if self.device_class == \"safety\":\r\n            return \"Warning!\" if self.state else \"OK\"\r\n        if self.device_class == \"plug\":\r\n            return \"Charging\" if self.state else \"Plug removed\"\r\n        if self.device_class == \"lock\":\r\n            return \"Unlocked\" if self.state else \"Locked\"\r\n        if self.state is None:\r\n            _LOGGER.error(\"Can not encode state %s:%s\", self._attr, self.state)\r\n            return \"?\"\r\n        return \"On\" if self.state else \"Off\"\r\n\r\n    @property\r\n    def state(self):\r\n        val = super().state\r\n        if isinstance(val, (bool, list)):\r\n            #  for list (e.g. bulb_failures):\r\n            #  empty list (False) means no problem\r\n            return bool(val)\r\n        elif isinstance(val, str):\r\n            return val != \"Normal\"\r\n        return val\r\n\r\n    @property\r\n    def is_on(self):\r\n        return self.state\r\n\r\n\r\nclass Lock(Instrument):\r\n    def __init__(self):\r\n        super().__init__(component=\"lock\", attr=\"lock\", name=\"Door lock\")\r\n\r\n    @property\r\n    def is_mutable(self):\r\n        return True\r\n\r\n    @property\r\n    def str_state(self):\r\n        return \"Locked\" if self.state else \"Unlocked\"\r\n\r\n    @property\r\n    def state(self):\r\n        return self._vehicle.doors_trunk_status == \"Locked\"\r\n\r\n    @property\r\n    def is_locked(self):\r\n        return self.state\r\n\r\n    async def lock(self):\r\n        await self._connection.set_vehicle_lock(self.vehicle_vin, True)\r\n\r\n    async def unlock(self):\r\n        await self._connection.set_vehicle_lock(self.vehicle_vin, False)\r\n\r\n\r\nclass Switch(Instrument):\r\n    def __init__(self, attr, name, icon):\r\n        super().__init__(component=\"switch\", attr=attr, name=name, icon=icon)\r\n\r\n    @property\r\n    def is_mutable(self):\r\n        return True\r\n\r\n    @property\r\n    def str_state(self):\r\n        return \"On\" if self.state else \"Off\"\r\n\r\n    def is_on(self):\r\n        return self.state\r\n\r\n    def turn_on(self):\r\n        pass\r\n\r\n    def turn_off(self):\r\n        pass\r\n\r\n\r\nclass Preheater(Instrument):\r\n    def __init__(self):\r\n        super().__init__(\r\n            component=\"switch\",\r\n            attr=\"preheater_active\",\r\n            name=\"Preheater\",\r\n            icon=\"mdi:radiator\",\r\n        )\r\n\r\n    @property\r\n    def is_mutable(self):\r\n        return True\r\n\r\n    @property\r\n    def str_state(self):\r\n        return \"On\" if self.state else \"Off\"\r\n\r\n    def is_on(self):\r\n        return self.state\r\n\r\n    async def turn_on(self):\r\n        await self._connection.set_vehicle_pre_heater(self.vehicle_vin, True)\r\n\r\n    async def turn_off(self):\r\n        await self._connection.set_vehicle_pre_heater(self.vehicle_vin, False)\r\n\r\n\r\nclass Position(Instrument):\r\n    def __init__(self):\r\n        super().__init__(component=\"device_tracker\", attr=\"position\", name=\"Position\")\r\n\r\n    @property\r\n    def is_mutable(self):\r\n        return False\r\n\r\n    @property\r\n    def state(self):\r\n        state = super().state or {}\r\n        return (\r\n            state.get(\"latitude\", None),\r\n            state.get(\"longitude\", None),\r\n            state.get(\"timestamp\", None),\r\n            state.get(\"parktime\", None),\r\n        )\r\n\r\n    @property\r\n    def str_state(self):\r\n        state = super().state or {}\r\n        ts = state.get(\"timestamp\")\r\n        pt = state.get(\"parktime\")\r\n        return (\r\n            state.get(\"latitude\", None),\r\n            state.get(\"longitude\", None),\r\n            str(ts.astimezone(tz=None)) if ts else None,\r\n            str(pt.astimezone(tz=None)) if pt else None,\r\n        )\r\n\r\n\r\nclass TripData(Instrument):\r\n    def __init__(self, attr, name):\r\n        super().__init__(component=\"sensor\", attr=attr, name=name)\r\n        self.device_class = SensorDeviceClass.TIMESTAMP\r\n        self.unit = None\r\n        self.state_class = None\r\n        self.entity_category = None\r\n\r\n    @property\r\n    def is_mutable(self):\r\n        return False\r\n\r\n    @property\r\n    def str_state(self):\r\n        val = super().state\r\n        txt = \"\"\r\n\r\n        if val[\"averageElectricEngineConsumption\"] is not None:\r\n            txt = \"{}{}_kWh__\".format(txt, val[\"averageElectricEngineConsumption\"])\r\n\r\n        if val[\"averageFuelConsumption\"] is not None:\r\n            txt = \"{}{}_ltr__\".format(txt, val[\"averageFuelConsumption\"])\r\n\r\n        return \"{}{}_kmh__{}:{:02d}h_({}_m)__{}_km__{}-{}_km\".format(\r\n            txt,\r\n            val[\"averageSpeed\"],\r\n            int(val[\"traveltime\"] / 60),\r\n            val[\"traveltime\"] % 60,\r\n            val[\"traveltime\"],\r\n            val[\"mileage\"],\r\n            val[\"startMileage\"],\r\n            val[\"overallMileage\"],\r\n        )\r\n\r\n    @property\r\n    def state(self):\r\n        td = super().state\r\n        return parse_datetime(td[\"timestamp\"])\r\n\r\n    @property\r\n    def extra_state_attributes(self):\r\n        td = super().state\r\n        attr = {\r\n            \"averageElectricEngineConsumption\": td.get(\r\n                \"averageElectricEngineConsumption\", None\r\n            ),\r\n            \"averageFuelConsumption\": td.get(\"averageFuelConsumption\", None),\r\n            \"averageSpeed\": td.get(\"averageSpeed\", None),\r\n            \"mileage\": td.get(\"mileage\", None),\r\n            \"overallMileage\": td.get(\"overallMileage\", None),\r\n            \"startMileage\": td.get(\"startMileage\", None),\r\n            \"traveltime\": td.get(\"traveltime\", None),\r\n            \"tripID\": td.get(\"tripID\", None),\r\n            \"zeroEmissionDistance\": td.get(\"zeroEmissionDistance\", None),\r\n        }\r\n        return attr\r\n\r\n\r\nclass LastUpdate(Instrument):\r\n    def __init__(self):\r\n        super().__init__(\r\n            component=\"sensor\",\r\n            attr=\"last_update_time\",\r\n            name=\"Last Update\",\r\n            icon=\"mdi:update\",\r\n        )\r\n        self.device_class = SensorDeviceClass.TIMESTAMP\r\n        self.unit = None\r\n        self.state_class = None\r\n        self.entity_category = None\r\n        self.extra_state_attributes = None\r\n\r\n    @property\r\n    def is_mutable(self):\r\n        return False\r\n\r\n    @property\r\n    def str_state(self):\r\n        ts = super().state\r\n        return ts.astimezone(tz=None).isoformat() if ts else None\r\n\r\n    @property\r\n    def state(self):\r\n        return super().state\r\n\r\n\r\ndef create_instruments():\r\n    return [\r\n        Position(),\r\n        LastUpdate(),\r\n        TripData(attr=\"shortterm_current\", name=\"ShortTerm Trip Data\"),\r\n        TripData(attr=\"shortterm_reset\", name=\"ShortTerm Trip User Reset\"),\r\n        TripData(attr=\"longterm_current\", name=\"LongTerm Trip Data\"),\r\n        TripData(attr=\"longterm_reset\", name=\"LongTerm Trip User Reset\"),\r\n        Lock(),\r\n        Preheater(),\r\n        Sensor(\r\n            attr=\"model\",\r\n            name=\"Model\",\r\n            icon=\"mdi:car-info\",\r\n            entity_category=EntityCategory.DIAGNOSTIC,\r\n        ),\r\n        Sensor(\r\n            attr=\"mileage\",\r\n            name=\"Mileage\",\r\n            icon=\"mdi:counter\",\r\n            unit=UnitOfLength.KILOMETERS,\r\n            state_class=SensorStateClass.TOTAL_INCREASING,\r\n            device_class=SensorDeviceClass.DISTANCE,\r\n            entity_category=EntityCategory.DIAGNOSTIC,\r\n            suggested_display_precision=0,\r\n        ),\r\n        Sensor(\r\n            attr=\"service_adblue_distance\",\r\n            name=\"AdBlue range\",\r\n            icon=\"mdi:map-marker-distance\",\r\n            unit=UnitOfLength.KILOMETERS,\r\n            device_class=SensorDeviceClass.DISTANCE,\r\n            suggested_display_precision=0,\r\n        ),\r\n        Sensor(\r\n            attr=\"range\",\r\n            name=\"Range\",\r\n            icon=\"mdi:map-marker-distance\",\r\n            unit=UnitOfLength.KILOMETERS,\r\n            device_class=SensorDeviceClass.DISTANCE,\r\n            suggested_display_precision=0,\r\n        ),\r\n        Sensor(\r\n            attr=\"hybrid_range\",\r\n            name=\"hybrid Range\",\r\n            icon=\"mdi:map-marker-distance\",\r\n            unit=UnitOfLength.KILOMETERS,\r\n            device_class=SensorDeviceClass.DISTANCE,\r\n            suggested_display_precision=0,\r\n        ),\r\n        Sensor(\r\n            attr=\"service_inspection_time\",\r\n            name=\"Service inspection time\",\r\n            icon=\"mdi:room-service-outline\",\r\n            unit=UnitOfTime.DAYS,\r\n            entity_category=EntityCategory.DIAGNOSTIC,\r\n        ),\r\n        Sensor(\r\n            attr=\"service_inspection_distance\",\r\n            name=\"Service inspection distance\",\r\n            icon=\"mdi:room-service-outline\",\r\n            unit=UnitOfLength.KILOMETERS,\r\n            device_class=SensorDeviceClass.DISTANCE,\r\n            entity_category=EntityCategory.DIAGNOSTIC,\r\n            suggested_display_precision=0,\r\n        ),\r\n        Sensor(\r\n            attr=\"oil_change_time\",\r\n            name=\"Oil change time\",\r\n            icon=\"mdi:oil\",\r\n            unit=UnitOfTime.DAYS,\r\n            entity_category=EntityCategory.DIAGNOSTIC,\r\n        ),\r\n        Sensor(\r\n            attr=\"oil_change_distance\",\r\n            name=\"Oil change distance\",\r\n            icon=\"mdi:oil\",\r\n            unit=UnitOfLength.KILOMETERS,\r\n            device_class=SensorDeviceClass.DISTANCE,\r\n            entity_category=EntityCategory.DIAGNOSTIC,\r\n            suggested_display_precision=0,\r\n        ),\r\n        Sensor(\r\n            attr=\"oil_level\",\r\n            name=\"Oil level\",\r\n            icon=\"mdi:oil\",\r\n            unit=PERCENTAGE,\r\n        ),\r\n        Sensor(\r\n            attr=\"charging_state\",\r\n            name=\"Charging state\",\r\n            icon=\"mdi:car-battery\",\r\n        ),\r\n        Sensor(\r\n            attr=\"charging_mode\",\r\n            name=\"Charging mode\",\r\n        ),\r\n        Sensor(\r\n            attr=\"charging_type\",\r\n            name=\"Charging type\",\r\n        ),\r\n        Sensor(\r\n            attr=\"energy_flow\",\r\n            name=\"Energy flow\",\r\n        ),\r\n        Sensor(\r\n            attr=\"max_charge_current\",\r\n            name=\"Max charge current\",\r\n            icon=\"mdi:current-ac\",\r\n            unit=UnitOfElectricCurrent.AMPERE,\r\n            device_class=SensorDeviceClass.CURRENT,\r\n        ),\r\n        Sensor(\r\n            attr=\"primary_engine_type\",\r\n            name=\"Primary engine type\",\r\n            icon=\"mdi:engine\",\r\n            entity_category=EntityCategory.DIAGNOSTIC,\r\n        ),\r\n        Sensor(\r\n            attr=\"secondary_engine_type\",\r\n            name=\"Secondary engine type\",\r\n            icon=\"mdi:engine\",\r\n            entity_category=EntityCategory.DIAGNOSTIC,\r\n        ),\r\n        Sensor(\r\n            attr=\"primary_engine_range\",\r\n            name=\"Primary engine range\",\r\n            icon=\"mdi:map-marker-distance\",\r\n            unit=UnitOfLength.KILOMETERS,\r\n            device_class=SensorDeviceClass.DISTANCE,\r\n            suggested_display_precision=0,\r\n        ),\r\n        Sensor(\r\n            attr=\"secondary_engine_range\",\r\n            name=\"Secondary engine range\",\r\n            icon=\"mdi:map-marker-distance\",\r\n            unit=UnitOfLength.KILOMETERS,\r\n            device_class=SensorDeviceClass.DISTANCE,\r\n            suggested_display_precision=0,\r\n        ),\r\n        Sensor(\r\n            attr=\"primary_engine_range_percent\",\r\n            name=\"Primary engine Percent\",\r\n            icon=\"mdi:gauge\",\r\n            unit=PERCENTAGE,\r\n        ),\r\n        Sensor(\r\n            attr=\"car_type\",\r\n            name=\"Car Type\",\r\n            icon=\"mdi:car-info\",\r\n            entity_category=EntityCategory.DIAGNOSTIC,\r\n        ),\r\n        Sensor(\r\n            attr=\"secondary_engine_range_percent\",\r\n            name=\"Secondary engine Percent\",\r\n            icon=\"mdi:gauge\",\r\n            unit=PERCENTAGE,\r\n        ),\r\n        Sensor(\r\n            attr=\"charging_power\",\r\n            name=\"Charging power\",\r\n            icon=\"mdi:flash\",\r\n            unit=UnitOfPower.KILO_WATT,\r\n            device_class=SensorDeviceClass.POWER,\r\n        ),\r\n        Sensor(\r\n            attr=\"actual_charge_rate\",\r\n            name=\"Charging rate\",\r\n            icon=\"mdi:electron-framework\",\r\n        ),\r\n        Sensor(\r\n            attr=\"tank_level\",\r\n            name=\"Tank level\",\r\n            icon=\"mdi:gauge\",\r\n            unit=PERCENTAGE,\r\n        ),\r\n        Sensor(\r\n            attr=\"state_of_charge\",\r\n            name=\"State of charge\",\r\n            unit=PERCENTAGE,\r\n            device_class=SensorDeviceClass.BATTERY,\r\n        ),\r\n        Sensor(\r\n            attr=\"remaining_charging_time\",\r\n            name=\"Remaining charge time\",\r\n            icon=\"mdi:battery-charging\",\r\n        ),\r\n        Sensor(\r\n            attr=\"charging_complete_time\",\r\n            name=\"Charging Complete Time\",\r\n            icon=\"mdi:battery-charging\",\r\n            device_class=SensorDeviceClass.TIMESTAMP,\r\n        ),\r\n        Sensor(\r\n            attr=\"target_state_of_charge\",\r\n            name=\"Target State of charge\",\r\n            icon=\"mdi:ev-station\",\r\n            unit=PERCENTAGE,\r\n        ),\r\n        BinarySensor(\r\n            attr=\"plug_state\",\r\n            name=\"Plug state\",\r\n            icon=\"mdi:ev-plug-type1\",\r\n            device_class=BinarySensorDeviceClass.PLUG,\r\n        ),\r\n        BinarySensor(\r\n            attr=\"plug_lock_state\",\r\n            name=\"Plug Lock state\",\r\n            icon=\"mdi:ev-plug-type1\",\r\n            device_class=BinarySensorDeviceClass.LOCK,\r\n        ),\r\n        Sensor(\r\n            attr=\"external_power\",\r\n            name=\"External Power\",\r\n            icon=\"mdi:ev-station\",\r\n        ),\r\n        Sensor(\r\n            attr=\"plug_led_color\",\r\n            name=\"Plug LED Color\",\r\n            icon=\"mdi:ev-plug-type1\",\r\n            entity_category=EntityCategory.DIAGNOSTIC,\r\n        ),\r\n        Sensor(\r\n            attr=\"doors_trunk_status\",\r\n            name=\"Doors/trunk state\",\r\n            icon=\"mdi:car-door\",\r\n        ),\r\n        Sensor(\r\n            attr=\"climatisation_state\",\r\n            name=\"Climatisation state\",\r\n            icon=\"mdi:air-conditioner\",\r\n        ),\r\n        Sensor(\r\n            attr=\"outdoor_temperature\",\r\n            name=\"Outdoor Temperature\",\r\n            icon=\"mdi:temperature-celsius\",\r\n            unit=UnitOfTemperature.CELSIUS,\r\n            device_class=SensorDeviceClass.TEMPERATURE,\r\n        ),\r\n        Sensor(\r\n            attr=\"park_time\",\r\n            name=\"Park Time\",\r\n            icon=\"mdi:car-clock\",\r\n            device_class=SensorDeviceClass.TIMESTAMP,\r\n        ),\r\n        Sensor(\r\n            attr=\"remaining_climatisation_time\",\r\n            name=\"Remaining Climatisation Time\",\r\n            icon=\"mdi:fan-clock\",\r\n            unit=UnitOfTime.MINUTES,\r\n        ),\r\n        BinarySensor(\r\n            attr=\"glass_surface_heating\",\r\n            name=\"Glass Surface Heating\",\r\n            icon=\"mdi:car-defrost-front\",\r\n            device_class=BinarySensorDeviceClass.RUNNING,\r\n        ),\r\n        Sensor(\r\n            attr=\"preheater_duration\",\r\n            name=\"Preheater runtime\",\r\n            icon=\"mdi:clock\",\r\n            unit=UnitOfTime.MINUTES,\r\n        ),\r\n        Sensor(\r\n            attr=\"preheater_remaining\",\r\n            name=\"Preheater remaining\",\r\n            icon=\"mdi:clock\",\r\n            unit=UnitOfTime.MINUTES,\r\n        ),\r\n        BinarySensor(\r\n            attr=\"sun_roof\",\r\n            name=\"Sun roof\",\r\n            device_class=BinarySensorDeviceClass.WINDOW,\r\n        ),\r\n        BinarySensor(\r\n            attr=\"roof_cover\",\r\n            name=\"Roof Cover\",\r\n            device_class=BinarySensorDeviceClass.WINDOW,\r\n        ),\r\n        BinarySensor(\r\n            attr=\"parking_light\",\r\n            name=\"Parking light\",\r\n            device_class=BinarySensorDeviceClass.SAFETY,\r\n            icon=\"mdi:lightbulb\",\r\n            entity_category=EntityCategory.DIAGNOSTIC,\r\n        ),\r\n        BinarySensor(\r\n            attr=\"any_window_open\",\r\n            name=\"Windows\",\r\n            device_class=BinarySensorDeviceClass.WINDOW,\r\n        ),\r\n        BinarySensor(\r\n            attr=\"any_door_unlocked\",\r\n            name=\"Doors lock\",\r\n            device_class=BinarySensorDeviceClass.LOCK,\r\n        ),\r\n        BinarySensor(\r\n            attr=\"any_door_open\",\r\n            name=\"Doors\",\r\n            device_class=BinarySensorDeviceClass.DOOR,\r\n        ),\r\n        BinarySensor(\r\n            attr=\"trunk_unlocked\",\r\n            name=\"Trunk lock\",\r\n            device_class=BinarySensorDeviceClass.LOCK,\r\n        ),\r\n        BinarySensor(\r\n            attr=\"trunk_open\",\r\n            name=\"Trunk\",\r\n            device_class=BinarySensorDeviceClass.DOOR,\r\n        ),\r\n        BinarySensor(\r\n            attr=\"hood_open\",\r\n            name=\"Hood\",\r\n            device_class=BinarySensorDeviceClass.DOOR,\r\n        ),\r\n        BinarySensor(\r\n            attr=\"left_front_door_open\",\r\n            name=\"Left front door\",\r\n            device_class=BinarySensorDeviceClass.DOOR,\r\n            entity_category=EntityCategory.DIAGNOSTIC,\r\n        ),\r\n        BinarySensor(\r\n            attr=\"right_front_door_open\",\r\n            name=\"Right front door\",\r\n            device_class=BinarySensorDeviceClass.DOOR,\r\n            entity_category=EntityCategory.DIAGNOSTIC,\r\n        ),\r\n        BinarySensor(\r\n            attr=\"left_rear_door_open\",\r\n            name=\"Left rear door\",\r\n            device_class=BinarySensorDeviceClass.DOOR,\r\n            entity_category=EntityCategory.DIAGNOSTIC,\r\n        ),\r\n        BinarySensor(\r\n            attr=\"right_rear_door_open\",\r\n            name=\"Right rear door\",\r\n            device_class=BinarySensorDeviceClass.DOOR,\r\n            entity_category=EntityCategory.DIAGNOSTIC,\r\n        ),\r\n        BinarySensor(\r\n            attr=\"left_front_window_open\",\r\n            name=\"Left front window\",\r\n            device_class=BinarySensorDeviceClass.WINDOW,\r\n            entity_category=EntityCategory.DIAGNOSTIC,\r\n        ),\r\n        BinarySensor(\r\n            attr=\"right_front_window_open\",\r\n            name=\"Right front window\",\r\n            device_class=BinarySensorDeviceClass.WINDOW,\r\n            entity_category=EntityCategory.DIAGNOSTIC,\r\n        ),\r\n        BinarySensor(\r\n            attr=\"left_rear_window_open\",\r\n            name=\"Left rear window\",\r\n            device_class=BinarySensorDeviceClass.WINDOW,\r\n            entity_category=EntityCategory.DIAGNOSTIC,\r\n        ),\r\n        BinarySensor(\r\n            attr=\"right_rear_window_open\",\r\n            name=\"Right rear window\",\r\n            device_class=BinarySensorDeviceClass.WINDOW,\r\n            entity_category=EntityCategory.DIAGNOSTIC,\r\n        ),\r\n        BinarySensor(\r\n            attr=\"braking_status\",\r\n            name=\"Braking status\",\r\n            device_class=BinarySensorDeviceClass.SAFETY,\r\n            icon=\"mdi:car-brake-abs\",\r\n        ),\r\n        BinarySensor(\r\n            attr=\"oil_level_binary\",\r\n            name=\"Oil Level Binary\",\r\n            icon=\"mdi:oil\",\r\n            device_class=BinarySensorDeviceClass.PROBLEM,\r\n            entity_category=EntityCategory.DIAGNOSTIC,\r\n        ),\r\n        BinarySensor(\r\n            attr=\"is_moving\",\r\n            name=\"Is moving\",\r\n            icon=\"mdi:motion-outline\",\r\n            device_class=BinarySensorDeviceClass.MOVING,\r\n            entity_category=EntityCategory.DIAGNOSTIC,\r\n        ),\r\n    ]\r\n\r\n\r\nclass Dashboard:\r\n    def __init__(self, connection, vehicle, **config):\r\n        self.instruments = [\r\n            instrument\r\n            for instrument in create_instruments()\r\n            if instrument.setup(connection, vehicle, **config)\r\n        ]\r\n"
  },
  {
    "path": "custom_components/audiconnect/device_tracker.py",
    "content": "\"\"\"Support for tracking an Audi.\"\"\"\n\nimport logging\nfrom typing import Any\n\nfrom homeassistant.components.device_tracker import SourceType\nfrom homeassistant.components.device_tracker.config_entry import TrackerEntity\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.const import CONF_USERNAME\nfrom homeassistant.core import HomeAssistant, callback\nfrom homeassistant.helpers.dispatcher import (\n    async_dispatcher_connect,\n    async_dispatcher_send,\n)\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\n\nfrom .const import DOMAIN, TRACKER_UPDATE\n\n_LOGGER = logging.getLogger(__name__)\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant,\n    config_entry: ConfigEntry,\n    async_add_entities: AddEntitiesCallback,\n) -> None:\n    \"\"\"Set up Audi device tracker entities.\"\"\"\n    account = config_entry.data.get(CONF_USERNAME)\n    audi_data = hass.data.get(DOMAIN, {}).get(account)\n\n    if not audi_data:\n        _LOGGER.error(\"Audi Connect data not found for account: %s\", account)\n        return\n\n    if \"devices\" not in hass.data[DOMAIN]:\n        hass.data[DOMAIN][\"devices\"] = set()\n\n    async def add_vehicle_tracker(instrument: Any) -> None:\n        \"\"\"Add a tracker entity when data is received.\"\"\"\n        unique_id = instrument.full_name\n        if unique_id in hass.data[DOMAIN][\"devices\"]:\n            return\n\n        _LOGGER.debug(\"Adding new AudiDeviceTracker: %s\", unique_id)\n        hass.data[DOMAIN][\"devices\"].add(unique_id)\n        async_add_entities([AudiDeviceTracker(config_entry, instrument)], True)\n\n    async_dispatcher_connect(hass, TRACKER_UPDATE, add_vehicle_tracker)\n\n    for config_vehicle in getattr(audi_data, \"config_vehicles\", []):\n        for tracker in getattr(config_vehicle, \"device_trackers\", []):\n            async_dispatcher_send(hass, TRACKER_UPDATE, tracker)\n\n\nclass AudiDeviceTracker(TrackerEntity):\n    \"\"\"Representation of an Audi device tracker.\"\"\"\n\n    _attr_icon = \"mdi:car\"\n    _attr_should_poll = False\n    _attr_source_type = SourceType.GPS\n\n    def __init__(self, config_entry: ConfigEntry, instrument: Any) -> None:\n        \"\"\"Initialize tracker.\"\"\"\n        self._instrument = instrument\n        self._config_entry = config_entry\n        self._attr_unique_id = instrument.full_name\n        self._latitude = None\n        self._longitude = None\n        self._update_state_from_instrument()\n\n        self._attr_device_info = {\n            \"identifiers\": {(DOMAIN, instrument.vehicle_name)},\n            \"manufacturer\": \"Audi\",\n            \"name\": instrument.vehicle_name,\n            \"model\": getattr(instrument, \"vehicle_model\", None),\n        }\n\n    def _update_state_from_instrument(self) -> None:\n        \"\"\"Update internal lat/lon state.\"\"\"\n        state = getattr(self._instrument, \"state\", None)\n        if isinstance(state, (list, tuple)) and len(state) >= 2:\n            try:\n                self._latitude = float(state[0])\n                self._longitude = float(state[1])\n            except (ValueError, TypeError):\n                _LOGGER.warning(\"Invalid GPS coordinates: %s\", state)\n                self._latitude = self._longitude = None\n        else:\n            _LOGGER.debug(\"Missing or invalid state for: %s\", self._attr_unique_id)\n\n    @property\n    def latitude(self) -> float | None:\n        return self._latitude\n\n    @property\n    def longitude(self) -> float | None:\n        return self._longitude\n\n    @property\n    def name(self) -> str:\n        return f\"{self._instrument.vehicle_name} {self._instrument.name}\"\n\n    @property\n    def extra_state_attributes(self) -> dict[str, Any]:\n        attrs = dict(getattr(self._instrument, \"attributes\", {}))\n        attrs.update(\n            {\n                \"model\": f\"{getattr(self._instrument, 'vehicle_model', 'Unknown')}/{self._instrument.vehicle_name}\",\n                \"model_year\": getattr(self._instrument, \"vehicle_model_year\", None),\n                \"model_family\": getattr(self._instrument, \"vehicle_model_family\", None),\n                \"csid\": getattr(self._instrument, \"vehicle_csid\", None),\n                \"vin\": getattr(self._instrument, \"vehicle_vin\", None),\n            }\n        )\n        return {k: v for k, v in attrs.items() if v is not None}\n\n    async def async_added_to_hass(self) -> None:\n        \"\"\"Register update dispatcher.\"\"\"\n        await super().async_added_to_hass()\n        self.async_on_remove(\n            async_dispatcher_connect(\n                self.hass, TRACKER_UPDATE, self._async_receive_data\n            )\n        )\n        _LOGGER.debug(\"%s registered for updates\", self.entity_id)\n\n    @callback\n    def _async_receive_data(self, instrument: Any) -> None:\n        \"\"\"Receive new tracker data.\"\"\"\n        if instrument.full_name != self._attr_unique_id:\n            return\n\n        _LOGGER.debug(\"Update received for %s\", self.entity_id)\n        self._instrument = instrument\n        self._update_state_from_instrument()\n        self.async_write_ha_state()\n"
  },
  {
    "path": "custom_components/audiconnect/lock.py",
    "content": "\"\"\"Support for Audi Connect locks.\"\"\"\n\nimport logging\n\nfrom homeassistant.components.lock import LockEntity\nfrom homeassistant.const import CONF_USERNAME\n\nfrom .audi_entity import AudiEntity\nfrom .const import DOMAIN\n\n_LOGGER = logging.getLogger(__name__)\n\n\nasync def async_setup_platform(hass, config, async_add_entities, discovery_info=None):\n    \"\"\"Old way.\"\"\"\n\n\nasync def async_setup_entry(hass, config_entry, async_add_entities):\n    sensors = []\n    account = config_entry.data.get(CONF_USERNAME)\n    audiData = hass.data[DOMAIN][account]\n\n    for config_vehicle in audiData.config_vehicles:\n        for lock in config_vehicle.locks:\n            sensors.append(AudiLock(config_vehicle, lock))\n\n    async_add_entities(sensors)\n\n\nclass AudiLock(AudiEntity, LockEntity):\n    \"\"\"Represents a car lock.\"\"\"\n\n    @property\n    def is_locked(self):\n        \"\"\"Return true if lock is locked.\"\"\"\n        return self._instrument.is_locked\n\n    async def async_lock(self, **kwargs):\n        \"\"\"Lock the car.\"\"\"\n        await self._instrument.lock()\n\n    async def async_unlock(self, **kwargs):\n        \"\"\"Unlock the car.\"\"\"\n        await self._instrument.unlock()\n"
  },
  {
    "path": "custom_components/audiconnect/manifest.json",
    "content": "{\n  \"domain\": \"audiconnect\",\n  \"name\": \"Audi Connect\",\n  \"codeowners\": [\"@audiconnect\"],\n  \"config_flow\": true,\n  \"documentation\": \"https://github.com/audiconnect/audi_connect_ha\",\n  \"integration_type\": \"hub\",\n  \"iot_class\": \"cloud_polling\",\n  \"issue_tracker\": \"https://github.com/audiconnect/audi_connect_ha/issues\",\n  \"loggers\": [\"audiconnect\"],\n  \"requirements\": [\"beautifulsoup4\"],\n  \"version\": \"1.19.1\"\n}\n"
  },
  {
    "path": "custom_components/audiconnect/sensor.py",
    "content": "\"\"\"Support for Audi Connect sensors.\"\"\"\n\nimport logging\n\nfrom homeassistant.components.sensor import SensorEntity\nfrom homeassistant.const import CONF_USERNAME\n\nfrom .audi_entity import AudiEntity\nfrom .const import DOMAIN\n\n_LOGGER = logging.getLogger(__name__)\n\n\nasync def async_setup_platform(hass, config, async_add_entities, discovery_info=None):\n    \"\"\"Old way of setting up platform.\"\"\"\n\n\nasync def async_setup_entry(hass, config_entry, async_add_entities):\n    \"\"\"Set up Audi sensors from a config entry.\"\"\"\n    sensors = []\n\n    account = config_entry.data.get(CONF_USERNAME)\n    audiData = hass.data[DOMAIN][account]\n\n    for config_vehicle in audiData.config_vehicles:\n        for sensor in config_vehicle.sensors:\n            sensors.append(AudiSensor(config_vehicle, sensor))\n\n    async_add_entities(sensors, True)\n\n\nclass AudiSensor(AudiEntity, SensorEntity):\n    \"\"\"Representation of an Audi sensor.\"\"\"\n\n    @property\n    def native_value(self):\n        \"\"\"Return the native value.\"\"\"\n        return self._instrument.state\n\n    @property\n    def native_unit_of_measurement(self):\n        \"\"\"Return the native unit of measurement.\"\"\"\n        return self._instrument.unit\n\n    @property\n    def device_class(self):\n        \"\"\"Return the device_class.\"\"\"\n        return self._instrument.device_class\n\n    @property\n    def state_class(self):\n        \"\"\"Return the state_class.\"\"\"\n        return self._instrument.state_class\n\n    @property\n    def entity_category(self):\n        \"\"\"Return the entity_category.\"\"\"\n        return self._instrument.entity_category\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Return additional state attributes.\"\"\"\n        return self._instrument.extra_state_attributes\n\n    @property\n    def suggested_display_precision(self):\n        \"\"\"Return the suggested number of decimal digits for display.\"\"\"\n        return self._instrument.suggested_display_precision\n"
  },
  {
    "path": "custom_components/audiconnect/services.yaml",
    "content": "---\n# Describes the format for available services for audiconnect\n\nrefresh_vehicle_data:\n  fields:\n    vin:\n      required: true\n      example: WBANXXXXXX1234567\n      selector:\n        text:\n\nexecute_vehicle_action:\n  fields:\n    vin:\n      required: true\n      example: WBANXXXXXX1234567\n      selector:\n        text:\n    action:\n      required: true\n      example: \"lock\"\n      selector:\n        select:\n          translation_key: vehicle_actions\n          options:\n            - lock\n            - unlock\n            - start_climatisation\n            - stop_climatisation\n            - start_charger\n            - start_timed_charger\n            - stop_charger\n            - start_preheater\n            - stop_preheater\n            - start_window_heating\n            - stop_window_heating\n\nstart_climate_control:\n  fields:\n    vin:\n      required: true\n      example: WBANXXXXXX1234567\n      selector:\n        text:\n    temp_f:\n      selector:\n        number:\n          min: 59\n          max: 85\n    temp_c:\n      selector:\n        number:\n          min: 15\n          max: 30\n    glass_heating:\n      selector:\n        boolean:\n    seat_fl:\n      selector:\n        boolean:\n    seat_fr:\n      selector:\n        boolean:\n    seat_rl:\n      selector:\n        boolean:\n    seat_rr:\n      selector:\n        boolean:\n    climatisation_at_unlock:\n      selector:\n        boolean:\n    climatisation_mode:\n      example: \"comfort\"\n      selector:\n        select:\n          options:\n            - comfort\n            - economy\n\nstart_auxiliary_heating:\n  fields:\n    vin:\n      required: true\n      example: WBANXXXXXX1234567\n      selector:\n        text:\n    duration:\n      selector:\n        number:\n          min: 10\n          max: 60\n          step: 10\n          unit_of_measurement: \"minutes\"\n\nset_target_soc:\n  fields:\n    vin:\n      required: true\n      example: WBANXXXXXX1234567\n      selector:\n        text:\n    target_soc:\n      required: true\n      example: 80\n      selector:\n        number:\n          min: 20\n          max: 100\n          step: 5\n          unit_of_measurement: \"%\"\n"
  },
  {
    "path": "custom_components/audiconnect/strings.json",
    "content": "{\n  \"config\": {\n    \"abort\": {\n      \"invalid_credentials\": \"Invalid credentials\",\n      \"user_already_configured\": \"Account has already been configured\"\n    },\n    \"create_entry\": {},\n    \"error\": {\n      \"invalid_credentials\": \"Invalid credentials\",\n      \"invalid_username\": \"Invalid username\",\n      \"unexpected\": \"Unexpected error communicating with Audi Connect server\",\n      \"user_already_configured\": \"Account has already been configured\"\n    },\n    \"step\": {\n      \"user\": {\n        \"data\": {\n          \"password\": \"Password\",\n          \"username\": \"Username\",\n          \"spin\": \"S-PIN\",\n          \"region\": \"Region\",\n          \"scan_interval\": \"Scan interval\",\n          \"api_level\": \"API Level\"\n        },\n        \"title\": \"Audi Connect Account Info\",\n        \"data_description\": {\n          \"api_level\": \"For Audi vehicles, the API request data structure varies by model. Newer vehicles use an updated data structure compared to older models. Adjusting the API Level ensures that the system automatically applies the correct data structure for each specific vehicle. This can be updated from the CONFIGURE menu later, if needed.\"\n        }\n      }\n    }\n  },\n  \"options\": {\n    \"step\": {\n      \"init\": {\n        \"data\": {\n          \"scan_initial\": \"Cloud Update at Startup\",\n          \"scan_active\": \"Active Polling at Scan Interval\",\n          \"scan_interval\": \"Scan Interval\",\n          \"api_level\": \"API Level\"\n        },\n        \"title\": \"Audi Connect Options\",\n        \"data_description\": {\n          \"scan_initial\": \"Perform a cloud update immediately upon startup.\",\n          \"scan_active\": \"Perform a cloud update at the set scan interval.\",\n          \"scan_interval\": \"Minutes between active polling. If 'Active Polling at Scan Interval' is off, this value will have no impact.\",\n          \"api_level\": \"For Audi vehicles, the API request data structure varies by model. Newer vehicles use an updated data structure compared to older models. Adjusting the API Level ensures that the system automatically applies the correct data structure for each specific vehicle.\"\n        }\n      }\n    }\n  },\n  \"selector\": {\n    \"vehicle_actions\": {\n      \"options\": {\n        \"lock\": \"Lock\",\n        \"unlock\": \"Unlock\",\n        \"start_climatisation\": \"Start Climatisation (Legacy)\",\n        \"stop_climatisation\": \"Stop Climatisation\",\n        \"start_charger\": \"Start Charger\",\n        \"start_timed_charger\": \"Start timed Charger\",\n        \"stop_charger\": \"Stop Charger\",\n        \"start_preheater\": \"Start Preheater (Legacy)\",\n        \"stop_preheater\": \"Stop Preheater\",\n        \"start_window_heating\": \"Start Window heating\",\n        \"stop_window_heating\": \"Stop Windows heating\"\n      }\n    }\n  },\n  \"services\": {\n    \"refresh_vehicle_data\": {\n      \"name\": \"Refresh Vehicle Data\",\n      \"description\": \"Requests an update of the vehicle state directly, as opposed to the normal update mechanism which only retrieves data from the cloud.\",\n      \"fields\": {\n        \"vin\": {\n          \"name\": \"VIN\",\n          \"description\": \"The Vehicle Identification Number (VIN) of the Audi vehicle. This should be a 17-character identifier unique to each vehicle.\"\n        }\n      }\n    },\n    \"execute_vehicle_action\": {\n      \"name\": \"Execute Vehicle Action\",\n      \"description\": \"Performs various actions on the vehicle.\",\n      \"fields\": {\n        \"vin\": {\n          \"name\": \"VIN\",\n          \"description\": \"The Vehicle Identification Number (VIN) of the Audi vehicle. This should be a 17-character identifier unique to each vehicle.\"\n        },\n        \"action\": {\n          \"name\": \"Action\",\n          \"description\": \"The specific action to perform on the vehicle. Note that available actions may vary based on the vehicle.\",\n          \"example\": \"lock\"\n        }\n      }\n    },\n    \"start_climate_control\": {\n      \"name\": \"Start Climate Control\",\n      \"description\": \"Start the climate control with options for temperature, glass surface heating, and auto seat comfort.\",\n      \"fields\": {\n        \"vin\": {\n          \"name\": \"VIN\",\n          \"description\": \"The Vehicle Identification Number (VIN) of the Audi vehicle. This should be a 17-character identifier unique to each vehicle.\"\n        },\n        \"temp_f\": {\n          \"name\": \"Target Temperature (Fahrenheit)\",\n          \"description\": \"(Optional) Set temperature in °F. Defaults to 70°F if not provided. Overrides 'temp_c'.\"\n        },\n        \"temp_c\": {\n          \"name\": \"Target Temperature (Celsius)\",\n          \"description\": \"(Optional) Set temperature in °C. Defaults to 21°C if not provided. Overridden if 'temp_f' is provided.\"\n        },\n        \"glass_heating\": {\n          \"name\": \"Glass Surface Heating\",\n          \"description\": \"(Optional) Enable or disable glass surface heating.\"\n        },\n        \"seat_fl\": {\n          \"name\": \"Auto Seat Comfort: Front-Left\",\n          \"description\": \"(Optional) Enable or disable Auto Seat Comfort for the front-left seat.\"\n        },\n        \"seat_fr\": {\n          \"name\": \"Auto Seat Comfort: Front-Right\",\n          \"description\": \"(Optional) Enable or disable Auto Seat Comfort for the front-right seat.\"\n        },\n        \"seat_rl\": {\n          \"name\": \"Auto Seat Comfort: Rear-Left\",\n          \"description\": \"(Optional) Enable or disable Auto Seat Comfort for the rear-left seat.\"\n        },\n        \"seat_rr\": {\n          \"name\": \"Auto Seat Comfort: Rear-Right\",\n          \"description\": \"(Optional) Enable or disable Auto Seat Comfort for the rear-right seat.\"\n        },\n        \"climatisation_at_unlock\": {\n          \"name\": \"Climatisation at Unlock\",\n          \"description\": \"(Optional) Enable climate control to continue when vehicle is unlocked.\"\n        },\n        \"climatisation_mode\": {\n          \"name\": \"Climatisation Mode\",\n          \"description\": \"(Optional) Maximum comfort (Comfort) or energy-saving (Economy). Default is Comfort.\"\n        }\n      }\n    },\n    \"refresh_cloud_data\": {\n      \"name\": \"Refresh Cloud Data\",\n      \"description\": \"Retrieves current cloud data without triggering a vehicle refresh. Data may be outdated if the vehicle has not checked in recently.\"\n    },\n    \"start_auxiliary_heating\": {\n      \"name\": \"Start Auxiliary Heating\",\n      \"description\": \"Start auxiliary heating the vehicle, with option for duration.\",\n      \"fields\": {\n        \"vin\": {\n          \"name\": \"VIN\",\n          \"description\": \"The Vehicle Identification Number (VIN) of the Audi vehicle. This should be a 17-character identifier unique to each vehicle.\"\n        },\n        \"duration\": {\n          \"name\": \"Duration\",\n          \"description\": \"The number of minutes the auxiliary heater should run before turning off. Default is 20 minutes if not provided.\"\n        }\n      }\n    },\n    \"set_target_soc\": {\n      \"name\": \"Set Target State of Charge\",\n      \"description\": \"Set the target state of charge (battery %) for the vehicle.\",\n      \"fields\": {\n        \"vin\": {\n          \"name\": \"VIN\",\n          \"description\": \"The Vehicle Identification Number (VIN) of the Audi vehicle. This should be a 17-character identifier unique to each vehicle.\"\n        },\n        \"target_soc\": {\n          \"name\": \"Target State of Charge\",\n          \"description\": \"Target state of charge percentage (20-100%).\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "custom_components/audiconnect/switch.py",
    "content": "\"\"\"Support for Audi Connect switches\"\"\"\n\nimport logging\n\nfrom homeassistant.helpers.entity import ToggleEntity\nfrom homeassistant.const import CONF_USERNAME\n\nfrom .audi_entity import AudiEntity\nfrom .const import DOMAIN\n\n_LOGGER = logging.getLogger(__name__)\n\n\nasync def async_setup_platform(hass, config, async_add_entities, discovery_info=None):\n    \"\"\"Old way.\"\"\"\n\n\nasync def async_setup_entry(hass, config_entry, async_add_entities):\n    sensors = []\n    account = config_entry.data.get(CONF_USERNAME)\n    audiData = hass.data[DOMAIN][account]\n\n    for config_vehicle in audiData.config_vehicles:\n        for switch in config_vehicle.switches:\n            sensors.append(AudiSwitch(config_vehicle, switch))\n\n    async_add_entities(sensors)\n\n\nclass AudiSwitch(AudiEntity, ToggleEntity):\n    \"\"\"Representation of a Audi switch.\"\"\"\n\n    @property\n    def is_on(self):\n        \"\"\"Return true if switch is on.\"\"\"\n        return self._instrument.state\n\n    async def async_turn_on(self, **kwargs):\n        \"\"\"Turn the switch on.\"\"\"\n        await self._instrument.turn_on()\n\n    async def async_turn_off(self, **kwargs):\n        \"\"\"Turn the switch off.\"\"\"\n        await self._instrument.turn_off()\n"
  },
  {
    "path": "custom_components/audiconnect/translations/de.json",
    "content": "{\n  \"config\": {\n    \"abort\": {\n      \"invalid_credentials\": \"Ung\\u00fcltige Anmeldeinformationen\",\n      \"user_already_configured\": \"Konto wurde bereits konfiguriert\"\n    },\n    \"create_entry\": {},\n    \"error\": {\n      \"invalid_credentials\": \"Ung\\u00fcltige Anmeldeinformationen\",\n      \"invalid_username\": \"Ung\\u00fcltiger Benutzername\",\n      \"unexpected\": \"Unerwarteter Fehler bei der Kommunikation mit dem Audi Connect Server\",\n      \"user_already_configured\": \"Konto wurde bereits konfiguriert\"\n    },\n    \"step\": {\n      \"user\": {\n        \"data\": {\n          \"password\": \"Passwort\",\n          \"username\": \"Benutzername\",\n          \"spin\": \"S-PIN\",\n          \"region\": \"Region\",\n          \"scan_interval\": \"Abfrageintervall\",\n          \"api_level\": \"API-Level\"\n        },\n        \"title\": \"Audi Connect Kontoinformationen\",\n        \"data_description\": {\n          \"api_level\": \"Die Datenstruktur des API-Requests variiert je nach Audi-Modell. Neuere Fahrzeuge verwenden eine aktualisierte Struktur im Vergleich zu älteren Modellen. Durch die Anpassung des API-Levels wird sichergestellt, dass das Fahrzeug die korrekte, fahrzeugspezifische Datenstruktur nutzt. Diese Einstellung kann später unter „KONFIGURATION“ geändert werden.\"\n        }\n      }\n    }\n  },\n  \"options\": {\n    \"step\": {\n      \"init\": {\n        \"data\": {\n          \"scan_initial\": \"Cloud-Update beim Start\",\n          \"scan_active\": \"Aktive Abfrage im Scanintervall\",\n          \"scan_interval\": \"Abfrageintervall\",\n          \"api_level\": \"API-Level\"\n        },\n        \"title\": \"Audi Connect-Optionen\",\n        \"data_description\": {\n          \"scan_initial\": \"Führen Sie sofort nach dem Start ein Cloud-Update durch.\",\n          \"scan_active\": \"Führen Sie im festgelegten Scanintervall ein Cloud-Update durch.\",\n          \"scan_interval\": \"Minuten zwischen aktiven Abfragen. Wenn „Aktive Abfrage im Scanintervall“ deaktiviert ist, hat dieser Wert keine Auswirkung.\",\n          \"api_level\": \"Die Datenstruktur des API-Requests variiert je nach Audi-Modell. Neuere Fahrzeuge verwenden eine aktualisierte Struktur im Vergleich zu älteren Modellen. Durch die Anpassung des API-Levels wird sichergestellt, dass das Fahrzeug die korrekte, fahrzeugspezifische Datenstruktur nutzt. Diese Einstellung kann später unter „KONFIGURATION“ geändert werden.\"\n        }\n      }\n    }\n  },\n  \"selector\": {\n    \"vehicle_actions\": {\n      \"options\": {\n        \"lock\": \"Sperren\",\n        \"unlock\": \"Freischalten\",\n        \"start_climatisation\": \"Klimatisierung starten (Legacy)\",\n        \"stop_climatisation\": \"Schluss mit der Klimatisierung\",\n        \"start_charger\": \"Ladegerät starten\",\n        \"start_timed_charger\": \"Starten Sie das zeitgesteuerte Ladegerät\",\n        \"stop_charger\": \"Stoppen Sie das Ladegerät\",\n        \"start_preheater\": \"Vorwärmer starten\",\n        \"stop_preheater\": \"Stoppen Sie den Vorwärmer\",\n        \"start_window_heating\": \"Fensterheizung starten\",\n        \"stop_window_heating\": \"Stoppen Sie die Fensterheizung\",\n        \"is_moving\": \"In Bewegung\"\n      }\n    }\n  },\n  \"services\": {\n    \"refresh_vehicle_data\": {\n      \"name\": \"Fahrzeugdaten aktualisieren\",\n      \"description\": \"Fordert direkt eine Aktualisierung des Fahrzeugstatus an, im Gegensatz zum normalen Aktualisierungsmechanismus, der nur Daten aus der Cloud abruft.\",\n      \"fields\": {\n        \"vin\": {\n          \"name\": \"VIN\",\n          \"description\": \"Die Fahrzeugidentifikationsnummer (VIN) des Audi-Fahrzeugs. Dies sollte eine 17-stellige Kennung sein, die für jedes Fahrzeug eindeutig ist.\"\n        }\n      }\n    },\n    \"execute_vehicle_action\": {\n      \"name\": \"Fahrzeugaktionen ausfuhren\",\n      \"description\": \"Führt verschiedene Aktionen am Fahrzeug aus.\",\n      \"fields\": {\n        \"vin\": {\n          \"name\": \"VIN\",\n          \"description\": \"Die Fahrzeugidentifikationsnummer (VIN) des Audi-Fahrzeugs. Dies sollte eine 17-stellige Kennung sein, die für jedes Fahrzeug eindeutig ist.\"\n        },\n        \"action\": {\n          \"name\": \"Aktion\",\n          \"description\": \"Die spezifische Aktion, die am Fahrzeug ausgeführt werden soll. Beachten Sie, dass die verfügbaren Aktionen je nach Fahrzeug variieren können.\",\n          \"example\": \"lock\"\n        }\n      }\n    },\n    \"start_climate_control\": {\n      \"name\": \"Starten Sie die Klimatisierung\",\n      \"description\": \"Starten Sie die Klimaanlage mit Optionen für Temperatur, Glasflächenheizung und automatischen Sitzkomfort.\",\n      \"fields\": {\n        \"vin\": {\n          \"name\": \"VIN\",\n          \"description\": \"Die Fahrzeugidentifikationsnummer (VIN) des Audi-Fahrzeugs. Dies sollte eine 17-stellige Kennung sein, die für jedes Fahrzeug eindeutig ist.\"\n        },\n        \"temp_f\": {\n          \"name\": \"Zieltemperatur (Fahrenheit)\",\n          \"description\": \"(Optional) Stellen Sie die Temperatur in °F ein. Standardmäßig 70 °F, sofern nicht angegeben. Überschreibt 'temp_c'.\"\n        },\n        \"temp_c\": {\n          \"name\": \"Zieltemperatur (Celsius)\",\n          \"description\": \"(Optional) Stellen Sie die Temperatur in °C ein. Standardmäßig 21 °C, sofern nicht angegeben. Wird überschrieben, wenn „temp_f“ bereitgestellt wird.\"\n        },\n        \"glass_heating\": {\n          \"name\": \"Glasflächenheizung\",\n          \"description\": \"(Optional) Aktivieren oder deaktivieren Sie die Glasflächenheizung.\"\n        },\n        \"seat_fl\": {\n          \"name\": \"Automatischer Sitzkomfort: Vorne links\",\n          \"description\": \"(Optional) Aktivieren oder deaktivieren Sie den automatischen Sitzkomfort für den vorderen linken Sitz.\"\n        },\n        \"seat_fr\": {\n          \"name\": \"Automatischer Sitzkomfort: Vorne rechts\",\n          \"description\": \"(Optional) Aktivieren oder deaktivieren Sie den automatischen Sitzkomfort für den Vordersitz rechts.\"\n        },\n        \"seat_rl\": {\n          \"name\": \"Automatischer Sitzkomfort: Hinten links\",\n          \"description\": \"(Optional) Aktivieren oder deaktivieren Sie den automatischen Sitzkomfort für den linken Rücksitz.\"\n        },\n        \"seat_rr\": {\n          \"name\": \"Automatischer Sitzkomfort: Hinten rechts\",\n          \"description\": \"(Optional) Aktivieren oder deaktivieren Sie den automatischen Sitzkomfort für den rechten Rücksitz.\"\n        }\n      }\n    },\n    \"refresh_cloud_data\": {\n      \"name\": \"Cloud-Daten aktualisieren\",\n      \"description\": \"Ruft aktuelle Cloud-Daten ab, ohne eine Fahrzeugaktualisierung auszulösen. Die Daten sind möglicherweise veraltet, wenn das Fahrzeug nicht kürzlich eingecheckt wurde.\"\n    }\n  }\n}\n"
  },
  {
    "path": "custom_components/audiconnect/translations/en.json",
    "content": "{\n  \"config\": {\n    \"abort\": {\n      \"invalid_credentials\": \"Invalid credentials\",\n      \"user_already_configured\": \"Account has already been configured\"\n    },\n    \"create_entry\": {},\n    \"error\": {\n      \"invalid_credentials\": \"Invalid credentials\",\n      \"invalid_username\": \"Invalid username\",\n      \"unexpected\": \"Unexpected error communicating with Audi Connect server\",\n      \"user_already_configured\": \"Account has already been configured\"\n    },\n    \"step\": {\n      \"user\": {\n        \"data\": {\n          \"password\": \"Password\",\n          \"username\": \"Username\",\n          \"spin\": \"S-PIN\",\n          \"region\": \"Region\",\n          \"scan_interval\": \"Scan interval\",\n          \"api_level\": \"API Level\"\n        },\n        \"title\": \"Audi Connect Account Info\",\n        \"data_description\": {\n          \"api_level\": \"For Audi vehicles, the API request data structure varies by model. Newer vehicles use an updated data structure compared to older models. Adjusting the API Level ensures that the system automatically applies the correct data structure for each specific vehicle. This can be updated from the CONFIGURE menu later, if needed.\"\n        }\n      }\n    }\n  },\n  \"options\": {\n    \"step\": {\n      \"init\": {\n        \"data\": {\n          \"scan_initial\": \"Cloud Update at Startup\",\n          \"scan_active\": \"Active Polling at Scan Interval\",\n          \"scan_interval\": \"Scan Interval\",\n          \"api_level\": \"API Level\"\n        },\n        \"title\": \"Audi Connect Options\",\n        \"data_description\": {\n          \"scan_initial\": \"Perform a cloud update immediately upon startup.\",\n          \"scan_active\": \"Perform a cloud update at the set scan interval.\",\n          \"scan_interval\": \"Minutes between active polling. If 'Active Polling at Scan Interval' is off, this value will have no impact.\",\n          \"api_level\": \"For Audi vehicles, the API request data structure varies by model. Newer vehicles use an updated data structure compared to older models. Adjusting the API Level ensures that the system automatically applies the correct data structure for each specific vehicle.\"\n        }\n      }\n    }\n  },\n  \"selector\": {\n    \"vehicle_actions\": {\n      \"options\": {\n        \"lock\": \"Lock\",\n        \"unlock\": \"Unlock\",\n        \"start_climatisation\": \"Start Climatisation (Legacy)\",\n        \"stop_climatisation\": \"Stop Climatisation\",\n        \"start_charger\": \"Start Charger\",\n        \"start_timed_charger\": \"Start timed Charger\",\n        \"stop_charger\": \"Stop Charger\",\n        \"start_preheater\": \"Start Preheater (Legacy)\",\n        \"stop_preheater\": \"Stop Preheater\",\n        \"start_window_heating\": \"Start Window heating\",\n        \"stop_window_heating\": \"Stop Windows heating\"\n      }\n    }\n  },\n  \"services\": {\n    \"refresh_vehicle_data\": {\n      \"name\": \"Refresh Vehicle Data\",\n      \"description\": \"Requests an update of the vehicle state directly, as opposed to the normal update mechanism which only retrieves data from the cloud.\",\n      \"fields\": {\n        \"vin\": {\n          \"name\": \"VIN\",\n          \"description\": \"The Vehicle Identification Number (VIN) of the Audi vehicle. This should be a 17-character identifier unique to each vehicle.\"\n        }\n      }\n    },\n    \"execute_vehicle_action\": {\n      \"name\": \"Execute Vehicle Action\",\n      \"description\": \"Performs various actions on the vehicle.\",\n      \"fields\": {\n        \"vin\": {\n          \"name\": \"VIN\",\n          \"description\": \"The Vehicle Identification Number (VIN) of the Audi vehicle. This should be a 17-character identifier unique to each vehicle.\"\n        },\n        \"action\": {\n          \"name\": \"Action\",\n          \"description\": \"The specific action to perform on the vehicle. Note that available actions may vary based on the vehicle.\",\n          \"example\": \"lock\"\n        }\n      }\n    },\n    \"start_climate_control\": {\n      \"name\": \"Start Climate Control\",\n      \"description\": \"Start the climate control with options for temperature, glass surface heating, and auto seat comfort.\",\n      \"fields\": {\n        \"vin\": {\n          \"name\": \"VIN\",\n          \"description\": \"The Vehicle Identification Number (VIN) of the Audi vehicle. This should be a 17-character identifier unique to each vehicle.\"\n        },\n        \"temp_f\": {\n          \"name\": \"Target Temperature (Fahrenheit)\",\n          \"description\": \"(Optional) Set temperature in °F. Defaults to 70°F if not provided. Overrides 'temp_c'.\"\n        },\n        \"temp_c\": {\n          \"name\": \"Target Temperature (Celsius)\",\n          \"description\": \"(Optional) Set temperature in °C. Defaults to 21°C if not provided. Overridden if 'temp_f' is provided.\"\n        },\n        \"glass_heating\": {\n          \"name\": \"Glass Surface Heating\",\n          \"description\": \"(Optional) Enable or disable glass surface heating.\"\n        },\n        \"seat_fl\": {\n          \"name\": \"Auto Seat Comfort: Front-Left\",\n          \"description\": \"(Optional) Enable or disable Auto Seat Comfort for the front-left seat.\"\n        },\n        \"seat_fr\": {\n          \"name\": \"Auto Seat Comfort: Front-Right\",\n          \"description\": \"(Optional) Enable or disable Auto Seat Comfort for the front-right seat.\"\n        },\n        \"seat_rl\": {\n          \"name\": \"Auto Seat Comfort: Rear-Left\",\n          \"description\": \"(Optional) Enable or disable Auto Seat Comfort for the rear-left seat.\"\n        },\n        \"seat_rr\": {\n          \"name\": \"Auto Seat Comfort: Rear-Right\",\n          \"description\": \"(Optional) Enable or disable Auto Seat Comfort for the rear-right seat.\"\n        },\n        \"climatisation_at_unlock\": {\n          \"name\": \"Climatisation at Unlock\",\n          \"description\": \"(Optional) Enable climate control to continue when vehicle is unlocked.\"\n        },\n        \"climatisation_mode\": {\n          \"name\": \"Climatisation Mode\",\n          \"description\": \"(Optional) Maximum comfort (Comfort) or energy-saving (Economy). Default is Comfort.\"\n        }\n      }\n    },\n    \"refresh_cloud_data\": {\n      \"name\": \"Refresh Cloud Data\",\n      \"description\": \"Retrieves current cloud data without triggering a vehicle refresh. Data may be outdated if the vehicle has not checked in recently.\"\n    },\n    \"start_auxiliary_heating\": {\n      \"name\": \"Start Auxiliary Heating\",\n      \"description\": \"Start auxiliary heating the vehicle, with option for duration.\",\n      \"fields\": {\n        \"vin\": {\n          \"name\": \"VIN\",\n          \"description\": \"The Vehicle Identification Number (VIN) of the Audi vehicle. This should be a 17-character identifier unique to each vehicle.\"\n        },\n        \"duration\": {\n          \"name\": \"Duration\",\n          \"description\": \"The number of minutes the auxiliary heater should run before turning off. Default is 20 minutes if not provided.\"\n        }\n      }\n    },\n    \"set_target_soc\": {\n      \"name\": \"Set Target State of Charge\",\n      \"description\": \"Set the target state of charge (battery %) for the vehicle.\",\n      \"fields\": {\n        \"vin\": {\n          \"name\": \"VIN\",\n          \"description\": \"The Vehicle Identification Number (VIN) of the Audi vehicle. This should be a 17-character identifier unique to each vehicle.\"\n        },\n        \"target_soc\": {\n          \"name\": \"Target State of Charge\",\n          \"description\": \"Target state of charge percentage (20-100%).\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "custom_components/audiconnect/translations/fi.json",
    "content": "{\n  \"config\": {\n    \"abort\": {\n      \"invalid_credentials\": \"Virheelliset tunnistetiedot\",\n      \"user_already_configured\": \"Tili on jo määritetty\"\n    },\n    \"create_entry\": {},\n    \"error\": {\n      \"invalid_credentials\": \"Virheelliset tunnistetiedot\",\n      \"invalid_username\": \"Virheellinen käyttäjätunnus\",\n      \"unexpected\": \"Odottamaton virhe yhdistettäessä Audi Connect -palveluun\",\n      \"user_already_configured\": \"Tili on jo määritetty\"\n    },\n    \"step\": {\n      \"user\": {\n        \"data\": {\n          \"password\": \"Salasana\",\n          \"username\": \"Käyttäjätunnus\",\n          \"spin\": \"S-PIN\",\n          \"region\": \"Alue\",\n          \"scan_interval\": \"Skannausväli\",\n          \"api_level\": \"API-taso\"\n        },\n        \"title\": \"Audi Connect -tilin tiedot\",\n        \"data_description\": {\n          \"api_level\": \"Audi-ajoneuvoissa API-pyynnön tietorakenne vaihtelee mallin mukaan. Uudemmat ajoneuvot käyttävät päivitettyä tietorakennetta verrattuna vanhempiin malleihin. API-tason säätäminen varmistaa, että järjestelmä käyttää kussakin ajoneuvossa oikeaa tietorakennetta. Tätä voi myöhemmin muuttaa ASETUKSET-valikon kautta tarvittaessa.\"\n        }\n      }\n    }\n  },\n  \"options\": {\n    \"step\": {\n      \"init\": {\n        \"data\": {\n          \"scan_initial\": \"Päivitä pilvestä käynnistyksessä\",\n          \"scan_active\": \"Aktiivinen tarkistus aikavälein\",\n          \"scan_interval\": \"Aikaväli\",\n          \"api_level\": \"API-taso\"\n        },\n        \"title\": \"Audi Connect -asetukset\",\n        \"data_description\": {\n          \"scan_initial\": \"Päivitä pilvestä heti käynnistyksen yhteydessä.\",\n          \"scan_active\": \"Päivitä pilvestä aikavälein.\",\n          \"scan_interval\": \"Minuutit aktiivisten tarkistusten välillä. Jos 'Aktiivinen tarkistus aikavälein' ei ole käytössä, tämä arvo ei vaikuta.\",\n          \"api_level\": \"Audi-ajoneuvoissa API-pyynnön tietorakenne vaihtelee mallin mukaan. Uudemmat ajoneuvot käyttävät päivitettyä tietorakennetta verrattuna vanhempiin malleihin. API-tason säätäminen varmistaa, että järjestelmä käyttää kussakin ajoneuvossa oikeaa tietorakennetta.\"\n        }\n      }\n    }\n  },\n  \"selector\": {\n    \"vehicle_actions\": {\n      \"options\": {\n        \"lock\": \"Lukitse\",\n        \"unlock\": \"Avaa lukitus\",\n        \"start_climatisation\": \"Käynnistä ilmastointi (perinteinen)\",\n        \"stop_climatisation\": \"Pysäytä ilmastointi\",\n        \"start_charger\": \"Käynnistä laturi\",\n        \"start_timed_charger\": \"Aloita ajastettu lataus\",\n        \"stop_charger\": \"Pysäytä laturi\",\n        \"start_preheater\": \"Käynnistä lisälämmitin (perinteinen)\",\n        \"stop_preheater\": \"Pysäytä lisälämmitin\",\n        \"start_window_heating\": \"Käynnistä ikkunalämmitys\",\n        \"stop_window_heating\": \"Pysäytä ikkunalämmitys\"\n      }\n    }\n  },\n  \"services\": {\n    \"refresh_vehicle_data\": {\n      \"name\": \"Päivitä ajoneuvon tiedot\",\n      \"description\": \"Pyytää ajoneuvon tilan päivitystä suoraan, pilvestä päivitystä odottamatta - toisin kuin normaali päivitys, joka hakee vain pilvitietoja.\",\n      \"fields\": {\n        \"vin\": {\n          \"name\": \"VIN\",\n          \"description\": \"Ajoneuvon tunnistenumero (VIN). Sen tulee olla 17-merkkinen yksilöllinen tunnus jokaiselle ajoneuvolle.\"\n        }\n      }\n    },\n    \"execute_vehicle_action\": {\n      \"name\": \"Suorita ajoneuvotoiminto\",\n      \"description\": \"Suorittaa erilaisia toimintoja ajoneuvolle.\",\n      \"fields\": {\n        \"vin\": {\n          \"name\": \"VIN\",\n          \"description\": \"Ajoneuvon tunnistenumero (VIN). VIN on 17-merkkiä pitkä, yksilöllinen tunnus.\"\n        },\n        \"action\": {\n          \"name\": \"Toiminto\",\n          \"description\": \"Ajoneuvolle suoritettava toiminto. Huomaa, että saatavilla olevat toiminnot voivat vaihdella ajoneuvon mukaan.\",\n          \"example\": \"lukitse\"\n        }\n      }\n    },\n    \"start_climate_control\": {\n      \"name\": \"Käynnistä ilmastointi\",\n      \"description\": \"Käynnistä ilmastointi lämpötila-, ikkunalämmitys- ja automaattisen istuinmukavuuden asetuksilla.\",\n      \"fields\": {\n        \"vin\": {\n          \"name\": \"VIN\",\n          \"description\": \"Ajoneuvon tunnistenumero (VIN). VIN on 17-merkkiä pitkä, yksilöllinen tunnus.\"\n        },\n        \"temp_f\": {\n          \"name\": \"Kohdelämpötila (Fahrenheit)\",\n          \"description\": \"(Valinnainen) Näytä lämpötila Fahrenheit-asteina. Oletusarvo on 70 °F, jos arvoa ei anneta. Ohittaa 'temp_c'-asetuksen.\"\n        },\n        \"temp_c\": {\n          \"name\": \"Kohdelämpötila (Celsius)\",\n          \"description\": \"(Valinnainen) Näytä lämpötila Celsius-asteina. Oletusarvo on 21 °C, jos arvoa ei anneta. Ohitetaan, jos 'temp_f' on asetettu.\"\n        },\n        \"glass_heating\": {\n          \"name\": \"Ikkunan lämmitys\",\n          \"description\": \"(Valinnainen) Ota ikkunan lämmitys käyttöön tai pois käytöstä.\"\n        },\n        \"seat_fl\": {\n          \"name\": \"Automaattinen istuimen mukavuustoiminto: vasen etuistuin\",\n          \"description\": \"(Valinnainen) Ota vasemman etuistuimen automaattinen istuimen mukavuustoiminto käyttöön tai pois käytöstä.\"\n        },\n        \"seat_fr\": {\n          \"name\": \"Automaattinen istuimen mukavuustoiminto: oikea etuistuin\",\n          \"description\": \"(Valinnainen) Ota oikean etuistuimen automaattinen istuimen mukavuustoiminto käyttöön tai pois käytöstä.\"\n        },\n        \"seat_rl\": {\n          \"name\": \"Automaattinen istuimen mukavuustoiminto: vasen takaistuin\",\n          \"description\": \"(Valinnainen) Ota vasemman takaistuimen automaattinen istuimen mukavuustoiminto käyttöön tai pois käytöstä.\"\n        },\n        \"seat_rr\": {\n          \"name\": \"Automaattinen istuimen mukavuustoiminto: oikea takaistuin\",\n          \"description\": \"(Valinnainen) Ota oikean takaistuimen automaattinen istuimen mukavuustoiminto käyttöön tai pois käytöstä.\"\n        }\n      }\n    },\n    \"refresh_cloud_data\": {\n      \"name\": \"Päivitä pilvitiedot\",\n      \"description\": \"Hakee nykyiset pilvitiedot käynnistämättä ajoneuvon päivitystä. Tiedot voivat olla vanhentuneita, jos ajoneuvo ei ole äskettäin ollut yhteydessä.\"\n    },\n    \"start_auxiliary_heating\": {\n      \"name\": \"Käynnistä lisälämmitys\",\n      \"description\": \"Käynnistä ajoneuvon lisälämmitys, kestoasetuksen valinta.\",\n      \"fields\": {\n        \"vin\": {\n          \"name\": \"VIN\",\n          \"description\": \"Ajoneuvon tunnistenumero (VIN). VIN on 17-merkkiä pitkä, yksilöllinen tunnus.\"\n        },\n        \"duration\": {\n          \"name\": \"Kesto\",\n          \"description\": \"Lisälämmittimen käyntiaika minuutteina ennen sammuttamista. Oletusarvo on 20 minuuttia, jos arvoa ei anneta.\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "custom_components/audiconnect/translations/fr.json",
    "content": "{\n  \"config\": {\n    \"abort\": {\n      \"invalid_credentials\": \"Informations d'identification invalides\",\n      \"user_already_configured\": \"Le compte a déjà été configuré\"\n    },\n    \"create_entry\": {},\n    \"error\": {\n      \"invalid_credentials\": \"Informations d'identification invalides\",\n      \"invalid_username\": \"Nom d'utilisateur invalide\",\n      \"unexpected\": \"Erreur inattendue lors de la communication avec le serveur Audi Connect\",\n      \"user_already_configured\": \"Le compte a déjà été configuré\"\n    },\n    \"step\": {\n      \"user\": {\n        \"data\": {\n          \"password\": \"Mot de passe\",\n          \"username\": \"Nom d'utilisateur\",\n          \"spin\": \"S-PIN\",\n          \"region\": \"Région\",\n          \"scan_interval\": \"Intervalle de scan\"\n        },\n        \"title\": \"Informations sur le compte Audi Connect\"\n      }\n    }\n  },\n  \"options\": {\n    \"step\": {\n      \"init\": {\n        \"data\": {\n          \"scan_initial\": \"Mise à jour du cloud au démarrage\",\n          \"scan_active\": \"Intervalle de mises à jour actives\",\n          \"scan_interval\": \"Intervalle de mises à jour\"\n        },\n        \"title\": \"Options Audi Connect\",\n        \"data_description\": {\n          \"scan_initial\": \"Effectuer une mise à jour du cloud immédiatement après le démarrage.\",\n          \"scan_active\": \"Effectuer une mise à jour du cloud à intervalle régulier défini.\",\n          \"scan_interval\": \"Minutes entre les mises à jour actives. Si 'Intervalle de mises à jour actives' est désactivé, cette valeur n'aura aucun impact.\"\n        }\n      }\n    }\n  },\n  \"selector\": {\n    \"vehicle_actions\": {\n      \"options\": {\n        \"lock\": \"Verrouiller\",\n        \"unlock\": \"Déverrouiller\",\n        \"start_climatisation\": \"Démarrer climatisation (Hérité)\",\n        \"stop_climatisation\": \"Arrêter climatisation\",\n        \"start_charger\": \"Démarrer chargeur\",\n        \"start_timed_charger\": \"Démarrage chronométré du chargeur\",\n        \"stop_charger\": \"Arrêter Chargeur\",\n        \"start_preheater\": \"Démarrer préchauffage\",\n        \"stop_preheater\": \"Arrêter préchauffage\",\n        \"start_window_heating\": \"Démarrer chauffage des fenêtres\",\n        \"stop_window_heating\": \"Arrêter chauffage des fenêtres\"\n      }\n    }\n  },\n  \"services\": {\n    \"refresh_vehicle_data\": {\n      \"name\": \"Actualiser les données du véhicule\",\n      \"description\": \"Demande directement une mise à jour de l'état du véhicule, contrairement au mécanisme de mise à jour normal qui récupère uniquement les données du cloud.\",\n      \"fields\": {\n        \"vin\": {\n          \"name\": \"VIN\",\n          \"description\": \"Le Vehicle Identification Number (VIN) du véhicule Audi. Il doit s'agir d'un identifiant de 17 caractères unique à chaque véhicule.\"\n        }\n      }\n    },\n    \"execute_vehicle_action\": {\n      \"name\": \"Exécuter l'action du véhicule\",\n      \"description\": \"Effectue diverses actions sur le véhicule.\",\n      \"fields\": {\n        \"vin\": {\n          \"name\": \"VIN\",\n          \"description\": \"Le Vehicle Identification Number (VIN) du véhicule Audi. Il doit s'agir d'un identifiant de 17 caractères unique à chaque véhicule.\"\n        },\n        \"action\": {\n          \"name\": \"Action\",\n          \"description\": \"L'action spécifique à effectuer sur le véhicule. Noter que les actions disponibles peuvent varier en fonction du véhicule.\",\n          \"example\": \"Verrouiller\"\n        }\n      }\n    },\n    \"start_climate_control\": {\n      \"name\": \"Démarrer la climatisation\",\n      \"description\": \"Démarrez la climatisation avec des options de température, de chauffage des vitres et de confort des sièges auto.\",\n      \"fields\": {\n        \"vin\": {\n          \"name\": \"VIN\",\n          \"description\": \"Le Vehicle Identification Number (VIN) du véhicule Audi. Il doit s'agir d'un identifiant de 17 caractères unique à chaque véhicule.\"\n        },\n        \"temp_f\": {\n          \"name\": \"Température cible (Fahrenheit)\",\n          \"description\": \"(Optionel) Régler la température en °F. La valeur par défaut est 70°F si elle n'est pas fournie. Remplace 'temp_c'.\"\n        },\n        \"temp_c\": {\n          \"name\": \"Température cible (Celsius)\",\n          \"description\": \"(Facultatif) Régler la température en °C. La valeur par défaut est 21°C si elle n'est pas fournie. Remplacé si 'temp_f' est fourni.\"\n        },\n        \"glass_heating\": {\n          \"name\": \"Chauffage des vitres\",\n          \"description\": \"(Facultatif) Activer ou désactiver le chauffage des surfaces vitrées.\"\n        },\n        \"seat_fl\": {\n          \"name\": \"Confort du siège auto: Avant-Gauche\",\n          \"description\": \"(Facultatif) Activer ou désactiver Confort du siège auto pour le siège avant gauche.\"\n        },\n        \"seat_fr\": {\n          \"name\": \"Confort du siège auto: Avant-Droit\",\n          \"description\": \"(Facultatif) Activer ou désactiver Confort du siège auto pour le siège avant droit.\"\n        },\n        \"seat_rl\": {\n          \"name\": \"Confort du siège auto: Arrière-Gauche\",\n          \"description\": \"(Facultatif) Activer ou désactiver Confort du siège auto pour le siège arrière gauche.\"\n        },\n        \"seat_rr\": {\n          \"name\": \"Confort du siège auto: Arrière-Droit\",\n          \"description\": \"(Facultatif) Activer ou désactiver Confort du siège auto pour le siège arrière droit.\"\n        }\n      }\n    },\n    \"refresh_cloud_data\": {\n      \"name\": \"Actualiser les données cloud\",\n      \"description\": \"Récupère les données cloud actuelles sans déclencher une actualisation du véhicule. Les données peuvent être obsolètes si le véhicule n'a pas été vérifié récemment.\"\n    }\n  }\n}\n"
  },
  {
    "path": "custom_components/audiconnect/translations/nb.json",
    "content": "{\n  \"config\": {\n    \"abort\": {\n      \"invalid_credentials\": \"Ugyldige innloggingsopplysninger\",\n      \"user_already_configured\": \"Kontoen er allerede konfigurert\"\n    },\n    \"create_entry\": {},\n    \"error\": {\n      \"invalid_credentials\": \"Ugyldige innloggingsopplysninger\",\n      \"invalid_username\": \"Ugyldig brukernavn\",\n      \"unexpected\": \"Uventet feil i kommunikasjonen med Audi Connect-serveren\",\n      \"user_already_configured\": \"Kontoen er allerede konfigurert\"\n    },\n    \"step\": {\n      \"user\": {\n        \"data\": {\n          \"password\": \"Passord\",\n          \"username\": \"Brukernavn\",\n          \"spin\": \"S-PIN\",\n          \"region\": \"Region\",\n          \"scan_interval\": \"Skanneintervall\"\n        },\n        \"title\": \"Audi Connect kontoinformasjon\"\n      }\n    }\n  },\n  \"options\": {\n    \"step\": {\n      \"init\": {\n        \"data\": {\n          \"scan_interval\": \"Skanneintervall\"\n        },\n        \"title\": \"Audi Connect-alternativer\",\n        \"data_description\": {\n          \"scan_interval\": \"(Minutter) Omstart kreves for at nytt skanneintervall skal tre i kraft.\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "custom_components/audiconnect/translations/nl.json",
    "content": "{\n  \"config\": {\n    \"abort\": {\n      \"invalid_credentials\": \"Ongeldige gebruikersgegevens\",\n      \"user_already_configured\": \"Account is al geconfigureerd\"\n    },\n    \"create_entry\": {},\n    \"error\": {\n      \"invalid_credentials\": \"Ongeldige gebruikersgegevens\",\n      \"invalid_username\": \"Ongeldige gebruikersnaam\",\n      \"unexpected\": \"Onverwachte fout bij communicatie met de Audi Connect server\",\n      \"user_already_configured\": \"Account is al geconfigureerd\"\n    },\n    \"step\": {\n      \"user\": {\n        \"data\": {\n          \"password\": \"Wachtwoord\",\n          \"username\": \"Gebruikersnaam\",\n          \"spin\": \"S-PIN\",\n          \"region\": \"Regio\",\n          \"scan_interval\": \"Update interval\"\n        },\n        \"title\": \"Audi Connect accountgegevens\"\n      }\n    }\n  },\n  \"options\": {\n    \"step\": {\n      \"init\": {\n        \"data\": {\n          \"scan_interval\": \"Scaninterval\"\n        },\n        \"title\": \"Audi Connect-opties\",\n        \"data_description\": {\n          \"scan_interval\": \"(Minuten) Opnieuw opstarten vereist om het nieuwe scaninterval van kracht te laten worden.\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "custom_components/audiconnect/translations/pt-BR.json",
    "content": "{\n  \"config\": {\n    \"abort\": {\n      \"invalid_credentials\": \"Credenciais inválidas\",\n      \"user_already_configured\": \"A conta já foi configurada\"\n    },\n    \"create_entry\": {},\n    \"error\": {\n      \"invalid_credentials\": \"Credenciais inválidas\",\n      \"invalid_username\": \"Nome de usuário inválido\",\n      \"unexpected\": \"Erro inesperado na comunicação com o servidor Audi Connect\",\n      \"user_already_configured\": \"A conta já foi configurada\"\n    },\n    \"step\": {\n      \"user\": {\n        \"data\": {\n          \"password\": \"Senha\",\n          \"username\": \"Nome de usuário\",\n          \"spin\": \"S-PIN\",\n          \"region\": \"Região\",\n          \"scan_interval\": \"Intervalo de escaneamento\"\n        },\n        \"title\": \"Informações da conta Audi Connect \"\n      }\n    }\n  },\n  \"options\": {\n    \"step\": {\n      \"init\": {\n        \"data\": {\n          \"scan_interval\": \"Intervalo de escaneamento\"\n        },\n        \"title\": \"Opções Audi Connect\",\n        \"data_description\": {\n          \"scan_interval\": \"(Minutos) É necessário reiniciar para que o novo intervalo de verificação entre em vigor.\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "custom_components/audiconnect/translations/pt.json",
    "content": "{\n  \"config\": {\n    \"abort\": {\n      \"invalid_credentials\": \"Credenciais inválidas\",\n      \"user_already_configured\": \"A conta já foi configurada\"\n    },\n    \"create_entry\": {},\n    \"error\": {\n      \"invalid_credentials\": \"Credenciais inválidas\",\n      \"invalid_username\": \"Nome de utilizador inválido\",\n      \"unexpected\": \"Erro inesperado na comunicação com o servidor Audi Connect\",\n      \"user_already_configured\": \"A conta já foi configurada\"\n    },\n    \"step\": {\n      \"user\": {\n        \"data\": {\n          \"password\": \"Senha\",\n          \"username\": \"Nome de utilizador\",\n          \"spin\": \"S-PIN\",\n          \"region\": \"Região\",\n          \"scan_interval\": \"Intervalo de pesquisa\"\n        },\n        \"title\": \"Informações da conta Audi Connect \"\n      }\n    }\n  },\n  \"options\": {\n    \"step\": {\n      \"init\": {\n        \"data\": {\n          \"scan_interval\": \"Intervalo de pesquisa\"\n        },\n        \"title\": \"Opções Audi Connect\",\n        \"data_description\": {\n          \"scan_interval\": \"(Minutos) É necessário reiniciar para que o novo intervalo de verificação entre em vigor.\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "custom_components/audiconnect/util.py",
    "content": "from functools import reduce\nfrom datetime import datetime, timezone\nimport logging\n\n_LOGGER = logging.getLogger(__name__)\n\n\ndef get_attr(dictionary, keys, default=None):\n    return reduce(\n        lambda d, key: d.get(key, default) if isinstance(d, dict) else default,\n        keys.split(\".\"),\n        dictionary,\n    )\n\n\ndef to_byte_array(hexString: str):\n    result = []\n    for i in range(0, len(hexString), 2):\n        result.append(int(hexString[i : i + 2], 16))\n\n    return result\n\n\ndef log_exception(exception, message):\n    err = message + \": \" + str(exception).rstrip(\"\\n\")\n    _LOGGER.error(err)\n\n\ndef parse_int(val: str):\n    try:\n        return int(val)\n    except (ValueError, TypeError):\n        return None\n\n\ndef parse_float(val: str):\n    try:\n        return float(val)\n    except (ValueError, TypeError):\n        return None\n\n\ndef parse_datetime(time_value):\n    \"\"\"Converts timestamp to datetime object if it's a string, or returns it directly if already datetime.\"\"\"\n    if isinstance(time_value, datetime):\n        return time_value  # Return the datetime object directly if already datetime\n    elif isinstance(time_value, str):\n        formats = [\n            \"%Y-%m-%d %H:%M:%S%z\",  # Format: 2024-04-12 05:56:17+00:00\n            \"%Y-%m-%dT%H:%M:%S.%fZ\",  # Format: 2024-04-12T05:56:13.025Z\n        ]\n        for fmt in formats:\n            try:\n                return datetime.strptime(time_value, fmt).replace(tzinfo=timezone.utc)\n            except ValueError:\n                continue\n    return None\n"
  },
  {
    "path": "custom_components/test.py",
    "content": "import sys\nimport asyncio\nimport getopt\n\nfrom audiconnect.audi_connect_account import AudiConnectAccount\nfrom audiconnect.dashboard import Dashboard\n\nfrom aiohttp import ClientSession\n\n\ndef printHelp():\n    print(\n        \"test.py --user <username> --password <password> --spin <spin> --country <region>\"\n    )\n\n\nasync def main(argv):\n    user = \"\"\n    password = \"\"\n    spin = \"\"\n    country = \"\"\n    try:\n        opts, _ = getopt.getopt(\n            argv, \"hu:p:s:r:\", [\"user=\", \"password=\", \"spin=\", \"country=\"]\n        )\n    except getopt.GetoptError:\n        printHelp()\n        sys.exit(2)\n    for opt, arg in opts:\n        if opt == \"-h\":\n            printHelp()\n            sys.exit()\n        elif opt in (\"-u\", \"--user\"):\n            user = arg\n        elif opt in (\"-p\", \"--password\"):\n            password = arg\n        elif opt in (\"-s\", \"--spin\"):\n            spin = arg\n        elif opt in (\"-r\", \"--country\"):\n            country = arg\n\n    if user == \"\" or password == \"\":\n        printHelp()\n        sys.exit()\n\n    async with ClientSession() as session:\n        account = AudiConnectAccount(session, user, password, country, spin)\n\n        await account.update(None)\n\n        for vehicle in account._vehicles:\n            dashboard = Dashboard(account, vehicle, miles=True)\n            for instrument in dashboard.instruments:\n                print(str(instrument), instrument.str_state)\n\n\nif __name__ == \"__main__\":\n    task = main(sys.argv[1:])\n    res = asyncio.get_event_loop().run_until_complete(task)\n"
  },
  {
    "path": "hacs.json",
    "content": "{\n  \"name\": \"Audi connect\",\n  \"homeassistant\": \"0.110.0\"\n}\n"
  },
  {
    "path": "info.md",
    "content": "[![hacs][hacsbadge]](hacs)\n![Project Maintenance][maintenance-shield]\n\n## Configuration\n\nConfiguration is done through the Home Assistant UI.\n\nTo add the integration, go to **Settings ➤ Devices & Services ➤ Integrations**, click **➕ Add Integration**, and search for \"Audi Connect\".\n\n![image](https://github.com/user-attachments/assets/68f4a38b-f09d-4486-a1a1-ab8a564095ab)\n\n### Configuration Variables\n\n**username**\n\n- (string)(Required) The username associated with your Audi Connect account.\n\n**password**\n\n- (string)(Required) The password for your Audi Connect account.\n\n**S-PIN**\n\n- (string)(Optional) The S-PIN for your Audi Connect account.\n\n**region**\n\n- (Required) The region where your Audi Connect account is registered.\n  - 'DE' for Europe (or leave unset)\n  - 'US' for United States of America\n  - 'CA' for Canada\n  - 'CN' for China\n\n**scan_interval**\n\n- (number)(Optional) The frequency in minutes for how often to fetch status data from Audi Connect. (Optional. Default is 15 minutes, can be no more frequent than 15 min.)\n\n**api_level**\n\n- (number)(Required) For Audi vehicles, the API request data structure varies by model. Newer models use an updated structure, while older models use a legacy format. Setting the API level ensures that the system automatically applies the correct structure for each vehicle. You can update this setting later from the CONFIGURE menu if needed.\n  - Level `0`: _Typically_ for gas vehicles\n  - Level `1`: _Typically_ for e-tron (electric) vehicles.\n\n[commits-shield]: https://img.shields.io/github/commit-activity/y/audiconnect/audi_connect_ha?style=for-the-badge\n[commits]: https://github.com/audiconnect/audi_connect_ha/commits/master\n[hacs]: https://github.com/custom-components/hacs\n[hacsbadge]: https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge\n[license-shield]: https://img.shields.io/github/license/arjenvrh/audi_connect_ha?style=for-the-badge\n[maintenance-shield]: https://img.shields.io/badge/maintainer-audiconnect-blue.svg?style=for-the-badge\n[blackbadge]: https://img.shields.io/badge/code%20style-black-000000.svg?style=for-the-badge\n[black]: https://github.com/ambv/black\n"
  },
  {
    "path": "readme.md",
    "content": "# Audi Connect Integration for Home Assistant\n\n[![GitHub Activity][commits-shield]][commits]\n[![License][license-shield]](LICENSE.md)\n[![Code Style][blackbadge]][black]\n[![hacs][hacsbadge]](hacs)\n\n## Notices\n\nDue to API changes, **currently not all functionality is available**. Please open a issue to report the topics you are missing.\n\n⚠️ Warning: Excessive use of certain features in this integration may result in temporary or permanent suspension of your Audi Connect account. Please use responsibly — abuse or misuse could potentially impact access for the entire community. Use at your own risk.\n\n## Maintainers Wanted\n\n- Looking for maintainers for Translation documents.\n- Always looking for more help from the community. If you can resolve an issue, please submit a PR or reach out to the maintainers with the working code.\n\n## Description\n\nThe `audiconnect` component provides an integration with the Audi Connect cloud service. It adds presence detection, sensors such as range, mileage, and fuel level, and provides car actions such as locking/unlocking and setting the pre-heater.\n\n**Note:** Certain functions require special permissions from Audi, such as position update via GPS.\n\nCredit for initial API discovery go to the guys at the ioBroker VW-Connect forum, who were able to figure out how the API and the PIN hashing works. Also some implementation credit to davidgiga1993 of the original [AudiAPI](https://github.com/davidgiga1993/AudiAPI) Python package, on which some of this code is loosely based.\n\nFull credit for this integration goes to @arjenvrh for their outstanding work in putting it all together. We wouldn’t have this without their contributions.\n\n## Installation\n\nThere are two ways this integration can be installed into [Home Assistant](https://www.home-assistant.io).\n\nThe easiest and recommended way is to install the integration using [HACS](https://hacs.xyz), which makes future updates easy to track and install.\n\nAlternatively, installation can be done manually by copying the files in this repository into the `custom_components` directory in the Home Assistant configuration directory:\n\n1. Open the configuration directory of your Home Assistant installation.\n2. If you do not have a `custom_components` directory, create it.\n3. In the `custom_components` directory, create a new directory called `audiconnect`.\n4. Copy all files from the `custom_components/audiconnect/` directory in this repository into the `audiconnect` directory.\n5. Restart Home Assistant.\n6. Add the integration to Home Assistant (see **Configuration**).\n\n## Configuration\n\nConfiguration is done through the Home Assistant UI.\n\nTo add the integration, go to **Settings ➤ Devices & Services ➤ Integrations**, click **➕ Add Integration**, and search for \"Audi Connect\".\n\n### Configuration Variables\n\n| Name            | Type     | Default | Description                                                                                                                                                                                                                          |\n| --------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `Username`      | `string` | –       | The username associated with your Audi Connect account.                                                                                                                                                                              |\n| `Password`      | `string` | –       | The password for your Audi Connect account.                                                                                                                                                                                          |\n| `S-PIN`         | `string` | –       | The S-PIN for your Audi Connect account to perform certain service actions.<br>(**Optional**)                                                                                                                                        |\n| `Region`        | `string` | `DE`    | The region where your Audi Connect account is registered:<br>• `DE` – Europe<br>• `US` – United States<br>• `CA` – Canada<br>• `CN` – China                                                                                          |\n| `Scan Interval` | `int`    | `15`    | Frequency (in minutes) to fetch status data from Audi Connect.<br>Minimum allowed is 15 minutes.<br>\\* _Can be updated later via the CONFIGURE menu._                                                                                |\n| `API Level`     | `int`    | `0`     | Determines the API structure used for service action calls:<br>• `0` – _Typically_ Gas vehicles (legacy format)<br>• `1` – _Typically_ e-tron (electric vehicles, newer format)<br>\\* _Can be updated later via the CONFIGURE menu._ |\n\n## Options\n\nFind configuration options under **Settings ➤ Devices & Services ➤ Integrations ➤ Audi Connect ➤ Configure**:\n\n| Name                              | Type   | Description                                                                                                                                                                     |\n| --------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `Cloud Update at Startup`         | `bool` | Toggle the initial cloud update when the integration starts. Useful for development or frequent Home Assistant restarts.                                                        |\n| `Active Polling at Scan Interval` | `bool` | Enable or disable active polling.                                                                                                                                               |\n| `Scan Interval`                   | `int`  | Defines polling frequency in minutes (minimum 15). Only effective if **Active Polling** is enabled.                                                                             |\n| `API Level`                       | `int`  | Determines the API structure used for service action calls:<br>• `0` – _Typically_ Gas vehicles (legacy format)<br>• `1` – _Typically_ e-tron (electric vehicles, newer format) |\n\n_Note: The integration will reload automatically upon clicking `Submit`, but a Home Assistant restart is suggested._\n\n## Service Actions\n\n### Audi Connect: Refresh Vehicle Data\n\n`audiconnect.refresh_vehicle_data`\n\nNormal updates retrieve data from the Audi Connect cloud service, and don't interact directly with the vehicle. _This_ service action triggers an update request from the vehicle itself. When data is retrieved successfully, Home Assistant is automatically updated. The service action requires a vehicle identification number (VIN) as a parameter.\n\n#### Parameters\n\n- **`vin`**: The Vehicle Identification Number (VIN) of the Audi you want to control.\n\n### Audi Connect: Refresh Cloud Data\n\n`audiconnect.refresh_cloud_data`\n\n_This_ service action triggers an update request from the cloud.\n\n- Functionality: Updates data for all vehicles from the online source, mirroring the action performed at integration startup or during scheduled refresh intervals.\n- Behavior: Does not force a vehicle-side data refresh. Consequently, if vehicles haven't recently pushed updates, retrieved data might be outdated.\n- Note: This service action replicates the function of active polling without scheduling, offering a more granular control over data refresh moments.\n- **IMPORTANT:** This service action has no built in usage limits. Excessive use may result in a temporary suspension of your account.\n\n#### Parameters\n\n- `none`\n\n### Audi Connect: Execute Vehicle Action\n\n`audiconnect.execute_vehicle_action`\n\nThis service action allows you to perform actions on your Audi vehicle, specified by the vehicle identification number (VIN) and the desired action.\n\n#### Service Parameters\n\n- **`vin`**: The Vehicle Identification Number (VIN) of the Audi you want to control.\n- **`action`**: The specific action to perform on the vehicle. Available actions include:\n  - **`lock`**: Lock the vehicle.\n  - **`unlock`**: Unlock the vehicle.\n  - **`start_climatisation`**: Start the vehicle's climatisation system. (Legacy) -- Deprecated\n  - **`stop_climatisation`**: Stop the vehicle's climatisation system.\n  - **`start_charger`**: Start charging the vehicle.\n  - **`start_timed_charger`**: Start the vehicle's charger with a timer.\n  - **`stop_charger`**: Stop charging the vehicle.\n  - **`start_preheater`**: Start the vehicle's preheater system. (Legacy) -- Deprecated\n  - **`stop_preheater`**: Stop the vehicle's preheater system.\n  - **`start_window_heating`**: Start heating the vehicle's windows.\n  - **`stop_window_heating`**: Stop heating the vehicle's windows.\n\n#### Usage Example\n\nTo initiate the lock action for a vehicle with VIN `WAUZZZ4G7EN123456`, use the following service call:\n\n```yaml\nservice: audiconnect.execute_vehicle_action\ndata:\n  vin: \"WAUZZZ4G7EN123456\"\n  action: \"lock\"\n```\n\n#### Notes\n\n- Certain service actions require the S-PIN to be set in the configuration.\n- When the service action is successfully performed, an update request is automatically triggered.\n\n### Audi Connect: Start Climate Control\n\n`audiconnect.start_climate_control`\n\nThis service action allows you to start the climate control with options for temperature, glass surface heating, and auto seat comfort.\n\n#### Parameters\n\n- **`vin`**: The Vehicle Identification Number (VIN) of the Audi you want to control.\n- **`temp_f`** (_optional_): Desired temperature in Fahrenheit. Default is `70`.\n- **`temp_c`** (_optional_): Desired temperature in Celsius. Default is `21`.\n- **`glass_heating`** (_optional_): Enable (`True`) or disable (`False`) glass heating. Default is `False`.\n- **`seat_fl`** (_optional_): Enable (`True`) or disable (`False`) the front-left seat heater. Default is `False`.\n- **`seat_fr`** (_optional_): Enable (`True`) or disable (`False`) the front-right seat heater. Default is `False`.\n- **`seat_rl`** (_optional_): Enable (`True`) or disable (`False`) the rear-left seat heater. Default is `False`.\n- **`seat_rr`** (_optional_): Enable (`True`) or disable (`False`) the rear-right seat heater. Default is `False`.\n\n#### Usage Example\n\nTo start the climate control for a vehicle with VIN `WAUZZZ4G7EN123456` with a temperature of 72°F, enable glass heating, and activate both front seat heaters, use the following service call:\n\n```yaml\nservice: audiconnect.start_climate_control\ndata:\n  vin: \"WAUZZZ4G7EN123456\"\n  temp_f: 72\n  glass_heating: True\n  seat_fl: True\n  seat_fr: True\n```\n\n#### Notes\n\n- The `temp_f` and `temp_c` parameters are mutually exclusive. If both are provided, `temp_f` takes precedence.\n- If neither `temp_f` nor `temp_c` is provided, the system defaults to 70°F or 21°C.\n- When the service action is successfully performed, an update request is automatically triggered.\n\n### Audi Connect: Start Auxiliary Heating\n\n`audiconnect.start_auxiliary_heating`\n\nThis service action allows you to start auxiliary heating the vehicle, with option for duration.\n\n#### Parameters\n\n- **`vin`**: The Vehicle Identification Number (VIN) of the Audi you want to control.\n- **`duration`** (_optional_): The number of minutes the auxiliary heater should run before turning off. Default is `20` minutes if not provided.\n\n#### Usage Example\n\nTo start the auxiliary heater for a vehicle with VIN `WAUZZZ4G7EN123456`, and a duration of 40 minutes, use the following service call action:\n\n```yaml\nservice: audiconnect.start_auxiliary_heating\ndata:\n  vin: \"WAUZZZ4G7EN123456\"\n  duration: 40\n```\n\n#### Notes\n\n- Requires the S-PIN to be set in the configuration.\n- When the service action is successfully performed, an update request is automatically triggered.\n\n## Example Dashboard Card\n\nBelow is an example Dashboard (Lovelace) card illustrating some of the sensors this Home Assistant addon provides.\n\n![Example Dashboard Card](card_example.png)\n\nThe card requires the following front end mods:\n\n- https://github.com/thomasloven/lovelace-card-mod\n- https://github.com/custom-cards/circle-sensor-card\n\nThese mods can (like this integration) be installed using HACS.\n\nThe card uses the following code in `ui-lovelace.yaml` (or wherever your Dashboard is configured).\n\n```yaml\n     - type: picture-elements\n        image: /local/pictures/audi_sq7.jpeg\n        style: |\n          ha-card {\n            border-radius: 10px;\n            border: solid 1px rgba(100,100,100,0.3);\n            box-shadow: 3px 3px rgba(0,0,0,0.4);\n            overflow: hidden;\n          }\n        elements:\n        - type: image\n          image: /local/pictures/cardbackK.png\n          style:\n            left: 50%\n            top: 90%\n            width: 100%\n            height: 60px\n\n        - type: icon\n          icon: mdi:car-door\n          entity: sensor.doors_trunk_sq7\n          tap_action: more_info\n          style: {color: white, left: 10%, top: 86%}\n        - type: state-label\n          entity: sensor.doors_trunk_sq7\n          style: {color: white, left: 10%, top: 95%}\n\n        - type: state-icon\n          entity: sensor.windows_sq7\n          tap_action: more_info\n          style: {color: white, left: 30%, top: 86%}\n        - type: state-label\n          entity: sensor.windows_sq7\n          style: {color: white, left: 30%, top: 95%}\n\n        - type: icon\n          icon: mdi:oil\n          entity: sensor.audi_sq7_oil_level\n          tap_action: more_info\n          style: {color: white, left: 50%, top: 86%}\n        - type: state-label\n          entity: sensor.audi_sq7_oil_level\n          style: {color: white, left: 50%, top: 95%}\n\n        - type: icon\n          icon: mdi:room-service-outline\n          entity: sensor.audi_sq7_service_inspection_time\n          tap_action: more_info\n          style: {color: white, left: 70%, top: 86%}\n        - type: state-label\n          entity: sensor.audi_sq7_service_inspection_time\n          style: {color: white, left: 70%, top: 95%}\n\n        - type: icon\n          icon: mdi:speedometer\n          entity: sensor.audi_sq7_mileage\n          tap_action: more_info\n          style: {color: white, left: 90%, top: 86%}\n        - type: state-label\n          entity: sensor.audi_sq7_mileage\n          style: {color: white, left: 90%, top: 95%}\n\n        - type: custom:circle-sensor-card\n          entity: sensor.audi_sq7_tank_level\n          max: 100\n          min: 0\n          stroke_width: 15\n          gradient: true\n          fill: '#aaaaaabb'\n          name: tank\n          units: ' '\n          font_style:\n            font-size: 1.0em\n            font-color: white\n            text-shadow: '1px 1px black'\n          style:\n            top: 5%\n            left: 80%\n            width: 4em\n            height: 4em\n            transform: none\n\n        - type: custom:circle-sensor-card\n          entity: sensor.audi_sq7_range\n          max: 630\n          min: 0\n          stroke_width: 15\n          gradient: true\n          fill: '#aaaaaabb'\n          name: range\n          units: ' '\n          font_style:\n            font-size: 1.0em\n            font-color: white\n            text-shadow: '1px 1px black'\n          style:\n            top: 5%\n            left: 5%\n            width: 4em\n            height: 4em\n            transform: none\n```\n\n[commits-shield]: https://img.shields.io/github/commit-activity/y/audiconnect/audi_connect_ha?style=for-the-badge\n[commits]: https://github.com/audiconnect/audi_connect_ha/commits/master\n[hacs]: https://github.com/custom-components/hacs\n[hacsbadge]: https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge\n[license-shield]: https://img.shields.io/github/license/audiconnect/audi_connect_ha?style=for-the-badge\n[maintenance-shield]: https://img.shields.io/badge/maintainer-audiconnect-blue.svg?style=for-the-badge\n[blackbadge]: https://img.shields.io/badge/code%20style-black-000000.svg?style=for-the-badge\n[black]: https://github.com/ambv/black\n"
  }
]