diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index f7cb83f0e..2104eaff3 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -906,6 +906,11 @@
org.openhab.binding.melcloud
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.mercedesme
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.meteoalerte
diff --git a/bundles/org.openhab.binding.mercedesme/NOTICE b/bundles/org.openhab.binding.mercedesme/NOTICE
new file mode 100644
index 000000000..38d625e34
--- /dev/null
+++ b/bundles/org.openhab.binding.mercedesme/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.mercedesme/README.md b/bundles/org.openhab.binding.mercedesme/README.md
new file mode 100644
index 000000000..ba06c97c5
--- /dev/null
+++ b/bundles/org.openhab.binding.mercedesme/README.md
@@ -0,0 +1,503 @@
+# MercedesMe Binding
+
+This binding provides similar access to your Mercedes Benz vehicle like the Smartphone App _Mercedes Me_.
+For this you need a Mercedes developer account to get data from your vehicles.
+Setup requires some, time so follow [the steps of bridge configuration](#bridge-configuration).
+
+If you face some problems during setup or runtime please have a look into the [Troubleshooting section](#troubleshooting)
+
+## Supported Things
+
+| Type | ID | Description |
+|-----------------|---------------|-------------------------------------------------|
+| Bridge | `account` | Connect your Mercedes Me account |
+| Thing | `combustion` | Conventional fuel vehicle |
+| Thing | `hybrid` | Fuel vehicle with supporting electric engine |
+| Thing | `bev` | Battery electric vehicle |
+
+## Bridge Configuration
+
+Bridge needs configuration in order to connect properly to your Mercedes Me Account.
+
+### Pre-Conditions
+
+- **each bridge shall have its own Mercedes Benz Client ID!**
+ Don't create several `account` bridges with the same client id! If this is not the case the tokens won't be stored properly and the authorization is jeopardized!
+- **each bridge shall have its own port.**
+ It's absolutely necessary to assign a different port for each `account` bridge. If this is not the case the tokens won't be stored properly and the authorization is jeopardized!
+
+### Bridge Setup
+
+Perform the following steps to obtain the configuration data and perform the authorization flow.
+
+1. Go to [Mercedes Developer Page](https://developer.mercedes-benz.com/). Login with your Mercedes Me credentials.
+2. Create a project in the [console tab](https://developer.mercedes-benz.com/console)
+ - _Project Name:_ unique name e.g. **openHAB Mercedes Me binding** plus **Your bridge ID**
+ - _Purpose URL:_ use link towards [this binding description](https://www.openhab.org/addons/bindings/mercedesme/)
+ - _Business Purpose:_ e.g. **Private usage in openHAB Smarthome system**
+3. After project is created subscribe [to these Mercedes Benz APIs](https://developer.mercedes-benz.com/products?vt=cars&vt=vans&vt=smart&p=BYOCAR) with _Add Products_ button
+4. For all Products perform the same steps
+ - Select product
+ - Choose _Get For Free_
+ - Choose _BYOCAR_ (Build Your Own Car)
+ - Button _Confirm_
+5. Select the following products
+ - Vehicle Status
+ - Vehicle Lock Status
+ - Pay as you drive insurance
+ - Electric Vehicle Status
+ - Fuel Status
+6. Optional: Subscribe also to _Vehicle images_. Select the _Basic Trial_ version. The images will be stored so the API is used just a few times.
+7. Press _Subscribe_ button. Your project should have [these product subscriptions](#mb-product-subscriptions)
+8. Generate the [project credentials](#mb-credentials)
+9. Open in new browser tab your openHAB page. Add a new Thing _Mercedes Me Account_
+10. Copy paste _Client ID_ , _Client Secret_ and _API Key_ from the Mercedes tab into the openHAB configuration
+11. Check if the registered Mercedes products _excluding Vehicle Images_ are matching exactly with the openHab configuration switches
+12. Create Thing!
+13. The fresh created [account has one property](#openhab-configuration) `callbackUrl`. Copy it and paste it in a new browser tab
+14. A [simple HTML page is shown including a link towards the Authorization flow](#callback-page) - **don't click yet**. If page isn't shown please adapt IP and port in openHAB configuration with Advanced Options activated
+15. The copied URL needs to be added in your [Mercedes project credentials](#mb-credentials) from 8
+16. Now click onto the link from 14. You'll be asked one time if you [grant access](#mb-access-request) towards the API. Click OK and authorization is done!
+
+Some supporting screenshots for the setup
+
+### MB Credentials
+
+
+
+### MB Product Subscriptions
+
+
+
+### openHAB Configuration
+
+
+
+### MB Access Request
+
+
+
+### Callback page
+
+
+
+
+### Bridge Configuration Parameters
+
+| Name | Type | Description | Default | Required | Advanced |
+|-----------------|---------|---------------------------------------|-------------|----------|----------|
+| clientId | text | Mercedes Benz Developer Client ID | N/A | yes | no |
+| clientSecret | text | Mercedes Benz Developer Client Secret | N/A | yes | no |
+| imageApiKey | text | Mercedes Benz Developer Image API Key | N/A | no | no |
+| odoScope | boolean | PayAsYourDrive Insurance | true | yes | no |
+| vehicleScope | boolean | Vehicle Status | true | yes | no |
+| lockScope | boolean | Lock status of doors and trunk | true | yes | no |
+| fuelScope | boolean | Fuel Status | true | yes | no |
+| evScope | boolean | Electric Vehicle Status | true | yes | no |
+| callbackIp | text | IP address of your openHAB server | auto detect | no | yes |
+| callbackPort | integer | **Unique** port number | auto detect | no | yes |
+
+The `callbackPort` needs to be unique for all created Mercedes Me account things. Otherwise token exchange will be corrupted.
+Set the advanced options by yourself if you know your IP and Port, otherwise give auto detect a try.
+
+
+## Thing Configuration
+
+For vehicle images Mercedes Benz Developer offers only a trial version with limited calls.
+Check in **beforehand** if your vehicle has some restrictions or even if it's supported at all.
+Visit [Vehicle Image Details](https://developer.mercedes-benz.com/products/vehicle_images/details) in order to check your vehicle capabilities.
+Visit [Image Settings](https://developer.mercedes-benz.com/products/vehicle_images/docs#_default_image_settings) to get more information about
+For example the EQA doesn't provide `night` images with `background`.
+If your configuration is set this way the API calls are wasted!
+
+
+
+See also [image channel section](#image) for further advise.
+
+| Name | Type | Description | Default | Required | Advanced |
+|-----------------|---------|-----------------------------------------------------|---------|----------|----------|
+| vin | text | Vehicle identification number | N/A | yes | no |
+| refreshInterval | integer | Refresh interval in minutes | 5 | yes | no |
+| background | boolean | Vehicle images provided with or without background | false | no | yes |
+| night | boolean | Vehicle images in night conditions | false | no | yes |
+| cropped | boolean | Vehicle images in 4:3 instead of 16:9 | false | no | yes |
+| roofOpen | boolean | Vehicle images with open roof (only Cabriolet) | false | no | yes |
+| format | text | Vehicle images format (webp or png) | webp | no | yes |
+
+For all vehicles you're free to give the tank / battery capacity.
+Giving these values in configuration the open fuel / charge capacities are reported in the [range](#range) channels.
+
+| Name | Type | Description | Default | Required | Advanced | combustion | bev | hybrid |
+|-----------------|---------|-----------------------------------------------------|---------|----------|----------|------------|-----|--------|
+| batteryCapacity | decimal | Battery Capacity | N/A | no | no | | X | X |
+| fuelCapacity | decimal | Fuel Capacity | N/A | no | no | X | | X |
+
+## Channels
+
+Channels are separated in groups:
+
+| Channel Group ID | Description |
+|----------------------------------|---------------------------------------------------|
+| [range](#range) | Provides mileage, range and charge / fuel levels |
+| [doors](#doors) | Details of all doors |
+| [windows](#windows) | Current position of windows |
+| [lights](#lights) | Interior lights and main light switch |
+| [lock](#lock) | Overall lock state of vehicle |
+| [location](#location) | Heading of the vehicle |
+| [image](#image) | Images of your vehicle |
+
+### Range
+
+Group name: `range`
+All channels `read-only`
+
+| Channel | Type | Description | bev | hybrid | combustion |
+|------------------|----------------------|------------------------------| ----|--------|------------|
+| mileage | Number:Length | Total mileage | X | X | X |
+| soc | Number:Dimensionless | Battery state of charge | X | X | |
+| charged | Number:Energy | Charged Battery Energy | X | X | |
+| uncharged | Number:Energy | Uncharged Battery Energy | X | X | |
+| soc | Number:Dimensionless | Battery state of charge | X | X | |
+| range-electric | Number:Length | Electric range | X | X | |
+| radius-electric | Number:Length | Electric radius for map | X | X | |
+| fuel-level | Number:Dimensionless | Fuel level in percent | | X | X |
+| fuel-remain | Number:Volume | Reamaining Fuel | | X | X |
+| fuel-open | Number:Volume | Open Fuel Capacity | | X | X |
+| range-fuel | Number:Length | Fuel range | | X | X |
+| radius-fuel | Number:Length | Fuel radius for map | | X | X |
+| range-hybrid | Number:Length | Hybrid range | | X | |
+| radius-hybrid | Number:Length | Hybrid radius for map | | X | |
+| last-update | DateTime | Last range update | X | X | X |
+
+Channels with `radius` are just giving a _guess_ which radius can be reached in a map display.
+
+### Doors
+
+Group name: `doors`
+All channels `read-only`
+
+| Channel | Type | Description |
+|------------------|----------------------|------------------------------|
+| driver-front | Contact | Driver door |
+| driver-rear | Contact | Driver door reat |
+| passenger-front | Contact | Passenger door |
+| passenger-rear | Contact | Passenger door rear |
+| deck-lid | Contact | Deck lid |
+| sunroof | Number | Sun roof (only Cabriolet) |
+| rooftop | Number | Roof top |
+| last-update | DateTime | Last doors update |
+
+Mapping table `sunroof`
+
+| Number | Mapping |
+|-----------------|---------------------|
+| 0 | Closed |
+| 1 | Open |
+| 2 | Open Lifting |
+| 3 | Running |
+| 4 | Closing |
+| 5 | Opening |
+| 6 | Closing |
+
+Mapping table `rootop`
+
+| Number | Mapping |
+|-----------------|---------------------|
+| 0 | Unlocked |
+| 1 | Open and locked |
+| 2 | Closed and locked |
+
+### Windows
+
+Group name: `windows`
+All channels `readonly`
+
+| Channel | Type | Description |
+|------------------|----------------------|------------------------------|
+| driver-front | Number | Driver window |
+| driver-rear | Number | Driver window rear |
+| passenger-front | Number | Passenger window |
+| passenger-rear | Number | Passenger window rear |
+| last-update | DateTime | Last windows update |
+
+Mapping table for all windows
+
+| Number | Mapping |
+|-----------------|---------------------|
+| 0 | Intermediate |
+| 1 | Open |
+| 2 | Closed |
+| 3 | Airing |
+| 4 | Intermediate |
+| 5 | Running |
+
+### Lights
+
+Group name: `lights`
+All channels `read-only`
+
+| Channel | Type | Description |
+|------------------|----------------------|------------------------------|
+| interior-front | Switch | Interior light front |
+| interior-rear | Switch | Interior light rear |
+| reading-left | Switch | Reading light left |
+| reading-right | Switch | Reading light right |
+| light-switch | Number | Main light switch |
+| last-update | DateTime | Last lights update |
+
+Mapping table `light-switch`
+
+| Number | Mapping |
+|-----------------|---------------------|
+| 0 | Auto |
+| 1 | Headlight |
+| 2 | Sidelight Left |
+| 3 | Sidelight Right |
+| 4 | Parking Light |
+
+### Lock
+
+Group name: `lock`
+All channels `read-only`
+
+| Channel | Type | Description |
+|------------------|----------------------|------------------------------|
+| doors | Number | Lock status all doors |
+| deck-lid | Switch | Deck lid lock |
+| flap | Switch | Flap lock |
+| last-update | DateTime | Last lock update |
+
+Mapping table `doors`
+
+| Number | Mapping |
+|-----------------|---------------------|
+| 0 | Unlocked |
+| 1 | Locked Internal |
+| 2 | Locked External |
+| 3 | Unlocked Selective |
+
+### Location
+
+Group name: `location`
+All channels `readonly`
+
+| Channel | Type | Description |
+|------------------|----------------------|------------------------------|
+| heading | Number:Angle | Vehicle heading |
+| last-update | DateTime | Last location update |
+
+### Image
+
+Provides exterior and interior images for your specific vehicle.
+Group name: `image`
+
+| Channel | Type | Description | Write |
+|------------------|----------------------|------------------------------|-------|
+| image-data | Raw | Vehicle image | |
+| image-view | text | Vehicle image viewpoint | X |
+| clear-cache | Switch | Remove all stored images | X |
+
+**If** the `imageApiKey` in [Bridge Configuration Parameters](#bridge-configuration-parameters) is set the vehicle thing will try to get images.
+Pay attention to the [Advanced Image Configuration Properties](#thing-configuration) before requesting new images.
+Sending commands towards the `image-view` channel will change the image.
+The `image-view` is providing options to select the available images for your specific vehicle.
+Images are stored in `jsondb` so if you requested all images the Mercedes Benz Image API will not be called anymore which is good because you have a restricted amount of calls!
+If you're not satisfied e.g. you want a background you need to
+
+1. change the [Advanced Image Configuration Properties](#thing-configuration)
+2. Switch `clear-cache` channel item to `ON` to clear all images
+3. request them via `image-view`
+
+### Image View Options
+
+You can access the options either in a rule via `YOUR_IMAGE_VIEW_ITEM.getStateDescription().getOptions()` or in UI in widget configuration as _Action: Command options_ and as _Action Item: YOUR_IMAGE_VIEW_ITEM_
+
+
+
+## Troubleshooting
+
+### Authorization fails
+
+The configuration of openHAB account thing and the Mercedes Developer project need an extract match regarding
+
+- MB project credentials vs. `clientId` `clientSecret` and `callbackUrl`
+- MB project subscription of products vs. `scope`
+
+If you follow the [bridge configuration steps](#bridge-configuration) both will match.
+Otherwise you'll receive some error message when clicking the link after opening the `callbackUrl` in your browser
+
+Most common errors:
+
+- redirect URL doesn't match: Double check if `callbackUrl` is really saved correctly in your Mercedes Benz Developer project
+- scope failure: the requested scope doesn't match with the subscribed products.
+ - Check [openHab configuration switches](#openhab-configuration)
+ - apply changes if necessary and don't forget to save
+ - after these steps refresh the `callbackUrl` in [your browser](#callback-page) to apply these changes
+ - try a new authorization clicking the link
+
+### Receive no data
+
+Especially after setting the frist Mercedes Benz Developer Project you'll receive no data.
+It seems that the API isn't _filled_ yet.
+
+**Pre-Condition**
+
+- The Mercedes Me bridge is online = authorization is fine
+- The Mercedes Me thing is online = API calls are fine
+
+**Solution**
+
+- Reduce `refreshInterval` to 1 minute
+- Go to your vehicle, open doors and windows, turn on lights, drive a bit ...
+- wait until values are providing the right states
+
+### Images
+
+Testing the whole image settings is hard due to the restricted call number towards the Image API.
+
+My personal experience during limited testing
+
+| Test |Tested | OK | Not OK | Comment |
+|------------------|-------|-----|---------|---------------------------------------------------------|
+| `format` webp | Yes | X | | |
+| `format` png | Yes | | X | Internal Server Error 500 on Mercedes Server side |
+| `format` jpeg | No | | | Not tested due to missing transparency in jpeg format |
+| all options off | Yes | X | | |
+| `background` | Yes | X | | |
+| `night` | No | | | Not support by my vehicle |
+| `roofOpen` | No | | | Not support by my vehicle |
+| `cropped` | No | | | Not desired from my side |
+
+## Storage
+
+Data is stored in directory `%USER_DATA%/jsondb` for handling tokens and vehicle images.
+
+ * _StorageHandler.For.OAuthClientService.json_ - token is stored with key `clientId` which is provided by `account` [Brige Configuration Parameters](#bridge-configuration-parameters)
+ * _mercedesme_%VEHICLE_VIN%.json_ - images are stored per vehicle. File name contains `vin` configured by [vehicle Thing Configuration](#thing-configuration)
+
+With this data the binding is able to operate without new authorization towards Mercedes each startup and reduces the restricted calls towards image API.
+Also these files are properly stored in your [backup](https://community.openhab.org/t/docs-on-how-to-backup-openhab/100182) e.g. if you perform `openhab-cli backup`
+
+
+## Full example
+
+The example is based on a battery electric vehicle.
+Exchange configuration parameters in the Things section
+
+Bridge
+
+* 4711 - your desired bridge id
+* YOUR_CLIENT_ID - Client ID of the Mercedes Developer project
+* YOUR_CLIENT_SECRET - Client Secret of the Mercedes Developer project
+* YOUR_API_KEY - Image API Key of the Mercedes Developer project
+* YOUR_OPENHAB_SERVER_IP - IP address of your openHAB server
+* 8090 - a **unique** port number - each bridge in your openHAB installation needs to have different port number!
+
+Thing
+
+* eqa - your desired vehicle thing id
+* VEHICLE_VIN - your Vehicle Identification Number
+
+### Things file
+
+```
+Bridge mercedesme:account:4711 "MercedesMe John Doe" [ clientId="YOUR_CLIENT_ID", clientSecret="YOUR_CLIENT_SECRET", imageApiKey="YOUR_API_KEY", callbackIp="YOUR_OPENHAB_SERVER_IP", callbackPort=8092, odoScope=true, vehicleScope=true, lockScope=true, fuelScope=true, evScope=true] {
+ Thing bev eqa "Mercedes EQA" [ vin="VEHICLE_VIN", refreshInterval=5, background=false, night=false, cropped=false, roofOpen=false, format="webp"]
+}
+```
+
+### Items file
+
+```
+Number:Length EQA_Mileage "Odometer [%d %unit%]" {channel="mercedesme:bev:4711:eqa:range#mileage" }
+Number:Length EQA_Range "Range [%d %unit%]" {channel="mercedesme:bev:4711:eqa:range#range-electric"}
+Number:Length EQA_RangeRadius "Range Radius [%d %unit%]" {channel="mercedesme:bev:4711:eqa:range#radius-electric"}
+Number:Dimensionless EQA_BatterySoc "Battery Charge [%.1f %%]" {channel="mercedesme:bev:4711:eqa:range#soc"}
+
+Contact EQA_DriverDoor "Driver Door [%s]" {channel="mercedesme:bev:4711:eqa:doors#driver-front" }
+Contact EQA_DriverDoorRear "Driver Door Rear [%s]" {channel="mercedesme:bev:4711:eqa:doors#driver-rear" }
+Contact EQA_PassengerDoor "Passenger Door [%s]" {channel="mercedesme:bev:4711:eqa:doors#passenger-front" }
+Contact EQA_PassengerDoorRear "Passenger Door Rear [%s]" {channel="mercedesme:bev:4711:eqa:doors#passenger-rear" }
+Number EQA_Trunk "Trunk [%s]" {channel="mercedesme:bev:4711:eqa:doors#deck-lid" }
+Number EQA_Rooftop "Rooftop [%s]" {channel="mercedesme:bev:4711:eqa:doors#rooftop" }
+Number EQA_Sunroof "Sunroof [%s]" {channel="mercedesme:bev:4711:eqa:doors#sunroof" }
+
+Number EQA_DoorLock "Door Lock [%s]" {channel="mercedesme:bev:4711:eqa:lock#doors" }
+Switch EQA_TrunkLock "Trunk Lock [%s]" {channel="mercedesme:bev:4711:eqa:lock#deck-lid" }
+Switch EQA_FlapLock "Charge Flap Lock [%s]" {channel="mercedesme:bev:4711:eqa:lock#flap" }
+
+Number EQA_DriverWindow "Driver Window [%s]" {channel="mercedesme:bev:4711:eqa:windows#driver-front" }
+Number EQA_DriverWindowRear "Driver Window Rear [%s]" {channel="mercedesme:bev:4711:eqa:windows#driver-rear" }
+Number EQA_PassengerWindow "Passenger Window [%s]" {channel="mercedesme:bev:4711:eqa:windows#passenger-front" }
+Number EQA_PassengerWindowRear "Passenger Window Rear [%s]" {channel="mercedesme:bev:4711:eqa:windows#passenger-rear" }
+
+Number:Angle EQA_Heading "Heading [%.1f %unit%]" {channel="mercedesme:bev:4711:eqa:location#heading" }
+
+Image EQA_Image "Image" {channel="mercedesme:bev:4711:eqa:image#image-data" }
+String EQA_ImageViewport "Image Viewport [%s]" {channel="mercedesme:bev:4711:eqa:image#image-view" }
+Switch EQA_ClearCache "Clear Cache [%s]" {channel="mercedesme:bev:4711:eqa:image#clear-cache" }
+
+Switch EQA_InteriorFront "Interior Front Light [%s]" {channel="mercedesme:bev:4711:eqa:lights#interior-front" }
+Switch EQA_InteriorRear "Interior Rear Light [%s]" {channel="mercedesme:bev:4711:eqa:lights#interior-rear" }
+Switch EQA_ReadingLeft "Reading Light Left [%s]" {channel="mercedesme:bev:4711:eqa:lights#reading-left" }
+Switch EQA_ReadingRight "Reading Light Right [%s]" {channel="mercedesme:bev:4711:eqa:lights#reading-right" }
+Number EQA_LightSwitch "Main Light Switch [%s]" {channel="mercedesme:bev:4711:eqa:lights#light-switch" }
+```
+
+### Sitemap
+
+```
+sitemap MB label="Mercedes Benz EQA" {
+ Frame label="EQA Image" {
+ Image item=EQA_Image
+
+ }
+ Frame label="Range" {
+ Text item=EQA_Mileage
+ Text item=EQA_Range
+ Text item=EQA_RangeRadius
+ Text item=EQA_BatterySoc
+ }
+
+ Frame label="Door Details" {
+ Text item=EQA_DriverDoor
+ Text item=EQA_DriverDoorRear
+ Text item=EQA_PassengerDoor
+ Text item=EQA_PassengerDoorRear
+ Text item=EQA_Trunk
+ Text item=EQA_Rooftop
+ Text item=EQA_Sunroof
+ Text item=EQA_DoorLock
+ Text item=EQA_TrunkLock
+ Text item=EQA_FlapLock
+ }
+
+ Frame label="Windows" {
+ Text item=EQA_DriverWindow
+ Text item=EQA_DriverWindowRear
+ Text item=EQA_PassengerWindow
+ Text item=EQA_PassengerWindowRear
+ }
+
+ Frame label="Location" {
+ Text item=EQA_Heading
+ }
+
+ Frame label="Lights" {
+ Text item=EQA_InteriorFront
+ Text item=EQA_InteriorRear
+ Text item=EQA_ReadingLeft
+ Text item=EQA_ReadingRight
+ Text item=EQA_LightSwitch
+ }
+
+ Frame label="Image Properties" {
+ Selection item=EQA_ImageViewport
+ Switch item=EQA_ClearCache
+ }
+}
+```
+
+## Mercedes Benz Developer
+
+Visit [Mercedes Benz Developer](https://developer.mercedes-benz.com/) to gain more deep information.
diff --git a/bundles/org.openhab.binding.mercedesme/doc/CallbackUrl_Page.png b/bundles/org.openhab.binding.mercedesme/doc/CallbackUrl_Page.png
new file mode 100644
index 000000000..eb01ea871
Binary files /dev/null and b/bundles/org.openhab.binding.mercedesme/doc/CallbackUrl_Page.png differ
diff --git a/bundles/org.openhab.binding.mercedesme/doc/ImageRestrictions.png b/bundles/org.openhab.binding.mercedesme/doc/ImageRestrictions.png
new file mode 100644
index 000000000..7203cb0e8
Binary files /dev/null and b/bundles/org.openhab.binding.mercedesme/doc/ImageRestrictions.png differ
diff --git a/bundles/org.openhab.binding.mercedesme/doc/ImageView-CommandOptions.png b/bundles/org.openhab.binding.mercedesme/doc/ImageView-CommandOptions.png
new file mode 100644
index 000000000..2df39306e
Binary files /dev/null and b/bundles/org.openhab.binding.mercedesme/doc/ImageView-CommandOptions.png differ
diff --git a/bundles/org.openhab.binding.mercedesme/doc/MBAccessRequest.png b/bundles/org.openhab.binding.mercedesme/doc/MBAccessRequest.png
new file mode 100644
index 000000000..9c441ed16
Binary files /dev/null and b/bundles/org.openhab.binding.mercedesme/doc/MBAccessRequest.png differ
diff --git a/bundles/org.openhab.binding.mercedesme/doc/MBDeveloper-Credentials.png b/bundles/org.openhab.binding.mercedesme/doc/MBDeveloper-Credentials.png
new file mode 100644
index 000000000..6f4bed5c0
Binary files /dev/null and b/bundles/org.openhab.binding.mercedesme/doc/MBDeveloper-Credentials.png differ
diff --git a/bundles/org.openhab.binding.mercedesme/doc/MBDeveloper-Subscriptions.png b/bundles/org.openhab.binding.mercedesme/doc/MBDeveloper-Subscriptions.png
new file mode 100644
index 000000000..2a3dc12da
Binary files /dev/null and b/bundles/org.openhab.binding.mercedesme/doc/MBDeveloper-Subscriptions.png differ
diff --git a/bundles/org.openhab.binding.mercedesme/doc/MercedesMeConfiguration.png b/bundles/org.openhab.binding.mercedesme/doc/MercedesMeConfiguration.png
new file mode 100644
index 000000000..8505deba8
Binary files /dev/null and b/bundles/org.openhab.binding.mercedesme/doc/MercedesMeConfiguration.png differ
diff --git a/bundles/org.openhab.binding.mercedesme/pom.xml b/bundles/org.openhab.binding.mercedesme/pom.xml
new file mode 100644
index 000000000..41ce846e2
--- /dev/null
+++ b/bundles/org.openhab.binding.mercedesme/pom.xml
@@ -0,0 +1,27 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.4.0-SNAPSHOT
+
+
+
+
+
+ org.json
+ json
+ 20180813
+ compile
+
+
+
+ org.openhab.binding.mercedesme
+
+ openHAB Add-ons :: Bundles :: MercedesMe Binding
+
+
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/feature/feature.xml b/bundles/org.openhab.binding.mercedesme/src/main/feature/feature.xml
new file mode 100644
index 000000000..ee51946a9
--- /dev/null
+++ b/bundles/org.openhab.binding.mercedesme/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.mercedesme/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/Constants.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/Constants.java
new file mode 100644
index 000000000..55fd210f5
--- /dev/null
+++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/Constants.java
@@ -0,0 +1,98 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mercedesme.internal;
+
+import javax.measure.Unit;
+import javax.measure.quantity.Length;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.library.unit.MetricPrefix;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link Constants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class Constants {
+ public static final String BINDING_ID = "mercedesme";
+
+ public static final String COMBUSTION = "combustion";
+ public static final String HYBRID = "hybrid";
+ public static final String BEV = "bev";
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account");
+ public static final ThingTypeUID THING_TYPE_COMB = new ThingTypeUID(BINDING_ID, COMBUSTION);
+ public static final ThingTypeUID THING_TYPE_HYBRID = new ThingTypeUID(BINDING_ID, HYBRID);
+ public static final ThingTypeUID THING_TYPE_BEV = new ThingTypeUID(BINDING_ID, BEV);
+
+ public static final String GROUP_RANGE = "range";
+ public static final String GROUP_DOORS = "doors";
+ public static final String GROUP_WINDOWS = "windows";
+ public static final String GROUP_LOCK = "lock";
+ public static final String GROUP_LIGHTS = "lights";
+ public static final String GROUP_LOCATION = "location";
+ public static final String GROUP_IMAGE = "image";
+
+ public static final String MB_AUTH_URL = "https://id.mercedes-benz.com/as/authorization.oauth2";
+ public static final String MB_TOKEN_URL = "https://id.mercedes-benz.com/as/token.oauth2";
+ public static final String CALLBACK_ENDPOINT = "/mb-callback";
+ public static final String OAUTH_CLIENT_NAME = "#byocar";
+
+ // https://developer.mercedes-benz.com/products/electric_vehicle_status/docs
+ public static final String SCOPE_EV = "mb:vehicle:mbdata:evstatus";
+ // https://developer.mercedes-benz.com/products/fuel_status/docs
+ public static final String SCOPE_FUEL = "mb:vehicle:mbdata:fuelstatus";
+ // https://developer.mercedes-benz.com/products/pay_as_you_drive_insurance/docs
+ public static final String SCOPE_ODO = "mb:vehicle:mbdata:payasyoudrive";
+ // https://developer.mercedes-benz.com/products/vehicle_lock_status/docs
+ public static final String SCOPE_LOCK = "mb:vehicle:mbdata:vehiclelock";
+ // https://developer.mercedes-benz.com/products/vehicle_status/docs
+ public static final String SCOPE_STATUS = "mb:vehicle:mbdata:vehiclestatus";
+ public static final String SCOPE_OFFLINE = "offline_access";
+
+ public static final String BASE_URL = "https://api.mercedes-benz.com/vehicledata/v2";
+ public static final String ODO_URL = BASE_URL + "/vehicles/%s/containers/payasyoudrive";
+ public static final String STATUS_URL = BASE_URL + "/vehicles/%s/containers/vehiclestatus";
+ public static final String LOCK_URL = BASE_URL + "/vehicles/%s/containers/vehiclelockstatus";
+ public static final String FUEL_URL = BASE_URL + "/vehicles/%s/containers/fuelstatus";
+ public static final String EV_URL = BASE_URL + "/vehicles/%s/containers/electricvehicle";
+
+ // https://developer.mercedes-benz.com/content-page/api_migration_guide
+ public static final String IMAGE_BASE_URL = "https://api.mercedes-benz.com/vehicle_images/v2";
+ public static final String IMAGE_EXTERIOR_RESOURCE_URL = IMAGE_BASE_URL + "/vehicles/%s";
+
+ public static final String STATUS_TEXT_PREFIX = "@text/mercedesme.";
+ public static final String STATUS_AUTH_NEEDED = ".status.authorization-needed";
+ public static final String STATUS_IP_MISSING = ".status.ip-missing";
+ public static final String STATUS_PORT_MISSING = ".status.port-missing";
+ public static final String STATUS_CLIENT_ID_MISSING = ".status.client-id-missing";
+ public static final String STATUS_CLIENT_SECRET_MISSING = ".status.client-secret-missing";
+ public static final String STATUS_SERVER_RESTART = ".status.server-restart";
+ public static final String STATUS_BRIDGE_MISSING = ".status.bridge-missing";
+ public static final String STATUS_BRIDGE_ATHORIZATION = ".status.bridge-authoriziation";
+
+ public static final String SPACE = " ";
+ public static final String EMPTY = "";
+ public static final String COLON = ":";
+ public static final String NOT_SET = "not set";
+
+ public static final String CODE = "code";
+ public static final String MIME_PREFIX = "image/";
+
+ public static final Unit KILOMETRE_UNIT = MetricPrefix.KILO(SIUnits.METRE);
+}
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/MercedesMeCommandOptionProvider.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/MercedesMeCommandOptionProvider.java
new file mode 100644
index 000000000..ea4a91d43
--- /dev/null
+++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/MercedesMeCommandOptionProvider.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mercedesme.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.events.EventPublisher;
+import org.openhab.core.thing.binding.BaseDynamicCommandDescriptionProvider;
+import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
+import org.openhab.core.thing.link.ItemChannelLinkRegistry;
+import org.openhab.core.thing.type.DynamicCommandDescriptionProvider;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * Dynamic provider of command options while leaving other state description fields as original.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = { DynamicCommandDescriptionProvider.class, MercedesMeCommandOptionProvider.class })
+public class MercedesMeCommandOptionProvider extends BaseDynamicCommandDescriptionProvider {
+ @Activate
+ public MercedesMeCommandOptionProvider(final @Reference EventPublisher eventPublisher, //
+ final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry, //
+ final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
+ this.eventPublisher = eventPublisher;
+ this.itemChannelLinkRegistry = itemChannelLinkRegistry;
+ this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
+ }
+}
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/MercedesMeHandlerFactory.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/MercedesMeHandlerFactory.java
new file mode 100644
index 000000000..f6edf1d28
--- /dev/null
+++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/MercedesMeHandlerFactory.java
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mercedesme.internal;
+
+import static org.openhab.binding.mercedesme.internal.Constants.*;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.WWWAuthenticationProtocolHandler;
+import org.openhab.binding.mercedesme.internal.handler.AccountHandler;
+import org.openhab.binding.mercedesme.internal.handler.VehicleHandler;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
+import org.openhab.core.i18n.TimeZoneProvider;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.storage.StorageService;
+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.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link MercedesMeHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.mercedesme", service = ThingHandlerFactory.class)
+public class MercedesMeHandlerFactory extends BaseThingHandlerFactory {
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_BEV, THING_TYPE_COMB,
+ THING_TYPE_HYBRID, THING_TYPE_ACCOUNT);
+
+ private final Logger logger = LoggerFactory.getLogger(MercedesMeHandlerFactory.class);
+ private final OAuthFactory oAuthFactory;
+ private final HttpClient httpClient;
+ private final MercedesMeCommandOptionProvider mmcop;
+ private final MercedesMeStateOptionProvider mmsop;
+ private final StorageService storageService;
+ private final TimeZoneProvider timeZoneProvider;
+
+ @Activate
+ public MercedesMeHandlerFactory(@Reference OAuthFactory oAuthFactory, @Reference HttpClientFactory hcf,
+ @Reference StorageService storageService, final @Reference MercedesMeCommandOptionProvider cop,
+ final @Reference MercedesMeStateOptionProvider sop, final @Reference TimeZoneProvider tzp) {
+ this.oAuthFactory = oAuthFactory;
+ this.storageService = storageService;
+ mmcop = cop;
+ mmsop = sop;
+ timeZoneProvider = tzp;
+ httpClient = hcf.createHttpClient(Constants.BINDING_ID);
+ // https://github.com/jetty-project/jetty-reactive-httpclient/issues/33
+ httpClient.getProtocolHandlers().remove(WWWAuthenticationProtocolHandler.NAME);
+ try {
+ httpClient.start();
+ } catch (Exception e) {
+ logger.warn("HTTP client not started: {} - no web access possible!", e.getLocalizedMessage());
+ }
+ }
+
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+ if (THING_TYPE_ACCOUNT.equals(thingTypeUID)) {
+ return new AccountHandler((Bridge) thing, httpClient, oAuthFactory);
+ }
+ return new VehicleHandler(thing, httpClient, thingTypeUID.getId(), storageService, mmcop, mmsop,
+ timeZoneProvider);
+ }
+
+ @Override
+ protected void deactivate(ComponentContext componentContext) {
+ super.deactivate(componentContext);
+ try {
+ httpClient.stop();
+ } catch (Exception e) {
+ logger.debug("HTTP client not stopped: {}", e.getLocalizedMessage());
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/MercedesMeStateOptionProvider.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/MercedesMeStateOptionProvider.java
new file mode 100644
index 000000000..ea3ded80c
--- /dev/null
+++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/MercedesMeStateOptionProvider.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mercedesme.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.events.EventPublisher;
+import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
+import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
+import org.openhab.core.thing.link.ItemChannelLinkRegistry;
+import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
+import org.osgi.service.component.annotations.Activate;
+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
+ */
+@NonNullByDefault
+@Component(service = { DynamicStateDescriptionProvider.class, MercedesMeStateOptionProvider.class })
+public class MercedesMeStateOptionProvider extends BaseDynamicStateDescriptionProvider {
+ @Activate
+ public MercedesMeStateOptionProvider(final @Reference EventPublisher eventPublisher, //
+ final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry, //
+ final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
+ this.eventPublisher = eventPublisher;
+ this.itemChannelLinkRegistry = itemChannelLinkRegistry;
+ this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
+ }
+}
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/config/AccountConfiguration.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/config/AccountConfiguration.java
new file mode 100644
index 000000000..6c4c43c04
--- /dev/null
+++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/config/AccountConfiguration.java
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mercedesme.internal.config;
+
+import static org.openhab.binding.mercedesme.internal.Constants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link AccountConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class AccountConfiguration {
+
+ public String clientId = NOT_SET;
+ public String clientSecret = NOT_SET;
+ public String imageApiKey = NOT_SET;
+
+ // Advanced Parameters
+ public String callbackIP = NOT_SET;
+ public int callbackPort = -1;
+ public boolean odoScope = true;
+ public boolean vehicleScope = true;
+ public boolean lockScope = true;
+ public boolean fuelScope = true;
+ public boolean evScope = true;
+
+ // https://developer.mercedes-benz.com/products/electric_vehicle_status/docs#_required_scopes
+ public String getScope() {
+ StringBuffer sb = new StringBuffer();
+ sb.append(SCOPE_OFFLINE);
+ if (odoScope) {
+ sb.append(SPACE).append(SCOPE_ODO);
+ }
+ if (vehicleScope) {
+ sb.append(SPACE).append(SCOPE_STATUS);
+ }
+ if (lockScope) {
+ sb.append(SPACE).append(SCOPE_LOCK);
+ }
+ if (fuelScope) {
+ sb.append(SPACE).append(SCOPE_FUEL);
+ }
+ if (evScope) {
+ sb.append(SPACE).append(SCOPE_EV);
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public String toString() {
+ return "ID " + clientId + ", Secret " + clientSecret + ", IP " + callbackIP + ", Port " + callbackPort
+ + ", scope " + getScope();
+ }
+}
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/config/VehicleConfiguration.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/config/VehicleConfiguration.java
new file mode 100644
index 000000000..798f1db75
--- /dev/null
+++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/config/VehicleConfiguration.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mercedesme.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mercedesme.internal.Constants;
+
+/**
+ * The {@link VehicleConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class VehicleConfiguration {
+
+ public String vin = Constants.NOT_SET;
+ public int refreshInterval = 5;
+ public float batteryCapacity = -1;
+ public float fuelCapacity = -1;
+
+ // Advanced
+ public boolean background = false;
+ public boolean night = false;
+ public boolean cropped = false;
+ public boolean roofOpen = false;
+ public String format = "webp";
+}
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/handler/AccountHandler.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/handler/AccountHandler.java
new file mode 100644
index 000000000..ccb60194e
--- /dev/null
+++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/handler/AccountHandler.java
@@ -0,0 +1,165 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mercedesme.internal.handler;
+
+import java.net.SocketException;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.mercedesme.internal.Constants;
+import org.openhab.binding.mercedesme.internal.config.AccountConfiguration;
+import org.openhab.binding.mercedesme.internal.server.CallbackServer;
+import org.openhab.binding.mercedesme.internal.server.Utils;
+import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener;
+import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
+import org.openhab.core.config.core.Configuration;
+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.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link AccountHandler} takes care of the valid authorization for the user account
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class AccountHandler extends BaseBridgeHandler implements AccessTokenRefreshListener {
+ private final Logger logger = LoggerFactory.getLogger(AccountHandler.class);
+ private final OAuthFactory oAuthFactory;
+ private final HttpClient httpClient;
+ private Optional server = Optional.empty();
+
+ Optional config = Optional.empty();
+
+ public AccountHandler(Bridge bridge, HttpClient hc, OAuthFactory oaf) {
+ super(bridge);
+ httpClient = hc;
+ oAuthFactory = oaf;
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ // no commands available
+ }
+
+ @Override
+ public void initialize() {
+ config = Optional.of(getConfigAs(AccountConfiguration.class));
+ autodetectCallback();
+ String configValidReason = configValid();
+ if (!configValidReason.isEmpty()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configValidReason);
+ } else {
+ String callbackUrl = Utils.getCallbackAddress(config.get().callbackIP, config.get().callbackPort);
+ thing.setProperty("callbackUrl", callbackUrl);
+ server = Optional.of(new CallbackServer(this, httpClient, oAuthFactory, config.get(), callbackUrl));
+ if (!server.get().start()) {
+ String textKey = Constants.STATUS_TEXT_PREFIX + thing.getThingTypeUID().getId()
+ + Constants.STATUS_SERVER_RESTART;
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, textKey);
+ } else {
+ // get fresh token
+ this.getToken();
+ }
+ }
+ }
+
+ private void autodetectCallback() {
+ // if Callback IP and Callback Port are not set => autodetect these values
+ config = Optional.of(getConfigAs(AccountConfiguration.class));
+ Configuration updateConfig = super.editConfiguration();
+ if (!updateConfig.containsKey("callbackPort")) {
+ updateConfig.put("callbackPort", Utils.getFreePort());
+ } else {
+ Utils.addPort(config.get().callbackPort);
+ }
+ if (!updateConfig.containsKey("callbackIP")) {
+ String ip;
+ try {
+ ip = Utils.getCallbackIP();
+ updateConfig.put("callbackIP", ip);
+ } catch (SocketException e) {
+ logger.info("Cannot detect IP address {}", e.getMessage());
+ }
+ }
+ super.updateConfiguration(updateConfig);
+ // get new config after update
+ config = Optional.of(getConfigAs(AccountConfiguration.class));
+ }
+
+ private String configValid() {
+ config = Optional.of(getConfigAs(AccountConfiguration.class));
+ String textKey = Constants.STATUS_TEXT_PREFIX + thing.getThingTypeUID().getId();
+ if (config.get().callbackIP.equals(Constants.NOT_SET)) {
+ return textKey + Constants.STATUS_IP_MISSING;
+ } else if (config.get().callbackPort == -1) {
+ return textKey + Constants.STATUS_PORT_MISSING;
+ } else if (config.get().clientId.equals(Constants.NOT_SET)) {
+ return textKey + Constants.STATUS_CLIENT_ID_MISSING;
+ } else if (config.get().clientSecret.equals(Constants.NOT_SET)) {
+ return textKey + Constants.STATUS_CLIENT_SECRET_MISSING;
+ } else {
+ return Constants.EMPTY;
+ }
+ }
+
+ @Override
+ public void dispose() {
+ if (!server.isEmpty()) {
+ server.get().stop();
+ Utils.removePort(config.get().callbackPort);
+ }
+ }
+
+ /**
+ * https://next.openhab.org/javadoc/latest/org/openhab/core/auth/client/oauth2/package-summary.html
+ */
+ @Override
+ public void onAccessTokenResponse(AccessTokenResponse tokenResponse) {
+ if (!tokenResponse.getAccessToken().isEmpty()) {
+ // token not empty - fine
+ updateStatus(ThingStatus.ONLINE);
+ } else if (server.isEmpty()) {
+ // server not running - fix first
+ String textKey = Constants.STATUS_TEXT_PREFIX + thing.getThingTypeUID().getId()
+ + Constants.STATUS_SERVER_RESTART;
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, textKey);
+ } else {
+ // all failed - start manual authorization
+ String textKey = Constants.STATUS_TEXT_PREFIX + thing.getThingTypeUID().getId()
+ + Constants.STATUS_AUTH_NEEDED;
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
+ textKey + " [\"" + thing.getProperties().get("callbackUrl") + "\"]");
+ }
+ }
+
+ public String getToken() {
+ return server.get().getToken();
+ }
+
+ public String getImageApiKey() {
+ return config.get().imageApiKey;
+ }
+
+ @Override
+ public String toString() {
+ return Integer.toString(config.get().callbackPort);
+ }
+}
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/handler/VehicleHandler.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/handler/VehicleHandler.java
new file mode 100644
index 000000000..b8f741127
--- /dev/null
+++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/handler/VehicleHandler.java
@@ -0,0 +1,579 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mercedesme.internal.handler;
+
+import static org.openhab.binding.mercedesme.internal.Constants.*;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.measure.quantity.Length;
+
+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.http.HttpHeader;
+import org.eclipse.jetty.util.MultiMap;
+import org.eclipse.jetty.util.UrlEncoded;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.openhab.binding.mercedesme.internal.Constants;
+import org.openhab.binding.mercedesme.internal.MercedesMeCommandOptionProvider;
+import org.openhab.binding.mercedesme.internal.MercedesMeStateOptionProvider;
+import org.openhab.binding.mercedesme.internal.config.VehicleConfiguration;
+import org.openhab.binding.mercedesme.internal.utils.ChannelStateMap;
+import org.openhab.binding.mercedesme.internal.utils.Mapper;
+import org.openhab.core.i18n.TimeZoneProvider;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.RawType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.storage.Storage;
+import org.openhab.core.storage.StorageService;
+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.BaseThingHandler;
+import org.openhab.core.thing.binding.BridgeHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.CommandOption;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.StateOption;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link VehicleHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class VehicleHandler extends BaseThingHandler {
+ private static final String EXT_IMG_RES = "ExtImageResources_";
+ private static final String INITIALIZE_COMMAND = "Initialze";
+
+ private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
+ private final Map timeHash = new HashMap();
+ private final MercedesMeCommandOptionProvider mmcop;
+ private final MercedesMeStateOptionProvider mmsop;
+ private final TimeZoneProvider timeZoneProvider;
+ private final StorageService storageService;
+ private final HttpClient httpClient;
+ private final String uid;
+
+ private Optional> refreshJob = Optional.empty();
+ private Optional accountHandler = Optional.empty();
+ private Optional> rangeElectric = Optional.empty();
+ private Optional> imageStorage = Optional.empty();
+ private Optional config = Optional.empty();
+ private Optional> rangeFuel = Optional.empty();
+ private Instant nextRefresh;
+ private boolean online = false;
+
+ public VehicleHandler(Thing thing, HttpClient hc, String uid, StorageService storageService,
+ MercedesMeCommandOptionProvider mmcop, MercedesMeStateOptionProvider mmsop, TimeZoneProvider tzp) {
+ super(thing);
+ httpClient = hc;
+ this.uid = uid;
+ this.mmcop = mmcop;
+ this.mmsop = mmsop;
+ timeZoneProvider = tzp;
+ this.storageService = storageService;
+ nextRefresh = Instant.now();
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ logger.trace("Received {} {} {}", channelUID.getAsString(), command.toFullString(), channelUID.getId());
+ if (command instanceof RefreshType) {
+ /**
+ * Refresh requested e.g. after adding new item
+ * Adding several items will frequently raise RefreshType command. Calling API each time shall be avoided
+ * API update is performed after 5 seconds for all items which should be sufficient for a frequent update
+ */
+ if (Instant.now().isAfter(nextRefresh)) {
+ nextRefresh = Instant.now().plus(Duration.ofSeconds(5));
+ logger.trace("Refresh granted - next at {}", nextRefresh);
+ scheduler.schedule(this::getData, 5, TimeUnit.SECONDS);
+ }
+ } else if ("image-view".equals(channelUID.getIdWithoutGroup())) {
+ if (imageStorage.isPresent()) {
+ if (INITIALIZE_COMMAND.equals(command.toFullString())) {
+ getImageResources();
+ }
+ String key = command.toFullString() + "_" + config.get().vin;
+ String encodedImage = EMPTY;
+ if (imageStorage.get().containsKey(key)) {
+ encodedImage = imageStorage.get().get(key);
+ logger.trace("Image {} found in storage", key);
+ } else {
+ logger.trace("Request Image {} ", key);
+ encodedImage = getImage(command.toFullString());
+ if (!encodedImage.isEmpty()) {
+ imageStorage.get().put(key, encodedImage);
+ }
+ }
+ if (encodedImage != null && !encodedImage.isEmpty()) {
+ RawType image = new RawType(Base64.getDecoder().decode(encodedImage),
+ MIME_PREFIX + config.get().format);
+ updateState(new ChannelUID(thing.getUID(), GROUP_IMAGE, "image-data"), image);
+ } else {
+ logger.debug("Image {} is empty", key);
+ }
+ }
+ } else if (channelUID.getIdWithoutGroup().equals("clear-cache") && command.equals(OnOffType.ON)) {
+ List removals = new ArrayList();
+ imageStorage.get().getKeys().forEach(entry -> {
+ if (entry.contains("_" + config.get().vin)) {
+ removals.add(entry);
+ }
+ });
+ removals.forEach(entry -> {
+ imageStorage.get().remove(entry);
+ });
+ updateState(new ChannelUID(thing.getUID(), GROUP_IMAGE, "clear-cache"), OnOffType.OFF);
+ getImageResources();
+ }
+ }
+
+ @Override
+ public void initialize() {
+ config = Optional.of(getConfigAs(VehicleConfiguration.class));
+ Bridge bridge = getBridge();
+ if (bridge != null) {
+ updateStatus(ThingStatus.UNKNOWN);
+ BridgeHandler handler = bridge.getHandler();
+ if (handler != null) {
+ accountHandler = Optional.of((AccountHandler) handler);
+ startSchedule(config.get().refreshInterval);
+ if (!config.get().vin.equals(NOT_SET)) {
+ imageStorage = Optional.of(storageService.getStorage(BINDING_ID + "_" + config.get().vin));
+ if (!imageStorage.get().containsKey(EXT_IMG_RES + config.get().vin)) {
+ getImageResources();
+ }
+ setImageOtions();
+ }
+ updateState(new ChannelUID(thing.getUID(), GROUP_IMAGE, "clear-cache"), OnOffType.OFF);
+ } else {
+ throw new IllegalStateException("BridgeHandler is null");
+ }
+ } else {
+ String textKey = Constants.STATUS_TEXT_PREFIX + "vehicle" + Constants.STATUS_BRIDGE_MISSING;
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, textKey);
+ }
+ }
+
+ private void startSchedule(int interval) {
+ refreshJob.ifPresentOrElse(job -> {
+ if (job.isCancelled()) {
+ refreshJob = Optional
+ .of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES));
+ } // else - scheduler is already running!
+ }, () -> {
+ refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES));
+ });
+ }
+
+ @Override
+ public void dispose() {
+ refreshJob.ifPresent(job -> job.cancel(true));
+ }
+
+ public void getData() {
+ if (accountHandler.isEmpty()) {
+ logger.warn("AccountHandler not set");
+ return;
+ }
+ String token = accountHandler.get().getToken();
+ if (token.isEmpty()) {
+ String textKey = Constants.STATUS_TEXT_PREFIX + "vehicle" + Constants.STATUS_BRIDGE_ATHORIZATION;
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, textKey);
+ return;
+ } else if (!online) { // only update if thing isn't already ONLINE
+ updateStatus(ThingStatus.ONLINE);
+ }
+
+ // Mileage for all cars
+ String odoUrl = String.format(ODO_URL, config.get().vin);
+ if (accountConfigAvailable()) {
+ if (accountHandler.get().config.get().odoScope) {
+ call(odoUrl);
+ } else {
+ logger.trace("{} Odo scope not activated", this.getThing().getLabel());
+ }
+ } else {
+ logger.trace("{} Account not properly configured", this.getThing().getLabel());
+ }
+
+ // Electric status for hybrid and electric
+ if (uid.equals(BEV) || uid.equals(HYBRID)) {
+ String evUrl = String.format(EV_URL, config.get().vin);
+ if (accountConfigAvailable()) {
+ if (accountHandler.get().config.get().evScope) {
+ call(evUrl);
+ } else {
+ logger.trace("{} Electric Status scope not activated", this.getThing().getLabel());
+ }
+ } else {
+ logger.trace("{} Account not properly configured", this.getThing().getLabel());
+ }
+ }
+
+ // Fuel for hybrid and combustion
+ if (uid.equals(COMBUSTION) || uid.equals(HYBRID)) {
+ String fuelUrl = String.format(FUEL_URL, config.get().vin);
+ if (accountConfigAvailable()) {
+ if (accountHandler.get().config.get().fuelScope) {
+ call(fuelUrl);
+ } else {
+ logger.trace("{} Fuel scope not activated", this.getThing().getLabel());
+ }
+ } else {
+ logger.trace("{} Account not properly configured", this.getThing().getLabel());
+ }
+ }
+
+ // Status and Lock for all
+ String statusUrl = String.format(STATUS_URL, config.get().vin);
+ if (accountConfigAvailable()) {
+ if (accountHandler.get().config.get().vehicleScope) {
+ call(statusUrl);
+ } else {
+ logger.trace("{} Vehicle Status scope not activated", this.getThing().getLabel());
+ }
+ } else {
+ logger.trace("{} Account not properly configured", this.getThing().getLabel());
+ }
+ String lockUrl = String.format(LOCK_URL, config.get().vin);
+ if (accountConfigAvailable()) {
+ if (accountHandler.get().config.get().lockScope) {
+ call(lockUrl);
+ } else {
+ logger.trace("{} Lock scope not activated", this.getThing().getLabel());
+ }
+ } else {
+ logger.trace("{} Account not properly configured", this.getThing().getLabel());
+ }
+
+ // Range radius for all types
+ updateRadius();
+ }
+
+ private boolean accountConfigAvailable() {
+ if (accountHandler.isPresent()) {
+ if (accountHandler.get().config.isPresent()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void getImageResources() {
+ if (accountHandler.get().getImageApiKey().equals(NOT_SET)) {
+ logger.debug("Image API key not set");
+ return;
+ }
+ // add config parameters
+ MultiMap parameterMap = new MultiMap();
+ parameterMap.add("background", Boolean.toString(config.get().background));
+ parameterMap.add("night", Boolean.toString(config.get().night));
+ parameterMap.add("cropped", Boolean.toString(config.get().cropped));
+ parameterMap.add("roofOpen", Boolean.toString(config.get().roofOpen));
+ parameterMap.add("fileFormat", config.get().format);
+ String params = UrlEncoded.encode(parameterMap, StandardCharsets.UTF_8, false);
+ String url = String.format(IMAGE_EXTERIOR_RESOURCE_URL, config.get().vin) + "?" + params;
+ logger.debug("Get Image resources {} {} ", accountHandler.get().getImageApiKey(), url);
+ Request req = httpClient.newRequest(url);
+ req.header("x-api-key", accountHandler.get().getImageApiKey());
+ req.header(HttpHeader.ACCEPT, "application/json");
+ try {
+ ContentResponse cr = req.send();
+ if (cr.getStatus() == 200) {
+ imageStorage.get().put(EXT_IMG_RES + config.get().vin, cr.getContentAsString());
+ setImageOtions();
+ } else {
+ logger.debug("Failed to get image resources {} {}", cr.getStatus(), cr.getContentAsString());
+ }
+ } catch (InterruptedException | TimeoutException | ExecutionException e) {
+ logger.debug("Error getting image resources {}", e.getMessage());
+ }
+ }
+
+ private void setImageOtions() {
+ List entries = new ArrayList();
+ if (imageStorage.get().containsKey(EXT_IMG_RES + config.get().vin)) {
+ String resources = imageStorage.get().get(EXT_IMG_RES + config.get().vin);
+ JSONObject jo = new JSONObject(resources);
+ jo.keySet().forEach(entry -> {
+ entries.add(entry);
+ });
+ }
+ Collections.sort(entries);
+ List commandOptions = new ArrayList();
+ List stateOptions = new ArrayList();
+ entries.forEach(entry -> {
+ CommandOption co = new CommandOption(entry, null);
+ commandOptions.add(co);
+ StateOption so = new StateOption(entry, null);
+ stateOptions.add(so);
+ });
+ if (commandOptions.isEmpty()) {
+ commandOptions.add(new CommandOption("Initilaze", null));
+ stateOptions.add(new StateOption("Initilaze", null));
+ }
+ ChannelUID cuid = new ChannelUID(thing.getUID(), GROUP_IMAGE, "image-view");
+ mmcop.setCommandOptions(cuid, commandOptions);
+ mmsop.setStateOptions(cuid, stateOptions);
+ }
+
+ private String getImage(String key) {
+ if (accountHandler.get().getImageApiKey().equals(NOT_SET)) {
+ logger.debug("Image API key not set");
+ return EMPTY;
+ }
+ String imageId = EMPTY;
+ if (imageStorage.get().containsKey(EXT_IMG_RES + config.get().vin)) {
+ String resources = imageStorage.get().get(EXT_IMG_RES + config.get().vin);
+ JSONObject jo = new JSONObject(resources);
+ if (jo.has(key)) {
+ imageId = jo.getString(key);
+ }
+ } else {
+ getImageResources();
+ return EMPTY;
+ }
+
+ String url = IMAGE_BASE_URL + "/images/" + imageId;
+ Request req = httpClient.newRequest(url);
+ req.header("x-api-key", accountHandler.get().getImageApiKey());
+ req.header(HttpHeader.ACCEPT, "*/*");
+ ContentResponse cr;
+ try {
+ cr = req.send();
+ byte[] response = cr.getContent();
+ return Base64.getEncoder().encodeToString(response);
+ } catch (InterruptedException | TimeoutException | ExecutionException e) {
+ logger.warn("Get Image {} error {}", url, e.getMessage());
+ }
+ return EMPTY;
+ }
+
+ private void call(String url) {
+ String requestUrl = String.format(url, config.get().vin);
+ // Calculate endpoint for debugging
+ String[] endpoint = requestUrl.split("/");
+ String finalEndpoint = endpoint[endpoint.length - 1];
+ // debug prefix contains Thing label and call endpoint for propper debugging
+ String debugPrefix = this.getThing().getLabel() + Constants.COLON + finalEndpoint;
+
+ Request req = httpClient.newRequest(requestUrl);
+ req.header(HttpHeader.AUTHORIZATION, "Bearer " + accountHandler.get().getToken());
+ try {
+ ContentResponse cr = req.send();
+ logger.trace("{} Response {} {}", debugPrefix, cr.getStatus(), cr.getContentAsString());
+ if (cr.getStatus() == 200) {
+ distributeContent(cr.getContentAsString().trim());
+ }
+ } catch (InterruptedException | TimeoutException | ExecutionException e) {
+ logger.info("{} Error getting data {}", debugPrefix, e.getMessage());
+ fallbackCall(requestUrl);
+ }
+ }
+
+ /**
+ * Fallback solution with Java11 classes
+ * Performs try with Java11 HttpClient - https://zetcode.com/java/getpostrequest/ to identify Community problem
+ * https://community.openhab.org/t/mercedes-me-binding/136852/21
+ *
+ * @param requestUrl
+ */
+ private void fallbackCall(String requestUrl) {
+ // Calculate endpoint for debugging
+ String[] endpoint = requestUrl.split("/");
+ String finalEndpoint = endpoint[endpoint.length - 1];
+ // debug prefix contains Thing label and call endpoint for propper debugging
+ String debugPrefix = this.getThing().getLabel() + Constants.COLON + finalEndpoint;
+
+ java.net.http.HttpClient client = java.net.http.HttpClient.newHttpClient();
+ HttpRequest request = HttpRequest.newBuilder().uri(URI.create(requestUrl))
+ .header(HttpHeader.AUTHORIZATION.toString(), "Bearer " + accountHandler.get().getToken()).GET().build();
+ try {
+ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
+ logger.debug("{} Fallback Response {} {}", debugPrefix, response.statusCode(), response.body());
+ if (response.statusCode() == 200) {
+ distributeContent(response.body().trim());
+ }
+ } catch (IOException | InterruptedException e) {
+ logger.warn("{} Error getting data via fallback {}", debugPrefix, e.getMessage());
+ }
+ }
+
+ private void distributeContent(String json) {
+ if (json.startsWith("[") && json.endsWith("]")) {
+ JSONArray ja = new JSONArray(json);
+ for (Iterator