<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>
--- /dev/null
+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
--- /dev/null
+# 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.
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+/**
+ * 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);
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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());
+ }
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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();
+ }
+}
--- /dev/null
+/**
+ * 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";
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+ }
+}
--- /dev/null
+/**
+ * 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());
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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();
+ }
+}
--- /dev/null
+/**
+ * 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 });
+ }
+}
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+# 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
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+/**
+ * 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");
+ }
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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");
+ }
+}
--- /dev/null
+[
+ {
+ "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
--- /dev/null
+[
+ {
+ "rangeelectric": {
+ "value": "325",
+ "timestamp": 1655401822000
+ }
+ },
+ {
+ "soc": {
+ "value": "78",
+ "timestamp": 1655399096000
+ }
+ }
+]
\ No newline at end of file
--- /dev/null
+[
+ {
+ "tanklevelpercent": {
+ "value": "90",
+ "timestamp": 1541080800000
+ }
+ },
+ {
+ "rangeliquid": {
+ "value": "1292",
+ "timestamp": 1541080800000
+ }
+ }
+]
\ No newline at end of file
--- /dev/null
+{
+ "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
--- /dev/null
+[
+ {
+ "wrong": {
+ "value": "4131",
+ "timestamp": 1655399236000
+ }
+ }
+]
\ No newline at end of file
--- /dev/null
+[
+ {
+ "odo": {
+ "value": "4131"
+ }
+ }
+]
\ No newline at end of file
--- /dev/null
+[
+ {
+ "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
--- /dev/null
+[
+ {
+ "odo": {
+ "value": "4131",
+ "timestamp": 1655399236000
+ }
+ }
+]
\ No newline at end of file
--- /dev/null
+[
+ {
+ "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
--- /dev/null
+[
+ {
+ "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
<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>