[mybmw] new binding contribution (#12006)

* solve pom.xml conflict

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* authorization working

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* Fully Authorization integration & cleanup

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* make project compilable

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* make code compilable & buildable

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* fix discovery test

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* fix property test

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* fix unit tests

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* unit tests fixed

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* solve checkstyle high & medium

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* include all status update channel calls

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* correct Closed/Connected/Locked states

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* add charge statistics

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* add ChargingProfile channels

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* add charging sessions

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* add discovery properties

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* provide general check-control info

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* add test data for different vehicles

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* fix some checkstyle

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* add more vehicles to unit test

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* add F11 test

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* add service mileage

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* add check controls

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* add fingerprint mechanism

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* cleanup channels after rework

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* bugfixe requesting vehicles

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* fix brand handling

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* anonymous fingerprint

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* add charge statistics channels

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* add tire channels

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* remove range max channels

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* resolve last checkstyle issues

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* fix chrge profile updates

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* fix remote service execution

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* fix image handling

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* adapt service & checkcontrol handling

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* bugfix session selection

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* rework km / mi handling

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* introduce fallbacks for range calculations

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* unit tests for all available vehicle fingerprints

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* bugfixes during creation of HMI and translations

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* bugfix translation and language selection

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* introduce climate-now start / stop

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* bugfixes translation

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* add all fuelindicator fields

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* correct remote service ids, commands and translations

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* cleanup remote requsts and responses

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* add remote response examples

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* rework command options

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* remove unused timezoneprovider

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* locale language debugging

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* fix range value calculation

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* introduce charge-info channel

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* localize charge info string

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* smaller charge status fixes

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* 1st readme adaptions plus corresponding bugfixes

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* bugfix date time conversion

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* readme channel group update

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* add motion status

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* raw test anonymous fingerprint

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* revert motion status

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* bugfixes todo sections

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* china login

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* remove unnecessary info logging

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* spell check and example update

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* resolve codeowner conflicts

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* delete rex responses

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* bugfix linux characters

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* bugfix unit test with static time comparison

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* Copyright header adaption

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* add raw data channel

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* add address channel

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* add motion channel

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* add mild hybrid vehicle support

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* review comment corrections

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* correct review comments

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>

* resolve bom conflict

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
This commit is contained in:
Bernd Weymann 2022-04-25 08:12:55 +02:00 committed by GitHub
parent ad71ca0055
commit c8d64ddeeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
194 changed files with 20628 additions and 0 deletions

View File

@ -202,6 +202,7 @@
/bundles/org.openhab.binding.mqtt.homeassistant/ @davidgraeff @antroids /bundles/org.openhab.binding.mqtt.homeassistant/ @davidgraeff @antroids
/bundles/org.openhab.binding.mqtt.homie/ @davidgraeff /bundles/org.openhab.binding.mqtt.homie/ @davidgraeff
/bundles/org.openhab.binding.mycroft/ @dalgwen /bundles/org.openhab.binding.mycroft/ @dalgwen
/bundles/org.openhab.binding.mybmw/ @weymann @ntruchsess
/bundles/org.openhab.binding.myq/ @digitaldan /bundles/org.openhab.binding.myq/ @digitaldan
/bundles/org.openhab.binding.mystrom/ @pail23 /bundles/org.openhab.binding.mystrom/ @pail23
/bundles/org.openhab.binding.nanoleaf/ @raepple @stefan-hoehn /bundles/org.openhab.binding.nanoleaf/ @raepple @stefan-hoehn

View File

@ -1001,6 +1001,11 @@
<artifactId>org.openhab.binding.mycroft</artifactId> <artifactId>org.openhab.binding.mycroft</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.mybmw</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.openhab.addons.bundles</groupId> <groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.myq</artifactId> <artifactId>org.openhab.binding.myq</artifactId>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,846 @@
# MyBMW Binding
The binding provides access like [MyBMW App](https://www.bmw.com/en/footer/mybmw-app.html) to openHAB.
All vehicles connected to an account will be detected by the discovery with the correct type:
* Conventional Fuel Vehicle
* Plugin-Hybrid Electrical Vehicle
* Battery Electric Vehicle with Range Extender
* Battery Electric Vehicle
In addition properties are attached with information and services provided by this vehicle.
The provided data depends on
1. the [Thing Type](#things) and
2. the [Properties](#properties) mentioned in Services
Different channel groups are clustering all information.
Check for each group if it's supported by your vehicle.
Please note **this isn't a real-time binding**.
If a door is opened the state isn't transmitted and changed immediately.
It's not a flaw in the binding itself because the state in BMW's own MyBMW App is also updated with some delay.
## Supported Things
### Bridge
The bridge establishes the connection between BMW API and openHAB.
| Name | Bridge Type ID | Description |
|----------------------------|----------------|------------------------------------------|
| MyBMW Account | `account` | Access to BMW API for a specific user |
### Things
Four different vehicle types are provided.
They differ in the supported channel groups & channels.
Conventional Fuel Vehicles don't provide e.g. _Charging Profile_, Electric Vehicles don't provide a _Fuel Range_.
For hybrid vehicles in addition to _Fuel and Electric Range_ the _Hybrid Range_ is shown.
| Name | Thing Type ID | Supported Channel Groups |
|-------------------------------------|---------------|---------------------------------------------------------------------|
| BMW Electric Vehicle | `bev` | Vehicle with electric drive train |
| BMW Electric Vehicle with REX | `bev_rex` | Vehicle with electric drive train plus fuel powered range extender |
| BMW Plug-In-Hybrid Electric Vehicle | `phev` | Vehicle with combustion and electric drive train |
| BMW Conventional Vehicle | `conv` | Vehicle with combustion drive train |
#### Properties
<img align="right" src="./doc/vehicle-properties.png" width="500" height="350"/>
For each vehicle properties are available.
Basic information is given regarding
* Vehicle properties like model type, drive train and construction year
* Which services are available / not available
In the right picture can see in *remoteServicesEnabled* e.g. the *Door Lock* and *Door Unlock* services are mentioned.
This ensures channel group [Remote Services](#remote-services) is supporting door lock and unlock remote control.
In *Services Supported* the entry *ChargingHistory* is mentioned.
So it's valid to connect channel group [Charge Sessions](#charge-sessions) in order to display your last charging sessions.
| Property Key | Property Value | Supported Channel Groups |
|------------------------|---------------------|------------------------------|
| servicesSupported | ChargingHistory | session |
| remoteServicesEnabled | _list of services_ | remote |
## Discovery
Auto discovery is starting after the bridge is created.
A list of your registered vehicles is queried and all found things are added in the inbox.
Unique identifier is the *Vehicle Identification Number* (VIN).
If a thing is already declared in a _.things_ configuration, discovery won't highlight it again.
Properties will be attached to predefined vehicles if the VIN is matching.
## Configuration
### Bridge Configuration
| Parameter | Type | Description |
|-----------------|---------|--------------------------------------------------------------------|
| userName | text | MyBMW Username |
| password | text | MyBMW Password |
| region | text | Select region in order to connect to the appropriate BMW server. |
The region Configuration has 3 different options
* _NORTH_AMERICA_
* _CHINA_
* _ROW_ (Rest of World)
#### Advanced Configuration
| Parameter | Type | Description |
|-----------------|---------|---------------------------------------------------------|
| language | text | Channel data can be returned in the desired language |
Language is predefined as *AUTODETECT*.
Some textual descriptions, date and times are delivered based on your local language.
You can overwrite this setting with lowercase 2-letter [language code reagrding ISO 639](https://www.oracle.com/java/technologies/javase/jdk8-jre8-suported-locales.html)
So if want your UI in english language place *en* as desired language.
### Thing Configuration
Same configuration is needed for all things
| Parameter | Type | Description |
|-----------------|---------|---------------------------------------|
| vin | text | Vehicle Identification Number (VIN) |
| refreshInterval | integer | Refresh Interval in Minutes |
#### Advanced Configuration
| Parameter | Type | Description |
|-----------------|---------|-----------------------------------|
| vehicleBrand | text | Vehicle Brand like BMW or Mini |
The _vehicleBrand_ is automatically obtained by the discovery service and shall not be changed.
If thing is defined manually via *.things file following brands are supported
* BMW
* MINI
## Channels
There are many channels available for each vehicle.
For better overview they are clustered in different channel groups.
They differ for each vehicle type, build-in sensors and activated services.
### Thing Channel Groups
| Channel Group ID | Description | conv | phev | bev_rex | bev |
|----------------------------------|---------------------------------------------------|------|------|---------|-----|
| [status](#vehicle-status) | Overall vehicle status | X | X | X | X |
| [range](#range-data) | Provides mileage, range and charge / fuel levels | X | X | X | X |
| [doors](#doors-details) | Detials of all doors and windows | X | X | X | X |
| [check](#check-control) | Shows current active CheckControl messages | X | X | X | X |
| [service](#services) | Future vehicle service schedules | X | X | X | X |
| [location](#location) | Coordinates and heading of the vehicle | X | X | X | X |
| [remote](#remote-services) | Remote control of the vehicle | X | X | X | X |
| [profile](#charge-profile) | Scheduled charging profiles of vehicle | | X | X | X |
| [statistic](#charge-statistics) | Charging statistics of current month | | X | X | X |
| [session](#charge-sessions) | Past charging sessions | | X | X | X |
| [tires](#tire-pressure) | Current and wanted pressure for all tires | X | X | X | X |
| [image](#image) | Provides an image of your vehicle | X | X | X | X |
#### Vehicle Status
Reflects overall status of the vehicle.
* Channel Group ID is **status**
* Available for all vehicles
* Read-only values
| Channel Label | Channel ID | Type | Description | conv | phev | bev_rex | bev |
|---------------------------|---------------------|---------------|------------------------------------------------|------|------|---------|-----|
| Overall Door Status | doors | String | Combined status for all doors | X | X | X | X |
| Overall Window Status | windows | String | Combined status for all windows | X | X | X | X |
| Doors Locked | lock | String | Status if vehicle is secured | X | X | X | X |
| Next Service Date | service-date | DateTime | Date of next upcoming service | X | X | X | X |
| Mileage till Next Service | service-mileage | Number:Length | Mileage till upcoming service | X | X | X | X |
| Check Control | check-control | String | Presence of active warning messages | X | X | X | X |
| Plug Connection Status | plug-connection | String | Plug is _Connected_ or _Not connected_ | | X | X | X |
| Charging Status | charge | String | Current charging status | | X | X | X |
| Charging Information | charge-info | String | Information regarding current charging session | | X | X | X |
| Motion Status | motion | Switch | Driving state - depends on vehicle hardware | X | X | X | X |
| Last Status Timestamp | last-update | DateTime | Date and time of last status update | X | X | X | X |
Overall Door Status values
* _Closed_ - all doors closed
* _Open_ - at least one door is open
* _Undef_ - no door data delivered at all
Overall Windows Status values
* _Closed_ - all windows closed
* _Open_ - at least one window is completely open
* _Intermediate_ - at least one window is partially open
* _Undef_ - no window data delivered at all
Check Control values
Localized String of current active warnings.
Examples:
* No Issues
* Multiple Issues
Charging Status values
* _Not Charging_
* _Charging_
* _Plugged In_
* _Fully Charged_
Charging Information values
Localized String of current active charging session
Examples
* 100% at ~00:43
* Starts at ~09:00
##### Vehicle Status Raw Data
The _raw data channel_ is marked as _advanced_ and isn't shown by default.
Target are advanced users to derive even more data out of BMW API replies.
As the replies are formatted as JSON use the [JsonPath Transformation Service](https://www.openhab.org/addons/transformations/jsonpath/) to extract data for an item,
| Channel Label | Channel ID | Type | Description |
|---------------------------|---------------------|---------------|------------------------------------------------|
| Raw Data | raw | String | Unfiltered JSON String of vehicle data |
<img align="right" src="./doc/RawData.png" width="400" height="125"/>
Examples:
_Country ISO Code_
```
$.properties.originCountryISO
```
_Drivers Guide URL_
```
$.driverGuideInfo.androidStoreUrl
```
#### Range Data
Based on vehicle type some channels are present or not.
Conventional fuel vehicles don't provide *Electric Range* and battery electric vehicles don't show *Fuel Range*.
Hybrid vehicles have both and in addition *Hybrid Range*.
See description [Range vs Range Radius](#range-vs-range-radius) to get more information.
* Channel Group ID is **range**
* Availability according to table
* Read-only values
| Channel Label | Channel ID | Type | conv | phev | bev_rex | bev |
|---------------------------|-------------------------|----------------------|------|------|---------|-----|
| Mileage | mileage | Number:Length | X | X | X | X |
| Fuel Range | range-fuel | Number:Length | X | X | X | |
| Electric Range | range-electric | Number:Length | | X | X | X |
| Hybrid Range | range-hybrid | Number:Length | | X | X | |
| Battery Charge Level | soc | Number:Dimensionless | | X | X | X |
| Remaining Fuel | remaining-fuel | Number:Volume | X | X | X | |
| Fuel Range Radius | range-radius-fuel | Number:Length | X | X | X | |
| Electric Range Radius | range-radius-electric | Number:Length | | X | X | X |
| Hybrid Range Radius | range-radius-hybrid | Number:Length | | X | X | |
#### Doors Details
Detailed status of all doors and windows.
* Channel Group ID is **doors**
* Available for all vehicles if corresponding sensors are built-in
* Read-only values
| Channel Label | Channel ID | Type |
|----------------------------|-------------------------|---------------|
| Driver Door | driver-front | String |
| Driver Door Rear | driver-rear | String |
| Passenger Door | passenger-front | String |
| Passenger Door Rear | passenger-rear | String |
| Trunk | trunk | String |
| Hood | hood | String |
| Driver Window | win-driver-front | String |
| Driver Rear Window | win-driver-rear | String |
| Passenger Window | win-passenger-front | String |
| Passenger Rear Window | win-passenger-rear | String |
| Rear Window | win-rear | String |
| Sunroof | sunroof | String |
Possible states
* _Undef_ - no status data available
* _Invalid_ - this door / window isn't applicable for this vehicle
* _Closed_ - the door / window is closed
* _Open_ - the door / window is open
* _Intermediate_ - window in intermediate position, not applicable for doors
#### Check Control
Group for all current active Check Control messages.
If more than one message is active the channel _name_ contains all active messages as options.
* Channel Group ID is **check**
* Available for all vehicles
* Read/Write access
| Channel Label | Channel ID | Type | Access |
|---------------------------------|---------------------|----------------|------------|
| Check Control Description | name | String | Read/Write |
| Check Control Details | details | String | Read |
| Severity Level | severity | String | Read |
Severity Levels
* Ok
* Low
* Medium
#### Services
Group for all upcoming services with description, service date and/or service mileage.
If more than one service is scheduled in the future the channel _name_ contains all future services as options.
* Channel Group ID is **service**
* Available for all vehicles
* Read/Write access
| Channel Label | Channel ID | Type | Access |
|--------------------------------|---------------------|----------------|------------|
| Service Name | name | String | Read/Write |
| Service Details | details | String | Read |
| Service Date | date | DateTime | Read |
| Mileage till Service | mileage | Number:Length | Read |
#### Location
GPS location and heading of the vehicle.
* Channel Group ID is **location**
* Available for all vehicles with built-in GPS sensor. Function can be enabled/disabled in the head unit
* Read-only values
| Channel Label | Channel ID | Type |
|-----------------|---------------------|--------------|
| GPS Coordinates | gps | Location |
| Heading | heading | Number:Angle |
| Address | address | String |
#### Remote Services
Remote control of the vehicle.
Send a *command* to the vehicle and the *state* is reporting the execution progress.
Only one command can be executed each time.
Parallel execution isn't supported.
* Channel Group ID is **remote**
* Available for all commands mentioned in *Services Activated*. See [Vehicle Properties](#properties) for further details
* Read/Write access
| Channel Label | Channel ID | Type | Access |
|-------------------------|---------------------|---------|--------|
| Remote Service Command | command | String | Write |
| Service Execution State | state | String | Read |
The channel _command_ provides options
* _flash-lights_
* _vehicle-finder_
* _door-lock_
* _door-unlock_
* _horn-low_
* _climate-now-start_
* _climate-now-stop_
The channel _state_ shows the progress of the command execution in the following order
1) _initiated_
2) _pending_
3) _delivered_
4) _executed_
#### Charge Profile
Charging options with date and time for preferred time windows and charging modes.
* Channel Group ID is **profile**
* Available for electric and hybrid vehicles
* Read access for UI.
* There are 4 timers *T1, T2, T3 and T4* available. Replace *X* with number 1,2 or 3 to target the correct timer
| Channel Label | Channel ID | Type |
|----------------------------|---------------------------|----------|
| Charge Mode | mode | String |
| Charge Preferences | prefs | String |
| Charging Plan | control | String |
| SoC Target | target | String |
| Charging Energy Limited | limit | Switch |
| Window Start Time | window-start | DateTime |
| Window End Time | window-end | DateTime |
| A/C at Departure | climate | Switch |
| T*X* Enabled | timer*X*-enabled | Switch |
| T*X* Departure Time | timer*X*-departure | DateTime |
| T*X* Monday | timer*X*-day-mon | Switch |
| T*X* Tuesday | timer*X*-day-tue | Switch |
| T*X* Wednesday | timer*X*-day-wed | Switch |
| T*X* Thursday | timer*X*-day-thu | Switch |
| T*X* Friday | timer*X*-day-fri | Switch |
| T*X* Saturday | timer*X*-day-sat | Switch |
| T*X* Sunday | timer*X*-day-sun | Switch |
The channel _profile-mode_ supports
* *immediateCharging*
* *delayedCharging*
The channel _profile-prefs_ supports
* *noPreSelection*
* *chargingWindow*
#### Charge Statistics
Shows charge statistics of the current month
* Channel Group ID is **statistic**
* Available for electric and hybrid vehicles
* Read-only values
| Channel Label | Channel ID | Type |
|----------------------------|-------------------------|----------------|
| Charge Statistic Month | title | String |
| Energy Charged | energy | Number:Energy |
| Charge Sessions | sessions | Number |
#### Charge Sessions
Group for past charging sessions.
If more than one message is active the channel _name_ contains all active messages as options.
* Channel Group ID is **session**
* Available for electric and hybrid vehicles
* Read-only values
| Channel Label | Channel ID | Type |
|---------------------------------|--------------|----------|
| Session Title | title | String |
| Session Details | subtitle | String |
| Charged Energy in Session | energy | String |
| Issues during Session | issue | String |
| Session Status | status | String |
#### Tire Pressure
Current and target tire pressure values
* Channel Group ID is **tires**
* Available for all vehicles if corresponding sensors are built-in
* Read-only values
| Channel Label | Channel ID | Type |
|----------------------------|-------------------------|------------------|
| Front Left | fl-current | Number:Pressure |
| Front Left Target | fl-target | Number:Pressure |
| Front Right | fr-current | Number:Pressure |
| Front Right Target | fr-target | Number:Pressure |
| Rear Left | rl-current | Number:Pressure |
| Rear Left Target | rl-target | Number:Pressure |
| Rear Right | rr-current | Number:Pressure |
| Rear Right Target | rr-target | Number:Pressure |
#### Image
Image representation of the vehicle.
* Channel Group ID is **image**
* Available for all vehicles
* Read/Write access
| Channel Label | Channel ID | Type | Access |
|----------------------------|---------------------|--------|----------|
| Rendered Vehicle Image | png | Image | Read |
| Image Viewport | view | String | Write |
Possible view ports:
* _VehicleStatus_ Front Side View
* _VehicleInfo_ Front View
* _ChargingHistory_ Side View
* _Default_ Front Side View
## Further Descriptions
### Dynamic Data
<img align="right" src="./doc/SessionOptions.png" width="400" height="250"/>
There are 3 occurrences of dynamic data delivered
* Upcoming Services delivered in group [Services](#services)
* Check Control Messages delivered in group [Check Control](#check-control)
* Charging Session data delivered in group [Charge Sessions](#charge-sessions)
The channel id _name_ shows the first element as default.
All other possibilities are attached as options.
The picture on the right shows the _Session Title_ item and 3 possible options.
Select the desired service and the corresponding Charge Session with _Energy Charged_, _Session Status_ and _Session Issues_ will be shown.
### TroubleShooting
BMW has a high range of vehicles supported by their API.
In case of any issues with this binding help to resolve it!
Please perform the following steps:
* Can you log into MyBMW App with your credentials?
* Is the vehicle listed in your account?
* Is the [MyBMW Brige](#bridge) status _Online_?
If these preconditions are fulfilled proceed with the fingerprint generation.
#### Generate Debug Fingerprint
<img align="right" src="./doc/DiscoveryScan.png" width="400" height="350"/>
First [enable debug logging](https://www.openhab.org/docs/administration/logging.html#defining-what-to-log) for the binding.
```
log:set DEBUG org.openhab.binding.mybmw
```
The debug fingerprint is generated every time the discovery is executed.
To force a new fingerprint perform a _Scan_ for MyBMW things.
Personal data is eliminated from the log entries so it should be possible to share them in public.
Data like
* Vehicle Identification Number (VIN)
* Location data
are anonymized.
You'll find the fingerprint in the logs with the command
```
grep "Discovery Fingerprint Data" openhab.log
```
After the corresponding fingerprint is generated please [follow the instructions to raise an issue](https://community.openhab.org/t/how-to-file-an-issue/68464) and attach the fingerprint data!
Your feedback is highly appreciated!
### Range vs Range Radius
<img align="right" src="./doc/range-radius.png" width="400" height="350"/>
You will observe differences in the vehicle range and range radius values.
While range is indicating the possible distance to be driven on roads the range radius indicates the reachable range on the map.
The right picture shows the distance between Kassel and Frankfurt in Germany.
While the air-line distance is 145 kilometers the route distance is 192 kilometers.
So range value is the normal remaining range while the range radius values can be used e.g. on [Mapview](https://www.openhab.org/docs/ui/sitemaps.html#element-type-mapview) to indicate the reachable range on map.
Please note this is just an indicator of the effective range.
Especially for electric vehicles it depends on many factors like driving style and usage of electric consumers.
## Full Example
The example is based on a BMW i3 with range extender (REX).
Exchange configuration parameters in the Things section
* 4711 - any id you want
* YOUR_USERNAME - with your MyBMW login username
* YOUR_PASSWORD - with your MyBMW password credentials
* VEHICLE_VIN - the vehicle identification number
In addition search for all occurrences of *i3* and replace it with your Vehicle Identification like *x3* or *535d* and you're ready to go!
### Things File
```
Bridge mybmw:account:4711 "MyBMW Account" [userName="YOUR_USERNAME",password="YOUR_PASSWORD",region="ROW"] {
Thing bev_rex i3 "BMW i3 94h REX" [ vin="VEHICLE_VIN",refreshInterval=5,vehicleBrand="BMW"]
}
```
### Items File
```
Number:Length i3Mileage "Odometer [%d %unit%]" <line> (i3) {channel="mybmw:bev_rex:4711:i3:range#mileage" }
Number:Length i3Range "Range [%d %unit%]" <motion> (i3) {channel="mybmw:bev_rex:4711:i3:range#hybrid"}
Number:Length i3RangeElectric "Electric Range [%d %unit%]" <motion> (i3,long) {channel="mybmw:bev_rex:4711:i3:range#electric"}
Number:Length i3RangeFuel "Fuel Range [%d %unit%]" <motion> (i3) {channel="mybmw:bev_rex:4711:i3:range#fuel"}
Number:Dimensionless i3BatterySoc "Battery Charge [%.1f %%]" <battery> (i3,long) {channel="mybmw:bev_rex:4711:i3:range#soc"}
Number:Volume i3Fuel "Fuel [%.1f %unit%]" <oil> (i3) {channel="mybmw:bev_rex:4711:i3:range#remaining-fuel"}
Number:Length i3RadiusElectric "Electric Radius [%d %unit%]" <zoom> (i3) {channel="mybmw:bev_rex:4711:i3:range#radius-electric" }
Number:Length i3RadiusFuel "Fuel Radius [%d %unit%]" <zoom> (i3) {channel="mybmw:bev_rex:4711:i3:range#radius-fuel" }
Number:Length i3RadiusHybrid "Hybrid Radius [%d %unit%]" <zoom> (i3) {channel="mybmw:bev_rex:4711:i3:range#radius-hybrid" }
String i3DoorStatus "Door Status [%s]" <lock> (i3) {channel="mybmw:bev_rex:4711:i3:status#doors" }
String i3WindowStatus "Window Status [%s]" <lock> (i3) {channel="mybmw:bev_rex:4711:i3:status#windows" }
String i3LockStatus "Lock Status [%s]" <lock> (i3) {channel="mybmw:bev_rex:4711:i3:status#lock" }
DateTime i3NextServiceDate "Next Service Date [%1$tb %1$tY]" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:status#service-date" }
String i3NextServiceMileage "Next Service Mileage [%d %unit%]" <line> (i3) {channel="mybmw:bev_rex:4711:i3:status#service-mileage" }
String i3CheckControl "Check Control [%s]" <error> (i3) {channel="mybmw:bev_rex:4711:i3:status#check-control" }
String i3PlugConnection "Plug [%s]" <energy> (i3) {channel="mybmw:bev_rex:4711:i3:status#plug-connection" }
String i3ChargingStatus "[%s]" <energy> (i3) {channel="mybmw:bev_rex:4711:i3:status#charge" }
String i3ChargingInfo "[%s]" <energy> (i3) {channel="mybmw:bev_rex:4711:i3:status#charge-info" }
DateTime i3LastUpdate "Update [%1$tA, %1$td.%1$tm. %1$tH:%1$tM]" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:status#last-update"}
Location i3Location "Location [%s]" <zoom> (i3) {channel="mybmw:bev_rex:4711:i3:location#gps" }
Number:Angle i3Heading "Heading [%.1f %unit%]" <zoom> (i3) {channel="mybmw:bev_rex:4711:i3:location#heading" }
String i3RemoteCommand "Command [%s]" <switch> (i3) {channel="mybmw:bev_rex:4711:i3:remote#command" }
String i3RemoteState "Remote Execution State [%s]" <status> (i3) {channel="mybmw:bev_rex:4711:i3:remote#state" }
String i3DriverDoor "Driver Door [%s]" <lock> (i3) {channel="mybmw:bev_rex:4711:i3:doors#driver-front" }
String i3DriverDoorRear "Driver Door Rear [%s]" <lock> (i3) {channel="mybmw:bev_rex:4711:i3:doors#driver-rear" }
String i3PassengerDoor "Passenger Door [%s]" <lock> (i3) {channel="mybmw:bev_rex:4711:i3:doors#passenger-front" }
String i3PassengerDoorRear "Passenger Door Rear [%s]" <lock> (i3) {channel="mybmw:bev_rex:4711:i3:doors#passenger-rear" }
String i3Hood "Hood [%s]" <lock> (i3) {channel="mybmw:bev_rex:4711:i3:doors#hood" }
String i3Trunk "Trunk [%s]" <lock> (i3) {channel="mybmw:bev_rex:4711:i3:doors#trunk" }
String i3DriverWindow "Driver Window [%s]" <lock> (i3) {channel="mybmw:bev_rex:4711:i3:doors#win-driver-front" }
String i3DriverWindowRear "Driver Window Rear [%s]" <lock> (i3) {channel="mybmw:bev_rex:4711:i3:doors#win-driver-rear" }
String i3PassengerWindow "Passenger Window [%s]" <lock> (i3) {channel="mybmw:bev_rex:4711:i3:doors#win-passenger-front" }
String i3PassengerWindowRear "Passenger Window Rear [%s]" <lock> (i3) {channel="mybmw:bev_rex:4711:i3:doors#win-passenger-rear" }
String i3RearWindow "Rear Window [%s]" <lock> (i3) {channel="mybmw:bev_rex:4711:i3:doors#win-rear" }
String i3Sunroof "Sunroof [%s]" <lock> (i3) {channel="mybmw:bev_rex:4711:i3:doors#sunroof" }
String i3ServiceName "Service Name [%s]" <text> (i3) {channel="mybmw:bev_rex:4711:i3:service#name" }
String i3ServiceDetails "Service Details [%s]" <text> (i3) {channel="mybmw:bev_rex:4711:i3:service#details" }
Number:Length i3ServiceMileage "Service Mileage [%d %unit%]" <line> (i3) {channel="mybmw:bev_rex:4711:i3:service#mileage" }
DateTime i3ServiceDate "Service Date [%1$tb %1$tY]" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:service#date" }
String i3CCName "CheckControl Name [%s]" <text> (i3) {channel="mybmw:bev_rex:4711:i3:check#name" }
String i3CCDetails "CheckControl Details [%s]" <text> (i3) {channel="mybmw:bev_rex:4711:i3:check#details" }
String i3CCSeverity "CheckControl Severity [%s]" <line> (i3) {channel="mybmw:bev_rex:4711:i3:check#severity" }
Switch i3ChargeProfileClimate "Charge Profile Climatization" <temperature> (i3) {channel="mybmw:bev_rex:4711:i3:profile#climate" }
String i3ChargeProfileMode "Charge Profile Mode [%s]" <energy> (i3) {channel="mybmw:bev_rex:4711:i3:profile#mode" }
String i3ChargeProfilePrefs "Charge Profile Preference [%s]" <energy> (i3) {channel="mybmw:bev_rex:4711:i3:profile#prefs" }
String i3ChargeProfileCtrl "Charge Profile Control [%s]" <energy> (i3) {channel="mybmw:bev_rex:4711:i3:profile#control" }
Number i3ChargeProfileTarget "Charge Profile SoC Target [%s]" <energy> (i3) {channel="mybmw:bev_rex:4711:i3:profile#target" }
Switch i3ChargeProfileLimit "Charge Profile limited" <energy> (i3) {channel="mybmw:bev_rex:4711:i3:profile#limit" }
DateTime i3ChargeWindowStart "Charge Window Start [%1$tH:%1$tM]" <time> (i3) {channel="mybmw:bev_rex:4711:i3:profile#window-start" }
DateTime i3ChargeWindowEnd "Charge Window End [%1$tH:%1$tM]" <time> (i3) {channel="mybmw:bev_rex:4711:i3:profile#window-end" }
DateTime i3Timer1Departure "Timer 1 Departure [%1$tH:%1$tM]" <time> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer1-departure" }
String i3Timer1Days "Timer 1 Days [%s]" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer1-days" }
Switch i3Timer1DayMon "Timer 1 Monday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer1-day-mon" }
Switch i3Timer1DayTue "Timer 1 Tuesday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer1-day-tue" }
Switch i3Timer1DayWed "Timer 1 Wednesday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer1-day-wed" }
Switch i3Timer1DayThu "Timer 1 Thursday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer1-day-thu" }
Switch i3Timer1DayFri "Timer 1 Friday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer1-day-fri" }
Switch i3Timer1DaySat "Timer 1 Saturday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer1-day-sat" }
Switch i3Timer1DaySun "Timer 1 Sunday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer1-day-sun" }
Switch i3Timer1Enabled "Timer 1 Enabled" <switch> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer1-enabled" }
DateTime i3Timer2Departure "Timer 2 Departure [%1$tH:%1$tM]" <time> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer2-departure" }
Switch i3Timer2DayMon "Timer 2 Monday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer2-day-mon" }
Switch i3Timer2DayTue "Timer 2 Tuesday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer2-day-tue" }
Switch i3Timer2DayWed "Timer 2 Wednesday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer2-day-wed" }
Switch i3Timer2DayThu "Timer 2 Thursday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer2-day-thu" }
Switch i3Timer2DayFri "Timer 2 Friday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer2-day-fri" }
Switch i3Timer2DaySat "Timer 2 Saturday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer2-day-sat" }
Switch i3Timer2DaySun "Timer 2 Sunday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer2-day-sun" }
Switch i3Timer2Enabled "Timer 2 Enabled" <switch> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer2-enabled" }
DateTime i3Timer3Departure "Timer 3 Departure [%1$tH:%1$tM]" <time> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer3-departure" }
Switch i3Timer3DayMon "Timer 3 Monday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer3-day-mon" }
Switch i3Timer3DayTue "Timer 3 Tuesday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer3-day-tue" }
Switch i3Timer3DayWed "Timer 3 Wednesday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer3-day-wed" }
Switch i3Timer3DayThu "Timer 3 Thursday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer3-day-thu" }
Switch i3Timer3DayFri "Timer 3 Friday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer3-day-fri" }
Switch i3Timer3DaySat "Timer 3 Saturday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer3-day-sat" }
Switch i3Timer3DaySun "Timer 3 Sunday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer3-day-sun" }
Switch i3Timer3Enabled "Timer 3 Enabled" <switch> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer3-enabled" }
DateTime i3Timer4Departure "Timer 4 Departure [%1$tH:%1$tM]" <time> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer4-departure" }
Switch i3Timer4DayMon "Timer 4 Monday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer4-day-mon" }
Switch i3Timer4DayTue "Timer 4 Tuesday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer4-day-tue" }
Switch i3Timer4DayWed "Timer 4 Wednesday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer4-day-wed" }
Switch i3Timer4DayThu "Timer 4 Thursday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer4-day-thu" }
Switch i3Timer4DayFri "Timer 4 Friday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer4-day-fri" }
Switch i3Timer4DaySat "Timer 4 Saturday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer4-day-sat" }
Switch i3Timer4DaySun "Timer 4 Sunday" <calendar> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer4-day-sun" }
Switch i3Timer4Enabled "Timer 4 Enabled" <switch> (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer4-enabled" }
String i3StatisticsTitle "[%s]" <text> (i3) {channel="mybmw:bev_rex:4711:i3:statistic#title" }
Number:Energy i3StatisticsEnergy "Charged [%d %unit%]" <energy> (i3) {channel="mybmw:bev_rex:4711:i3:statistic#energy" }
Number i3StatisticsSessions "Sessions [%d]" <line> (i3) {channel="mybmw:bev_rex:4711:i3:statistic#sessions" }
String i3SessionTitle "[%s]" <text> (i3) {channel="mybmw:bev_rex:4711:i3:session#title" }
String i3SessionDetails "[%s]" <text> (i3) {channel="mybmw:bev_rex:4711:i3:session#subtitle" }
String i3SessionCharged "Energy Charged [%s]" <energy> (i3) {channel="mybmw:bev_rex:4711:i3:session#energy" }
String i3SessionProblems "Problems [%s]" <error> (i3) {channel="mybmw:bev_rex:4711:i3:session#issue" }
String i3SessionStatus "Session status [%s]" <text> (i3) {channel="mybmw:bev_rex:4711:i3:session#status" }
Number:Pressure i3TireFLCurrent "Tire Front Left [%.1f %unit%]" <text> (i3) {channel="mybmw:bev_rex:4711:i3:tires#fl-current" }
Number:Pressure i3TireFLTarget "Tire Front Left Target [%.1f %unit%]" <text> (i3) {channel="mybmw:bev_rex:4711:i3:tires#fl-target" }
Number:Pressure i3TireFRCurrent "Tire Front Right [%.1f %unit%]" <text> (i3) {channel="mybmw:bev_rex:4711:i3:tires#fr-current" }
Number:Pressure i3TireFRTarget "Tire Front Right Target [%.1f %unit%]" <text> (i3) {channel="mybmw:bev_rex:4711:i3:tires#fr-target" }
Number:Pressure i3TireRLCurrent "Tire Rear Left [%.1f %unit%]" <text> (i3) {channel="mybmw:bev_rex:4711:i3:tires#rl-current" }
Number:Pressure i3TireRLTarget "Tire Rear Left Target [%.1f %unit%]" <text> (i3) {channel="mybmw:bev_rex:4711:i3:tires#rl-target" }
Number:Pressure i3TireRRCurrent "Tire Rear Right [%.1f %unit%]" <text> (i3) {channel="mybmw:bev_rex:4711:i3:tires#rr-current" }
Number:Pressure i3TireRRTarget "Tire Rear Right Target [%.1f %unit%]" <text> (i3) {channel="mybmw:bev_rex:4711:i3:tires#rr-target" }
Image i3Image "Image" (i3) {channel="mybmw:bev_rex:4711:i3:image#png" }
String i3ImageViewport "Image Viewport [%s]" <zoom> (i3) {channel="mybmw:bev_rex:4711:i3:image#view" }
```
### Sitemap File
```
sitemap BMW label="BMW" {
Frame label="BMW i3" {
Image item=i3Image
}
Frame label="Status" {
Text item=i3DoorStatus
Text item=i3WindowStatus
Text item=i3LockStatus
Text item=i3NextServiceDate
Text item=i3NextServiceMileage
Text item=i3CheckControl
Text item=i3ChargingStatus
Text item=i3LastUpdate
}
Frame label="Range" {
Text item=i3Mileage
Text item=i3Range
Text item=i3RangeElectric
Text item=i3RangeFuel
Text item=i3BatterySoc
Text item=i3Fuel
Text item=i3RadiusElectric
Text item=i3RadiusHybrid
}
Frame label="Remote Services" {
Selection item=i3RemoteCommand
Text item=i3RemoteState
}
Frame label="Services" {
Selection item=i3ServiceName
Text item=i3ServiceDetails
Text item=i3ServiceMileage
Text item=i3ServiceDate
}
Frame label="CheckControl" {
Selection item=i3CCName
Text item=i3CCDetails
Text item=i3CCSeverity
}
Frame label="Door Details" {
Text item=i3DriverDoor visibility=[i3DriverDoor!="INVALID"]
Text item=i3DriverDoorRear visibility=[i3DriverDoorRear!="INVALID"]
Text item=i3PassengerDoor visibility=[i3PassengerDoor!="INVALID"]
Text item=i3PassengerDoorRear visibility=[i3PassengerDoorRear!="INVALID"]
Text item=i3Hood visibility=[i3Hood!="INVALID"]
Text item=i3Trunk visibility=[i3Trunk!="INVALID"]
Text item=i3DriverWindow visibility=[i3DriverWindow!="INVALID"]
Text item=i3DriverWindowRear visibility=[i3DriverWindowRear!="INVALID"]
Text item=i3PassengerWindow visibility=[i3PassengerWindow!="INVALID"]
Text item=i3PassengerWindowRear visibility=[i3PassengerWindowRear!="INVALID"]
Text item=i3RearWindow visibility=[i3RearWindow!="INVALID"]
Text item=i3Sunroof visibility=[i3Sunroof!="INVALID"]
}
Frame label="Location" {
Text item=i3Location
Text item=i3Heading
}
Frame label="Charge Profile" {
Switch item=i3ChargeProfileClimate
Selection item=i3ChargeProfileMode
Text item=i3ChargeWindowStart
Text item=i3ChargeWindowEnd
Text item=i3Timer1Departure
Switch item=i3Timer1DayMon
Switch item=i3Timer1DayTue
Switch item=i3Timer1DayWed
Switch item=i3Timer1DayThu
Switch item=i3Timer1DayFri
Switch item=i3Timer1DaySat
Switch item=i3Timer1DaySun
Switch item=i3Timer1Enabled
Text item=i3Timer2Departure
Switch item=i3Timer2DayMon
Switch item=i3Timer2DayTue
Switch item=i3Timer2DayWed
Switch item=i3Timer2DayThu
Switch item=i3Timer2DayFri
Switch item=i3Timer2DaySat
Switch item=i3Timer2DaySun
Switch item=i3Timer2Enabled
Text item=i3Timer3Departure
Switch item=i3Timer3DayMon
Switch item=i3Timer3DayTue
Switch item=i3Timer3DayWed
Switch item=i3Timer3DayThu
Switch item=i3Timer3DayFri
Switch item=i3Timer3DaySat
Switch item=i3Timer3DaySun
Switch item=i3Timer3Enabled
Text item=i3Timer4Departure
Switch item=i3Timer4DayMon
Switch item=i3Timer4DayTue
Switch item=i3Timer4DayWed
Switch item=i3Timer4DayThu
Switch item=i3Timer4DayFri
Switch item=i3Timer4DaySat
Switch item=i3Timer4DaySun
Switch item=i3Timer4Enabled
}
Frame label="Charge Statistics" {
Text item=i3StatisticsTitle
Text item=i3StatisticsEnergy
Text item=i3StatisticsSessions
}
Frame label="Charge Sessions" {
Selection item=i3SessionTitle
Text item=i3SessionDetails
Text item=i3SessionCharged
Text item=i3SessionProblems
Text item=i3SessionStatus
}
Frame label="Tires" {
Text item=i3TireFLCurrent
Text item=i3TireFLTarget
Text item=i3TireFRCurrent
Text item=i3TireFRTarget
Text item=i3TireRLCurrent
Text item=i3TireRLTarget
Text item=i3TireRRCurrent
Text item=i3TireRRTarget
}
Frame label="Image Properties" {
Selection item=i3ImageViewport
}
}
```
## Credits
This work is based on the project of [Bimmer Connected](https://github.com/bimmerconnected/bimmer_connected).

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.3.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.mybmw</artifactId>
<name>openHAB Add-ons :: Bundles :: MyBMW Binding</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.mybmw-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${project.version}/xml/features</repository>
<feature name="openhab-binding-mybmw" description="MyBMW Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.mybmw/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,45 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mybmw.internal.utils.Constants;
/**
* The {@link MyBMWConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class MyBMWConfiguration {
/**
* Depending on the location the correct server needs to be called
*/
public String region = Constants.EMPTY;
/**
* MyBMW App Username
*/
public String userName = Constants.EMPTY;
/**
* MyBMW App Password
*/
public String password = Constants.EMPTY;
/**
* Preferred Locale language
*/
public String language = Constants.LANGUAGE_AUTODETECT;
}

View File

@ -0,0 +1,204 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link MyBMWConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Bernd Weymann - Initial contribution
* @author Norbert Truchsess - edit & send of charge profile
*/
@NonNullByDefault
public class MyBMWConstants {
private static final String BINDING_ID = "mybmw";
public static final String VIN = "vin";
public static final int DEFAULT_IMAGE_SIZE_PX = 1024;
public static final int DEFAULT_REFRESH_INTERVAL_MINUTES = 5;
// See constants from bimmer-connected
// https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/vehicle.py
public enum VehicleType {
CONVENTIONAL("conv"),
PLUGIN_HYBRID("phev"),
MILD_HYBRID("hybrid"),
ELECTRIC_REX("bev_rex"),
ELECTRIC("bev"),
UNKNOWN("unknown");
private final String type;
VehicleType(String s) {
type = s;
}
@Override
public String toString() {
return type;
}
}
public enum ChargingMode {
immediateCharging,
delayedCharging
}
public enum ChargingPreference {
noPreSelection,
chargingWindow
}
public static final Set<String> FUEL_VEHICLES = Set.of(VehicleType.CONVENTIONAL.toString(),
VehicleType.PLUGIN_HYBRID.toString(), VehicleType.ELECTRIC_REX.toString());
public static final Set<String> ELECTRIC_VEHICLES = Set.of(VehicleType.ELECTRIC.toString(),
VehicleType.PLUGIN_HYBRID.toString(), VehicleType.ELECTRIC_REX.toString());
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_CONNECTED_DRIVE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account");
public static final ThingTypeUID THING_TYPE_CONV = new ThingTypeUID(BINDING_ID,
VehicleType.CONVENTIONAL.toString());
public static final ThingTypeUID THING_TYPE_PHEV = new ThingTypeUID(BINDING_ID,
VehicleType.PLUGIN_HYBRID.toString());
public static final ThingTypeUID THING_TYPE_BEV_REX = new ThingTypeUID(BINDING_ID,
VehicleType.ELECTRIC_REX.toString());
public static final ThingTypeUID THING_TYPE_BEV = new ThingTypeUID(BINDING_ID, VehicleType.ELECTRIC.toString());
public static final Set<ThingTypeUID> SUPPORTED_THING_SET = Set.of(THING_TYPE_CONNECTED_DRIVE_ACCOUNT,
THING_TYPE_CONV, THING_TYPE_PHEV, THING_TYPE_BEV_REX, THING_TYPE_BEV);
// Thing Group definitions
public static final String CHANNEL_GROUP_STATUS = "status";
public static final String CHANNEL_GROUP_SERVICE = "service";
public static final String CHANNEL_GROUP_CHECK_CONTROL = "check";
public static final String CHANNEL_GROUP_DOORS = "doors";
public static final String CHANNEL_GROUP_RANGE = "range";
public static final String CHANNEL_GROUP_LOCATION = "location";
public static final String CHANNEL_GROUP_REMOTE = "remote";
public static final String CHANNEL_GROUP_CHARGE_PROFILE = "profile";
public static final String CHANNEL_GROUP_CHARGE_STATISTICS = "statistic";
public static final String CHANNEL_GROUP_CHARGE_SESSION = "session";
public static final String CHANNEL_GROUP_TIRES = "tires";
public static final String CHANNEL_GROUP_VEHICLE_IMAGE = "image";
// Charge Statistics & Sessions
public static final String SESSIONS = "sessions";
public static final String ENERGY = "energy";
public static final String TITLE = "title";
public static final String SUBTITLE = "subtitle";
public static final String ISSUE = "issue";
public static final String STATUS = "status";
// Generic Constants for several groups
public static final String NAME = "name";
public static final String DETAILS = "details";
public static final String SEVERITY = "severity";
public static final String DATE = "date";
public static final String MILEAGE = "mileage";
public static final String GPS = "gps";
public static final String HEADING = "heading";
public static final String ADDRESS = "address";
// Status
public static final String DOORS = "doors";
public static final String WINDOWS = "windows";
public static final String LOCK = "lock";
public static final String SERVICE_DATE = "service-date";
public static final String SERVICE_MILEAGE = "service-mileage";
public static final String CHECK_CONTROL = "check-control";
public static final String PLUG_CONNECTION = "plug-connection";
public static final String CHARGE_STATUS = "charge";
public static final String CHARGE_INFO = "charge-info";
public static final String MOTION = "motion";
public static final String LAST_UPDATE = "last-update";
public static final String RAW = "raw";
// Door Details
public static final String DOOR_DRIVER_FRONT = "driver-front";
public static final String DOOR_DRIVER_REAR = "driver-rear";
public static final String DOOR_PASSENGER_FRONT = "passenger-front";
public static final String DOOR_PASSENGER_REAR = "passenger-rear";
public static final String HOOD = "hood";
public static final String TRUNK = "trunk";
public static final String WINDOW_DOOR_DRIVER_FRONT = "win-driver-front";
public static final String WINDOW_DOOR_DRIVER_REAR = "win-driver-rear";
public static final String WINDOW_DOOR_PASSENGER_FRONT = "win-passenger-front";
public static final String WINDOW_DOOR_PASSENGER_REAR = "win-passenger-rear";
public static final String WINDOW_REAR = "win-rear";
public static final String SUNROOF = "sunroof";
// Charge Profile
public static final String CHARGE_PROFILE_CLIMATE = "climate";
public static final String CHARGE_PROFILE_MODE = "mode";
public static final String CHARGE_PROFILE_PREFERENCE = "prefs";
public static final String CHARGE_PROFILE_CONTROL = "control";
public static final String CHARGE_PROFILE_TARGET = "target";
public static final String CHARGE_PROFILE_LIMIT = "limit";
public static final String CHARGE_WINDOW_START = "window-start";
public static final String CHARGE_WINDOW_END = "window-end";
public static final String CHARGE_TIMER1 = "timer1";
public static final String CHARGE_TIMER2 = "timer2";
public static final String CHARGE_TIMER3 = "timer3";
public static final String CHARGE_TIMER4 = "timer4";
public static final String CHARGE_DEPARTURE = "-departure";
public static final String CHARGE_ENABLED = "-enabled";
public static final String CHARGE_DAY_MON = "-day-mon";
public static final String CHARGE_DAY_TUE = "-day-tue";
public static final String CHARGE_DAY_WED = "-day-wed";
public static final String CHARGE_DAY_THU = "-day-thu";
public static final String CHARGE_DAY_FRI = "-day-fri";
public static final String CHARGE_DAY_SAT = "-day-sat";
public static final String CHARGE_DAY_SUN = "-day-sun";
// Range
public static final String RANGE_ELECTRIC = "electric";
public static final String RANGE_RADIUS_ELECTRIC = "radius-electric";
public static final String RANGE_FUEL = "fuel";
public static final String RANGE_RADIUS_FUEL = "radius-fuel";
public static final String RANGE_HYBRID = "hybrid";
public static final String RANGE_RADIUS_HYBRID = "radius-hybrid";
public static final String REMAINING_FUEL = "remaining-fuel";
public static final String SOC = "soc";
// Image
public static final String IMAGE_FORMAT = "png";
public static final String IMAGE_VIEWPORT = "view";
// Remote Services
public static final String REMOTE_SERVICE_LIGHT_FLASH = "light-flash";
public static final String REMOTE_SERVICE_VEHICLE_FINDER = "vehicle-finder";
public static final String REMOTE_SERVICE_DOOR_LOCK = "door-lock";
public static final String REMOTE_SERVICE_DOOR_UNLOCK = "door-unlock";
public static final String REMOTE_SERVICE_HORN = "horn-blow";
public static final String REMOTE_SERVICE_AIR_CONDITIONING_START = "climate-now-start";
public static final String REMOTE_SERVICE_AIR_CONDITIONING_STOP = "climate-now-stop";
public static final String REMOTE_SERVICE_COMMAND = "command";
public static final String REMOTE_STATE = "state";
// TIRES
public static final String FRONT_LEFT_CURRENT = "fl-current";
public static final String FRONT_LEFT_TARGET = "fl-target";
public static final String FRONT_RIGHT_CURRENT = "fr-current";
public static final String FRONT_RIGHT_TARGET = "fr-target";
public static final String REAR_LEFT_CURRENT = "rl-current";
public static final String REAR_LEFT_TARGET = "rl-target";
public static final String REAR_RIGHT_CURRENT = "rr-current";
public static final String REAR_RIGHT_TARGET = "rr-target";
}

View File

@ -0,0 +1,71 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal;
import static org.openhab.binding.mybmw.internal.MyBMWConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mybmw.internal.handler.MyBMWBridgeHandler;
import org.openhab.binding.mybmw.internal.handler.MyBMWCommandOptionProvider;
import org.openhab.binding.mybmw.internal.handler.VehicleHandler;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link MyBMWHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.mybmw", service = ThingHandlerFactory.class)
public class MyBMWHandlerFactory extends BaseThingHandlerFactory {
private final HttpClientFactory httpClientFactory;
private final MyBMWCommandOptionProvider commandOptionProvider;
private String localeLanguage;
@Activate
public MyBMWHandlerFactory(final @Reference HttpClientFactory hcf, final @Reference MyBMWCommandOptionProvider cop,
final @Reference LocaleProvider lp) {
httpClientFactory = hcf;
commandOptionProvider = cop;
localeLanguage = lp.getLocale().getLanguage().toLowerCase();
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_SET.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_CONNECTED_DRIVE_ACCOUNT.equals(thingTypeUID)) {
return new MyBMWBridgeHandler((Bridge) thing, httpClientFactory, localeLanguage);
} else if (SUPPORTED_THING_SET.contains(thingTypeUID)) {
VehicleHandler vh = new VehicleHandler(thing, commandOptionProvider, thingTypeUID.getId());
return vh;
}
return null;
}
}

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mybmw.internal.utils.Constants;
/**
* The {@link VehicleConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class VehicleConfiguration {
/**
* Vehicle Identification Number (VIN)
*/
public String vin = Constants.EMPTY;
/**
* Vehicle brand
* - bmw
* - mini
*/
public String vehicleBrand = Constants.EMPTY;
/**
* Data refresh rate in minutes
*/
public int refreshInterval = MyBMWConstants.DEFAULT_REFRESH_INTERVAL_MINUTES;
}

View File

@ -0,0 +1,223 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.discovery;
import static org.openhab.binding.mybmw.internal.MyBMWConstants.SUPPORTED_THING_SET;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mybmw.internal.MyBMWConstants;
import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
import org.openhab.binding.mybmw.internal.handler.MyBMWBridgeHandler;
import org.openhab.binding.mybmw.internal.handler.RemoteServiceHandler;
import org.openhab.binding.mybmw.internal.utils.Constants;
import org.openhab.binding.mybmw.internal.utils.VehicleStatusUtils;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link VehicleDiscovery} requests data from BMW API and is identifying the Vehicles after response
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class VehicleDiscovery extends AbstractDiscoveryService implements DiscoveryService, ThingHandlerService {
private static final Logger LOGGER = LoggerFactory.getLogger(VehicleDiscovery.class);
public static final String SUPPORTED_SUFFIX = "Supported";
public static final String ENABLE_SUFFIX = "Enable";
public static final String ENABLED_SUFFIX = "Enabled";
private static final int DISCOVERY_TIMEOUT = 10;
private Optional<MyBMWBridgeHandler> bridgeHandler = Optional.empty();
public VehicleDiscovery() {
super(SUPPORTED_THING_SET, DISCOVERY_TIMEOUT, false);
}
public void onResponse(List<Vehicle> vehicleList) {
bridgeHandler.ifPresent(bridge -> {
final ThingUID bridgeUID = bridge.getThing().getUID();
vehicleList.forEach(vehicle -> {
// the DriveTrain field in the delivered json is defining the Vehicle Type
String vehicleType = VehicleStatusUtils.vehicleType(vehicle.driveTrain, vehicle.model).toString();
SUPPORTED_THING_SET.forEach(entry -> {
if (entry.getId().equals(vehicleType)) {
ThingUID uid = new ThingUID(entry, vehicle.vin, bridgeUID.getId());
Map<String, String> properties = new HashMap<>();
// Vehicle Properties
properties.put("vehicleModel", vehicle.model);
properties.put("vehicleDriveTrain", vehicle.driveTrain);
properties.put("vehicleConstructionYear", Integer.toString(vehicle.year));
properties.put("vehicleBodytype", vehicle.bodyType);
properties.put("servicesSupported", getServices(vehicle, SUPPORTED_SUFFIX, true));
properties.put("servicesUnsupported", getServices(vehicle, SUPPORTED_SUFFIX, false));
String servicesEnabled = getServices(vehicle, ENABLED_SUFFIX, true) + Constants.SEMICOLON
+ getServices(vehicle, ENABLE_SUFFIX, true);
properties.put("servicesEnabled", servicesEnabled.trim());
String servicesDisabled = getServices(vehicle, ENABLED_SUFFIX, false) + Constants.SEMICOLON
+ getServices(vehicle, ENABLE_SUFFIX, false);
properties.put("servicesDisabled", servicesDisabled.trim());
// For RemoteServices we need to do it step-by-step
StringBuffer remoteServicesEnabled = new StringBuffer();
StringBuffer remoteServicesDisabled = new StringBuffer();
if (vehicle.capabilities.lock.isEnabled) {
remoteServicesEnabled.append(
RemoteServiceHandler.RemoteService.DOOR_LOCK.getLabel() + Constants.SEMICOLON);
} else {
remoteServicesDisabled.append(
RemoteServiceHandler.RemoteService.DOOR_LOCK.getLabel() + Constants.SEMICOLON);
}
if (vehicle.capabilities.unlock.isEnabled) {
remoteServicesEnabled.append(
RemoteServiceHandler.RemoteService.DOOR_UNLOCK.getLabel() + Constants.SEMICOLON);
} else {
remoteServicesDisabled.append(
RemoteServiceHandler.RemoteService.DOOR_UNLOCK.getLabel() + Constants.SEMICOLON);
}
if (vehicle.capabilities.lights.isEnabled) {
remoteServicesEnabled.append(
RemoteServiceHandler.RemoteService.LIGHT_FLASH.getLabel() + Constants.SEMICOLON);
} else {
remoteServicesDisabled.append(
RemoteServiceHandler.RemoteService.LIGHT_FLASH.getLabel() + Constants.SEMICOLON);
}
if (vehicle.capabilities.horn.isEnabled) {
remoteServicesEnabled.append(
RemoteServiceHandler.RemoteService.HORN_BLOW.getLabel() + Constants.SEMICOLON);
} else {
remoteServicesDisabled.append(
RemoteServiceHandler.RemoteService.HORN_BLOW.getLabel() + Constants.SEMICOLON);
}
if (vehicle.capabilities.vehicleFinder.isEnabled) {
remoteServicesEnabled.append(
RemoteServiceHandler.RemoteService.VEHICLE_FINDER.getLabel() + Constants.SEMICOLON);
} else {
remoteServicesDisabled.append(
RemoteServiceHandler.RemoteService.VEHICLE_FINDER.getLabel() + Constants.SEMICOLON);
}
if (vehicle.capabilities.climateNow.isEnabled) {
remoteServicesEnabled.append(RemoteServiceHandler.RemoteService.CLIMATE_NOW_START.getLabel()
+ Constants.SEMICOLON);
} else {
remoteServicesDisabled
.append(RemoteServiceHandler.RemoteService.CLIMATE_NOW_START.getLabel()
+ Constants.SEMICOLON);
}
properties.put("remoteServicesEnabled", remoteServicesEnabled.toString().trim());
properties.put("remoteServicesDisabled", remoteServicesDisabled.toString().trim());
// Update Properties for already created Things
bridge.getThing().getThings().forEach(vehicleThing -> {
Configuration c = vehicleThing.getConfiguration();
if (c.containsKey(MyBMWConstants.VIN)) {
String thingVIN = c.get(MyBMWConstants.VIN).toString();
if (vehicle.vin.equals(thingVIN)) {
vehicleThing.setProperties(properties);
}
}
});
// Properties needed for functional Thing
properties.put(MyBMWConstants.VIN, vehicle.vin);
properties.put("vehicleBrand", vehicle.brand);
properties.put("refreshInterval",
Integer.toString(MyBMWConstants.DEFAULT_REFRESH_INTERVAL_MINUTES));
String vehicleLabel = vehicle.brand + " " + vehicle.model;
Map<String, Object> convertedProperties = new HashMap<String, Object>(properties);
thingDiscovered(DiscoveryResultBuilder.create(uid).withBridge(bridgeUID)
.withRepresentationProperty(MyBMWConstants.VIN).withLabel(vehicleLabel)
.withProperties(convertedProperties).build());
}
});
});
});
}
@Override
public void setThingHandler(ThingHandler handler) {
if (handler instanceof MyBMWBridgeHandler) {
bridgeHandler = Optional.of((MyBMWBridgeHandler) handler);
bridgeHandler.get().setDiscoveryService(this);
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return bridgeHandler.orElse(null);
}
@Override
protected void startScan() {
bridgeHandler.ifPresent(MyBMWBridgeHandler::requestVehicles);
}
@Override
public void deactivate() {
super.deactivate();
}
public static String getServices(Vehicle vehicle, String suffix, boolean enabled) {
StringBuffer sb = new StringBuffer();
List<String> l = getObject(vehicle.capabilities, enabled);
for (String capEntry : l) {
// remove "is" prefix
String cut = capEntry.substring(2);
if (cut.endsWith(suffix)) {
if (sb.length() > 0) {
sb.append(Constants.SEMICOLON);
}
sb.append(cut.substring(0, cut.length() - suffix.length()));
}
}
return sb.toString();
}
/**
* Get all field names from a DTO with a specific value
* Used to get e.g. all services which are "ACTIVATED"
*
* @param DTO Object
* @param compare String which needs to map with the value
* @return String with all field names matching this value separated with Spaces
*/
public static List<String> getObject(Object dto, Object compare) {
List<String> l = new ArrayList<String>();
for (Field field : dto.getClass().getDeclaredFields()) {
try {
Object value = field.get(dto);
if (compare.equals(value)) {
l.add(field.getName());
}
} catch (IllegalArgumentException | IllegalAccessException e) {
LOGGER.debug("Field {} not found {}", compare, e.getMessage());
}
}
return l;
}
}

View File

@ -0,0 +1,50 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.auth;
import java.util.List;
/**
* The {@link AuthQueryResponse} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class AuthQueryResponse {
public String clientName;// ": "mybmwapp",
public String clientSecret;// ": "c0e3393d-70a2-4f6f-9d3c-8530af64d552",
public String clientId;// ": "31c357a0-7a1d-4590-aa99-33b97244d048",
public String gcdmBaseUrl;// ": "https://customer.bmwgroup.com",
public String returnUrl;// ": "com.bmw.connected://oauth",
public String brand;// ": "bmw",
public String language;// ": "en",
public String country;// ": "US",
public String authorizationEndpoint;// ": "https://customer.bmwgroup.com/oneid/login",
public String tokenEndpoint;// ": "https://customer.bmwgroup.com/gcdm/oauth/token",
public List<String> scopes;// ;": [
// "openid",
// "profile",
// "email",
// "offline_access",
// "smacc",
// "vehicle_data",
// "perseus",
// "dlm",
// "svds",
// "cesim",
// "vsapi",
// "remote_services",
// "fupo",
// "authenticate_user"
// ],
public List<String> promptValues; // ": ["login"]
}

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.auth;
import org.openhab.binding.mybmw.internal.utils.Constants;
import com.google.gson.annotations.SerializedName;
/**
* The {@link AuthResponse} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class AuthResponse {
@SerializedName("access_token")
public String accessToken = Constants.EMPTY;
@SerializedName("token_type")
public String tokenType = Constants.EMPTY;
@SerializedName("expires_in")
public int expiresIn = -1;
@Override
public String toString() {
return "Token " + accessToken + " type " + tokenType + " expires in " + expiresIn;
}
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.auth;
import org.openhab.binding.mybmw.internal.utils.Constants;
import com.google.gson.annotations.SerializedName;
/**
* The {@link ChinaAccessToken} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class ChinaAccessToken {
@SerializedName("access_token")
public String accessToken = Constants.EMPTY;
@SerializedName("token_type")
public String tokenType = Constants.EMPTY;
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.auth;
/**
* The {@link ChinaPublicKey} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class ChinaPublicKey {
public String value;// ": "-----BEGIN PUBLIC
// KEY-----\r\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCteEZFIGa2z5cj7sAmX40y8/ige01T2r+VUzkMshAYwotZFvrVWZLQ6W9+ltvINJoRfZEZkmdP2lsidhqj1H1+RWyC78ear7Fm6xd9Gp9LnKtVVBJRM/9cBRg0AGiTJ7IO/x6MpKkBxxHmProFqPI40hueunV85RlaPBrjZVNIpQIDAQAB\r\n-----END
// PUBLIC KEY-----",
public String expires;// ": "3600"
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.auth;
/**
* The {@link ChinaPublicKeyResponse} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class ChinaPublicKeyResponse {
public ChinaPublicKey data;
public int code;// ":200,
public String error;// ":false,
public String description;// ":"ok"
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.auth;
/**
* The {@link ChinaTokenExpiration} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class ChinaTokenExpiration {
public String jti;// ":"DUMMY$1$A$1637707916782",
public long nbf;// ":1637707916,
public long exp;// ":1637711216,
public long iat;// ":1637707916}
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.auth;
/**
* The {@link ChinaTokenResponse} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class ChinaTokenResponse {
public ChinaAccessToken data;
public int code;// ":200,
public String error;// ":false,
public String description;// ":"ok"
}

View File

@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.charge;
import java.util.List;
/**
* The {@link ChargeProfile} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
* @author Norbert Truchsess - edit & send of charge profile
*/
public class ChargeProfile {
public static final Timer INVALID_TIMER = new Timer();
public ChargingWindow reductionOfChargeCurrent;
public String chargingMode;// ": "immediateCharging",
public String chargingPreference;// ": "chargingWindow",
public String chargingControlType;// ": "weeklyPlanner",
public List<Timer> departureTimes;
public boolean climatisationOn;// ": false,
public ChargingSettings chargingSettings;
public Timer getTimerId(int id) {
if (departureTimes != null) {
for (Timer t : departureTimes) {
if (t.id == id) {
return t;
}
}
}
return INVALID_TIMER;
}
}

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.charge;
/**
* The {@link ChargeSession} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class ChargeSession {
public String id;// ": "2021-12-26T16:57:20Z_128fa4af",
public String title;// ": "Gestern 17:57",
public String subtitle;// ": "Uferstraße 4B 7h 45min -- EUR",
public String energyCharged;// ": "~ 31 kWh",
public String sessionStatus;// ": "FINISHED",
public String issues;// ": "2 Probleme",
public String isPublic;// ": false
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.charge;
import java.util.List;
/**
* The {@link ChargeSessions} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class ChargeSessions {
public String total;// ": "~ 218 kWh",
public String numberOfSessions;// ": "17",
public String chargingListState;// ": "HAS_SESSIONS",
public List<ChargeSession> sessions;
}

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.charge;
/**
* The {@link ChargeSessionsContainer} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class ChargeSessionsContainer {
public Object paginationInfo;
public ChargeSessions chargingSessions;
}

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.charge;
/**
* The {@link ChargeStatistics} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class ChargeStatistics {
public int totalEnergyCharged;// ": 173,
public String totalEnergyChargedSemantics;// ": "Insgesamt circa 173 Kilowattstunden geladen",
public String symbol;// ": "~",
public int numberOfChargingSessions;// ": 13,
public String numberOfChargingSessionsSemantics;// ": "13 Ladevorgänge"
}

View File

@ -0,0 +1,24 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.charge;
/**
* The {@link ChargeStatisticsContainer} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class ChargeStatisticsContainer {
public String description;// ": "Dezember 2021",
public String optStateType;// ": "OPT_IN_WITH_SESSIONS",
public ChargeStatistics statistics;// ": {
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.charge;
/**
* The {@link ChargingSettings} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class ChargingSettings {
public int targetSoc;// ": 100,
public boolean isAcCurrentLimitActive;// ": false,
public String hospitality;// ": "NO_ACTION",
public String idcc;// ": "NO_ACTION"
}

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.charge;
/**
* The {@link ChargingWindow} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class ChargingWindow {
public Time start;
public Time end;
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.charge;
import org.openhab.binding.mybmw.internal.utils.Converter;
/**
* The {@link Time} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
* @author Norbert Truchsess - edit & send of charge profile
*/
public class Time {
public int hour;// ": 11,
public int minute;// ": 0
@Override
public String toString() {
return Converter.getTime(this);
}
}

View File

@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.charge;
import java.util.List;
import org.openhab.binding.mybmw.internal.utils.Constants;
/**
* The {@link Timer} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
* @author Norbert Truchsess - edit & send of charge profile
*/
public class Timer {
public int id = -1;// ": 1,
public String action;// ": "deactivate",
public Time timeStamp;
public List<String> timerWeekDays;
@Override
public String toString() {
return id + Constants.COLON + action + Constants.COLON + timeStamp + Constants.COLON + timerWeekDays;
}
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.network;
import org.openhab.binding.mybmw.internal.utils.Constants;
import org.openhab.binding.mybmw.internal.utils.Converter;
/**
* The {@link NetworkError} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class NetworkError {
public String url;
public int status;
public String reason;
public String params;
@Override
public String toString() {
return new StringBuilder(url).append(Constants.HYPHEN).append(status).append(Constants.HYPHEN).append(reason)
.append(params).toString();
}
public String toJson() {
return Converter.getGson().toJson(this);
}
}

View File

@ -0,0 +1,22 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.properties;
/**
* The {@link Address} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class Address {
public String formatted;
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.properties;
import org.openhab.binding.mybmw.internal.utils.Constants;
/**
* The {@link CBS} Data Transfer Object ConditionBasedService
*
* @author Bernd Weymann - Initial contribution
*/
public class CBS {
public String type = Constants.NO_ENTRIES;// ": "BRAKE_FLUID",
public String status = Constants.NO_ENTRIES;// ": "OK",
public String dateTime;// ": "2023-11-01T00:00:00.000Z"
public Distance distance;
}

View File

@ -0,0 +1,22 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.properties;
/**
* The {@link CCM} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class CCM {
// [todo] [todo] definition currently unknown
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.properties;
/**
* The {@link ChargingState} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class ChargingState {
public int chargePercentage;// ": 74,
public String state;// ": "NOT_CHARGING",
public String type;// ": "NOT_AVAILABLE",
public boolean isChargerConnected;// ": false
}

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.properties;
/**
* The {@link Coordinates} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class Coordinates {
public double latitude;// ": 50.556049,
public double longitude;// ": 8.495669
}

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.properties;
/**
* The {@link Distance} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class Distance {
public int value;// ": 31,
public String units;// ": "KILOMETERS"
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.properties;
/**
* The {@link Doors} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class Doors {
public String driverFront;// ": "CLOSED",
public String driverRear;// ": "CLOSED",
public String passengerFront;// ": "CLOSED",
public String passengerRear;// ": "CLOSED"
}

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.properties;
/**
* The {@link DoorsWindows} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class DoorsWindows {
public Doors doors;
public Windows windows;
public String trunk;// ": "CLOSED",
public String hood;// ": "CLOSED",
public String moonroof;// ": "CLOSED"
}

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.properties;
/**
* The {@link FuelLevel} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class FuelLevel {
public int value;// ": 4,
public String units;// ": "LITERS"
}

View File

@ -0,0 +1,24 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.properties;
/**
* The {@link Location} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class Location {
public Coordinates coordinates;
public Address address;
public int heading;// ": 222
}

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.properties;
import java.util.List;
/**
* The {@link Properties} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class Properties {
public String lastUpdatedAt;// ": "2021-12-21T16:46:02Z",
public boolean inMotion;// ": false,
public boolean areDoorsLocked;// ": true,
public String originCountryISO;// ": "DE",
public boolean areDoorsClosed;// ": true,
public boolean areDoorsOpen;// ": false,
public boolean areWindowsClosed;// ": true,
public DoorsWindows doorsAndWindows;// ":
public boolean isServiceRequired;// ":false
public FuelLevel fuelLevel;
public ChargingState chargingState;// ":
public Range combustionRange;
public Range combinedRange;
public Range electricRange;
public Range electricRangeAndStatus;
public List<CCM> checkControlMessages;
public List<CBS> serviceRequired;
public Location vehicleLocation;
public Tires tires;
// "climateControl":{} [todo] definition currently unknown
}

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.properties;
/**
* The {@link Range} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class Range {
public int chargePercentage;
public Distance distance;
}

View File

@ -0,0 +1,22 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.properties;
/**
* The {@link Tire} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class Tire {
public TireStatus status;
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.properties;
/**
* The {@link TireStatus} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class TireStatus {
public double currentPressure;// ": 220,
public String localizedCurrentPressure;// ": "2.2 bar",
public String localizedTargetPressure;// ": "2.3 bar",
public double targetPressure;// ": 230
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.properties;
/**
* The {@link Tires} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class Tires {
public Tire frontLeft;
public Tire frontRight;
public Tire rearLeft;
public Tire rearRight;
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.properties;
/**
* The {@link Windows} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class Windows {
public String driverFront;// ": "CLOSED",
public String driverRear;// ": "CLOSED",
public String passengerFront;// ": "CLOSED",
public String passengerRear;// ": "CLOSED"
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.remote;
/**
* The {@link ExecutionError} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class ExecutionError {
public String title;// ": "Etwas ist schiefgelaufen",
public String description;// ": "Die folgenden Einschränkungen verbieten die Ausführung von Remote Services: Aus
// Sicherheitsgründen sind Remote Services nicht verfügbar, wenn die Fahrbereitschaft
// eingeschaltet ist. Remote Services können nur mit einem ausreichenden Ladezustand
// durchgeführt werden. Die Remote Services Verriegeln und Entriegeln können nur
// ausgeführt werden, wenn die Fahrertür geschlossen und der Türstatus bekannt ist.",
public String presentationType;// ": "PAGE",
public int iconId;// ": 60217,
public boolean isRetriable;// ": true,
public String errorDetails;// ": "NACK"
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.remote;
/**
* The {@link ExecutionStatusContainer} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class ExecutionStatusContainer {
public String eventId;
public String creationTime;
public String eventStatus;
public ExecutionError errorDetails;
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.status;
/**
* The {@link CBSMessage} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class CBSMessage {
public String id;// ": "BrakeFluid",
public String title;// ": "Brake fluid",
public int iconId;// ": 60223,
public String longDescription;// ": "Next service due by the specified date.",
public String subtitle;// ": "Due in November 2023",
public String criticalness;// ": "nonCritical"
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.status;
import org.openhab.binding.mybmw.internal.utils.Constants;
/**
* The {@link CCMMessage} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class CCMMessage {
public String criticalness;// ": "semiCritical",
public int iconId;// ": 60217,
public String state = Constants.NO_ENTRIES;// ": "Medium",
public String title = Constants.NO_ENTRIES;// ": "Battery discharged: Start engine"
public String id;// ": "229",
public String longDescription = Constants.NO_ENTRIES;// ": "Charge by driving for longer periods or use external
// charger. Functions requiring battery will be switched off.
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.status;
/**
* The {@link DoorWindow} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class DoorWindow {
public int iconId;// ": 59757,
public String title;// ": "Lock status",
public String state;// ": "Locked",
public String criticalness;// ": "nonCritical"
}

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.status;
/**
* The {@link FuelIndicator} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class FuelIndicator {
public int mainBarValue;// ": 74,
public String rangeUnits;// ": "km",
public String rangeValue;// ": "76",
public String levelUnits;// ": "%",
public String levelValue;// ": "74",
public int secondaryBarValue;// ": 0,
public int infoIconId;// ": 59694,
public int rangeIconId;// ": 59683,
public int levelIconId;// ": 59694,
public boolean showsBar;// ": true,
public boolean showBarGoal;// ": false,
public String barType;// ": null,
public String infoLabel;// ": "State of Charge",
public boolean isInaccurate;// ": false,
public boolean isCircleIcon;// ": false,
public String iconOpacity;// ": "high",
public String chargingType;// ": null,
public String chargingStatusType;// ": "DEFAULT",
public String chargingStatusIndicatorType;// ": "DEFAULT"
}

View File

@ -0,0 +1,22 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.status;
/**
* The {@link Issues} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class Issues {
// [todo] definition currently unknown
}

View File

@ -0,0 +1,24 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.status;
/**
* The {@link Mileage} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class Mileage {
public int mileage;// ": 31537,
public String units;// ": "km",
public String formattedMileage;// ": "31537"
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.status;
import java.util.List;
import org.openhab.binding.mybmw.internal.dto.charge.ChargeProfile;
/**
* The {@link Status} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class Status {
public String lastUpdatedAt;// ": "2021-12-21T16:46:02Z",
public Mileage currentMileage;
public Issues issues;
public String doorsGeneralState;// ":"Locked",
public String checkControlMessagesGeneralState;// ":"No Issues",
public List<DoorWindow> doorsAndWindows;// ":[
public List<CCMMessage> checkControlMessages;//
public List<CBSMessage> requiredServices;//
// "recallMessages":[],
// "recallExternalUrl":null,
public List<FuelIndicator> fuelIndicators;
public String timestampMessage;// ":"Updated from vehicle 12/21/2021 05:46 PM",
public ChargeProfile chargingProfile;
}

View File

@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.vehicle;
/**
* The {@link Capabilities} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class Capabilities {
public boolean isRemoteServicesBookingRequired;
public boolean isRemoteServicesActivationRequired;
public boolean isRemoteHistorySupported;
public boolean canRemoteHistoryBeDeleted;
public boolean isChargingHistorySupported;
public boolean isScanAndChargeSupported;
public boolean isDCSContractManagementSupported;
public boolean isBmwChargingSupported;
public boolean isMiniChargingSupported;
public boolean isChargeNowForBusinessSupported;
public boolean isDataPrivacyEnabled;
public boolean isChargingPlanSupported;
public boolean isChargingPowerLimitEnable;
public boolean isChargingTargetSocEnable;
public boolean isChargingLoudnessEnable;
public boolean isChargingSettingsEnabled;
public boolean isChargingHospitalityEnabled;
public boolean isEvGoChargingSupported;
public boolean isFindChargingEnabled;
public boolean isCustomerEsimSupported;
public boolean isCarSharingSupported;
public boolean isEasyChargeSupported;
public RemoteService lock;
public RemoteService unlock;
public RemoteService lights;
public RemoteService horn;
public RemoteService vehicleFinder;
public RemoteService sendPoi;
public RemoteService climateNow;
}

View File

@ -0,0 +1,24 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.vehicle;
/**
* The {@link RemoteService} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class RemoteService {
public boolean isEnabled;// ": true,
public boolean isPinAuthenticationRequired;// ": false,
public String executionMessage;// ": "Lock your vehicle now? Remote functions may take a few seconds."
}

View File

@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.dto.vehicle;
import org.openhab.binding.mybmw.internal.dto.properties.Properties;
import org.openhab.binding.mybmw.internal.dto.status.Status;
/**
* The {@link Vehicle} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
public class Vehicle {
public String vin;// ": "WBY1Z81040V905639",
public String model;// ": "i3 94 (+ REX)",
public int year;// ": 2017,
public String brand;// ": "BMW",
public String headUnit;// ": "ID5",
public boolean isLscSupported;// ": true,
public String driveTrain;// ": "ELECTRIC",
public String puStep;// ": "0321",
public String iStep;// ": "I001-21-03-530",
public String telematicsUnit;// ": "TCB1",
public String hmiVersion;// ": "ID4",
public String bodyType;// ": "I01",
public String a4aType;// ": "USB_ONLY",
public String exFactoryPUStep;// ": "0717",
public String exFactoryILevel;// ": "I001-17-07-500"
public Capabilities capabilities;
// "connectedDriveServices": [] currently no clue how to resolve,
public Properties properties;
public boolean isMappingPending;// ":false,"
public boolean isMappingUnconfirmed;// ":false,
public Status status;
public boolean valid = false;
}

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link ByteResponseCallback} Interface for all raw byte results from ASYNC REST API
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public interface ByteResponseCallback extends ResponseCallback {
public void onResponse(byte[] result);
}

View File

@ -0,0 +1,141 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.handler;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mybmw.internal.MyBMWConfiguration;
import org.openhab.binding.mybmw.internal.discovery.VehicleDiscovery;
import org.openhab.binding.mybmw.internal.dto.network.NetworkError;
import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
import org.openhab.binding.mybmw.internal.utils.BimmerConstants;
import org.openhab.binding.mybmw.internal.utils.Constants;
import org.openhab.binding.mybmw.internal.utils.Converter;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MyBMWBridgeHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class MyBMWBridgeHandler extends BaseBridgeHandler implements StringResponseCallback {
private final Logger logger = LoggerFactory.getLogger(MyBMWBridgeHandler.class);
private HttpClientFactory httpClientFactory;
private Optional<VehicleDiscovery> discoveryService = Optional.empty();
private Optional<MyBMWProxy> proxy = Optional.empty();
private Optional<ScheduledFuture<?>> initializerJob = Optional.empty();
private Optional<String> troubleshootFingerprint = Optional.empty();
private String localeLanguage;
public MyBMWBridgeHandler(Bridge bridge, HttpClientFactory hcf, String language) {
super(bridge);
httpClientFactory = hcf;
localeLanguage = language;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// no commands available
}
@Override
public void initialize() {
troubleshootFingerprint = Optional.empty();
updateStatus(ThingStatus.UNKNOWN);
MyBMWConfiguration config = getConfigAs(MyBMWConfiguration.class);
if (config.language.equals(Constants.LANGUAGE_AUTODETECT)) {
config.language = localeLanguage;
}
if (!checkConfiguration(config)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
} else {
proxy = Optional.of(new MyBMWProxy(httpClientFactory, config));
initializerJob = Optional.of(scheduler.schedule(this::requestVehicles, 2, TimeUnit.SECONDS));
}
}
public static boolean checkConfiguration(MyBMWConfiguration config) {
if (Constants.EMPTY.equals(config.userName) || Constants.EMPTY.equals(config.password)) {
return false;
} else {
return BimmerConstants.EADRAX_SERVER_MAP.containsKey(config.region);
}
}
@Override
public void dispose() {
initializerJob.ifPresent(job -> job.cancel(true));
}
public void requestVehicles() {
proxy.ifPresent(prox -> prox.requestVehicles(this));
}
private void logFingerPrint() {
logger.debug("###### Discovery Fingerprint Data - BEGIN ######");
logger.debug("{}", troubleshootFingerprint.get());
logger.debug("###### Discovery Fingerprint Data - END ######");
}
/**
* Response for vehicle request
*/
@Override
public synchronized void onResponse(@Nullable String response) {
if (response != null) {
updateStatus(ThingStatus.ONLINE);
List<Vehicle> vehicleList = Converter.getVehicleList(response);
discoveryService.get().onResponse(vehicleList);
troubleshootFingerprint = Optional.of(Converter.anonymousFingerprint(response));
logFingerPrint();
}
}
@Override
public void onError(NetworkError error) {
troubleshootFingerprint = Optional.of(error.toJson());
logFingerPrint();
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error.reason);
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(VehicleDiscovery.class);
}
public Optional<MyBMWProxy> getProxy() {
return proxy;
}
public void setDiscoveryService(VehicleDiscovery discoveryService) {
this.discoveryService = Optional.of(discoveryService);
}
}

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.thing.binding.BaseDynamicCommandDescriptionProvider;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.link.ItemChannelLinkRegistry;
import org.openhab.core.thing.type.DynamicCommandDescriptionProvider;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* Dynamic provider of command options while leaving other state description fields as original.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
@Component(service = { DynamicCommandDescriptionProvider.class, MyBMWCommandOptionProvider.class })
public class MyBMWCommandOptionProvider extends BaseDynamicCommandDescriptionProvider {
@Activate
public MyBMWCommandOptionProvider(final @Reference EventPublisher eventPublisher, //
final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry, //
final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.eventPublisher = eventPublisher;
this.itemChannelLinkRegistry = itemChannelLinkRegistry;
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
}

View File

@ -0,0 +1,510 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.handler;
import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.*;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import javax.crypto.Cipher;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpResponseException;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.UrlEncoded;
import org.openhab.binding.mybmw.internal.MyBMWConfiguration;
import org.openhab.binding.mybmw.internal.VehicleConfiguration;
import org.openhab.binding.mybmw.internal.dto.auth.AuthQueryResponse;
import org.openhab.binding.mybmw.internal.dto.auth.AuthResponse;
import org.openhab.binding.mybmw.internal.dto.auth.ChinaPublicKeyResponse;
import org.openhab.binding.mybmw.internal.dto.auth.ChinaTokenExpiration;
import org.openhab.binding.mybmw.internal.dto.auth.ChinaTokenResponse;
import org.openhab.binding.mybmw.internal.dto.network.NetworkError;
import org.openhab.binding.mybmw.internal.handler.simulation.Injector;
import org.openhab.binding.mybmw.internal.utils.BimmerConstants;
import org.openhab.binding.mybmw.internal.utils.Constants;
import org.openhab.binding.mybmw.internal.utils.Converter;
import org.openhab.binding.mybmw.internal.utils.HTTPConstants;
import org.openhab.binding.mybmw.internal.utils.ImageProperties;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MyBMWProxy} This class holds the important constants for the BMW Connected Drive Authorization.
* They
* are taken from the Bimmercode from github {@link https://github.com/bimmerconnected/bimmer_connected}
* File defining these constants
* {@link https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/account.py}
* https://customer.bmwgroup.com/one/app/oauth.js
*
* @author Bernd Weymann - Initial contribution
* @author Norbert Truchsess - edit & send of charge profile
*/
@NonNullByDefault
public class MyBMWProxy {
private final Logger logger = LoggerFactory.getLogger(MyBMWProxy.class);
private Optional<RemoteServiceHandler> remoteServiceHandler = Optional.empty();
private final Token token = new Token();
private final HttpClient httpClient;
private final MyBMWConfiguration configuration;
/**
* URLs taken from https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/const.py
*/
final String vehicleUrl;
final String remoteCommandUrl;
final String remoteStatusUrl;
final String serviceExecutionAPI = "/executeService";
final String serviceExecutionStateAPI = "/serviceExecutionStatus";
final String remoteServiceEADRXstatusUrl = BimmerConstants.API_REMOTE_SERVICE_BASE_URL
+ "eventStatus?eventId={event_id}";
public MyBMWProxy(HttpClientFactory httpClientFactory, MyBMWConfiguration config) {
httpClient = httpClientFactory.getCommonHttpClient();
configuration = config;
vehicleUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
+ BimmerConstants.API_VEHICLES;
remoteCommandUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
+ BimmerConstants.API_REMOTE_SERVICE_BASE_URL;
remoteStatusUrl = remoteCommandUrl + "eventStatus";
}
public synchronized void call(final String url, final boolean post, final @Nullable String encoding,
final @Nullable String params, final String brand, final ResponseCallback callback) {
// only executed in "simulation mode"
// SimulationTest.testSimulationOff() assures Injector is off when releasing
if (Injector.isActive()) {
if (url.equals(vehicleUrl)) {
((StringResponseCallback) callback).onResponse(Injector.getDiscovery());
} else if (url.endsWith(vehicleUrl)) {
((StringResponseCallback) callback).onResponse(Injector.getStatus());
} else {
logger.debug("Simulation of {} not supported", url);
}
return;
}
// return in case of unknown brand
String userAgent = BimmerConstants.BRAND_USER_AGENTS_MAP.get(brand.toLowerCase());
if (userAgent == null) {
logger.warn("Unknown Brand {}", brand);
return;
}
final Request req;
final String completeUrl;
if (post) {
completeUrl = url;
req = httpClient.POST(url);
if (encoding != null) {
req.header(HttpHeader.CONTENT_TYPE, encoding);
if (CONTENT_TYPE_URL_ENCODED.equals(encoding)) {
req.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, params, StandardCharsets.UTF_8));
} else if (CONTENT_TYPE_JSON_ENCODED.equals(encoding)) {
req.content(new StringContentProvider(CONTENT_TYPE_JSON_ENCODED, params, StandardCharsets.UTF_8));
}
}
} else {
completeUrl = params == null ? url : url + Constants.QUESTION + params;
req = httpClient.newRequest(completeUrl);
}
req.header(HttpHeader.AUTHORIZATION, getToken().getBearerToken());
req.header(HTTPConstants.X_USER_AGENT, userAgent);
req.header(HttpHeader.ACCEPT_LANGUAGE, configuration.language);
if (callback instanceof ByteResponseCallback) {
req.header(HttpHeader.ACCEPT, "image/png");
} else {
req.header(HttpHeader.ACCEPT, CONTENT_TYPE_JSON_ENCODED);
}
req.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(new BufferingResponseListener() {
@NonNullByDefault({})
@Override
public void onComplete(Result result) {
if (result.getResponse().getStatus() != 200) {
NetworkError error = new NetworkError();
error.url = completeUrl;
error.status = result.getResponse().getStatus();
if (result.getResponse().getReason() != null) {
error.reason = result.getResponse().getReason();
} else {
error.reason = result.getFailure().getMessage();
}
error.params = result.getRequest().getParams().toString();
logger.debug("HTTP Error {}", error.toString());
callback.onError(error);
} else {
if (callback instanceof StringResponseCallback) {
((StringResponseCallback) callback).onResponse(getContentAsString());
} else if (callback instanceof ByteResponseCallback) {
((ByteResponseCallback) callback).onResponse(getContent());
} else {
logger.error("unexpected reponse type {}", callback.getClass().getName());
}
}
}
});
}
public void get(String url, @Nullable String coding, @Nullable String params, final String brand,
ResponseCallback callback) {
call(url, false, coding, params, brand, callback);
}
public void post(String url, @Nullable String coding, @Nullable String params, final String brand,
ResponseCallback callback) {
call(url, true, coding, params, brand, callback);
}
/**
* request all vehicles for one specific brand
*
* @param brand
* @param callback
*/
public void requestVehicles(String brand, StringResponseCallback callback) {
// calculate necessary parameters for query
MultiMap<String> vehicleParams = new MultiMap<String>();
vehicleParams.put(BimmerConstants.TIRE_GUARD_MODE, Constants.ENABLED);
vehicleParams.put(BimmerConstants.APP_DATE_TIME, Long.toString(System.currentTimeMillis()));
vehicleParams.put(BimmerConstants.APP_TIMEZONE, Integer.toString(Converter.getOffsetMinutes()));
String params = UrlEncoded.encode(vehicleParams, StandardCharsets.UTF_8, false);
get(vehicleUrl + "?" + params, null, null, brand, callback);
}
/**
* request vehicles for all possible brands
*
* @param callback
*/
public void requestVehicles(StringResponseCallback callback) {
BimmerConstants.ALL_BRANDS.forEach(brand -> {
requestVehicles(brand, callback);
});
}
public void requestImage(VehicleConfiguration config, ImageProperties props, ByteResponseCallback callback) {
final String localImageUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
+ "/eadrax-ics/v3/presentation/vehicles/" + config.vin + "/images?carView=" + props.viewport;
get(localImageUrl, null, null, config.vehicleBrand, callback);
}
/**
* request charge statistics for electric vehicles
*
* @param callback
*/
public void requestChargeStatistics(VehicleConfiguration config, StringResponseCallback callback) {
MultiMap<String> chargeStatisticsParams = new MultiMap<String>();
chargeStatisticsParams.put("vin", config.vin);
chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime());
String params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false);
String chargeStatisticsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
+ "/eadrax-chs/v1/charging-statistics?" + params;
get(chargeStatisticsUrl, null, null, config.vehicleBrand, callback);
}
/**
* request charge statistics for electric vehicles
*
* @param callback
*/
public void requestChargeSessions(VehicleConfiguration config, StringResponseCallback callback) {
MultiMap<String> chargeSessionsParams = new MultiMap<String>();
chargeSessionsParams.put("vin", "WBY1Z81040V905639");
chargeSessionsParams.put("maxResults", "40");
chargeSessionsParams.put("include_date_picker", "true");
String params = UrlEncoded.encode(chargeSessionsParams, StandardCharsets.UTF_8, false);
String chargeSessionsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
+ "/eadrax-chs/v1/charging-sessions?" + params;
get(chargeSessionsUrl, null, null, config.vehicleBrand, callback);
}
RemoteServiceHandler getRemoteServiceHandler(VehicleHandler vehicleHandler) {
remoteServiceHandler = Optional.of(new RemoteServiceHandler(vehicleHandler, this));
return remoteServiceHandler.get();
}
// Token handling
/**
* Gets new token if old one is expired or invalid. In case of error the token remains.
* So if token refresh fails the corresponding requests will also fail and update the
* Thing status accordingly.
*
* @return token
*/
public Token getToken() {
if (!token.isValid()) {
boolean tokenUpdateSuccess = false;
switch (configuration.region) {
case BimmerConstants.REGION_CHINA:
tokenUpdateSuccess = updateTokenChina();
break;
case BimmerConstants.REGION_NORTH_AMERICA:
tokenUpdateSuccess = updateToken();
break;
case BimmerConstants.REGION_ROW:
tokenUpdateSuccess = updateToken();
break;
default:
logger.warn("Region {} not supported", configuration.region);
break;
}
if (!tokenUpdateSuccess) {
logger.debug("Authorization failed!");
}
}
return token;
}
/**
* Everything is catched by surroundig try catch
* - HTTP Exceptions
* - JSONSyntax Exceptions
* - potential NullPointer Exceptions
*
* @return
*/
@SuppressWarnings("null")
public synchronized boolean updateToken() {
try {
/*
* Step 1) Get basic values for further queries
*/
String authValuesUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
+ BimmerConstants.API_OAUTH_CONFIG;
Request authValuesRequest = httpClient.newRequest(authValuesUrl);
authValuesRequest.header(ACP_SUBSCRIPTION_KEY, BimmerConstants.OCP_APIM_KEYS.get(configuration.region));
authValuesRequest.header(X_USER_AGENT, BimmerConstants.USER_AGENT_BMW);
ContentResponse authValuesResponse = authValuesRequest.send();
if (authValuesResponse.getStatus() != 200) {
throw new HttpResponseException("URL: " + authValuesRequest.getURI() + ", Error: "
+ authValuesResponse.getStatus() + ", Message: " + authValuesResponse.getContentAsString(),
authValuesResponse);
}
AuthQueryResponse aqr = Converter.getGson().fromJson(authValuesResponse.getContentAsString(),
AuthQueryResponse.class);
/*
* Step 2) Calculate values for base parameters
*/
String verfifierBytes = Converter.getRandomString(64);
String codeVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(verfifierBytes.getBytes());
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(codeVerifier.getBytes(StandardCharsets.UTF_8));
String codeChallange = Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
String stateBytes = Converter.getRandomString(16);
String state = Base64.getUrlEncoder().withoutPadding().encodeToString(stateBytes.getBytes());
MultiMap<String> baseParams = new MultiMap<String>();
baseParams.put(CLIENT_ID, aqr.clientId);
baseParams.put(RESPONSE_TYPE, CODE);
baseParams.put(REDIRECT_URI, aqr.returnUrl);
baseParams.put(STATE, state);
baseParams.put(NONCE, BimmerConstants.LOGIN_NONCE);
baseParams.put(SCOPE, String.join(Constants.SPACE, aqr.scopes));
baseParams.put(CODE_CHALLENGE, codeChallange);
baseParams.put(CODE_CHALLENGE_METHOD, "S256");
/**
* Step 3) Authorization with username and password
*/
String loginUrl = aqr.gcdmBaseUrl + BimmerConstants.OAUTH_ENDPOINT;
Request loginRequest = httpClient.POST(loginUrl);
loginRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED);
MultiMap<String> loginParams = new MultiMap<String>(baseParams);
loginParams.put(GRANT_TYPE, BimmerConstants.AUTHORIZATION_CODE);
loginParams.put(USERNAME, configuration.userName);
loginParams.put(PASSWORD, configuration.password);
loginRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
UrlEncoded.encode(loginParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
ContentResponse loginResponse = loginRequest.send();
if (loginResponse.getStatus() != 200) {
throw new HttpResponseException("URL: " + loginRequest.getURI() + ", Error: "
+ loginResponse.getStatus() + ", Message: " + loginResponse.getContentAsString(),
loginResponse);
}
String authCode = getAuthCode(loginResponse.getContentAsString());
/**
* Step 4) Authorize with code
*/
Request authRequest = httpClient.POST(loginUrl).followRedirects(false);
MultiMap<String> authParams = new MultiMap<String>(baseParams);
authParams.put(AUTHORIZATION, authCode);
authRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED);
authRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
UrlEncoded.encode(authParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
ContentResponse authResponse = authRequest.send();
if (authResponse.getStatus() != 302) {
throw new HttpResponseException("URL: " + authRequest.getURI() + ", Error: " + authResponse.getStatus()
+ ", Message: " + authResponse.getContentAsString(), authResponse);
}
String code = MyBMWProxy.codeFromUrl(authResponse.getHeaders().get(HttpHeader.LOCATION));
/**
* Step 5) Request token
*/
Request codeRequest = httpClient.POST(aqr.tokenEndpoint);
String basicAuth = "Basic "
+ Base64.getUrlEncoder().encodeToString((aqr.clientId + ":" + aqr.clientSecret).getBytes());
codeRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED);
codeRequest.header(AUTHORIZATION, basicAuth);
MultiMap<String> codeParams = new MultiMap<String>();
codeParams.put(CODE, code);
codeParams.put(CODE_VERIFIER, codeVerifier);
codeParams.put(REDIRECT_URI, aqr.returnUrl);
codeParams.put(GRANT_TYPE, BimmerConstants.AUTHORIZATION_CODE);
codeRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
UrlEncoded.encode(codeParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
ContentResponse codeResponse = codeRequest.send();
if (codeResponse.getStatus() != 200) {
throw new HttpResponseException("URL: " + codeRequest.getURI() + ", Error: " + codeResponse.getStatus()
+ ", Message: " + codeResponse.getContentAsString(), codeResponse);
}
AuthResponse ar = Converter.getGson().fromJson(codeResponse.getContentAsString(), AuthResponse.class);
token.setType(ar.tokenType);
token.setToken(ar.accessToken);
token.setExpiration(ar.expiresIn);
return true;
} catch (Exception e) {
logger.warn("Authorization Exception: {}", e.getMessage());
}
return false;
}
private String getAuthCode(String response) {
String[] keys = response.split("&");
for (int i = 0; i < keys.length; i++) {
if (keys[i].startsWith(AUTHORIZATION)) {
String authCode = keys[i].split("=")[1];
authCode = authCode.split("\"")[0];
return authCode;
}
}
return Constants.EMPTY;
}
public static String codeFromUrl(String encodedUrl) {
final MultiMap<String> tokenMap = new MultiMap<String>();
UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII);
final StringBuilder codeFound = new StringBuilder();
tokenMap.forEach((key, value) -> {
if (value.size() > 0) {
String val = value.get(0);
if (key.endsWith(CODE)) {
codeFound.append(val.toString());
}
}
});
return codeFound.toString();
}
@SuppressWarnings("null")
public synchronized boolean updateTokenChina() {
try {
/**
* Step 1) get public key
*/
String publicKeyUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_CHINA)
+ BimmerConstants.CHINA_PUBLIC_KEY;
Request oauthQueryRequest = httpClient.newRequest(publicKeyUrl);
oauthQueryRequest.header(X_USER_AGENT, BimmerConstants.USER_AGENT_BMW);
ContentResponse publicKeyResponse = oauthQueryRequest.send();
if (publicKeyResponse.getStatus() != 200) {
throw new HttpResponseException("URL: " + oauthQueryRequest.getURI() + ", Error: "
+ publicKeyResponse.getStatus() + ", Message: " + publicKeyResponse.getContentAsString(),
publicKeyResponse);
}
ChinaPublicKeyResponse pkr = Converter.getGson().fromJson(publicKeyResponse.getContentAsString(),
ChinaPublicKeyResponse.class);
/**
* Step 2) Encode password with public key
*/
// https://www.baeldung.com/java-read-pem-file-keys
String publicKeyStr = pkr.data.value;
String publicKeyPEM = publicKeyStr.replace("-----BEGIN PUBLIC KEY-----", "")
.replaceAll(System.lineSeparator(), "").replace("-----END PUBLIC KEY-----", "").replace("\\r", "")
.replace("\\n", "").trim();
byte[] encoded = Base64.getDecoder().decode(publicKeyPEM);
X509EncodedKeySpec spec = new X509EncodedKeySpec(encoded);
KeyFactory kf = KeyFactory.getInstance("RSA");
PublicKey publicKey = kf.generatePublic(spec);
// https://www.thexcoders.net/java-ciphers-rsa/
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encryptedBytes = cipher.doFinal(configuration.password.getBytes());
String encodedPassword = Base64.getEncoder().encodeToString(encryptedBytes);
/**
* Step 3) Send Auth with encoded password
*/
String tokenUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_CHINA)
+ BimmerConstants.CHINA_LOGIN;
Request loginRequest = httpClient.POST(tokenUrl);
loginRequest.header(X_USER_AGENT, BimmerConstants.USER_AGENT_BMW);
String jsonContent = "{ \"mobile\":\"" + configuration.userName + "\", \"password\":\"" + encodedPassword
+ "\"}";
loginRequest.content(new StringContentProvider(jsonContent));
ContentResponse tokenResponse = loginRequest.send();
if (tokenResponse.getStatus() != 200) {
throw new HttpResponseException("URL: " + loginRequest.getURI() + ", Error: "
+ tokenResponse.getStatus() + ", Message: " + tokenResponse.getContentAsString(),
tokenResponse);
}
String authCode = getAuthCode(tokenResponse.getContentAsString());
/**
* Step 4) Decode access token
*/
ChinaTokenResponse cat = Converter.getGson().fromJson(authCode, ChinaTokenResponse.class);
String token = cat.data.accessToken;
// https://www.baeldung.com/java-jwt-token-decode
String[] chunks = token.split("\\.");
String tokenJwtDecodeStr = new String(Base64.getUrlDecoder().decode(chunks[1]));
ChinaTokenExpiration cte = Converter.getGson().fromJson(tokenJwtDecodeStr, ChinaTokenExpiration.class);
Token t = new Token();
t.setToken(token);
t.setType(cat.data.tokenType);
t.setExpirationTotal(cte.exp);
return true;
} catch (Exception e) {
logger.warn("Authorization Exception: {}", e.getMessage());
}
return false;
}
}

View File

@ -0,0 +1,227 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.handler;
import static org.openhab.binding.mybmw.internal.MyBMWConstants.*;
import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.CONTENT_TYPE_JSON_ENCODED;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.UrlEncoded;
import org.openhab.binding.mybmw.internal.VehicleConfiguration;
import org.openhab.binding.mybmw.internal.dto.network.NetworkError;
import org.openhab.binding.mybmw.internal.dto.remote.ExecutionStatusContainer;
import org.openhab.binding.mybmw.internal.utils.Constants;
import org.openhab.binding.mybmw.internal.utils.Converter;
import org.openhab.binding.mybmw.internal.utils.HTTPConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonSyntaxException;
/**
* The {@link RemoteServiceHandler} handles executions of remote services towards your Vehicle
*
* @see https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/remote_services.py
*
* @author Bernd Weymann - Initial contribution
* @author Norbert Truchsess - edit & send of charge profile
*/
@NonNullByDefault
public class RemoteServiceHandler implements StringResponseCallback {
private final Logger logger = LoggerFactory.getLogger(RemoteServiceHandler.class);
private static final String EVENT_ID = "eventId";
private static final String DATA = "data";
private static final int GIVEUP_COUNTER = 12; // after 12 retries the state update will give up
private static final int STATE_UPDATE_SEC = HTTPConstants.HTTP_TIMEOUT_SEC + 1; // regular timeout + 1sec
private final MyBMWProxy proxy;
private final VehicleHandler handler;
private final String serviceExecutionAPI;
private final String serviceExecutionStateAPI;
private int counter = 0;
private Optional<ScheduledFuture<?>> stateJob = Optional.empty();
private Optional<String> serviceExecuting = Optional.empty();
private Optional<String> executingEventId = Optional.empty();
public enum ExecutionState {
READY,
INITIATED,
PENDING,
DELIVERED,
EXECUTED,
ERROR,
TIMEOUT
}
public enum RemoteService {
LIGHT_FLASH("Flash Lights", REMOTE_SERVICE_LIGHT_FLASH, REMOTE_SERVICE_LIGHT_FLASH),
VEHICLE_FINDER("Vehicle Finder", REMOTE_SERVICE_VEHICLE_FINDER, REMOTE_SERVICE_VEHICLE_FINDER),
DOOR_LOCK("Door Lock", REMOTE_SERVICE_DOOR_LOCK, REMOTE_SERVICE_DOOR_LOCK),
DOOR_UNLOCK("Door Unlock", REMOTE_SERVICE_DOOR_UNLOCK, REMOTE_SERVICE_DOOR_UNLOCK),
HORN_BLOW("Horn Blow", REMOTE_SERVICE_HORN, REMOTE_SERVICE_HORN),
CLIMATE_NOW_START("Start Climate", REMOTE_SERVICE_AIR_CONDITIONING_START, "climate-now?action=START"),
CLIMATE_NOW_STOP("Stop Climate", REMOTE_SERVICE_AIR_CONDITIONING_STOP, "climate-now?action=STOP");
private final String label;
private final String id;
private final String command;
RemoteService(final String label, final String id, String command) {
this.label = label;
this.id = id;
this.command = command;
}
public String getLabel() {
return label;
}
public String getId() {
return id;
}
public String getCommand() {
return command;
}
}
public RemoteServiceHandler(VehicleHandler vehicleHandler, MyBMWProxy myBmwProxy) {
handler = vehicleHandler;
proxy = myBmwProxy;
final VehicleConfiguration config = handler.getConfiguration().get();
serviceExecutionAPI = proxy.remoteCommandUrl + config.vin + "/";
serviceExecutionStateAPI = proxy.remoteStatusUrl;
}
boolean execute(RemoteService service, String... data) {
synchronized (this) {
if (serviceExecuting.isPresent()) {
logger.debug("Execution rejected - {} still pending", serviceExecuting.get());
// only one service executing
return false;
}
serviceExecuting = Optional.of(service.getId());
}
final MultiMap<String> dataMap = new MultiMap<String>();
if (data.length > 0) {
dataMap.add(DATA, data[0]);
proxy.post(serviceExecutionAPI + service.getCommand(), CONTENT_TYPE_JSON_ENCODED, data[0],
handler.getConfiguration().get().vehicleBrand, this);
} else {
proxy.post(serviceExecutionAPI + service.getCommand(), null, null,
handler.getConfiguration().get().vehicleBrand, this);
}
return true;
}
public void getState() {
synchronized (this) {
serviceExecuting.ifPresentOrElse(service -> {
if (counter >= GIVEUP_COUNTER) {
logger.warn("Giving up updating state for {} after {} times", service, GIVEUP_COUNTER);
handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null),
ExecutionState.TIMEOUT.name().toLowerCase());
reset();
// immediately refresh data
handler.getData();
} else {
counter++;
final MultiMap<String> dataMap = new MultiMap<String>();
dataMap.add(EVENT_ID, executingEventId.get());
final String encoded = UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false);
proxy.post(serviceExecutionStateAPI + Constants.QUESTION + encoded, null, null,
handler.getConfiguration().get().vehicleBrand, this);
}
}, () -> {
logger.warn("No Service executed to get state");
});
stateJob = Optional.empty();
}
}
@Override
public void onResponse(@Nullable String result) {
if (result != null) {
try {
ExecutionStatusContainer esc = Converter.getGson().fromJson(result, ExecutionStatusContainer.class);
if (esc != null) {
if (esc.eventId != null) {
// service initiated - store event id for further MyBMW updates
executingEventId = Optional.of(esc.eventId);
handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null),
ExecutionState.INITIATED.name().toLowerCase());
} else if (esc.eventStatus != null) {
// service status updated
synchronized (this) {
handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null),
esc.eventStatus.toLowerCase());
if (ExecutionState.EXECUTED.name().equalsIgnoreCase(esc.eventStatus)
|| ExecutionState.ERROR.name().equalsIgnoreCase(esc.eventStatus)) {
// refresh loop ends - update of status handled in the normal refreshInterval.
// Earlier update doesn't show better results!
reset();
return;
}
}
}
}
} catch (JsonSyntaxException jse) {
logger.debug("RemoteService response is unparseable: {} {}", result, jse.getMessage());
}
}
// schedule even if no result is present until retries exceeded
synchronized (this) {
stateJob.ifPresent(job -> {
if (!job.isDone()) {
job.cancel(true);
}
});
stateJob = Optional.of(handler.getScheduler().schedule(this::getState, STATE_UPDATE_SEC, TimeUnit.SECONDS));
}
}
@Override
public void onError(NetworkError error) {
synchronized (this) {
handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null),
ExecutionState.ERROR.name().toLowerCase() + Constants.SPACE + Integer.toString(error.status));
reset();
}
}
private void reset() {
serviceExecuting = Optional.empty();
executingEventId = Optional.empty();
counter = 0;
}
public void cancel() {
synchronized (this) {
stateJob.ifPresent(action -> {
if (!action.isDone()) {
action.cancel(true);
}
stateJob = Optional.empty();
});
}
}
}

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mybmw.internal.dto.network.NetworkError;
/**
* The {@link ResponseCallback} Marker Interface for all ASYNC REST API callbacks
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public interface ResponseCallback {
public void onError(NetworkError error);
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link StringResponseCallback} Interface for all String results from ASYNC REST API
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public interface StringResponseCallback extends ResponseCallback {
public void onResponse(@Nullable String result);
}

View File

@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mybmw.internal.utils.Constants;
/**
* The {@link Token} MyBMW Token storage
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class Token {
private String token = Constants.EMPTY;
private String tokenType = Constants.EMPTY;
private long expiration = 0;
public String getBearerToken() {
return new StringBuilder(tokenType).append(Constants.SPACE).append(token).toString();
}
public void setToken(String token) {
this.token = token;
}
public void setExpiration(int expiration) {
this.expiration = System.currentTimeMillis() / 1000 + expiration;
}
public void setExpirationTotal(long expiration) {
this.expiration = expiration;
}
public void setType(String type) {
tokenType = type;
}
public boolean isValid() {
return (!token.equals(Constants.EMPTY) && !tokenType.equals(Constants.EMPTY)
&& (this.expiration - System.currentTimeMillis() / 1000) > 1);
}
@Override
public String toString() {
return tokenType + Constants.COLON + token + Constants.COLON + isValid();
}
}

View File

@ -0,0 +1,456 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.handler;
import static org.openhab.binding.mybmw.internal.MyBMWConstants.*;
import java.time.DayOfWeek;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import javax.measure.Unit;
import javax.measure.quantity.Length;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mybmw.internal.MyBMWConstants.VehicleType;
import org.openhab.binding.mybmw.internal.dto.charge.ChargeProfile;
import org.openhab.binding.mybmw.internal.dto.charge.ChargeSession;
import org.openhab.binding.mybmw.internal.dto.charge.ChargeStatisticsContainer;
import org.openhab.binding.mybmw.internal.dto.charge.ChargingSettings;
import org.openhab.binding.mybmw.internal.dto.properties.CBS;
import org.openhab.binding.mybmw.internal.dto.properties.DoorsWindows;
import org.openhab.binding.mybmw.internal.dto.properties.Location;
import org.openhab.binding.mybmw.internal.dto.properties.Tires;
import org.openhab.binding.mybmw.internal.dto.status.CCMMessage;
import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
import org.openhab.binding.mybmw.internal.utils.ChargeProfileUtils;
import org.openhab.binding.mybmw.internal.utils.ChargeProfileUtils.TimedChannel;
import org.openhab.binding.mybmw.internal.utils.ChargeProfileWrapper;
import org.openhab.binding.mybmw.internal.utils.ChargeProfileWrapper.ProfileKey;
import org.openhab.binding.mybmw.internal.utils.Constants;
import org.openhab.binding.mybmw.internal.utils.Converter;
import org.openhab.binding.mybmw.internal.utils.RemoteServiceUtils;
import org.openhab.binding.mybmw.internal.utils.VehicleStatusUtils;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.CommandOption;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link VehicleChannelHandler} handles Channel updates
*
* @author Bernd Weymann - Initial contribution
* @author Norbert Truchsess - edit & send of charge profile
*/
@NonNullByDefault
public abstract class VehicleChannelHandler extends BaseThingHandler {
protected final Logger logger = LoggerFactory.getLogger(VehicleChannelHandler.class);
protected boolean hasFuel = false;
protected boolean isElectric = false;
protected boolean isHybrid = false;
// List Interfaces
protected List<CBS> serviceList = new ArrayList<CBS>();
protected String selectedService = Constants.UNDEF;
protected List<CCMMessage> checkControlList = new ArrayList<CCMMessage>();
protected String selectedCC = Constants.UNDEF;
protected List<ChargeSession> sessionList = new ArrayList<ChargeSession>();
protected String selectedSession = Constants.UNDEF;
protected MyBMWCommandOptionProvider commandOptionProvider;
// Data Caches
protected Optional<String> vehicleStatusCache = Optional.empty();
protected Optional<byte[]> imageCache = Optional.empty();
public VehicleChannelHandler(Thing thing, MyBMWCommandOptionProvider cop, String type) {
super(thing);
commandOptionProvider = cop;
hasFuel = type.equals(VehicleType.CONVENTIONAL.toString()) || type.equals(VehicleType.PLUGIN_HYBRID.toString())
|| type.equals(VehicleType.ELECTRIC_REX.toString());
isElectric = type.equals(VehicleType.PLUGIN_HYBRID.toString())
|| type.equals(VehicleType.ELECTRIC_REX.toString()) || type.equals(VehicleType.ELECTRIC.toString());
isHybrid = hasFuel && isElectric;
setOptions(CHANNEL_GROUP_REMOTE, REMOTE_SERVICE_COMMAND, RemoteServiceUtils.getOptions(isElectric));
}
private void setOptions(final String group, final String id, List<CommandOption> options) {
commandOptionProvider.setCommandOptions(new ChannelUID(thing.getUID(), group, id), options);
}
protected void updateChannel(final String group, final String id, final State state) {
updateState(new ChannelUID(thing.getUID(), group, id), state);
}
protected void updateChargeStatistics(ChargeStatisticsContainer csc) {
updateChannel(CHANNEL_GROUP_CHARGE_STATISTICS, TITLE, StringType.valueOf(csc.description));
updateChannel(CHANNEL_GROUP_CHARGE_STATISTICS, ENERGY,
QuantityType.valueOf(csc.statistics.totalEnergyCharged, Units.KILOWATT_HOUR));
updateChannel(CHANNEL_GROUP_CHARGE_STATISTICS, SESSIONS,
DecimalType.valueOf(Integer.toString(csc.statistics.numberOfChargingSessions)));
}
protected void updateVehicle(Vehicle v) {
updateVehicleStatus(v);
updateRange(v);
updateDoors(v.properties.doorsAndWindows);
updateWindows(v.properties.doorsAndWindows);
updatePosition(v.properties.vehicleLocation);
updateServices(v.properties.serviceRequired);
updateCheckControls(v.status.checkControlMessages);
updateTires(v.properties.tires);
}
private void updateTires(@Nullable Tires tires) {
if (tires == null) {
updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_CURRENT, UnDefType.UNDEF);
updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_TARGET, UnDefType.UNDEF);
updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_CURRENT, UnDefType.UNDEF);
updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_TARGET, UnDefType.UNDEF);
updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_CURRENT, UnDefType.UNDEF);
updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_TARGET, UnDefType.UNDEF);
updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_CURRENT, UnDefType.UNDEF);
updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_TARGET, UnDefType.UNDEF);
} else {
updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_CURRENT,
QuantityType.valueOf(tires.frontLeft.status.currentPressure / 100, Units.BAR));
updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_TARGET,
QuantityType.valueOf(tires.frontLeft.status.targetPressure / 100, Units.BAR));
updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_CURRENT,
QuantityType.valueOf(tires.frontRight.status.currentPressure / 100, Units.BAR));
updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_TARGET,
QuantityType.valueOf(tires.frontRight.status.targetPressure / 100, Units.BAR));
updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_CURRENT,
QuantityType.valueOf(tires.rearLeft.status.currentPressure / 100, Units.BAR));
updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_TARGET,
QuantityType.valueOf(tires.rearLeft.status.targetPressure / 100, Units.BAR));
updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_CURRENT,
QuantityType.valueOf(tires.rearRight.status.currentPressure / 100, Units.BAR));
updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_TARGET,
QuantityType.valueOf(tires.rearRight.status.targetPressure / 100, Units.BAR));
}
}
protected void updateVehicleStatus(Vehicle v) {
updateChannel(CHANNEL_GROUP_STATUS, LOCK, Converter.getLockState(v.properties.areDoorsLocked));
updateChannel(CHANNEL_GROUP_STATUS, SERVICE_DATE,
VehicleStatusUtils.getNextServiceDate(v.properties.serviceRequired));
updateChannel(CHANNEL_GROUP_STATUS, SERVICE_MILEAGE,
VehicleStatusUtils.getNextServiceMileage(v.properties.serviceRequired));
updateChannel(CHANNEL_GROUP_STATUS, CHECK_CONTROL,
StringType.valueOf(v.status.checkControlMessagesGeneralState));
updateChannel(CHANNEL_GROUP_STATUS, MOTION, OnOffType.from(v.properties.inMotion));
updateChannel(CHANNEL_GROUP_STATUS, LAST_UPDATE,
DateTimeType.valueOf(Converter.zonedToLocalDateTime(v.properties.lastUpdatedAt)));
updateChannel(CHANNEL_GROUP_STATUS, DOORS, Converter.getClosedState(v.properties.areDoorsClosed));
updateChannel(CHANNEL_GROUP_STATUS, WINDOWS, Converter.getClosedState(v.properties.areWindowsClosed));
if (isElectric) {
updateChannel(CHANNEL_GROUP_STATUS, PLUG_CONNECTION,
Converter.getConnectionState(v.properties.chargingState.isChargerConnected));
updateChannel(CHANNEL_GROUP_STATUS, CHARGE_STATUS,
StringType.valueOf(Converter.toTitleCase(VehicleStatusUtils.getChargStatus(v))));
updateChannel(CHANNEL_GROUP_STATUS, CHARGE_INFO,
StringType.valueOf(Converter.getLocalTime(VehicleStatusUtils.getChargeInfo(v))));
}
}
protected void updateRange(Vehicle v) {
// get the right unit
Unit<Length> lengthUnit = VehicleStatusUtils.getLengthUnit(v.status.fuelIndicators);
if (lengthUnit == null) {
return;
}
if (isElectric) {
int rangeElectric = VehicleStatusUtils.getRange(Constants.UNIT_PRECENT_JSON, v);
QuantityType<Length> qtElectricRange = QuantityType.valueOf(rangeElectric, lengthUnit);
QuantityType<Length> qtElectricRadius = QuantityType.valueOf(Converter.guessRangeRadius(rangeElectric),
lengthUnit);
updateChannel(CHANNEL_GROUP_RANGE, RANGE_ELECTRIC, qtElectricRange);
updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_ELECTRIC, qtElectricRadius);
}
if (hasFuel) {
int rangeFuel = VehicleStatusUtils.getRange(Constants.UNIT_LITER_JSON, v);
QuantityType<Length> qtFuelRange = QuantityType.valueOf(rangeFuel, lengthUnit);
QuantityType<Length> qtFuelRadius = QuantityType.valueOf(Converter.guessRangeRadius(rangeFuel), lengthUnit);
updateChannel(CHANNEL_GROUP_RANGE, RANGE_FUEL, qtFuelRange);
updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_FUEL, qtFuelRadius);
}
if (isHybrid) {
int rangeCombined = VehicleStatusUtils.getRange(Constants.PHEV, v);
QuantityType<Length> qtHybridRange = QuantityType.valueOf(rangeCombined, lengthUnit);
QuantityType<Length> qtHybridRadius = QuantityType.valueOf(Converter.guessRangeRadius(rangeCombined),
lengthUnit);
updateChannel(CHANNEL_GROUP_RANGE, RANGE_HYBRID, qtHybridRange);
updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_HYBRID, qtHybridRadius);
}
if (v.status.currentMileage.mileage == Constants.INT_UNDEF) {
updateChannel(CHANNEL_GROUP_RANGE, MILEAGE, UnDefType.UNDEF);
} else {
updateChannel(CHANNEL_GROUP_RANGE, MILEAGE,
QuantityType.valueOf(v.status.currentMileage.mileage, lengthUnit));
}
if (isElectric) {
updateChannel(CHANNEL_GROUP_RANGE, SOC,
QuantityType.valueOf(v.properties.chargingState.chargePercentage, Units.PERCENT));
}
if (hasFuel) {
updateChannel(CHANNEL_GROUP_RANGE, REMAINING_FUEL,
QuantityType.valueOf(v.properties.fuelLevel.value, Units.LITRE));
}
}
protected void updateCheckControls(List<CCMMessage> ccl) {
if (ccl.isEmpty()) {
// No Check Control available - show not active
CCMMessage ccm = new CCMMessage();
ccm.title = Constants.NO_ENTRIES;
ccm.longDescription = Constants.NO_ENTRIES;
ccm.state = Constants.NO_ENTRIES;
ccl.add(ccm);
}
// add all elements to options
checkControlList = ccl;
List<CommandOption> ccmDescriptionOptions = new ArrayList<>();
boolean isSelectedElementIn = false;
int index = 0;
for (CCMMessage ccEntry : checkControlList) {
ccmDescriptionOptions.add(new CommandOption(Integer.toString(index), ccEntry.title));
if (selectedCC.equals(ccEntry.title)) {
isSelectedElementIn = true;
}
index++;
}
setOptions(CHANNEL_GROUP_CHECK_CONTROL, NAME, ccmDescriptionOptions);
// if current selected item isn't anymore in the list select first entry
if (!isSelectedElementIn) {
selectCheckControl(0);
}
}
protected void selectCheckControl(int index) {
if (index >= 0 && index < checkControlList.size()) {
CCMMessage ccEntry = checkControlList.get(index);
selectedCC = ccEntry.title;
updateChannel(CHANNEL_GROUP_CHECK_CONTROL, NAME, StringType.valueOf(ccEntry.title));
updateChannel(CHANNEL_GROUP_CHECK_CONTROL, DETAILS, StringType.valueOf(ccEntry.longDescription));
updateChannel(CHANNEL_GROUP_CHECK_CONTROL, SEVERITY, StringType.valueOf(ccEntry.state));
}
}
protected void updateServices(List<CBS> sl) {
// if list is empty add "undefined" element
if (sl.isEmpty()) {
CBS cbsm = new CBS();
cbsm.type = Constants.NO_ENTRIES;
sl.add(cbsm);
}
// add all elements to options
serviceList = sl;
List<CommandOption> serviceNameOptions = new ArrayList<>();
boolean isSelectedElementIn = false;
int index = 0;
for (CBS serviceEntry : serviceList) {
// create StateOption with "value = list index" and "label = human readable string"
serviceNameOptions.add(new CommandOption(Integer.toString(index), serviceEntry.type));
if (selectedService.equals(serviceEntry.type)) {
isSelectedElementIn = true;
}
index++;
}
setOptions(CHANNEL_GROUP_SERVICE, NAME, serviceNameOptions);
// if current selected item isn't anymore in the list select first entry
if (!isSelectedElementIn) {
selectService(0);
}
}
protected void selectService(int index) {
if (index >= 0 && index < serviceList.size()) {
CBS serviceEntry = serviceList.get(index);
selectedService = serviceEntry.type;
updateChannel(CHANNEL_GROUP_SERVICE, NAME, StringType.valueOf(Converter.toTitleCase(serviceEntry.type)));
if (serviceEntry.dateTime != null) {
updateChannel(CHANNEL_GROUP_SERVICE, DATE,
DateTimeType.valueOf(Converter.zonedToLocalDateTime(serviceEntry.dateTime)));
} else {
updateChannel(CHANNEL_GROUP_SERVICE, DATE, UnDefType.UNDEF);
}
if (serviceEntry.distance != null) {
if (Constants.KILOMETERS_JSON.equals(serviceEntry.distance.units)) {
updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE,
QuantityType.valueOf(serviceEntry.distance.value, Constants.KILOMETRE_UNIT));
} else {
updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE,
QuantityType.valueOf(serviceEntry.distance.value, ImperialUnits.MILE));
}
} else {
updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE,
QuantityType.valueOf(Constants.INT_UNDEF, Constants.KILOMETRE_UNIT));
}
}
}
protected void updateSessions(List<ChargeSession> sl) {
// if list is empty add "undefined" element
if (sl.isEmpty()) {
ChargeSession cs = new ChargeSession();
cs.title = Constants.NO_ENTRIES;
sl.add(cs);
}
// add all elements to options
sessionList = sl;
List<CommandOption> sessionNameOptions = new ArrayList<>();
boolean isSelectedElementIn = false;
int index = 0;
for (ChargeSession session : sessionList) {
// create StateOption with "value = list index" and "label = human readable string"
sessionNameOptions.add(new CommandOption(Integer.toString(index), session.title));
if (selectedService.equals(session.title)) {
isSelectedElementIn = true;
}
index++;
}
setOptions(CHANNEL_GROUP_CHARGE_SESSION, TITLE, sessionNameOptions);
// if current selected item isn't anymore in the list select first entry
if (!isSelectedElementIn) {
selectSession(0);
}
}
protected void selectSession(int index) {
if (index >= 0 && index < sessionList.size()) {
ChargeSession sessionEntry = sessionList.get(index);
selectedService = sessionEntry.title;
updateChannel(CHANNEL_GROUP_CHARGE_SESSION, TITLE, StringType.valueOf(sessionEntry.title));
updateChannel(CHANNEL_GROUP_CHARGE_SESSION, SUBTITLE, StringType.valueOf(sessionEntry.subtitle));
if (sessionEntry.energyCharged != null) {
updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ENERGY, StringType.valueOf(sessionEntry.energyCharged));
} else {
updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ENERGY, StringType.valueOf(Constants.UNDEF));
}
if (sessionEntry.issues != null) {
updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ISSUE, StringType.valueOf(sessionEntry.issues));
} else {
updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ISSUE, StringType.valueOf(Constants.HYPHEN));
}
updateChannel(CHANNEL_GROUP_CHARGE_SESSION, STATUS, StringType.valueOf(sessionEntry.sessionStatus));
}
}
protected void updateChargeProfile(ChargeProfile cp) {
ChargeProfileWrapper cpw = new ChargeProfileWrapper(cp);
updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_PREFERENCE, StringType.valueOf(cpw.getPreference()));
updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_MODE, StringType.valueOf(cpw.getMode()));
updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_CONTROL, StringType.valueOf(cpw.getControlType()));
ChargingSettings cs = cpw.getChargeSettings();
if (cs != null) {
updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_TARGET,
DecimalType.valueOf(Integer.toString(cs.targetSoc)));
updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_LIMIT,
OnOffType.from(cs.isAcCurrentLimitActive));
}
final Boolean climate = cpw.isEnabled(ProfileKey.CLIMATE);
updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_CLIMATE,
climate == null ? UnDefType.UNDEF : OnOffType.from(climate));
updateTimedState(cpw, ProfileKey.WINDOWSTART);
updateTimedState(cpw, ProfileKey.WINDOWEND);
updateTimedState(cpw, ProfileKey.TIMER1);
updateTimedState(cpw, ProfileKey.TIMER2);
updateTimedState(cpw, ProfileKey.TIMER3);
updateTimedState(cpw, ProfileKey.TIMER4);
}
protected void updateTimedState(ChargeProfileWrapper profile, ProfileKey key) {
final TimedChannel timed = ChargeProfileUtils.getTimedChannel(key);
if (timed != null) {
final LocalTime time = profile.getTime(key);
updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, timed.time,
time.equals(Constants.NULL_LOCAL_TIME) ? UnDefType.UNDEF
: new DateTimeType(ZonedDateTime.of(Constants.EPOCH_DAY, time, ZoneId.systemDefault())));
if (timed.timer != null) {
final Boolean enabled = profile.isEnabled(key);
updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, timed.timer + CHARGE_ENABLED,
enabled == null ? UnDefType.UNDEF : OnOffType.from(enabled));
if (timed.hasDays) {
final Set<DayOfWeek> days = profile.getDays(key);
EnumSet.allOf(DayOfWeek.class).forEach(day -> {
updateChannel(CHANNEL_GROUP_CHARGE_PROFILE,
timed.timer + ChargeProfileUtils.getDaysChannel(day),
days == null ? UnDefType.UNDEF : OnOffType.from(days.contains(day)));
});
}
}
}
}
protected void updateDoors(DoorsWindows dw) {
updateChannel(CHANNEL_GROUP_DOORS, DOOR_DRIVER_FRONT,
StringType.valueOf(Converter.toTitleCase(dw.doors.driverFront)));
updateChannel(CHANNEL_GROUP_DOORS, DOOR_DRIVER_REAR,
StringType.valueOf(Converter.toTitleCase(dw.doors.driverRear)));
updateChannel(CHANNEL_GROUP_DOORS, DOOR_PASSENGER_FRONT,
StringType.valueOf(Converter.toTitleCase(dw.doors.passengerFront)));
updateChannel(CHANNEL_GROUP_DOORS, DOOR_PASSENGER_REAR,
StringType.valueOf(Converter.toTitleCase(dw.doors.passengerRear)));
updateChannel(CHANNEL_GROUP_DOORS, TRUNK, StringType.valueOf(Converter.toTitleCase(dw.trunk)));
updateChannel(CHANNEL_GROUP_DOORS, HOOD, StringType.valueOf(Converter.toTitleCase(dw.hood)));
}
protected void updateWindows(DoorsWindows dw) {
updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_DRIVER_FRONT,
StringType.valueOf(Converter.toTitleCase(dw.windows.driverFront)));
updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_DRIVER_REAR,
StringType.valueOf(Converter.toTitleCase(dw.windows.driverRear)));
updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_PASSENGER_FRONT,
StringType.valueOf(Converter.toTitleCase(dw.windows.passengerFront)));
updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_PASSENGER_REAR,
StringType.valueOf(Converter.toTitleCase(dw.windows.passengerRear)));
updateChannel(CHANNEL_GROUP_DOORS, SUNROOF, StringType.valueOf(Converter.toTitleCase(dw.moonroof)));
}
protected void updatePosition(Location pos) {
updateChannel(CHANNEL_GROUP_LOCATION, GPS, PointType
.valueOf(Double.toString(pos.coordinates.latitude) + "," + Double.toString(pos.coordinates.longitude)));
updateChannel(CHANNEL_GROUP_LOCATION, HEADING, QuantityType.valueOf(pos.heading, Units.DEGREE_ANGLE));
updateChannel(CHANNEL_GROUP_LOCATION, ADDRESS, StringType.valueOf(pos.address.formatted));
}
}

