diff --git a/CODEOWNERS b/CODEOWNERS
index 9f5136c9d..3a9dcf65f 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -37,6 +37,7 @@
/bundles/org.openhab.binding.bluetooth.govee/ @cpmeister
/bundles/org.openhab.binding.bluetooth.roaming/ @cpmeister
/bundles/org.openhab.binding.bluetooth.ruuvitag/ @ssalonen
+/bundles/org.openhab.binding.bmwconnecteddrive/ @weymann @ntruchsess
/bundles/org.openhab.binding.boschindego/ @jofleck
/bundles/org.openhab.binding.boschshc/ @stefan-kaestle @coeing @GerdZanker
/bundles/org.openhab.binding.bosesoundtouch/ @marvkis @tratho
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index c6d486fce..82397f5e9 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -171,6 +171,11 @@
org.openhab.binding.bluetooth.ruuvitag
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.bmwconnecteddrive
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.boschindego
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/NOTICE b/bundles/org.openhab.binding.bmwconnecteddrive/NOTICE
new file mode 100644
index 000000000..38d625e34
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/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.bmwconnecteddrive/README.md b/bundles/org.openhab.binding.bmwconnecteddrive/README.md
new file mode 100644
index 000000000..2ed6771af
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/README.md
@@ -0,0 +1,964 @@
+# BMW ConnectedDrive Binding
+
+The binding provides a connection between [BMW's ConnectedDrive Portal](https://www.bmw-connecteddrive.com/country-region-select/country-region-selection.html) and 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 informations.
+Check for each group if it's supported for this Vehicle.
+
+Please note **this isn't a real-time binding**.
+If a door is opened the state isn't transmitted and changed immediately.
+This isn't a flaw in the binding itself because the state in BMW's own ConnectedDrive App is also updated with some delay.
+
+## Supported Things
+
+### Bridge
+
+The bridge establishes the connection between BMW's ConnectedDrive Portal and openHAB.
+
+| Name | Bridge Type ID | Description |
+|----------------------------|----------------|------------------------------------------------------------|
+| BMW ConnectedDrive Account | `account` | Access to BMW ConnectedDrive Portal for a specific user |
+
+
+### Things
+
+Four different vehicle types are provided.
+They differ in the supported channel groups & channels.
+Conventional Fuel Vehicles have no _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` | status, range, location, service, check, charge, image |
+| BMW Electric Vehicle with REX | `bev_rex` | status, range, location, service, check, charge, image |
+| BMW Plug-In-Hybrid Electric Vehicle | `phev` | status, range, location, service, check, charge, image |
+| BMW Conventional Vehicle | `conv` | status, range, location, service, check, image |
+
+
+#### Properties
+
+
+
+For each vehicle properties are available.
+Basically 3 types of information are registered as properties
+
+* Informations regarding your dealer with address and phone number
+* Which services are available / not available
+* Vehicle properties like color, model type, drive train and construction year
+
+In the right picture can see in *Services Activated* e.g. the *DoorLock* and *DoorUnlock* services are mentioned.
+This ensures channel group [Remote Services](#remote-services) is supporting door lock and unlock remote control.
+
+In *Services Supported* the entry *LastDestination* is mentioned.
+So it's valid to connect channel group [Last Destinations](#destinations) in order to display and select the last navigation destinations.
+
+| Property Key | Property Value | Supported Channel Groups |
+|--------------------|---------------------|------------------------------|
+| servicesSupported | Statistics | last-trip, lifetime |
+| servicesSupported | LastDestinations | destinations |
+| servicesActivated | _list of services_ | remote |
+
+
+## Discovery
+
+Auto discovery is starting after the bridge towards BMW's ConnectedDrive 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 | BMW ConnectedDrive Username |
+| password | text | BMW ConnectedDrive 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)
+
+### Thing Configuration
+
+Same configuration is needed for all things
+
+| Parameter | Type | Description |
+|-----------------|---------|---------------------------------------|
+| vin | text | Vehicle Identification Number (VIN) |
+| refreshInterval | integer | Refresh Interval in Minutes |
+| units | text | Unit Selection. See below. |
+| imageSize | integer | Image Size |
+| imageViewport | text | Image Viewport |
+
+The unit configuration has 3 options
+
+* _AUTODETECT_ selects miles for US & UK, kilometer otherwise
+* _METRIC_ selects directly kilometers
+* _IMPERIAL_ selects directly miles
+
+The _imageVieport_ allows to show the vehicle from different angels.
+Possible options are
+
+* _FRONT_
+* _REAR_
+* _SIDE_
+* _DASHBOARD_
+* _DRIVERDOOR_
+
+## 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
+
+#### 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 |
+|---------------------------|---------------------|---------------|------------------------------------------------|
+| Overall Door Status | doors | String | Combined status for all doors |
+| Overall Window Status | windows | String | Combined status for all windows |
+| Doors Locked | lock | String | Status if doors are locked or unlocked |
+| Next Service Date | service-date | DateTime | Date of upcoming service |
+| Mileage till Next Service | service-mileage | Number:Length | Mileage till upcoming service |
+| Check Control | check-control | String | Presence of active warning messages |
+| Charging Status | charge | String | Only available for phev, bev_rex and bev |
+| Last Status Timestamp | last-update | DateTime | Date and time of last status update |
+
+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
+
+* _Active_ - at least one warning message is active
+* _Not Active_ - no warning message is active
+* _Undef_ - no data for warnings delivered
+
+Charging Status values
+
+* _Charging_
+* _Error_
+* _Finished Fully Charged_
+* _Finished Not Full_
+* _Invalid_
+* _Not Charging_
+* _Charging Goal reached_
+* _Waiting For Charging_
+
+#### 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 | Number | Read |
+| Mileage till Service | mileage | Number:Length | Read |
+
+#### Check Control
+
+Group for all current active CheckControl 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 |
+|---------------------------------|---------------------|----------------|------------|
+| CheckControl Description | name | String | Read/Write |
+| CheckControl Details | details | String | Read |
+| Mileage Occurrence | mileage | Number:Length | Read |
+
+#### 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
+
+#### 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 | |
+| Battery 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 | |
+
+
+#### Charge Profile
+
+Charging options with date and time for preferred time windows and charging modes.
+
+* Channel Group ID is **charge**
+* Available for electric and hybrid vehicles
+* Read/Write access for UI. Use [Charge Profile Editing Action](#charge-profile-editing) in rules
+* There are 3 timers *T1, T2 and T3* available. Replace *X* with number 1,2 or 3 to target the correct timer
+* Additional override Timer *OT* defines a single departure besides the 3 predefined schedule timers
+
+| Channel Label | Channel Group ID | Channel ID | Type |
+|----------------------------|------------------|---------------------------|----------|
+| Charge Mode | charge | profile-mode | String |
+| Charge Preferences | charge | profile-prefs | String |
+| Window Start Time | charge | window-start | DateTime |
+| Window End Time | charge | window-end | DateTime |
+| A/C at Departure | charge | profile-climate | Switch |
+| T*X* Enabled | charge | timer*X*-enabled | Switch |
+| T*X* Departure Time | charge | timer*X*-departure | DateTime |
+| T*X* Days | charge | timer*X*-days | String |
+| T*X* Monday | charge | timer*X*-day-mon | Switch |
+| T*X* Tuesday | charge | timer*X*-day-tue | Switch |
+| T*X* Wednesday | charge | timer*X*-day-wed | Switch |
+| T*X* Thursday | charge | timer*X*-day-thu | Switch |
+| T*X* Friday | charge | timer*X*-day-fri | Switch |
+| T*X* Saturday | charge | timer*X*-day-sat | Switch |
+| T*X* Sunday | charge | timer*X*-day-sun | Switch |
+| OT Enabled | charge | override-enabled | Switch |
+| OT Departure Time | charge | override-departure | DateTime |
+
+The channel _profile-mode_ supports
+
+* *IMMEDIATE_CHARGING*
+* *DELAYED_CHARGING*
+
+The channel _profile-prefs_ supports
+
+* *NO_PRESELECTION*
+* *CHARGING_WINDOW*
+
+#### 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 |
+
+#### Last Trip
+
+Statistic values of duration, distance and consumption of the last trip.
+
+* Channel Group ID is **last-trip**
+* Available if *Statistics* is present in *Services Supported*. See [Vehicle Properties](#properties) for further details
+* Read-only values
+* Depending on units configuration in [Thing Configuration](#thing-configuration) average values are given for 100 kilometers or miles
+
+| Channel Label | Channel ID | Type |
+|-----------------------------------------|------------------------------|---------------|
+| Last Trip Date | date | DateTime |
+| Last Trip Duration | duration | Number:Time |
+| Last Trip Distance | distance | Number:Length |
+| Distance since Charge | distance-since-charging | Number:Length |
+| Avg. Power Consumption | avg-consumption | Number:Power |
+| Avg. Power Recuperation | avg-recuperation | Number:Power |
+| Avg. Combined Consumption | avg-combined-consumption | Number:Volume |
+
+
+#### Lifetime Statistics
+
+Providing lifetime consumption values.
+
+* Channel Group ID is **lifetime**
+* Available if *Statistics* is present in *Services Supported*. See [Vehicle Properties](#properties) for further details
+* Read-only values
+* Depending on units configuration in [Thing Configuration](#thing-configuration) average values are given for 100 kilometers or miles
+
+| Channel Label | Channel ID | Type |
+|-----------------------------------------|------------------------------|---------------|
+| Total Electric Distance | total-driven-distance | Number:Length |
+| Longest 1-Charge Distance | single-longest-distance | Number:Length |
+| Avg. Power Consumption | avg-consumption | Number:Power |
+| Avg. Power Recuperation | avg-recuperation | Number:Power |
+| Avg. Combined Consumption | avg-combined-consumption | Number:Volume |
+
+
+#### 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 Blow_
+* _Climate Control_
+* _Start Charging_
+* _Send Charging Profile_
+
+The channel _state_ shows the progress of the command execution in the following order
+
+1) _Initiated_
+2) _Pending_
+3) _Delivered_
+4) _Executed_
+
+#### Destinations
+
+Shows the last destinations stored in the navigation system.
+If several last destinations are stored in the navigation system the channel _name_ contains all addresses as options.
+
+* Channel Group ID is **destination**
+* Available if *LastDestinations* is present in *Services Supported*. Check [Vehicle Properties](#properties) for further details
+* Read/Write access
+
+
+| Channel Label | Channel ID | Type | Access |
+|----------------------|---------------|-----------|-------------|
+| Name | name | String | Read/Write |
+| GPS Coordinates | gps | Location | Read |
+
+
+
+#### Image
+
+Image representation of the vehicle. Size and viewport are writable and can be
+The possible values are the same mentioned in [Thing Configuration](#thing-configuration).
+
+* 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 |
+| Image Picture Size | size | Number | Write |
+
+## Actions
+
+Get the _Actions_ object for your vehicle using the Thing ID
+
+* bmwconnecteddrive - Binding ID, don't change!
+* bev_rex - [Thing UID](#things) of your car
+* user - Thing ID of the [Bridge](#bridge)
+* i3 - Thing ID of your car
+
+```
+ val profile = getActions("bmwconnecteddrive", "bmwconnecteddrive:bev_rex:user:i3")
+```
+
+### Charge Profile Editing
+
+Like in the Charge Profile Channels 3 Timers are provided. Replace *X* with 1, 2 or 3 to address the right timer.
+
+| Function | Parameters | Returns | Description |
+|---------------------------------------|------------------|---------------------------|------------------------------------------------------------|
+| getClimatizationEnabled | void | Boolean | Returns the enabled state of climatization |
+| setClimatizationEnabled | Boolean | void | Sets the enabled state of climatization |
+| getChargingMode | void | String | Gets the charging-mode, see valid options below |
+| setChargingMode | String | void | Sets the charging-mode, see valid options below |
+| getPreferredWindowStart | void | LocalTime | Returns the preferred charging-window start time |
+| setPreferredWindowStart | LocalTime | void | Sets the preferred charging-window start time |
+| getPreferredWindowEnd | void | LocalTime | Returns the preferred charging-window end time |
+| setPreferredWindowEnd | LocalTime | void | Sets the preferred charging-window end time |
+| getTimer*X*Enabled | void | Boolean | Returns the enabled state of timer*X* |
+| setTimer*X*Enabled | Boolean | void | Returns the enabled state of timer*X* |
+| getTimer*X*Departure | void | LocalTime | Returns the departure time of timer*X* |
+| setTimer*X*Departure | LocalTime | void | Sets the timer*X* departure time |
+| getTimer*X*Days | void | Set | Returns the days of week timer*X* is enabled for |
+| setTimer*X*Days | Set | void | sets the days of week timer*X* is enabled for |
+| getOverrideTimerEnabled | void | Boolean | Returns the enabled state of override timer |
+| setOverrideTimerEnabled | Boolean | void | Sets the enabled state of override timer |
+| getOverrideTimerDeparture | void | LocalTime | Returns the departure time of override timer |
+| setOverrideTimerDeparture | LocalTime | void | Sets the override timer departure time |
+| getOverrideTimerDays | void | Set | Returns the days of week the overrideTimer is enabled for |
+| setOverrideTimerDays | Set | void | Sets the days of week the overrideTimer is enabled for |
+| cancelEditChargeProfile | void | void | Cancel current edit of charging profile |
+| sendChargeProfile | void | void | Sends the charging profile to the vehicle |
+
+Values for valid charging mode get/set
+
+* *IMMEDIATE_CHARGING*
+* *DELAYED_CHARGING*
+
+
+## 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)
+* Last Destinations delivered in group [Destinations](#destinations)
+
+The channel id _name_ shows the first element as default.
+All other possibilities are attached as options.
+The picture on the right shows the _Service Name_ item and all four possible options.
+Select the desired service and the corresponding _Service Date & Milage_ will be shown.
+
+### TroubleShooting
+
+BMW has a high range of vehicles supported by ConnectedDrive.
+In case of any issues with this binding help to resolve it!
+Please perform the following steps:
+
+* Can you [log into ConnectedDrive](https://www.bmw-connecteddrive.com/country-region-select/country-region-selection.html) with your credentials? Please note this isn't the BMW Customer portal - it's the ConnectedDrive portal
+* Is the vehicle listed in your account? There's a one-to-one relation from user to vehicle
+
+If the access to the portal is working and the vehicle is listed some debug data is needed in order to identify the issue.
+
+#### Generate Debug Fingerprint
+
+If you checked the above pre-conditions you need to get the debug fingerprint from the logs.
+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.bmwconnecteddrive
+```
+
+The debug fingerprint is generated immediately after the vehicle thing is initialized the first time, e.g. after openHAB startup.
+To force a new fingerprint disable the thing shortly and enable it again.
+Personal data is eliminated from the log entries so it should be possible to share them in public.
+Data like
+
+* Dealer Properties
+* Vehicle Identification Number (VIN)
+* Location latitude / longitude
+
+are anonymized.
+You'll find the fingerprint in the logs with the command
+
+```
+grep "Troubleshoot 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 kilometer the route distance is ~192 kilometer.
+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/configuration/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 the three configuration parameters in the Things section
+
+* YOUR_USERNAME - with your ConnectedDrive login username
+* YOUR_PASSWORD - with your ConnectedDrive 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 bmwconnecteddrive:account:user "BMW ConnectedDrive Account" [userName="YOUR_USERNAME",password="YOUR_PASSWORD",region="ROW"] {
+ Thing bev_rex i3 "BMW i3 94h REX" [ vin="VEHICLE_VIN",units="AUTODETECT",imageSize=600,imageViewport="FRONT",refreshInterval=5]
+}
+```
+
+### Items File
+
+```
+Number:Length i3Mileage "Odometer [%d %unit%]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:range#mileage" }
+Number:Length i3Range "Range [%d %unit%]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:range#hybrid"}
+Number:Length i3RangeElectric "Electric Range [%d %unit%]" (i3,long) {channel="bmwconnecteddrive:bev_rex:user:i3:range#electric"}
+Number:Length i3RangeFuel "Fuel Range [%d %unit%]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:range#fuel"}
+Number:Dimensionless i3BatterySoc "Battery Charge [%.1f %%]" (i3,long) {channel="bmwconnecteddrive:bev_rex:user:i3:range#soc"}
+Number:Volume i3Fuel "Fuel [%.1f %unit%]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:range#remaining-fuel"}
+Number:Length i3RadiusElectric "Electric Radius [%d %unit%]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:range#radius-electric" }
+Number:Length i3RadiusHybrid "Hybrid Radius [%d %unit%]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:range#radius-hybrid" }
+
+String i3DoorStatus "Door Status [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:status#doors" }
+String i3WindowStatus "Window Status [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:status#windows" }
+String i3LockStatus "Lock Status [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:status#lock" }
+DateTime i3NextServiceDate "Next Service Date [%1$tb %1$tY]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:status#service-date" }
+String i3NextServiceMileage "Next Service Mileage [%d %unit%]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:status#service-mileage" }
+String i3CheckControl "Check Control [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:status#check-control" }
+String i3ChargingStatus "Charging [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:status#charge" }
+DateTime i3LastUpdate "Update [%1$tA, %1$td.%1$tm. %1$tH:%1$tM]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:status#last-update"}
+
+DateTime i3TripDateTime "Trip Date [%1$tA, %1$td.%1$tm. %1$tH:%1$tM]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:last-trip#date"}
+Number:Time i3TripDuration "Trip Duration [%d %unit%]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:last-trip#duration"}
+Number:Length i3TripDistance "Distance [%d %unit%]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:last-trip#distance" }
+Number:Length i3TripDistanceSinceCharge "Distance since last Charge [%d %unit%]" (i3,long) {channel="bmwconnecteddrive:bev_rex:user:i3:last-trip#distance-since-charging" }
+Number:Energy i3AvgTripConsumption "Average Consumption [%.1f %unit%]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:last-trip#avg-consumption" }
+Number:Volume i3AvgTripCombined "Average Combined Consumption [%.1f %unit%]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:last-trip#avg-combined-consumption" }
+Number:Energy i3AvgTripRecuperation "Average Recuperation [%.1f %unit%]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:last-trip#avg-recuperation" }
+
+Number:Length i3TotalElectric "Electric Distance Driven [%d %unit%]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:lifetime#total-driven-distance" }
+Number:Length i3LongestEVTrip "Longest Electric Trip [%d %unit%]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:lifetime#single-longest-distance" }
+Number:Energy i3AvgConsumption "Average Consumption [%.1f %unit%]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:lifetime#avg-consumption" }
+Number:Volume i3AvgCombined "Average Combined Consumption [%.1f %unit%]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:lifetime#avg-combined-consumption" }
+Number:Energy i3AvgRecuperation "Average Recuperation [%.1f %unit%]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:lifetime#avg-recuperation" }
+
+Location i3Location "Location [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:location#gps" }
+Number:Angle i3Heading "Heading [%.1f %unit%]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:location#heading" }
+
+String i3RemoteCommand "Command [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:remote#command" }
+String i3RemoteState "Remote Execution State [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:remote#state" }
+
+String i3DriverDoor "Driver Door [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:doors#driver-front" }
+String i3DriverDoorRear "Driver Door Rear [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:doors#driver-rear" }
+String i3PassengerDoor "Passenger Door [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:doors#passenger-front" }
+String i3PassengerDoorRear "Passenger Door Rear [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:doors#passenger-rear" }
+String i3Hood "Hood [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:doors#hood" }
+String i3Trunk "Trunk [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:doors#trunk" }
+String i3DriverWindow "Driver Window [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:doors#win-driver-front" }
+String i3DriverWindowRear "Driver Window Rear [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:doors#win-driver-rear" }
+String i3PassengerWindow "Passenger Window [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:doors#win-passenger-front" }
+String i3PassengerWindowRear "Passenger Window Rear [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:doors#win-passenger-rear" }
+String i3RearWindow "Rear Window [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:doors#win-rear" }
+String i3Sunroof "Sunroof [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:doors#sunroof" }
+
+String i3ServiceName "Service Name [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:service#name" }
+String i3ServiceDetails "Service Details [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:service#details" }
+Number:Length i3ServiceMileage "Service Mileage [%d %unit%]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:service#mileage" }
+DateTime i3ServiceDate "Service Date [%1$tb %1$tY]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:service#date" }
+
+String i3CCName "CheckControl Name [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:check#name" }
+String i3CCDetails "CheckControl Details [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:check#details" }
+Number:Length i3CCMileage "CheckControl Mileage [%d %unit%]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:check#mileage" }
+
+String i3DestName "Destination [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:destination#name" }
+Location i3DestLocation "GPS [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:destination#gps" }
+
+Switch i3ChargeProfileClimate "Charge Profile Climatization" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#profile-climate" }
+String i3ChargeProfileMode "Charge Profile Mode [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#profile-mode" }
+DateTime i3ChargeWindowStart "Charge Window Start [%1$tH:%1$tM]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#window-start" }
+Number i3ChargeWindowStartHour "Charge Window Start Hour [%d]" (i3)
+Number i3ChargeWindowStartMinute "Charge Window Start Minute [%d]" (i3)
+DateTime i3ChargeWindowEnd "Charge Window End [%1$tH:%1$tM]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#window-end" }
+Number i3ChargeWindowEndHour "Charge Window End Hour [%d]" (i3)
+Number i3ChargeWindowEndMinute "Charge Window End Minute [%d]" (i3)
+DateTime i3Timer1Departure "Timer 1 Departure [%1$tH:%1$tM]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-departure" }
+Number i3Timer1DepartureHour "Timer 1 Departure Hour [%d]" (i3)
+Number i3Timer1DepartureMinute "Timer 1 Departure Minute [%d]" (i3)
+String i3Timer1Days "Timer 1 Days [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-days" }
+Switch i3Timer1DayMon "Timer 1 Monday" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-day-mon" }
+Switch i3Timer1DayTue "Timer 1 Tuesday" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-day-tue" }
+Switch i3Timer1DayWed "Timer 1 Wednesday" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-day-wed" }
+Switch i3Timer1DayThu "Timer 1 Thursday" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-day-thu" }
+Switch i3Timer1DayFri "Timer 1 Friday" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-day-fri" }
+Switch i3Timer1DaySat "Timer 1 Saturday" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-day-sat" }
+Switch i3Timer1DaySun "Timer 1 Sunday" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-day-sun" }
+Switch i3Timer1Enabled "Timer 1 Enabled" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-enabled" }
+DateTime i3Timer2Departure "Timer 2 Departure [%1$tH:%1$tM]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-departure" }
+Number i3Timer2DepartureHour "Timer 2 Departure Hour [%d]" (i3)
+Number i3Timer2DepartureMinute "Timer 2 Departure Minute [%d]" (i3)
+String i3Timer2Days "Timer 2 Days [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-days" }
+Switch i3Timer2DayMon "Timer 2 Monday" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-day-mon" }
+Switch i3Timer2DayTue "Timer 2 Tuesday" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-day-tue" }
+Switch i3Timer2DayWed "Timer 2 Wednesday" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-day-wed" }
+Switch i3Timer2DayThu "Timer 2 Thursday" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-day-thu" }
+Switch i3Timer2DayFri "Timer 2 Friday" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-day-fri" }
+Switch i3Timer2DaySat "Timer 2 Saturday" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-day-sat" }
+Switch i3Timer2DaySun "Timer 2 Sunday" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-day-sun" }
+Switch i3Timer2Enabled "Timer 2 Enabled" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-enabled" }
+DateTime i3Timer3Departure "Timer 3 Departure [%1$tH:%1$tM]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-departure" }
+Number i3Timer3DepartureHour "Timer 3 Departure Hour [%d]" (i3)
+Number i3Timer3DepartureMinute "Timer 3 Departure Minute [%d]" (i3)
+String i3Timer3Days "Timer 3 Days [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-days" }
+Switch i3Timer3DayMon "Timer 3 Monday" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-day-mon" }
+Switch i3Timer3DayTue "Timer 3 Tuesday" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-day-tue" }
+Switch i3Timer3DayWed "Timer 3 Wednesday" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-day-wed" }
+Switch i3Timer3DayThu "Timer 3 Thursday" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-day-thu" }
+Switch i3Timer3DayFri "Timer 3 Friday" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-day-fri" }
+Switch i3Timer3DaySat "Timer 3 Saturday" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-day-sat" }
+Switch i3Timer3DaySun "Timer 3 Sunday" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-day-sun" }
+Switch i3Timer3Enabled "Timer 3 Enabled" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-enabled" }
+Switch i3OverrideEnabled "Override Timer Enabled" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#override-enabled"}
+DateTime i3OverrideDeparture "Override Timer Departure [%1$tH:%1$tM]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#override-departure" }
+Number i3OverrideDepartureHour "Override Timer Departure Hour [%d]" (i3)
+Number i3OverrideDepartureMinute "Override Timer Departure Minute [%d]" (i3)
+
+Image i3Image "Image" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:image#png" }
+String i3ImageViewport "Image Viewport [%s]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:image#view" }
+Number i3ImageSize "Image Size [%d]" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:image#size" }
+```
+
+### Sitemap File
+
+```
+sitemap BMW label="BMW" {
+ Frame label="BMW i3" {
+ Image item=i3Image
+
+ }
+ 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="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="Remote Services" {
+ Selection item=i3RemoteCommand
+ Text item=i3RemoteState
+ }
+ Frame label="Last Trip" {
+ Text item=i3TripDateTime
+ Text item=i3TripDuration
+ Text item=i3TripDistance
+ Text item=i3TripDistanceSinceCharge
+ Text item=i3AvgTripConsumption
+ Text item=i3AvgTripRecuperation
+ Text item=i3AvgTripCombined
+ }
+ Frame label="Lifetime" {
+ Text item=i3TotalElectric
+ Text item=i3LongestEVTrip
+ Text item=i3AvgConsumption
+ Text item=i3AvgRecuperation
+ Text item=i3AvgCombined
+ }
+ Frame label="Services" {
+ Text item=i3ServiceName
+ Text item=i3ServiceMileage
+ Text item=i3ServiceDate
+ }
+ Frame label="CheckControl" {
+ Text item=i3CCName
+ Text item=i3CCMileage
+ }
+ 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
+ Setpoint item=i3ChargeWindowStartHour maxValue=23 step=1 icon="time"
+ Setpoint item=i3ChargeWindowStartMinute maxValue=55 step=5 icon="time"
+ Text item=i3ChargeWindowEnd
+ Setpoint item=i3ChargeWindowEndHour maxValue=23 step=1 icon="time"
+ Setpoint item=i3ChargeWindowEndMinute maxValue=55 step=5 icon="time"
+ Text item=i3Timer1Departure
+ Setpoint item=i3Timer1DepartureHour maxValue=23 step=1 icon="time"
+ Setpoint item=i3Timer1DepartureMinute maxValue=55 step=5 icon="time"
+ Text item=i3Timer1Days
+ 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
+ Setpoint item=i3Timer2DepartureHour maxValue=23 step=1 icon="time"
+ Setpoint item=i3Timer2DepartureMinute maxValue=55 step=5 icon="time"
+ Text item=i3Timer2Days
+ 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
+ Setpoint item=i3Timer3DepartureHour maxValue=23 step=1 icon="time"
+ Setpoint item=i3Timer3DepartureMinute maxValue=55 step=5 icon="time"
+ Text item=i3Timer3Days
+ Switch item=i3Timer3DayMon
+ Switch item=i3Timer3DayTue
+ Switch item=i3Timer3DayWed
+ Switch item=i3Timer3DayThu
+ Switch item=i3Timer3DayFri
+ Switch item=i3Timer3DaySat
+ Switch item=i3Timer3DaySun
+ Switch item=i3Timer3Enabled
+ Switch item=i3OverrideEnabled
+ Text item=i3OverrideDeparture
+ Setpoint item=i3OverrideDepartureHour maxValue=23 step=1 icon="time"
+ Setpoint item=i3OverrideDepartureMinute maxValue=55 step=5 icon="time"
+ }
+ Frame label="Last Destinations" {
+ Text item=i3DestName
+ Text item=i3DestLocation
+ }
+ Frame label="Image Properties" {
+ Text item=i3ImageViewport
+ Text item=i3ImageSize
+ }
+}
+```
+
+### Rules File
+
+```
+rule "i3ChargeWindowStartSetpoint"
+when
+ Item i3ChargeWindowStartMinute changed or
+ Item i3ChargeWindowStartHour changed
+then
+ val hour = (i3ChargeWindowStartHour.state as Number).intValue
+ val minute = (i3ChargeWindowStartMinute.state as Number).intValue
+ val time = (i3ChargeWindowStart.state as DateTimeType).zonedDateTime
+ i3ChargeWindowStart.sendCommand(new DateTimeType(time.withHour(hour).withMinute(minute)))
+end
+
+rule "i3ChargeWindowStart"
+when
+ Item i3ChargeWindowStart changed
+then
+ val time = (i3ChargeWindowStart.state as DateTimeType).zonedDateTime
+ i3ChargeWindowStartMinute.sendCommand(time.minute)
+ i3ChargeWindowStartHour.sendCommand(time.hour)
+end
+
+rule "i3ChargeWindowEndSetpoint"
+when
+ Item i3ChargeWindowEndMinute changed or
+ Item i3ChargeWindowEndHour changed
+then
+ val hour = (i3ChargeWindowEndHour.state as Number).intValue
+ val minute = (i3ChargeWindowEndMinute.state as Number).intValue
+ val time = (i3ChargeWindowEnd.state as DateTimeType).zonedDateTime
+ i3ChargeWindowEnd.sendCommand(new DateTimeType(time.withHour(hour).withMinute(minute)))
+end
+
+rule "i3ChargeWindowEnd"
+when
+ Item i3ChargeWindowEnd changed
+then
+ val time = (i3ChargeWindowEnd.state as DateTimeType).zonedDateTime
+ i3ChargeWindowEndMinute.sendCommand(time.minute)
+ i3ChargeWindowEndHour.sendCommand(time.hour)
+end
+
+rule "i3Timer1DepartureSetpoint"
+when
+ Item i3Timer1DepartureMinute changed or
+ Item i3Timer1DepartureHour changed
+then
+ val hour = (i3Timer1DepartureHour.state as Number).intValue
+ val minute = (i3Timer1DepartureMinute.state as Number).intValue
+ val time = (i3Timer1Departure.state as DateTimeType).zonedDateTime
+ i3Timer1Departure.sendCommand(new DateTimeType(time.withHour(hour).withMinute(minute)))
+end
+
+rule "i3Timer1Departure"
+when
+ Item i3Timer1Departure changed
+then
+ val time = (i3Timer1Departure.state as DateTimeType).zonedDateTime
+ i3Timer1DepartureMinute.sendCommand(time.minute)
+ i3Timer1DepartureHour.sendCommand(time.hour)
+end
+
+rule "i3Timer2DepartureSetpoint"
+when
+ Item i3Timer2DepartureMinute changed or
+ Item i3Timer2DepartureHour changed
+then
+ val hour = (i3Timer2DepartureHour.state as Number).intValue
+ val minute = (i3Timer2DepartureMinute.state as Number).intValue
+ val time = (i3Timer2Departure.state as DateTimeType).zonedDateTime
+ i3Timer2Departure.sendCommand(new DateTimeType(time.withHour(hour).withMinute(minute)))
+end
+
+rule "i3Timer2Departure"
+when
+ Item i3Timer2Departure changed
+then
+ val time = (i3Timer2Departure.state as DateTimeType).zonedDateTime
+ i3Timer2DepartureMinute.sendCommand(time.minute)
+ i3Timer2DepartureHour.sendCommand(time.hour)
+end
+
+rule "i3Timer3DepartureSetpoint"
+when
+ Item i3Timer3DepartureMinute changed or
+ Item i3Timer3DepartureHour changed
+then
+ val hour = (i3Timer3DepartureHour.state as Number).intValue
+ val minute = (i3Timer3DepartureMinute.state as Number).intValue
+ val time = (i3Timer3Departure.state as DateTimeType).zonedDateTime
+ i3Timer3Departure.sendCommand(new DateTimeType(time.withHour(hour).withMinute(minute)))
+end
+
+rule "i3Timer3Departure"
+when
+ Item i3Timer3Departure changed
+then
+ val time = (i3Timer3Departure.state as DateTimeType).zonedDateTime
+ i3Timer3DepartureMinute.sendCommand(time.minute)
+ i3Timer3DepartureHour.sendCommand(time.hour)
+end
+
+rule "i3OverrideDepartureSetpoint"
+when
+ Item i3OverrideDepartureMinute changed or
+ Item i3OverrideDepartureHour changed
+then
+ val hour = (i3OverrideDepartureHour.state as Number).intValue
+ val minute = (i3OverrideDepartureMinute.state as Number).intValue
+ val time = (i3OverrideDeparture.state as DateTimeType).zonedDateTime
+ i3OverrideDeparture.sendCommand(new DateTimeType(time.withHour(hour).withMinute(minute)))
+end
+
+rule "i3OverrideDeparture"
+when
+ Item i3OverrideDeparture changed
+then
+ val time = (i3OverrideDeparture.state as DateTimeType).zonedDateTime
+ i3OverrideDepartureMinute.sendCommand(time.minute)
+ i3OverrideDepartureHour.sendCommand(time.hour)
+end
+```
+
+### Action example
+
+```
+ val profile = getActions("bmwconnecteddrive", "bmwconnecteddrive:bev_rex:user:i3")
+ val now = ZonedDateTime.now.toLocalTime
+ profile.setChargingMode("DELAYED_CHARGING")
+ profile.setTimer1Departure(now.minusHours(2))
+ profile.setTimer1Days(java.util.Set())
+ profile.setTimer1Enabled(true)
+ profile.setTimer2Enabled(false)
+ profile.setTimer3Enabled(false)
+ profile.setPreferredWindowStart(now.minusHours(6))
+ profile.setPreferredWindowEnd(now.minusHours(2))
+ profile.sendChargeProfile()
+```
+
+## Credits
+
+This work is based on the project of [Bimmer Connected](https://github.com/bimmerconnected/bimmer_connected).
+Also a [manual installation based on python](https://community.openhab.org/t/script-to-access-the-bmw-connecteddrive-portal-via-oh/37345) was already available for openHAB.
+This binding is basically a port to openHAB based on these concept works!
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/doc/AwayImage.png b/bundles/org.openhab.binding.bmwconnecteddrive/doc/AwayImage.png
new file mode 100644
index 000000000..24dd33cd9
Binary files /dev/null and b/bundles/org.openhab.binding.bmwconnecteddrive/doc/AwayImage.png differ
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/doc/CarStatusImages.png b/bundles/org.openhab.binding.bmwconnecteddrive/doc/CarStatusImages.png
new file mode 100644
index 000000000..b198ede53
Binary files /dev/null and b/bundles/org.openhab.binding.bmwconnecteddrive/doc/CarStatusImages.png differ
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/doc/ChargingImage.png b/bundles/org.openhab.binding.bmwconnecteddrive/doc/ChargingImage.png
new file mode 100644
index 000000000..09a132f25
Binary files /dev/null and b/bundles/org.openhab.binding.bmwconnecteddrive/doc/ChargingImage.png differ
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/doc/CheckControlImage.png b/bundles/org.openhab.binding.bmwconnecteddrive/doc/CheckControlImage.png
new file mode 100644
index 000000000..4f8aa93c5
Binary files /dev/null and b/bundles/org.openhab.binding.bmwconnecteddrive/doc/CheckControlImage.png differ
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/doc/ServiceOptions.png b/bundles/org.openhab.binding.bmwconnecteddrive/doc/ServiceOptions.png
new file mode 100644
index 000000000..5458f866b
Binary files /dev/null and b/bundles/org.openhab.binding.bmwconnecteddrive/doc/ServiceOptions.png differ
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/doc/UnlockedImage.png b/bundles/org.openhab.binding.bmwconnecteddrive/doc/UnlockedImage.png
new file mode 100644
index 000000000..7518330d7
Binary files /dev/null and b/bundles/org.openhab.binding.bmwconnecteddrive/doc/UnlockedImage.png differ
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/doc/panel.png b/bundles/org.openhab.binding.bmwconnecteddrive/doc/panel.png
new file mode 100644
index 000000000..efc3a61bc
Binary files /dev/null and b/bundles/org.openhab.binding.bmwconnecteddrive/doc/panel.png differ
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/doc/properties.png b/bundles/org.openhab.binding.bmwconnecteddrive/doc/properties.png
new file mode 100644
index 000000000..ce2a26350
Binary files /dev/null and b/bundles/org.openhab.binding.bmwconnecteddrive/doc/properties.png differ
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/doc/range-radius.png b/bundles/org.openhab.binding.bmwconnecteddrive/doc/range-radius.png
new file mode 100644
index 000000000..21fc8fb8d
Binary files /dev/null and b/bundles/org.openhab.binding.bmwconnecteddrive/doc/range-radius.png differ
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/pom.xml b/bundles/org.openhab.binding.bmwconnecteddrive/pom.xml
new file mode 100644
index 000000000..36567a623
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.1.0-SNAPSHOT
+
+
+ org.openhab.binding.bmwconnecteddrive
+
+ openHAB Add-ons :: Bundles :: BMWConnectedDrive Binding
+
+
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/feature/feature.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/feature/feature.xml
new file mode 100644
index 000000000..bf96a9905
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/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.bmwconnecteddrive/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveConfiguration.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveConfiguration.java
new file mode 100644
index 000000000..c082bff34
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveConfiguration.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+/**
+ * The {@link ConnectedDriveConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class ConnectedDriveConfiguration {
+
+ /**
+ * Depending on the location the correct server needs to be called
+ */
+ public String region = Constants.EMPTY;
+
+ /**
+ * BMW Connected Drive Username
+ */
+ public String userName = Constants.EMPTY;
+
+ /**
+ * BMW Connected Drive Password
+ */
+ public String password = Constants.EMPTY;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveConstants.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveConstants.java
new file mode 100644
index 000000000..31e22c4a0
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveConstants.java
@@ -0,0 +1,200 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link ConnectedDriveConstants} 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 ConnectedDriveConstants {
+
+ private static final String BINDING_ID = "bmwconnecteddrive";
+
+ // Units
+ public static final String UNITS_AUTODETECT = "AUTODETECT";
+ public static final String UNITS_IMPERIAL = "IMPERIAL";
+ public static final String UNITS_METRIC = "METRIC";
+
+ public static final String VIN = "vin";
+
+ public static final int DEFAULT_IMAGE_SIZE_PX = 1024;
+ public static final int DEFAULT_REFRESH_INTERVAL_MINUTES = 5;
+ public static final String DEFAULT_IMAGE_VIEWPORT = "FRONT";
+
+ // 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"),
+ ELECTRIC_REX("bev_rex"),
+ ELECTRIC("bev");
+
+ private final String type;
+
+ VehicleType(String s) {
+ type = s;
+ }
+
+ @Override
+ public String toString() {
+ return type;
+ }
+ }
+
+ public enum ChargingMode {
+ IMMEDIATE_CHARGING,
+ DELAYED_CHARGING
+ }
+
+ public enum ChargingPreference {
+ NO_PRESELECTION,
+ CHARGING_WINDOW
+ }
+
+ 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());
+
+ // Countries with Mileage display
+ public static final Set IMPERIAL_COUNTRIES = Set.of("US", "GB");
+
+ // 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_LAST_TRIP = "last-trip";
+ public static final String CHANNEL_GROUP_LIFETIME = "lifetime";
+ public static final String CHANNEL_GROUP_REMOTE = "remote";
+ public static final String CHANNEL_GROUP_CHARGE = "charge";
+ public static final String CHANNEL_GROUP_VEHICLE_IMAGE = "image";
+ public static final String CHANNEL_GROUP_DESTINATION = "destination";
+
+ // Generic Constants for several groups
+ public static final String NAME = "name";
+ public static final String DETAILS = "details";
+ public static final String DATE = "date";
+ public static final String MILEAGE = "mileage";
+ public static final String GPS = "gps";
+ public static final String HEADING = "heading";
+
+ // 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 CHARGE_STATUS = "charge";
+ public static final String CHARGE_END_REASON = "reason";
+ public static final String CHARGE_REMAINING = "remaining";
+ public static final String LAST_UPDATE = "last-update";
+
+ // 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 = "profile-climate";
+ public static final String CHARGE_PROFILE_MODE = "profile-mode";
+ public static final String CHARGE_PROFILE_PREFERENCE = "profile-prefs";
+ 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_OVERRIDE = "override";
+ public static final String CHARGE_DEPARTURE = "-departure";
+ public static final String CHARGE_ENABLED = "-enabled";
+ public static final String CHARGE_DAYS = "-days";
+ 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_HYBRID = "hybrid";
+ public static final String RANGE_ELECTRIC = "electric";
+ public static final String SOC = "soc";
+ public static final String RANGE_FUEL = "fuel";
+ public static final String REMAINING_FUEL = "remaining-fuel";
+ public static final String RANGE_RADIUS_ELECTRIC = "radius-electric";
+ public static final String RANGE_RADIUS_FUEL = "radius-fuel";
+ public static final String RANGE_RADIUS_HYBRID = "radius-hybrid";
+
+ // Last Trip
+ public static final String DURATION = "duration";
+ public static final String DISTANCE = "distance";
+ public static final String DISTANCE_SINCE_CHARGING = "distance-since-charging";
+ public static final String AVG_CONSUMPTION = "avg-consumption";
+ public static final String AVG_COMBINED_CONSUMPTION = "avg-combined-consumption";
+ public static final String AVG_RECUPERATION = "avg-recuperation";
+
+ // Lifetime + Average Consumptions
+ public static final String TOTAL_DRIVEN_DISTANCE = "total-driven-distance";
+ public static final String SINGLE_LONGEST_DISTANCE = "single-longest-distance";
+
+ // Image
+ public static final String IMAGE_FORMAT = "png";
+ public static final String IMAGE_VIEWPORT = "view";
+ public static final String IMAGE_SIZE = "size";
+
+ // Remote Services
+ public static final String REMOTE_SERVICE_LIGHT_FLASH = "light";
+ public static final String REMOTE_SERVICE_VEHICLE_FINDER = "finder";
+ public static final String REMOTE_SERVICE_DOOR_LOCK = "lock";
+ public static final String REMOTE_SERVICE_DOOR_UNLOCK = "unlock";
+ public static final String REMOTE_SERVICE_HORN = "horn";
+ public static final String REMOTE_SERVICE_AIR_CONDITIONING = "climate";
+ public static final String REMOTE_SERVICE_CHARGE_NOW = "charge-now";
+ public static final String REMOTE_SERVICE_CHARGING_CONTROL = "charge-control";
+ public static final String REMOTE_SERVICE_COMMAND = "command";
+ public static final String REMOTE_STATE = "state";
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveHandlerFactory.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveHandlerFactory.java
new file mode 100644
index 000000000..11f2b0130
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveHandlerFactory.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.BMWConnectedDriveOptionProvider;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.ConnectedDriveBridgeHandler;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.VehicleHandler;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.core.i18n.LocaleProvider;
+import org.openhab.core.i18n.TimeZoneProvider;
+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 ConnectedDriveHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.bmwconnecteddrive", service = ThingHandlerFactory.class)
+public class ConnectedDriveHandlerFactory extends BaseThingHandlerFactory {
+
+ private final HttpClientFactory httpClientFactory;
+ private final BMWConnectedDriveOptionProvider optionProvider;
+ private boolean imperial = false;
+
+ @Activate
+ public ConnectedDriveHandlerFactory(final @Reference HttpClientFactory hcf,
+ final @Reference BMWConnectedDriveOptionProvider op, final @Reference LocaleProvider lp,
+ final @Reference TimeZoneProvider timeZoneProvider) {
+ httpClientFactory = hcf;
+ optionProvider = op;
+ imperial = IMPERIAL_COUNTRIES.contains(lp.getLocale().getCountry());
+ Converter.setTimeZoneProvider(timeZoneProvider);
+ }
+
+ @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 ConnectedDriveBridgeHandler((Bridge) thing, httpClientFactory);
+ } else if (SUPPORTED_THING_SET.contains(thingTypeUID)) {
+ VehicleHandler vh = new VehicleHandler(thing, optionProvider, thingTypeUID.getId(), imperial);
+ return vh;
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/VehicleConfiguration.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/VehicleConfiguration.java
new file mode 100644
index 000000000..d0af7f79d
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/VehicleConfiguration.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.bmwconnecteddrive.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;
+
+ /**
+ * Data refresh rate in minutes
+ */
+ public int refreshInterval = ConnectedDriveConstants.DEFAULT_REFRESH_INTERVAL_MINUTES;
+
+ /**
+ * Either Auto Detect Miles units (UK & US) or select Format directly
+ * Auto Detect
+ * Metric
+ * Imperial
+ */
+ public String units = ConnectedDriveConstants.UNITS_AUTODETECT;
+
+ /**
+ * image size - width & length (square)
+ */
+ public int imageSize = ConnectedDriveConstants.DEFAULT_IMAGE_SIZE_PX;
+
+ /**
+ * image viewport defined as options in thing xml
+ * Front
+ * Rear
+ * Slide
+ * Dashboard
+ * Driver Door
+ */
+ public String imageViewport = ConnectedDriveConstants.DEFAULT_IMAGE_VIEWPORT;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/action/BMWConnectedDriveActions.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/action/BMWConnectedDriveActions.java
new file mode 100644
index 000000000..10884ddaa
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/action/BMWConnectedDriveActions.java
@@ -0,0 +1,406 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.action;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper.ProfileKey.*;
+
+import java.time.DayOfWeek;
+import java.time.LocalTime;
+import java.util.Optional;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.VehicleHandler;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper.ProfileKey;
+import org.openhab.core.automation.annotation.ActionInput;
+import org.openhab.core.automation.annotation.ActionOutput;
+import org.openhab.core.automation.annotation.RuleAction;
+import org.openhab.core.thing.binding.ThingActions;
+import org.openhab.core.thing.binding.ThingActionsScope;
+import org.openhab.core.thing.binding.ThingHandler;
+
+/**
+ * The {@link BMWConnectedDriveActions} provides actions for VehicleHandler
+ *
+ * @author Norbert Truchsess - Initial contribution
+ */
+@ThingActionsScope(name = "bmwconnecteddrive")
+@NonNullByDefault
+public class BMWConnectedDriveActions implements ThingActions {
+
+ private Optional handler = Optional.empty();
+
+ private Optional profile = Optional.empty();
+
+ @RuleAction(label = "getTimer1Departure", description = "returns the departure time of timer1")
+ public @ActionOutput(name = "time", type = "java.util.Optional") Optional getTimer1Departure() {
+ return getTime(TIMER1);
+ }
+
+ @RuleAction(label = "setTimer1Departure", description = "sets the timer1 departure time")
+ public void setTimer1Departure(@ActionInput(name = "time", type = "java.time.LocalTime") @Nullable LocalTime time) {
+ setTime(TIMER1, time);
+ }
+
+ @RuleAction(label = "getTimer1Enabled", description = "returns the enabled state of timer1")
+ public @ActionOutput(name = "enabled", type = "java.util.Optional") Optional getTimer1Enabled() {
+ return getEnabled(TIMER1);
+ }
+
+ @RuleAction(label = "setTimer1Enabled", description = "sets the enabled state of timer1")
+ public void setTimer1Enabled(@ActionInput(name = "enabled", type = "java.lang.Boolean") @Nullable Boolean enabled) {
+ setEnabled(TIMER1, enabled);
+ }
+
+ @RuleAction(label = "getTimer2Departure", description = "returns the departure time of timer2")
+ public @ActionOutput(name = "time", type = "java.util.Optional") Optional getTimer2Departure() {
+ return getTime(TIMER2);
+ }
+
+ @RuleAction(label = "setTimer2Departure", description = "sets the timer2 departure time")
+ public void setTimer2Departure(@ActionInput(name = "time", type = "java.time.LocalTime") @Nullable LocalTime time) {
+ setTime(TIMER2, time);
+ }
+
+ @RuleAction(label = "getTimer2Enabled", description = "returns the enabled state of timer2")
+ public @ActionOutput(name = "enabled", type = "java.util.Optional") Optional getTimer2Enabled() {
+ return getEnabled(TIMER2);
+ }
+
+ @RuleAction(label = "setTimer2Enabled", description = "sets the enabled state of timer2")
+ public void setTimer2Enabled(@ActionInput(name = "enabled", type = "java.lang.Boolean") @Nullable Boolean enabled) {
+ setEnabled(TIMER2, enabled);
+ }
+
+ @RuleAction(label = "getTimer3Departure", description = "returns the departure time of timer3")
+ public @ActionOutput(name = "time", type = "java.util.Optional") Optional getTimer3Departure() {
+ return getTime(TIMER3);
+ }
+
+ @RuleAction(label = "setTimer3Departure", description = "sets the timer3 departure time")
+ public void setTimer3Departure(@ActionInput(name = "time", type = "java.time.LocalTime") @Nullable LocalTime time) {
+ setTime(TIMER3, time);
+ }
+
+ @RuleAction(label = "getTimer3Enabled", description = "returns the enabled state of timer3")
+ public @ActionOutput(name = "enabled", type = "java.util.Optional") Optional getTimer3Enabled() {
+ return getEnabled(TIMER3);
+ }
+
+ @RuleAction(label = "setTimer3Enabled", description = "sets the enabled state of timer3")
+ public void setTimer3Enabled(@ActionInput(name = "enabled", type = "java.lang.Boolean") @Nullable Boolean enabled) {
+ setEnabled(TIMER3, enabled);
+ }
+
+ @RuleAction(label = "getOverrideTimerDeparture", description = "returns the departure time of overrideTimer")
+ public @ActionOutput(name = "time", type = "java.util.Optional") Optional getOverrideTimerDeparture() {
+ return getTime(OVERRIDE);
+ }
+
+ @RuleAction(label = "setOverrideTimerDeparture", description = "sets the overrideTimer departure time")
+ public void setOverrideTimerDeparture(
+ @ActionInput(name = "time", type = "java.time.LocalTime") @Nullable LocalTime time) {
+ setTime(OVERRIDE, time);
+ }
+
+ @RuleAction(label = "getOverrideTimerEnabled", description = "returns the enabled state of overrideTimer")
+ public @ActionOutput(name = "enabled", type = "java.util.Optional") Optional getOverrideTimerEnabled() {
+ return getEnabled(OVERRIDE);
+ }
+
+ @RuleAction(label = "setOverrideTimerEnabled", description = "sets the enabled state of overrideTimer")
+ public void setOverrideTimerEnabled(
+ @ActionInput(name = "enabled", type = "java.lang.Boolean") @Nullable Boolean enabled) {
+ setEnabled(OVERRIDE, enabled);
+ }
+
+ @RuleAction(label = "getPreferredWindowStart", description = "returns the preferred charging-window start time")
+ public @ActionOutput(name = "time", type = "java.util.Optional") Optional getPreferredWindowStart() {
+ return getTime(WINDOWSTART);
+ }
+
+ @RuleAction(label = "setPreferredWindowStart", description = "sets the preferred charging-window start time")
+ public void setPreferredWindowStart(
+ @ActionInput(name = "time", type = "java.time.LocalTime") @Nullable LocalTime time) {
+ setTime(WINDOWSTART, time);
+ }
+
+ @RuleAction(label = "getPreferredWindowEnd", description = "returns the preferred charging-window end time")
+ public @ActionOutput(name = "time", type = "java.util.Optional") Optional getPreferredWindowEnd() {
+ return getTime(WINDOWEND);
+ }
+
+ @RuleAction(label = "setPreferredWindowEnd", description = "sets the preferred charging-window end time")
+ public void setPreferredWindowEnd(
+ @ActionInput(name = "time", type = "java.time.LocalTime") @Nullable LocalTime time) {
+ setTime(WINDOWEND, time);
+ }
+
+ @RuleAction(label = "getClimatizationEnabled", description = "returns the enabled state of climatization")
+ public @ActionOutput(name = "enabled", type = "java.util.Optional") Optional getClimatizationEnabled() {
+ return getEnabled(CLIMATE);
+ }
+
+ @RuleAction(label = "setClimatizationEnabled", description = "sets the enabled state of climatization")
+ public void setClimatizationEnabled(
+ @ActionInput(name = "enabled", type = "java.lang.Boolean") @Nullable Boolean enabled) {
+ setEnabled(CLIMATE, enabled);
+ }
+
+ @RuleAction(label = "getChargingMode", description = "gets the charging-mode")
+ public @ActionOutput(name = "mode", type = "java.util.Optional") Optional getChargingMode() {
+ return getProfile().map(profile -> profile.getMode());
+ }
+
+ @RuleAction(label = "setChargingMode", description = "sets the charging-mode")
+ public void setChargingMode(@ActionInput(name = "mode", type = "java.lang.String") @Nullable String mode) {
+ getProfile().ifPresent(profile -> profile.setMode(mode));
+ }
+
+ @RuleAction(label = "getTimer1Days", description = "returns the days of week timer1 is enabled for")
+ public @ActionOutput(name = "days", type = "java.util.Optional>") Optional> getTimer1Days() {
+ return getDays(TIMER1);
+ }
+
+ @RuleAction(label = "setTimer1Days", description = "sets the days of week timer1 is enabled for")
+ public void setTimer1Days(
+ @ActionInput(name = "days", type = "java.util.Set") @Nullable Set days) {
+ setDays(TIMER1, days);
+ }
+
+ @RuleAction(label = "getTimer2Days", description = "returns the days of week timer2 is enabled for")
+ public @ActionOutput(name = "days", type = "java.util.Optional>") Optional> getTimer2Days() {
+ return getDays(TIMER2);
+ }
+
+ @RuleAction(label = "setTimer2Days", description = "sets the days of week timer2 is enabled for")
+ public void setTimer2Days(
+ @ActionInput(name = "days", type = "java.util.Set") @Nullable Set days) {
+ setDays(TIMER2, days);
+ }
+
+ @RuleAction(label = "getTimer3Days", description = "returns the days of week timer3 is enabled for")
+ public @ActionOutput(name = "days", type = "java.util.Optional>") Optional> getTimer3Days() {
+ return getDays(TIMER3);
+ }
+
+ @RuleAction(label = "setTimer3Days", description = "sets the days of week timer3 is enabled for")
+ public void setTimer3Days(
+ @ActionInput(name = "days", type = "java.util.Set") @Nullable Set days) {
+ setDays(TIMER3, days);
+ }
+
+ @RuleAction(label = "getOverrideTimerDays", description = "returns the days of week the overrideTimer is enabled for")
+ public @ActionOutput(name = "days", type = "java.util.Optional>") Optional> getOverrideTimerDays() {
+ return getDays(OVERRIDE);
+ }
+
+ @RuleAction(label = "setOverrideTimerDays", description = "sets the days of week the overrideTimer is enabled for")
+ public void setOverrideTimerDays(
+ @ActionInput(name = "days", type = "java.util.Set") @Nullable Set days) {
+ setDays(OVERRIDE, days);
+ }
+
+ @RuleAction(label = "sendChargeProfile", description = "sends the charging profile to the vehicle")
+ public void sendChargeProfile() {
+ handler.ifPresent(handle -> handle.sendChargeProfile(getProfile()));
+ }
+
+ @RuleAction(label = "cancel", description = "cancel current edit of charging profile")
+ public void cancelEditChargeProfile() {
+ profile = Optional.empty();
+ }
+
+ public static Optional getTimer1Departure(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getTimer1Departure();
+ }
+
+ public static void setTimer1Departure(ThingActions actions, @Nullable LocalTime time) {
+ ((BMWConnectedDriveActions) actions).setTimer1Departure(time);
+ }
+
+ public static Optional getTimer1Enabled(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getTimer1Enabled();
+ }
+
+ public static void setTimer1Enabled(ThingActions actions, @Nullable Boolean enabled) {
+ ((BMWConnectedDriveActions) actions).setTimer1Enabled(enabled);
+ }
+
+ public static Optional getTimer2Departure(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getTimer2Departure();
+ }
+
+ public static void setTimer2Departure(ThingActions actions, @Nullable LocalTime time) {
+ ((BMWConnectedDriveActions) actions).setTimer2Departure(time);
+ }
+
+ public static Optional getTimer2Enabled(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getTimer2Enabled();
+ }
+
+ public static void setTimer2Enabled(ThingActions actions, @Nullable Boolean enabled) {
+ ((BMWConnectedDriveActions) actions).setTimer2Enabled(enabled);
+ }
+
+ public static Optional getTimer3Departure(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getTimer3Departure();
+ }
+
+ public static void setTimer3Departure(ThingActions actions, @Nullable LocalTime time) {
+ ((BMWConnectedDriveActions) actions).setTimer3Departure(time);
+ }
+
+ public static Optional getTimer3Enabled(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getTimer3Enabled();
+ }
+
+ public static void setTimer3Enabled(ThingActions actions, @Nullable Boolean enabled) {
+ ((BMWConnectedDriveActions) actions).setTimer3Enabled(enabled);
+ }
+
+ public static Optional getOverrideTimerDeparture(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getOverrideTimerDeparture();
+ }
+
+ public static void setOverrideTimerDeparture(ThingActions actions, @Nullable LocalTime time) {
+ ((BMWConnectedDriveActions) actions).setOverrideTimerDeparture(time);
+ }
+
+ public static Optional getOverrideTimerEnabled(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getOverrideTimerEnabled();
+ }
+
+ public static void setOverrideTimerEnabled(ThingActions actions, @Nullable Boolean enabled) {
+ ((BMWConnectedDriveActions) actions).setOverrideTimerEnabled(enabled);
+ }
+
+ public static Optional getPreferredWindowStart(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getPreferredWindowStart();
+ }
+
+ public static void setPreferredWindowStart(ThingActions actions, @Nullable LocalTime time) {
+ ((BMWConnectedDriveActions) actions).setPreferredWindowStart(time);
+ }
+
+ public static Optional getPreferredWindowEnd(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getPreferredWindowEnd();
+ }
+
+ public static void setPreferredWindowEnd(ThingActions actions, @Nullable LocalTime time) {
+ ((BMWConnectedDriveActions) actions).setPreferredWindowEnd(time);
+ }
+
+ public static Optional getClimatizationEnabled(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getClimatizationEnabled();
+ }
+
+ public static void setClimatizationEnabled(ThingActions actions, @Nullable Boolean enabled) {
+ ((BMWConnectedDriveActions) actions).setClimatizationEnabled(enabled);
+ }
+
+ public static Optional getChargingMode(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getChargingMode();
+ }
+
+ public static void setChargingMode(ThingActions actions, @Nullable String mode) {
+ ((BMWConnectedDriveActions) actions).setChargingMode(mode);
+ }
+
+ public static Optional> getTimer1Days(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getTimer1Days();
+ }
+
+ public static void setTimer1Days(ThingActions actions, @Nullable Set days) {
+ ((BMWConnectedDriveActions) actions).setTimer1Days(days);
+ }
+
+ public static Optional> getTimer2Days(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getTimer2Days();
+ }
+
+ public static void setTimer2Days(ThingActions actions, @Nullable Set days) {
+ ((BMWConnectedDriveActions) actions).setTimer2Days(days);
+ }
+
+ public static Optional> getTimer3Days(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getTimer3Days();
+ }
+
+ public static void setTimer3Days(ThingActions actions, @Nullable Set days) {
+ ((BMWConnectedDriveActions) actions).setTimer3Days(days);
+ }
+
+ public static Optional> getOverrideTimerDays(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getOverrideTimerDays();
+ }
+
+ public static void setOverrideTimerDays(ThingActions actions, @Nullable Set days) {
+ ((BMWConnectedDriveActions) actions).setOverrideTimerDays(days);
+ }
+
+ public static void sendChargeProfile(ThingActions actions) {
+ ((BMWConnectedDriveActions) actions).sendChargeProfile();
+ }
+
+ public static void cancelEditChargeProfile(ThingActions actions) {
+ ((BMWConnectedDriveActions) actions).cancelEditChargeProfile();
+ }
+
+ @Override
+ public void setThingHandler(@Nullable ThingHandler handler) {
+ if (handler instanceof VehicleHandler) {
+ this.handler = Optional.of((VehicleHandler) handler);
+ }
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return handler.get();
+ }
+
+ private Optional getProfile() {
+ if (profile.isEmpty()) {
+ profile = handler.flatMap(handle -> handle.getChargeProfileWrapper());
+ }
+ return profile;
+ }
+
+ private Optional getTime(ProfileKey key) {
+ return getProfile().map(profile -> profile.getTime(key));
+ }
+
+ private void setTime(ProfileKey key, @Nullable LocalTime time) {
+ getProfile().ifPresent(profile -> profile.setTime(key, time));
+ }
+
+ private Optional getEnabled(ProfileKey key) {
+ return getProfile().map(profile -> profile.isEnabled(key));
+ }
+
+ private void setEnabled(ProfileKey key, @Nullable Boolean enabled) {
+ getProfile().ifPresent(profile -> profile.setEnabled(key, enabled));
+ }
+
+ private Optional> getDays(ProfileKey key) {
+ return getProfile().map(profile -> profile.getDays(key));
+ }
+
+ private void setDays(ProfileKey key, @Nullable Set days) {
+ getProfile().ifPresent(profile -> {
+ profile.setDays(key, days);
+ });
+ }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/discovery/VehicleDiscovery.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/discovery/VehicleDiscovery.java
new file mode 100644
index 000000000..55c66b5b4
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/discovery/VehicleDiscovery.java
@@ -0,0 +1,181 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.discovery;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.SUPPORTED_THING_SET;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.discovery.VehiclesContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.ConnectedDriveBridgeHandler;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+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 ConnectedDrive and is identifying the Vehicles after response
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class VehicleDiscovery extends AbstractDiscoveryService implements DiscoveryService, ThingHandlerService {
+
+ private final Logger logger = LoggerFactory.getLogger(VehicleDiscovery.class);
+ private static final int DISCOVERY_TIMEOUT = 10;
+ private Optional bridgeHandler = Optional.empty();
+
+ public VehicleDiscovery() {
+ super(SUPPORTED_THING_SET, DISCOVERY_TIMEOUT, false);
+ }
+
+ public void onResponse(VehiclesContainer container) {
+ bridgeHandler.ifPresent(bridge -> {
+ final ThingUID bridgeUID = bridge.getThing().getUID();
+ container.vehicles.forEach(vehicle -> {
+ // the DriveTrain field in the delivered json is defining the Vehicle Type
+ String vehicleType = vehicle.driveTrain.toLowerCase();
+ SUPPORTED_THING_SET.forEach(entry -> {
+ if (entry.getId().equals(vehicleType)) {
+ ThingUID uid = new ThingUID(entry, vehicle.vin, bridgeUID.getId());
+ Map properties = new HashMap<>();
+ // Dealer
+ if (vehicle.dealer != null) {
+ properties.put("dealer", vehicle.dealer.name);
+ properties.put("dealerAddress", vehicle.dealer.street + " " + vehicle.dealer.country + " "
+ + vehicle.dealer.postalCode + " " + vehicle.dealer.city);
+ properties.put("dealerPhone", vehicle.dealer.phone);
+ }
+
+ // Services & Support
+ properties.put("servicesActivated", getObject(vehicle, Constants.ACTIVATED));
+ String servicesSupported = getObject(vehicle, Constants.SUPPORTED);
+ String servicesNotSupported = getObject(vehicle, Constants.NOT_SUPPORTED);
+ if (vehicle.statisticsAvailable) {
+ servicesSupported += Constants.STATISTICS;
+ } else {
+ servicesNotSupported += Constants.STATISTICS;
+ }
+ properties.put(Constants.SERVICES_SUPPORTED, servicesSupported);
+ properties.put("servicesNotSupported", servicesNotSupported);
+ properties.put("supportBreakdownNumber", vehicle.breakdownNumber);
+
+ // Vehicle Properties
+ if (vehicle.supportedChargingModes != null) {
+ properties.put("vehicleChargeModes",
+ String.join(Constants.SPACE, vehicle.supportedChargingModes));
+ }
+ if (vehicle.hasAlarmSystem) {
+ properties.put("vehicleAlarmSystem", "Available");
+ } else {
+ properties.put("vehicleAlarmSystem", "Not Available");
+ }
+ properties.put("vehicleBrand", vehicle.brand);
+ properties.put("vehicleBodytype", vehicle.bodytype);
+ properties.put("vehicleColor", vehicle.color);
+ properties.put("vehicleConstructionYear", Short.toString(vehicle.yearOfConstruction));
+ properties.put("vehicleDriveTrain", vehicle.driveTrain);
+ properties.put("vehicleModel", vehicle.model);
+ if (vehicle.chargingControl != null) {
+ properties.put("vehicleChargeControl", Converter.toTitleCase(vehicle.model));
+ }
+
+ // Update Properties for already created Things
+ bridge.getThing().getThings().forEach(vehicleThing -> {
+ Configuration c = vehicleThing.getConfiguration();
+ if (c.containsKey(ConnectedDriveConstants.VIN)) {
+ String thingVIN = c.get(ConnectedDriveConstants.VIN).toString();
+ if (vehicle.vin.equals(thingVIN)) {
+ vehicleThing.setProperties(properties);
+ }
+ }
+ });
+
+ // Properties needed for functional THing
+ properties.put(ConnectedDriveConstants.VIN, vehicle.vin);
+ properties.put("refreshInterval",
+ Integer.toString(ConnectedDriveConstants.DEFAULT_REFRESH_INTERVAL_MINUTES));
+ properties.put("units", ConnectedDriveConstants.UNITS_AUTODETECT);
+ properties.put("imageSize", Integer.toString(ConnectedDriveConstants.DEFAULT_IMAGE_SIZE_PX));
+ properties.put("imageViewport", ConnectedDriveConstants.DEFAULT_IMAGE_VIEWPORT);
+
+ String vehicleLabel = vehicle.brand + " " + vehicle.model;
+ Map convertedProperties = new HashMap(properties);
+ thingDiscovered(DiscoveryResultBuilder.create(uid).withBridge(bridgeUID)
+ .withRepresentationProperty(ConnectedDriveConstants.VIN).withLabel(vehicleLabel)
+ .withProperties(convertedProperties).build());
+ }
+ });
+ });
+ });
+ };
+
+ /**
+ * 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 String getObject(Object dto, String compare) {
+ StringBuilder buf = new StringBuilder();
+ for (Field field : dto.getClass().getDeclaredFields()) {
+ try {
+ Object value = field.get(dto);
+ if (compare.equals(value)) {
+ buf.append(Converter.capitalizeFirst(field.getName()) + Constants.SPACE);
+ }
+ } catch (IllegalArgumentException | IllegalAccessException e) {
+ logger.debug("Field {} not found {}", compare, e.getMessage());
+ }
+ }
+ return buf.toString();
+ }
+
+ @Override
+ public void setThingHandler(ThingHandler handler) {
+ if (handler instanceof ConnectedDriveBridgeHandler) {
+ bridgeHandler = Optional.of((ConnectedDriveBridgeHandler) handler);
+ bridgeHandler.get().setDiscoveryService(this);
+ }
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return bridgeHandler.orElse(null);
+ }
+
+ @Override
+ protected void startScan() {
+ bridgeHandler.ifPresent(ConnectedDriveBridgeHandler::requestVehicles);
+ }
+
+ @Override
+ public void deactivate() {
+ super.deactivate();
+ }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/Destination.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/Destination.java
new file mode 100644
index 000000000..e9d653a8e
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/Destination.java
@@ -0,0 +1,60 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.utils.Constants.*;
+
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+
+/**
+ * The {@link Destination} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Destination {
+ public float lat;
+ public float lon;
+ public String country;
+ public String city;
+ public String street;
+ public String streetNumber;
+ public String type;
+ public String createdAt;
+
+ public String getAddress() {
+ StringBuilder buf = new StringBuilder();
+ if (street != null) {
+ buf.append(street);
+ if (streetNumber != null) {
+ buf.append(SPACE).append(streetNumber);
+ }
+ }
+ if (city != null) {
+ if (buf.length() > 0) {
+ buf.append(COMMA).append(SPACE).append(city);
+ } else {
+ buf.append(city);
+ }
+ }
+ if (buf.length() == 0) {
+ return UNDEF;
+ } else {
+ return Converter.toTitleCase(buf.toString());
+ }
+ }
+
+ public String getCoordinates() {
+ return lat + Constants.COMMA + lon;
+ }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/DestinationContainer.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/DestinationContainer.java
new file mode 100644
index 000000000..882d7c0d2
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/DestinationContainer.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto;
+
+import java.util.List;
+
+/**
+ * The {@link DestinationContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class DestinationContainer {
+ public List destinations;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/NetworkError.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/NetworkError.java
new file mode 100644
index 000000000..d751a73c6
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/NetworkError.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto;
+
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.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.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/auth/AuthResponse.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/auth/AuthResponse.java
new file mode 100644
index 000000000..7363d4989
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/auth/AuthResponse.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.auth;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link Timer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class AuthResponse {
+ @SerializedName("access_token")
+ public String accessToken;
+ @SerializedName("token_type")
+ public String tokenType;
+ @SerializedName("expires_in")
+ public int expiresIn;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/charge/ChargeProfile.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/charge/ChargeProfile.java
new file mode 100644
index 000000000..40da7f895
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/charge/ChargeProfile.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.charge;
+
+/**
+ * The {@link ChargeProfile} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - edit & send of charge profile
+ */
+public class ChargeProfile {
+ public WeeklyPlanner weeklyPlanner;
+ public WeeklyPlanner twoTimesTimer;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/charge/ChargingWindow.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/charge/ChargingWindow.java
new file mode 100644
index 000000000..b303ddd24
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/charge/ChargingWindow.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.charge;
+
+/**
+ * The {@link ChargingWindow} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class ChargingWindow {
+ public String startTime;// ":"11:00",
+ public String endTime;// ":"17:00"}}
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/charge/Timer.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/charge/Timer.java
new file mode 100644
index 000000000..65a3f23c8
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/charge/Timer.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.charge;
+
+import java.util.List;
+
+/**
+ * The {@link Timer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - edit & send of charge profile
+ */
+public class Timer {
+ public String departureTime;// ": "05:00",
+ public Boolean timerEnabled;// ": false,
+ public List weekdays;
+ /**
+ * "MONDAY",
+ * "TUESDAY",
+ * "WEDNESDAY",
+ * "THURSDAY",
+ * "FRIDAY"
+ * ] '
+ */
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/charge/WeeklyPlanner.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/charge/WeeklyPlanner.java
new file mode 100644
index 000000000..c169e1a28
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/charge/WeeklyPlanner.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.charge;
+
+/**
+ * The {@link WeeklyPlanner} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - edit & send of charge profile
+ */
+public class WeeklyPlanner {
+ public Boolean climatizationEnabled; // ": true,
+ public String chargingMode;// ": "IMMEDIATE_CHARGING",
+ public String chargingPreferences; // ": "CHARGING_WINDOW",
+ public Timer timer1; // : {
+ public Timer timer2;// ": {
+ public Timer timer3;// ":{"departureTime":"00:00","timerEnabled":false,"weekdays":[]},"
+ public Timer overrideTimer;// ":{"departureTime":"12:00","timerEnabled":false,"weekdays":["SATURDAY"]},"
+ public ChargingWindow preferredChargingWindow;// ":{"startTime":"11:00","endTime":"17:00"}}
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/CBSMessageCompat.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/CBSMessageCompat.java
new file mode 100644
index 000000000..39677a381
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/CBSMessageCompat.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.compat;
+
+/**
+ * The {@link CBSMessageCompat} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CBSMessageCompat {
+ public String description; // "Nächster Wechsel spätestens zum angegebenen Termin.",
+ public String text; // "Bremsflüssigkeit",
+ public int id; // 3,
+ public String status; // "OK",
+ public String messageType; // "CBS",
+ public String date; // "2021-11"
+ public int unitOfLengthRemaining; // ": "2000"
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/CCMMessageCompat.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/CCMMessageCompat.java
new file mode 100644
index 000000000..5e0aafc73
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/CCMMessageCompat.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.compat;
+
+/**
+ * The {@link CCMMessageCompat} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CCMMessageCompat {
+ public String text;// "Laden nicht möglich"
+ public int id;// 804,
+ public String status;// "NULL",
+ public String messageType;// "CCM",
+ public int unitOfLengthRemaining = -1; // "18312"
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/VehicleAttributes.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/VehicleAttributes.java
new file mode 100644
index 000000000..72cdde1e7
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/VehicleAttributes.java
@@ -0,0 +1,142 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.compat;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link VehicleAttributes} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class VehicleAttributes {
+ // Windows & Doors
+ @SerializedName("door_driver_front")
+ public String doorDriverFront;// "CLOSED",
+ @SerializedName("door_driver_rear")
+ public String doorDriverRear;// "CLOSED",
+ @SerializedName("door_passenger_front")
+ public String doorPassengerFront;// "CLOSED",
+ @SerializedName("door_passenger_rear")
+ public String doorPassengerRear;// "CLOSED",
+ @SerializedName("hood_state")
+ public String hoodState;// "CLOSED",
+ @SerializedName("trunk_state")
+ public String trunkState;// "CLOSED",
+ @SerializedName("window_driver_front")
+ public String winDriverFront;// "CLOSED",
+ @SerializedName("window_driver_rear")
+ public String winDriverRear;// "CLOSED",
+ @SerializedName("window_passenger_front")
+ public String winPassengerFront;// "CLOSED",
+ @SerializedName("window_passenger_rear")
+ public String winPassengerRear;// "CLOSED",
+ @SerializedName("sunroof_state")
+ public String sunroofState;// "CLOSED",
+ @SerializedName("door_lock_state")
+ public String doorLockState;// "SECURED",
+ public String shdStatusUnified;// "CLOSED",
+
+ // Charge Status
+ public String chargingHVStatus;// "INVALID",
+ public String lastChargingEndReason;// "CHARGING_GOAL_REACHED",
+ public String connectorStatus;// "DISCONNECTED",
+ public String chargingLogicCurrentlyActive;// "NOT_CHARGING",
+ public String chargeNowAllowed;// "NOT_ALLOWED",
+ @SerializedName("charging_status")
+ public String chargingStatus;// "NOCHARGING",
+ public String lastChargingEndResult;// "SUCCESS",
+ public String chargingSystemStatus;// "NOCHARGING",
+ public String lastUpdateReason;// "VEHCSHUTDOWN_SECURED"
+
+ // Range
+ public int mileage;// "17236",
+ public double beMaxRangeElectric;// "209.0",
+ public double beMaxRangeElectricKm;// "209.0",
+ public double beRemainingRangeElectric;// "179.0",
+ public double beRemainingRangeElectricKm;// "179.0",
+ public double beMaxRangeElectricMile;// "129.0",
+ public double beRemainingRangeElectricMile;// "111.0",
+ public double beRemainingRangeFuelKm;// "67.0",
+ public double beRemainingRangeFuelMile;// "41.0",
+ public double beRemainingRangeFuel;// "67.0",
+ @SerializedName("kombi_current_remaining_range_fuel")
+ public double kombiRemainingRangeFuel;// "67.0",
+
+ public double chargingLevelHv;// "89.0",
+ @SerializedName("soc_hv_percent")
+ public double socHvPercent;// "82.6",
+ @SerializedName("remaining_fuel")
+ public double remainingFuel;// "4",
+ public double fuelPercent;// "47",
+
+ // Last Status update
+ public String updateTime;// "22.08.2020 12:55:46 UTC",
+ @SerializedName("updateTime_converted")
+ public String updateTimeConverted;// "22.08.2020 13:55",
+ @SerializedName("updateTime_converted_date")
+ public String updateTimeConvertedDate;// "22.08.2020",
+ @SerializedName("updateTime_converted_time")
+ public String updateTimeConvertedTime;// "13:55",
+ @SerializedName("updateTime_converted_timestamp")
+ public String updateTimeConvertedTimestamp;// "1598104546000",
+
+ // Last Trip Update
+ @SerializedName("Segment_LastTrip_time_segment_end")
+ public String lastTripEnd;// "22.08.2020 14:52:00 UTC",
+ @SerializedName("Segment_LastTrip_time_segment_end_formatted")
+ public String lastTripEndFormatted;// "22.08.2020 14:52",
+ @SerializedName("Segment_LastTrip_time_segment_end_formatted_date")
+ public String lastTripEndFormattedDate;// "22.08.2020",
+ @SerializedName("Segment_LastTrip_time_segment_end_formatted_time")
+ public String lastTripEndFormattedTime;// "14:52",
+
+ // Location
+ @SerializedName("gps_lat")
+ public float gpsLat;// "43.21",
+ @SerializedName("gps_lng")
+ public float gpsLon;// "8.765",
+ public int heading;// "41",
+
+ public String unitOfLength;// "km",
+ public String unitOfEnergy;// "kWh",
+ @SerializedName("vehicle_tracking")
+ public String vehicleTracking;// "1",
+ @SerializedName("head_unit_pu_software")
+ public String headunitSoftware;// "07/16",
+ @SerializedName("check_control_messages")
+ public String checkControlMessages;// "",
+ @SerializedName("sunroof_position")
+ public String sunroofPosition;// "0",
+ @SerializedName("single_immediate_charging")
+ public String singleImmediateCharging;// "isUnused",
+ public String unitOfCombustionConsumption;// "l/100km",
+ @SerializedName("Segment_LastTrip_ratio_electric_driven_distance")
+ public String lastTripElectricRation;// "100",
+ @SerializedName("condition_based_services")
+ public String conditionBasedServices;// "00003,OK,2021-11,;00017,OK,2021-11,;00001,OK,2021-11,;00032,OK,2021-11,",
+ @SerializedName("charging_inductive_positioning")
+ public String chargingInductivePositioning;// "not_positioned",
+ @SerializedName("lsc_trigger")
+ public String lscTrigger;// "VEHCSHUTDOWN_SECURED",
+ @SerializedName("lights_parking")
+ public String lightsParking;// "OFF",
+ public String prognosisWhileChargingStatus;// "NOT_NEEDED",
+ @SerializedName("head_unit")
+ public String headunit;// "EntryNav",
+ @SerializedName("battery_size_max")
+ public String batterySizeMax;// "33200",
+ @SerializedName("charging_connection_type")
+ public String chargingConnectionType;// "CONDUCTIVE",
+ public String unitOfElectricConsumption;// "kWh/100km",
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/VehicleAttributesContainer.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/VehicleAttributesContainer.java
new file mode 100644
index 000000000..3e8087f6e
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/VehicleAttributesContainer.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.compat;
+
+/**
+ * The {@link VehicleAttributesContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class VehicleAttributesContainer {
+ public VehicleAttributes attributesMap;
+ public VehicleMessages vehicleMessages;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/VehicleMessages.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/VehicleMessages.java
new file mode 100644
index 000000000..7b7613500
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/VehicleMessages.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.compat;
+
+import java.util.List;
+
+/**
+ * The {@link VehicleMessages} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @param
+ */
+public class VehicleMessages {
+ public List ccmMessages;
+ public List cbsMessages;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/discovery/Dealer.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/discovery/Dealer.java
new file mode 100644
index 000000000..1b1392bae
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/discovery/Dealer.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.discovery;
+
+/**
+ * The {@link Dealer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Dealer {
+ public String name;
+ public String street;
+ public String postalCode;
+ public String city;
+ public String country;
+ public String phone;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/discovery/Vehicle.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/discovery/Vehicle.java
new file mode 100644
index 000000000..f963b7d7f
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/discovery/Vehicle.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.discovery;
+
+import java.util.List;
+
+/**
+ * The {@link Vehicle} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Vehicle {
+ public String vin;
+ public String model;
+ public String driveTrain;
+ public String brand;
+ public short yearOfConstruction;
+ public String bodytype;
+ public String color;
+ public boolean statisticsCommunityEnabled;
+ public boolean statisticsAvailable;
+ public boolean hasAlarmSystem;
+ public Dealer dealer;
+ public String breakdownNumber;
+ public List supportedChargingModes;
+ public String chargingControl;// ": "WEEKLY_PLANNER",
+
+ // Remote Services
+ public String vehicleFinder; // ACTIVATED
+ public String hornBlow; // ACTIVATED
+ public String lightFlash; // ACTIVATED
+ public String doorLock; // ACTIVATED
+ public String doorUnlock; // ACTIVATED
+ public String climateNow; // ACTIVATED
+ public String sendPoi; // ACTIVATED
+
+ public String remote360; // SUPPORTED
+ public String climateControl; // SUPPORTED
+ public String chargeNow; // SUPPORTED
+ public String lastDestinations; // SUPPORTED
+ public String carCloud; // SUPPORTED
+ public String remoteSoftwareUpgrade; // SUPPORTED
+
+ public String climateNowRES;// ": "NOT_SUPPORTED",
+ public String climateControlRES;// ": "NOT_SUPPORTED",
+ public String smartSolution;// ": "NOT_SUPPORTED",
+ public String ipa;// ": "NOT_SUPPORTED",
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/discovery/VehiclesContainer.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/discovery/VehiclesContainer.java
new file mode 100644
index 000000000..9c7a1a78d
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/discovery/VehiclesContainer.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.discovery;
+
+import java.util.List;
+
+/**
+ * The {@link VehiclesContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class VehiclesContainer {
+ public List vehicles;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/remote/ExecutionStatus.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/remote/ExecutionStatus.java
new file mode 100644
index 000000000..4f80ac78d
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/remote/ExecutionStatus.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.remote;
+
+/**
+ * The {@link ExecutionStatus} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class ExecutionStatus {
+ public String serviceType;// ": "DOOR_UNLOCK",
+ public String status;// ": "EXECUTED",
+ public String eventId;// ": "5639303536333926DA7B9400@bmw.de",
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/remote/ExecutionStatusContainer.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/remote/ExecutionStatusContainer.java
new file mode 100644
index 000000000..eca56f1dd
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/remote/ExecutionStatusContainer.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.remote;
+
+/**
+ * The {@link ExecutionStatusContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class ExecutionStatusContainer {
+ public ExecutionStatus executionStatus;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/AllTrips.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/AllTrips.java
new file mode 100644
index 000000000..c2417e5a0
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/AllTrips.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.statistics;
+
+/**
+ * The {@link AllTrips} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class AllTrips {
+ public CommunityPowerEntry avgElectricConsumption;
+ public CommunityPowerEntry avgRecuperation;
+ public CommunityChargeCycleEntry chargecycleRange;
+ public CommunityEletricDistanceEntry totalElectricDistance;
+ public CommunityPowerEntry avgCombinedConsumption;
+ public float savedCO2;// ":461.083,"
+ public float savedCO2greenEnergy;// ":2712.255,"
+ public float totalSavedFuel;// ":0,"
+ public String resetDate;// ":"2020-08-24T14:40:40+0000","
+ public int batterySizeMax;// ":33200
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/AllTripsContainer.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/AllTripsContainer.java
new file mode 100644
index 000000000..607b79ba1
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/AllTripsContainer.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.statistics;
+
+/**
+ * The {@link AllTripsContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class AllTripsContainer {
+ public AllTrips allTrips;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/CommunityChargeCycleEntry.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/CommunityChargeCycleEntry.java
new file mode 100644
index 000000000..a2e4b6aa1
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/CommunityChargeCycleEntry.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.statistics;
+
+/**
+ * The {@link CommunityChargeCycleEntry} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CommunityChargeCycleEntry {
+ public float communityAverage;// ": 194.21,
+ public float communityHigh;// ": 270,
+ public float userAverage;// ": 57.3,
+ public float userHigh;// ": 185.48,
+ public float userCurrentChargeCycle;// ": 68
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/CommunityEletricDistanceEntry.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/CommunityEletricDistanceEntry.java
new file mode 100644
index 000000000..6c525fdad
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/CommunityEletricDistanceEntry.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.statistics;
+
+/**
+ * The {@link CommunityEletricDistanceEntry} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CommunityEletricDistanceEntry {
+ public float communityLow;// ": 19,
+ public float communityAverage;// ": 40850.56,
+ public float communityHigh;// ": 193006,
+ public float userTotal;// ": 16629.4
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/CommunityPowerEntry.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/CommunityPowerEntry.java
new file mode 100644
index 000000000..2e4f57e12
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/CommunityPowerEntry.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.statistics;
+
+/**
+ * The {@link CommunityPowerEntry} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CommunityPowerEntry {
+ public float communityLow;// ": 11.05,
+ public float communityAverage;// ": 16.28,
+ public float communityHigh;// ": 21.99,
+ public float userAverage;// ": 16.46
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/LastTrip.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/LastTrip.java
new file mode 100644
index 000000000..69f7106c3
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/LastTrip.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.statistics;
+
+/**
+ * The {@link LastTrip} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class LastTrip {
+ public float efficiencyValue;// ": 0.98,
+ public float totalDistance;// ": 2,
+ public float electricDistance;// ": 2,
+ public float avgElectricConsumption;// ": 7,
+ public float avgRecuperation;// ": 6,
+ public float drivingModeValue;// ": 0.87,
+ public float accelerationValue;// ": 0.99,
+ public float anticipationValue;// ": 0.99,
+ public float totalConsumptionValue;// ": 1.25,
+ public float auxiliaryConsumptionValue;// ": 0.78,
+ public float avgCombinedConsumption;// ": 0,
+ public float electricDistanceRatio;// ": 100,
+ public float savedFuel;// ": 0,
+ public String date;// ": "2020-08-24T17:55:00+0000",
+ public float duration;// ": 5
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/LastTripContainer.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/LastTripContainer.java
new file mode 100644
index 000000000..86e46c937
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/LastTripContainer.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.statistics;
+
+/**
+ * The {@link LastTripContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class LastTripContainer {
+ public LastTrip lastTrip;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/CBSMessage.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/CBSMessage.java
new file mode 100644
index 000000000..14e127ae3
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/CBSMessage.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.status;
+
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+
+/**
+ * The {@link CBSMessage} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CBSMessage {
+ public String cbsType;// ": "BRAKE_FLUID",
+ public String cbsState;// ": "OK",
+ public String cbsDueDate;// ": "2021-11",
+ public String cbsDescription;// ": "Next change due at the latest by the stated date."
+ public int cbsRemainingMileage = -1; // 46000
+
+ public String cbsTypeConverted = null;
+ public String cbsDescriptionConverted = null;
+
+ public String getDueDate() {
+ if (cbsDueDate == null) {
+ return Constants.NULL_DATE;
+ } else {
+ return cbsDueDate + Constants.UTC_APPENDIX;
+ }
+ }
+
+ public String getType() {
+ if (cbsTypeConverted == null) {
+ if (cbsType == null) {
+ cbsTypeConverted = Constants.INVALID;
+ } else {
+ cbsTypeConverted = Converter.toTitleCase(cbsType);
+ }
+ }
+ return cbsTypeConverted;
+ }
+
+ public String getDescription() {
+ if (cbsDescriptionConverted == null) {
+ if (cbsDescription == null) {
+ cbsDescriptionConverted = Constants.INVALID;
+ } else {
+ cbsDescriptionConverted = cbsDescription;
+ }
+ }
+ return cbsDescriptionConverted;
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder(cbsDueDate).append(Constants.HYPHEN).append(cbsRemainingMileage)
+ .append(Constants.HYPHEN).append(cbsType).toString();
+ }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/CCMMessage.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/CCMMessage.java
new file mode 100644
index 000000000..4d9a43d3b
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/CCMMessage.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.status;
+
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+/**
+ * The {@link CCMMessage} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CCMMessage {
+ // if necessary. Perform reset after adjustment. See Owner's Handbook for further
+ // information.",
+ public String ccmDescriptionShort = Constants.INVALID;// ": "Tyre pressure notification",
+ public String ccmDescriptionLong = Constants.INVALID;// ": "You can continue driving. Check tyre pressure when tyres
+ // are cold and adjust
+ public int ccmId = -1;// ": 955,
+ public int ccmMileage = -1;// ": 41544
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/Doors.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/Doors.java
new file mode 100644
index 000000000..afab4e2c6
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/Doors.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.status;
+
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+/**
+ * The {@link Doors} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Doors {
+ public String doorDriverFront = Constants.UNDEF;// ": "CLOSED",
+ public String doorDriverRear = Constants.UNDEF;// ": "CLOSED",
+ public String doorPassengerFront = Constants.UNDEF;// ": "CLOSED",
+ public String doorPassengerRear = Constants.UNDEF;// ": "CLOSED",
+ public String trunk = Constants.UNDEF;// ": "CLOSED",
+ public String hood = Constants.UNDEF;// ": "CLOSED",
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/Position.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/Position.java
new file mode 100644
index 000000000..ee46dd372
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/Position.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.status;
+
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+/**
+ * The {@link Position} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Position {
+ public float lat;// ": 46.55605,
+ public float lon;// ": 10.495669,
+ public int heading;// ": 219,
+ public String status;// ": "OK"
+
+ public String getCoordinates() {
+ return new StringBuilder(Float.toString(lat)).append(Constants.COMMA).append(Float.toString(lon)).toString();
+ }
+
+ @Override
+ public String toString() {
+ return getCoordinates();
+ }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/VehicleStatus.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/VehicleStatus.java
new file mode 100644
index 000000000..57119de59
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/VehicleStatus.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.status;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link VehicleStatus} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class VehicleStatus {
+ public int mileage = Constants.INT_UNDEF;// ": 17273,
+ public double remainingFuel = Constants.INT_UNDEF;// ": 4,
+ public double remainingRangeElectric = Constants.INT_UNDEF;// ": 148,
+ public double remainingRangeElectricMls;// ": 91,
+ public double remainingRangeFuel = Constants.INT_UNDEF;// ": 70,"
+ public double remainingRangeFuelMls;// ":43,"
+ public double maxRangeElectric = Constants.INT_UNDEF;// ":216,"
+ public double maxRangeElectricMls;// ":134,"
+ public double maxFuel;// ":8.5,
+ public double chargingLevelHv;// ":71,
+ public String vin;// : "ANONYMOUS",
+ public String updateReason;// ": "VEHICLE_SHUTDOWN_SECURED",
+ public String updateTime;// ": "2020-08-24 T15:55:32+0000",
+ public String doorDriverFront = Constants.UNDEF;// ": "CLOSED",
+ public String doorDriverRear = Constants.UNDEF;// ": "CLOSED",
+ public String doorPassengerFront = Constants.UNDEF;// ": "CLOSED",
+ public String doorPassengerRear = Constants.UNDEF;// ": "CLOSED",
+ public String windowDriverFront = Constants.UNDEF;// ": "CLOSED",
+ public String windowDriverRear = Constants.UNDEF;// ": "CLOSED",
+ public String windowPassengerFront = Constants.UNDEF;// ": "CLOSED",
+ public String windowPassengerRear = Constants.UNDEF;// ": "CLOSED",
+ public String sunroof = Constants.UNDEF;// ": "CLOSED",
+ public String trunk = Constants.UNDEF;// ": "CLOSED",
+ public String rearWindow = Constants.UNDEF;// ": "INVALID",
+ public String hood = Constants.UNDEF;// ": "CLOSED",
+ public String doorLockState;// ": "SECURED",
+ public String parkingLight;// ": "OFF",
+ public String positionLight;// ": "ON",
+ public String connectionStatus;// ": "DISCONNECTED",
+ public String chargingStatus;// ": "INVALID","
+ public String lastChargingEndReason;// ": "CHARGING_GOAL_REACHED",
+ public String lastChargingEndResult;// ": "SUCCESS","
+ public Double chargingTimeRemaining;// ": "45",
+ public Position position;
+ public String internalDataTimeUTC;// ": "2020-08-24 T15:55:32",
+ public boolean singleImmediateCharging;// ":false,
+ public String chargingConnectionType;// ": "CONDUCTIVE",
+ public String chargingInductivePositioning;// ": "NOT_POSITIONED",
+ public String vehicleCountry;// ": "DE","+"
+ @SerializedName("DCS_CCH_Activation")
+ public String dcsCchActivation;// ": "NA",
+ @SerializedName("DCS_CCH_Ongoing")
+ public boolean dcsCchOngoing;// ":false
+ public List checkControlMessages = new ArrayList();// ":[],
+ public List cbsData = new ArrayList();
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/VehicleStatusContainer.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/VehicleStatusContainer.java
new file mode 100644
index 000000000..673930bf6
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/VehicleStatusContainer.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.status;
+
+/**
+ * The {@link VehicleStatusContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class VehicleStatusContainer {
+ public VehicleStatus vehicleStatus;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/Windows.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/Windows.java
new file mode 100644
index 000000000..a530f31ea
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/Windows.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.status;
+
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+/**
+ * The {@link Windows} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Windows {
+ public String windowDriverFront = Constants.UNDEF;// ": "CLOSED",
+ public String windowDriverRear = Constants.UNDEF;// ": "CLOSED",
+ public String windowPassengerFront = Constants.UNDEF;// ": "CLOSED",
+ public String windowPassengerRear = Constants.UNDEF;// ": "CLOSED",
+ public String sunroof = Constants.UNDEF;// ": "CLOSED",
+ public String rearWindow = Constants.UNDEF;// ": "INVALID",
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/BMWConnectedDriveOptionProvider.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/BMWConnectedDriveOptionProvider.java
new file mode 100644
index 000000000..82690ea67
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/BMWConnectedDriveOptionProvider.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
+import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
+import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * Dynamic provider of state options while leaving other state description fields as original.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@Component(service = { DynamicStateDescriptionProvider.class, BMWConnectedDriveOptionProvider.class })
+@NonNullByDefault
+public class BMWConnectedDriveOptionProvider extends BaseDynamicStateDescriptionProvider {
+
+ @Reference
+ protected void setChannelTypeI18nLocalizationService(
+ final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
+ this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
+ }
+
+ protected void unsetChannelTypeI18nLocalizationService(
+ final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
+ this.channelTypeI18nLocalizationService = null;
+ }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ByteResponseCallback.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ByteResponseCallback.java
new file mode 100644
index 000000000..7aafc76db
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ByteResponseCallback.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.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.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveBridgeHandler.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveBridgeHandler.java
new file mode 100644
index 000000000..5c7ac485d
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveBridgeHandler.java
@@ -0,0 +1,213 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.utils.Constants.ANONYMOUS;
+
+import java.util.Collection;
+import java.util.Collections;
+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.bmwconnecteddrive.internal.ConnectedDriveConfiguration;
+import org.openhab.binding.bmwconnecteddrive.internal.discovery.VehicleDiscovery;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.discovery.Dealer;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.discovery.VehiclesContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.BimmerConstants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.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;
+
+import com.google.gson.JsonParseException;
+
+/**
+ * The {@link ConnectedDriveBridgeHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class ConnectedDriveBridgeHandler extends BaseBridgeHandler implements StringResponseCallback {
+ private final Logger logger = LoggerFactory.getLogger(ConnectedDriveBridgeHandler.class);
+ private HttpClientFactory httpClientFactory;
+ private Optional discoveryService = Optional.empty();
+ private Optional proxy = Optional.empty();
+ private Optional> initializerJob = Optional.empty();
+ private Optional troubleshootFingerprint = Optional.empty();
+
+ public ConnectedDriveBridgeHandler(Bridge bridge, HttpClientFactory hcf) {
+ super(bridge);
+ httpClientFactory = hcf;
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ // no commands available
+ }
+
+ @Override
+ public void initialize() {
+ troubleshootFingerprint = Optional.empty();
+ updateStatus(ThingStatus.UNKNOWN);
+ ConnectedDriveConfiguration config = getConfigAs(ConnectedDriveConfiguration.class);
+ if (!checkConfiguration(config)) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
+ } else {
+ proxy = Optional.of(new ConnectedDriveProxy(httpClientFactory, config));
+ // give the system some time to create all predefined Vehicles
+ // check with API call if bridge is online
+ initializerJob = Optional.of(scheduler.schedule(this::requestVehicles, 5, TimeUnit.SECONDS));
+ }
+ }
+
+ public static boolean checkConfiguration(ConnectedDriveConfiguration config) {
+ if (Constants.EMPTY.equals(config.userName) || Constants.EMPTY.equals(config.password)) {
+ return false;
+ } else if (BimmerConstants.AUTH_SERVER_MAP.containsKey(config.region)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void dispose() {
+ initializerJob.ifPresent(job -> job.cancel(true));
+ }
+
+ public void requestVehicles() {
+ proxy.ifPresent(prox -> prox.requestVehicles(this));
+ }
+
+ public String getDiscoveryFingerprint() {
+ return troubleshootFingerprint.map(fingerprint -> {
+ VehiclesContainer container = null;
+ try {
+ container = Converter.getGson().fromJson(fingerprint, VehiclesContainer.class);
+ if (container != null) {
+ if (container.vehicles != null) {
+ if (container.vehicles.isEmpty()) {
+ return Constants.EMPTY_JSON;
+ } else {
+ container.vehicles.forEach(entry -> {
+ entry.vin = ANONYMOUS;
+ entry.breakdownNumber = ANONYMOUS;
+ if (entry.dealer != null) {
+ Dealer d = entry.dealer;
+ d.city = ANONYMOUS;
+ d.country = ANONYMOUS;
+ d.name = ANONYMOUS;
+ d.phone = ANONYMOUS;
+ d.postalCode = ANONYMOUS;
+ d.street = ANONYMOUS;
+ }
+ });
+ return Converter.getGson().toJson(container);
+ }
+ }
+ }
+ } catch (JsonParseException jpe) {
+ logger.debug("Cannot parse fingerprint {}", jpe.getMessage());
+ }
+ // Not a VehiclesContainer or Vehicles is empty so deliver fingerprint as it is
+ return fingerprint;
+ }).orElse(Constants.INVALID);
+ }
+
+ private void logFingerPrint() {
+ logger.debug("###### Discovery Troubleshoot Fingerprint Data - BEGIN ######");
+ logger.debug("### Discovery Result ###");
+ logger.debug("{}", getDiscoveryFingerprint());
+ logger.debug("###### Discovery Troubleshoot Fingerprint Data - END ######");
+ }
+
+ /**
+ * There's only the Vehicles response available
+ */
+ @Override
+ public void onResponse(@Nullable String response) {
+ boolean firstResponse = troubleshootFingerprint.isEmpty();
+ if (response != null) {
+ updateStatus(ThingStatus.ONLINE);
+ troubleshootFingerprint = discoveryService.map(discovery -> {
+ try {
+ VehiclesContainer container = Converter.getGson().fromJson(response, VehiclesContainer.class);
+ if (container != null) {
+ if (container.vehicles != null) {
+ discovery.onResponse(container);
+ container.vehicles.forEach(entry -> {
+ entry.vin = ANONYMOUS;
+ entry.breakdownNumber = ANONYMOUS;
+ if (entry.dealer != null) {
+ Dealer d = entry.dealer;
+ d.city = ANONYMOUS;
+ d.country = ANONYMOUS;
+ d.name = ANONYMOUS;
+ d.phone = ANONYMOUS;
+ d.postalCode = ANONYMOUS;
+ d.street = ANONYMOUS;
+ }
+ });
+ }
+ return Converter.getGson().toJson(container);
+ }
+ } catch (JsonParseException jpe) {
+ logger.debug("Fingerprint parse exception {}", jpe.getMessage());
+ }
+ // Unparseable or not a VehiclesContainer:
+ return response;
+ });
+ } else {
+ troubleshootFingerprint = Optional.of(Constants.EMPTY_JSON);
+ }
+ if (firstResponse) {
+ logFingerPrint();
+ }
+ }
+
+ @Override
+ public void onError(NetworkError error) {
+ boolean firstResponse = troubleshootFingerprint.isEmpty();
+ troubleshootFingerprint = Optional.of(error.toJson());
+ if (firstResponse) {
+ 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.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java
new file mode 100644
index 000000000..af2164bf8
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java
@@ -0,0 +1,324 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.utils.HTTPConstants.*;
+
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+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.api.Result;
+import org.eclipse.jetty.client.util.BufferingResponseListener;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.util.MultiMap;
+import org.eclipse.jetty.util.UrlEncoded;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConfiguration;
+import org.openhab.binding.bmwconnecteddrive.internal.VehicleConfiguration;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.auth.AuthResponse;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.simulation.Injector;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.BimmerConstants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ImageProperties;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link ConnectedDriveProxy} 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 ConnectedDriveProxy {
+ private final Logger logger = LoggerFactory.getLogger(ConnectedDriveProxy.class);
+ private final Token token = new Token();
+ private final HttpClient httpClient;
+ private final HttpClient authHttpClient;
+ private final String legacyAuthUri;
+ private final ConnectedDriveConfiguration configuration;
+
+ /**
+ * URLs taken from https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/const.py
+ */
+ final String baseUrl;
+ final String vehicleUrl;
+ final String legacyUrl;
+ final String vehicleStatusAPI = "/status";
+ final String lastTripAPI = "/statistics/lastTrip";
+ final String allTripsAPI = "/statistics/allTrips";
+ final String chargeAPI = "/chargingprofile";
+ final String destinationAPI = "/destinations";
+ final String imageAPI = "/image";
+ final String rangeMapAPI = "/rangemap";
+ final String serviceExecutionAPI = "/executeService";
+ final String serviceExecutionStateAPI = "/serviceExecutionStatus";
+
+ public ConnectedDriveProxy(HttpClientFactory httpClientFactory, ConnectedDriveConfiguration config) {
+ httpClient = httpClientFactory.getCommonHttpClient();
+ authHttpClient = httpClientFactory.createHttpClient(AUTH_HTTP_CLIENT_NAME);
+ authHttpClient.setFollowRedirects(false);
+ configuration = config;
+
+ final StringBuilder legacyAuth = new StringBuilder();
+ legacyAuth.append("https://");
+ legacyAuth.append(BimmerConstants.AUTH_SERVER_MAP.get(configuration.region));
+ legacyAuth.append(BimmerConstants.OAUTH_ENDPOINT);
+ legacyAuthUri = legacyAuth.toString();
+ vehicleUrl = "https://" + getRegionServer() + "/webapi/v1/user/vehicles";
+ baseUrl = vehicleUrl + "/";
+ legacyUrl = "https://" + getRegionServer() + "/api/vehicle/dynamic/v1/";
+ }
+
+ private synchronized void call(final String url, final boolean post, final @Nullable MultiMap params,
+ final ResponseCallback callback) {
+ // only executed in "simulation mode"
+ // SimulationTest.testSimulationOff() assures Injector is off when releasing
+ if (Injector.isActive()) {
+ if (url.equals(baseUrl)) {
+ ((StringResponseCallback) callback).onResponse(Injector.getDiscovery());
+ } else if (url.endsWith(vehicleStatusAPI)) {
+ ((StringResponseCallback) callback).onResponse(Injector.getStatus());
+ } else {
+ logger.debug("Simulation of {} not supported", url);
+ }
+ return;
+ }
+ final Request req;
+ final String encoded = params == null || params.isEmpty() ? null
+ : UrlEncoded.encode(params, StandardCharsets.UTF_8, false);
+ final String completeUrl;
+
+ if (post) {
+ completeUrl = url;
+ req = httpClient.POST(url);
+ if (encoded != null) {
+ req.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, encoded, StandardCharsets.UTF_8));
+ }
+ } else {
+ completeUrl = encoded == null ? url : url + Constants.QUESTION + encoded;
+ req = httpClient.newRequest(completeUrl);
+ }
+ req.header(HttpHeader.AUTHORIZATION, getToken().getBearerToken());
+ req.header(HttpHeader.REFERER, BimmerConstants.REFERER_URL);
+
+ 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 MultiMap params, ResponseCallback callback) {
+ call(url, false, params, callback);
+ }
+
+ public void post(String url, @Nullable MultiMap params, ResponseCallback callback) {
+ call(url, true, params, callback);
+ }
+
+ public void requestVehicles(StringResponseCallback callback) {
+ get(vehicleUrl, null, callback);
+ }
+
+ public void requestVehcileStatus(VehicleConfiguration config, StringResponseCallback callback) {
+ get(baseUrl + config.vin + vehicleStatusAPI, null, callback);
+ }
+
+ public void requestLegacyVehcileStatus(VehicleConfiguration config, StringResponseCallback callback) {
+ // see https://github.com/jupe76/bmwcdapi/search?q=dynamic%2Fv1
+ get(legacyUrl + config.vin + "?offset=-60", null, callback);
+ }
+
+ public void requestLastTrip(VehicleConfiguration config, StringResponseCallback callback) {
+ get(baseUrl + config.vin + lastTripAPI, null, callback);
+ }
+
+ public void requestAllTrips(VehicleConfiguration config, StringResponseCallback callback) {
+ get(baseUrl + config.vin + allTripsAPI, null, callback);
+ }
+
+ public void requestChargingProfile(VehicleConfiguration config, StringResponseCallback callback) {
+ get(baseUrl + config.vin + chargeAPI, null, callback);
+ }
+
+ public void requestDestinations(VehicleConfiguration config, StringResponseCallback callback) {
+ get(baseUrl + config.vin + destinationAPI, null, callback);
+ }
+
+ public void requestRangeMap(VehicleConfiguration config, @Nullable MultiMap params,
+ StringResponseCallback callback) {
+ get(baseUrl + config.vin + rangeMapAPI, params, callback);
+ }
+
+ public void requestImage(VehicleConfiguration config, ImageProperties props, ByteResponseCallback callback) {
+ final String localImageUrl = baseUrl + config.vin + imageAPI;
+ final MultiMap dataMap = new MultiMap();
+ dataMap.add("width", Integer.toString(props.size));
+ dataMap.add("height", Integer.toString(props.size));
+ dataMap.add("view", props.viewport);
+ get(localImageUrl, dataMap, callback);
+ }
+
+ private String getRegionServer() {
+ final String retVal = BimmerConstants.SERVER_MAP.get(configuration.region);
+ return retVal == null ? Constants.INVALID : retVal;
+ }
+
+ private String getAuthorizationValue() {
+ final String retVal = BimmerConstants.AUTHORIZATION_VALUE_MAP.get(configuration.region);
+ return retVal == null ? Constants.INVALID : retVal;
+ }
+
+ RemoteServiceHandler getRemoteServiceHandler(VehicleHandler vehicleHandler) {
+ return new RemoteServiceHandler(vehicleHandler, this);
+ }
+
+ // 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.isExpired() || !token.isValid()) {
+ updateToken();
+ }
+ return token;
+ }
+
+ /**
+ * Authorize at BMW Connected Drive Portal and get Token
+ *
+ * @return
+ */
+ private synchronized void updateToken() {
+ if (!authHttpClient.isStarted()) {
+ try {
+ authHttpClient.start();
+ } catch (Exception e) {
+ logger.warn("Auth Http Client cannot be started {}", e.getMessage());
+ return;
+ }
+ }
+
+ final Request req = authHttpClient.POST(legacyAuthUri);
+ req.header(HttpHeader.CONNECTION, KEEP_ALIVE);
+ req.header(HttpHeader.HOST, getRegionServer());
+ req.header(HttpHeader.AUTHORIZATION, getAuthorizationValue());
+ req.header(CREDENTIALS, BimmerConstants.CREDENTIAL_VALUES);
+ req.header(HttpHeader.REFERER, BimmerConstants.REFERER_URL);
+
+ final MultiMap dataMap = new MultiMap();
+ dataMap.add("grant_type", "password");
+ dataMap.add(SCOPE, BimmerConstants.SCOPE_VALUES);
+ dataMap.add(USERNAME, configuration.userName);
+ dataMap.add(PASSWORD, configuration.password);
+ req.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
+ UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
+ try {
+ ContentResponse contentResponse = req.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send();
+ // Status needs to be 302 - Response is stored in Header
+ if (contentResponse.getStatus() == 302) {
+ final HttpFields fields = contentResponse.getHeaders();
+ final HttpField field = fields.getField(HttpHeader.LOCATION);
+ tokenFromUrl(field.getValue());
+ } else if (contentResponse.getStatus() == 200) {
+ final String stringContent = contentResponse.getContentAsString();
+ if (stringContent != null && !stringContent.isEmpty()) {
+ try {
+ final AuthResponse authResponse = Converter.getGson().fromJson(stringContent,
+ AuthResponse.class);
+ if (authResponse != null) {
+ token.setToken(authResponse.accessToken);
+ token.setType(authResponse.tokenType);
+ token.setExpiration(authResponse.expiresIn);
+ } else {
+ logger.debug("not an Authorization response: {}", stringContent);
+ }
+ } catch (JsonSyntaxException jse) {
+ logger.debug("Authorization response unparsable: {}", stringContent);
+ }
+ } else {
+ logger.debug("Authorization response has no content");
+ }
+ } else {
+ logger.debug("Authorization status {} reason {}", contentResponse.getStatus(),
+ contentResponse.getReason());
+ }
+ } catch (InterruptedException | ExecutionException | TimeoutException e) {
+ logger.debug("Authorization exception: {}", e.getMessage());
+ }
+ }
+
+ void tokenFromUrl(String encodedUrl) {
+ final MultiMap tokenMap = new MultiMap();
+ UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII);
+ tokenMap.forEach((key, value) -> {
+ if (value.size() > 0) {
+ String val = value.get(0);
+ if (key.endsWith(ACCESS_TOKEN)) {
+ token.setToken(val.toString());
+ } else if (key.equals(EXPIRES_IN)) {
+ token.setExpiration(Integer.parseInt(val.toString()));
+ } else if (key.equals(TOKEN_TYPE)) {
+ token.setType(val.toString());
+ }
+ }
+ });
+ }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/RemoteServiceHandler.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/RemoteServiceHandler.java
new file mode 100644
index 000000000..fc1999d46
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/RemoteServiceHandler.java
@@ -0,0 +1,199 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
+
+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.openhab.binding.bmwconnecteddrive.internal.VehicleConfiguration;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.remote.ExecutionStatusContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.binding.bmwconnecteddrive.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 SERVICE_TYPE = "serviceType";
+ private static final String DATA = "data";
+ // after 6 retries the state update will give up
+ private static final int GIVEUP_COUNTER = 6;
+ private static final int STATE_UPDATE_SEC = HTTPConstants.HTTP_TIMEOUT_SEC + 1; // regular timeout + 1sec
+
+ private final ConnectedDriveProxy 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();
+
+ public enum ExecutionState {
+ READY,
+ INITIATED,
+ PENDING,
+ DELIVERED,
+ EXECUTED,
+ ERROR,
+ }
+
+ public enum RemoteService {
+ LIGHT_FLASH(REMOTE_SERVICE_LIGHT_FLASH, "Flash Lights"),
+ VEHICLE_FINDER(REMOTE_SERVICE_VEHICLE_FINDER, "Vehicle Finder"),
+ DOOR_LOCK(REMOTE_SERVICE_DOOR_LOCK, "Door Lock"),
+ DOOR_UNLOCK(REMOTE_SERVICE_DOOR_UNLOCK, "Door Unlock"),
+ HORN_BLOW(REMOTE_SERVICE_HORN, "Horn Blow"),
+ CLIMATE_NOW(REMOTE_SERVICE_AIR_CONDITIONING, "Climate Control"),
+ CHARGE_NOW(REMOTE_SERVICE_CHARGE_NOW, "Start Charging"),
+ CHARGING_CONTROL(REMOTE_SERVICE_CHARGING_CONTROL, "Send Charging Profile");
+
+ private final String command;
+ private final String label;
+
+ RemoteService(final String command, final String label) {
+ this.command = command;
+ this.label = label;
+ }
+
+ public String getCommand() {
+ return command;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+ }
+
+ public RemoteServiceHandler(VehicleHandler vehicleHandler, ConnectedDriveProxy connectedDriveProxy) {
+ handler = vehicleHandler;
+ proxy = connectedDriveProxy;
+ final VehicleConfiguration config = handler.getConfiguration().get();
+ serviceExecutionAPI = proxy.baseUrl + config.vin + proxy.serviceExecutionAPI;
+ serviceExecutionStateAPI = proxy.baseUrl + config.vin + proxy.serviceExecutionStateAPI;
+ }
+
+ boolean execute(RemoteService service, String... data) {
+ synchronized (this) {
+ if (serviceExecuting.isPresent()) {
+ // only one service executing
+ return false;
+ }
+ serviceExecuting = Optional.of(service.name());
+ }
+ final MultiMap dataMap = new MultiMap();
+ dataMap.add(SERVICE_TYPE, service.name());
+ if (data.length > 0) {
+ dataMap.add(DATA, data[0]);
+ }
+ proxy.post(serviceExecutionAPI, dataMap, 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);
+ reset();
+ // immediately refresh data
+ handler.getData();
+ }
+ counter++;
+ final MultiMap dataMap = new MultiMap();
+ dataMap.add(SERVICE_TYPE, service);
+ proxy.get(serviceExecutionStateAPI, dataMap, 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 && esc.executionStatus != null) {
+ String status = esc.executionStatus.status;
+ synchronized (this) {
+ handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), status);
+ if (ExecutionState.EXECUTED.name().equals(status)) {
+ // 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() + Constants.SPACE + Integer.toString(error.status));
+ reset();
+ }
+ }
+
+ private void reset() {
+ serviceExecuting = 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.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ResponseCallback.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ResponseCallback.java
new file mode 100644
index 000000000..5785cb245
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ResponseCallback.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.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.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/StringResponseCallback.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/StringResponseCallback.java
new file mode 100644
index 000000000..45b50913a
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/StringResponseCallback.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.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.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/Token.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/Token.java
new file mode 100644
index 000000000..22e421704
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/Token.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+/**
+ * The {@link Token} BMW ConnectedDrive 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;
+ }
+
+ /**
+ * @return true if Token expires in less than 1 second
+ */
+ public boolean isExpired() {
+ return (expiration - System.currentTimeMillis() / 1000) < 1;
+ }
+
+ public void setType(String type) {
+ tokenType = type;
+ }
+
+ public boolean isValid() {
+ return (!token.equals(Constants.EMPTY) && !tokenType.equals(Constants.EMPTY) && expiration > 0);
+ }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleChannelHandler.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleChannelHandler.java
new file mode 100644
index 000000000..6ff369b17
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleChannelHandler.java
@@ -0,0 +1,515 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
+
+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.quantity.Length;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.VehicleType;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.Destination;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.AllTrips;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.LastTrip;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.CBSMessage;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.CCMMessage;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.Doors;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.Position;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatus;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.Windows;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileUtils;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileUtils.TimedChannel;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper.ProfileKey;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.RemoteServiceUtils;
+import org.openhab.binding.bmwconnecteddrive.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.thing.Thing;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.State;
+import org.openhab.core.types.StateOption;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link VehicleChannelHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @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 imperial = false;
+ 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 destinationList = new ArrayList();
+ protected String selectedDestination = Constants.UNDEF;
+
+ protected BMWConnectedDriveOptionProvider optionProvider;
+
+ // Data Caches
+ protected Optional vehicleStatusCache = Optional.empty();
+ protected Optional lastTripCache = Optional.empty();
+ protected Optional allTripsCache = Optional.empty();
+ protected Optional chargeProfileCache = Optional.empty();
+ protected Optional rangeMapCache = Optional.empty();
+ protected Optional destinationCache = Optional.empty();
+ protected Optional imageCache = Optional.empty();
+
+ public VehicleChannelHandler(Thing thing, BMWConnectedDriveOptionProvider op, String type, boolean imperial) {
+ super(thing);
+ optionProvider = op;
+
+ this.imperial = imperial;
+ 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) {
+ optionProvider.setStateOptions(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 updateCheckControls(List ccl) {
+ if (ccl.size() == 0) {
+ // No Check Control available - show not active
+ CCMMessage ccm = new CCMMessage();
+ ccm.ccmDescriptionLong = Constants.NO_ENTRIES;
+ ccm.ccmDescriptionShort = Constants.NO_ENTRIES;
+ ccm.ccmId = -1;
+ ccm.ccmMileage = -1;
+ ccl.add(ccm);
+ }
+
+ // add all elements to options
+ checkControlList = ccl;
+ List ccmDescriptionOptions = new ArrayList<>();
+ List ccmDetailsOptions = new ArrayList<>();
+ List ccmMileageOptions = new ArrayList<>();
+ boolean isSelectedElementIn = false;
+ int index = 0;
+ for (CCMMessage ccEntry : checkControlList) {
+ ccmDescriptionOptions.add(new StateOption(Integer.toString(index), ccEntry.ccmDescriptionShort));
+ ccmDetailsOptions.add(new StateOption(Integer.toString(index), ccEntry.ccmDescriptionLong));
+ ccmMileageOptions.add(new StateOption(Integer.toString(index), Integer.toString(ccEntry.ccmMileage)));
+ if (selectedCC.equals(ccEntry.ccmDescriptionShort)) {
+ isSelectedElementIn = true;
+ }
+ index++;
+ }
+ setOptions(CHANNEL_GROUP_CHECK_CONTROL, NAME, ccmDescriptionOptions);
+ setOptions(CHANNEL_GROUP_CHECK_CONTROL, DETAILS, ccmDetailsOptions);
+ setOptions(CHANNEL_GROUP_CHECK_CONTROL, MILEAGE, ccmMileageOptions);
+
+ // 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.ccmDescriptionShort;
+ updateChannel(CHANNEL_GROUP_CHECK_CONTROL, NAME, StringType.valueOf(ccEntry.ccmDescriptionShort));
+ updateChannel(CHANNEL_GROUP_CHECK_CONTROL, DETAILS, StringType.valueOf(ccEntry.ccmDescriptionLong));
+ updateChannel(CHANNEL_GROUP_CHECK_CONTROL, MILEAGE, QuantityType.valueOf(
+ Converter.round(ccEntry.ccmMileage), imperial ? ImperialUnits.MILE : Constants.KILOMETRE_UNIT));
+ }
+ }
+
+ protected void updateServices(List sl) {
+ // if list is empty add "undefined" element
+ if (sl.size() == 0) {
+ CBSMessage cbsm = new CBSMessage();
+ cbsm.cbsType = Constants.NO_ENTRIES;
+ cbsm.cbsDescription = Constants.NO_ENTRIES;
+ sl.add(cbsm);
+ }
+
+ // add all elements to options
+ serviceList = sl;
+ List serviceNameOptions = new ArrayList<>();
+ List serviceDetailsOptions = new ArrayList<>();
+ List serviceDateOptions = new ArrayList<>();
+ List serviceMileageOptions = new ArrayList<>();
+ boolean isSelectedElementIn = false;
+ int index = 0;
+ for (CBSMessage serviceEntry : serviceList) {
+ // create StateOption with "value = list index" and "label = human readable string"
+ serviceNameOptions.add(new StateOption(Integer.toString(index), serviceEntry.getType()));
+ serviceDetailsOptions.add(new StateOption(Integer.toString(index), serviceEntry.getDescription()));
+ serviceDateOptions.add(new StateOption(Integer.toString(index), serviceEntry.getDueDate()));
+ serviceMileageOptions
+ .add(new StateOption(Integer.toString(index), Integer.toString(serviceEntry.cbsRemainingMileage)));
+ if (selectedService.equals(serviceEntry.getType())) {
+ isSelectedElementIn = true;
+ }
+ index++;
+ }
+ setOptions(CHANNEL_GROUP_SERVICE, NAME, serviceNameOptions);
+ setOptions(CHANNEL_GROUP_SERVICE, DETAILS, serviceDetailsOptions);
+ setOptions(CHANNEL_GROUP_SERVICE, DATE, serviceDateOptions);
+ setOptions(CHANNEL_GROUP_SERVICE, MILEAGE, serviceMileageOptions);
+
+ // 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()) {
+ CBSMessage serviceEntry = serviceList.get(index);
+ selectedService = serviceEntry.cbsType;
+ updateChannel(CHANNEL_GROUP_SERVICE, NAME,
+ StringType.valueOf(Converter.toTitleCase(serviceEntry.getType())));
+ updateChannel(CHANNEL_GROUP_SERVICE, DETAILS,
+ StringType.valueOf(Converter.toTitleCase(serviceEntry.getDescription())));
+ updateChannel(CHANNEL_GROUP_SERVICE, DATE,
+ DateTimeType.valueOf(Converter.getLocalDateTime(serviceEntry.getDueDate())));
+ updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE,
+ QuantityType.valueOf(Converter.round(serviceEntry.cbsRemainingMileage),
+ imperial ? ImperialUnits.MILE : Constants.KILOMETRE_UNIT));
+ }
+ }
+
+ protected void updateDestinations(List dl) {
+ // if list is empty add "undefined" element
+ if (dl.size() == 0) {
+ Destination dest = new Destination();
+ dest.city = Constants.NO_ENTRIES;
+ dest.lat = -1;
+ dest.lon = -1;
+ dl.add(dest);
+ }
+
+ // add all elements to options
+ destinationList = dl;
+ List destinationNameOptions = new ArrayList<>();
+ List destinationGPSOptions = new ArrayList<>();
+ boolean isSelectedElementIn = false;
+ int index = 0;
+ for (Destination destination : destinationList) {
+ destinationNameOptions.add(new StateOption(Integer.toString(index), destination.getAddress()));
+ destinationGPSOptions.add(new StateOption(Integer.toString(index), destination.getCoordinates()));
+ if (selectedDestination.equals(destination.getAddress())) {
+ isSelectedElementIn = true;
+ }
+ index++;
+ }
+ setOptions(CHANNEL_GROUP_DESTINATION, NAME, destinationNameOptions);
+ setOptions(CHANNEL_GROUP_DESTINATION, GPS, destinationGPSOptions);
+
+ // if current selected item isn't anymore in the list select first entry
+ if (!isSelectedElementIn) {
+ selectDestination(0);
+ }
+ }
+
+ protected void selectDestination(int index) {
+ if (index >= 0 && index < destinationList.size()) {
+ Destination destinationEntry = destinationList.get(index);
+ // update selected Item
+ selectedDestination = destinationEntry.getAddress();
+ // update coordinates according to new set location
+ updateChannel(CHANNEL_GROUP_DESTINATION, NAME, StringType.valueOf(destinationEntry.getAddress()));
+ updateChannel(CHANNEL_GROUP_DESTINATION, GPS, PointType.valueOf(destinationEntry.getCoordinates()));
+ }
+ }
+
+ protected void updateAllTrips(AllTrips allTrips) {
+ QuantityType qtTotalElectric = QuantityType
+ .valueOf(Converter.round(allTrips.totalElectricDistance.userTotal), Constants.KILOMETRE_UNIT);
+ QuantityType qtLongestElectricRange = QuantityType
+ .valueOf(Converter.round(allTrips.chargecycleRange.userHigh), Constants.KILOMETRE_UNIT);
+ QuantityType qtDistanceSinceCharge = QuantityType
+ .valueOf(Converter.round(allTrips.chargecycleRange.userCurrentChargeCycle), Constants.KILOMETRE_UNIT);
+
+ updateChannel(CHANNEL_GROUP_LIFETIME, TOTAL_DRIVEN_DISTANCE,
+ imperial ? Converter.getMiles(qtTotalElectric) : qtTotalElectric);
+ updateChannel(CHANNEL_GROUP_LIFETIME, SINGLE_LONGEST_DISTANCE,
+ imperial ? Converter.getMiles(qtLongestElectricRange) : qtLongestElectricRange);
+ updateChannel(CHANNEL_GROUP_LAST_TRIP, DISTANCE_SINCE_CHARGING,
+ imperial ? Converter.getMiles(qtDistanceSinceCharge) : qtDistanceSinceCharge);
+
+ // Conversion from kwh/100km to kwh/10mi has to be done manually
+ double avgConsumotion = imperial ? allTrips.avgElectricConsumption.userAverage * Converter.MILES_TO_KM_RATIO
+ : allTrips.avgElectricConsumption.userAverage;
+ double avgCombinedConsumption = imperial
+ ? allTrips.avgCombinedConsumption.userAverage * Converter.MILES_TO_KM_RATIO
+ : allTrips.avgCombinedConsumption.userAverage;
+ double avgRecuperation = imperial ? allTrips.avgRecuperation.userAverage * Converter.MILES_TO_KM_RATIO
+ : allTrips.avgRecuperation.userAverage;
+
+ updateChannel(CHANNEL_GROUP_LIFETIME, AVG_CONSUMPTION,
+ QuantityType.valueOf(Converter.round(avgConsumotion), Units.KILOWATT_HOUR));
+ updateChannel(CHANNEL_GROUP_LIFETIME, AVG_COMBINED_CONSUMPTION,
+ QuantityType.valueOf(Converter.round(avgCombinedConsumption), Units.LITRE));
+ updateChannel(CHANNEL_GROUP_LIFETIME, AVG_RECUPERATION,
+ QuantityType.valueOf(Converter.round(avgRecuperation), Units.KILOWATT_HOUR));
+ }
+
+ protected void updateLastTrip(LastTrip trip) {
+ // Whyever the Last Trip DateTime is delivered without offest - so LocalTime
+ updateChannel(CHANNEL_GROUP_LAST_TRIP, DATE,
+ DateTimeType.valueOf(Converter.getLocalDateTimeWithoutOffest(trip.date)));
+ updateChannel(CHANNEL_GROUP_LAST_TRIP, DURATION, QuantityType.valueOf(trip.duration, Units.MINUTE));
+
+ QuantityType qtTotalDistance = QuantityType.valueOf(Converter.round(trip.totalDistance),
+ Constants.KILOMETRE_UNIT);
+ updateChannel(CHANNEL_GROUP_LAST_TRIP, DISTANCE,
+ imperial ? Converter.getMiles(qtTotalDistance) : qtTotalDistance);
+
+ // Conversion from kwh/100km to kwh/10mi has to be done manually
+ double avgConsumtption = imperial ? trip.avgElectricConsumption * Converter.MILES_TO_KM_RATIO
+ : trip.avgElectricConsumption;
+ double avgCombinedConsumption = imperial ? trip.avgCombinedConsumption * Converter.MILES_TO_KM_RATIO
+ : trip.avgCombinedConsumption;
+ double avgRecuperation = imperial ? trip.avgRecuperation * Converter.MILES_TO_KM_RATIO : trip.avgRecuperation;
+
+ updateChannel(CHANNEL_GROUP_LAST_TRIP, AVG_CONSUMPTION,
+ QuantityType.valueOf(Converter.round(avgConsumtption), Units.KILOWATT_HOUR));
+ updateChannel(CHANNEL_GROUP_LAST_TRIP, AVG_COMBINED_CONSUMPTION,
+ QuantityType.valueOf(Converter.round(avgCombinedConsumption), Units.LITRE));
+ updateChannel(CHANNEL_GROUP_LAST_TRIP, AVG_RECUPERATION,
+ QuantityType.valueOf(Converter.round(avgRecuperation), Units.KILOWATT_HOUR));
+ }
+
+ protected void updateChargeProfileFromContent(String content) {
+ ChargeProfileWrapper.fromJson(content).ifPresent(this::updateChargeProfile);
+ }
+
+ protected void updateChargeProfile(ChargeProfileWrapper wrapper) {
+ updateChannel(CHANNEL_GROUP_CHARGE, CHARGE_PROFILE_PREFERENCE,
+ StringType.valueOf(Converter.toTitleCase(wrapper.getPreference())));
+ updateChannel(CHANNEL_GROUP_CHARGE, CHARGE_PROFILE_MODE,
+ StringType.valueOf(Converter.toTitleCase(wrapper.getMode())));
+ final Boolean climate = wrapper.isEnabled(ProfileKey.CLIMATE);
+ updateChannel(CHANNEL_GROUP_CHARGE, CHARGE_PROFILE_CLIMATE,
+ climate == null ? UnDefType.UNDEF : OnOffType.from(climate));
+ updateTimedState(wrapper, ProfileKey.WINDOWSTART);
+ updateTimedState(wrapper, ProfileKey.WINDOWEND);
+ updateTimedState(wrapper, ProfileKey.TIMER1);
+ updateTimedState(wrapper, ProfileKey.TIMER2);
+ updateTimedState(wrapper, ProfileKey.TIMER3);
+ updateTimedState(wrapper, ProfileKey.OVERRIDE);
+ }
+
+ 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, timed.time, time == null ? 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, timed.timer + CHARGE_ENABLED,
+ enabled == null ? UnDefType.UNDEF : OnOffType.from(enabled));
+ if (timed.hasDays) {
+ final Set days = profile.getDays(key);
+ updateChannel(CHANNEL_GROUP_CHARGE, timed.timer + CHARGE_DAYS,
+ days == null ? UnDefType.UNDEF : StringType.valueOf(ChargeProfileUtils.formatDays(days)));
+ EnumSet.allOf(DayOfWeek.class).forEach(day -> {
+ updateChannel(CHANNEL_GROUP_CHARGE, timed.timer + ChargeProfileUtils.getDaysChannel(day),
+ days == null ? UnDefType.UNDEF : OnOffType.from(days.contains(day)));
+ });
+ }
+ }
+ }
+ }
+
+ protected void updateDoors(Doors doorState) {
+ updateChannel(CHANNEL_GROUP_DOORS, DOOR_DRIVER_FRONT,
+ StringType.valueOf(Converter.toTitleCase(doorState.doorDriverFront)));
+ updateChannel(CHANNEL_GROUP_DOORS, DOOR_DRIVER_REAR,
+ StringType.valueOf(Converter.toTitleCase(doorState.doorDriverRear)));
+ updateChannel(CHANNEL_GROUP_DOORS, DOOR_PASSENGER_FRONT,
+ StringType.valueOf(Converter.toTitleCase(doorState.doorPassengerFront)));
+ updateChannel(CHANNEL_GROUP_DOORS, DOOR_PASSENGER_REAR,
+ StringType.valueOf(Converter.toTitleCase(doorState.doorPassengerRear)));
+ updateChannel(CHANNEL_GROUP_DOORS, TRUNK, StringType.valueOf(Converter.toTitleCase(doorState.trunk)));
+ updateChannel(CHANNEL_GROUP_DOORS, HOOD, StringType.valueOf(Converter.toTitleCase(doorState.hood)));
+ }
+
+ protected void updateWindows(Windows windowState) {
+ updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_DRIVER_FRONT,
+ StringType.valueOf(Converter.toTitleCase(windowState.windowDriverFront)));
+ updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_DRIVER_REAR,
+ StringType.valueOf(Converter.toTitleCase(windowState.windowDriverRear)));
+ updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_PASSENGER_FRONT,
+ StringType.valueOf(Converter.toTitleCase(windowState.windowPassengerFront)));
+ updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_PASSENGER_REAR,
+ StringType.valueOf(Converter.toTitleCase(windowState.windowPassengerRear)));
+ updateChannel(CHANNEL_GROUP_DOORS, WINDOW_REAR,
+ StringType.valueOf(Converter.toTitleCase(windowState.rearWindow)));
+ updateChannel(CHANNEL_GROUP_DOORS, SUNROOF, StringType.valueOf(Converter.toTitleCase(windowState.sunroof)));
+ }
+
+ protected void updatePosition(Position pos) {
+ updateChannel(CHANNEL_GROUP_LOCATION, GPS, PointType.valueOf(pos.getCoordinates()));
+ updateChannel(CHANNEL_GROUP_LOCATION, HEADING, QuantityType.valueOf(pos.heading, Units.DEGREE_ANGLE));
+ }
+
+ protected void updateVehicleStatus(VehicleStatus vStatus) {
+ // Vehicle Status
+ updateChannel(CHANNEL_GROUP_STATUS, LOCK, StringType.valueOf(Converter.toTitleCase(vStatus.doorLockState)));
+
+ // Service Updates
+ updateChannel(CHANNEL_GROUP_STATUS, SERVICE_DATE,
+ DateTimeType.valueOf(Converter.getLocalDateTime(VehicleStatusUtils.getNextServiceDate(vStatus))));
+
+ updateChannel(CHANNEL_GROUP_STATUS, SERVICE_MILEAGE,
+ QuantityType.valueOf(Converter.round(VehicleStatusUtils.getNextServiceMileage(vStatus)),
+ imperial ? ImperialUnits.MILE : Constants.KILOMETRE_UNIT));
+ // CheckControl Active?
+ updateChannel(CHANNEL_GROUP_STATUS, CHECK_CONTROL,
+ StringType.valueOf(Converter.toTitleCase(VehicleStatusUtils.checkControlActive(vStatus))));
+ // last update Time
+ updateChannel(CHANNEL_GROUP_STATUS, LAST_UPDATE,
+ DateTimeType.valueOf(Converter.getLocalDateTime(VehicleStatusUtils.getUpdateTime(vStatus))));
+
+ Doors doorState = null;
+ try {
+ doorState = Converter.getGson().fromJson(Converter.getGson().toJson(vStatus), Doors.class);
+ } catch (JsonSyntaxException jse) {
+ logger.debug("Doors parse exception {}", jse.getMessage());
+ }
+ if (doorState != null) {
+ updateChannel(CHANNEL_GROUP_STATUS, DOORS, StringType.valueOf(VehicleStatusUtils.checkClosed(doorState)));
+ updateDoors(doorState);
+ }
+ Windows windowState = null;
+ try {
+ windowState = Converter.getGson().fromJson(Converter.getGson().toJson(vStatus), Windows.class);
+ } catch (JsonSyntaxException jse) {
+ logger.debug("Windows parse exception {}", jse.getMessage());
+ }
+ if (windowState != null) {
+ updateChannel(CHANNEL_GROUP_STATUS, WINDOWS,
+ StringType.valueOf(VehicleStatusUtils.checkClosed(windowState)));
+ updateWindows(windowState);
+ }
+
+ // Range values
+ // based on unit of length decide if range shall be reported in km or miles
+ float totalRange = 0;
+ if (isElectric) {
+ totalRange += vStatus.remainingRangeElectric;
+ QuantityType qtElectricRange = QuantityType.valueOf(vStatus.remainingRangeElectric,
+ Constants.KILOMETRE_UNIT);
+ QuantityType qtElectricRadius = QuantityType
+ .valueOf(Converter.guessRangeRadius(vStatus.remainingRangeElectric), Constants.KILOMETRE_UNIT);
+
+ updateChannel(CHANNEL_GROUP_RANGE, RANGE_ELECTRIC,
+ imperial ? Converter.getMiles(qtElectricRange) : qtElectricRange);
+ updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_ELECTRIC,
+ imperial ? Converter.getMiles(qtElectricRadius) : qtElectricRadius);
+ }
+ if (hasFuel) {
+ totalRange += vStatus.remainingRangeFuel;
+ QuantityType qtFuelRange = QuantityType.valueOf(vStatus.remainingRangeFuel,
+ Constants.KILOMETRE_UNIT);
+ QuantityType qtFuelRadius = QuantityType
+ .valueOf(Converter.guessRangeRadius(vStatus.remainingRangeFuel), Constants.KILOMETRE_UNIT);
+
+ updateChannel(CHANNEL_GROUP_RANGE, RANGE_FUEL, imperial ? Converter.getMiles(qtFuelRange) : qtFuelRange);
+ updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_FUEL,
+ imperial ? Converter.getMiles(qtFuelRadius) : qtFuelRadius);
+ }
+ if (isHybrid) {
+ QuantityType qtHybridRange = QuantityType.valueOf(totalRange, Constants.KILOMETRE_UNIT);
+ QuantityType qtHybridRadius = QuantityType.valueOf(Converter.guessRangeRadius(totalRange),
+ Constants.KILOMETRE_UNIT);
+ updateChannel(CHANNEL_GROUP_RANGE, RANGE_HYBRID,
+ imperial ? Converter.getMiles(qtHybridRange) : qtHybridRange);
+ updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_HYBRID,
+ imperial ? Converter.getMiles(qtHybridRadius) : qtHybridRadius);
+ }
+
+ updateChannel(CHANNEL_GROUP_RANGE, MILEAGE,
+ QuantityType.valueOf(vStatus.mileage, imperial ? ImperialUnits.MILE : Constants.KILOMETRE_UNIT));
+ if (isElectric) {
+ updateChannel(CHANNEL_GROUP_RANGE, SOC, QuantityType.valueOf(vStatus.chargingLevelHv, Units.PERCENT));
+ }
+ if (hasFuel) {
+ updateChannel(CHANNEL_GROUP_RANGE, REMAINING_FUEL,
+ QuantityType.valueOf(vStatus.remainingFuel, Units.LITRE));
+ }
+
+ // Charge Values
+ if (isElectric) {
+ if (vStatus.chargingStatus != null) {
+ if (Constants.INVALID.equals(vStatus.chargingStatus)) {
+ updateChannel(CHANNEL_GROUP_STATUS, CHARGE_STATUS,
+ StringType.valueOf(Converter.toTitleCase(vStatus.lastChargingEndReason)));
+ } else {
+ // State INVALID is somehow misleading. Instead show the Last Charging End Reason
+ updateChannel(CHANNEL_GROUP_STATUS, CHARGE_STATUS,
+ StringType.valueOf(Converter.toTitleCase(vStatus.chargingStatus)));
+ }
+ } else {
+ updateChannel(CHANNEL_GROUP_STATUS, CHARGE_STATUS, UnDefType.NULL);
+ }
+ if (vStatus.chargingTimeRemaining != null) {
+ try {
+ updateChannel(CHANNEL_GROUP_STATUS, CHARGE_REMAINING,
+ QuantityType.valueOf(vStatus.chargingTimeRemaining, Units.MINUTE));
+ } catch (NumberFormatException nfe) {
+ updateChannel(CHANNEL_GROUP_STATUS, CHARGE_REMAINING, UnDefType.UNDEF);
+ }
+ } else {
+ updateChannel(CHANNEL_GROUP_STATUS, CHARGE_REMAINING, UnDefType.NULL);
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java
new file mode 100644
index 000000000..035f0c1b6
--- /dev/null
+++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java
@@ -0,0 +1,793 @@
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+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.bmwconnecteddrive.internal.VehicleConfiguration;
+import org.openhab.binding.bmwconnecteddrive.internal.action.BMWConnectedDriveActions;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.DestinationContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.compat.VehicleAttributesContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.AllTrips;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.AllTripsContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.LastTrip;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.LastTripContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatus;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatusContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.RemoteServiceHandler.ExecutionState;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.RemoteServiceHandler.RemoteService;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileUtils;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileUtils.ChargeKeyDay;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper.ProfileKey;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ImageProperties;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.RemoteServiceUtils;
+import org.openhab.core.io.net.http.HttpUtil;
+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.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.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link VehicleHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - edit & send charge profile
+ */
+@NonNullByDefault
+public class VehicleHandler extends VehicleChannelHandler {
+ private int legacyMode = Constants.INT_UNDEF; // switch to legacy API in case of 404 Errors
+
+ private Optional proxy = Optional.empty();
+ private Optional