Full Code of arjenvrh/audi_connect_ha for AI

master 5dcc3ae09c49 cached
42 files
293.0 KB
66.1k tokens
414 symbols
1 requests
Download .txt
Showing preview only (308K chars total). Download the full file or copy to clipboard to get everything.
Repository: arjenvrh/audi_connect_ha
Branch: master
Commit: 5dcc3ae09c49
Files: 42
Total size: 293.0 KB

Directory structure:
gitextract_84fzj90j/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── inactiveIssues.yml
│       ├── release.yml
│       ├── semanticTitle.yaml
│       └── validate.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CONTRIBUTING.md
├── LICENSE
├── custom_components/
│   ├── audiconnect/
│   │   ├── __init__.py
│   │   ├── audi_account.py
│   │   ├── audi_api.py
│   │   ├── audi_connect_account.py
│   │   ├── audi_entity.py
│   │   ├── audi_models.py
│   │   ├── audi_services.py
│   │   ├── binary_sensor.py
│   │   ├── config_flow.py
│   │   ├── const.py
│   │   ├── dashboard.py
│   │   ├── device_tracker.py
│   │   ├── lock.py
│   │   ├── manifest.json
│   │   ├── sensor.py
│   │   ├── services.yaml
│   │   ├── strings.json
│   │   ├── switch.py
│   │   ├── translations/
│   │   │   ├── de.json
│   │   │   ├── en.json
│   │   │   ├── fi.json
│   │   │   ├── fr.json
│   │   │   ├── nb.json
│   │   │   ├── nl.json
│   │   │   ├── pt-BR.json
│   │   │   └── pt.json
│   │   └── util.py
│   └── test.py
├── hacs.json
├── info.md
└── readme.md

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ""
labels: ""
assignees: ""
---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:

1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Logfile**
How to enable audiconnect debugging?
Settings > Integrations > Audi Connect > Enable Debug Logging
Run service refresh_cloud_data
Disable Debug Logging

**Your Vehicle Details**
Model:
Year:
Type (Gas/Hybrid/Electric):
Region (EU/US/CA/CN):


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ""
labels: ""
assignees: ""
---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.

**Logfile**
How to enable audiconnect debugging?
Settings > Integrations > Audi Connect > Enable Debug Logging
Run service refresh_cloud_data
Disable Debug Logging

**Your Vehicle Details**
Model:
Year:
Type (ICE/PHEV/BEV):


================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  # Enable version updates for Python
  - package-ecosystem: "github-actions"
    directory: "/"
    # Check for updates once a week
    schedule:
      interval: "weekly"


================================================
FILE: .github/workflows/inactiveIssues.yml
================================================
name: Close inactive issues
on:
  schedule:
    - cron: "30 1 * * *"

jobs:
  close-issues:
    runs-on: ubuntu-latest
    permissions:
      issues: write
      actions: write
      pull-requests: write
    steps:
      - uses: actions/stale@v10
        with:
          days-before-issue-stale: 45
          days-before-issue-close: 15
          stale-issue-label: "stale"
          stale-issue-message: "This issue is stale because it has been open for 45 days with no activity. Are you still experiencing this issue? "
          close-issue-message: "This issue was closed because it has been inactive for 15 days since being marked as stale."
          days-before-pr-stale: -1
          days-before-pr-close: -1
          repo-token: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
  workflow_dispatch:
  schedule:
    - cron: "0 8 * * Wed,Sun"
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6

      - name: Gets semantic release info
        id: semantic_release_info
        uses: jossef/action-semantic-release-info@v3.0.0
        env:
          GITHUB_TOKEN: ${{ github.token }}
      - name: Update Version and Commit
        if: ${{steps.semantic_release_info.outputs.version != ''}}
        run: |
          echo "Version: ${{steps.semantic_release_info.outputs.version}}"
          sed -i "s/\"version\": \".*\"/\"version\": \"${{steps.semantic_release_info.outputs.version}}\"/g" custom_components/audiconnect/manifest.json
          git config --local user.email "action@github.com"
          git config --local user.name "GitHub Action"
          git add -A
          git commit -m "chore: bumping version to ${{steps.semantic_release_info.outputs.version}}"
          git tag ${{ steps.semantic_release_info.outputs.git_tag }}

      - name: Push changes
        if: ${{steps.semantic_release_info.outputs.version != ''}}
        uses: ad-m/github-push-action@v1.1.0
        with:
          github_token: ${{ github.token }}
          tags: true

      - name: Create GitHub Release
        if: ${{steps.semantic_release_info.outputs.version != ''}}
        uses: ncipollo/release-action@v1
        env:
          GITHUB_TOKEN: ${{ github.token }}
        with:
          tag: ${{ steps.semantic_release_info.outputs.git_tag }}
          name: ${{ steps.semantic_release_info.outputs.git_tag }}
          body: ${{ steps.semantic_release_info.outputs.notes }}
          draft: false
          prerelease: false


================================================
FILE: .github/workflows/semanticTitle.yaml
================================================
name: "Semantic Title"

on:
  pull_request_target:
    types:
      - opened
      - edited
      - synchronize

jobs:
  main:
    runs-on: ubuntu-latest
    steps:
      # Please look up the latest version from
      # https://github.com/amannn/action-semantic-pull-request/releases
      - uses: amannn/action-semantic-pull-request@v6.1.1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/validate.yml
================================================
name: Validate

on:
  push:
  pull_request:
  schedule:
    - cron: "0 0 * * *"
  workflow_dispatch:

jobs:
  validate-hassfest:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - name: Hassfest validation
        uses: home-assistant/actions/hassfest@master

  validate-hacs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - name: HACS validation
        uses: hacs/action@main
        with:
          category: integration


================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.idea


================================================
FILE: .pre-commit-config.yaml
================================================
---
ci:
  autoupdate_commit_msg: "chore: pre-commit autoupdate"
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.15.10
    hooks:
      - id: ruff
        args:
          - --fix
      - id: ruff-format
  - repo: https://github.com/codespell-project/codespell
    rev: v2.4.2
    hooks:
      - id: codespell
        args:
          - --ignore-words-list=fro,hass
          - --skip="./.*,*.csv,*.json,*.ambr"
          - --quiet-level=2
        exclude_types: [csv, json]
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v6.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-executables-have-shebangs
        stages: [manual]
      - id: check-json
        exclude: (.vscode|.devcontainer)
  - repo: https://github.com/asottile/pyupgrade
    rev: v3.21.2
    hooks:
      - id: pyupgrade
  - repo: https://github.com/adrienverge/yamllint.git
    rev: v1.38.0
    hooks:
      - id: yamllint
        exclude: (.github|.vscode|.devcontainer)
  - repo: https://github.com/pre-commit/mirrors-prettier
    rev: v4.0.0-alpha.8
    hooks:
      - id: prettier
  - repo: https://github.com/cdce8p/python-typing-update
    rev: v0.8.1
    hooks:
      # Run `python-typing-update` hook manually from time to time
      # to update python typing syntax.
      # Will require manual work, before submitting changes!
      # pre-commit run --hook-stage manual python-typing-update --all-files
      - id: python-typing-update
        stages: [manual]
        args:
          - --py311-plus
          - --force
          - --keep-updates
        files: ^(/.+)?[^/]+\.py$
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.20.1
    hooks:
      - id: mypy
        args: [--strict, --ignore-missing-imports]
        files: ^(/.+)?[^/]+\.py$


================================================
FILE: CONTRIBUTING.md
================================================
# Contribution guidelines

Contributing to this project should be as easy and transparent as possible, whether it's:

- Reporting a bug
- Discussing the current state of the code
- Submitting a fix
- Proposing new features

## Contributing Code

Github is used to host code, to track issues and feature requests, as well as accept pull requests.

Pull requests are the best way to propose changes to the codebase.

1. Fork the repo and create your branch from `master`.
2. If you've changed something, update the documentation.
3. Issue a pull request

By contributing, you agree that your contributions will be licensed under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project.
Feel free to contact the maintainers if that's a concern.

## Coding Style

This project uses [black](https://github.com/ambv/black) to ensure the code follows a consistent style.

## Report bugs using Github's issues

GitHub issues are used to track public bugs. Report a bug by [opening a new issue](../../issues/new/choose)

## Write bug reports with details

**Great Bug Reports** tend to have:

- A quick summary and/or background
- Steps to reproduce
- What you expected would happen
- What actually happens
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2019 Arjen van Rhijn @arjenvrh

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: custom_components/audiconnect/__init__.py
================================================
"""Support for Audi Connect."""

from datetime import timedelta
import voluptuous as vol
import logging

import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.core import HomeAssistant
from homeassistant.util.dt import utcnow
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
    CONF_NAME,
    CONF_PASSWORD,
    CONF_RESOURCES,
    CONF_SCAN_INTERVAL,
    CONF_USERNAME,
)
from homeassistant.helpers.device_registry import DeviceEntry


from .audi_account import AudiAccount

from .const import (
    DOMAIN,
    CONF_REGION,
    CONF_MUTABLE,
    CONF_SCAN_INITIAL,
    CONF_SCAN_ACTIVE,
    DEFAULT_UPDATE_INTERVAL,
    MIN_UPDATE_INTERVAL,
    RESOURCES,
    COMPONENTS,
    CONF_API_LEVEL,
    DEFAULT_API_LEVEL,
    API_LEVELS,
)

_LOGGER = logging.getLogger(__name__)

CONFIG_SCHEMA = vol.Schema(
    {
        DOMAIN: vol.Schema(
            {
                vol.Required(CONF_USERNAME): cv.string,
                vol.Required(CONF_PASSWORD): cv.string,
                vol.Optional(
                    CONF_SCAN_INTERVAL,
                    default=timedelta(minutes=DEFAULT_UPDATE_INTERVAL),
                ): vol.All(
                    cv.time_period,
                    vol.Clamp(min=timedelta(minutes=MIN_UPDATE_INTERVAL)),
                ),
                vol.Optional(CONF_NAME, default={}): cv.schema_with_slug_keys(
                    cv.string
                ),
                vol.Optional(CONF_RESOURCES): vol.All(
                    cv.ensure_list, [vol.In(RESOURCES)]
                ),
                vol.Optional(CONF_REGION): cv.string,
                vol.Optional(CONF_MUTABLE, default=True): cv.boolean,
                vol.Optional(
                    CONF_API_LEVEL, default=API_LEVELS[DEFAULT_API_LEVEL]
                ): vol.All(vol.Coerce(int), vol.In(API_LEVELS)),
            }
        )
    },
    extra=vol.ALLOW_EXTRA,
)


async def async_setup(hass, config):
    if hass.config_entries.async_entries(DOMAIN):
        return True

    if DOMAIN not in config:
        return True

    names = config[DOMAIN].get(CONF_NAME)
    if len(names) == 0:
        return True

    data = {}
    data[CONF_USERNAME] = config[DOMAIN].get(CONF_USERNAME)
    data[CONF_PASSWORD] = config[DOMAIN].get(CONF_PASSWORD)
    data[CONF_SCAN_INTERVAL] = config[DOMAIN].get(CONF_SCAN_INTERVAL).seconds / 60
    data[CONF_REGION] = config[DOMAIN].get(CONF_REGION)
    data[CONF_API_LEVEL] = config[DOMAIN].get(CONF_API_LEVEL)

    hass.async_create_task(
        hass.config_entries.flow.async_init(
            DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=data
        )
    )

    return True


async def async_update_listener(hass, config_entry):
    _LOGGER.debug("Updates detected, reloading configuration...")
    await hass.config_entries.async_reload(config_entry.entry_id)


async def async_setup_entry(hass, config_entry):
    """Set up this integration using UI."""
    _LOGGER.debug("Audi Connect starting...")

    # Register the update listener so that changes to configuration options are applied immediately.
    config_entry.async_on_unload(
        config_entry.add_update_listener(async_update_listener)
    )

    if DOMAIN not in hass.data:
        hass.data[DOMAIN] = {}

    """Set up the Audi Connect component."""
    hass.data[DOMAIN]["devices"] = set()

    # Attempt to retrieve the scan interval from options, then fall back to data, or use default
    scan_interval = timedelta(
        minutes=config_entry.options.get(
            CONF_SCAN_INTERVAL,
            config_entry.data.get(CONF_SCAN_INTERVAL, DEFAULT_UPDATE_INTERVAL),
        )
    )
    _LOGGER.debug("User option for CONF_SCAN_INTERVAL is %s", scan_interval)

    # Get Initial Scan Option - Default to True
    _scan_initial = config_entry.options.get(CONF_SCAN_INITIAL, True)
    _LOGGER.debug("User option for CONF_SCAN_INITIAL is %s.", _scan_initial)

    # Get Active Scan Option - Default to True
    _scan_active = config_entry.options.get(CONF_SCAN_ACTIVE, True)
    _LOGGER.debug("User option for CONF_SCAN_ACTIVE is %s.", _scan_active)

    account = config_entry.data.get(CONF_USERNAME)

    if account not in hass.data[DOMAIN]:
        data = hass.data[DOMAIN][account] = AudiAccount(hass, config_entry)
        data.init_connection()
    else:
        data = hass.data[DOMAIN][account]

    # Define a callback function for the timer to update data
    async def update_data(now):
        """Update the data with the latest information."""
        _LOGGER.debug("ACTIVE POLLING: Requesting scheduled cloud data refresh...")
        await data.update(utcnow())

    # Schedule the update_data function if option is true
    if _scan_active:
        _LOGGER.debug(
            "ACTIVE POLLING: Scheduling cloud data refresh every %d minutes.",
            scan_interval.seconds / 60,
        )
        async_track_time_interval(hass, update_data, scan_interval)
    else:
        _LOGGER.debug(
            "ACTIVE POLLING: Active Polling at Scan Interval is turned off in user options. Skipping scheduling..."
        )

    # Initially update the data if option is true
    if _scan_initial:
        _LOGGER.debug("Requesting initial cloud data update...")
        return await data.update(utcnow())
    else:
        _LOGGER.debug(
            "Cloud Update at Start is turned off in user options. Skipping initial update..."
        )

    _LOGGER.debug("Audi Connect Setup Complete")
    return True


async def async_unload_entry(hass, config_entry):
    account = config_entry.data.get(CONF_USERNAME)

    data = hass.data[DOMAIN][account]

    for component in COMPONENTS:
        await hass.config_entries.async_forward_entry_unload(
            data.config_entry, component
        )

    del hass.data[DOMAIN][account]

    return True


async def async_remove_config_entry_device(
    hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry
) -> bool:
    """Remove a config entry from a device."""
    return True


================================================
FILE: custom_components/audiconnect/audi_account.py
================================================
import asyncio
import logging

import voluptuous as vol

from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.util.dt import utcnow

from .audi_connect_account import AudiConnectAccount, AudiConnectObserver
from .audi_models import VehicleData
from .const import (
    COMPONENTS,
    CONF_ACTION,
    CONF_CLIMATE_GLASS,
    CONF_CLIMATE_SEAT_FL,
    CONF_CLIMATE_SEAT_FR,
    CONF_CLIMATE_SEAT_RL,
    CONF_CLIMATE_SEAT_RR,
    CONF_CLIMATE_AT_UNLOCK,
    CONF_CLIMATE_MODE,
    CONF_CLIMATE_TEMP_C,
    CONF_CLIMATE_TEMP_F,
    CONF_REGION,
    CONF_SPIN,
    CONF_VIN,
    DOMAIN,
    SIGNAL_STATE_UPDATED,
    TRACKER_UPDATE,
    UPDATE_SLEEP,
    CONF_API_LEVEL,
    DEFAULT_API_LEVEL,
    API_LEVELS,
    CONF_DURATION,
    CONF_TARGET_SOC,
    CONF_FILTER_VINS,
)
from .dashboard import Dashboard

REFRESH_VEHICLE_DATA_FAILED_EVENT = "refresh_failed"
REFRESH_VEHICLE_DATA_COMPLETED_EVENT = "refresh_completed"

SERVICE_REFRESH_VEHICLE_DATA = "refresh_vehicle_data"
SERVICE_REFRESH_VEHICLE_DATA_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_VIN): cv.string,
    }
)

SERVICE_EXECUTE_VEHICLE_ACTION = "execute_vehicle_action"
SERVICE_EXECUTE_VEHICLE_ACTION_SCHEMA = vol.Schema(
    {vol.Required(CONF_VIN): cv.string, vol.Required(CONF_ACTION): cv.string}
)

SERVICE_START_CLIMATE_CONTROL = "start_climate_control"
SERVICE_START_CLIMATE_CONTROL_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_VIN): cv.string,
        vol.Optional(CONF_CLIMATE_TEMP_F): cv.positive_int,
        vol.Optional(CONF_CLIMATE_TEMP_C): cv.positive_int,
        vol.Optional(CONF_CLIMATE_GLASS): cv.boolean,
        vol.Optional(CONF_CLIMATE_SEAT_FL): cv.boolean,
        vol.Optional(CONF_CLIMATE_SEAT_FR): cv.boolean,
        vol.Optional(CONF_CLIMATE_SEAT_RL): cv.boolean,
        vol.Optional(CONF_CLIMATE_SEAT_RR): cv.boolean,
        vol.Optional(CONF_CLIMATE_AT_UNLOCK): cv.boolean,
        vol.Optional(CONF_CLIMATE_MODE): cv.string,
    }
)

SERVICE_START_AUXILIARY_HEATING = "start_auxiliary_heating"
SERVICE_START_AUXILIARY_HEATING_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_VIN): cv.string,
        vol.Optional(CONF_DURATION): cv.positive_int,
    }
)

SERVICE_SET_TARGET_SOC = "set_target_soc"
SERVICE_SET_TARGET_SOC_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_VIN): cv.string,
        vol.Required(CONF_TARGET_SOC): vol.All(
            cv.positive_int, vol.Range(min=20, max=100)
        ),
    }
)

PLATFORMS: list[str] = [
    Platform.BINARY_SENSOR,
    Platform.SENSOR,
    Platform.DEVICE_TRACKER,
    Platform.LOCK,
    Platform.SWITCH,
]

SERVICE_REFRESH_CLOUD_DATA = "refresh_cloud_data"

_LOGGER = logging.getLogger(__name__)


class AudiAccount(AudiConnectObserver):
    def __init__(self, hass, config_entry):
        """Initialize the component state."""
        self.hass = hass
        self.config_entry = config_entry
        self.config_vehicles = set()
        self.vehicles = set()

    def init_connection(self):
        session = async_get_clientsession(self.hass)
        excluded_vins = [
            x.strip()
            for x in self.config_entry.options.get(
                CONF_FILTER_VINS, self.config_entry.data.get(CONF_FILTER_VINS, "")
            ).split(",")
            if x.strip()
        ]

        self.connection = AudiConnectAccount(
            session=session,
            username=self.config_entry.data.get(CONF_USERNAME),
            password=self.config_entry.data.get(CONF_PASSWORD),
            country=self.config_entry.data.get(CONF_REGION),
            spin=self.config_entry.data.get(CONF_SPIN),
            api_level=self.config_entry.options.get(
                CONF_API_LEVEL,
                self.config_entry.data.get(
                    CONF_API_LEVEL, API_LEVELS[DEFAULT_API_LEVEL]
                ),
            ),
            excluded_vins=excluded_vins,
        )

        self.hass.services.async_register(
            DOMAIN,
            SERVICE_REFRESH_VEHICLE_DATA,
            self.refresh_vehicle_data,
            schema=SERVICE_REFRESH_VEHICLE_DATA_SCHEMA,
        )
        self.hass.services.async_register(
            DOMAIN,
            SERVICE_EXECUTE_VEHICLE_ACTION,
            self.execute_vehicle_action,
            schema=SERVICE_EXECUTE_VEHICLE_ACTION_SCHEMA,
        )
        self.hass.services.async_register(
            DOMAIN,
            SERVICE_START_CLIMATE_CONTROL,
            self.start_climate_control,
            schema=SERVICE_START_CLIMATE_CONTROL_SCHEMA,
        )
        self.hass.services.async_register(
            DOMAIN,
            SERVICE_REFRESH_CLOUD_DATA,
            self.update,
        )
        self.hass.services.async_register(
            DOMAIN,
            SERVICE_START_AUXILIARY_HEATING,
            self.start_auxiliary_heating,
            schema=SERVICE_START_AUXILIARY_HEATING_SCHEMA,
        )
        self.hass.services.async_register(
            DOMAIN,
            SERVICE_SET_TARGET_SOC,
            self.set_target_soc,
            schema=SERVICE_SET_TARGET_SOC_SCHEMA,
        )

        self.connection.add_observer(self)

    def is_enabled(self, attr):
        return True
        # """Return true if the user has enabled the resource."""
        # return attr in config[DOMAIN].get(CONF_RESOURCES, [attr])

    async def discover_vehicles(self, vehicles):
        if len(vehicles) > 0:
            for vehicle in vehicles:
                vin = vehicle.vin.lower()

                self.vehicles.add(vin)

                cfg_vehicle = VehicleData(self.config_entry)
                cfg_vehicle.vehicle = vehicle
                self.config_vehicles.add(cfg_vehicle)

                dashboard = Dashboard(self.connection, vehicle)

                for instrument in (
                    instrument
                    for instrument in dashboard.instruments
                    if instrument._component in COMPONENTS
                    and self.is_enabled(instrument.slug_attr)
                ):
                    if instrument._component == "sensor":
                        cfg_vehicle.sensors.add(instrument)
                    if instrument._component == "binary_sensor":
                        cfg_vehicle.binary_sensors.add(instrument)
                    if instrument._component == "switch":
                        cfg_vehicle.switches.add(instrument)
                    if instrument._component == "device_tracker":
                        cfg_vehicle.device_trackers.add(instrument)
                    if instrument._component == "lock":
                        cfg_vehicle.locks.add(instrument)

            await self.hass.config_entries.async_forward_entry_setups(
                self.config_entry, PLATFORMS
            )

    async def update(self, now):
        """Update status from the cloud."""
        _LOGGER.debug("Starting refresh cloud data...")
        if not await self.connection.update(None):
            _LOGGER.warning("Failed refresh cloud data")
            return False

        # Discover new vehicles that have not been added yet
        new_vehicles = [
            x for x in self.connection._vehicles if x.vin not in self.vehicles
        ]
        if new_vehicles:
            _LOGGER.debug("Retrieved %d vehicle(s)", len(new_vehicles))
        await self.discover_vehicles(new_vehicles)

        async_dispatcher_send(self.hass, SIGNAL_STATE_UPDATED)

        for config_vehicle in self.config_vehicles:
            for instrument in config_vehicle.device_trackers:
                async_dispatcher_send(self.hass, TRACKER_UPDATE, instrument)

        _LOGGER.debug("Successfully refreshed cloud data")
        return True

    async def execute_vehicle_action(self, service):
        vin = service.data.get(CONF_VIN).lower()
        action = service.data.get(CONF_ACTION).lower()

        if action == "lock":
            await self.connection.set_vehicle_lock(vin, True)
        if action == "unlock":
            await self.connection.set_vehicle_lock(vin, False)
        if action == "start_climatisation":
            await self.connection.set_vehicle_climatisation(vin, True)
        if action == "stop_climatisation":
            await self.connection.set_vehicle_climatisation(vin, False)
        if action == "start_charger":
            await self.connection.set_battery_charger(vin, True, False)
        if action == "start_timed_charger":
            await self.connection.set_battery_charger(vin, True, True)
        if action == "stop_charger":
            await self.connection.set_battery_charger(vin, False, False)
        if action == "start_preheater":
            _LOGGER.warning(
                'The "Start Preheater (Legacy)" action is deprecated and will be removed in a future release.'
                'Please use the "Start Auxiliary Heating" service instead.'
            )
            await self.connection.set_vehicle_pre_heater(vin, True)
        if action == "stop_preheater":
            await self.connection.set_vehicle_pre_heater(vin, False)
        if action == "start_window_heating":
            await self.connection.set_vehicle_window_heating(vin, True)
        if action == "stop_window_heating":
            await self.connection.set_vehicle_window_heating(vin, False)

    async def start_climate_control(self, service):
        _LOGGER.debug("Initiating Start Climate Control Service...")
        vin = service.data.get(CONF_VIN).lower()
        # Optional Parameters
        temp_f = service.data.get(CONF_CLIMATE_TEMP_F, None)
        temp_c = service.data.get(CONF_CLIMATE_TEMP_C, None)
        glass_heating = service.data.get(CONF_CLIMATE_GLASS, False)
        seat_fl = service.data.get(CONF_CLIMATE_SEAT_FL, False)
        seat_fr = service.data.get(CONF_CLIMATE_SEAT_FR, False)
        seat_rl = service.data.get(CONF_CLIMATE_SEAT_RL, False)
        seat_rr = service.data.get(CONF_CLIMATE_SEAT_RR, False)
        climatisation_at_unlock = service.data.get(CONF_CLIMATE_AT_UNLOCK, False)
        climatisation_mode = service.data.get(CONF_CLIMATE_MODE)

        await self.connection.start_climate_control(
            vin,
            temp_f,
            temp_c,
            glass_heating,
            seat_fl,
            seat_fr,
            seat_rl,
            seat_rr,
            climatisation_at_unlock,
            climatisation_mode,
        )

    async def start_auxiliary_heating(self, service):
        vin = service.data.get(CONF_VIN)

        # Optional Parameters
        duration = service.data.get(CONF_DURATION, None)

        if duration is None:
            _LOGGER.debug('Initiating "Start Auxiliary Heating" action...')
        else:
            _LOGGER.debug(
                f'Initiating "Start Auxiliary Heating" action for {duration} minutes...'
            )

        await self.connection.set_vehicle_pre_heater(
            vin=vin,
            activate=True,
            duration=duration,
        )

    async def set_target_soc(self, service):
        """Set the target state of charge for the vehicle battery."""
        vin = service.data.get(CONF_VIN).lower()
        target_soc = service.data.get(CONF_TARGET_SOC)

        _LOGGER.debug(
            f"Initiating 'Set Target SOC' action to {target_soc}% for VIN {vin}..."
        )

        await self.connection.set_target_state_of_charge(vin, target_soc)

    async def handle_notification(self, vin: str, action: str) -> None:
        await self._refresh_vehicle_data(vin)

    async def refresh_vehicle_data(self, service):
        vin = service.data.get(CONF_VIN).lower()
        await self._refresh_vehicle_data(vin)

    async def _refresh_vehicle_data(self, vin):
        redacted_vin = "*" * (len(vin) - 4) + vin[-4:]
        res = await self.connection.refresh_vehicle_data(vin)

        if res is True:
            _LOGGER.debug("Refresh vehicle data successful for VIN: %s", redacted_vin)
            self.hass.bus.fire(
                "{}_{}".format(DOMAIN, REFRESH_VEHICLE_DATA_COMPLETED_EVENT),
                {"vin": redacted_vin},
            )
        elif res == "disabled":
            _LOGGER.debug("Refresh vehicle data is disabled for VIN: %s", redacted_vin)
        else:
            _LOGGER.debug("Refresh vehicle data failed for VIN: %s", redacted_vin)
            self.hass.bus.fire(
                "{}_{}".format(DOMAIN, REFRESH_VEHICLE_DATA_FAILED_EVENT),
                {"vin": redacted_vin},
            )

        _LOGGER.debug("Requesting to refresh cloud data in %d seconds...", UPDATE_SLEEP)
        await asyncio.sleep(UPDATE_SLEEP)

        try:
            _LOGGER.debug("Requesting to refresh cloud data now...")
            await self.update(utcnow())
        except Exception as e:
            _LOGGER.exception("Refresh cloud data failed: %s", str(e))