View File

@ -0,0 +1,351 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.handler;
import static org.openhab.binding.mybmw.internal.MyBMWConstants.*;
import java.util.Optional;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mybmw.internal.VehicleConfiguration;
import org.openhab.binding.mybmw.internal.dto.charge.ChargeSessionsContainer;
import org.openhab.binding.mybmw.internal.dto.charge.ChargeStatisticsContainer;
import org.openhab.binding.mybmw.internal.dto.network.NetworkError;
import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
import org.openhab.binding.mybmw.internal.utils.Constants;
import org.openhab.binding.mybmw.internal.utils.Converter;
import org.openhab.binding.mybmw.internal.utils.ImageProperties;
import org.openhab.binding.mybmw.internal.utils.RemoteServiceUtils;
import org.openhab.core.io.net.http.HttpUtil;
import org.openhab.core.library.types.RawType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import com.google.gson.JsonSyntaxException;
/**
* The {@link VehicleHandler} handles responses from BMW API
*
* @author Bernd Weymann - Initial contribution
* @author Norbert Truchsess - edit & send charge profile
*/
@NonNullByDefault
public class VehicleHandler extends VehicleChannelHandler {
private Optional<MyBMWProxy> proxy = Optional.empty();
private Optional<RemoteServiceHandler> remote = Optional.empty();
public Optional<VehicleConfiguration> configuration = Optional.empty();
private Optional<ScheduledFuture<?>> refreshJob = Optional.empty();
private Optional<ScheduledFuture<?>> editTimeout = Optional.empty();
private ImageProperties imageProperties = new ImageProperties();
VehicleStatusCallback vehicleStatusCallback = new VehicleStatusCallback();
ChargeStatisticsCallback chargeStatisticsCallback = new ChargeStatisticsCallback();
ChargeSessionsCallback chargeSessionCallback = new ChargeSessionsCallback();
ByteResponseCallback imageCallback = new ImageCallback();
public VehicleHandler(Thing thing, MyBMWCommandOptionProvider cop, String driveTrain) {
super(thing, cop, driveTrain);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
String group = channelUID.getGroupId();
// Refresh of Channels with cached values
if (command instanceof RefreshType) {
if (CHANNEL_GROUP_STATUS.equals(group)) {
vehicleStatusCache.ifPresent(vehicleStatus -> vehicleStatusCallback.onResponse(vehicleStatus));
} else if (CHANNEL_GROUP_VEHICLE_IMAGE.equals(group)) {
imageCache.ifPresent(image -> imageCallback.onResponse(image));
}
// Check for Channel Group and corresponding Actions
} else if (CHANNEL_GROUP_REMOTE.equals(group)) {
// Executing Remote Services
if (command instanceof StringType) {
String serviceCommand = ((StringType) command).toFullString();
remote.ifPresent(remot -> {
switch (serviceCommand) {
case REMOTE_SERVICE_LIGHT_FLASH:
case REMOTE_SERVICE_DOOR_LOCK:
case REMOTE_SERVICE_DOOR_UNLOCK:
case REMOTE_SERVICE_HORN:
case REMOTE_SERVICE_VEHICLE_FINDER:
RemoteServiceUtils.getRemoteService(serviceCommand)
.ifPresentOrElse(service -> remot.execute(service), () -> {
logger.debug("Remote service execution {} unknown", serviceCommand);
});
break;
case REMOTE_SERVICE_AIR_CONDITIONING_START:
RemoteServiceUtils.getRemoteService(serviceCommand)
.ifPresentOrElse(service -> remot.execute(service), () -> {
logger.debug("Remote service execution {} unknown", serviceCommand);
});
break;
case REMOTE_SERVICE_AIR_CONDITIONING_STOP:
RemoteServiceUtils.getRemoteService(serviceCommand)
.ifPresentOrElse(service -> remot.execute(service), () -> {
logger.debug("Remote service execution {} unknown", serviceCommand);
});
break;
default:
logger.debug("Remote service execution {} unknown", serviceCommand);
break;
}
});
}
} else if (CHANNEL_GROUP_VEHICLE_IMAGE.equals(group)) {
// Image Change
configuration.ifPresent(config -> {
if (command instanceof StringType) {
if (channelUID.getIdWithoutGroup().equals(IMAGE_VIEWPORT)) {
String newViewport = command.toString();
synchronized (imageProperties) {
if (!imageProperties.viewport.equals(newViewport)) {
imageProperties = new ImageProperties(newViewport);
imageCache = Optional.empty();
proxy.ifPresent(prox -> prox.requestImage(config, imageProperties, imageCallback));
}
}
updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, StringType.valueOf(newViewport));
}
}
});
} else if (CHANNEL_GROUP_SERVICE.equals(group)) {
if (command instanceof StringType) {
int index = Converter.getIndex(command.toFullString());
if (index != -1) {
selectService(index);
} else {
logger.debug("Cannot select Service index {}", command.toFullString());
}
}
} else if (CHANNEL_GROUP_CHECK_CONTROL.equals(group)) {
if (command instanceof StringType) {
int index = Converter.getIndex(command.toFullString());
if (index != -1) {
selectCheckControl(index);
} else {
logger.debug("Cannot select CheckControl index {}", command.toFullString());
}
}
} else if (CHANNEL_GROUP_CHARGE_SESSION.equals(group)) {
if (command instanceof StringType) {
int index = Converter.getIndex(command.toFullString());
if (index != -1) {
selectSession(index);
} else {
logger.debug("Cannot select Session index {}", command.toFullString());
}
}
}
}
@Override
public void initialize() {
updateStatus(ThingStatus.UNKNOWN);
final VehicleConfiguration config = getConfigAs(VehicleConfiguration.class);
configuration = Optional.of(config);
Bridge bridge = getBridge();
if (bridge != null) {
BridgeHandler handler = bridge.getHandler();
if (handler != null) {
proxy = ((MyBMWBridgeHandler) handler).getProxy();
remote = proxy.map(prox -> prox.getRemoteServiceHandler(this));
} else {
logger.debug("Bridge Handler null");
}
} else {
logger.debug("Bridge null");
}
imageProperties = new ImageProperties();
updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, StringType.valueOf(imageProperties.viewport));
// start update schedule
startSchedule(config.refreshInterval);
}
private void startSchedule(int interval) {
refreshJob.ifPresentOrElse(job -> {
if (job.isCancelled()) {
refreshJob = Optional
.of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES));
} // else - scheduler is already running!
}, () -> {
refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES));
});
}
@Override
public void dispose() {
refreshJob.ifPresent(job -> job.cancel(true));
editTimeout.ifPresent(job -> job.cancel(true));
remote.ifPresent(RemoteServiceHandler::cancel);
}
public void getData() {
proxy.ifPresentOrElse(prox -> {
configuration.ifPresentOrElse(config -> {
prox.requestVehicles(config.vehicleBrand, vehicleStatusCallback);
if (isElectric) {
prox.requestChargeStatistics(config, chargeStatisticsCallback);
prox.requestChargeSessions(config, chargeSessionCallback);
}
if (!imageCache.isPresent() && !imageProperties.failLimitReached()) {
prox.requestImage(config, imageProperties, imageCallback);
}
}, () -> {
logger.warn("MyBMW Vehicle Configuration isn't present");
});
}, () -> {
logger.warn("MyBMWProxy isn't present");
});
}
public void updateRemoteExecutionStatus(@Nullable String service, String status) {
updateChannel(CHANNEL_GROUP_REMOTE, REMOTE_STATE,
StringType.valueOf((service == null ? "-" : service) + Constants.SPACE + status.toLowerCase()));
}
public Optional<VehicleConfiguration> getConfiguration() {
return configuration;
}
public ScheduledExecutorService getScheduler() {
return scheduler;
}
public class ImageCallback implements ByteResponseCallback {
@Override
public void onResponse(byte[] content) {
if (content.length > 0) {
imageCache = Optional.of(content);
String contentType = HttpUtil.guessContentTypeFromData(content);
updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_FORMAT, new RawType(content, contentType));
} else {
synchronized (imageProperties) {
imageProperties.failed();
}
}
}
/**
* Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
*/
@Override
public void onError(NetworkError error) {
logger.debug("{}", error.toString());
synchronized (imageProperties) {
imageProperties.failed();
}
}
}
/**
* The VehicleStatus is supported by all Vehicle Types so it's used to reflect the Thing Status
*/
public class VehicleStatusCallback implements StringResponseCallback {
@Override
public void onResponse(@Nullable String content) {
if (content != null) {
if (getConfiguration().isPresent()) {
Vehicle v = Converter.getVehicle(configuration.get().vin, content);
if (v.valid) {
vehicleStatusCache = Optional.of(content);
updateStatus(ThingStatus.ONLINE);
updateChannel(CHANNEL_GROUP_STATUS, RAW,
StringType.valueOf(Converter.getRawVehicleContent(configuration.get().vin, content)));
updateVehicle(v);
if (isElectric) {
updateChargeProfile(v.status.chargingProfile);
}
} else {
logger.debug("Vehicle {} not valid", configuration.get().vin);
}
} else {
logger.debug("configuration not present");
}
} else {
updateChannel(CHANNEL_GROUP_STATUS, RAW, StringType.valueOf(Constants.EMPTY_JSON));
logger.debug("Content not valid");
}
}
@Override
public void onError(NetworkError error) {
logger.debug("{}", error.toString());
vehicleStatusCache = Optional.of(Converter.getGson().toJson(error));
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error.reason);
}
}
public class ChargeStatisticsCallback implements StringResponseCallback {
@Override
public void onResponse(@Nullable String content) {
if (content != null) {
try {
ChargeStatisticsContainer csc = Converter.getGson().fromJson(content,
ChargeStatisticsContainer.class);
if (csc != null) {
updateChargeStatistics(csc);
}
} catch (JsonSyntaxException jse) {
logger.warn("{}", jse.getLocalizedMessage());
}
} else {
logger.debug("Content not valid");
}
}
@Override
public void onError(NetworkError error) {
logger.debug("{}", error.toString());
}
}
public class ChargeSessionsCallback implements StringResponseCallback {
@Override
public void onResponse(@Nullable String content) {
if (content != null) {
try {
ChargeSessionsContainer csc = Converter.getGson().fromJson(content, ChargeSessionsContainer.class);
if (csc != null) {
if (csc.chargingSessions != null) {
updateSessions(csc.chargingSessions.sessions);
}
}
} catch (JsonSyntaxException jse) {
logger.warn("{}", jse.getLocalizedMessage());
}
} else {
logger.debug("Content not valid");
}
}
@Override
public void onError(NetworkError error) {
logger.debug("{}", error.toString());
}
}
}

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.handler.simulation;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link Injector} Simulates feedback of the BMW API
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class Injector {
private static boolean active = false;
// copy discovery json here
private static String discovery = "";
// copy vehicle status json here
private static String status = "";
public static boolean isActive() {
return active;
}
public static String getDiscovery() {
return discovery;
}
public static String getStatus() {
return status;
}
}

