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

"); + response.getWriter().println("Configured scopes
"); + String[] scopes = CallbackServer.getScope(request.getLocalPort()).split(Constants.SPACE); + for (int i = 0; i < scopes.length; i++) { + response.getWriter().println(scopes[i] + "
"); + } + response.getWriter().println("

"); + response.getWriter().println("Get your access token for openHAB MercedesMe Binding"); + response.getWriter().println("
"); + response.getWriter().println("Start Authorization"); + response.getWriter().println(""); + response.getWriter().println(""); + } + logger.debug("Call from {}:{} parameters {}", request.getLocalAddr(), request.getLocalPort(), + request.getParameterMap()); + } +} diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/Utils.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/Utils.java new file mode 100644 index 000000000..0c98f72ac --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/Utils.java @@ -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 PORTS = new ArrayList(); + 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 ifaces = NetworkInterface.getNetworkInterfaces(); ifaces + .hasMoreElements();) { + NetworkInterface iface = ifaces.nextElement(); + try { + if (!iface.isLoopback()) { + if (iface.isUp()) { + for (Enumeration addresses = iface.getInetAddresses(); addresses + .hasMoreElements();) { + InetAddress address = addresses.nextElement(); + return address.getHostAddress(); + } + } + } + } catch (SocketException se) { + // Calling one network interface failed - continue searching + LOGGER.trace("Network {} failed {}", iface.getName(), se.getMessage()); + } + } + throw new SocketException("IP address not detected"); + } + + public static String getCallbackAddress(String callbackIP, int callbackPort) { + return "http://" + callbackIP + Constants.COLON + callbackPort + Constants.CALLBACK_ENDPOINT; + } +} diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/utils/ChannelStateMap.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/utils/ChannelStateMap.java new file mode 100644 index 000000000..71c1a0e84 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/utils/ChannelStateMap.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mercedesme.internal.utils; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.types.State; + +/** + * The {@link ChannelStateMap} holds the necessary values to update a channel state + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class ChannelStateMap { + private String channel; + private String group; + private State state; + private long timestamp; + + public ChannelStateMap(String ch, String grp, State st, long ts) { + channel = ch; + group = grp; + state = st; + timestamp = ts; + } + + public String getChannel() { + return channel; + } + + public String getGroup() { + return group; + } + + public State getState() { + return state; + } + + public long getTimestamp() { + return timestamp; + } + + @Override + public String toString() { + return group + ":" + channel + " " + state; + } + + public boolean isValid() { + return !channel.isEmpty(); + } +} diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/utils/Mapper.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/utils/Mapper.java new file mode 100644 index 000000000..512e843a7 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/utils/Mapper.java @@ -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 CHANNELS = new HashMap(); + public static final String TIMESTAMP = "timestamp"; + public static final String VALUE = "value"; + + public static ChannelStateMap getChannelStateMap(JSONObject jo) { + if (CHANNELS.isEmpty()) { + init(); + } + Set s = jo.keySet(); + if (s.size() == 1) { + String id = s.toArray()[0].toString(); + String[] ch = CHANNELS.get(id); + if (ch != null) { + State state; + switch (id) { + // Kilometer values + case "odo": + case "rangeelectric": + case "rangeliquid": + state = getKilometers((JSONObject) jo.get(id)); + return new ChannelStateMap(ch[0], ch[1], state, getTimestamp((JSONObject) jo.get(id))); + + // Percentages + case "soc": + case "tanklevelpercent": + state = getPercentage((JSONObject) jo.get(id)); + return new ChannelStateMap(ch[0], ch[1], state, getTimestamp((JSONObject) jo.get(id))); + + // Contacts + case "decklidstatus": + case "doorstatusfrontleft": + case "doorstatusfrontright": + case "doorstatusrearleft": + case "doorstatusrearright": + state = getContact((JSONObject) jo.get(id)); + return new ChannelStateMap(ch[0], ch[1], state, getTimestamp((JSONObject) jo.get(id))); + + // Number Status + case "lightswitchposition": + case "rooftopstatus": + case "sunroofstatus": + case "windowstatusfrontleft": + case "windowstatusfrontright": + case "windowstatusrearleft": + case "windowstatusrearright": + case "doorlockstatusvehicle": + state = getDecimal((JSONObject) jo.get(id)); + return new ChannelStateMap(ch[0], ch[1], state, getTimestamp((JSONObject) jo.get(id))); + + // Switches + case "interiorLightsFront": + case "interiorLightsRear": + case "readingLampFrontLeft": + case "readingLampFrontRight": + state = getOnOffType((JSONObject) jo.get(id)); + return new ChannelStateMap(ch[0], ch[1], state, getTimestamp((JSONObject) jo.get(id))); + + case "doorlockstatusdecklid": + case "doorlockstatusgas": + state = getOnOffTypeLock((JSONObject) jo.get(id)); + return new ChannelStateMap(ch[0], ch[1], state, getTimestamp((JSONObject) jo.get(id))); + + // Angle + case "positionHeading": + state = getAngle((JSONObject) jo.get(id)); + return new ChannelStateMap(ch[0], ch[1], state, getTimestamp((JSONObject) jo.get(id))); + default: + LOGGER.trace("No mapping available for {}", id); + } + } else { + LOGGER.trace("No mapping available for {}", id); + } + } else { + LOGGER.debug("More than one key found {}", s); + } + return INVALID_MAP; + } + + private static long getTimestamp(JSONObject jo) { + if (jo.has(TIMESTAMP)) { + return jo.getLong(TIMESTAMP); + } + return -1; + } + + private static State getOnOffType(JSONObject jo) { + if (jo.has(VALUE)) { + String value = jo.get(VALUE).toString(); + boolean b = Boolean.valueOf(value); + return OnOffType.from(b); + } else { + LOGGER.warn("JSONObject contains no value {}", jo); + return UnDefType.UNDEF; + } + } + + private static State getOnOffTypeLock(JSONObject jo) { + if (jo.has(VALUE)) { + String value = jo.get(VALUE).toString(); + boolean b = Boolean.valueOf(value); + // Yes, false is locked and true unlocked + // https://developer.mercedes-benz.com/products/vehicle_lock_status/specifications/vehicle_lock_status_api + return OnOffType.from(!b); + } else { + LOGGER.warn("JSONObject contains no value {}", jo); + return UnDefType.UNDEF; + } + } + + private static State getAngle(JSONObject jo) { + if (jo.has(VALUE)) { + String value = jo.get(VALUE).toString(); + return QuantityType.valueOf(Double.valueOf(value), Units.DEGREE_ANGLE); + } else { + LOGGER.warn("JSONObject contains no value {}", jo); + return UnDefType.UNDEF; + } + } + + private static State getDecimal(JSONObject jo) { + if (jo.has(VALUE)) { + String value = jo.get(VALUE).toString(); + return DecimalType.valueOf(value); + } else { + LOGGER.warn("JSONObject contains no value {}", jo); + return UnDefType.UNDEF; + } + } + + private static State getContact(JSONObject jo) { + if (jo.has(VALUE)) { + String value = jo.get(VALUE).toString(); + boolean b = Boolean.valueOf(value); + if (!b) { + return OpenClosedType.CLOSED; + } else { + return OpenClosedType.OPEN; + } + } else { + LOGGER.warn("JSONObject contains no value {}", jo); + return UnDefType.UNDEF; + } + } + + private static State getKilometers(JSONObject jo) { + if (jo.has(VALUE)) { + String value = jo.get(VALUE).toString(); + return QuantityType.valueOf(Integer.valueOf(value), KILOMETRE_UNIT); + } else { + LOGGER.warn("JSONObject contains no value {}", jo); + return UnDefType.UNDEF; + } + } + + private static State getPercentage(JSONObject jo) { + if (jo.has(VALUE)) { + String value = jo.get(VALUE).toString(); + return QuantityType.valueOf(Integer.valueOf(value), Units.PERCENT); + } else { + LOGGER.warn("JSONObject contains no value {}", jo); + return UnDefType.UNDEF; + } + } + + /** + * Mapping of json id towards channel group and id + */ + private static void init() { + CHANNELS.put("odo", new String[] { "mileage", GROUP_RANGE }); + CHANNELS.put("rangeelectric", new String[] { "range-electric", GROUP_RANGE }); + CHANNELS.put("soc", new String[] { "soc", GROUP_RANGE }); + CHANNELS.put("rangeliquid", new String[] { "range-fuel", GROUP_RANGE }); + CHANNELS.put("tanklevelpercent", new String[] { "fuel-level", GROUP_RANGE }); + CHANNELS.put("decklidstatus", new String[] { "deck-lid", GROUP_DOORS }); + CHANNELS.put("doorstatusfrontleft", new String[] { "driver-front", GROUP_DOORS }); + CHANNELS.put("doorstatusfrontright", new String[] { "passenger-front", GROUP_DOORS }); + CHANNELS.put("doorstatusrearleft", new String[] { "driver-rear", GROUP_DOORS }); + CHANNELS.put("doorstatusrearright", new String[] { "passenger-rear", GROUP_DOORS }); + CHANNELS.put("interiorLightsFront", new String[] { "interior-front", GROUP_LIGHTS }); + CHANNELS.put("interiorLightsRear", new String[] { "interior-rear", GROUP_LIGHTS }); + CHANNELS.put("lightswitchposition", new String[] { "light-switch", GROUP_LIGHTS }); + CHANNELS.put("readingLampFrontLeft", new String[] { "reading-left", GROUP_LIGHTS }); + CHANNELS.put("readingLampFrontRight", new String[] { "reading-right", GROUP_LIGHTS }); + CHANNELS.put("rooftopstatus", new String[] { "rooftop", GROUP_DOORS }); + CHANNELS.put("sunroofstatus", new String[] { "sunroof", GROUP_DOORS }); + CHANNELS.put("windowstatusfrontleft", new String[] { "driver-front", GROUP_WINDOWS }); + CHANNELS.put("windowstatusfrontright", new String[] { "passenger-front", GROUP_WINDOWS }); + CHANNELS.put("windowstatusrearleft", new String[] { "driver-rear", GROUP_WINDOWS }); + CHANNELS.put("windowstatusrearright", new String[] { "passenger-rear", GROUP_WINDOWS }); + CHANNELS.put("doorlockstatusvehicle", new String[] { "doors", GROUP_LOCK }); + CHANNELS.put("doorlockstatusdecklid", new String[] { "deck-lid", GROUP_LOCK }); + CHANNELS.put("doorlockstatusgas", new String[] { "flap", GROUP_LOCK }); + CHANNELS.put("positionHeading", new String[] { "heading", GROUP_LOCATION }); + } +} diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 000000000..e0b0cfa97 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,9 @@ + + + + Mercedes Me Binding + The binding provides access to your Mercedes developer account and vehicles + + diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/bev-config.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/bev-config.xml new file mode 100644 index 000000000..785291633 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/bev-config.xml @@ -0,0 +1,57 @@ + + + + + + + + + + Data refresh rate for vehicle data + 5 + + + + Battery capacity in kWh of vehicle + + + + + Vehicle images provided with or without background + false + true + + + + Vehicle images in night conditions + false + true + + + + Vehicle images in 4:3 instead of 16:9 + false + true + + + + Vehicle images with open roof (only Cabriolet) + false + true + + + + Preferred Image Format + webp + true + + + + + + + + diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/bridge-config.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/bridge-config.xml new file mode 100644 index 000000000..30f6b2049 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/bridge-config.xml @@ -0,0 +1,56 @@ + + + + + + + Mercedes Benz Developer Client ID + + + + Mercedes Benz Developer Client Secret + + + + Mercedes Benz Developer Image API Key + + + + Provides total Mileage + true + + + + Status of doors, windows lights + true + + + + Lock status of doors and trunk + true + + + + Tank level and range + true + + + + Electric charge and range + true + + + + IP address for openHAB callback URL + true + + + + Port Number for openHAB callback URL + true + + + diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/conv-config.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/conv-config.xml new file mode 100644 index 000000000..d4bc9869b --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/conv-config.xml @@ -0,0 +1,57 @@ + + + + + + + + + + Data refresh rate for your vehicle data + 5 + + + + Fuel capacity in liters of vehicle + + + + + Vehicle images provided with or without background + false + true + + + + Vehicle images in night conditions + false + true + + + + Vehicle images in 4:3 instead of 16:9 + false + true + + + + Vehicle images with open roof (only Cabriolet) + false + true + + + + Preferred Image Format + webp + true + + + + + + + + diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/hybrid-config.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/hybrid-config.xml new file mode 100644 index 000000000..55415dd1c --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/hybrid-config.xml @@ -0,0 +1,61 @@ + + + + + + + + + + Data refresh rate for vehicle data + 5 + + + + Battery capacity in kWh of vehicle + + + + Fuel capacity in liters of vehicle + + + + + Vehicle images provided with or without background + false + true + + + + Vehicle images in night conditions + false + true + + + + Vehicle images in 4:3 instead of 16:9 + false + true + + + + Vehicle images with open roof (only Cabriolet) + false + true + + + + Preferred Image Format + webp + true + + + + + + + + diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/i18n/mercedesme.properties b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/i18n/mercedesme.properties new file mode 100644 index 000000000..e9328cf48 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/i18n/mercedesme.properties @@ -0,0 +1,218 @@ +# binding + +binding.mercedesme.name = Mercedes Me Binding +binding.mercedesme.description = The binding provides access to your Mercedes developer account and vehicles + +# thing types + +thing-type.mercedesme.account.label = Mercedes Me Account +thing-type.mercedesme.account.description = Mercedes Benz account data +thing-type.mercedesme.bev.label = Mercedes Benz BEV +thing-type.mercedesme.bev.description = Battery Electric Vehicle +thing-type.mercedesme.combustion.label = Mercedes Benz +thing-type.mercedesme.combustion.description = Conventional Fuel Vehicle +thing-type.mercedesme.hybrid.label = Mercedes Benz Hybrid +thing-type.mercedesme.hybrid.description = Conventional Fuel Vehicle with supporting Electric Engine + +# thing types config + +thing-type.config.mercedesme.bev.background.label = Background Image +thing-type.config.mercedesme.bev.background.description = Vehicle images provided with or without background +thing-type.config.mercedesme.bev.batteryCapacity.label = Battery Capacity +thing-type.config.mercedesme.bev.batteryCapacity.description = Battery capacity in kwh of vehicle +thing-type.config.mercedesme.bev.cropped.label = Cropped Image +thing-type.config.mercedesme.bev.cropped.description = Vehicle images in 4:3 instead of 16:9 +thing-type.config.mercedesme.bev.format.label = Image Format +thing-type.config.mercedesme.bev.format.description = Preferred Image Format +thing-type.config.mercedesme.bev.format.option.webp = webp +thing-type.config.mercedesme.bev.format.option.png = png +thing-type.config.mercedesme.bev.format.option.jpeg = jpeg +thing-type.config.mercedesme.bev.night.label = Night Image +thing-type.config.mercedesme.bev.night.description = Vehicle images in night conditions +thing-type.config.mercedesme.bev.refreshInterval.label = Refresh Interval +thing-type.config.mercedesme.bev.refreshInterval.description = Data refresh rate for vehicle data +thing-type.config.mercedesme.bev.roofOpen.label = Cabriolet Open Roof +thing-type.config.mercedesme.bev.roofOpen.description = Vehicle images with open roof (only Cabriolet) +thing-type.config.mercedesme.bev.vin.label = Vehicle Identification Number +thing-type.config.mercedesme.bridge.callbackIP.label = Callback IP Address +thing-type.config.mercedesme.bridge.callbackIP.description = IP address for openHAB callback URL +thing-type.config.mercedesme.bridge.callbackPort.label = Callback Port Number +thing-type.config.mercedesme.bridge.callbackPort.description = Port Number for openHAB callback URL +thing-type.config.mercedesme.bridge.clientId.label = MB Developer Client ID +thing-type.config.mercedesme.bridge.clientId.description = Mercedes Benz Developer Client ID +thing-type.config.mercedesme.bridge.clientSecret.label = MB Developer Client Secret +thing-type.config.mercedesme.bridge.clientSecret.description = Mercedes Benz Developer Client Secret +thing-type.config.mercedesme.bridge.evScope.label = Electric Vehicle Status +thing-type.config.mercedesme.bridge.evScope.description = Electric charge and range +thing-type.config.mercedesme.bridge.fuelScope.label = Fuel Status +thing-type.config.mercedesme.bridge.fuelScope.description = Tank level and range +thing-type.config.mercedesme.bridge.imageApiKey.label = MB Developer Image API Key +thing-type.config.mercedesme.bridge.imageApiKey.description = Mercedes Benz Developer Image API Key +thing-type.config.mercedesme.bridge.lockScope.label = Vehicle Lock Status +thing-type.config.mercedesme.bridge.lockScope.description = Lock status of doors and trunk +thing-type.config.mercedesme.bridge.odoScope.label = PayAsYourDrive Insurance +thing-type.config.mercedesme.bridge.odoScope.description = Provides total Mileage +thing-type.config.mercedesme.bridge.vehicleScope.label = Vehicle Status +thing-type.config.mercedesme.bridge.vehicleScope.description = Status of doors, windows lights +thing-type.config.mercedesme.conv.background.label = Background Image +thing-type.config.mercedesme.conv.background.description = Vehicle images provided with or without background +thing-type.config.mercedesme.conv.cropped.label = Cropped Image +thing-type.config.mercedesme.conv.cropped.description = Vehicle images in 4:3 instead of 16:9 +thing-type.config.mercedesme.conv.format.label = Image Format +thing-type.config.mercedesme.conv.format.description = Preferred Image Format +thing-type.config.mercedesme.conv.format.option.webp = webp +thing-type.config.mercedesme.conv.format.option.png = png +thing-type.config.mercedesme.conv.format.option.jpeg = jpeg +thing-type.config.mercedesme.conv.fuelCapacity.label = Fuel Capacity +thing-type.config.mercedesme.conv.fuelCapacity.description = Fuel capacity in liters of vehicle +thing-type.config.mercedesme.conv.night.label = Night Image +thing-type.config.mercedesme.conv.night.description = Vehicle images in night conditions +thing-type.config.mercedesme.conv.refreshInterval.label = Refresh Interval +thing-type.config.mercedesme.conv.refreshInterval.description = Data refresh rate for your vehicle data +thing-type.config.mercedesme.conv.roofOpen.label = Cabriolet Open Roof +thing-type.config.mercedesme.conv.roofOpen.description = Vehicle images with open roof (only Cabriolet) +thing-type.config.mercedesme.conv.vin.label = Vehicle Identification Number +thing-type.config.mercedesme.hybrid.background.label = Background Image +thing-type.config.mercedesme.hybrid.background.description = Vehicle images provided with or without background +thing-type.config.mercedesme.hybrid.batteryCapacity.label = Battery Capacity +thing-type.config.mercedesme.hybrid.batteryCapacity.description = Battery capacity in kwh of vehicle +thing-type.config.mercedesme.hybrid.cropped.label = Cropped Image +thing-type.config.mercedesme.hybrid.cropped.description = Vehicle images in 4:3 instead of 16:9 +thing-type.config.mercedesme.hybrid.format.label = Image Format +thing-type.config.mercedesme.hybrid.format.description = Preferred Image Format +thing-type.config.mercedesme.hybrid.format.option.webp = webp +thing-type.config.mercedesme.hybrid.format.option.png = png +thing-type.config.mercedesme.hybrid.format.option.jpeg = jpeg +thing-type.config.mercedesme.hybrid.fuelCapacity.label = Fuel Capacity +thing-type.config.mercedesme.hybrid.fuelCapacity.description = Fuel capacity in liters of vehicle +thing-type.config.mercedesme.hybrid.night.label = Night Image +thing-type.config.mercedesme.hybrid.night.description = Vehicle images in night conditions +thing-type.config.mercedesme.hybrid.refreshInterval.label = Refresh Interval +thing-type.config.mercedesme.hybrid.refreshInterval.description = Data refresh rate for vehicle data +thing-type.config.mercedesme.hybrid.roofOpen.label = Cabriolet Open Roof +thing-type.config.mercedesme.hybrid.roofOpen.description = Vehicle images with open roof (only Cabriolet) +thing-type.config.mercedesme.hybrid.vin.label = Vehicle Identification Number + +# channel group types + +channel-group-type.mercedesme.door-values.label = Detailed Door Status +channel-group-type.mercedesme.door-values.description = Detailed Status of all Doors and Windows +channel-group-type.mercedesme.image-values.label = Vehicle Images +channel-group-type.mercedesme.light-values.label = Light Status +channel-group-type.mercedesme.light-values.description = Light Status of interior lights and main light switch +channel-group-type.mercedesme.location-values.label = Vehicle Location +channel-group-type.mercedesme.location-values.description = Heading of vehicle +channel-group-type.mercedesme.lock-values.label = Lock Status +channel-group-type.mercedesme.lock-values.description = Vehicle Lock Status +channel-group-type.mercedesme.range-conv-values.label = Range and Fuel Data +channel-group-type.mercedesme.range-conv-values.description = Provides Mileage, remaining range and fuel level values +channel-group-type.mercedesme.range-ev-values.label = Range and Charge Data +channel-group-type.mercedesme.range-ev-values.description = Provides Mileage, remaining range and charge level values +channel-group-type.mercedesme.range-hybrid-values.label = Range, Charge / Fuel Data +channel-group-type.mercedesme.range-hybrid-values.description = Provides mileage, remaining fuel and range data for hybrid vehicles +channel-group-type.mercedesme.window-values.label = Detailed Window Status +channel-group-type.mercedesme.window-values.description = Detailed Status Windows + +# channel types + +channel-type.mercedesme.charged-channel.label = Charged Battery Energy +channel-type.mercedesme.clear-cache-channel.label = Remove All Stored Images +channel-type.mercedesme.deck-lid-channel.label = Deck Lid +channel-type.mercedesme.deck-lid-lock-channel.label = Deck Lid Lock +channel-type.mercedesme.doors-lock-channel.label = Door Lock Status +channel-type.mercedesme.doors-lock-channel.state.option.0 = Unlocked +channel-type.mercedesme.doors-lock-channel.state.option.1 = Locked Internal +channel-type.mercedesme.doors-lock-channel.state.option.2 = Locked External +channel-type.mercedesme.doors-lock-channel.state.option.3 = Unlocked Selective +channel-type.mercedesme.driver-front-channel.label = Driver Door +channel-type.mercedesme.driver-rear-channel.label = Driver Door Rear +channel-type.mercedesme.flap-lock-channel.label = Flap Lock +channel-type.mercedesme.fuel-level-channel.label = Fuel Level +channel-type.mercedesme.fuel-open-channel.label = Open Fuel Capacity +channel-type.mercedesme.fuel-remain-channel.label = Remaining Fuel +channel-type.mercedesme.heading-channel.label = Heading Angle +channel-type.mercedesme.image-data-channel.label = Rendered Vehicle Image +channel-type.mercedesme.image-view-channel.label = Image Viewport +channel-type.mercedesme.interior-front-channel.label = Interior Light Front +channel-type.mercedesme.interior-rear-channel.label = Interior Light Rear +channel-type.mercedesme.last-doors-update-channel.label = Last Doors Update +channel-type.mercedesme.last-doors-update-channel.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM +channel-type.mercedesme.last-lights-update-channel.label = Last Light Update +channel-type.mercedesme.last-lights-update-channel.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM +channel-type.mercedesme.last-location-update-channel.label = Last Location Update +channel-type.mercedesme.last-location-update-channel.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM +channel-type.mercedesme.last-lock-update-channel.label = Last Lock Update +channel-type.mercedesme.last-lock-update-channel.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM +channel-type.mercedesme.last-range-update-channel.label = Last Range Update +channel-type.mercedesme.last-range-update-channel.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM +channel-type.mercedesme.last-windows-update-channel.label = Last Window Update +channel-type.mercedesme.last-windows-update-channel.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM +channel-type.mercedesme.light-switch-channel.label = Main Light Rotary +channel-type.mercedesme.light-switch-channel.state.option.0 = Auto +channel-type.mercedesme.light-switch-channel.state.option.1 = Headlight +channel-type.mercedesme.light-switch-channel.state.option.2 = Sidelight Left +channel-type.mercedesme.light-switch-channel.state.option.3 = Sidelight Right +channel-type.mercedesme.light-switch-channel.state.option.4 = Parking Light +channel-type.mercedesme.mileage-channel.label = Mileage +channel-type.mercedesme.passenger-front-channel.label = Passenger Door +channel-type.mercedesme.passenger-rear-channel.label = Passenger Door Rear +channel-type.mercedesme.radius-electric-channel.label = Electric Radius +channel-type.mercedesme.radius-fuel-channel.label = Fuel Radius +channel-type.mercedesme.radius-hybrid-channel.label = Hybrid Radius +channel-type.mercedesme.range-electric-channel.label = Electric Range +channel-type.mercedesme.range-fuel-channel.label = Fuel Range +channel-type.mercedesme.range-hybrid-channel.label = Hybrid Range +channel-type.mercedesme.reading-left-channel.label = Reading Light Left +channel-type.mercedesme.reading-right-channel.label = Reading Light Right +channel-type.mercedesme.rooftop-channel.label = Roof top +channel-type.mercedesme.rooftop-channel.state.option.0 = Unlocked +channel-type.mercedesme.rooftop-channel.state.option.1 = Open and locked +channel-type.mercedesme.rooftop-channel.state.option.2 = Closed and locked +channel-type.mercedesme.soc-channel.label = Battery Charge Level +channel-type.mercedesme.sunroof-channel.label = Sun Roof +channel-type.mercedesme.sunroof-channel.state.option.0 = Closed +channel-type.mercedesme.sunroof-channel.state.option.1 = Open +channel-type.mercedesme.sunroof-channel.state.option.2 = Open Lifting +channel-type.mercedesme.sunroof-channel.state.option.3 = Running +channel-type.mercedesme.sunroof-channel.state.option.4 = Closing +channel-type.mercedesme.sunroof-channel.state.option.5 = Opening +channel-type.mercedesme.sunroof-channel.state.option.6 = Closing +channel-type.mercedesme.uncharged-channel.label = Uncharged Battery Energy +channel-type.mercedesme.window-driver-front-channel.label = Driver Window +channel-type.mercedesme.window-driver-front-channel.state.option.0 = Intermediate +channel-type.mercedesme.window-driver-front-channel.state.option.1 = Open +channel-type.mercedesme.window-driver-front-channel.state.option.2 = Closed +channel-type.mercedesme.window-driver-front-channel.state.option.3 = Airing +channel-type.mercedesme.window-driver-front-channel.state.option.4 = Intermediate +channel-type.mercedesme.window-driver-front-channel.state.option.5 = Running +channel-type.mercedesme.window-driver-rear-channel.label = Driver Window Rear +channel-type.mercedesme.window-driver-rear-channel.state.option.0 = Intermediate +channel-type.mercedesme.window-driver-rear-channel.state.option.1 = Open +channel-type.mercedesme.window-driver-rear-channel.state.option.2 = Closed +channel-type.mercedesme.window-driver-rear-channel.state.option.3 = Airing +channel-type.mercedesme.window-driver-rear-channel.state.option.4 = Intermediate +channel-type.mercedesme.window-driver-rear-channel.state.option.5 = Running +channel-type.mercedesme.window-passenger-front-channel.label = Passenger Window +channel-type.mercedesme.window-passenger-front-channel.state.option.0 = Intermediate +channel-type.mercedesme.window-passenger-front-channel.state.option.1 = Open +channel-type.mercedesme.window-passenger-front-channel.state.option.2 = Closed +channel-type.mercedesme.window-passenger-front-channel.state.option.3 = Airing +channel-type.mercedesme.window-passenger-front-channel.state.option.4 = Intermediate +channel-type.mercedesme.window-passenger-front-channel.state.option.5 = Running +channel-type.mercedesme.window-passenger-rear-channel.label = Passenger Window Rear +channel-type.mercedesme.window-passenger-rear-channel.state.option.0 = Intermediate +channel-type.mercedesme.window-passenger-rear-channel.state.option.1 = Open +channel-type.mercedesme.window-passenger-rear-channel.state.option.2 = Closed +channel-type.mercedesme.window-passenger-rear-channel.state.option.3 = Airing +channel-type.mercedesme.window-passenger-rear-channel.state.option.4 = Intermediate +channel-type.mercedesme.window-passenger-rear-channel.state.option.5 = Running + +# MercedesMe Things Status Details +mercedesme.account.status.authorization-needed = Manual Authorization needed at {0} +mercedesme.account.status.ip-missing = Callback IP missing +mercedesme.account.status.port-missing = Callback Port missing +mercedesme.account.status.client-id-missing = Client ID missing +mercedesme.account.status.client-secret-missing = Client Secret missing +mercedesme.account.status.server-restart = Disable and enable Bridge to restart Authorization Server +mercedesme.vehicle.status.bridge-missing = Bridge not set +mercedesme.vehicle.status.bridge-authoriziation = Check Bridge Authorization diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/bridge-account.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/bridge-account.xml new file mode 100644 index 000000000..91c28fd75 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/bridge-account.xml @@ -0,0 +1,12 @@ + + + + + + Mercedes Benz account data + + + diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/door-channel-types.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/door-channel-types.xml new file mode 100644 index 000000000..c5341b4d9 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/door-channel-types.xml @@ -0,0 +1,62 @@ + + + + Contact + + + + + Contact + + + + + Contact + + + + + Contact + + + + + Contact + + + + + Number + + + + + + + + + + + Number + + + + + + + + + + + + + + + DateTime + + + + diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/doors-group.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/doors-group.xml new file mode 100644 index 000000000..8d6651098 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/doors-group.xml @@ -0,0 +1,20 @@ + + + + + Detailed Status of all Doors and Windows + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/image-channel-types.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/image-channel-types.xml new file mode 100644 index 000000000..b0e597ed9 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/image-channel-types.xml @@ -0,0 +1,19 @@ + + + + Image + + + + + String + + + + Switch + + + diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/image-group.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/image-group.xml new file mode 100644 index 000000000..2e1e701a5 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/image-group.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/light-channel-types.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/light-channel-types.xml new file mode 100644 index 000000000..029c20c70 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/light-channel-types.xml @@ -0,0 +1,44 @@ + + + + Switch + + + + + Switch + + + + + Switch + + + + + Switch + + + + + Number + + + + + + + + + + + + + DateTime + + + + diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/lights-group.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/lights-group.xml new file mode 100644 index 000000000..3caebd7ae --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/lights-group.xml @@ -0,0 +1,18 @@ + + + + + Light Status of interior lights and main light switch + + + + + + + + + + diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/location-channel-group.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/location-channel-group.xml new file mode 100644 index 000000000..c91bfef0d --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/location-channel-group.xml @@ -0,0 +1,14 @@ + + + + + Heading of vehicle + + + + + + diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/location-channel-types.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/location-channel-types.xml new file mode 100644 index 000000000..06dfc9844 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/location-channel-types.xml @@ -0,0 +1,16 @@ + + + + Number:Angle + + + + + DateTime + + + + diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/lock-channel-types.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/lock-channel-types.xml new file mode 100644 index 000000000..7ce9ccc5a --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/lock-channel-types.xml @@ -0,0 +1,33 @@ + + + + Number + + + + + + + + + + + + Switch + + + + + Switch + + + + + DateTime + + + + diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/lock-group.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/lock-group.xml new file mode 100644 index 000000000..cde1b07f9 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/lock-group.xml @@ -0,0 +1,16 @@ + + + + + Vehicle Lock Status + + + + + + + + diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/range-channel-types.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/range-channel-types.xml new file mode 100644 index 000000000..ac39ba02a --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/range-channel-types.xml @@ -0,0 +1,76 @@ + + + + Number:Length + + + + + Number:Length + + + + + Number:Length + + + + + Number:Dimensionless + + + + + Number:Energy + + + + + Number:Energy + + + + + Number:Length + + + + + Number:Length + + + + + Number:Dimensionless + + + + + Number:Volume + + + + + Number:Volume + + + + + Number:Length + + + + + Number:Length + + + + + DateTime + + + + diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/range-conv-channel-group.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/range-conv-channel-group.xml new file mode 100644 index 000000000..0599c93ba --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/range-conv-channel-group.xml @@ -0,0 +1,19 @@ + + + + + Provides Mileage, remaining range and fuel level values + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/range-ev-channel-group.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/range-ev-channel-group.xml new file mode 100644 index 000000000..4fbd83ebb --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/range-ev-channel-group.xml @@ -0,0 +1,19 @@ + + + + + Provides Mileage, remaining range and charge level values + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/range-hybrid-channel-group.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/range-hybrid-channel-group.xml new file mode 100644 index 000000000..a9ec537eb --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/range-hybrid-channel-group.xml @@ -0,0 +1,26 @@ + + + + + Provides mileage, remaining fuel and range data for hybrid vehicles + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/thing-bev.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/thing-bev.xml new file mode 100644 index 000000000..8964e7d33 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/thing-bev.xml @@ -0,0 +1,27 @@ + + + + + + + + + + Battery Electric Vehicle + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/thing-combustion.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/thing-combustion.xml new file mode 100644 index 000000000..672735d3b --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/thing-combustion.xml @@ -0,0 +1,27 @@ + + + + + + + + + + Conventional Fuel Vehicle + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/thing-hybrid.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/thing-hybrid.xml new file mode 100644 index 000000000..a84adc040 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/thing-hybrid.xml @@ -0,0 +1,27 @@ + + + + + + + + + + Conventional Fuel Vehicle with supporting Electric Engine + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/window-channel-types.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/window-channel-types.xml new file mode 100644 index 000000000..ec59ab2a6 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/window-channel-types.xml @@ -0,0 +1,67 @@ + + + + Number + + + + + + + + + + + + + + Number + + + + + + + + + + + + + + Number + + + + + + + + + + + + + + Number + + + + + + + + + + + + + + DateTime + + + + diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/window-group.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/window-group.xml new file mode 100644 index 000000000..aca8ffb40 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/thing/window-group.xml @@ -0,0 +1,17 @@ + + + + + Detailed Status Windows + + + + + + + + + diff --git a/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/ConfigurationTest.java b/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/ConfigurationTest.java new file mode 100644 index 000000000..d8361a751 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/ConfigurationTest.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mercedesme; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.SocketException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.mercedesme.internal.Constants; +import org.openhab.binding.mercedesme.internal.config.AccountConfiguration; +import org.openhab.binding.mercedesme.internal.server.Utils; + +/** + * The {@link ConfigurationTest} Test configuration settings + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +class ConfigurationTest { + + @Test + void testScope() { + AccountConfiguration ac = new AccountConfiguration(); + assertEquals( + "offline_access mb:vehicle:mbdata:payasyoudrive mb:vehicle:mbdata:vehiclestatus mb:vehicle:mbdata:vehiclelock mb:vehicle:mbdata:fuelstatus mb:vehicle:mbdata:evstatus", + ac.getScope()); + } + + @Test + void testApiUrlEndpoint() { + String url = Constants.FUEL_URL; + String[] endpoint = url.split("/"); + String finalEndpoint = endpoint[endpoint.length - 1]; + assertEquals("fuelstatus", finalEndpoint); + } + + @Test + void testRound() { + int socValue = 66; + double batteryCapacity = 66.5; + float chargedValue = Math.round(socValue * 1000 * (float) batteryCapacity / 1000) / (float) 100; + assertEquals(43.89, chargedValue, 0.01); + float unchargedValue = Math.round((100 - socValue) * 1000 * (float) batteryCapacity / 1000) / (float) 100; + assertEquals(22.61, unchargedValue, 0.01); + assertEquals(batteryCapacity, chargedValue + unchargedValue, 0.01); + } + + @Test + public void testCallbackUrl() throws SocketException { + String ip = Utils.getCallbackIP(); + try { + assertTrue(InetAddress.getByName(ip).isReachable(10)); + } catch (IOException e) { + assertTrue(false, "IP " + ip + " not reachable"); + } + } +} diff --git a/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/ImageTest.java b/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/ImageTest.java new file mode 100644 index 000000000..1b4556da0 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/ImageTest.java @@ -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 config = Optional.of(new VehicleConfiguration()); + MultiMap parameterMap = new MultiMap(); + parameterMap.add("background", Boolean.toString(config.get().background)); + parameterMap.add("night", Boolean.toString(config.get().night)); + parameterMap.add("cropped", Boolean.toString(config.get().cropped)); + parameterMap.add("roofOpen", Boolean.toString(config.get().roofOpen)); + parameterMap.add("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(); + parameterMap.add("background", Boolean.toString(config.get().background)); + parameterMap.add("night", Boolean.toString(config.get().night)); + parameterMap.add("cropped", Boolean.toString(config.get().cropped)); + parameterMap.add("roofOpen", Boolean.toString(config.get().roofOpen)); + parameterMap.add("format", config.get().format); + params = UrlEncoded.encode(parameterMap, null, false); + assertEquals("background=true&night=false&cropped=true&roofOpen=false&format=png", params); + } +} diff --git a/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/JsonTest.java b/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/JsonTest.java new file mode 100644 index 000000000..4be0d7214 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/JsonTest.java @@ -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 expectedResults = new ArrayList(); + 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 expectedResults = new ArrayList(); + 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 expectedResults = new ArrayList(); + 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 expectedResults = new ArrayList(); + 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 expectedResults = new ArrayList(); + 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 expectedResults = new ArrayList(); + 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 iterator = ja.iterator(); iterator.hasNext();) { + JSONObject jo = (JSONObject) iterator.next(); + Set 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 expectedResults = new ArrayList(); + expectedResults.add("range:mileage 4131 km"); + String content = Files.readString(Path.of("src/test/resources/invalid-timestamp.json")); + JSONArray ja = new JSONArray(content); + assertTrue(ja.length() > 0); + ja.forEach(entry -> { + JSONObject jo = (JSONObject) entry; + ChannelStateMap csm = Mapper.getChannelStateMap(jo); + assertNotNull(csm); + assertTrue(expectedResults.contains(csm.toString())); + assertEquals(-1, csm.getTimestamp()); + boolean removed = expectedResults.remove(csm.toString()); + if (!removed) { + assertTrue(false, csm.toString() + " not removed"); + } + }); + assertEquals(0, expectedResults.size(), "All content delivered"); + } +} diff --git a/bundles/org.openhab.binding.mercedesme/src/test/resources/eqa-light-sample.json b/bundles/org.openhab.binding.mercedesme/src/test/resources/eqa-light-sample.json new file mode 100644 index 000000000..8bc65899c --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/test/resources/eqa-light-sample.json @@ -0,0 +1,62 @@ +[ + { + "doorstatusfrontright": { + "value": "true", + "timestamp": 1655650113000 + } + }, + { + "doorstatusfrontleft": { + "value": "false", + "timestamp": 1655650104000 + } + }, + { + "windowstatusfrontleft": { + "value": "1", + "timestamp": 1655648946000 + } + }, + { + "windowstatusrearleft": { + "value": "1", + "timestamp": 1655648959000 + } + }, + { + "windowstatusrearright": { + "value": "1", + "timestamp": 1655648959000 + } + }, + { + "doorstatusrearleft": { + "value": "false", + "timestamp": 1655498496000 + } + }, + { + "windowstatusfrontright": { + "value": "1", + "timestamp": 1655648953000 + } + }, + { + "lightswitchposition": { + "value": "0", + "timestamp": 1655650824000 + } + }, + { + "readingLampFrontRight": { + "value": "true", + "timestamp": 1655649991000 + } + }, + { + "readingLampFrontLeft": { + "value": "true", + "timestamp": 1655649991000 + } + } +] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mercedesme/src/test/resources/evstatus.json b/bundles/org.openhab.binding.mercedesme/src/test/resources/evstatus.json new file mode 100644 index 000000000..907154a11 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/test/resources/evstatus.json @@ -0,0 +1,14 @@ +[ + { + "rangeelectric": { + "value": "325", + "timestamp": 1655401822000 + } + }, + { + "soc": { + "value": "78", + "timestamp": 1655399096000 + } + } +] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mercedesme/src/test/resources/fuel.json b/bundles/org.openhab.binding.mercedesme/src/test/resources/fuel.json new file mode 100644 index 000000000..a71eb02d2 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/test/resources/fuel.json @@ -0,0 +1,14 @@ +[ + { + "tanklevelpercent": { + "value": "90", + "timestamp": 1541080800000 + } + }, + { + "rangeliquid": { + "value": "1292", + "timestamp": 1541080800000 + } + } +] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mercedesme/src/test/resources/image/ext.json b/bundles/org.openhab.binding.mercedesme/src/test/resources/image/ext.json new file mode 100644 index 000000000..30b34dc4c --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/test/resources/image/ext.json @@ -0,0 +1,13 @@ +{ + "EXT000": "5jgA6wXiEiufaoAJcWWhQGALZUoltT2pnoXIFsCc6NBvSKz5wtbR3tfFFRzvg2aZWU9OeL75vJZpAQeh3jgzSDB1vXLyXEtErBzLP7WGVMu1Mvz5w6vf77fW_R_4vE57-FsZZQO8i4VCpAIvOhwTtV-2uvVw9qAtXQrAwpJz7r839JlvNf4uYyVFjiRjrE75_vDZA7bbkw33xDs2fSs5htgd39Zz3KfuKZvLmGW7qpCHRnr9lZixKCGydR8Hj4LM3XsEa0ebaCcdWOg4tuy0qN-YBH2-DrE7xQVso5iEiKIndBdA-ecF42jIYPdKk9x8gIKeSx-zcYFpwYXUZPtWo3vZQjqdiBVTxBVVK2JRYTOk9I0Qzp735Mqi5PtIYGrg2hIOH4F0CKuiknhc4XBabN22dAaxaUuf5juveQVD-h0WwV2xue6vQ8nvaTN2gyVpO4JXDmW9jE8kShKvDRZeh3JZKq8zGW4FfCP78rtyt8kc671d9PLcZBdi1_TgDMUBWldBP4xHrY_5aJvxn9nNji7E1A9Cw8tfmMyjuq0Wy3nsUHpctYXb0eXrrunj0-Uk_Aq2vx1PI1i8ko6-05uilE026reCDysoWY8Re2Ea1SRocp4P3B67Luqa__kgxqtdY3VdYtWuAuPrTESuHQZBEFa2EfNktvpZdXOYrvcqhGGbq76ybJXuQGB6ZwwiCi5ZFsq9ejYt9qcFYN_kxbpk6phLu-2yzqpcMEMMftFhZnY=", + "EXT090": "oHtxoRJTvmza2fm4AFhd2_fFYmRFC-uuZeQP7B8uENz9cCHgN9jMXS0bWGprZhIuhM65tPtdUSsvjoVqYD-n_HcQrqUB_PQkvr61RAiHz5UOXZSW7_5QPv_0Tdt3g1NHQFs5zPdI1KffucgiuNlhLNaWgpzT5z0AcZba9o7IwxzF5WKOkT4HsQ0etc3FYXvfnXeHi71F0223JkRQtlXEu4aNHELonubumptt9YJM0qXbcttPs-eNFE8l2eHOsKvh34g6X4Z2LZAENmABjMXQMQhxyWGRr_N7mIoeQWo6AVQ01QavM_MSKZY6Bw3-WdDHr8pybwg7uee52NgrwMsN7_ufeEG3xkAeSXixMhNt4yutYwaWkdxSdGKp4UnINj493VL8_5XAF7nfeHRioXe3XWU6mfg-PGzJwywm3ll7qbp_8OGkws3u03xXKxINkgZWVjmAtOnmFm80XGTQL8HxsPspI2ItF1QQDyQhvYYrGe-saxRstxxr1HM2VkualA3hxlNkE5CSNfbm3B1fUSH25Ba-YBo4SluEsu5mQIOemT1NBqMMZeQBCiO_73PwgIoRZnjH3cRPl_oUO3jwQEPPqH-jXBdFdQ9mksVWrHBcAQrVPAY5QQYaXN0Io8Va0q11GR8RUj_avraROJ_dLlWUDJCU1DUTv677lifWhIxyDXHj4CfgartLyYAPsGWuXm7wLc41dOXuS-WsiJoJjwuK2S83oUfUVH56Hf6gwZhvK8E=", + "EXT150": "FyyrR6D7K99MXXbCQ2Jt1N2GErxjQricdwDCJtzY5vW3TnqDr3JmEf5PUIy_yYnqscuiVLGTrBw1-9r6eR92uJncUPCkaNYHHIXt3p5ZfEu9TVcnURaydDH6GBB3yV9Cocv-8EeYbT2njjEe-n5qwhj44LfILKmMVC0lFzGRdixcAr1cu4chtnhvitIzOoCIG43BeTfWxjz4CwiIt3d4X05dS15qTWXFFlNCBc1xXGngdrqqO9USE3D8Hrq_0RuL_m4utANmLw2ud_rzlmpJFN7smkUJ62eKrpuJrn-aSrt3q9_OPewzC88-CpybvorcuVK9Oy_UZbU5aTFomUKH5DTEydJFBDeGLF6uZJSySKHWf395mmh1JBIxpghji4NhjONG8vlRKAZu9MbMQSWBiEJj2i1yWQQi9E8tYJN5g8uadP1SpSnIsM1FkDW8Esnu8bfqrYMQoobilCRkrmqaWCE4YDJcJRrf04oas12qqMZwLsRSvafSh7Cc6jIwggXxZ9u5d4wu9ov0oKfWMiBmvyt9Pb1kYxrt-nbL_pTkDOCAkBJw2AjiEFqcocMoCzx2uh1xQ8K9-DP4gGq4yKmY5EizbYMmYYhtorjKzmequ0BbKJ2YG-wIe65XA8i_dE5V-JeTL5CoSsRhoOBQK6hHtXk7PtgcdBV_0RAI85cnDSsoGNti2Nu3WT_S4PIstSSKrPpI9Yj6KeEV2EiM9a6flUkNo_QMwjKCuM3kCnvopjE=", + "EXT180": "h2nEDkftnTN7t3b93Rv-_KnASjdCQMiVbFWDIH4UQPlmDw44dH54TqJg1KgQFqVIojWgPRHLvh9DBfQCHh3wTpk-b_UGrsv22uWrm4DwVgC9ZMPaYM8nWUGmVfN2Aigzp8nH5gWNmRP2rE3rEPRC7W6E3E_-U8CHSEZ9rvld04eYbZEb-zdrcLmo9DhgzX6ETMq3Rac2ehXSO5KipM0t2-X5LtPXmdf1NcH_EBCSBW1bSrRCJr6RvGjRXvPiDFXR7QGhEYKHzIW6o0smbRp8BnIXnL7LlRe4cDH_M4Wk3wYTAsi6ET5gXed82Ubf2qtl-aHLL3_Su5DfyGAuzEc6rjcIljpZrgTE1UfcBxYgob2kYGr8iHBltgNhs69iAe2V_e6mS3ogQE8Hc2BMe9T8aLylmKtainufJKfzCOnyuMUDnBDNe84oKRNsyJHT8pKHewMJVxW8mOsm3sPC_nTb-UsUN9jqXmCugPqYdPAYtEp5mOpBpsltUX7a6LMcJERX0bGRGXYuGSpaFXCeynlQyTCnGJk2tgt65f4c2MGxg3OLWNh5FCQMq3miC8S2tIN15h2rw0vXrTYrvPXJPBk6Kjw74c0xUWtdf9EQwR79g0JPnjEUIeeeJvbipXPvZi3TPCev0d1eE3lOq_3onIZI16Noqjm7WHvUPGgn5g8QxDRI9t4aZ1MSKxxYrKUJBlJrMV7GmGHuLl_jYihCeslaXR5qyirZxB0r4KFyLeXuyJ4=", + "EXT270": "V1B7Vmu7mPFnDobcpvy5gVMAsnmi3sG_1AISG5nFaIhKw3h8NVwLYTAnptyiYtYaqcTzznk9jQROerdO0loInPHaBFN4lY59RoVQv7uA1ZMpQ4TUu_s7igbwgKEdcB55w3oei1ocLWcdn7QNvaxIqYl3ch5MAJlEOxhljGd5Hu4XHtmKe9IW7KZLqSwJKoI6M5HmiVY16UeqRcKJcKRaYoyaIq3jOJSUPfPJw22An2_oAp0aniq6Mydf3t-LNn8oDCPA5ztT9ivDhasZdWwk3dDF_IBEnwBc4X-UKGk04MRwnDkFnE-jmV5f4_k49VcuWLJEq2S5g-5lpZI55Et19lvGZv_tD1rsBMHcZ4beHwsUWrtykLSWRWt9XDVHk9cvgDAWeutKReMp3Rt3yN9gLMV-Puo9wscg51CuDvGXOsVO_V7vF2_1OEq5E6WJcDa9XqqphUrIhMs7l-s5gT16S4wYZ07BrhRWx1vCATJdP0Oo26QvE-tWthaPkcD1PUsePbx-dXrj-5IRsotGgoEzNRMmahkwNbc_SbILNVhCZXVMyo9h32n1Kdf-uNtF-KBb0aODmti2HV3qe52qILoKeO2XoIDMceNeVp_HjO6UL1np-tm4JOEqkF_as6VCGXfz2YE5cq8wIBfX9uKQwmPmtisK3tmfZKF6nuz4yxGo78cDJZdkE6xY8819ObqZyuFCDbONoxOkdA6KYUjjvhiPikDxzhFJXaq-tegNoDNWUJs=", + "EXT330": "-VkMlhWJez5VxQgKzZjpqaXFNL5wjxxAs9TGLMQCkmfrYqHs5qONanQHnzU6Dz7QQwb8a51qk7ULc3dMHtUMvx4ILa-vKpQYGZ2uGenWFIS7lK5YWjHUvHu3uGwgTbAfzigiLq13opE9a5lauK0e4zmJ4aMbWkhIMxHjrak83zsmjFJa0IYhD0qD_f7oH8z-eCJ9tww4cy-p3Rd_pJCfGYi7j3Kz_Z9-gwFe7WFdKLowzzMpCNHr8hQxujtWzZZVQVrH4RSUAZEaN8sMm8uhkR3mE_21wFPFLLG2-Ui3bvObEALZ8ajMUdKJNu0etHGfUTYnHUAWxwesBDu5rqgSXDbeZiN6-4FDSkfTl2IkXOXrtFrW1YzmPvR5SBjed_QuNKrDCvSi9NBxP1vq4by6xJUaSCPyBJb9SfVBswfk6-_2g_d6LNLoT9Qz4Xn9RRHlACNyZe6tb-NNCCHYxY7nz88cDO0Kfb0qkEXyoOXWeDjCLvRXI-dCHCqNSq2D54P_A7e_xtqsoy4qjWp_4EKc7QRDX1j9BN7_CfUtt4MuKLMskP1MpBm07aZFLP6eHwVw8So5cZRDMB1zeQcfjFcHzoZ5nVBvVEPT3UkPfKdFAPkfuf3p233QbJiTGTnbsI05xdQQHtEWbrHc7sf446DsT_oH6w9ej0KwQ41if7jzz5uMIsGTgtOfyGz23k-IUsJQH-T237GegQKNXG0_SetgL8gn0OADUkdkaGbvAAxqMjg=", + "INT1": "Kr29P2pI2LE9y2Y7S7kSD-EzhXs6sOfP-HVFAqoNbaIIb34CaauXwOlTz7NjjJ9xBg88y73aR-RGcIg_aqSqmwZvViIrs5esGXOJOvXROoWpodYsYK65XfmSD3o6PtkPXtTFEtfj_3OEgtmvqXuHLB17VXhPWB-GV5O4U4z8Wki7aZTzQi3Xh8ry9Or3GT7JD6z5JWGDFH3wHDkzNuTzuCR8SocQSDBlkH8uSTtl-ZuSQZczYyLuJcqff86niQZbaXEQGArqYifqW-hs3f0Xjm3-Pqvt8sMQcpDZsNLTOzz5GgExE5BDlMMewVlGHK6SkVmA5DDcXSpr0EQjoWjUET5rLDXNiNUMwk_gCJLFBE3qOP-dNFOYnPtyGCHOAt1fWHrLV-iDXBoyRm5nl7fvxGglECyEB1DVLDvOkd882z32G5OFy2oRMpq3qY1sccPE2L8i_lViNtKUJqRuwygkfEFrygQTaoFhqaGfnAp64a8PDsYMpBlHEl4-Y_m0l-7FhPQiYoYJBZDrKG5h37O9RZ8Jttt0jdzhtgHPnTiyz9LEdtBNMgqGr9QFYjSVnGSE3dAHpNuo6J4VeNTEGViVBj0Imqy5-s9N0VR2aziC4kqhQ2M4mRniu2ad6PycqJEWwP4YzlIKt6Pox9goDEhCpY8d9fuQv5ppj0butxtBDC_nw4eyWRlctlH8Lx-pfwjwQb08H3syFM6eCXCtHKGilsyeN2E7HZ3mq1LoprwZHUo=", + "INT2": "JFFG7zz5OlAgoUkJUUPXu9h29bBg03yCTOCCtGzQPWCW3cqS453QCVSGd_K-TwXuRYnG54ZLHRPdJ4HQjMTDUZv6ExZck9td5r27H5cpO6wpV7hofz4bW5Klj-9kKOcyF8wHTEwPnoDv1lMVSEL6mcVEXFB0CnzngMs-njRnEXdvhgMrprEoSu1hSeikP5kUaFViOBDAuv9-6V6cfpZehoMQhjBmq6WMTNjhDS-fGsZH0Z2jqu7Oe8yaoydD3tJXZ1KY8GRpf048piD9bv-URJPR5531f4t5b6t6l9sivHDW0wgDoa0K-aF5mRKj6k8Psuu_Krsvm4EAJHZCXyZ-ixg8i58X8TODy70HXS1wRGrLVOBqGYsTpMEWxLOl7YLrn3VUCVN8yYavIDNdxU9nr6o5dA187IqStMEUS7NDUxSWcwmFmpeYDImh4or95wa7cEk1-7w74HAyVg8sC8-VHfwgKPLA2QrSVTp-F9lOu4CmStzf7C7SU3mkcb6XmyHQRAavjiCzAI8eeFMgT8ma_8B62gS1XKmppEQwddz5bZ2f8eKSc4q5WKoq9scxur42d-7JjsSzuenPwVAd9MSqWa2q5qnVMTbWtuLlO9ipLXXGTTYacAQJjZ9dOEdJIIh8MsSteJ6tpas3bAoX15IfvLGuObJFs2vjQ1h4AonjTquPIdo1JAhjActBgNCurWElyBpJ3dprvQ_y9qXgt12kKM0SvWV1zky_p8x53UPcHHk=", + "INT3": "utnpbmDAfakTIjB-oSkx7eFoRjXxfxpbexmHUi2RPFCs5A3w8wtH0EM6cThnwEFHulWqSmXFc6v_hd3dyLNsKDAaIUHFktCoE3m5fX2ORgUi4tdXSrx2T8H0xpf0cfFZcg_aS9SK0fgtj2NYaWjoafEI6aLc79XNoiIpTyBONJi0QU6IjlLCA13a47QjVIkrQtPjIntm1EKY34Hyoq8QErZsZQG_f8JEXQMUfM-oOAY0U8gj0outMTtWRAnfdrgaSWhVyIWloUJkXP6cWhZsypKp7e8bm-poc4nE4R-tniMWegY7BLlvtJeSjs71txBaDomr9xqi9lhmX6ENkvGPAHpBbrTOmsMLvEJu6nQR7P08XYalXpiJQjrIQJ77hE499jOIdgSoBjuy6JKlaLc4ejpHeGNXYCPUBJM5KVLITLYpLOjgZFzbhzI8qr4GdjxcfW82PAxhlTDywjDH-eMzbXnVZ-YaLrEocNZ8i88R1bLnv90lUrwzgn7n3zUcTeiRILfkApNZRh_K9ClGSu612LzGv7E-_zN1JdOt4UCcfSSkJLMppbeyIXjE6tDsxjgUyRr0Xj1azvHtEDfFYKj76x9doyk8Xm6_sKF4z9rPrgEmo9u6zr4dhb6fmhTnBjdhWBFjQ1Zpe7P7IfaXeDFrWSQBt7jdADWzB5L8cjaMnULm295uSo1k_ka1FQtFg4NtHiZ2G_zNLINh4DBjZdo5hmqBW7tl4gnNWaUf8YO7J1A=", + "INT4": "kJ4tgroRxuaYe0vF9xMe-nI4NUy4Eg1NyAr_E-WxJKlt5jZuoOcWYijc7cQEOiY3xlRPLTnfN-l9nf4UWO9rN4uMIGH9tuEql1g2ATLxff1ML3tCiexoxn45l-LQRH-PL2bL8jsQ3ByiHxLdzMHUzAarWj76rZk4mDCM9tRAE-hQsUZaaGMbO_hL3BCu6J-mI9hU3em4RuFwNTzCfBxgOJShU_PAU3tt-xROhld5Re3cyZzQZfQI17xDJKpTdZmnZCeabv7BIjh6qEapxhCHi0Ue6_EX3DPld1Faj438FptfBTn8UXW9Es6tkM1BGHw8npbHtGVqI1g7DdWuYUb5qeAtUzkEnFpxIl-67sKctYGBWvGkRnZrpu33jwaorW7xInwSymTg0isVCDQCfTEsa8zszj-bD4UMF2jeWr_pL-g4Vk_Ns0vMDFhsZGJTLO9Vv5DNf0WYOMWBISHXgW9z_d0UzCwlJpojqqO1DNhEt1aRRSb_zIBhdfAUHmJy35KmA0tPiDNb1K6uEVVcKSQBD2G9BfrvGXq-o_E3JAUzO2-u4ues0eWseOa0dQQIasjMZ0ZNAakfxCwg7dHp9Lq0c_0fti37OAk69Cyi0EMThCTYr8Yij_sidT9sts-YeQgTniI2LFsMnDnGkWB7wubmxUvG9qOq5ZmonBArkOt8xHWT9Xy5xq0hQj6Ba6Dgcy8qrcCdq8HoIneUaHH_c7pkRW2V-MKHQ6gr9iopAtP1Ifw=", + "BET1": "hismr3VKvRu_fQ25jJjIjjBKs-nhdOYsbtMA7Sgc0-P99yjGnVvOq8nJBGFHcjCLHGb6lTcLyA4eVSflnKER2r6R35PqRznX63s1bDGZuuyPexBCTVZSh1ubtI3ImQuO5mRb9wFAyeUcG53JnZEXQmOnCdmLW8seIugeSrsj5-HC_Z7vOT-KTmcONFP2q2tbKqDTziLVXEJ0dy2EdgvSk8l-sn1QsgodDVe-FCaAp35jlswp8faJuyTv0j7luc5dRP7xvSVXX08uTD9AWHooVCbsGSJqwVRsjY0bFJMcvKqzCCE5xlyBVVmkbphD3aEsBqMVsG4wL2_W_pEgdbmI06qHgJK0sotwj0iEbGVBOYOuBp4uAQdadKYbQL2OkJaqXkDWJTy0MDUSRBbZ6qH3KCyVRfUFcxlFUxqw9I7l5G35vnDMCpoJyj6wyzyq4oFTKgDXkn6zMTDD7lgpYWW1zRDtCTecU6nkmsUQnwe-9EXBfcePlRtJasG40ykj5x4MRJQoDiX9F8VBDnquaR_8K5HOL3de8ypEl7q22bKkKv-pHEPO8hH5lZynfQ86RHyHhDGxXuNuCEX-ekqK7K8PGTF8gO_fIGMpp_Z5aqWSHQgu7Ge1R-FTtfKZ5L_E-RjnR9QWFqEIC2_hNiZoQBdSTC8XUMmnpltTBQJOngEBhmgGepekZmuGnP3xmmvVbK1VijHtgB6PCtIVaTIzkwpVo1o4aj25zQG_3VSqgT8Ufnw=" +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mercedesme/src/test/resources/invalid-key.json b/bundles/org.openhab.binding.mercedesme/src/test/resources/invalid-key.json new file mode 100644 index 000000000..06e434b6a --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/test/resources/invalid-key.json @@ -0,0 +1,8 @@ +[ + { + "wrong": { + "value": "4131", + "timestamp": 1655399236000 + } + } +] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mercedesme/src/test/resources/invalid-timestamp.json b/bundles/org.openhab.binding.mercedesme/src/test/resources/invalid-timestamp.json new file mode 100644 index 000000000..a04379f39 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/test/resources/invalid-timestamp.json @@ -0,0 +1,7 @@ +[ + { + "odo": { + "value": "4131" + } + } +] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mercedesme/src/test/resources/lock.json b/bundles/org.openhab.binding.mercedesme/src/test/resources/lock.json new file mode 100644 index 000000000..9655d6bd1 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/test/resources/lock.json @@ -0,0 +1,26 @@ +[ + { + "doorlockstatusvehicle": { + "value": "0", + "timestamp": 1541080800000 + } + }, + { + "doorlockstatusdecklid": { + "value": "false", + "timestamp": 1541080800000 + } + }, + { + "doorlockstatusgas": { + "value": "false", + "timestamp": 1541080800000 + } + }, + { + "positionHeading": { + "value": "120", + "timestamp": 1541080800000 + } + } +] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mercedesme/src/test/resources/odo.json b/bundles/org.openhab.binding.mercedesme/src/test/resources/odo.json new file mode 100644 index 000000000..bf98a0681 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/test/resources/odo.json @@ -0,0 +1,8 @@ +[ + { + "odo": { + "value": "4131", + "timestamp": 1655399236000 + } + } +] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mercedesme/src/test/resources/status-resources.json b/bundles/org.openhab.binding.mercedesme/src/test/resources/status-resources.json new file mode 100644 index 000000000..8dc557453 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/test/resources/status-resources.json @@ -0,0 +1,82 @@ +[ + { + "name": "decklidstatus", + "version": "1.0", + "href": "/vehicles/WDB111111ZZZ22222/resources/decklidstatus" + }, + { + "name": "doorstatusfrontleft", + "version": "1.0", + "href": "/vehicles/WDB111111ZZZ22222/resources/doorstatusfrontleft" + }, + { + "name": "doorstatusfrontright", + "version": "1.0", + "href": "/vehicles/WDB111111ZZZ22222/resources/doorstatusfrontright" + }, + { + "name": "doorstatusrearleft", + "version": "1.0", + "href": "/vehicles/WDB111111ZZZ22222/resources/doorstatusrearleft" + }, + { + "name": "doorstatusrearright", + "version": "1.0", + "href": "/vehicles/WDB111111ZZZ22222/resources/doorstatusrearright" + }, + { + "name": "interiorLightsFront", + "version": "1.0", + "href": "/vehicles/WDB111111ZZZ22222/resources/interiorLightsFront" + }, + { + "name": "interiorLightsRear", + "version": "1.0", + "href": "/vehicles/WDB111111ZZZ22222/resources/interiorLightsRear" + }, + { + "name": "lightswitchposition", + "version": "1.0", + "href": "/vehicles/WDB111111ZZZ22222/resources/lightswitchposition" + }, + { + "name": "readingLampFrontLeft", + "version": "1.0", + "href": "/vehicles/WDB111111ZZZ22222/resources/readingLampFrontLeft" + }, + { + "name": "readingLampFrontRight", + "version": "1.0", + "href": "/vehicles/WDB111111ZZZ22222/resources/readingLampFrontRight" + }, + { + "name": "rooftopstatus", + "version": "1.0", + "href": "/vehicles/WDB111111ZZZ22222/resources/rooftopstatus" + }, + { + "name": "sunroofstatus", + "version": "1.0", + "href": "/vehicles/WDB111111ZZZ22222/resources/sunroofstatus" + }, + { + "name": "windowstatusfrontleft", + "version": "1.0", + "href": "/vehicles/WDB111111ZZZ22222/resources/windowstatusfrontleft" + }, + { + "name": "windowstatusfrontright", + "version": "1.0", + "href": "/vehicles/WDB111111ZZZ22222/resources/windowstatusfrontright" + }, + { + "name": "windowstatusrearleft", + "version": "1.0", + "href": "/vehicles/WDB111111ZZZ22222/resources/windowstatusrearleft" + }, + { + "name": "windowstatusrearright", + "version": "1.0", + "href": "/vehicles/WDB111111ZZZ22222/resources/windowstatusrearright" + } +] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mercedesme/src/test/resources/status.json b/bundles/org.openhab.binding.mercedesme/src/test/resources/status.json new file mode 100644 index 000000000..6093bb3e8 --- /dev/null +++ b/bundles/org.openhab.binding.mercedesme/src/test/resources/status.json @@ -0,0 +1,98 @@ +[ + { + "decklidstatus": { + "value": "false", + "timestamp": 1541080800000 + } + }, + { + "doorstatusfrontleft": { + "value": "false", + "timestamp": 1541080800000 + } + }, + { + "doorstatusfrontright": { + "value": "false", + "timestamp": 1541080800000 + } + }, + { + "doorstatusrearleft": { + "value": "false", + "timestamp": 1541080800000 + } + }, + { + "doorstatusrearright": { + "value": "false", + "timestamp": 1541080800000 + } + }, + { + "interiorLightsFront": { + "value": "false", + "timestamp": 1541080800000 + } + }, + { + "interiorLightsRear": { + "value": "false", + "timestamp": 1541080800000 + } + }, + { + "lightswitchposition": { + "value": "0", + "timestamp": 1541080800000 + } + }, + { + "readingLampFrontLeft": { + "value": "false", + "timestamp": 1541080800000 + } + }, + { + "readingLampFrontRight": { + "value": "false", + "timestamp": 1541080800000 + } + }, + { + "rooftopstatus": { + "value": "0", + "timestamp": 1541080800000 + } + }, + { + "sunroofstatus": { + "value": "0", + "timestamp": 1541080800000 + } + }, + { + "windowstatusfrontleft": { + "value": "0", + "timestamp": 1541080800000 + } + }, + { + "windowstatusfrontright": { + "value": "0", + "timestamp": 1541080800000 + } + }, + { + "windowstatusrearleft": { + "value": "0", + "timestamp": 1541080800000 + } + }, + { + "windowstatusrearright": { + "value": "0", + "timestamp": 1541080800000 + } + } +] \ No newline at end of file diff --git a/bundles/pom.xml b/bundles/pom.xml index 61df9d6e0..6f924d288 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -215,6 +215,7 @@ org.openhab.binding.mcp23017 org.openhab.binding.mecmeter org.openhab.binding.melcloud + org.openhab.binding.mercedesme org.openhab.binding.meteoalerte org.openhab.binding.meteoblue org.openhab.binding.meteostick