================================================
FILE: custom_components/audiconnect/audi_api.py
================================================
import json
import logging
from datetime import datetime
import asyncio
from asyncio import TimeoutError, CancelledError
from aiohttp import ClientResponseError
from aiohttp.hdrs import METH_GET, METH_POST, METH_PUT
from typing import Dict

# ===========================================
# VERBOSE DEBUG TOGGLE
# Set to True to log EVERYTHING (full headers, full body, raw JSON, etc.)
# Set to False for normal operation (minimal debug output)
DEBUG_VERBOSE = False
# ===========================================

TIMEOUT = 30
_LOGGER = logging.getLogger(__name__)


class AudiAPI:
    HDR_XAPP_VERSION = "4.31.0"
    HDR_USER_AGENT = "Android/4.31.0 (Build 800341641.root project 'myaudi_android'.ext.buildTime) Android/13"

    def __init__(self, session, proxy=None):
        self.__token = None
        self.__xclientid = None
        self._session = session
        self.__proxy = {"http": proxy, "https": proxy} if proxy else None

    def use_token(self, token):
        self.__token = token
        if DEBUG_VERBOSE:
            _LOGGER.debug("[use_token] Token set: %s", token)

    def set_xclient_id(self, xclientid):
        self.__xclientid = xclientid
        if DEBUG_VERBOSE:
            _LOGGER.debug("[set_xclient_id] X-Client-ID set: %s", xclientid)

    async def request(
        self,
        method,
        url,
        data,
        headers: Dict[str, str] = None,
        raw_reply: bool = False,
        raw_contents: bool = False,
        rsp_wtxt: bool = False,
        **kwargs,
    ):
        if DEBUG_VERBOSE:
            _LOGGER.debug("[REQUEST INITIATED]")
            _LOGGER.debug("Method: %s", method)
            _LOGGER.debug("URL: %s", url)
            _LOGGER.debug("Data: %s", data)
            _LOGGER.debug("Headers: %s", headers)
            _LOGGER.debug("Kwargs: %s", kwargs)
            _LOGGER.debug("Proxy: %s", self.__proxy)

        try:
            async with asyncio.timeout(TIMEOUT):
                async with self._session.request(
                    method, url, headers=headers, data=data, **kwargs
                ) as response:
                    if DEBUG_VERBOSE:
                        _LOGGER.debug("[RESPONSE RECEIVED]")
                        _LOGGER.debug("Status: %s", response.status)
                        _LOGGER.debug("Reason: %s", response.reason)
                        _LOGGER.debug("Headers: %s", dict(response.headers))

                    if raw_reply:
                        if DEBUG_VERBOSE:
                            _LOGGER.debug("Returning raw reply object.")
                        return response

                    if rsp_wtxt:
                        txt = await response.text()
                        if DEBUG_VERBOSE:
                            _LOGGER.debug("Response text (full): %s", txt)
                        else:
                            _LOGGER.debug(
                                "Returning response text; length=%d", len(txt)
                            )
                        return response, txt

                    elif raw_contents:
                        contents = await response.read()
                        if DEBUG_VERBOSE:
                            _LOGGER.debug("Raw contents (bytes): %s", contents)
                        else:
                            _LOGGER.debug(
                                "Returning raw contents; length=%d", len(contents)
                            )
                        return contents

                    elif response.status in (200, 202, 207):
                        raw_body = await response.text()
                        if DEBUG_VERBOSE:
                            _LOGGER.debug(
                                "Raw JSON text (before parsing): %s", raw_body
                            )
                        json_data = json_loads(raw_body)
                        if DEBUG_VERBOSE:
                            _LOGGER.debug("Parsed JSON data (full): %s", json_data)
                        else:
                            _LOGGER.debug("Returning JSON data: %s", json_data)
                        return json_data

                    else:
                        # this should be refactored:
                        # 204 is a valid response for some requests (e.g. update_vehicle_position)
                        # and should not raise an error.
                        # request should return a tuple indicating the response itself and the
                        # http-status
                        if response.status != 204:
                            _LOGGER.debug(
                                "Non-success response: status=%s, reason=%s — will be handled by caller",
                                response.status,
                                response.reason,
                            )
                            if DEBUG_VERBOSE:
                                _LOGGER.debug(
                                    "Response url: %s, body: %s",
                                    url,
                                    await response.text(),
                                )
                        raise ClientResponseError(
                            response.request_info,
                            response.history,
                            status=response.status,
                            message=response.reason,
                        )

        except CancelledError:
            if DEBUG_VERBOSE:
                _LOGGER.debug("Request cancelled (CancelledError).")
            raise TimeoutError("Timeout error")

        except TimeoutError:
            if DEBUG_VERBOSE:
                _LOGGER.debug("Request timed out after %s seconds.", TIMEOUT)
            raise TimeoutError("Timeout error")

        except Exception as e:
            if DEBUG_VERBOSE:
                _LOGGER.exception("Unexpected exception during request: %s", e)
            raise

    async def get(
        self, url, raw_reply: bool = False, raw_contents: bool = False, **kwargs
    ):
        full_headers = self.__get_headers()
        if DEBUG_VERBOSE:
            _LOGGER.debug("[GET] URL: %s | Headers: %s", url, full_headers)
        return await self.request(
            METH_GET,
            url,
            data=None,
            headers=full_headers,
            raw_reply=raw_reply,
            raw_contents=raw_contents,
            **kwargs,
        )

    async def put(self, url, data=None, headers: Dict[str, str] = None):
        full_headers = self.__get_headers()
        if headers:
            full_headers.update(headers)
        if DEBUG_VERBOSE:
            _LOGGER.debug(
                "[PUT] URL: %s | Data: %s | Headers: %s", url, data, full_headers
            )
        return await self.request(METH_PUT, url, headers=full_headers, data=data)

    async def post(
        self,
        url,
        data=None,
        headers: Dict[str, str] = None,
        use_json: bool = True,
        raw_reply: bool = False,
        raw_contents: bool = False,
        **kwargs,
    ):
        full_headers = self.__get_headers()
        if headers:
            full_headers.update(headers)
        if use_json and data is not None:
            data = json.dumps(data)
        if DEBUG_VERBOSE:
            _LOGGER.debug(
                "[POST] URL: %s | Data: %s | Headers: %s", url, data, full_headers
            )
        return await self.request(
            METH_POST,
            url,
            headers=full_headers,
            data=data,
            raw_reply=raw_reply,
            raw_contents=raw_contents,
            **kwargs,
        )

    def __get_headers(self):
        data = {
            "Accept": "application/json",
            "Accept-Charset": "utf-8",
            "X-App-Version": self.HDR_XAPP_VERSION,
            "X-App-Name": "myAudi",
            "User-Agent": self.HDR_USER_AGENT,
        }
        if self.__token is not None:
            data["Authorization"] = "Bearer " + self.__token.get("access_token")
        if self.__xclientid is not None:
            data["X-Client-ID"] = self.__xclientid
        if DEBUG_VERBOSE:
            _LOGGER.debug("[HEADERS BUILT]: %s", data)
        return data


def obj_parser(obj):
    """Parse datetime."""
    for key, val in obj.items():
        try:
            obj[key] = datetime.strptime(val, "%Y-%m-%dT%H:%M:%S%z")
        except (TypeError, ValueError):
            pass
    return obj


def json_loads(s):
    return json.loads(s, object_hook=obj_parser)


================================================
FILE: custom_components/audiconnect/audi_connect_account.py
================================================
import time
from datetime import datetime, timezone, timedelta
import logging
import asyncio
from typing import List
import re

from asyncio import TimeoutError
from aiohttp import ClientResponseError

from abc import ABC, abstractmethod

from .audi_services import AudiService
from .audi_api import AudiAPI
from .util import log_exception, get_attr, parse_int, parse_float, parse_datetime

_LOGGER = logging.getLogger(__name__)

MAX_RESPONSE_ATTEMPTS = 10
REQUEST_STATUS_SLEEP = 5

ACTION_LOCK = "lock"
ACTION_CLIMATISATION = "climatisation"
ACTION_CHARGER = "charger"
ACTION_WINDOW_HEATING = "window_heating"
ACTION_PRE_HEATER = "pre_heater"


class AudiConnectObserver(ABC):
    @abstractmethod
    async def handle_notification(self, vin: str, action: str) -> None:
        pass


class AudiConnectAccount:
    """Representation of an Audi Connect Account."""

    def __init__(
        self,
        session,
        username: str,
        password: str,
        country: str,
        spin: str,
        api_level: int,
        excluded_vins: List[str] = None,
    ) -> None:
        self._api = AudiAPI(session)
        self._audi_service = AudiService(self._api, country, spin, api_level)

        self._username = username
        self._password = password
        self._loggedin = False
        self._support_vehicle_refresh = True
        self._logintime = 0

        self._connect_retries = 3
        self._connect_delay = 10

        self._update_listeners = []

        self._vehicles = []
        self._audi_vehicles = []
        self._excluded_vins = [v.lower() for v in (excluded_vins or [])]

        self._observers: List[AudiConnectObserver] = []

    def add_observer(self, observer: AudiConnectObserver) -> None:
        self._observers.append(observer)

    async def notify(self, vin: str, action: str) -> None:
        for observer in self._observers:
            await observer.handle_notification(vin, action)

    async def login(self):
        for i in range(self._connect_retries):
            self._loggedin = await self.try_login(i == self._connect_retries - 1)
            if self._loggedin is True:
                self._logintime = time.time()
                break

            if i < self._connect_retries - 1:
                _LOGGER.warning(
                    "LOGIN: Login to Audi service failed, retrying in %s seconds",
                    self._connect_delay,
                )
                await asyncio.sleep(self._connect_delay)

    async def try_login(self, logError):
        try:
            _LOGGER.debug("LOGIN: Requesting login to Audi service...")
            await self._audi_service.login(self._username, self._password, False)
            _LOGGER.debug("LOGIN: Login to Audi service successful")
            return True
        except Exception as exception:
            if logError is True:
                _LOGGER.error(
                    "LOGIN: Failed to log in to the Audi service: %s."
                    "You may need to open the myAudi app, or log in via a web browser, to accept updated terms and conditions.",
                    str(exception),
                )
            return False

    async def update(self, vinlist):
        if not self._loggedin:
            await self.login()

        if not self._loggedin:
            return False

        #
        elapsed_sec = time.time() - self._logintime
        if await self._audi_service.refresh_token_if_necessary(elapsed_sec):
            # Store current timestamp when refresh was performed and successful
            self._logintime = time.time()

        """Update the state of all vehicles."""
        try:
            if len(self._audi_vehicles) > 0:
                for vehicle in self._audi_vehicles:
                    if vehicle.vin and vehicle.vin.lower() in self._excluded_vins:
                        continue
                    await self.add_or_update_vehicle(vehicle, vinlist)

            else:
                vehicles_response = await self._audi_service.get_vehicle_information()
                self._audi_vehicles = vehicles_response.vehicles
                self._vehicles = []
                for vehicle in self._audi_vehicles:
                    if vehicle.vin and vehicle.vin.lower() in self._excluded_vins:
                        _LOGGER.debug("Skipping excluded vehicle VIN: %s", vehicle.vin)
                        continue
                    await self.add_or_update_vehicle(vehicle, vinlist)

            for listener in self._update_listeners:
                listener()

            # TR/2021-12-01: do not set to False as refresh_token is used
            # self._loggedin = False

            return True

        except OSError as exception:
            # Force a re-login in case of failure/exception
            self._loggedin = False
            _LOGGER.exception(exception)
            return False

    async def add_or_update_vehicle(self, vehicle, vinlist):
        if vehicle.vin is not None:
            if vinlist is None or vehicle.vin.lower() in vinlist:
                vupd = [x for x in self._vehicles if x.vin == vehicle.vin.lower()]
                if len(vupd) > 0:
                    if await vupd[0].update() is False:
                        self._loggedin = False
                else:
                    try:
                        audiVehicle = AudiConnectVehicle(self._audi_service, vehicle)
                        if await audiVehicle.update() is False:
                            self._loggedin = False
                        self._vehicles.append(audiVehicle)
                    except Exception:
                        pass

    async def refresh_vehicle_data(self, vin: str):
        redacted_vin = "*" * (len(vin) - 4) + vin[-4:]
        if not self._loggedin:
            await self.login()

        if not self._loggedin:
            return False

        if not self._support_vehicle_refresh:
            _LOGGER.debug(
                "Vehicle refresh support is disabled for VIN: %s. Exiting update process.",
                redacted_vin,
            )
            return "disabled"

        try:
            _LOGGER.debug(
                "Sending command to refresh vehicle data for VIN: %s",
                redacted_vin,
            )

            await self._audi_service.refresh_vehicle_data(vin)

            _LOGGER.debug(
                "Successfully refreshed vehicle data for VIN: %s",
                redacted_vin,
            )

            return True

        except TimeoutError:
            _LOGGER.debug(
                "TimeoutError encountered while refreshing vehicle data for VIN: %s.",
                redacted_vin,
            )
            return False
        except ClientResponseError as cre:
            if cre.status in (403, 404):
                _LOGGER.debug(
                    "VEHICLE REFRESH: ClientResponseError with status %s for VIN: %s. Vehicle does not support vehicle refresh — disabling.",
                    cre.status,
                    redacted_vin,
                )
                self._support_vehicle_refresh = False
                return "disabled"
            elif cre.status == 502:
                _LOGGER.debug(
                    "VEHICLE REFRESH: Received status %s while refreshing vehicle data for VIN: %s. This is typically transient and may resolve on its own.",
                    cre.status,
                    redacted_vin,
                )
                return False
            elif cre.status != 204:
                _LOGGER.debug(
                    "VEHICLE REFRESH: ClientResponseError with status %s while refreshing vehicle data for VIN: %s. Error: %s",
                    cre.status,
                    redacted_vin,
                    cre,
                )
                return False
            else:
                _LOGGER.debug(
                    "VEHICLE REFRESH: Refresh vehicle data currently not available for VIN: %s. Received 204 status.",
                    redacted_vin,
                )
                return False

        except Exception as e:
            _LOGGER.error(
                "VEHICLE REFRESH: An unexpected error occurred while refreshing vehicle data for VIN: %s. Error: %s",
                redacted_vin,
                e,
            )
            return False

    async def set_vehicle_lock(self, vin: str, lock: bool):
        if not self._loggedin:
            await self.login()

        if not self._loggedin:
            return False

        try:
            _LOGGER.debug(
                "Sending command to {action} to vehicle {vin}".format(
                    action="lock" if lock else "unlock", vin=vin
                ),
            )

            await self._audi_service.set_vehicle_lock(vin, lock)

            _LOGGER.debug(
                "Successfully {action} vehicle {vin}".format(
                    action="locked" if lock else "unlocked", vin=vin
                ),
            )

            await self.notify(vin, ACTION_LOCK)

            return True

        except Exception as exception:
            log_exception(
                exception,
                "Unable to {action} {vin}".format(
                    action="lock" if lock else "unlock", vin=vin
                ),
            )

    async def set_target_state_of_charge(self, vin: str, target_soc: int):
        """Set the target state of charge for the vehicle battery."""
        if not self._loggedin:
            await self.login()

        if not self._loggedin:
            return False

        try:
            _LOGGER.debug(
                "Setting target state of charge to %d%% for vehicle %s",
                target_soc,
                vin,
            )

            await self._audi_service.set_target_state_of_charge(vin, target_soc)

            _LOGGER.debug(
                "Successfully set target state of charge to %d%% for vehicle %s",
                target_soc,
                vin,
            )

            return True

        except Exception as exception:
            log_exception(
                exception,
                "Unable to set target state of charge for vehicle {}".format(vin),
            )
            return False

    async def set_vehicle_climatisation(self, vin: str, activate: bool):
        if not self._loggedin:
            await self.login()

        if not self._loggedin:
            return False

        try:
            _LOGGER.debug(
                "Sending command to {action} climatisation to vehicle {vin}".format(
                    action="start" if activate else "stop", vin=vin
                ),
            )

            await self._audi_service.set_climatisation(vin, activate)

            _LOGGER.debug(
                "Successfully {action} climatisation of vehicle {vin}".format(
                    action="started" if activate else "stopped", vin=vin
                ),
            )

            await self.notify(vin, ACTION_CLIMATISATION)

            return True

        except Exception as exception:
            log_exception(
                exception,
                "Unable to {action} climatisation of vehicle {vin}".format(
                    action="start" if activate else "stop", vin=vin
                ),
            )

    async def start_climate_control(
        self,
        vin: str,
        temp_f: int,
        temp_c: int,
        glass_heating: bool,
        seat_fl: bool,
        seat_fr: bool,
        seat_rl: bool,
        seat_rr: bool,
        climatisation_at_unlock: bool,
        climatisation_mode: str,
    ):
        if not self._loggedin:
            await self.login()

        if not self._loggedin:
            return False

        try:
            _LOGGER.debug(
                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}"
            )

            await self._audi_service.start_climate_control(
                vin,
                temp_f,
                temp_c,
                glass_heating,
                seat_fl,
                seat_fr,
                seat_rl,
                seat_rr,
                climatisation_at_unlock,
                climatisation_mode,
            )

            _LOGGER.debug(f"Successfully started climate control of vehicle {vin}")

            await self.notify(vin, ACTION_CLIMATISATION)

            return True

        except Exception as exception:
            _LOGGER.error(
                f"Unable to start climate control of vehicle {vin}. Error: {exception}",
                exc_info=True,
            )
            return False

    async def set_battery_charger(self, vin: str, activate: bool, timer: bool):
        if not self._loggedin:
            await self.login()

        if not self._loggedin:
            return False

        try:
            _LOGGER.debug(
                "Sending command to {action}{timer} charger to vehicle {vin}".format(
                    action="start" if activate else "stop",
                    vin=vin,
                    timer=" timed" if timer else "",
                ),
            )

            await self._audi_service.set_battery_charger(vin, activate, timer)

            _LOGGER.debug(
                "Successfully {action}{timer} charger of vehicle {vin}".format(
                    action="started" if activate else "stopped",
                    vin=vin,
                    timer=" timed" if timer else "",
                ),
            )

            await self.notify(vin, ACTION_CHARGER)

            return True

        except Exception as exception:
            log_exception(
                exception,
                "Unable to {action} charger of vehicle {vin}".format(
                    action="start" if activate else "stop", vin=vin
                ),
            )

    async def set_vehicle_window_heating(self, vin: str, activate: bool):
        if not self._loggedin:
            await self.login()

        if not self._loggedin:
            return False

        try:
            _LOGGER.debug(
                "Sending command to {action} window heating to vehicle {vin}".format(
                    action="start" if activate else "stop", vin=vin
                ),
            )

            await self._audi_service.set_window_heating(vin, activate)

            _LOGGER.debug(
                "Successfully {action} window heating of vehicle {vin}".format(
                    action="started" if activate else "stopped", vin=vin
                ),
            )

            await self.notify(vin, ACTION_WINDOW_HEATING)

            return True

        except Exception as exception:
            log_exception(
                exception,
                "Unable to {action} window heating of vehicle {vin}".format(
                    action="start" if activate else "stop", vin=vin
                ),
            )

    async def set_vehicle_pre_heater(self, vin: str, activate: bool, **kwargs):
        if not self._loggedin:
            await self.login()

        if not self._loggedin:
            return False

        try:
            _LOGGER.debug(
                "Sending command to {action} pre-heater to vehicle {vin}".format(
                    action="start" if activate else "stop", vin=vin
                ),
            )

            # Pass **kwargs down
            await self._audi_service.set_pre_heater(vin, activate, **kwargs)

            _LOGGER.debug(
                "Successfully {action} pre-heater of vehicle {vin}".format(
                    action="started" if activate else "stopped", vin=vin
                ),
            )

            await self.notify(vin, ACTION_PRE_HEATER)

            return True

        except Exception as exception:
            log_exception(
                exception,
                "Unable to {action} pre-heater of vehicle {vin}".format(
                    action="start" if activate else "stop", vin=vin
                ),
            )


