Full Code of ReneNulschDE/mbapi2020 for AI

master 6b8f1844e7c1 cached
94 files
1.1 MB
319.7k tokens
330 symbols
1 requests
Download .txt
Showing preview only (1,235K chars total). Download the full file or copy to clipboard to get everything.
Repository: ReneNulschDE/mbapi2020
Branch: master
Commit: 6b8f1844e7c1
Files: 94
Total size: 1.1 MB

Directory structure:
gitextract_fszsclw1/

├── .devcontainer.json
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   └── config.yml
│   ├── dependabot.yaml
│   └── workflows/
│       ├── HACS_validate.yaml
│       ├── hassfest.yaml
│       └── publish.yaml
├── .gitignore
├── .pre-commit-config.yaml
├── .yamllint.yaml
├── CLAUDE.md
├── LICENSE
├── README.md
├── SECURITY.md
├── custom_components/
│   └── mbapi2020/
│       ├── __init__.py
│       ├── binary_sensor.py
│       ├── button.py
│       ├── car.py
│       ├── client.py
│       ├── config_flow.py
│       ├── const.py
│       ├── coordinator.py
│       ├── device_tracker.py
│       ├── diagnostics.py
│       ├── errors.py
│       ├── helper.py
│       ├── icons.json
│       ├── lock.py
│       ├── manifest.json
│       ├── oauth.py
│       ├── proto/
│       │   ├── acp_pb2.py
│       │   ├── client_pb2.py
│       │   ├── cluster_pb2.py
│       │   ├── eventpush_pb2.py
│       │   ├── gogo_pb2.py
│       │   ├── protos_pb2.py
│       │   ├── service_activation_pb2.py
│       │   ├── user_events_pb2.py
│       │   ├── vehicle_commands_pb2.py
│       │   ├── vehicle_events_pb2.py
│       │   ├── vehicleapi_pb2.py
│       │   └── vin_events_pb2.py
│       ├── repairs.py
│       ├── sensor.py
│       ├── services.py
│       ├── services.yaml
│       ├── switch.py
│       ├── system_health.py
│       ├── translations/
│       │   ├── cs.json
│       │   ├── da.json
│       │   ├── de.json
│       │   ├── en.json
│       │   ├── es.json
│       │   ├── fi.json
│       │   ├── fr.json
│       │   ├── he.json
│       │   ├── it.json
│       │   ├── nb_NO.json
│       │   ├── nl.json
│       │   ├── pl.json
│       │   ├── pt.json
│       │   ├── sv.json
│       │   └── ta.json
│       ├── webapi.py
│       └── websocket.py
├── hacs.json
├── mypi.ini
├── pyproject.toml
├── requirements.txt
├── scripts/
│   ├── burp-redirector.py
│   ├── https-bff.py
│   ├── https-ws-case-429.py
│   └── setup
└── token-requester/
    ├── macOS/
    │   ├── .gitignore
    │   └── MBAPI2020 Token Helper/
    │       ├── MBAPI2020 Token Helper/
    │       │   ├── AppDelegate.swift
    │       │   ├── Assets.xcassets/
    │       │   │   ├── AccentColor.colorset/
    │       │   │   │   └── Contents.json
    │       │   │   ├── AppIcon.appiconset/
    │       │   │   │   └── Contents.json
    │       │   │   └── Contents.json
    │       │   ├── Base.lproj/
    │       │   │   └── Main.storyboard
    │       │   ├── MBAPI2020_Token_Helper.entitlements
    │       │   └── ViewController.swift
    │       ├── MBAPI2020 Token Helper.xcodeproj/
    │       │   ├── project.pbxproj
    │       │   └── project.xcworkspace/
    │       │       └── contents.xcworkspacedata
    │       └── MBAPI2020-Token-Helper-Info.plist
    └── net-core/
        └── mb-token-requester/
            ├── CallbackManager.cs
            ├── DesktopEntryHandler.cs
            ├── Program.cs
            ├── RegistryConfig.cs
            ├── appsettings.json
            ├── callback.bat
            ├── mb-shortcut-handler.desktop
            ├── mb-token-requester.csproj
            └── mb-token-requester.sln

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

================================================
FILE: .devcontainer.json
================================================
{
  "name": "renenulschde/dev-mbapi2020",
  "image": "mcr.microsoft.com/devcontainers/python:1-3.12",
  "postCreateCommand": "scripts/setup",
  "appPort": [
    "9123:8123"
  ],
  "portsAttributes": {
    "8123": {
      "label": "Home Assistant internal",
      "onAutoForward": "notify"
    },
    "9123": {
      "label": "Home Assistant remote",
      "onAutoForward": "notify"
    }
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "ms-python.python",
        "github.vscode-pull-request-github",
        "ryanluker.vscode-coverage-gutters",
        "ms-python.vscode-pylance",
        "ms-python.pylint",
        "charliermarsh.ruff"
      ],
      "settings": {
        "files.eol": "\n",
        "editor.tabSize": 4,
        "python.pythonPath": "/usr/bin/python3",
        "python.analysis.autoSearchPaths": false,
        "[python]": {
          "editor.defaultFormatter": "charliermarsh.ruff",
          "editor.formatOnSave": true
        },
        "editor.formatOnPaste": false,
        "editor.formatOnSave": true,
        "editor.formatOnType": true,
        "files.trimTrailingWhitespace": true,
        "[markdown]": {
          "files.trimTrailingWhitespace": false
        },
        "terminal.integrated.defaultProfile.linux": "zsh"
      }
    }
  },
  "remoteUser": "vscode",
  "features": {
    "ghcr.io/devcontainers/features/rust:1": {},
    "ghcr.io/devcontainers-extra/features/ffmpeg-apt-get:1": {}
  }
}

