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/).
[](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/).
[](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
   
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": [
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
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/..\"\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.