class AudiConnectVehicle:
    def __init__(self, audi_service: AudiService, vehicle) -> None:
        self._audi_service = audi_service
        self._vehicle = vehicle
        self._vin = vehicle.vin.lower()
        self._vehicle.state = {}
        self._vehicle.fields = {}
        self._logged_errors = set()
        self._no_error = False

        self.support_status_report = True
        self.support_position = True
        self.support_climater = True
        self.support_preheater = True
        self.support_charger = True
        self.support_trip_data = True

        self.charging_complete_time_frozen = None

    @property
    def vin(self):
        return self._vin

    @property
    def csid(self):
        return self._vehicle.csid

    @property
    def title(self):
        return self._vehicle.title

    @property
    def model(self):
        return self._vehicle.model

    @property
    def model_year(self):
        return self._vehicle.model_year

    @property
    def model_family(self):
        return self._vehicle.model_family

    async def call_update(self, func, ntries: int):
        try:
            await func()
        except TimeoutError:
            if ntries > 1:
                await asyncio.sleep(2)
                await self.call_update(func, ntries - 1)
            else:
                raise

    async def update(self):
        info = ""
        try:
            self._no_error = True
            info = "statusreport"
            await self.call_update(self.update_vehicle_statusreport, 3)
            info = "shortterm"
            await self.call_update(self.update_vehicle_shortterm, 3)
            info = "longterm"
            await self.call_update(self.update_vehicle_longterm, 3)
            info = "position"
            await self.call_update(self.update_vehicle_position, 3)
            info = "climater"
            await self.call_update(self.update_vehicle_climater, 3)
            # info = "charger"
            # await self.call_update(self.update_vehicle_charger, 3)
            info = "preheater"
            await self.call_update(self.update_vehicle_preheater, 3)
            # Return True on success, False on error
            return self._no_error
        except Exception as exception:
            log_exception(
                exception,
                "Unable to update vehicle data {} of {}".format(
                    info, self._vehicle.vin
                ),
            )

    def log_exception_once(self, exception, message):
        self._no_error = False
        err = message + ": " + str(exception).rstrip("\n")
        if err not in self._logged_errors:
            self._logged_errors.add(err)
            _LOGGER.error(err, exc_info=True)

    async def update_vehicle_statusreport(self):
        if not self.support_status_report:
            return

        try:
            status = await self._audi_service.get_stored_vehicle_data(self._vehicle.vin)
            self._vehicle.fields = {
                status.data_fields[i].name: status.data_fields[i].value
                for i in range(len(status.data_fields))
            }

            # Initialize with a default very old datetime
            self._vehicle.state["last_update_time"] = datetime(
                1970, 1, 1, tzinfo=timezone.utc
            )

            # Update with the newest carCapturedTimestamp from data_fields
            for f in status.data_fields:
                new_time = parse_datetime(f.measure_time)
                if new_time:
                    self._vehicle.state["last_update_time"] = max(
                        self._vehicle.state["last_update_time"], new_time
                    )

            # Update with the newest carCapturedTimestamp from states
            for state in status.states:
                new_time = parse_datetime(state.get("measure_time"))
                if new_time:
                    self._vehicle.state["last_update_time"] = max(
                        self._vehicle.state["last_update_time"], new_time
                    )

            # Update other states
            for state in status.states:
                self._vehicle.state[state["name"]] = state["value"]

        except TimeoutError:
            raise
        except ClientResponseError as resp_exception:
            if resp_exception.status in (403, 404):
                self.support_status_report = False
            else:
                self.log_exception_once(
                    resp_exception,
                    "Unable to obtain the vehicle status report of {}".format(
                        self._vehicle.vin
                    ),
                )
        except Exception as exception:
            self.log_exception_once(
                exception,
                "Unable to obtain the vehicle status report of {}".format(
                    self._vehicle.vin
                ),
            )

    async def update_vehicle_position(self):
        # Redact all but the last 4 characters of the VIN
        redacted_vin = "*" * (len(self._vehicle.vin) - 4) + self._vehicle.vin[-4:]
        _LOGGER.debug(
            "POSITION: Starting update_vehicle_position for VIN: %s", redacted_vin
        )

        if not self.support_position:
            _LOGGER.debug(
                "POSITION: Vehicle position support is disabled for VIN: %s. Exiting update process.",
                redacted_vin,
            )
            return

        try:
            _LOGGER.debug(
                "POSITION: Attempting to retrieve stored vehicle position for VIN: %s",
                redacted_vin,
            )
            resp = await self._audi_service.get_stored_position(self._vehicle.vin)
            # To enable detailed logging of raw vehicle position data for debugging purposes:
            # 1. Remove the '#' from the start of the _LOGGER.debug line below.
            # 2. Save the file.
            # 3. Restart Home Assistant to apply the changes.
            # Note: This will log sensitive data. To stop logging this data:
            # 1. Add the '#' back at the start of the _LOGGER.debug line.
            # 2. Save the file and restart Home Assistant again.
            # _LOGGER.debug("POSITION - UNREDACTED SENSITIVE DATA: Raw vehicle position data: %s", resp)
            if resp is not None:
                redacted_lat = re.sub(r"\d", "#", str(resp["data"]["lat"]))
                redacted_lon = re.sub(r"\d", "#", str(resp["data"]["lon"]))

                # Check if 'carCapturedTimestamp' is available in the data
                if "carCapturedTimestamp" in resp["data"]:
                    timestamp = parse_datetime(resp["data"]["carCapturedTimestamp"])
                    parktime = parse_datetime(resp["data"]["carCapturedTimestamp"])
                else:
                    # Log and use None timestamp and parktime
                    timestamp = None
                    parktime = None
                    _LOGGER.debug(
                        "POSITION: Timestamp not available for vehicle position data of VIN: %s.",
                        redacted_vin,
                    )
                _LOGGER.debug(
                    "POSITION: Vehicle position data received for VIN: %s, lat: %s, lon: %s, timestamp: %s, parktime: %s",
                    redacted_vin,
                    redacted_lat,
                    redacted_lon,
                    timestamp,
                    parktime,
                )

                self._vehicle.state["position"] = {
                    "latitude": resp["data"]["lat"],
                    "longitude": resp["data"]["lon"],
                    "timestamp": timestamp,
                    "parktime": parktime,
                }

                self._vehicle.state["is_moving"] = False

                _LOGGER.debug(
                    "POSITION: Vehicle position updated successfully for VIN: %s",
                    redacted_vin,
                )
            else:
                _LOGGER.warning(
                    "POSITION: No vehicle position data received for VIN: %s. Response was None.",
                    redacted_vin,
                )

        except TimeoutError:
            _LOGGER.warning(
                "POSITION: TimeoutError encountered while updating vehicle position for VIN: %s.",
                redacted_vin,
            )
            raise
        except ClientResponseError as cre:
            if cre.status in (403, 404):
                _LOGGER.debug(
                    "POSITION: ClientResponseError with status %s for VIN: %s. Vehicle does not support position — disabling.",
                    cre.status,
                    redacted_vin,
                )
                self.support_position = False
            elif cre.status == 502:
                _LOGGER.debug(
                    "POSITION: Received status %s while updating vehicle position for VIN: %s. This is typically transient and may resolve on its own.",
                    cre.status,
                    redacted_vin,
                )
            elif cre.status != 204:
                _LOGGER.warning(
                    "POSITION: ClientResponseError with status %s for VIN: %s. Error: %s",
                    cre.status,
                    redacted_vin,
                    cre,
                )
            else:
                _LOGGER.debug(
                    "POSITION: Vehicle position currently not available for VIN: %s (Is moving?!). Received 204 status.",
                    redacted_vin,
                )
                # we receive a 204 when the vehicle is moving.
                self._vehicle.state["is_moving"] = True

        except Exception as e:
            _LOGGER.error(
                "POSITION: An unexpected error occurred while updating vehicle position for VIN: %s. Error: %s",
                redacted_vin,
                e,
            )

    async def update_vehicle_climater(self):
        redacted_vin = "*" * (len(self._vehicle.vin) - 4) + self._vehicle.vin[-4:]
        if not self.support_climater:
            return

        try:
            result = await self._audi_service.get_climater(self._vehicle.vin)
            if result:
                self._vehicle.state["climatisationState"] = get_attr(
                    result,
                    "climater.status.climatisationStatusData.climatisationState.content",
                )
                tmp = get_attr(
                    result,
                    "climater.status.temperatureStatusData.outdoorTemperature.content",
                )
                if tmp is not None:
                    self._vehicle.state["outdoorTemperature"] = round(
                        float(tmp) / 10 - 273, 1
                    )
                else:
                    self._vehicle.state["outdoorTemperature"] = None

                remainingClimatisationTime = get_attr(
                    result,
                    "climater.status.climatisationStatusData.remainingClimatisationTime.content",
                )
                self._vehicle.state["remainingClimatisationTime"] = (
                    remainingClimatisationTime
                )
                _LOGGER.debug(
                    "CLIMATER: remainingClimatisationTime: %s",
                    remainingClimatisationTime,
                )

                vehicleParkingClock = get_attr(
                    result,
                    "climater.status.vehicleParkingClockStatusData.vehicleParkingClock.content",
                )
                self._vehicle.state["vehicleParkingClock"] = parse_datetime(
                    vehicleParkingClock
                )
                _LOGGER.debug("CLIMATER: vehicleParkingClock: %s", vehicleParkingClock)

                isMirrorHeatingActive = get_attr(
                    result,
                    "climater.status.climatisationStatusData.climatisationElementStates.isMirrorHeatingActive.content",
                )
                self._vehicle.state["isMirrorHeatingActive"] = isMirrorHeatingActive
                _LOGGER.debug(
                    "CLIMATER: isMirrorHeatingActive: %s", isMirrorHeatingActive
                )

            else:
                _LOGGER.debug(
                    "No climater data received for VIN: %s. Response was None.",
                    redacted_vin,
                )

        except TimeoutError:
            _LOGGER.debug(
                "TimeoutError encountered while updating climater for VIN: %s.",
                redacted_vin,
            )
            raise
        except ClientResponseError as cre:
            if cre.status in (403, 404):
                _LOGGER.debug(
                    "CLIMATER: ClientResponseError with status %s for VIN: %s. Vehicle does not support climater — disabling.",
                    cre.status,
                    redacted_vin,
                )
                self.support_climater = False
            elif cre.status == 502:
                _LOGGER.debug(
                    "CLIMATER: Received status %s while updating climater for VIN: %s. This is typically transient and may resolve on its own.",
                    cre.status,
                    redacted_vin,
                )
            elif cre.status != 204:
                _LOGGER.debug(
                    "ClientResponseError with status %s while updating climater for VIN: %s. Error: %s",
                    cre.status,
                    redacted_vin,
                    cre,
                )
            else:
                _LOGGER.debug(
                    "Climater currently not available for VIN: %s. Received 204 status.",
                    redacted_vin,
                )

        except Exception as e:
            _LOGGER.error(
                "An unexpected error occurred while updating climater for VIN: %s. Error: %s",
                redacted_vin,
                e,
            )

    async def update_vehicle_preheater(self):
        redacted_vin = "*" * (len(self._vehicle.vin) - 4) + self._vehicle.vin[-4:]
        if not self.support_preheater:
            return

        try:
            result = await self._audi_service.get_preheater(self._vehicle.vin)
            if result:
                self._vehicle.state["preheaterState"] = get_attr(
                    result,
                    "statusResponse",
                )

        except TimeoutError:
            raise
        except ClientResponseError as cre:
            if cre.status in (403, 404, 502):
                _LOGGER.debug(
                    "PREHEATER: ClientResponseError with status %s for VIN: %s. Vehicle does not support preheater — disabling.",
                    cre.status,
                    redacted_vin,
                )
                self.support_preheater = False
            # elif cre.status == 502:
            #    _LOGGER.warning(
            #        "PREHEATER: ClientResponseError with status %s while updating preheater for VIN: %s. This issue may resolve in time. If it persists, please open an issue.",
            #        cre.status,
            #        redacted_vin,
            #    )
            else:
                self.log_exception_once(
                    cre,
                    "Unable to obtain the vehicle preheater state for {}".format(
                        self._vehicle.vin
                    ),
                )
        except Exception as exception:
            self.log_exception_once(
                exception,
                "Unable to obtain the vehicle preheater state for {}".format(
                    self._vehicle.vin
                ),
            )

    async def update_vehicle_charger(self):
        redacted_vin = "*" * (len(self._vehicle.vin) - 4) + self._vehicle.vin[-4:]
        if not self.support_charger:
            return

        try:
            result = await self._audi_service.get_charger(self._vehicle.vin)
            if result:
                self._vehicle.state["maxChargeCurrent"] = get_attr(
                    result, "charger.settings.maxChargeCurrent.content"
                )

                self._vehicle.state["chargingState"] = get_attr(
                    result, "charger.status.chargingStatusData.chargingState.content"
                )
                self._vehicle.state["actualChargeRate"] = get_attr(
                    result, "charger.status.chargingStatusData.actualChargeRate.content"
                )
                if self._vehicle.state["actualChargeRate"] is not None:
                    self._vehicle.state["actualChargeRate"] = float(
                        self._vehicle.state["actualChargeRate"]
                    )
                self._vehicle.state["actualChargeRateUnit"] = get_attr(
                    result, "charger.status.chargingStatusData.chargeRateUnit.content"
                )
                self._vehicle.state["chargingPower"] = get_attr(
                    result, "charger.status.chargingStatusData.chargingPower.content"
                )
                self._vehicle.state["chargingMode"] = get_attr(
                    result, "charger.status.chargingStatusData.chargingMode.content"
                )

                self._vehicle.state["energyFlow"] = get_attr(
                    result, "charger.status.chargingStatusData.energyFlow.content"
                )

                self._vehicle.state["engineTypeFirstEngine"] = get_attr(
                    result,
                    "charger.status.cruisingRangeStatusData.engineTypeFirstEngine.content",
                )
                self._vehicle.state["engineTypeSecondEngine"] = get_attr(
                    result,
                    "charger.status.cruisingRangeStatusData.engineTypeSecondEngine.content",
                )
                self._vehicle.state["hybridRange"] = get_attr(
                    result, "charger.status.cruisingRangeStatusData.hybridRange.content"
                )
                self._vehicle.state["primaryEngineRange"] = get_attr(
                    result,
                    "charger.status.cruisingRangeStatusData.primaryEngineRange.content",
                )
                self._vehicle.state["secondaryEngineRange"] = get_attr(
                    result,
                    "charger.status.cruisingRangeStatusData.secondaryEngineRange.content",
                )

                self._vehicle.state["stateOfCharge"] = get_attr(
                    result, "charger.status.batteryStatusData.stateOfCharge.content"
                )
                self._vehicle.state["remainingChargingTime"] = get_attr(
                    result,
                    "charger.status.batteryStatusData.remainingChargingTime.content",
                )
                self._vehicle.state["plugState"] = get_attr(
                    result, "charger.status.plugStatusData.plugState.content"
                )
                self._vehicle.state["plugLockState"] = get_attr(
                    result, "charger.status.plugStatusData.plugLockState.content"
                )
                self._vehicle.state["externalPower"] = get_attr(
                    result, "charger.status.plugStatusData.externalPower.content"
                )
                self._vehicle.state["plugledColor"] = get_attr(
                    result, "charger.status.plugStatusData.plugledColor.content"
                )

        except TimeoutError:
            raise
        except ClientResponseError as cre:
            if cre.status in (403, 404):
                _LOGGER.debug(
                    "CHARGER: ClientResponseError with status %s for VIN: %s. Vehicle does not support charger — disabling.",
                    cre.status,
                    redacted_vin,
                )
                self.support_charger = False
            elif cre.status == 502:
                _LOGGER.debug(
                    "CHARGER: Received status %s while updating charger for VIN: %s. This is typically transient and may resolve on its own.",
                    cre.status,
                    redacted_vin,
                )
            else:
                self.log_exception_once(
                    cre,
                    "Unable to obtain the vehicle charger state for {}".format(
                        self._vehicle.vin
                    ),
                )
        except Exception as exception:
            self.log_exception_once(
                exception,
                "Unable to obtain the vehicle charger state for {}".format(
                    self._vehicle.vin
                ),
            )

    async def update_vehicle_longterm(self):
        await self.update_vehicle_tripdata("longTerm")

    async def update_vehicle_shortterm(self):
        await self.update_vehicle_tripdata("shortTerm")

    async def update_vehicle_tripdata(self, kind: str):
        redacted_vin = "*" * (len(self._vehicle.vin) - 4) + self._vehicle.vin[-4:]
        if not self.support_trip_data:
            _LOGGER.debug(
                "TRIP DATA: Trip data support is disabled for VIN: %s. Exiting update process.",
                redacted_vin,
            )
            return
        try:
            td_cur, td_rst = await self._audi_service.get_tripdata(
                self._vehicle.vin, kind
            )
            self._vehicle.state[kind.lower() + "_current"] = {
                "tripID": td_cur.tripID,
                "averageElectricEngineConsumption": td_cur.averageElectricEngineConsumption,
                "averageFuelConsumption": td_cur.averageFuelConsumption,
                "averageSpeed": td_cur.averageSpeed,
                "mileage": td_cur.mileage,
                "startMileage": td_cur.startMileage,
                "traveltime": td_cur.traveltime,
                "timestamp": td_cur.timestamp,
                "overallMileage": td_cur.overallMileage,
                "zeroEmissionDistance": td_cur.zeroEmissionDistance,
            }
            self._vehicle.state[kind.lower() + "_reset"] = {
                "tripID": td_rst.tripID,
                "averageElectricEngineConsumption": td_rst.averageElectricEngineConsumption,
                "averageFuelConsumption": td_rst.averageFuelConsumption,
                "averageSpeed": td_rst.averageSpeed,
                "mileage": td_rst.mileage,
                "startMileage": td_rst.startMileage,
                "traveltime": td_rst.traveltime,
                "timestamp": td_rst.timestamp,
                "overallMileage": td_rst.overallMileage,
                "zeroEmissionDistance": td_rst.zeroEmissionDistance,
            }

        except TimeoutError:
            _LOGGER.debug(
                "TRIP DATA: TimeoutError encountered while updating trip data for VIN: %s.",
                redacted_vin,
            )
            raise
        except ClientResponseError as cre:
            if cre.status in (403, 404):
                _LOGGER.debug(
                    "TRIP DATA: ClientResponseError with status %s for VIN: %s. Vehicle does not support trip data — disabling.",
                    cre.status,
                    redacted_vin,
                )
                self.support_trip_data = False
            elif cre.status == 502:
                _LOGGER.debug(
                    "TRIP DATA: Received status %s while updating trip data for VIN: %s. This is typically transient and may resolve on its own.",
                    cre.status,
                    redacted_vin,
                )
            elif cre.status != 204:
                _LOGGER.debug(
                    "TRIP DATA: ClientResponseError with status %s while updating trip data for VIN: %s. Error: %s",
                    cre.status,
                    redacted_vin,
                    cre,
                )
            else:
                _LOGGER.debug(
                    "TRIP DATA: Trip data currently not available for VIN: %s. Received 204 status.",
                    redacted_vin,
                )

        except Exception as e:
            _LOGGER.error(
                "TRIP DATA: An unexpected error occurred while updating trip data for VIN: %s. Error: %s",
                redacted_vin,
                e,
            )

    @property
    def last_update_time(self):
        if self.last_update_time_supported:
            return self._vehicle.state.get("last_update_time")

    @property
    def last_update_time_supported(self):
        check = self._vehicle.state.get("last_update_time")
        if check:
            return True

    @property
    def service_inspection_time(self):
        """Return time left for service inspection"""
        if self.service_inspection_time_supported:
            return int(
                self._vehicle.fields.get("MAINTENANCE_INTERVAL_TIME_TO_INSPECTION")
            )

    @property
    def service_inspection_time_supported(self):
        check = self._vehicle.fields.get("MAINTENANCE_INTERVAL_TIME_TO_INSPECTION")
        if check and parse_int(check):
            return True

    @property
    def service_inspection_distance(self):
        """Return distance left for service inspection"""
        if self.service_inspection_distance_supported:
            return int(
                self._vehicle.fields.get("MAINTENANCE_INTERVAL_DISTANCE_TO_INSPECTION")
            )

    @property
    def service_inspection_distance_supported(self):
        check = self._vehicle.fields.get("MAINTENANCE_INTERVAL_DISTANCE_TO_INSPECTION")
        if check and parse_int(check):
            return True

    @property
    def service_adblue_distance(self):
        """Return distance left for service inspection"""
        if self.service_adblue_distance_supported:
            return int(self._vehicle.fields.get("ADBLUE_RANGE"))

    @property
    def service_adblue_distance_supported(self):
        check = self._vehicle.fields.get("ADBLUE_RANGE")
        if check and parse_int(check):
            return True

    @property
    def oil_change_time(self):
        """Return time left for oil change"""
        if self.oil_change_time_supported:
            return int(
                self._vehicle.fields.get("MAINTENANCE_INTERVAL_TIME_TO_OIL_CHANGE")
            )

    @property
    def oil_change_time_supported(self):
        check = self._vehicle.fields.get("MAINTENANCE_INTERVAL_TIME_TO_OIL_CHANGE")
        if check and parse_int(check):
            return True

    @property
    def oil_change_distance(self):
        """Return distance left for oil change"""
        if self.oil_change_distance_supported:
            return int(
                self._vehicle.fields.get("MAINTENANCE_INTERVAL_DISTANCE_TO_OIL_CHANGE")
            )

    @property
    def oil_change_distance_supported(self):
        check = self._vehicle.fields.get("MAINTENANCE_INTERVAL_DISTANCE_TO_OIL_CHANGE")
        if check and parse_int(check):
            return True

    @property
    def oil_level(self):
        """Return oil level percentage"""
        if self.oil_level_supported:
            return parse_float(
                self._vehicle.fields.get("OIL_LEVEL_DIPSTICKS_PERCENTAGE")
            )

    @property
    def oil_level_supported(self):
        """Check if oil level is supported."""
        check = self._vehicle.fields.get("OIL_LEVEL_DIPSTICKS_PERCENTAGE")
        return not isinstance(check, bool) and check is not None

    @property
    def oil_level_binary(self):
        """Return oil level binary."""
        if self.oil_level_binary_supported:
            return not self._vehicle.fields.get("OIL_LEVEL_DIPSTICKS_PERCENTAGE")

    @property
    def oil_level_binary_supported(self):
        """Check if oil level binary is supported."""
        return isinstance(
            self._vehicle.fields.get("OIL_LEVEL_DIPSTICKS_PERCENTAGE"), bool
        )

    @property
    def preheater_active(self):
        if self.preheater_active_supported:
            res = (
                self._vehicle.state["preheaterState"]
                .get("climatisationStateReport")
                .get("climatisationState")
            )
            return res != "off"

    @property
    def preheater_active_supported(self):
        return self.preheater_state_supported

    @property
    def preheater_duration(self):
        if self.preheater_duration_supported:
            res = (
                self._vehicle.state["preheaterState"]
                .get("climatisationStateReport")
                .get("climatisationDuration")
            )
            return parse_int(res)

    @property
    def preheater_duration_supported(self):
        return self.preheater_state_supported

    @property
    def preheater_remaining_supported(self):
        return self.preheater_state_supported

    @property
    def preheater_remaining(self):
        if self.preheater_remaining_supported:
            res = (
                self._vehicle.state["preheaterState"]
                .get("climatisationStateReport")
                .get("remainingClimateTime")
            )
            return parse_int(res)

    @property
    def parking_light(self):
        """Return true if parking light is on"""
        if self.parking_light_supported:
            try:
                check = self._vehicle.fields.get("LIGHT_STATUS")
                return check[0]["status"] != "off" or check[1]["status"] != "off"
            except KeyError:
                return False

    @property
    def parking_light_supported(self):
        """Return true if parking light is supported"""
        check = self._vehicle.fields.get("LIGHT_STATUS")
        if check:
            return True

    @property
    def braking_status(self):
        """Return true if braking status is on"""
        if self.braking_status_supported:
            check = self._vehicle.fields.get("BRAKING_STATUS")
            return check != "2"

    @property
    def braking_status_supported(self):
        """Return true if braking status is supported"""
        check = self._vehicle.fields.get("BRAKING_STATUS")
        if check:
            return True

    @property
    def mileage(self):
        if self.mileage_supported:
            check = self._vehicle.fields.get("UTC_TIME_AND_KILOMETER_STATUS")
            return parse_int(check)

    @property
    def mileage_supported(self):
        """Return true if mileage is supported"""
        check = self._vehicle.fields.get("UTC_TIME_AND_KILOMETER_STATUS")
        if check and parse_int(check):
            return True

    @property
    def range(self):
        if self.range_supported:
            check = self._vehicle.fields.get("TOTAL_RANGE")
            return parse_int(check)

    @property
    def range_supported(self):
        """Return true if range is supported"""
        check = self._vehicle.fields.get("TOTAL_RANGE")
        if check and parse_int(check):
            return True

    @property
    def tank_level(self):
        if self.tank_level_supported:
            check = self._vehicle.fields.get("TANK_LEVEL_IN_PERCENTAGE")
            return parse_int(check)

    @property
    def tank_level_supported(self):
        """Return true if tank_level is supported"""
        check = self._vehicle.fields.get("TANK_LEVEL_IN_PERCENTAGE")
        if check and parse_int(check):
            return True

    @property
    def position(self):
        """Return position."""
        if self.position_supported:
            return self._vehicle.state.get("position")

    @property
    def position_supported(self):
        """Return true if vehicle has position."""
        check = self._vehicle.state.get("position")
        if check:
            return True

    @property
    def any_window_open_supported(self):
        """Return true if window state is supported"""
        checkLeftFront = self._vehicle.fields.get("STATE_LEFT_FRONT_WINDOW")
        checkLeftRear = self._vehicle.fields.get("STATE_LEFT_REAR_WINDOW")
        checkRightFront = self._vehicle.fields.get("STATE_RIGHT_FRONT_WINDOW")
        checkRightRear = self._vehicle.fields.get("STATE_RIGHT_REAR_WINDOW")
        checkSunRoof = self._vehicle.fields.get("STATE_SUN_ROOF_MOTOR_COVER", None)
        checkRoofCover = self._vehicle.fields.get("STATE_ROOF_COVER_WINDOW", None)
        acceptable_window_states = ["3", "0", None]
        if (
            checkLeftFront
            and checkLeftRear
            and checkRightFront
            and checkRightRear
            and (checkSunRoof in acceptable_window_states)
            and (checkRoofCover in acceptable_window_states)
        ):
            return True

    @property
    def any_window_open(self):
        if self.any_window_open_supported:
            checkLeftFront = self._vehicle.fields.get("STATE_LEFT_FRONT_WINDOW")
            checkLeftRear = self._vehicle.fields.get("STATE_LEFT_REAR_WINDOW")
            checkRightFront = self._vehicle.fields.get("STATE_RIGHT_FRONT_WINDOW")
            checkRightRear = self._vehicle.fields.get("STATE_RIGHT_REAR_WINDOW")
            checkSunRoof = self._vehicle.fields.get("STATE_SUN_ROOF_MOTOR_COVER", None)
            checkRoofCover = self._vehicle.fields.get("STATE_ROOF_COVER_WINDOW", None)
            acceptable_window_states = ["3", None]
            return not (
                checkLeftFront == "3"
                and checkLeftRear == "3"
                and checkRightFront == "3"
                and checkRightRear == "3"
                and (checkSunRoof in acceptable_window_states)
                and (checkRoofCover in acceptable_window_states)
            )

    @property
    def left_front_window_open_supported(self):
        return self._vehicle.fields.get("STATE_LEFT_FRONT_WINDOW")

    @property
    def left_front_window_open(self):
        if self.left_front_window_open_supported:
            return self._vehicle.fields.get("STATE_LEFT_FRONT_WINDOW") != "3"

    @property
    def right_front_window_open_supported(self):
        return self._vehicle.fields.get("STATE_RIGHT_FRONT_WINDOW")

    @property
    def right_front_window_open(self):
        if self.right_front_window_open_supported:
            return self._vehicle.fields.get("STATE_RIGHT_FRONT_WINDOW") != "3"

    @property
    def left_rear_window_open_supported(self):
        return self._vehicle.fields.get("STATE_LEFT_REAR_WINDOW")

    @property
    def left_rear_window_open(self):
        if self.left_rear_window_open_supported:
            return self._vehicle.fields.get("STATE_LEFT_REAR_WINDOW") != "3"

    @property
    def right_rear_window_open_supported(self):
        return self._vehicle.fields.get("STATE_RIGHT_REAR_WINDOW")

    @property
    def right_rear_window_open(self):
        if self.right_rear_window_open_supported:
            return self._vehicle.fields.get("STATE_RIGHT_REAR_WINDOW") != "3"

    @property
    def sun_roof_supported(self):
        return self._vehicle.fields.get("STATE_SUN_ROOF_MOTOR_COVER")

    @property
    def sun_roof(self):
        if self.sun_roof_supported:
            return self._vehicle.fields.get("STATE_SUN_ROOF_MOTOR_COVER") != "3"

    @property
    def roof_cover_supported(self):
        return self._vehicle.fields.get("STATE_ROOF_COVER_WINDOW")

    @property
    def roof_cover(self):
        if self.roof_cover_supported:
            return self._vehicle.fields.get("STATE_ROOF_COVER_WINDOW") != "3"

    @property
    def any_door_unlocked_supported(self):
        checkLeftFront = self._vehicle.fields.get("LOCK_STATE_LEFT_FRONT_DOOR")
        checkLeftRear = self._vehicle.fields.get("LOCK_STATE_LEFT_REAR_DOOR")
        checkRightFront = self._vehicle.fields.get("LOCK_STATE_RIGHT_FRONT_DOOR")
        checkRightRear = self._vehicle.fields.get("LOCK_STATE_RIGHT_REAR_DOOR")
        if checkLeftFront and checkLeftRear and checkRightFront and checkRightRear:
            return True

    @property
    def any_door_unlocked(self):
        if self.any_door_unlocked_supported:
            checkLeftFront = self._vehicle.fields.get("LOCK_STATE_LEFT_FRONT_DOOR")
            checkLeftRear = self._vehicle.fields.get("LOCK_STATE_LEFT_REAR_DOOR")
            checkRightFront = self._vehicle.fields.get("LOCK_STATE_RIGHT_FRONT_DOOR")
            checkRightRear = self._vehicle.fields.get("LOCK_STATE_RIGHT_REAR_DOOR")
            return not (
                checkLeftFront == "2"
                and checkLeftRear == "2"
                and checkRightFront == "2"
                and checkRightRear == "2"
            )

    @property
    def any_door_open_supported(self):
        checkLeftFront = self._vehicle.fields.get("OPEN_STATE_LEFT_FRONT_DOOR")
        checkLeftRear = self._vehicle.fields.get("OPEN_STATE_LEFT_REAR_DOOR")
        checkRightFront = self._vehicle.fields.get("OPEN_STATE_RIGHT_FRONT_DOOR")
        checkRightRear = self._vehicle.fields.get("OPEN_STATE_RIGHT_REAR_DOOR")
        if checkLeftFront and checkLeftRear and checkRightFront and checkRightRear:
            return True

    @property
    def any_door_open(self):
        if self.any_door_open_supported:
            checkLeftFront = self._vehicle.fields.get("OPEN_STATE_LEFT_FRONT_DOOR")
            checkLeftRear = self._vehicle.fields.get("OPEN_STATE_LEFT_REAR_DOOR")
            checkRightFront = self._vehicle.fields.get("OPEN_STATE_RIGHT_FRONT_DOOR")
            checkRightRear = self._vehicle.fields.get("OPEN_STATE_RIGHT_REAR_DOOR")
            return not (
                checkLeftFront == "3"
                and checkLeftRear == "3"
                and checkRightFront == "3"
                and checkRightRear == "3"
            )

    @property
    def left_front_door_open_supported(self):
        return self._vehicle.fields.get("OPEN_STATE_LEFT_FRONT_DOOR")

    @property
    def left_front_door_open(self):
        if self.left_front_door_open_supported:
            return self._vehicle.fields.get("OPEN_STATE_LEFT_FRONT_DOOR") != "3"

    @property
    def right_front_door_open_supported(self):
        return self._vehicle.fields.get("OPEN_STATE_RIGHT_FRONT_DOOR")

    @property
    def right_front_door_open(self):
        if self.right_front_door_open_supported:
            return self._vehicle.fields.get("OPEN_STATE_RIGHT_FRONT_DOOR") != "3"

    @property
    def left_rear_door_open_supported(self):
        return self._vehicle.fields.get("OPEN_STATE_LEFT_REAR_DOOR")

    @property
    def left_rear_door_open(self):
        if self.left_rear_door_open_supported:
            return self._vehicle.fields.get("OPEN_STATE_LEFT_REAR_DOOR") != "3"

    @property
    def right_rear_door_open_supported(self):
        return self._vehicle.fields.get("OPEN_STATE_RIGHT_REAR_DOOR")

    @property
    def right_rear_door_open(self):
        if self.right_rear_door_open_supported:
            return self._vehicle.fields.get("OPEN_STATE_RIGHT_REAR_DOOR") != "3"

    @property
    def doors_trunk_status_supported(self):
        return (
            self.any_door_open_supported
            and self.any_door_unlocked_supported
            and self.trunk_open_supported
            and self.trunk_unlocked_supported
        )

    @property
    def doors_trunk_status(self):
        if (
            self.any_door_open_supported
            and self.any_door_unlocked_supported
            and self.trunk_open_supported
            and self.trunk_unlocked_supported
        ):
            if self.any_door_open or self.trunk_open:
                return "Open"
            elif self.any_door_unlocked or self.trunk_unlocked:
                return "Closed"
            else:
                return "Locked"

    @property
    def trunk_unlocked(self):
        if self.trunk_unlocked_supported:
            check = self._vehicle.fields.get("LOCK_STATE_TRUNK_LID")
            return check != "2"

    @property
    def trunk_unlocked_supported(self):
        check = self._vehicle.fields.get("LOCK_STATE_TRUNK_LID")
        if check:
            return True

    @property
    def trunk_open(self):
        if self.trunk_open_supported:
            check = self._vehicle.fields.get("OPEN_STATE_TRUNK_LID")
            return check != "3"

    @property
    def trunk_open_supported(self):
        check = self._vehicle.fields.get("OPEN_STATE_TRUNK_LID")
        if check:
            return True

    @property
    def hood_open(self):
        if self.hood_open_supported:
            check = self._vehicle.fields.get("OPEN_STATE_HOOD")
            return check != "3"

    @property
    def hood_open_supported(self):
        check = self._vehicle.fields.get("OPEN_STATE_HOOD")
        if check:
            return True

    @property
    def charging_state(self):
        """Return charging state"""
        if self.charging_state_supported:
            return self._vehicle.state.get("chargingState")

    @property
    def charging_state_supported(self):
        check = self._vehicle.state.get("chargingState")
        if check:
            return True

    @property
    def charging_mode(self):
        """Return charging mode"""
        if self.charging_mode_supported:
            return self._vehicle.state.get("chargeMode")

    @property
    def charging_mode_supported(self):
        check = self._vehicle.state.get("chargeMode")
        return check is not None and check != "unsupported"

    @property
    def energy_flow(self):
        """Return charging mode"""
        if self.energy_flow_supported:
            return self._vehicle.state.get("energyFlow")

    @property
    def energy_flow_supported(self):
        check = self._vehicle.state.get("energyFlow")
        if check is not None:
            return True

    @property
    def charging_type(self):
        """Return charging type"""
        if self.charging_type_supported:
            return self._vehicle.state.get("chargeType")

    @property
    def charging_type_supported(self):
        check = self._vehicle.state.get("chargeType")
        if check and check != "unsupported":
            return True

    @property
    def max_charge_current(self):
        """Return max charge current"""
        if self.max_charge_current_supported:
            try:
                return parse_float(self._vehicle.state.get("maxChargeCurrent"))
            except ValueError:
                return -1

    @property
    def max_charge_current_supported(self):
        check = self._vehicle.state.get("maxChargeCurrent")
        if check is not None:
            return True

    @property
    def actual_charge_rate(self):
        """Return actual charge rate"""
        if self.actual_charge_rate_supported:
            try:
                return parse_float(self._vehicle.state.get("actualChargeRate"))
            except ValueError:
                return -1

    @property
    def actual_charge_rate_supported(self):
        check = self._vehicle.state.get("actualChargeRate")
        if check is not None:
            return True

    @property
    def actual_charge_rate_unit(self):
        return "km/h"

    @property
    def charging_power(self):
        """Return charging power"""
        if self.charging_power_supported:
            try:
                return parse_float(self._vehicle.state.get("chargingPower"))
            except ValueError:
                return -1

    @property
    def charging_power_supported(self):
        check = self._vehicle.state.get("chargingPower")
        if check is not None:
            return True

    @property
    def primary_engine_type(self):
        """Return primary engine type"""
        if self.primary_engine_type_supported:
            return self._vehicle.state.get("engineTypeFirstEngine")

    @property
    def primary_engine_type_supported(self):
        check = self._vehicle.state.get("engineTypeFirstEngine")
        if check and check != "unsupported":
            return True

    @property
    def secondary_engine_type(self):
        """Return secondary engine type"""
        if self.secondary_engine_type_supported:
            return self._vehicle.state.get("engineTypeSecondEngine")

    @property
    def secondary_engine_type_supported(self):
        check = self._vehicle.state.get("engineTypeSecondEngine")
        if check and check != "unsupported":
            return True

    @property
    def primary_engine_range(self):
        """Return primary engine range"""
        if self.primary_engine_range_supported:
            return self._vehicle.state.get("primaryEngineRange")

    @property
    def primary_engine_range_supported(self):
        check = self._vehicle.state.get("primaryEngineRange")
        if check and check != "unsupported":
            return True

    @property
    def primary_engine_range_percent(self):
        """Return primary engine range"""
        if self.primary_engine_range_percent_supported:
            return self._vehicle.state.get("primaryEngineRangePercent")

    @property
    def primary_engine_range_percent_supported(self):
        check = self._vehicle.state.get("primaryEngineRangePercent")
        if check and check != "unsupported":
            return True

    @property
    def secondary_engine_range(self):
        """Return secondary engine range"""
        if self.secondary_engine_range_supported:
            return self._vehicle.state.get("secondaryEngineRange")

    @property
    def secondary_engine_range_supported(self):
        check = self._vehicle.state.get("secondaryEngineRange")
        if check is not None and check != "unsupported":
            return True

    @property
    def car_type(self):
        """Return secondary engine range"""
        if self.car_type_supported:
            return self._vehicle.state.get("carType")

    @property
    def car_type_supported(self):
        check = self._vehicle.state.get("carType")
        if check and check != "unsupported":
            return True

    @property
    def secondary_engine_range_percent(self):
        """Return secondary engine range"""
        if self.secondary_engine_range_percent_supported:
            return self._vehicle.state.get("secondaryEngineRangePercent")

    @property
    def secondary_engine_range_percent_supported(self):
        check = self._vehicle.state.get("secondaryEngineRangePercent")
        if check and check != "unsupported":
            return True

    @property
    def hybrid_range(self):
        """Return hybrid range"""
        if self.hybrid_range_supported:
            return self._vehicle.state.get("hybridRange")

    @property
    def hybrid_range_supported(self):
        check = self._vehicle.state.get("hybridRange")
        if check and check != "unsupported":
            return True

    @property
    def state_of_charge(self):
        """Return state of charge"""
        if self.state_of_charge_supported:
            return parse_int(self._vehicle.state.get("stateOfCharge"))

    @property
    def state_of_charge_supported(self):
        return parse_int(self._vehicle.state.get("stateOfCharge")) is not None

    @property
    def remaining_charging_time(self):
        """Return remaining charging time"""
        if self.remaining_charging_time_supported:
            return self._vehicle.state.get("remainingChargingTime", 0)

    @property
    def remaining_charging_time_unit(self):
        return "min"

    @property
    def remaining_charging_time_supported(self):
        return self.car_type in ["hybrid", "electric"]

    @property
    def charging_complete_time(self):
        """Return the datetime when charging is or was expected to be complete."""
        # Check if remaining charging time is not supported
        if not self.remaining_charging_time_supported:
            return None
        # If there's no last update or remaining time, we can't calculate
        if self.last_update_time is None or self.remaining_charging_time is None:
            return None
        # Calculate the complete time whenever there is a positive remaining time
        if self.remaining_charging_time > 0:
            calculated_time = self.last_update_time + timedelta(
                minutes=self.remaining_charging_time
            )
            self.charging_complete_time_frozen = (
                calculated_time  # Always update the frozen time
            )
            return calculated_time
        # If the remaining time is zero or negative, and no frozen time is set,
        # we have no knowledge of the last completion time, so return None
        if self.charging_complete_time_frozen is None:
            return None
        # Otherwise, return the frozen complete time
        return self.charging_complete_time_frozen

    @property
    def target_state_of_charge(self):
        """Return state of charge"""
        if self.target_state_of_charge_supported:
            return parse_int(self._vehicle.state.get("targetstateOfCharge"))

    @property
    def target_state_of_charge_supported(self):
        return parse_int(self._vehicle.state.get("targetstateOfCharge")) is not None

    @property
    def plug_state(self):
        """Return plug state"""
        if self.plug_state_supported:
            check = self._vehicle.state.get("plugState")
            return check != "disconnected"

    @property
    def plug_state_supported(self):
        check = self._vehicle.state.get("plugState")
        if check:
            return True

    @property
    def plug_lock_state(self):
        """Return plug lock state"""
        if self.plug_lock_state_supported:
            check = self._vehicle.state.get("plugLockState")
            return check != "locked"

    @property
    def plug_lock_state_supported(self):
        check = self._vehicle.state.get("plugLockState")
        if check:
            return True

    @property
    def external_power(self):
        """Return external Power"""
        if self.external_power_supported:
            external_power_status = self._vehicle.state.get("externalPower")
            if external_power_status == "unavailable":
                return "Not Ready"
            elif external_power_status == "ready":
                return "Ready"
            else:
                return external_power_status

    @property
    def external_power_supported(self):
        return self._vehicle.state.get("externalPower") is not None

    @property
    def plug_led_color(self):
        """Return plug LED Color"""
        if self.plug_led_color_supported:
            return self._vehicle.state.get("plugledColor")

    @property
    def plug_led_color_supported(self):
        check = self._vehicle.state.get("plugledColor")
        if check:
            return True

    @property
    def climatisation_state(self):
        if self.climatisation_state_supported:
            return self._vehicle.state.get("climatisationState")

    @property
    def climatisation_state_supported(self):
        check = self._vehicle.state.get("climatisationState")
        if check:
            return True

    @property
    def outdoor_temperature(self):
        if self.outdoor_temperature_supported:
            return self._vehicle.state.get("outdoorTemperature")

    @property
    def outdoor_temperature_supported(self):
        check = self._vehicle.state.get("outdoorTemperature")
        if check:
            return True

    @property
    def glass_surface_heating(self):
        if self.glass_surface_heating_supported:
            return self._vehicle.state.get("isMirrorHeatingActive")

    @property
    def glass_surface_heating_supported(self):
        return self._vehicle.state.get("isMirrorHeatingActive") is not None

    @property
    def park_time(self):
        if self.park_time_supported:
            return self._vehicle.state.get("vehicleParkingClock")

    @property
    def park_time_supported(self):
        return self._vehicle.state.get("vehicleParkingClock") is not None

    @property
    def remaining_climatisation_time(self):
        if self.remaining_climatisation_time_supported:
            remaining_time = self._vehicle.state.get("remainingClimatisationTime")
            if remaining_time is not None and remaining_time < 0:
                return 0
            elif remaining_time is not None:
                return remaining_time
        return None

    @property
    def remaining_climatisation_time_supported(self):
        return self._vehicle.state.get("remainingClimatisationTime") is not None

    @property
    def preheater_state(self):
        check = self._vehicle.state.get("preheaterState")
        if check:
            return True

    @property
    def preheater_state_supported(self):
        check = self._vehicle.state.get("preheaterState")
        if check:
            return True

    def lock_supported(self):
        return (
            self.doors_trunk_status_supported and self._audi_service._spin is not None
        )

    @property
    def shortterm_current(self):
        """Return shortterm."""
        if self.shortterm_current_supported:
            return self._vehicle.state.get("shortterm_current")

    @property
    def shortterm_current_supported(self):
        """Return true if vehicle has shortterm_current."""
        check = self._vehicle.state.get("shortterm_current")
        if check:
            return True

    @property
    def shortterm_reset(self):
        """Return shortterm."""
        if self.shortterm_reset_supported:
            return self._vehicle.state.get("shortterm_reset")

    @property
    def shortterm_reset_supported(self):
        """Return true if vehicle has shortterm_reset."""
        check = self._vehicle.state.get("shortterm_reset")
        if check:
            return True

    @property
    def longterm_current(self):
        """Return longterm."""
        if self.longterm_current_supported:
            return self._vehicle.state.get("longterm_current")

    @property
    def longterm_current_supported(self):
        """Return true if vehicle has longterm_current."""
        check = self._vehicle.state.get("longterm_current")
        if check:
            return True

    @property
    def longterm_reset(self):
        """Return longterm."""
        if self.longterm_reset_supported:
            return self._vehicle.state.get("longterm_reset")

    @property
    def longterm_reset_supported(self):
        """Return true if vehicle has longterm_reset."""
        check = self._vehicle.state.get("longterm_reset")
        if check:
            return True

    @property
    def is_moving(self):
        """Return true if the vehicle is moving."""
        if self.is_moving_supported:
            return self._vehicle.state.get("is_moving")

    @property
    def is_moving_supported(self):
        """Return true if vehicle can move."""
        return True


