]> git.basschouten.com Git - openhab-addons.git/commitdiff
[mercedesme] Initial contribution (#13044)
authorBernd Weymann <bernd.weymann@gmail.com>
Tue, 23 Aug 2022 20:42:39 +0000 (22:42 +0200)
committerGitHub <noreply@github.com>
Tue, 23 Aug 2022 20:42:39 +0000 (22:42 +0200)
Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
65 files changed:
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.mercedesme/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/README.md [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/doc/CallbackUrl_Page.png [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/doc/ImageRestrictions.png [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/doc/ImageView-CommandOptions.png [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/doc/MBAccessRequest.png [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/doc/MBDeveloper-Credentials.png [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/doc/MBDeveloper-Subscriptions.png [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/doc/MercedesMeConfiguration.png [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/Constants.java [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/MercedesMeCommandOptionProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/MercedesMeHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/MercedesMeStateOptionProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/config/AccountConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/config/VehicleConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/handler/AccountHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/handler/VehicleHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/CallbackServer.java [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/CallbackServlet.java [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/Utils.java [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/utils/ChannelStateMap.java [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/utils/Mapper.java [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/bev-config.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/bridge-config.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/conv-config.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/hybrid-config.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/i18n/mercedesme.properties [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/bridge-account.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/door-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/doors-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/image-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/image-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/light-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/lights-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/location-channel-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/location-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/lock-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/lock-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/range-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/range-conv-channel-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/range-ev-channel-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/range-hybrid-channel-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/thing-bev.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/thing-combustion.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/thing-hybrid.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/window-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/window-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/ConfigurationTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/ImageTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/JsonTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/test/resources/eqa-light-sample.json [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/test/resources/evstatus.json [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/test/resources/fuel.json [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/test/resources/image/ext.json [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/test/resources/invalid-key.json [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/test/resources/invalid-timestamp.json [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/test/resources/lock.json [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/test/resources/odo.json [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/test/resources/status-resources.json [new file with mode: 0644]
bundles/org.openhab.binding.mercedesme/src/test/resources/status.json [new file with mode: 0644]
bundles/pom.xml

index f7cb83f0ef58873de1a0785cd945efeb12500a23..2104eaff37df180ae662ac5d95a04b8ecdf9d757 100644 (file)
       <artifactId>org.openhab.binding.melcloud</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.mercedesme</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.meteoalerte</artifactId>
diff --git a/bundles/org.openhab.binding.mercedesme/NOTICE b/bundles/org.openhab.binding.mercedesme/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -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 (file)
index 0000000..ba06c97
--- /dev/null
@@ -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
+
+<img src="./doc/MBDeveloper-Credentials.png" width="500" height="280"/>
+
+### MB Product Subscriptions
+
+<img src="./doc/MBDeveloper-Subscriptions.png" width="500" height="300"/>
+
+### openHAB Configuration
+
+<img src="./doc/MercedesMeConfiguration.png" width="400" height="500"/>
+
+### MB Access Request
+
+<img src="./doc/MBAccessRequest.png" width="500" height="220"/>
+
+### Callback page
+
+<img src="./doc/CallbackUrl_Page.png" width="500" height="350"/>
+
+
+### 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!
+
+<img src="./doc/ImageRestrictions.png" width="800" height="36"/>
+
+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_ 
+
+<img src="./doc/ImageView-CommandOptions.png" width="400" height="350"/>
+
+## 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 (file)
index 0000000..eb01ea8
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 (file)
index 0000000..7203cb0
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 (file)
index 0000000..2df3930
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 (file)
index 0000000..9c441ed
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 (file)
index 0000000..6f4bed5
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 (file)
index 0000000..2a3dc12
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 (file)
index 0000000..8505deb
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 (file)
index 0000000..41ce846
--- /dev/null
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.4.0-SNAPSHOT</version>
+  </parent>
+
+  <dependencies>
+    <!-- version needs to match with other projects like org.openhab.io.openhabcloud.pom.xml -->
+    <dependency>
+      <groupId>org.json</groupId>
+      <artifactId>json</artifactId>
+      <version>20180813</version>
+      <scope>compile</scope>
+    </dependency>
+  </dependencies>
+
+  <artifactId>org.openhab.binding.mercedesme</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: MercedesMe Binding</name>
+
+</project>
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 (file)
index 0000000..ee51946
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.mercedesme-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-mercedesme" description="MercedesMe Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.mercedesme/${project.version}</bundle>
+       </feature>
+</features>
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 (file)
index 0000000..55fd210
--- /dev/null
@@ -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<Length> 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 (file)
index 0000000..ea4a91d
--- /dev/null
@@ -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 (file)
index 0000000..f6edf1d
--- /dev/null
@@ -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<ThingTypeUID> 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 (file)
index 0000000..ea3ded8
--- /dev/null
@@ -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 (file)
index 0000000..6c4c43c
--- /dev/null
@@ -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 (file)
index 0000000..798f1db
--- /dev/null
@@ -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 (file)
index 0000000..ccb6019
--- /dev/null
@@ -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<CallbackServer> server = Optional.empty();
+
+    Optional<AccountConfiguration> 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 (file)
index 0000000..b8f7411
--- /dev/null
@@ -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<String, Long> timeHash = new HashMap<String, Long>();
+    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<ScheduledFuture<?>> refreshJob = Optional.empty();
+    private Optional<AccountHandler> accountHandler = Optional.empty();
+    private Optional<QuantityType<?>> rangeElectric = Optional.empty();
+    private Optional<Storage<String>> imageStorage = Optional.empty();
+    private Optional<VehicleConfiguration> config = Optional.empty();
+    private Optional<QuantityType<?>> 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<String> removals = new ArrayList<String>();
+            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<String> parameterMap = new MultiMap<String>();
+        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<String> entries = new ArrayList<String>();
+        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<CommandOption> commandOptions = new ArrayList<CommandOption>();
+        List<StateOption> stateOptions = new ArrayList<StateOption>();
+        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<String> 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<Object> iterator = ja.iterator(); iterator.hasNext();) {
+                JSONObject jo = (JSONObject) iterator.next();
+                ChannelStateMap csm = Mapper.getChannelStateMap(jo);
+                if (csm.isValid()) {
+                    updateChannel(csm);
+
+                    /**
+                     * handle some specific channels
+                     */
+                    // store ChannelMap for range radius calculation
+                    String channel = csm.getChannel();
+                    if ("range-electric".equals(channel)) {
+                        rangeElectric = Optional.of((QuantityType<?>) csm.getState());
+                    } else if ("range-fuel".equals(channel)) {
+                        rangeFuel = Optional.of((QuantityType<?>) csm.getState());
+                    } else if ("soc".equals(channel)) {
+                        if (config.get().batteryCapacity > 0) {
+                            float socValue = ((QuantityType<?>) csm.getState()).floatValue();
+                            float batteryCapacity = config.get().batteryCapacity;
+                            float chargedValue = Math.round(socValue * 1000 * batteryCapacity / 1000) / (float) 100;
+                            ChannelStateMap charged = new ChannelStateMap("charged", GROUP_RANGE,
+                                    QuantityType.valueOf(chargedValue, Units.KILOWATT_HOUR), csm.getTimestamp());
+                            updateChannel(charged);
+                            float unchargedValue = Math.round((100 - socValue) * 1000 * batteryCapacity / 1000)
+                                    / (float) 100;
+                            ChannelStateMap uncharged = new ChannelStateMap("uncharged", GROUP_RANGE,
+                                    QuantityType.valueOf(unchargedValue, Units.KILOWATT_HOUR), csm.getTimestamp());
+                            updateChannel(uncharged);
+                        } else {
+                            logger.debug("No battery capacity given");
+                        }
+                    } else if ("fuel-level".equals(channel)) {
+                        if (config.get().fuelCapacity > 0) {
+                            float fuelLevelValue = ((QuantityType<?>) csm.getState()).floatValue();
+                            float fuelCapacity = config.get().fuelCapacity;
+                            float litersInTank = Math.round(fuelLevelValue * 1000 * fuelCapacity / 1000) / (float) 100;
+                            ChannelStateMap tankFilled = new ChannelStateMap("tank-remain", GROUP_RANGE,
+                                    QuantityType.valueOf(litersInTank, Units.LITRE), csm.getTimestamp());
+                            updateChannel(tankFilled);
+                            float litersFree = Math.round((100 - fuelLevelValue) * 1000 * fuelCapacity / 1000)
+                                    / (float) 100;
+                            ChannelStateMap tankOpen = new ChannelStateMap("tank-open", GROUP_RANGE,
+                                    QuantityType.valueOf(litersFree, Units.LITRE), csm.getTimestamp());
+                            updateChannel(tankOpen);
+                        } else {
+                            logger.debug("No fuel capacity given");
+                        }
+                    }
+                } else {
+                    logger.warn("Unable to deliver state for {}", jo);
+                }
+            }
+        } else {
+            logger.debug("JSON Array expected but received {}", json);
+        }
+    }
+
+    private void updateRadius() {
+        if (rangeElectric.isPresent()) {
+            // update electric radius
+            ChannelStateMap radiusElectric = new ChannelStateMap("radius-electric", GROUP_RANGE,
+                    guessRangeRadius(rangeElectric.get()), 0);
+            updateChannel(radiusElectric);
+            if (rangeFuel.isPresent()) {
+                // update fuel & hybrid radius
+                ChannelStateMap radiusFuel = new ChannelStateMap("radius-fuel", GROUP_RANGE,
+                        guessRangeRadius(rangeFuel.get()), 0);
+                updateChannel(radiusFuel);
+                int hybridKm = rangeElectric.get().intValue() + rangeFuel.get().intValue();
+                QuantityType<Length> hybridRangeState = QuantityType.valueOf(hybridKm, KILOMETRE_UNIT);
+                ChannelStateMap rangeHybrid = new ChannelStateMap("range-hybrid", GROUP_RANGE, hybridRangeState, 0);
+                updateChannel(rangeHybrid);
+                ChannelStateMap radiusHybrid = new ChannelStateMap("radius-hybrid", GROUP_RANGE,
+                        guessRangeRadius(hybridRangeState), 0);
+                updateChannel(radiusHybrid);
+            }
+        } else if (rangeFuel.isPresent()) {
+            // update fuel & hybrid radius
+            ChannelStateMap radiusFuel = new ChannelStateMap("radius-fuel", GROUP_RANGE,
+                    guessRangeRadius(rangeFuel.get()), 0);
+            updateChannel(radiusFuel);
+        }
+    }
+
+    /**
+     * Easy function but there's some measures behind:
+     * Guessing the range of the Vehicle on Map. If you can drive x kilometers with your Vehicle it's not feasible to
+     * project this x km Radius on Map. The roads to be taken are causing some overhead because they are not a straight
+     * line from Location A to B.
+     * I've taken some measurements to calculate the overhead factor based on Google Maps
+     * Berlin - Dresden: Road Distance: 193 air-line Distance 167 = Factor 87%
+     * Kassel - Frankfurt: Road Distance: 199 air-line Distance 143 = Factor 72%
+     * After measuring more distances you'll find out that the outcome is between 70% and 90%. So
+     *
+     * This depends also on the roads of a concrete route but this is only a guess without any Route Navigation behind
+     *
+     * @param range
+     * @return mapping from air-line distance to "real road" distance
+     */
+    public static State guessRangeRadius(QuantityType<?> s) {
+        double radius = s.intValue() * 0.8;
+        return QuantityType.valueOf(Math.round(radius), KILOMETRE_UNIT);
+    }
+
+    protected void updateChannel(ChannelStateMap csm) {
+        updateTime(csm.getGroup(), csm.getTimestamp());
+        updateState(new ChannelUID(thing.getUID(), csm.getGroup(), csm.getChannel()), csm.getState());
+    }
+
+    private void updateTime(String group, long timestamp) {
+        boolean updateTime = false;
+        Long l = timeHash.get(group);
+        if (l != null) {
+            if (l.longValue() < timestamp) {
+                updateTime = true;
+            }
+        } else {
+            updateTime = true;
+        }
+        if (updateTime) {
+            timeHash.put(group, timestamp);
+            DateTimeType dtt = new DateTimeType(Instant.ofEpochMilli(timestamp).atZone(timeZoneProvider.getTimeZone()));
+            updateState(new ChannelUID(thing.getUID(), group, "last-update"), dtt);
+        }
+    }
+
+    @Override
+    public void updateStatus(ThingStatus ts, ThingStatusDetail tsd, @Nullable String details) {
+        online = ts.equals(ThingStatus.ONLINE);
+        super.updateStatus(ts, tsd, details);
+    }
+}
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/CallbackServer.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/CallbackServer.java
new file mode 100644 (file)
index 0000000..22e9757
--- /dev/null
@@ -0,0 +1,183 @@
+/**
+ * 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.server;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.servlet.ServletHandler;
+import org.openhab.binding.mercedesme.internal.Constants;
+import org.openhab.binding.mercedesme.internal.config.AccountConfiguration;
+import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener;
+import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
+import org.openhab.core.auth.client.oauth2.OAuthClientService;
+import org.openhab.core.auth.client.oauth2.OAuthException;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
+import org.openhab.core.auth.client.oauth2.OAuthResponseException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link CallbackServer} class defines an HTTP Server for authentication callbacks
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class CallbackServer {
+    private static final Logger LOGGER = LoggerFactory.getLogger(CallbackServer.class);
+    private static final Map<Integer, OAuthClientService> AUTH_MAP = new HashMap<Integer, OAuthClientService>();
+    private static final Map<Integer, CallbackServer> SERVER_MAP = new HashMap<Integer, CallbackServer>();
+    private static final AccessTokenResponse INVALID_ACCESS_TOKEN = new AccessTokenResponse();
+
+    private Optional<Server> server = Optional.empty();
+    private AccessTokenRefreshListener listener;
+    private AccountConfiguration config;
+    private OAuthClientService oacs;
+    private String callbackUrl;
+
+    public CallbackServer(AccessTokenRefreshListener l, HttpClient hc, OAuthFactory oAuthFactory,
+            AccountConfiguration config, String callbackUrl) {
+        oacs = oAuthFactory.createOAuthClientService(config.clientId, Constants.MB_TOKEN_URL, Constants.MB_AUTH_URL,
+                config.clientId, config.clientSecret, config.getScope(), false);
+        listener = l;
+        AUTH_MAP.put(Integer.valueOf(config.callbackPort), oacs);
+        SERVER_MAP.put(Integer.valueOf(config.callbackPort), this);
+        this.config = config;
+        this.callbackUrl = callbackUrl;
+        INVALID_ACCESS_TOKEN.setAccessToken(Constants.EMPTY);
+    }
+
+    public String getAuthorizationUrl() {
+        try {
+            return oacs.getAuthorizationUrl(callbackUrl, null, null);
+        } catch (OAuthException e) {
+            LOGGER.warn("Error creating Authorization URL {}", e.getMessage());
+            return Constants.EMPTY;
+        }
+    }
+
+    public String getScope() {
+        return config.getScope();
+    }
+
+    public boolean start() {
+        LOGGER.debug("Start Callback Server for port {}", config.callbackPort);
+        if (!server.isEmpty()) {
+            LOGGER.debug("Callback server for port {} already started", config.callbackPort);
+            return true;
+        }
+        server = Optional.of(new Server());
+        ServerConnector connector = new ServerConnector(server.get());
+        connector.setPort(config.callbackPort);
+        server.get().setConnectors(new Connector[] { connector });
+        ServletHandler servletHandler = new ServletHandler();
+        server.get().setHandler(servletHandler);
+        servletHandler.addServletWithMapping(CallbackServlet.class, Constants.CALLBACK_ENDPOINT);
+        try {
+            server.get().start();
+        } catch (Exception e) {
+            LOGGER.warn("Cannot start Callback Server for port {}, Error {}", config.callbackPort, e.getMessage());
+            return false;
+        }
+        return true;
+    }
+
+    public void stop() {
+        LOGGER.debug("Stop Callback Server");
+        try {
+            if (!server.isEmpty()) {
+                server.get().stop();
+                server = Optional.empty();
+            }
+        } catch (Exception e) {
+            LOGGER.warn("Cannot start Callback Server for port {}, Error {}", config.callbackPort, e.getMessage());
+        }
+    }
+
+    public String getToken() {
+        AccessTokenResponse atr = null;
+        try {
+            /*
+             * this will automatically trigger
+             * - return last stored token if it's still valid
+             * - refreshToken if current token is expired
+             * - inform listeners if refresh delivered new token
+             * - store new token in persistence
+             */
+            atr = oacs.getAccessTokenResponse();
+        } catch (OAuthException | IOException | OAuthResponseException e) {
+            LOGGER.warn("Exception getting token {}", e.getMessage());
+        }
+        if (atr == null) {
+            LOGGER.debug("Token empty - Manual Authorization needed at {}", callbackUrl);
+            listener.onAccessTokenResponse(INVALID_ACCESS_TOKEN);
+            return INVALID_ACCESS_TOKEN.getAccessToken();
+        }
+        listener.onAccessTokenResponse(atr);
+        return atr.getAccessToken();
+    }
+
+    /**
+     * Static callback for Servlet calls
+     *
+     * @param port
+     * @param code
+     */
+    public static void callback(int port, String code) {
+        LOGGER.trace("Callback from Servlet {} {}", port, code);
+        try {
+            OAuthClientService oacs = AUTH_MAP.get(port);
+            LOGGER.trace("Get token from code {}", code);
+            // get CallbackServer instance
+            CallbackServer srv = SERVER_MAP.get(port);
+            LOGGER.trace("Deliver token to {}", srv);
+            if (srv != null && oacs != null) {
+                // token stored and persisted inside oacs
+                AccessTokenResponse atr = oacs.getAccessTokenResponseByAuthorizationCode(code, srv.callbackUrl);
+                // inform listener - not done by oacs
+                srv.listener.onAccessTokenResponse(atr);
+            } else {
+                LOGGER.warn("Either Callbackserver  {} or Authorization Service {} not found", srv, oacs);
+            }
+        } catch (OAuthException | IOException | OAuthResponseException e) {
+            LOGGER.warn("Exception getting token from code {} {}", code, e.getMessage());
+        }
+    }
+
+    public static String getAuthorizationUrl(int port) {
+        CallbackServer srv = SERVER_MAP.get(port);
+        if (srv != null) {
+            return srv.getAuthorizationUrl();
+        } else {
+            LOGGER.debug("No Callbackserver found for {}", port);
+            return Constants.EMPTY;
+        }
+    }
+
+    public static String getScope(int port) {
+        CallbackServer srv = SERVER_MAP.get(port);
+        if (srv != null) {
+            return srv.getScope();
+        } else {
+            LOGGER.debug("No Callbackserver found for {}", port);
+            return Constants.EMPTY;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/CallbackServlet.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/CallbackServlet.java
new file mode 100644 (file)
index 0000000..5a418c6
--- /dev/null
@@ -0,0 +1,73 @@
+/**
+ * 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.server;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mercedesme.internal.Constants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link CallbackServlet} class provides authentication callback endpoint
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@SuppressWarnings("serial")
+@NonNullByDefault
+public class CallbackServlet extends HttpServlet {
+    private final Logger logger = LoggerFactory.getLogger(CallbackServlet.class);
+
+    @Override
+    protected void doGet(HttpServletRequest request, HttpServletResponse response)
+            throws ServletException, IOException {
+        String code = request.getParameter(Constants.CODE);
+        if (code != null) {
+            CallbackServer.callback(request.getLocalPort(), code);
+            logger.trace("Code successfully extracted {}", request.getParameterMap());
+            response.setContentType("application/json");
+            response.setStatus(HttpServletResponse.SC_OK);
+            response.getWriter().println(request.getParameterMap());
+            response.getWriter().println("{ \"status\": \"ok\"}");
+        } else {
+            response.setContentType("text/html");
+            response.setStatus(HttpServletResponse.SC_OK);
+            response.getWriter().println("<HTML>");
+            response.getWriter().println("<BODY>");
+            response.getWriter().println("<B>Call Parameters</B>");
+            response.getWriter().println("<BR>");
+            response.getWriter().println(request.getParameterMap());
+            response.getWriter().println("<BR><BR>");
+            response.getWriter().println("<B>Configured scopes</B><BR>");
+            String[] scopes = CallbackServer.getScope(request.getLocalPort()).split(Constants.SPACE);
+            for (int i = 0; i < scopes.length; i++) {
+                response.getWriter().println(scopes[i] + "<BR>");
+            }
+            response.getWriter().println("<BR><BR>");
+            response.getWriter().println("<B>Get your access token for openHAB MercedesMe Binding</B>");
+            response.getWriter().println("<BR>");
+            response.getWriter().println("<a href=\"" + CallbackServer.getAuthorizationUrl(request.getLocalPort())
+                    + "\">Start Authorization</a>");
+            response.getWriter().println("</BODY>");
+            response.getWriter().println("</HTML>");
+        }
+        logger.debug("Call from {}:{} parameters {}", request.getLocalAddr(), request.getLocalPort(),
+                request.getParameterMap());
+    }
+}
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/Utils.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/Utils.java
new file mode 100644 (file)
index 0000000..0c98f72
--- /dev/null
@@ -0,0 +1,88 @@
+/**
+ * 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.server;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mercedesme.internal.Constants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link Utils} class defines an HTTP Server for authentication callbacks
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class Utils {
+    private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class);
+    private static final List<Integer> PORTS = new ArrayList<Integer>();
+    private static int port = 8090;
+
+    /**
+     * Get free port without other Thread interference
+     *
+     * @return
+     */
+    public static synchronized int getFreePort() {
+        while (PORTS.contains(port)) {
+            port++;
+        }
+        PORTS.add(port);
+        return port;
+    }
+
+    public static synchronized void addPort(int portNr) {
+        if (PORTS.contains(portNr)) {
+            LOGGER.warn("Port {} already occupied", portNr);
+        }
+        PORTS.add(portNr);
+    }
+
+    public static synchronized void removePort(int portNr) {
+        PORTS.remove(Integer.valueOf(portNr));
+    }
+
+    public static String getCallbackIP() throws SocketException {
+        // https://stackoverflow.com/questions/1062041/ip-address-not-obtained-in-java
+        for (Enumeration<NetworkInterface> ifaces = NetworkInterface.getNetworkInterfaces(); ifaces
+                .hasMoreElements();) {
+            NetworkInterface iface = ifaces.nextElement();
+            try {
+                if (!iface.isLoopback()) {
+                    if (iface.isUp()) {
+                        for (Enumeration<InetAddress> addresses = iface.getInetAddresses(); addresses
+                                .hasMoreElements();) {
+                            InetAddress address = addresses.nextElement();
+                            return address.getHostAddress();
+                        }
+                    }
+                }
+            } catch (SocketException se) {
+                // Calling one network interface failed - continue searching
+                LOGGER.trace("Network {} failed {}", iface.getName(), se.getMessage());
+            }
+        }
+        throw new SocketException("IP address not detected");
+    }
+
+    public static String getCallbackAddress(String callbackIP, int callbackPort) {
+        return "http://" + callbackIP + Constants.COLON + callbackPort + Constants.CALLBACK_ENDPOINT;
+    }
+}
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/utils/ChannelStateMap.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/utils/ChannelStateMap.java
new file mode 100644 (file)
index 0000000..71c1a0e
--- /dev/null
@@ -0,0 +1,61 @@
+/**
+ * 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.utils;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.types.State;
+
+/**
+ * The {@link ChannelStateMap} holds the necessary values to update a channel state
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class ChannelStateMap {
+    private String channel;
+    private String group;
+    private State state;
+    private long timestamp;
+
+    public ChannelStateMap(String ch, String grp, State st, long ts) {
+        channel = ch;
+        group = grp;
+        state = st;
+        timestamp = ts;
+    }
+
+    public String getChannel() {
+        return channel;
+    }
+
+    public String getGroup() {
+        return group;
+    }
+
+    public State getState() {
+        return state;
+    }
+
+    public long getTimestamp() {
+        return timestamp;
+    }
+
+    @Override
+    public String toString() {
+        return group + ":" + channel + " " + state;
+    }
+
+    public boolean isValid() {
+        return !channel.isEmpty();
+    }
+}
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/utils/Mapper.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/utils/Mapper.java
new file mode 100644 (file)
index 0000000..512e843
--- /dev/null
@@ -0,0 +1,237 @@
+/**
+ * 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.utils;
+
+import static org.openhab.binding.mercedesme.internal.Constants.*;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.json.JSONObject;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.OpenClosedType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link Mapper} maps a given Json Object towards a channel, group and state
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class Mapper {
+    private static final Logger LOGGER = LoggerFactory.getLogger(Mapper.class);
+
+    public static final ChannelStateMap INVALID_MAP = new ChannelStateMap(EMPTY, EMPTY, UnDefType.UNDEF, -1);
+    public static final Map<String, String[]> CHANNELS = new HashMap<String, String[]>();
+    public static final String TIMESTAMP = "timestamp";
+    public static final String VALUE = "value";
+
+    public static ChannelStateMap getChannelStateMap(JSONObject jo) {
+        if (CHANNELS.isEmpty()) {
+            init();
+        }
+        Set<String> s = jo.keySet();
+        if (s.size() == 1) {
+            String id = s.toArray()[0].toString();
+            String[] ch = CHANNELS.get(id);
+            if (ch != null) {
+                State state;
+                switch (id) {
+                    // Kilometer values
+                    case "odo":
+                    case "rangeelectric":
+                    case "rangeliquid":
+                        state = getKilometers((JSONObject) jo.get(id));
+                        return new ChannelStateMap(ch[0], ch[1], state, getTimestamp((JSONObject) jo.get(id)));
+
+                    // Percentages
+                    case "soc":
+                    case "tanklevelpercent":
+                        state = getPercentage((JSONObject) jo.get(id));
+                        return new ChannelStateMap(ch[0], ch[1], state, getTimestamp((JSONObject) jo.get(id)));
+
+                    // Contacts
+                    case "decklidstatus":
+                    case "doorstatusfrontleft":
+                    case "doorstatusfrontright":
+                    case "doorstatusrearleft":
+                    case "doorstatusrearright":
+                        state = getContact((JSONObject) jo.get(id));
+                        return new ChannelStateMap(ch[0], ch[1], state, getTimestamp((JSONObject) jo.get(id)));
+
+                    // Number Status
+                    case "lightswitchposition":
+                    case "rooftopstatus":
+                    case "sunroofstatus":
+                    case "windowstatusfrontleft":
+                    case "windowstatusfrontright":
+                    case "windowstatusrearleft":
+                    case "windowstatusrearright":
+                    case "doorlockstatusvehicle":
+                        state = getDecimal((JSONObject) jo.get(id));
+                        return new ChannelStateMap(ch[0], ch[1], state, getTimestamp((JSONObject) jo.get(id)));
+
+                    // Switches
+                    case "interiorLightsFront":
+                    case "interiorLightsRear":
+                    case "readingLampFrontLeft":
+                    case "readingLampFrontRight":
+                        state = getOnOffType((JSONObject) jo.get(id));
+                        return new ChannelStateMap(ch[0], ch[1], state, getTimestamp((JSONObject) jo.get(id)));
+
+                    case "doorlockstatusdecklid":
+                    case "doorlockstatusgas":
+                        state = getOnOffTypeLock((JSONObject) jo.get(id));
+                        return new ChannelStateMap(ch[0], ch[1], state, getTimestamp((JSONObject) jo.get(id)));
+
+                    // Angle
+                    case "positionHeading":
+                        state = getAngle((JSONObject) jo.get(id));
+                        return new ChannelStateMap(ch[0], ch[1], state, getTimestamp((JSONObject) jo.get(id)));
+                    default:
+                        LOGGER.trace("No mapping available for {}", id);
+                }
+            } else {
+                LOGGER.trace("No mapping available for {}", id);
+            }
+        } else {
+            LOGGER.debug("More than one key found {}", s);
+        }
+        return INVALID_MAP;
+    }
+
+    private static long getTimestamp(JSONObject jo) {
+        if (jo.has(TIMESTAMP)) {
+            return jo.getLong(TIMESTAMP);
+        }
+        return -1;
+    }
+
+    private static State getOnOffType(JSONObject jo) {
+        if (jo.has(VALUE)) {
+            String value = jo.get(VALUE).toString();
+            boolean b = Boolean.valueOf(value);
+            return OnOffType.from(b);
+        } else {
+            LOGGER.warn("JSONObject contains no value {}", jo);
+            return UnDefType.UNDEF;
+        }
+    }
+
+    private static State getOnOffTypeLock(JSONObject jo) {
+        if (jo.has(VALUE)) {
+            String value = jo.get(VALUE).toString();
+            boolean b = Boolean.valueOf(value);
+            // Yes, false is locked and true unlocked
+            // https://developer.mercedes-benz.com/products/vehicle_lock_status/specifications/vehicle_lock_status_api
+            return OnOffType.from(!b);
+        } else {
+            LOGGER.warn("JSONObject contains no value {}", jo);
+            return UnDefType.UNDEF;
+        }
+    }
+
+    private static State getAngle(JSONObject jo) {
+        if (jo.has(VALUE)) {
+            String value = jo.get(VALUE).toString();
+            return QuantityType.valueOf(Double.valueOf(value), Units.DEGREE_ANGLE);
+        } else {
+            LOGGER.warn("JSONObject contains no value {}", jo);
+            return UnDefType.UNDEF;
+        }
+    }
+
+    private static State getDecimal(JSONObject jo) {
+        if (jo.has(VALUE)) {
+            String value = jo.get(VALUE).toString();
+            return DecimalType.valueOf(value);
+        } else {
+            LOGGER.warn("JSONObject contains no value {}", jo);
+            return UnDefType.UNDEF;
+        }
+    }
+
+    private static State getContact(JSONObject jo) {
+        if (jo.has(VALUE)) {
+            String value = jo.get(VALUE).toString();
+            boolean b = Boolean.valueOf(value);
+            if (!b) {
+                return OpenClosedType.CLOSED;
+            } else {
+                return OpenClosedType.OPEN;
+            }
+        } else {
+            LOGGER.warn("JSONObject contains no value {}", jo);
+            return UnDefType.UNDEF;
+        }
+    }
+
+    private static State getKilometers(JSONObject jo) {
+        if (jo.has(VALUE)) {
+            String value = jo.get(VALUE).toString();
+            return QuantityType.valueOf(Integer.valueOf(value), KILOMETRE_UNIT);
+        } else {
+            LOGGER.warn("JSONObject contains no value {}", jo);
+            return UnDefType.UNDEF;
+        }
+    }
+
+    private static State getPercentage(JSONObject jo) {
+        if (jo.has(VALUE)) {
+            String value = jo.get(VALUE).toString();
+            return QuantityType.valueOf(Integer.valueOf(value), Units.PERCENT);
+        } else {
+            LOGGER.warn("JSONObject contains no value {}", jo);
+            return UnDefType.UNDEF;
+        }
+    }
+
+    /**
+     * Mapping of json id towards channel group and id
+     */
+    private static void init() {
+        CHANNELS.put("odo", new String[] { "mileage", GROUP_RANGE });
+        CHANNELS.put("rangeelectric", new String[] { "range-electric", GROUP_RANGE });
+        CHANNELS.put("soc", new String[] { "soc", GROUP_RANGE });
+        CHANNELS.put("rangeliquid", new String[] { "range-fuel", GROUP_RANGE });
+        CHANNELS.put("tanklevelpercent", new String[] { "fuel-level", GROUP_RANGE });
+        CHANNELS.put("decklidstatus", new String[] { "deck-lid", GROUP_DOORS });
+        CHANNELS.put("doorstatusfrontleft", new String[] { "driver-front", GROUP_DOORS });
+        CHANNELS.put("doorstatusfrontright", new String[] { "passenger-front", GROUP_DOORS });
+        CHANNELS.put("doorstatusrearleft", new String[] { "driver-rear", GROUP_DOORS });
+        CHANNELS.put("doorstatusrearright", new String[] { "passenger-rear", GROUP_DOORS });
+        CHANNELS.put("interiorLightsFront", new String[] { "interior-front", GROUP_LIGHTS });
+        CHANNELS.put("interiorLightsRear", new String[] { "interior-rear", GROUP_LIGHTS });
+        CHANNELS.put("lightswitchposition", new String[] { "light-switch", GROUP_LIGHTS });
+        CHANNELS.put("readingLampFrontLeft", new String[] { "reading-left", GROUP_LIGHTS });
+        CHANNELS.put("readingLampFrontRight", new String[] { "reading-right", GROUP_LIGHTS });
+        CHANNELS.put("rooftopstatus", new String[] { "rooftop", GROUP_DOORS });
+        CHANNELS.put("sunroofstatus", new String[] { "sunroof", GROUP_DOORS });
+        CHANNELS.put("windowstatusfrontleft", new String[] { "driver-front", GROUP_WINDOWS });
+        CHANNELS.put("windowstatusfrontright", new String[] { "passenger-front", GROUP_WINDOWS });
+        CHANNELS.put("windowstatusrearleft", new String[] { "driver-rear", GROUP_WINDOWS });
+        CHANNELS.put("windowstatusrearright", new String[] { "passenger-rear", GROUP_WINDOWS });
+        CHANNELS.put("doorlockstatusvehicle", new String[] { "doors", GROUP_LOCK });
+        CHANNELS.put("doorlockstatusdecklid", new String[] { "deck-lid", GROUP_LOCK });
+        CHANNELS.put("doorlockstatusgas", new String[] { "flap", GROUP_LOCK });
+        CHANNELS.put("positionHeading", new String[] { "heading", GROUP_LOCATION });
+    }
+}
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644 (file)
index 0000000..e0b0cfa
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="mercedesme" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+       <name>Mercedes Me Binding</name>
+       <description>The binding provides access to your Mercedes developer account and vehicles</description>
+
+</binding:binding>
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/bev-config.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/bev-config.xml
new file mode 100644 (file)
index 0000000..7852916
--- /dev/null
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+       <config-description uri="thing-type:mercedesme:bev">
+               <parameter name="vin" type="text" required="true">
+                       <label>Vehicle Identification Number</label>
+               </parameter>
+               <parameter name="refreshInterval" type="integer" min="1" unit="min" required="true">
+                       <label>Refresh Interval</label>
+                       <description>Data refresh rate for vehicle data</description>
+                       <default>5</default>
+               </parameter>
+               <parameter name="batteryCapacity" type="decimal">
+                       <label>Battery Capacity</label>
+                       <description>Battery capacity in kWh of vehicle</description>
+               </parameter>
+               <!-- https://developer.mercedes-benz.com/products/vehicle_images/docs#_default_image_settings -->
+               <parameter name="background" type="boolean">
+                       <label>Background Image</label>
+                       <description>Vehicle images provided with or without background</description>
+                       <default>false</default>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="night" type="boolean">
+                       <label>Night Image</label>
+                       <description>Vehicle images in night conditions</description>
+                       <default>false</default>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="cropped" type="boolean">
+                       <label>Cropped Image</label>
+                       <description>Vehicle images in 4:3 instead of 16:9</description>
+                       <default>false</default>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="roofOpen" type="boolean">
+                       <label>Cabriolet Open Roof</label>
+                       <description>Vehicle images with open roof (only Cabriolet)</description>
+                       <default>false</default>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="format" type="text">
+                       <label>Image Format</label>
+                       <description>Preferred Image Format</description>
+                       <default>webp</default>
+                       <advanced>true</advanced>
+                       <options>
+                               <option value="webp">webp</option>
+                               <option value="png">png</option>
+                               <option value="jpeg">jpeg</option>
+                       </options>
+               </parameter>
+       </config-description>
+</config-description:config-descriptions>
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/bridge-config.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/bridge-config.xml
new file mode 100644 (file)
index 0000000..30f6b20
--- /dev/null
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+       <config-description uri="thing-type:mercedesme:bridge">
+               <parameter name="clientId" type="text" required="true">
+                       <label>MB Developer Client ID</label>
+                       <description>Mercedes Benz Developer Client ID</description>
+               </parameter>
+               <parameter name="clientSecret" type="text" required="true">
+                       <label>MB Developer Client Secret</label>
+                       <description>Mercedes Benz Developer Client Secret</description>
+               </parameter>
+               <parameter name="imageApiKey" type="text">
+                       <label>MB Developer Image API Key</label>
+                       <description>Mercedes Benz Developer Image API Key</description>
+               </parameter>
+               <parameter name="odoScope" type="boolean">
+                       <label>PayAsYourDrive Insurance</label>
+                       <description>Provides total Mileage</description>
+                       <default>true</default>
+               </parameter>
+               <parameter name="vehicleScope" type="boolean">
+                       <label>Vehicle Status</label>
+                       <description>Status of doors, windows lights</description>
+                       <default>true</default>
+               </parameter>
+               <parameter name="lockScope" type="boolean">
+                       <label>Vehicle Lock Status</label>
+                       <description>Lock status of doors and trunk</description>
+                       <default>true</default>
+               </parameter>
+               <parameter name="fuelScope" type="boolean">
+                       <label>Fuel Status</label>
+                       <description>Tank level and range</description>
+                       <default>true</default>
+               </parameter>
+               <parameter name="evScope" type="boolean">
+                       <label>Electric Vehicle Status</label>
+                       <description>Electric charge and range</description>
+                       <default>true</default>
+               </parameter>
+               <parameter name="callbackIP" type="text">
+                       <label>Callback IP Address</label>
+                       <description>IP address for openHAB callback URL</description>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="callbackPort" type="integer">
+                       <label>Callback Port Number</label>
+                       <description>Port Number for openHAB callback URL</description>
+                       <advanced>true</advanced>
+               </parameter>
+       </config-description>
+</config-description:config-descriptions>
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/conv-config.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/conv-config.xml
new file mode 100644 (file)
index 0000000..d4bc986
--- /dev/null
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+       <config-description uri="thing-type:mercedesme:conv">
+               <parameter name="vin" type="text" required="true">
+                       <label>Vehicle Identification Number</label>
+               </parameter>
+               <parameter name="refreshInterval" type="integer" min="1" unit="min" required="true">
+                       <label>Refresh Interval</label>
+                       <description>Data refresh rate for your vehicle data</description>
+                       <default>5</default>
+               </parameter>
+               <parameter name="fuelCapacity" type="decimal" min="0">
+                       <label>Fuel Capacity</label>
+                       <description>Fuel capacity in liters of vehicle</description>
+               </parameter>
+               <!-- https://developer.mercedes-benz.com/products/vehicle_images/docs#_default_image_settings -->
+               <parameter name="background" type="boolean">
+                       <label>Background Image</label>
+                       <description>Vehicle images provided with or without background</description>
+                       <default>false</default>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="night" type="boolean">
+                       <label>Night Image</label>
+                       <description>Vehicle images in night conditions</description>
+                       <default>false</default>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="cropped" type="boolean">
+                       <label>Cropped Image</label>
+                       <description>Vehicle images in 4:3 instead of 16:9</description>
+                       <default>false</default>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="roofOpen" type="boolean">
+                       <label>Cabriolet Open Roof</label>
+                       <description>Vehicle images with open roof (only Cabriolet)</description>
+                       <default>false</default>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="format" type="text">
+                       <label>Image Format</label>
+                       <description>Preferred Image Format</description>
+                       <default>webp</default>
+                       <advanced>true</advanced>
+                       <options>
+                               <option value="webp">webp</option>
+                               <option value="png">png</option>
+                               <option value="jpeg">jpeg</option>
+                       </options>
+               </parameter>
+       </config-description>
+</config-description:config-descriptions>
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/hybrid-config.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/hybrid-config.xml
new file mode 100644 (file)
index 0000000..55415dd
--- /dev/null
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+       <config-description uri="thing-type:mercedesme:hybrid">
+               <parameter name="vin" type="text" required="true">
+                       <label>Vehicle Identification Number</label>
+               </parameter>
+               <parameter name="refreshInterval" type="integer" min="1" unit="min" required="true">
+                       <label>Refresh Interval</label>
+                       <description>Data refresh rate for vehicle data</description>
+                       <default>5</default>
+               </parameter>
+               <parameter name="batteryCapacity" type="decimal">
+                       <label>Battery Capacity</label>
+                       <description>Battery capacity in kWh of vehicle</description>
+               </parameter>
+               <parameter name="fuelCapacity" type="decimal">
+                       <label>Fuel Capacity</label>
+                       <description>Fuel capacity in liters of vehicle</description>
+               </parameter>
+               <!-- https://developer.mercedes-benz.com/products/vehicle_images/docs#_default_image_settings -->
+               <parameter name="background" type="boolean">
+                       <label>Background Image</label>
+                       <description>Vehicle images provided with or without background</description>
+                       <default>false</default>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="night" type="boolean">
+                       <label>Night Image</label>
+                       <description>Vehicle images in night conditions</description>
+                       <default>false</default>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="cropped" type="boolean">
+                       <label>Cropped Image</label>
+                       <description>Vehicle images in 4:3 instead of 16:9</description>
+                       <default>false</default>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="roofOpen" type="boolean">
+                       <label>Cabriolet Open Roof</label>
+                       <description>Vehicle images with open roof (only Cabriolet)</description>
+                       <default>false</default>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="format" type="text">
+                       <label>Image Format</label>
+                       <description>Preferred Image Format</description>
+                       <default>webp</default>
+                       <advanced>true</advanced>
+                       <options>
+                               <option value="webp">webp</option>
+                               <option value="png">png</option>
+                               <option value="jpeg">jpeg</option>
+                       </options>
+               </parameter>
+       </config-description>
+</config-description:config-descriptions>
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/i18n/mercedesme.properties b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/i18n/mercedesme.properties
new file mode 100644 (file)
index 0000000..e9328cf
--- /dev/null
@@ -0,0 +1,218 @@
+# binding
+
+binding.mercedesme.name = Mercedes Me Binding
+binding.mercedesme.description = The binding provides access to your Mercedes developer account and vehicles
+
+# thing types
+
+thing-type.mercedesme.account.label = Mercedes Me Account
+thing-type.mercedesme.account.description = Mercedes Benz account data
+thing-type.mercedesme.bev.label = Mercedes Benz BEV
+thing-type.mercedesme.bev.description = Battery Electric Vehicle
+thing-type.mercedesme.combustion.label = Mercedes Benz
+thing-type.mercedesme.combustion.description = Conventional Fuel Vehicle
+thing-type.mercedesme.hybrid.label = Mercedes Benz Hybrid
+thing-type.mercedesme.hybrid.description = Conventional Fuel Vehicle with supporting Electric Engine
+
+# thing types config
+
+thing-type.config.mercedesme.bev.background.label = Background Image
+thing-type.config.mercedesme.bev.background.description = Vehicle images provided with or without background
+thing-type.config.mercedesme.bev.batteryCapacity.label = Battery Capacity
+thing-type.config.mercedesme.bev.batteryCapacity.description = Battery capacity in kwh of vehicle
+thing-type.config.mercedesme.bev.cropped.label = Cropped Image
+thing-type.config.mercedesme.bev.cropped.description = Vehicle images in 4:3 instead of 16:9
+thing-type.config.mercedesme.bev.format.label = Image Format
+thing-type.config.mercedesme.bev.format.description = Preferred Image Format
+thing-type.config.mercedesme.bev.format.option.webp = webp
+thing-type.config.mercedesme.bev.format.option.png = png
+thing-type.config.mercedesme.bev.format.option.jpeg = jpeg
+thing-type.config.mercedesme.bev.night.label = Night Image
+thing-type.config.mercedesme.bev.night.description = Vehicle images in night conditions
+thing-type.config.mercedesme.bev.refreshInterval.label = Refresh Interval
+thing-type.config.mercedesme.bev.refreshInterval.description = Data refresh rate for vehicle data
+thing-type.config.mercedesme.bev.roofOpen.label = Cabriolet Open Roof
+thing-type.config.mercedesme.bev.roofOpen.description = Vehicle images with open roof (only Cabriolet)
+thing-type.config.mercedesme.bev.vin.label = Vehicle Identification Number
+thing-type.config.mercedesme.bridge.callbackIP.label = Callback IP Address
+thing-type.config.mercedesme.bridge.callbackIP.description = IP address for openHAB callback URL
+thing-type.config.mercedesme.bridge.callbackPort.label = Callback Port Number
+thing-type.config.mercedesme.bridge.callbackPort.description = Port Number for openHAB callback URL
+thing-type.config.mercedesme.bridge.clientId.label = MB Developer Client ID
+thing-type.config.mercedesme.bridge.clientId.description = Mercedes Benz Developer Client ID
+thing-type.config.mercedesme.bridge.clientSecret.label = MB Developer Client Secret
+thing-type.config.mercedesme.bridge.clientSecret.description = Mercedes Benz Developer Client Secret
+thing-type.config.mercedesme.bridge.evScope.label = Electric Vehicle Status
+thing-type.config.mercedesme.bridge.evScope.description = Electric charge and range
+thing-type.config.mercedesme.bridge.fuelScope.label = Fuel Status
+thing-type.config.mercedesme.bridge.fuelScope.description = Tank level and range
+thing-type.config.mercedesme.bridge.imageApiKey.label = MB Developer Image API Key
+thing-type.config.mercedesme.bridge.imageApiKey.description = Mercedes Benz Developer Image API Key
+thing-type.config.mercedesme.bridge.lockScope.label = Vehicle Lock Status
+thing-type.config.mercedesme.bridge.lockScope.description = Lock status of doors and trunk
+thing-type.config.mercedesme.bridge.odoScope.label = PayAsYourDrive Insurance
+thing-type.config.mercedesme.bridge.odoScope.description = Provides total Mileage
+thing-type.config.mercedesme.bridge.vehicleScope.label = Vehicle Status
+thing-type.config.mercedesme.bridge.vehicleScope.description = Status of doors, windows lights
+thing-type.config.mercedesme.conv.background.label = Background Image
+thing-type.config.mercedesme.conv.background.description = Vehicle images provided with or without background
+thing-type.config.mercedesme.conv.cropped.label = Cropped Image
+thing-type.config.mercedesme.conv.cropped.description = Vehicle images in 4:3 instead of 16:9
+thing-type.config.mercedesme.conv.format.label = Image Format
+thing-type.config.mercedesme.conv.format.description = Preferred Image Format
+thing-type.config.mercedesme.conv.format.option.webp = webp
+thing-type.config.mercedesme.conv.format.option.png = png
+thing-type.config.mercedesme.conv.format.option.jpeg = jpeg
+thing-type.config.mercedesme.conv.fuelCapacity.label = Fuel Capacity
+thing-type.config.mercedesme.conv.fuelCapacity.description = Fuel capacity in liters of vehicle
+thing-type.config.mercedesme.conv.night.label = Night Image
+thing-type.config.mercedesme.conv.night.description = Vehicle images in night conditions
+thing-type.config.mercedesme.conv.refreshInterval.label = Refresh Interval
+thing-type.config.mercedesme.conv.refreshInterval.description = Data refresh rate for your vehicle data
+thing-type.config.mercedesme.conv.roofOpen.label = Cabriolet Open Roof
+thing-type.config.mercedesme.conv.roofOpen.description = Vehicle images with open roof (only Cabriolet)
+thing-type.config.mercedesme.conv.vin.label = Vehicle Identification Number
+thing-type.config.mercedesme.hybrid.background.label = Background Image
+thing-type.config.mercedesme.hybrid.background.description = Vehicle images provided with or without background
+thing-type.config.mercedesme.hybrid.batteryCapacity.label = Battery Capacity
+thing-type.config.mercedesme.hybrid.batteryCapacity.description = Battery capacity in kwh of vehicle
+thing-type.config.mercedesme.hybrid.cropped.label = Cropped Image
+thing-type.config.mercedesme.hybrid.cropped.description = Vehicle images in 4:3 instead of 16:9
+thing-type.config.mercedesme.hybrid.format.label = Image Format
+thing-type.config.mercedesme.hybrid.format.description = Preferred Image Format
+thing-type.config.mercedesme.hybrid.format.option.webp = webp
+thing-type.config.mercedesme.hybrid.format.option.png = png
+thing-type.config.mercedesme.hybrid.format.option.jpeg = jpeg
+thing-type.config.mercedesme.hybrid.fuelCapacity.label = Fuel Capacity
+thing-type.config.mercedesme.hybrid.fuelCapacity.description = Fuel capacity in liters of vehicle
+thing-type.config.mercedesme.hybrid.night.label = Night Image
+thing-type.config.mercedesme.hybrid.night.description = Vehicle images in night conditions
+thing-type.config.mercedesme.hybrid.refreshInterval.label = Refresh Interval
+thing-type.config.mercedesme.hybrid.refreshInterval.description = Data refresh rate for vehicle data
+thing-type.config.mercedesme.hybrid.roofOpen.label = Cabriolet Open Roof
+thing-type.config.mercedesme.hybrid.roofOpen.description = Vehicle images with open roof (only Cabriolet)
+thing-type.config.mercedesme.hybrid.vin.label = Vehicle Identification Number
+
+# channel group types
+
+channel-group-type.mercedesme.door-values.label = Detailed Door Status
+channel-group-type.mercedesme.door-values.description = Detailed Status of all Doors and Windows
+channel-group-type.mercedesme.image-values.label = Vehicle Images
+channel-group-type.mercedesme.light-values.label = Light Status
+channel-group-type.mercedesme.light-values.description = Light Status of interior lights and main light switch
+channel-group-type.mercedesme.location-values.label = Vehicle Location
+channel-group-type.mercedesme.location-values.description = Heading of vehicle
+channel-group-type.mercedesme.lock-values.label = Lock Status
+channel-group-type.mercedesme.lock-values.description = Vehicle Lock Status
+channel-group-type.mercedesme.range-conv-values.label = Range and Fuel Data
+channel-group-type.mercedesme.range-conv-values.description = Provides Mileage, remaining range and fuel level values
+channel-group-type.mercedesme.range-ev-values.label = Range and Charge Data
+channel-group-type.mercedesme.range-ev-values.description = Provides Mileage, remaining range and charge level values
+channel-group-type.mercedesme.range-hybrid-values.label = Range, Charge / Fuel Data
+channel-group-type.mercedesme.range-hybrid-values.description = Provides mileage, remaining fuel and range data for hybrid vehicles
+channel-group-type.mercedesme.window-values.label = Detailed Window Status
+channel-group-type.mercedesme.window-values.description = Detailed Status Windows
+
+# channel types
+
+channel-type.mercedesme.charged-channel.label = Charged Battery Energy
+channel-type.mercedesme.clear-cache-channel.label = Remove All Stored Images
+channel-type.mercedesme.deck-lid-channel.label = Deck Lid
+channel-type.mercedesme.deck-lid-lock-channel.label = Deck Lid Lock
+channel-type.mercedesme.doors-lock-channel.label = Door Lock Status
+channel-type.mercedesme.doors-lock-channel.state.option.0 = Unlocked
+channel-type.mercedesme.doors-lock-channel.state.option.1 = Locked Internal
+channel-type.mercedesme.doors-lock-channel.state.option.2 = Locked External
+channel-type.mercedesme.doors-lock-channel.state.option.3 = Unlocked Selective
+channel-type.mercedesme.driver-front-channel.label = Driver Door
+channel-type.mercedesme.driver-rear-channel.label = Driver Door Rear
+channel-type.mercedesme.flap-lock-channel.label = Flap Lock
+channel-type.mercedesme.fuel-level-channel.label = Fuel Level
+channel-type.mercedesme.fuel-open-channel.label = Open Fuel Capacity
+channel-type.mercedesme.fuel-remain-channel.label = Remaining Fuel
+channel-type.mercedesme.heading-channel.label = Heading Angle
+channel-type.mercedesme.image-data-channel.label = Rendered Vehicle Image
+channel-type.mercedesme.image-view-channel.label = Image Viewport
+channel-type.mercedesme.interior-front-channel.label = Interior Light Front
+channel-type.mercedesme.interior-rear-channel.label = Interior Light Rear
+channel-type.mercedesme.last-doors-update-channel.label = Last Doors Update
+channel-type.mercedesme.last-doors-update-channel.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM
+channel-type.mercedesme.last-lights-update-channel.label = Last Light Update
+channel-type.mercedesme.last-lights-update-channel.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM
+channel-type.mercedesme.last-location-update-channel.label = Last Location Update
+channel-type.mercedesme.last-location-update-channel.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM
+channel-type.mercedesme.last-lock-update-channel.label = Last Lock Update
+channel-type.mercedesme.last-lock-update-channel.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM
+channel-type.mercedesme.last-range-update-channel.label = Last Range Update
+channel-type.mercedesme.last-range-update-channel.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM
+channel-type.mercedesme.last-windows-update-channel.label = Last Window Update
+channel-type.mercedesme.last-windows-update-channel.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM
+channel-type.mercedesme.light-switch-channel.label = Main Light Rotary
+channel-type.mercedesme.light-switch-channel.state.option.0 = Auto
+channel-type.mercedesme.light-switch-channel.state.option.1 = Headlight
+channel-type.mercedesme.light-switch-channel.state.option.2 = Sidelight Left
+channel-type.mercedesme.light-switch-channel.state.option.3 = Sidelight Right
+channel-type.mercedesme.light-switch-channel.state.option.4 = Parking Light
+channel-type.mercedesme.mileage-channel.label = Mileage
+channel-type.mercedesme.passenger-front-channel.label = Passenger Door
+channel-type.mercedesme.passenger-rear-channel.label = Passenger Door Rear
+channel-type.mercedesme.radius-electric-channel.label = Electric Radius
+channel-type.mercedesme.radius-fuel-channel.label = Fuel Radius
+channel-type.mercedesme.radius-hybrid-channel.label = Hybrid Radius
+channel-type.mercedesme.range-electric-channel.label = Electric Range
+channel-type.mercedesme.range-fuel-channel.label = Fuel Range
+channel-type.mercedesme.range-hybrid-channel.label = Hybrid Range
+channel-type.mercedesme.reading-left-channel.label = Reading Light Left
+channel-type.mercedesme.reading-right-channel.label = Reading Light Right
+channel-type.mercedesme.rooftop-channel.label = Roof top
+channel-type.mercedesme.rooftop-channel.state.option.0 = Unlocked
+channel-type.mercedesme.rooftop-channel.state.option.1 = Open and locked
+channel-type.mercedesme.rooftop-channel.state.option.2 = Closed and locked
+channel-type.mercedesme.soc-channel.label = Battery Charge Level
+channel-type.mercedesme.sunroof-channel.label = Sun Roof
+channel-type.mercedesme.sunroof-channel.state.option.0 = Closed
+channel-type.mercedesme.sunroof-channel.state.option.1 = Open
+channel-type.mercedesme.sunroof-channel.state.option.2 = Open Lifting
+channel-type.mercedesme.sunroof-channel.state.option.3 = Running
+channel-type.mercedesme.sunroof-channel.state.option.4 = Closing
+channel-type.mercedesme.sunroof-channel.state.option.5 = Opening
+channel-type.mercedesme.sunroof-channel.state.option.6 = Closing
+channel-type.mercedesme.uncharged-channel.label = Uncharged Battery Energy
+channel-type.mercedesme.window-driver-front-channel.label = Driver Window
+channel-type.mercedesme.window-driver-front-channel.state.option.0 = Intermediate
+channel-type.mercedesme.window-driver-front-channel.state.option.1 = Open
+channel-type.mercedesme.window-driver-front-channel.state.option.2 = Closed
+channel-type.mercedesme.window-driver-front-channel.state.option.3 = Airing
+channel-type.mercedesme.window-driver-front-channel.state.option.4 = Intermediate
+channel-type.mercedesme.window-driver-front-channel.state.option.5 = Running
+channel-type.mercedesme.window-driver-rear-channel.label = Driver Window Rear
+channel-type.mercedesme.window-driver-rear-channel.state.option.0 = Intermediate
+channel-type.mercedesme.window-driver-rear-channel.state.option.1 = Open
+channel-type.mercedesme.window-driver-rear-channel.state.option.2 = Closed
+channel-type.mercedesme.window-driver-rear-channel.state.option.3 = Airing
+channel-type.mercedesme.window-driver-rear-channel.state.option.4 = Intermediate
+channel-type.mercedesme.window-driver-rear-channel.state.option.5 = Running
+channel-type.mercedesme.window-passenger-front-channel.label = Passenger Window
+channel-type.mercedesme.window-passenger-front-channel.state.option.0 = Intermediate
+channel-type.mercedesme.window-passenger-front-channel.state.option.1 = Open
+channel-type.mercedesme.window-passenger-front-channel.state.option.2 = Closed
+channel-type.mercedesme.window-passenger-front-channel.state.option.3 = Airing
+channel-type.mercedesme.window-passenger-front-channel.state.option.4 = Intermediate
+channel-type.mercedesme.window-passenger-front-channel.state.option.5 = Running
+channel-type.mercedesme.window-passenger-rear-channel.label = Passenger Window Rear
+channel-type.mercedesme.window-passenger-rear-channel.state.option.0 = Intermediate
+channel-type.mercedesme.window-passenger-rear-channel.state.option.1 = Open
+channel-type.mercedesme.window-passenger-rear-channel.state.option.2 = Closed
+channel-type.mercedesme.window-passenger-rear-channel.state.option.3 = Airing
+channel-type.mercedesme.window-passenger-rear-channel.state.option.4 = Intermediate
+channel-type.mercedesme.window-passenger-rear-channel.state.option.5 = Running
+
+# MercedesMe Things Status Details
+mercedesme.account.status.authorization-needed = Manual Authorization needed at {0}
+mercedesme.account.status.ip-missing = Callback IP missing
+mercedesme.account.status.port-missing = Callback Port missing
+mercedesme.account.status.client-id-missing = Client ID missing
+mercedesme.account.status.client-secret-missing = Client Secret missing
+mercedesme.account.status.server-restart = Disable and enable Bridge to restart Authorization Server
+mercedesme.vehicle.status.bridge-missing = Bridge not set
+mercedesme.vehicle.status.bridge-authoriziation = Check Bridge Authorization
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/bridge-account.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/bridge-account.xml
new file mode 100644 (file)
index 0000000..91c28fd
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mercedesme"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <bridge-type id="account">
+               <label>Mercedes Me Account</label>
+               <description>Mercedes Benz account data</description>
+               <config-description-ref uri="thing-type:mercedesme:bridge"/>
+       </bridge-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/door-channel-types.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/door-channel-types.xml
new file mode 100644 (file)
index 0000000..c5341b4
--- /dev/null
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mercedesme"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-type id="driver-front-channel">
+               <item-type>Contact</item-type>
+               <label>Driver Door</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="driver-rear-channel">
+               <item-type>Contact</item-type>
+               <label>Driver Door Rear</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="passenger-front-channel">
+               <item-type>Contact</item-type>
+               <label>Passenger Door</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="passenger-rear-channel">
+               <item-type>Contact</item-type>
+               <label>Passenger Door Rear</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="deck-lid-channel">
+               <item-type>Contact</item-type>
+               <label>Deck Lid</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="rooftop-channel">
+               <item-type>Number</item-type>
+               <label>Roof top</label>
+               <state readOnly="true">
+                       <options>
+                               <option value="0">Unlocked</option>
+                               <option value="1">Open and locked</option>
+                               <option value="2">Closed and locked</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="sunroof-channel">
+               <item-type>Number</item-type>
+               <label>Sun Roof</label>
+               <state readOnly="true">
+                       <options>
+                               <option value="0">Closed</option>
+                               <option value="1">Open</option>
+                               <option value="2">Open Lifting</option>
+                               <option value="3">Running</option>
+                               <option value="4">Closing</option>
+                               <option value="5">Opening</option>
+                               <option value="6">Closing</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="last-doors-update-channel">
+               <item-type>DateTime</item-type>
+               <label>Last Doors Update</label>
+               <state pattern="%1$tA, %1$td.%1$tm. %1$tH:%1$tM" readOnly="true"/>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/doors-group.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/doors-group.xml
new file mode 100644 (file)
index 0000000..8d66510
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mercedesme"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="door-values">
+               <label>Detailed Door Status</label>
+               <description>Detailed Status of all Doors and Windows</description>
+               <channels>
+                       <channel id="driver-front" typeId="driver-front-channel"/>
+                       <channel id="driver-rear" typeId="driver-rear-channel"/>
+                       <channel id="passenger-front" typeId="passenger-front-channel"/>
+                       <channel id="passenger-rear" typeId="passenger-rear-channel"/>
+                       <channel id="deck-lid" typeId="deck-lid-channel"/>
+                       <channel id="sunroof" typeId="sunroof-channel"/>
+                       <channel id="rooftop" typeId="rooftop-channel"/>
+                       <channel id="last-update" typeId="last-doors-update-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/image-channel-types.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/image-channel-types.xml
new file mode 100644 (file)
index 0000000..b0e597e
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mercedesme"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-type id="image-data-channel">
+               <item-type>Image</item-type>
+               <label>Rendered Vehicle Image</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="image-view-channel">
+               <item-type>String</item-type>
+               <label>Image Viewport</label>
+       </channel-type>
+       <channel-type id="clear-cache-channel">
+               <item-type>Switch</item-type>
+               <label>Remove All Stored Images</label>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/image-group.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/image-group.xml
new file mode 100644 (file)
index 0000000..2e1e701
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mercedesme"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="image-values">
+               <label>Vehicle Images</label>
+               <channels>
+                       <channel id="image-data" typeId="image-data-channel"/>
+                       <channel id="image-view" typeId="image-view-channel"/>
+                       <channel id="clear-cache" typeId="clear-cache-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/light-channel-types.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/light-channel-types.xml
new file mode 100644 (file)
index 0000000..029c20c
--- /dev/null
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mercedesme"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-type id="interior-front-channel">
+               <item-type>Switch</item-type>
+               <label>Interior Light Front</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="interior-rear-channel">
+               <item-type>Switch</item-type>
+               <label>Interior Light Rear</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="reading-left-channel">
+               <item-type>Switch</item-type>
+               <label>Reading Light Left</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="reading-right-channel">
+               <item-type>Switch</item-type>
+               <label>Reading Light Right</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="light-switch-channel">
+               <item-type>Number</item-type>
+               <label>Main Light Rotary</label>
+               <state readOnly="true">
+                       <options>
+                               <option value="0">Auto</option>
+                               <option value="1">Headlight</option>
+                               <option value="2">Sidelight Left</option>
+                               <option value="3">Sidelight Right</option>
+                               <option value="4">Parking Light</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="last-lights-update-channel">
+               <item-type>DateTime</item-type>
+               <label>Last Light Update</label>
+               <state pattern="%1$tA, %1$td.%1$tm. %1$tH:%1$tM" readOnly="true"/>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/lights-group.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/lights-group.xml
new file mode 100644 (file)
index 0000000..3caebd7
--- /dev/null
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mercedesme"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="light-values">
+               <label>Light Status</label>
+               <description>Light Status of interior lights and main light switch</description>
+               <channels>
+                       <channel id="interior-front" typeId="interior-front-channel"/>
+                       <channel id="interior-rear" typeId="interior-rear-channel"/>
+                       <channel id="light-switch" typeId="light-switch-channel"/>
+                       <channel id="reading-left" typeId="reading-left-channel"/>
+                       <channel id="reading-right" typeId="reading-right-channel"/>
+                       <channel id="last-update" typeId="last-lights-update-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/location-channel-group.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/location-channel-group.xml
new file mode 100644 (file)
index 0000000..c91bfef
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mercedesme"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="location-values">
+               <label>Vehicle Location</label>
+               <description>Heading of vehicle</description>
+               <channels>
+                       <channel id="heading" typeId="heading-channel"/>
+                       <channel id="last-update" typeId="last-location-update-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/location-channel-types.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/location-channel-types.xml
new file mode 100644 (file)
index 0000000..06dfc98
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mercedesme"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-type id="heading-channel">
+               <item-type>Number:Angle</item-type>
+               <label>Heading Angle</label>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="last-location-update-channel">
+               <item-type>DateTime</item-type>
+               <label>Last Location Update</label>
+               <state pattern="%1$tA, %1$td.%1$tm. %1$tH:%1$tM" readOnly="true"/>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/lock-channel-types.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/lock-channel-types.xml
new file mode 100644 (file)
index 0000000..7ce9ccc
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mercedesme"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-type id="doors-lock-channel">
+               <item-type>Number</item-type>
+               <label>Door Lock Status</label>
+               <state readOnly="true">
+                       <options>
+                               <option value="0">Unlocked</option>
+                               <option value="1">Locked Internal</option>
+                               <option value="2">Locked External</option>
+                               <option value="3">Unlocked Selective</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="deck-lid-lock-channel">
+               <item-type>Switch</item-type>
+               <label>Deck Lid Lock</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="flap-lock-channel">
+               <item-type>Switch</item-type>
+               <label>Flap Lock</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="last-lock-update-channel">
+               <item-type>DateTime</item-type>
+               <label>Last Lock Update</label>
+               <state pattern="%1$tA, %1$td.%1$tm. %1$tH:%1$tM" readOnly="true"/>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/lock-group.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/lock-group.xml
new file mode 100644 (file)
index 0000000..cde1b07
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mercedesme"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="lock-values">
+               <label>Lock Status</label>
+               <description>Vehicle Lock Status</description>
+               <channels>
+                       <channel id="doors" typeId="doors-lock-channel"/>
+                       <channel id="deck-lid" typeId="deck-lid-lock-channel"/>
+                       <channel id="flap" typeId="flap-lock-channel"/>
+                       <channel id="last-update" typeId="last-lock-update-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/range-channel-types.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/range-channel-types.xml
new file mode 100644 (file)
index 0000000..ac39ba0
--- /dev/null
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mercedesme"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-type id="mileage-channel">
+               <item-type>Number:Length</item-type>
+               <label>Mileage</label>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="range-electric-channel">
+               <item-type>Number:Length</item-type>
+               <label>Electric Range</label>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="radius-electric-channel">
+               <item-type>Number:Length</item-type>
+               <label>Electric Radius</label>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="soc-channel">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Battery Charge Level</label>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="charged-channel">
+               <item-type>Number:Energy</item-type>
+               <label>Charged Battery Energy</label>
+               <state pattern="%.2f %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="uncharged-channel">
+               <item-type>Number:Energy</item-type>
+               <label>Uncharged Battery Energy</label>
+               <state pattern="%.2f %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="range-fuel-channel">
+               <item-type>Number:Length</item-type>
+               <label>Fuel Range</label>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="radius-fuel-channel">
+               <item-type>Number:Length</item-type>
+               <label>Fuel Radius</label>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="fuel-level-channel">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Fuel Level</label>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="fuel-remain-channel">
+               <item-type>Number:Volume</item-type>
+               <label>Remaining Fuel</label>
+               <state pattern="%.2f %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="fuel-open-channel">
+               <item-type>Number:Volume</item-type>
+               <label>Open Fuel Capacity</label>
+               <state pattern="%.2f %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="range-hybrid-channel">
+               <item-type>Number:Length</item-type>
+               <label>Hybrid Range</label>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="radius-hybrid-channel">
+               <item-type>Number:Length</item-type>
+               <label>Hybrid Radius</label>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="last-range-update-channel">
+               <item-type>DateTime</item-type>
+               <label>Last Range Update</label>
+               <state pattern="%1$tA, %1$td.%1$tm. %1$tH:%1$tM" readOnly="true"/>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/range-conv-channel-group.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/range-conv-channel-group.xml
new file mode 100644 (file)
index 0000000..0599c93
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mercedesme"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="range-conv-values">
+               <label>Range and Fuel Data</label>
+               <description>Provides Mileage, remaining range and fuel level values</description>
+               <channels>
+                       <channel id="mileage" typeId="mileage-channel"/>
+                       <channel id="range-fuel" typeId="range-fuel-channel"/>
+                       <channel id="radius-fuel" typeId="radius-fuel-channel"/>
+                       <channel id="fuel-level" typeId="fuel-level-channel"/>
+                       <channel id="fuel-remain" typeId="fuel-remain-channel"/>
+                       <channel id="fuel-open" typeId="fuel-open-channel"/>
+                       <channel id="last-update" typeId="last-range-update-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/range-ev-channel-group.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/range-ev-channel-group.xml
new file mode 100644 (file)
index 0000000..4fbd83e
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mercedesme"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="range-ev-values">
+               <label>Range and Charge Data</label>
+               <description>Provides Mileage, remaining range and charge level values</description>
+               <channels>
+                       <channel id="mileage" typeId="mileage-channel"/>
+                       <channel id="range-electric" typeId="range-electric-channel"/>
+                       <channel id="radius-electric" typeId="radius-electric-channel"/>
+                       <channel id="soc" typeId="soc-channel"/>
+                       <channel id="charged" typeId="charged-channel"/>
+                       <channel id="uncharged" typeId="uncharged-channel"/>
+                       <channel id="last-update" typeId="last-range-update-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/range-hybrid-channel-group.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/range-hybrid-channel-group.xml
new file mode 100644 (file)
index 0000000..a9ec537
--- /dev/null
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mercedesme"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="range-hybrid-values">
+               <label>Range, Charge / Fuel Data</label>
+               <description>Provides mileage, remaining fuel and range data for hybrid vehicles</description>
+               <channels>
+                       <channel id="mileage" typeId="mileage-channel"/>
+                       <channel id="range-electric" typeId="range-electric-channel"/>
+                       <channel id="radius-electric" typeId="radius-electric-channel"/>
+                       <channel id="soc" typeId="soc-channel"/>
+                       <channel id="charged" typeId="charged-channel"/>
+                       <channel id="uncharged" typeId="uncharged-channel"/>
+                       <channel id="range-fuel" typeId="range-fuel-channel"/>
+                       <channel id="radius-fuel" typeId="radius-fuel-channel"/>
+                       <channel id="fuel-level" typeId="fuel-level-channel"/>
+                       <channel id="fuel-remain" typeId="fuel-remain-channel"/>
+                       <channel id="fuel-open" typeId="fuel-open-channel"/>
+                       <channel id="range-hybrid" typeId="range-hybrid-channel"/>
+                       <channel id="radius-hybrid" typeId="radius-fuel-channel"/>
+                       <channel id="last-update" typeId="last-range-update-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/thing-bev.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/thing-bev.xml
new file mode 100644 (file)
index 0000000..8964e7d
--- /dev/null
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mercedesme"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="bev">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="account"/>
+               </supported-bridge-type-refs>
+
+               <label>Mercedes Benz BEV</label>
+               <description>Battery Electric Vehicle</description>
+
+               <channel-groups>
+                       <channel-group id="range" typeId="range-ev-values"/>
+                       <channel-group id="doors" typeId="door-values"/>
+                       <channel-group id="windows" typeId="window-values"/>
+                       <channel-group id="lights" typeId="light-values"/>
+                       <channel-group id="lock" typeId="lock-values"/>
+                       <channel-group id="location" typeId="location-values"/>
+                       <channel-group id="image" typeId="image-values"/>
+               </channel-groups>
+
+               <config-description-ref uri="thing-type:mercedesme:bev"/>
+       </thing-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/thing-combustion.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/thing-combustion.xml
new file mode 100644 (file)
index 0000000..672735d
--- /dev/null
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mercedesme"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="combustion">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="account"/>
+               </supported-bridge-type-refs>
+
+               <label>Mercedes Benz</label>
+               <description>Conventional Fuel Vehicle</description>
+
+               <channel-groups>
+                       <channel-group id="range" typeId="range-conv-values"/>
+                       <channel-group id="doors" typeId="door-values"/>
+                       <channel-group id="windows" typeId="window-values"/>
+                       <channel-group id="lights" typeId="light-values"/>
+                       <channel-group id="lock" typeId="lock-values"/>
+                       <channel-group id="location" typeId="location-values"/>
+                       <channel-group id="image" typeId="image-values"/>
+               </channel-groups>
+
+               <config-description-ref uri="thing-type:mercedesme:conv"/>
+       </thing-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/thing-hybrid.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/thing-hybrid.xml
new file mode 100644 (file)
index 0000000..a84adc0
--- /dev/null
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mercedesme"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="hybrid">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="account"/>
+               </supported-bridge-type-refs>
+
+               <label>Mercedes Benz Hybrid</label>
+               <description>Conventional Fuel Vehicle with supporting Electric Engine</description>
+
+               <channel-groups>
+                       <channel-group id="range" typeId="range-hybrid-values"/>
+                       <channel-group id="doors" typeId="door-values"/>
+                       <channel-group id="windows" typeId="window-values"/>
+                       <channel-group id="lights" typeId="light-values"/>
+                       <channel-group id="lock" typeId="lock-values"/>
+                       <channel-group id="location" typeId="location-values"/>
+                       <channel-group id="image" typeId="image-values"/>
+               </channel-groups>
+
+               <config-description-ref uri="thing-type:mercedesme:hybrid"/>
+       </thing-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/window-channel-types.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/window-channel-types.xml
new file mode 100644 (file)
index 0000000..ec59ab2
--- /dev/null
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mercedesme"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-type id="window-driver-front-channel">
+               <item-type>Number</item-type>
+               <label>Driver Window</label>
+               <state readOnly="true">
+                       <options>
+                               <option value="0">Intermediate</option>
+                               <option value="1">Open</option>
+                               <option value="2">Closed</option>
+                               <option value="3">Airing</option>
+                               <option value="4">Intermediate</option>
+                               <option value="5">Running</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="window-driver-rear-channel">
+               <item-type>Number</item-type>
+               <label>Driver Window Rear</label>
+               <state readOnly="true">
+                       <options>
+                               <option value="0">Intermediate</option>
+                               <option value="1">Open</option>
+                               <option value="2">Closed</option>
+                               <option value="3">Airing</option>
+                               <option value="4">Intermediate</option>
+                               <option value="5">Running</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="window-passenger-front-channel">
+               <item-type>Number</item-type>
+               <label>Passenger Window</label>
+               <state readOnly="true">
+                       <options>
+                               <option value="0">Intermediate</option>
+                               <option value="1">Open</option>
+                               <option value="2">Closed</option>
+                               <option value="3">Airing</option>
+                               <option value="4">Intermediate</option>
+                               <option value="5">Running</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="window-passenger-rear-channel">
+               <item-type>Number</item-type>
+               <label>Passenger Window Rear</label>
+               <state readOnly="true">
+                       <options>
+                               <option value="0">Intermediate</option>
+                               <option value="1">Open</option>
+                               <option value="2">Closed</option>
+                               <option value="3">Airing</option>
+                               <option value="4">Intermediate</option>
+                               <option value="5">Running</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="last-windows-update-channel">
+               <item-type>DateTime</item-type>
+               <label>Last Window Update</label>
+               <state pattern="%1$tA, %1$td.%1$tm. %1$tH:%1$tM" readOnly="true"/>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/window-group.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/window-group.xml
new file mode 100644 (file)
index 0000000..aca8ffb
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mercedesme"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="window-values">
+               <label>Detailed Window Status</label>
+               <description>Detailed Status Windows</description>
+               <channels>
+                       <channel id="driver-front" typeId="window-driver-front-channel"/>
+                       <channel id="driver-rear" typeId="window-driver-rear-channel"/>
+                       <channel id="passenger-front" typeId="window-passenger-front-channel"/>
+                       <channel id="passenger-rear" typeId="window-passenger-rear-channel"/>
+                       <channel id="last-update" typeId="last-windows-update-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/ConfigurationTest.java b/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/ConfigurationTest.java
new file mode 100644 (file)
index 0000000..d8361a7
--- /dev/null
@@ -0,0 +1,71 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mercedesme;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.SocketException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mercedesme.internal.Constants;
+import org.openhab.binding.mercedesme.internal.config.AccountConfiguration;
+import org.openhab.binding.mercedesme.internal.server.Utils;
+
+/**
+ * The {@link ConfigurationTest} Test configuration settings
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+class ConfigurationTest {
+
+    @Test
+    void testScope() {
+        AccountConfiguration ac = new AccountConfiguration();
+        assertEquals(
+                "offline_access mb:vehicle:mbdata:payasyoudrive mb:vehicle:mbdata:vehiclestatus mb:vehicle:mbdata:vehiclelock mb:vehicle:mbdata:fuelstatus mb:vehicle:mbdata:evstatus",
+                ac.getScope());
+    }
+
+    @Test
+    void testApiUrlEndpoint() {
+        String url = Constants.FUEL_URL;
+        String[] endpoint = url.split("/");
+        String finalEndpoint = endpoint[endpoint.length - 1];
+        assertEquals("fuelstatus", finalEndpoint);
+    }
+
+    @Test
+    void testRound() {
+        int socValue = 66;
+        double batteryCapacity = 66.5;
+        float chargedValue = Math.round(socValue * 1000 * (float) batteryCapacity / 1000) / (float) 100;
+        assertEquals(43.89, chargedValue, 0.01);
+        float unchargedValue = Math.round((100 - socValue) * 1000 * (float) batteryCapacity / 1000) / (float) 100;
+        assertEquals(22.61, unchargedValue, 0.01);
+        assertEquals(batteryCapacity, chargedValue + unchargedValue, 0.01);
+    }
+
+    @Test
+    public void testCallbackUrl() throws SocketException {
+        String ip = Utils.getCallbackIP();
+        try {
+            assertTrue(InetAddress.getByName(ip).isReachable(10));
+        } catch (IOException e) {
+            assertTrue(false, "IP " + ip + " not reachable");
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/ImageTest.java b/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/ImageTest.java
new file mode 100644 (file)
index 0000000..1b4556d
--- /dev/null
@@ -0,0 +1,57 @@
+/**
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.util.MultiMap;
+import org.eclipse.jetty.util.UrlEncoded;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mercedesme.internal.config.VehicleConfiguration;
+
+/**
+ * The {@link ImageTest} Test Image conversions
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+class ImageTest {
+
+    @Test
+    public void testConfig() {
+        Optional<VehicleConfiguration> config = Optional.of(new VehicleConfiguration());
+        MultiMap<String> parameterMap = new MultiMap<String>();
+        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("format", config.get().format);
+        String params = UrlEncoded.encode(parameterMap, null, false);
+        assertEquals("background=false&night=false&cropped=false&roofOpen=false&format=webp", params);
+
+        config.get().background = true;
+        config.get().format = "png";
+        config.get().cropped = true;
+        parameterMap = new MultiMap<String>();
+        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("format", config.get().format);
+        params = UrlEncoded.encode(parameterMap, null, false);
+        assertEquals("background=true&night=false&cropped=true&roofOpen=false&format=png", params);
+    }
+}
diff --git a/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/JsonTest.java b/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/JsonTest.java
new file mode 100644 (file)
index 0000000..4be0d72
--- /dev/null
@@ -0,0 +1,250 @@
+/**
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mercedesme.internal.utils.ChannelStateMap;
+import org.openhab.binding.mercedesme.internal.utils.Mapper;
+
+/**
+ * The {@link JsonTest} Test Json conversions
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+class JsonTest {
+    public static final String DATE_INPUT_PATTERN_STRING = "yyyy-MM-dd'T'HH:mm:ss";
+    public static final DateTimeFormatter DATE_INPUT_PATTERN = DateTimeFormatter.ofPattern(DATE_INPUT_PATTERN_STRING);
+
+    @Test
+    void testOdoMapper() throws Exception {
+        List<String> expectedResults = new ArrayList<String>();
+        expectedResults.add("range:mileage 4131 km");
+        String content = Files.readString(Path.of("src/test/resources/odo.json"));
+        JSONArray ja = new JSONArray(content);
+        assertTrue(ja.length() > 0);
+        ja.forEach(entry -> {
+            JSONObject jo = (JSONObject) entry;
+            ChannelStateMap csm = Mapper.getChannelStateMap(jo);
+            assertNotNull(csm);
+            assertTrue(expectedResults.contains(csm.toString()));
+            boolean removed = expectedResults.remove(csm.toString());
+            if (!removed) {
+                assertTrue(false, csm.toString() + " not removed");
+            }
+        });
+        assertEquals(0, expectedResults.size(), "All content delivered");
+    }
+
+    @Test
+    void testEVMapper() throws IOException {
+        List<String> expectedResults = new ArrayList<String>();
+        expectedResults.add("range:range-electric 325 km");
+        expectedResults.add("range:soc 78 %");
+        String content = Files.readString(Path.of("src/test/resources/evstatus.json"));
+        JSONArray ja = new JSONArray(content);
+        assertTrue(ja.length() > 0);
+        ja.forEach(entry -> {
+            JSONObject jo = (JSONObject) entry;
+            ChannelStateMap csm = Mapper.getChannelStateMap(jo);
+            assertNotNull(csm);
+            assertTrue(expectedResults.contains(csm.toString()));
+            boolean removed = expectedResults.remove(csm.toString());
+            if (!removed) {
+                assertTrue(false, csm.toString() + " not removed");
+            }
+        });
+        assertEquals(0, expectedResults.size(), "All content delivered");
+    }
+
+    @Test
+    void testFuelMapper() throws IOException {
+        List<String> expectedResults = new ArrayList<String>();
+        expectedResults.add("range:range-fuel 1292 km");
+        expectedResults.add("range:fuel-level 90 %");
+        String content = Files.readString(Path.of("src/test/resources/fuel.json"));
+        JSONArray ja = new JSONArray(content);
+        assertTrue(ja.length() > 0);
+        ja.forEach(entry -> {
+            JSONObject jo = (JSONObject) entry;
+            ChannelStateMap csm = Mapper.getChannelStateMap(jo);
+            assertNotNull(csm);
+            assertTrue(expectedResults.contains(csm.toString()));
+            boolean removed = expectedResults.remove(csm.toString());
+            if (!removed) {
+                assertTrue(false, csm.toString() + " not removed");
+            }
+        });
+    }
+
+    @Test
+    void testLockMapper() throws IOException {
+        List<String> expectedResults = new ArrayList<String>();
+        expectedResults.add("lock:doors 0");
+        expectedResults.add("lock:deck-lid ON");
+        expectedResults.add("lock:flap ON");
+        expectedResults.add("location:heading 120 Â°");
+        String content = Files.readString(Path.of("src/test/resources/lock.json"));
+        JSONArray ja = new JSONArray(content);
+        assertTrue(ja.length() > 0);
+        ja.forEach(entry -> {
+            JSONObject jo = (JSONObject) entry;
+            ChannelStateMap csm = Mapper.getChannelStateMap(jo);
+            assertNotNull(csm);
+            assertTrue(expectedResults.contains(csm.toString()));
+            boolean removed = expectedResults.remove(csm.toString());
+            if (!removed) {
+                assertTrue(false, csm.toString() + " not removed");
+            }
+        });
+    }
+
+    @Test
+    void testStatusMapper() throws IOException {
+        List<String> expectedResults = new ArrayList<String>();
+        expectedResults.add("doors:deck-lid CLOSED");
+        expectedResults.add("doors:driver-front CLOSED");
+        expectedResults.add("doors:passenger-front CLOSED");
+        expectedResults.add("doors:driver-rear CLOSED");
+        expectedResults.add("doors:passenger-rear CLOSED");
+        expectedResults.add("lights:interior-front OFF");
+        expectedResults.add("lights:interior-rear OFF");
+        expectedResults.add("lights:light-switch 0");
+        expectedResults.add("lights:reading-left OFF");
+        expectedResults.add("lights:reading-right OFF");
+        expectedResults.add("doors:rooftop 0");
+        expectedResults.add("doors:sunroof 0");
+        expectedResults.add("windows:driver-front 0");
+        expectedResults.add("windows:passenger-front 0");
+        expectedResults.add("windows:driver-rear 0");
+        expectedResults.add("windows:passenger-rear 0");
+
+        String content = Files.readString(Path.of("src/test/resources/status.json"));
+        JSONArray ja = new JSONArray(content);
+        assertTrue(ja.length() > 0);
+        ja.forEach(entry -> {
+            JSONObject jo = (JSONObject) entry;
+            ChannelStateMap csm = Mapper.getChannelStateMap(jo);
+            assertNotNull(csm);
+            assertTrue(expectedResults.contains(csm.toString()));
+            boolean removed = expectedResults.remove(csm.toString());
+            if (!removed) {
+                assertTrue(false, csm.toString() + " not removed");
+            }
+        });
+        assertEquals(0, expectedResults.size(), "All content delivered");
+    }
+
+    @Test
+    void testEQALightsMapper() throws IOException {
+        // real life example
+        List<String> expectedResults = new ArrayList<String>();
+        expectedResults.add("doors:passenger-front OPEN");
+        expectedResults.add("windows:driver-front 1");
+        expectedResults.add("windows:driver-rear 1");
+        expectedResults.add("windows:passenger-rear 1");
+        expectedResults.add("windows:passenger-front 1");
+        expectedResults.add("lights:light-switch 0");
+        expectedResults.add("lights:reading-right ON");
+        expectedResults.add("lights:reading-left ON");
+        expectedResults.add("doors:driver-front CLOSED");
+        expectedResults.add("doors:driver-rear CLOSED");
+
+        String content = Files.readString(Path.of("src/test/resources/eqa-light-sample.json"));
+        JSONArray ja = new JSONArray(content);
+        assertTrue(ja.length() > 0);
+        ja.forEach(entry -> {
+            JSONObject jo = (JSONObject) entry;
+            ChannelStateMap csm = Mapper.getChannelStateMap(jo);
+            assertTrue(expectedResults.contains(csm.toString()));
+            boolean removed = expectedResults.remove(csm.toString());
+            if (!removed) {
+                assertTrue(false, csm.toString() + " not removed");
+            }
+        });
+        assertEquals(0, expectedResults.size(), "All content delivered");
+    }
+
+    @Test
+    void testTimeStamp() throws IOException {
+        String content = Files.readString(Path.of("src/test/resources/eqa-light-sample.json"));
+        JSONArray ja = new JSONArray(content);
+        assertTrue(ja.length() > 0);
+        long lastTimestamp = 0;
+        for (Iterator<Object> iterator = ja.iterator(); iterator.hasNext();) {
+            JSONObject jo = (JSONObject) iterator.next();
+            Set<String> s = jo.keySet();
+            if (!s.isEmpty()) {
+                String id = s.toArray()[0].toString();
+                JSONObject val = jo.getJSONObject(id);
+                if (val.has("timestamp")) {
+                    lastTimestamp = val.getLong("timestamp");
+                }
+            }
+        }
+        Date d = new Date(lastTimestamp);
+        ZonedDateTime zdt = d.toInstant().atZone(ZoneId.of("Europe/Paris"));
+        assertEquals("2022-06-19T16:46:31", zdt.format(DATE_INPUT_PATTERN));
+    }
+
+    @Test
+    void testInvalidData() throws IOException {
+        String content = Files.readString(Path.of("src/test/resources/invalid-key.json"));
+        JSONArray ja = new JSONArray(content);
+        assertTrue(ja.length() > 0);
+        ja.forEach(entry -> {
+            JSONObject jo = (JSONObject) entry;
+            ChannelStateMap csm = Mapper.getChannelStateMap(jo);
+            assertNotNull(csm);
+            assertFalse(csm.isValid());
+        });
+    }
+
+    @Test
+    void testMissingTimestamp() throws IOException {
+        List<String> expectedResults = new ArrayList<String>();
+        expectedResults.add("range:mileage 4131 km");
+        String content = Files.readString(Path.of("src/test/resources/invalid-timestamp.json"));
+        JSONArray ja = new JSONArray(content);
+        assertTrue(ja.length() > 0);
+        ja.forEach(entry -> {
+            JSONObject jo = (JSONObject) entry;
+            ChannelStateMap csm = Mapper.getChannelStateMap(jo);
+            assertNotNull(csm);
+            assertTrue(expectedResults.contains(csm.toString()));
+            assertEquals(-1, csm.getTimestamp());
+            boolean removed = expectedResults.remove(csm.toString());
+            if (!removed) {
+                assertTrue(false, csm.toString() + " not removed");
+            }
+        });
+        assertEquals(0, expectedResults.size(), "All content delivered");
+    }
+}
diff --git a/bundles/org.openhab.binding.mercedesme/src/test/resources/eqa-light-sample.json b/bundles/org.openhab.binding.mercedesme/src/test/resources/eqa-light-sample.json
new file mode 100644 (file)
index 0000000..8bc6589
--- /dev/null
@@ -0,0 +1,62 @@
+[
+       {
+               "doorstatusfrontright": {
+                       "value": "true",
+                       "timestamp": 1655650113000
+               }
+       },
+       {
+               "doorstatusfrontleft": {
+                       "value": "false",
+                       "timestamp": 1655650104000
+               }
+       },
+       {
+               "windowstatusfrontleft": {
+                       "value": "1",
+                       "timestamp": 1655648946000
+               }
+       },
+       {
+               "windowstatusrearleft": {
+                       "value": "1",
+                       "timestamp": 1655648959000
+               }
+       },
+       {
+               "windowstatusrearright": {
+                       "value": "1",
+                       "timestamp": 1655648959000
+               }
+       },
+       {
+               "doorstatusrearleft": {
+                       "value": "false",
+                       "timestamp": 1655498496000
+               }
+       },
+       {
+               "windowstatusfrontright": {
+                       "value": "1",
+                       "timestamp": 1655648953000
+               }
+       },
+       {
+               "lightswitchposition": {
+                       "value": "0",
+                       "timestamp": 1655650824000
+               }
+       },
+       {
+               "readingLampFrontRight": {
+                       "value": "true",
+                       "timestamp": 1655649991000
+               }
+       },
+       {
+               "readingLampFrontLeft": {
+                       "value": "true",
+                       "timestamp": 1655649991000
+               }
+       }
+]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mercedesme/src/test/resources/evstatus.json b/bundles/org.openhab.binding.mercedesme/src/test/resources/evstatus.json
new file mode 100644 (file)
index 0000000..907154a
--- /dev/null
@@ -0,0 +1,14 @@
+[
+       {
+               "rangeelectric": {
+                       "value": "325",
+                       "timestamp": 1655401822000
+               }
+       },
+       {
+               "soc": {
+                       "value": "78",
+                       "timestamp": 1655399096000
+               }
+       }
+]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mercedesme/src/test/resources/fuel.json b/bundles/org.openhab.binding.mercedesme/src/test/resources/fuel.json
new file mode 100644 (file)
index 0000000..a71eb02
--- /dev/null
@@ -0,0 +1,14 @@
+[
+  {
+    "tanklevelpercent": {
+      "value": "90",
+      "timestamp": 1541080800000
+    }
+  },
+  {
+    "rangeliquid": {
+      "value": "1292",
+      "timestamp": 1541080800000
+    }
+  }
+]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mercedesme/src/test/resources/image/ext.json b/bundles/org.openhab.binding.mercedesme/src/test/resources/image/ext.json
new file mode 100644 (file)
index 0000000..30b34dc
--- /dev/null
@@ -0,0 +1,13 @@
+{
+       "EXT000": "5jgA6wXiEiufaoAJcWWhQGALZUoltT2pnoXIFsCc6NBvSKz5wtbR3tfFFRzvg2aZWU9OeL75vJZpAQeh3jgzSDB1vXLyXEtErBzLP7WGVMu1Mvz5w6vf77fW_R_4vE57-FsZZQO8i4VCpAIvOhwTtV-2uvVw9qAtXQrAwpJz7r839JlvNf4uYyVFjiRjrE75_vDZA7bbkw33xDs2fSs5htgd39Zz3KfuKZvLmGW7qpCHRnr9lZixKCGydR8Hj4LM3XsEa0ebaCcdWOg4tuy0qN-YBH2-DrE7xQVso5iEiKIndBdA-ecF42jIYPdKk9x8gIKeSx-zcYFpwYXUZPtWo3vZQjqdiBVTxBVVK2JRYTOk9I0Qzp735Mqi5PtIYGrg2hIOH4F0CKuiknhc4XBabN22dAaxaUuf5juveQVD-h0WwV2xue6vQ8nvaTN2gyVpO4JXDmW9jE8kShKvDRZeh3JZKq8zGW4FfCP78rtyt8kc671d9PLcZBdi1_TgDMUBWldBP4xHrY_5aJvxn9nNji7E1A9Cw8tfmMyjuq0Wy3nsUHpctYXb0eXrrunj0-Uk_Aq2vx1PI1i8ko6-05uilE026reCDysoWY8Re2Ea1SRocp4P3B67Luqa__kgxqtdY3VdYtWuAuPrTESuHQZBEFa2EfNktvpZdXOYrvcqhGGbq76ybJXuQGB6ZwwiCi5ZFsq9ejYt9qcFYN_kxbpk6phLu-2yzqpcMEMMftFhZnY=",
+       "EXT090": "oHtxoRJTvmza2fm4AFhd2_fFYmRFC-uuZeQP7B8uENz9cCHgN9jMXS0bWGprZhIuhM65tPtdUSsvjoVqYD-n_HcQrqUB_PQkvr61RAiHz5UOXZSW7_5QPv_0Tdt3g1NHQFs5zPdI1KffucgiuNlhLNaWgpzT5z0AcZba9o7IwxzF5WKOkT4HsQ0etc3FYXvfnXeHi71F0223JkRQtlXEu4aNHELonubumptt9YJM0qXbcttPs-eNFE8l2eHOsKvh34g6X4Z2LZAENmABjMXQMQhxyWGRr_N7mIoeQWo6AVQ01QavM_MSKZY6Bw3-WdDHr8pybwg7uee52NgrwMsN7_ufeEG3xkAeSXixMhNt4yutYwaWkdxSdGKp4UnINj493VL8_5XAF7nfeHRioXe3XWU6mfg-PGzJwywm3ll7qbp_8OGkws3u03xXKxINkgZWVjmAtOnmFm80XGTQL8HxsPspI2ItF1QQDyQhvYYrGe-saxRstxxr1HM2VkualA3hxlNkE5CSNfbm3B1fUSH25Ba-YBo4SluEsu5mQIOemT1NBqMMZeQBCiO_73PwgIoRZnjH3cRPl_oUO3jwQEPPqH-jXBdFdQ9mksVWrHBcAQrVPAY5QQYaXN0Io8Va0q11GR8RUj_avraROJ_dLlWUDJCU1DUTv677lifWhIxyDXHj4CfgartLyYAPsGWuXm7wLc41dOXuS-WsiJoJjwuK2S83oUfUVH56Hf6gwZhvK8E=",
+       "EXT150": "FyyrR6D7K99MXXbCQ2Jt1N2GErxjQricdwDCJtzY5vW3TnqDr3JmEf5PUIy_yYnqscuiVLGTrBw1-9r6eR92uJncUPCkaNYHHIXt3p5ZfEu9TVcnURaydDH6GBB3yV9Cocv-8EeYbT2njjEe-n5qwhj44LfILKmMVC0lFzGRdixcAr1cu4chtnhvitIzOoCIG43BeTfWxjz4CwiIt3d4X05dS15qTWXFFlNCBc1xXGngdrqqO9USE3D8Hrq_0RuL_m4utANmLw2ud_rzlmpJFN7smkUJ62eKrpuJrn-aSrt3q9_OPewzC88-CpybvorcuVK9Oy_UZbU5aTFomUKH5DTEydJFBDeGLF6uZJSySKHWf395mmh1JBIxpghji4NhjONG8vlRKAZu9MbMQSWBiEJj2i1yWQQi9E8tYJN5g8uadP1SpSnIsM1FkDW8Esnu8bfqrYMQoobilCRkrmqaWCE4YDJcJRrf04oas12qqMZwLsRSvafSh7Cc6jIwggXxZ9u5d4wu9ov0oKfWMiBmvyt9Pb1kYxrt-nbL_pTkDOCAkBJw2AjiEFqcocMoCzx2uh1xQ8K9-DP4gGq4yKmY5EizbYMmYYhtorjKzmequ0BbKJ2YG-wIe65XA8i_dE5V-JeTL5CoSsRhoOBQK6hHtXk7PtgcdBV_0RAI85cnDSsoGNti2Nu3WT_S4PIstSSKrPpI9Yj6KeEV2EiM9a6flUkNo_QMwjKCuM3kCnvopjE=",
+       "EXT180": "h2nEDkftnTN7t3b93Rv-_KnASjdCQMiVbFWDIH4UQPlmDw44dH54TqJg1KgQFqVIojWgPRHLvh9DBfQCHh3wTpk-b_UGrsv22uWrm4DwVgC9ZMPaYM8nWUGmVfN2Aigzp8nH5gWNmRP2rE3rEPRC7W6E3E_-U8CHSEZ9rvld04eYbZEb-zdrcLmo9DhgzX6ETMq3Rac2ehXSO5KipM0t2-X5LtPXmdf1NcH_EBCSBW1bSrRCJr6RvGjRXvPiDFXR7QGhEYKHzIW6o0smbRp8BnIXnL7LlRe4cDH_M4Wk3wYTAsi6ET5gXed82Ubf2qtl-aHLL3_Su5DfyGAuzEc6rjcIljpZrgTE1UfcBxYgob2kYGr8iHBltgNhs69iAe2V_e6mS3ogQE8Hc2BMe9T8aLylmKtainufJKfzCOnyuMUDnBDNe84oKRNsyJHT8pKHewMJVxW8mOsm3sPC_nTb-UsUN9jqXmCugPqYdPAYtEp5mOpBpsltUX7a6LMcJERX0bGRGXYuGSpaFXCeynlQyTCnGJk2tgt65f4c2MGxg3OLWNh5FCQMq3miC8S2tIN15h2rw0vXrTYrvPXJPBk6Kjw74c0xUWtdf9EQwR79g0JPnjEUIeeeJvbipXPvZi3TPCev0d1eE3lOq_3onIZI16Noqjm7WHvUPGgn5g8QxDRI9t4aZ1MSKxxYrKUJBlJrMV7GmGHuLl_jYihCeslaXR5qyirZxB0r4KFyLeXuyJ4=",
+       "EXT270": "V1B7Vmu7mPFnDobcpvy5gVMAsnmi3sG_1AISG5nFaIhKw3h8NVwLYTAnptyiYtYaqcTzznk9jQROerdO0loInPHaBFN4lY59RoVQv7uA1ZMpQ4TUu_s7igbwgKEdcB55w3oei1ocLWcdn7QNvaxIqYl3ch5MAJlEOxhljGd5Hu4XHtmKe9IW7KZLqSwJKoI6M5HmiVY16UeqRcKJcKRaYoyaIq3jOJSUPfPJw22An2_oAp0aniq6Mydf3t-LNn8oDCPA5ztT9ivDhasZdWwk3dDF_IBEnwBc4X-UKGk04MRwnDkFnE-jmV5f4_k49VcuWLJEq2S5g-5lpZI55Et19lvGZv_tD1rsBMHcZ4beHwsUWrtykLSWRWt9XDVHk9cvgDAWeutKReMp3Rt3yN9gLMV-Puo9wscg51CuDvGXOsVO_V7vF2_1OEq5E6WJcDa9XqqphUrIhMs7l-s5gT16S4wYZ07BrhRWx1vCATJdP0Oo26QvE-tWthaPkcD1PUsePbx-dXrj-5IRsotGgoEzNRMmahkwNbc_SbILNVhCZXVMyo9h32n1Kdf-uNtF-KBb0aODmti2HV3qe52qILoKeO2XoIDMceNeVp_HjO6UL1np-tm4JOEqkF_as6VCGXfz2YE5cq8wIBfX9uKQwmPmtisK3tmfZKF6nuz4yxGo78cDJZdkE6xY8819ObqZyuFCDbONoxOkdA6KYUjjvhiPikDxzhFJXaq-tegNoDNWUJs=",
+       "EXT330": "-VkMlhWJez5VxQgKzZjpqaXFNL5wjxxAs9TGLMQCkmfrYqHs5qONanQHnzU6Dz7QQwb8a51qk7ULc3dMHtUMvx4ILa-vKpQYGZ2uGenWFIS7lK5YWjHUvHu3uGwgTbAfzigiLq13opE9a5lauK0e4zmJ4aMbWkhIMxHjrak83zsmjFJa0IYhD0qD_f7oH8z-eCJ9tww4cy-p3Rd_pJCfGYi7j3Kz_Z9-gwFe7WFdKLowzzMpCNHr8hQxujtWzZZVQVrH4RSUAZEaN8sMm8uhkR3mE_21wFPFLLG2-Ui3bvObEALZ8ajMUdKJNu0etHGfUTYnHUAWxwesBDu5rqgSXDbeZiN6-4FDSkfTl2IkXOXrtFrW1YzmPvR5SBjed_QuNKrDCvSi9NBxP1vq4by6xJUaSCPyBJb9SfVBswfk6-_2g_d6LNLoT9Qz4Xn9RRHlACNyZe6tb-NNCCHYxY7nz88cDO0Kfb0qkEXyoOXWeDjCLvRXI-dCHCqNSq2D54P_A7e_xtqsoy4qjWp_4EKc7QRDX1j9BN7_CfUtt4MuKLMskP1MpBm07aZFLP6eHwVw8So5cZRDMB1zeQcfjFcHzoZ5nVBvVEPT3UkPfKdFAPkfuf3p233QbJiTGTnbsI05xdQQHtEWbrHc7sf446DsT_oH6w9ej0KwQ41if7jzz5uMIsGTgtOfyGz23k-IUsJQH-T237GegQKNXG0_SetgL8gn0OADUkdkaGbvAAxqMjg=",
+       "INT1": "Kr29P2pI2LE9y2Y7S7kSD-EzhXs6sOfP-HVFAqoNbaIIb34CaauXwOlTz7NjjJ9xBg88y73aR-RGcIg_aqSqmwZvViIrs5esGXOJOvXROoWpodYsYK65XfmSD3o6PtkPXtTFEtfj_3OEgtmvqXuHLB17VXhPWB-GV5O4U4z8Wki7aZTzQi3Xh8ry9Or3GT7JD6z5JWGDFH3wHDkzNuTzuCR8SocQSDBlkH8uSTtl-ZuSQZczYyLuJcqff86niQZbaXEQGArqYifqW-hs3f0Xjm3-Pqvt8sMQcpDZsNLTOzz5GgExE5BDlMMewVlGHK6SkVmA5DDcXSpr0EQjoWjUET5rLDXNiNUMwk_gCJLFBE3qOP-dNFOYnPtyGCHOAt1fWHrLV-iDXBoyRm5nl7fvxGglECyEB1DVLDvOkd882z32G5OFy2oRMpq3qY1sccPE2L8i_lViNtKUJqRuwygkfEFrygQTaoFhqaGfnAp64a8PDsYMpBlHEl4-Y_m0l-7FhPQiYoYJBZDrKG5h37O9RZ8Jttt0jdzhtgHPnTiyz9LEdtBNMgqGr9QFYjSVnGSE3dAHpNuo6J4VeNTEGViVBj0Imqy5-s9N0VR2aziC4kqhQ2M4mRniu2ad6PycqJEWwP4YzlIKt6Pox9goDEhCpY8d9fuQv5ppj0butxtBDC_nw4eyWRlctlH8Lx-pfwjwQb08H3syFM6eCXCtHKGilsyeN2E7HZ3mq1LoprwZHUo=",
+       "INT2": "JFFG7zz5OlAgoUkJUUPXu9h29bBg03yCTOCCtGzQPWCW3cqS453QCVSGd_K-TwXuRYnG54ZLHRPdJ4HQjMTDUZv6ExZck9td5r27H5cpO6wpV7hofz4bW5Klj-9kKOcyF8wHTEwPnoDv1lMVSEL6mcVEXFB0CnzngMs-njRnEXdvhgMrprEoSu1hSeikP5kUaFViOBDAuv9-6V6cfpZehoMQhjBmq6WMTNjhDS-fGsZH0Z2jqu7Oe8yaoydD3tJXZ1KY8GRpf048piD9bv-URJPR5531f4t5b6t6l9sivHDW0wgDoa0K-aF5mRKj6k8Psuu_Krsvm4EAJHZCXyZ-ixg8i58X8TODy70HXS1wRGrLVOBqGYsTpMEWxLOl7YLrn3VUCVN8yYavIDNdxU9nr6o5dA187IqStMEUS7NDUxSWcwmFmpeYDImh4or95wa7cEk1-7w74HAyVg8sC8-VHfwgKPLA2QrSVTp-F9lOu4CmStzf7C7SU3mkcb6XmyHQRAavjiCzAI8eeFMgT8ma_8B62gS1XKmppEQwddz5bZ2f8eKSc4q5WKoq9scxur42d-7JjsSzuenPwVAd9MSqWa2q5qnVMTbWtuLlO9ipLXXGTTYacAQJjZ9dOEdJIIh8MsSteJ6tpas3bAoX15IfvLGuObJFs2vjQ1h4AonjTquPIdo1JAhjActBgNCurWElyBpJ3dprvQ_y9qXgt12kKM0SvWV1zky_p8x53UPcHHk=",
+       "INT3": "utnpbmDAfakTIjB-oSkx7eFoRjXxfxpbexmHUi2RPFCs5A3w8wtH0EM6cThnwEFHulWqSmXFc6v_hd3dyLNsKDAaIUHFktCoE3m5fX2ORgUi4tdXSrx2T8H0xpf0cfFZcg_aS9SK0fgtj2NYaWjoafEI6aLc79XNoiIpTyBONJi0QU6IjlLCA13a47QjVIkrQtPjIntm1EKY34Hyoq8QErZsZQG_f8JEXQMUfM-oOAY0U8gj0outMTtWRAnfdrgaSWhVyIWloUJkXP6cWhZsypKp7e8bm-poc4nE4R-tniMWegY7BLlvtJeSjs71txBaDomr9xqi9lhmX6ENkvGPAHpBbrTOmsMLvEJu6nQR7P08XYalXpiJQjrIQJ77hE499jOIdgSoBjuy6JKlaLc4ejpHeGNXYCPUBJM5KVLITLYpLOjgZFzbhzI8qr4GdjxcfW82PAxhlTDywjDH-eMzbXnVZ-YaLrEocNZ8i88R1bLnv90lUrwzgn7n3zUcTeiRILfkApNZRh_K9ClGSu612LzGv7E-_zN1JdOt4UCcfSSkJLMppbeyIXjE6tDsxjgUyRr0Xj1azvHtEDfFYKj76x9doyk8Xm6_sKF4z9rPrgEmo9u6zr4dhb6fmhTnBjdhWBFjQ1Zpe7P7IfaXeDFrWSQBt7jdADWzB5L8cjaMnULm295uSo1k_ka1FQtFg4NtHiZ2G_zNLINh4DBjZdo5hmqBW7tl4gnNWaUf8YO7J1A=",
+       "INT4": "kJ4tgroRxuaYe0vF9xMe-nI4NUy4Eg1NyAr_E-WxJKlt5jZuoOcWYijc7cQEOiY3xlRPLTnfN-l9nf4UWO9rN4uMIGH9tuEql1g2ATLxff1ML3tCiexoxn45l-LQRH-PL2bL8jsQ3ByiHxLdzMHUzAarWj76rZk4mDCM9tRAE-hQsUZaaGMbO_hL3BCu6J-mI9hU3em4RuFwNTzCfBxgOJShU_PAU3tt-xROhld5Re3cyZzQZfQI17xDJKpTdZmnZCeabv7BIjh6qEapxhCHi0Ue6_EX3DPld1Faj438FptfBTn8UXW9Es6tkM1BGHw8npbHtGVqI1g7DdWuYUb5qeAtUzkEnFpxIl-67sKctYGBWvGkRnZrpu33jwaorW7xInwSymTg0isVCDQCfTEsa8zszj-bD4UMF2jeWr_pL-g4Vk_Ns0vMDFhsZGJTLO9Vv5DNf0WYOMWBISHXgW9z_d0UzCwlJpojqqO1DNhEt1aRRSb_zIBhdfAUHmJy35KmA0tPiDNb1K6uEVVcKSQBD2G9BfrvGXq-o_E3JAUzO2-u4ues0eWseOa0dQQIasjMZ0ZNAakfxCwg7dHp9Lq0c_0fti37OAk69Cyi0EMThCTYr8Yij_sidT9sts-YeQgTniI2LFsMnDnGkWB7wubmxUvG9qOq5ZmonBArkOt8xHWT9Xy5xq0hQj6Ba6Dgcy8qrcCdq8HoIneUaHH_c7pkRW2V-MKHQ6gr9iopAtP1Ifw=",
+       "BET1": "hismr3VKvRu_fQ25jJjIjjBKs-nhdOYsbtMA7Sgc0-P99yjGnVvOq8nJBGFHcjCLHGb6lTcLyA4eVSflnKER2r6R35PqRznX63s1bDGZuuyPexBCTVZSh1ubtI3ImQuO5mRb9wFAyeUcG53JnZEXQmOnCdmLW8seIugeSrsj5-HC_Z7vOT-KTmcONFP2q2tbKqDTziLVXEJ0dy2EdgvSk8l-sn1QsgodDVe-FCaAp35jlswp8faJuyTv0j7luc5dRP7xvSVXX08uTD9AWHooVCbsGSJqwVRsjY0bFJMcvKqzCCE5xlyBVVmkbphD3aEsBqMVsG4wL2_W_pEgdbmI06qHgJK0sotwj0iEbGVBOYOuBp4uAQdadKYbQL2OkJaqXkDWJTy0MDUSRBbZ6qH3KCyVRfUFcxlFUxqw9I7l5G35vnDMCpoJyj6wyzyq4oFTKgDXkn6zMTDD7lgpYWW1zRDtCTecU6nkmsUQnwe-9EXBfcePlRtJasG40ykj5x4MRJQoDiX9F8VBDnquaR_8K5HOL3de8ypEl7q22bKkKv-pHEPO8hH5lZynfQ86RHyHhDGxXuNuCEX-ekqK7K8PGTF8gO_fIGMpp_Z5aqWSHQgu7Ge1R-FTtfKZ5L_E-RjnR9QWFqEIC2_hNiZoQBdSTC8XUMmnpltTBQJOngEBhmgGepekZmuGnP3xmmvVbK1VijHtgB6PCtIVaTIzkwpVo1o4aj25zQG_3VSqgT8Ufnw="
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mercedesme/src/test/resources/invalid-key.json b/bundles/org.openhab.binding.mercedesme/src/test/resources/invalid-key.json
new file mode 100644 (file)
index 0000000..06e434b
--- /dev/null
@@ -0,0 +1,8 @@
+[
+       {
+               "wrong": {
+                       "value": "4131",
+                       "timestamp": 1655399236000
+               }
+       }
+]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mercedesme/src/test/resources/invalid-timestamp.json b/bundles/org.openhab.binding.mercedesme/src/test/resources/invalid-timestamp.json
new file mode 100644 (file)
index 0000000..a04379f
--- /dev/null
@@ -0,0 +1,7 @@
+[
+       {
+               "odo": {
+                       "value": "4131"
+               }
+       }
+]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mercedesme/src/test/resources/lock.json b/bundles/org.openhab.binding.mercedesme/src/test/resources/lock.json
new file mode 100644 (file)
index 0000000..9655d6b
--- /dev/null
@@ -0,0 +1,26 @@
+[
+  {
+    "doorlockstatusvehicle": {
+      "value": "0",
+      "timestamp": 1541080800000
+    }
+  },
+  {
+    "doorlockstatusdecklid": {
+      "value": "false",
+      "timestamp": 1541080800000
+    }
+  },
+  {
+    "doorlockstatusgas": {
+      "value": "false",
+      "timestamp": 1541080800000
+    }
+  },
+  {
+    "positionHeading": {
+      "value": "120",
+      "timestamp": 1541080800000
+    }
+  }
+]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mercedesme/src/test/resources/odo.json b/bundles/org.openhab.binding.mercedesme/src/test/resources/odo.json
new file mode 100644 (file)
index 0000000..bf98a06
--- /dev/null
@@ -0,0 +1,8 @@
+[
+       {
+               "odo": {
+                       "value": "4131",
+                       "timestamp": 1655399236000
+               }
+       }
+]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mercedesme/src/test/resources/status-resources.json b/bundles/org.openhab.binding.mercedesme/src/test/resources/status-resources.json
new file mode 100644 (file)
index 0000000..8dc5574
--- /dev/null
@@ -0,0 +1,82 @@
+[
+  {
+    "name": "decklidstatus",
+    "version": "1.0",
+    "href": "/vehicles/WDB111111ZZZ22222/resources/decklidstatus"
+  },
+  {
+    "name": "doorstatusfrontleft",
+    "version": "1.0",
+    "href": "/vehicles/WDB111111ZZZ22222/resources/doorstatusfrontleft"
+  },
+  {
+    "name": "doorstatusfrontright",
+    "version": "1.0",
+    "href": "/vehicles/WDB111111ZZZ22222/resources/doorstatusfrontright"
+  },
+  {
+    "name": "doorstatusrearleft",
+    "version": "1.0",
+    "href": "/vehicles/WDB111111ZZZ22222/resources/doorstatusrearleft"
+  },
+  {
+    "name": "doorstatusrearright",
+    "version": "1.0",
+    "href": "/vehicles/WDB111111ZZZ22222/resources/doorstatusrearright"
+  },
+  {
+    "name": "interiorLightsFront",
+    "version": "1.0",
+    "href": "/vehicles/WDB111111ZZZ22222/resources/interiorLightsFront"
+  },
+  {
+    "name": "interiorLightsRear",
+    "version": "1.0",
+    "href": "/vehicles/WDB111111ZZZ22222/resources/interiorLightsRear"
+  },
+  {
+    "name": "lightswitchposition",
+    "version": "1.0",
+    "href": "/vehicles/WDB111111ZZZ22222/resources/lightswitchposition"
+  },
+  {
+    "name": "readingLampFrontLeft",
+    "version": "1.0",
+    "href": "/vehicles/WDB111111ZZZ22222/resources/readingLampFrontLeft"
+  },
+  {
+    "name": "readingLampFrontRight",
+    "version": "1.0",
+    "href": "/vehicles/WDB111111ZZZ22222/resources/readingLampFrontRight"
+  },
+  {
+    "name": "rooftopstatus",
+    "version": "1.0",
+    "href": "/vehicles/WDB111111ZZZ22222/resources/rooftopstatus"
+  },
+  {
+    "name": "sunroofstatus",
+    "version": "1.0",
+    "href": "/vehicles/WDB111111ZZZ22222/resources/sunroofstatus"
+  },
+  {
+    "name": "windowstatusfrontleft",
+    "version": "1.0",
+    "href": "/vehicles/WDB111111ZZZ22222/resources/windowstatusfrontleft"
+  },
+  {
+    "name": "windowstatusfrontright",
+    "version": "1.0",
+    "href": "/vehicles/WDB111111ZZZ22222/resources/windowstatusfrontright"
+  },
+  {
+    "name": "windowstatusrearleft",
+    "version": "1.0",
+    "href": "/vehicles/WDB111111ZZZ22222/resources/windowstatusrearleft"
+  },
+  {
+    "name": "windowstatusrearright",
+    "version": "1.0",
+    "href": "/vehicles/WDB111111ZZZ22222/resources/windowstatusrearright"
+  }
+]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mercedesme/src/test/resources/status.json b/bundles/org.openhab.binding.mercedesme/src/test/resources/status.json
new file mode 100644 (file)
index 0000000..6093bb3
--- /dev/null
@@ -0,0 +1,98 @@
+[
+  {
+    "decklidstatus": {
+      "value": "false",
+      "timestamp": 1541080800000
+    }
+  },
+  {
+    "doorstatusfrontleft": {
+      "value": "false",
+      "timestamp": 1541080800000
+    }
+  },
+  {
+    "doorstatusfrontright": {
+      "value": "false",
+      "timestamp": 1541080800000
+    }
+  },
+  {
+    "doorstatusrearleft": {
+      "value": "false",
+      "timestamp": 1541080800000
+    }
+  },
+  {
+    "doorstatusrearright": {
+      "value": "false",
+      "timestamp": 1541080800000
+    }
+  },
+  {
+    "interiorLightsFront": {
+      "value": "false",
+      "timestamp": 1541080800000
+    }
+  },
+  {
+    "interiorLightsRear": {
+      "value": "false",
+      "timestamp": 1541080800000
+    }
+  },
+  {
+    "lightswitchposition": {
+      "value": "0",
+      "timestamp": 1541080800000
+    }
+  },
+  {
+    "readingLampFrontLeft": {
+      "value": "false",
+      "timestamp": 1541080800000
+    }
+  },
+  {
+    "readingLampFrontRight": {
+      "value": "false",
+      "timestamp": 1541080800000
+    }
+  },
+  {
+    "rooftopstatus": {
+      "value": "0",
+      "timestamp": 1541080800000
+    }
+  },
+  {
+    "sunroofstatus": {
+      "value": "0",
+      "timestamp": 1541080800000
+    }
+  },
+  {
+    "windowstatusfrontleft": {
+      "value": "0",
+      "timestamp": 1541080800000
+    }
+  },
+  {
+    "windowstatusfrontright": {
+      "value": "0",
+      "timestamp": 1541080800000
+    }
+  },
+  {
+    "windowstatusrearleft": {
+      "value": "0",
+      "timestamp": 1541080800000
+    }
+  },
+  {
+    "windowstatusrearright": {
+      "value": "0",
+      "timestamp": 1541080800000
+    }
+  }
+]
\ No newline at end of file
index 61df9d6e0ce5cdcdb4fb53354e888b2ba5339c38..6f924d28826a35662f31fd57ae6e83a5b6a49ad6 100644 (file)
     <module>org.openhab.binding.mcp23017</module>
     <module>org.openhab.binding.mecmeter</module>
     <module>org.openhab.binding.melcloud</module>
+    <module>org.openhab.binding.mercedesme</module>
     <module>org.openhab.binding.meteoalerte</module>
     <module>org.openhab.binding.meteoblue</module>
     <module>org.openhab.binding.meteostick</module>