diff --git a/CODEOWNERS b/CODEOWNERS
index ee0088abd55..2250a960fa9 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -202,6 +202,7 @@
/bundles/org.openhab.binding.mqtt.homeassistant/ @davidgraeff @antroids
/bundles/org.openhab.binding.mqtt.homie/ @davidgraeff
/bundles/org.openhab.binding.mycroft/ @dalgwen
+/bundles/org.openhab.binding.mybmw/ @weymann @ntruchsess
/bundles/org.openhab.binding.myq/ @digitaldan
/bundles/org.openhab.binding.mystrom/ @pail23
/bundles/org.openhab.binding.nanoleaf/ @raepple @stefan-hoehn
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 87c62166da0..7176056bb0d 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -1001,6 +1001,11 @@
org.openhab.binding.mycroft
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.mybmw
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.myq
diff --git a/bundles/org.openhab.binding.mybmw/NOTICE b/bundles/org.openhab.binding.mybmw/NOTICE
new file mode 100644
index 00000000000..38d625e3492
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/NOTICE
@@ -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
diff --git a/bundles/org.openhab.binding.mybmw/README.md b/bundles/org.openhab.binding.mybmw/README.md
new file mode 100644
index 00000000000..9ee702052bd
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/README.md
@@ -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
+
+
+
+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 |
+
+
+
+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
+
+
+
+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
+
+
+
+
+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
+
+
+
+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%]" (i3) {channel="mybmw:bev_rex:4711:i3:range#mileage" }
+Number:Length i3Range "Range [%d %unit%]" (i3) {channel="mybmw:bev_rex:4711:i3:range#hybrid"}
+Number:Length i3RangeElectric "Electric Range [%d %unit%]" (i3,long) {channel="mybmw:bev_rex:4711:i3:range#electric"}
+Number:Length i3RangeFuel "Fuel Range [%d %unit%]" (i3) {channel="mybmw:bev_rex:4711:i3:range#fuel"}
+Number:Dimensionless i3BatterySoc "Battery Charge [%.1f %%]" (i3,long) {channel="mybmw:bev_rex:4711:i3:range#soc"}
+Number:Volume i3Fuel "Fuel [%.1f %unit%]" (i3) {channel="mybmw:bev_rex:4711:i3:range#remaining-fuel"}
+Number:Length i3RadiusElectric "Electric Radius [%d %unit%]" (i3) {channel="mybmw:bev_rex:4711:i3:range#radius-electric" }
+Number:Length i3RadiusFuel "Fuel Radius [%d %unit%]" (i3) {channel="mybmw:bev_rex:4711:i3:range#radius-fuel" }
+Number:Length i3RadiusHybrid "Hybrid Radius [%d %unit%]" (i3) {channel="mybmw:bev_rex:4711:i3:range#radius-hybrid" }
+
+String i3DoorStatus "Door Status [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:status#doors" }
+String i3WindowStatus "Window Status [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:status#windows" }
+String i3LockStatus "Lock Status [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:status#lock" }
+DateTime i3NextServiceDate "Next Service Date [%1$tb %1$tY]" (i3) {channel="mybmw:bev_rex:4711:i3:status#service-date" }
+String i3NextServiceMileage "Next Service Mileage [%d %unit%]" (i3) {channel="mybmw:bev_rex:4711:i3:status#service-mileage" }
+String i3CheckControl "Check Control [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:status#check-control" }
+String i3PlugConnection "Plug [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:status#plug-connection" }
+String i3ChargingStatus "[%s]" (i3) {channel="mybmw:bev_rex:4711:i3:status#charge" }
+String i3ChargingInfo "[%s]" (i3) {channel="mybmw:bev_rex:4711:i3:status#charge-info" }
+DateTime i3LastUpdate "Update [%1$tA, %1$td.%1$tm. %1$tH:%1$tM]" (i3) {channel="mybmw:bev_rex:4711:i3:status#last-update"}
+
+Location i3Location "Location [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:location#gps" }
+Number:Angle i3Heading "Heading [%.1f %unit%]" (i3) {channel="mybmw:bev_rex:4711:i3:location#heading" }
+
+String i3RemoteCommand "Command [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:remote#command" }
+String i3RemoteState "Remote Execution State [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:remote#state" }
+
+String i3DriverDoor "Driver Door [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:doors#driver-front" }
+String i3DriverDoorRear "Driver Door Rear [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:doors#driver-rear" }
+String i3PassengerDoor "Passenger Door [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:doors#passenger-front" }
+String i3PassengerDoorRear "Passenger Door Rear [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:doors#passenger-rear" }
+String i3Hood "Hood [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:doors#hood" }
+String i3Trunk "Trunk [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:doors#trunk" }
+String i3DriverWindow "Driver Window [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:doors#win-driver-front" }
+String i3DriverWindowRear "Driver Window Rear [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:doors#win-driver-rear" }
+String i3PassengerWindow "Passenger Window [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:doors#win-passenger-front" }
+String i3PassengerWindowRear "Passenger Window Rear [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:doors#win-passenger-rear" }
+String i3RearWindow "Rear Window [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:doors#win-rear" }
+String i3Sunroof "Sunroof [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:doors#sunroof" }
+
+String i3ServiceName "Service Name [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:service#name" }
+String i3ServiceDetails "Service Details [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:service#details" }
+Number:Length i3ServiceMileage "Service Mileage [%d %unit%]" (i3) {channel="mybmw:bev_rex:4711:i3:service#mileage" }
+DateTime i3ServiceDate "Service Date [%1$tb %1$tY]" (i3) {channel="mybmw:bev_rex:4711:i3:service#date" }
+
+String i3CCName "CheckControl Name [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:check#name" }
+String i3CCDetails "CheckControl Details [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:check#details" }
+String i3CCSeverity "CheckControl Severity [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:check#severity" }
+
+Switch i3ChargeProfileClimate "Charge Profile Climatization" (i3) {channel="mybmw:bev_rex:4711:i3:profile#climate" }
+String i3ChargeProfileMode "Charge Profile Mode [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:profile#mode" }
+String i3ChargeProfilePrefs "Charge Profile Preference [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:profile#prefs" }
+String i3ChargeProfileCtrl "Charge Profile Control [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:profile#control" }
+Number i3ChargeProfileTarget "Charge Profile SoC Target [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:profile#target" }
+Switch i3ChargeProfileLimit "Charge Profile limited" (i3) {channel="mybmw:bev_rex:4711:i3:profile#limit" }
+DateTime i3ChargeWindowStart "Charge Window Start [%1$tH:%1$tM]" (i3) {channel="mybmw:bev_rex:4711:i3:profile#window-start" }
+DateTime i3ChargeWindowEnd "Charge Window End [%1$tH:%1$tM]" (i3) {channel="mybmw:bev_rex:4711:i3:profile#window-end" }
+DateTime i3Timer1Departure "Timer 1 Departure [%1$tH:%1$tM]" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer1-departure" }
+String i3Timer1Days "Timer 1 Days [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer1-days" }
+Switch i3Timer1DayMon "Timer 1 Monday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer1-day-mon" }
+Switch i3Timer1DayTue "Timer 1 Tuesday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer1-day-tue" }
+Switch i3Timer1DayWed "Timer 1 Wednesday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer1-day-wed" }
+Switch i3Timer1DayThu "Timer 1 Thursday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer1-day-thu" }
+Switch i3Timer1DayFri "Timer 1 Friday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer1-day-fri" }
+Switch i3Timer1DaySat "Timer 1 Saturday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer1-day-sat" }
+Switch i3Timer1DaySun "Timer 1 Sunday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer1-day-sun" }
+Switch i3Timer1Enabled "Timer 1 Enabled" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer1-enabled" }
+DateTime i3Timer2Departure "Timer 2 Departure [%1$tH:%1$tM]" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer2-departure" }
+Switch i3Timer2DayMon "Timer 2 Monday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer2-day-mon" }
+Switch i3Timer2DayTue "Timer 2 Tuesday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer2-day-tue" }
+Switch i3Timer2DayWed "Timer 2 Wednesday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer2-day-wed" }
+Switch i3Timer2DayThu "Timer 2 Thursday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer2-day-thu" }
+Switch i3Timer2DayFri "Timer 2 Friday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer2-day-fri" }
+Switch i3Timer2DaySat "Timer 2 Saturday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer2-day-sat" }
+Switch i3Timer2DaySun "Timer 2 Sunday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer2-day-sun" }
+Switch i3Timer2Enabled "Timer 2 Enabled" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer2-enabled" }
+DateTime i3Timer3Departure "Timer 3 Departure [%1$tH:%1$tM]" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer3-departure" }
+Switch i3Timer3DayMon "Timer 3 Monday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer3-day-mon" }
+Switch i3Timer3DayTue "Timer 3 Tuesday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer3-day-tue" }
+Switch i3Timer3DayWed "Timer 3 Wednesday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer3-day-wed" }
+Switch i3Timer3DayThu "Timer 3 Thursday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer3-day-thu" }
+Switch i3Timer3DayFri "Timer 3 Friday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer3-day-fri" }
+Switch i3Timer3DaySat "Timer 3 Saturday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer3-day-sat" }
+Switch i3Timer3DaySun "Timer 3 Sunday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer3-day-sun" }
+Switch i3Timer3Enabled "Timer 3 Enabled" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer3-enabled" }
+DateTime i3Timer4Departure "Timer 4 Departure [%1$tH:%1$tM]" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer4-departure" }
+Switch i3Timer4DayMon "Timer 4 Monday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer4-day-mon" }
+Switch i3Timer4DayTue "Timer 4 Tuesday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer4-day-tue" }
+Switch i3Timer4DayWed "Timer 4 Wednesday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer4-day-wed" }
+Switch i3Timer4DayThu "Timer 4 Thursday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer4-day-thu" }
+Switch i3Timer4DayFri "Timer 4 Friday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer4-day-fri" }
+Switch i3Timer4DaySat "Timer 4 Saturday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer4-day-sat" }
+Switch i3Timer4DaySun "Timer 4 Sunday" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer4-day-sun" }
+Switch i3Timer4Enabled "Timer 4 Enabled" (i3) {channel="mybmw:bev_rex:4711:i3:profile#timer4-enabled" }
+
+String i3StatisticsTitle "[%s]" (i3) {channel="mybmw:bev_rex:4711:i3:statistic#title" }
+Number:Energy i3StatisticsEnergy "Charged [%d %unit%]" (i3) {channel="mybmw:bev_rex:4711:i3:statistic#energy" }
+Number i3StatisticsSessions "Sessions [%d]" (i3) {channel="mybmw:bev_rex:4711:i3:statistic#sessions" }
+
+String i3SessionTitle "[%s]" (i3) {channel="mybmw:bev_rex:4711:i3:session#title" }
+String i3SessionDetails "[%s]" (i3) {channel="mybmw:bev_rex:4711:i3:session#subtitle" }
+String i3SessionCharged "Energy Charged [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:session#energy" }
+String i3SessionProblems "Problems [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:session#issue" }
+String i3SessionStatus "Session status [%s]" (i3) {channel="mybmw:bev_rex:4711:i3:session#status" }
+
+Number:Pressure i3TireFLCurrent "Tire Front Left [%.1f %unit%]" (i3) {channel="mybmw:bev_rex:4711:i3:tires#fl-current" }
+Number:Pressure i3TireFLTarget "Tire Front Left Target [%.1f %unit%]" (i3) {channel="mybmw:bev_rex:4711:i3:tires#fl-target" }
+Number:Pressure i3TireFRCurrent "Tire Front Right [%.1f %unit%]" (i3) {channel="mybmw:bev_rex:4711:i3:tires#fr-current" }
+Number:Pressure i3TireFRTarget "Tire Front Right Target [%.1f %unit%]" (i3) {channel="mybmw:bev_rex:4711:i3:tires#fr-target" }
+Number:Pressure i3TireRLCurrent "Tire Rear Left [%.1f %unit%]" (i3) {channel="mybmw:bev_rex:4711:i3:tires#rl-current" }
+Number:Pressure i3TireRLTarget "Tire Rear Left Target [%.1f %unit%]" (i3) {channel="mybmw:bev_rex:4711:i3:tires#rl-target" }
+Number:Pressure i3TireRRCurrent "Tire Rear Right [%.1f %unit%]" (i3) {channel="mybmw:bev_rex:4711:i3:tires#rr-current" }
+Number:Pressure i3TireRRTarget "Tire Rear Right Target [%.1f %unit%]" (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]" (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).
+
diff --git a/bundles/org.openhab.binding.mybmw/doc/AwayImage.png b/bundles/org.openhab.binding.mybmw/doc/AwayImage.png
new file mode 100644
index 00000000000..24dd33cd9aa
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/AwayImage.png differ
diff --git a/bundles/org.openhab.binding.mybmw/doc/CarStatusImages.png b/bundles/org.openhab.binding.mybmw/doc/CarStatusImages.png
new file mode 100644
index 00000000000..b198ede53d5
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/CarStatusImages.png differ
diff --git a/bundles/org.openhab.binding.mybmw/doc/ChargingImage.png b/bundles/org.openhab.binding.mybmw/doc/ChargingImage.png
new file mode 100644
index 00000000000..09a132f2555
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/ChargingImage.png differ
diff --git a/bundles/org.openhab.binding.mybmw/doc/CheckControlImage.png b/bundles/org.openhab.binding.mybmw/doc/CheckControlImage.png
new file mode 100644
index 00000000000..4f8aa93c5d8
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/CheckControlImage.png differ
diff --git a/bundles/org.openhab.binding.mybmw/doc/DiscoveryScan.png b/bundles/org.openhab.binding.mybmw/doc/DiscoveryScan.png
new file mode 100644
index 00000000000..bba0f1965ed
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/DiscoveryScan.png differ
diff --git a/bundles/org.openhab.binding.mybmw/doc/RawData.png b/bundles/org.openhab.binding.mybmw/doc/RawData.png
new file mode 100644
index 00000000000..c474ef72cf1
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/RawData.png differ
diff --git a/bundles/org.openhab.binding.mybmw/doc/RawDataItems.png b/bundles/org.openhab.binding.mybmw/doc/RawDataItems.png
new file mode 100644
index 00000000000..3a5a140c014
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/RawDataItems.png differ
diff --git a/bundles/org.openhab.binding.mybmw/doc/SessionOptions.png b/bundles/org.openhab.binding.mybmw/doc/SessionOptions.png
new file mode 100644
index 00000000000..87138ba81da
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/SessionOptions.png differ
diff --git a/bundles/org.openhab.binding.mybmw/doc/UnlockedImage.png b/bundles/org.openhab.binding.mybmw/doc/UnlockedImage.png
new file mode 100644
index 00000000000..7518330d706
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/UnlockedImage.png differ
diff --git a/bundles/org.openhab.binding.mybmw/doc/panel.png b/bundles/org.openhab.binding.mybmw/doc/panel.png
new file mode 100644
index 00000000000..efc3a61bc65
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/panel.png differ
diff --git a/bundles/org.openhab.binding.mybmw/doc/properties.png b/bundles/org.openhab.binding.mybmw/doc/properties.png
new file mode 100644
index 00000000000..ce2a26350d3
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/properties.png differ
diff --git a/bundles/org.openhab.binding.mybmw/doc/range-radius.png b/bundles/org.openhab.binding.mybmw/doc/range-radius.png
new file mode 100644
index 00000000000..21fc8fb8def
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/range-radius.png differ
diff --git a/bundles/org.openhab.binding.mybmw/doc/vehicle-properties.png b/bundles/org.openhab.binding.mybmw/doc/vehicle-properties.png
new file mode 100644
index 00000000000..7c96844ac66
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/vehicle-properties.png differ
diff --git a/bundles/org.openhab.binding.mybmw/pom.xml b/bundles/org.openhab.binding.mybmw/pom.xml
new file mode 100644
index 00000000000..794a20de733
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.3.0-SNAPSHOT
+
+
+ org.openhab.binding.mybmw
+
+ openHAB Add-ons :: Bundles :: MyBMW Binding
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/feature/feature.xml b/bundles/org.openhab.binding.mybmw/src/main/feature/feature.xml
new file mode 100644
index 00000000000..983d7b548d1
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${project.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.mybmw/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConfiguration.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConfiguration.java
new file mode 100644
index 00000000000..107ce1986ff
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConfiguration.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConstants.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConstants.java
new file mode 100644
index 00000000000..b26cc0a965e
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConstants.java
@@ -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 FUEL_VEHICLES = Set.of(VehicleType.CONVENTIONAL.toString(),
+ VehicleType.PLUGIN_HYBRID.toString(), VehicleType.ELECTRIC_REX.toString());
+ public static final Set 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 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";
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWHandlerFactory.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWHandlerFactory.java
new file mode 100644
index 00000000000..902d51e069f
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWHandlerFactory.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/VehicleConfiguration.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/VehicleConfiguration.java
new file mode 100644
index 00000000000..6d8d04a8e04
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/VehicleConfiguration.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java
new file mode 100644
index 00000000000..9c6d80ccce2
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java
@@ -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 bridgeHandler = Optional.empty();
+
+ public VehicleDiscovery() {
+ super(SUPPORTED_THING_SET, DISCOVERY_TIMEOUT, false);
+ }
+
+ public void onResponse(List 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 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 convertedProperties = new HashMap(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 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 getObject(Object dto, Object compare) {
+ List l = new ArrayList();
+ 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;
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/AuthQueryResponse.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/AuthQueryResponse.java
new file mode 100644
index 00000000000..faee2f0e521
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/AuthQueryResponse.java
@@ -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 scopes;// ;": [
+ // "openid",
+ // "profile",
+ // "email",
+ // "offline_access",
+ // "smacc",
+ // "vehicle_data",
+ // "perseus",
+ // "dlm",
+ // "svds",
+ // "cesim",
+ // "vsapi",
+ // "remote_services",
+ // "fupo",
+ // "authenticate_user"
+ // ],
+ public List promptValues; // ": ["login"]
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/AuthResponse.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/AuthResponse.java
new file mode 100644
index 00000000000..82061576c16
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/AuthResponse.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaAccessToken.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaAccessToken.java
new file mode 100644
index 00000000000..2ffb191d8b0
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaAccessToken.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaPublicKey.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaPublicKey.java
new file mode 100644
index 00000000000..ff76b0a4be5
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaPublicKey.java
@@ -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"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaPublicKeyResponse.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaPublicKeyResponse.java
new file mode 100644
index 00000000000..1bb2cc9642f
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaPublicKeyResponse.java
@@ -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"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaTokenExpiration.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaTokenExpiration.java
new file mode 100644
index 00000000000..fc00a4a11ed
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaTokenExpiration.java
@@ -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}
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaTokenResponse.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaTokenResponse.java
new file mode 100644
index 00000000000..aad71b6cc43
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaTokenResponse.java
@@ -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"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeProfile.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeProfile.java
new file mode 100644
index 00000000000..4cc97222c4f
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeProfile.java
@@ -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 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;
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSession.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSession.java
new file mode 100644
index 00000000000..1a7b4afa9a6
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSession.java
@@ -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
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSessions.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSessions.java
new file mode 100644
index 00000000000..77862a3cd98
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSessions.java
@@ -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 sessions;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSessionsContainer.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSessionsContainer.java
new file mode 100644
index 00000000000..5e5649370ac
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSessionsContainer.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeStatistics.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeStatistics.java
new file mode 100644
index 00000000000..c6185308483
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeStatistics.java
@@ -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"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeStatisticsContainer.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeStatisticsContainer.java
new file mode 100644
index 00000000000..d737acbdce7
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeStatisticsContainer.java
@@ -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;// ": {
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSettings.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSettings.java
new file mode 100644
index 00000000000..cb2ea46a4f2
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSettings.java
@@ -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"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingWindow.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingWindow.java
new file mode 100644
index 00000000000..9e94bb9279e
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingWindow.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/Time.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/Time.java
new file mode 100644
index 00000000000..ebd65a943cc
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/Time.java
@@ -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);
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/Timer.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/Timer.java
new file mode 100644
index 00000000000..066da9fae73
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/Timer.java
@@ -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 timerWeekDays;
+
+ @Override
+ public String toString() {
+ return id + Constants.COLON + action + Constants.COLON + timeStamp + Constants.COLON + timerWeekDays;
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/network/NetworkError.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/network/NetworkError.java
new file mode 100644
index 00000000000..c374e71912f
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/network/NetworkError.java
@@ -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);
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Address.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Address.java
new file mode 100644
index 00000000000..343754d6d35
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Address.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/CBS.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/CBS.java
new file mode 100644
index 00000000000..c5d4744e6f5
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/CBS.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/CCM.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/CCM.java
new file mode 100644
index 00000000000..ab2517da9cc
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/CCM.java
@@ -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
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/ChargingState.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/ChargingState.java
new file mode 100644
index 00000000000..a16c217c731
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/ChargingState.java
@@ -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
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Coordinates.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Coordinates.java
new file mode 100644
index 00000000000..9e74e3e9dd7
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Coordinates.java
@@ -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
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Distance.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Distance.java
new file mode 100644
index 00000000000..966003ac632
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Distance.java
@@ -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"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Doors.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Doors.java
new file mode 100644
index 00000000000..273e98376c2
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Doors.java
@@ -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"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/DoorsWindows.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/DoorsWindows.java
new file mode 100644
index 00000000000..bccc905b618
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/DoorsWindows.java
@@ -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"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/FuelLevel.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/FuelLevel.java
new file mode 100644
index 00000000000..60c039efb0f
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/FuelLevel.java
@@ -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"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Location.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Location.java
new file mode 100644
index 00000000000..e19decfdd5a
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Location.java
@@ -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
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Properties.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Properties.java
new file mode 100644
index 00000000000..0198e40024a
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Properties.java
@@ -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 checkControlMessages;
+ public List serviceRequired;
+ public Location vehicleLocation;
+ public Tires tires;
+ // "climateControl":{} [todo] definition currently unknown
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Range.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Range.java
new file mode 100644
index 00000000000..354e6422743
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Range.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Tire.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Tire.java
new file mode 100644
index 00000000000..60aabdf76e0
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Tire.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/TireStatus.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/TireStatus.java
new file mode 100644
index 00000000000..9cdef21d562
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/TireStatus.java
@@ -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
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Tires.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Tires.java
new file mode 100644
index 00000000000..46a87be0f0a
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Tires.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Windows.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Windows.java
new file mode 100644
index 00000000000..322d3157833
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Windows.java
@@ -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"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/remote/ExecutionError.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/remote/ExecutionError.java
new file mode 100644
index 00000000000..f402c53bf84
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/remote/ExecutionError.java
@@ -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"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/remote/ExecutionStatusContainer.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/remote/ExecutionStatusContainer.java
new file mode 100644
index 00000000000..919f53a18c4
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/remote/ExecutionStatusContainer.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/CBSMessage.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/CBSMessage.java
new file mode 100644
index 00000000000..6e8cafef0f6
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/CBSMessage.java
@@ -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"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/CCMMessage.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/CCMMessage.java
new file mode 100644
index 00000000000..818f1cfd271
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/CCMMessage.java
@@ -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.
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/DoorWindow.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/DoorWindow.java
new file mode 100644
index 00000000000..37b10a13601
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/DoorWindow.java
@@ -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"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/FuelIndicator.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/FuelIndicator.java
new file mode 100644
index 00000000000..4a823baa5b1
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/FuelIndicator.java
@@ -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"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Issues.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Issues.java
new file mode 100644
index 00000000000..7b7d5fe4db1
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Issues.java
@@ -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
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Mileage.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Mileage.java
new file mode 100644
index 00000000000..1c6f9ce9adc
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Mileage.java
@@ -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"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Status.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Status.java
new file mode 100644
index 00000000000..903b4515e7b
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Status.java
@@ -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 doorsAndWindows;// ":[
+ public List checkControlMessages;//
+ public List requiredServices;//
+ // "recallMessages":[],
+ // "recallExternalUrl":null,
+ public List fuelIndicators;
+ public String timestampMessage;// ":"Updated from vehicle 12/21/2021 05:46 PM",
+ public ChargeProfile chargingProfile;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Capabilities.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Capabilities.java
new file mode 100644
index 00000000000..e2bf78236db
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Capabilities.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/RemoteService.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/RemoteService.java
new file mode 100644
index 00000000000..8ad719c35a2
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/RemoteService.java
@@ -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."
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Vehicle.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Vehicle.java
new file mode 100644
index 00000000000..292728fad0c
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Vehicle.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/ByteResponseCallback.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/ByteResponseCallback.java
new file mode 100644
index 00000000000..24a6309a5b9
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/ByteResponseCallback.java
@@ -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);
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java
new file mode 100644
index 00000000000..06c0f6b1f5a
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java
@@ -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 discoveryService = Optional.empty();
+ private Optional proxy = Optional.empty();
+ private Optional> initializerJob = Optional.empty();
+ private Optional 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 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> getServices() {
+ return Collections.singleton(VehicleDiscovery.class);
+ }
+
+ public Optional getProxy() {
+ return proxy;
+ }
+
+ public void setDiscoveryService(VehicleDiscovery discoveryService) {
+ this.discoveryService = Optional.of(discoveryService);
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWCommandOptionProvider.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWCommandOptionProvider.java
new file mode 100644
index 00000000000..d922dd3aac1
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWCommandOptionProvider.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWProxy.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWProxy.java
new file mode 100644
index 00000000000..20a4f4c5dc1
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWProxy.java
@@ -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 = 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 vehicleParams = new MultiMap();
+ 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 chargeStatisticsParams = new MultiMap();
+ 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 chargeSessionsParams = new MultiMap();
+ 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 baseParams = new MultiMap();
+ 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 loginParams = new MultiMap(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 authParams = new MultiMap(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 codeParams = new MultiMap();
+ 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 tokenMap = new MultiMap();
+ 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;
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceHandler.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceHandler.java
new file mode 100644
index 00000000000..116733e425b
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceHandler.java
@@ -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> stateJob = Optional.empty();
+ private Optional serviceExecuting = Optional.empty();
+ private Optional 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 dataMap = new MultiMap();
+ 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 dataMap = new MultiMap();
+ 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();
+ });
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/ResponseCallback.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/ResponseCallback.java
new file mode 100644
index 00000000000..a64ee4423b5
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/ResponseCallback.java
@@ -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);
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/StringResponseCallback.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/StringResponseCallback.java
new file mode 100644
index 00000000000..630a57460e5
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/StringResponseCallback.java
@@ -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);
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/Token.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/Token.java
new file mode 100644
index 00000000000..34e0cef6346
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/Token.java
@@ -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();
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleChannelHandler.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleChannelHandler.java
new file mode 100644
index 00000000000..1dec0d62e6b
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleChannelHandler.java
@@ -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 serviceList = new ArrayList();
+ protected String selectedService = Constants.UNDEF;
+ protected List checkControlList = new ArrayList();
+ protected String selectedCC = Constants.UNDEF;
+ protected List sessionList = new ArrayList();
+ protected String selectedSession = Constants.UNDEF;
+
+ protected MyBMWCommandOptionProvider commandOptionProvider;
+
+ // Data Caches
+ protected Optional vehicleStatusCache = Optional.empty();
+ protected Optional 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 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 lengthUnit = VehicleStatusUtils.getLengthUnit(v.status.fuelIndicators);
+ if (lengthUnit == null) {
+ return;
+ }
+ if (isElectric) {
+ int rangeElectric = VehicleStatusUtils.getRange(Constants.UNIT_PRECENT_JSON, v);
+ QuantityType qtElectricRange = QuantityType.valueOf(rangeElectric, lengthUnit);
+ QuantityType 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 qtFuelRange = QuantityType.valueOf(rangeFuel, lengthUnit);
+ QuantityType 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 qtHybridRange = QuantityType.valueOf(rangeCombined, lengthUnit);
+ QuantityType 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 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 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 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 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 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 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 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));
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java
new file mode 100644
index 00000000000..c7b2da83dfd
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java
@@ -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 proxy = Optional.empty();
+ private Optional remote = Optional.empty();
+ public Optional configuration = Optional.empty();
+ private Optional> refreshJob = Optional.empty();
+ private Optional> 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 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());
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/simulation/Injector.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/simulation/Injector.java
new file mode 100644
index 00000000000..c3279d52630
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/simulation/Injector.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/BimmerConstants.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/BimmerConstants.java
new file mode 100644
index 00000000000..5e19e8fef79
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/BimmerConstants.java
@@ -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 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 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 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 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";
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargeProfileUtils.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargeProfileUtils.java
new file mode 100644
index 00000000000..bd474e7de42
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargeProfileUtils.java
@@ -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 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 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 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 CHARGE_TIME_CHANNEL_KEYS = new HashMap<>() {
+ {
+ TIMED_CHANNELS.forEach((key, channel) -> {
+ put(channel.time, key);
+ });
+ }
+ };
+
+ @SuppressWarnings("serial")
+ private static final Map 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 weekdays) {
+ return weekdays.stream().map(day -> Constants.DAYS.get(day)).collect(Collectors.joining(Constants.COMMA));
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargeProfileWrapper.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargeProfileWrapper.java
new file mode 100644
index 00000000000..c734c333331
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargeProfileWrapper.java
@@ -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 mode = Optional.empty();
+ private Optional preference = Optional.empty();
+ private Optional controlType = Optional.empty();
+ private Optional chargeSettings = Optional.empty();
+
+ private final Map enabled = new HashMap<>();
+ private final Map times = new HashMap<>();
+ private final Map> 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 getDays(final ProfileKey key) {
+ return daysOfWeek.get(key);
+ }
+
+ public void setDays(final ProfileKey key, final @Nullable Set 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 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();
+ 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 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 days = daysOfWeek.get(key);
+ if (days != null) {
+ timer.timerWeekDays = new ArrayList<>();
+ for (DayOfWeek day : days) {
+ timer.timerWeekDays.add(day.name().toLowerCase());
+ }
+ }
+ return timer;
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/Constants.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/Constants.java
new file mode 100644
index 00000000000..7311bb454ac
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/Constants.java
@@ -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 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 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";
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/Converter.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/Converter.java
new file mode 100644
index 00000000000..6f8fa6d3194
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/Converter.java
@@ -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>() {
+ }.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 getVehicleList(String json) {
+ try {
+ List l = GSON.fromJson(json, VEHICLE_LIST_TYPE);
+ if (l != null) {
+ return l;
+ } else {
+ return new ArrayList();
+ }
+ } catch (JsonSyntaxException e) {
+ LOGGER.warn("JsonSyntaxException {}", e.getMessage());
+ return new ArrayList();
+ }
+ }
+
+ public static Vehicle getVehicle(String vin, String json) {
+ List 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;
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/HTTPConstants.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/HTTPConstants.java
new file mode 100644
index 00000000000..81a0e17f319
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/HTTPConstants.java
@@ -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";
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ImageProperties.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ImageProperties.java
new file mode 100644
index 00000000000..510a89ba7ac
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ImageProperties.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/RemoteServiceUtils.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/RemoteServiceUtils.java
new file mode 100644
index 00000000000..db2bf6e2ab9
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/RemoteServiceUtils.java
@@ -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 COMMAND_SERVICES = Stream.of(RemoteService.values())
+ .collect(Collectors.toUnmodifiableMap(RemoteService::getId, service -> service));
+
+ public static Optional getRemoteService(final String command) {
+ return Optional.ofNullable(COMMAND_SERVICES.get(command));
+ }
+
+ public static List getOptions(final boolean isElectric) {
+ return Stream.of(RemoteService.values()).map(service -> new CommandOption(service.getId(), service.getLabel()))
+ .collect(Collectors.toUnmodifiableList());
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/VehicleStatusUtils.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/VehicleStatusUtils.java
new file mode 100644
index 00000000000..752827efe60
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/VehicleStatusUtils.java
@@ -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 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 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 getLengthUnit(List indicators) {
+ Unit 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();
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644
index 00000000000..0f484b393e4
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,9 @@
+
+
+
+ MyBMW
+ Provides access to your Vehicle Data like MyBMW App
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/config/bridge-config.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/config/bridge-config.xml
new file mode 100644
index 00000000000..5decb677799
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/config/bridge-config.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+ Username
+ MyBMW Username
+
+
+ Password
+ MyBMW Password
+ password
+
+
+ Region
+ Select Region in order to connect to the appropriate BMW Server
+
+ North America
+ China
+ Rest of the World
+
+ ROW
+
+
+ Language Settings
+ Channel data can be returned in the desired language like en, de, fr ...
+ true
+ AUTODETECT
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/config/thing-config.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/config/thing-config.xml
new file mode 100644
index 00000000000..0b45eb09e64
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/config/thing-config.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+ Vehicle Identification Number (VIN)
+ Unique VIN given by BMW
+
+
+ Refresh Interval
+ Data refresh rate for your vehicle data
+ 5
+
+
+ Brand of the Vehicle
+ Vehicle brand like BMW or Mini
+ true
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties
new file mode 100644
index 00000000000..929220e4fa5
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties
@@ -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 fr das BMW Benutzerkonto
+
+# bridge config
+thing-type.config.mybmw.bridge.userName.label = Benutzername
+thing-type.config.mybmw.bridge.userName.description = Benutzername fr die MyBMW App
+thing-type.config.mybmw.bridge.password.label = Passwort
+thing-type.config.mybmw.bridge.password.description = Passwort fr 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 fr die gewnschte 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 Fllstnde
+channel-group-type.mybmw.hybrid-range-values.description = Tachostand, Reichweite, Ladezustand und Tankfllung fr hybride Fahrzeuge
+channel-group-type.mybmw.conv-range-values.label = Verbrenner Reichweiten und Fllstnde
+channel-group-type.mybmw.conv-range-values.description = Tachostand, Reichweite und Tankfllung des Fahrzeugs
+channel-group-type.mybmw.door-values.label = Details aller Tren
+channel-group-type.mybmw.door-values.description = Zeigt die Details der Tren 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 Ladevorgnge
+channel-group-type.mybmw.charge-statistic.label = Elektrische Ladestatistik
+channel-group-type.mybmw.charge-statistic.description = Statistik der Ladevorgnge im Monat
+channel-group-type.mybmw.session-values.label = Elektrische Ladevorgnge
+channel-group-type.mybmw.session-values.description = Liste der letzten Ladevorgnge
+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 ausgewhlten Ansicht
+
+
+
+# Channel Types
+channel-type.mybmw.doors-channel.label = Gesamtzustand der Tren
+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 = Nchster Service Termin
+channel-type.mybmw.next-service-mileage-channel.label = Nchster 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 = Fahrertr
+channel-type.mybmw.driver-rear-channel.label = Fahrertr Hinten
+channel-type.mybmw.passenger-front-channel.label = Beifahrertr
+channel-type.mybmw.passenger-rear-channel.label = Beifahrertr Hinten
+channel-type.mybmw.hood-channel.label = Frontklappe
+channel-type.mybmw.trunk-channel.label = Heckklappe
+channel-type.mybmw.window-driver-front-channel.label = Fahrertr Fenster
+channel-type.mybmw.window-driver-rear-channel.label = Fahrertr Hinten Fenster
+channel-type.mybmw.window-passenger-front-channel.label = Beifahrertr Fenster
+channel-type.mybmw.window-passenger-rear-channel.label = Beifahrertr 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 Prioritt
+
+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 = Ladeverzgerung
+channel-type.mybmw.profile-prefs-channel.label = Ladeprofil Prferenz
+channel-type.mybmw.profile-prefs-channel.command.option.noPreSelection = Keine Prferenz
+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 = Erwnschter 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 Ausfhren
+channel-type.mybmw.remote-command-channel.command.option.vehicle-finder = Fahrzeug Lokalisieren
+channel-type.mybmw.remote-command-channel.command.option.door-lock = Fahrzeug Abschlieen
+channel-type.mybmw.remote-command-channel.command.option.door-unlock = Fahrzug Aufschlieen
+channel-type.mybmw.remote-command-channel.command.option.horn-blow = Hupe Aktivieren
+channel-type.mybmw.remote-command-channel.command.option.climate-now-start = Klimatisierung Ausfhren
+channel-type.mybmw.remote-command-channel.command.option.climate-now-stop = Klimatisierung Beenden
+channel-type.mybmw.remote-state-channel.label = Ausfhrungszustand
+
+# 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 = Ladevorgnge Monat
+channel-type.mybmw.statistic-sessions-channel.description = Anzahl der Ladevorgnge 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
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/bridge-connected-drive.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/bridge-connected-drive.xml
new file mode 100644
index 00000000000..7363192d6f2
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/bridge-connected-drive.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ MyBMW Account
+ Your BMW account data
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/charge-statistics-channel-groups.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/charge-statistics-channel-groups.xml
new file mode 100644
index 00000000000..caa0b67bb56
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/charge-statistics-channel-groups.xml
@@ -0,0 +1,15 @@
+
+
+
+ Charging Statistics
+ Charging statistics of current month
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/charge-statistics-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/charge-statistics-channel-types.xml
new file mode 100644
index 00000000000..806bcb0762e
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/charge-statistics-channel-types.xml
@@ -0,0 +1,21 @@
+
+
+
+ String
+ Charge Statistic Month
+
+
+ Number:Energy
+ Energy Charged
+ Total energy charged in current month
+
+
+
+ Number
+ Charge Sessions
+ Number of charging sessions this month
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/check-control-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/check-control-channel-types.xml
new file mode 100644
index 00000000000..48247d51543
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/check-control-channel-types.xml
@@ -0,0 +1,18 @@
+
+
+
+ String
+ CheckControl Description
+
+
+ String
+ CheckControl Details
+
+
+ String
+ Severity Level
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/check-control-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/check-control-group.xml
new file mode 100644
index 00000000000..574a6ab1868
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/check-control-group.xml
@@ -0,0 +1,15 @@
+
+
+
+ Check Control Messages
+ Shows current active CheckControl messages
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/conv-range-channel-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/conv-range-channel-group.xml
new file mode 100644
index 00000000000..c3a79423fa0
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/conv-range-channel-group.xml
@@ -0,0 +1,16 @@
+
+
+
+ Range and Fuel Data
+ Provides Mileage, remaining range and fuel level values
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/door-status-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/door-status-channel-types.xml
new file mode 100644
index 00000000000..92247efe8a8
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/door-status-channel-types.xml
@@ -0,0 +1,61 @@
+
+
+
+ String
+ Driver Door
+
+
+
+ String
+ Driver Door Rear
+
+
+
+ String
+ Passenger Door
+
+
+
+ String
+ Passenger Door Rear
+
+
+
+ String
+ Hood
+
+
+
+ String
+ Trunk
+
+
+
+ String
+ Driver Window
+
+
+
+ String
+ Driver Rear Window
+
+
+
+ String
+ Passenger Window
+
+
+
+ String
+ Passenger Rear Window
+
+
+
+ String
+ Sunroof
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/doors-status-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/doors-status-group.xml
new file mode 100644
index 00000000000..d508188e19d
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/doors-status-group.xml
@@ -0,0 +1,23 @@
+
+
+
+ Detailed Door Status
+ Detailed Status of all Doors and Windows
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/ev-range-channel-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/ev-range-channel-group.xml
new file mode 100644
index 00000000000..046125ebfbe
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/ev-range-channel-group.xml
@@ -0,0 +1,16 @@
+
+
+
+ Range and Charge Data
+ Provides Mileage, remaining range and charge level values
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/ev-vehicle-status-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/ev-vehicle-status-group.xml
new file mode 100644
index 00000000000..39d9ca4e8fb
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/ev-vehicle-status-group.xml
@@ -0,0 +1,24 @@
+
+
+
+ Vehicle Status
+ Overall vehicle status
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/hybrid-range-channel-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/hybrid-range-channel-group.xml
new file mode 100644
index 00000000000..59b413e767e
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/hybrid-range-channel-group.xml
@@ -0,0 +1,21 @@
+
+
+
+ Range, Charge / Fuel Data
+ >Provides mileage, remaining fuel and range data for hybrid vehicles
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/image-channel-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/image-channel-group.xml
new file mode 100644
index 00000000000..944c3adf8c8
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/image-channel-group.xml
@@ -0,0 +1,14 @@
+
+
+
+ Vehicle Image
+ Provides an image of your vehicle
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/image-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/image-channel-types.xml
new file mode 100644
index 00000000000..b758b425ac8
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/image-channel-types.xml
@@ -0,0 +1,23 @@
+
+
+
+ Image
+ Rendered Vehicle Image
+
+
+
+ String
+ Image Viewport
+
+
+ Front Side View
+ Front View
+ Side View
+ Default View
+
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/location-channel-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/location-channel-group.xml
new file mode 100644
index 00000000000..46cbbcf7156
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/location-channel-group.xml
@@ -0,0 +1,15 @@
+
+
+
+ Vehicle Location
+ Coordinates and heading of the vehicle
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/location-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/location-channel-types.xml
new file mode 100644
index 00000000000..04fe7b52212
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/location-channel-types.xml
@@ -0,0 +1,19 @@
+
+
+
+ Location
+ GPS Coordinates
+
+
+ Number:Angle
+ Heading Angle
+
+
+
+ String
+ Address
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/profile-channel-groups.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/profile-channel-groups.xml
new file mode 100644
index 00000000000..0de16d98d60
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/profile-channel-groups.xml
@@ -0,0 +1,56 @@
+
+
+
+ Electric Charging Profile
+ Scheduled charging profiles
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/profile-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/profile-channel-types.xml
new file mode 100644
index 00000000000..c677f62056e
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/profile-channel-types.xml
@@ -0,0 +1,248 @@
+
+
+
+ Switch
+ A/C at Departure Time
+
+
+ String
+ Charge Mode
+ Mode for selecting immediate or delyed charging
+
+
+ Immediate Charging
+ Use Charging Preference
+
+
+
+
+ String
+ Charge Preferences
+ Preferences for delayed charging
+
+
+ No Selection
+ Charging Window
+
+
+
+
+ String
+ Charging Plan
+ Charging plan selection
+
+
+ Weekly Schedule
+
+
+
+
+ Number
+ SOC Target
+ SOC charging target
+
+
+ Switch
+ Charging Energy Limited
+ Limited charging activated
+
+
+ DateTime
+ Window Start Time
+ Start time of charging window
+
+
+
+ DateTime
+ Window End Time
+ End time of charging window
+
+
+
+ DateTime
+ T1 Departure Time
+ Departure time for regular schedule timer 1
+
+
+
+ Switch
+ T1 Monday
+ Monday scheduled for timer 1
+
+
+ Switch
+ T1 Tuesday
+ Tuesday scheduled for timer 1
+
+
+ Switch
+ T1 Wednesday
+ Wednesday scheduled for timer 1
+
+
+ Switch
+ T1 Thursday
+ Thursday scheduled for timer 1
+
+
+ Switch
+ T1 Friday
+ Friday scheduled for timer 1
+
+
+ Switch
+ T1 Saturday
+ Saturday scheduled for timer 1
+
+
+ Switch
+ T1 Sunday
+ Sunday scheduled for timer 1
+
+
+ Switch
+ T1 Enabled
+ Timer 1 enabled
+
+
+ DateTime
+ T2 Departure Time
+ Departure time for regular schedule timer 2
+
+
+
+ Switch
+ T2 Monday
+ Monday scheduled for timer 2
+
+
+ Switch
+ T2 Tuesday
+ Tuesday scheduled for timer 2
+
+
+ Switch
+ T2 Wednesday
+ Wednesday scheduled for timer 2
+
+
+ Switch
+ T2 Thursday
+ Thursday scheduled for timer 2
+
+
+ Switch
+ T2 Friday
+ Friday scheduled for timer 2
+
+
+ Switch
+ T2 Saturday
+ Saturday scheduled for timer 2
+
+
+ Switch
+ T2 Sunday
+ Sunday scheduled for timer 2
+
+
+ Switch
+ T2 Enabled
+ Timer 2 enabled
+
+
+ DateTime
+ T3 Departure Time
+ Departure time for regular schedule timer 3
+
+
+
+ Switch
+ T3 Monday
+ Monday scheduled for timer 3
+
+
+ Switch
+ T3 Tuesday
+ Tuesday scheduled for timer 3
+
+
+ Switch
+ T3 Wednesday
+ Wednesday scheduled for timer 3
+
+
+ Switch
+ T3 Thursday
+ Thursday scheduled for timer 3
+
+
+ Switch
+ T3 Friday
+ Friday scheduled for timer 3
+
+
+ Switch
+ T3 Saturday
+ Saturday scheduled for timer 3
+
+
+ Switch
+ T3 Sunday
+ Sunday scheduled for timer 3
+
+
+ Switch
+ T3 Enabled
+ Timer 3 enabled
+
+
+ DateTime
+ T4 Departure Time
+ Departure time for regular schedule timer 4
+
+
+
+ Switch
+ T4 Monday
+ Monday scheduled for timer 4
+
+
+ Switch
+ T4 Tuesday
+ Tuesday scheduled for timer 4
+
+
+ Switch
+ T4 Wednesday
+ Wednesday scheduled for timer 4
+
+
+ Switch
+ T4 Thursday
+ Thursday scheduled for timer 4
+
+
+ Switch
+ T4 Friday
+ Friday scheduled for timer 4
+
+
+ Switch
+ T4 Saturday
+ Saturday scheduled for timer 4
+
+
+ Switch
+ T4 Sunday
+ Sunday scheduled for timer 4
+
+
+ Switch
+ T4 Enabled
+ Timer 4 enabled
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/range-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/range-channel-types.xml
new file mode 100644
index 00000000000..7bcfadde4ac
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/range-channel-types.xml
@@ -0,0 +1,51 @@
+
+
+
+ Number:Length
+ Total Distance Driven
+
+
+
+ Number:Length
+ Electric Range
+
+
+
+ Number:Length
+ Fuel Range
+
+
+
+ Number:Length
+ Hybrid Range
+
+
+
+ Number:Dimensionless
+ Battery Charge Level
+
+
+
+ Number:Volume
+ Remaining Fuel
+
+
+
+ Number:Length
+ Electric Range Radius
+
+
+
+ Number:Length
+ Fuel Range Radius
+
+
+
+ Number:Length
+ Hybrid Range Radius
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/remote-services-channel-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/remote-services-channel-group.xml
new file mode 100644
index 00000000000..00799962019
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/remote-services-channel-group.xml
@@ -0,0 +1,14 @@
+
+
+
+ Remote Services
+ Remote control of the vehicle
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/remote-services-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/remote-services-channel-types.xml
new file mode 100644
index 00000000000..b8dd159603b
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/remote-services-channel-types.xml
@@ -0,0 +1,15 @@
+
+
+
+ String
+ Remote Command
+
+
+ String
+ Service Execution State
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/service-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/service-channel-types.xml
new file mode 100644
index 00000000000..827dd8a808b
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/service-channel-types.xml
@@ -0,0 +1,24 @@
+
+
+
+ String
+ Service Name
+
+
+ String
+ Service Details
+
+
+ DateTime
+ Service Date
+
+
+
+ Number:Length
+ Mileage till Service
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/service-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/service-group.xml
new file mode 100644
index 00000000000..1aea1e8566c
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/service-group.xml
@@ -0,0 +1,16 @@
+
+
+
+ Vehicle Services
+ Future vehicle service schedules
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/session-channel-groups.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/session-channel-groups.xml
new file mode 100644
index 00000000000..2f02d153ece
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/session-channel-groups.xml
@@ -0,0 +1,17 @@
+
+
+
+ Electric Charging Sessions
+ Past charging sessions
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/session-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/session-channel-types.xml
new file mode 100644
index 00000000000..6df170c39b6
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/session-channel-types.xml
@@ -0,0 +1,26 @@
+
+
+
+ String
+ Session Title
+
+
+ String
+ Session Details
+
+
+ String
+ Charged Energy in Session
+
+
+ String
+ Issues during Session
+
+
+ String
+ Session Status
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-bev.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-bev.xml
new file mode 100644
index 00000000000..593b9f516cc
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-bev.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+ Electric Vehicle
+ Battery Electric Vehicle (BEV)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ vin
+
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-bev_rex.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-bev_rex.xml
new file mode 100644
index 00000000000..5e45f36d971
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-bev_rex.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+ Electric Vehicle with REX
+ Battery Electric Vehicle with Range Extender (BEV_REX)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ vin
+
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-conv.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-conv.xml
new file mode 100644
index 00000000000..b95df69f01b
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-conv.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+ Conventional Vehicle
+ Conventional Fuel Vehicle (CONV)
+
+
+
+
+
+
+
+
+
+
+
+
+
+ vin
+
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-phev.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-phev.xml
new file mode 100644
index 00000000000..35be350bfa5
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-phev.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+ Plug-In-Hybrid Electric Vehicle
+ Conventional Fuel Vehicle with supporting Electric Engine (PHEV)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ vin
+
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/tires-channel-groups.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/tires-channel-groups.xml
new file mode 100644
index 00000000000..cb2a0161553
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/tires-channel-groups.xml
@@ -0,0 +1,20 @@
+
+
+
+ Tire Pressure
+ Current and wanted pressure for all tires
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/tires-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/tires-channel-types.xml
new file mode 100644
index 00000000000..e403c049faf
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/tires-channel-types.xml
@@ -0,0 +1,46 @@
+
+
+
+ Number:Pressure
+ Tire Pressure Front Left
+
+
+
+ Number:Pressure
+ Tire Pressure Front Left Target
+
+
+
+ Number:Pressure
+ Tire Pressure Front Right
+
+
+
+ Number:Pressure
+ Tire Pressure Front Right Target
+
+
+
+ Number:Pressure
+ Tire Pressure Rear Left
+
+
+
+ Number:Pressure
+ Tire Pressure Rear Left Target
+
+
+
+ Number:Pressure
+ Tire Pressure Rear Right
+
+
+
+ Number:Pressure
+ Tire Pressure Rear Right Target
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml
new file mode 100644
index 00000000000..72087c0ad73
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml
@@ -0,0 +1,64 @@
+
+
+
+ String
+ Overall Door Status
+
+
+
+ String
+ Overall Window Status
+
+
+
+ String
+ Doors Locked
+
+
+ DateTime
+ Next Service Date
+
+
+
+ Number:Length
+ Mileage Till Next Service
+
+
+
+ String
+ Check Control
+
+
+
+ String
+ Charging Status
+
+
+
+ String
+ Charging Information
+
+
+
+ String
+ Plug Connection Status
+
+
+
+ Switch
+ Motion Status
+
+
+
+ DateTime
+ Last Status Timestamp
+
+
+
+ String
+ Raw Data
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-group.xml
new file mode 100644
index 00000000000..0c3982aa689
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-group.xml
@@ -0,0 +1,21 @@
+
+
+
+ Vehicle Status
+ Overall vehicle status
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/discovery/DiscoveryTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/discovery/DiscoveryTest.java
new file mode 100644
index 00000000000..0840c863e62
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/discovery/DiscoveryTest.java
@@ -0,0 +1,116 @@
+/**
+ * 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.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
+import org.openhab.binding.mybmw.internal.handler.MyBMWBridgeHandler;
+import org.openhab.binding.mybmw.internal.util.FileReader;
+import org.openhab.binding.mybmw.internal.utils.Constants;
+import org.openhab.binding.mybmw.internal.utils.Converter;
+import org.openhab.core.config.discovery.DiscoveryListener;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ThingUID;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+/**
+ * The {@link DiscoveryTest} Test Discovery Results
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class DiscoveryTest {
+
+ @Test
+ public void testDiscovery() {
+ String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/vehicles.json");
+ Bridge b = mock(Bridge.class);
+ MyBMWBridgeHandler bh = new MyBMWBridgeHandler(b, mock(HttpClientFactory.class), "en");
+ when(b.getUID()).thenReturn(new ThingUID("mybmw", "account", "abc"));
+ VehicleDiscovery discovery = new VehicleDiscovery();
+ discovery.setThingHandler(bh);
+ DiscoveryListener listener = mock(DiscoveryListener.class);
+ discovery.addDiscoveryListener(listener);
+ List vl = Converter.getVehicleList(content);
+ assertEquals(1, vl.size(), "Vehicles found");
+ ArgumentCaptor discoveries = ArgumentCaptor.forClass(DiscoveryResult.class);
+ ArgumentCaptor services = ArgumentCaptor.forClass(DiscoveryService.class);
+ bh.onResponse(content);
+ verify(listener, times(1)).thingDiscovered(services.capture(), discoveries.capture());
+ List results = discoveries.getAllValues();
+ assertEquals(1, results.size(), "Found Vehicles");
+ DiscoveryResult result = results.get(0);
+ assertEquals("mybmw:bev_rex:abc:anonymous", result.getThingUID().getAsString(), "Thing UID");
+ }
+
+ @Test
+ public void testProperties() {
+ String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/vehicles.json");
+ Vehicle vehicle = Converter.getVehicle(Constants.ANONYMOUS, content);
+ String servicesSuppoertedReference = "RemoteHistory;ChargingHistory;ScanAndCharge;DCSContractManagement;BmwCharging;ChargeNowForBusiness;ChargingPlan";
+ String servicesUnsuppoertedReference = "MiniCharging;EvGoCharging;CustomerEsim;CarSharing;EasyCharge";
+ String servicesEnabledReference = "FindCharging;";
+ String servicesDisabledReference = "DataPrivacy;ChargingSettings;ChargingHospitality;ChargingPowerLimit;ChargingTargetSoc;ChargingLoudness";
+ assertEquals(servicesSuppoertedReference,
+ VehicleDiscovery.getServices(vehicle, VehicleDiscovery.SUPPORTED_SUFFIX, true), "Services supported");
+ assertEquals(servicesUnsuppoertedReference,
+ VehicleDiscovery.getServices(vehicle, VehicleDiscovery.SUPPORTED_SUFFIX, false),
+ "Services unsupported");
+
+ String servicesEnabled = VehicleDiscovery.getServices(vehicle, VehicleDiscovery.ENABLED_SUFFIX, true)
+ + Constants.SEMICOLON + VehicleDiscovery.getServices(vehicle, VehicleDiscovery.ENABLE_SUFFIX, true);
+ assertEquals(servicesEnabledReference, servicesEnabled, "Services enabled");
+ String servicesDisabled = VehicleDiscovery.getServices(vehicle, VehicleDiscovery.ENABLED_SUFFIX, false)
+ + Constants.SEMICOLON + VehicleDiscovery.getServices(vehicle, VehicleDiscovery.ENABLE_SUFFIX, false);
+ assertEquals(servicesDisabledReference, servicesDisabled, "Services disabled");
+ }
+
+ @Test
+ public void testAnonymousFingerPrint() {
+ String content = FileReader.readFileInString("src/test/resources/responses/fingerprint-raw.json");
+ String anonymous = Converter.anonymousFingerprint(content);
+ assertFalse(anonymous.contains("ABC45678"), "VIN deleted");
+
+ anonymous = Converter.anonymousFingerprint(Constants.EMPTY);
+ assertEquals(Constants.EMPTY, anonymous, "Equal Fingerprint if Empty");
+
+ anonymous = Converter.anonymousFingerprint(Constants.EMPTY_JSON);
+ assertEquals(Constants.EMPTY_JSON, anonymous, "Equal Fingerprint if Empty JSon");
+ }
+
+ @Test
+ public void testRawVehicleData() {
+ String content = FileReader.readFileInString("src/test/resources/responses/TwoVehicles/two-vehicles.json");
+ String anonymousVehicle = Converter.getRawVehicleContent("anonymous", content);
+ String contentAnon = FileReader.readFileInString("src/test/resources/responses/TwoVehicles/anonymous-raw.json");
+ // remove formatting
+ JsonObject jo = JsonParser.parseString(contentAnon).getAsJsonObject();
+ assertEquals(jo.toString(), anonymousVehicle, "Anonymous VIN raw data");
+ String contentF11 = FileReader.readFileInString("src/test/resources/responses/TwoVehicles/f11-raw.json");
+ String f11Vehicle = Converter.getRawVehicleContent("some_vin_F11", content);
+ jo = JsonParser.parseString(contentF11).getAsJsonObject();
+ assertEquals(jo.toString(), f11Vehicle, "F11 VIN raw data");
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/ChargeProfileTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/ChargeProfileTest.java
new file mode 100644
index 00000000000..7cb52adf21f
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/ChargeProfileTest.java
@@ -0,0 +1,54 @@
+/**
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mybmw.internal.dto.charge.ChargeProfile;
+import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
+import org.openhab.binding.mybmw.internal.util.FileReader;
+import org.openhab.binding.mybmw.internal.utils.ChargeProfileWrapper;
+import org.openhab.binding.mybmw.internal.utils.Constants;
+import org.openhab.binding.mybmw.internal.utils.Converter;
+
+/**
+ * The {@link ChargeProfileTest} is testing locale settings
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class ChargeProfileTest {
+
+ @Test
+ public void testWeeklyPlanner() {
+ String json = FileReader
+ .readFileInString("src/test/resources/responses/chargingprofile/weekly-planner-t2-active.json");
+ Vehicle v = Converter.getVehicle(Constants.ANONYMOUS, json);
+ ChargeProfile cp = v.status.chargingProfile;
+ String cpJson = Converter.getGson().toJson(cp);
+ ChargeProfileWrapper cpw = new ChargeProfileWrapper(v.status.chargingProfile);
+ assertEquals(cpJson, cpw.getJson(), "JSON comparison");
+ }
+
+ @Test
+ public void testTwoWeeksPlanner() {
+ String json = FileReader.readFileInString("src/test/resources/responses/chargingprofile/two-weeks-timer.json");
+ Vehicle v = Converter.getVehicle(Constants.ANONYMOUS, json);
+ ChargeProfile cp = v.status.chargingProfile;
+ String cpJson = Converter.getGson().toJson(cp);
+ ChargeProfileWrapper cpw = new ChargeProfileWrapper(v.status.chargingProfile);
+ assertEquals(cpJson, cpw.getJson(), "JSON comparison");
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/ChargeStatisticWrapper.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/ChargeStatisticWrapper.java
new file mode 100644
index 00000000000..3dbde114aa6
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/ChargeStatisticWrapper.java
@@ -0,0 +1,107 @@
+/**
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.openhab.binding.mybmw.internal.MyBMWConstants.*;
+
+import java.util.List;
+
+import javax.measure.quantity.Energy;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mybmw.internal.dto.charge.ChargeStatisticsContainer;
+import org.openhab.binding.mybmw.internal.utils.Converter;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.types.State;
+
+/**
+ * The {@link ChargeStatisticWrapper} tests stored fingerprint responses from BMW API
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class ChargeStatisticWrapper {
+ private ChargeStatisticsContainer chargeStatisticContainer;
+
+ public ChargeStatisticWrapper(String content) {
+ ChargeStatisticsContainer fromJson = Converter.getGson().fromJson(content, ChargeStatisticsContainer.class);
+ if (fromJson != null) {
+ chargeStatisticContainer = fromJson;
+ } else {
+ chargeStatisticContainer = new ChargeStatisticsContainer();
+ }
+ }
+
+ /**
+ * Test results auctomatically against json values
+ *
+ * @param channels
+ * @param states
+ * @return
+ */
+ public boolean checkResults(@Nullable List channels, @Nullable List states) {
+ assertNotNull(channels);
+ assertNotNull(states);
+ assertTrue(channels.size() == states.size(), "Same list sizes");
+ for (int i = 0; i < channels.size(); i++) {
+ checkResult(channels.get(i), states.get(i));
+ }
+ return true;
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ private void checkResult(ChannelUID channelUID, State state) {
+ String cUid = channelUID.getIdWithoutGroup();
+ String gUid = channelUID.getGroupId();
+ StringType st;
+ DecimalType dt;
+ QuantityType qte;
+ switch (cUid) {
+ case TITLE:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ switch (gUid) {
+ case CHANNEL_GROUP_CHARGE_STATISTICS:
+ assertEquals(chargeStatisticContainer.description, st.toString(), "Statistics name");
+ break;
+ default:
+ assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+ break;
+ }
+ break;
+ case SESSIONS:
+ assertTrue(state instanceof DecimalType);
+ dt = ((DecimalType) state);
+ assertEquals(chargeStatisticContainer.statistics.numberOfChargingSessions, dt.intValue(),
+ "Charge Sessions");
+ break;
+ case ENERGY:
+ assertTrue(state instanceof QuantityType);
+ qte = ((QuantityType) state);
+ assertEquals(Units.KILOWATT_HOUR, qte.getUnit(), "kwh");
+ assertEquals(chargeStatisticContainer.statistics.totalEnergyCharged, qte.intValue(), "Energy");
+ break;
+ default:
+ // fail in case of unknown update
+ assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+ break;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/StatusWrapper.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/StatusWrapper.java
new file mode 100644
index 00000000000..dabcd71ee1c
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/StatusWrapper.java
@@ -0,0 +1,587 @@
+/**
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.openhab.binding.mybmw.internal.MyBMWConstants.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+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.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.VehicleStatusUtils;
+import org.openhab.core.library.types.DateTimeType;
+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.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link StatusWrapper} tests stored fingerprint responses from BMW API
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class StatusWrapper {
+ private static final Unit KILOMETRE = Constants.KILOMETRE_UNIT;
+
+ private Vehicle vehicle;
+ private boolean isElectric;
+ private boolean hasFuel;
+ private boolean isHybrid;
+
+ private Map specialHandlingMap = new HashMap();
+
+ public StatusWrapper(String type, String statusJson) {
+ 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;
+ List vl = Converter.getVehicleList(statusJson);
+ assertEquals(1, vl.size(), "Vehciles found");
+ vehicle = Converter.getConsistentVehcile(vl.get(0));
+ }
+
+ /**
+ * Test results auctomatically against json values
+ *
+ * @param channels
+ * @param states
+ * @return
+ */
+ public boolean checkResults(@Nullable List channels, @Nullable List states) {
+ assertNotNull(channels);
+ assertNotNull(states);
+ assertTrue(channels.size() == states.size(), "Same list sizes");
+ for (int i = 0; i < channels.size(); i++) {
+ checkResult(channels.get(i), states.get(i));
+ }
+ return true;
+ }
+
+ /**
+ * Add a specific check for a value e.g. hard coded "Upcoming Service" in order to check the right ordering
+ *
+ * @param specialHand
+ * @return
+ */
+ public StatusWrapper append(Map compareMap) {
+ specialHandlingMap.putAll(compareMap);
+ return this;
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ private void checkResult(ChannelUID channelUID, State state) {
+ String cUid = channelUID.getIdWithoutGroup();
+ String gUid = channelUID.getGroupId();
+ QuantityType qt;
+ StringType st;
+ StringType wanted;
+ DateTimeType dtt;
+ PointType pt;
+ OnOffType oot;
+ Unit wantedUnit;
+ switch (cUid) {
+ case MILEAGE:
+ switch (gUid) {
+ case CHANNEL_GROUP_RANGE:
+ if (!state.equals(UnDefType.UNDEF)) {
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ if (Constants.KM_JSON.equals(vehicle.status.currentMileage.units)) {
+ assertEquals(KILOMETRE, qt.getUnit(), "KM");
+ } else {
+ assertEquals(ImperialUnits.MILE, qt.getUnit(), "Miles");
+ }
+ assertEquals(qt.intValue(), vehicle.status.currentMileage.mileage, "Mileage");
+ } else {
+ assertEquals(Constants.INT_UNDEF, vehicle.status.currentMileage.mileage,
+ "Mileage undefined");
+ }
+ break;
+ case CHANNEL_GROUP_SERVICE:
+ State wantedMileage = QuantityType.valueOf(Constants.INT_UNDEF, Constants.KILOMETRE_UNIT);
+ if (!vehicle.properties.serviceRequired.isEmpty()) {
+ if (vehicle.properties.serviceRequired.get(0).distance != null) {
+ if (vehicle.properties.serviceRequired.get(0).distance.units
+ .equals(Constants.KILOMETERS_JSON)) {
+ wantedMileage = QuantityType.valueOf(
+ vehicle.properties.serviceRequired.get(0).distance.value,
+ Constants.KILOMETRE_UNIT);
+ } else {
+ wantedMileage = QuantityType.valueOf(
+ vehicle.properties.serviceRequired.get(0).distance.value,
+ ImperialUnits.MILE);
+ }
+ }
+ }
+ assertEquals(wantedMileage, state, "Service Mileage");
+ break;
+ default:
+ assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+ break;
+ }
+ break;
+ case RANGE_ELECTRIC:
+ assertTrue(isElectric, "Is Electric");
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ wantedUnit = VehicleStatusUtils.getLengthUnit(vehicle.status.fuelIndicators);
+ assertEquals(wantedUnit, qt.getUnit());
+ assertEquals(VehicleStatusUtils.getRange(Constants.UNIT_PRECENT_JSON, vehicle), qt.intValue(),
+ "Range Electric");
+ break;
+ case RANGE_FUEL:
+ assertTrue(hasFuel, "Has Fuel");
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ wantedUnit = VehicleStatusUtils.getLengthUnit(vehicle.status.fuelIndicators);
+ assertEquals(wantedUnit, qt.getUnit());
+ assertEquals(VehicleStatusUtils.getRange(Constants.UNIT_LITER_JSON, vehicle), qt.intValue(),
+ "Range Combustion");
+ break;
+ case RANGE_HYBRID:
+ assertTrue(isHybrid, "Is Hybrid");
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ wantedUnit = VehicleStatusUtils.getLengthUnit(vehicle.status.fuelIndicators);
+ assertEquals(wantedUnit, qt.getUnit());
+ assertEquals(VehicleStatusUtils.getRange(Constants.PHEV, vehicle), qt.intValue(), "Range Combined");
+ break;
+ case REMAINING_FUEL:
+ assertTrue(hasFuel, "Has Fuel");
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ assertEquals(Units.LITRE, qt.getUnit(), "Liter Unit");
+ assertEquals(vehicle.properties.fuelLevel.value, qt.intValue(), "Fuel Level");
+ break;
+ case SOC:
+ assertTrue(isElectric, "Is Ee wantedQt = (QuantityType) VehicleStatusUtils
+ .getNextServiceMileage(vehicle.properties.serviceRequired);
+ assertEquals(wantedQt.getUnit(), qt.getUnit(), "Next Service Miles");
+ assertEquals(wantedQt.intValue(), qt.intValue(), "Mileage");
+ } else if (gUid.equals(CHANNEL_GROUP_SERVICE)) {
+ assertEquals(vehicle.properties.serviceRequired.get(0).distance.units, qt.getUnit(),
+ "First Service Unit");
+ assertEquals(vehicle.properties.serviceRequired.get(0).distance.value, qt.intValue(),
+ "First Service Mileage");
+ }
+ }
+ break;
+ case NAME:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ switch (gUid) {
+ case CHANNEL_GROUP_SERVICE:
+ wanted = StringType.valueOf(Constants.NO_ENTRIES);
+ if (!vehicle.properties.serviceRequired.isEmpty()) {
+ wanted = StringType
+ .valueOf(Converter.toTitleCase(vehicle.properties.serviceRequired.get(0).type));
+ }
+ assertEquals(wanted.toString(), st.toString(), "Service Name");
+ break;
+ case CHANNEL_GROUP_CHECK_CONTROL:
+ wanted = StringType.valueOf(Constants.NO_ENTRIES);
+ if (!vehicle.status.checkControlMessages.isEmpty()) {
+ wanted = StringType.valueOf(vehicle.status.checkControlMessages.get(0).title);
+ }
+ assertEquals(wanted.toString(), st.toString(), "CheckControl Name");
+ break;
+ default:
+ assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+ break;
+ }
+ break;
+ case DETAILS:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ switch (gUid) {
+ case CHANNEL_GROUP_SERVICE:
+ wanted = StringType.valueOf(Converter.toTitleCase(Constants.NO_ENTRIES));
+ if (!vehicle.properties.serviceRequired.isEmpty()) {
+ wanted = StringType
+ .valueOf(Converter.toTitleCase(vehicle.properties.serviceRequired.get(0).type));
+ }
+ assertEquals(wanted.toString(), st.toString(), "Service Details");
+ break;
+ case CHANNEL_GROUP_CHECK_CONTROL:
+ wanted = StringType.valueOf(Constants.NO_ENTRIES);
+ if (!vehicle.status.checkControlMessages.isEmpty()) {
+ wanted = StringType.valueOf(vehicle.status.checkControlMessages.get(0).longDescription);
+ }
+ assertEquals(wanted.toString(), st.toString(), "CheckControl Details");
+ break;
+ default:
+ assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+ break;
+ }
+ break;
+ case SEVERITY:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ wanted = StringType.valueOf(Constants.NO_ENTRIES);
+ if (!vehicle.status.checkControlMessages.isEmpty()) {
+ wanted = StringType.valueOf(vehicle.status.checkControlMessages.get(0).state);
+ }
+ assertEquals(wanted.toString(), st.toString(), "CheckControl Details");
+ break;
+ case DATE:
+ if (state.equals(UnDefType.UNDEF)) {
+ for (CBS serviceEntry : vehicle.properties.serviceRequired) {
+ assertTrue(serviceEntry.dateTime == null, "No Service Date available");
+ }
+ } else {
+ assertTrue(state instanceof DateTimeType);
+ dtt = (DateTimeType) state;
+ switch (gUid) {
+ case CHANNEL_GROUP_SERVICE:
+ String dueDateString = vehicle.properties.serviceRequired.get(0).dateTime;
+ DateTimeType expectedDTT = DateTimeType
+ .valueOf(Converter.zonedToLocalDateTime(dueDateString));
+ assertEquals(expectedDTT.toString(), dtt.toString(), "ServiceSate");
+ break;
+ default:
+ assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+ break;
+ }
+ }
+ break;
+ case FRONT_LEFT_CURRENT:
+ if (vehicle.properties.tires != null) {
+ assertTrue(state instanceof QuantityType);
+ qt = (QuantityType) state;
+ assertEquals(vehicle.properties.tires.frontLeft.status.currentPressure / 100, qt.doubleValue(),
+ "Fron Left Current");
+ } else {
+ assertTrue(state.equals(UnDefType.UNDEF));
+ }
+ break;
+ case FRONT_LEFT_TARGET:
+ if (vehicle.properties.tires != null) {
+ assertTrue(state instanceof QuantityType);
+ qt = (QuantityType) state;
+ assertEquals(vehicle.properties.tires.frontLeft.status.targetPressure / 100, qt.doubleValue(),
+ "Fron Left Current");
+ } else {
+ assertTrue(state.equals(UnDefType.UNDEF));
+ }
+ break;
+ case FRONT_RIGHT_CURRENT:
+ if (vehicle.properties.tires != null) {
+ assertTrue(state instanceof QuantityType);
+ qt = (QuantityType) state;
+ assertEquals(vehicle.properties.tires.frontRight.status.currentPressure / 100, qt.doubleValue(),
+ "Fron Left Current");
+ } else {
+ assertTrue(state.equals(UnDefType.UNDEF));
+ }
+ break;
+ case FRONT_RIGHT_TARGET:
+ if (vehicle.properties.tires != null) {
+ assertTrue(state instanceof QuantityType);
+ qt = (QuantityType) state;
+ assertEquals(vehicle.properties.tires.frontRight.status.targetPressure / 100, qt.doubleValue(),
+ "Fron Left Current");
+ } else {
+ assertTrue(state.equals(UnDefType.UNDEF));
+ }
+ break;
+ case REAR_LEFT_CURRENT:
+ if (vehicle.properties.tires != null) {
+ assertTrue(state instanceof QuantityType);
+ qt = (QuantityType) state;
+ assertEquals(vehicle.properties.tires.rearLeft.status.currentPressure / 100, qt.doubleValue(),
+ "Fron Left Current");
+ } else {
+ assertTrue(state.equals(UnDefType.UNDEF));
+ }
+ break;
+ case REAR_LEFT_TARGET:
+ if (vehicle.properties.tires != null) {
+ assertTrue(state instanceof QuantityType);
+ qt = (QuantityType) state;
+ assertEquals(vehicle.properties.tires.rearLeft.status.targetPressure / 100, qt.doubleValue(),
+ "Fron Left Current");
+ } else {
+ assertTrue(state.equals(UnDefType.UNDEF));
+ }
+ break;
+ case REAR_RIGHT_CURRENT:
+ if (vehicle.properties.tires != null) {
+ assertTrue(state instanceof QuantityType);
+ qt = (QuantityType) state;
+ assertEquals(vehicle.properties.tires.rearRight.status.currentPressure / 100, qt.doubleValue(),
+ "Fron Left Current");
+ } else {
+ assertTrue(state.equals(UnDefType.UNDEF));
+ }
+ break;
+ case REAR_RIGHT_TARGET:
+ if (vehicle.properties.tires != null) {
+ assertTrue(state instanceof QuantityType);
+ qt = (QuantityType) state;
+ assertEquals(vehicle.properties.tires.rearRight.status.targetPressure / 100, qt.doubleValue(),
+ "Fron Left Current");
+ } else {
+ assertTrue(state.equals(UnDefType.UNDEF));
+ }
+ break;
+ case MOTION:
+ assertTrue(state instanceof OnOffType);
+ oot = (OnOffType) state;
+ if (vehicle.properties.inMotion) {
+ assertEquals(oot.toFullString(), OnOffType.ON.toFullString(), "Vehicle Driving");
+ } else {
+ assertEquals(oot.toFullString(), OnOffType.OFF.toFullString(), "Vehicle Stationary");
+ }
+ break;
+ case ADDRESS:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ assertEquals(st.toFullString(), vehicle.properties.vehicleLocation.address.formatted,
+ "Location Address");
+ break;
+ case RAW:
+ // don't assert raw channel
+ break;
+ default:
+ if (!gUid.equals(CHANNEL_GROUP_CHARGE_PROFILE)) {
+ // fail in case of unknown update
+ assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+ }
+ break;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/VehiclePropertiesTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/VehiclePropertiesTest.java
new file mode 100644
index 00000000000..1ce3050f2b2
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/VehiclePropertiesTest.java
@@ -0,0 +1,63 @@
+/**
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
+import org.openhab.binding.mybmw.internal.util.FileReader;
+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.core.thing.ThingTypeUID;
+import org.openhab.core.types.CommandOption;
+
+/**
+ * The {@link VehiclePropertiesTest} tests stored fingerprint responses from BMW API
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class VehiclePropertiesTest {
+
+ @Test
+ public void testUserInfo() {
+ String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/vehicles.json");
+ List vl = Converter.getVehicleList(content);
+
+ assertEquals(1, vl.size(), "Number of Vehicles");
+ Vehicle v = vl.get(0);
+ assertEquals(Constants.ANONYMOUS, v.vin, "VIN");
+ assertEquals("i3 94 (+ REX)", v.model, "Model");
+ assertEquals(Constants.BEV, v.driveTrain, "DriveTrain");
+ assertEquals("BMW", v.brand, "Brand");
+ assertEquals(2017, v.year, "Year of Construction");
+ }
+
+ @Test
+ public void testChannelUID() {
+ ThingTypeUID thingTypePHEV = new ThingTypeUID("mybmw", "plugin-hybrid-vehicle");
+ assertEquals("plugin-hybrid-vehicle", thingTypePHEV.getId(), "Vehicle Type");
+ }
+
+ @Test
+ public void testRemoteServiceOptions() {
+ String commandReference = "[CommandOption [command=light-flash, label=Flash Lights], CommandOption [command=vehicle-finder, label=Vehicle Finder], CommandOption [command=door-lock, label=Door Lock], CommandOption [command=door-unlock, label=Door Unlock], CommandOption [command=horn-blow, label=Horn Blow], CommandOption [command=climate-now-start, label=Start Climate], CommandOption [command=climate-now-stop, label=Stop Climate]]";
+ List l = RemoteServiceUtils.getOptions(true);
+ assertEquals(commandReference, l.toString(), "Commad Options");
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/VehicleStatusTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/VehicleStatusTest.java
new file mode 100644
index 00000000000..80f8f1ed888
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/VehicleStatusTest.java
@@ -0,0 +1,115 @@
+/**
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
+import org.openhab.binding.mybmw.internal.util.FileReader;
+import org.openhab.binding.mybmw.internal.utils.Constants;
+import org.openhab.binding.mybmw.internal.utils.Converter;
+import org.openhab.binding.mybmw.internal.utils.VehicleStatusUtils;
+import org.openhab.core.library.types.DateTimeType;
+
+/**
+ * The {@link VehicleStatusTest} tests stored fingerprint responses from BMW API
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class VehicleStatusTest {
+
+ @Test
+ public void testServiceDate() {
+ String json = FileReader.readFileInString("src/test/resources/responses/I01_REX/vehicles.json");
+ Vehicle v = Converter.getVehicle(Constants.ANONYMOUS, json);
+ assertEquals(Constants.ANONYMOUS, v.vin, "VIN check");
+ assertEquals("2023-11-01T00:00",
+ ((DateTimeType) VehicleStatusUtils.getNextServiceDate(v.properties.serviceRequired)).getZonedDateTime()
+ .toLocalDateTime().toString(),
+ "Service Date");
+
+ ZonedDateTime zdt = ZonedDateTime.parse("2021-12-21T16:46:02Z").withZoneSameInstant(ZoneId.systemDefault());
+ LocalDateTime ldt = zdt.toLocalDateTime();
+ assertEquals(ldt.format(Converter.DATE_INPUT_PATTERN),
+ Converter.zonedToLocalDateTime(v.properties.lastUpdatedAt), "Last update time");
+ }
+
+ @Test
+ public void testBevRexValues() {
+ String vehiclesJSON = FileReader.readFileInString("src/test/resources/responses/I01_REX/vehicles.json");
+ List vehicleList = Converter.getVehicleList(vehiclesJSON);
+ assertEquals(1, vehicleList.size(), "Vehicles found");
+ Vehicle v = vehicleList.get(0);
+ assertEquals("BMW", v.brand, "Car brand");
+ assertEquals(true, v.properties.areDoorsClosed, "Doors Closed");
+ assertEquals(76, v.properties.electricRange.distance.value, "Electric Range");
+ assertEquals(6.789, v.properties.vehicleLocation.coordinates.longitude, 0.1, "Location lon");
+ assertEquals("immediateCharging", v.status.chargingProfile.chargingMode, "Charging Mode");
+ assertEquals(2, v.status.chargingProfile.getTimerId(2).id, "Timer ID");
+ assertEquals("[sunday]", v.status.chargingProfile.getTimerId(2).timerWeekDays.toString(), "Timer Weekdays");
+ }
+
+ @Test
+ public void testGuessRange() {
+ /**
+ * PHEV G01
+ * fuelIndicator electric unit = %
+ * fuelIndicator fuel unit = l
+ * fuelIndicator hybrid unit = null
+ */
+ String vehiclesJSON = FileReader.readFileInString("src/test/resources/responses/G01/vehicles_v2_bmw_0.json");
+ List vehicleList = Converter.getVehicleList(vehiclesJSON);
+ assertEquals(1, vehicleList.size(), "Vehicles found");
+ Vehicle vehicle = vehicleList.get(0);
+ assertEquals(2, VehicleStatusUtils.getRange(Constants.UNIT_PRECENT_JSON, vehicle), "Electric Range");
+ assertEquals(437, VehicleStatusUtils.getRange(Constants.UNIT_LITER_JSON, vehicle), "Fuel Range");
+ assertEquals(439, VehicleStatusUtils.getRange(Constants.PHEV, vehicle), "Hybrid Range");
+
+ /**
+ * Electric REX I01
+ * fuelIndicator electric unit = %
+ * fuelIndicator fuel unit = null
+ * fuelIndicator hybrid unit = null
+ */
+ vehiclesJSON = FileReader.readFileInString("src/test/resources/responses/I01_REX/vehicles_v2_bmw_0.json");
+ vehicleList = Converter.getVehicleList(vehiclesJSON);
+ assertEquals(1, vehicleList.size(), "Vehicles found");
+ vehicle = vehicleList.get(0);
+ assertEquals(164, VehicleStatusUtils.getRange(Constants.UNIT_PRECENT_JSON, vehicle), "Electric Range");
+ assertEquals(64, VehicleStatusUtils.getRange(Constants.UNIT_LITER_JSON, vehicle), "Fuel Range");
+ assertEquals(228, VehicleStatusUtils.getRange(Constants.PHEV, vehicle), "Hybrid Range");
+
+ /**
+ * PHEV G05
+ * fuelIndicator electric unit = %
+ * fuelIndicator fuel unit = %
+ * fuelIndicator hybrid unit = null
+ */
+ vehiclesJSON = FileReader.readFileInString("src/test/resources/responses/G05/vehicles_v2_bmw_0.json");
+ vehicleList = Converter.getVehicleList(vehiclesJSON);
+ assertEquals(1, vehicleList.size(), "Vehicles found");
+ vehicle = vehicleList.get(0);
+ assertEquals(48, VehicleStatusUtils.getRange(Constants.UNIT_PRECENT_JSON, vehicle), "Electric Range");
+ assertEquals(418, VehicleStatusUtils.getRange(Constants.UNIT_LITER_JSON, vehicle), "Fuel Range");
+ assertEquals(466, VehicleStatusUtils.getRange(Constants.PHEV, vehicle), "Hybrid Range");
+ }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/AuthTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/AuthTest.java
new file mode 100644
index 00000000000..b735562890c
--- /dev/null
+++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/AuthTest.java
@@ -0,0 +1,423 @@
+/**
+ * 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.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+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 javax.crypto.Cipher;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+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.eclipse.jetty.util.ssl.SslContextFactory;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mybmw.internal.MyBMWConfiguration;
+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.util.FileReader;
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link AuthTest} test authorization flow
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+class AuthTest {
+ private final Logger logger = LoggerFactory.getLogger(AuthTest.class);
+
+ void testAuth() {
+ String user = "usr";
+ String pwd = "pwd";
+
+ SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
+ HttpClient authHttpClient = new HttpClient(sslContextFactory);
+ try {
+ authHttpClient.start();
+ Request firstRequest = authHttpClient
+ .newRequest("https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
+ + "/eadrax-ucs/v1/presentation/oauth/config");
+ firstRequest.header("ocp-apim-subscription-key",
+ BimmerConstants.OCP_APIM_KEYS.get(BimmerConstants.REGION_ROW));
+ firstRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
+
+ ContentResponse firstResponse = firstRequest.send();
+ logger.info(firstResponse.getContentAsString());
+ AuthQueryResponse aqr = Converter.getGson().fromJson(firstResponse.getContentAsString(),
+ AuthQueryResponse.class);
+
+ // String verifier_bytes = RandomStringUtils.randomAlphanumeric(64);
+ String verifierBytes = Converter.getRandomString(64);
+ String codeVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(verifierBytes.getBytes());
+
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] hash = digest.digest(codeVerifier.getBytes(StandardCharsets.UTF_8));
+ String codeChallenge = Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
+
+ // String state_bytes = RandomStringUtils.randomAlphanumeric(16);
+ String stateBytes = Converter.getRandomString(16);
+ String state = Base64.getUrlEncoder().withoutPadding().encodeToString(stateBytes.getBytes());
+
+ String authUrl = aqr.gcdmBaseUrl + BimmerConstants.OAUTH_ENDPOINT;
+ logger.info(authUrl);
+ Request loginRequest = authHttpClient.POST(authUrl);
+ loginRequest.header("Content-Type", "application/x-www-form-urlencoded");
+
+ MultiMap baseParams = new MultiMap();
+ baseParams.put("client_id", aqr.clientId);
+ baseParams.put("response_type", "code");
+ baseParams.put("redirect_uri", aqr.returnUrl);
+ baseParams.put("state", state);
+ baseParams.put("nonce", "login_nonce");
+ baseParams.put("scope", String.join(" ", aqr.scopes));
+ baseParams.put("code_challenge", codeChallenge);
+ baseParams.put("code_challenge_method", "S256");
+
+ MultiMap loginParams = new MultiMap(baseParams);
+ loginParams.put("grant_type", "authorization_code");
+ loginParams.put("username", user);
+ loginParams.put("password", pwd);
+ loginRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
+ UrlEncoded.encode(loginParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
+ ContentResponse secondResonse = loginRequest.send();
+ logger.info(secondResonse.getContentAsString());
+ String authCode = getAuthCode(secondResonse.getContentAsString());
+ logger.info(authCode);
+
+ MultiMap authParams = new MultiMap(baseParams);
+ authParams.put("authorization", authCode);
+ Request authRequest = authHttpClient.POST(authUrl).followRedirects(false);
+ authRequest.header("Content-Type", "application/x-www-form-urlencoded");
+ authRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
+ UrlEncoded.encode(authParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
+ ContentResponse authResponse = authRequest.send();
+ logger.info("{}", authResponse.getHeaders());
+ logger.info("Response " + authResponse.getHeaders().get(HttpHeader.LOCATION));
+ String code = AuthTest.codeFromUrl(authResponse.getHeaders().get(HttpHeader.LOCATION));
+ logger.info("Code " + code);
+ logger.info("Auth");
+
+ logger.info(aqr.tokenEndpoint);
+ // AuthenticationStore authenticationStore = authHttpClient.getAuthenticationStore();
+ // BasicAuthentication ba = new BasicAuthentication(new URI(aqr.tokenEndpoint), Authentication.ANY_REALM,
+ // aqr.clientId, aqr.clientSecret);
+ // authenticationStore.addAuthentication(ba);
+ Request codeRequest = authHttpClient.POST(aqr.tokenEndpoint);
+ String basicAuth = "Basic "
+ + Base64.getUrlEncoder().encodeToString((aqr.clientId + ":" + aqr.clientSecret).getBytes());
+ logger.info(basicAuth);
+ codeRequest.header("Content-Type", "application/x-www-form-urlencoded");
+ codeRequest.header(AUTHORIZATION, basicAuth);
+
+ MultiMap