================================================
FILE: custom_components/audiconnect/audi_entity.py
================================================
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.dispatcher import (
    async_dispatcher_connect,
)
from homeassistant.helpers.entity import DeviceInfo

from .const import DOMAIN, SIGNAL_STATE_UPDATED


class AudiEntity(Entity):
    """Base class for all Audi entities."""

    def __init__(self, data, instrument):
        """Initialize the entity."""
        self._data = data
        self._instrument = instrument
        self._vin = self._instrument.vehicle_name
        self._component = self._instrument.component
        self._attribute = self._instrument.attr

    async def async_added_to_hass(self):
        """Register update dispatcher."""
        async_dispatcher_connect(
            self.hass, SIGNAL_STATE_UPDATED, self.async_schedule_update_ha_state
        )

    @property
    def icon(self):
        """Return the icon."""
        return self._instrument.icon

    @property
    def _entity_name(self):
        return self._instrument.name

    @property
    def _vehicle_name(self):
        return self._instrument.vehicle_name

    @property
    def name(self):
        """Return full name of the entity."""
        return "{} {}".format(self._vehicle_name, self._entity_name)

    @property
    def should_poll(self):
        """Return the polling state."""
        return False

    @property
    def assumed_state(self):
        """Return true if unable to access real state of entity."""
        return True

    @property
    def extra_state_attributes(self):
        """Return device specific state attributes."""
        return dict(
            self._instrument.attributes,
            model="{}/{}".format(
                self._instrument.vehicle_model, self._instrument.vehicle_name
            ),
            model_year=self._instrument.vehicle_model_year,
            model_family=self._instrument.vehicle_model_family,
            title=self._instrument.vehicle_name,
            csid=self._instrument.vehicle_csid,
            vin=self._instrument.vehicle_vin,
        )

    @property
    def unique_id(self):
        """Return a unique ID."""
        return self._instrument.full_name

    @property
    def device_info(self):
        """Return device information."""
        if self._instrument.vehicle_model:
            model_info = self._instrument.vehicle_model.replace("Audi ", "")
        elif self._instrument.vehicle_name:
            model_info = self._instrument.vehicle_name
        else:
            model_info = "Unknown"
        return DeviceInfo(
            identifiers={(DOMAIN, self._instrument.vehicle_name)},
            manufacturer="Audi",
            name=self._instrument.vehicle_name,
            model="{} ({})".format(model_info, self._instrument.vehicle_model_year),
        )


================================================
FILE: custom_components/audiconnect/audi_models.py
================================================
import logging
from .util import get_attr

_LOGGER = logging.getLogger(__name__)


class VehicleData:
    def __init__(self, config_entry):
        self.sensors = set()
        self.binary_sensors = set()
        self.switches = set()
        self.device_trackers = set()
        self.locks = set()
        self.config_entry = config_entry
        self.vehicle = None


class CurrentVehicleDataResponse:
    def __init__(self, data):
        data = data["CurrentVehicleDataResponse"]
        self.request_id = data["requestId"]
        self.vin = data["vin"]


class VehicleDataResponse:
    OLDAPI_MAPPING = {
        "frontRightLock": "LOCK_STATE_RIGHT_FRONT_DOOR",
        "frontRightOpen": "OPEN_STATE_RIGHT_FRONT_DOOR",
        "frontLeftLock": "LOCK_STATE_LEFT_FRONT_DOOR",
        "frontLeftOpen": "OPEN_STATE_LEFT_FRONT_DOOR",
        "rearRightLock": "LOCK_STATE_RIGHT_REAR_DOOR",
        "rearRightOpen": "OPEN_STATE_RIGHT_REAR_DOOR",
        "rearLeftLock": "LOCK_STATE_LEFT_REAR_DOOR",
        "rearLeftOpen": "OPEN_STATE_LEFT_REAR_DOOR",
        "trunkLock": "LOCK_STATE_TRUNK_LID",
        "trunkOpen": "OPEN_STATE_TRUNK_LID",
        "bonnetLock": "LOCK_STATE_HOOD",
        "bonnetOpen": "OPEN_STATE_HOOD",
        "sunRoofWindow": "STATE_SUN_ROOF_MOTOR_COVER",
        "frontLeftWindow": "STATE_LEFT_FRONT_WINDOW",
        "frontRightWindow": "STATE_RIGHT_FRONT_WINDOW",
        "rearLeftWindow": "STATE_LEFT_REAR_WINDOW",
        "rearRightWindow": "STATE_RIGHT_REAR_WINDOW",
        "roofCoverWindow": "STATE_ROOF_COVER_WINDOW",
    }

    def __init__(self, data):
        self.data_fields = []
        self.states = []

        self._tryAppendFieldWithTs(
            data, "TOTAL_RANGE", ["fuelStatus", "rangeStatus", "value", "totalRange_km"]
        )
        self._tryAppendFieldWithTs(
            data,
            "TANK_LEVEL_IN_PERCENTAGE",
            ["measurements", "fuelLevelStatus", "value", "currentFuelLevel_pct"],
        )
        self._tryAppendFieldWithTs(
            data,
            "UTC_TIME_AND_KILOMETER_STATUS",
            ["measurements", "odometerStatus", "value", "odometer"],
        )
        self._tryAppendFieldWithTs(
            data,
            "MAINTENANCE_INTERVAL_TIME_TO_INSPECTION",
            [
                "vehicleHealthInspection",
                "maintenanceStatus",
                "value",
                "inspectionDue_days",
            ],
        )
        self._tryAppendFieldWithTs(
            data,
            "MAINTENANCE_INTERVAL_DISTANCE_TO_INSPECTION",
            [
                "vehicleHealthInspection",
                "maintenanceStatus",
                "value",
                "inspectionDue_km",
            ],
        )

        self._tryAppendFieldWithTs(
            data,
            "MAINTENANCE_INTERVAL_TIME_TO_OIL_CHANGE",
            [
                "vehicleHealthInspection",
                "maintenanceStatus",
                "value",
                "oilServiceDue_days",
            ],
        )
        self._tryAppendFieldWithTs(
            data,
            "MAINTENANCE_INTERVAL_DISTANCE_TO_OIL_CHANGE",
            [
                "vehicleHealthInspection",
                "maintenanceStatus",
                "value",
                "oilServiceDue_km",
            ],
        )

        self._tryAppendFieldWithTs(
            data,
            "OIL_LEVEL_DIPSTICKS_PERCENTAGE",
            ["oilLevel", "oilLevelStatus", "value", "value"],
        )
        self._tryAppendFieldWithTs(
            data,
            "ADBLUE_RANGE",
            ["measurements", "rangeStatus", "value", "adBlueRange"],
        )

        self._tryAppendFieldWithTs(
            data, "LIGHT_STATUS", ["vehicleLights", "lightsStatus", "value", "lights"]
        )

        self.appendWindowState(data)
        self.appendDoorState(data)

        self._tryAppendStateWithTs(
            data, "carType", -1, ["fuelStatus", "rangeStatus", "value", "carType"]
        )

        self._tryAppendStateWithTs(
            data,
            "engineTypeFirstEngine",
            -2,
            ["fuelStatus", "rangeStatus", "value", "primaryEngine", "type"],
        )
        self._tryAppendStateWithTs(
            data,
            "primaryEngineRange",
            -2,
            [
                "fuelStatus",
                "rangeStatus",
                "value",
                "primaryEngine",
                "remainingRange_km",
            ],
        )
        self._tryAppendStateWithTs(
            data,
            "primaryEngineRangePercent",
            -2,
            ["fuelStatus", "rangeStatus", "value", "primaryEngine", "currentSOC_pct"],
        )
        self._tryAppendStateWithTs(
            data,
            "engineTypeSecondEngine",
            -2,
            ["fuelStatus", "rangeStatus", "value", "secondaryEngine", "type"],
        )
        self._tryAppendStateWithTs(
            data,
            "secondaryEngineRange",
            -2,
            [
                "fuelStatus",
                "rangeStatus",
                "value",
                "secondaryEngine",
                "remainingRange_km",
            ],
        )
        self._tryAppendStateWithTs(
            data,
            "secondaryEngineRangePercent",
            -2,
            ["fuelStatus", "rangeStatus", "value", "secondaryEngine", "currentSOC_pct"],
        )
        self._tryAppendStateWithTs(
            data,
            "hybridRange",
            -1,
            ["fuelStatus", "rangeStatus", "value", "totalRange_km"],
        )
        self._tryAppendStateWithTs(
            data,
            "stateOfCharge",
            -1,
            ["charging", "batteryStatus", "value", "currentSOC_pct"],
        )
        self._tryAppendStateWithTs(
            data,
            "chargingState",
            -1,
            ["charging", "chargingStatus", "value", "chargingState"],
        )
        self._tryAppendStateWithTs(
            data,
            "chargeMode",
            -1,
            ["charging", "chargingStatus", "value", "chargeMode"],
        )
        self._tryAppendStateWithTs(
            data,
            "chargingPower",
            -1,
            ["charging", "chargingStatus", "value", "chargePower_kW"],
        )
        self._tryAppendStateWithTs(
            data,
            "actualChargeRate",
            -1,
            ["charging", "chargingStatus", "value", "chargeRate_kmph"],
        )
        self._tryAppendStateWithTs(
            data,
            "chargeType",
            -1,
            ["charging", "chargingStatus", "value", "chargeType"],
        )
        self._tryAppendStateWithTs(
            data,
            "targetstateOfCharge",
            -1,
            ["charging", "chargingSettings", "value", "targetSOC_pct"],
        )
        self._tryAppendStateWithTs(
            data,
            "plugState",
            -1,
            ["charging", "plugStatus", "value", "plugConnectionState"],
        )
        self._tryAppendStateWithTs(
            data,
            "remainingChargingTime",
            -1,
            [
                "charging",
                "chargingStatus",
                "value",
                "remainingChargingTimeToComplete_min",
            ],
        )
        self._tryAppendStateWithTs(
            data,
            "plugLockState",
            -1,
            ["charging", "plugStatus", "value", "plugLockState"],
        )
        self._tryAppendStateWithTs(
            data,
            "externalPower",
            -1,
            ["charging", "plugStatus", "value", "externalPower"],
        )
        self._tryAppendStateWithTs(
            data,
            "plugledColor",
            -1,
            ["charging", "plugStatus", "value", "ledColor"],
        )
        self._tryAppendStateWithTs(
            data,
            "climatisationState",
            -1,
            ["climatisation", "auxiliaryHeatingStatus", "value", "climatisationState"],
        )
        # 2024 Q4 updated data structure for climate data
        self._tryAppendStateWithTs(
            data,
            "climatisationState",
            -1,
            ["climatisation", "climatisationStatus", "value", "climatisationState"],
        )
        self._tryAppendStateWithTs(
            data,
            "remainingClimatisationTime",
            -1,
            [
                "climatisation",
                "climatisationStatus",
                "value",
                "remainingClimatisationTime_min",
            ],
        )

    def _tryAppendStateWithTs(self, json, name, tsoff, loc):
        _LOGGER.debug(
            "TRY APPEND STATE: Searching for '%s' at location=%s, tsoff=%s",
            name,
            loc,
            tsoff,
        )

        ts = None
        val = self._getFromJson(json, loc)
        # _LOGGER.debug("Initial value retrieved for '%s': %s", name, val)

        if val is not None:
            loc[tsoff:] = ["carCapturedTimestamp"]
            # _LOGGER.debug("Updated loc for timestamp retrieval: %s", loc)
            ts = self._getFromJson(json, loc)
            # _LOGGER.debug("Timestamp retrieved for '%s': %s", name, ts)

        if val is not None and ts:
            self.states.append({"name": name, "value": val, "measure_time": ts})
            _LOGGER.debug(
                "TRY APPEND STATE: Found '%s' with value=%s, tsoff=%s, loc=%s, ts=%s",
                name,
                val,
                tsoff,
                loc,
                ts,
            )
        else:
            if val is None:
                _LOGGER.debug(
                    "TRY APPEND STATE: Value for '%s' is None; not appending state.",
                    name,
                )
            elif not ts:
                _LOGGER.debug(
                    "TRY APPEND STATE: Timestamp for '%s' is None or missing; not appending state.",
                    name,
                )

    def _tryAppendFieldWithTs(self, json, textId, loc):
        _LOGGER.debug(
            "TRY APPEND FIELD: Searching for '%s' at location=%s",
            textId,
            loc,
        )

        ts = None
        val = self._getFromJson(json, loc)
        # _LOGGER.debug("Initial value retrieved for '%s': %s", textId, val)

        if val is not None:
            loc[-1:] = ["carCapturedTimestamp"]
            # _LOGGER.debug("Updated loc for timestamp retrieval: %s", loc)
            ts = self._getFromJson(json, loc)
            # _LOGGER.debug("Timestamp retrieved for '%s': %s", textId, ts)

        if val is not None and ts:
            self.data_fields.append(
                Field(
                    {
                        "textId": textId,
                        "value": val,
                        "tsCarCaptured": ts,
                    }
                )
            )
            _LOGGER.debug(
                "TRY APPEND FIELD: Found '%s' with value=%s, loc=%s, ts=%s",
                textId,
                val,
                loc,
                ts,
            )
        else:
            if val is None:
                _LOGGER.debug(
                    "TRY APPEND FIELD: Value for '%s' is None or missing; not appending field.",
                    textId,
                )
            elif not ts:
                _LOGGER.debug(
                    "TRY APPEND FIELD: Timestamp for '%s' is None or missing; not appending field.",
                    textId,
                )

    def _getFromJson(self, json, loc):
        child = json
        for i in loc:
            if i not in child:
                return None
            child = child[i]
        return child

    def appendDoorState(self, data):
        _LOGGER.debug("APPEND DOOR: Starting to append doors...")
        doors = get_attr(data, "access.accessStatus.value.doors", [])
        tsCarCapturedAccess = get_attr(
            data, "access.accessStatus.value.carCapturedTimestamp"
        )
        _LOGGER.debug(
            "APPEND DOOR: Timestamp captured from car: %s", tsCarCapturedAccess
        )
        for door in doors:
            status = door["status"]
            name = door["name"]
            _LOGGER.debug(
                "APPEND DOOR: Processing door: %s with status: %s", name, status
            )
            if name + "Lock" not in self.OLDAPI_MAPPING:
                _LOGGER.debug(
                    "APPEND DOOR: Skipping door not mapped in OLDAPI_MAPPING: %s", name
                )
                continue
            lock = "0"
            open = "0"
            unsupported = False
            for state in status:
                if state == "unsupported":
                    unsupported = True
                    _LOGGER.debug("APPEND DOOR: Unsupported state for door: %s", name)
                if state == "locked":
                    lock = "2"
                if state == "closed":
                    open = "3"
            if not unsupported:
                doorFieldLock = {
                    "textId": self.OLDAPI_MAPPING[name + "Lock"],
                    "value": lock,
                    "tsCarCaptured": tsCarCapturedAccess,
                }
                _LOGGER.debug(
                    "APPEND DOOR: Appended door lock field: %s", doorFieldLock
                )
                self.data_fields.append(Field(doorFieldLock))

                doorFieldOpen = {
                    "textId": self.OLDAPI_MAPPING[name + "Open"],
                    "value": open,
                    "tsCarCaptured": tsCarCapturedAccess,
                }
                _LOGGER.debug(
                    "APPEND DOOR: Appended door open field: %s", doorFieldOpen
                )
                self.data_fields.append(Field(doorFieldOpen))
        _LOGGER.debug("APPEND DOOR: Finished appending doors")

    def appendWindowState(self, data):
        _LOGGER.debug("APPEND WINDOW: Starting to append windows...")
        windows = get_attr(data, "access.accessStatus.value.windows", [])
        tsCarCapturedAccess = get_attr(
            data, "access.accessStatus.value.carCapturedTimestamp"
        )
        _LOGGER.debug(
            "APPEND WINDOW: Timestamp captured from car: %s", tsCarCapturedAccess
        )
        for window in windows:
            name = window["name"]
            status = window["status"]
            _LOGGER.debug(
                "APPEND WINDOW: Processing window: %s with status: %s", name, status
            )
            if (
                status[0] == "unsupported"
            ) or name + "Window" not in self.OLDAPI_MAPPING:
                _LOGGER.debug(
                    "APPEND WINDOW: Skipping unsupported window or not mapped in OLDAPI_MAPPING: %s",
                    name,
                )
                continue
            windowField = {
                "textId": self.OLDAPI_MAPPING[name + "Window"],
                "value": "3" if status[0] == "closed" else "0",
                "tsCarCaptured": tsCarCapturedAccess,
            }
            _LOGGER.debug("APPEND WINDOW: Appended window field: %s", windowField)
            self.data_fields.append(Field(windowField))
        _LOGGER.debug("APPEND WINDOW: Finished appending windows")