================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: ReneNulschDE
buy_me_a_coffee: renenulsch


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Report an issue with MBAPI2020
description: Report an issue with Mercedes ME integration.
body:
  - type: markdown
    attributes:
      value: |
        This issue form is for reporting bugs only!

        If you have a feature or enhancement request, please use the  [Forum][fr].

        [fr]: https://community.home-assistant.io/t/mercedes-me-component/41911
  - type: textarea
    validations:
      required: true
    attributes:
      label: The problem
      description: >-
        Describe the issue you are experiencing here, to communicate to the
        maintainers. Tell us what you were trying to do and what happened.

        Provide a clear and concise description of what the problem is.
  - type: markdown
    attributes:
      value: |
        ## Environment
  - type: input
    id: version
    validations:
      required: true
    attributes:
      label: What version of MBAPI2020 do use?
      placeholder: v0.xx.x
      description: >
        Can be found in: [Settings ⇒ System ⇒ Repairs ⇒ Three Dots in Upper Right ⇒ System information](https://my.home-assistant.io/redirect/system_health/).

        [![Open your Home Assistant instance and show the system information.](https://my.home-assistant.io/badges/system_health.svg)](https://my.home-assistant.io/redirect/system_health/)
  - type: input
    attributes:
      label: What was the last working version of MBAPI2020?
      placeholder: v0.xx.x
      description: >
        If known, otherwise leave blank.
  - type: dropdown
    validations:
      required: true
    attributes:
      label: What type of installation are you running?
      description: >
        Can be found in: [Settings ⇒ System ⇒ Repairs ⇒ Three Dots in Upper Right ⇒ System information](https://my.home-assistant.io/redirect/system_health/).

        [![Open your Home Assistant instance and show the system information.](https://my.home-assistant.io/badges/system_health.svg)](https://my.home-assistant.io/redirect/system_health/)
      options:
        - Home Assistant OS
        - Home Assistant Container
        - Home Assistant Supervised
        - Home Assistant Core
  - type: markdown
    attributes:
      value: |
        # Details
  - type: textarea
    attributes:
      label: Diagnostics information
      placeholder: "drag-and-drop the diagnostics data file here (do not copy-and-paste the content)"
      description: >-
        The MBAPI2020 integration provides the ability to [download diagnostic data](https://www.home-assistant.io/docs/configuration/troubleshooting/#download-diagnostics).

        **It would really help if you could download the diagnostics data for the account/hub you are having issues with,
        and <ins>drag-and-drop that file into the textbox below.</ins>**

        It generally allows pinpointing defects and thus resolving issues faster.
  - type: textarea
    attributes:
      label: Example YAML snippet
      description: |
        If applicable, please provide an example piece of YAML that can help reproduce this problem.
        This can be from an automation, script, scene or configuration.
      render: yaml
  - type: textarea
    attributes:
      label: Anything in the logs that might be useful for us?
      description: For example, error message, or stack traces.
      render: txt
  - type: textarea
    attributes:
      label: Additional information
      description: >
        If you have any additional information for us, use the field below.


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
  - name: Feature Request or other questions
    url: https://community.home-assistant.io/t/mercedes-me-component/41911
    about: Please use our Community Forum for making feature requests or asking general questions.
  - name: I'm unsure where to go
    url: https://community.home-assistant.io/t/mercedes-me-component/41911
    about: If you are unsure where to go, then joining and searching in the Forum is a good start.


================================================
FILE: .github/dependabot.yaml
================================================
version: 2
updates:
- package-ecosystem: pip
  directory: "/"
  schedule:
    interval: daily
    time: "04:00"
  reviewers:
  - ReneNulschDE
  assignees:
  - ReneNulschDE
  labels:
  - dependencies

================================================
FILE: .github/workflows/HACS_validate.yaml
================================================
name: Validate with HACS

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

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


================================================
FILE: .github/workflows/hassfest.yaml
================================================
name: Validate with hassfest

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

jobs:
  validate:
    runs-on: "ubuntu-latest"
    steps:
      - uses: "actions/checkout@v4"
      - uses: home-assistant/actions/hassfest@master


================================================
FILE: .github/workflows/publish.yaml
================================================
name: Publish Workflow

on:
  release:
    types:
      - published

jobs:
  release:
    name: Release
    runs-on: ubuntu-latest
    steps:
      - name: Checkout the repository
        uses: actions/checkout@v4
      - name: Get integration information
        id: information
        run: |
          name=$(find custom_components/ -type d -maxdepth 1 | tail -n 1 | cut -d "/" -f2)
          echo "name=$name" >> $GITHUB_OUTPUT
      - name: Adjust version number
        if: ${{ github.event_name == 'release' }}
        shell: bash
        env:
          TAG_NAME: ${{ github.event.release.tag_name }}
        run: |
          yq -i -o json ".version=\"$TAG_NAME\"" \
            "${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/manifest.json"
      - name: Create zip file for the integration
        shell: bash
        run: |
          cd "${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}"
          zip ${{ steps.information.outputs.name }}.zip -r ./
      - name: Upload the zipfile as a release asset
        uses: softprops/action-gh-release@v2
        if: ${{ github.event_name == 'release' }}
        with:
          files: ${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/${{ steps.information.outputs.name }}.zip
          tag_name: ${{ github.event.release.tag_name }}


================================================
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/


sec_*.txt
.vscode/
/custom_components/mbapi2020/messages/*

/custom_components/mbapi2020/resources*
/local

bin/
obj/
.DS_Store
*.csproj.user
[Tt]humbs.db
.ruff_cache
.claude/settings.local.json

================================================
FILE: .pre-commit-config.yaml
================================================
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.14.5
    hooks:
#      - id: ruff
#        args:
#          - --fix
      - id: ruff-format
        files: ^((custom_components/mbapi2020|pylint|script|tests|simulator)/.+)?[^/]+\.py$
  - repo: https://github.com/adrienverge/yamllint.git
    rev: v1.37.1
    hooks:
      - id: yamllint
  - 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:
          - --py313-plus
          - --force
          - --keep-updates
        files: ^(custom_components/ha-mysmartbike|tests|script|simulator)/.+\.py$
  - repo: local
    hooks:
      - id: const-check-proxy-not-disabled
        name: const-check-proxy-not-disabled
        entry: "USE_PROXY = True"
        language: pygrep
        types: [python]
      - id: const-check-ssl-check-not-correct
        name: const-check-ssl-check-not-correct
        entry: "VERIFY_SSL = False"
        language: pygrep
        types: [python]


================================================
FILE: .yamllint.yaml
================================================
extends: default

rules:
  # 120 chars should be enough, but don't fail if a line is longer
  line-length:
    max: 120
    level: warning


================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Home Assistant custom component integration for Mercedes-Benz vehicles. Connects to Mercedes-Benz API via OAuth2 and WebSocket to monitor and control vehicle features (charging, locks, preconditioning, etc.).

**Python:** 3.13 | **Home Assistant:** >= 2024.02.0 | **Domain:** `mbapi2020`

## Development Commands

```bash
# Setup development environment (installs dependencies + pre-commit hooks)
scripts/setup

# Run ruff formatter
ruff format custom_components/mbapi2020

# Run ruff linter
ruff check custom_components/mbapi2020

# Run pylint on integration
pylint custom_components/mbapi2020

# Validate Home Assistant manifest
# (done via GitHub Actions: hassfest.yaml, HACS_validate.yaml)
```

## Code Style

- **Line length:** 120 characters
- **Formatter:** Ruff (v0.6.8+)
- **Linting:** Ruff and PyLint
- **Type checking:** MyPy (Python 3.13)
- **Import alias conventions:** `voluptuous` as `vol`, `homeassistant.helpers.config_validation` as `cv`
- Pre-commit hooks enforce formatting and check that `USE_PROXY = True` and `VERIFY_SSL = False` are not committed

## Architecture

### Core Files

| File | Purpose |
|------|---------|
| `custom_components/mbapi2020/__init__.py` | Integration setup, async_setup_entry |
| `client.py` | Main API client - OAuth2, WebSocket, command handling |
| `car.py` | Vehicle data model with nested components (Tires, Doors, Windows, Electric, Auxheat, Precond) |
| `coordinator.py` | Home Assistant DataUpdateCoordinator |
| `oauth.py` | OAuth2 authentication with token caching |
| `websocket.py` | Real-time updates via WebSocket |
| `webapi.py` | REST API wrapper for general queries |
| `const.py` | All constants, enums, and sensor definitions |

### Entity Types

Each entity type has its own file: `sensor.py`, `binary_sensor.py`, `lock.py`, `switch.py`, `button.py`, `device_tracker.py`

All entities extend `MercedesMeEntity` base class and use the coordinator pattern.

### Protocol Buffers

The `proto/` directory contains auto-generated Python files from `.proto` definitions. Do not edit these files directly.

### Data Flow

1. `oauth.py` handles authentication and token refresh
2. `client.py` establishes WebSocket connection for real-time updates
3. `coordinator.py` manages data updates and distributes to entities
4. Vehicle state stored in `Car` objects with nested component classes

## Home Assistant Patterns

### Async Programming
- All external I/O operations must be async
- Use `asyncio.gather()` instead of awaiting in loops
- Use `hass.async_add_executor_job()` for blocking operations
- Use `asyncio.sleep()` instead of `time.sleep()`
- Use `@callback` decorator for event loop safe functions

### Error Handling
- `ConfigEntryNotReady`: Temporary setup issues (device offline, timeout)
- `ConfigEntryAuthFailed`: Authentication problems
- `ConfigEntryError`: Permanent setup issues
- `ServiceValidationError`: User input errors
- Keep try blocks minimal - process data after the try/catch
- Bare exceptions allowed only in config flows and background tasks

### Logging Guidelines
- No periods at end of messages
- No integration names/domains (added automatically)
- No sensitive data (keys, tokens, passwords)
- Use lazy logging: `_LOGGER.debug("Message with %s", variable)`
- Use debug level for non-user-facing messages

### Documentation
- File headers: `"""Integration for Mercedes-Benz vehicles."""`
- All functions/methods require docstrings
- American English, sentence case

## Key Patterns

- Config entries for per-account configuration
- Services defined in `services.yaml` with implementations in `services.py`
- PIN required for secured commands (locks, windows, engine start)
- Capability checking enabled by default (can be disabled for North America)
- Entity names use `_attr_translation_key` for translations

## Region Notes

- Tested regions: EU, NA, AU, and others (see README)
- Thailand/India: Use "Europe" region
- China: Currently not working
- North America: Cars 2019 or newer only; may need capability check disabled


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

Copyright (c) 2020 Rene Nulsch

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.


// Protocol Buffers for Go with Gadgets
//
// Copyright (c) 2013, The GoGo Authors. All rights reserved.
// http://github.com/gogo/protobuf
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

MIT License

Copyright (c) 2019 MBition GmbH

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: README.md
================================================
# "Mercedes-Benz" custom component

![HassFest tests](https://github.com/renenulschde/mbapi2020/workflows/Validate%20with%20hassfest/badge.svg) ![Validate with HACS](https://github.com/ReneNulschDE/mbapi2020/workflows/Validate%20with%20HACS/badge.svg) ![](https://img.shields.io/github/downloads/renenulschde/mbapi2020/total) ![](https://img.shields.io/github/downloads/renenulschde/mbapi2020/latest/total)

Mercedes-Benz platform as a Custom Component for Home Assistant.


> ⚠️ **SEEKING NEW MAINTAINER** ⚠️  
> After 8+ years of development, I'm selling my last Mercedes and can no longer maintain this integration effectively. **[Looking for someone to take over →](https://github.com/ReneNulschDE/mbapi2020/issues/372)**  



IMPORTANT:

- Please login once into the Mercedes-Benz IOS or Android app before you install this component. (For North America, the app name is Mercedes Me Connect)

- Tested Countries: AT, AU, BE, CA, CH, ~~CN~~, DE, DK, ES, FI, FR, IN, IT, IR, NL, NO, NZ, PT, RO, SE, TH, UK, US

- North America: For Cars built 2019 or newer only
- Thailand, India: Please use the region "Europe".
- Mexico, Brazil,...: Please use the region "APAC"
- China: Is not working currently (captcha).
- Smart cars data are not available after 2025-01-06
- Discussions, Feature Requests via [HA-Community Forum](https://community.home-assistant.io/t/mercedes-me-component/41911)

### Installation

- First: This is not a Home Assistant Add-On. It's a custom component.
- There are two ways to install. First you can download the folder custom_component and copy it into your Home-Assistant config folder. Second option is to install HACS (Home Assistant Custom Component Store) and select "MercedesME 2020" from the Integrations catalog.
- [How to install a custom component?](https://www.google.com/search?q=how+to+install+custom+components+home+assistant)
- [How to install HACS?](https://hacs.xyz/docs/use/)
- Restart HA after the installation
- Make sure that you refresh your browser window too
- Use the "Add Integration" in Home Assistant, Settings, Devices & Services and select "MercedesME 2020".
- Enter your Mercedes-Benz account credentials (username/password) in the integration setup
  **Important Notes:**
- consider using a dedicated Mercedes-Benz account for Home Assistant
- if MFA is enabled on your Mercedes-Benz account, authentication will fail. You must disable MFA or use a separate account without MFA.


### How to Prevent Account Blocking

To reduce the risk of your account being blocked, please follow these recommendations:

1. **Create a separate MB user account for use with this component.**
2. **Invite the new user to the vehicle:**  
   The primary user of the vehicle can invite the new HA-MB account to access the vehicle. Up to six additional users can be invited to each vehicle.
3. **Use each account in a single environment only:**  
   Use one account exclusively in HA or in the official MB tools, but never in both simultaneously.

#### Important Notes

- Certain features, such as geofencing data, are available only to the primary user.
- If geofencing is required in your HA environment, use the primary user account in HA and the secondary accounts in the official MB apps.

---

### Optional configuration values

See Options dialog in the Integration under Home-Assistant/Configuration/Integration.

```
Excluded Cars: comma-separated list of VINs.
PIN: Security PIN to execute special services. Please use your MB mobile app to setup
Disable Capability Check: By default the component checks the capabilities of a car. Active this option to disable the capability check. (For North America)
Debug Save Messages: Enable this option to save all relevant received message into the messages folder of the component
```

## Available components

Depends on your own car or purchased Mercedes-Benz licenses.

### Binary Sensors

- warningwashwater

- warningcoolantlevellow

- warningbrakefluid

- warningenginelight

  ```
  attributes:
  warningbrakefluid, warningwashwater, warningcoolantlevellow, warninglowbattery
  ```

- parkbrakestatus

  ```
  attributes:
  preWarningBrakeLiningWear
  ```

- theftsystemarmed

  ```
  attributes:
  carAlarmLastTime, carAlarmReason, collisionAlarmTimestamp, interiorSensor, interiorProtectionStatus, interiorMonitoringLastEvent, interiorMonitoringStatus, exteriorMonitoringLastEvent, exteriorMonitoringStatus, lastParkEvent, lastTheftWarning, lastTheftWarningReason, parkEventLevel, parkEventType, theftAlarmActive, towProtectionSensorStatus, towSensor,
  ```

- tirewarninglamp

  ```
  attributes:
  tireMarkerFrontRight, tireMarkerFrontLeft,tireMarkerRearLeft, tireMarkerRearRight, tirewarningsrdk, tirewarningsprw, tireTemperatureRearLeft, tireTemperatureFrontRight,
  tireTemperatureRearRight, tireTemperatureFrontLeft
  ```

- windowsClosed

  ```
  attributes:
  windowstatusrearleft, windowstatusrearright, windowstatusfrontright, windowstatusfrontleft
  ```

- remoteStartActive

  ```
  attributes:
  remoteStartTemperature
  ```

- engineState

- chargeFlapACStatus

- Preclimate Status (Preconditioning)

  ```
  attributes:
  precondState, precondActive, precondError, precondNow, precondNowError, precondDuration, precondatdeparture, precondAtDepartureDisable, precondSeatFrontLeft, precondSeatFrontRight, precondSeatRearLeft, precondSeatRearRight, temperature_points_frontLeft, temperature_points_frontRight, temperature_points_rearLeft, temperature_points_rearRight,

  ```

- wiperHealth

  ```
  attributes:
  wiperLifetimeExceeded
  ```

### Buttons

- Flash light
- Preclimate start
- Preclimate stop

### Device Tracker

```
attributes:
positionHeading
```

### Locks

- lock

  PIN setup in MB App is required. If the pin is not set in the integration options then the lock asks for the PIN.

### Sensors

- lock

  ```
  attributes:
  decklidstatus, doorStatusOverall, doorLockStatusOverall, doorlockstatusgas, doorlockstatusvehicle, doorlockstatusfrontleft,doorlockstatusfrontright, doorlockstatusrearright, doorlockstatusrearleft, doorlockstatusdecklid, doorstatusrearleft, doorstatusfrontright, doorstatusrearright, doorstatusfrontleft, rooftopstatus, sunroofstatus, engineHoodStatus
  ```

  Internal value: doorlockstatusvehicle

  Values:
  0: vehicle unlocked
  1: vehicle internal locked
  2: vehicle external locked
  3: vehicle selective unlocked

- Fuel Level (%)

  ```
  attributes:
  tankLevelAdBlue
  ```

- Geofencing Violation

  ```
  attributes:
  Last_event_zone
  ```

  Values:
  ENTER
  LEAVE

- odometer

  ```
  attributes:
  distanceReset, distanceStart, averageSpeedReset, averageSpeedStart, distanceZEReset, drivenTimeZEReset, drivenTimeReset, drivenTimeStart, ecoscoretotal, ecoscorefreewhl, ecoscorebonusrange, ecoscoreconst, ecoscoreaccel, gasconsumptionstart, gasconsumptionreset, gasTankRange, gasTankLevel, liquidconsumptionstart, liquidconsumptionreset, liquidRangeSkipIndication, rangeliquid, serviceintervaldays, tanklevelpercent, tankReserveLamp, batteryState, tankLevelAdBlue
  ```

- Oil Level (%)

- Range Electric

  ```
  attributes:
  chargingstatus, distanceElectricalReset, distanceElectricalStart, ecoElectricBatteryTemperature, endofchargetime, maxrange, selectedChargeProgram, precondActive [DEPRECATED], precondNow [DEPRECATED], precondDuration [DEPRECATED]

  ```

- Electric consumption start

- Electric consumption reset

- Charging power

- Charging Power Limit

  ```
  attributes:
  chargingPowerRestriction
  ```

- Starter Battery State

  ```
  Internal Name: starterBatteryState

  Values     Description_short     Description_long
  "0"        "green"               "Vehicle ok"
  "1"        "yellow"              "Battery partly charged"
  "2"        "red"                 "Vehicle not available"
  "3"        "serviceDisabled"     "Remote service disabled"
  "4"        "vehicleNotAvalable"  "Vehicle no longer available"
  ```

- tirepressureRearLeft

- tirepressureRearRight

- tirepressureFrontRight

- tirepressureFrontLeft

- State of Charge (soc)

  ```
  Internal Name: soc

  State of charge (SoC) is the level of charge of an electric battery relative to its capacity. The units of SoC are percentage points (0% = empty; 100% = full).

  attributes:
  maxSocLowerLimit, maxSoc

  ```

- Ignition state

  ```
  Internal Name: ignitionstate

  Values     Description_short     Description_long
  "0"        "lock"                "Ignition lock"
  "1"        "off"                 "Ignition off"
  "2"        "accessory"           "Ignition accessory"
  "4"        "on"                  "Ignition on"
  "5"        "start"               "Ignition start"
  ```

- Aux Heat Status

  ```
  Internal Name: auxheatstatus

  Values    Description
  "0"       inactive
  "1"       normal heating
  "2"       normal ventilation
  "3"       manual heating
  "4"       post heating
  "5"       post ventilation
  "6"       auto heating

  attributes:
  auxheattime1, auxheattime2, auxheattime3, auxheattimeselection, auxheatActive, auxheatwarnings, auxheattime2, temperature_points_frontLeft, temperature_points_frontRight

  ```

- Departure Time

  ```
  Internal Name: departuretime

  Planned departure time to initiate preclimate functions

  attributes:
  departureTimeWeekday

  ```

### Diagnostic Sensors

[Diagnostic sensors](https://www.home-assistant.io/blog/2021/11/03/release-202111/#entity-categorization) are hidden by default, check the devices page to see the current values

- Car

  ```
  attributes:
  full_update_messages_received, partital_update_messages_received, last_message_received, last_command_type, last_command_state, last_command_error_code, last_command_error_message
  ```

- RCP_Features

  Sensor shows true if extended configuration like interior lighting is available. This feature requires a reauthentication in case you used a version <0.6 before (We need some more permissions...). Shows False in case reauthentication has not happened or the feature is not available for your car.

  ```
  attributes:
  rcp_supported_settings (List of all remote configuration options, I'll implement them step by step as services or buttons)
  ```

### Services

Some services require that the security PIN is created in your mobile Android/IOS app. Please store the pin to the options-dialog of the integration

- refresh_access_token:
  Refresh the API access token

- auxheat_start:
  Start the auxiliary heating of a car defined by a vin.

- auxheat_stop:
  Stop the auxiliary heating of a car defined by a vin.

- battery_max_soc_configure:
  Configure the maximum value for the state of charge of the HV battery of a car defined by a vin.

- doors_unlock:
  Unlock a car defined by a vin. PIN required.

- doors_lock:
  Lock a car defined by a vin.

- engine_start:
  Start the engine of a car defined by a vin. PIN required.

- engine_stop:
  Stop the engine of a car defined by a vin.

- preconditioning_configure_seats:
  Configure which seats should be preconditioned of a car defined by a vin.

- preheat_start:
  Start the preheating of a zero emission car defined by a vin.

- preheat_start_departure_time:
  Start the preheating of a zero emission car defined by a vin and the departure time in minutes since midnight

- preheat_stop:
  Stop the preheating of a zero emission car defined by a vin.

- preheat_stop_departure_time:
  Disable scheduled departure preconditioning of a zero emission car defined by a vin.

- preconditioning_configure:
  Configure the departure time preconditioning mode (disabled, single, or weekly) of a zero emission car defined by a vin.

- send_route:
  Send a route to a car defined by a vin.

- sigpos_start:
  Start light signaling of a car defined by a vin.

- sunroof_open:
  Open the sunroof of a car defined by a vin. PIN required.

- sunroof_tilt:
  Tilt the sunroof of a car defined by a vin. PIN required.

- sunroof_close:
  Close the sunroof of a car defined by a vin.

- temperature_configure:
  Configure the target preconditioning/auxheat temperatures for zones in a car defined by a VIN.

- windows_close:
  Close the windows of a car defined by a vin.

- windows_move
  Move the windows to a given position. PIN required.

- windows_open:
  Open the windows of a car defined by a vin. PIN required.

### Switches

- AuxHeat - Start/Stop the auxiliary heating of the car
- Preclimate - Start/Stop the preclimate function of the car

### Logging

Set the logging to debug with the following settings in case of problems.

```
logger:
  default: warn
  logs:
    custom_components.mbapi2020: debug
```

### Open Items

- Find a maintainer

### Useful links

- [Forum post](https://community.home-assistant.io/t/mercedes-me-component/41911/520)

## Custom Lovelace Card

Enhance your experience with this integration by using [VEHICLE INFO CARD](https://github.com/ngocjohn/vehicle-info-card). This card is designed to work seamlessly with the integration, providing a beautiful and intuitive interface to display the data in your Home Assistant dashboard.

### Key Features

- **Seamless Integration**: Automatically pulls in data from the integration.
- **Customizable**: Easily modify the card’s appearance to fit your theme.
- **Interactive**: Includes controls to interact with the data directly from your dashboard.
- **Multilingual Support**: The card includes various translations, making it accessible in multiple languages.

[Check out the Custom Lovelace Card](https://github.com/ngocjohn/vehicle-info-card) for more details and installation instructions.


================================================
FILE: SECURITY.md
================================================
# Security Policy

## Reporting Security Issues

**Please do not report security vulnerabilities through public GitHub issues.**

Please send an email to [secure-mbapi2020@nulsch.de](mailto:secure-mbapi2020@nulsch.de). 

You should receive a response within 24 hours. If for some reason you do not, please follow up via email and keep in mind this project is the hobby of one person.

Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:

  * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
  * Full paths of source file(s) related to the manifestation of the issue
  * The location of the affected source code (tag/branch/commit or direct URL)
  * Any special configuration required to reproduce the issue
  * Step-by-step instructions to reproduce the issue
  * Proof-of-concept or exploit code (if possible)
  * Impact of the issue, including how an attacker might exploit the issue

This information will help me triage your report more quickly.

If you are reporting for a bug bounty, then this is the wrong project. I do not have a bug bounty programm.

## Preferred Languages

I prefer all communications to be in English or German.


================================================
FILE: custom_components/mbapi2020/__init__.py
================================================
"""The MercedesME 2020 integration."""

from __future__ import annotations

import asyncio
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from datetime import datetime
import time
from typing import Any

import aiohttp
import voluptuous as vol

from custom_components.mbapi2020.car import Car, CarAttribute, RcpOptions
from custom_components.mbapi2020.const import (
    ATTR_MB_MANUFACTURER,
    CONF_ENABLE_CHINA_GCJ_02,
    CONF_OVERWRITE_PRECONDNOW,
    DOMAIN,
    LOGGER,
    LOGIN_BASE_URI,
    MERCEDESME_COMPONENTS,
    UNITS,
    SensorConfigFields as scf,
)
from custom_components.mbapi2020.coordinator import MBAPI2020DataUpdateCoordinator
from custom_components.mbapi2020.errors import WebsocketError
from custom_components.mbapi2020.helper import LogHelper as loghelper
from custom_components.mbapi2020.services import setup_services
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryError, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import slugify

CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
    """Set up MBAPI2020."""
    LOGGER.debug("Start async_setup - Initializing services.")
    hass.data.setdefault(DOMAIN, {})
    setup_services(hass)

    return True


async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
    """Set up MercedesME 2020 from a config entry."""
    LOGGER.debug("Start async_setup_entry.")

    try:
        coordinator = MBAPI2020DataUpdateCoordinator(hass, config_entry)
        hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator

        await coordinator.client.set_rlock_mode()

        try:
            token_info = await coordinator.client.oauth.async_get_cached_token()
        except aiohttp.ClientError as err:
            LOGGER.warning("Can not connect to MB OAuth API %s. Will try again.", LOGIN_BASE_URI)
            LOGGER.debug("Can not connect to MB OAuth API %s. Will try again. %s", LOGIN_BASE_URI, err)
            raise ConfigEntryNotReady from err

        if token_info is None:
            LOGGER.error("Authentication failed. Please reauthenticate.")
            raise ConfigEntryAuthFailed

        bff_app_config = await coordinator.client.webapi.get_config()
        masterdata = await coordinator.client.webapi.get_user_info()
        hass.async_add_executor_job(coordinator.client.write_debug_json_output, bff_app_config, "app", True)
        hass.async_add_executor_job(coordinator.client.write_debug_json_output, masterdata, "md", True)

        vehicles = []

        if not masterdata:
            LOGGER.error("No masterdata found. Please check your account/credentials.")
            raise ConfigEntryNotReady("No masterdata found. Please check your account/credentials.")

        for fleet in masterdata.get("fleets", []):
            company_id = fleet.get("companyId")
            fleet_id = fleet.get("fleetId")
            LOGGER.debug(
                "Fleet %s with company %s found. Collectting car information.",
                fleet.get("fleetName", "unknown fleet name"),
                fleet.get("companyName", "unknown company"),
            )
            fleet_info = await coordinator.client.webapi.get_fleet_info(company_id, fleet_id)
            hass.async_add_executor_job(
                coordinator.client.write_debug_json_output,
                fleet_info,
                f"fleet_{company_id}_{fleet_id}",
                True,
            )

            vehicles.extend(fleet.get("bookedVehicles", []))

        vehicles.extend(masterdata.get("assignedVehicles", []))

        for car in vehicles:
            # Check if the car has a separate VIN key, if not, use the FIN.
            vin = car.get("vin")
            if vin is None:
                vin = car.get("fin")
                LOGGER.debug(
                    "VIN not found in masterdata. Used FIN %s instead.",
                    loghelper.Mask_VIN(vin),
                )

            # Car is excluded, we do not add this
            if vin in config_entry.options.get("excluded_cars", ""):
                continue

            features: dict[str, bool] = {}
            vehicle_information: dict = {}

            try:
                car_capabilities = await coordinator.client.webapi.get_car_capabilities(vin)
                hass.async_add_executor_job(
                    coordinator.client.write_debug_json_output,
                    car_capabilities,
                    f"cai-{loghelper.Mask_VIN(vin)}-",
                    True,
                )
                if car_capabilities and "features" in car_capabilities:
                    features.update(car_capabilities["features"])
                if car_capabilities and "vehicle" in car_capabilities:
                    vehicle_information = car_capabilities["vehicle"]
            except aiohttp.ClientError:
                # For some cars a HTTP401 is raised when asking for capabilities, see github issue #83
                LOGGER.info(
                    "Car Capabilities not available for the car with VIN %s.",
                    loghelper.Mask_VIN(vin),
                )

            try:
                capabilities = await coordinator.client.webapi.get_car_capabilities_commands(vin)
                hass.async_add_executor_job(
                    coordinator.client.write_debug_json_output,
                    capabilities,
                    f"ca-{loghelper.Mask_VIN(vin)}-",
                    True,
                )
                if capabilities:
                    for feature in capabilities.get("commands"):
                        features[feature.get("commandName")] = bool(feature.get("isAvailable"))
                        if feature.get("commandName", "") == "ZEV_PRECONDITION_CONFIGURE_SEATS":
                            capabilityInformation = feature.get("capabilityInformation", None)
                            if capabilityInformation and len(capabilityInformation) > 0:
                                features[feature.get("capabilityInformation")[0]] = bool(feature.get("isAvailable"))
                        if feature.get("commandName", "") == "CHARGE_PROGRAM_CONFIGURE":
                            max_soc_found = False
                            parameters = feature.get("parameters", [])
                            if parameters is not None:
                                for parameter in parameters:
                                    if parameter.get("parameterName", "") == "MAX_SOC":
                                        max_soc_found = True
                            features["CHARGE_PROGRAM_CONFIGURE"] = max_soc_found
            except aiohttp.ClientError:
                # For some cars a HTTP401 is raised when asking for capabilities, see github issue #83
                # We just ignore the capabilities
                LOGGER.info(
                    "Command Capabilities not available for the car with VIN %s. Make sure you disable the capability check in the option of this component.",
                    loghelper.Mask_VIN(vin),
                )

            rcp_options = RcpOptions()
            rcp_supported = False  # await coordinator.client.webapi.is_car_rcp_supported(vin)
            LOGGER.debug("RCP supported for car %s: %s", loghelper.Mask_VIN(vin), rcp_supported)
            setattr(rcp_options, "rcp_supported", CarAttribute(rcp_supported, "VALID", 0))
            # rcp_supported = False
            # if rcp_supported:
            #     rcp_supported_settings = await coordinator.client.webapi.get_car_rcp_supported_settings(vin)
            #     if rcp_supported_settings:
            #         hass.async_add_executor_job(
            #             coordinator.client.write_debug_json_output,
            #             rcp_supported_settings,
            #             "rcs",
            #         )
            #         if rcp_supported_settings.get("data"):
            #             if rcp_supported_settings.get("data").get("attributes"):
            #                 if rcp_supported_settings.get("data").get("attributes").get("supportedSettings"):
            #                     LOGGER.debug(
            #                         "RCP supported settings: %s",
            #                         str(rcp_supported_settings.get("data").get("attributes").get("supportedSettings")),
            #                     )
            #                     setattr(
            #                         rcp_options,
            #                         "rcp_supported_settings",
            #                         CarAttribute(
            #                             rcp_supported_settings.get("data").get("attributes").get("supportedSettings"),
            #                             "VALID",
            #                             0,
            #                         ),
            #                     )

            #                     for setting in (
            #                         rcp_supported_settings.get("data").get("attributes").get("supportedSettings")
            #                     ):
            #                         setting_result = await coordinator.client.webapi.get_car_rcp_settings(vin, setting)
            #                         if setting_result is not None:
            #                             hass.async_add_executor_job(
            #                                 coordinator.client.write_debug_json_output,
            #                                 setting_result,
            #                                 f"rcs_{setting}",
            #                             )

            current_car = Car(vin)
            current_car.licenseplate = car.get("licensePlate", vin)
            current_car.baumuster_description = (
                car.get("salesRelatedInformation", "").get("baumuster", "").get("baumusterDescription", "")
            )
            if not current_car.licenseplate.strip():
                current_car.licenseplate = vin
            current_car.features = features
            current_car.vehicle_information = vehicle_information
            current_car.masterdata = car
            current_car.app_configuration = bff_app_config
            current_car.rcp_options = rcp_options
            current_car.capabilities = capabilities
            current_car.last_message_received = int(round(time.time() * 1000))
            current_car.is_owner = car.get("isOwner")

            if config_entry.options.get(CONF_OVERWRITE_PRECONDNOW, False):
                current_car.features["precondNow"] = True

            coordinator.client.cars[vin] = current_car
            # await coordinator.client.update_poll_states(vin)

            LOGGER.debug("Init - car added - %s", loghelper.Mask_VIN(current_car.finorvin))

        await coordinator.async_config_entry_first_refresh()

        if len(coordinator.client.cars) == 0:
            LOGGER.error("No cars found. Please check your account/credentials or excluded VINs.")
            raise ConfigEntryError("No cars found. Please check your account/credentials or excluded VINs.")

        hass.loop.create_task(coordinator.ws_connect())

    except aiohttp.ClientError as err:
        LOGGER.warning("Can't connect to MB APIs; Retrying in background: %s", err)
        raise ConfigEntryNotReady from err
    except WebsocketError as err:
        LOGGER.error("Websocket error: %s", err)
        raise ConfigEntryNotReady from err

    retry_counter: int = 0
    while not coordinator.entry_setup_complete:
        # async websocket data load not complete, wait 0.5 seconds or break up after 60 checks (30sec)
        if retry_counter == 60 and coordinator.client.websocket.account_blocked:
            for vin in coordinator.client.cars:
                await coordinator.client.update_poll_states(vin)
            LOGGER.warning("Account is blocked. Reload will happen after unblock at midnight (GMT).")
            break
        if retry_counter == 60 and not coordinator.client.account_blocked:
            for vin in coordinator.client.cars:
                await coordinator.client.update_poll_states(vin)
            LOGGER.warning(
                "No full_update set received via websocket for some/all cars. Not all sensors may be available. Missing sensors will be created after the data will be available."
            )
            break

        await asyncio.sleep(0.5)
        retry_counter += 1

    return True


async def config_entry_update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
    """Update listener, called when the config entry options are changed."""
    LOGGER.debug("Start config_entry_update async_reload")
    await hass.config_entries.async_reload(config_entry.entry_id)


async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
    """Unload mbapi2020 Home config entry."""
    LOGGER.debug("Start unload component.")
    unload_ok = False

    if len(hass.data[DOMAIN][config_entry.entry_id].client.cars) > 0:
        # Cancel all watchdogs on final shutdown
        websocket = hass.data[DOMAIN][config_entry.entry_id].client.websocket
        websocket._reconnectwatchdog.cancel()
        websocket._watchdog.cancel()
        websocket._pingwatchdog.cancel()
        websocket.component_reload_watcher.cancel()

        # EVENT_HOMEASSISTANT_STOP-Listener deregistrieren, damit alte
        # Instanzen beim HA-Shutdown nicht erneut aufgerufen werden.
        if websocket.ha_stop_handler:
            websocket.ha_stop_handler()
            websocket.ha_stop_handler = None

        result = await websocket.async_stop()

        websocket._reconnectwatchdog.cancel()
        websocket._watchdog.cancel()
        websocket._pingwatchdog.cancel()
        websocket.component_reload_watcher.cancel()

        hass.data[DOMAIN][config_entry.entry_id].client.websocket = None
        if unload_ok := await hass.config_entries.async_unload_platforms(config_entry, MERCEDESME_COMPONENTS):
            del hass.data[DOMAIN][config_entry.entry_id]
    else:
        # No cars loaded, we destroy the config entry only
        del hass.data[DOMAIN][config_entry.entry_id]
        unload_ok = True

    LOGGER.debug("unload result: %s", unload_ok)
    return unload_ok


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


@dataclass(frozen=True, kw_only=True)
class MercedesMeEntityDescription(EntityDescription):
    """Configuration class for MercedesMe entities."""

    attributes: list[str] | None = None
    check_capability_fn: Callable[[Car], Callable[[], Coroutine[Any, Any, bool]]]


class MercedesMeEntity(CoordinatorEntity[MBAPI2020DataUpdateCoordinator], Entity):
    """Entity class for MercedesMe devices."""

    _attr_has_entity_name = True

    def __init__(
        self,
        internal_name: str,
        config: list | EntityDescription,
        vin: str,
        coordinator: MBAPI2020DataUpdateCoordinator,
        should_poll: bool = False,
    ) -> None:
        """Initialize the MercedesMe entity."""

        self._hass = coordinator.hass
        self._coordinator = coordinator
        self._vin = vin
        self._internal_name = internal_name
        self._sensor_config = config
        self._car = self._coordinator.client.cars[self._vin]
        self._feature_name = None
        self._object_name = None
        self._attrib_name = None

        self._flip_result = False
        self._state = None

        # Temporary workaround: If PR get's approved, all entity types should be migrated to the new config classes
        if isinstance(config, EntityDescription):
            self._attributes = config.attributes
            self.entity_description = config
        else:
            self._feature_name = config[scf.OBJECT_NAME.value]
            self._object_name = config[scf.ATTRIBUTE_NAME.value]
            self._attrib_name = config[scf.VALUE_FIELD_NAME.value]
            self._flip_result = config[scf.FLIP_RESULT.value]
            self._attr_device_class = self._sensor_config[scf.DEVICE_CLASS.value]
            self._attr_icon = self._sensor_config[scf.ICON.value]
            self._attr_state_class = self._sensor_config[scf.STATE_CLASS.value]
            self._attr_entity_category = self._sensor_config[scf.ENTITY_CATEGORY.value]
            self._attributes = self._sensor_config[scf.EXTENDED_ATTRIBUTE_LIST.value]
            self._attr_native_unit_of_measurement = self.unit_of_measurement
            self._attr_suggested_display_precision = self._sensor_config[scf.SUGGESTED_DISPLAY_PRECISION.value]
            self._use_chinese_location_data: bool = self._coordinator.config_entry.options.get(
                CONF_ENABLE_CHINA_GCJ_02, False
            )
            self._attr_translation_key = self._internal_name.lower()
            self._attr_name = config[scf.DISPLAY_NAME.value]
            self._name = f"{self._car.licenseplate} {config[scf.DISPLAY_NAME.value]}"

        self._attr_device_info = {"identifiers": {(DOMAIN, self._vin)}}
        self._attr_should_poll = should_poll
        self._attr_unique_id = slugify(f"{self._vin}_{self._internal_name}")

        super().__init__(coordinator)

    def device_retrieval_status(self):
        """Return the retrieval_status of the sensor."""
        if self._internal_name == "car":
            return "VALID"

        return self._get_car_value(self._feature_name, self._object_name, "retrievalstatus", "error")

    @property
    def extra_state_attributes(self):
        """Return the state attributes."""

        state = {"car": self._car.licenseplate, "vin": self._vin}

        if self._attrib_name == "display_value":
            value = self._get_car_value(self._feature_name, self._object_name, "value", None)
            if value:
                state["original_value"] = value

        for item in ["retrievalstatus", "timestamp", "unit"]:
            value = self._get_car_value(self._feature_name, self._object_name, item, None)
            if value:
                state[item] = value if item != "timestamp" else datetime.fromtimestamp(int(value))

        if self._attributes is not None:
            for attrib in sorted(self._attributes):
                if "." in attrib:
                    object_name = attrib.split(".")[0]
                    attrib_name = attrib.split(".")[1]
                else:
                    object_name = self._feature_name
                    attrib_name = attrib
                retrievalstatus = self._get_car_value(object_name, attrib_name, "retrievalstatus", "error")

                if retrievalstatus == "VALID":
                    state[attrib_name] = self._get_car_value(object_name, attrib_name, "display_value", None)
                    if not state[attrib_name]:
                        state[attrib_name] = self._get_car_value(object_name, attrib_name, "value", "error")

                if retrievalstatus in ["NOT_RECEIVED"]:
                    state[attrib_name] = "NOT_RECEIVED"
        return state

    @property
    def device_info(self) -> DeviceInfo:
        """Device information."""
        return DeviceInfo(
            identifiers={(DOMAIN, self._vin)},
            manufacturer=ATTR_MB_MANUFACTURER,
            model=self._car.baumuster_description,
            name=self._car.licenseplate,
            sw_version=f"{self._car.vehicle_information.get('headUnitSoftwareVersion', '')} - {self._car.vehicle_information.get('headUnitType', '')}",
            hw_version=f"{self._car.vehicle_information.get('starArchitecture', '')} - {self._car.vehicle_information.get('tcuType', '')}",
        )

    @property
    def unit_of_measurement(self):
        """Return the unit of measurement."""

        if "unit" in self.extra_state_attributes:
            reported_unit: str = self.extra_state_attributes["unit"]
            if reported_unit.upper() in UNITS:
                return UNITS[reported_unit.upper()]

            LOGGER.warning(
                "Unknown unit %s found. Please report via issue https://www.github.com/renenulschde/mbapi2020/issues",
                reported_unit,
            )
            return reported_unit

        if isinstance(self._sensor_config, EntityDescription):
            return None
        return self._sensor_config[scf.UNIT_OF_MEASUREMENT.value]

    def update(self):
        """Get the latest data and updates the states."""
        if not self.enabled:
            return

        if isinstance(self._sensor_config, EntityDescription):
            self._mercedes_me_update()
        else:
            self._state = self._get_car_value(self._feature_name, self._object_name, self._attrib_name, "error")
            self.async_write_ha_state()

    def _mercedes_me_update(self) -> None:
        """Update Mercedes Me entity."""
        raise NotImplementedError

    def _get_car_value(self, feature, object_name, attrib_name, default_value):
        value = None

        if object_name:
            if not feature:
                value = getattr(
                    getattr(self._car, object_name, default_value),
                    attrib_name,
                    default_value,
                )
            else:
                value = getattr(
                    getattr(
                        getattr(self._car, feature, default_value),
                        object_name,
                        default_value,
                    ),
                    attrib_name,
                    default_value,
                )

        else:
            value = getattr(self._car, attrib_name, default_value)

        return value

    def _get_car_attribute(self, feature, object_name):
        """Get the CarAttribute object for this sensor."""
        if object_name:
            if not feature:
                return getattr(self._car, object_name, None)
            feature_obj = getattr(self._car, feature, None)
            if feature_obj:
                return getattr(feature_obj, object_name, None)
        else:
            return getattr(self._car, self._attrib_name, None)

        return None

    def pushdata_update_callback(self):
        """Schedule a state update."""
        self.update()

    @callback
    def _handle_coordinator_update(self) -> None:
        """Handle updated data from the coordinator."""
        self.update()

    async def async_added_to_hass(self):
        """Add callback after being added to hass.

        Show latest data after startup.
        """
        await super().async_added_to_hass()
        if not self._attr_should_poll:
            self._car.add_update_listener(self.pushdata_update_callback)

        self.async_schedule_update_ha_state(True)
        self._handle_coordinator_update()

    async def async_will_remove_from_hass(self):
        """Entity being removed from hass."""
        await super().async_will_remove_from_hass()
        self._car.remove_update_callback(self.pushdata_update_callback)


================================================
FILE: custom_components/mbapi2020/binary_sensor.py
================================================
"""Support for Mercedes cars with Mercedes ME.

For more details about this component, please refer to the documentation at
https://github.com/ReneNulschDE/mbapi2020/
"""

from __future__ import annotations

from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity

from . import MercedesMeEntity
from .const import (
    CONF_FT_DISABLE_CAPABILITY_CHECK,
    DOMAIN,
    LOGGER,
    BinarySensors,
    DefaultValueModeType,
    SensorConfigFields as scf,
)
from .coordinator import MBAPI2020DataUpdateCoordinator


def _create_binary_sensor_if_eligible(key, config, car, coordinator):
    """Check if binary sensor should be created and return device if eligible."""
    # Skip special sensors that should not be created dynamically
    if key in ["car", "data_mode"]:
        return None

    if (
        config[scf.CAPABILITIES_LIST.value] is None
        or coordinator.config_entry.options.get(CONF_FT_DISABLE_CAPABILITY_CHECK, False)
        or car.features.get(config[scf.CAPABILITIES_LIST.value], False)
    ):
        device = MercedesMEBinarySensor(
            internal_name=key,
            config=config,
            vin=car.finorvin,
            coordinator=coordinator,
        )

        # Check eligibility
        status = device.device_retrieval_status()
        is_eligible = status in ["VALID", "NOT_RECEIVED", "3", 3] or (
            config[scf.DEFAULT_VALUE_MODE.value] is not None
            and config[scf.DEFAULT_VALUE_MODE.value] != DefaultValueModeType.NONE
            and str(status) != "4"
        )

        if is_eligible:
            return device

    return None


async def create_missing_binary_sensors_for_car(car, coordinator, async_add_entities):
    """Create missing binary sensors for a specific car."""

    missing_sensors = []

    for key, value in sorted(BinarySensors.items()):
        device = _create_binary_sensor_if_eligible(key, value, car, coordinator)
        if device and f"binary_sensor.{device.unique_id}" not in car.sensors:
            missing_sensors.append(device)
            LOGGER.debug("Sensor added: %s", device._name)

    if missing_sensors:
        await async_add_entities(missing_sensors, True)
        return len(missing_sensors)
    return 0


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: ConfigEntry,
    async_add_entities: AddEntitiesCallback,
):
    """Set up integration from a config entry."""

    coordinator: MBAPI2020DataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]

    if not coordinator.client.cars:
        LOGGER.info("No Cars found.")
        return

    sensors = []
    for car in coordinator.client.cars.values():
        for key, value in sorted(BinarySensors.items()):
            device = _create_binary_sensor_if_eligible(key, value, car, coordinator)
            if device:
                sensors.append(device)

    async_add_entities(sensors, True)


class MercedesMEBinarySensor(MercedesMeEntity, BinarySensorEntity, RestoreEntity):
    """Representation of a Sensor."""

    def flip(self, state):
        """Flip the result."""
        if self._flip_result:
            return not state
        return state

    @property
    def is_on(self):
        """Return the state of the binary sensor."""

        if self._state is None:
            self.update()

        if self._state == "INACTIVE":
            return self.flip(False)
        if self._state == "ACTIVE":
            return self.flip(True)
        if self._state == "0":
            return self.flip(False)
        if self._state == "1":
            return self.flip(True)
        if self._state == "2":
            return self.flip(False)
        if self._state == 0:
            return self.flip(False)
        if self._state == 1:
            return self.flip(True)
        if self._state == 2:
            return self.flip(False)
        if self._state == "true":
            return self.flip(True)
        if self._state == "false":
            return self.flip(False)
        if self._state is False:
            return self.flip(False)
        if self._state is True:
            return self.flip(True)

        return self._state

    async def async_added_to_hass(self):
        """Add callback after being added to hass."""

        self._car.add_sensor(f"binary_sensor.{self._attr_unique_id}")
        await super().async_added_to_hass()

    async def async_will_remove_from_hass(self):
        """Entity being removed from hass."""

        self._car.remove_sensor(f"binary_sensor.{self._attr_unique_id}")
        await super().async_will_remove_from_hass()


================================================
FILE: custom_components/mbapi2020/button.py
================================================
"""Button support for Mercedes cars with Mercedes ME.

For more details about this component, please refer to the documentation at
https://github.com/ReneNulschDE/mbapi2020/
"""

from __future__ import annotations

from homeassistant.components.button import ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import MercedesMeEntity
from .const import BUTTONS, CONF_FT_DISABLE_CAPABILITY_CHECK, DOMAIN, LOGGER, SensorConfigFields as scf
from .coordinator import MBAPI2020DataUpdateCoordinator


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: ConfigEntry,
    async_add_entities: AddEntitiesCallback,
) -> None:
    """Set up the demo button platform."""

    coordinator: MBAPI2020DataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]

    if not coordinator.client.cars:
        LOGGER.info("No Cars found.")
        return

    button_list = []
    for car in coordinator.client.cars.values():
        for key, value in sorted(BUTTONS.items()):
            if (
                value[scf.CAPABILITIES_LIST.value] is None
                or config_entry.options.get(CONF_FT_DISABLE_CAPABILITY_CHECK, False) is True
                or car.features.get(value[scf.CAPABILITIES_LIST.value], False) is True
            ):
                device = MercedesMEButton(
                    internal_name=key,
                    config=value,
                    vin=car.finorvin,
                    coordinator=coordinator,
                )

                button_list.append(device)

    async_add_entities(button_list, False)


class MercedesMEButton(MercedesMeEntity, ButtonEntity):
    """Representation of a Sensor."""

    async def async_press(self) -> None:
        """Send out a persistent notification."""
        service = getattr(self._coordinator.client, self._sensor_config[3])
        await service(self._vin)
        self._state = None

    def update(self):
        """Nothing to update as buttons are stateless."""


================================================
FILE: custom_components/mbapi2020/car.py
================================================
"""Define the objects to store care data."""

from __future__ import annotations

import collections
from dataclasses import dataclass
from datetime import datetime
from typing import Any

ODOMETER_OPTIONS = [
    "odo",
    "distanceReset",
    "distanceStart",
    "averageSpeedReset",
    "averageSpeedStart",
    "drivenTimeZEReset",
    "drivenTimeReset",
    "drivenTimeStart",
    "ecoscoretotal",
    "ecoscorefreewhl",
    "ecoscorebonusrange",
    "ecoscoreconst",
    "ecoscoreaccel",
    "gasconsumptionstart",
    "gasconsumptionreset",
    "gasTankRange",
    "gasTankLevel",
    "gasTankLevelPercent",
    "liquidconsumptionstart",
    "liquidconsumptionreset",
    "liquidRangeSkipIndication",
    "outsideTemperature",
    "rangeliquid",
    "remoteStartTemperature",
    "serviceintervaldays",
    "serviceintervaldistance",
    "tanklevelpercent",
    "tankReserveLamp",
    "batteryState",
    "tankLevelAdBlue",
    "vehicleDataConnectionState",
    "ignitionstate",
    "oilLevel",
    "departuretime",
    "departureTimeWeekday",
]

LOCATION_OPTIONS = ["positionLat", "positionLong", "positionHeading"]

TIRE_OPTIONS = [
    "tirepressureRearLeft",
    "tirepressureRearRight",
    "tirepressureFrontRight",
    "tirepressureFrontLeft",
    "tirewarninglamp",
    "tirewarningsrdk",
    "tirewarningsprw",
    "tireMarkerFrontRight",
    "tireMarkerFrontLeft",
    "tireMarkerRearLeft",
    "tireMarkerRearRight",
    "tireWarningRollup",
    "lastTirepressureTimestamp",
    "tireTemperatureRearLeft",
    "tireTemperatureFrontRight",
    "tireTemperatureRearRight",
    "tireTemperatureFrontLeft",
]

WINDOW_OPTIONS = [
    "windowstatusrearleft",
    "windowstatusrearright",
    "windowstatusfrontright",
    "windowstatusfrontleft",
    "windowStatusOverall",
    "flipWindowStatus",
]

DOOR_OPTIONS = [
    "decklidstatus",
    "doorStatusOverall",
    "doorLockStatusOverall",
    "doorlockstatusgas",
    "doorlockstatusvehicle",
    "doorlockstatusfrontleft",
    "doorlockstatusfrontright",
    "doorlockstatusrearright",
    "doorlockstatusrearleft",
    "doorlockstatusdecklid",
    "doorstatusrearleft",
    "doorstatusfrontright",
    "doorstatusrearright",
    "doorstatusfrontleft",
    "rooftopstatus",
    "sunroofstatus",
    "engineHoodStatus",
    "chargeFlapDCStatus",
    "chargeFlapACStatus",
]

ELECTRIC_OPTIONS = [
    "rangeelectric",
    "chargeflap",
    "chargeinletcoupler",
    "chargeinletlock",
    "chargeCouplerACStatus",
    "chargeCouplerDCStatus",
    "chargeCouplerACLockStatus",
    "chargeCouplerDCLockStatus",
    "chargePrograms",
    "chargingactive",
    "chargingBreakClockTimer",
    "chargingstatus",
    "chargingPower",
    "chargingPowerEcoLimit",
    "chargingPowerRestriction",
    "departureTimeMode",
    "distanceElectricalReset",
    "distanceElectricalStart",
    "distanceZEReset",
    "distanceZEStart",
    "ecoElectricBatteryTemperature",
    "electricconsumptionstart",
    "electricconsumptionreset",
    "electricRatioStart",
    "electricRatioReset",
    "electricRatioOverall",
    "endofchargetime",
    "endofChargeTimeWeekday",
    "selectedChargeProgram",
    "maxrange",
    "maxSocLowerLimit",
    "maxSoc",
    "max_soc",
    "soc",
]

BINARY_SENSOR_OPTIONS = [
    "warningwashwater",
    "warningenginelight",
    "warningbrakefluid",
    "warningcoolantlevellow",
    "parkbrakestatus",
    #'readingLampFrontRight',
    #'readingLampFrontLeft',
    "warningBrakeLiningWear",
    "warninglowbattery",
    "starterBatteryState",
    "liquidRangeCritical",
    "tankCapOpenLamp",
    "remoteStartActive",
    "engineState",
]

AUX_HEAT_OPTIONS = [
    "auxheatActive",
    "auxheatwarnings",
    "auxheatruntime",
    "auxheatstatus",
    "auxheatwarningsPush",
    "auxheattimeselection",
    "auxheattime1",
    "auxheattime2",
    "auxheattime3",
]

WIPER_OPTIONS = ["wiperLifetimeExceeded", "wiperHealthPercent"]

PRE_COND_OPTIONS = [
    "precondStatus",
    "precondOperatingMode",
    "precondState",
    "precondActive",
    "precondError",
    "precondNow",
    "precondNowError",
    "precondDuration",
    "precondatdeparture",
    "precondAtDepartureDisable",
    "precondSeatFrontLeft",
    "precondSeatFrontRight",
    "precondSeatRearLeft",
    "precondSeatRearRight",
    "temperature_points_frontLeft",
    "temperature_points_frontRight",
    "temperature_points_rearLeft",
    "temperature_points_rearRight",
]

RemoteStart_OPTIONS = ["remoteEngine", "remoteStartEndtime", "remoteStartTemperature"]

CarAlarm_OPTIONS = [
    "carAlarm",
    "carAlarmLastTime",
    "carAlarmReason",
    "collisionAlarmTimestamp",
    "interiorSensor",
    "lastParkEvent",
    "lastTheftWarning",
    "lastTheftWarningReason",
    "parkEventLevel",
    "parkEventType",
    "theftAlarmActive",
    "theftSystemArmed",
    "towProtectionSensorStatus",
    "towSensor",
    "interiorProtectionSensorStatus",
    "exteriorProtectionSensorStatus",
]

GeofenceEvents_OPTIONS = ["last_event_zone", "last_event_timestamp", "last_event_type"]


class Car:
    """Car class, stores the car values at runtime."""

    def __init__(self, vin: str):
        """Initialize the Car instance."""
        self.finorvin = vin
        self.vehicle_information: dict = {}
        self.capabilities: dict[str, Any] = {}

        self.licenseplate = ""
        self._is_owner = False
        self.messages_received = collections.Counter(f=0, p=0)
        self._last_message_received = 0
        self._last_command_type = ""
        self._last_command_state = ""
        self._last_command_error_code = ""
        self._last_command_error_message = ""
        self.last_command_time_stamp = 0

        self.binarysensors = None
        self.tires = None
        self.wipers = None
        self.odometer = None
        self.doors = None
        self.location = None
        self.windows = None
        self.rcp_options = None
        self.auxheat = None
        self.precond = None
        self.electric = None
        self.caralarm = None
        self.last_full_message = None
        self.geofence_events = GeofenceEvents()
        self.features = {}
        self.masterdata: dict[str, Any] = {}
        self.app_configuration: dict[str, Any] = {}
        self.entry_setup_complete = False
        self._update_listeners = set()
        self.sensors: set[str] = set()
        self.baumuster_description: str = ""
        self.features: dict[str, bool]
        self.geofence_events: GeofenceEvents
        self.geo_fencing_retry_counter: int = 0
        self.has_geofencing: bool = True
        self._data_collection_mode: str = "push"
        self._data_collection_mode_ts: float = 0

    @property
    def is_owner(self):
        """Get/set if the account is owner of the car."""
        return CarAttribute(self._is_owner, "VALID", None)

    @is_owner.setter
    def is_owner(self, value: bool):
        self._is_owner = value

    @property
    def full_updatemessages_received(self):
        """Get number of received full updates messages."""
        return CarAttribute(self.messages_received["f"], "VALID", None)

    @property
    def partital_updatemessages_received(self):
        """Get number of received partial updates messages."""
        return CarAttribute(self.messages_received["p"], "VALID", None)

    @property
    def last_message_received(self):
        """Get/Set last message received."""
        if self._last_message_received > 0:
            return CarAttribute(datetime.fromtimestamp(int(round(self._last_message_received / 1000))), "VALID", None)

        return CarAttribute(None, "NOT_RECEIVED", None)

    @last_message_received.setter
    def last_message_received(self, value):
        self._last_message_received = value

    @property
    def data_collection_mode(self):
        """Get/Set last message received."""
        return CarAttribute(self._data_collection_mode, "VALID", self._data_collection_mode_ts)

    @data_collection_mode.setter
    def data_collection_mode(self, value):
        self._data_collection_mode = value
        self._data_collection_mode_ts = datetime.now().timestamp()

    @property
    def last_command_type(self):
        """Get/Set last command type."""
        return CarAttribute(self._last_command_type, "VALID", self.last_command_time_stamp)

    @last_command_type.setter
    def last_command_type(self, value):
        self._last_command_type = value

    @property
    def last_command_state(self):
        """Get/Set last command state."""
        return CarAttribute(self._last_command_state, "VALID", self.last_command_time_stamp)

    @last_command_state.setter
    def last_command_state(self, value):
        self._last_command_state = value

    @property
    def last_command_error_code(self):
        """Get/Set last command error code."""
        return CarAttribute(self._last_command_error_code, "VALID", self.last_command_time_stamp)

    @last_command_error_code.setter
    def last_command_error_code(self, value):
        self._last_command_error_code = value

    @property
    def last_command_error_message(self):
        """Get/Set last command error message."""
        return CarAttribute(self._last_command_error_message, "VALID", self.last_command_time_stamp)

    @last_command_error_message.setter
    def last_command_error_message(self, value):
        self._last_command_error_message = value

    def add_update_listener(self, listener):
        """Add a listener for update notifications."""
        self._update_listeners.add(listener)

    def remove_update_callback(self, listener):
        """Remove a listener for update notifications."""
        self._update_listeners.discard(listener)

    def add_sensor(self, unique_id: str):
        """Add a sensor to the car."""
        if unique_id not in self.sensors:
            self.sensors.add(unique_id)

    def remove_sensor(self, unique_id: str):
        """Remove a sensor from the car."""
        if unique_id in self.sensors:
            self.sensors.remove(unique_id)

    def publish_updates(self):
        """Schedule call all registered callbacks."""
        for callback in self._update_listeners:
            callback()

    def check_capabilities(self, required_capabilities: list[str]) -> bool:
        """Check if the car has the required capabilities."""
        return any(self.features.get(capability) is True for capability in required_capabilities)


@dataclass(init=False)
class Tires:
    """Stores the Tires values at runtime."""

    name: str = "Tires"


@dataclass(init=False)
class Wipers:
    """Stores the Wiper values at runtime."""

    name: str = "Wipers"


@dataclass(init=False)
class Odometer:
    """Stores the Odometer values at runtime."""

    name: str = "Odometer"


@dataclass(init=False)
class RcpOptions:
    """Stores the RcpOptions values at runtime."""

    name: str = "RCP_Options"


@dataclass(init=False)
class Windows:
    """Stores the Windows values at runtime."""

    name: str = "Windows"


@dataclass(init=False)
class Doors:
    """Stores the Doors values at runtime."""

    name: str = "Doors"


@dataclass(init=False)
class Electric:
    """Stores the Electric values at runtime."""

    name: str = "Electric"


@dataclass(init=False)
class Auxheat:
    """Stores the Auxheat values at runtime."""

    name: str = "Auxheat"


@dataclass(init=False)
class Precond:
    """Stores the Precondining values at runtime."""

    name: str = "Precond"


@dataclass(init=False)
class BinarySensors:
    """Stores the BinarySensors values at runtime."""

    name: str = "BinarySensors"


@dataclass(init=False)
class RemoteStart:
    """Stores the RemoteStart values at runtime."""

    name: str = "RemoteStart"


@dataclass(init=False)
class CarAlarm:
    """Stores the CarAlarm values at runtime."""

    name: str = "CarAlarm"


@dataclass(init=False)
class Location:
    """Stores the Location values at runtime."""

    name: str = "Location"


@dataclass(init=False)
class GeofenceEvents:
    """Stores the geofence violation values at runtime."""

    last_event_type: CarAttribute | None = None
    last_event_timestamp: CarAttribute | None = None
    last_event_zone: CarAttribute | None = None
    name: str = "GeofenceEvents"

    def __post_init__(self):
        """Initialize mutable attributes."""
        self.events = []


@dataclass(init=False)
class CarAttribute:
    """Stores the CarAttribute values at runtime."""

    def __init__(self, value, retrievalstatus, timestamp, display_value=None, unit=None, sensor_created=False):
        """Initialize the instance."""
        self.value = value
        self.retrievalstatus = retrievalstatus
        self.timestamp = timestamp
        self.display_value = display_value
        self.unit = unit
        self.sensor_created = sensor_created


================================================
FILE: custom_components/mbapi2020/client.py
================================================
"""The MercedesME 2020 client."""

from __future__ import annotations

import asyncio
import datetime as dt
from datetime import datetime, timezone
import json
import logging
from pathlib import Path
import threading
import time
import traceback
import uuid

from aiohttp import ClientSession
from google.protobuf.json_format import MessageToJson

from custom_components.mbapi2020.proto import client_pb2
import custom_components.mbapi2020.proto.vehicle_commands_pb2 as pb2_commands
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import system_info

from .car import (
    AUX_HEAT_OPTIONS,
    BINARY_SENSOR_OPTIONS,
    DOOR_OPTIONS,
    ELECTRIC_OPTIONS,
    LOCATION_OPTIONS,
    ODOMETER_OPTIONS,
    PRE_COND_OPTIONS,
    TIRE_OPTIONS,
    WINDOW_OPTIONS,
    WIPER_OPTIONS,
    Auxheat,
    BinarySensors,
    Car,
    CarAlarm,
    CarAlarm_OPTIONS,
    CarAttribute,
    Doors,
    Electric,
    GeofenceEvents,
    Location,
    Odometer,
    Precond,
    Tires,
    Windows,
    Wipers,
)
from .const import (
    CONF_DEBUG_FILE_SAVE,
    CONF_EXCLUDED_CARS,
    CONF_FT_DISABLE_CAPABILITY_CHECK,
    CONF_PIN,
    DEFAULT_CACHE_PATH,
    DEFAULT_DOWNLOAD_PATH,
    DEFAULT_SOCKET_MIN_RETRY,
)
from .helper import LogHelper as loghelper
from .oauth import Oauth
from .webapi import WebApi
from .websocket import Websocket

LOGGER = logging.getLogger(__name__)

DEBUG_SIMULATE_PARTIAL_UPDATES_ONLY = False
GEOFENCING_MAX_RETRIES = 1


class Client:
    """define the client."""

    def __init__(
        self,
        hass: HomeAssistant,
        session: ClientSession,
        config_entry: ConfigEntry,
        region: str = "",
    ) -> None:
        """Initialize the client instance."""

        self.long_running_operation_active: bool = False
        self.ignition_states: dict[str, bool] = {}
        self.account_blocked: bool = False

        self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY
        self._hass = hass
        self._region = region
        self._on_dataload_complete = None
        self._dataload_complete_fired = False
        self._coordinator_ref = None
        self._disable_rlock = False
        self.__lock = None
        self._debug_save_path = self._hass.config.path(DEFAULT_CACHE_PATH)
        self.config_entry = config_entry
        self.session_id = str(uuid.uuid4()).upper()

        self._first_vepupdates_processed: bool = False
        self._vepupdates_timeout_seconds: int = 25
        self._vepupdates_time_first_message: datetime | None = None

        self.oauth: Oauth = Oauth(
            hass=self._hass,
            session=session,
            region=self._region,
            config_entry=config_entry,
        )
        self.oauth.session_id = self.session_id
        self.webapi: WebApi = WebApi(self._hass, session=session, oauth=self.oauth, region=self._region)
        self.webapi.session_id = self.session_id
        self.websocket: Websocket = Websocket(
            hass=self._hass,
            oauth=self.oauth,
            region=self._region,
            session_id=self.session_id,
            ignition_states=self.ignition_states,
        )

        self.cars: dict[str, Car] = {}

    @property
    def pin(self) -> str:
        """Return the security pin of an account."""
        if self.config_entry:
            if self.config_entry.options:
                return self.config_entry.options.get(CONF_PIN, None)
        return ""

    @property
    def excluded_cars(self):
        """Return the list of exluded/ignored VIN/FIN."""
        if self.config_entry:
            if self.config_entry.options:
                return self.config_entry.options.get(CONF_EXCLUDED_CARS, [])
        return []

    def on_data(self, data):
        """Define a handler to fire when the data is received."""

        msg_type = data.WhichOneof("msg")

        if self.websocket and self.websocket.ws_connect_retry_counter > 0:
            self.websocket.ws_connect_retry_counter = 0
            self.websocket.ws_connect_retry_counter_reseted = True

        if msg_type == "vepUpdate":  # VEPUpdate
            LOGGER.debug("vepUpdate")
            return None

        if msg_type == "vepUpdates":  # VEPUpdatesByVIN
            self._process_vep_updates(data)

            sequence_number = data.vepUpdates.sequence_number
            LOGGER.debug("vepUpdates Sequence: %s", sequence_number)
            ack_command = client_pb2.ClientMessage()
            ack_command.acknowledge_vep_updates_by_vin.sequence_number = sequence_number
            return ack_command

        if msg_type == "debugMessage":  # DebugMessage
            self._write_debug_output(data, "deb")
            if data.debugMessage:
                LOGGER.debug("debugMessage - Data: %s", data.debugMessage.message)

            return None

        if msg_type == "service_status_updates":
            self._write_debug_output(data, "ssu")
            sequence_number = data.service_status_updates.sequence_number
            LOGGER.debug("service_status_update Sequence: %s", sequence_number)
            ack_command = client_pb2.ClientMessage()
            ack_command.acknowledge_service_status_update.sequence_number = sequence_number
            return ack_command

        if msg_type == "user_data_update":
            self._write_debug_output(data, "udu")
            sequence_number = data.user_data_update.sequence_number
            LOGGER.debug("user_data_update Sequence: %s", sequence_number)
            ack_command = client_pb2.ClientMessage()
            ack_command.acknowledge_user_data_update.sequence_number = sequence_number
            return ack_command

        if msg_type == "user_vehicle_auth_changed_update":
            LOGGER.debug(
                "user_vehicle_auth_changed_update - Data: %s",
                MessageToJson(data, preserving_proto_field_name=True),
            )
            return None

        if msg_type == "user_picture_update":
            self._write_debug_output(data, "upu")
            sequence_number = data.user_picture_update.sequence_number
            LOGGER.debug("user_picture_update Sequence: %s", sequence_number)
            ack_command = client_pb2.ClientMessage()
            ack_command.acknowledge_user_picture_update.sequence_number = sequence_number
            return ack_command

        if msg_type == "user_pin_update":
            self._write_debug_output(data, "pin")
            sequence_number = data.user_pin_update.sequence_number
            LOGGER.debug("user_pin_update Sequence: %s", sequence_number)
            ack_command = client_pb2.ClientMessage()
            ack_command.acknowledge_user_pin_update.sequence_number = sequence_number
            return ack_command

        if msg_type == "vehicle_updated":
            self._write_debug_output(data, "vup")
            sequence_number = data.vehicle_updated.sequence_number
            LOGGER.debug("vehicle_updated Sequence: %s", sequence_number)
            ack_command = client_pb2.ClientMessage()
            ack_command.acknowledge_vehicle_updated.sequence_number = sequence_number
            return ack_command

        if msg_type == "preferred_dealer_change":
            self._write_debug_output(data, "pdc")
            sequence_number = data.preferred_dealer_change.sequence_number
            LOGGER.debug("preferred_dealer_change Sequence: %s", sequence_number)
            ack_command = client_pb2.ClientMessage()
            ack_command.acknowledge_preferred_dealer_change.sequence_number = sequence_number
            return ack_command

        if msg_type == "apptwin_command_status_updates_by_vin":
            LOGGER.debug(
                "apptwin_command_status_updates_by_vin - Data: %s",
                MessageToJson(data, preserving_proto_field_name=True),
            )

            self._process_apptwin_command_status_updates_by_vin(data)

            sequence_number = data.apptwin_command_status_updates_by_vin.sequence_number
            LOGGER.debug("apptwin_command_status_updates_by_vin: %s", sequence_number)
            ack_command = client_pb2.ClientMessage()
            ack_command.acknowledge_apptwin_command_status_update_by_vin.sequence_number = sequence_number
            return ack_command

        if msg_type == "apptwin_pending_command_request":
            self._process_assigned_vehicles(data)
            return "aa0100"

        if msg_type == "assigned_vehicles":
            self._write_debug_output(data, "asv")
            self._process_assigned_vehicles(data)
            return "ba0100"

        if msg_type == "data_change_event":
            self._write_debug_output(data, "dce")
            sequence_number = data.data_change_event.sequence_number
            LOGGER.debug("data_change_event: %s", sequence_number)
            ack_command = client_pb2.ClientMessage()
            ack_command.acknowledge_data_change_event.sequence_number = sequence_number
            return ack_command

        if msg_type == "vehicle_status_updates":
            self._write_debug_output(data, "vsu")
            LOGGER.info(
                "vehicle_status_updates - Data: %s",
                MessageToJson(data, preserving_proto_field_name=True),
            )
            sequence_number = data.vehicle_status_updates.sequence_number
            LOGGER.debug("vehicle_status_updates Sequence: %s", sequence_number)
            ack_command = client_pb2.ClientMessage()
            ack_command.acknowledge_vehicle_status_updates.sequence_number = sequence_number
            return ack_command

        self._write_debug_output(data, "unk")
        LOGGER.debug("Message Type not implemented: %s", msg_type)

        return None

    async def attempt_connect(self, callback_dataload_complete, coordinator_ref=None):
        """Attempt to connect to the socket."""
        LOGGER.debug("attempt_connect")

        self._on_dataload_complete = callback_dataload_complete
        self._coordinator_ref = coordinator_ref
        await self.websocket.async_connect(self.on_data)

    def _build_car(self, received_car_data, update_mode, is_rest_data=False):
        if received_car_data.get("vin") in self.excluded_cars:
            LOGGER.debug("CAR excluded: %s", loghelper.Mask_VIN(received_car_data.get("vin")))
            return

        if received_car_data.get("vin") not in self.cars:
            LOGGER.info(
                "Flow Problem - VepUpdate for unknown car: %s",
                loghelper.Mask_VIN(received_car_data.get("vin")),
            )

            current_car = Car(received_car_data.get("vin"))
            current_car.licenseplate = received_car_data.get("vin")
            self.cars[received_car_data.get("vin")] = current_car

        car: Car = self.cars.get(received_car_data.get("vin"), Car(received_car_data.get("vin")))

        car.messages_received.update("p" if update_mode else "f")
        car.last_message_received = int(round(time.time() * 1000))

        if not update_mode:
            car.last_full_message = received_car_data

        # Set data collection mode based on data source
        if is_rest_data:
            car.data_collection_mode = "pull"
        else:
            car.data_collection_mode = "push"

        # For REST data, create synthetic windowStatusOverall if missing
        if is_rest_data and received_car_data.get("attributes"):
            if "windowStatusOverall" not in received_car_data["attributes"]:
                self._create_synthetic_window_status_overall(received_car_data, car.finorvin)

        car.odometer = self._get_car_values(
            received_car_data,
            car.finorvin,
            Odometer() if not car.odometer else car.odometer,
            ODOMETER_OPTIONS,
            update_mode,
        )

        car.tires = self._get_car_values(
            received_car_data,
            car.finorvin,
            Tires() if not car.tires else car.tires,
            TIRE_OPTIONS,
            update_mode,
        )

        car.wipers = self._get_car_values(
            received_car_data,
            car.finorvin,
            Wipers() if not car.wipers else car.wipers,
            WIPER_OPTIONS,
            update_mode,
        )

        car.doors = self._get_car_values(
            received_car_data,
            car.finorvin,
            Doors() if not car.doors else car.doors,
            DOOR_OPTIONS,
            update_mode,
        )

        car.location = self._get_car_values(
            received_car_data,
            car.finorvin,
            Location() if not car.location else car.location,
            LOCATION_OPTIONS,
            update_mode,
        )

        car.binarysensors = self._get_car_values(
            received_car_data,
            car.finorvin,
            BinarySensors() if not car.binarysensors else car.binarysensors,
            BINARY_SENSOR_OPTIONS,
            update_mode,
        )

        car.windows = self._get_car_values(
            received_car_data,
            car.finorvin,
            Windows() if not car.windows else car.windows,
            WINDOW_OPTIONS,
            update_mode,
        )

        car.electric = self._get_car_values(
            received_car_data,
            car.finorvin,
            Electric() if not car.electric else car.electric,
            ELECTRIC_OPTIONS,
            update_mode,
        )

        car.auxheat = self._get_car_values(
            received_car_data,
            car.finorvin,
            Auxheat() if not car.auxheat else car.auxheat,
            AUX_HEAT_OPTIONS,
            update_mode,
        )

        car.precond = self._get_car_values(
            received_car_data,
            car.finorvin,
            Precond() if not car.precond else car.precond,
            PRE_COND_OPTIONS,
            update_mode,
        )

        car.caralarm = self._get_car_values(
            received_car_data,
            car.finorvin,
            CarAlarm() if not car.caralarm else car.caralarm,
            CarAlarm_OPTIONS,
            update_mode,
        )

        if not update_mode:
            car.entry_setup_complete = True

        self.cars[car.finorvin] = car

    def _get_car_values(self, car_detail, vin, class_instance, options, update):
        # Define handlers for specific options and the generic case
        option_handlers = {
            "max_soc": self._get_car_values_handle_max_soc,
            "chargeflap": self._get_car_values_handle_chargeflap,
            "chargeinletcoupler": self._get_car_values_handle_chargeinletcoupler,
            "chargeinletlock": self._get_car_values_handle_chargeinletlock,
            "chargePrograms": self._get_car_values_handle_chargePrograms,
            "chargingBreakClockTimer": self._get_car_values_handle_charging_break_clock_timer,
            "chargingPowerRestriction": self._get_car_values_handle_charging_power_restriction,
            "endofchargetime": self._get_car_values_handle_endofchargetime,
            "ignitionstate": self._get_car_values_handle_ignitionstate,
            "precondStatus": self._get_car_values_handle_precond_status,
            "temperature_points_frontLeft": self._get_car_values_handle_temperature_points,
            "temperature_points_frontRight": self._get_car_values_handle_temperature_points,
            "temperature_points_rearLeft": self._get_car_values_handle_temperature_points,
            "temperature_points_rearRight": self._get_car_values_handle_temperature_points,
        }

        if car_detail is None or not car_detail.get("attributes"):
            LOGGER.debug(
                "get_car_values %s has incomplete update data – attributes not found",
                loghelper.Mask_VIN(vin),
            )
            return class_instance

        for option in options:
            # Select the specific handler or the generic handler
            handler = option_handlers.get(option, self._get_car_values_handle_generic)

            curr_status = handler(car_detail, class_instance, option, update, vin)
            if curr_status is None:
                continue

            # Set the value only if the timestamp is newer
            # curr_timestamp = float(curr_status.timestamp or 0)
            # car_value_timestamp = float(self._get_car_value(class_instance, option, "ts", 0))
            # if curr_timestamp > car_value_timestamp:
            #     setattr(class_instance, option, curr_status)
            # elif curr_timestamp < car_value_timestamp:
            #     LOGGER.warning(
            #         "get_car_values %s received older attribute data for %s. Ignoring value.",
            #         loghelper.Mask_VIN(vin),
            #         option,
            #     )
            setattr(class_instance, option, curr_status)
        return class_instance

    def _get_car_values_handle_generic(self, car_detail, class_instance, option, update, vin: str):
        curr = car_detail.get("attributes", {}).get(option)
        if curr:
            # Simplify value extraction by checking for existing keys
            value = next(
                (curr[key] for key in ("value", "int_value", "double_value", "bool_value") if key in curr),
                0,
            )
            status = curr.get("status", "VALID")
            time_stamp = curr.get("timestamp", 0)
            curr_display_value = curr.get("display_value")

            unit_keys = [
                "distance_unit",
                "ratio_unit",
                "clock_hour_unit",
                "gas_consumption_unit",
                "pressure_unit",
                "electricity_consumption_unit",
                "combustion_consumption_unit",
                "speed_unit",
            ]
            unit = next((curr[key] for key in unit_keys if key in curr), None)

            return CarAttribute(
                value=value,
                retrievalstatus=status,
                timestamp=time_stamp,
                display_value=curr_display_value,
                unit=unit,
            )

        if not update:
            # Set status for non-existing values when no update occurs
            return CarAttribute(0, 4, 0)

        return None

    def _get_car_values_handle_max_soc(
        self, car_detail, class_instance, option, update, vin: str, use_last_full_message: bool = False
    ):
        # Handle the case when the selected charge program changed but chargePrograms is not available in the update message.
        if not use_last_full_message:
            attributes = car_detail.get("attributes", {})
            charge_programs = attributes.get("chargePrograms")
            if not charge_programs:
                if not attributes.get("selectedChargeProgram"):
                    return None

                return self._get_car_values_handle_max_soc(
                    car_detail, class_instance, option, update, vin, use_last_full_message=True
                )
        else:
            current_car = self.cars.get(vin)
            if not current_car or not current_car.last_full_message:
                LOGGER.debug(
                    "get_car_values_handle_max_soc - No last_full_message found for car %s",
                    loghelper.Mask_VIN(vin),
                )
                return None
            car_detail = current_car.last_full_message or car_detail
            attributes = car_detail.get("attributes", {})
            charge_programs = attributes.get("chargePrograms")
            if not charge_programs:
                return None

        time_stamp = charge_programs.get("timestamp", 0)
        charge_programs_value = charge_programs.get("charge_programs_value", {})
        charge_program_parameters = charge_programs_value.get("charge_program_parameters", [])

        selected_program_index = int(self._get_car_value(class_instance, "selectedChargeProgram", "value", 0))

        # Ensure the selected index is within bounds
        if 0 <= selected_program_index < len(charge_program_parameters):
            program_parameters = charge_program_parameters[selected_program_index]
            max_soc = program_parameters.get("max_soc")
            if max_soc is not None:
                return CarAttribute(
                    value=max_soc,
                    retrievalstatus="VALID",
                    timestamp=time_stamp,
                    display_value=max_soc,
                    unit="PERCENT",
                )

        return None

    def _get_car_values_handle_chargeflap(self, car_detail, class_instance, option, update, vin: str):
        attributes = car_detail.get("attributes", {})
        curr = attributes.get("chargeFlaps")
        if not curr:
            return None

        charge_flaps_value = curr.get("charge_flaps", {})
        values = charge_flaps_value.get("entries", [])
        if not values:
            return None

        status = curr.get("status", "VALID")
        time_stamp = curr.get("timestamp", 0)
        value = values[0].get("position_state", None)

        if value is None:
            return None

        if value == "CHARGE_FLAPS_POSITION_STATE_OPEN":
            value = "open"
        elif value == "CHARGE_FLAPS_POSITION_STATE_CLOSED":
            value = "closed"
        elif value == "CHARGE_FLAPS_POSITION_STATE_FLAP_PRESSED":
            value = "pressed"
        elif value == "CHARGE_FLAPS_POSITION_STATE_UNKNOWN":
            value = STATE_UNKNOWN
        else:
            value = STATE_UNKNOWN
            LOGGER.debug(
                "Unknown chargeFlaps position_state value: %s. Please report this value via an github issue.", value
            )

        return CarAttribute(
            value=value,
            retrievalstatus=status,
            timestamp=time_stamp,
            display_value=None,
            unit=None,
        )

    def _get_car_values_handle_chargeinletcoupler(self, car_detail, class_instance, option, update, vin: str):
        attributes = car_detail.get("attributes", {})
        curr = attributes.get("chargeInlets")
        if not curr:
            return None

        values = curr.get("charge_inlets", {}).get("entries", [])
        if not values:
            return None

        status = curr.get("status", "VALID")
        time_stamp = curr.get("timestamp", 0)
        value = values[0].get("coupler_state", None)

        if value is None:
            return None

        if value == "CHARGE_INLETS_COUPLER_STATE_PLUGGED":
            value = "plugged"
        elif value == "CHARGE_INLETS_COUPLER_STATE_VEHICLE_PLUGGED":
            value = "vehicle plugged"
        elif value == "CHARGE_INLETS_COUPLER_STATE_VEHICLE_NOT_PLUGGED":
            value = "vehicle not plugged"
        elif value == "CHARGE_INLETS_COUPLER_STATE_DEFECT":
            value = "defect"
        elif value == "CHARGE_INLETS_COUPLER_STATE_UNKNOWN":
            value = STATE_UNKNOWN
        else:
            value = STATE_UNKNOWN
            LOGGER.debug(
                "Unknown chargeInlets coupler_state value: %s. Please report this value via an github issue.", value
            )

        return CarAttribute(
            value=value,
            retrievalstatus=status,
            timestamp=time_stamp,
            display_value=None,
            unit=None,
        )

    def _get_car_values_handle_chargeinletlock(self, car_detail, class_instance, option, update, vin: str):
        attributes = car_detail.get("attributes", {})
        curr = attributes.get("chargeInlets")
        if not curr:
            return None

        values = curr.get("charge_inlets", {}).get("entries", [])
        if not values:
            return None

        status = curr.get("status", "VALID")
        time_stamp = curr.get("timestamp", 0)
        value = values[0].get("lock_state", None)

        if value is None:
            return None

        if value == "CHARGE_INLETS_LOCK_STATE_UNLOCKED":
            value = "unlocked"
        elif value == "CHARGE_INLETS_LOCK_STATE_LOCKED":
            value = "locked"
        elif value == "CHARGE_INLETS_LOCK_STATE_LOCK_STATE_NOT_CLEAR":
            value = "state not clear"
        elif value == "CHARGE_INLETS_LOCK_STATE_NOT_AVAILABLE":
            value = "state not available"
        elif value == "CHARGE_INLETS_LOCK_STATE_UNKNOWN":
            value = STATE_UNKNOWN
        else:
            value = STATE_UNKNOWN
            LOGGER.debug(
                "Unknown chargeInlets lock_state value: %s. Please report this value via an github issue.", value
            )

        return CarAttribute(
            value=value,
            retrievalstatus=status,
            timestamp=time_stamp,
            display_value=None,
            unit=None,
        )

    def _get_car_values_handle_chargePrograms(self, car_detail, class_instance, option, update, vin: str):
        attributes = car_detail.get("attributes", {})
        curr = attributes.get(option)
        if not curr:
            return None

        charge_programs_value = curr.get("charge_programs_value", {})
        value = charge_programs_value.get("charge_program_parameters", [])
        if not value:
            return None

        status = curr.get("status", "VALID")
        time_stamp = curr.get("timestamp", 0)

        return CarAttribute(
            value=value,
            retrievalstatus=status,
            timestamp=time_stamp,
            display_value=None,
            unit=None,
        )

    def _get_car_values_handle_charging_power_restriction(self, car_detail, class_instance, option, update, vin: str):
        attributes = car_detail.get("attributes", {})
        curr = attributes.get("chargingPowerRestriction")
        if not curr:
            return None

        charge_flaps_value = curr.get("charging_power_restrictions", {})
        values = charge_flaps_value.get("charging_power_restriction", [])
        if not values:
            return None

        status = curr.get("status", "VALID")
        time_stamp = curr.get("timestamp", 0)

        if len(values) == 0:
            return None

        value = ", ".join(item.replace("CHARGING_POWER_RESTRICTION_", "") for item in values)

        return CarAttribute(
            value=value,
            retrievalstatus=status,
            timestamp=time_stamp,
            display_value=None,
            unit=None,
        )

    def _get_car_values_handle_endofchargetime(self, car_detail, class_instance, option, update, vin: str):
        # Beginning with CLA 2025 charge end time is part of chargingPredictionMaxSoc

        attributes = car_detail.get("attributes", {})

        chargingPredictionMaxSoc = attributes.get("chargingPredictionMaxSoc", {})
        charging_prediction_soc = chargingPredictionMaxSoc.get("charging_prediction_soc", {})
        predicted_end_time = charging_prediction_soc.get("predicted_end_time", None)
        if predicted_end_time is not None:
            status = chargingPredictionMaxSoc.get("status", "VALID")
            time_stamp = chargingPredictionMaxSoc.get("timestamp", 0)

            if isinstance(predicted_end_time, datetime):
                value = predicted_end_time
            elif isinstance(predicted_end_time, str):
                try:
                    value = datetime.strptime(predicted_end_time, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
                except Exception:
                    value = None
            else:
                value = None
            if value is not None:
                return CarAttribute(
                    value=value,
                    retrievalstatus=status,
                    timestamp=time_stamp,
                    display_value=value,
                    unit=None,
                )

        # if chargingPredictionMaxSoc is not available and update is false (not a full_message), we want to check if the car is >= CLA2025 and create the data structure with value unknown
        # endofchargetime is not present for these cars
        if not update and not predicted_end_time:
            current_car = self.cars.get(vin)
            if not attributes.get("endofchargetime") and not (
                current_car
                and current_car.last_full_message
                and current_car.last_full_message.get("attributes", {}).get("endofchargetime")
            ):
                return CarAttribute(
                    value=STATE_UNKNOWN,
                    retrievalstatus="VALID",
                    timestamp=datetime.now().timestamp(),
                    display_value=STATE_UNKNOWN,
                    unit=None,
                )

        # Older cars have two attributes endofchargetime and endofChargeTimeWeekday
        # endofchargetime is in minutes after midnight
        try:
            endofchargetime = attributes.get("endofchargetime", {})
            if not endofchargetime:
                return None
            time_stamp = endofchargetime.get("timestamp", 0)
            end_time_value = endofchargetime.get("int_value", None)
            status = endofchargetime.get("status", "VALID")
            if end_time_value is None and status == 3:
                return CarAttribute(
                    value=STATE_UNKNOWN,
                    retrievalstatus=status,
                    timestamp=time_stamp,
                    display_value=STATE_UNKNOWN,
                    unit=None,
                )

            if end_time_value is None:
                LOGGER.warning(
                    "get_car_values_handle_endofchargetime - endofChargeTime value is None for car %s",
                    loghelper.Mask_VIN(vin),
                )
                return None

            # endofChargeTimeWeekday is sometimes not present in the update message, we need to get it from the last full message then
            if "endofChargeTimeWeekday" not in attributes:
                current_car = self.cars.get(vin)
                if not current_car or not current_car.last_full_message:
                    LOGGER.debug(
                        "get_car_values_handle_endofchargetime - No last_full_message found for car %s",
                        loghelper.Mask_VIN(vin),
                    )
                    return None
                car_detail = current_car.last_full_message
                attributes = car_detail.get("attributes", {})

            local_tz = dt.datetime.now().astimezone().tzinfo
            end_weekday_attr = attributes.get("endofChargeTimeWeekday", {})
            end_weekday_value = end_weekday_attr.get("int_value", None)
            if end_weekday_value is None:
                # Wenn kein Wochentag vorhanden ist (sehr alte elek. modelle), aus end_time_value ableiten:
                # Ist die Uhrzeit bereits vergangen -> Wochentag von morgen, sonst von heute.
                now = dt.datetime.now(local_tz)
                hour = int(end_time_value) // 60
                minute = int(end_time_value) % 60
                target_dt_today = dt.datetime(now.year, now.month, now.day, hour, minute, tzinfo=local_tz)
                if target_dt_today < now:
                    end_weekday_value = (now + dt.timedelta(days=1)).weekday()
                else:
                    end_weekday_value = now.weekday()

            # Calculate the next occurrence of the given weekday and time in local timezone
            now = dt.datetime.now(local_tz)
            # Python's weekday: Monday=0
            target_weekday = float(end_weekday_value) % 7
            # Calculate days until next target_weekday
            days_ahead = (target_weekday - now.weekday()) % 7

            target_date = now + dt.timedelta(days=days_ahead)
            hour = int(end_time_value) // 60
            minute = int(end_time_value) % 60
            dt_with_time = dt.datetime(
                target_date.year, target_date.month, target_date.day, hour, minute, tzinfo=local_tz
            )

            return CarAttribute(
                value=dt_with_time,
                retrievalstatus=status,
                timestamp=time_stamp,
                display_value=dt_with_time.isoformat(),
                unit=None,
            )
        except Exception as e:
            LOGGER.error(
                "Error processing endofchargetime for car %s: %s, %s",
                loghelper.Mask_VIN(vin),
                e,
                traceback.format_exc(),
            )
            return None

    def _get_car_values_handle_charging_break_clock_timer(self, car_detail, class_instance, option, update, vin: str):
        attributes = car_detail.get("attributes", {})
        curr = attributes.get(option)
        if not curr:
            return None

        charging_timer_value = curr.get("chargingbreak_clocktimer_value", {})
        value = charging_timer_value.get("chargingbreak_clocktimer_entry")
        if value is None:
            return None

        status = curr.get("status", "VALID")
        time_stamp = curr.get("timestamp", 0)
        curr_display_value = curr.get("display_value")

        return CarAttribute(
            value=value,
            retrievalstatus=status,
            timestamp=time_stamp,
            display_value=curr_display_value,
            unit=None,
        )

    def _get_car_values_handle_ignitionstate(self, car_detail, class_instance, option, update, vin: str):
        value = self._get_car_values_handle_generic(car_detail, class_instance, option, update, vin)
        if value:
            vin = car_detail.get("vin")
            self.ignition_states[vin] = value.value == "4"
            if vin in self.excluded_cars:
                self.ignition_states[vin] = False

        return value

    def _get_car_values_handle_precond_status(self, car_detail, class_instance, option, update, vin: str):
        attributes = car_detail.get("attributes", {})

        # Retrieve attributes with defaults to handle missing keys
        precond_now_attr = attributes.get("precondNow", {})
        precond_active_attr = attributes.get("precondActive", {})
        precond_operating_mode_attr = attributes.get("precondOperatingMode", {})

        # Extract values and convert to boolean where necessary
        precond_now_value = precond_now_attr.get("bool_value", False)
        precond_active_value = precond_active_attr.get("bool_value", False)
        precond_operating_mode_value = precond_operating_mode_attr.get("int_value", 0)
        precond_operating_mode_bool = int(precond_operating_mode_value) > 0

        # Calculate precondStatus
        value = precond_now_value or precond_active_value or precond_operating_mode_bool

        # Determine if any of the attributes are present
        if precond_now_attr or precond_active_attr or precond_operating_mode_attr:
            status = "VALID"
            time_stamp = max(
                int(precond_now_attr.get("timestamp", 0)),
                int(precond_active_attr.get("timestamp", 0)),
                int(precond_operating_mode_attr.get("timestamp", 0)),
            )
            return CarAttribute(
                value=value,
                retrievalstatus=status,
                timestamp=time_stamp,
                display_value=str(value),
                unit=None,
            )

        if not update:
            # Set status for non-existing values when no update occurs
            return CarAttribute(False, 4, 0)

        return None

    def _get_car_values_handle_temperature_points(self, car_detail, class_instance, option: str, update, vin: str):
        curr_zone = option.replace("temperature_points_", "")
        attributes = car_detail.get("attributes", {})
        temperaturePoints = attributes.get("temperaturePoints")
        if not temperaturePoints:
            return None

        time_stamp = temperaturePoints.get("timestamp", 0)
        temperature_points_value = temperaturePoints.get("temperature_points_value", {})
        temperature_points = temperature_points_value.get("temperature_points", [])

        for point in temperature_points:
            if point.get("zone", "") == curr_zone:
                return CarAttribute(
                    value=point.get("temperature", 0),
                    retrievalstatus="VALID",
                    timestamp=time_stamp,
                    display_value=point.get("temperature_display_value"),
                    unit=temperaturePoints.get("temperature_unit", None),
                )

        return None

    def _create_synthetic_window_status_overall(self, car_data, vin):
        """Create a synthetic windowStatusOverall based on individual window statuses for REST data."""
        if not car_data.get("attributes"):
            return

        # Debug: Log all available window-related attributes
        # window_attrs_found = [key for key in car_data["attributes"].keys() if "window" in key.lower()]
        # if window_attrs_found:
        #     LOGGER.debug("Available window attributes for %s: %s", loghelper.Mask_VIN(vin), window_attrs_found)
        # else:
        #     LOGGER.debug("No window attributes found in REST data for %s", loghelper.Mask_VIN(vin))

        # Define main window status attributes to check
        main_window_attrs = [
            "windowstatusfrontleft",
            "windowstatusfrontright",
            "windowstatusrearleft",
            "windowstatusrearright",
        ]

        # Check individual window statuses
        window_statuses = []
        latest_timestamp = 0

        for attr_name in main_window_attrs:
            if attr_name in car_data["attributes"]:
                attr_data = car_data["attributes"][attr_name]
                value = attr_data.get("int_value", 0)
                timestamp = attr_data.get("timestamp", 0)

                # Convert timestamp to int for comparison
                try:
                    timestamp = int(timestamp) if timestamp else 0
                    latest_timestamp = max(latest_timestamp, timestamp)
                except (ValueError, TypeError):
                    pass

                # Add to window statuses if value is available
                if value is not None:
                    window_statuses.append(value)

        # Calculate overall status based on individual windows
        # If we have valid window statuses, determine overall state
        if window_statuses:
            # Assume "CLOSED" = 0, "OPEN" = 1 or similar numeric values
            # If all windows are closed (0), overall should be "CLOSED"
            # If any window is open (>0), overall should be "OPEN"
            try:
                numeric_statuses = [int(status) for status in window_statuses if status is not None]
                if numeric_statuses:
                    overall_value = "OPEN" if any(status != 2 for status in numeric_statuses) else "CLOSED"
                else:
                    overall_value = "CLOSED"  # Default to closed if no valid data
            except (ValueError, TypeError):
                # If values are not numeric, try string comparison
                overall_value = "CLOSED"
        else:
            # No individual window data available, default to CLOSED
            overall_value = "CLOSED"

        # Create the synthetic windowStatusOverall attribute
        car_data["attributes"]["windowStatusOverall"] = {
            "timestamp": str(latest_timestamp) if latest_timestamp > 0 else "0",
            "bool_value": overall_value == "CLOSED",
            "status": "VALID",  # Use same status as other synthetic attributes
            "timestamp_in_ms": str(latest_timestamp * 1000 + 223),
        }

        LOGGER.debug(
            "Created synthetic windowStatusOverall for %s: %s (based on %d individual windows)",
            loghelper.Mask_VIN(vin),
            overall_value,
            len(window_statuses),
        )

    def _get_car_value(self, class_instance, object_name, attrib_name, default_value):
        return getattr(
            getattr(class_instance, object_name, default_value),
            attrib_name,
            default_value,
        )

    def _process_rest_vep_update(self, data):
        LOGGER.debug("Start _process_rest_vep_update")

        self._write_debug_output(data, "rfu")
        # Don't understand the protobuf dict errors --> convert to json
        vep_json = json.loads(MessageToJson(data, preserving_proto_field_name=True))

        # Check if this is a nested vepUpdates structure or a direct VIN structure
        if "vepUpdates" in vep_json and "updates" in vep_json["vepUpdates"]:
            # This is a multi-car update structure
            cars = vep_json["vepUpdates"]["updates"]
            for vin in cars:
                if vin in self.excluded_cars:
                    continue
                current_car = cars.get(vin)
                if current_car:
                    self._build_car(current_car, update_mode=False, is_rest_data=True)
        else:
            # This is a single car structure
            vin = vep_json.get("vin", None)
            if not vin:
                LOGGER.debug("No VIN found in VEPUpdate data: %s", vep_json)
                return
            self._build_car(vep_json, update_mode=False, is_rest_data=True)

        if not self._first_vepupdates_processed:
            self._vepupdates_time_first_message = datetime.now()
            self._first_vepupdates_processed = True
            self._hass.loop.call_later(
                30, lambda: self._hass.async_add_executor_job(self._safe_create_on_dataload_complete_task)
            )

        self._build_car(vep_json, update_mode=False)
        if self._dataload_complete_fired:
            current_car = self.cars.get(vin)
            current_car.data_collection_mode = "pull"

            if current_car:
                current_car.publish_updates()

        if not self._dataload_complete_fired:
            fire_complete_event: bool = True
            for car in self.cars.values():
                if not car.entry_setup_complete:
                    fire_complete_event = False
                LOGGER.debug(
                    "_process_vep_updates - %s - complete: %s - %s",
                    loghelper.Mask_VIN(car.finorvin),
                    car.entry_setup_complete,
                    car.messages_received,
                )
            if fire_complete_event:
                LOGGER.debug("_process_vep_updates - all completed - fire event: _on_dataload_complete")
                self._hass.async_create_task(self._on_dataload_complete())
                self._dataload_complete_fired = True

    def _process_vep_updates(self, data):
        LOGGER.debug("Start _process_vep_updates")

        self._write_debug_output(data, "vep")

        # Don't understand the protobuf dict errors --> convert to json
        vep_json = json.loads(MessageToJson(data, preserving_proto_field_name=True))
        cars = vep_json["vepUpdates"]["updates"]

        if not self._first_vepupdates_processed:
            self._vepupdates_time_first_message = datetime.now()
            self._first_vepupdates_processed = True
            self._hass.loop.call_later(
                30, lambda: self._hass.async_add_executor_job(self._safe_create_on_dataload_complete_task)
            )

        for vin in cars:
            if vin in self.excluded_cars:
                continue

            current_car = cars.get(vin)

            if DEBUG_SIMULATE_PARTIAL_UPDATES_ONLY and current_car.get("full_update", False) is True:
                current_car["full_update"] = False
                LOGGER.debug(
                    "DEBUG_SIMULATE_PARTIAL_UPDATES_ONLY mode. %s",
                    loghelper.Mask_VIN(vin),
                )

            if current_car.get("full_update") is True:
                LOGGER.debug("Full Update. %s", loghelper.Mask_VIN(vin))
                if not self._disable_rlock:
                    with self.__lock:
                        self._build_car(current_car, update_mode=False)
                else:
                    self._build_car(current_car, update_mode=False)

            else:
                LOGGER.debug("Partial Update. %s", loghelper.Mask_VIN(vin))
                if not self._disable_rlock:
                    with self.__lock:
                        self._build_car(current_car, update_mode=True)
                else:
                    self._build_car(current_car, update_mode=True)

            if self._dataload_complete_fired:
                current_car = self.cars.get(vin)
                current_car.data_collection_mode = "push"

                if current_car:
                    current_car.publish_updates()

                    # Check for newly available sensors after vep_update
                    if self._coordinator_ref:
                        self._hass.async_create_task(self._coordinator_ref.check_missing_sensors_for_vin(vin))

        if not self._dataload_complete_fired:
            fire_complete_event: bool = True
            for car in self.cars.values():
                if not car.entry_setup_complete:
                    fire_complete_event = False
                LOGGER.debug(
                    "_process_vep_updates - %s - complete: %s - %s",
                    loghelper.Mask_VIN(car.finorvin),
                    car.entry_setup_complete,
                    car.messages_received,
                )
            if fire_complete_event:
                LOGGER.debug("_process_vep_updates - all completed - fire event: _on_dataload_complete")
                self._hass.async_create_task(self._on_dataload_complete())
                self._dataload_complete_fired = True

    def _process_assigned_vehicles(self, data):
        if not self._dataload_complete_fired:
            LOGGER.debug("Start _process_assigned_vehicles")

            # self._write_debug_output(data, "asv")

            if not self._disable_rlock:
                with self.__lock:
                    for vin in data.assigned_vehicles.vins:
                        if vin in self.excluded_cars:
                            continue

                        _car = self.cars.get(vin)

                        if _car is None:
                            current_car = Car(vin)
                            current_car.licenseplate = vin
                            self.cars[vin] = current_car
            else:
                for vin in data.assigned_vehicles.vins:
                    if vin in self.excluded_cars:
                        continue

                    _car = self.cars.get(vin)

                    if _car is None:
                        current_car = Car(vin)
                        current_car.licenseplate = vin
                        self.cars[vin] = current_car

            current_time = int(round(time.time() * 1000))
            for key, value in self.cars.items():
                LOGGER.debug(
                    "_process_assigned_vehicles - %s - %s - %s - %s",
                    loghelper.Mask_VIN(key),
                    value.entry_setup_complete,
                    value.messages_received,
                    current_time - value.last_message_received.value.timestamp(),
                )

    def _process_apptwin_command_status_updates_by_vin(self, data):
        LOGGER.debug("Start _process_assigned_vehicles")

        # Don't understand the protobuf dict errors --> convert to json
        apptwin_json = json.loads(MessageToJson(data, preserving_proto_field_name=True))

        self._write_debug_output(data, "acr")

        if apptwin_json["apptwin_command_status_updates_by_vin"]:
            if apptwin_json["apptwin_command_status_updates_by_vin"]["updates_by_vin"]:
                car = list(apptwin_json["apptwin_command_status_updates_by_vin"]["updates_by_vin"].keys())[0]
                car = apptwin_json["apptwin_command_status_updates_by_vin"]["updates_by_vin"][car]
                vin = car.get("vin", None)
                if vin:
                    if car["updates_by_pid"]:
                        command = list(car["updates_by_pid"].keys())[0]
                        command = car["updates_by_pid"][command]
                        if command:
                            command_type = command.get("type")
                            command_state = command.get("state")
                            command_error_code = ""
                            command_error_message = ""
                            if command.get("errors"):
                                for err in command["errors"]:
                                    command_error_code = err.get("code")
                                    command_error_message = err.get("message")
                                    LOGGER.warning(
                                        "Car action: %s failed. error_code: %s, error_message: %s",
                                        command_type,
                                        command_error_code,
                                        command_error_message,
                                    )

                            current_car = self.cars.get(vin)

                            if current_car:
                                current_car.last_command_type = command_type
                                current_car.last_command_state = command_state
                                current_car.last_command_error_code = command_error_code
                                current_car.last_command_error_message = command_error_message
                                current_car.last_command_time_stamp = command.get("timestamp_in_ms", 0)

                                current_car.publish_updates()

    async def charge_program_configure(self, vin: str, program: int, max_soc: None | int = None) -> None:
        """Send the selected charge program to the car."""
        if not self._is_car_feature_available(vin, "CHARGE_PROGRAM_CONFIGURE"):
            raise ServiceValidationError(
                "Can't set the charge program. Feature CHARGE_PROGRAM_CONFIGURE not availabe for this car."
            )

        LOGGER.debug("Start charge_program_configure")
        message = client_pb2.ClientMessage()
        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())
        charge_programm = pb2_commands.ChargeProgramConfigure()
        charge_programm.charge_program = program
        if max_soc is not None:
            charge_programm.max_soc.value = max_soc
        else:
            try:
                current_charge_programs = getattr(self.cars.get(vin).electric, "chargePrograms", None)
                if current_charge_programs:
                    charge_programm.max_soc.value = current_charge_programs.value[program].get("max_soc")
            except (KeyError, IndexError, TypeError, AttributeError) as err:
                LOGGER.warning(
                    "charge_program_configure - Error: %s - %s",
                    err,
                    getattr(self.cars.get(vin).electric, "chargePrograms", None),
                )

        message.commandRequest.charge_program_configure.CopyFrom(charge_programm)
        LOGGER.debug(
            "charge_program_configure - vin: %s - program: %s - max_soc: %s",
            loghelper.Mask_VIN(vin),
            charge_programm.charge_program,
            charge_programm.max_soc.value,
        )
        await self.execute_car_command(message)
        LOGGER.debug("End charge_program_configure for vin %s", loghelper.Mask_VIN(vin))

    async def charging_break_clocktimer_configure(
        self,
        vin: str,
        status_t1: str,
        start_t1: datetime.timedelta,
        stop_t1: datetime.timedelta,
        status_t2: str,
        start_t2: datetime.timedelta,
        stop_t2: datetime.timedelta,
        status_t3: str,
        start_t3: datetime.timedelta,
        stop_t3: datetime.timedelta,
        status_t4: str,
        start_t4: datetime.timedelta,
        stop_t4: datetime.timedelta,
    ) -> None:
        """Send the charging_break_clocktimer_configure command to the car."""
        if not self._is_car_feature_available(vin, "chargingClockTimer"):
            LOGGER.warning(
                "Can't send charging_break_clocktimer_configure for car %s. Feature not availabe for this car.",
                loghelper.Mask_VIN(vin),
            )
            return

        message = client_pb2.ClientMessage()

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())
        config = pb2_commands.ChargingBreakClocktimerConfigure()
        entry_set: bool = False

        if status_t1 and start_t1 is not None and stop_t1 is not None and status_t1 in ("active", "inactive"):
            t1 = config.chargingbreak_clocktimer_configure_entry.add()
            t1.timerId = 1
            if status_t1 == "active":
                t1.action = pb2_commands.ChargingBreakClockTimerEntryStatus.ACTIVE
            else:
                t1.action = pb2_commands.ChargingBreakClockTimerEntryStatus.INACTIVE

            t1.startTimeHour = start_t1.seconds // 3600
            t1.startTimeMinute = (start_t1.seconds % 3600) // 60
            t1.endTimeHour = stop_t1.seconds // 3600
            t1.endTimeMinute = (stop_t1.seconds % 3600) // 60
            entry_set = True

        if status_t2 and start_t2 is not None and stop_t2 is not None and status_t2 in ("active", "inactive"):
            t2 = config.chargingbreak_clocktimer_configure_entry.add()
            t2.timerId = 2
            if status_t2 == "active":
                t2.action = pb2_commands.ChargingBreakClockTimerEntryStatus.ACTIVE
            else:
                t2.action = pb2_commands.ChargingBreakClockTimerEntryStatus.INACTIVE

            t2.startTimeHour = start_t2.seconds // 3600
            t2.startTimeMinute = (start_t2.seconds % 3600) // 60
            t2.endTimeHour = stop_t2.seconds // 3600
            t2.endTimeMinute = (stop_t2.seconds % 3600) // 60
            entry_set = True

        if status_t3 and start_t3 is not None and stop_t3 is not None and status_t3 in ("active", "inactive"):
            t3 = config.chargingbreak_clocktimer_configure_entry.add()
            t3.timerId = 3
            if status_t3 == "active":
                t3.action = pb2_commands.ChargingBreakClockTimerEntryStatus.ACTIVE
            else:
                t3.action = pb2_commands.ChargingBreakClockTimerEntryStatus.INACTIVE

            t3.startTimeHour = start_t3.seconds // 3600
            t3.startTimeMinute = (start_t3.seconds % 3600) // 60
            t3.endTimeHour = stop_t3.seconds // 3600
            t3.endTimeMinute = (stop_t3.seconds % 3600) // 60
            entry_set = True

        if status_t4 and start_t4 is not None and stop_t4 is not None and status_t4 in ("active", "inactive"):
            t4 = config.chargingbreak_clocktimer_configure_entry.add()
            t4.timerId = 4
            if status_t4 == "active":
                t4.action = pb2_commands.ChargingBreakClockTimerEntryStatus.ACTIVE
            else:
                t4.action = pb2_commands.ChargingBreakClockTimerEntryStatus.INACTIVE

            t4.startTimeHour = start_t4.seconds // 3600
            t4.startTimeMinute = (start_t4.seconds % 3600) // 60
            t4.endTimeHour = stop_t4.seconds // 3600
            t4.endTimeMinute = (stop_t4.seconds % 3600) // 60
            entry_set = True

        if entry_set:
            message.commandRequest.chargingbreak_clocktimer_configure.CopyFrom(config)
            await self.execute_car_command(message)
            LOGGER.info(
                "End charging_break_clocktimer_configure for vin %s",
                loghelper.Mask_VIN(vin),
            )
        else:
            LOGGER.info(
                "End charging_break_clocktimer_configure for vin %s - No actions",
                loghelper.Mask_VIN(vin),
            )

        return

    async def doors_unlock(self, vin: str, pin: str = ""):
        """Send the doors unlock command to the car."""
        if not self._is_car_feature_available(vin, "DOORS_UNLOCK"):
            LOGGER.warning(
                "Can't unlock car %s. Feature not availabe for this car.",
                loghelper.Mask_VIN(vin),
            )
            return

        if pin and pin.strip():
            LOGGER.debug("Start unlock with user provided pin")
            await self.doors_unlock_with_pin(vin, pin)
            return

        if not self.pin:
            LOGGER.warning(
                "Can't unlock car %s. PIN not set. Please set the PIN -> Integration, Options ",
                loghelper.Mask_VIN(vin),
            )
            return

        await self.doors_unlock_with_pin(vin, self.pin)

    async def doors_unlock_with_pin(self, vin: str, pin: str):
        """Send the doors unlock command to the car."""
        LOGGER.info("Start Doors_unlock_with_pin for vin %s", loghelper.Mask_VIN(vin))

        if not self._is_car_feature_available(vin, "DOORS_UNLOCK"):
            LOGGER.warning(
                "Can't unlock car %s. Feature not availabe for this car.",
                loghelper.Mask_VIN(vin),
            )
            return

        message = client_pb2.ClientMessage()

        if not pin:
            LOGGER.warning("Can't unlock car %s. Pin is required.", loghelper.Mask_VIN(vin))
            return

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())
        message.commandRequest.doors_unlock.pin = pin

        await self.execute_car_command(message)
        LOGGER.info("End Doors_unlock for vin %s", loghelper.Mask_VIN(vin))

    async def doors_lock(self, vin: str):
        """Send the doors lock command to the car."""
        LOGGER.info("Start Doors_lock for vin %s", loghelper.Mask_VIN(vin))

        if not self._is_car_feature_available(vin, "DOORS_LOCK"):
            LOGGER.warning(
                "Can't lock car %s. Feature not availabe for this car.",
                loghelper.Mask_VIN(vin),
            )
            return

        message = client_pb2.ClientMessage()

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())
        message.commandRequest.doors_lock.doors.extend([])

        await self.execute_car_command(message)
        LOGGER.info("End Doors_lock for vin %s", loghelper.Mask_VIN(vin))

    async def download_images(self, vin: str):
        """Download the car related images."""
        LOGGER.info("Start download_images for vin %s", loghelper.Mask_VIN(vin))

        download_path = self._hass.config.path(DEFAULT_DOWNLOAD_PATH)
        target_file_name = Path(download_path, f"{vin}.zip")
        zip_file = await self.webapi.download_images(vin)
        if zip_file:
            Path(download_path).mkdir(parents=True, exist_ok=True)

            def save_images() -> None:
                with Path(target_file_name).open(mode="wb") as zf:
                    zf.write(zip_file)

            try:
                await self._hass.async_add_executor_job(save_images)
            except OSError as err:
                LOGGER.error("Can't write %s: %s", target_file_name, err)

        LOGGER.info("End download_images for vin %s", loghelper.Mask_VIN(vin))

    async def auxheat_configure(self, vin: str, time_selection: int, time_1: int, time_2: int, time_3: int):
        """Send the auxheat configure command to the car."""
        LOGGER.info("Start auxheat_configure for vin %s", loghelper.Mask_VIN(vin))

        if not self._is_car_feature_available(vin, feature_list=["AUXHEAT_CONFIGURE", "auxHeat"]):
            LOGGER.warning(
                "Can't start auxheat_configure for car %s. Feature not availabe for this car.",
                loghelper.Mask_VIN(vin),
            )
            return

        message = client_pb2.ClientMessage()

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())
        auxheat_configure = pb2_commands.AuxheatConfigure()
        auxheat_configure.time_selection = time_selection
        auxheat_configure.time_1 = time_1
        auxheat_configure.time_2 = time_2
        auxheat_configure.time_3 = time_3
        message.commandRequest.auxheat_configure.CopyFrom(auxheat_configure)

        await self.execute_car_command(message)
        LOGGER.info("End auxheat_configure for vin %s", loghelper.Mask_VIN(vin))

    async def auxheat_start(self, vin: str):
        """Send the auxheat start command to the car."""
        LOGGER.info("Start auxheat start for vin %s", loghelper.Mask_VIN(vin))

        if not self._is_car_feature_available(vin, feature_list=["AUXHEAT_START", "auxHeat"]):
            LOGGER.warning(
                "Can't start auxheat for car %s. Feature not availabe for this car.",
                loghelper.Mask_VIN(vin),
            )
            return

        message = client_pb2.ClientMessage()

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())
        auxheat_start = pb2_commands.AuxheatStart()
        message.commandRequest.auxheat_start.CopyFrom(auxheat_start)

        await self.execute_car_command(message)
        LOGGER.info("End auxheat start for vin %s", loghelper.Mask_VIN(vin))

    async def auxheat_stop(self, vin: str):
        """Send the auxheat stop command to the car."""
        LOGGER.info("Start auxheat_stop for vin %s", loghelper.Mask_VIN(vin))

        if not self._is_car_feature_available(vin, feature_list=["AUXHEAT_STOP", "auxHeat"]):
            LOGGER.warning(
                "Can't stop auxheat for car %s. Feature not availabe for this car.",
                loghelper.Mask_VIN(vin),
            )
            return

        message = client_pb2.ClientMessage()

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())
        auxheat_stop = pb2_commands.AuxheatStop()
        message.commandRequest.auxheat_stop.CopyFrom(auxheat_stop)

        await self.execute_car_command(message)
        LOGGER.info("End auxheat_stop for vin %s", loghelper.Mask_VIN(vin))

    async def battery_max_soc_configure(self, vin: str, max_soc: int, charge_program: int = 0):
        """Send the maxsoc configure command to the car."""
        LOGGER.info(
            "Start battery_max_soc_configure to %s for vin %s and program %s",
            max_soc,
            loghelper.Mask_VIN(vin),
            charge_program,
        )

        if not self._is_car_feature_available(vin, "BATTERY_MAX_SOC_CONFIGURE") and not self._is_car_feature_available(
            vin, "CHARGING_CONFIGURE"
        ):
            LOGGER.warning(
                "Can't configure battery_max_soc for car %s. Features BATTERY_MAX_SOC_CONFIGURE or CHARGING_CONFIGURE not availabe for this car.",
                loghelper.Mask_VIN(vin),
            )
            return

        message = client_pb2.ClientMessage()

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())

        if self._is_car_feature_available(vin, "CHARGING_CONFIGURE"):
            LOGGER.debug(
                "CHARGING_CONFIGURE=true for car %s. Will use ChargingConfigure.",
                loghelper.Mask_VIN(vin),
            )
            charging_config = pb2_commands.ChargingConfigure()
            charging_config.max_soc.value = max_soc
            # charging_config.charge_program = charge_program
            message.commandRequest.charging_configure.CopyFrom(charging_config)
        else:
            max_soc = max(max_soc, 50)
            charge_program_config = pb2_commands.ChargeProgramConfigure()
            charge_program_config.max_soc.value = max_soc
            charge_program_config.charge_program = charge_program
            message.commandRequest.charge_program_configure.CopyFrom(charge_program_config)

        await self.execute_car_command(message)
        LOGGER.info("End battery_max_soc_configure for vin %s", loghelper.Mask_VIN(vin))

    async def engine_start(self, vin: str, pin: str = ""):
        """Send the engine start command to the car."""
        LOGGER.info("Start engine start for vin %s", loghelper.Mask_VIN(vin))

        if pin and pin.strip():
            _pin = pin
        else:
            _pin = self.pin

        if not _pin:
            LOGGER.warning(
                "Can't start the car %s. PIN not given. Please set the PIN -> Integration, Options or use the optional parameter of the service.",
                loghelper.Mask_VIN(vin),
            )
            return

        if not self._is_car_feature_available(vin, "ENGINE_START"):
            LOGGER.warning(
                "Can't start engine for car %s. Feature not availabe for this car.",
                loghelper.Mask_VIN(vin),
            )
            return

        message = client_pb2.ClientMessage()

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())
        message.commandRequest.engine_start.pin = _pin

        await self.execute_car_command(message)
        LOGGER.info("End engine start for vin %s", loghelper.Mask_VIN(vin))

    async def engine_stop(self, vin: str):
        """Send the engine stop command to the car."""
        LOGGER.info("Start engine_stop for vin %s", loghelper.Mask_VIN(vin))

        if not self._is_car_feature_available(vin, "ENGINE_STOP"):
            LOGGER.warning(
                "Can't stop engine for car %s. Feature not availabe for this car.",
                loghelper.Mask_VIN(vin),
            )
            return

        message = client_pb2.ClientMessage()

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())
        engine_stop = pb2_commands.EngineStop()
        message.commandRequest.engine_stop.CopyFrom(engine_stop)

        await self.execute_car_command(message)
        LOGGER.info("End engine_stop for vin %s", loghelper.Mask_VIN(vin))

    async def hv_battery_start_conditioning(self, vin: str):
        """Send the hv battery conditioning start command to the car."""
        LOGGER.info("Start hv_battery_start_conditioning for vin %s", loghelper.Mask_VIN(vin))

        if not self._is_car_feature_available(vin, "HVBATTERY_START_CONDITIONING"):
            LOGGER.warning(
                "Can't start hv battery conditioning for car %s. Feature not available for this car.",
                loghelper.Mask_VIN(vin),
            )
            return

        message = client_pb2.ClientMessage()

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())
        hv_battery_conditioning_start = pb2_commands.HvBatteryStartConditioning()
        message.commandRequest.hv_battery_start_conditioning.CopyFrom(hv_battery_conditioning_start)

        await self.execute_car_command(message)
        LOGGER.info("End hv_battery_start_conditioning for vin %s", loghelper.Mask_VIN(vin))

    async def hv_battery_stop_conditioning(self, vin: str):
        """Send the hv battery conditioning stop command to the car."""
        LOGGER.info("Start hv_battery_stop_conditioning for vin %s", loghelper.Mask_VIN(vin))

        if not self._is_car_feature_available(vin, "HVBATTERY_STOP_CONDITIONING"):
            LOGGER.warning(
                "Can't stop hv battery conditioning for car %s. Feature not available for this car.",
                loghelper.Mask_VIN(vin),
            )
            return

        message = client_pb2.ClientMessage()

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())
        hv_battery_conditioning_stop = pb2_commands.HvBatteryStopConditioning()
        message.commandRequest.hv_battery_stop_conditioning.CopyFrom(hv_battery_conditioning_stop)

        await self.execute_car_command(message)
        LOGGER.info("End hv_battery_stop_conditioning for vin %s", loghelper.Mask_VIN(vin))

    async def send_route_to_car(
        self,
        vin: str,
        title: str,
        latitude: float,
        longitude: float,
        city: str,
        postcode: str,
        street: str,
    ):
        """Send a route target to the car."""
        LOGGER.info("Start send_route_to_car for vin %s", loghelper.Mask_VIN(vin))

        await self.webapi.send_route_to_car(vin, title, latitude, longitude, city, postcode, street)

        LOGGER.info("End send_route_to_car for vin %s", loghelper.Mask_VIN(vin))

    async def sigpos_start(self, vin: str):
        """Send a sigpos command to the car."""
        LOGGER.info("Start sigpos_start for vin %s", loghelper.Mask_VIN(vin))

        if not self._is_car_feature_available(vin, "SIGPOS_START"):
            LOGGER.warning(
                "Can't start signaling for car %s. Feature not availabe for this car.",
                loghelper.Mask_VIN(vin),
            )
            return

        message = client_pb2.ClientMessage()

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())
        message.commandRequest.sigpos_start.light_type = 1
        message.commandRequest.sigpos_start.sigpos_type = 0

        await self.execute_car_command(message)
        LOGGER.info("End sigpos_start for vin %s", loghelper.Mask_VIN(vin))

    async def sunroof_open(self, vin: str, pin: str = ""):
        """Send a sunroof open command to the car."""
        LOGGER.info("Start sunroof_open for vin %s", loghelper.Mask_VIN(vin))

        if pin and pin.strip():
            _pin = pin
        else:
            _pin = self.pin

        if not _pin:
            LOGGER.warning(
                "Can't open the sunroof - car %s. PIN not given. Please set the PIN -> Integration, Options or use the optional parameter of the service.",
                loghelper.Mask_VIN(vin),
            )
            return

        if not self._is_car_feature_available(vin, "SUNROOF_OPEN"):
            LOGGER.warning(
                "Can't open the sunroof for car %s. Feature not availabe for this car.",
                loghelper.Mask_VIN(vin),
            )
            return

        message = client_pb2.ClientMessage()

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())
        message.commandRequest.sunroof_open.pin = _pin

        await self.execute_car_command(message)
        LOGGER.info("End sunroof_open for vin %s", loghelper.Mask_VIN(vin))

    async def sunroof_tilt(self, vin: str, pin: str = ""):
        """Send a sunroof tilt command to the car."""
        LOGGER.info("Start sunroof_tilt for vin %s", loghelper.Mask_VIN(vin))

        if pin and pin.strip():
            _pin = pin
        else:
            _pin = self.pin

        if not _pin:
            LOGGER.warning(
                "Can't tilt the sunroof - car %s. PIN not given. Please set the PIN -> Integration, Options or use the optional parameter of the service.",
                loghelper.Mask_VIN(vin),
            )
            return

        if not self._is_car_feature_available(vin, "SUNROOF_LIFT"):
            LOGGER.warning(
                "Can't tilt the sunroof for car %s. Feature not availabe for this car.",
                loghelper.Mask_VIN(vin),
            )
            return

        message = client_pb2.ClientMessage()

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())
        message.commandRequest.sunroof_lift.pin = _pin

        await self.execute_car_command(message)
        LOGGER.info("End sunroof_tilt for vin %s", loghelper.Mask_VIN(vin))

    async def sunroof_close(self, vin: str):
        """Send a sunroof close command to the car."""
        LOGGER.info("Start sunroof_close for vin %s", loghelper.Mask_VIN(vin))

        if not self._is_car_feature_available(vin, "SUNROOF_CLOSE"):
            LOGGER.warning(
                "Can't close the sunroof for car %s. Feature not availabe for this car.",
                loghelper.Mask_VIN(vin),
            )
            return

        message = client_pb2.ClientMessage()

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())
        sunroof_close = pb2_commands.SunroofClose()
        message.commandRequest.sunroof_close.CopyFrom(sunroof_close)

        await self.execute_car_command(message)
        LOGGER.info("End sunroof_close for vin %s", loghelper.Mask_VIN(vin))

    async def preconditioning_configure_seats(
        self,
        vin: str,
        front_left: bool,
        front_right: bool,
        rear_left: bool,
        rear_right: bool,
    ):
        """Send a preconditioning seat configuration command to the car."""
        LOGGER.info("Start preconditioning_configure_seats for vin %s", loghelper.Mask_VIN(vin))

        if not self._is_car_feature_available(vin, "ZEV_PRECONDITION_CONFIGURE_SEATS"):
            LOGGER.warning(
                "Can't configure seats for PreCond for car %s. Feature %s not availabe for this car.",
                loghelper.Mask_VIN(vin),
                "ZEV_PRECONDITION_CONFIGURE_SEATS",
            )
            return
        message = client_pb2.ClientMessage()

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())
        message.commandRequest.zev_precondition_configure_seats.front_left = front_left
        message.commandRequest.zev_precondition_configure_seats.front_right = front_right
        message.commandRequest.zev_precondition_configure_seats.rear_left = rear_left
        message.commandRequest.zev_precondition_configure_seats.rear_right = rear_right

        await self.execute_car_command(message)
        LOGGER.info("End preconditioning_configure_seats for vin %s", loghelper.Mask_VIN(vin))

    async def preheat_start(self, vin: str):
        """Send a preconditioning start command to the car."""
        LOGGER.info("Start preheat_start for vin %s", loghelper.Mask_VIN(vin))

        if not self._is_car_feature_available(vin, "ZEV_PRECONDITIONING_START"):
            LOGGER.warning(
                "Can't start PreCond for car %s. Feature not availabe for this car.",
                loghelper.Mask_VIN(vin),
            )
            return

        message = client_pb2.ClientMessage()

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())
        message.commandRequest.zev_preconditioning_start.departure_time = 0
        message.commandRequest.zev_preconditioning_start.type = pb2_commands.ZEVPreconditioningType.now

        await self.execute_car_command(message)
        LOGGER.info("End preheat_start for vin %s", loghelper.Mask_VIN(vin))

    async def preheat_start_immediate(self, vin: str):
        """Send a preconditioning immediatestart command to the car."""
        LOGGER.info("Start preheat_start_immediate for vin %s", loghelper.Mask_VIN(vin))

        if not self._is_car_feature_available(vin, "ZEV_PRECONDITIONING_START"):
            LOGGER.warning(
                "Can't start PreCond for car %s. Feature not availabe for this car.",
                loghelper.Mask_VIN(vin),
            )
            return

        message = client_pb2.ClientMessage()

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())
        message.commandRequest.zev_preconditioning_start.departure_time = 0
        message.commandRequest.zev_preconditioning_start.type = pb2_commands.ZEVPreconditioningType.immediate

        await self.execute_car_command(message)
        LOGGER.info("End preheat_start_immediate for vin %s", loghelper.Mask_VIN(vin))

    async def preheat_start_universal(self, vin: str) -> None:
        """Turn on preheat universally for any car model."""
        if self._is_car_feature_available(vin, "precondNow"):
            await self.preheat_start(vin)
        else:
            await self.preheat_start_immediate(vin)

    async def preheat_start_departure_time(self, vin: str, departure_time: int):
        """Send a preconditioning start by time command to the car."""
        LOGGER.info("Start preheat_start_departure_time for vin %s", loghelper.Mask_VIN(vin))

        if not self._is_car_feature_available(vin, "ZEV_PRECONDITIONING_START"):
            LOGGER.warning(
                "Can't start PreCond for car %s. Feature not availabe for this car.",
                loghelper.Mask_VIN(vin),
            )
            return

        message = client_pb2.ClientMessage()

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())
        message.commandRequest.zev_preconditioning_start.departure_time = departure_time
        message.commandRequest.zev_preconditioning_start.type = pb2_commands.ZEVPreconditioningType.departure

        await self.execute_car_command(message)
        LOGGER.info("End preheat_start_departure_time for vin %s", loghelper.Mask_VIN(vin))

    async def preheat_stop(self, vin: str):
        """Send a preconditioning stop command to the car."""
        LOGGER.info("Start preheat_stop for vin %s", loghelper.Mask_VIN(vin))

        if not self._is_car_feature_available(vin, "ZEV_PRECONDITIONING_STOP"):
            LOGGER.warning(
                "Can't stop PreCond for car %s. Feature not availabe for this car.",
                loghelper.Mask_VIN(vin),
            )
            return
        message = client_pb2.ClientMessage()

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())
        message.commandRequest.zev_preconditioning_stop.type = pb2_commands.ZEVPreconditioningType.now

        await self.execute_car_command(message)
        LOGGER.info("End preheat_stop for vin %s", loghelper.Mask_VIN(vin))

    async def preheat_stop_departure_time(self, vin: str):
        """Disable scheduled departure preconditioning."""
        LOGGER.info("Start preheat_stop_departure_time for vin %s", loghelper.Mask_VIN(vin))
        await self.preconditioning_configure(vin, departure_time_mode=0)
        LOGGER.info("End preheat_stop_departure_time for vin %s", loghelper.Mask_VIN(vin))

    async def preconditioning_configure(self, vin: str, departure_time_mode: int = 0, departure_time: int = 0):
        """Configure preconditioning departure time mode.

        departure_time_mode: 0=DISABLED, 1=SINGLE_DEPARTURE, 2=WEEKLY_DEPARTURE
            Note: WEEKLY_DEPARTURE (mode 2) is not supported on all car models
            (e.g., EQB supports only DISABLED and SINGLE_DEPARTURE)
        departure_time: Minutes after midnight (0-1439), only used when mode > 0
        """
        LOGGER.info(
            "Start preconditioning_configure for vin %s with mode %s", loghelper.Mask_VIN(vin), departure_time_mode
        )

        if not self._is_car_feature_available(vin, "ZEV_PRECONDITION_CONFIGURE"):
            LOGGER.warning(
                "Can't configure PreCond for car %s. Feature not available for this car",
                loghelper.Mask_VIN(vin),
            )
            return

        message = client_pb2.ClientMessage()

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())
        message.commandRequest.zev_precondition_configure.departure_time_mode = departure_time_mode
        if departure_time_mode > 0:
            message.commandRequest.zev_precondition_configure.departure_time = departure_time

        await self.execute_car_command(message)
        LOGGER.info("End preconditioning_configure for vin %s", loghelper.Mask_VIN(vin))

    async def temperature_configure(
        self,
        vin: str,
        front_left: int | None = None,
        front_right: int | None = None,
        rear_left: int | None = None,
        rear_right: int | None = None,
    ):
        """Send a temperature_configure  command to the car."""
        LOGGER.info("Start temperature_configure for vin %s", loghelper.Mask_VIN(vin))

        if not self._is_car_feature_available(vin, "TEMPERATURE_CONFIGURE"):
            LOGGER.warning(
                "Can't configure the temperature zones for car %s. Feature %s not availabe for this car.",
                loghelper.Mask_VIN(vin),
                "TEMPERATURE_CONFIGURE",
            )
            return
        message = client_pb2.ClientMessage()

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())

        config = pb2_commands.TemperatureConfigure()
        entry_set: bool = False

        car = self.cars.get(vin)

        if front_left or front_right or rear_left or rear_right:
            zone_front_left = config.temperature_points.add()
            zone_front_left.zone = 1
            if front_left:
                zone_front_left.temperature_in_celsius = front_left
            elif car.precond.temperature_points_frontLeft:
                zone_front_left.temperature_in_celsius = car.precond.temperature_points_frontLeft.value
            entry_set = True

        if front_right or rear_left or rear_right:
            zone_front_right = config.temperature_points.add()
            zone_front_right.zone = 2
            if front_right:
                zone_front_right.temperature_in_celsius = front_right
            elif car.precond.temperature_points_frontRight:
                zone_front_right.temperature_in_celsius = car.precond.temperature_points_frontRight.value
            entry_set = True

        if rear_left or rear_right:
            zone_rear_left = config.temperature_points.add()
            zone_rear_left.zone = 4
            if rear_left:
                zone_rear_left.temperature_in_celsius = rear_left
            elif car.precond.temperature_points_rearLeft:
                zone_rear_left.temperature_in_celsius = car.precond.temperature_points_rearLeft.value
            entry_set = True

        if rear_right or rear_left:
            zone_rear_right = config.temperature_points.add()
            zone_rear_right.zone = 5
            if rear_right:
                zone_rear_right.temperature_in_celsius = rear_right
            elif car.precond.temperature_points_rearRight:
                zone_rear_right.temperature_in_celsius = car.precond.temperature_points_rearRight.value
            entry_set = True

        if entry_set:
            message.commandRequest.temperature_configure.CopyFrom(config)
            self._hass.async_add_executor_job(
                self.write_debug_json_output,
                MessageToJson(message, preserving_proto_field_name=True),
                "out_temperature_",
                False,
            )
            await self.execute_car_command(message)
            LOGGER.info("End temperature_configure for vin %s", loghelper.Mask_VIN(vin))
        else:
            LOGGER.info(
                "End temperature_configure for vin %s - No actions",
                loghelper.Mask_VIN(vin),
            )

    async def windows_open(self, vin: str, pin: str = ""):
        """Send a window open command to the car."""
        LOGGER.info("Start windows_open for vin %s", loghelper.Mask_VIN(vin))

        _pin: str = None

        if not self._is_car_feature_available(vin, "WINDOWS_OPEN"):
            LOGGER.warning(
                "Can't open the windows for car %s. Feature not marked as available for this car.",
                loghelper.Mask_VIN(vin),
            )
            return

        if pin and pin.strip():
            _pin = pin
        else:
            _pin = self.pin

        if not _pin:
            LOGGER.warning(
                "Can't open the windows - car %s. PIN not given. Please set the PIN -> Integration, Options or use the optional parameter of the service.",
                loghelper.Mask_VIN(vin),
            )
            return

        message = client_pb2.ClientMessage()

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())
        message.commandRequest.windows_open.pin = _pin

        await self.execute_car_command(message)
        LOGGER.info("End windows_open for vin %s", loghelper.Mask_VIN(vin))

    async def windows_close(self, vin: str):
        """Send a window close command to the car."""
        LOGGER.info("Start windows_close for vin %s", loghelper.Mask_VIN(vin))

        if not self._is_car_feature_available(vin, "WINDOWS_CLOSE"):
            LOGGER.warning(
                "Can't close the windows for car %s. Feature not availabe for this car.",
                loghelper.Mask_VIN(vin),
            )
            return

        message = client_pb2.ClientMessage()

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())
        windows_close = pb2_commands.WindowsClose()
        message.commandRequest.windows_close.CopyFrom(windows_close)

        await self.execute_car_command(message)
        LOGGER.info("End windows_close for vin %s", loghelper.Mask_VIN(vin))

    async def windows_move(
        self, vin: str, front_left: int, front_right: int, rear_left: int, rear_right: int, pin: str = ""
    ):
        """Send the windows move command to the car."""
        LOGGER.info(
            "Start windows_move for vin %s, fl-%s, fr-%s, rl-%s, rr-%s",
            loghelper.Mask_VIN(vin),
            front_left,
            front_right,
            rear_left,
            rear_right,
        )

        if pin and pin.strip():
            _pin = pin
        else:
            _pin = self.pin

        if not _pin:
            LOGGER.warning(
                "Can't move the windows - car %s. PIN not given. Please set the PIN -> Integration, Options or use the optional parameter of the service.",
                loghelper.Mask_VIN(vin),
            )
            return

        if not self._is_car_feature_available(vin, "variableOpenableWindow"):
            LOGGER.warning(
                "Can't move windows for car %s. Feature not availabe for this car.",
                loghelper.Mask_VIN(vin),
            )
            return

        message = client_pb2.ClientMessage()

        message.commandRequest.vin = vin
        message.commandRequest.request_id = str(uuid.uuid4())
        message.commandRequest.windows_move.pin = _pin
        if front_left is not None:
            if front_left == 0:
                message.commandRequest.windows_move.front_left.SetInParent()
            else:
                message.commandRequest.windows_move.front_left.value = front_left
        if front_right is not None:
            if front_right == 0:
                message.commandRequest.windows_move.front_right.SetInParent()
            else:
                message.commandRequest.windows_move.front_right.value = front_right
        if rear_left is not None:
            if rear_left == 0:
                message.commandRequest.windows_move.rear_left.SetInParent()
            else:
                message.commandRequest.windows_move.rear_left.value = rear_left
        if rear_right is not None:
            if rear_right == 0:
                message.commandRequest.windows_move.rear_right.SetInParent()
            else:
                message.commandRequest.windows_move.rear_right.value = rear_right

        await self.execute_car_command(message)
        LOGGER.info("End windows_move for vin %s", loghelper.Mask_VIN(vin))

    async def execute_car_command(self, message):
        """Execute a car command."""
        LOGGER.debug("execute_car_command - ws-connection: %s", self.websocket.connection_state)
        await self.websocket.call(message.SerializeToString(), car_command=True)

    def _is_car_feature_available(self, vin: str, feature: str = "", feature_list=None) -> bool:
        if self.config_entry.options.get(CONF_FT_DISABLE_CAPABILITY_CHECK, False):
            return True

        current_car = self.cars.get(vin)

        if current_car:
            if feature_list:
                return current_car.check_capabilities(feature_list)

            if feature:
                return current_car.features.get(feature, False)

        return False

    def _write_debug_output(self, data, datatype):
        if self.config_entry.options.get(CONF_DEBUG_FILE_SAVE, False):
            self._hass.async_add_executor_job(self.__write_debug_output, data, datatype)

    def __write_debug_output(self, data, datatype):
        if self.config_entry.options.get(CONF_DEBUG_FILE_SAVE, False):
            # LOGGER.debug("Start _write_debug_output")

            path = self._debug_save_path
            Path(path).mkdir(parents=True, exist_ok=True)

            with Path(f"{path}/{datatype}{int(round(time.time() * 1000))}").open("wb") as current_file:
                current_file.write(data.SerializeToString())

            self.write_debug_json_output(MessageToJson(data, preserving_proto_field_name=True), datatype)

    def write_debug_json_output(self, data, datatype, use_dumps: bool = False):
        """Write text to files based on datatype."""
        # LOGGER.debug(self.config_entry.options)
        if self.config_entry.options.get(CONF_DEBUG_FILE_SAVE, False):
            path = self._debug_save_path
            Path(path).mkdir(parents=True, exist_ok=True)

            with Path(f"{path}/{datatype}{int(round(time.time() * 1000))}.json").open(
                "w", encoding="utf-8"
            ) as current_file:
                if use_dumps:
                    current_file.write(f"{json.dumps(data, indent=4)}")
                else:
                    current_file.write(f"{data}")

    async def set_rlock_mode(self):
        """Set thread locking mode on init."""
        # In rare cases the ha-core system_info component runs in error when detecting the supervisor
        # See https://github.com/ReneNulschDE/mbapi2020/issues/126
        info = None
        try:
            info = await system_info.async_get_system_info(self._hass)
        except AttributeError:
            LOGGER.debug("WSL detection not possible. Error in HA-Core get_system_info. Force rlock mode.")

        if info and "WSL" not in str(info.get("os_version")):
            self._disable_rlock = False
            self.__lock = threading.RLock()
            LOGGER.debug("WSL not detected - running in rlock mode")
        else:
            self._disable_rlock = True
            self.__lock = None
            LOGGER.debug("WSL detected - rlock mode disabled")

        return info

    async def update_poll_states(self, vin: str):
        """Update the values for poll states, currently geofencing only."""

        if vin in self.cars:
            car = self.cars[vin]

            if self.websocket and self.websocket.account_blocked:
                LOGGER.debug("start get_car_p2b_data_via_rest: %s", loghelper.Mask_VIN(vin))
                p2b_data = await self.webapi.get_car_p2b_data_via_rest(vin)
                self._process_rest_vep_update(p2b_data)

            if not car.has_geofencing:
                return

            if car.geofence_events is None:
                car.geofence_events = GeofenceEvents()

            LOGGER.debug("start get_car_geofencing_violations: %s", loghelper.Mask_VIN(vin))
            geofencing_violotions = await self.webapi.get_car_geofencing_violations(car.finorvin)
            if geofencing_violotions and len(geofencing_violotions) > 0:
                car.geofence_events.last_event_type = CarAttribute(
                    geofencing_violotions[-1].get("type"),
                    "VALID",
                    geofencing_violotions[-1].get("time"),
                )
                car.geofence_events.last_event_timestamp = CarAttribute(
                    geofencing_violotions[-1].get("time"),
                    "VALID",
                    geofencing_violotions[-1].get("time"),
                )
                car.geofence_events.last_event_zone = CarAttribute(
                    geofencing_violotions[-1].get("snapshot").get("name"),
                    "VALID",
                    geofencing_violotions[-1].get("time"),
                )
                car.has_geofencing = True
                car.geo_fencing_retry_counter = 0
            else:
                if car.geo_fencing_retry_counter >= GEOFENCING_MAX_RETRIES:
                    car.has_geofencing = False
                car.geo_fencing_retry_counter = car.geo_fencing_retry_counter + 1

    def _safe_create_on_dataload_complete_task(self):
        """Create task in a thread-safe way."""
        # Zurück zum Event Loop
        asyncio.run_coroutine_threadsafe(self._on_dataload_complete(), self._hass.loop)


================================================
FILE: custom_components/mbapi2020/config_flow.py
================================================
"""Config flow for mbapi2020 integration."""

from __future__ import annotations

from copy import deepcopy
import uuid

from awesomeversion import AwesomeVersion
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, __version__ as HAVERSION
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.storage import STORAGE_DIR

from .client import Client
from .const import (
    CONF_ALLOWED_REGIONS,
    CONF_DEBUG_FILE_SAVE,
    CONF_DELETE_AUTH_FILE,
    CONF_ENABLE_CHINA_GCJ_02,
    CONF_EXCLUDED_CARS,
    CONF_FT_DISABLE_CAPABILITY_CHECK,
    CONF_OVERWRITE_PRECONDNOW,
    CONF_PIN,
    CONF_REGION,
    DOMAIN,
    LOGGER,
    REGION_CHINA,
    TOKEN_FILE_PREFIX,
    VERIFY_SSL,
)
from .errors import MbapiError, MBAuth2FAError, MBAuthError, MBLegalTermsError

AUTH_METHOD_TOKEN = "token"
AUTH_METHOD_USERPASS = "userpass"

REGION_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_REGION): vol.In(CONF_ALLOWED_REGIONS),
    }
)

USER_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_USERNAME): str,
        vol.Required(CONF_PASSWORD): str,
    }
)

USER_SCHEMA_CHINA = vol.Schema(
    {
        vol.Required(CONF_USERNAME): str,
    }
)

USER_STEP_PIN = vol.Schema({vol.Required(CONF_PASSWORD): str})


# Version threshold for config_entry setting in options flow
# See: https://github.com/home-assistant/core/pull/129562
HA_OPTIONS_FLOW_VERSION_THRESHOLD = "2024.11.99"


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
    """Handle a config flow for mbapi2020."""

    VERSION = 1

    def __init__(self):
        """Initialize the ConfigFlow state."""
        self._reauth_entry = None
        self._data = None
        self._reauth_mode = False
        self._auth_method = AUTH_METHOD_TOKEN
        self._region = None

    async def async_step_user(self, user_input=None):
        """Region selection step."""

        if user_input is not None:
            self._region = user_input[CONF_REGION]
            return await self.async_step_credentials()

        return self.async_show_form(step_id="user", data_schema=REGION_SCHEMA)

    async def async_step_credentials(self, user_input=None):
        """Credentials step - username/password or username only for China."""

        is_china = self._region == REGION_CHINA
        schema = USER_SCHEMA_CHINA if is_china else USER_SCHEMA

        if user_input is not None:
            user_input[CONF_REGION] = self._region
            await self.async_set_unique_id(f"{user_input[CONF_USERNAME]}-{user_input[CONF_REGION]}")

            if not self._reauth_mode:
                self._abort_if_unique_id_configured()

            session = async_get_clientsession(self.hass, VERIFY_SSL)
            client = Client(self.hass, session, None, region=user_input[CONF_REGION])
            user_input[CONF_USERNAME] = user_input[CONF_USERNAME].strip()

            if is_china:
                nonce = str(uuid.uuid4())
                user_input["nonce"] = nonce
                user_input["device_guid"] = client.oauth._device_guid  # noqa: SLF001
                errors = {}

                try:
                    await client.oauth.request_pin(user_input[CONF_USERNAME], nonce)
                except (MBAuthError, MbapiError):
                    errors = {"base": "pinrequest_failed"}
                    return self.async_show_form(step_id="credentials", data_schema=schema, errors=errors)

                if not errors:
                    self._data = user_input
                    return await self.async_step_pin()

                LOGGER.error("Request PIN error: %s", errors)

                self._data = {
                    CONF_USERNAME: user_input[CONF_USERNAME],
                    CONF_REGION: user_input[CONF_REGION],
                    "nonce": nonce,
                    "device_guid": user_input["device_guid"],
                }
            else:
                try:
                    token_info = await client.oauth.async_login_new(
                        user_input[CONF_USERNAME], user_input[CONF_PASSWORD]
                    )
                except (MBAuthError, MbapiError) as error:
                    LOGGER.error("Login error: %s", error)
                    return self.async_show_form(
                        step_id="credentials", data_schema=schema, errors={"base": "invalid_auth"}
                    )
                except MBAuth2FAError as error:
                    LOGGER.error("Login error - 2FA accounts are not supported: %s", error)
                    return self.async_show_form(
                        step_id="credentials", data_schema=schema, errors={"base": "2fa_required"}
                    )
                except MBLegalTermsError as error:
                    LOGGER.error("Login error - Legal terms not accepted: %s", error)
                    return self.async_show_form(
                        step_id="credentials", data_schema=schema, errors={"base": "legal_terms"}
                    )
                self._data = {
                    CONF_USERNAME: user_input[CONF_USERNAME],
                    CONF_REGION: user_input[CONF_REGION],
                    CONF_PASSWORD: user_input[CONF_PASSWORD],
                    "token": token_info,
                    "device_guid": client.oauth._device_guid,  # noqa: SLF001
                }

            if self._reauth_mode:
                self.hass.config_entries.async_update_entry(self._reauth_entry, data=self._data)
                self.hass.config_entries.async_schedule_reload(self._reauth_entry.entry_id)
                return self.async_abort(reason="reauth_successful")

            return self.async_create_entry(
                title=f"{self._data[CONF_USERNAME]} (Region: {self._data[CONF_REGION]})",
                data=self._data,
            )

        return self.async_show_form(step_id="credentials", data_schema=schema)

    async def async_step_pin(self, user_input=None):
        """Handle the step where the user inputs his/her station."""

        errors = {}

        if user_input is not None:
            pin = user_input[CONF_PASSWORD]
            nonce = self._data["nonce"]
            new_config_entry: config_entries.ConfigEntry = await self.async_set_unique_id(
                f"{self._data[CONF_USERNAME]}-{self._data[CONF_REGION]}"
            )
            session = async_get_clientsession(self.hass, VERIFY_SSL)

            client = Client(self.hass, session, new_config_entry, self._data[CONF_REGION])
            try:
                result = await client.oauth.request_access_token_with_pin(self._data[CONF_USERNAME], pin, nonce)
            except MbapiError as error:
                LOGGER.error("Request token error: %s", error)
                errors = {"base": "token_with_pin_request_failed"}

            if not errors:
                LOGGER.debug("Token received")
                self._data["token"] = result

                if self._reauth_mode:
                    self.hass.config_entries.async_update_entry(self._reauth_entry, data=self._data)
                    self.hass.async_create_task(self.hass.config_entries.async_reload(self._reauth_entry.entry_id))
                    return self.async_abort(reason="reauth_successful")

                return self.async_create_entry(
                    title=f"{self._data[CONF_USERNAME]} (Region: {self._data[CONF_REGION]})",
                    data=self._data,
                )

        return self.async_show_form(step_id="pin", data_schema=USER_STEP_PIN, errors=errors)

    async def async_step_reauth(self, user_input=None):
        """Get new tokens for a config entry that can't authenticate."""

        self._reauth_mode = True
        self._reauth_entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
        self._region = self._reauth_entry.data.get(CONF_REGION)

        return await self.async_step_credentials()

    # async def async_step_reconfigure(self, user_input=None):
    #     """Get new tokens for a config entry that can't authenticate."""
    #     self._reauth_mode = True
    #     self._reauth_entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
    #     return self.async_show_form(step_id="user", data_schema=SCHEMA_STEP_AUTH_SELECT)

    @staticmethod
    @callback
    def async_get_options_flow(config_entry):
        """Get options flow."""
        return OptionsFlowHandler(config_entry)


class OptionsFlowHandler(config_entries.OptionsFlow):
    """Options flow handler."""

    def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
        """Initialize MBAI2020 options flow."""
        self.options = dict(config_entry.options)
        # See: https://github.com/home-assistant/core/pull/129562
        if AwesomeVersion(HAVERSION) < HA_OPTIONS_FLOW_VERSION_THRESHOLD:
            self.config_entry = config_entry

    async def async_step_init(self, user_input=None):
        """Manage the options."""

        if user_input is not None:
            LOGGER.debug(
                "user_input: %s",
                {k: ("xxxx" if v else v) if k == CONF_PIN else v for k, v in user_input.items()},
            )
            if user_input[CONF_DELETE_AUTH_FILE] is True:
                auth_file = self.hass.config.path(STORAGE_DIR, f"{TOKEN_FILE_PREFIX}-{self.config_entry.entry_id}")
                LOGGER.warning("DELETE Auth Information requested %s", auth_file)
                new_config_entry_data = deepcopy(dict(self.config_entry.data))
                new_config_entry_data["token"] = None
                changed = self.hass.config_entries.async_update_entry(self.config_entry, data=new_config_entry_data)

                LOGGER.debug("%s Creating restart_required issue", DOMAIN)
                async_create_issue(
                    hass=self.hass,
                    domain=DOMAIN,
                    issue_id="restart_required_auth_deleted",
                    is_fixable=True,
                    issue_domain=DOMAIN,
                    severity=IssueSeverity.WARNING,
                    translation_key="restart_required",
                    translation_placeholders={
                        "name": DOMAIN,
                    },
                )

            self.options.update(user_input)
            changed = self.hass.config_entries.async_update_entry(
                self.config_entry,
                options=user_input,
            )
            if changed:
                await self.hass.config_entries.async_reload(self.config_entry.entry_id)
            return self.async_create_entry(title=DOMAIN, data=self.options)

        excluded_cars = self.options.get(CONF_EXCLUDED_CARS, "")
        pin = self.options.get(CONF_PIN, "")
        cap_check_disabled = self.options.get(CONF_FT_DISABLE_CAPABILITY_CHECK, False)
        save_debug_files = self.options.get(CONF_DEBUG_FILE_SAVE, False)
        enable_china_gcj_02 = self.options.get(CONF_ENABLE_CHINA_GCJ_02, False)
        overwrite_cap_precondnow = self.options.get(CONF_OVERWRITE_PRECONDNOW, False)

        return self.async_show_form(
            step_id="init",
            data_schema=vol.Schema(
                {
                    vol.Optional(CONF_EXCLUDED_CARS, default="", description={"suggested_value": excluded_cars}): str,
                    vol.Optional(CONF_PIN, default="", description={"suggested_value": pin}): str,
                    vol.Optional(CONF_FT_DISABLE_CAPABILITY_CHECK, default=cap_check_disabled): bool,
                    vol.Optional(CONF_DEBUG_FILE_SAVE, default=save_debug_files): bool,
                    vol.Optional(CONF_DELETE_AUTH_FILE, default=False): bool,
                    vol.Optional(CONF_ENABLE_CHINA_GCJ_02, default=enable_china_gcj_02): bool,
                    vol.Optional(CONF_OVERWRITE_PRECONDNOW, default=overwrite_cap_precondnow): bool,
                }
            ),
        )


================================================
FILE: custom_components/mbapi2020/const.py
================================================
"""Constants for the MercedesME 2020 integration."""

from __future__ import annotations

from datetime import timedelta
from enum import Enum, StrEnum
import logging

import voluptuous as vol

from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.const import (
    PERCENTAGE,
    EntityCategory,
    Platform,
    UnitOfEnergy,
    UnitOfLength,
    UnitOfMass,
    UnitOfPower,
    UnitOfPressure,
    UnitOfSpeed,
    UnitOfTemperature,
    UnitOfVolume,
)
from homeassistant.helpers import config_validation as cv

MERCEDESME_COMPONENTS = [
    Platform.SENSOR,
    Platform.LOCK,
    Platform.BINARY_SENSOR,
    Platform.BUTTON,
    Platform.DEVICE_TRACKER,
    Platform.SWITCH,
]

REGION_EUROPE = "Europe"
REGION_NORAM = "North America"
REGION_APAC = "Asia-Pacific"
REGION_CHINA = "China"

CONF_ALLOWED_REGIONS = [REGION_EUROPE, REGION_NORAM, REGION_APAC, REGION_CHINA]
CONF_LOCALE = "locale"
CONF_COUNTRY_CODE = "country_code"
CONF_EXCLUDED_CARS = "excluded_cars"
CONF_PIN = "pin"
CONF_REGION = "region"
CONF_VIN = "vin"
CONF_TIME = "time"
CONF_DEBUG_FILE_SAVE = "save_files"
CONF_FT_DISABLE_CAPABILITY_CHECK = "cap_check_disabled"
CONF_DELETE_AUTH_FILE = "delete_auth_file"
CONF_ENABLE_CHINA_GCJ_02 = "enable_china_gcj_02"
CONF_AUTH_METHOD = "auth_method"
CONF_ACCESS_TOKEN = "access_token"
CONF_REFRESH_TOKEN = "refresh_token"
CONF_OVERWRITE_PRECONDNOW = "overwrite_cap_precondnow"

DOMAIN = "mbapi2020"
LOGGER = logging.getLogger(__package__)

UPDATE_INTERVAL = timedelta(seconds=180)

# Duration to wait for state confirmation of interactive entitiess in seconds
STATE_CONFIRMATION_DURATION = 60

DEFAULT_CACHE_PATH = "custom_components/mbapi2020/messages"
DEFAULT_DOWNLOAD_PATH = "custom_components/mbapi2020/resources"
DEFAULT_LOCALE = "en-GB"
DEFAULT_COUNTRY_CODE = "EN"

TOKEN_FILE_PREFIX = ".mercedesme-token-cache"

JSON_EXPORT_IGNORED_KEYS = (
    "pin",
    "access_token",
    "refresh_token",
    "username",
    "unique_id",
    "nonce",
    "_update_listeners",
    "finorvin",
    "licenseplate",
    "fin",
    "licensePlate",
    "vin",
    "dealers",
    "positionLong",
    "positionHeading",
    "id_token",
    "password",
    "title",
)


RIS_APPLICATION_VERSION_NA = "3.65.1"
RIS_APPLICATION_VERSION_CN = "1.65.0"
RIS_APPLICATION_VERSION_PA = "1.65.0"
RIS_APPLICATION_VERSION = "1.65.1 (3174)"
RIS_SDK_VERSION = "4.4.2"
RIS_SDK_VERSION_CN = "2.132.2"
RIS_OS_VERSION = "26.3"
RIS_OS_NAME = "ios"
X_APPLICATIONNAME = "mycar-store-ece"
X_APPLICATIONNAME_ECE = "mycar-store-ece"
X_APPLICATIONNAME_CN = "mycar-store-cn"
X_APPLICATIONNAME_US = "mycar-store-us"
X_APPLICATIONNAME_AP = "mycar-store-ap"

USE_PROXY = False
VERIFY_SSL = True
SYSTEM_PROXY: str | None = None if not USE_PROXY else "http://192.168.178.68:9090"

LOGIN_APP_ID_EU = "62778dc4-1de3-44f4-af95-115f06a3a008"
LOGIN_APP_ID_CN = "3f36efb1-f84b-4402-b5a2-68a118fec33e"
LOGIN_BASE_URI = "https://id.mercedes-benz.com"
LOGIN_BASE_URI_CN = "https://ciam-1.mercedes-benz.com.cn"
PSAG_BASE_URI = "https://psag.query.api.dvb.corpinter.net"
PSAG_BASE_URI_CN = "https://psag.query.api.dvb.corpinter.net.cn"
RCP_BASE_URI = "https://rcp-rs.query.api.dvb.corpinter.net"
RCP_BASE_URI_CN = "https://rcp-rs.query.api.dvb.corpinter.net.cn"
REST_API_BASE = "https://bff.emea-prod.mobilesdk.mercedes-benz.com"
REST_API_BASE_CN = "https://bff.cn-prod.mobilesdk.mercedes-benz.com"
REST_API_BASE_NA = "https://bff.amap-prod.mobilesdk.mercedes-benz.com"
REST_API_BASE_PA = "https://bff.amap-prod.mobilesdk.mercedes-benz.com"

WEBSOCKET_API_BASE = "wss://websocket.emea-prod.mobilesdk.mercedes-benz.com/v2/ws"
WEBSOCKET_API_BASE_NA = "wss://websocket.amap-prod.mobilesdk.mercedes-benz.com/v2/ws"
WEBSOCKET_API_BASE_PA = "wss://websocket.amap-prod.mobilesdk.mercedes-benz.com/v2/ws"
WEBSOCKET_API_BASE_CN = "wss://websocket.cn-prod.mobilesdk.mercedes-benz.com/v2/ws"
WEBSOCKET_USER_AGENT = "Mercedes-Benz/3044 CFNetwork/3860.400.22 Darwin/25.3.0"
WEBSOCKET_USER_AGENT_CN = "MyStarCN/1.63.0 (com.daimler.ris.mercedesme.cn.ios; build:1758; iOS 16.3.1) Alamofire/5.4.0"
WEBSOCKET_USER_AGENT_PA = (
    f"mycar-store-ap {RIS_APPLICATION_VERSION}, {RIS_OS_NAME} {RIS_OS_VERSION}, SDK {RIS_SDK_VERSION}"
)
WEBSOCKET_USER_AGENT_US = (
    f"mycar-store-us v{RIS_APPLICATION_VERSION_NA}, {RIS_OS_NAME} {RIS_OS_VERSION}, SDK {RIS_SDK_VERSION}"
)
WIDGET_API_BASE = "https://widget.emea-prod.mobilesdk.mercedes-benz.com"
WIDGET_API_BASE_NA = "https://widget.amap-prod.mobilesdk.mercedes-benz.com"
WIDGET_API_BASE_PA = "https://widget.amap-prod.mobilesdk.mercedes-benz.com"
WIDGET_API_BASE_CN = "https://widget.cn-prod.mobilesdk.mercedes-benz.com"
DEFAULT_SOCKET_MIN_RETRY = 15

SERVICE_AUXHEAT_CONFIGURE = "auxheat_configure"
SERVICE_AUXHEAT_START = "auxheat_start"
SERVICE_AUXHEAT_STOP = "auxheat_stop"
SERVICE_BATTERY_MAX_SOC_CONFIGURE = "battery_max_soc_configure"
SERVICE_CHARGE_PROGRAM_CONFIGURE = "charge_program_configure"
SERVICE_CHARGING_BREAK_CLOCKTIMER_CONFIGURE = "charging_break_clocktimer_configure"
SERVICE_DOORS_LOCK_URL = "doors_lock"
SERVICE_DOORS_UNLOCK_URL = "doors_unlock"
SERVICE_ENGINE_START = "engine_start"
SERVICE_ENGINE_STOP = "engine_stop"
SERVICE_SEND_ROUTE = "send_route"
SERVICE_SIGPOS_START = "sigpos_start"
SERVICE_SUNROOF_OPEN = "sunroof_open"
SERVICE_SUNROOF_TILT = "sunroof_tilt"
SERVICE_SUNROOF_CLOSE = "sunroof_close"
SERVICE_PREHEAT_START = "preheat_start"
SERVICE_PREHEAT_START_DEPARTURE_TIME = "preheat_start_departure_time"
SERVICE_PREHEAT_STOP_DEPARTURE_TIME = "preheat_stop_departure_time"
SERVICE_PREHEAT_STOP = "preheat_stop"
SERVICE_PRECONDITIONING_CONFIGURE = "preconditioning_configure"
SERVICE_WINDOWS_OPEN = "windows_open"
SERVICE_WINDOWS_CLOSE = "windows_close"
SERVICE_WINDOWS_MOVE = "windows_move"
SERVICE_DOWNLOAD_IMAGES = "download_images"
SERVICE_PRECONDITIONING_CONFIGURE_SEATS = "preconditioning_configure_seats"
SERVICE_TEMPERATURE_CONFIGURE = "temperature_configure"
SERVICE_HV_BATTERY_START_CONDITIONING = "hv_battery_start_conditioning"
SERVICE_HV_BATTERY_STOP_CONDITIONING = "hv_battery_stop_conditioning"

SERVICE_AUXHEAT_CONFIGURE_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_VIN): cv.string,
        vol.Required("time_selection"): vol.All(vol.Coerce(int), vol.Range(min=0, max=3)),
        vol.Required("time_1"): vol.All(vol.Coerce(int), vol.Range(min=0, max=1439)),
        vol.Required("time_2"): vol.All(vol.Coerce(int), vol.Range(min=0, max=1439)),
        vol.Required("time_3"): vol.All(vol.Coerce(int), vol.Range(min=0, max=1439)),
    }
)
SERVICE_CHARGING_BREAK_CLOCKTIMER_CONFIGURE_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_VIN): cv.string,
        vol.Optional("status_timer_1"): cv.string,
        vol.Optional("starttime_timer_1"): cv.time_period,
        vol.Optional("stoptime_timer_1"): cv.time_period,
        vol.Optional("status_timer_2"): cv.string,
        vol.Optional("starttime_timer_2"): cv.time_period,
        vol.Optional("stoptime_timer_2"): cv.time_period,
        vol.Optional("status_timer_3"): cv.string,
        vol.Optional("starttime_timer_3"): cv.time_period,
        vol.Optional("stoptime_timer_3"): cv.time_period,
        vol.Optional("status_timer_4"): cv.string,
        vol.Optional("starttime_timer_4"): cv.time_period,
        vol.Optional("stoptime_timer_4"): cv.time_period,
    }
)
SERVICE_PREHEAT_START_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_VIN): cv.string,
        vol.Required("type", default=0): vol.All(vol.Coerce(int), vol.Range(min=0, max=1)),
    }
)
SERVICE_SEND_ROUTE_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_VIN): cv.string,
        vol.Required("title"): cv.string,
        vol.Required("latitude"): cv.latitude,
        vol.Required("longitude"): cv.longitude,
        vol.Required("city"): cv.string,
        vol.Required("postcode"): cv.string,
        vol.Required("street"): cv.string,
    }
)
SERVICE_BATTERY_MAX_SOC_CONFIGURE_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_VIN): cv.string,
        vol.Required("max_soc", default=100): vol.All(vol.Coerce(int), vol.In([30, 40, 50, 60, 70, 80, 90, 100])),
        vol.Optional("charge_program", default=0): vol.All(vol.Coerce(int), vol.In([0, 2, 3])),
    }
)
SERVICE_VIN_SCHEMA = vol.Schema({vol.Required(CONF_VIN): cv.string})
SERVICE_VIN_PIN_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_VIN): cv.string,
        vol.Optional(CONF_PIN): cv.string,
    }
)
SERVICE_VIN_TIME_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_VIN): cv.string,
        vol.Required(CONF_TIME): vol.All(vol.Coerce(int), vol.Range(min=0, max=1439)),
    }
)
SERVICE_VIN_CHARGE_PROGRAM_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_VIN): cv.string,
        vol.Required("charge_program", default=0): vol.All(vol.Coerce(int), vol.In([0, 2, 3])),
        vol.Optional("max_soc", default=None): vol.Any(
            None,
            vol.All(vol.Coerce(int), vol.In([50, 60, 70, 80, 90, 100])),
        ),
    }
)
SERVICE_WINDOWS_MOVE_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_VIN): cv.string,
        vol.Optional(CONF_PIN): cv.string,
        vol.Optional("front_left", default=None): vol.Any(
            None,
            vol.All(vol.Coerce(int), vol.In([0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100])),
        ),
        vol.Optional("front_right", default=None): vol.Any(
            None,
            vol.All(vol.Coerce(int), vol.In([0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100])),
        ),
        vol.Optional("rear_left", default=None): vol.Any(
            None,
            vol.All(vol.Coerce(int), vol.In([0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100])),
        ),
        vol.Optional("rear_right", default=None): vol.Any(
            None,
            vol.All(vol.Coerce(int), vol.In([0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100])),
        ),
    }
)
SERVICE_TEMPERATURE_CONFIGURE_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_VIN): cv.string,
        vol.Optional("front_left", default=None): vol.Any(
            None,
            vol.All(
                vol.Coerce(float),
                vol.In(
                    [
                        0,
                        16,
                        16.5,
                        17,
                        17.5,
                        18,
                        18.5,
                        19,
                        19.5,
                        20,
                        20.5,
                        21,
                        21.5,
                        22,
                        22.5,
                        23,
                        23.5,
                        24,
                        24.5,
                        25,
                        25.5,
                        26,
                        26.5,
                        27,
                        27.5,
                        28,
                        30,
                    ]
                ),
            ),
        ),
        vol.Optional("front_right", default=None): vol.Any(
            None,
            vol.All(
                vol.Coerce(float),
                vol.In(
                    [
                        0,
                        16,
                        16.5,
                        17,
                        17.5,
                        18,
                        18.5,
                        19,
                        19.5,
                        20,
                        20.5,
                        21,
                        21.5,
                        22,
                        22.5,
                        23,
                        23.5,
                        24,
                        24.5,
                        25,
                        25.5,
                        26,
                        26.5,
                        27,
                        27.5,
                        28,
                        30,
                    ]
                ),
            ),
        ),
        vol.Optional("rear_left", default=None): vol.Any(
            None,
            vol.All(
                vol.Coerce(float),
                vol.In(
                    [
                        0,
                        16,
                        16.5,
                        17,
                        17.5,
                        18,
                        18.5,
                        19,
                        19.5,
                        20,
                        20.5,
                        21,
                        21.5,
                        22,
                        22.5,
                        23,
                        23.5,
                        24,
                        24.5,
                        25,
                        25.5,
                        26,
                        26.5,
                        27,
                        27.5,
                        28,
                        30,
                    ]
                ),
            ),
        ),
        vol.Optional("rear_right", default=None): vol.Any(
            None,
            vol.All(
                vol.Coerce(float),
                vol.In(
                    [
                        0,
                        16,
                        16.5,
                        17,
                        17.5,
                        18,
                        18.5,
                        19,
                        19.5,
                        20,
                        20.5,
                        21,
                        21.5,
                        22,
                        22.5,
                        23,
                        23.5,
                        24,
                        24.5,
                        25,
                        25.5,
                        26,
                        26.5,
                        27,
                        27.5,
                        28,
                        30,
                    ]
                ),
            ),
        ),
    }
)
SERVICE_PRECONDITIONING_CONFIGURE_SEATS_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_VIN): cv.string,
        vol.Required("front_left"): cv.boolean,
        vol.Required("front_right"): cv.boolean,
        vol.Required("rear_left"): cv.boolean,
        vol.Required("rear_right"): cv.boolean,
    }
)
SERVICE_PRECONDITIONING_CONFIGURE_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_VIN): cv.string,
        vol.Required("departure_time_mode", default=0): vol.All(vol.Coerce(int), vol.In([0, 1, 2])),
        vol.Optional("departure_time", default=0): vol.All(vol.Coerce(int), vol.Range(min=0, max=1439)),
    }
)

ATTR_MB_MANUFACTURER = "Mercedes Benz"


BinarySensors = {
    "chargingactive": [
        "Charging active",
        None,  # Deprecated: DO NOT USE
        "electric",
        "chargingactive",
        "value",
        None,
        None,
        None,
        BinarySensorDeviceClass.BATTERY_CHARGING,
        False,
        None,
        None,
        None,
        None,
    ],
    "liquidRangeCritical": [
        "Liquid Range Critical",
        None,  # Deprecated: DO NOT USE
        "binarysensors",
        "liquidRangeCritical",
        "value",
        None,
        None,
        "mdi:gas-station",
        BinarySensorDeviceClass.PROBLEM,
        False,
        None,
        None,
        None,
        None,
    ],
    "warningbrakefluid": [
        "Low Brake Fluid Warning",
        None,  # Deprecated: DO NOT USE
        "binarysensors",
        "warningbrakefluid",
        "value",
        None,
        None,
        "mdi:car-brake-alert",
        BinarySensorDeviceClass.PROBLEM,
        False,
        None,
        None,
        None,
        None,
    ],
    "warningwashwater": [
     
Download .txt
gitextract_fszsclw1/

├── .devcontainer.json
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   └── config.yml
│   ├── dependabot.yaml
│   └── workflows/
│       ├── HACS_validate.yaml
│       ├── hassfest.yaml
│       └── publish.yaml
├── .gitignore
├── .pre-commit-config.yaml
├── .yamllint.yaml
├── CLAUDE.md
├── LICENSE
├── README.md
├── SECURITY.md
├── custom_components/
│   └── mbapi2020/
│       ├── __init__.py
│       ├── binary_sensor.py
│       ├── button.py
│       ├── car.py
│       ├── client.py
│       ├── config_flow.py
│       ├── const.py
│       ├── coordinator.py
│       ├── device_tracker.py
│       ├── diagnostics.py
│       ├── errors.py
│       ├── helper.py
│       ├── icons.json
│       ├── lock.py
│       ├── manifest.json
│       ├── oauth.py
│       ├── proto/
│       │   ├── acp_pb2.py
│       │   ├── client_pb2.py
│       │   ├── cluster_pb2.py
│       │   ├── eventpush_pb2.py
│       │   ├── gogo_pb2.py
│       │   ├── protos_pb2.py
│       │   ├── service_activation_pb2.py
│       │   ├── user_events_pb2.py
│       │   ├── vehicle_commands_pb2.py
│       │   ├── vehicle_events_pb2.py
│       │   ├── vehicleapi_pb2.py
│       │   └── vin_events_pb2.py
│       ├── repairs.py
│       ├── sensor.py
│       ├── services.py
│       ├── services.yaml
│       ├── switch.py
│       ├── system_health.py
│       ├── translations/
│       │   ├── cs.json
│       │   ├── da.json
│       │   ├── de.json
│       │   ├── en.json
│       │   ├── es.json
│       │   ├── fi.json
│       │   ├── fr.json
│       │   ├── he.json
│       │   ├── it.json
│       │   ├── nb_NO.json
│       │   ├── nl.json
│       │   ├── pl.json
│       │   ├── pt.json
│       │   ├── sv.json
│       │   └── ta.json
│       ├── webapi.py
│       └── websocket.py
├── hacs.json
├── mypi.ini
├── pyproject.toml
├── requirements.txt
├── scripts/
│   ├── burp-redirector.py
│   ├── https-bff.py
│   ├── https-ws-case-429.py
│   └── setup
└── token-requester/
    ├── macOS/
    │   ├── .gitignore
    │   └── MBAPI2020 Token Helper/
    │       ├── MBAPI2020 Token Helper/
    │       │   ├── AppDelegate.swift
    │       │   ├── Assets.xcassets/
    │       │   │   ├── AccentColor.colorset/
    │       │   │   │   └── Contents.json
    │       │   │   ├── AppIcon.appiconset/
    │       │   │   │   └── Contents.json
    │       │   │   └── Contents.json
    │       │   ├── Base.lproj/
    │       │   │   └── Main.storyboard
    │       │   ├── MBAPI2020_Token_Helper.entitlements
    │       │   └── ViewController.swift
    │       ├── MBAPI2020 Token Helper.xcodeproj/
    │       │   ├── project.pbxproj
    │       │   └── project.xcworkspace/
    │       │       └── contents.xcworkspacedata
    │       └── MBAPI2020-Token-Helper-Info.plist
    └── net-core/
        └── mb-token-requester/
            ├── CallbackManager.cs
            ├── DesktopEntryHandler.cs
            ├── Program.cs
            ├── RegistryConfig.cs
            ├── appsettings.json
            ├── callback.bat
            ├── mb-shortcut-handler.desktop
            ├── mb-token-requester.csproj
            └── mb-token-requester.sln
Download .txt
SYMBOL INDEX (330 symbols across 28 files)

FILE: custom_components/mbapi2020/__init__.py
  function async_setup (line 44) | async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
  function async_setup_entry (line 53) | async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEnt...
  function config_entry_update_listener (line 274) | async def config_entry_update_listener(hass: HomeAssistant, config_entry...
  function async_unload_entry (line 280) | async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEn...
  function async_remove_config_entry_device (line 318) | async def async_remove_config_entry_device(
  class MercedesMeEntityDescription (line 326) | class MercedesMeEntityDescription(EntityDescription):
  class MercedesMeEntity (line 333) | class MercedesMeEntity(CoordinatorEntity[MBAPI2020DataUpdateCoordinator]...
    method __init__ (line 338) | def __init__(
    method device_retrieval_status (line 390) | def device_retrieval_status(self):
    method extra_state_attributes (line 398) | def extra_state_attributes(self):
    method device_info (line 433) | def device_info(self) -> DeviceInfo:
    method unit_of_measurement (line 445) | def unit_of_measurement(self):
    method update (line 463) | def update(self):
    method _mercedes_me_update (line 474) | def _mercedes_me_update(self) -> None:
    method _get_car_value (line 478) | def _get_car_value(self, feature, object_name, attrib_name, default_va...
    method _get_car_attribute (line 504) | def _get_car_attribute(self, feature, object_name):
    method pushdata_update_callback (line 517) | def pushdata_update_callback(self):
    method _handle_coordinator_update (line 522) | def _handle_coordinator_update(self) -> None:
    method async_added_to_hass (line 526) | async def async_added_to_hass(self):
    method async_will_remove_from_hass (line 538) | async def async_will_remove_from_hass(self):

FILE: custom_components/mbapi2020/binary_sensor.py
  function _create_binary_sensor_if_eligible (line 27) | def _create_binary_sensor_if_eligible(key, config, car, coordinator):
  function create_missing_binary_sensors_for_car (line 59) | async def create_missing_binary_sensors_for_car(car, coordinator, async_...
  function async_setup_entry (line 76) | async def async_setup_entry(
  class MercedesMEBinarySensor (line 99) | class MercedesMEBinarySensor(MercedesMeEntity, BinarySensorEntity, Resto...
    method flip (line 102) | def flip(self, state):
    method is_on (line 109) | def is_on(self):
    method async_added_to_hass (line 142) | async def async_added_to_hass(self):
    method async_will_remove_from_hass (line 148) | async def async_will_remove_from_hass(self):

FILE: custom_components/mbapi2020/button.py
  function async_setup_entry (line 19) | async def async_setup_entry(
  class MercedesMEButton (line 52) | class MercedesMEButton(MercedesMeEntity, ButtonEntity):
    method async_press (line 55) | async def async_press(self) -> None:
    method update (line 61) | def update(self):

FILE: custom_components/mbapi2020/car.py
  class Car (line 214) | class Car:
    method __init__ (line 217) | def __init__(self, vin: str):
    method is_owner (line 262) | def is_owner(self):
    method is_owner (line 267) | def is_owner(self, value: bool):
    method full_updatemessages_received (line 271) | def full_updatemessages_received(self):
    method partital_updatemessages_received (line 276) | def partital_updatemessages_received(self):
    method last_message_received (line 281) | def last_message_received(self):
    method last_message_received (line 289) | def last_message_received(self, value):
    method data_collection_mode (line 293) | def data_collection_mode(self):
    method data_collection_mode (line 298) | def data_collection_mode(self, value):
    method last_command_type (line 303) | def last_command_type(self):
    method last_command_type (line 308) | def last_command_type(self, value):
    method last_command_state (line 312) | def last_command_state(self):
    method last_command_state (line 317) | def last_command_state(self, value):
    method last_command_error_code (line 321) | def last_command_error_code(self):
    method last_command_error_code (line 326) | def last_command_error_code(self, value):
    method last_command_error_message (line 330) | def last_command_error_message(self):
    method last_command_error_message (line 335) | def last_command_error_message(self, value):
    method add_update_listener (line 338) | def add_update_listener(self, listener):
    method remove_update_callback (line 342) | def remove_update_callback(self, listener):
    method add_sensor (line 346) | def add_sensor(self, unique_id: str):
    method remove_sensor (line 351) | def remove_sensor(self, unique_id: str):
    method publish_updates (line 356) | def publish_updates(self):
    method check_capabilities (line 361) | def check_capabilities(self, required_capabilities: list[str]) -> bool:
  class Tires (line 367) | class Tires:
  class Wipers (line 374) | class Wipers:
  class Odometer (line 381) | class Odometer:
  class RcpOptions (line 388) | class RcpOptions:
  class Windows (line 395) | class Windows:
  class Doors (line 402) | class Doors:
  class Electric (line 409) | class Electric:
  class Auxheat (line 416) | class Auxheat:
  class Precond (line 423) | class Precond:
  class BinarySensors (line 430) | class BinarySensors:
  class RemoteStart (line 437) | class RemoteStart:
  class CarAlarm (line 444) | class CarAlarm:
  class Location (line 451) | class Location:
  class GeofenceEvents (line 458) | class GeofenceEvents:
    method __post_init__ (line 466) | def __post_init__(self):
  class CarAttribute (line 472) | class CarAttribute:
    method __init__ (line 475) | def __init__(self, value, retrievalstatus, timestamp, display_value=No...

FILE: custom_components/mbapi2020/client.py
  class Client (line 74) | class Client:
    method __init__ (line 77) | def __init__(
    method pin (line 126) | def pin(self) -> str:
    method excluded_cars (line 134) | def excluded_cars(self):
    method on_data (line 141) | def on_data(self, data):
    method attempt_connect (line 273) | async def attempt_connect(self, callback_dataload_complete, coordinato...
    method _build_car (line 281) | def _build_car(self, received_car_data, update_mode, is_rest_data=False):
    method _get_car_values (line 408) | def _get_car_values(self, car_detail, vin, class_instance, options, up...
    method _get_car_values_handle_generic (line 456) | def _get_car_values_handle_generic(self, car_detail, class_instance, o...
    method _get_car_values_handle_max_soc (line 494) | def _get_car_values_handle_max_soc(
    method _get_car_values_handle_chargeflap (line 543) | def _get_car_values_handle_chargeflap(self, car_detail, class_instance...
    method _get_car_values_handle_chargeinletcoupler (line 583) | def _get_car_values_handle_chargeinletcoupler(self, car_detail, class_...
    method _get_car_values_handle_chargeinletlock (line 624) | def _get_car_values_handle_chargeinletlock(self, car_detail, class_ins...
    method _get_car_values_handle_chargePrograms (line 665) | def _get_car_values_handle_chargePrograms(self, car_detail, class_inst...
    method _get_car_values_handle_charging_power_restriction (line 687) | def _get_car_values_handle_charging_power_restriction(self, car_detail...
    method _get_car_values_handle_endofchargetime (line 714) | def _get_car_values_handle_endofchargetime(self, car_detail, class_ins...
    method _get_car_values_handle_charging_break_clock_timer (line 843) | def _get_car_values_handle_charging_break_clock_timer(self, car_detail...
    method _get_car_values_handle_ignitionstate (line 866) | def _get_car_values_handle_ignitionstate(self, car_detail, class_insta...
    method _get_car_values_handle_precond_status (line 876) | def _get_car_values_handle_precond_status(self, car_detail, class_inst...
    method _get_car_values_handle_temperature_points (line 915) | def _get_car_values_handle_temperature_points(self, car_detail, class_...
    method _create_synthetic_window_status_overall (line 938) | def _create_synthetic_window_status_overall(self, car_data, vin):
    method _get_car_value (line 1013) | def _get_car_value(self, class_instance, object_name, attrib_name, def...
    method _process_rest_vep_update (line 1020) | def _process_rest_vep_update(self, data):
    method _process_vep_updates (line 1076) | def _process_vep_updates(self, data):
    method _process_assigned_vehicles (line 1148) | def _process_assigned_vehicles(self, data):
    method _process_apptwin_command_status_updates_by_vin (line 1188) | def _process_apptwin_command_status_updates_by_vin(self, data):
    method charge_program_configure (line 1232) | async def charge_program_configure(self, vin: str, program: int, max_s...
    method charging_break_clocktimer_configure (line 1269) | async def charging_break_clocktimer_configure(
    method doors_unlock (line 1371) | async def doors_unlock(self, vin: str, pin: str = ""):
    method doors_unlock_with_pin (line 1394) | async def doors_unlock_with_pin(self, vin: str, pin: str):
    method doors_lock (line 1418) | async def doors_lock(self, vin: str):
    method download_images (line 1438) | async def download_images(self, vin: str):
    method auxheat_configure (line 1459) | async def auxheat_configure(self, vin: str, time_selection: int, time_...
    method auxheat_start (line 1484) | async def auxheat_start(self, vin: str):
    method auxheat_stop (line 1505) | async def auxheat_stop(self, vin: str):
    method battery_max_soc_configure (line 1526) | async def battery_max_soc_configure(self, vin: str, max_soc: int, char...
    method engine_start (line 1568) | async def engine_start(self, vin: str, pin: str = ""):
    method engine_stop (line 1600) | async def engine_stop(self, vin: str):
    method hv_battery_start_conditioning (line 1621) | async def hv_battery_start_conditioning(self, vin: str):
    method hv_battery_stop_conditioning (line 1642) | async def hv_battery_stop_conditioning(self, vin: str):
    method send_route_to_car (line 1663) | async def send_route_to_car(
    method sigpos_start (line 1680) | async def sigpos_start(self, vin: str):
    method sunroof_open (line 1701) | async def sunroof_open(self, vin: str, pin: str = ""):
    method sunroof_tilt (line 1733) | async def sunroof_tilt(self, vin: str, pin: str = ""):
    method sunroof_close (line 1765) | async def sunroof_close(self, vin: str):
    method preconditioning_configure_seats (line 1786) | async def preconditioning_configure_seats(
    method preheat_start (line 1816) | async def preheat_start(self, vin: str):
    method preheat_start_immediate (line 1837) | async def preheat_start_immediate(self, vin: str):
    method preheat_start_universal (line 1858) | async def preheat_start_universal(self, vin: str) -> None:
    method preheat_start_departure_time (line 1865) | async def preheat_start_departure_time(self, vin: str, departure_time:...
    method preheat_stop (line 1886) | async def preheat_stop(self, vin: str):
    method preheat_stop_departure_time (line 1905) | async def preheat_stop_departure_time(self, vin: str):
    method preconditioning_configure (line 1911) | async def preconditioning_configure(self, vin: str, departure_time_mod...
    method temperature_configure (line 1941) | async def temperature_configure(
    method windows_open (line 2021) | async def windows_open(self, vin: str, pin: str = ""):
    method windows_close (line 2055) | async def windows_close(self, vin: str):
    method windows_move (line 2076) | async def windows_move(
    method execute_car_command (line 2137) | async def execute_car_command(self, message):
    method _is_car_feature_available (line 2142) | def _is_car_feature_available(self, vin: str, feature: str = "", featu...
    method _write_debug_output (line 2157) | def _write_debug_output(self, data, datatype):
    method __write_debug_output (line 2161) | def __write_debug_output(self, data, datatype):
    method write_debug_json_output (line 2173) | def write_debug_json_output(self, data, datatype, use_dumps: bool = Fa...
    method set_rlock_mode (line 2188) | async def set_rlock_mode(self):
    method update_poll_states (line 2209) | async def update_poll_states(self, vin: str):
    method _safe_create_on_dataload_complete_task (line 2251) | def _safe_create_on_dataload_complete_task(self):

FILE: custom_components/mbapi2020/config_flow.py
  class ConfigFlow (line 67) | class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
    method __init__ (line 72) | def __init__(self):
    method async_step_user (line 80) | async def async_step_user(self, user_input=None):
    method async_step_credentials (line 89) | async def async_step_credentials(self, user_input=None):
    method async_step_pin (line 170) | async def async_step_pin(self, user_input=None):
    method async_step_reauth (line 206) | async def async_step_reauth(self, user_input=None):
    method async_get_options_flow (line 223) | def async_get_options_flow(config_entry):
  class OptionsFlowHandler (line 228) | class OptionsFlowHandler(config_entries.OptionsFlow):
    method __init__ (line 231) | def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
    method async_step_init (line 238) | async def async_step_init(self, user_input=None):

FILE: custom_components/mbapi2020/const.py
  class SensorConfigFields (line 1745) | class SensorConfigFields(Enum):
  class DefaultValueModeType (line 1779) | class DefaultValueModeType(StrEnum):

FILE: custom_components/mbapi2020/coordinator.py
  class MBAPI2020DataUpdateCoordinator (line 30) | class MBAPI2020DataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any...
    method __init__ (line 33) | def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> ...
    method _async_update_data (line 54) | async def _async_update_data(self) -> dict[str, Car]:
    method on_dataload_complete (line 67) | async def on_dataload_complete(self):
    method ws_connect (line 76) | async def ws_connect(self):
    method check_missing_sensors_for_vin (line 81) | async def check_missing_sensors_for_vin(self, vin: str):

FILE: custom_components/mbapi2020/device_tracker.py
  function async_setup_entry (line 25) | async def async_setup_entry(
  class MercedesMEDeviceTracker (line 54) | class MercedesMEDeviceTracker(MercedesMeEntity, TrackerEntity, RestoreEn...
    method latitude (line 58) | def latitude(self) -> float | None:
    method longitude (line 69) | def longitude(self) -> float | None:
    method source_type (line 80) | def source_type(self):
    method device_class (line 85) | def device_class(self):

FILE: custom_components/mbapi2020/diagnostics.py
  function async_get_config_entry_diagnostics (line 16) | async def async_get_config_entry_diagnostics(

FILE: custom_components/mbapi2020/errors.py
  class MbapiError (line 8) | class MbapiError(HomeAssistantError):
  class WebsocketError (line 12) | class WebsocketError(MbapiError):
  class RequestError (line 16) | class RequestError(MbapiError):
  class MBAuthError (line 20) | class MBAuthError(ConfigEntryAuthFailed):
  class MBAuth2FAError (line 24) | class MBAuth2FAError(ConfigEntryAuthFailed):
  class MBLegalTermsError (line 28) | class MBLegalTermsError(ConfigEntryAuthFailed):

FILE: custom_components/mbapi2020/helper.py
  class LogHelper (line 41) | class LogHelper:
    method Mask_VIN (line 45) | def Mask_VIN(vin: str) -> str:
    method Mask_email (line 51) | def Mask_email(email: str) -> str:
  class UrlHelper (line 57) | class UrlHelper:
    method Rest_url (line 61) | def Rest_url(region: str) -> str:
    method Websocket_url (line 73) | def Websocket_url(region: str) -> str:
    method Widget_url (line 85) | def Widget_url(region: str) -> str:
    method Device_code_confirm_url (line 99) | def Device_code_confirm_url(region: str, device_code: str) -> str:
    method RCP_url (line 116) | def RCP_url(region: str) -> str:
    method PSAG_url (line 124) | def PSAG_url(region: str) -> str:
    method Login_Base_Url (line 132) | def Login_Base_Url(region: str) -> str:
    method Login_App_Id (line 140) | def Login_App_Id(region: str) -> str:
  class CoordinatesHelper (line 148) | class CoordinatesHelper:
    method _transform_lat (line 150) | def _transform_lat(lon, lat):
    method _transform_lon (line 164) | def _transform_lon(lon, lat):
    method wgs84_to_gcj02 (line 178) | def wgs84_to_gcj02(lon, lat):
    method gcj02_to_wgs84 (line 201) | def gcj02_to_wgs84(gcj_lon, gcj_lat):
  function get_class_property_names (line 224) | def get_class_property_names(obj: object):
  class MBJSONEncoder (line 229) | class MBJSONEncoder(json.JSONEncoder):
    method default (line 232) | def default(self, o) -> str | dict:  # noqa: D102
  class Watchdog (line 242) | class Watchdog:
    method __init__ (line 245) | def __init__(self, action: Callable[..., Awaitable], timeout_seconds: ...
    method cancel (line 255) | def cancel(self, *, graceful: bool = True):
    method on_expire (line 280) | async def on_expire(self):
    method trigger (line 290) | async def trigger(self):
    method _on_timer_expire (line 304) | def _on_timer_expire(self):
    method _on_expire_task_done (line 312) | def _on_expire_task_done(self, task: asyncio.Task):

FILE: custom_components/mbapi2020/lock.py
  function async_setup_entry (line 23) | async def async_setup_entry(
  class MercedesMELock (line 55) | class MercedesMELock(MercedesMeEntity, LockEntity, RestoreEntity):
    method async_lock (line 58) | async def async_lock(self, **kwargs):
    method async_unlock (line 78) | async def async_unlock(self, **kwargs):
    method is_locked (line 108) | def is_locked(self):
    method code_format (line 121) | def code_format(self):

FILE: custom_components/mbapi2020/oauth.py
  class Oauth (line 56) | class Oauth:
    method __init__ (line 64) | def __init__(
    method _generate_pkce_parameters (line 88) | def _generate_pkce_parameters(self) -> tuple[str, str]:
    method _ensure_pkce_parameters (line 105) | def _ensure_pkce_parameters(self) -> None:
    method async_login_new (line 110) | async def async_login_new(self, email: str, password: str) -> dict[str...
    method _get_mobile_safari_headers (line 181) | def _get_mobile_safari_headers(
    method _extract_code_from_redirect_url (line 201) | def _extract_code_from_redirect_url(self, redirect_url: str) -> str:
    method _get_authorization_resume (line 212) | async def _get_authorization_resume(self) -> str:
    method _send_user_agent_info (line 248) | async def _send_user_agent_info(self) -> None:
    method _submit_username (line 267) | async def _submit_username(self, email: str) -> None:
    method _submit_password (line 277) | async def _submit_password(self, email: str, password: str) -> dict[st...
    method _submit_legal_consent (line 298) | async def _submit_legal_consent(self, home_country: str, consent_count...
    method _resume_authorization (line 317) | async def _resume_authorization(self, resume_url: str, token: str) -> ...
    method _exchange_code_for_tokens (line 356) | async def _exchange_code_for_tokens(self, code: str) -> dict[str, Any]:
    method request_access_token_with_pin (line 384) | async def request_access_token_with_pin(self, email: str, pin: str, no...
    method request_pin (line 410) | async def request_pin(self, email: str, nonce: str):
    method async_refresh_access_token (line 424) | async def async_refresh_access_token(self, refresh_token: str, is_retr...
    method async_get_cached_token (line 462) | async def async_get_cached_token(self):
    method is_token_expired (line 491) | def is_token_expired(cls, token_info) -> bool:
    method _save_token_info (line 498) | def _save_token_info(self, token_info):
    method _add_custom_values_to_token_info (line 516) | def _add_custom_values_to_token_info(cls, token_info):
    method _get_header (line 521) | def _get_header(self):
    method _get_region_header (line 540) | def _get_region_header(self, header):
    method _async_request (line 562) | async def _async_request(self, method: str, url: str, data: str = "", ...

FILE: custom_components/mbapi2020/repairs.py
  class RestartRequiredFixFlow (line 16) | class RestartRequiredFixFlow(RepairsFlow):
    method __init__ (line 19) | def __init__(self, issue_id: str) -> None:
    method async_step_init (line 22) | async def async_step_init(self, user_input: dict[str, str] | None = No...
    method async_step_confirm_restart (line 27) | async def async_step_confirm_restart(self, user_input: dict[str, str] ...
  function async_create_fix_flow (line 40) | async def async_create_fix_flow(

FILE: custom_components/mbapi2020/sensor.py
  function _create_sensor_if_eligible (line 30) | def _create_sensor_if_eligible(key, config, car, coordinator, should_pol...
  function create_missing_sensors_for_car (line 73) | async def create_missing_sensors_for_car(car, coordinator, async_add_ent...
  function async_setup_entry (line 101) | async def async_setup_entry(
  class MercedesMESensor (line 129) | class MercedesMESensor(MercedesMeEntity, RestoreSensor):
    method native_value (line 133) | def native_value(self) -> str | int | float | datetime | None:
    method state (line 138) | def state(self):
    method async_added_to_hass (line 169) | async def async_added_to_hass(self):
    method async_will_remove_from_hass (line 175) | async def async_will_remove_from_hass(self):
  class MercedesMESensorPoll (line 182) | class MercedesMESensorPoll(MercedesMeEntity, RestoreSensor):
    method native_value (line 186) | def native_value(self) -> str | int | float | datetime | None:
    method state (line 191) | def state(self):
    method async_added_to_hass (line 202) | async def async_added_to_hass(self):
    method async_will_remove_from_hass (line 208) | async def async_will_remove_from_hass(self):

FILE: custom_components/mbapi2020/services.py
  function setup_services (line 57) | def setup_services(hass: HomeAssistant) -> None:
  function remove_services (line 310) | def remove_services(hass: HomeAssistant) -> None:

FILE: custom_components/mbapi2020/switch.py
  class MercedesMeSwitchEntityDescription (line 28) | class MercedesMeSwitchEntityDescription(MercedesMeEntityDescription, Swi...
  class MercedesMeSwitch (line 60) | class MercedesMeSwitch(MercedesMeEntity, SwitchEntity, RestoreEntity):
    method __init__ (line 63) | def __init__(self, description: MercedesMeSwitchEntityDescription, vin...
    method async_turn_on (line 73) | async def async_turn_on(self, **kwargs: dict) -> None:
    method async_turn_off (line 77) | async def async_turn_off(self, **kwargs: dict) -> None:
    method _async_handle_state_change (line 81) | async def _async_handle_state_change(self, state: bool, **kwargs) -> N...
    method _reset_expected_state (line 116) | async def _reset_expected_state(self, _):
    method _mercedes_me_update (line 123) | def _mercedes_me_update(self) -> None:
    method assumed_state (line 147) | def assumed_state(self) -> bool:
  function async_setup_entry (line 152) | async def async_setup_entry(

FILE: custom_components/mbapi2020/system_health.py
  function async_register (line 14) | def async_register(hass: HomeAssistant, register: system_health.SystemHe...
  function system_health_info (line 19) | async def system_health_info(hass: HomeAssistant):

FILE: custom_components/mbapi2020/webapi.py
  class WebApi (line 36) | class WebApi:
    method __init__ (line 39) | def __init__(
    method _request (line 53) | async def _request(
    method get_config (line 133) | async def get_config(self):
    method get_user (line 137) | async def get_user(self):
    method get_user_info (line 141) | async def get_user_info(self):
    method get_car_capabilities (line 145) | async def get_car_capabilities(self, vin: str):
    method get_car_capabilities_commands (line 149) | async def get_car_capabilities_commands(self, vin: str):
    method get_car_rcp_supported_settings (line 153) | async def get_car_rcp_supported_settings(self, vin: str):
    method get_car_rcp_settings (line 160) | async def get_car_rcp_settings(self, vin: str, setting: str):
    method send_route_to_car (line 167) | async def send_route_to_car(
    method get_car_geofencing_violations (line 195) | async def get_car_geofencing_violations(self, vin: str):
    method get_fleet_info (line 200) | async def get_fleet_info(self, company: str, fleet: str):
    method is_car_rcp_supported (line 205) | async def is_car_rcp_supported(self, vin: str, **kwargs):
    method download_images (line 226) | async def download_images(self, vin: str):
    method get_car_p2b_data_via_rest (line 231) | async def get_car_p2b_data_via_rest(self, vin: str):

FILE: custom_components/mbapi2020/websocket.py
  class _PrefixAdapter (line 57) | class _PrefixAdapter(logging.LoggerAdapter):
    method process (line 60) | def process(self, msg, kwargs):
  class Websocket (line 65) | class Websocket:
    method __init__ (line 70) | def __init__(
    method _reconnect_attempt (line 135) | async def _reconnect_attempt(self) -> None:
    method async_connect (line 148) | async def async_connect(self, on_data=None) -> None:
    method _async_connect_internal (line 154) | async def _async_connect_internal(self, on_data=None) -> None:
    method async_stop (line 202) | async def async_stop(self, now: datetime = datetime.now()):
    method initiatiate_connection_disconnect_with_reconnect (line 250) | async def initiatiate_connection_disconnect_with_reconnect(self):
    method _graceful_shutdown_and_optionally_reconnect (line 270) | async def _graceful_shutdown_and_optionally_reconnect(self):
    method ping (line 277) | async def ping(self):
    method call (line 288) | async def call(self, message, car_command: bool = False):
    method _start_queue_handler (line 317) | async def _start_queue_handler(self):
    method _queue_handler (line 321) | async def _queue_handler(self):
    method _start_websocket_handler (line 371) | async def _start_websocket_handler(self, session: ClientSession):
    method _websocket_handler (line 447) | async def _websocket_handler(self, session: ClientSession, **kwargs):
    method _websocket_connection_headers (line 490) | async def _websocket_connection_headers(self):
    method _get_region_header (line 509) | def _get_region_header(self, header) -> list:
    method _blocked_account_reload_check (line 535) | async def _blocked_account_reload_check(self):
    method _should_trigger_backup_reload (line 562) | def _should_trigger_backup_reload(self, current_time: float) -> bool:
    method _await_tasks_then_cleanup (line 601) | async def _await_tasks_then_cleanup(self):
    method _cleanup_tasks (line 635) | async def _cleanup_tasks(self):
    method _cleanup_queue (line 664) | async def _cleanup_queue(self, caller: str = "unknown"):
    method _set_watchdog_timeout (line 686) | def _set_watchdog_timeout(self, timeout: int) -> None:
    method _reset_watchdog_timeout (line 699) | def _reset_watchdog_timeout(self) -> None:

FILE: scripts/burp-redirector.py
  class BurpExtender (line 11) | class BurpExtender(IBurpExtender, IHttpListener):
    method registerExtenderCallbacks (line 16) | def registerExtenderCallbacks(self, callbacks):
    method processHttpMessage (line 30) | def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo):

FILE: scripts/https-bff.py
  class SecureHTTPRequestHandler (line 11) | class SecureHTTPRequestHandler(SimpleHTTPRequestHandler):
    method translate_path (line 14) | def translate_path(self, path):
  function set_logger (line 31) | def set_logger():
  function start_server (line 44) | def start_server(host="127.0.0.1", port=8002, certfile="../local/selfsig...

FILE: scripts/https-ws-case-429.py
  class MBAPI2020SimulatorServer (line 16) | class MBAPI2020SimulatorServer(BaseHTTPRequestHandler):
    method do_GET (line 19) | def do_GET(self):
    method do_POST (line 30) | def do_POST(self):
  function set_logger (line 35) | def set_logger():

FILE: token-requester/net-core/mb-token-requester/CallbackManager.cs
  class CallbackManager (line 5) | class CallbackManager
    method CallbackManager (line 9) | public CallbackManager(string name)
    method RunClient (line 16) | public async Task RunClient(string args)
    method RunServer (line 29) | public async Task<string> RunServer(CancellationToken? token = null)

FILE: token-requester/net-core/mb-token-requester/DesktopEntryHandler.cs
  class DesktopEntryHandler (line 8) | [System.Runtime.Versioning.SupportedOSPlatform("linux")]
    method DesktopEntryHandler (line 11) | public DesktopEntryHandler(string uriScheme)

FILE: token-requester/net-core/mb-token-requester/Program.cs
  class Program (line 10) | class Program
    method Main (line 14) | static async Task Main(string[] args)
    method ProcessCallback (line 55) | private static async Task ProcessCallback(string args)
    method Run (line 73) | static async Task Run()
    method SignIn (line 103) | private async Task SignIn()
    method GetConsoleWindow (line 187) | [DllImport("kernel32.dll", ExactSpelling = true)]
    method SetForegroundWindow (line 191) | [DllImport("user32.dll")]
    method BringConsoleToFront (line 196) | [System.Runtime.Versioning.SupportedOSPlatform("windows")]

FILE: token-requester/net-core/mb-token-requester/RegistryConfig.cs
  class RegistryConfig (line 7) | [System.Runtime.Versioning.SupportedOSPlatform("windows")]
    method RegistryConfig (line 10) | public RegistryConfig(string uriScheme)
    method Configure (line 15) | public void Configure()
    method DeleteRegKeys (line 20) | public void DeleteRegKeys()
    method NeedToAddKeys (line 50) | bool NeedToAddKeys()
    method AddRegKeys (line 71) | void AddRegKeys()
Condensed preview — 94 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,318K chars).
[
  {
    "path": ".devcontainer.json",
    "chars": 1457,
    "preview": "{\n  \"name\": \"renenulschde/dev-mbapi2020\",\n  \"image\": \"mcr.microsoft.com/devcontainers/python:1-3.12\",\n  \"postCreateComma"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 96,
    "preview": "# These are supported funding model platforms\n\ngithub: ReneNulschDE\nbuy_me_a_coffee: renenulsch\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "chars": 3488,
    "preview": "name: Report an issue with MBAPI2020\ndescription: Report an issue with Mercedes ME integration.\nbody:\n  - type: markdown"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 469,
    "preview": "blank_issues_enabled: false\ncontact_links:\n  - name: Feature Request or other questions\n    url: https://community.home-"
  },
  {
    "path": ".github/dependabot.yaml",
    "chars": 198,
    "preview": "version: 2\nupdates:\n- package-ecosystem: pip\n  directory: \"/\"\n  schedule:\n    interval: daily\n    time: \"04:00\"\n  review"
  },
  {
    "path": ".github/workflows/HACS_validate.yaml",
    "chars": 296,
    "preview": "name: Validate with HACS\n\non:\n  push:\n  pull_request:\n  schedule:\n    - cron: \"0 0 * * *\"\n\njobs:\n  validate:\n    runs-on"
  },
  {
    "path": ".github/workflows/hassfest.yaml",
    "chars": 242,
    "preview": "name: Validate with hassfest\n\non:\n  push:\n  pull_request:\n  schedule:\n    - cron: \"0 0 * * *\"\n\njobs:\n  validate:\n    run"
  },
  {
    "path": ".github/workflows/publish.yaml",
    "chars": 1381,
    "preview": "name: Publish Workflow\n\non:\n  release:\n    types:\n      - published\n\njobs:\n  release:\n    name: Release\n    runs-on: ubu"
  },
  {
    "path": ".gitignore",
    "chars": 1399,
    "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": 1292,
    "preview": "repos:\n  - repo: https://github.com/astral-sh/ruff-pre-commit\n    rev: v0.14.5\n    hooks:\n#      - id: ruff\n#        arg"
  },
  {
    "path": ".yamllint.yaml",
    "chars": 139,
    "preview": "extends: default\n\nrules:\n  # 120 chars should be enough, but don't fail if a line is longer\n  line-length:\n    max: 120\n"
  },
  {
    "path": "CLAUDE.md",
    "chars": 4162,
    "preview": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## "
  },
  {
    "path": "LICENSE",
    "chars": 3586,
    "preview": "MIT License\n\nCopyright (c) 2020 Rene Nulsch\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
  },
  {
    "path": "README.md",
    "chars": 13539,
    "preview": "# \"Mercedes-Benz\" custom component\n\n![HassFest tests](https://github.com/renenulschde/mbapi2020/workflows/Validate%20wit"
  },
  {
    "path": "SECURITY.md",
    "chars": 1275,
    "preview": "# Security Policy\n\n## Reporting Security Issues\n\n**Please do not report security vulnerabilities through public GitHub i"
  },
  {
    "path": "custom_components/mbapi2020/__init__.py",
    "chars": 23592,
    "preview": "\"\"\"The MercedesME 2020 integration.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom collections.abc import C"
  },
  {
    "path": "custom_components/mbapi2020/binary_sensor.py",
    "chars": 4829,
    "preview": "\"\"\"Support for Mercedes cars with Mercedes ME.\n\nFor more details about this component, please refer to the documentation"
  },
  {
    "path": "custom_components/mbapi2020/button.py",
    "chars": 2103,
    "preview": "\"\"\"Button support for Mercedes cars with Mercedes ME.\n\nFor more details about this component, please refer to the docume"
  },
  {
    "path": "custom_components/mbapi2020/car.py",
    "chars": 12811,
    "preview": "\"\"\"Define the objects to store care data.\"\"\"\n\nfrom __future__ import annotations\n\nimport collections\nfrom dataclasses im"
  },
  {
    "path": "custom_components/mbapi2020/client.py",
    "chars": 92762,
    "preview": "\"\"\"The MercedesME 2020 client.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport datetime as dt\nfrom datetim"
  },
  {
    "path": "custom_components/mbapi2020/config_flow.py",
    "chars": 12115,
    "preview": "\"\"\"Config flow for mbapi2020 integration.\"\"\"\n\nfrom __future__ import annotations\n\nfrom copy import deepcopy\nimport uuid\n"
  },
  {
    "path": "custom_components/mbapi2020/const.py",
    "chars": 45143,
    "preview": "\"\"\"Constants for the MercedesME 2020 integration.\"\"\"\n\nfrom __future__ import annotations\n\nfrom datetime import timedelta"
  },
  {
    "path": "custom_components/mbapi2020/coordinator.py",
    "chars": 4479,
    "preview": "\"\"\"DataUpdateCoordinator class for the MBAPI2020 Integration.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfro"
  },
  {
    "path": "custom_components/mbapi2020/device_tracker.py",
    "chars": 2808,
    "preview": "\"\"\"Device Tracker support for Mercedes cars with Mercedes ME.\n\nFor more details about this component, please refer to th"
  },
  {
    "path": "custom_components/mbapi2020/diagnostics.py",
    "chars": 895,
    "preview": "\"\"\"Diagnostics support for MBAPI2020.\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nfrom typing import Any\n\nfrom h"
  },
  {
    "path": "custom_components/mbapi2020/errors.py",
    "chars": 740,
    "preview": "\"\"\"Define package errors.\"\"\"\n\nfrom __future__ import annotations\n\nfrom homeassistant.exceptions import ConfigEntryAuthFa"
  },
  {
    "path": "custom_components/mbapi2020/helper.py",
    "chars": 11796,
    "preview": "\"\"\"Helper functions for MBAPI2020 integration.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport base64\nfrom"
  },
  {
    "path": "custom_components/mbapi2020/icons.json",
    "chars": 1508,
    "preview": "{\n  \"services\": {\n    \"auxheat_configure\": \"mdi:radiator\",\n    \"auxheat_start\": \"mdi:radiator\",\n    \"auxheat_stop\": \"mdi"
  },
  {
    "path": "custom_components/mbapi2020/lock.py",
    "chars": 4352,
    "preview": "\"\"\"Lock Support for Mercedes cars with Mercedes ME.\n\nFor more details about this component, please refer to the document"
  },
  {
    "path": "custom_components/mbapi2020/manifest.json",
    "chars": 439,
    "preview": "{\n  \"domain\": \"mbapi2020\",\n  \"name\": \"MercedesME 2020\",\n  \"codeowners\": [\"@ReneNulschDE\"],\n  \"config_flow\": true,\n  \"dep"
  },
  {
    "path": "custom_components/mbapi2020/oauth.py",
    "chars": 23343,
    "preview": "\"\"\"Integration of optimized Mercedes Me OAuth2 LoginNew functionality.\"\"\"\n\nfrom __future__ import annotations\n\nimport as"
  },
  {
    "path": "custom_components/mbapi2020/proto/acp_pb2.py",
    "chars": 17417,
    "preview": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: acp.proto\n# Protobuf Python Version: 5.29.5\n\"\"\"Gene"
  },
  {
    "path": "custom_components/mbapi2020/proto/client_pb2.py",
    "chars": 4235,
    "preview": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: client.proto\n# Protobuf Python Version: 5.29.5\n\"\"\"G"
  },
  {
    "path": "custom_components/mbapi2020/proto/cluster_pb2.py",
    "chars": 1348,
    "preview": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: cluster.proto\n# Protobuf Python Version: 5.29.5\n\"\"\""
  },
  {
    "path": "custom_components/mbapi2020/proto/eventpush_pb2.py",
    "chars": 1935,
    "preview": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: eventpush.proto\n# Protobuf Python Version: 5.29.5\n\""
  },
  {
    "path": "custom_components/mbapi2020/proto/gogo_pb2.py",
    "chars": 7630,
    "preview": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: gogo.proto\n# Protobuf Python Version: 5.29.5\n\"\"\"Gen"
  },
  {
    "path": "custom_components/mbapi2020/proto/protos_pb2.py",
    "chars": 6988,
    "preview": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: protos.proto\n# Protobuf Python Version: 5.29.5\n\"\"\"G"
  },
  {
    "path": "custom_components/mbapi2020/proto/service_activation_pb2.py",
    "chars": 3503,
    "preview": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: service-activation.proto\n# Protobuf Python Version:"
  },
  {
    "path": "custom_components/mbapi2020/proto/user_events_pb2.py",
    "chars": 6513,
    "preview": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: user-events.proto\n# Protobuf Python Version: 5.29.5"
  },
  {
    "path": "custom_components/mbapi2020/proto/vehicle_commands_pb2.py",
    "chars": 35171,
    "preview": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: vehicle-commands.proto\n# Protobuf Python Version: 5"
  },
  {
    "path": "custom_components/mbapi2020/proto/vehicle_events_pb2.py",
    "chars": 175344,
    "preview": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: vehicle-events.proto\n# Protobuf Python Version: 5.2"
  },
  {
    "path": "custom_components/mbapi2020/proto/vehicleapi_pb2.py",
    "chars": 8502,
    "preview": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: vehicleapi.proto\n# Protobuf Python Version: 5.29.5\n"
  },
  {
    "path": "custom_components/mbapi2020/proto/vin_events_pb2.py",
    "chars": 1169,
    "preview": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: vin-events.proto\n# Protobuf Python Version: 5.29.5\n"
  },
  {
    "path": "custom_components/mbapi2020/repairs.py",
    "chars": 1547,
    "preview": "\"\"\"Repairs platform for MBAPI2020.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nimport voluptuous as "
  },
  {
    "path": "custom_components/mbapi2020/sensor.py",
    "chars": 7272,
    "preview": "\"\"\"Sensor support for Mercedes cars with Mercedes ME.\n\nFor more details about this component, please refer to the docume"
  },
  {
    "path": "custom_components/mbapi2020/services.py",
    "chars": 14840,
    "preview": "\"\"\"Services for the Blink integration.\"\"\"\n\nfrom __future__ import annotations\n\nfrom homeassistant.core import HomeAssist"
  },
  {
    "path": "custom_components/mbapi2020/services.yaml",
    "chars": 19740,
    "preview": "auxheat_configure:\n  description: \"Command for configuring the auxiliary heating. It is possible to define three daytime"
  },
  {
    "path": "custom_components/mbapi2020/switch.py",
    "chars": 8502,
    "preview": "\"\"\"Switch support for Mercedes cars with Mercedes ME.\n\nFor more details about this component, please refer to the docume"
  },
  {
    "path": "custom_components/mbapi2020/system_health.py",
    "chars": 1896,
    "preview": "\"\"\"Provide info to system health.\"\"\"\n\nfrom __future__ import annotations\n\nfrom homeassistant.components import system_he"
  },
  {
    "path": "custom_components/mbapi2020/translations/cs.json",
    "chars": 26226,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"Komponenta je již nakonfigurována.\",\n      \"reauth_successfu"
  },
  {
    "path": "custom_components/mbapi2020/translations/da.json",
    "chars": 26236,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"Komponenten er allerede konfigureret.\",\n      \"reauth_succes"
  },
  {
    "path": "custom_components/mbapi2020/translations/de.json",
    "chars": 26068,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"Komponente ist bereits konfiguriert.\",\n      \"reauth_success"
  },
  {
    "path": "custom_components/mbapi2020/translations/en.json",
    "chars": 25851,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"Component is configured already.\",\n      \"reauth_successful\""
  },
  {
    "path": "custom_components/mbapi2020/translations/es.json",
    "chars": 28164,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"El componente ya está configurado.\",\n      \"reauth_successfu"
  },
  {
    "path": "custom_components/mbapi2020/translations/fi.json",
    "chars": 26447,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"Komponentti on jo määritetty.\",\n      \"reauth_successful\": \""
  },
  {
    "path": "custom_components/mbapi2020/translations/fr.json",
    "chars": 28276,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"Le composant est deja configure.\",\n      \"reauth_successful\""
  },
  {
    "path": "custom_components/mbapi2020/translations/he.json",
    "chars": 23903,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"הרכיב כבר מוגדר.\",\n      \"reauth_successful\": \"האימות החוזר "
  },
  {
    "path": "custom_components/mbapi2020/translations/it.json",
    "chars": 27825,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"Componente già configurato.\",\n      \"reauth_successful\": \"Ri"
  },
  {
    "path": "custom_components/mbapi2020/translations/nb_NO.json",
    "chars": 26066,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"Komponenten er allerede konfigurert.\",\n      \"reauth_success"
  },
  {
    "path": "custom_components/mbapi2020/translations/nl.json",
    "chars": 27823,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"Component is al geconfigureerd.\",\n      \"reauth_successful\":"
  },
  {
    "path": "custom_components/mbapi2020/translations/pl.json",
    "chars": 26713,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"Komponent jest już skonfigurowany.\",\n      \"reauth_successfu"
  },
  {
    "path": "custom_components/mbapi2020/translations/pt.json",
    "chars": 27728,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"O componente já está configurado.\",\n      \"reauth_successful"
  },
  {
    "path": "custom_components/mbapi2020/translations/sv.json",
    "chars": 26646,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"Komponenten är redan konfigurerad.\",\n      \"reauth_successfu"
  },
  {
    "path": "custom_components/mbapi2020/translations/ta.json",
    "chars": 34561,
    "preview": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"கூறு ஏற்கனவே கட்டமைக்கப்பட்டுள்ளது.\",\n          "
  },
  {
    "path": "custom_components/mbapi2020/webapi.py",
    "chars": 9018,
    "preview": "\"\"\"Define an object to interact with the REST API.\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport logging\nim"
  },
  {
    "path": "custom_components/mbapi2020/websocket.py",
    "chars": 30047,
    "preview": "\"\"\"Define an object to interact with the Websocket API.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom coll"
  },
  {
    "path": "hacs.json",
    "chars": 152,
    "preview": "{\n    \"name\": \"MercedesME 2020\",\n    \"homeassistant\": \"2024.02.0\",\n    \"zip_release\": true,\n    \"filename\": \"mbapi2020.z"
  },
  {
    "path": "mypi.ini",
    "chars": 1935,
    "preview": "# Automatically generated by hassfest.\n#\n# To update, run python3 -m script.hassfest -p mypy_config\n\n[mypy]\npython_versi"
  },
  {
    "path": "pyproject.toml",
    "chars": 22509,
    "preview": "[tool.pylint.MAIN]\npy-version = \"3.14\"\n# Use a conservative default here; 2 should speed up most setups and not hurt\n# a"
  },
  {
    "path": "requirements.txt",
    "chars": 27,
    "preview": "protobuf>=3.19.1\npre-commit"
  },
  {
    "path": "scripts/burp-redirector.py",
    "chars": 1177,
    "preview": "from burp import IBurpExtender, IHttpListener\n\nHOST_TO = \"localhost\"\n\nSERVER_PORT_MAP = {\n    \"bff.emea-prod.mobilesdk.m"
  },
  {
    "path": "scripts/https-bff.py",
    "chars": 2010,
    "preview": "from http.server import HTTPServer, SimpleHTTPRequestHandler\nimport logging\nimport os\nfrom pathlib import Path\nimport ss"
  },
  {
    "path": "scripts/https-ws-case-429.py",
    "chars": 1776,
    "preview": "\"\"\"Simple HTTP Server to simulate http429.\"\"\"\n\nfrom __future__ import annotations\n\nfrom http.server import BaseHTTPReque"
  },
  {
    "path": "scripts/setup",
    "chars": 231,
    "preview": "#!/usr/bin/env bash\n\nset -e\n\ncd \"$(dirname \"$0\")/..\"\nsudo apt-get update\nsudo apt-get install -y --no-install-recommends"
  },
  {
    "path": "token-requester/macOS/.gitignore",
    "chars": 1993,
    "preview": "# Xcode\n#\n# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore\n\n"
  },
  {
    "path": "token-requester/macOS/MBAPI2020 Token Helper/MBAPI2020 Token Helper/AppDelegate.swift",
    "chars": 563,
    "preview": "//\n//  AppDelegate.swift\n//  MBAPI2020 Token Helper\n//\n//  Created by Rene Nulsch on 27.12.24.\n//\n\nimport Cocoa\n\n@main\nc"
  },
  {
    "path": "token-requester/macOS/MBAPI2020 Token Helper/MBAPI2020 Token Helper/Assets.xcassets/AccentColor.colorset/Contents.json",
    "chars": 123,
    "preview": "{\n  \"colors\" : [\n    {\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }"
  },
  {
    "path": "token-requester/macOS/MBAPI2020 Token Helper/MBAPI2020 Token Helper/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "chars": 778,
    "preview": "{\"info\":{\"version\":1,\"author\":\"xcode\"},\"images\":[{\"scale\":\"1x\",\"size\":\"16x16\",\"filename\":\"mac16pt1x.png\",\"idiom\":\"mac\"},"
  },
  {
    "path": "token-requester/macOS/MBAPI2020 Token Helper/MBAPI2020 Token Helper/Assets.xcassets/Contents.json",
    "chars": 63,
    "preview": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "token-requester/macOS/MBAPI2020 Token Helper/MBAPI2020 Token Helper/Base.lproj/Main.storyboard",
    "chars": 69782,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB\" version=\"3.0\" t"
  },
  {
    "path": "token-requester/macOS/MBAPI2020 Token Helper/MBAPI2020 Token Helper/MBAPI2020_Token_Helper.entitlements",
    "chars": 365,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "token-requester/macOS/MBAPI2020 Token Helper/MBAPI2020 Token Helper/ViewController.swift",
    "chars": 8028,
    "preview": "import Cocoa\nimport AuthenticationServices\nimport CryptoKit\n\nclass ViewController: NSViewController, ASWebAuthentication"
  },
  {
    "path": "token-requester/macOS/MBAPI2020 Token Helper/MBAPI2020 Token Helper.xcodeproj/project.pbxproj",
    "chars": 11802,
    "preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 77;\n\tobjects = {\n\n/* Begin PBXFileReference secti"
  },
  {
    "path": "token-requester/macOS/MBAPI2020 Token Helper/MBAPI2020 Token Helper.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "chars": 135,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:\">\n   </FileRef"
  },
  {
    "path": "token-requester/macOS/MBAPI2020 Token Helper/MBAPI2020-Token-Helper-Info.plist",
    "chars": 458,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "token-requester/net-core/mb-token-requester/CallbackManager.cs",
    "chars": 1275,
    "preview": "using System.IO.Pipes;\n\nnamespace mbtokenrequester\n{\n    class CallbackManager\n    {\n        private readonly string _n"
  },
  {
    "path": "token-requester/net-core/mb-token-requester/DesktopEntryHandler.cs",
    "chars": 287,
    "preview": "using System;\nusing System.IO;\nusing System.Reflection;\nusing System.Runtime;\n\nnamespace mbtokenrequester\n{\n    [System"
  },
  {
    "path": "token-requester/net-core/mb-token-requester/Program.cs",
    "chars": 6435,
    "preview": "using IdentityModel.Client;\nusing IdentityModel.OidcClient;\nusing System.Diagnostics;\nusing System.Runtime.InteropServi"
  },
  {
    "path": "token-requester/net-core/mb-token-requester/RegistryConfig.cs",
    "chars": 3740,
    "preview": "using System.Reflection;\nusing Microsoft.Win32;\nusing System.Runtime;\n\nnamespace mbtokenrequester\n{\n    [System.Runtime"
  },
  {
    "path": "token-requester/net-core/mb-token-requester/appsettings.json",
    "chars": 782,
    "preview": "{\n    \"AppSettings\": {\n        \"Authority\": \"https://id.mercedes-benz.com/\",\n        \"ClientId\": \"62778dc4-1de3-44f4-af9"
  },
  {
    "path": "token-requester/net-core/mb-token-requester/callback.bat",
    "chars": 43,
    "preview": "@echo off\n\"%~dp0mb-token-requester.exe\" %*\n"
  },
  {
    "path": "token-requester/net-core/mb-token-requester/mb-shortcut-handler.desktop",
    "chars": 165,
    "preview": "[Desktop Entry]\nVersion=1.0\nType=Application\nName=MBTockenCreator\nExec=~/temp/linux-x64/mb-token-requester %u\nIcon=\nMime"
  },
  {
    "path": "token-requester/net-core/mb-token-requester/mb-token-requester.csproj",
    "chars": 2265,
    "preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<OutputType>Exe</OutputType>\n\t\t<TargetFramework>net8.0</TargetFra"
  },
  {
    "path": "token-requester/net-core/mb-token-requester/mb-token-requester.sln",
    "chars": 1337,
    "preview": "\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 17\nVisualStudioVersion = 17.12.355"
  }
]

About this extraction

This page contains the full source code of the ReneNulschDE/mbapi2020 GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 94 files (1.1 MB), approximately 319.7k tokens, and a symbol index with 330 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!