[mercedesme] Initial contribution (#13044)

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
This commit is contained in:
Bernd Weymann 2022-08-23 22:42:39 +02:00 committed by GitHub
parent 0a41c3f2f6
commit 704000eda9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 4074 additions and 0 deletions

View File

@ -906,6 +906,11 @@
<artifactId>org.openhab.binding.melcloud</artifactId> <artifactId>org.openhab.binding.melcloud</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.mercedesme</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.openhab.addons.bundles</groupId> <groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.meteoalerte</artifactId> <artifactId>org.openhab.binding.meteoalerte</artifactId>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,503 @@
# MercedesMe Binding
This binding provides similar access to your Mercedes Benz vehicle like the Smartphone App _Mercedes Me_.
For this you need a Mercedes developer account to get data from your vehicles.
Setup requires some, time so follow [the steps of bridge configuration](#bridge-configuration).
If you face some problems during setup or runtime please have a look into the [Troubleshooting section](#troubleshooting)
## Supported Things
| Type | ID | Description |
|-----------------|---------------|-------------------------------------------------|
| Bridge | `account` | Connect your Mercedes Me account |
| Thing | `combustion` | Conventional fuel vehicle |
| Thing | `hybrid` | Fuel vehicle with supporting electric engine |
| Thing | `bev` | Battery electric vehicle |
## Bridge Configuration
Bridge needs configuration in order to connect properly to your Mercedes Me Account.
### Pre-Conditions
- **each bridge shall have its own Mercedes Benz Client ID!**
Don't create several `account` bridges with the same client id! If this is not the case the tokens won't be stored properly and the authorization is jeopardized!
- **each bridge shall have its own port.**
It's absolutely necessary to assign a different port for each `account` bridge. If this is not the case the tokens won't be stored properly and the authorization is jeopardized!
### Bridge Setup
Perform the following steps to obtain the configuration data and perform the authorization flow.
1. Go to [Mercedes Developer Page](https://developer.mercedes-benz.com/). Login with your Mercedes Me credentials.
2. Create a project in the [console tab](https://developer.mercedes-benz.com/console)
- _Project Name:_ unique name e.g. **openHAB Mercedes Me binding** plus **Your bridge ID**
- _Purpose URL:_ use link towards [this binding description](https://www.openhab.org/addons/bindings/mercedesme/)
- _Business Purpose:_ e.g. **Private usage in openHAB Smarthome system**
3. After project is created subscribe [to these Mercedes Benz APIs](https://developer.mercedes-benz.com/products?vt=cars&vt=vans&vt=smart&p=BYOCAR) with _Add Products_ button
4. For all Products perform the same steps
- Select product
- Choose _Get For Free_
- Choose _BYOCAR_ (Build Your Own Car)
- Button _Confirm_
5. Select the following products
- Vehicle Status
- Vehicle Lock Status
- Pay as you drive insurance
- Electric Vehicle Status
- Fuel Status
6. Optional: Subscribe also to _Vehicle images_. Select the _Basic Trial_ version. The images will be stored so the API is used just a few times.
7. Press _Subscribe_ button. Your project should have [these product subscriptions](#mb-product-subscriptions)
8. Generate the [project credentials](#mb-credentials)
9. Open in new browser tab your openHAB page. Add a new Thing _Mercedes Me Account_
10. Copy paste _Client ID_ , _Client Secret_ and _API Key_ from the Mercedes tab into the openHAB configuration
11. Check if the registered Mercedes products _excluding Vehicle Images_ are matching exactly with the openHab configuration switches
12. Create Thing!
13. The fresh created [account has one property](#openhab-configuration) `callbackUrl`. Copy it and paste it in a new browser tab
14. A [simple HTML page is shown including a link towards the Authorization flow](#callback-page) - **don't click yet**. If page isn't shown please adapt IP and port in openHAB configuration with Advanced Options activated
15. The copied URL needs to be added in your [Mercedes project credentials](#mb-credentials) from 8
16. Now click onto the link from 14. You'll be asked one time if you [grant access](#mb-access-request) towards the API. Click OK and authorization is done!
Some supporting screenshots for the setup
### MB Credentials
<img src="./doc/MBDeveloper-Credentials.png" width="500" height="280"/>
### MB Product Subscriptions
<img src="./doc/MBDeveloper-Subscriptions.png" width="500" height="300"/>
### openHAB Configuration
<img src="./doc/MercedesMeConfiguration.png" width="400" height="500"/>
### MB Access Request
<img src="./doc/MBAccessRequest.png" width="500" height="220"/>
### Callback page
<img src="./doc/CallbackUrl_Page.png" width="500" height="350"/>
### Bridge Configuration Parameters
| Name | Type | Description | Default | Required | Advanced |
|-----------------|---------|---------------------------------------|-------------|----------|----------|
| clientId | text | Mercedes Benz Developer Client ID | N/A | yes | no |
| clientSecret | text | Mercedes Benz Developer Client Secret | N/A | yes | no |
| imageApiKey | text | Mercedes Benz Developer Image API Key | N/A | no | no |
| odoScope | boolean | PayAsYourDrive Insurance | true | yes | no |
| vehicleScope | boolean | Vehicle Status | true | yes | no |
| lockScope | boolean | Lock status of doors and trunk | true | yes | no |
| fuelScope | boolean | Fuel Status | true | yes | no |
| evScope | boolean | Electric Vehicle Status | true | yes | no |
| callbackIp | text | IP address of your openHAB server | auto detect | no | yes |
| callbackPort | integer | **Unique** port number | auto detect | no | yes |
The `callbackPort` needs to be unique for all created Mercedes Me account things. Otherwise token exchange will be corrupted.
Set the advanced options by yourself if you know your IP and Port, otherwise give auto detect a try.
## Thing Configuration
For vehicle images Mercedes Benz Developer offers only a trial version with limited calls.
Check in **beforehand** if your vehicle has some restrictions or even if it's supported at all.
Visit [Vehicle Image Details](https://developer.mercedes-benz.com/products/vehicle_images/details) in order to check your vehicle capabilities.
Visit [Image Settings](https://developer.mercedes-benz.com/products/vehicle_images/docs#_default_image_settings) to get more information about
For example the EQA doesn't provide `night` images with `background`.
If your configuration is set this way the API calls are wasted!
<img src="./doc/ImageRestrictions.png" width="800" height="36"/>
See also [image channel section](#image) for further advise.
| Name | Type | Description | Default | Required | Advanced |
|-----------------|---------|-----------------------------------------------------|---------|----------|----------|
| vin | text | Vehicle identification number | N/A | yes | no |
| refreshInterval | integer | Refresh interval in minutes | 5 | yes | no |
| background | boolean | Vehicle images provided with or without background | false | no | yes |
| night | boolean | Vehicle images in night conditions | false | no | yes |
| cropped | boolean | Vehicle images in 4:3 instead of 16:9 | false | no | yes |
| roofOpen | boolean | Vehicle images with open roof (only Cabriolet) | false | no | yes |
| format | text | Vehicle images format (webp or png) | webp | no | yes |
For all vehicles you're free to give the tank / battery capacity.
Giving these values in configuration the open fuel / charge capacities are reported in the [range](#range) channels.
| Name | Type | Description | Default | Required | Advanced | combustion | bev | hybrid |
|-----------------|---------|-----------------------------------------------------|---------|----------|----------|------------|-----|--------|
| batteryCapacity | decimal | Battery Capacity | N/A | no | no | | X | X |
| fuelCapacity | decimal | Fuel Capacity | N/A | no | no | X | | X |
## Channels
Channels are separated in groups:
| Channel Group ID | Description |
|----------------------------------|---------------------------------------------------|
| [range](#range) | Provides mileage, range and charge / fuel levels |
| [doors](#doors) | Details of all doors |
| [windows](#windows) | Current position of windows |
| [lights](#lights) | Interior lights and main light switch |
| [lock](#lock) | Overall lock state of vehicle |
| [location](#location) | Heading of the vehicle |
| [image](#image) | Images of your vehicle |
### Range
Group name: `range`
All channels `read-only`
| Channel | Type | Description | bev | hybrid | combustion |
|------------------|----------------------|------------------------------| ----|--------|------------|
| mileage | Number:Length | Total mileage | X | X | X |
| soc | Number:Dimensionless | Battery state of charge | X | X | |
| charged | Number:Energy | Charged Battery Energy | X | X | |
| uncharged | Number:Energy | Uncharged Battery Energy | X | X | |
| soc | Number:Dimensionless | Battery state of charge | X | X | |
| range-electric | Number:Length | Electric range | X | X | |
| radius-electric | Number:Length | Electric radius for map | X | X | |
| fuel-level | Number:Dimensionless | Fuel level in percent | | X | X |
| fuel-remain | Number:Volume | Reamaining Fuel | | X | X |
| fuel-open | Number:Volume | Open Fuel Capacity | | X | X |
| range-fuel | Number:Length | Fuel range | | X | X |
| radius-fuel | Number:Length | Fuel radius for map | | X | X |
| range-hybrid | Number:Length | Hybrid range | | X | |
| radius-hybrid | Number:Length | Hybrid radius for map | | X | |
| last-update | DateTime | Last range update | X | X | X |
Channels with `radius` are just giving a _guess_ which radius can be reached in a map display.
### Doors
Group name: `doors`
All channels `read-only`
| Channel | Type | Description |
|------------------|----------------------|------------------------------|
| driver-front | Contact | Driver door |
| driver-rear | Contact | Driver door reat |
| passenger-front | Contact | Passenger door |
| passenger-rear | Contact | Passenger door rear |
| deck-lid | Contact | Deck lid |
| sunroof | Number | Sun roof (only Cabriolet) |
| rooftop | Number | Roof top |
| last-update | DateTime | Last doors update |
Mapping table `sunroof`
| Number | Mapping |
|-----------------|---------------------|
| 0 | Closed |
| 1 | Open |
| 2 | Open Lifting |
| 3 | Running |
| 4 | Closing |
| 5 | Opening |
| 6 | Closing |
Mapping table `rootop`
| Number | Mapping |
|-----------------|---------------------|
| 0 | Unlocked |
| 1 | Open and locked |
| 2 | Closed and locked |
### Windows
Group name: `windows`
All channels `readonly`
| Channel | Type | Description |
|------------------|----------------------|------------------------------|
| driver-front | Number | Driver window |
| driver-rear | Number | Driver window rear |
| passenger-front | Number | Passenger window |
| passenger-rear | Number | Passenger window rear |
| last-update | DateTime | Last windows update |
Mapping table for all windows
| Number | Mapping |
|-----------------|---------------------|
| 0 | Intermediate |
| 1 | Open |
| 2 | Closed |
| 3 | Airing |
| 4 | Intermediate |
| 5 | Running |
### Lights
Group name: `lights`
All channels `read-only`
| Channel | Type | Description |
|------------------|----------------------|------------------------------|
| interior-front | Switch | Interior light front |
| interior-rear | Switch | Interior light rear |
| reading-left | Switch | Reading light left |
| reading-right | Switch | Reading light right |
| light-switch | Number | Main light switch |
| last-update | DateTime | Last lights update |
Mapping table `light-switch`
| Number | Mapping |
|-----------------|---------------------|
| 0 | Auto |
| 1 | Headlight |
| 2 | Sidelight Left |
| 3 | Sidelight Right |
| 4 | Parking Light |
### Lock
Group name: `lock`
All channels `read-only`
| Channel | Type | Description |
|------------------|----------------------|------------------------------|
| doors | Number | Lock status all doors |
| deck-lid | Switch | Deck lid lock |
| flap | Switch | Flap lock |
| last-update | DateTime | Last lock update |
Mapping table `doors`
| Number | Mapping |
|-----------------|---------------------|
| 0 | Unlocked |
| 1 | Locked Internal |
| 2 | Locked External |
| 3 | Unlocked Selective |
### Location
Group name: `location`
All channels `readonly`
| Channel | Type | Description |
|------------------|----------------------|------------------------------|
| heading | Number:Angle | Vehicle heading |
| last-update | DateTime | Last location update |
### Image
Provides exterior and interior images for your specific vehicle.
Group name: `image`
| Channel | Type | Description | Write |
|------------------|----------------------|------------------------------|-------|
| image-data | Raw | Vehicle image | |
| image-view | text | Vehicle image viewpoint | X |
| clear-cache | Switch | Remove all stored images | X |
**If** the `imageApiKey` in [Bridge Configuration Parameters](#bridge-configuration-parameters) is set the vehicle thing will try to get images.
Pay attention to the [Advanced Image Configuration Properties](#thing-configuration) before requesting new images.
Sending commands towards the `image-view` channel will change the image.
The `image-view` is providing options to select the available images for your specific vehicle.
Images are stored in `jsondb` so if you requested all images the Mercedes Benz Image API will not be called anymore which is good because you have a restricted amount of calls!
If you're not satisfied e.g. you want a background you need to
1. change the [Advanced Image Configuration Properties](#thing-configuration)
2. Switch `clear-cache` channel item to `ON` to clear all images
3. request them via `image-view`
### Image View Options
You can access the options either in a rule via `YOUR_IMAGE_VIEW_ITEM.getStateDescription().getOptions()` or in UI in widget configuration as _Action: Command options_ and as _Action Item: YOUR_IMAGE_VIEW_ITEM_
<img src="./doc/ImageView-CommandOptions.png" width="400" height="350"/>
## Troubleshooting
### Authorization fails
The configuration of openHAB account thing and the Mercedes Developer project need an extract match regarding
- MB project credentials vs. `clientId` `clientSecret` and `callbackUrl`
- MB project subscription of products vs. `scope`
If you follow the [bridge configuration steps](#bridge-configuration) both will match.
Otherwise you'll receive some error message when clicking the link after opening the `callbackUrl` in your browser
Most common errors:
- redirect URL doesn't match: Double check if `callbackUrl` is really saved correctly in your Mercedes Benz Developer project
- scope failure: the requested scope doesn't match with the subscribed products.
- Check [openHab configuration switches](#openhab-configuration)
- apply changes if necessary and don't forget to save
- after these steps refresh the `callbackUrl` in [your browser](#callback-page) to apply these changes
- try a new authorization clicking the link
### Receive no data
Especially after setting the frist Mercedes Benz Developer Project you'll receive no data.
It seems that the API isn't _filled_ yet.
**Pre-Condition**
- The Mercedes Me bridge is online = authorization is fine
- The Mercedes Me thing is online = API calls are fine
**Solution**
- Reduce `refreshInterval` to 1 minute
- Go to your vehicle, open doors and windows, turn on lights, drive a bit ...
- wait until values are providing the right states
### Images
Testing the whole image settings is hard due to the restricted call number towards the Image API.
My personal experience during limited testing
| Test |Tested | OK | Not OK | Comment |
|------------------|-------|-----|---------|---------------------------------------------------------|
| `format` webp | Yes | X | | |
| `format` png | Yes | | X | Internal Server Error 500 on Mercedes Server side |
| `format` jpeg | No | | | Not tested due to missing transparency in jpeg format |
| all options off | Yes | X | | |
| `background` | Yes | X | | |
| `night` | No | | | Not support by my vehicle |
| `roofOpen` | No | | | Not support by my vehicle |
| `cropped` | No | | | Not desired from my side |
## Storage
Data is stored in directory `%USER_DATA%/jsondb` for handling tokens and vehicle images.
* _StorageHandler.For.OAuthClientService.json_ - token is stored with key `clientId` which is provided by `account` [Brige Configuration Parameters](#bridge-configuration-parameters)
* _mercedesme_%VEHICLE_VIN%.json_ - images are stored per vehicle. File name contains `vin` configured by [vehicle Thing Configuration](#thing-configuration)
With this data the binding is able to operate without new authorization towards Mercedes each startup and reduces the restricted calls towards image API.
Also these files are properly stored in your [backup](https://community.openhab.org/t/docs-on-how-to-backup-openhab/100182) e.g. if you perform `openhab-cli backup`
## Full example
The example is based on a battery electric vehicle.
Exchange configuration parameters in the Things section
Bridge
* 4711 - your desired bridge id
* YOUR_CLIENT_ID - Client ID of the Mercedes Developer project
* YOUR_CLIENT_SECRET - Client Secret of the Mercedes Developer project
* YOUR_API_KEY - Image API Key of the Mercedes Developer project
* YOUR_OPENHAB_SERVER_IP - IP address of your openHAB server
* 8090 - a **unique** port number - each bridge in your openHAB installation needs to have different port number!
Thing
* eqa - your desired vehicle thing id
* VEHICLE_VIN - your Vehicle Identification Number
### Things file
```
Bridge mercedesme:account:4711 "MercedesMe John Doe" [ clientId="YOUR_CLIENT_ID", clientSecret="YOUR_CLIENT_SECRET", imageApiKey="YOUR_API_KEY", callbackIp="YOUR_OPENHAB_SERVER_IP", callbackPort=8092, odoScope=true, vehicleScope=true, lockScope=true, fuelScope=true, evScope=true] {
Thing bev eqa "Mercedes EQA" [ vin="VEHICLE_VIN", refreshInterval=5, background=false, night=false, cropped=false, roofOpen=false, format="webp"]
}
```
### Items file
```
Number:Length EQA_Mileage "Odometer [%d %unit%]" {channel="mercedesme:bev:4711:eqa:range#mileage" }
Number:Length EQA_Range "Range [%d %unit%]" {channel="mercedesme:bev:4711:eqa:range#range-electric"}
Number:Length EQA_RangeRadius "Range Radius [%d %unit%]" {channel="mercedesme:bev:4711:eqa:range#radius-electric"}
Number:Dimensionless EQA_BatterySoc "Battery Charge [%.1f %%]" {channel="mercedesme:bev:4711:eqa:range#soc"}
Contact EQA_DriverDoor "Driver Door [%s]" {channel="mercedesme:bev:4711:eqa:doors#driver-front" }
Contact EQA_DriverDoorRear "Driver Door Rear [%s]" {channel="mercedesme:bev:4711:eqa:doors#driver-rear" }
Contact EQA_PassengerDoor "Passenger Door [%s]" {channel="mercedesme:bev:4711:eqa:doors#passenger-front" }
Contact EQA_PassengerDoorRear "Passenger Door Rear [%s]" {channel="mercedesme:bev:4711:eqa:doors#passenger-rear" }
Number EQA_Trunk "Trunk [%s]" {channel="mercedesme:bev:4711:eqa:doors#deck-lid" }
Number EQA_Rooftop "Rooftop [%s]" {channel="mercedesme:bev:4711:eqa:doors#rooftop" }
Number EQA_Sunroof "Sunroof [%s]" {channel="mercedesme:bev:4711:eqa:doors#sunroof" }
Number EQA_DoorLock "Door Lock [%s]" {channel="mercedesme:bev:4711:eqa:lock#doors" }
Switch EQA_TrunkLock "Trunk Lock [%s]" {channel="mercedesme:bev:4711:eqa:lock#deck-lid" }
Switch EQA_FlapLock "Charge Flap Lock [%s]" {channel="mercedesme:bev:4711:eqa:lock#flap" }
Number EQA_DriverWindow "Driver Window [%s]" {channel="mercedesme:bev:4711:eqa:windows#driver-front" }
Number EQA_DriverWindowRear "Driver Window Rear [%s]" {channel="mercedesme:bev:4711:eqa:windows#driver-rear" }
Number EQA_PassengerWindow "Passenger Window [%s]" {channel="mercedesme:bev:4711:eqa:windows#passenger-front" }
Number EQA_PassengerWindowRear "Passenger Window Rear [%s]" {channel="mercedesme:bev:4711:eqa:windows#passenger-rear" }
Number:Angle EQA_Heading "Heading [%.1f %unit%]" {channel="mercedesme:bev:4711:eqa:location#heading" }
Image EQA_Image "Image" {channel="mercedesme:bev:4711:eqa:image#image-data" }
String EQA_ImageViewport "Image Viewport [%s]" {channel="mercedesme:bev:4711:eqa:image#image-view" }
Switch EQA_ClearCache "Clear Cache [%s]" {channel="mercedesme:bev:4711:eqa:image#clear-cache" }
Switch EQA_InteriorFront "Interior Front Light [%s]" {channel="mercedesme:bev:4711:eqa:lights#interior-front" }
Switch EQA_InteriorRear "Interior Rear Light [%s]" {channel="mercedesme:bev:4711:eqa:lights#interior-rear" }
Switch EQA_ReadingLeft "Reading Light Left [%s]" {channel="mercedesme:bev:4711:eqa:lights#reading-left" }
Switch EQA_ReadingRight "Reading Light Right [%s]" {channel="mercedesme:bev:4711:eqa:lights#reading-right" }
Number EQA_LightSwitch "Main Light Switch [%s]" {channel="mercedesme:bev:4711:eqa:lights#light-switch" }
```
### Sitemap
```
sitemap MB label="Mercedes Benz EQA" {
Frame label="EQA Image" {
Image item=EQA_Image
}
Frame label="Range" {
Text item=EQA_Mileage
Text item=EQA_Range
Text item=EQA_RangeRadius
Text item=EQA_BatterySoc
}
Frame label="Door Details" {
Text item=EQA_DriverDoor
Text item=EQA_DriverDoorRear
Text item=EQA_PassengerDoor
Text item=EQA_PassengerDoorRear
Text item=EQA_Trunk
Text item=EQA_Rooftop
Text item=EQA_Sunroof
Text item=EQA_DoorLock
Text item=EQA_TrunkLock
Text item=EQA_FlapLock
}
Frame label="Windows" {
Text item=EQA_DriverWindow
Text item=EQA_DriverWindowRear
Text item=EQA_PassengerWindow
Text item=EQA_PassengerWindowRear
}
Frame label="Location" {
Text item=EQA_Heading
}
Frame label="Lights" {
Text item=EQA_InteriorFront
Text item=EQA_InteriorRear
Text item=EQA_ReadingLeft
Text item=EQA_ReadingRight
Text item=EQA_LightSwitch
}
Frame label="Image Properties" {
Selection item=EQA_ImageViewport
Switch item=EQA_ClearCache
}
}
```
## Mercedes Benz Developer
Visit [Mercedes Benz Developer](https://developer.mercedes-benz.com/) to gain more deep information.

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.4.0-SNAPSHOT</version>
</parent>
<dependencies>
<!-- version needs to match with other projects like org.openhab.io.openhabcloud.pom.xml -->
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20180813</version>
<scope>compile</scope>
</dependency>
</dependencies>
<artifactId>org.openhab.binding.mercedesme</artifactId>
<name>openHAB Add-ons :: Bundles :: MercedesMe Binding</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.mercedesme-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-mercedesme" description="MercedesMe Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.mercedesme/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,98 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mercedesme.internal;
import javax.measure.Unit;
import javax.measure.quantity.Length;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.unit.MetricPrefix;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link Constants} class defines common constants, which are
* used across the whole binding.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class Constants {
public static final String BINDING_ID = "mercedesme";
public static final String COMBUSTION = "combustion";
public static final String HYBRID = "hybrid";
public static final String BEV = "bev";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account");
public static final ThingTypeUID THING_TYPE_COMB = new ThingTypeUID(BINDING_ID, COMBUSTION);
public static final ThingTypeUID THING_TYPE_HYBRID = new ThingTypeUID(BINDING_ID, HYBRID);
public static final ThingTypeUID THING_TYPE_BEV = new ThingTypeUID(BINDING_ID, BEV);
public static final String GROUP_RANGE = "range";
public static final String GROUP_DOORS = "doors";
public static final String GROUP_WINDOWS = "windows";
public static final String GROUP_LOCK = "lock";
public static final String GROUP_LIGHTS = "lights";
public static final String GROUP_LOCATION = "location";
public static final String GROUP_IMAGE = "image";
public static final String MB_AUTH_URL = "https://id.mercedes-benz.com/as/authorization.oauth2";
public static final String MB_TOKEN_URL = "https://id.mercedes-benz.com/as/token.oauth2";
public static final String CALLBACK_ENDPOINT = "/mb-callback";
public static final String OAUTH_CLIENT_NAME = "#byocar";
// https://developer.mercedes-benz.com/products/electric_vehicle_status/docs
public static final String SCOPE_EV = "mb:vehicle:mbdata:evstatus";
// https://developer.mercedes-benz.com/products/fuel_status/docs
public static final String SCOPE_FUEL = "mb:vehicle:mbdata:fuelstatus";
// https://developer.mercedes-benz.com/products/pay_as_you_drive_insurance/docs
public static final String SCOPE_ODO = "mb:vehicle:mbdata:payasyoudrive";
// https://developer.mercedes-benz.com/products/vehicle_lock_status/docs
public static final String SCOPE_LOCK = "mb:vehicle:mbdata:vehiclelock";
// https://developer.mercedes-benz.com/products/vehicle_status/docs
public static final String SCOPE_STATUS = "mb:vehicle:mbdata:vehiclestatus";
public static final String SCOPE_OFFLINE = "offline_access";
public static final String BASE_URL = "https://api.mercedes-benz.com/vehicledata/v2";
public static final String ODO_URL = BASE_URL + "/vehicles/%s/containers/payasyoudrive";
public static final String STATUS_URL = BASE_URL + "/vehicles/%s/containers/vehiclestatus";
public static final String LOCK_URL = BASE_URL + "/vehicles/%s/containers/vehiclelockstatus";
public static final String FUEL_URL = BASE_URL + "/vehicles/%s/containers/fuelstatus";
public static final String EV_URL = BASE_URL + "/vehicles/%s/containers/electricvehicle";
// https://developer.mercedes-benz.com/content-page/api_migration_guide
public static final String IMAGE_BASE_URL = "https://api.mercedes-benz.com/vehicle_images/v2";
public static final String IMAGE_EXTERIOR_RESOURCE_URL = IMAGE_BASE_URL + "/vehicles/%s";
public static final String STATUS_TEXT_PREFIX = "@text/mercedesme.";
public static final String STATUS_AUTH_NEEDED = ".status.authorization-needed";
public static final String STATUS_IP_MISSING = ".status.ip-missing";
public static final String STATUS_PORT_MISSING = ".status.port-missing";
public static final String STATUS_CLIENT_ID_MISSING = ".status.client-id-missing";
public static final String STATUS_CLIENT_SECRET_MISSING = ".status.client-secret-missing";
public static final String STATUS_SERVER_RESTART = ".status.server-restart";
public static final String STATUS_BRIDGE_MISSING = ".status.bridge-missing";
public static final String STATUS_BRIDGE_ATHORIZATION = ".status.bridge-authoriziation";
public static final String SPACE = " ";
public static final String EMPTY = "";
public static final String COLON = ":";
public static final String NOT_SET = "not set";
public static final String CODE = "code";
public static final String MIME_PREFIX = "image/";
public static final Unit<Length> KILOMETRE_UNIT = MetricPrefix.KILO(SIUnits.METRE);
}

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mercedesme.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.thing.binding.BaseDynamicCommandDescriptionProvider;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.link.ItemChannelLinkRegistry;
import org.openhab.core.thing.type.DynamicCommandDescriptionProvider;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* Dynamic provider of command options while leaving other state description fields as original.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
@Component(service = { DynamicCommandDescriptionProvider.class, MercedesMeCommandOptionProvider.class })
public class MercedesMeCommandOptionProvider extends BaseDynamicCommandDescriptionProvider {
@Activate
public MercedesMeCommandOptionProvider(final @Reference EventPublisher eventPublisher, //
final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry, //
final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.eventPublisher = eventPublisher;
this.itemChannelLinkRegistry = itemChannelLinkRegistry;
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
}

View File

@ -0,0 +1,105 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mercedesme.internal;
import static org.openhab.binding.mercedesme.internal.Constants.*;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.WWWAuthenticationProtocolHandler;
import org.openhab.binding.mercedesme.internal.handler.AccountHandler;
import org.openhab.binding.mercedesme.internal.handler.VehicleHandler;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.storage.StorageService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MercedesMeHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.mercedesme", service = ThingHandlerFactory.class)
public class MercedesMeHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_BEV, THING_TYPE_COMB,
THING_TYPE_HYBRID, THING_TYPE_ACCOUNT);
private final Logger logger = LoggerFactory.getLogger(MercedesMeHandlerFactory.class);
private final OAuthFactory oAuthFactory;
private final HttpClient httpClient;
private final MercedesMeCommandOptionProvider mmcop;
private final MercedesMeStateOptionProvider mmsop;
private final StorageService storageService;
private final TimeZoneProvider timeZoneProvider;
@Activate
public MercedesMeHandlerFactory(@Reference OAuthFactory oAuthFactory, @Reference HttpClientFactory hcf,
@Reference StorageService storageService, final @Reference MercedesMeCommandOptionProvider cop,
final @Reference MercedesMeStateOptionProvider sop, final @Reference TimeZoneProvider tzp) {
this.oAuthFactory = oAuthFactory;
this.storageService = storageService;
mmcop = cop;
mmsop = sop;
timeZoneProvider = tzp;
httpClient = hcf.createHttpClient(Constants.BINDING_ID);
// https://github.com/jetty-project/jetty-reactive-httpclient/issues/33
httpClient.getProtocolHandlers().remove(WWWAuthenticationProtocolHandler.NAME);
try {
httpClient.start();
} catch (Exception e) {
logger.warn("HTTP client not started: {} - no web access possible!", e.getLocalizedMessage());
}
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_ACCOUNT.equals(thingTypeUID)) {
return new AccountHandler((Bridge) thing, httpClient, oAuthFactory);
}
return new VehicleHandler(thing, httpClient, thingTypeUID.getId(), storageService, mmcop, mmsop,
timeZoneProvider);
}
@Override
protected void deactivate(ComponentContext componentContext) {
super.deactivate(componentContext);
try {
httpClient.stop();
} catch (Exception e) {
logger.debug("HTTP client not stopped: {}", e.getLocalizedMessage());
}
}
}

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mercedesme.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.link.ItemChannelLinkRegistry;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* Dynamic provider of state options while leaving other state description fields as original.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
@Component(service = { DynamicStateDescriptionProvider.class, MercedesMeStateOptionProvider.class })
public class MercedesMeStateOptionProvider extends BaseDynamicStateDescriptionProvider {
@Activate
public MercedesMeStateOptionProvider(final @Reference EventPublisher eventPublisher, //
final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry, //
final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.eventPublisher = eventPublisher;
this.itemChannelLinkRegistry = itemChannelLinkRegistry;
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
}

View File

@ -0,0 +1,67 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mercedesme.internal.config;
import static org.openhab.binding.mercedesme.internal.Constants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link AccountConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class AccountConfiguration {
public String clientId = NOT_SET;
public String clientSecret = NOT_SET;
public String imageApiKey = NOT_SET;
// Advanced Parameters
public String callbackIP = NOT_SET;
public int callbackPort = -1;
public boolean odoScope = true;
public boolean vehicleScope = true;
public boolean lockScope = true;
public boolean fuelScope = true;
public boolean evScope = true;
// https://developer.mercedes-benz.com/products/electric_vehicle_status/docs#_required_scopes
public String getScope() {
StringBuffer sb = new StringBuffer();
sb.append(SCOPE_OFFLINE);
if (odoScope) {
sb.append(SPACE).append(SCOPE_ODO);
}
if (vehicleScope) {
sb.append(SPACE).append(SCOPE_STATUS);
}
if (lockScope) {
sb.append(SPACE).append(SCOPE_LOCK);
}
if (fuelScope) {
sb.append(SPACE).append(SCOPE_FUEL);
}
if (evScope) {
sb.append(SPACE).append(SCOPE_EV);
}
return sb.toString();
}
@Override
public String toString() {
return "ID " + clientId + ", Secret " + clientSecret + ", IP " + callbackIP + ", Port " + callbackPort
+ ", scope " + getScope();
}
}

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mercedesme.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mercedesme.internal.Constants;
/**
* The {@link VehicleConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class VehicleConfiguration {
public String vin = Constants.NOT_SET;
public int refreshInterval = 5;
public float batteryCapacity = -1;
public float fuelCapacity = -1;
// Advanced
public boolean background = false;
public boolean night = false;
public boolean cropped = false;
public boolean roofOpen = false;
public String format = "webp";
}

View File

@ -0,0 +1,165 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mercedesme.internal.handler;
import java.net.SocketException;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.mercedesme.internal.Constants;
import org.openhab.binding.mercedesme.internal.config.AccountConfiguration;
import org.openhab.binding.mercedesme.internal.server.CallbackServer;
import org.openhab.binding.mercedesme.internal.server.Utils;
import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link AccountHandler} takes care of the valid authorization for the user account
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class AccountHandler extends BaseBridgeHandler implements AccessTokenRefreshListener {
private final Logger logger = LoggerFactory.getLogger(AccountHandler.class);
private final OAuthFactory oAuthFactory;
private final HttpClient httpClient;
private Optional<CallbackServer> server = Optional.empty();
Optional<AccountConfiguration> config = Optional.empty();
public AccountHandler(Bridge bridge, HttpClient hc, OAuthFactory oaf) {
super(bridge);
httpClient = hc;
oAuthFactory = oaf;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// no commands available
}
@Override
public void initialize() {
config = Optional.of(getConfigAs(AccountConfiguration.class));
autodetectCallback();
String configValidReason = configValid();
if (!configValidReason.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configValidReason);
} else {
String callbackUrl = Utils.getCallbackAddress(config.get().callbackIP, config.get().callbackPort);
thing.setProperty("callbackUrl", callbackUrl);
server = Optional.of(new CallbackServer(this, httpClient, oAuthFactory, config.get(), callbackUrl));
if (!server.get().start()) {
String textKey = Constants.STATUS_TEXT_PREFIX + thing.getThingTypeUID().getId()
+ Constants.STATUS_SERVER_RESTART;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, textKey);
} else {
// get fresh token
this.getToken();
}
}
}
private void autodetectCallback() {
// if Callback IP and Callback Port are not set => autodetect these values
config = Optional.of(getConfigAs(AccountConfiguration.class));
Configuration updateConfig = super.editConfiguration();
if (!updateConfig.containsKey("callbackPort")) {
updateConfig.put("callbackPort", Utils.getFreePort());
} else {
Utils.addPort(config.get().callbackPort);
}
if (!updateConfig.containsKey("callbackIP")) {
String ip;
try {
ip = Utils.getCallbackIP();
updateConfig.put("callbackIP", ip);
} catch (SocketException e) {
logger.info("Cannot detect IP address {}", e.getMessage());
}
}
super.updateConfiguration(updateConfig);
// get new config after update
config = Optional.of(getConfigAs(AccountConfiguration.class));
}
private String configValid() {
config = Optional.of(getConfigAs(AccountConfiguration.class));
String textKey = Constants.STATUS_TEXT_PREFIX + thing.getThingTypeUID().getId();
if (config.get().callbackIP.equals(Constants.NOT_SET)) {
return textKey + Constants.STATUS_IP_MISSING;
} else if (config.get().callbackPort == -1) {
return textKey + Constants.STATUS_PORT_MISSING;
} else if (config.get().clientId.equals(Constants.NOT_SET)) {
return textKey + Constants.STATUS_CLIENT_ID_MISSING;
} else if (config.get().clientSecret.equals(Constants.NOT_SET)) {
return textKey + Constants.STATUS_CLIENT_SECRET_MISSING;
} else {
return Constants.EMPTY;
}
}
@Override
public void dispose() {
if (!server.isEmpty()) {
server.get().stop();
Utils.removePort(config.get().callbackPort);
}
}
/**
* https://next.openhab.org/javadoc/latest/org/openhab/core/auth/client/oauth2/package-summary.html
*/
@Override
public void onAccessTokenResponse(AccessTokenResponse tokenResponse) {
if (!tokenResponse.getAccessToken().isEmpty()) {
// token not empty - fine
updateStatus(ThingStatus.ONLINE);
} else if (server.isEmpty()) {
// server not running - fix first
String textKey = Constants.STATUS_TEXT_PREFIX + thing.getThingTypeUID().getId()
+ Constants.STATUS_SERVER_RESTART;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, textKey);
} else {
// all failed - start manual authorization
String textKey = Constants.STATUS_TEXT_PREFIX + thing.getThingTypeUID().getId()
+ Constants.STATUS_AUTH_NEEDED;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
textKey + " [\"" + thing.getProperties().get("callbackUrl") + "\"]");
}
}
public String getToken() {
return server.get().getToken();
}
public String getImageApiKey() {
return config.get().imageApiKey;
}
@Override
public String toString() {
return Integer.toString(config.get().callbackPort);
}
}

View File

@ -0,0 +1,579 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mercedesme.internal.handler;
import static org.openhab.binding.mercedesme.internal.Constants.*;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.measure.quantity.Length;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.UrlEncoded;
import org.json.JSONArray;
import org.json.JSONObject;
import org.openhab.binding.mercedesme.internal.Constants;
import org.openhab.binding.mercedesme.internal.MercedesMeCommandOptionProvider;
import org.openhab.binding.mercedesme.internal.MercedesMeStateOptionProvider;
import org.openhab.binding.mercedesme.internal.config.VehicleConfiguration;
import org.openhab.binding.mercedesme.internal.utils.ChannelStateMap;
import org.openhab.binding.mercedesme.internal.utils.Mapper;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.RawType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.storage.Storage;
import org.openhab.core.storage.StorageService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.CommandOption;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.StateOption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link VehicleHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class VehicleHandler extends BaseThingHandler {
private static final String EXT_IMG_RES = "ExtImageResources_";
private static final String INITIALIZE_COMMAND = "Initialze";
private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
private final Map<String, Long> timeHash = new HashMap<String, Long>();
private final MercedesMeCommandOptionProvider mmcop;
private final MercedesMeStateOptionProvider mmsop;
private final TimeZoneProvider timeZoneProvider;
private final StorageService storageService;
private final HttpClient httpClient;
private final String uid;
private Optional<ScheduledFuture<?>> refreshJob = Optional.empty();
private Optional<AccountHandler> accountHandler = Optional.empty();
private Optional<QuantityType<?>> rangeElectric = Optional.empty();
private Optional<Storage<String>> imageStorage = Optional.empty();
private Optional<VehicleConfiguration> config = Optional.empty();
private Optional<QuantityType<?>> rangeFuel = Optional.empty();
private Instant nextRefresh;
private boolean online = false;
public VehicleHandler(Thing thing, HttpClient hc, String uid, StorageService storageService,
MercedesMeCommandOptionProvider mmcop, MercedesMeStateOptionProvider mmsop, TimeZoneProvider tzp) {
super(thing);
httpClient = hc;
this.uid = uid;
this.mmcop = mmcop;
this.mmsop = mmsop;
timeZoneProvider = tzp;
this.storageService = storageService;
nextRefresh = Instant.now();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.trace("Received {} {} {}", channelUID.getAsString(), command.toFullString(), channelUID.getId());
if (command instanceof RefreshType) {
/**
* Refresh requested e.g. after adding new item
* Adding several items will frequently raise RefreshType command. Calling API each time shall be avoided
* API update is performed after 5 seconds for all items which should be sufficient for a frequent update
*/
if (Instant.now().isAfter(nextRefresh)) {
nextRefresh = Instant.now().plus(Duration.ofSeconds(5));
logger.trace("Refresh granted - next at {}", nextRefresh);
scheduler.schedule(this::getData, 5, TimeUnit.SECONDS);
}
} else if ("image-view".equals(channelUID.getIdWithoutGroup())) {
if (imageStorage.isPresent()) {
if (INITIALIZE_COMMAND.equals(command.toFullString())) {
getImageResources();
}
String key = command.toFullString() + "_" + config.get().vin;
String encodedImage = EMPTY;
if (imageStorage.get().containsKey(key)) {
encodedImage = imageStorage.get().get(key);
logger.trace("Image {} found in storage", key);
} else {
logger.trace("Request Image {} ", key);
encodedImage = getImage(command.toFullString());
if (!encodedImage.isEmpty()) {
imageStorage.get().put(key, encodedImage);
}
}
if (encodedImage != null && !encodedImage.isEmpty()) {
RawType image = new RawType(Base64.getDecoder().decode(encodedImage),
MIME_PREFIX + config.get().format);
updateState(new ChannelUID(thing.getUID(), GROUP_IMAGE, "image-data"), image);
} else {
logger.debug("Image {} is empty", key);
}
}
} else if (channelUID.getIdWithoutGroup().equals("clear-cache") && command.equals(OnOffType.ON)) {
List<String> removals = new ArrayList<String>();
imageStorage.get().getKeys().forEach(entry -> {
if (entry.contains("_" + config.get().vin)) {
removals.add(entry);
}
});
removals.forEach(entry -> {
imageStorage.get().remove(entry);
});
updateState(new ChannelUID(thing.getUID(), GROUP_IMAGE, "clear-cache"), OnOffType.OFF);
getImageResources();
}
}
@Override
public void initialize() {
config = Optional.of(getConfigAs(VehicleConfiguration.class));
Bridge bridge = getBridge();
if (bridge != null) {
updateStatus(ThingStatus.UNKNOWN);
BridgeHandler handler = bridge.getHandler();
if (handler != null) {
accountHandler = Optional.of((AccountHandler) handler);
startSchedule(config.get().refreshInterval);
if (!config.get().vin.equals(NOT_SET)) {
imageStorage = Optional.of(storageService.getStorage(BINDING_ID + "_" + config.get().vin));
if (!imageStorage.get().containsKey(EXT_IMG_RES + config.get().vin)) {
getImageResources();
}
setImageOtions();
}
updateState(new ChannelUID(thing.getUID(), GROUP_IMAGE, "clear-cache"), OnOffType.OFF);
} else {
throw new IllegalStateException("BridgeHandler is null");
}
} else {
String textKey = Constants.STATUS_TEXT_PREFIX + "vehicle" + Constants.STATUS_BRIDGE_MISSING;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, textKey);
}
}
private void startSchedule(int interval) {
refreshJob.ifPresentOrElse(job -> {
if (job.isCancelled()) {
refreshJob = Optional
.of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES));
} // else - scheduler is already running!
}, () -> {
refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES));
});
}
@Override
public void dispose() {
refreshJob.ifPresent(job -> job.cancel(true));
}
public void getData() {
if (accountHandler.isEmpty()) {
logger.warn("AccountHandler not set");
return;
}
String token = accountHandler.get().getToken();
if (token.isEmpty()) {
String textKey = Constants.STATUS_TEXT_PREFIX + "vehicle" + Constants.STATUS_BRIDGE_ATHORIZATION;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, textKey);
return;
} else if (!online) { // only update if thing isn't already ONLINE
updateStatus(ThingStatus.ONLINE);
}
// Mileage for all cars
String odoUrl = String.format(ODO_URL, config.get().vin);
if (accountConfigAvailable()) {
if (accountHandler.get().config.get().odoScope) {
call(odoUrl);
} else {
logger.trace("{} Odo scope not activated", this.getThing().getLabel());
}
} else {
logger.trace("{} Account not properly configured", this.getThing().getLabel());
}
// Electric status for hybrid and electric
if (uid.equals(BEV) || uid.equals(HYBRID)) {
String evUrl = String.format(EV_URL, config.get().vin);
if (accountConfigAvailable()) {
if (accountHandler.get().config.get().evScope) {
call(evUrl);
} else {
logger.trace("{} Electric Status scope not activated", this.getThing().getLabel());
}
} else {
logger.trace("{} Account not properly configured", this.getThing().getLabel());
}
}
// Fuel for hybrid and combustion
if (uid.equals(COMBUSTION) || uid.equals(HYBRID)) {
String fuelUrl = String.format(FUEL_URL, config.get().vin);
if (accountConfigAvailable()) {
if (accountHandler.get().config.get().fuelScope) {
call(fuelUrl);
} else {
logger.trace("{} Fuel scope not activated", this.getThing().getLabel());
}
} else {
logger.trace("{} Account not properly configured", this.getThing().getLabel());
}
}
// Status and Lock for all
String statusUrl = String.format(STATUS_URL, config.get().vin);
if (accountConfigAvailable()) {
if (accountHandler.get().config.get().vehicleScope) {
call(statusUrl);
} else {
logger.trace("{} Vehicle Status scope not activated", this.getThing().getLabel());
}
} else {
logger.trace("{} Account not properly configured", this.getThing().getLabel());
}
String lockUrl = String.format(LOCK_URL, config.get().vin);
if (accountConfigAvailable()) {
if (accountHandler.get().config.get().lockScope) {
call(lockUrl);
} else {
logger.trace("{} Lock scope not activated", this.getThing().getLabel());
}
} else {
logger.trace("{} Account not properly configured", this.getThing().getLabel());
}
// Range radius for all types
updateRadius();
}
private boolean accountConfigAvailable() {
if (accountHandler.isPresent()) {
if (accountHandler.get().config.isPresent()) {
return true;
}
}
return false;
}
private void getImageResources() {
if (accountHandler.get().getImageApiKey().equals(NOT_SET)) {
logger.debug("Image API key not set");
return;
}
// add config parameters
MultiMap<String> parameterMap = new MultiMap<String>();
parameterMap.add("background", Boolean.toString(config.get().background));
parameterMap.add("night", Boolean.toString(config.get().night));
parameterMap.add("cropped", Boolean.toString(config.get().cropped));
parameterMap.add("roofOpen", Boolean.toString(config.get().roofOpen));
parameterMap.add("fileFormat", config.get().format);
String params = UrlEncoded.encode(parameterMap, StandardCharsets.UTF_8, false);
String url = String.format(IMAGE_EXTERIOR_RESOURCE_URL, config.get().vin) + "?" + params;
logger.debug("Get Image resources {} {} ", accountHandler.get().getImageApiKey(), url);
Request req = httpClient.newRequest(url);
req.header("x-api-key", accountHandler.get().getImageApiKey());
req.header(HttpHeader.ACCEPT, "application/json");
try {
ContentResponse cr = req.send();
if (cr.getStatus() == 200) {
imageStorage.get().put(EXT_IMG_RES + config.get().vin, cr.getContentAsString());
setImageOtions();
} else {
logger.debug("Failed to get image resources {} {}", cr.getStatus(), cr.getContentAsString());
}
} catch (InterruptedException | TimeoutException | ExecutionException e) {
logger.debug("Error getting image resources {}", e.getMessage());
}
}
private void setImageOtions() {
List<String> entries = new ArrayList<String>();
if (imageStorage.get().containsKey(EXT_IMG_RES + config.get().vin)) {
String resources = imageStorage.get().get(EXT_IMG_RES + config.get().vin);
JSONObject jo = new JSONObject(resources);
jo.keySet().forEach(entry -> {
entries.add(entry);
});
}
Collections.sort(entries);
List<CommandOption> commandOptions = new ArrayList<CommandOption>();
List<StateOption> stateOptions = new ArrayList<StateOption>();
entries.forEach(entry -> {
CommandOption co = new CommandOption(entry, null);
commandOptions.add(co);
StateOption so = new StateOption(entry, null);
stateOptions.add(so);
});
if (commandOptions.isEmpty()) {
commandOptions.add(new CommandOption("Initilaze", null));
stateOptions.add(new StateOption("Initilaze", null));
}
ChannelUID cuid = new ChannelUID(thing.getUID(), GROUP_IMAGE, "image-view");
mmcop.setCommandOptions(cuid, commandOptions);
mmsop.setStateOptions(cuid, stateOptions);
}
private String getImage(String key) {
if (accountHandler.get().getImageApiKey().equals(NOT_SET)) {
logger.debug("Image API key not set");
return EMPTY;
}
String imageId = EMPTY;
if (imageStorage.get().containsKey(EXT_IMG_RES + config.get().vin)) {
String resources = imageStorage.get().get(EXT_IMG_RES + config.get().vin);
JSONObject jo = new JSONObject(resources);
if (jo.has(key)) {
imageId = jo.getString(key);
}
} else {
getImageResources();
return EMPTY;
}
String url = IMAGE_BASE_URL + "/images/" + imageId;
Request req = httpClient.newRequest(url);
req.header("x-api-key", accountHandler.get().getImageApiKey());
req.header(HttpHeader.ACCEPT, "*/*");
ContentResponse cr;
try {
cr = req.send();
byte[] response = cr.getContent();
return Base64.getEncoder().encodeToString(response);
} catch (InterruptedException | TimeoutException | ExecutionException e) {
logger.warn("Get Image {} error {}", url, e.getMessage());
}
return EMPTY;
}
private void call(String url) {
String requestUrl = String.format(url, config.get().vin);
// Calculate endpoint for debugging
String[] endpoint = requestUrl.split("/");
String finalEndpoint = endpoint[endpoint.length - 1];
// debug prefix contains Thing label and call endpoint for propper debugging
String debugPrefix = this.getThing().getLabel() + Constants.COLON + finalEndpoint;
Request req = httpClient.newRequest(requestUrl);
req.header(HttpHeader.AUTHORIZATION, "Bearer " + accountHandler.get().getToken());
try {
ContentResponse cr = req.send();
logger.trace("{} Response {} {}", debugPrefix, cr.getStatus(), cr.getContentAsString());
if (cr.getStatus() == 200) {
distributeContent(cr.getContentAsString().trim());
}
} catch (InterruptedException | TimeoutException | ExecutionException e) {
logger.info("{} Error getting data {}", debugPrefix, e.getMessage());
fallbackCall(requestUrl);
}
}
/**
* Fallback solution with Java11 classes
* Performs try with Java11 HttpClient - https://zetcode.com/java/getpostrequest/ to identify Community problem
* https://community.openhab.org/t/mercedes-me-binding/136852/21
*
* @param requestUrl
*/
private void fallbackCall(String requestUrl) {
// Calculate endpoint for debugging
String[] endpoint = requestUrl.split("/");
String finalEndpoint = endpoint[endpoint.length - 1];
// debug prefix contains Thing label and call endpoint for propper debugging
String debugPrefix = this.getThing().getLabel() + Constants.COLON + finalEndpoint;
java.net.http.HttpClient client = java.net.http.HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(requestUrl))
.header(HttpHeader.AUTHORIZATION.toString(), "Bearer " + accountHandler.get().getToken()).GET().build();
try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
logger.debug("{} Fallback Response {} {}", debugPrefix, response.statusCode(), response.body());
if (response.statusCode() == 200) {
distributeContent(response.body().trim());
}
} catch (IOException | InterruptedException e) {
logger.warn("{} Error getting data via fallback {}", debugPrefix, e.getMessage());
}
}
private void distributeContent(String json) {
if (json.startsWith("[") && json.endsWith("]")) {
JSONArray ja = new JSONArray(json);
for (Iterator<Object> iterator = ja.iterator(); iterator.hasNext();) {
JSONObject jo = (JSONObject) iterator.next();
ChannelStateMap csm = Mapper.getChannelStateMap(jo);
if (csm.isValid()) {
updateChannel(csm);
/**
* handle some specific channels
*/
// store ChannelMap for range radius calculation
String channel = csm.getChannel();
if ("range-electric".equals(channel)) {
rangeElectric = Optional.of((QuantityType<?>) csm.getState());
} else if ("range-fuel".equals(channel)) {
rangeFuel = Optional.of((QuantityType<?>) csm.getState());
} else if ("soc".equals(channel)) {
if (config.get().batteryCapacity > 0) {
float socValue = ((QuantityType<?>) csm.getState()).floatValue();
float batteryCapacity = config.get().batteryCapacity;
float chargedValue = Math.round(socValue * 1000 * batteryCapacity / 1000) / (float) 100;
ChannelStateMap charged = new ChannelStateMap("charged", GROUP_RANGE,
QuantityType.valueOf(chargedValue, Units.KILOWATT_HOUR), csm.getTimestamp());
updateChannel(charged);
float unchargedValue = Math.round((100 - socValue) * 1000 * batteryCapacity / 1000)
/ (float) 100;
ChannelStateMap uncharged = new ChannelStateMap("uncharged", GROUP_RANGE,
QuantityType.valueOf(unchargedValue, Units.KILOWATT_HOUR), csm.getTimestamp());
updateChannel(uncharged);
} else {
logger.debug("No battery capacity given");
}
} else if ("fuel-level".equals(channel)) {
if (config.get().fuelCapacity > 0) {
float fuelLevelValue = ((QuantityType<?>) csm.getState()).floatValue();
float fuelCapacity = config.get().fuelCapacity;
float litersInTank = Math.round(fuelLevelValue * 1000 * fuelCapacity / 1000) / (float) 100;
ChannelStateMap tankFilled = new ChannelStateMap("tank-remain", GROUP_RANGE,
QuantityType.valueOf(litersInTank, Units.LITRE), csm.getTimestamp());
updateChannel(tankFilled);
float litersFree = Math.round((100 - fuelLevelValue) * 1000 * fuelCapacity / 1000)
/ (float) 100;
ChannelStateMap tankOpen = new ChannelStateMap("tank-open", GROUP_RANGE,
QuantityType.valueOf(litersFree, Units.LITRE), csm.getTimestamp());
updateChannel(tankOpen);
} else {
logger.debug("No fuel capacity given");
}
}
} else {
logger.warn("Unable to deliver state for {}", jo);
}
}
} else {
logger.debug("JSON Array expected but received {}", json);
}
}
private void updateRadius() {
if (rangeElectric.isPresent()) {
// update electric radius
ChannelStateMap radiusElectric = new ChannelStateMap("radius-electric", GROUP_RANGE,
guessRangeRadius(rangeElectric.get()), 0);
updateChannel(radiusElectric);
if (rangeFuel.isPresent()) {
// update fuel & hybrid radius
ChannelStateMap radiusFuel = new ChannelStateMap("radius-fuel", GROUP_RANGE,
guessRangeRadius(rangeFuel.get()), 0);
updateChannel(radiusFuel);
int hybridKm = rangeElectric.get().intValue() + rangeFuel.get().intValue();
QuantityType<Length> hybridRangeState = QuantityType.valueOf(hybridKm, KILOMETRE_UNIT);
ChannelStateMap rangeHybrid = new ChannelStateMap("range-hybrid", GROUP_RANGE, hybridRangeState, 0);
updateChannel(rangeHybrid);
ChannelStateMap radiusHybrid = new ChannelStateMap("radius-hybrid", GROUP_RANGE,
guessRangeRadius(hybridRangeState), 0);
updateChannel(radiusHybrid);
}
} else if (rangeFuel.isPresent()) {
// update fuel & hybrid radius
ChannelStateMap radiusFuel = new ChannelStateMap("radius-fuel", GROUP_RANGE,
guessRangeRadius(rangeFuel.get()), 0);
updateChannel(radiusFuel);
}
}
/**
* Easy function but there's some measures behind:
* Guessing the range of the Vehicle on Map. If you can drive x kilometers with your Vehicle it's not feasible to
* project this x km Radius on Map. The roads to be taken are causing some overhead because they are not a straight
* line from Location A to B.
* I've taken some measurements to calculate the overhead factor based on Google Maps
* Berlin - Dresden: Road Distance: 193 air-line Distance 167 = Factor 87%
* Kassel - Frankfurt: Road Distance: 199 air-line Distance 143 = Factor 72%
* After measuring more distances you'll find out that the outcome is between 70% and 90%. So
*
* This depends also on the roads of a concrete route but this is only a guess without any Route Navigation behind
*
* @param range
* @return mapping from air-line distance to "real road" distance
*/
public static State guessRangeRadius(QuantityType<?> s) {
double radius = s.intValue() * 0.8;
return QuantityType.valueOf(Math.round(radius), KILOMETRE_UNIT);
}
protected void updateChannel(ChannelStateMap csm) {
updateTime(csm.getGroup(), csm.getTimestamp());
updateState(new ChannelUID(thing.getUID(), csm.getGroup(), csm.getChannel()), csm.getState());
}
private void updateTime(String group, long timestamp) {
boolean updateTime = false;
Long l = timeHash.get(group);
if (l != null) {
if (l.longValue() < timestamp) {
updateTime = true;
}
} else {
updateTime = true;
}
if (updateTime) {
timeHash.put(group, timestamp);
DateTimeType dtt = new DateTimeType(Instant.ofEpochMilli(timestamp).atZone(timeZoneProvider.getTimeZone()));
updateState(new ChannelUID(thing.getUID(), group, "last-update"), dtt);
}
}
@Override
public void updateStatus(ThingStatus ts, ThingStatusDetail tsd, @Nullable String details) {
online = ts.equals(ThingStatus.ONLINE);
super.updateStatus(ts, tsd, details);
}
}

View File

@ -0,0 +1,183 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mercedesme.internal.server;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletHandler;
import org.openhab.binding.mercedesme.internal.Constants;
import org.openhab.binding.mercedesme.internal.config.AccountConfiguration;
import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.auth.client.oauth2.OAuthException;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.auth.client.oauth2.OAuthResponseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link CallbackServer} class defines an HTTP Server for authentication callbacks
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class CallbackServer {
private static final Logger LOGGER = LoggerFactory.getLogger(CallbackServer.class);
private static final Map<Integer, OAuthClientService> AUTH_MAP = new HashMap<Integer, OAuthClientService>();
private static final Map<Integer, CallbackServer> SERVER_MAP = new HashMap<Integer, CallbackServer>();
private static final AccessTokenResponse INVALID_ACCESS_TOKEN = new AccessTokenResponse();
private Optional<Server> server = Optional.empty();
private AccessTokenRefreshListener listener;
private AccountConfiguration config;
private OAuthClientService oacs;
private String callbackUrl;
public CallbackServer(AccessTokenRefreshListener l, HttpClient hc, OAuthFactory oAuthFactory,
AccountConfiguration config, String callbackUrl) {
oacs = oAuthFactory.createOAuthClientService(config.clientId, Constants.MB_TOKEN_URL, Constants.MB_AUTH_URL,
config.clientId, config.clientSecret, config.getScope(), false);
listener = l;
AUTH_MAP.put(Integer.valueOf(config.callbackPort), oacs);
SERVER_MAP.put(Integer.valueOf(config.callbackPort), this);
this.config = config;
this.callbackUrl = callbackUrl;
INVALID_ACCESS_TOKEN.setAccessToken(Constants.EMPTY);
}
public String getAuthorizationUrl() {
try {
return oacs.getAuthorizationUrl(callbackUrl, null, null);
} catch (OAuthException e) {
LOGGER.warn("Error creating Authorization URL {}", e.getMessage());
return Constants.EMPTY;
}
}
public String getScope() {
return config.getScope();
}
public boolean start() {
LOGGER.debug("Start Callback Server for port {}", config.callbackPort);
if (!server.isEmpty()) {
LOGGER.debug("Callback server for port {} already started", config.callbackPort);
return true;
}
server = Optional.of(new Server());
ServerConnector connector = new ServerConnector(server.get());
connector.setPort(config.callbackPort);
server.get().setConnectors(new Connector[] { connector });
ServletHandler servletHandler = new ServletHandler();
server.get().setHandler(servletHandler);
servletHandler.addServletWithMapping(CallbackServlet.class, Constants.CALLBACK_ENDPOINT);
try {
server.get().start();
} catch (Exception e) {
LOGGER.warn("Cannot start Callback Server for port {}, Error {}", config.callbackPort, e.getMessage());
return false;
}
return true;
}
public void stop() {
LOGGER.debug("Stop Callback Server");
try {
if (!server.isEmpty()) {
server.get().stop();
server = Optional.empty();
}
} catch (Exception e) {
LOGGER.warn("Cannot start Callback Server for port {}, Error {}", config.callbackPort, e.getMessage());
}
}
public String getToken() {
AccessTokenResponse atr = null;
try {
/*
* this will automatically trigger
* - return last stored token if it's still valid
* - refreshToken if current token is expired
* - inform listeners if refresh delivered new token
* - store new token in persistence
*/
atr = oacs.getAccessTokenResponse();
} catch (OAuthException | IOException | OAuthResponseException e) {
LOGGER.warn("Exception getting token {}", e.getMessage());
}
if (atr == null) {
LOGGER.debug("Token empty - Manual Authorization needed at {}", callbackUrl);
listener.onAccessTokenResponse(INVALID_ACCESS_TOKEN);
return INVALID_ACCESS_TOKEN.getAccessToken();
}
listener.onAccessTokenResponse(atr);
return atr.getAccessToken();
}
/**
* Static callback for Servlet calls
*
* @param port
* @param code
*/
public static void callback(int port, String code) {
LOGGER.trace("Callback from Servlet {} {}", port, code);
try {
OAuthClientService oacs = AUTH_MAP.get(port);
LOGGER.trace("Get token from code {}", code);
// get CallbackServer instance
CallbackServer srv = SERVER_MAP.get(port);
LOGGER.trace("Deliver token to {}", srv);
if (srv != null && oacs != null) {
// token stored and persisted inside oacs
AccessTokenResponse atr = oacs.getAccessTokenResponseByAuthorizationCode(code, srv.callbackUrl);
// inform listener - not done by oacs
srv.listener.onAccessTokenResponse(atr);
} else {
LOGGER.warn("Either Callbackserver {} or Authorization Service {} not found", srv, oacs);
}
} catch (OAuthException | IOException | OAuthResponseException e) {
LOGGER.warn("Exception getting token from code {} {}", code, e.getMessage());
}
}
public static String getAuthorizationUrl(int port) {
CallbackServer srv = SERVER_MAP.get(port);
if (srv != null) {
return srv.getAuthorizationUrl();
} else {
LOGGER.debug("No Callbackserver found for {}", port);
return Constants.EMPTY;
}
}
public static String getScope(int port) {
CallbackServer srv = SERVER_MAP.get(port);
if (srv != null) {
return srv.getScope();
} else {
LOGGER.debug("No Callbackserver found for {}", port);
return Constants.EMPTY;
}
}
}

View File

@ -0,0 +1,73 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mercedesme.internal.server;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mercedesme.internal.Constants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link CallbackServlet} class provides authentication callback endpoint
*
* @author Bernd Weymann - Initial contribution
*/
@SuppressWarnings("serial")
@NonNullByDefault
public class CallbackServlet extends HttpServlet {
private final Logger logger = LoggerFactory.getLogger(CallbackServlet.class);
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String code = request.getParameter(Constants.CODE);
if (code != null) {
CallbackServer.callback(request.getLocalPort(), code);
logger.trace("Code successfully extracted {}", request.getParameterMap());
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().println(request.getParameterMap());
response.getWriter().println("{ \"status\": \"ok\"}");
} else {
response.setContentType("text/html");
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().println("<HTML>");
response.getWriter().println("<BODY>");
response.getWriter().println("<B>Call Parameters</B>");
response.getWriter().println("<BR>");
response.getWriter().println(request.getParameterMap());
response.getWriter().println("<BR><BR>");
response.getWriter().println("<B>Configured scopes</B><BR>");
String[] scopes = CallbackServer.getScope(request.getLocalPort()).split(Constants.SPACE);
for (int i = 0; i < scopes.length; i++) {
response.getWriter().println(scopes[i] + "<BR>");
}
response.getWriter().println("<BR><BR>");
response.getWriter().println("<B>Get your access token for openHAB MercedesMe Binding</B>");
response.getWriter().println("<BR>");
response.getWriter().println("<a href=\"" + CallbackServer.getAuthorizationUrl(request.getLocalPort())
+ "\">Start Authorization</a>");
response.getWriter().println("</BODY>");
response.getWriter().println("</HTML>");
}
logger.debug("Call from {}:{} parameters {}", request.getLocalAddr(), request.getLocalPort(),
request.getParameterMap());
}
}

View File

@ -0,0 +1,88 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mercedesme.internal.server;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mercedesme.internal.Constants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link Utils} class defines an HTTP Server for authentication callbacks
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class Utils {
private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class);
private static final List<Integer> PORTS = new ArrayList<Integer>();
private static int port = 8090;
/**
* Get free port without other Thread interference
*
* @return
*/
public static synchronized int getFreePort() {
while (PORTS.contains(port)) {
port++;
}
PORTS.add(port);
return port;
}
public static synchronized void addPort(int portNr) {
if (PORTS.contains(portNr)) {
LOGGER.warn("Port {} already occupied", portNr);
}
PORTS.add(portNr);
}
public static synchronized void removePort(int portNr) {
PORTS.remove(Integer.valueOf(portNr));
}
public static String getCallbackIP() throws SocketException {
// https://stackoverflow.com/questions/1062041/ip-address-not-obtained-in-java
for (Enumeration<NetworkInterface> ifaces = NetworkInterface.getNetworkInterfaces(); ifaces
.hasMoreElements();) {
NetworkInterface iface = ifaces.nextElement();
try {
if (!iface.isLoopback()) {
if (iface.isUp()) {
for (Enumeration<InetAddress> addresses = iface.getInetAddresses(); addresses
.hasMoreElements();) {
InetAddress address = addresses.nextElement();
return address.getHostAddress();
}
}
}
} catch (SocketException se) {
// Calling one network interface failed - continue searching
LOGGER.trace("Network {} failed {}", iface.getName(), se.getMessage());
}
}
throw new SocketException("IP address not detected");
}
public static String getCallbackAddress(String callbackIP, int callbackPort) {
return "http://" + callbackIP + Constants.COLON + callbackPort + Constants.CALLBACK_ENDPOINT;
}
}

View File

@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mercedesme.internal.utils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.types.State;
/**
* The {@link ChannelStateMap} holds the necessary values to update a channel state
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class ChannelStateMap {
private String channel;
private String group;
private State state;
private long timestamp;
public ChannelStateMap(String ch, String grp, State st, long ts) {
channel = ch;
group = grp;
state = st;
timestamp = ts;
}
public String getChannel() {
return channel;
}
public String getGroup() {
return group;
}
public State getState() {
return state;
}
public long getTimestamp() {
return timestamp;
}
@Override
public String toString() {
return group + ":" + channel + " " + state;
}
public boolean isValid() {
return !channel.isEmpty();
}
}

View File

@ -0,0 +1,237 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mercedesme.internal.utils;
import static org.openhab.binding.mercedesme.internal.Constants.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONObject;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link Mapper} maps a given Json Object towards a channel, group and state
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class Mapper {
private static final Logger LOGGER = LoggerFactory.getLogger(Mapper.class);
public static final ChannelStateMap INVALID_MAP = new ChannelStateMap(EMPTY, EMPTY, UnDefType.UNDEF, -1);
public static final Map<String, String[]> CHANNELS = new HashMap<String, String[]>();
public static final String TIMESTAMP = "timestamp";
public static final String VALUE = "value";
public static ChannelStateMap getChannelStateMap(JSONObject jo) {
if (CHANNELS.isEmpty()) {
init();
}
Set<String> s = jo.keySet();
if (s.size() == 1) {
String id = s.toArray()[0].toString();
String[] ch = CHANNELS.get(id);
if (ch != null) {
State state;
switch (id) {
// Kilometer values
case "odo":
case "rangeelectric":
case "rangeliquid":
state = getKilometers((JSONObject) jo.get(id));
return new ChannelStateMap(ch[0], ch[1], state, getTimestamp((JSONObject) jo.get(id)));
// Percentages
case "soc":
case "tanklevelpercent":
state = getPercentage((JSONObject) jo.get(id));
return new ChannelStateMap(ch[0], ch[1], state, getTimestamp((JSONObject) jo.get(id)));
// Contacts
case "decklidstatus":
case "doorstatusfrontleft":
case "doorstatusfrontright":
case "doorstatusrearleft":
case "doorstatusrearright":
state = getContact((JSONObject) jo.get(id));
return new ChannelStateMap(ch[0], ch[1], state, getTimestamp((JSONObject) jo.get(id)));
// Number Status
case "lightswitchposition":
case "rooftopstatus":
case "sunroofstatus":
case "windowstatusfrontleft":
case "windowstatusfrontright":
case "windowstatusrearleft":
case "windowstatusrearright":
case "doorlockstatusvehicle":
state = getDecimal((JSONObject) jo.get(id));
return new ChannelStateMap(ch[0], ch[1], state, getTimestamp((JSONObject) jo.get(id)));
// Switches
case "interiorLightsFront":
case "interiorLightsRear":
case "readingLampFrontLeft":
case "readingLampFrontRight":
state = getOnOffType((JSONObject) jo.get(id));
return new ChannelStateMap(ch[0], ch[1], state, getTimestamp((JSONObject) jo.get(id)));
case "doorlockstatusdecklid":
case "doorlockstatusgas":
state = getOnOffTypeLock((JSONObject) jo.get(id));
return new ChannelStateMap(ch[0], ch[1], state, getTimestamp((JSONObject) jo.get(id)));
// Angle
case "positionHeading":
state = getAngle((JSONObject) jo.get(id));
return new ChannelStateMap(ch[0], ch[1], state, getTimestamp((JSONObject) jo.get(id)));
default:
LOGGER.trace("No mapping available for {}", id);
}
} else {
LOGGER.trace("No mapping available for {}", id);
}
} else {
LOGGER.debug("More than one key found {}", s);
}
return INVALID_MAP;
}
private static long getTimestamp(JSONObject jo) {
if (jo.has(TIMESTAMP)) {
return jo.getLong(TIMESTAMP);
}
return -1;
}
private static State getOnOffType(JSONObject jo) {
if (jo.has(VALUE)) {
String value = jo.get(VALUE).toString();
boolean b = Boolean.valueOf(value);
return OnOffType.from(b);
} else {
LOGGER.warn("JSONObject contains no value {}", jo);
return UnDefType.UNDEF;
}
}
private static State getOnOffTypeLock(JSONObject jo) {
if (jo.has(VALUE)) {
String value = jo.get(VALUE).toString();
boolean b = Boolean.valueOf(value);
// Yes, false is locked and true unlocked
// https://developer.mercedes-benz.com/products/vehicle_lock_status/specifications/vehicle_lock_status_api
return OnOffType.from(!b);
} else {
LOGGER.warn("JSONObject contains no value {}", jo);
return UnDefType.UNDEF;
}
}
private static State getAngle(JSONObject jo) {
if (jo.has(VALUE)) {
String value = jo.get(VALUE).toString();
return QuantityType.valueOf(Double.valueOf(value), Units.DEGREE_ANGLE);
} else {
LOGGER.warn("JSONObject contains no value {}", jo);
return UnDefType.UNDEF;
}
}
private static State getDecimal(JSONObject jo) {
if (jo.has(VALUE)) {
String value = jo.get(VALUE).toString();
return DecimalType.valueOf(value);
} else {
LOGGER.warn("JSONObject contains no value {}", jo);
return UnDefType.UNDEF;
}
}
private static State getContact(JSONObject jo) {
if (jo.has(VALUE)) {
String value = jo.get(VALUE).toString();
boolean b = Boolean.valueOf(value);
if (!b) {
return OpenClosedType.CLOSED;
} else {
return OpenClosedType.OPEN;
}
} else {
LOGGER.warn("JSONObject contains no value {}", jo);
return UnDefType.UNDEF;
}
}
private static State getKilometers(JSONObject jo) {
if (jo.has(VALUE)) {
String value = jo.get(VALUE).toString();
return QuantityType.valueOf(Integer.valueOf(value), KILOMETRE_UNIT);
} else {
LOGGER.warn("JSONObject contains no value {}", jo);
return UnDefType.UNDEF;
}
}
private static State getPercentage(JSONObject jo) {
if (jo.has(VALUE)) {
String value = jo.get(VALUE).toString();
return QuantityType.valueOf(Integer.valueOf(value), Units.PERCENT);
} else {
LOGGER.warn("JSONObject contains no value {}", jo);
return UnDefType.UNDEF;
}
}
/**
* Mapping of json id towards channel group and id
*/
private static void init() {
CHANNELS.put("odo", new String[] { "mileage", GROUP_RANGE });
CHANNELS.put("rangeelectric", new String[] { "range-electric", GROUP_RANGE });
CHANNELS.put("soc", new String[] { "soc", GROUP_RANGE });
CHANNELS.put("rangeliquid", new String[] { "range-fuel", GROUP_RANGE });
CHANNELS.put("tanklevelpercent", new String[] { "fuel-level", GROUP_RANGE });
CHANNELS.put("decklidstatus", new String[] { "deck-lid", GROUP_DOORS });
CHANNELS.put("doorstatusfrontleft", new String[] { "driver-front", GROUP_DOORS });
CHANNELS.put("doorstatusfrontright", new String[] { "passenger-front", GROUP_DOORS });
CHANNELS.put("doorstatusrearleft", new String[] { "driver-rear", GROUP_DOORS });
CHANNELS.put("doorstatusrearright", new String[] { "passenger-rear", GROUP_DOORS });
CHANNELS.put("interiorLightsFront", new String[] { "interior-front", GROUP_LIGHTS });
CHANNELS.put("interiorLightsRear", new String[] { "interior-rear", GROUP_LIGHTS });
CHANNELS.put("lightswitchposition", new String[] { "light-switch", GROUP_LIGHTS });
CHANNELS.put("readingLampFrontLeft", new String[] { "reading-left", GROUP_LIGHTS });
CHANNELS.put("readingLampFrontRight", new String[] { "reading-right", GROUP_LIGHTS });
CHANNELS.put("rooftopstatus", new String[] { "rooftop", GROUP_DOORS });
CHANNELS.put("sunroofstatus", new String[] { "sunroof", GROUP_DOORS });
CHANNELS.put("windowstatusfrontleft", new String[] { "driver-front", GROUP_WINDOWS });
CHANNELS.put("windowstatusfrontright", new String[] { "passenger-front", GROUP_WINDOWS });
CHANNELS.put("windowstatusrearleft", new String[] { "driver-rear", GROUP_WINDOWS });
CHANNELS.put("windowstatusrearright", new String[] { "passenger-rear", GROUP_WINDOWS });
CHANNELS.put("doorlockstatusvehicle", new String[] { "doors", GROUP_LOCK });
CHANNELS.put("doorlockstatusdecklid", new String[] { "deck-lid", GROUP_LOCK });
CHANNELS.put("doorlockstatusgas", new String[] { "flap", GROUP_LOCK });
CHANNELS.put("positionHeading", new String[] { "heading", GROUP_LOCATION });
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="mercedesme" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Mercedes Me Binding</name>
<description>The binding provides access to your Mercedes developer account and vehicles</description>
</binding:binding>

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:mercedesme:bev">
<parameter name="vin" type="text" required="true">
<label>Vehicle Identification Number</label>
</parameter>
<parameter name="refreshInterval" type="integer" min="1" unit="min" required="true">
<label>Refresh Interval</label>
<description>Data refresh rate for vehicle data</description>
<default>5</default>
</parameter>
<parameter name="batteryCapacity" type="decimal">
<label>Battery Capacity</label>
<description>Battery capacity in kWh of vehicle</description>
</parameter>
<!-- https://developer.mercedes-benz.com/products/vehicle_images/docs#_default_image_settings -->
<parameter name="background" type="boolean">
<label>Background Image</label>
<description>Vehicle images provided with or without background</description>
<default>false</default>
<advanced>true</advanced>
</parameter>
<parameter name="night" type="boolean">
<label>Night Image</label>
<description>Vehicle images in night conditions</description>
<default>false</default>
<advanced>true</advanced>
</parameter>
<parameter name="cropped" type="boolean">
<label>Cropped Image</label>
<description>Vehicle images in 4:3 instead of 16:9</description>
<default>false</default>
<advanced>true</advanced>
</parameter>
<parameter name="roofOpen" type="boolean">
<label>Cabriolet Open Roof</label>
<description>Vehicle images with open roof (only Cabriolet)</description>
<default>false</default>
<advanced>true</advanced>
</parameter>
<parameter name="format" type="text">
<label>Image Format</label>
<description>Preferred Image Format</description>
<default>webp</default>
<advanced>true</advanced>
<options>
<option value="webp">webp</option>
<option value="png">png</option>
<option value="jpeg">jpeg</option>
</options>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:mercedesme:bridge">
<parameter name="clientId" type="text" required="true">
<label>MB Developer Client ID</label>
<description>Mercedes Benz Developer Client ID</description>
</parameter>
<parameter name="clientSecret" type="text" required="true">
<label>MB Developer Client Secret</label>
<description>Mercedes Benz Developer Client Secret</description>
</parameter>
<parameter name="imageApiKey" type="text">
<label>MB Developer Image API Key</label>
<description>Mercedes Benz Developer Image API Key</description>
</parameter>
<parameter name="odoScope" type="boolean">
<label>PayAsYourDrive Insurance</label>
<description>Provides total Mileage</description>
<default>true</default>
</parameter>
<parameter name="vehicleScope" type="boolean">
<label>Vehicle Status</label>
<description>Status of doors, windows lights</description>
<default>true</default>
</parameter>
<parameter name="lockScope" type="boolean">
<label>Vehicle Lock Status</label>
<description>Lock status of doors and trunk</description>
<default>true</default>
</parameter>
<parameter name="fuelScope" type="boolean">
<label>Fuel Status</label>
<description>Tank level and range</description>
<default>true</default>
</parameter>
<parameter name="evScope" type="boolean">
<label>Electric Vehicle Status</label>
<description>Electric charge and range</description>
<default>true</default>
</parameter>
<parameter name="callbackIP" type="text">
<label>Callback IP Address</label>
<description>IP address for openHAB callback URL</description>
<advanced>true</advanced>
</parameter>
<parameter name="callbackPort" type="integer">
<label>Callback Port Number</label>
<description>Port Number for openHAB callback URL</description>
<advanced>true</advanced>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:mercedesme:conv">
<parameter name="vin" type="text" required="true">
<label>Vehicle Identification Number</label>
</parameter>
<parameter name="refreshInterval" type="integer" min="1" unit="min" required="true">
<label>Refresh Interval</label>
<description>Data refresh rate for your vehicle data</description>
<default>5</default>
</parameter>
<parameter name="fuelCapacity" type="decimal" min="0">
<label>Fuel Capacity</label>
<description>Fuel capacity in liters of vehicle</description>
</parameter>
<!-- https://developer.mercedes-benz.com/products/vehicle_images/docs#_default_image_settings -->
<parameter name="background" type="boolean">
<label>Background Image</label>
<description>Vehicle images provided with or without background</description>
<default>false</default>
<advanced>true</advanced>
</parameter>
<parameter name="night" type="boolean">
<label>Night Image</label>
<description>Vehicle images in night conditions</description>
<default>false</default>
<advanced>true</advanced>
</parameter>
<parameter name="cropped" type="boolean">
<label>Cropped Image</label>
<description>Vehicle images in 4:3 instead of 16:9</description>
<default>false</default>
<advanced>true</advanced>
</parameter>
<parameter name="roofOpen" type="boolean">
<label>Cabriolet Open Roof</label>
<description>Vehicle images with open roof (only Cabriolet)</description>
<default>false</default>
<advanced>true</advanced>
</parameter>
<parameter name="format" type="text">
<label>Image Format</label>
<description>Preferred Image Format</description>
<default>webp</default>
<advanced>true</advanced>
<options>
<option value="webp">webp</option>
<option value="png">png</option>
<option value="jpeg">jpeg</option>
</options>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:mercedesme:hybrid">
<parameter name="vin" type="text" required="true">
<label>Vehicle Identification Number</label>
</parameter>
<parameter name="refreshInterval" type="integer" min="1" unit="min" required="true">
<label>Refresh Interval</label>
<description>Data refresh rate for vehicle data</description>
<default>5</default>
</parameter>
<parameter name="batteryCapacity" type="decimal">
<label>Battery Capacity</label>
<description>Battery capacity in kWh of vehicle</description>
</parameter>
<parameter name="fuelCapacity" type="decimal">
<label>Fuel Capacity</label>
<description>Fuel capacity in liters of vehicle</description>
</parameter>
<!-- https://developer.mercedes-benz.com/products/vehicle_images/docs#_default_image_settings -->
<parameter name="background" type="boolean">
<label>Background Image</label>
<description>Vehicle images provided with or without background</description>
<default>false</default>
<advanced>true</advanced>
</parameter>
<parameter name="night" type="boolean">
<label>Night Image</label>
<description>Vehicle images in night conditions</description>
<default>false</default>
<advanced>true</advanced>
</parameter>
<parameter name="cropped" type="boolean">
<label>Cropped Image</label>
<description>Vehicle images in 4:3 instead of 16:9</description>
<default>false</default>
<advanced>true</advanced>
</parameter>
<parameter name="roofOpen" type="boolean">
<label>Cabriolet Open Roof</label>
<description>Vehicle images with open roof (only Cabriolet)</description>
<default>false</default>
<advanced>true</advanced>
</parameter>
<parameter name="format" type="text">
<label>Image Format</label>
<description>Preferred Image Format</description>
<default>webp</default>
<advanced>true</advanced>
<options>
<option value="webp">webp</option>
<option value="png">png</option>
<option value="jpeg">jpeg</option>
</options>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,218 @@
# binding
binding.mercedesme.name = Mercedes Me Binding
binding.mercedesme.description = The binding provides access to your Mercedes developer account and vehicles
# thing types
thing-type.mercedesme.account.label = Mercedes Me Account
thing-type.mercedesme.account.description = Mercedes Benz account data
thing-type.mercedesme.bev.label = Mercedes Benz BEV
thing-type.mercedesme.bev.description = Battery Electric Vehicle
thing-type.mercedesme.combustion.label = Mercedes Benz
thing-type.mercedesme.combustion.description = Conventional Fuel Vehicle
thing-type.mercedesme.hybrid.label = Mercedes Benz Hybrid
thing-type.mercedesme.hybrid.description = Conventional Fuel Vehicle with supporting Electric Engine
# thing types config
thing-type.config.mercedesme.bev.background.label = Background Image
thing-type.config.mercedesme.bev.background.description = Vehicle images provided with or without background
thing-type.config.mercedesme.bev.batteryCapacity.label = Battery Capacity
thing-type.config.mercedesme.bev.batteryCapacity.description = Battery capacity in kwh of vehicle
thing-type.config.mercedesme.bev.cropped.label = Cropped Image
thing-type.config.mercedesme.bev.cropped.description = Vehicle images in 4:3 instead of 16:9
thing-type.config.mercedesme.bev.format.label = Image Format
thing-type.config.mercedesme.bev.format.description = Preferred Image Format
thing-type.config.mercedesme.bev.format.option.webp = webp
thing-type.config.mercedesme.bev.format.option.png = png
thing-type.config.mercedesme.bev.format.option.jpeg = jpeg
thing-type.config.mercedesme.bev.night.label = Night Image
thing-type.config.mercedesme.bev.night.description = Vehicle images in night conditions
thing-type.config.mercedesme.bev.refreshInterval.label = Refresh Interval
thing-type.config.mercedesme.bev.refreshInterval.description = Data refresh rate for vehicle data
thing-type.config.mercedesme.bev.roofOpen.label = Cabriolet Open Roof
thing-type.config.mercedesme.bev.roofOpen.description = Vehicle images with open roof (only Cabriolet)
thing-type.config.mercedesme.bev.vin.label = Vehicle Identification Number
thing-type.config.mercedesme.bridge.callbackIP.label = Callback IP Address
thing-type.config.mercedesme.bridge.callbackIP.description = IP address for openHAB callback URL
thing-type.config.mercedesme.bridge.callbackPort.label = Callback Port Number
thing-type.config.mercedesme.bridge.callbackPort.description = Port Number for openHAB callback URL
thing-type.config.mercedesme.bridge.clientId.label = MB Developer Client ID
thing-type.config.mercedesme.bridge.clientId.description = Mercedes Benz Developer Client ID
thing-type.config.mercedesme.bridge.clientSecret.label = MB Developer Client Secret
thing-type.config.mercedesme.bridge.clientSecret.description = Mercedes Benz Developer Client Secret
thing-type.config.mercedesme.bridge.evScope.label = Electric Vehicle Status
thing-type.config.mercedesme.bridge.evScope.description = Electric charge and range
thing-type.config.mercedesme.bridge.fuelScope.label = Fuel Status
thing-type.config.mercedesme.bridge.fuelScope.description = Tank level and range
thing-type.config.mercedesme.bridge.imageApiKey.label = MB Developer Image API Key
thing-type.config.mercedesme.bridge.imageApiKey.description = Mercedes Benz Developer Image API Key
thing-type.config.mercedesme.bridge.lockScope.label = Vehicle Lock Status
thing-type.config.mercedesme.bridge.lockScope.description = Lock status of doors and trunk
thing-type.config.mercedesme.bridge.odoScope.label = PayAsYourDrive Insurance
thing-type.config.mercedesme.bridge.odoScope.description = Provides total Mileage
thing-type.config.mercedesme.bridge.vehicleScope.label = Vehicle Status
thing-type.config.mercedesme.bridge.vehicleScope.description = Status of doors, windows lights
thing-type.config.mercedesme.conv.background.label = Background Image
thing-type.config.mercedesme.conv.background.description = Vehicle images provided with or without background
thing-type.config.mercedesme.conv.cropped.label = Cropped Image
thing-type.config.mercedesme.conv.cropped.description = Vehicle images in 4:3 instead of 16:9
thing-type.config.mercedesme.conv.format.label = Image Format
thing-type.config.mercedesme.conv.format.description = Preferred Image Format
thing-type.config.mercedesme.conv.format.option.webp = webp
thing-type.config.mercedesme.conv.format.option.png = png
thing-type.config.mercedesme.conv.format.option.jpeg = jpeg
thing-type.config.mercedesme.conv.fuelCapacity.label = Fuel Capacity
thing-type.config.mercedesme.conv.fuelCapacity.description = Fuel capacity in liters of vehicle
thing-type.config.mercedesme.conv.night.label = Night Image
thing-type.config.mercedesme.conv.night.description = Vehicle images in night conditions
thing-type.config.mercedesme.conv.refreshInterval.label = Refresh Interval
thing-type.config.mercedesme.conv.refreshInterval.description = Data refresh rate for your vehicle data
thing-type.config.mercedesme.conv.roofOpen.label = Cabriolet Open Roof
thing-type.config.mercedesme.conv.roofOpen.description = Vehicle images with open roof (only Cabriolet)
thing-type.config.mercedesme.conv.vin.label = Vehicle Identification Number
thing-type.config.mercedesme.hybrid.background.label = Background Image
thing-type.config.mercedesme.hybrid.background.description = Vehicle images provided with or without background
thing-type.config.mercedesme.hybrid.batteryCapacity.label = Battery Capacity
thing-type.config.mercedesme.hybrid.batteryCapacity.description = Battery capacity in kwh of vehicle
thing-type.config.mercedesme.hybrid.cropped.label = Cropped Image
thing-type.config.mercedesme.hybrid.cropped.description = Vehicle images in 4:3 instead of 16:9
thing-type.config.mercedesme.hybrid.format.label = Image Format
thing-type.config.mercedesme.hybrid.format.description = Preferred Image Format
thing-type.config.mercedesme.hybrid.format.option.webp = webp
thing-type.config.mercedesme.hybrid.format.option.png = png
thing-type.config.mercedesme.hybrid.format.option.jpeg = jpeg
thing-type.config.mercedesme.hybrid.fuelCapacity.label = Fuel Capacity
thing-type.config.mercedesme.hybrid.fuelCapacity.description = Fuel capacity in liters of vehicle
thing-type.config.mercedesme.hybrid.night.label = Night Image
thing-type.config.mercedesme.hybrid.night.description = Vehicle images in night conditions
thing-type.config.mercedesme.hybrid.refreshInterval.label = Refresh Interval
thing-type.config.mercedesme.hybrid.refreshInterval.description = Data refresh rate for vehicle data
thing-type.config.mercedesme.hybrid.roofOpen.label = Cabriolet Open Roof
thing-type.config.mercedesme.hybrid.roofOpen.description = Vehicle images with open roof (only Cabriolet)
thing-type.config.mercedesme.hybrid.vin.label = Vehicle Identification Number
# channel group types
channel-group-type.mercedesme.door-values.label = Detailed Door Status
channel-group-type.mercedesme.door-values.description = Detailed Status of all Doors and Windows
channel-group-type.mercedesme.image-values.label = Vehicle Images
channel-group-type.mercedesme.light-values.label = Light Status
channel-group-type.mercedesme.light-values.description = Light Status of interior lights and main light switch
channel-group-type.mercedesme.location-values.label = Vehicle Location
channel-group-type.mercedesme.location-values.description = Heading of vehicle
channel-group-type.mercedesme.lock-values.label = Lock Status
channel-group-type.mercedesme.lock-values.description = Vehicle Lock Status
channel-group-type.mercedesme.range-conv-values.label = Range and Fuel Data
channel-group-type.mercedesme.range-conv-values.description = Provides Mileage, remaining range and fuel level values
channel-group-type.mercedesme.range-ev-values.label = Range and Charge Data
channel-group-type.mercedesme.range-ev-values.description = Provides Mileage, remaining range and charge level values
channel-group-type.mercedesme.range-hybrid-values.label = Range, Charge / Fuel Data
channel-group-type.mercedesme.range-hybrid-values.description = Provides mileage, remaining fuel and range data for hybrid vehicles
channel-group-type.mercedesme.window-values.label = Detailed Window Status
channel-group-type.mercedesme.window-values.description = Detailed Status Windows
# channel types
channel-type.mercedesme.charged-channel.label = Charged Battery Energy
channel-type.mercedesme.clear-cache-channel.label = Remove All Stored Images
channel-type.mercedesme.deck-lid-channel.label = Deck Lid
channel-type.mercedesme.deck-lid-lock-channel.label = Deck Lid Lock
channel-type.mercedesme.doors-lock-channel.label = Door Lock Status
channel-type.mercedesme.doors-lock-channel.state.option.0 = Unlocked
channel-type.mercedesme.doors-lock-channel.state.option.1 = Locked Internal
channel-type.mercedesme.doors-lock-channel.state.option.2 = Locked External
channel-type.mercedesme.doors-lock-channel.state.option.3 = Unlocked Selective
channel-type.mercedesme.driver-front-channel.label = Driver Door
channel-type.mercedesme.driver-rear-channel.label = Driver Door Rear
channel-type.mercedesme.flap-lock-channel.label = Flap Lock
channel-type.mercedesme.fuel-level-channel.label = Fuel Level
channel-type.mercedesme.fuel-open-channel.label = Open Fuel Capacity
channel-type.mercedesme.fuel-remain-channel.label = Remaining Fuel
channel-type.mercedesme.heading-channel.label = Heading Angle
channel-type.mercedesme.image-data-channel.label = Rendered Vehicle Image
channel-type.mercedesme.image-view-channel.label = Image Viewport
channel-type.mercedesme.interior-front-channel.label = Interior Light Front
channel-type.mercedesme.interior-rear-channel.label = Interior Light Rear
channel-type.mercedesme.last-doors-update-channel.label = Last Doors Update
channel-type.mercedesme.last-doors-update-channel.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM
channel-type.mercedesme.last-lights-update-channel.label = Last Light Update
channel-type.mercedesme.last-lights-update-channel.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM
channel-type.mercedesme.last-location-update-channel.label = Last Location Update
channel-type.mercedesme.last-location-update-channel.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM
channel-type.mercedesme.last-lock-update-channel.label = Last Lock Update
channel-type.mercedesme.last-lock-update-channel.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM
channel-type.mercedesme.last-range-update-channel.label = Last Range Update
channel-type.mercedesme.last-range-update-channel.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM
channel-type.mercedesme.last-windows-update-channel.label = Last Window Update
channel-type.mercedesme.last-windows-update-channel.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM
channel-type.mercedesme.light-switch-channel.label = Main Light Rotary
channel-type.mercedesme.light-switch-channel.state.option.0 = Auto
channel-type.mercedesme.light-switch-channel.state.option.1 = Headlight
channel-type.mercedesme.light-switch-channel.state.option.2 = Sidelight Left
channel-type.mercedesme.light-switch-channel.state.option.3 = Sidelight Right
channel-type.mercedesme.light-switch-channel.state.option.4 = Parking Light
channel-type.mercedesme.mileage-channel.label = Mileage
channel-type.mercedesme.passenger-front-channel.label = Passenger Door
channel-type.mercedesme.passenger-rear-channel.label = Passenger Door Rear
channel-type.mercedesme.radius-electric-channel.label = Electric Radius
channel-type.mercedesme.radius-fuel-channel.label = Fuel Radius
channel-type.mercedesme.radius-hybrid-channel.label = Hybrid Radius
channel-type.mercedesme.range-electric-channel.label = Electric Range
channel-type.mercedesme.range-fuel-channel.label = Fuel Range
channel-type.mercedesme.range-hybrid-channel.label = Hybrid Range
channel-type.mercedesme.reading-left-channel.label = Reading Light Left
channel-type.mercedesme.reading-right-channel.label = Reading Light Right
channel-type.mercedesme.rooftop-channel.label = Roof top
channel-type.mercedesme.rooftop-channel.state.option.0 = Unlocked
channel-type.mercedesme.rooftop-channel.state.option.1 = Open and locked
channel-type.mercedesme.rooftop-channel.state.option.2 = Closed and locked
channel-type.mercedesme.soc-channel.label = Battery Charge Level
channel-type.mercedesme.sunroof-channel.label = Sun Roof
channel-type.mercedesme.sunroof-channel.state.option.0 = Closed
channel-type.mercedesme.sunroof-channel.state.option.1 = Open
channel-type.mercedesme.sunroof-channel.state.option.2 = Open Lifting
channel-type.mercedesme.sunroof-channel.state.option.3 = Running
channel-type.mercedesme.sunroof-channel.state.option.4 = Closing
channel-type.mercedesme.sunroof-channel.state.option.5 = Opening
channel-type.mercedesme.sunroof-channel.state.option.6 = Closing
channel-type.mercedesme.uncharged-channel.label = Uncharged Battery Energy
channel-type.mercedesme.window-driver-front-channel.label = Driver Window
channel-type.mercedesme.window-driver-front-channel.state.option.0 = Intermediate
channel-type.mercedesme.window-driver-front-channel.state.option.1 = Open
channel-type.mercedesme.window-driver-front-channel.state.option.2 = Closed
channel-type.mercedesme.window-driver-front-channel.state.option.3 = Airing
channel-type.mercedesme.window-driver-front-channel.state.option.4 = Intermediate
channel-type.mercedesme.window-driver-front-channel.state.option.5 = Running
channel-type.mercedesme.window-driver-rear-channel.label = Driver Window Rear
channel-type.mercedesme.window-driver-rear-channel.state.option.0 = Intermediate
channel-type.mercedesme.window-driver-rear-channel.state.option.1 = Open
channel-type.mercedesme.window-driver-rear-channel.state.option.2 = Closed
channel-type.mercedesme.window-driver-rear-channel.state.option.3 = Airing
channel-type.mercedesme.window-driver-rear-channel.state.option.4 = Intermediate
channel-type.mercedesme.window-driver-rear-channel.state.option.5 = Running
channel-type.mercedesme.window-passenger-front-channel.label = Passenger Window
channel-type.mercedesme.window-passenger-front-channel.state.option.0 = Intermediate
channel-type.mercedesme.window-passenger-front-channel.state.option.1 = Open
channel-type.mercedesme.window-passenger-front-channel.state.option.2 = Closed
channel-type.mercedesme.window-passenger-front-channel.state.option.3 = Airing
channel-type.mercedesme.window-passenger-front-channel.state.option.4 = Intermediate
channel-type.mercedesme.window-passenger-front-channel.state.option.5 = Running
channel-type.mercedesme.window-passenger-rear-channel.label = Passenger Window Rear
channel-type.mercedesme.window-passenger-rear-channel.state.option.0 = Intermediate
channel-type.mercedesme.window-passenger-rear-channel.state.option.1 = Open
channel-type.mercedesme.window-passenger-rear-channel.state.option.2 = Closed
channel-type.mercedesme.window-passenger-rear-channel.state.option.3 = Airing
channel-type.mercedesme.window-passenger-rear-channel.state.option.4 = Intermediate
channel-type.mercedesme.window-passenger-rear-channel.state.option.5 = Running
# MercedesMe Things Status Details
mercedesme.account.status.authorization-needed = Manual Authorization needed at {0}
mercedesme.account.status.ip-missing = Callback IP missing
mercedesme.account.status.port-missing = Callback Port missing
mercedesme.account.status.client-id-missing = Client ID missing
mercedesme.account.status.client-secret-missing = Client Secret missing
mercedesme.account.status.server-restart = Disable and enable Bridge to restart Authorization Server
mercedesme.vehicle.status.bridge-missing = Bridge not set
mercedesme.vehicle.status.bridge-authoriziation = Check Bridge Authorization

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mercedesme"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="account">
<label>Mercedes Me Account</label>
<description>Mercedes Benz account data</description>
<config-description-ref uri="thing-type:mercedesme:bridge"/>
</bridge-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mercedesme"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-type id="driver-front-channel">
<item-type>Contact</item-type>
<label>Driver Door</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="driver-rear-channel">
<item-type>Contact</item-type>
<label>Driver Door Rear</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="passenger-front-channel">
<item-type>Contact</item-type>
<label>Passenger Door</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="passenger-rear-channel">
<item-type>Contact</item-type>
<label>Passenger Door Rear</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="deck-lid-channel">
<item-type>Contact</item-type>
<label>Deck Lid</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="rooftop-channel">
<item-type>Number</item-type>
<label>Roof top</label>
<state readOnly="true">
<options>
<option value="0">Unlocked</option>
<option value="1">Open and locked</option>
<option value="2">Closed and locked</option>
</options>
</state>
</channel-type>
<channel-type id="sunroof-channel">
<item-type>Number</item-type>
<label>Sun Roof</label>
<state readOnly="true">
<options>
<option value="0">Closed</option>
<option value="1">Open</option>
<option value="2">Open Lifting</option>
<option value="3">Running</option>
<option value="4">Closing</option>
<option value="5">Opening</option>
<option value="6">Closing</option>
</options>
</state>
</channel-type>
<channel-type id="last-doors-update-channel">
<item-type>DateTime</item-type>
<label>Last Doors Update</label>
<state pattern="%1$tA, %1$td.%1$tm. %1$tH:%1$tM" readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mercedesme"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-group-type id="door-values">
<label>Detailed Door Status</label>
<description>Detailed Status of all Doors and Windows</description>
<channels>
<channel id="driver-front" typeId="driver-front-channel"/>
<channel id="driver-rear" typeId="driver-rear-channel"/>
<channel id="passenger-front" typeId="passenger-front-channel"/>
<channel id="passenger-rear" typeId="passenger-rear-channel"/>
<channel id="deck-lid" typeId="deck-lid-channel"/>
<channel id="sunroof" typeId="sunroof-channel"/>
<channel id="rooftop" typeId="rooftop-channel"/>
<channel id="last-update" typeId="last-doors-update-channel"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mercedesme"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-type id="image-data-channel">
<item-type>Image</item-type>
<label>Rendered Vehicle Image</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="image-view-channel">
<item-type>String</item-type>
<label>Image Viewport</label>
</channel-type>
<channel-type id="clear-cache-channel">
<item-type>Switch</item-type>
<label>Remove All Stored Images</label>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mercedesme"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-group-type id="image-values">
<label>Vehicle Images</label>
<channels>
<channel id="image-data" typeId="image-data-channel"/>
<channel id="image-view" typeId="image-view-channel"/>
<channel id="clear-cache" typeId="clear-cache-channel"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mercedesme"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-type id="interior-front-channel">
<item-type>Switch</item-type>
<label>Interior Light Front</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="interior-rear-channel">
<item-type>Switch</item-type>
<label>Interior Light Rear</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="reading-left-channel">
<item-type>Switch</item-type>
<label>Reading Light Left</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="reading-right-channel">
<item-type>Switch</item-type>
<label>Reading Light Right</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="light-switch-channel">
<item-type>Number</item-type>
<label>Main Light Rotary</label>
<state readOnly="true">
<options>
<option value="0">Auto</option>
<option value="1">Headlight</option>
<option value="2">Sidelight Left</option>
<option value="3">Sidelight Right</option>
<option value="4">Parking Light</option>
</options>
</state>
</channel-type>
<channel-type id="last-lights-update-channel">
<item-type>DateTime</item-type>
<label>Last Light Update</label>
<state pattern="%1$tA, %1$td.%1$tm. %1$tH:%1$tM" readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mercedesme"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-group-type id="light-values">
<label>Light Status</label>
<description>Light Status of interior lights and main light switch</description>
<channels>
<channel id="interior-front" typeId="interior-front-channel"/>
<channel id="interior-rear" typeId="interior-rear-channel"/>
<channel id="light-switch" typeId="light-switch-channel"/>
<channel id="reading-left" typeId="reading-left-channel"/>
<channel id="reading-right" typeId="reading-right-channel"/>
<channel id="last-update" typeId="last-lights-update-channel"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mercedesme"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-group-type id="location-values">
<label>Vehicle Location</label>
<description>Heading of vehicle</description>
<channels>
<channel id="heading" typeId="heading-channel"/>
<channel id="last-update" typeId="last-location-update-channel"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mercedesme"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-type id="heading-channel">
<item-type>Number:Angle</item-type>
<label>Heading Angle</label>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="last-location-update-channel">
<item-type>DateTime</item-type>
<label>Last Location Update</label>
<state pattern="%1$tA, %1$td.%1$tm. %1$tH:%1$tM" readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mercedesme"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-type id="doors-lock-channel">
<item-type>Number</item-type>
<label>Door Lock Status</label>
<state readOnly="true">
<options>
<option value="0">Unlocked</option>
<option value="1">Locked Internal</option>
<option value="2">Locked External</option>
<option value="3">Unlocked Selective</option>
</options>
</state>
</channel-type>
<channel-type id="deck-lid-lock-channel">
<item-type>Switch</item-type>
<label>Deck Lid Lock</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="flap-lock-channel">
<item-type>Switch</item-type>
<label>Flap Lock</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="last-lock-update-channel">
<item-type>DateTime</item-type>
<label>Last Lock Update</label>
<state pattern="%1$tA, %1$td.%1$tm. %1$tH:%1$tM" readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mercedesme"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-group-type id="lock-values">
<label>Lock Status</label>
<description>Vehicle Lock Status</description>
<channels>
<channel id="doors" typeId="doors-lock-channel"/>
<channel id="deck-lid" typeId="deck-lid-lock-channel"/>
<channel id="flap" typeId="flap-lock-channel"/>
<channel id="last-update" typeId="last-lock-update-channel"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mercedesme"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-type id="mileage-channel">
<item-type>Number:Length</item-type>
<label>Mileage</label>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="range-electric-channel">
<item-type>Number:Length</item-type>
<label>Electric Range</label>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="radius-electric-channel">
<item-type>Number:Length</item-type>
<label>Electric Radius</label>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="soc-channel">
<item-type>Number:Dimensionless</item-type>
<label>Battery Charge Level</label>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="charged-channel">
<item-type>Number:Energy</item-type>
<label>Charged Battery Energy</label>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="uncharged-channel">
<item-type>Number:Energy</item-type>
<label>Uncharged Battery Energy</label>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="range-fuel-channel">
<item-type>Number:Length</item-type>
<label>Fuel Range</label>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="radius-fuel-channel">
<item-type>Number:Length</item-type>
<label>Fuel Radius</label>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fuel-level-channel">
<item-type>Number:Dimensionless</item-type>
<label>Fuel Level</label>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fuel-remain-channel">
<item-type>Number:Volume</item-type>
<label>Remaining Fuel</label>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fuel-open-channel">
<item-type>Number:Volume</item-type>
<label>Open Fuel Capacity</label>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="range-hybrid-channel">
<item-type>Number:Length</item-type>
<label>Hybrid Range</label>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="radius-hybrid-channel">
<item-type>Number:Length</item-type>
<label>Hybrid Radius</label>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="last-range-update-channel">
<item-type>DateTime</item-type>
<label>Last Range Update</label>
<state pattern="%1$tA, %1$td.%1$tm. %1$tH:%1$tM" readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mercedesme"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-group-type id="range-conv-values">
<label>Range and Fuel Data</label>
<description>Provides Mileage, remaining range and fuel level values</description>
<channels>
<channel id="mileage" typeId="mileage-channel"/>
<channel id="range-fuel" typeId="range-fuel-channel"/>
<channel id="radius-fuel" typeId="radius-fuel-channel"/>
<channel id="fuel-level" typeId="fuel-level-channel"/>
<channel id="fuel-remain" typeId="fuel-remain-channel"/>
<channel id="fuel-open" typeId="fuel-open-channel"/>
<channel id="last-update" typeId="last-range-update-channel"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mercedesme"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-group-type id="range-ev-values">
<label>Range and Charge Data</label>
<description>Provides Mileage, remaining range and charge level values</description>
<channels>
<channel id="mileage" typeId="mileage-channel"/>
<channel id="range-electric" typeId="range-electric-channel"/>
<channel id="radius-electric" typeId="radius-electric-channel"/>
<channel id="soc" typeId="soc-channel"/>
<channel id="charged" typeId="charged-channel"/>
<channel id="uncharged" typeId="uncharged-channel"/>
<channel id="last-update" typeId="last-range-update-channel"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mercedesme"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-group-type id="range-hybrid-values">
<label>Range, Charge / Fuel Data</label>
<description>Provides mileage, remaining fuel and range data for hybrid vehicles</description>
<channels>
<channel id="mileage" typeId="mileage-channel"/>
<channel id="range-electric" typeId="range-electric-channel"/>
<channel id="radius-electric" typeId="radius-electric-channel"/>
<channel id="soc" typeId="soc-channel"/>
<channel id="charged" typeId="charged-channel"/>
<channel id="uncharged" typeId="uncharged-channel"/>
<channel id="range-fuel" typeId="range-fuel-channel"/>
<channel id="radius-fuel" typeId="radius-fuel-channel"/>
<channel id="fuel-level" typeId="fuel-level-channel"/>
<channel id="fuel-remain" typeId="fuel-remain-channel"/>
<channel id="fuel-open" typeId="fuel-open-channel"/>
<channel id="range-hybrid" typeId="range-hybrid-channel"/>
<channel id="radius-hybrid" typeId="radius-fuel-channel"/>
<channel id="last-update" typeId="last-range-update-channel"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mercedesme"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="bev">
<supported-bridge-type-refs>
<bridge-type-ref id="account"/>
</supported-bridge-type-refs>
<label>Mercedes Benz BEV</label>
<description>Battery Electric Vehicle</description>
<channel-groups>
<channel-group id="range" typeId="range-ev-values"/>
<channel-group id="doors" typeId="door-values"/>
<channel-group id="windows" typeId="window-values"/>
<channel-group id="lights" typeId="light-values"/>
<channel-group id="lock" typeId="lock-values"/>
<channel-group id="location" typeId="location-values"/>
<channel-group id="image" typeId="image-values"/>
</channel-groups>
<config-description-ref uri="thing-type:mercedesme:bev"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mercedesme"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="combustion">
<supported-bridge-type-refs>
<bridge-type-ref id="account"/>
</supported-bridge-type-refs>
<label>Mercedes Benz</label>
<description>Conventional Fuel Vehicle</description>
<channel-groups>
<channel-group id="range" typeId="range-conv-values"/>
<channel-group id="doors" typeId="door-values"/>
<channel-group id="windows" typeId="window-values"/>
<channel-group id="lights" typeId="light-values"/>
<channel-group id="lock" typeId="lock-values"/>
<channel-group id="location" typeId="location-values"/>
<channel-group id="image" typeId="image-values"/>
</channel-groups>
<config-description-ref uri="thing-type:mercedesme:conv"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mercedesme"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="hybrid">
<supported-bridge-type-refs>
<bridge-type-ref id="account"/>
</supported-bridge-type-refs>
<label>Mercedes Benz Hybrid</label>
<description>Conventional Fuel Vehicle with supporting Electric Engine</description>
<channel-groups>
<channel-group id="range" typeId="range-hybrid-values"/>
<channel-group id="doors" typeId="door-values"/>
<channel-group id="windows" typeId="window-values"/>
<channel-group id="lights" typeId="light-values"/>
<channel-group id="lock" typeId="lock-values"/>
<channel-group id="location" typeId="location-values"/>
<channel-group id="image" typeId="image-values"/>
</channel-groups>
<config-description-ref uri="thing-type:mercedesme:hybrid"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mercedesme"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-type id="window-driver-front-channel">
<item-type>Number</item-type>
<label>Driver Window</label>
<state readOnly="true">
<options>
<option value="0">Intermediate</option>
<option value="1">Open</option>
<option value="2">Closed</option>
<option value="3">Airing</option>
<option value="4">Intermediate</option>
<option value="5">Running</option>
</options>
</state>
</channel-type>
<channel-type id="window-driver-rear-channel">
<item-type>Number</item-type>
<label>Driver Window Rear</label>
<state readOnly="true">
<options>
<option value="0">Intermediate</option>
<option value="1">Open</option>
<option value="2">Closed</option>
<option value="3">Airing</option>
<option value="4">Intermediate</option>
<option value="5">Running</option>
</options>
</state>
</channel-type>
<channel-type id="window-passenger-front-channel">
<item-type>Number</item-type>
<label>Passenger Window</label>
<state readOnly="true">
<options>
<option value="0">Intermediate</option>
<option value="1">Open</option>
<option value="2">Closed</option>
<option value="3">Airing</option>
<option value="4">Intermediate</option>
<option value="5">Running</option>
</options>
</state>
</channel-type>
<channel-type id="window-passenger-rear-channel">
<item-type>Number</item-type>
<label>Passenger Window Rear</label>
<state readOnly="true">
<options>
<option value="0">Intermediate</option>
<option value="1">Open</option>
<option value="2">Closed</option>
<option value="3">Airing</option>
<option value="4">Intermediate</option>
<option value="5">Running</option>
</options>
</state>
</channel-type>
<channel-type id="last-windows-update-channel">
<item-type>DateTime</item-type>
<label>Last Window Update</label>
<state pattern="%1$tA, %1$td.%1$tm. %1$tH:%1$tM" readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mercedesme"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-group-type id="window-values">
<label>Detailed Window Status</label>
<description>Detailed Status Windows</description>
<channels>
<channel id="driver-front" typeId="window-driver-front-channel"/>
<channel id="driver-rear" typeId="window-driver-rear-channel"/>
<channel id="passenger-front" typeId="window-passenger-front-channel"/>
<channel id="passenger-rear" typeId="window-passenger-rear-channel"/>
<channel id="last-update" typeId="last-windows-update-channel"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,71 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mercedesme;
import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import java.net.InetAddress;
import java.net.SocketException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.mercedesme.internal.Constants;
import org.openhab.binding.mercedesme.internal.config.AccountConfiguration;
import org.openhab.binding.mercedesme.internal.server.Utils;
/**
* The {@link ConfigurationTest} Test configuration settings
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
class ConfigurationTest {
@Test
void testScope() {
AccountConfiguration ac = new AccountConfiguration();
assertEquals(
"offline_access mb:vehicle:mbdata:payasyoudrive mb:vehicle:mbdata:vehiclestatus mb:vehicle:mbdata:vehiclelock mb:vehicle:mbdata:fuelstatus mb:vehicle:mbdata:evstatus",
ac.getScope());
}
@Test
void testApiUrlEndpoint() {
String url = Constants.FUEL_URL;
String[] endpoint = url.split("/");
String finalEndpoint = endpoint[endpoint.length - 1];
assertEquals("fuelstatus", finalEndpoint);
}
@Test
void testRound() {
int socValue = 66;
double batteryCapacity = 66.5;
float chargedValue = Math.round(socValue * 1000 * (float) batteryCapacity / 1000) / (float) 100;
assertEquals(43.89, chargedValue, 0.01);
float unchargedValue = Math.round((100 - socValue) * 1000 * (float) batteryCapacity / 1000) / (float) 100;
assertEquals(22.61, unchargedValue, 0.01);
assertEquals(batteryCapacity, chargedValue + unchargedValue, 0.01);
}
@Test
public void testCallbackUrl() throws SocketException {
String ip = Utils.getCallbackIP();
try {
assertTrue(InetAddress.getByName(ip).isReachable(10));
} catch (IOException e) {
assertTrue(false, "IP " + ip + " not reachable");
}
}
}

View File

@ -0,0 +1,57 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mercedesme;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.UrlEncoded;
import org.junit.jupiter.api.Test;
import org.openhab.binding.mercedesme.internal.config.VehicleConfiguration;
/**
* The {@link ImageTest} Test Image conversions
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
class ImageTest {
@Test
public void testConfig() {
Optional<VehicleConfiguration> config = Optional.of(new VehicleConfiguration());
MultiMap<String> parameterMap = new MultiMap<String>();
parameterMap.add("background", Boolean.toString(config.get().background));
parameterMap.add("night", Boolean.toString(config.get().night));
parameterMap.add("cropped", Boolean.toString(config.get().cropped));
parameterMap.add("roofOpen", Boolean.toString(config.get().roofOpen));
parameterMap.add("format", config.get().format);
String params = UrlEncoded.encode(parameterMap, null, false);
assertEquals("background=false&night=false&cropped=false&roofOpen=false&format=webp", params);
config.get().background = true;
config.get().format = "png";
config.get().cropped = true;
parameterMap = new MultiMap<String>();
parameterMap.add("background", Boolean.toString(config.get().background));
parameterMap.add("night", Boolean.toString(config.get().night));
parameterMap.add("cropped", Boolean.toString(config.get().cropped));
parameterMap.add("roofOpen", Boolean.toString(config.get().roofOpen));
parameterMap.add("format", config.get().format);
params = UrlEncoded.encode(parameterMap, null, false);
assertEquals("background=true&night=false&cropped=true&roofOpen=false&format=png", params);
}
}

View File

@ -0,0 +1,250 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mercedesme;
import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import org.openhab.binding.mercedesme.internal.utils.ChannelStateMap;
import org.openhab.binding.mercedesme.internal.utils.Mapper;
/**
* The {@link JsonTest} Test Json conversions
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
class JsonTest {
public static final String DATE_INPUT_PATTERN_STRING = "yyyy-MM-dd'T'HH:mm:ss";
public static final DateTimeFormatter DATE_INPUT_PATTERN = DateTimeFormatter.ofPattern(DATE_INPUT_PATTERN_STRING);
@Test
void testOdoMapper() throws Exception {
List<String> expectedResults = new ArrayList<String>();
expectedResults.add("range:mileage 4131 km");
String content = Files.readString(Path.of("src/test/resources/odo.json"));
JSONArray ja = new JSONArray(content);
assertTrue(ja.length() > 0);
ja.forEach(entry -> {
JSONObject jo = (JSONObject) entry;
ChannelStateMap csm = Mapper.getChannelStateMap(jo);
assertNotNull(csm);
assertTrue(expectedResults.contains(csm.toString()));
boolean removed = expectedResults.remove(csm.toString());
if (!removed) {
assertTrue(false, csm.toString() + " not removed");
}
});
assertEquals(0, expectedResults.size(), "All content delivered");
}
@Test
void testEVMapper() throws IOException {
List<String> expectedResults = new ArrayList<String>();
expectedResults.add("range:range-electric 325 km");
expectedResults.add("range:soc 78 %");
String content = Files.readString(Path.of("src/test/resources/evstatus.json"));
JSONArray ja = new JSONArray(content);
assertTrue(ja.length() > 0);
ja.forEach(entry -> {
JSONObject jo = (JSONObject) entry;
ChannelStateMap csm = Mapper.getChannelStateMap(jo);
assertNotNull(csm);
assertTrue(expectedResults.contains(csm.toString()));
boolean removed = expectedResults.remove(csm.toString());
if (!removed) {
assertTrue(false, csm.toString() + " not removed");
}
});
assertEquals(0, expectedResults.size(), "All content delivered");
}
@Test
void testFuelMapper() throws IOException {
List<String> expectedResults = new ArrayList<String>();
expectedResults.add("range:range-fuel 1292 km");
expectedResults.add("range:fuel-level 90 %");
String content = Files.readString(Path.of("src/test/resources/fuel.json"));
JSONArray ja = new JSONArray(content);
assertTrue(ja.length() > 0);
ja.forEach(entry -> {
JSONObject jo = (JSONObject) entry;
ChannelStateMap csm = Mapper.getChannelStateMap(jo);
assertNotNull(csm);
assertTrue(expectedResults.contains(csm.toString()));
boolean removed = expectedResults.remove(csm.toString());
if (!removed) {
assertTrue(false, csm.toString() + " not removed");
}
});
}
@Test
void testLockMapper() throws IOException {
List<String> expectedResults = new ArrayList<String>();
expectedResults.add("lock:doors 0");
expectedResults.add("lock:deck-lid ON");
expectedResults.add("lock:flap ON");
expectedResults.add("location:heading 120 °");
String content = Files.readString(Path.of("src/test/resources/lock.json"));
JSONArray ja = new JSONArray(content);
assertTrue(ja.length() > 0);
ja.forEach(entry -> {
JSONObject jo = (JSONObject) entry;
ChannelStateMap csm = Mapper.getChannelStateMap(jo);
assertNotNull(csm);
assertTrue(expectedResults.contains(csm.toString()));
boolean removed = expectedResults.remove(csm.toString());
if (!removed) {
assertTrue(false, csm.toString() + " not removed");
}
});
}
@Test
void testStatusMapper() throws IOException {
List<String> expectedResults = new ArrayList<String>();
expectedResults.add("doors:deck-lid CLOSED");
expectedResults.add("doors:driver-front CLOSED");
expectedResults.add("doors:passenger-front CLOSED");
expectedResults.add("doors:driver-rear CLOSED");
expectedResults.add("doors:passenger-rear CLOSED");
expectedResults.add("lights:interior-front OFF");
expectedResults.add("lights:interior-rear OFF");
expectedResults.add("lights:light-switch 0");
expectedResults.add("lights:reading-left OFF");
expectedResults.add("lights:reading-right OFF");
expectedResults.add("doors:rooftop 0");
expectedResults.add("doors:sunroof 0");
expectedResults.add("windows:driver-front 0");
expectedResults.add("windows:passenger-front 0");
expectedResults.add("windows:driver-rear 0");
expectedResults.add("windows:passenger-rear 0");
String content = Files.readString(Path.of("src/test/resources/status.json"));
JSONArray ja = new JSONArray(content);
assertTrue(ja.length() > 0);
ja.forEach(entry -> {
JSONObject jo = (JSONObject) entry;
ChannelStateMap csm = Mapper.getChannelStateMap(jo);
assertNotNull(csm);
assertTrue(expectedResults.contains(csm.toString()));
boolean removed = expectedResults.remove(csm.toString());
if (!removed) {
assertTrue(false, csm.toString() + " not removed");
}
});
assertEquals(0, expectedResults.size(), "All content delivered");
}
@Test
void testEQALightsMapper() throws IOException {
// real life example
List<String> expectedResults = new ArrayList<String>();
expectedResults.add("doors:passenger-front OPEN");
expectedResults.add("windows:driver-front 1");
expectedResults.add("windows:driver-rear 1");
expectedResults.add("windows:passenger-rear 1");
expectedResults.add("windows:passenger-front 1");
expectedResults.add("lights:light-switch 0");
expectedResults.add("lights:reading-right ON");
expectedResults.add("lights:reading-left ON");
expectedResults.add("doors:driver-front CLOSED");
expectedResults.add("doors:driver-rear CLOSED");
String content = Files.readString(Path.of("src/test/resources/eqa-light-sample.json"));
JSONArray ja = new JSONArray(content);
assertTrue(ja.length() > 0);
ja.forEach(entry -> {
JSONObject jo = (JSONObject) entry;
ChannelStateMap csm = Mapper.getChannelStateMap(jo);
assertTrue(expectedResults.contains(csm.toString()));
boolean removed = expectedResults.remove(csm.toString());
if (!removed) {
assertTrue(false, csm.toString() + " not removed");
}
});
assertEquals(0, expectedResults.size(), "All content delivered");
}
@Test
void testTimeStamp() throws IOException {
String content = Files.readString(Path.of("src/test/resources/eqa-light-sample.json"));
JSONArray ja = new JSONArray(content);
assertTrue(ja.length() > 0);
long lastTimestamp = 0;
for (Iterator<Object> iterator = ja.iterator(); iterator.hasNext();) {
JSONObject jo = (JSONObject) iterator.next();
Set<String> s = jo.keySet();
if (!s.isEmpty()) {
String id = s.toArray()[0].toString();
JSONObject val = jo.getJSONObject(id);
if (val.has("timestamp")) {
lastTimestamp = val.getLong("timestamp");
}
}
}
Date d = new Date(lastTimestamp);
ZonedDateTime zdt = d.toInstant().atZone(ZoneId.of("Europe/Paris"));
assertEquals("2022-06-19T16:46:31", zdt.format(DATE_INPUT_PATTERN));
}
@Test
void testInvalidData() throws IOException {
String content = Files.readString(Path.of("src/test/resources/invalid-key.json"));
JSONArray ja = new JSONArray(content);
assertTrue(ja.length() > 0);
ja.forEach(entry -> {
JSONObject jo = (JSONObject) entry;
ChannelStateMap csm = Mapper.getChannelStateMap(jo);
assertNotNull(csm);
assertFalse(csm.isValid());
});
}
@Test
void testMissingTimestamp() throws IOException {
List<String> expectedResults = new ArrayList<String>();
expectedResults.add("range:mileage 4131 km");
String content = Files.readString(Path.of("src/test/resources/invalid-timestamp.json"));
JSONArray ja = new JSONArray(content);
assertTrue(ja.length() > 0);
ja.forEach(entry -> {
JSONObject jo = (JSONObject) entry;
ChannelStateMap csm = Mapper.getChannelStateMap(jo);
assertNotNull(csm);
assertTrue(expectedResults.contains(csm.toString()));
assertEquals(-1, csm.getTimestamp());
boolean removed = expectedResults.remove(csm.toString());
if (!removed) {
assertTrue(false, csm.toString() + " not removed");
}
});
assertEquals(0, expectedResults.size(), "All content delivered");
}
}

View File

@ -0,0 +1,62 @@
[
{
"doorstatusfrontright": {
"value": "true",
"timestamp": 1655650113000
}
},
{
"doorstatusfrontleft": {
"value": "false",
"timestamp": 1655650104000
}
},
{
"windowstatusfrontleft": {
"value": "1",
"timestamp": 1655648946000
}
},
{
"windowstatusrearleft": {
"value": "1",
"timestamp": 1655648959000
}
},
{
"windowstatusrearright": {
"value": "1",
"timestamp": 1655648959000
}
},
{
"doorstatusrearleft": {
"value": "false",
"timestamp": 1655498496000
}
},
{
"windowstatusfrontright": {
"value": "1",
"timestamp": 1655648953000
}
},
{
"lightswitchposition": {
"value": "0",
"timestamp": 1655650824000
}
},
{
"readingLampFrontRight": {
"value": "true",
"timestamp": 1655649991000
}
},
{
"readingLampFrontLeft": {
"value": "true",
"timestamp": 1655649991000
}
}
]

View File

@ -0,0 +1,14 @@
[
{
"rangeelectric": {
"value": "325",
"timestamp": 1655401822000
}
},
{
"soc": {
"value": "78",
"timestamp": 1655399096000
}
}
]

View File

@ -0,0 +1,14 @@
[
{
"tanklevelpercent": {
"value": "90",
"timestamp": 1541080800000
}
},
{
"rangeliquid": {
"value": "1292",
"timestamp": 1541080800000
}
}
]

View File

@ -0,0 +1,13 @@
{
"EXT000": "5jgA6wXiEiufaoAJcWWhQGALZUoltT2pnoXIFsCc6NBvSKz5wtbR3tfFFRzvg2aZWU9OeL75vJZpAQeh3jgzSDB1vXLyXEtErBzLP7WGVMu1Mvz5w6vf77fW_R_4vE57-FsZZQO8i4VCpAIvOhwTtV-2uvVw9qAtXQrAwpJz7r839JlvNf4uYyVFjiRjrE75_vDZA7bbkw33xDs2fSs5htgd39Zz3KfuKZvLmGW7qpCHRnr9lZixKCGydR8Hj4LM3XsEa0ebaCcdWOg4tuy0qN-YBH2-DrE7xQVso5iEiKIndBdA-ecF42jIYPdKk9x8gIKeSx-zcYFpwYXUZPtWo3vZQjqdiBVTxBVVK2JRYTOk9I0Qzp735Mqi5PtIYGrg2hIOH4F0CKuiknhc4XBabN22dAaxaUuf5juveQVD-h0WwV2xue6vQ8nvaTN2gyVpO4JXDmW9jE8kShKvDRZeh3JZKq8zGW4FfCP78rtyt8kc671d9PLcZBdi1_TgDMUBWldBP4xHrY_5aJvxn9nNji7E1A9Cw8tfmMyjuq0Wy3nsUHpctYXb0eXrrunj0-Uk_Aq2vx1PI1i8ko6-05uilE026reCDysoWY8Re2Ea1SRocp4P3B67Luqa__kgxqtdY3VdYtWuAuPrTESuHQZBEFa2EfNktvpZdXOYrvcqhGGbq76ybJXuQGB6ZwwiCi5ZFsq9ejYt9qcFYN_kxbpk6phLu-2yzqpcMEMMftFhZnY=",
"EXT090": "oHtxoRJTvmza2fm4AFhd2_fFYmRFC-uuZeQP7B8uENz9cCHgN9jMXS0bWGprZhIuhM65tPtdUSsvjoVqYD-n_HcQrqUB_PQkvr61RAiHz5UOXZSW7_5QPv_0Tdt3g1NHQFs5zPdI1KffucgiuNlhLNaWgpzT5z0AcZba9o7IwxzF5WKOkT4HsQ0etc3FYXvfnXeHi71F0223JkRQtlXEu4aNHELonubumptt9YJM0qXbcttPs-eNFE8l2eHOsKvh34g6X4Z2LZAENmABjMXQMQhxyWGRr_N7mIoeQWo6AVQ01QavM_MSKZY6Bw3-WdDHr8pybwg7uee52NgrwMsN7_ufeEG3xkAeSXixMhNt4yutYwaWkdxSdGKp4UnINj493VL8_5XAF7nfeHRioXe3XWU6mfg-PGzJwywm3ll7qbp_8OGkws3u03xXKxINkgZWVjmAtOnmFm80XGTQL8HxsPspI2ItF1QQDyQhvYYrGe-saxRstxxr1HM2VkualA3hxlNkE5CSNfbm3B1fUSH25Ba-YBo4SluEsu5mQIOemT1NBqMMZeQBCiO_73PwgIoRZnjH3cRPl_oUO3jwQEPPqH-jXBdFdQ9mksVWrHBcAQrVPAY5QQYaXN0Io8Va0q11GR8RUj_avraROJ_dLlWUDJCU1DUTv677lifWhIxyDXHj4CfgartLyYAPsGWuXm7wLc41dOXuS-WsiJoJjwuK2S83oUfUVH56Hf6gwZhvK8E=",
"EXT150": "FyyrR6D7K99MXXbCQ2Jt1N2GErxjQricdwDCJtzY5vW3TnqDr3JmEf5PUIy_yYnqscuiVLGTrBw1-9r6eR92uJncUPCkaNYHHIXt3p5ZfEu9TVcnURaydDH6GBB3yV9Cocv-8EeYbT2njjEe-n5qwhj44LfILKmMVC0lFzGRdixcAr1cu4chtnhvitIzOoCIG43BeTfWxjz4CwiIt3d4X05dS15qTWXFFlNCBc1xXGngdrqqO9USE3D8Hrq_0RuL_m4utANmLw2ud_rzlmpJFN7smkUJ62eKrpuJrn-aSrt3q9_OPewzC88-CpybvorcuVK9Oy_UZbU5aTFomUKH5DTEydJFBDeGLF6uZJSySKHWf395mmh1JBIxpghji4NhjONG8vlRKAZu9MbMQSWBiEJj2i1yWQQi9E8tYJN5g8uadP1SpSnIsM1FkDW8Esnu8bfqrYMQoobilCRkrmqaWCE4YDJcJRrf04oas12qqMZwLsRSvafSh7Cc6jIwggXxZ9u5d4wu9ov0oKfWMiBmvyt9Pb1kYxrt-nbL_pTkDOCAkBJw2AjiEFqcocMoCzx2uh1xQ8K9-DP4gGq4yKmY5EizbYMmYYhtorjKzmequ0BbKJ2YG-wIe65XA8i_dE5V-JeTL5CoSsRhoOBQK6hHtXk7PtgcdBV_0RAI85cnDSsoGNti2Nu3WT_S4PIstSSKrPpI9Yj6KeEV2EiM9a6flUkNo_QMwjKCuM3kCnvopjE=",
"EXT180": "h2nEDkftnTN7t3b93Rv-_KnASjdCQMiVbFWDIH4UQPlmDw44dH54TqJg1KgQFqVIojWgPRHLvh9DBfQCHh3wTpk-b_UGrsv22uWrm4DwVgC9ZMPaYM8nWUGmVfN2Aigzp8nH5gWNmRP2rE3rEPRC7W6E3E_-U8CHSEZ9rvld04eYbZEb-zdrcLmo9DhgzX6ETMq3Rac2ehXSO5KipM0t2-X5LtPXmdf1NcH_EBCSBW1bSrRCJr6RvGjRXvPiDFXR7QGhEYKHzIW6o0smbRp8BnIXnL7LlRe4cDH_M4Wk3wYTAsi6ET5gXed82Ubf2qtl-aHLL3_Su5DfyGAuzEc6rjcIljpZrgTE1UfcBxYgob2kYGr8iHBltgNhs69iAe2V_e6mS3ogQE8Hc2BMe9T8aLylmKtainufJKfzCOnyuMUDnBDNe84oKRNsyJHT8pKHewMJVxW8mOsm3sPC_nTb-UsUN9jqXmCugPqYdPAYtEp5mOpBpsltUX7a6LMcJERX0bGRGXYuGSpaFXCeynlQyTCnGJk2tgt65f4c2MGxg3OLWNh5FCQMq3miC8S2tIN15h2rw0vXrTYrvPXJPBk6Kjw74c0xUWtdf9EQwR79g0JPnjEUIeeeJvbipXPvZi3TPCev0d1eE3lOq_3onIZI16Noqjm7WHvUPGgn5g8QxDRI9t4aZ1MSKxxYrKUJBlJrMV7GmGHuLl_jYihCeslaXR5qyirZxB0r4KFyLeXuyJ4=",
"EXT270": "V1B7Vmu7mPFnDobcpvy5gVMAsnmi3sG_1AISG5nFaIhKw3h8NVwLYTAnptyiYtYaqcTzznk9jQROerdO0loInPHaBFN4lY59RoVQv7uA1ZMpQ4TUu_s7igbwgKEdcB55w3oei1ocLWcdn7QNvaxIqYl3ch5MAJlEOxhljGd5Hu4XHtmKe9IW7KZLqSwJKoI6M5HmiVY16UeqRcKJcKRaYoyaIq3jOJSUPfPJw22An2_oAp0aniq6Mydf3t-LNn8oDCPA5ztT9ivDhasZdWwk3dDF_IBEnwBc4X-UKGk04MRwnDkFnE-jmV5f4_k49VcuWLJEq2S5g-5lpZI55Et19lvGZv_tD1rsBMHcZ4beHwsUWrtykLSWRWt9XDVHk9cvgDAWeutKReMp3Rt3yN9gLMV-Puo9wscg51CuDvGXOsVO_V7vF2_1OEq5E6WJcDa9XqqphUrIhMs7l-s5gT16S4wYZ07BrhRWx1vCATJdP0Oo26QvE-tWthaPkcD1PUsePbx-dXrj-5IRsotGgoEzNRMmahkwNbc_SbILNVhCZXVMyo9h32n1Kdf-uNtF-KBb0aODmti2HV3qe52qILoKeO2XoIDMceNeVp_HjO6UL1np-tm4JOEqkF_as6VCGXfz2YE5cq8wIBfX9uKQwmPmtisK3tmfZKF6nuz4yxGo78cDJZdkE6xY8819ObqZyuFCDbONoxOkdA6KYUjjvhiPikDxzhFJXaq-tegNoDNWUJs=",
"EXT330": "-VkMlhWJez5VxQgKzZjpqaXFNL5wjxxAs9TGLMQCkmfrYqHs5qONanQHnzU6Dz7QQwb8a51qk7ULc3dMHtUMvx4ILa-vKpQYGZ2uGenWFIS7lK5YWjHUvHu3uGwgTbAfzigiLq13opE9a5lauK0e4zmJ4aMbWkhIMxHjrak83zsmjFJa0IYhD0qD_f7oH8z-eCJ9tww4cy-p3Rd_pJCfGYi7j3Kz_Z9-gwFe7WFdKLowzzMpCNHr8hQxujtWzZZVQVrH4RSUAZEaN8sMm8uhkR3mE_21wFPFLLG2-Ui3bvObEALZ8ajMUdKJNu0etHGfUTYnHUAWxwesBDu5rqgSXDbeZiN6-4FDSkfTl2IkXOXrtFrW1YzmPvR5SBjed_QuNKrDCvSi9NBxP1vq4by6xJUaSCPyBJb9SfVBswfk6-_2g_d6LNLoT9Qz4Xn9RRHlACNyZe6tb-NNCCHYxY7nz88cDO0Kfb0qkEXyoOXWeDjCLvRXI-dCHCqNSq2D54P_A7e_xtqsoy4qjWp_4EKc7QRDX1j9BN7_CfUtt4MuKLMskP1MpBm07aZFLP6eHwVw8So5cZRDMB1zeQcfjFcHzoZ5nVBvVEPT3UkPfKdFAPkfuf3p233QbJiTGTnbsI05xdQQHtEWbrHc7sf446DsT_oH6w9ej0KwQ41if7jzz5uMIsGTgtOfyGz23k-IUsJQH-T237GegQKNXG0_SetgL8gn0OADUkdkaGbvAAxqMjg=",
"INT1": "Kr29P2pI2LE9y2Y7S7kSD-EzhXs6sOfP-HVFAqoNbaIIb34CaauXwOlTz7NjjJ9xBg88y73aR-RGcIg_aqSqmwZvViIrs5esGXOJOvXROoWpodYsYK65XfmSD3o6PtkPXtTFEtfj_3OEgtmvqXuHLB17VXhPWB-GV5O4U4z8Wki7aZTzQi3Xh8ry9Or3GT7JD6z5JWGDFH3wHDkzNuTzuCR8SocQSDBlkH8uSTtl-ZuSQZczYyLuJcqff86niQZbaXEQGArqYifqW-hs3f0Xjm3-Pqvt8sMQcpDZsNLTOzz5GgExE5BDlMMewVlGHK6SkVmA5DDcXSpr0EQjoWjUET5rLDXNiNUMwk_gCJLFBE3qOP-dNFOYnPtyGCHOAt1fWHrLV-iDXBoyRm5nl7fvxGglECyEB1DVLDvOkd882z32G5OFy2oRMpq3qY1sccPE2L8i_lViNtKUJqRuwygkfEFrygQTaoFhqaGfnAp64a8PDsYMpBlHEl4-Y_m0l-7FhPQiYoYJBZDrKG5h37O9RZ8Jttt0jdzhtgHPnTiyz9LEdtBNMgqGr9QFYjSVnGSE3dAHpNuo6J4VeNTEGViVBj0Imqy5-s9N0VR2aziC4kqhQ2M4mRniu2ad6PycqJEWwP4YzlIKt6Pox9goDEhCpY8d9fuQv5ppj0butxtBDC_nw4eyWRlctlH8Lx-pfwjwQb08H3syFM6eCXCtHKGilsyeN2E7HZ3mq1LoprwZHUo=",
"INT2": "JFFG7zz5OlAgoUkJUUPXu9h29bBg03yCTOCCtGzQPWCW3cqS453QCVSGd_K-TwXuRYnG54ZLHRPdJ4HQjMTDUZv6ExZck9td5r27H5cpO6wpV7hofz4bW5Klj-9kKOcyF8wHTEwPnoDv1lMVSEL6mcVEXFB0CnzngMs-njRnEXdvhgMrprEoSu1hSeikP5kUaFViOBDAuv9-6V6cfpZehoMQhjBmq6WMTNjhDS-fGsZH0Z2jqu7Oe8yaoydD3tJXZ1KY8GRpf048piD9bv-URJPR5531f4t5b6t6l9sivHDW0wgDoa0K-aF5mRKj6k8Psuu_Krsvm4EAJHZCXyZ-ixg8i58X8TODy70HXS1wRGrLVOBqGYsTpMEWxLOl7YLrn3VUCVN8yYavIDNdxU9nr6o5dA187IqStMEUS7NDUxSWcwmFmpeYDImh4or95wa7cEk1-7w74HAyVg8sC8-VHfwgKPLA2QrSVTp-F9lOu4CmStzf7C7SU3mkcb6XmyHQRAavjiCzAI8eeFMgT8ma_8B62gS1XKmppEQwddz5bZ2f8eKSc4q5WKoq9scxur42d-7JjsSzuenPwVAd9MSqWa2q5qnVMTbWtuLlO9ipLXXGTTYacAQJjZ9dOEdJIIh8MsSteJ6tpas3bAoX15IfvLGuObJFs2vjQ1h4AonjTquPIdo1JAhjActBgNCurWElyBpJ3dprvQ_y9qXgt12kKM0SvWV1zky_p8x53UPcHHk=",
"INT3": "utnpbmDAfakTIjB-oSkx7eFoRjXxfxpbexmHUi2RPFCs5A3w8wtH0EM6cThnwEFHulWqSmXFc6v_hd3dyLNsKDAaIUHFktCoE3m5fX2ORgUi4tdXSrx2T8H0xpf0cfFZcg_aS9SK0fgtj2NYaWjoafEI6aLc79XNoiIpTyBONJi0QU6IjlLCA13a47QjVIkrQtPjIntm1EKY34Hyoq8QErZsZQG_f8JEXQMUfM-oOAY0U8gj0outMTtWRAnfdrgaSWhVyIWloUJkXP6cWhZsypKp7e8bm-poc4nE4R-tniMWegY7BLlvtJeSjs71txBaDomr9xqi9lhmX6ENkvGPAHpBbrTOmsMLvEJu6nQR7P08XYalXpiJQjrIQJ77hE499jOIdgSoBjuy6JKlaLc4ejpHeGNXYCPUBJM5KVLITLYpLOjgZFzbhzI8qr4GdjxcfW82PAxhlTDywjDH-eMzbXnVZ-YaLrEocNZ8i88R1bLnv90lUrwzgn7n3zUcTeiRILfkApNZRh_K9ClGSu612LzGv7E-_zN1JdOt4UCcfSSkJLMppbeyIXjE6tDsxjgUyRr0Xj1azvHtEDfFYKj76x9doyk8Xm6_sKF4z9rPrgEmo9u6zr4dhb6fmhTnBjdhWBFjQ1Zpe7P7IfaXeDFrWSQBt7jdADWzB5L8cjaMnULm295uSo1k_ka1FQtFg4NtHiZ2G_zNLINh4DBjZdo5hmqBW7tl4gnNWaUf8YO7J1A=",
"INT4": "kJ4tgroRxuaYe0vF9xMe-nI4NUy4Eg1NyAr_E-WxJKlt5jZuoOcWYijc7cQEOiY3xlRPLTnfN-l9nf4UWO9rN4uMIGH9tuEql1g2ATLxff1ML3tCiexoxn45l-LQRH-PL2bL8jsQ3ByiHxLdzMHUzAarWj76rZk4mDCM9tRAE-hQsUZaaGMbO_hL3BCu6J-mI9hU3em4RuFwNTzCfBxgOJShU_PAU3tt-xROhld5Re3cyZzQZfQI17xDJKpTdZmnZCeabv7BIjh6qEapxhCHi0Ue6_EX3DPld1Faj438FptfBTn8UXW9Es6tkM1BGHw8npbHtGVqI1g7DdWuYUb5qeAtUzkEnFpxIl-67sKctYGBWvGkRnZrpu33jwaorW7xInwSymTg0isVCDQCfTEsa8zszj-bD4UMF2jeWr_pL-g4Vk_Ns0vMDFhsZGJTLO9Vv5DNf0WYOMWBISHXgW9z_d0UzCwlJpojqqO1DNhEt1aRRSb_zIBhdfAUHmJy35KmA0tPiDNb1K6uEVVcKSQBD2G9BfrvGXq-o_E3JAUzO2-u4ues0eWseOa0dQQIasjMZ0ZNAakfxCwg7dHp9Lq0c_0fti37OAk69Cyi0EMThCTYr8Yij_sidT9sts-YeQgTniI2LFsMnDnGkWB7wubmxUvG9qOq5ZmonBArkOt8xHWT9Xy5xq0hQj6Ba6Dgcy8qrcCdq8HoIneUaHH_c7pkRW2V-MKHQ6gr9iopAtP1Ifw=",
"BET1": "hismr3VKvRu_fQ25jJjIjjBKs-nhdOYsbtMA7Sgc0-P99yjGnVvOq8nJBGFHcjCLHGb6lTcLyA4eVSflnKER2r6R35PqRznX63s1bDGZuuyPexBCTVZSh1ubtI3ImQuO5mRb9wFAyeUcG53JnZEXQmOnCdmLW8seIugeSrsj5-HC_Z7vOT-KTmcONFP2q2tbKqDTziLVXEJ0dy2EdgvSk8l-sn1QsgodDVe-FCaAp35jlswp8faJuyTv0j7luc5dRP7xvSVXX08uTD9AWHooVCbsGSJqwVRsjY0bFJMcvKqzCCE5xlyBVVmkbphD3aEsBqMVsG4wL2_W_pEgdbmI06qHgJK0sotwj0iEbGVBOYOuBp4uAQdadKYbQL2OkJaqXkDWJTy0MDUSRBbZ6qH3KCyVRfUFcxlFUxqw9I7l5G35vnDMCpoJyj6wyzyq4oFTKgDXkn6zMTDD7lgpYWW1zRDtCTecU6nkmsUQnwe-9EXBfcePlRtJasG40ykj5x4MRJQoDiX9F8VBDnquaR_8K5HOL3de8ypEl7q22bKkKv-pHEPO8hH5lZynfQ86RHyHhDGxXuNuCEX-ekqK7K8PGTF8gO_fIGMpp_Z5aqWSHQgu7Ge1R-FTtfKZ5L_E-RjnR9QWFqEIC2_hNiZoQBdSTC8XUMmnpltTBQJOngEBhmgGepekZmuGnP3xmmvVbK1VijHtgB6PCtIVaTIzkwpVo1o4aj25zQG_3VSqgT8Ufnw="
}

View File

@ -0,0 +1,8 @@
[
{
"wrong": {
"value": "4131",
"timestamp": 1655399236000
}
}
]

View File

@ -0,0 +1,7 @@
[
{
"odo": {
"value": "4131"
}
}
]

View File

@ -0,0 +1,26 @@
[
{
"doorlockstatusvehicle": {
"value": "0",
"timestamp": 1541080800000
}
},
{
"doorlockstatusdecklid": {
"value": "false",
"timestamp": 1541080800000
}
},
{
"doorlockstatusgas": {
"value": "false",
"timestamp": 1541080800000
}
},
{
"positionHeading": {
"value": "120",
"timestamp": 1541080800000
}
}
]

View File

@ -0,0 +1,8 @@
[
{
"odo": {
"value": "4131",
"timestamp": 1655399236000
}
}
]

View File

@ -0,0 +1,82 @@
[
{
"name": "decklidstatus",
"version": "1.0",
"href": "/vehicles/WDB111111ZZZ22222/resources/decklidstatus"
},
{
"name": "doorstatusfrontleft",
"version": "1.0",
"href": "/vehicles/WDB111111ZZZ22222/resources/doorstatusfrontleft"
},
{
"name": "doorstatusfrontright",
"version": "1.0",
"href": "/vehicles/WDB111111ZZZ22222/resources/doorstatusfrontright"
},
{
"name": "doorstatusrearleft",
"version": "1.0",
"href": "/vehicles/WDB111111ZZZ22222/resources/doorstatusrearleft"
},
{
"name": "doorstatusrearright",
"version": "1.0",
"href": "/vehicles/WDB111111ZZZ22222/resources/doorstatusrearright"
},
{
"name": "interiorLightsFront",
"version": "1.0",
"href": "/vehicles/WDB111111ZZZ22222/resources/interiorLightsFront"
},
{
"name": "interiorLightsRear",
"version": "1.0",
"href": "/vehicles/WDB111111ZZZ22222/resources/interiorLightsRear"
},
{
"name": "lightswitchposition",
"version": "1.0",
"href": "/vehicles/WDB111111ZZZ22222/resources/lightswitchposition"
},
{
"name": "readingLampFrontLeft",
"version": "1.0",
"href": "/vehicles/WDB111111ZZZ22222/resources/readingLampFrontLeft"
},
{
"name": "readingLampFrontRight",
"version": "1.0",
"href": "/vehicles/WDB111111ZZZ22222/resources/readingLampFrontRight"
},
{
"name": "rooftopstatus",
"version": "1.0",
"href": "/vehicles/WDB111111ZZZ22222/resources/rooftopstatus"
},
{
"name": "sunroofstatus",
"version": "1.0",
"href": "/vehicles/WDB111111ZZZ22222/resources/sunroofstatus"
},
{
"name": "windowstatusfrontleft",
"version": "1.0",
"href": "/vehicles/WDB111111ZZZ22222/resources/windowstatusfrontleft"
},
{
"name": "windowstatusfrontright",
"version": "1.0",
"href": "/vehicles/WDB111111ZZZ22222/resources/windowstatusfrontright"
},
{
"name": "windowstatusrearleft",
"version": "1.0",
"href": "/vehicles/WDB111111ZZZ22222/resources/windowstatusrearleft"
},
{
"name": "windowstatusrearright",
"version": "1.0",
"href": "/vehicles/WDB111111ZZZ22222/resources/windowstatusrearright"
}
]

View File

@ -0,0 +1,98 @@
[
{
"decklidstatus": {
"value": "false",
"timestamp": 1541080800000
}
},
{
"doorstatusfrontleft": {
"value": "false",
"timestamp": 1541080800000
}
},
{
"doorstatusfrontright": {
"value": "false",
"timestamp": 1541080800000
}
},
{
"doorstatusrearleft": {
"value": "false",
"timestamp": 1541080800000
}
},
{
"doorstatusrearright": {
"value": "false",
"timestamp": 1541080800000
}
},
{
"interiorLightsFront": {
"value": "false",
"timestamp": 1541080800000
}
},
{
"interiorLightsRear": {
"value": "false",
"timestamp": 1541080800000
}
},
{
"lightswitchposition": {
"value": "0",
"timestamp": 1541080800000
}
},
{
"readingLampFrontLeft": {
"value": "false",
"timestamp": 1541080800000
}
},
{
"readingLampFrontRight": {
"value": "false",
"timestamp": 1541080800000
}
},
{
"rooftopstatus": {
"value": "0",
"timestamp": 1541080800000
}
},
{
"sunroofstatus": {
"value": "0",
"timestamp": 1541080800000
}
},
{
"windowstatusfrontleft": {
"value": "0",
"timestamp": 1541080800000
}
},
{
"windowstatusfrontright": {
"value": "0",
"timestamp": 1541080800000
}
},
{
"windowstatusrearleft": {
"value": "0",
"timestamp": 1541080800000
}
},
{
"windowstatusrearright": {
"value": "0",
"timestamp": 1541080800000
}
}
]

View File

@ -215,6 +215,7 @@
<module>org.openhab.binding.mcp23017</module> <module>org.openhab.binding.mcp23017</module>
<module>org.openhab.binding.mecmeter</module> <module>org.openhab.binding.mecmeter</module>
<module>org.openhab.binding.melcloud</module> <module>org.openhab.binding.melcloud</module>
<module>org.openhab.binding.mercedesme</module>
<module>org.openhab.binding.meteoalerte</module> <module>org.openhab.binding.meteoalerte</module>
<module>org.openhab.binding.meteoblue</module> <module>org.openhab.binding.meteoblue</module>
<module>org.openhab.binding.meteostick</module> <module>org.openhab.binding.meteostick</module>