class TripDataResponse:
    def __init__(self, data):
        self.data_fields = []

        self.tripID = data["tripID"]

        self.averageElectricEngineConsumption = None
        if "averageElectricEngineConsumption" in data:
            self.averageElectricEngineConsumption = (
                float(data["averageElectricEngineConsumption"]) / 10
            )

        self.averageFuelConsumption = None
        if "averageFuelConsumption" in data:
            self.averageFuelConsumption = float(data["averageFuelConsumption"]) / 10

        self.averageSpeed = None
        if "averageSpeed" in data:
            self.averageSpeed = int(data["averageSpeed"])

        self.mileage = None
        if "mileage" in data:
            self.mileage = int(data["mileage"])

        self.startMileage = None
        if "startMileage" in data:
            self.startMileage = int(data["startMileage"])

        self.traveltime = None
        if "traveltime" in data:
            self.traveltime = int(data["traveltime"])

        self.timestamp = None
        if "timestamp" in data:
            self.timestamp = data["timestamp"]

        self.overallMileage = None
        if "overallMileage" in data:
            self.overallMileage = int(data["overallMileage"])

        self.zeroEmissionDistance = None
        if "zeroEmissionDistance" in data:
            self.zeroEmissionDistance = int(data["zeroEmissionDistance"])


class Field:
    IDS = {
        "0x0": "UNKNOWN",
        "0x0101010002": "UTC_TIME_AND_KILOMETER_STATUS",
        "0x0203010001": "MAINTENANCE_INTERVAL_DISTANCE_TO_OIL_CHANGE",
        "0x0203010002": "MAINTENANCE_INTERVAL_TIME_TO_OIL_CHANGE",
        "0x0203010003": "MAINTENANCE_INTERVAL_DISTANCE_TO_INSPECTION",
        "0x0203010004": "MAINTENANCE_INTERVAL_TIME_TO_INSPECTION",
        "0x0203010006": "MAINTENANCE_INTERVAL_ALARM_INSPECTION",
        "0x0203010007": "MAINTENANCE_INTERVAL_MONTHLY_MILEAGE",
        "0x0203010005": "WARNING_OIL_CHANGE",
        "0x0204040001": "OIL_LEVEL_AMOUNT_IN_LITERS",
        "0x0204040002": "OIL_LEVEL_MINIMUM_WARNING",
        "0x0204040003": "OIL_LEVEL_DIPSTICKS_PERCENTAGE",
        "0x02040C0001": "ADBLUE_RANGE",
        "0x0301010001": "LIGHT_STATUS",
        "0x0301030001": "BRAKING_STATUS",
        "0x0301030005": "TOTAL_RANGE",
        "0x030103000A": "TANK_LEVEL_IN_PERCENTAGE",
        "0x0301040001": "LOCK_STATE_LEFT_FRONT_DOOR",
        "0x0301040002": "OPEN_STATE_LEFT_FRONT_DOOR",
        "0x0301040003": "SAFETY_STATE_LEFT_FRONT_DOOR",
        "0x0301040004": "LOCK_STATE_LEFT_REAR_DOOR",
        "0x0301040005": "OPEN_STATE_LEFT_REAR_DOOR",
        "0x0301040006": "SAFETY_STATE_LEFT_REAR_DOOR",
        "0x0301040007": "LOCK_STATE_RIGHT_FRONT_DOOR",
        "0x0301040008": "OPEN_STATE_RIGHT_FRONT_DOOR",
        "0x0301040009": "SAFETY_STATE_RIGHT_FRONT_DOOR",
        "0x030104000A": "LOCK_STATE_RIGHT_REAR_DOOR",
        "0x030104000B": "OPEN_STATE_RIGHT_REAR_DOOR",
        "0x030104000C": "SAFETY_STATE_RIGHT_REAR_DOOR",
        "0x030104000D": "LOCK_STATE_TRUNK_LID",
        "0x030104000E": "OPEN_STATE_TRUNK_LID",
        "0x030104000F": "SAFETY_STATE_TRUNK_LID",
        "0x0301040010": "LOCK_STATE_HOOD",
        "0x0301040011": "OPEN_STATE_HOOD",
        "0x0301040012": "SAFETY_STATE_HOOD",
        "0x0301050001": "STATE_LEFT_FRONT_WINDOW",
        "0x0301050003": "STATE_LEFT_REAR_WINDOW",
        "0x0301050005": "STATE_RIGHT_FRONT_WINDOW",
        "0x0301050007": "STATE_RIGHT_REAR_WINDOW",
        "0x0301050009": "STATE_DECK",
        "0x030105000B": "STATE_SUN_ROOF_MOTOR_COVER",
        "0x0301030006": "PRIMARY_RANGE",
        "0x0301030007": "PRIMARY_DRIVE",
        "0x0301030008": "SECONDARY_RANGE",
        "0x0301030009": "SECONDARY_DRIVE",
        "0x0301030002": "STATE_OF_CHARGE",
        "0x0301020001": "TEMPERATURE_OUTSIDE",
        "0x0202": "ACTIVE_INSTRUMENT_CLUSTER_WARNING",
    }

    def __init__(self, data):
        self.name = None
        self.id = data.get("id")
        self.unit = data.get("unit")
        self.value = data.get("value")
        self.measure_time = data.get("tsTssReceivedUtc")
        if self.measure_time is None:
            self.measure_time = data.get("tsCarCaptured")
        self.send_time = data.get("tsCarSentUtc")
        self.measure_mileage = data.get("milCarCaptured")
        self.send_mileage = data.get("milCarSent")

        for field_id, name in self.IDS.items():
            if field_id == self.id:
                self.name = name
                break
        if self.name is None:
            # No direct mapping found - maybe we've at least got a text id
            self.name = data.get("textId")

    def __str__(self):
        str_rep = str(self.name) + " " + str(self.value)
        if self.unit is not None:
            str_rep += self.unit
        return str_rep


class Vehicle:
    def __init__(self):
        self.vin = ""
        self.csid = ""
        self.model = ""
        self.model_year = ""
        self.model_family = ""
        self.title = ""

    def parse(self, data):
        self.vin = data.get("vin")
        self.csid = data.get("csid")
        if (
            data.get("vehicle") is not None
            and data.get("vehicle").get("media") is not None
        ):
            self.model = data.get("vehicle").get("media").get("longName")
        if (
            data.get("vehicle") is not None
            and data.get("vehicle").get("core") is not None
        ):
            self.model_year = data.get("vehicle").get("core").get("modelYear")
        if data.get("nickname") is not None and len(data.get("nickname")) > 0:
            self.title = data.get("nickname")
        elif (
            data.get("vehicle") is not None
            and data.get("vehicle").get("media") is not None
        ):
            self.title = data.get("vehicle").get("media").get("shortName")

    def __str__(self):
        return str(self.__dict__)


class VehiclesResponse:
    def __init__(self):
        self.vehicles = []
        self.blacklisted_vins = 0

    def parse(self, data):
        user_vehicles = data.get("userVehicles")
        if user_vehicles is None:
            _LOGGER.warning("No vehicle data received from API. Check authentication.")
            return

        for item in user_vehicles:
            vehicle = Vehicle()
            vehicle.parse(item)
            self.vehicles.append(vehicle)


================================================
FILE: custom_components/audiconnect/audi_services.py
================================================
import json
import uuid
import base64
import os
import re
import logging
from datetime import timedelta, datetime
from typing import Optional

from .audi_models import (
    TripDataResponse,
    CurrentVehicleDataResponse,
    VehicleDataResponse,
    VehiclesResponse,
)
from .audi_api import AudiAPI
from .const import DEFAULT_API_LEVEL
from .util import to_byte_array, get_attr

from hashlib import sha256, sha512
import hmac
import asyncio

from urllib.parse import urlparse, parse_qs, urlencode

import requests
from bs4 import BeautifulSoup
from requests import RequestException

from typing import Dict


MAX_RESPONSE_ATTEMPTS = 10
REQUEST_STATUS_SLEEP = 10

SUCCEEDED = "succeeded"
FAILED = "failed"
REQUEST_SUCCESSFUL = "request_successful"
REQUEST_FAILED = "request_failed"

_LOGGER = logging.getLogger(__name__)


class BrowserLoginResponse:
    def __init__(self, response: requests.Response, url: str):
        self.response = response  # type: requests.Response
        self.url = url  # type : str

    def get_location(self) -> str:
        """
        Returns the location the previous request redirected to
        """
        location = self.response.headers["Location"]
        if location.startswith("/"):
            # Relative URL
            return BrowserLoginResponse.to_absolute(self.url, location)
        return location

    @classmethod
    def to_absolute(cls, absolute_url, relative_url) -> str:
        """
        Converts a relative url to an absolute url
        :param absolute_url: Absolute url used as baseline
        :param relative_url: Relative url (must start with /)
        :return: New absolute url
        """
        url_parts = urlparse(absolute_url)
        return url_parts.scheme + "://" + url_parts.netloc + relative_url