View File

@ -0,0 +1,75 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.utils;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link BimmerConstants} This class holds the important constants for the BMW Connected Drive Authorization. They
* are taken from the Bimmercode from github {@link https://github.com/bimmerconnected/bimmer_connected}
* File defining these constants
* {@link https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/account.py}
* https://customer.bmwgroup.com/one/app/oauth.js
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class BimmerConstants {
public static final String REGION_NORTH_AMERICA = "NORTH_AMERICA";
public static final String REGION_CHINA = "CHINA";
public static final String REGION_ROW = "ROW";
public static final String BRAND_BMW = "bmw";
public static final String BRAND_MINI = "mini";
public static final List<String> ALL_BRANDS = List.of(BRAND_BMW, BRAND_MINI);
public static final String OAUTH_ENDPOINT = "/gcdm/oauth/authenticate";
public static final String EADRAX_SERVER_NORTH_AMERICA = "cocoapi.bmwgroup.us";
public static final String EADRAX_SERVER_ROW = "cocoapi.bmwgroup.com";
public static final String EADRAX_SERVER_CHINA = "myprofile.bmw.com.cn";
public static final Map<String, String> EADRAX_SERVER_MAP = Map.of(REGION_NORTH_AMERICA,
EADRAX_SERVER_NORTH_AMERICA, REGION_CHINA, EADRAX_SERVER_CHINA, REGION_ROW, EADRAX_SERVER_ROW);
public static final String OCP_APIM_KEY_NORTH_AMERICA = "31e102f5-6f7e-7ef3-9044-ddce63891362";
public static final String OCP_APIM_KEY_ROW = "4f1c85a3-758f-a37d-bbb6-f8704494acfa";
public static final Map<String, String> OCP_APIM_KEYS = Map.of(REGION_NORTH_AMERICA, OCP_APIM_KEY_NORTH_AMERICA,
REGION_ROW, OCP_APIM_KEY_ROW);
public static final String CHINA_PUBLIC_KEY = "/eadrax-coas/v1/cop/publickey";
public static final String CHINA_LOGIN = "/eadrax-coas/v1/login/pwd";
// Http variables
public static final String USER_AGENT_BMW = "android(v1.07_20200330);bmw;1.7.0(11152)";
public static final String USER_AGENT_MINI = "android(v1.07_20200330);mini;1.7.0(11152)";
public static final Map<String, String> BRAND_USER_AGENTS_MAP = Map.of(BRAND_BMW, USER_AGENT_BMW, BRAND_MINI,
USER_AGENT_MINI);
public static final String LOGIN_NONCE = "login_nonce";
public static final String AUTHORIZATION_CODE = "authorization_code";
// Parameters for API Requests
public static final String TIRE_GUARD_MODE = "tireGuardMode";
public static final String APP_DATE_TIME = "appDateTime";
public static final String APP_TIMEZONE = "apptimezone";
// API endpoints
public static final String API_OAUTH_CONFIG = "/eadrax-ucs/v1/presentation/oauth/config";
public static final String API_VEHICLES = "/eadrax-vcs/v1/vehicles";
public static final String API_REMOTE_SERVICE_BASE_URL = "/eadrax-vrccs/v2/presentation/remote-commands/"; // '/{vin}/{service_type}'
public static final String API_POI = "/eadrax-dcs/v1/send-to-car/send-to-car";
}

View File

@ -0,0 +1,137 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.utils;
import static org.openhab.binding.mybmw.internal.MyBMWConstants.*;
import java.time.DayOfWeek;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mybmw.internal.utils.ChargeProfileWrapper.ProfileKey;
/**
* The {@link ChargeProfileUtils} utility functions for charging profiles
*
* @author Norbert Truchsess - initial contribution
*/
@NonNullByDefault
public class ChargeProfileUtils {
// Charging
public static class TimedChannel {
public final String time;
public final @Nullable String timer;
public final boolean hasDays;
TimedChannel(final String time, @Nullable final String timer, final boolean hasDays) {
this.time = time;
this.timer = timer;
this.hasDays = hasDays;
}
}
@SuppressWarnings("serial")
private static final Map<ProfileKey, TimedChannel> TIMED_CHANNELS = new HashMap<>() {
{
put(ProfileKey.WINDOWSTART, new TimedChannel(CHARGE_WINDOW_START, null, false));
put(ProfileKey.WINDOWEND, new TimedChannel(CHARGE_WINDOW_END, null, false));
put(ProfileKey.TIMER1, new TimedChannel(CHARGE_TIMER1 + CHARGE_DEPARTURE, CHARGE_TIMER1, true));
put(ProfileKey.TIMER2, new TimedChannel(CHARGE_TIMER2 + CHARGE_DEPARTURE, CHARGE_TIMER2, true));
put(ProfileKey.TIMER3, new TimedChannel(CHARGE_TIMER3 + CHARGE_DEPARTURE, CHARGE_TIMER3, true));
put(ProfileKey.TIMER4, new TimedChannel(CHARGE_TIMER4 + CHARGE_DEPARTURE, CHARGE_TIMER4, true));
}
};
@SuppressWarnings("serial")
private static final Map<DayOfWeek, String> DAY_CHANNELS = new HashMap<>() {
{
put(DayOfWeek.MONDAY, CHARGE_DAY_MON);
put(DayOfWeek.TUESDAY, CHARGE_DAY_TUE);
put(DayOfWeek.WEDNESDAY, CHARGE_DAY_WED);
put(DayOfWeek.THURSDAY, CHARGE_DAY_THU);
put(DayOfWeek.FRIDAY, CHARGE_DAY_FRI);
put(DayOfWeek.SATURDAY, CHARGE_DAY_SAT);
put(DayOfWeek.SUNDAY, CHARGE_DAY_SUN);
}
};
public static class ChargeKeyDay {
public final ProfileKey key;
public final DayOfWeek day;
ChargeKeyDay(final ProfileKey key, final DayOfWeek day) {
this.key = key;
this.day = day;
}
}
@SuppressWarnings("serial")
private static final Map<String, ProfileKey> CHARGE_ENABLED_CHANNEL_KEYS = new HashMap<>() {
{
TIMED_CHANNELS.forEach((key, channel) -> {
put(channel.timer + CHARGE_ENABLED, key);
});
put(CHARGE_PROFILE_CLIMATE, ProfileKey.CLIMATE);
}
};
@SuppressWarnings("serial")
private static final Map<String, ProfileKey> CHARGE_TIME_CHANNEL_KEYS = new HashMap<>() {
{
TIMED_CHANNELS.forEach((key, channel) -> {
put(channel.time, key);
});
}
};
@SuppressWarnings("serial")
private static final Map<String, ChargeKeyDay> CHARGE_DAYS_CHANNEL_KEYS = new HashMap<>() {
{
DAY_CHANNELS.forEach((dayOfWeek, dayChannel) -> {
put(CHARGE_TIMER1 + dayChannel, new ChargeKeyDay(ProfileKey.TIMER1, dayOfWeek));
put(CHARGE_TIMER2 + dayChannel, new ChargeKeyDay(ProfileKey.TIMER2, dayOfWeek));
put(CHARGE_TIMER3 + dayChannel, new ChargeKeyDay(ProfileKey.TIMER3, dayOfWeek));
put(CHARGE_TIMER4 + dayChannel, new ChargeKeyDay(ProfileKey.TIMER3, dayOfWeek));
});
}
};
public static @Nullable TimedChannel getTimedChannel(ProfileKey key) {
return TIMED_CHANNELS.get(key);
}
public static @Nullable String getDaysChannel(DayOfWeek day) {
return DAY_CHANNELS.get(day);
}
public static @Nullable ProfileKey getEnableKey(final String id) {
return CHARGE_ENABLED_CHANNEL_KEYS.get(id);
}
public static @Nullable ChargeKeyDay getKeyDay(final String id) {
return CHARGE_DAYS_CHANNEL_KEYS.get(id);
}
public static @Nullable ProfileKey getTimeKey(final String id) {
return CHARGE_TIME_CHANNEL_KEYS.get(id);
}
public static String formatDays(final Set<DayOfWeek> weekdays) {
return weekdays.stream().map(day -> Constants.DAYS.get(day)).collect(Collectors.joining(Constants.COMMA));
}
}

View File

@ -0,0 +1,303 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.utils;
import static org.openhab.binding.mybmw.internal.utils.ChargeProfileWrapper.ProfileKey.*;
import static org.openhab.binding.mybmw.internal.utils.Constants.*;
import java.time.DayOfWeek;
import java.time.LocalTime;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mybmw.internal.MyBMWConstants.ChargingMode;
import org.openhab.binding.mybmw.internal.MyBMWConstants.ChargingPreference;
import org.openhab.binding.mybmw.internal.dto.charge.ChargeProfile;
import org.openhab.binding.mybmw.internal.dto.charge.ChargingSettings;
import org.openhab.binding.mybmw.internal.dto.charge.ChargingWindow;
import org.openhab.binding.mybmw.internal.dto.charge.Time;
import org.openhab.binding.mybmw.internal.dto.charge.Timer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ChargeProfileWrapper} Wrapper for ChargeProfiles
*
* @author Bernd Weymann - Initial contribution
* @author Norbert Truchsess - add ChargeProfileActions
*/
@NonNullByDefault
public class ChargeProfileWrapper {
private static final Logger LOGGER = LoggerFactory.getLogger(ChargeProfileWrapper.class);
private static final String CHARGING_WINDOW = "chargingWindow";
private static final String WEEKLY_PLANNER = "weeklyPlanner";
private static final String ACTIVATE = "activate";
private static final String DEACTIVATE = "deactivate";
public enum ProfileKey {
CLIMATE,
TIMER1,
TIMER2,
TIMER3,
TIMER4,
WINDOWSTART,
WINDOWEND
}
private Optional<ChargingMode> mode = Optional.empty();
private Optional<ChargingPreference> preference = Optional.empty();
private Optional<String> controlType = Optional.empty();
private Optional<ChargingSettings> chargeSettings = Optional.empty();
private final Map<ProfileKey, Boolean> enabled = new HashMap<>();
private final Map<ProfileKey, LocalTime> times = new HashMap<>();
private final Map<ProfileKey, Set<DayOfWeek>> daysOfWeek = new HashMap<>();
public ChargeProfileWrapper(final ChargeProfile profile) {
setPreference(profile.chargingPreference);
setMode(profile.chargingMode);
controlType = Optional.of(profile.chargingControlType);
chargeSettings = Optional.of(profile.chargingSettings);
setEnabled(CLIMATE, profile.climatisationOn);
addTimer(TIMER1, profile.getTimerId(1));
addTimer(TIMER2, profile.getTimerId(2));
if (profile.chargingControlType.equals(WEEKLY_PLANNER)) {
addTimer(TIMER3, profile.getTimerId(3));
addTimer(TIMER4, profile.getTimerId(4));
}
if (CHARGING_WINDOW.equals(profile.chargingPreference)) {
addTime(WINDOWSTART, profile.reductionOfChargeCurrent.start);
addTime(WINDOWEND, profile.reductionOfChargeCurrent.end);
} else {
preference.ifPresent(pref -> {
if (ChargingPreference.chargingWindow.equals(pref)) {
addTime(WINDOWSTART, null);
addTime(WINDOWEND, null);
}
});
}
}
public @Nullable Boolean isEnabled(final ProfileKey key) {
return enabled.get(key);
}
public void setEnabled(final ProfileKey key, @Nullable final Boolean enabled) {
if (enabled == null) {
this.enabled.remove(key);
} else {
this.enabled.put(key, enabled);
}
}
public @Nullable String getMode() {
return mode.map(m -> m.name()).orElse(null);
}
public @Nullable String getControlType() {
return controlType.get();
}
public @Nullable ChargingSettings getChargeSettings() {
return chargeSettings.get();
}
public void setMode(final @Nullable String mode) {
if (mode != null) {
try {
this.mode = Optional.of(ChargingMode.valueOf(mode));
return;
} catch (IllegalArgumentException iae) {
LOGGER.warn("unexpected value for chargingMode: {}", mode);
}
}
this.mode = Optional.empty();
}
public @Nullable String getPreference() {
return preference.map(pref -> pref.name()).orElse(null);
}
public void setPreference(final @Nullable String preference) {
if (preference != null) {
try {
this.preference = Optional.of(ChargingPreference.valueOf(preference));
return;
} catch (IllegalArgumentException iae) {
LOGGER.warn("unexpected value for chargingPreference: {}", preference);
}
}
this.preference = Optional.empty();
}
public @Nullable Set<DayOfWeek> getDays(final ProfileKey key) {
return daysOfWeek.get(key);
}
public void setDays(final ProfileKey key, final @Nullable Set<DayOfWeek> days) {
if (days == null) {
daysOfWeek.remove(key);
} else {
daysOfWeek.put(key, days);
}
}
public void setDayEnabled(final ProfileKey key, final DayOfWeek day, final boolean enabled) {
final Set<DayOfWeek> days = daysOfWeek.get(key);
if (days == null) {
daysOfWeek.put(key, enabled ? EnumSet.of(day) : EnumSet.noneOf(DayOfWeek.class));
} else {
if (enabled) {
days.add(day);
} else {
days.remove(day);
}
}
}
public LocalTime getTime(final ProfileKey key) {
LocalTime t = times.get(key);
if (t != null) {
return t;
} else {
LOGGER.debug("Profile not valid - Key {} doesn't contain boolean value", key);
return Constants.NULL_LOCAL_TIME;
}
}
public void setTime(final ProfileKey key, @Nullable LocalTime time) {
if (time == null) {
times.remove(key);
} else {
times.put(key, time);
}
}
public String getJson() {
final ChargeProfile profile = new ChargeProfile();
preference.ifPresent(pref -> profile.chargingPreference = pref.name());
profile.chargingControlType = controlType.get();
Boolean enabledBool = isEnabled(CLIMATE);
profile.climatisationOn = enabledBool == null ? false : enabledBool;
preference.ifPresent(pref -> {
if (ChargingPreference.chargingWindow.equals(pref)) {
profile.chargingMode = getMode();
final LocalTime start = getTime(WINDOWSTART);
final LocalTime end = getTime(WINDOWEND);
if (!start.equals(Constants.NULL_LOCAL_TIME) && !end.equals(Constants.NULL_LOCAL_TIME)) {
ChargingWindow cw = new ChargingWindow();
profile.reductionOfChargeCurrent = cw;
cw.start = new Time();
cw.start.hour = start.getHour();
cw.start.minute = start.getMinute();
cw.end = new Time();
cw.end.hour = end.getHour();
cw.end.minute = end.getMinute();
}
}
});
profile.departureTimes = new ArrayList<Timer>();
profile.departureTimes.add(getTimer(TIMER1));
profile.departureTimes.add(getTimer(TIMER2));
if (profile.chargingControlType.equals(WEEKLY_PLANNER)) {
profile.departureTimes.add(getTimer(TIMER3));
profile.departureTimes.add(getTimer(TIMER4));
}
profile.chargingSettings = chargeSettings.get();
return Converter.getGson().toJson(profile);
}
private void addTime(final ProfileKey key, @Nullable final Time time) {
try {
times.put(key, time == null ? NULL_LOCAL_TIME : LocalTime.parse(Converter.getTime(time), TIME_FORMATER));
} catch (DateTimeParseException dtpe) {
LOGGER.warn("unexpected value for {} time: {}", key.name(), time);
}
}
private void addTimer(final ProfileKey key, @Nullable final Timer timer) {
if (timer == null) {
enabled.put(key, false);
addTime(key, null);
daysOfWeek.put(key, EnumSet.noneOf(DayOfWeek.class));
} else {
enabled.put(key, ACTIVATE.equals(timer.action));
addTime(key, timer.timeStamp);
final EnumSet<DayOfWeek> daySet = EnumSet.noneOf(DayOfWeek.class);
if (timer.timerWeekDays != null) {
daysOfWeek.put(key, EnumSet.noneOf(DayOfWeek.class));
for (String day : timer.timerWeekDays) {
try {
daySet.add(DayOfWeek.valueOf(day.toUpperCase()));
} catch (IllegalArgumentException iae) {
LOGGER.warn("unexpected value for {} day: {}", key.name(), day);
}
daysOfWeek.put(key, daySet);
}
}
}
}
private Timer getTimer(final ProfileKey key) {
final Timer timer = new Timer();
switch (key) {
case TIMER1:
timer.id = 1;
break;
case TIMER2:
timer.id = 2;
break;
case TIMER3:
timer.id = 3;
break;
case TIMER4:
timer.id = 4;
break;
default:
// timer id stays -1
break;
}
Boolean enabledBool = isEnabled(key);
if (enabledBool != null) {
timer.action = enabledBool ? ACTIVATE : DEACTIVATE;
} else {
timer.action = DEACTIVATE;
}
final LocalTime time = getTime(key);
if (!time.equals(Constants.NULL_LOCAL_TIME)) {
timer.timeStamp = new Time();
timer.timeStamp.hour = time.getHour();
timer.timeStamp.minute = time.getMinute();
}
final Set<DayOfWeek> days = daysOfWeek.get(key);
if (days != null) {
timer.timerWeekDays = new ArrayList<>();
for (DayOfWeek day : days) {
timer.timerWeekDays.add(day.name().toLowerCase());
}
}
return timer;
}
}