class AudiService:
    def __init__(self, api: AudiAPI, country: str, spin: str, api_level: int):
        self._api = api
        self._country = country
        self._language = None
        self._type = "Audi"
        self._spin = spin
        self._homeRegion = {}
        self._homeRegionSetter = {}
        self.mbbOAuthBaseURL = None
        self.mbboauthToken = None
        self.xclientId = None
        self._tokenEndpoint = ""
        self._bearer_token_json = None
        self._client_id = ""
        self._authorizationServerBaseURLLive = ""
        self._api_level = api_level

        if self._api_level is None:
            self._api_level = DEFAULT_API_LEVEL

        if self._country is None:
            self._country = "DE"

    def get_hidden_html_input_form_data(self, response, form_data: Dict[str, str]):
        # Now parse the html body and extract the target url, csrf token and other required parameters
        html = BeautifulSoup(response, "html.parser")
        form_inputs = html.find_all("input", attrs={"type": "hidden"})
        for form_input in form_inputs:
            name = form_input.get("name")
            form_data[name] = form_input.get("value")

        return form_data

    def get_post_url(self, response, url):
        # Now parse the html body and extract the target url, csrf token and other required parameters
        html = BeautifulSoup(response, "html.parser")
        form_tag = html.find("form")

        # Extract the target url
        action = form_tag.get("action")
        if action.startswith("http"):
            # Absolute url
            username_post_url = action
        elif action.startswith("/"):
            # Relative to domain
            username_post_url = BrowserLoginResponse.to_absolute(url, action)
        else:
            raise RequestException("Unknown form action: " + action)
        return username_post_url

    async def login(self, user: str, password: str, persist_token: bool = True):
        _LOGGER.debug("LOGIN: Starting login to Audi service...")
        await self.login_request(user, password)

    async def refresh_vehicle_data(self, vin: str):
        res = await self.request_current_vehicle_data(vin.upper())
        request_id = res.request_id

        checkUrl = "{homeRegion}/fs-car/bs/vsr/v1/{type}/{country}/vehicles/{vin}/requests/{requestId}/jobstatus".format(
            homeRegion=await self._get_home_region(vin.upper()),
            type=self._type,
            country=self._country,
            vin=vin.upper(),
            requestId=request_id,
        )

        await self.check_request_succeeded(
            checkUrl,
            "refresh vehicle data",
            REQUEST_SUCCESSFUL,
            REQUEST_FAILED,
            "requestStatusResponse.status",
        )

    async def request_current_vehicle_data(self, vin: str):
        self._api.use_token(self.vwToken)
        data = await self._api.post(
            "{homeRegion}/fs-car/bs/vsr/v1/{type}/{country}/vehicles/{vin}/requests".format(
                homeRegion=await self._get_home_region(vin.upper()),
                type=self._type,
                country=self._country,
                vin=vin.upper(),
            )
        )
        return CurrentVehicleDataResponse(data)

    async def get_preheater(self, vin: str):
        self._api.use_token(self.vwToken)
        return await self._api.get(
            "{homeRegion}/fs-car/bs/rs/v1/{type}/{country}/vehicles/{vin}/status".format(
                homeRegion=await self._get_home_region(vin.upper()),
                type=self._type,
                country=self._country,
                vin=vin.upper(),
            )
        )

    async def get_stored_vehicle_data(self, vin: str):
        redacted_vin = "*" * (len(vin) - 4) + vin[-4:]
        JOBS2QUERY = {
            "access",
            "activeVentilation",
            "auxiliaryHeating",
            "batteryChargingCare",
            "batterySupport",
            "charging",
            "chargingProfiles",
            "chargingTimers",
            "climatisation",
            "climatisationTimers",
            "departureProfiles",
            "departureTimers",
            "fuelStatus",
            "honkAndFlash",
            "hybridCarAuxiliaryHeating",
            "lvBattery",
            "measurements",
            "oilLevel",
            "readiness",
            # "userCapabilities",
            "vehicleHealthInspection",
            "vehicleHealthWarnings",
            "vehicleLights",
        }
        self._api.use_token(self._bearer_token_json)
        data = await self._api.get(
            self.__get_cariad_url_for_vin(
                vin, "selectivestatus?jobs={jobs}", jobs=",".join(JOBS2QUERY)
            )
        )

        _LOGGER.debug("Vehicle data returned for VIN: %s: %s", redacted_vin, data)
        return VehicleDataResponse(data)

    async def get_charger(self, vin: str):
        self._api.use_token(self.vwToken)
        return await self._api.get(
            "{homeRegion}/fs-car/bs/batterycharge/v1/{type}/{country}/vehicles/{vin}/charger".format(
                homeRegion=await self._get_home_region(vin.upper()),
                type=self._type,
                country=self._country,
                vin=vin.upper(),
            )
        )

    async def get_climater(self, vin: str):
        self._api.use_token(self.vwToken)
        return await self._api.get(
            "{homeRegion}/fs-car/bs/climatisation/v1/{type}/{country}/vehicles/{vin}/climater".format(
                homeRegion=await self._get_home_region(vin.upper()),
                type=self._type,
                country=self._country,
                vin=vin.upper(),
            )
        )

    async def get_stored_position(self, vin: str):
        self._api.use_token(self._bearer_token_json)
        return await self._api.get(
            self.__get_cariad_url_for_vin(vin, "parkingposition")
        )

    async def get_operations_list(self, vin: str):
        self._api.use_token(self.vwToken)
        return await self._api.get(
            "https://mal-1a.prd.ece.vwg-connect.com/api/rolesrights/operationlist/v3/vehicles/"
            + vin.upper()
        )

    async def get_timer(self, vin: str):
        self._api.use_token(self.vwToken)
        return await self._api.get(
            "{homeRegion}/fs-car/bs/departuretimer/v1/{type}/{country}/vehicles/{vin}/timer".format(
                homeRegion=await self._get_home_region(vin.upper()),
                type=self._type,
                country=self._country,
                vin=vin.upper(),
            )
        )

    async def get_vehicles(self):
        self._api.use_token(self.vwToken)
        return await self._api.get(
            "https://msg.volkswagen.de/fs-car/usermanagement/users/v1/{type}/{country}/vehicles".format(
                type=self._type, country=self._country
            )
        )

    async def get_vehicle_information(self):
        headers = {
            "Accept": "application/json",
            "Accept-Charset": "utf-8",
            "X-App-Name": "myAudi",
            "X-App-Version": AudiAPI.HDR_XAPP_VERSION,
            "Accept-Language": "{l}-{c}".format(
                l=self._language, c=self._country.upper()
            ),
            "X-User-Country": self._country.upper(),
            "User-Agent": AudiAPI.HDR_USER_AGENT,
            "Authorization": "Bearer " + self.audiToken["access_token"],
            "Content-Type": "application/json; charset=utf-8",
        }
        req_data = {
            "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}"
        }
        req_rsp, rep_rsptxt = await self._api.request(
            "POST",
            "https://app-api.my.aoa.audi.com/vgql/v1/graphql"
            if self._country.upper() == "US"
            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.
            json.dumps(req_data),
            headers=headers,
            allow_redirects=False,
            rsp_wtxt=True,
        )
        vins = json.loads(rep_rsptxt)
        if "errors" in vins:
            raise Exception(f"API returned errors: {vins['errors']}")

        if "data" not in vins or vins["data"] is None:
            raise Exception("No data in API response")

        if vins["data"].get("userVehicles") is None:
            raise Exception(
                "No vehicle data in API response - possible authentication issue"
            )

        response = VehiclesResponse()
        response.parse(vins["data"])
        return response

    async def get_vehicle_data(self, vin: str):
        self._api.use_token(self.vwToken)
        return await self._api.get(
            "{homeRegion}/fs-car/vehicleMgmt/vehicledata/v2/{type}/{country}/vehicles/{vin}/".format(
                homeRegion=await self._get_home_region(vin.upper()),
                type=self._type,
                country=self._country,
                vin=vin.upper(),
            )
        )

    async def get_tripdata(self, vin: str, kind: str):
        self._api.use_token(self.vwToken)

        # read tripdata
        headers = {
            "Accept": "application/json",
            "Accept-Charset": "utf-8",
            "X-App-Name": "myAudi",
            "X-App-Version": AudiAPI.HDR_XAPP_VERSION,
            "X-Client-ID": self.xclientId,
            "User-Agent": AudiAPI.HDR_USER_AGENT,
            "Authorization": "Bearer " + self.vwToken["access_token"],
        }
        td_reqdata = {
            "type": "list",
            "from": "1970-01-01T00:00:00Z",
            # "from":(datetime.utcnow() - timedelta(days=365)).strftime("%Y-%m-%dT%H:%M:%SZ"),
            "to": (datetime.utcnow() + timedelta(minutes=90)).strftime(
                "%Y-%m-%dT%H:%M:%SZ"
            ),
        }
        data = await self._api.request(
            "GET",
            "{homeRegion}/api/bs/tripstatistics/v1/vehicles/{vin}/tripdata/{kind}".format(
                homeRegion=await self._get_home_region_setter(vin.upper()),
                vin=vin.upper(),
                kind=kind,
            ),
            None,
            params=td_reqdata,
            headers=headers,
        )
        td_sorted = sorted(
            data["tripDataList"]["tripData"],
            key=lambda k: k["overallMileage"],
            reverse=True,
        )
        # _LOGGER.debug("get_tripdata: td_sorted: %s", td_sorted)
        td_current = td_sorted[0]
        # FIX, TR/2023-03-25: Assign just in case td_sorted contains only one item
        td_reset_trip = td_sorted[0]

        for trip in td_sorted:
            if (td_current["startMileage"] - trip["startMileage"]) > 2:
                td_reset_trip = trip
                break
            else:
                td_current["tripID"] = trip["tripID"]
                td_current["startMileage"] = trip["startMileage"]
        _LOGGER.debug("TRIP DATA: td_current: %s", td_current)
        _LOGGER.debug("TRIP DATA: td_reset_trip: %s", td_reset_trip)

        return TripDataResponse(td_current), TripDataResponse(td_reset_trip)

    async def _fill_home_region(self, vin: str):
        # the home-region endpoint returns
        # https://ha-5a.prd.eu.vwg.vwautocloud.net which is no valid endpoint
        # (at least not in DE region). set it statically.
        if self._country.upper() != "US" and self._api_level == 1:
            self._homeRegion[vin] = "https://mal-3a.prd.eu.dp.vwg-connect.com"
            self._homeRegionSetter[vin] = "https://mal-3a.prd.eu.dp.vwg-connect.com"
            return
        self._homeRegion[vin] = "https://msg.volkswagen.de"
        self._homeRegionSetter[vin] = "https://mal-1a.prd.ece.vwg-connect.com"

        try:
            self._api.use_token(self.vwToken)
            res = await self._api.get(
                "https://mal-1a.prd.ece.vwg-connect.com/api/cs/vds/v1/vehicles/{vin}/homeRegion".format(
                    vin=vin
                )
            )
            if (
                res is not None
                and res.get("homeRegion") is not None
                and res["homeRegion"].get("baseUri") is not None
                and res["homeRegion"]["baseUri"].get("content") is not None
            ):
                uri = res["homeRegion"]["baseUri"]["content"]
                if uri != "https://mal-1a.prd.ece.vwg-connect.com/api":
                    self._homeRegionSetter[vin] = uri.split("/api")[0]
                    self._homeRegion[vin] = self._homeRegionSetter[vin].replace(
                        "mal-", "fal-"
                    )
        except Exception:
            pass

    async def _get_home_region(self, vin: str):
        if self._homeRegion.get(vin) is not None:
            return self._homeRegion[vin]

        await self._fill_home_region(vin)

        return self._homeRegion[vin]

    async def _get_home_region_setter(self, vin: str):
        if self._homeRegionSetter.get(vin) is not None:
            return self._homeRegionSetter[vin]

        await self._fill_home_region(vin)

        return self._homeRegionSetter[vin]

    async def _get_security_token(self, vin: str, action: str):
        # Challenge
        headers = {
            "User-Agent": "okhttp/3.7.0",
            "X-App-Version": "3.14.0",
            "X-App-Name": "myAudi",
            "Accept": "application/json",
            "Authorization": "Bearer " + self.vwToken.get("access_token"),
        }

        body = await self._api.request(
            "GET",
            "{homeRegionSetter}/api/rolesrights/authorization/v2/vehicles/".format(
                homeRegionSetter=await self._get_home_region_setter(vin.upper())
            )
            + vin.upper()
            + "/services/"
            + action
            + "/security-pin-auth-requested",
            headers=headers,
            data=None,
        )
        secToken = body["securityPinAuthInfo"]["securityToken"]
        challenge = body["securityPinAuthInfo"]["securityPinTransmission"]["challenge"]

        # Response
        securityPinHash = self._generate_security_pin_hash(challenge)
        data = {
            "securityPinAuthentication": {
                "securityPin": {
                    "challenge": challenge,
                    "securityPinHash": securityPinHash,
                },
                "securityToken": secToken,
            }
        }

        headers = {
            "User-Agent": "okhttp/3.7.0",
            "Content-Type": "application/json",
            "X-App-Version": "3.14.0",
            "X-App-Name": "myAudi",
            "Accept": "application/json",
            "Authorization": "Bearer " + self.vwToken.get("access_token"),
        }

        body = await self._api.request(
            "POST",
            "{homeRegionSetter}/api/rolesrights/authorization/v2/security-pin-auth-completed".format(
                homeRegionSetter=await self._get_home_region_setter(vin.upper())
            ),
            headers=headers,
            data=json.dumps(data),
        )
        return body["securityToken"]

    def _get_vehicle_action_header(
        self, content_type: str, security_token: str, host: Optional[str] = None
    ):
        if not host:
            host = (
                "mal-3a.prd.eu.dp.vwg-connect.com"
                if self._country in {"DE", "US"}
                else "msg.volkswagen.de"
            )

        headers = {
            "User-Agent": AudiAPI.HDR_USER_AGENT,
            "Host": host,
            "X-App-Version": AudiAPI.HDR_XAPP_VERSION,
            "X-App-Name": "myAudi",
            "Authorization": "Bearer " + self.vwToken.get("access_token"),
            "Accept-charset": "UTF-8",
            "Content-Type": content_type,
            "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,*/*",
        }

        if security_token:
            headers["x-securityToken"] = security_token

        return headers

    def __build_url(
        self, base_url: str, path_and_query: str, **path_and_query_kwargs: dict
    ):
        action_path = path_and_query.format(**path_and_query_kwargs)

        return base_url.rstrip("/") + "/" + action_path.lstrip("/")

    def __get_cariad_url(self, path_and_query: str, **path_and_query_kwargs: dict):
        base_url = "https://{region}.bff.cariad.digital".format(
            region="emea" if self._country.upper() != "US" else "na"
        )

        return self.__build_url(base_url, path_and_query, **path_and_query_kwargs)

    def __get_cariad_url_for_vin(
        self, vin: str, path_and_query: str, **path_and_query_kwargs: dict
    ):
        base_url = self.__get_cariad_url("/vehicle/v1/vehicles/{vin}", vin=vin.upper())

        return self.__build_url(base_url, path_and_query, **path_and_query_kwargs)

    async def set_vehicle_lock(self, vin: str, lock: bool):
        security_token = await self._get_security_token(
            vin, "rlu_v1/operations/" + ("LOCK" if lock else "UNLOCK")
        )
        # deprecated data removed on 24Mar2025
        # data = '<?xml version="1.0" encoding= "UTF-8" ?><rluAction xmlns="http://audi.de/connect/rlu"><action>{action}</action></rluAction>'.format(
        #     action="lock" if lock else "unlock"
        # )
        data = None

        headers = self._get_vehicle_action_header(
            "application/vnd.vwg.mbb.RemoteLockUnlock_v1_0_0+xml", security_token
        )
        res = await self._api.request(
            "POST",
            "{homeRegionSetter}/api/bs/rlu/v1/vehicles/{vin}/{action}".format(
                homeRegionSetter=await self._get_home_region_setter(vin.upper()),
                vin=vin.upper(),
                action="lock" if lock else "unlock",
            ),
            headers=headers,
            data=data,
        )

        checkUrl = "{homeRegionSetter}/api/bs/rlu/v1/vehicles/{vin}/requests/{requestId}/status".format(
            homeRegionSetter=await self._get_home_region_setter(vin.upper()),
            vin=vin.upper(),
            requestId=res["rluActionResponse"]["requestId"],
        )

        await self.check_request_succeeded(
            checkUrl,
            "lock vehicle" if lock else "unlock vehicle",
            REQUEST_SUCCESSFUL,
            REQUEST_FAILED,
            "requestStatusResponse.status",
        )

    async def set_battery_charger(self, vin: str, start: bool, timer: bool):
        if start and timer:
            data = {"preferredChargeMode": "timer"}
        elif start:
            data = {"preferredChargeMode": "manual"}
        else:
            raise NotImplementedError(
                "The 'Stop Charger' service is deprecated and will be removed in a future release."
            )

        data = json.dumps(data)
        headers = {"Authorization": "Bearer " + self._bearer_token_json["access_token"]}

        await self._api.request(
            "PUT",
            self.__get_cariad_url_for_vin(vin, "charging/mode"),
            headers=headers,
            data=data,
        )

        # checkUrl = "{homeRegion}/fs-car/bs/batterycharge/v1/{type}/{country}/vehicles/{vin}/charger/actions/{actionid}".format(
        #     homeRegion=await self._get_home_region(vin.upper()),
        #     type=self._type,
        #     country=self._country,
        #     vin=vin.upper(),
        #     actionid=res["action"]["actionId"],
        # )

        # await self.check_request_succeeded(
        #     checkUrl,
        #     "start charger" if start else "stop charger",
        #     SUCCEEDED,
        #     FAILED,
        #     "action.actionState",
        # )

    async def set_target_state_of_charge(self, vin: str, target_soc: int):
        """Set the target state of charge (battery percentage)."""
        if not (20 <= target_soc <= 100):
            raise ValueError(
                "Target state of charge must be between 20 and 100 percent"
            )

        # Use Cariad BFF API (requires API level 1)
        headers = {"Authorization": "Bearer " + self._bearer_token_json["access_token"]}

        data = {"targetSOC_pct": target_soc}

        await self._api.request(
            "PUT",
            self.__get_cariad_url_for_vin(vin, "charging/settings"),
            headers=headers,
            data=json.dumps(data),
        )

    async def set_climatisation(self, vin: str, start: bool):
        api_level = self._api_level
        country = self._country

        if start:
            raise NotImplementedError(
                "The 'Start Climatisation (Legacy)' service is deprecated and no longer functional. "
                "Please use the 'Start Climate Control' service instead."
            )
            # data = '{"action":{"type": "startClimatisation","settings": {"targetTemperature": 2940,"climatisationWithoutHVpower": true,"heaterSource": "electric","climaterElementSettings": {"isClimatisationAtUnlock": false, "isMirrorHeatingEnabled": true,}}}}'
        else:
            if api_level == 0:
                data = '{"action":{"type": "stopClimatisation"}}'

                if country == "US":
                    headers = self._get_vehicle_action_header("application/json", None)
                    res = await self._api.request(
                        "POST",
                        "https://mal-3a.prd.eu.dp.vwg-connect.com/api/bs/climatisation/v1/vehicles/{vin}/climater/actions".format(
                            vin=vin.upper(),
                        ),
                        headers=headers,
                        data=data,
                    )
                    checkUrl = "https://mal-3a.prd.eu.dp.vwg-connect.com/api/bs/climatisation/v1/vehicles/{vin}/climater/actions/{actionid}".format(
                        vin=vin.upper(),
                        actionid=res["action"]["actionId"],
                    )

                else:
                    headers = self._get_vehicle_action_header(
                        "application/json", None, "msg.volkswagen.de"
                    )
                    res = await self._api.request(
                        "POST",
                        "{homeRegion}/fs-car/bs/climatisation/v1/{type}/{country}/vehicles/{vin}/climater/actions".format(
                            homeRegion=await self._get_home_region(vin.upper()),
                            type=self._type,
                            country=self._country,
                            vin=vin.upper(),
                        ),
                        headers=headers,
                        data=data,
                    )

                    checkUrl = "{homeRegion}/fs-car/bs/climatisation/v1/{type}/{country}/vehicles/{vin}/climater/actions/{actionid}".format(
                        homeRegion=await self._get_home_region(vin.upper()),
                        type=self._type,
                        country=self._country,
                        vin=vin.upper(),
                        actionid=res["action"]["actionId"],
                    )

                await self.check_request_succeeded(
                    checkUrl,
                    "stop climatisation",
                    SUCCEEDED,
                    FAILED,
                    "action.actionState",
                )

            elif api_level == 1:
                data = None
                headers = {
                    "Authorization": "Bearer " + self._bearer_token_json["access_token"]
                }
                res = await self._api.request(
                    "POST",
                    self.__get_cariad_url_for_vin(vin, "climatisation/stop"),
                    headers=headers,
                    data=data,
                )

                # checkUrl = "https://emea.bff.cariad.digital/vehicle/v1/vehicles/{vin}/pendingrequests".format(
                #     vin=vin.upper(),
                #     actionid=res["action"]["actionId"],
                # )

                # await self.check_request_succeeded(
                #     checkUrl,
                #     "startClimatisation",
                #     SUCCEEDED,
                #     FAILED,
                #     "action.actionState",
                # )

    async def start_climate_control(
        self,
        vin: str,
        temp_f: int,
        temp_c: int,
        glass_heating: bool,
        seat_fl: bool,
        seat_fr: bool,
        seat_rl: bool,
        seat_rr: bool,
        climatisation_at_unlock: bool = False,
        climatisation_mode: str = "comfort",
    ):
        api_level = self._api_level
        country = self._country
        target_temperature = None

        _LOGGER.debug(
            f"Attempting to start climate control with API Level {api_level} and country {country}."
        )

        if api_level == 0:
            target_temperature = None
            if temp_f is not None:
                target_temperature = int(((temp_f - 32) * (5 / 9)) * 10 + 2731)
            elif temp_c is not None:
                target_temperature = int(temp_c * 10 + 2731)

            # Default Temp
            target_temperature = target_temperature or 2941

            # Construct Zone Settings
            zone_settings = [
                {"value": {"isEnabled": seat_fl, "position": "frontLeft"}},
                {"value": {"isEnabled": seat_fr, "position": "frontRight"}},
                {"value": {"isEnabled": seat_rl, "position": "rearLeft"}},
                {"value": {"isEnabled": seat_rr, "position": "rearRight"}},
            ]

            data = {
                "action": {
                    "type": "startClimatisation",
                    "settings": {
                        "targetTemperature": target_temperature,
                        "climatisationWithoutHVpower": True,
                        "heaterSource": "electric",
                        "climaterElementSettings": {
                            "isClimatisationAtUnlock": climatisation_at_unlock,
                            "isMirrorHeatingEnabled": glass_heating,
                            "zoneSettings": {"zoneSetting": zone_settings},
                        },
                    },
                }
            }

            data = json.dumps(data)

            if country == "US":
                headers = self._get_vehicle_action_header("application/json", None)
                res = await self._api.request(
                    "POST",
                    "https://mal-3a.prd.eu.dp.vwg-connect.com/api/bs/climatisation/v1/vehicles/{vin}/climater/actions".format(
                        vin=vin.upper(),
                    ),
                    headers=headers,
                    data=data,
                )

                checkUrl = "https://mal-3a.prd.eu.dp.vwg-connect.com/api/bs/climatisation/v1/vehicles/{vin}/climater/actions/{actionid}".format(
                    vin=vin.upper(),
                    actionid=res["action"]["actionId"],
                )
            else:
                headers = self._get_vehicle_action_header(
                    "application/json", None, "msg.volkswagen.de"
                )
                res = await self._api.request(
                    "POST",
                    "{homeRegion}/fs-car/bs/climatisation/v1/{type}/{country}/vehicles/{vin}/climater/actions".format(
                        homeRegion=await self._get_home_region(vin.upper()),
                        type=self._type,
                        country=self._country,
                        vin=vin.upper(),
                    ),
                    headers=headers,
                    data=data,
                )

                checkUrl = "{homeRegion}/fs-car/bs/climatisation/v1/{type}/{country}/vehicles/{vin}/climater/actions/{actionid}".format(
                    homeRegion=await self._get_home_region(vin.upper()),
                    type=self._type,
                    country=self._country,
                    vin=vin.upper(),
                    actionid=res["action"]["actionId"],
                )

            await self.check_request_succeeded(
                checkUrl,
                "startClimatisation",
                SUCCEEDED,
                FAILED,
                "action.actionState",
            )

        elif api_level == 1:
            if temp_f is not None:
                target_temperature = int((temp_f - 32) * (5 / 9))
            elif temp_c is not None:
                target_temperature = int(temp_c)

            target_temperature = target_temperature or 21

            data = {
                "climatisationMode": climatisation_mode,
                "targetTemperature": target_temperature,
                "targetTemperatureUnit": "celsius",
                "climatisationWithoutExternalPower": True,
                "climatizationAtUnlock": climatisation_at_unlock,
                "windowHeatingEnabled": glass_heating,
                "zoneFrontLeftEnabled": seat_fl,
                "zoneFrontRightEnabled": seat_fr,
                "zoneRearLeftEnabled": seat_rl,
                "zoneRearRightEnabled": seat_rr,
            }

            data = json.dumps(data)
            headers = {
                "Authorization": "Bearer " + self._bearer_token_json["access_token"]
            }
            res = await self._api.request(
                "POST",
                self.__get_cariad_url_for_vin(vin, "climatisation/start"),
                headers=headers,
                data=data,
            )

            # checkUrl = "https://emea.bff.cariad.digital/vehicle/v1/vehicles/{vin}/pendingrequests".format(
            #     vin=vin.upper(),
            #     actionid=res["action"]["actionId"],
            # )

            # await self.check_request_succeeded(
            #     checkUrl,
            #     "startClimatisation",
            #     SUCCEEDED,
            #     FAILED,
            #     "action.actionState",
            # )

    async def set_window_heating(self, vin: str, start: bool):
        data = '<?xml version="1.0" encoding= "UTF-8" ?><action><type>{action}</type></action>'.format(
            action="startWindowHeating" if start else "stopWindowHeating"
        )

        headers = self._get_vehicle_action_header(
            "application/vnd.vwg.mbb.ClimaterAction_v1_0_0+xml", None
        )
        res = await self._api.request(
            "POST",
            "{homeRegion}/fs-car/bs/climatisation/v1/{type}/{country}/vehicles/{vin}/climater/actions".format(
                homeRegion=await self._get_home_region(vin.upper()),
                type=self._type,
                country=self._country,
                vin=vin.upper(),
            ),
            headers=headers,
            data=data,
        )

        checkUrl = "{homeRegion}/fs-car/bs/climatisation/v1/{type}/{country}/vehicles/{vin}/climater/actions/{actionid}".format(
            homeRegion=await self._get_home_region(vin.upper()),
            type=self._type,
            country=self._country,
            vin=vin.upper(),
            actionid=res["action"]["actionId"],
        )

        await self.check_request_succeeded(
            checkUrl,
            "start window heating" if start else "stop window heating",
            SUCCEEDED,
            FAILED,
            "action.actionState",
        )

    async def set_pre_heater(
        self, vin: str, activate: bool, duration: Optional[int] = None
    ):
        if activate:
            if not duration:
                duration = 30
            data = {
                "duration_min": int(duration),
                "spin": self._spin,
            }

            data = json.dumps(data)
        else:
            data = None

        headers = {
            "Accept": "application/json",
            "Accept-charset": "utf-8",
            "Authorization": "Bearer " + self._bearer_token_json["access_token"],
            "User-Agent": AudiAPI.HDR_USER_AGENT,
            "Content-Type": "application/json; charset=utf-8",
            "Accept-encoding": "gzip",
        }
        res = await self._api.request(
            "POST",
            self.__get_cariad_url_for_vin(
                vin, "auxiliaryheating/{action}", action="start" if activate else "stop"
            ),
            headers=headers,
            data=data,
        )

        await self.check_bff_request_succeeded(vin, res["data"]["requestID"])

    async def check_bff_request_succeeded(self, vin: str, request_id: str):
        headers = {
            "Accept": "application/json",
            "Accept-charset": "utf-8",
            "Authorization": "Bearer " + self._bearer_token_json["access_token"],
            "User-Agent": AudiAPI.HDR_USER_AGENT,
            "Content-Type": "application/json; charset=utf-8",
            "Accept-encoding": "gzip",
        }

        for _ in range(MAX_RESPONSE_ATTEMPTS):
            await asyncio.sleep(REQUEST_STATUS_SLEEP)
            res = await self._api.request(
                "GET",
                "https://{homeRegion}.bff.cariad.digital/vehicle/v1/vehicles/{vin}/pendingrequests".format(
                    homeRegion="na" if self._country.upper() == "US" else "emea",
                    vin=vin.upper(),
                ),
                headers=headers,
                data=None,
            )

            for pending_request in res["data"]:
                if pending_request["id"] == request_id:
                    if pending_request["status"] == "in_progress":
                        break  # continue waiting

                    if pending_request["status"] == "successful":
                        return

                    raise Exception(
                        "Request {} reached unexpected status {}".format(
                            request_id, pending_request["status"]
                        )
                    )

        raise Exception("Request {} timed out".format(request_id))

    async def check_request_succeeded(
        self, url: str, action: str, successCode: str, failedCode: str, path: str
    ):
        for _ in range(MAX_RESPONSE_ATTEMPTS):
            await asyncio.sleep(REQUEST_STATUS_SLEEP)

            self._api.use_token(self.vwToken)
            res = await self._api.get(url)

            status = get_attr(res, path)

            if status is None or (failedCode is not None and status == failedCode):
                raise Exception(
                    "Cannot {action}, return code '{code}'".format(
                        action=action, code=status
                    )
                )

            if status == successCode:
                return

        raise Exception("Cannot {action}, operation timed out".format(action=action))

    # TR/2022-12-20: New secret for X_QMAuth
    def _calculate_X_QMAuth(self):
        # Calculate X-QMAuth value
        gmtime_100sec = int(
            (datetime.utcnow() - datetime(1970, 1, 1)).total_seconds() / 100
        )
        xqmauth_secret = bytes(
            [
                26,
                256 - 74,
                256 - 103,
                37,
                256 - 84,
                23,
                256 - 102,
                256 - 86,
                78,
                256 - 125,
                256 - 85,
                256 - 26,
                113,
                256 - 87,
                71,
                109,
                23,
                100,
                24,
                256 - 72,
                91,
                256 - 41,
                6,
                256 - 15,
                67,
                108,
                256 - 95,
                91,
                256 - 26,
                71,
                256 - 104,
                256 - 100,
            ]
        )
        xqmauth_val = hmac.new(
            xqmauth_secret,
            str(gmtime_100sec).encode("ascii", "ignore"),
            digestmod="sha256",
        ).hexdigest()

        # v1:01da27b0:fbdb6e4ba3109bc68040cb83f380796f4d3bb178a626c4cc7e166815b806e4b5
        return "v1:01da27b0:" + xqmauth_val

    # TR/2021-12-01: Refresh token before it expires
    # returns True when refresh was required and successful, otherwise False
    async def refresh_token_if_necessary(self, elapsed_sec: int) -> bool:
        if self.mbboauthToken is None:
            return False
        if "refresh_token" not in self.mbboauthToken:
            return False
        if "expires_in" not in self.mbboauthToken:
            return False

        if (elapsed_sec + 5 * 60) < self.mbboauthToken["expires_in"]:
            # refresh not needed now
            return False

        try:
            headers = {
                "Accept": "application/json",
                "Accept-Charset": "utf-8",
                "User-Agent": AudiAPI.HDR_USER_AGENT,
                "Content-Type": "application/x-www-form-urlencoded",
                "X-Client-ID": self.xclientId,
            }
            mbboauth_refresh_data = {
                "grant_type": "refresh_token",
                "token": self.mbboauthToken["refresh_token"],
                "scope": "sc2:fal",
                # "vin": vin,  << App uses a dedicated VIN here, but it works without, don't know
            }
            encoded_mbboauth_refresh_data = urlencode(
                mbboauth_refresh_data, encoding="utf-8"
            ).replace("+", "%20")
            mbboauth_refresh_rsp, mbboauth_refresh_rsptxt = await self._api.request(
                "POST",
                self.mbbOAuthBaseURL + "/mobile/oauth2/v1/token",
                encoded_mbboauth_refresh_data,
                headers=headers,
                allow_redirects=False,
                rsp_wtxt=True,
            )

            # this code is the old "vwToken"
            self.vwToken = json.loads(mbboauth_refresh_rsptxt)

            # TR/2022-02-10: If a new refresh_token is provided, save it for further refreshes
            if "refresh_token" in self.vwToken:
                self.mbboauthToken["refresh_token"] = self.vwToken["refresh_token"]

            # hdr
            headers = {
                "Accept": "application/json",
                "Accept-Charset": "utf-8",
                "X-QMAuth": self._calculate_X_QMAuth(),
                "User-Agent": AudiAPI.HDR_USER_AGENT,
                "Content-Type": "application/x-www-form-urlencoded",
            }
            # IDK token request data
            tokenreq_data = {
                "client_id": self._client_id,
                "grant_type": "refresh_token",
                "refresh_token": self._bearer_token_json.get("refresh_token"),
                "response_type": "token id_token",
            }
            # IDK token request
            encoded_tokenreq_data = urlencode(tokenreq_data, encoding="utf-8").replace(
                "+", "%20"
            )
            bearer_token_rsp, bearer_token_rsptxt = await self._api.request(
                "POST",
                self._tokenEndpoint,
                encoded_tokenreq_data,
                headers=headers,
                allow_redirects=False,
                rsp_wtxt=True,
            )
            self._bearer_token_json = json.loads(bearer_token_rsptxt)

            # AZS token
            headers = {
                "Accept": "application/json",
                "Accept-Charset": "utf-8",
                "X-App-Version": AudiAPI.HDR_XAPP_VERSION,
                "X-App-Name": "myAudi",
                "User-Agent": AudiAPI.HDR_USER_AGENT,
                "Content-Type": "application/json; charset=utf-8",
            }
            asz_req_data = {
                "token": self._bearer_token_json["access_token"],
                "grant_type": "id_token",
                "stage": "live",
                "config": "myaudi",
            }
            azs_token_rsp, azs_token_rsptxt = await self._api.request(
                "POST",
                self._authorizationServerBaseURLLive + "/token",
                json.dumps(asz_req_data),
                headers=headers,
                allow_redirects=False,
                rsp_wtxt=True,
            )
            azs_token_json = json.loads(azs_token_rsptxt)
            self.audiToken = azs_token_json

            return True

        except Exception as exception:
            _LOGGER.error("Refresh token failed: " + str(exception))
            return False

    # TR/2021-12-01 updated to match behaviour of Android myAudi 4.5.0
    async def login_request(self, user: str, password: str):
        self._api.use_token(None)
        self._api.set_xclient_id(None)
        self.xclientId = None

        # get markets
        markets_json = await self._api.request(
            "GET",
            "https://content.app.my.audi.com/service/mobileapp/configurations/markets",
            None,
        )
        if (
            self._country.upper()
            not in markets_json["countries"]["countrySpecifications"]
        ):
            raise Exception("Country not found")
        self._language = markets_json["countries"]["countrySpecifications"][
            self._country.upper()
        ]["defaultLanguage"]

        # Dynamic configuration URLs
        marketcfg_url = "https://content.app.my.audi.com/service/mobileapp/configurations/market/{c}/{l}?v=4.23.1".format(
            c=self._country, l=self._language
        )
        openidcfg_url = self.__get_cariad_url("/login/v1/idk/openid-configuration")

        # get market config
        marketcfg_json = await self._api.request("GET", marketcfg_url, None)

        # use dynamic config from marketcfg
        self._client_id = "09b6cbec-cd19-4589-82fd-363dfa8c24da@apps_vw-dilab_com"
        if "idkClientIDAndroidLive" in marketcfg_json:
            self._client_id = marketcfg_json["idkClientIDAndroidLive"]

        self._authorizationServerBaseURLLive = self.__get_cariad_url("/login/v1/audi")

        if "authorizationServerBaseURLLive" in marketcfg_json:
            self._authorizationServerBaseURLLive = marketcfg_json[
                "myAudiAuthorizationServerProxyServiceURLProduction"
            ]
        self.mbbOAuthBaseURL = "https://mbboauth-1d.prd.ece.vwg-connect.com/mbbcoauth"
        if "mbbOAuthBaseURLLive" in marketcfg_json:
            self.mbbOAuthBaseURL = marketcfg_json["mbbOAuthBaseURLLive"]

        # get openId config
        openidcfg_json = await self._api.request("GET", openidcfg_url, None)

        # use dynamic config from openId config
        authorization_endpoint = "https://identity.vwgroup.io/oidc/v1/authorize"
        if "authorization_endpoint" in openidcfg_json:
            authorization_endpoint = openidcfg_json["authorization_endpoint"]

        self._tokenEndpoint = self.__get_cariad_url("/login/v1/idk/token")

        if "token_endpoint" in openidcfg_json:
            self._tokenEndpoint = openidcfg_json["token_endpoint"]
        # revocation_endpoint = self.__get_cariad_base_url("/login/v1/idk/revoke")
        # if "revocation_endpoint" in openidcfg_json:
        # revocation_endpoint = openidcfg_json["revocation_endpoint"]

        # generate code_challenge
        code_verifier = str(base64.urlsafe_b64encode(os.urandom(32)), "utf-8").strip(
            "="
        )
        code_challenge = str(
            base64.urlsafe_b64encode(
                sha256(code_verifier.encode("ascii", "ignore")).digest()
            ),
            "utf-8",
        ).strip("=")
        code_challenge_method = "S256"

        #
        state = str(uuid.uuid4())
        nonce = str(uuid.uuid4())

        # login page
        headers = {
            "Accept": "application/json",
            "Accept-Charset": "utf-8",
            "X-App-Version": AudiAPI.HDR_XAPP_VERSION,
            "X-App-Name": "myAudi",
            "User-Agent": AudiAPI.HDR_USER_AGENT,
        }
        idk_data = {
            "response_type": "code",
            "client_id": self._client_id,
            "redirect_uri": "myaudi:///",
            "scope": "address profile badge birthdate birthplace nationalIdentifier nationality profession email vin phone nickname name picture mbb gallery openid",
            "state": state,
            "nonce": nonce,
            "prompt": "login",
            "code_challenge": code_challenge,
            "code_challenge_method": code_challenge_method,
            "ui_locales": "de-de de",
        }
        idk_rsp, idk_rsptxt = await self._api.request(
            "GET",
            authorization_endpoint,
            None,
            headers=headers,
            params=idk_data,
            rsp_wtxt=True,
        )

        # form_data with email
        submit_data = self.get_hidden_html_input_form_data(idk_rsptxt, {"email": user})
        submit_url = self.get_post_url(idk_rsptxt, authorization_endpoint)
        # send email
        email_rsp, email_rsptxt = await self._api.request(
            "POST",
            submit_url,
            submit_data,
            headers=headers,
            cookies=idk_rsp.cookies,
            allow_redirects=True,
            rsp_wtxt=True,
        )

        # form_data with password
        # 2022-01-29: new HTML response uses a js two build the html form data + button.
        #             Therefore it's not possible to extract hmac and other form data.
        #             --> extract hmac from embedded js snippet.
        regex_res = re.findall('"hmac"\\s*:\\s*"[0-9a-fA-F]+"', email_rsptxt)
        if regex_res:
            submit_url = submit_url.replace("identifier", "authenticate")
            submit_data["hmac"] = regex_res[0].split(":")[1].strip('"')
            submit_data["password"] = password
        else:
            submit_data = self.get_hidden_html_input_form_data(
                email_rsptxt, {"password": password}
            )
            submit_url = self.get_post_url(email_rsptxt, submit_url)

        # send password
        pw_rsp, pw_rsptxt = await self._api.request(
            "POST",
            submit_url,
            submit_data,
            headers=headers,
            cookies=idk_rsp.cookies,
            allow_redirects=False,
            rsp_wtxt=True,
        )

        # forward1 after pwd
        fwd1_rsp, fwd1_rsptxt = await self._api.request(
            "GET",
            pw_rsp.headers["Location"],
            None,
            headers=headers,
            cookies=idk_rsp.cookies,
            allow_redirects=False,
            rsp_wtxt=True,
        )
        # forward2 after pwd
        fwd2_rsp, fwd2_rsptxt = await self._api.request(
            "GET",
            fwd1_rsp.headers["Location"],
            None,
            headers=headers,
            cookies=idk_rsp.cookies,
            allow_redirects=False,
            rsp_wtxt=True,
        )
        # get tokens
        codeauth_rsp, codeauth_rsptxt = await self._api.request(
            "GET",
            fwd2_rsp.headers["Location"],
            None,
            headers=headers,
            cookies=fwd2_rsp.cookies,
            allow_redirects=False,
            rsp_wtxt=True,
        )
        authcode_parsed = urlparse(
            codeauth_rsp.headers["Location"][len("myaudi:///?") :]
        )
        authcode_strings = parse_qs(authcode_parsed.path)

        # hdr
        headers = {
            "Accept": "application/json",
            "Accept-Charset": "utf-8",
            "X-QMAuth": self._calculate_X_QMAuth(),
            "User-Agent": AudiAPI.HDR_USER_AGENT,
            "Content-Type": "application/x-www-form-urlencoded",
        }
        # IDK token request data
        tokenreq_data = {
            "client_id": self._client_id,
            "grant_type": "authorization_code",
            "code": authcode_strings["code"][0],
            "redirect_uri": "myaudi:///",
            "response_type": "token id_token",
            "code_verifier": code_verifier,
        }
        # IDK token request
        encoded_tokenreq_data = urlencode(tokenreq_data, encoding="utf-8").replace(
            "+", "%20"
        )
        bearer_token_rsp, bearer_token_rsptxt = await self._api.request(
            "POST",
            self._tokenEndpoint,
            encoded_tokenreq_data,
            headers=headers,
            allow_redirects=False,
            rsp_wtxt=True,
        )
        self._bearer_token_json = json.loads(bearer_token_rsptxt)

        # AZS token
        headers = {
            "Accept": "application/json",
            "Accept-Charset": "utf-8",
            "X-App-Version": AudiAPI.HDR_XAPP_VERSION,
            "X-App-Name": "myAudi",
            "User-Agent": AudiAPI.HDR_USER_AGENT,
            "Content-Type": "application/json; charset=utf-8",
        }
        asz_req_data = {
            "token": self._bearer_token_json["access_token"],
            "grant_type": "id_token",
            "stage": "live",
            "config": "myaudi",
        }
        azs_token_rsp, azs_token_rsptxt = await self._api.request(
            "POST",
            self._authorizationServerBaseURLLive + "/token",
            json.dumps(asz_req_data),
            headers=headers,
            allow_redirects=False,
            rsp_wtxt=True,
        )
        azs_token_json = json.loads(azs_token_rsptxt)
        self.audiToken = azs_token_json

        # mbboauth client register
        headers = {
            "Accept": "application/json",
            "Accept-Charset": "utf-8",
            "User-Agent": AudiAPI.HDR_USER_AGENT,
            "Content-Type": "application/json; charset=utf-8",
        }
        mbboauth_reg_data = {
            "client_name": "SM-A405FN",
            "platform": "google",
            "client_brand": "Audi",
            "appName": "myAudi",
            "appVersion": AudiAPI.HDR_XAPP_VERSION,
            "appId": "de.myaudi.mobile.assistant",
        }
        mbboauth_client_reg_rsp, mbboauth_client_reg_rsptxt = await self._api.request(
            "POST",
            self.mbbOAuthBaseURL + "/mobile/register/v1",
            json.dumps(mbboauth_reg_data),
            headers=headers,
            allow_redirects=False,
            rsp_wtxt=True,
        )
        mbboauth_client_reg_json = json.loads(mbboauth_client_reg_rsptxt)
        self.xclientId = mbboauth_client_reg_json["client_id"]
        self._api.set_xclient_id(self.xclientId)

        # mbboauth auth
        headers = {
            "Accept": "application/json",
            "Accept-Charset": "utf-8",
            "User-Agent": AudiAPI.HDR_USER_AGENT,
            "Content-Type": "application/x-www-form-urlencoded",
            "X-Client-ID": self.xclientId,
        }
        mbboauth_auth_data = {
            "grant_type": "id_token",
            "token": self._bearer_token_json["id_token"],
            "scope": "sc2:fal",
        }
        encoded_mbboauth_auth_data = urlencode(
            mbboauth_auth_data, encoding="utf-8"
        ).replace("+", "%20")
        mbboauth_auth_rsp, mbboauth_auth_rsptxt = await self._api.request(
            "POST",
            self.mbbOAuthBaseURL + "/mobile/oauth2/v1/token",
            encoded_mbboauth_auth_data,
            headers=headers,
            allow_redirects=False,
            rsp_wtxt=True,
        )
        mbboauth_auth_json = json.loads(mbboauth_auth_rsptxt)
        # store token and expiration time
        self.mbboauthToken = mbboauth_auth_json

        # mbboauth refresh (app immediately refreshes the token)
        headers = {
            "Accept": "application/json",
            "Accept-Charset": "utf-8",
            "User-Agent": AudiAPI.HDR_USER_AGENT,
            "Content-Type": "application/x-www-form-urlencoded",
            "X-Client-ID": self.xclientId,
        }
        mbboauth_refresh_data = {
            "grant_type": "refresh_token",
            "token": mbboauth_auth_json["refresh_token"],
            "scope": "sc2:fal",
            # "vin": vin,  << App uses a dedicated VIN here, but it works without, don't know
        }
        encoded_mbboauth_refresh_data = urlencode(
            mbboauth_refresh_data, encoding="utf-8"
        ).replace("+", "%20")
        mbboauth_refresh_rsp, mbboauth_refresh_rsptxt = await self._api.request(
            "POST",
            self.mbbOAuthBaseURL + "/mobile/oauth2/v1/token",
            encoded_mbboauth_refresh_data,
            headers=headers,
            allow_redirects=False,
            cookies=mbboauth_client_reg_rsp.cookies,
            rsp_wtxt=True,
        )
        # this code is the old "vwToken"
        self.vwToken = json.loads(mbboauth_refresh_rsptxt)

    def _generate_security_pin_hash(self, challenge):
        if self._spin is None:
            raise Exception("sPin is required to perform this action")

        pin = to_byte_array(self._spin)
        byteChallenge = to_byte_array(challenge)
        b = bytes(pin + byteChallenge)
        return sha512(b).hexdigest().upper()

    async def _emulate_browser(
        self, reply: BrowserLoginResponse, form_data: Dict[str, str]
    ) -> BrowserLoginResponse:
        # The reply redirects to the login page
        login_location = reply.get_location()
        page_reply = await self._api.get(login_location, raw_contents=True)

        # Now parse the html body and extract the target url, csrf token and other required parameters
        html = BeautifulSoup(page_reply, "html.parser")
        form_tag = html.find("form")

        form_inputs = html.find_all("input", attrs={"type": "hidden"})
        for form_input in form_inputs:
            name = form_input.get("name")
            form_data[name] = form_input.get("value")

        # Extract the target url
        action = form_tag.get("action")
        if action.startswith("http"):
            # Absolute url
            username_post_url = action
        elif action.startswith("/"):
            # Relative to domain
            username_post_url = BrowserLoginResponse.to_absolute(login_location, action)
        else:
            raise RequestException("Unknown form action: " + action)

        headers = {"referer": login_location}
        reply = await self._api.post(
            username_post_url,
            form_data,
            headers=headers,
            use_json=False,
            raw_reply=True,
            allow_redirects=False,
        )
        return BrowserLoginResponse(reply, username_post_url)