View File

@ -0,0 +1,106 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.utils;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import javax.measure.Unit;
import javax.measure.quantity.Length;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.unit.MetricPrefix;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.types.UnDefType;
/**
* The {@link Constants} General Constant Definitions
*
* @author Bernd Weymann - Initial contribution
* @author Norbert Truchsess - contributor
*/
@NonNullByDefault
public class Constants {
// For Vehicle Status
public static final String NO_ENTRIES = "-";
public static final String OPEN = "Open";
public static final String CLOSED = "Closed";
public static final String LOCKED = "Locked";
public static final String UNLOCKED = "Unlocked";
public static final String CONNECTED = "Connected";
public static final String UNCONNECTED = "Not connected";
public static final String UNDEF = UnDefType.UNDEF.toFullString();
public static final String NULL_TIME = "00:00";
public static final String KILOMETERS_JSON = "KILOMETERS";
public static final String KM_JSON = "km";
public static final String MI_JSON = "mi";
public static final String UNIT_PRECENT_JSON = "%";
public static final String UNIT_LITER_JSON = "l";
public static final Unit<Length> KILOMETRE_UNIT = MetricPrefix.KILO(SIUnits.METRE);
public static final int INT_UNDEF = -1;
// Services in Discovery
public static final String ENABLED = "ENABLED";
// General Constants for String concatenation
public static final String NULL = "null";
public static final String SPACE = " ";
public static final String UNDERLINE = "_";
public static final String HYPHEN = " - ";
public static final String PLUS = "+";
public static final String EMPTY = "";
public static final String COMMA = ",";
public static final String QUESTION = "?";
public static final String COLON = ":";
public static final String SEMICOLON = ";";
public static final String TILDE = "~";
public static final String ANONYMOUS = "anonymous";
public static final String EMPTY_JSON = "{}";
public static final String LANGUAGE_AUTODETECT = "AUTODETECT";
// Time Constants for DateTime channels
public static final LocalDate EPOCH_DAY = LocalDate.ofEpochDay(0);
public static final DateTimeFormatter TIME_FORMATER = DateTimeFormatter.ofPattern("HH:mm");
public static final LocalTime NULL_LOCAL_TIME = LocalTime.parse(NULL_TIME, TIME_FORMATER);
@SuppressWarnings("serial")
public static final Map<DayOfWeek, String> DAYS = new HashMap<>() {
{
put(DayOfWeek.MONDAY, "Mon");
put(DayOfWeek.TUESDAY, "Tue");
put(DayOfWeek.WEDNESDAY, "Wed");
put(DayOfWeek.THURSDAY, "Thu");
put(DayOfWeek.FRIDAY, "Fri");
put(DayOfWeek.SATURDAY, "Sat");
put(DayOfWeek.SUNDAY, "Sun");
}
};
// Drive Train definitions from json
public static final String BEV = "ELECTRIC";
public static final String REX_EXTENSION = "(+ REX)";
public static final String HYBRID = "HYBRID";
public static final String CONV = "COMBUSTION";
public static final String PHEV = "PLUGIN_HYBRID";
// Carging States
public static final String DEFAULT = "DEFAULT";
public static final String NOT_CHARGING_STATE = "NOT_CHARGING";
public static final String CHARGING_STATE = "CHARGING";
public static final String PLUGGED_STATE = "PLUGGED_IN";
}

View File

@ -0,0 +1,371 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.utils;
import java.lang.reflect.Type;
import java.text.SimpleDateFormat;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.TimeZone;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mybmw.internal.MyBMWConstants;
import org.openhab.binding.mybmw.internal.dto.charge.Time;
import org.openhab.binding.mybmw.internal.dto.properties.Address;
import org.openhab.binding.mybmw.internal.dto.properties.Coordinates;
import org.openhab.binding.mybmw.internal.dto.properties.Distance;
import org.openhab.binding.mybmw.internal.dto.properties.Location;
import org.openhab.binding.mybmw.internal.dto.properties.Range;
import org.openhab.binding.mybmw.internal.dto.status.Mileage;
import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
/**
* The {@link Converter} Conversion Helpers
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class Converter {
public static final Logger LOGGER = LoggerFactory.getLogger(Converter.class);
public static final String DATE_INPUT_PATTERN_STRING = "yyyy-MM-dd'T'HH:mm:ss";
public static final DateTimeFormatter DATE_INPUT_PATTERN = DateTimeFormatter.ofPattern(DATE_INPUT_PATTERN_STRING);
public static final DateTimeFormatter LOCALE_ENGLISH_TIMEFORMATTER = DateTimeFormatter.ofPattern("hh:mm a",
Locale.ENGLISH);
public static final SimpleDateFormat ISO_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS");
private static final Gson GSON = new Gson();
private static final Vehicle INVALID_VEHICLE = new Vehicle();
private static final String SPLIT_HYPHEN = "-";
private static final String SPLIT_BRACKET = "\\(";
private static final String VIN_PATTERN = "\"vin\":";
private static final String VEHICLE_LOCATION_PATTERN = "\"vehicleLocation\":";
private static final String VEHICLE_LOCATION_REPLACEMENT = "\"vehicleLocation\": {\"coordinates\": {\"latitude\": 1.1,\"longitude\": 2.2},\"address\": {\"formatted\": \"anonymous\"},\"heading\": -1}";
private static final char OPEN_BRACKET = "{".charAt(0);
private static final char CLOSING_BRACKET = "}".charAt(0);
// https://www.baeldung.com/gson-list
public static final Type VEHICLE_LIST_TYPE = new TypeToken<ArrayList<Vehicle>>() {
}.getType();
public static int offsetMinutes = -1;
public static String zonedToLocalDateTime(String input) {
try {
ZonedDateTime d = ZonedDateTime.parse(input).withZoneSameInstant(ZoneId.systemDefault());
return d.toLocalDateTime().format(Converter.DATE_INPUT_PATTERN);
} catch (Exception e) {
LOGGER.debug("Unable to parse date {} - {}", input, e.getMessage());
}
return input;
}
public static String toTitleCase(@Nullable String input) {
if (input == null) {
return toTitleCase(Constants.UNDEF);
} else if (input.length() == 1) {
return input;
} else {
String lower = input.replaceAll(Constants.UNDERLINE, Constants.SPACE).toLowerCase();
String converted = toTitleCase(lower, Constants.SPACE);
converted = toTitleCase(converted, SPLIT_HYPHEN);
converted = toTitleCase(converted, SPLIT_BRACKET);
return converted;
}
}
private static String toTitleCase(String input, String splitter) {
String[] arr = input.split(splitter);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < arr.length; i++) {
if (i > 0) {
sb.append(splitter.replaceAll("\\\\", Constants.EMPTY));
}
sb.append(Character.toUpperCase(arr[i].charAt(0))).append(arr[i].substring(1));
}
return sb.toString().trim();
}
public static String capitalizeFirst(String str) {
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
public static Gson getGson() {
return GSON;
}
/**
* Measure distance between 2 coordinates
*
* @param sourceLatitude
* @param sourceLongitude
* @param destinationLatitude
* @param destinationLongitude
* @return distance
*/
public static double measureDistance(double sourceLatitude, double sourceLongitude, double destinationLatitude,
double destinationLongitude) {
double earthRadius = 6378.137; // Radius of earth in KM
double dLat = destinationLatitude * Math.PI / 180 - sourceLatitude * Math.PI / 180;
double dLon = destinationLongitude * Math.PI / 180 - sourceLongitude * Math.PI / 180;
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(sourceLatitude * Math.PI / 180)
* Math.cos(destinationLatitude * Math.PI / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return earthRadius * c;
}
/**
* Easy function but there's some measures behind:
* Guessing the range of the Vehicle on Map. If you can drive x kilometers with your Vehicle it's not feasible to
* project this x km Radius on Map. The roads to be taken are causing some overhead because they are not a straight
* line from Location A to B.
* I've taken some measurements to calculate the overhead factor based on Google Maps
* Berlin - Dresden: Road Distance: 193 air-line Distance 167 = Factor 87%
* Kassel - Frankfurt: Road Distance: 199 air-line Distance 143 = Factor 72%
* After measuring more distances you'll find out that the outcome is between 70% and 90%. So
*
* This depends also on the roads of a concrete route but this is only a guess without any Route Navigation behind
*
* In future it's foreseen to replace this with BMW RangeMap Service which isn't running at the moment.
*
* @param range
* @return mapping from air-line distance to "real road" distance
*/
public static int guessRangeRadius(double range) {
return (int) (range * 0.8);
}
public static int getIndex(String fullString) {
int index = -1;
try {
index = Integer.parseInt(fullString);
} catch (NumberFormatException nfe) {
}
return index;
}
/**
* Returns list of found vehicles
* In case of errors return empty list
*
* @param json
* @return
*/
public static List<Vehicle> getVehicleList(String json) {
try {
List<Vehicle> l = GSON.fromJson(json, VEHICLE_LIST_TYPE);
if (l != null) {
return l;
} else {
return new ArrayList<Vehicle>();
}
} catch (JsonSyntaxException e) {
LOGGER.warn("JsonSyntaxException {}", e.getMessage());
return new ArrayList<Vehicle>();
}
}
public static Vehicle getVehicle(String vin, String json) {
List<Vehicle> l = getVehicleList(json);
for (Vehicle vehicle : l) {
if (vin.equals(vehicle.vin)) {
// declare vehicle as valid
vehicle.valid = true;
return getConsistentVehcile(vehicle);
}
}
return INVALID_VEHICLE;
}
public static String getRawVehicleContent(String vin, String json) {
JsonArray jArr = JsonParser.parseString(json).getAsJsonArray();
for (int i = 0; i < jArr.size(); i++) {
JsonObject jo = jArr.get(i).getAsJsonObject();
String jsonVin = jo.getAsJsonPrimitive(MyBMWConstants.VIN).getAsString();
if (vin.equals(jsonVin)) {
return jo.toString();
}
}
return Constants.EMPTY_JSON;
}
/**
* ensure basic data like mileage and location data are available every time
*
* @param v
* @return
*/
public static Vehicle getConsistentVehcile(Vehicle v) {
if (v.status.currentMileage == null) {
v.status.currentMileage = new Mileage();
v.status.currentMileage.mileage = -1;
v.status.currentMileage.units = "km";
}
if (v.properties.combustionRange == null) {
v.properties.combustionRange = new Range();
v.properties.combustionRange.distance = new Distance();
v.properties.combustionRange.distance.value = -1;
v.properties.combustionRange.distance.units = Constants.EMPTY;
}
if (v.properties.vehicleLocation == null) {
v.properties.vehicleLocation = new Location();
v.properties.vehicleLocation.heading = -1;
v.properties.vehicleLocation.coordinates = new Coordinates();
v.properties.vehicleLocation.coordinates.latitude = -1.234;
v.properties.vehicleLocation.coordinates.longitude = -9.876;
v.properties.vehicleLocation.address = new Address();
v.properties.vehicleLocation.address.formatted = Constants.UNDEF;
}
return v;
}
public static String getRandomString(int size) {
int leftLimit = 97; // letter 'a'
int rightLimit = 122; // letter 'z'
Random random = new Random();
String generatedString = random.ints(leftLimit, rightLimit + 1).limit(size)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString();
return generatedString;
}
public static State getLockState(boolean lock) {
if (lock) {
return StringType.valueOf(Constants.LOCKED);
} else {
return StringType.valueOf(Constants.UNLOCKED);
}
}
public static State getClosedState(boolean close) {
if (close) {
return StringType.valueOf(Constants.CLOSED);
} else {
return StringType.valueOf(Constants.OPEN);
}
}
public static State getConnectionState(boolean connected) {
if (connected) {
return StringType.valueOf(Constants.CONNECTED);
} else {
return StringType.valueOf(Constants.UNCONNECTED);
}
}
public static String getCurrentISOTime() {
Date date = new Date(System.currentTimeMillis());
synchronized (ISO_FORMATTER) {
ISO_FORMATTER.setTimeZone(TimeZone.getTimeZone("UTC"));
return ISO_FORMATTER.format(date);
}
}
public static String getTime(Time t) {
StringBuffer time = new StringBuffer();
if (t.hour < 10) {
time.append("0");
}
time.append(Integer.toString(t.hour)).append(":");
if (t.minute < 10) {
time.append("0");
}
time.append(Integer.toString(t.minute));
return time.toString();
}
public static int getOffsetMinutes() {
if (offsetMinutes == -1) {
ZoneOffset zo = ZonedDateTime.now().getOffset();
offsetMinutes = zo.getTotalSeconds() / 60;
}
return offsetMinutes;
}
public static int stringToInt(String intStr) {
int integer = Constants.INT_UNDEF;
try {
integer = Integer.parseInt(intStr);
} catch (Exception e) {
LOGGER.debug("Unable to convert range {} into int value", intStr);
}
return integer;
}
public static String getLocalTime(String chrageInfoLabel) {
String[] timeSplit = chrageInfoLabel.split(Constants.TILDE);
if (timeSplit.length == 2) {
try {
LocalTime timeL = LocalTime.parse(timeSplit[1].trim(), LOCALE_ENGLISH_TIMEFORMATTER);
return timeSplit[0] + Constants.TILDE + timeL.toString();
} catch (Exception e) {
LOGGER.debug("Unable to parse date {} - {}", timeSplit[1], e.getMessage());
}
}
return chrageInfoLabel;
}
public static String anonymousFingerprint(String raw) {
String anonymousFingerprintString = raw;
int vinStartIndex = raw.indexOf(VIN_PATTERN);
if (vinStartIndex != -1) {
String[] arr = raw.substring(vinStartIndex + VIN_PATTERN.length()).trim().split("\"");
String vin = arr[1].trim();
anonymousFingerprintString = raw.replace(vin, "anonymous");
}
int locationStartIndex = raw.indexOf(VEHICLE_LOCATION_PATTERN);
int bracketCounter = -1;
if (locationStartIndex != -1) {
int endLocationIndex = 0;
for (int i = locationStartIndex; i < raw.length() && bracketCounter != 0; i++) {
endLocationIndex = i;
if (raw.charAt(i) == OPEN_BRACKET) {
if (bracketCounter == -1) {
// start point
bracketCounter = 1;
} else {
bracketCounter++;
}
} else if (raw.charAt(i) == CLOSING_BRACKET) {
bracketCounter--;
}
}
String locationReplacement = raw.substring(locationStartIndex, endLocationIndex + 1);
anonymousFingerprintString = anonymousFingerprintString.replace(locationReplacement,
VEHICLE_LOCATION_REPLACEMENT);
}
return anonymousFingerprintString;
}
}