================================================
FILE: custom_components/audiconnect/binary_sensor.py
================================================
"""Support for Audi Connect sensors."""

import logging

from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.const import CONF_USERNAME

from .audi_entity import AudiEntity
from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)


async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
    """Old way."""


async def async_setup_entry(hass, config_entry, async_add_entities):
    sensors = []
    account = config_entry.data.get(CONF_USERNAME)
    audiData = hass.data[DOMAIN][account]

    for config_vehicle in audiData.config_vehicles:
        for binary_sensor in config_vehicle.binary_sensors:
            sensors.append(AudiSensor(config_vehicle, binary_sensor))

    async_add_entities(sensors)


class AudiSensor(AudiEntity, BinarySensorEntity):
    """Representation of an Audi sensor."""

    @property
    def is_on(self):
        """Return True if the binary sensor is on."""
        return self._instrument.is_on

    @property
    def device_class(self):
        """Return the device_class of this sensor."""
        return self._instrument.device_class

    @property
    def entity_category(self):
        """Return the entity_category."""
        return self._instrument.entity_category


================================================
FILE: custom_components/audiconnect/config_flow.py
================================================
from collections import OrderedDict
import logging
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import (
    CONF_PASSWORD,
    CONF_USERNAME,
    CONF_REGION,
    CONF_SCAN_INTERVAL,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.core import callback

from .audi_connect_account import AudiConnectAccount
from .const import (
    DOMAIN,
    CONF_SPIN,
    DEFAULT_UPDATE_INTERVAL,
    MIN_UPDATE_INTERVAL,
    CONF_SCAN_INITIAL,
    CONF_SCAN_ACTIVE,
    REGIONS,
    CONF_API_LEVEL,
    DEFAULT_API_LEVEL,
    API_LEVELS,
    CONF_FILTER_VINS,
)

_LOGGER = logging.getLogger(__name__)


@callback
def configured_accounts(hass):
    """Return tuple of configured usernames."""
    entries = hass.config_entries.async_entries(DOMAIN)
    if entries:
        return (entry.data[CONF_USERNAME] for entry in entries)
    return ()


@config_entries.HANDLERS.register(DOMAIN)
class AudiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
    def __init__(self):
        """Initialize."""
        self._username = vol.UNDEFINED
        self._password = vol.UNDEFINED
        self._spin = vol.UNDEFINED
        self._region = vol.UNDEFINED
        self._scan_interval = DEFAULT_UPDATE_INTERVAL
        self._api_level = DEFAULT_API_LEVEL

    async def async_step_user(self, user_input=None):
        """Handle a user initiated config flow."""
        errors = {}

        if user_input is not None:
            self._username = user_input[CONF_USERNAME]
            self._password = user_input[CONF_PASSWORD]
            self._spin = user_input.get(CONF_SPIN)
            self._region = REGIONS[user_input.get(CONF_REGION)]
            self._scan_interval = user_input[CONF_SCAN_INTERVAL]
            self._api_level = user_input[CONF_API_LEVEL]

            try:
                # pylint: disable=no-value-for-parameter
                session = async_get_clientsession(self.hass)
                connection = AudiConnectAccount(
                    session=session,
                    username=vol.Email()(self._username),
                    password=self._password,
                    country=self._region,
                    spin=self._spin,
                    api_level=self._api_level,
                )

                if await connection.try_login(False) is False:
                    raise Exception(
                        "Unexpected error communicating with the Audi server"
                    )

            except vol.Invalid:
                errors[CONF_USERNAME] = "invalid_username"
            except Exception:
                errors["base"] = "invalid_credentials"
            else:
                if self._username in configured_accounts(self.hass):
                    errors["base"] = "user_already_configured"
                else:
                    return self.async_create_entry(
                        title=f"{self._username}",
                        data={
                            CONF_USERNAME: self._username,
                            CONF_PASSWORD: self._password,
                            CONF_SPIN: self._spin,
                            CONF_REGION: self._region,
                            CONF_SCAN_INTERVAL: self._scan_interval,
                            CONF_API_LEVEL: self._api_level,
                        },
                    )

        data_schema = OrderedDict()
        data_schema[vol.Required(CONF_USERNAME, default=self._username)] = str
        data_schema[vol.Required(CONF_PASSWORD, default=self._password)] = str
        data_schema[vol.Optional(CONF_SPIN, default=self._spin)] = str
        data_schema[vol.Required(CONF_REGION, default=self._region)] = vol.In(REGIONS)
        data_schema[
            vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_UPDATE_INTERVAL)
        ] = int
     
Download .txt
gitextract_84fzj90j/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── inactiveIssues.yml
│       ├── release.yml
│       ├── semanticTitle.yaml
│       └── validate.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CONTRIBUTING.md
├── LICENSE
├── custom_components/
│   ├── audiconnect/
│   │   ├── __init__.py
│   │   ├── audi_account.py
│   │   ├── audi_api.py
│   │   ├── audi_connect_account.py
│   │   ├── audi_entity.py
│   │   ├── audi_models.py
│   │   ├── audi_services.py
│   │   ├── binary_sensor.py
│   │   ├── config_flow.py
│   │   ├── const.py
│   │   ├── dashboard.py
│   │   ├── device_tracker.py
│   │   ├── lock.py
│   │   ├── manifest.json
│   │   ├── sensor.py
│   │   ├── services.yaml
│   │   ├── strings.json
│   │   ├── switch.py
│   │   ├── translations/
│   │   │   ├── de.json
│   │   │   ├── en.json
│   │   │   ├── fi.json
│   │   │   ├── fr.json
│   │   │   ├── nb.json
│   │   │   ├── nl.json
│   │   │   ├── pt-BR.json
│   │   │   └── pt.json
│   │   └── util.py
│   └── test.py
├── hacs.json
├── info.md
└── readme.md
Download .txt
SYMBOL INDEX (414 symbols across 16 files)