View File

@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.utils;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link HTTPConstants} class contains fields mapping thing configuration parameters.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class HTTPConstants {
public static final int HTTP_TIMEOUT_SEC = 10;
public static final String CONTENT_TYPE_URL_ENCODED = "application/x-www-form-urlencoded";
public static final String CONTENT_TYPE_JSON_ENCODED = "application/json";
public static final String KEEP_ALIVE = "Keep-Alive";
public static final String CLIENT_ID = "client_id";
public static final String RESPONSE_TYPE = "response_type";
public static final String TOKEN = "token";
public static final String CODE = "code";
public static final String CODE_VERIFIER = "code_verifier";
public static final String STATE = "state";
public static final String NONCE = "nonce";
public static final String REDIRECT_URI = "redirect_uri";
public static final String AUTHORIZATION = "authorization";
public static final String GRANT_TYPE = "grant_type";
public static final String SCOPE = "scope";
public static final String CREDENTIALS = "Credentials";
public static final String USERNAME = "username";
public static final String PASSWORD = "password";
public static final String CONTENT_LENGTH = "Content-Length";
public static final String CODE_CHALLENGE = "code_challenge";
public static final String CODE_CHALLENGE_METHOD = "code_challenge_method";
public static final String ACCESS_TOKEN = "access_token";
public static final String TOKEN_TYPE = "token_type";
public static final String EXPIRES_IN = "expires_in";
public static final String CHUNKED = "chunked";
public static final String ACP_SUBSCRIPTION_KEY = "ocp-apim-subscription-key";
public static final String X_USER_AGENT = "x-user-agent";
}

View File

@ -0,0 +1,47 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.utils;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link ImageProperties} Properties of current Vehicle Image
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class ImageProperties {
public static final int RETRY_COUNTER = 5;
public int failCounter = 0;
public String viewport = "Default";
public ImageProperties(String viewport) {
this.viewport = viewport;
}
public ImageProperties() {
}
public void failed() {
failCounter++;
}
public boolean failLimitReached() {
return failCounter > RETRY_COUNTER;
}
@Override
public String toString() {
return viewport;
}
}

View File

@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.utils;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mybmw.internal.handler.RemoteServiceHandler.RemoteService;
import org.openhab.core.types.CommandOption;
/**
* Helper class for Remote Service Commands
*
* @author Norbert Truchsess - Initial contribution
*/
@NonNullByDefault
public class RemoteServiceUtils {
private static final Map<String, RemoteService> COMMAND_SERVICES = Stream.of(RemoteService.values())
.collect(Collectors.toUnmodifiableMap(RemoteService::getId, service -> service));
public static Optional<RemoteService> getRemoteService(final String command) {
return Optional.ofNullable(COMMAND_SERVICES.get(command));
}
public static List<CommandOption> getOptions(final boolean isElectric) {
return Stream.of(RemoteService.values()).map(service -> new CommandOption(service.getId(), service.getLabel()))
.collect(Collectors.toUnmodifiableList());
}
}

View File