FILE: custom_components/audiconnect/__init__.py
  function async_setup (line 73) | async def async_setup(hass, config):
  function async_update_listener (line 100) | async def async_update_listener(hass, config_entry):
  function async_setup_entry (line 105) | async def async_setup_entry(hass, config_entry):
  function async_unload_entry (line 176) | async def async_unload_entry(hass, config_entry):
  function async_remove_config_entry_device (line 191) | async def async_remove_config_entry_device(

FILE: custom_components/audiconnect/audi_account.py
  class AudiAccount (line 104) | class AudiAccount(AudiConnectObserver):
    method __init__ (line 105) | def __init__(self, hass, config_entry):
    method init_connection (line 112) | def init_connection(self):
    method is_enabled (line 175) | def is_enabled(self, attr):
    method discover_vehicles (line 180) | async def discover_vehicles(self, vehicles):
    method update (line 214) | async def update(self, now):
    method execute_vehicle_action (line 238) | async def execute_vehicle_action(self, service):
    method start_climate_control (line 269) | async def start_climate_control(self, service):
    method start_auxiliary_heating (line 296) | async def start_auxiliary_heating(self, service):
    method set_target_soc (line 315) | async def set_target_soc(self, service):
    method handle_notification (line 326) | async def handle_notification(self, vin: str, action: str) -> None:
    method refresh_vehicle_data (line 329) | async def refresh_vehicle_data(self, service):
    method _refresh_vehicle_data (line 333) | async def _refresh_vehicle_data(self, vin):

FILE: custom_components/audiconnect/audi_api.py
  class AudiAPI (line 21) | class AudiAPI:
    method __init__ (line 25) | def __init__(self, session, proxy=None):
    method use_token (line 31) | def use_token(self, token):
    method set_xclient_id (line 36) | def set_xclient_id(self, xclientid):
    method request (line 41) | async def request(
    method get (line 150) | async def get(
    method put (line 166) | async def put(self, url, data=None, headers: Dict[str, str] = None):
    method post (line 176) | async def post(
    method __get_headers (line 205) | def __get_headers(self):
  function obj_parser (line 222) | def obj_parser(obj):
  function json_loads (line 232) | def json_loads(s):

FILE: custom_components/audiconnect/audi_connect_account.py
  class AudiConnectObserver (line 29) | class AudiConnectObserver(ABC):
    method handle_notification (line 31) | async def handle_notification(self, vin: str, action: str) -> None:
  class AudiConnectAccount (line 35) | class AudiConnectAccount:
    method __init__ (line 38) | def __init__(
    method add_observer (line 68) | def add_observer(self, observer: AudiConnectObserver) -> None:
    method notify (line 71) | async def notify(self, vin: str, action: str) -> None:
    method login (line 75) | async def login(self):
    method try_login (line 89) | async def try_login(self, logError):
    method update (line 104) | async def update(self, vinlist):
    method add_or_update_vehicle (line 149) | async def add_or_update_vehicle(self, vehicle, vinlist):
    method refresh_vehicle_data (line 165) | async def refresh_vehicle_data(self, vin: str):
    method set_vehicle_lock (line 240) | async def set_vehicle_lock(self, vin: str, lock: bool):
    method set_target_state_of_charge (line 274) | async def set_target_state_of_charge(self, vin: str, target_soc: int):
    method set_vehicle_climatisation (line 306) | async def set_vehicle_climatisation(self, vin: str, activate: bool):
    method start_climate_control (line 340) | async def start_climate_control(
    method set_battery_charger (line 390) | async def set_battery_charger(self, vin: str, activate: bool, timer: b...
    method set_vehicle_window_heating (line 428) | async def set_vehicle_window_heating(self, vin: str, activate: bool):
    method set_vehicle_pre_heater (line 462) | async def set_vehicle_pre_heater(self, vin: str, activate: bool, **kwa...
  class AudiConnectVehicle (line 498) | class AudiConnectVehicle:
    method __init__ (line 499) | def __init__(self, audi_service: AudiService, vehicle) -> None:
    method vin (line 518) | def vin(self):
    method csid (line 522) | def csid(self):
    method title (line 526) | def title(self):
    method model (line 530) | def model(self):
    method model_year (line 534) | def model_year(self):
    method model_family (line 538) | def model_family(self):
    method call_update (line 541) | async def call_update(self, func, ntries: int):
    method update (line 551) | async def update(self):
    method log_exception_once (line 579) | def log_exception_once(self, exception, message):
    method update_vehicle_statusreport (line 586) | async def update_vehicle_statusreport(self):
    method update_vehicle_position (line 642) | async def update_vehicle_position(self):
    method update_vehicle_climater (line 756) | async def update_vehicle_climater(self):
    method update_vehicle_preheater (line 855) | async def update_vehicle_preheater(self):
    method update_vehicle_charger (line 899) | async def update_vehicle_charger(self):
    method update_vehicle_longterm (line 1006) | async def update_vehicle_longterm(self):
    method update_vehicle_shortterm (line 1009) | async def update_vehicle_shortterm(self):
    method update_vehicle_tripdata (line 1012) | async def update_vehicle_tripdata(self, kind: str):
    method last_update_time (line 1090) | def last_update_time(self):
    method last_update_time_supported (line 1095) | def last_update_time_supported(self):
    method service_inspection_time (line 1101) | def service_inspection_time(self):
    method service_inspection_time_supported (line 1109) | def service_inspection_time_supported(self):
    method service_inspection_distance (line 1115) | def service_inspection_distance(self):
    method service_inspection_distance_supported (line 1123) | def service_inspection_distance_supported(self):
    method service_adblue_distance (line 1129) | def service_adblue_distance(self):
    method service_adblue_distance_supported (line 1135) | def service_adblue_distance_supported(self):
    method oil_change_time (line 1141) | def oil_change_time(self):
    method oil_change_time_supported (line 1149) | def oil_change_time_supported(self):
    method oil_change_distance (line 1155) | def oil_change_distance(self):
    method oil_change_distance_supported (line 1163) | def oil_change_distance_supported(self):
    method oil_level (line 1169) | def oil_level(self):
    method oil_level_supported (line 1177) | def oil_level_supported(self):
    method oil_level_binary (line 1183) | def oil_level_binary(self):
    method oil_level_binary_supported (line 1189) | def oil_level_binary_supported(self):
    method preheater_active (line 1196) | def preheater_active(self):
    method preheater_active_supported (line 1206) | def preheater_active_supported(self):
    method preheater_duration (line 1210) | def preheater_duration(self):
    method preheater_duration_supported (line 1220) | def preheater_duration_supported(self):
    method preheater_remaining_supported (line 1224) | def preheater_remaining_supported(self):
    method preheater_remaining (line 1228) | def preheater_remaining(self):
    method parking_light (line 1238) | def parking_light(self):
    method parking_light_supported (line 1248) | def parking_light_supported(self):
    method braking_status (line 1255) | def braking_status(self):
    method braking_status_supported (line 1262) | def braking_status_supported(self):
    method mileage (line 1269) | def mileage(self):
    method mileage_supported (line 1275) | def mileage_supported(self):
    method range (line 1282) | def range(self):
    method range_supported (line 1288) | def range_supported(self):
    method tank_level (line 1295) | def tank_level(self):
    method tank_level_supported (line 1301) | def tank_level_supported(self):
    method position (line 1308) | def position(self):
    method position_supported (line 1314) | def position_supported(self):
    method any_window_open_supported (line 1321) | def any_window_open_supported(self):
    method any_window_open (line 1341) | def any_window_open(self):
    method left_front_window_open_supported (line 1360) | def left_front_window_open_supported(self):
    method left_front_window_open (line 1364) | def left_front_window_open(self):
    method right_front_window_open_supported (line 1369) | def right_front_window_open_supported(self):
    method right_front_window_open (line 1373) | def right_front_window_open(self):
    method left_rear_window_open_supported (line 1378) | def left_rear_window_open_supported(self):
    method left_rear_window_open (line 1382) | def left_rear_window_open(self):
    method right_rear_window_open_supported (line 1387) | def right_rear_window_open_supported(self):
    method right_rear_window_open (line 1391) | def right_rear_window_open(self):
    method sun_roof_supported (line 1396) | def sun_roof_supported(self):
    method sun_roof (line 1400) | def sun_roof(self):
    method roof_cover_supported (line 1405) | def roof_cover_supported(self):
    method roof_cover (line 1409) | def roof_cover(self):
    method any_door_unlocked_supported (line 1414) | def any_door_unlocked_supported(self):
    method any_door_unlocked (line 1423) | def any_door_unlocked(self):
    method any_door_open_supported (line 1437) | def any_door_open_supported(self):
    method any_door_open (line 1446) | def any_door_open(self):
    method left_front_door_open_supported (line 1460) | def left_front_door_open_supported(self):
    method left_front_door_open (line 1464) | def left_front_door_open(self):
    method right_front_door_open_supported (line 1469) | def right_front_door_open_supported(self):
    method right_front_door_open (line 1473) | def right_front_door_open(self):
    method left_rear_door_open_supported (line 1478) | def left_rear_door_open_supported(self):
    method left_rear_door_open (line 1482) | def left_rear_door_open(self):
    method right_rear_door_open_supported (line 1487) | def right_rear_door_open_supported(self):
    method right_rear_door_open (line 1491) | def right_rear_door_open(self):
    method doors_trunk_status_supported (line 1496) | def doors_trunk_status_supported(self):
    method doors_trunk_status (line 1505) | def doors_trunk_status(self):
    method trunk_unlocked (line 1520) | def trunk_unlocked(self):
    method trunk_unlocked_supported (line 1526) | def trunk_unlocked_supported(self):
    method trunk_open (line 1532) | def trunk_open(self):
    method trunk_open_supported (line 1538) | def trunk_open_supported(self):
    method hood_open (line 1544) | def hood_open(self):
    method hood_open_supported (line 1550) | def hood_open_supported(self):
    method charging_state (line 1556) | def charging_state(self):
    method charging_state_supported (line 1562) | def charging_state_supported(self):
    method charging_mode (line 1568) | def charging_mode(self):
    method charging_mode_supported (line 1574) | def charging_mode_supported(self):
    method energy_flow (line 1579) | def energy_flow(self):
    method energy_flow_supported (line 1585) | def energy_flow_supported(self):
    method charging_type (line 1591) | def charging_type(self):
    method charging_type_supported (line 1597) | def charging_type_supported(self):
    method max_charge_current (line 1603) | def max_charge_current(self):
    method max_charge_current_supported (line 1612) | def max_charge_current_supported(self):
    method actual_charge_rate (line 1618) | def actual_charge_rate(self):
    method actual_charge_rate_supported (line 1627) | def actual_charge_rate_supported(self):
    method actual_charge_rate_unit (line 1633) | def actual_charge_rate_unit(self):
    method charging_power (line 1637) | def charging_power(self):
    method charging_power_supported (line 1646) | def charging_power_supported(self):
    method primary_engine_type (line 1652) | def primary_engine_type(self):
    method primary_engine_type_supported (line 1658) | def primary_engine_type_supported(self):
    method secondary_engine_type (line 1664) | def secondary_engine_type(self):
    method secondary_engine_type_supported (line 1670) | def secondary_engine_type_supported(self):
    method primary_engine_range (line 1676) | def primary_engine_range(self):
    method primary_engine_range_supported (line 1682) | def primary_engine_range_supported(self):
    method primary_engine_range_percent (line 1688) | def primary_engine_range_percent(self):
    method primary_engine_range_percent_supported (line 1694) | def primary_engine_range_percent_supported(self):
    method secondary_engine_range (line 1700) | def secondary_engine_range(self):
    method secondary_engine_range_supported (line 1706) | def secondary_engine_range_supported(self):
    method car_type (line 1712) | def car_type(self):
    method car_type_supported (line 1718) | def car_type_supported(self):
    method secondary_engine_range_percent (line 1724) | def secondary_engine_range_percent(self):
    method secondary_engine_range_percent_supported (line 1730) | def secondary_engine_range_percent_supported(self):
    method hybrid_range (line 1736) | def hybrid_range(self):
    method hybrid_range_supported (line 1742) | def hybrid_range_supported(self):
    method state_of_charge (line 1748) | def state_of_charge(self):
    method state_of_charge_supported (line 1754) | def state_of_charge_supported(self):
    method remaining_charging_time (line 1758) | def remaining_charging_time(self):
    method remaining_charging_time_unit (line 1764) | def remaining_charging_time_unit(self):
    method remaining_charging_time_supported (line 1768) | def remaining_charging_time_supported(self):
    method charging_complete_time (line 1772) | def charging_complete_time(self):
    method target_state_of_charge (line 1797) | def target_state_of_charge(self):
    method target_state_of_charge_supported (line 1803) | def target_state_of_charge_supported(self):
    method plug_state (line 1807) | def plug_state(self):
    method plug_state_supported (line 1814) | def plug_state_supported(self):
    method plug_lock_state (line 1820) | def plug_lock_state(self):
    method plug_lock_state_supported (line 1827) | def plug_lock_state_supported(self):
    method external_power (line 1833) | def external_power(self):
    method external_power_supported (line 1845) | def external_power_supported(self):
    method plug_led_color (line 1849) | def plug_led_color(self):
    method plug_led_color_supported (line 1855) | def plug_led_color_supported(self):
    method climatisation_state (line 1861) | def climatisation_state(self):
    method climatisation_state_supported (line 1866) | def climatisation_state_supported(self):
    method outdoor_temperature (line 1872) | def outdoor_temperature(self):
    method outdoor_temperature_supported (line 1877) | def outdoor_temperature_supported(self):
    method glass_surface_heating (line 1883) | def glass_surface_heating(self):
    method glass_surface_heating_supported (line 1888) | def glass_surface_heating_supported(self):
    method park_time (line 1892) | def park_time(self):
    method park_time_supported (line 1897) | def park_time_supported(self):
    method remaining_climatisation_time (line 1901) | def remaining_climatisation_time(self):
    method remaining_climatisation_time_supported (line 1911) | def remaining_climatisation_time_supported(self):
    method preheater_state (line 1915) | def preheater_state(self):
    method preheater_state_supported (line 1921) | def preheater_state_supported(self):
    method lock_supported (line 1926) | def lock_supported(self):
    method shortterm_current (line 1932) | def shortterm_current(self):
    method shortterm_current_supported (line 1938) | def shortterm_current_supported(self):
    method shortterm_reset (line 1945) | def shortterm_reset(self):
    method shortterm_reset_supported (line 1951) | def shortterm_reset_supported(self):
    method longterm_current (line 1958) | def longterm_current(self):
    method longterm_current_supported (line 1964) | def longterm_current_supported(self):
    method longterm_reset (line 1971) | def longterm_reset(self):
    method longterm_reset_supported (line 1977) | def longterm_reset_supported(self):
    method is_moving (line 1984) | def is_moving(self):
    method is_moving_supported (line 1990) | def is_moving_supported(self):

FILE: custom_components/audiconnect/audi_entity.py
  class AudiEntity (line 10) | class AudiEntity(Entity):
    method __init__ (line 13) | def __init__(self, data, instrument):
    method async_added_to_hass (line 21) | async def async_added_to_hass(self):
    method icon (line 28) | def icon(self):
    method _entity_name (line 33) | def _entity_name(self):
    method _vehicle_name (line 37) | def _vehicle_name(self):
    method name (line 41) | def name(self):
    method should_poll (line 46) | def should_poll(self):
    method assumed_state (line 51) | def assumed_state(self):
    method extra_state_attributes (line 56) | def extra_state_attributes(self):
    method unique_id (line 71) | def unique_id(self):
    method device_info (line 76) | def device_info(self):

FILE: custom_components/audiconnect/audi_models.py
  class VehicleData (line 7) | class VehicleData:
    method __init__ (line 8) | def __init__(self, config_entry):
  class CurrentVehicleDataResponse (line 18) | class CurrentVehicleDataResponse:
    method __init__ (line 19) | def __init__(self, data):
  class VehicleDataResponse (line 25) | class VehicleDataResponse:
    method __init__ (line 47) | def __init__(self, data):
    method _tryAppendStateWithTs (line 284) | def _tryAppendStateWithTs(self, json, name, tsoff, loc):
    method _tryAppendFieldWithTs (line 324) | def _tryAppendFieldWithTs(self, json, textId, loc):
    method _getFromJson (line 370) | def _getFromJson(self, json, loc):
    method appendDoorState (line 378) | def appendDoorState(self, data):
    method appendWindowState (line 431) | def appendWindowState(self, data):
  class TripDataResponse (line 464) | class TripDataResponse:
    method __init__ (line 465) | def __init__(self, data):
  class Field (line 509) | class Field:
    method __init__ (line 561) | def __init__(self, data):
    method __str__ (line 581) | def __str__(self):
  class Vehicle (line 588) | class Vehicle:
    method __init__ (line 589) | def __init__(self):
    method parse (line 597) | def parse(self, data):
    method __str__ (line 618) | def __str__(self):
  class VehiclesResponse (line 622) | class VehiclesResponse:
    method __init__ (line 623) | def __init__(self):
    method parse (line 627) | def parse(self, data):

FILE: custom_components/audiconnect/audi_services.py
  class BrowserLoginResponse (line 44) | class BrowserLoginResponse:
    method __init__ (line 45) | def __init__(self, response: requests.Response, url: str):
    method get_location (line 49) | def get_location(self) -> str:
    method to_absolute (line 60) | def to_absolute(cls, absolute_url, relative_url) -> str:
  class AudiService (line 71) | class AudiService:
    method __init__ (line 72) | def __init__(self, api: AudiAPI, country: str, spin: str, api_level: i...
    method get_hidden_html_input_form_data (line 95) | def get_hidden_html_input_form_data(self, response, form_data: Dict[st...
    method get_post_url (line 105) | def get_post_url(self, response, url):
    method login (line 122) | async def login(self, user: str, password: str, persist_token: bool = ...
    method refresh_vehicle_data (line 126) | async def refresh_vehicle_data(self, vin: str):
    method request_current_vehicle_data (line 146) | async def request_current_vehicle_data(self, vin: str):
    method get_preheater (line 158) | async def get_preheater(self, vin: str):
    method get_stored_vehicle_data (line 169) | async def get_stored_vehicle_data(self, vin: str):
    method get_charger (line 206) | async def get_charger(self, vin: str):
    method get_climater (line 217) | async def get_climater(self, vin: str):
    method get_stored_position (line 228) | async def get_stored_position(self, vin: str):
    method get_operations_list (line 234) | async def get_operations_list(self, vin: str):
    method get_timer (line 241) | async def get_timer(self, vin: str):
    method get_vehicles (line 252) | async def get_vehicles(self):
    method get_vehicle_information (line 260) | async def get_vehicle_information(self):
    method get_vehicle_data (line 303) | async def get_vehicle_data(self, vin: str):
    method get_tripdata (line 314) | async def get_tripdata(self, vin: str, kind: str):
    method _fill_home_region (line 368) | async def _fill_home_region(self, vin: str):
    method _get_home_region (line 401) | async def _get_home_region(self, vin: str):
    method _get_home_region_setter (line 409) | async def _get_home_region_setter(self, vin: str):
    method _get_security_token (line 417) | async def _get_security_token(self, vin: str, action: str):
    method _get_vehicle_action_header (line 473) | def _get_vehicle_action_header(
    method __build_url (line 499) | def __build_url(
    method __get_cariad_url (line 506) | def __get_cariad_url(self, path_and_query: str, **path_and_query_kwarg...
    method __get_cariad_url_for_vin (line 513) | def __get_cariad_url_for_vin(
    method set_vehicle_lock (line 520) | async def set_vehicle_lock(self, vin: str, lock: bool):
    method set_battery_charger (line 558) | async def set_battery_charger(self, vin: str, start: bool, timer: bool):
    method set_target_state_of_charge (line 594) | async def set_target_state_of_charge(self, vin: str, target_soc: int):
    method set_climatisation (line 613) | async def set_climatisation(self, vin: str, start: bool):
    method start_climate_control (line 699) | async def start_climate_control(
    method set_window_heating (line 848) | async def set_window_heating(self, vin: str, start: bool):
    method set_pre_heater (line 884) | async def set_pre_heater(
    method check_bff_request_succeeded (line 918) | async def check_bff_request_succeeded(self, vin: str, request_id: str):
    method check_request_succeeded (line 956) | async def check_request_succeeded(
    method _calculate_X_QMAuth (line 980) | def _calculate_X_QMAuth(self):
    method refresh_token_if_necessary (line 1032) | async def refresh_token_if_necessary(self, elapsed_sec: int) -> bool:
    method login_request (line 1139) | async def login_request(self, user: str, password: str):
    method _generate_security_pin_hash (line 1460) | def _generate_security_pin_hash(self, challenge):
    method _emulate_browser (line 1469) | async def _emulate_browser(

FILE: custom_components/audiconnect/binary_sensor.py
  function async_setup_platform (line 14) | async def async_setup_platform(hass, config, async_add_entities, discove...
  function async_setup_entry (line 18) | async def async_setup_entry(hass, config_entry, async_add_entities):
  class AudiSensor (line 30) | class AudiSensor(AudiEntity, BinarySensorEntity):
    method is_on (line 34) | def is_on(self):
    method device_class (line 39) | def device_class(self):
    method entity_category (line 44) | def entity_category(self):

FILE: custom_components/audiconnect/config_flow.py
  function configured_accounts (line 34) | def configured_accounts(hass):
  class AudiConfigFlow (line 43) | class AudiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
    method __init__ (line 44) | def __init__(self):
    method async_step_user (line 53) | async def async_step_user(self, user_input=None):
    method async_step_import (line 120) | async def async_step_import(self, user_input):
    method async_get_options_flow (line 174) | def async_get_options_flow(config_entry):
  class OptionsFlowHandler (line 179) | class OptionsFlowHandler(config_entries.OptionsFlow):
    method __init__ (line 180) | def __init__(self, config_entry):
    method async_step_init (line 186) | async def async_step_init(self, user_input=None):

FILE: custom_components/audiconnect/dashboard.py
  class Instrument (line 25) | class Instrument:
    method __init__ (line 26) | def __init__(
    method __repr__ (line 37) | def __repr__(self):
    method camel2slug (line 40) | def camel2slug(self, s):
    method slug_attr (line 48) | def slug_attr(self):
    method setup (line 51) | def setup(self, connection, vehicle, mutable=True, **config):
    method component (line 70) | def component(self):
    method icon (line 74) | def icon(self):
    method name (line 78) | def name(self):
    method attr (line 82) | def attr(self):
    method suggested_display_precision (line 86) | def suggested_display_precision(self):
    method vehicle_name (line 90) | def vehicle_name(self):
    method full_name (line 94) | def full_name(self):
    method vehicle_model (line 98) | def vehicle_model(self):
    method vehicle_model_year (line 102) | def vehicle_model_year(self):
    method vehicle_model_family (line 106) | def vehicle_model_family(self):
    method vehicle_vin (line 110) | def vehicle_vin(self):
    method vehicle_csid (line 114) | def vehicle_csid(self):
    method is_mutable (line 118) | def is_mutable(self):
    method is_supported (line 122) | def is_supported(self):
    method str_state (line 131) | def str_state(self):
    method state (line 135) | def state(self):
    method attributes (line 141) | def attributes(self):
  class Sensor (line 145) | class Sensor(Instrument):
    method __init__ (line 146) | def __init__(
    method is_mutable (line 173) | def is_mutable(self):
    method str_state (line 177) | def str_state(self):
    method state (line 184) | def state(self):
    method unit (line 188) | def unit(self):
  class BinarySensor (line 196) | class BinarySensor(Instrument):
    method __init__ (line 197) | def __init__(self, attr, name, device_class=None, icon=None, entity_ca...
    method is_mutable (line 203) | def is_mutable(self):
    method str_state (line 207) | def str_state(self):
    method state (line 222) | def state(self):
    method is_on (line 233) | def is_on(self):
  class Lock (line 237) | class Lock(Instrument):
    method __init__ (line 238) | def __init__(self):
    method is_mutable (line 242) | def is_mutable(self):
    method str_state (line 246) | def str_state(self):
    method state (line 250) | def state(self):
    method is_locked (line 254) | def is_locked(self):
    method lock (line 257) | async def lock(self):
    method unlock (line 260) | async def unlock(self):
  class Switch (line 264) | class Switch(Instrument):
    method __init__ (line 265) | def __init__(self, attr, name, icon):
    method is_mutable (line 269) | def is_mutable(self):
    method str_state (line 273) | def str_state(self):
    method is_on (line 276) | def is_on(self):
    method turn_on (line 279) | def turn_on(self):
    method turn_off (line 282) | def turn_off(self):
  class Preheater (line 286) | class Preheater(Instrument):
    method __init__ (line 287) | def __init__(self):
    method is_mutable (line 296) | def is_mutable(self):
    method str_state (line 300) | def str_state(self):
    method is_on (line 303) | def is_on(self):
    method turn_on (line 306) | async def turn_on(self):
    method turn_off (line 309) | async def turn_off(self):
  class Position (line 313) | class Position(Instrument):
    method __init__ (line 314) | def __init__(self):
    method is_mutable (line 318) | def is_mutable(self):
    method state (line 322) | def state(self):
    method str_state (line 332) | def str_state(self):
  class TripData (line 344) | class TripData(Instrument):
    method __init__ (line 345) | def __init__(self, attr, name):
    method is_mutable (line 353) | def is_mutable(self):
    method str_state (line 357) | def str_state(self):
    method state (line 379) | def state(self):
    method extra_state_attributes (line 384) | def extra_state_attributes(self):
  class LastUpdate (line 402) | class LastUpdate(Instrument):
    method __init__ (line 403) | def __init__(self):
    method is_mutable (line 417) | def is_mutable(self):
    method str_state (line 421) | def str_state(self):
    method state (line 426) | def state(self):
  function create_instruments (line 430) | def create_instruments():
  class Dashboard (line 817) | class Dashboard:
    method __init__ (line 818) | def __init__(self, connection, vehicle, **config):

FILE: custom_components/audiconnect/device_tracker.py
  function async_setup_entry (line 22) | async def async_setup_entry(
  class AudiDeviceTracker (line 55) | class AudiDeviceTracker(TrackerEntity):
    method __init__ (line 62) | def __init__(self, config_entry: ConfigEntry, instrument: Any) -> None:
    method _update_state_from_instrument (line 78) | def _update_state_from_instrument(self) -> None:
    method latitude (line 92) | def latitude(self) -> float | None:
    method longitude (line 96) | def longitude(self) -> float | None:
    method name (line 100) | def name(self) -> str:
    method extra_state_attributes (line 104) | def extra_state_attributes(self) -> dict[str, Any]:
    method async_added_to_hass (line 117) | async def async_added_to_hass(self) -> None:
    method _async_receive_data (line 128) | def _async_receive_data(self, instrument: Any) -> None:

FILE: custom_components/audiconnect/lock.py
  function async_setup_platform (line 14) | async def async_setup_platform(hass, config, async_add_entities, discove...
  function async_setup_entry (line 18) | async def async_setup_entry(hass, config_entry, async_add_entities):
  class AudiLock (line 30) | class AudiLock(AudiEntity, LockEntity):
    method is_locked (line 34) | def is_locked(self):
    method async_lock (line 38) | async def async_lock(self, **kwargs):
    method async_unlock (line 42) | async def async_unlock(self, **kwargs):

FILE: custom_components/audiconnect/sensor.py
  function async_setup_platform (line 14) | async def async_setup_platform(hass, config, async_add_entities, discove...
  function async_setup_entry (line 18) | async def async_setup_entry(hass, config_entry, async_add_entities):
  class AudiSensor (line 32) | class AudiSensor(AudiEntity, SensorEntity):
    method native_value (line 36) | def native_value(self):
    method native_unit_of_measurement (line 41) | def native_unit_of_measurement(self):
    method device_class (line 46) | def device_class(self):
    method state_class (line 51) | def state_class(self):
    method entity_category (line 56) | def entity_category(self):
    method extra_state_attributes (line 61) | def extra_state_attributes(self):
    method suggested_display_precision (line 66) | def suggested_display_precision(self):

FILE: custom_components/audiconnect/switch.py
  function async_setup_platform (line 14) | async def async_setup_platform(hass, config, async_add_entities, discove...
  function async_setup_entry (line 18) | async def async_setup_entry(hass, config_entry, async_add_entities):
  class AudiSwitch (line 30) | class AudiSwitch(AudiEntity, ToggleEntity):
    method is_on (line 34) | def is_on(self):
    method async_turn_on (line 38) | async def async_turn_on(self, **kwargs):
    method async_turn_off (line 42) | async def async_turn_off(self, **kwargs):

FILE: custom_components/audiconnect/util.py
  function get_attr (line 8) | def get_attr(dictionary, keys, default=None):
  function to_byte_array (line 16) | def to_byte_array(hexString: str):
  function log_exception (line 24) | def log_exception(exception, message):
  function parse_int (line 29) | def parse_int(val: str):
  function parse_float (line 36) | def parse_float(val: str):
  function parse_datetime (line 43) | def parse_datetime(time_value):

FILE: custom_components/test.py
  function printHelp (line 11) | def printHelp():
  function main (line 17) | async def main(argv):
Condensed preview — 42 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (322K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 719,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: \"\"\nlabels: \"\"\nassignees: \"\"\n---\n\n**Describe the bu"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 819,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: \"\"\nlabels: \"\"\nassignees: \"\"\n---\n\n**Is your feat"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 192,
    "preview": "version: 2\nupdates:\n  # Enable version updates for Python\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n   "
  },
  {
    "path": ".github/workflows/inactiveIssues.yml",
    "chars": 767,
    "preview": "name: Close inactive issues\non:\n  schedule:\n    - cron: \"30 1 * * *\"\n\njobs:\n  close-issues:\n    runs-on: ubuntu-latest\n "
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 1698,
    "preview": "name: Release\non:\n  workflow_dispatch:\n  schedule:\n    - cron: \"0 8 * * Wed,Sun\"\njobs:\n  build:\n    runs-on: ubuntu-late"
  },
  {
    "path": ".github/workflows/semanticTitle.yaml",
    "chars": 406,
    "preview": "name: \"Semantic Title\"\n\non:\n  pull_request_target:\n    types:\n      - opened\n      - edited\n      - synchronize\n\njobs:\n "
  },
  {
    "path": ".github/workflows/validate.yml",
    "chars": 485,
    "preview": "name: Validate\n\non:\n  push:\n  pull_request:\n  schedule:\n    - cron: \"0 0 * * *\"\n  workflow_dispatch:\n\njobs:\n  validate-h"
  },
  {
    "path": ".gitignore",
    "chars": 1209,
    "preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 1827,
    "preview": "---\nci:\n  autoupdate_commit_msg: \"chore: pre-commit autoupdate\"\nrepos:\n  - repo: https://github.com/astral-sh/ruff-pre-c"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1334,
    "preview": "# Contribution guidelines\n\nContributing to this project should be as easy and transparent as possible, whether it's:\n\n- "
  },
  {
    "path": "LICENSE",
    "chars": 1082,
    "preview": "MIT License\n\nCopyright (c) 2019 Arjen van Rhijn @arjenvrh\n\nPermission is hereby granted, free of charge, to any person o"
  },
  {
    "path": "custom_components/audiconnect/__init__.py",
    "chars": 6165,
    "preview": "\"\"\"Support for Audi Connect.\"\"\"\n\nfrom datetime import timedelta\nimport voluptuous as vol\nimport logging\n\nimport homeassi"
  },
  {
    "path": "custom_components/audiconnect/audi_account.py",
    "chars": 12993,
    "preview": "import asyncio\nimport logging\n\nimport voluptuous as vol\n\nfrom homeassistant.const import CONF_PASSWORD, CONF_USERNAME, P"
  },
  {
    "path": "custom_components/audiconnect/audi_api.py",
    "chars": 8498,
    "preview": "import json\nimport logging\nfrom datetime import datetime\nimport asyncio\nfrom asyncio import TimeoutError, CancelledError"
  },
  {
    "path": "custom_components/audiconnect/audi_connect_account.py",
    "chars": 72854,
    "preview": "import time\r\nfrom datetime import datetime, timezone, timedelta\r\nimport logging\r\nimport asyncio\r\nfrom typing import List"
  },
  {
    "path": "custom_components/audiconnect/audi_entity.py",
    "chars": 2772,
    "preview": "from homeassistant.helpers.entity import Entity\nfrom homeassistant.helpers.dispatcher import (\n    async_dispatcher_conn"
  },
  {
    "path": "custom_components/audiconnect/audi_models.py",
    "chars": 21650,
    "preview": "import logging\nfrom .util import get_attr\n\n_LOGGER = logging.getLogger(__name__)\n\n\nclass VehicleData:\n    def __init__(s"
  },
  {
    "path": "custom_components/audiconnect/audi_services.py",
    "chars": 56554,
    "preview": "import json\nimport uuid\nimport base64\nimport os\nimport re\nimport logging\nfrom datetime import timedelta, datetime\nfrom t"
  },
  {
    "path": "custom_components/audiconnect/binary_sensor.py",
    "chars": 1282,
    "preview": "\"\"\"Support for Audi Connect sensors.\"\"\"\n\nimport logging\n\nfrom homeassistant.components.binary_sensor import BinarySensor"
  },
  {
    "path": "custom_components/audiconnect/config_flow.py",
    "chars": 8813,
    "preview": "from collections import OrderedDict\nimport logging\nimport voluptuous as vol\n\nfrom homeassistant import config_entries\nfr"
  },
  {
    "path": "custom_components/audiconnect/const.py",
    "chars": 2318,
    "preview": "DOMAIN = \"audiconnect\"\n\nCONF_VIN = \"vin\"\nCONF_CARNAME = \"carname\"\nCONF_ACTION = \"action\"\nCONF_CLIMATE_TEMP_F = \"temp_f\"\n"
  },
  {
    "path": "custom_components/audiconnect/dashboard.py",
    "chars": 24748,
    "preview": "#  Utilities for integration with Home Assistant (directly or via MQTT)\r\n\r\nimport logging\r\nimport re\r\n\r\nfrom homeassista"
  },
  {
    "path": "custom_components/audiconnect/device_tracker.py",
    "chars": 4969,
    "preview": "\"\"\"Support for tracking an Audi.\"\"\"\n\nimport logging\nfrom typing import Any\n\nfrom homeassistant.components.device_tracker"
  },
  {
    "path": "custom_components/audiconnect/lock.py",
    "chars": 1158,
    "preview": "\"\"\"Support for Audi Connect locks.\"\"\"\n\nimport logging\n\nfrom homeassistant.components.lock import LockEntity\nfrom homeass"
  },
  {
    "path": "custom_components/audiconnect/manifest.json",
    "chars": 410,
    "preview": "{\n  \"domain\": \"audiconnect\",\n  \"name\": \"Audi Connect\",\n  \"codeowners\": [\"@audiconnect\"],\n  \"config_flow\": true,\n  \"docum"
  },
  {
    "path": "custom_components/audiconnect/sensor.py",
    "chars": 1920,
    "preview": "\"\"\"Support for Audi Connect sensors.\"\"\"\n\nimport logging\n\nfrom homeassistant.components.sensor import SensorEntity\nfrom h"
  },
  {
    "path": "custom_components/audiconnect/services.yaml",
    "chars": 2090,
    "preview": "---\n# Describes the format for available services for audiconnect\n\nrefresh_vehicle_data:\n  fields:\n    vin:\n      requir"
  },
  {
    "path": "custom_components/audiconnect/strings.json",
    "chars": 7280,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"invalid_credentials\": \"Invalid credentials\",\n      \"user_already_configured\": \"Acc"
  },
  {
    "path": "custom_components/audiconnect/switch.py",
    "chars": 1195,
    "preview": "\"\"\"Support for Audi Connect switches\"\"\"\n\nimport logging\n\nfrom homeassistant.helpers.entity import ToggleEntity\nfrom home"
  },
  {
    "path": "custom_components/audiconnect/translations/de.json",
    "chars": 6493,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"invalid_credentials\": \"Ung\\u00fcltige Anmeldeinformationen\",\n      \"user_already_c"
  },
  {
    "path": "custom_components/audiconnect/translations/en.json",
    "chars": 7280,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"invalid_credentials\": \"Invalid credentials\",\n      \"user_already_configured\": \"Acc"
  },
  {
    "path": "custom_components/audiconnect/translations/fi.json",
    "chars": 6555,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"invalid_credentials\": \"Virheelliset tunnistetiedot\",\n      \"user_already_configure"
  },
  {
    "path": "custom_components/audiconnect/translations/fr.json",
    "chars": 5531,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"invalid_credentials\": \"Informations d'identification invalides\",\n      \"user_alrea"
  },
  {
    "path": "custom_components/audiconnect/translations/nb.json",
    "chars": 1100,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"invalid_credentials\": \"Ugyldige innloggingsopplysninger\",\n      \"user_already_conf"
  },
  {
    "path": "custom_components/audiconnect/translations/nl.json",
    "chars": 1111,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"invalid_credentials\": \"Ongeldige gebruikersgegevens\",\n      \"user_already_configur"
  },
  {
    "path": "custom_components/audiconnect/translations/pt-BR.json",
    "chars": 1116,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"invalid_credentials\": \"Credenciais inválidas\",\n      \"user_already_configured\": \"A"
  },
  {
    "path": "custom_components/audiconnect/translations/pt.json",
    "chars": 1114,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"invalid_credentials\": \"Credenciais inválidas\",\n      \"user_already_configured\": \"A"
  },
  {
    "path": "custom_components/audiconnect/util.py",
    "chars": 1530,
    "preview": "from functools import reduce\nfrom datetime import datetime, timezone\nimport logging\n\n_LOGGER = logging.getLogger(__name_"
  },
  {
    "path": "custom_components/test.py",
    "chars": 1525,
    "preview": "import sys\nimport asyncio\nimport getopt\n\nfrom audiconnect.audi_connect_account import AudiConnectAccount\nfrom audiconnec"
  },
  {
    "path": "hacs.json",
    "chars": 59,
    "preview": "{\n  \"name\": \"Audi connect\",\n  \"homeassistant\": \"0.110.0\"\n}\n"
  },
  {
    "path": "info.md",
    "chars": 2162,
    "preview": "[![hacs][hacsbadge]](hacs)\n![Project Maintenance][maintenance-shield]\n\n## Configuration\n\nConfiguration is done through t"
  },
  {
    "path": "readme.md",
    "chars": 16223,
    "preview": "# Audi Connect Integration for Home Assistant\n\n[![GitHub Activity][commits-shield]][commits]\n[![License][license-shield]"
  }
]

About this extraction

This page contains the full source code of the arjenvrh/audi_connect_ha GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 42 files (293.0 KB), approximately 66.1k tokens, and a symbol index with 414 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!