@ -0,0 +1,241 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mybmw.internal.utils;
import java.time.ZonedDateTime;
import java.util.List;
import javax.measure.Unit;
import javax.measure.quantity.Length;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mybmw.internal.MyBMWConstants.VehicleType;
import org.openhab.binding.mybmw.internal.dto.properties.CBS;
import org.openhab.binding.mybmw.internal.dto.status.FuelIndicator;
import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link VehicleStatusUtils} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class VehicleStatusUtils {
public static final Logger LOGGER = LoggerFactory.getLogger(VehicleStatusUtils.class);
public static State getNextServiceDate(List<CBS> cbsMessageList) {
ZonedDateTime farFuture = ZonedDateTime.now().plusYears(100);
ZonedDateTime serviceDate = farFuture;
for (CBS service : cbsMessageList) {
if (service.dateTime != null) {
ZonedDateTime d = ZonedDateTime.parse(service.dateTime);
if (d.isBefore(serviceDate)) {
serviceDate = d;
} // else skip
}
}
if (serviceDate.equals(farFuture)) {
return UnDefType.UNDEF;
} else {
DateTimeType dt = DateTimeType.valueOf(serviceDate.format(Converter.DATE_INPUT_PATTERN));
return dt;
}
}
public static State getNextServiceMileage(List<CBS> cbsMessageList) {
boolean imperial = false;
int serviceMileage = Integer.MAX_VALUE;
for (CBS service : cbsMessageList) {
if (service.distance != null) {
if (service.distance.value < serviceMileage) {
serviceMileage = service.distance.value;
imperial = !Constants.KILOMETERS_JSON.equals(service.distance.units);
}
}
}
if (serviceMileage != Integer.MAX_VALUE) {
if (imperial) {
return QuantityType.valueOf(serviceMileage, ImperialUnits.MILE);
} else {
return QuantityType.valueOf(serviceMileage, Constants.KILOMETRE_UNIT);
}
} else {
return UnDefType.UNDEF;
}
}
/**
* calculates the mapping of thing type
*
* @param driveTrain
* @param model
* @return
*/
public static VehicleType vehicleType(String driveTrain, String model) {
if (Constants.BEV.equals(driveTrain)) {
if (model.endsWith(Constants.REX_EXTENSION)) {
return VehicleType.ELECTRIC_REX;
} else {
return VehicleType.ELECTRIC;
}
} else if (Constants.PHEV.equals(driveTrain)) {
return VehicleType.PLUGIN_HYBRID;
} else if (Constants.CONV.equals(driveTrain) || Constants.HYBRID.equals(driveTrain)) {
return VehicleType.CONVENTIONAL;
}
LOGGER.warn("Unknown Vehicle Type: {} | {}", model, driveTrain);
return VehicleType.UNKNOWN;
}
public static @Nullable Unit<Length> getLengthUnit(List<FuelIndicator> indicators) {
Unit<Length> ret = null;
for (FuelIndicator fuelIndicator : indicators) {
String unitAbbrev = fuelIndicator.rangeUnits;
switch (unitAbbrev) {
case Constants.KM_JSON:
if (ret != null) {
if (!ret.equals(Constants.KILOMETRE_UNIT)) {
LOGGER.debug("Ambigious Unit declarations. Found {} before {}", ret, Constants.KM_JSON);
} // else - fine!
} else {
ret = Constants.KILOMETRE_UNIT;
}
break;
case Constants.MI_JSON:
if (ret != null) {
if (!ret.equals(ImperialUnits.MILE)) {
LOGGER.debug("Ambigious Unit declarations. Found {} before {}", ret, Constants.MI_JSON);
} // else - fine!
} else {
ret = ImperialUnits.MILE;
}
break;
default:
LOGGER.debug("Cannot evaluate Unit for {}", unitAbbrev);
break;
}
}
return ret;
}
/**
* The range values delivered by BMW are quite ambiguous!
* - status fuel indicators are missing a unique identifier
* - properties ranges delivering wrong values for hybrid and fuel range
* - properties ranges are not reflecting mi / km - every time km
*
* So getRange will try
* 1) fuel indicator
* 2) ranges from properties, except combined range
* 3) take a guess from fuel indicators
*
* @param unitJson
* @param indicators
* @return
*/
public static int getRange(String unitJson, Vehicle vehicle) {
if (vehicle.status.fuelIndicators.size() == 1) {
return Converter.stringToInt(vehicle.status.fuelIndicators.get(0).rangeValue);
} else {
return guessRange(unitJson, vehicle);
}
}
/**
* Guesses the range from 3 fuelindicators
* - electric range calculation is correct
* - for the 2 other values:
* -- smaller one is assigned to fuel range
* -- bigger one is assigned to hybrid range
*
* @see VehicleStatusTest testGuessRange
*
* @param unitJson
* @param vehicle
* @return
*/
public static int guessRange(String unitJson, Vehicle vehicle) {
int electricGuess = Constants.INT_UNDEF;
int fuelGuess = Constants.INT_UNDEF;
int hybridGuess = Constants.INT_UNDEF;
for (FuelIndicator fuelIndicator : vehicle.status.fuelIndicators) {
// electric range - this fits 100%
if (Constants.UNIT_PRECENT_JSON.equals(fuelIndicator.levelUnits)
&& fuelIndicator.chargingStatusType != null) {
// found electric
electricGuess = Converter.stringToInt(fuelIndicator.rangeValue);
} else {
if (fuelGuess == Constants.INT_UNDEF) {
// fuel not set? then assume it's fuel
fuelGuess = Converter.stringToInt(fuelIndicator.rangeValue);
} else {
// fuel already guessed - take smaller value for fuel, bigger for hybrid
int newGuess = Converter.stringToInt(fuelIndicator.rangeValue);
hybridGuess = Math.max(fuelGuess, newGuess);
fuelGuess = Math.min(fuelGuess, newGuess);
}
}
}
switch (unitJson) {
case Constants.UNIT_PRECENT_JSON:
return electricGuess;
case Constants.UNIT_LITER_JSON:
return fuelGuess;
case Constants.PHEV:
return hybridGuess;
default:
return Constants.INT_UNDEF;
}
}
public static String getChargStatus(Vehicle vehicle) {
FuelIndicator fi = getElectricFuelIndicator(vehicle);
if (fi.chargingStatusType != null) {
if (fi.chargingStatusType.equals(Constants.DEFAULT)) {
return Constants.NOT_CHARGING_STATE;
} else {
return fi.chargingStatusType;
}
}
return Constants.UNDEF;
}
public static String getChargeInfo(Vehicle vehicle) {
FuelIndicator fi = getElectricFuelIndicator(vehicle);
if (fi.chargingStatusType != null && fi.infoLabel != null) {
if (fi.chargingStatusType.equals(Constants.CHARGING_STATE)
|| fi.chargingStatusType.equals(Constants.PLUGGED_STATE)) {
return fi.infoLabel;
}
}
return Constants.HYPHEN;
}
private static FuelIndicator getElectricFuelIndicator(Vehicle vehicle) {
for (FuelIndicator fuelIndicator : vehicle.status.fuelIndicators) {
if (Constants.UNIT_PRECENT_JSON.equals(fuelIndicator.levelUnits)
&& fuelIndicator.chargingStatusType != null) {
return fuelIndicator;
}
}
return new FuelIndicator();
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="mybmw" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>MyBMW</name>
<description>Provides access to your Vehicle Data like MyBMW App</description>
</binding:binding>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:mybmw:bridge">
<parameter name="userName" type="text" required="true">
<label>Username</label>
<description>MyBMW Username</description>
</parameter>
<parameter name="password" type="text" required="true">
<label>Password</label>
<description>MyBMW Password</description>
<context>password</context>
</parameter>
<parameter name="region" type="text" required="true">
<label>Region</label>
<description>Select Region in order to connect to the appropriate BMW Server</description>
<options>
<option value="NORTH_AMERICA">North America</option>
<option value="CHINA">China</option>
<option value="ROW">Rest of the World</option>
</options>
<default>ROW</default>
</parameter>
<parameter name="language" type="text">
<label>Language Settings</label>
<description>Channel data can be returned in the desired language like en, de, fr ...</description>
<advanced>true</advanced>
<default>AUTODETECT</default>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:mybmw:vehicle">
<parameter name="vin" type="text" required="true">
<label>Vehicle Identification Number (VIN)</label>
<description>Unique VIN given by BMW</description>
</parameter>
<parameter name="refreshInterval" type="integer" min="1" unit="min" required="true">
<label>Refresh Interval</label>
<description>Data refresh rate for your vehicle data</description>
<default>5</default>
</parameter>
<parameter name="vehicleBrand" type="text" required="true">
<label>Brand of the Vehicle</label>
<description>Vehicle brand like BMW or Mini</description>
<advanced>true</advanced>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,225 @@
# Binding
binding.mybmw.name = MyBMW
binding.mybmw.description = Fahrzeugdaten über die MyBMW App
# bridge types
thing-type.mybmw.account.label = MyBMW Benutzerkonto
thing-type.mybmw.account.description = Kontodaten für das BMW Benutzerkonto
# bridge config
thing-type.config.mybmw.bridge.userName.label = Benutzername
thing-type.config.mybmw.bridge.userName.description = Benutzername für die MyBMW App
thing-type.config.mybmw.bridge.password.label = Passwort
thing-type.config.mybmw.bridge.password.description = Passwort für die MyBMW App
thing-type.config.mybmw.bridge.region.label = Region
thing-type.config.mybmw.bridge.region.description = Auswahl Ihrer Region
thing-type.config.mybmw.bridge.region.option.NORTH_AMERICA = Nordamerika
thing-type.config.mybmw.bridge.region.option.CHINA = China
thing-type.config.mybmw.bridge.region.option.ROW = Rest der Welt
thing-type.config.mybmw.bridge.language.label = Sprachauswahl
thing-type.config.mybmw.bridge.language.description = Daten werden für die gewünschte Sprache angefordert (en, de, fr ...)
# thing types
thing-type.mybmw.bev_rex.label = Elektrofahrzeug mit REX
thing-type.mybmw.bev_rex.description = Elektrofahrzeug mit Range Extender (bev_rex)
thing-type.mybmw.bev.label = Elektrofahrzeug
thing-type.mybmw.bev.description = Batterieelektrisches Fahrzeug (bev)
thing-type.mybmw.phev.label = Plug-in-Hybrid Elektrofahrzeug
thing-type.mybmw.phev.description = Konventionelles Fahrzeug mit Elektromotor (phev)
thing-type.mybmw.conv.label = Konventionelles Fahrzeug
thing-type.mybmw.conv.description = Konventionelles Benzin/Diesel Fahrzeug (conv)
# thing config
thing-type.config.mybmw.vehicle.vin.label = Fahrzeug Identifikationsnummer (VIN)
thing-type.config.mybmw.vehicle.vin.description = VIN des Fahrzeugs
thing-type.config.mybmw.vehicle.refreshInterval.label = Datenaktualisierung in Minuten
thing-type.config.mybmw.vehicle.refreshInterval.description = Rate der Datenaktualisierung Ihres Fahrzeugs
thing-type.config.mybmw.vehicle.vehicleBrand.label = Marke des Fahrzeugs
thing-type.config.mybmw.vehicle.vehicleBrand.description = Fahrzeugmarke wie z.B. BMW oder Mini.
# Channel Groups
channel-group-type.mybmw.ev-vehicle-status.label = Fahrzeug Zustand
channel-group-type.mybmw.ev-vehicle-status.description = Gesamtzustand des Fahrzeugs
channel-group-type.mybmw.vehicle-status.label = Fahrzeug Zustand
channel-group-type.mybmw.vehicle-status.description = Gesamtzustand des Fahrzeugs
channel-group-type.mybmw.ev-range-values.label = Elektro- Reichweiten und Batterieladung
channel-group-type.mybmw.ev-range-values.description = Tachostand, Reichweiten und Ladestand des Fahrzeugs
channel-group-type.mybmw.hybrid-range-values.label = Hybride Reichweiten und Füllstände
channel-group-type.mybmw.hybrid-range-values.description = Tachostand, Reichweite, Ladezustand und Tankfüllung für hybride Fahrzeuge
channel-group-type.mybmw.conv-range-values.label = Verbrenner Reichweiten und Füllstände
channel-group-type.mybmw.conv-range-values.description = Tachostand, Reichweite und Tankfüllung des Fahrzeugs
channel-group-type.mybmw.door-values.label = Details aller Türen
channel-group-type.mybmw.door-values.description = Zeigt die Details der Türen und Fenster des Fahrzeugs
channel-group-type.mybmw.check-control-values.label = Warnungen
channel-group-type.mybmw.check-control-values.description = Aktuelle Warnungen des Fahrzeugs
channel-group-type.mybmw.service-values.label = Wartung
channel-group-type.mybmw.service-values.description = Anstehende Wartungstermine des Fahrzeugs
channel-group-type.mybmw.location-values.label = Fahrzeug Standort
channel-group-type.mybmw.location-values.description = Koordinaten und Ausrichtung des Fahrzeugs
channel-group-type.mybmw.remote-services.label = Fernsteuerung
channel-group-type.mybmw.remote-services.description = Fernsteuerung des Fahrzeugs
channel-group-type.mybmw.profile-values.label = Elektrisches Ladeprofil
channel-group-type.mybmw.profile-values.description = Zeitplanung der Ladevorgänge
channel-group-type.mybmw.charge-statistic.label = Elektrische Ladestatistik
channel-group-type.mybmw.charge-statistic.description = Statistik der Ladevorgänge im Monat
channel-group-type.mybmw.session-values.label = Elektrische Ladevorgänge
channel-group-type.mybmw.session-values.description = Liste der letzten Ladevorgänge
channel-group-type.mybmw.tire-pressures.label = Reifen Luftdruck
channel-group-type.mybmw.tire-pressures.description = Reifen Luftdruck Ist und Sollwerte
channel-group-type.mybmw.image-values.label = Fahrzeug Bild
channel-group-type.mybmw.image-values.description = Bild des Fahrzeug basierend auf der ausgewählten Ansicht
# Channel Types
channel-type.mybmw.doors-channel.label = Gesamtzustand der Türen
channel-type.mybmw.windows-channel.label = Gesamtzustand der Fenster
channel-type.mybmw.lock-channel.label = Fahrzeug Abgeschlossen
channel-type.mybmw.next-service-date-channel.label = Nächster Service Termin
channel-type.mybmw.next-service-mileage-channel.label = Nächster Service in Kilometern
channel-type.mybmw.check-control-channel.label = Warnung Aktiv
channel-type.mybmw.plug-connection-channel.label = Ladestecker
channel-type.mybmw.charging-status-channel.label = Ladezustand
channel-type.mybmw.charging-info-channel.label = Ladeinformationen
channel-type.mybmw.motion-channel.label = Fahrzustand
channel-type.mybmw.last-update-channel.label = Letzte Aktualisierung
channel-type.mybmw.raw-channel.label = Rohdaten
channel-type.mybmw.driver-front-channel.label = Fahrertür
channel-type.mybmw.driver-rear-channel.label = Fahrertür Hinten
channel-type.mybmw.passenger-front-channel.label = Beifahrertür
channel-type.mybmw.passenger-rear-channel.label = Beifahrertür Hinten
channel-type.mybmw.hood-channel.label = Frontklappe
channel-type.mybmw.trunk-channel.label = Heckklappe
channel-type.mybmw.window-driver-front-channel.label = Fahrertür Fenster
channel-type.mybmw.window-driver-rear-channel.label = Fahrertür Hinten Fenster
channel-type.mybmw.window-passenger-front-channel.label = Beifahrertür Fenster
channel-type.mybmw.window-passenger-rear-channel.label = Beifahrertür Hinten Fenster
channel-type.mybmw.window-rear-channel.label = Heckfenster
channel-type.mybmw.sunroof-channel.label = Schiebedach
channel-type.mybmw.mileage-channel.label = Tachostand
channel-type.mybmw.range-hybrid-channel.label = Hybride Reichweite
channel-type.mybmw.range-electric-channel.label = Elektrische Reichweite
channel-type.mybmw.soc-channel.label = Batterie Ladestand
channel-type.mybmw.range-fuel-channel.label = Verbrenner Reichweite
channel-type.mybmw.remaining-fuel-channel.label = Tankstand
channel-type.mybmw.range-radius-electric-channel.label = Elektrischer Reichweiten-Radius
channel-type.mybmw.range-radius-fuel-channel.label = Verbrenner Reichweiten-Radius
channel-type.mybmw.range-radius-hybrid-channel.label = Hybrider Reichweiten-Radius
channel-type.mybmw.service-name-channel.label = Service
channel-type.mybmw.service-details-channel.label = Service Details
channel-type.mybmw.service-date-channel.label = Service Termin
channel-type.mybmw.service-mileage-channel.label = Service in Kilometern
channel-type.mybmw.checkcontrol-name-channel.label = Warnung
channel-type.mybmw.checkcontrol-details-channel.label = Warnung Details
channel-type.mybmw.checkcontrol-severity-channel.label = Warnung Priorität
channel-type.mybmw.profile-climate-channel.label = Klimatisierung bei Abfahrt
channel-type.mybmw.profile-mode-channel.label = Ladeprofil
channel-type.mybmw.profile-mode-channel.command.option.immediateCharging = Sofort Laden
channel-type.mybmw.profile-mode-channel.command.option.delayedCharging = Ladeverzögerung
channel-type.mybmw.profile-prefs-channel.label = Ladeprofil Präferenz
channel-type.mybmw.profile-prefs-channel.command.option.noPreSelection = Keine Präferenz
channel-type.mybmw.profile-prefs-channel.command.option.chargingWindow = Laden im Zeitfenster
channel-type.mybmw.profile-control-channel.label = Ladeplan
channel-type.mybmw.profile-control-channel.description = Ladeplan Auswahl
channel-type.mybmw.profile-target-channel.label = Ziel Ladezustand
channel-type.mybmw.profile-target-channel.description = Erwünschter Batterie Ladezustand
channel-type.mybmw.profile-limit-channel.label = Ladung Limitiert
channel-type.mybmw.profile-limit-channell.description = Limitierte Ladung aktiviert
channel-type.mybmw.window-start-channel.label = Ladefenster Startzeit
channel-type.mybmw.window-end-channel.label = Ladefenster Endzeit
channel-type.mybmw.timer1-enabled-channel.label = Zeitprofil 1 - Aktiviert
channel-type.mybmw.timer1-departure-channel.label = Zeitprofil 1 - Abfahrtszeit
channel-type.mybmw.timer1-days-channel.label = Zeitprofil 1 - Tage
channel-type.mybmw.timer1-day-mon-channel.label = Zeitprofil 1 - Montag
channel-type.mybmw.timer1-day-tue-channel.label = Zeitprofil 1 - Dienstag
channel-type.mybmw.timer1-day-wed-channel.label = Zeitprofil 1 - Mittwoch
channel-type.mybmw.timer1-day-thu-channel.label = Zeitprofil 1 - Donnerstag
channel-type.mybmw.timer1-day-fri-channel.label = Zeitprofil 1 - Freitag
channel-type.mybmw.timer1-day-sat-channel.label = Zeitprofil 1 - Samstag
channel-type.mybmw.timer1-day-sun-channel.label = Zeitprofil 1 - Sonntag
channel-type.mybmw.timer2-enabled-channel.label = Zeitprofil 2 - Aktiviert
channel-type.mybmw.timer2-departure-channel.label = Zeitprofil 2 - Abfahrtszeit
channel-type.mybmw.timer2-days-channel.label = Zeitprofil 2 - Tage
channel-type.mybmw.timer2-day-mon-channel.label = Zeitprofil 2 - Montag
channel-type.mybmw.timer2-day-tue-channel.label = Zeitprofil 2 - Dienstag
channel-type.mybmw.timer2-day-wed-channel.label = Zeitprofil 2 - Mittwoch
channel-type.mybmw.timer2-day-thu-channel.label = Zeitprofil 2 - Donnerstag
channel-type.mybmw.timer2-day-fri-channel.label = Zeitprofil 2 - Freitag
channel-type.mybmw.timer2-day-sat-channel.label = Zeitprofil 2 - Samstag
channel-type.mybmw.timer2-day-sun-channel.label = Zeitprofil 2 - Sonntag
channel-type.mybmw.timer3-enabled-channel.label = Zeitprofil 3 - Aktiviert
channel-type.mybmw.timer3-departure-channel.label = Zeitprofil 3 - Abfahrtszeit
channel-type.mybmw.timer3-days-channel.label = Zeitprofil 3 - Tage
channel-type.mybmw.timer3-day-mon-channel.label = Zeitprofil 3 - Montag
channel-type.mybmw.timer3-day-tue-channel.label = Zeitprofil 3 - Dienstag
channel-type.mybmw.timer3-day-wed-channel.label = Zeitprofil 3 - Mittwoch
channel-type.mybmw.timer3-day-thu-channel.label = Zeitprofil 3 - Donnerstag
channel-type.mybmw.timer3-day-fri-channel.label = Zeitprofil 3 - Freitag
channel-type.mybmw.timer3-day-sat-channel.label = Zeitprofil 3 - Samstag
channel-type.mybmw.timer3-day-sun-channel.label = Zeitprofil 3 - Sonntag
channel-type.mybmw.timer4-enabled-channel.label = Zeitprofil 4 - Aktiviert
channel-type.mybmw.timer4-departure-channel.label = Zeitprofil 4 - Abfahrtszeit
channel-type.mybmw.timer4-days-channel.label = Zeitprofil 4 - Tage
channel-type.mybmw.timer4-day-mon-channel.label = Zeitprofil 4 - Montag
channel-type.mybmw.timer4-day-tue-channel.label = Zeitprofil 4 - Dienstag
channel-type.mybmw.timer4-day-wed-channel.label = Zeitprofil 4 - Mittwoch
channel-type.mybmw.timer4-day-thu-channel.label = Zeitprofil 4 - Donnerstag
channel-type.mybmw.timer4-day-fri-channel.label = Zeitprofil 4 - Freitag
channel-type.mybmw.timer4-day-sat-channel.label = Zeitprofil 4 - Samstag
channel-type.mybmw.timer4-day-sun-channel.label = Zeitprofil 4 - Sonntag
# Location
channel-type.mybmw.gps-channel.label = Koordinaten
channel-type.mybmw.heading-channel.label = Ausrichtung
channel-type.mybmw.address-channel.label = Adresse
#Remote
channel-type.mybmw.remote-command-channel.label = Kommando Auswahl
channel-type.mybmw.remote-command-channel.command.option.light-flash = Lichthupe Ausführen
channel-type.mybmw.remote-command-channel.command.option.vehicle-finder = Fahrzeug Lokalisieren
channel-type.mybmw.remote-command-channel.command.option.door-lock = Fahrzeug Abschließen
channel-type.mybmw.remote-command-channel.command.option.door-unlock = Fahrzug Aufschließen
channel-type.mybmw.remote-command-channel.command.option.horn-blow = Hupe Aktivieren
channel-type.mybmw.remote-command-channel.command.option.climate-now-start = Klimatisierung Ausführen
channel-type.mybmw.remote-command-channel.command.option.climate-now-stop = Klimatisierung Beenden
channel-type.mybmw.remote-state-channel.label = Ausführungszustand
# Image
channel-type.mybmw.png-channel.label = Fahrzeug Bild
channel-type.mybmw.image-view-channel.label = Fahrzeug Ansicht
channel-type.mybmw.image-view-channel.command.option.VehicleStatus = Front Seitenansicht
channel-type.mybmw.image-view-channel.command.option.VehicleInfo = Frontansicht
channel-type.mybmw.image-view-channel.command.option.ChargingHistory = Seitenansicht
channel-type.mybmw.image-view-channel.command.option.Default = Standard Ansicht
# Charge Sessions
channel-type.mybmw.session-title-channel.label = Ladevorgang Beschreibung
channel-type.mybmw.session-subtitle-channel.label = Ladevorgang Details
channel-type.mybmw.session-energy-channel.label = Energie Geladen
channel-type.mybmw.session-issue-channel.label = Ladevorgang Probleme
channel-type.mybmw.session-status-channel.label = Ladevorgang Zustand
# Charge Statistcis
channel-type.mybmw.statistic-title-channel.label = Ladestatistik Monat
channel-type.mybmw.statistic-energy-channel.label = Energie Geladen Monat
channel-type.mybmw.statistic-energy-channel.description = Geladene Energie in diesem Monat
channel-type.mybmw.statistic-sessions-channel.label = Ladevorgänge Monat
channel-type.mybmw.statistic-sessions-channel.description = Anzahl der Ladevorgänge in diesem Monat
#Tires
channel-type.mybmw.front-left-current-channel.label = Reifen Luftdruck Vorne Links
channel-type.mybmw.front-left-wanted-channel.label = Reifen Luftdruck Vorne Links Sollwert
channel-type.mybmw.front-right-current-channel.label = Reifen Luftdruck Vorne Rechts
channel-type.mybmw.front-right-wanted-channel.label = Reifen Luftdruck Vorne Rechts Sollwert
channel-type.mybmw.rear-left-current-channel.label = Reifen Luftdruck Hinten Links
channel-type.mybmw.rear-left-wanted-channel.label = Reifen Luftdruck Hinten Links Sollwert
channel-type.mybmw.rear-right-current-channel.label = Reifen Luftdruck Hinten Rechts
channel-type.mybmw.rear-right-wanted-channel.label = Reifen Luftdruck Hinten Rechts Sollwert

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mybmw"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="account">
<label>MyBMW Account</label>
<description>Your BMW account data</description>
<config-description-ref uri="thing-type:mybmw:bridge"/>
</bridge-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mybmw"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-group-type id="charge-statistic">
<label>Charging Statistics</label>
<description>Charging statistics of current month</description>
<channels>
<channel id="title" typeId="statistic-title-channel"/>
<channel id="energy" typeId="statistic-energy-channel"/>
<channel id="sessions" typeId="statistic-sessions-channel"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mybmw"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-type id="statistic-title-channel">
<item-type>String</item-type>
<label>Charge Statistic Month</label>
</channel-type>
<channel-type id="statistic-energy-channel">
<item-type>Number:Energy</item-type>
<label>Energy Charged</label>
<description>Total energy charged in current month</description>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="statistic-sessions-channel">
<item-type>Number</item-type>
<label>Charge Sessions</label>
<description>Number of charging sessions this month</description>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mybmw"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-type id="checkcontrol-name-channel">
<item-type>String</item-type>
<label>CheckControl Description</label>
</channel-type>
<channel-type id="checkcontrol-details-channel">
<item-type>String</item-type>
<label>CheckControl Details</label>
</channel-type>
<channel-type id="checkcontrol-severity-channel">
<item-type>String</item-type>
<label>Severity Level</label>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mybmw"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-group-type id="check-control-values">
<label>Check Control Messages</label>
<description>Shows current active CheckControl messages</description>
<channels>
<channel id="name" typeId="checkcontrol-name-channel"/>
<channel id="details" typeId="checkcontrol-details-channel"/>
<channel id="severity" typeId="checkcontrol-severity-channel"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mybmw"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-group-type id="conv-range-values">
<label>Range and Fuel Data</label>
<description>Provides Mileage, remaining range and fuel level values</description>
<channels>
<channel id="mileage" typeId="mileage-channel"/>
<channel id="fuel" typeId="range-fuel-channel"/>
<channel id="remaining-fuel" typeId="remaining-fuel-channel"/>
<channel id="radius-fuel" typeId="range-radius-fuel-channel"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>

Some files were not shown because too many files have changed in this diff Show More