/bundles/org.openhab.binding.bluetooth.govee/ @cpmeister
/bundles/org.openhab.binding.bluetooth.roaming/ @cpmeister
/bundles/org.openhab.binding.bluetooth.ruuvitag/ @ssalonen
+/bundles/org.openhab.binding.bmwconnecteddrive/ @weymann @ntruchsess
/bundles/org.openhab.binding.boschindego/ @jofleck
/bundles/org.openhab.binding.boschshc/ @stefan-kaestle @coeing @GerdZanker
/bundles/org.openhab.binding.bosesoundtouch/ @marvkis @tratho
<artifactId>org.openhab.binding.bluetooth.ruuvitag</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.openhab.addons.bundles</groupId>
+ <artifactId>org.openhab.binding.bmwconnecteddrive</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.boschindego</artifactId>
--- /dev/null
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
--- /dev/null
+# BMW ConnectedDrive Binding
+
+The binding provides a connection between [BMW's ConnectedDrive Portal](https://www.bmw-connecteddrive.com/country-region-select/country-region-selection.html) and openHAB.
+All vehicles connected to an account will be detected by the discovery with the correct type
+
+* Conventional Fuel Vehicle
+* Plugin-Hybrid Electrical Vehicle
+* Battery Electric Vehicle with Range Extender
+* Battery Electric Vehicle
+
+In addition properties are attached with information and services provided by this vehicle.
+The provided data depends on
+
+1. the [Thing Type](#things) and
+2. the [Properties](#properties) mentioned in Services
+
+Different channel groups are clustering all informations.
+Check for each group if it's supported for this Vehicle.
+
+Please note **this isn't a real-time binding**.
+If a door is opened the state isn't transmitted and changed immediately.
+This isn't a flaw in the binding itself because the state in BMW's own ConnectedDrive App is also updated with some delay.
+
+## Supported Things
+
+### Bridge
+
+The bridge establishes the connection between BMW's ConnectedDrive Portal and openHAB.
+
+| Name | Bridge Type ID | Description |
+|----------------------------|----------------|------------------------------------------------------------|
+| BMW ConnectedDrive Account | `account` | Access to BMW ConnectedDrive Portal for a specific user |
+
+
+### Things
+
+Four different vehicle types are provided.
+They differ in the supported channel groups & channels.
+Conventional Fuel Vehicles have no _Charging Profile_, Electric Vehicles don't provide a _Fuel Range_.
+For hybrid vehicles in addition to _Fuel and Electric Range_ the _Hybrid Range_ is shown.
+
+| Name | Thing Type ID | Supported Channel Groups |
+|-------------------------------------|---------------|--------------------------------------------------------|
+| BMW Electric Vehicle | `bev` | status, range, location, service, check, charge, image |
+| BMW Electric Vehicle with REX | `bev_rex` | status, range, location, service, check, charge, image |
+| BMW Plug-In-Hybrid Electric Vehicle | `phev` | status, range, location, service, check, charge, image |
+| BMW Conventional Vehicle | `conv` | status, range, location, service, check, image |
+
+
+#### Properties
+
+<img align="right" src="./doc/properties.png" width="500" height="225"/>
+
+For each vehicle properties are available.
+Basically 3 types of information are registered as properties
+
+* Informations regarding your dealer with address and phone number
+* Which services are available / not available
+* Vehicle properties like color, model type, drive train and construction year
+
+In the right picture can see in *Services Activated* e.g. the *DoorLock* and *DoorUnlock* services are mentioned.
+This ensures channel group [Remote Services](#remote-services) is supporting door lock and unlock remote control.
+
+In *Services Supported* the entry *LastDestination* is mentioned.
+So it's valid to connect channel group [Last Destinations](#destinations) in order to display and select the last navigation destinations.
+
+| Property Key | Property Value | Supported Channel Groups |
+|--------------------|---------------------|------------------------------|
+| servicesSupported | Statistics | last-trip, lifetime |
+| servicesSupported | LastDestinations | destinations |
+| servicesActivated | _list of services_ | remote |
+
+
+## Discovery
+
+Auto discovery is starting after the bridge towards BMW's ConnectedDrive is created.
+A list of your registered vehicles is queried and all found things are added in the inbox.
+Unique identifier is the *Vehicle Identification Number* (VIN).
+If a thing is already declared in a _.things_ configuration, discovery won't highlight it again.
+Properties will be attached to predefined vehicles if the VIN is matching.
+
+## Configuration
+
+### Bridge Configuration
+
+| Parameter | Type | Description |
+|-----------------|---------|--------------------------------------------------------------------|
+| userName | text | BMW ConnectedDrive Username |
+| password | text | BMW ConnectedDrive Password |
+| region | text | Select region in order to connect to the appropriate BMW server. |
+
+The region Configuration has 3 different options
+
+* _NORTH_AMERICA_
+* _CHINA_
+* _ROW_ (Rest of World)
+
+### Thing Configuration
+
+Same configuration is needed for all things
+
+| Parameter | Type | Description |
+|-----------------|---------|---------------------------------------|
+| vin | text | Vehicle Identification Number (VIN) |
+| refreshInterval | integer | Refresh Interval in Minutes |
+| units | text | Unit Selection. See below. |
+| imageSize | integer | Image Size |
+| imageViewport | text | Image Viewport |
+
+The unit configuration has 3 options
+
+* _AUTODETECT_ selects miles for US & UK, kilometer otherwise
+* _METRIC_ selects directly kilometers
+* _IMPERIAL_ selects directly miles
+
+The _imageVieport_ allows to show the vehicle from different angels.
+Possible options are
+
+* _FRONT_
+* _REAR_
+* _SIDE_
+* _DASHBOARD_
+* _DRIVERDOOR_
+
+## Channels
+
+There are many channels available for each vehicle.
+For better overview they are clustered in different channel groups.
+They differ for each vehicle type, build-in sensors and activated services.
+
+
+### Thing Channel Groups
+
+#### Vehicle Status
+
+Reflects overall status of the vehicle.
+
+* Channel Group ID is **status**
+* Available for all vehicles
+* Read-only values
+
+| Channel Label | Channel ID | Type | Description |
+|---------------------------|---------------------|---------------|------------------------------------------------|
+| Overall Door Status | doors | String | Combined status for all doors |
+| Overall Window Status | windows | String | Combined status for all windows |
+| Doors Locked | lock | String | Status if doors are locked or unlocked |
+| Next Service Date | service-date | DateTime | Date of upcoming service |
+| Mileage till Next Service | service-mileage | Number:Length | Mileage till upcoming service |
+| Check Control | check-control | String | Presence of active warning messages |
+| Charging Status | charge | String | Only available for phev, bev_rex and bev |
+| Last Status Timestamp | last-update | DateTime | Date and time of last status update |
+
+Overall Door Status values
+
+* _Closed_ - all doors closed
+* _Open_ - at least one door is open
+* _Undef_ - no door data delivered at all
+
+Overall Windows Status values
+
+* _Closed_ - all windows closed
+* _Open_ - at least one window is completely open
+* _Intermediate_ - at least one window is partially open
+* _Undef_ - no window data delivered at all
+
+Check Control values
+
+* _Active_ - at least one warning message is active
+* _Not Active_ - no warning message is active
+* _Undef_ - no data for warnings delivered
+
+Charging Status values
+
+* _Charging_
+* _Error_
+* _Finished Fully Charged_
+* _Finished Not Full_
+* _Invalid_
+* _Not Charging_
+* _Charging Goal reached_
+* _Waiting For Charging_
+
+#### Services
+
+Group for all upcoming services with description, service date and/or service mileage.
+If more than one service is scheduled in the future the channel _name_ contains all future services as options.
+
+* Channel Group ID is **service**
+* Available for all vehicles
+* Read/Write access
+
+| Channel Label | Channel ID | Type | Access |
+|--------------------------------|---------------------|----------------|------------|
+| Service Name | name | String | Read/Write |
+| Service Details | details | String | Read |
+| Service Date | date | Number | Read |
+| Mileage till Service | mileage | Number:Length | Read |
+
+#### Check Control
+
+Group for all current active CheckControl messages.
+If more than one message is active the channel _name_ contains all active messages as options.
+
+* Channel Group ID is **check**
+* Available for all vehicles
+* Read/Write access
+
+| Channel Label | Channel ID | Type | Access |
+|---------------------------------|---------------------|----------------|------------|
+| CheckControl Description | name | String | Read/Write |
+| CheckControl Details | details | String | Read |
+| Mileage Occurrence | mileage | Number:Length | Read |
+
+#### Doors Details
+
+Detailed status of all doors and windows.
+
+* Channel Group ID is **doors**
+* Available for all vehicles if corresponding sensors are built-in
+* Read-only values
+
+| Channel Label | Channel ID | Type |
+|----------------------------|-------------------------|---------------|
+| Driver Door | driver-front | String |
+| Driver Door Rear | driver-rear | String |
+| Passenger Door | passenger-front | String |
+| Passenger Door Rear | passenger-rear | String |
+| Trunk | trunk | String |
+| Hood | hood | String |
+| Driver Window | win-driver-front | String |
+| Driver Rear Window | win-driver-rear | String |
+| Passenger Window | win-passenger-front | String |
+| Passenger Rear Window | win-passenger-rear | String |
+| Rear Window | win-rear | String |
+| Sunroof | sunroof | String |
+
+Possible states
+
+* _Undef_ - no status data available
+* _Invalid_ - this door / window isn't applicable for this vehicle
+* _Closed_ - the door / window is closed
+* _Open_ - the door / window is open
+* _Intermediate_ - window in intermediate position, not applicable for doors
+
+#### Range Data
+
+Based on vehicle type some channels are present or not.
+Conventional fuel vehicles don't provide *Electric Range* and battery electric vehicles don't show *Fuel Range*.
+Hybrid vehicles have both and in addition *Hybrid Range*.
+See description [Range vs Range Radius](#range-vs-range-radius) to get more information.
+
+* Channel Group ID is **range**
+* Availability according to table
+* Read-only values
+
+| Channel Label | Channel ID | Type | conv | phev | bev_rex | bev |
+|-----------------------|-----------------------|----------------------|------|------|---------|-----|
+| Mileage | mileage | Number:Length | X | X | X | X |
+| Fuel Range | range-fuel | Number:Length | X | X | X | |
+| Battery Range | range-electric | Number:Length | | X | X | X |
+| Hybrid Range | range-hybrid | Number:Length | | X | X | |
+| Battery Charge Level | soc | Number:Dimensionless | | X | X | X |
+| Remaining Fuel | remaining-fuel | Number:Volume | X | X | X | |
+| Fuel Range Radius | range-radius-fuel | Number:Length | X | X | X | |
+| Electric Range Radius | range-radius-electric | Number:Length | | X | X | X |
+| Hybrid Range Radius | range-radius-hybrid | Number:Length | | X | X | |
+
+
+#### Charge Profile
+
+Charging options with date and time for preferred time windows and charging modes.
+
+* Channel Group ID is **charge**
+* Available for electric and hybrid vehicles
+* Read/Write access for UI. Use [Charge Profile Editing Action](#charge-profile-editing) in rules
+* There are 3 timers *T1, T2 and T3* available. Replace *X* with number 1,2 or 3 to target the correct timer
+* Additional override Timer *OT* defines a single departure besides the 3 predefined schedule timers
+
+| Channel Label | Channel Group ID | Channel ID | Type |
+|----------------------------|------------------|---------------------------|----------|
+| Charge Mode | charge | profile-mode | String |
+| Charge Preferences | charge | profile-prefs | String |
+| Window Start Time | charge | window-start | DateTime |
+| Window End Time | charge | window-end | DateTime |
+| A/C at Departure | charge | profile-climate | Switch |
+| T*X* Enabled | charge | timer*X*-enabled | Switch |
+| T*X* Departure Time | charge | timer*X*-departure | DateTime |
+| T*X* Days | charge | timer*X*-days | String |
+| T*X* Monday | charge | timer*X*-day-mon | Switch |
+| T*X* Tuesday | charge | timer*X*-day-tue | Switch |
+| T*X* Wednesday | charge | timer*X*-day-wed | Switch |
+| T*X* Thursday | charge | timer*X*-day-thu | Switch |
+| T*X* Friday | charge | timer*X*-day-fri | Switch |
+| T*X* Saturday | charge | timer*X*-day-sat | Switch |
+| T*X* Sunday | charge | timer*X*-day-sun | Switch |
+| OT Enabled | charge | override-enabled | Switch |
+| OT Departure Time | charge | override-departure | DateTime |
+
+The channel _profile-mode_ supports
+
+* *IMMEDIATE_CHARGING*
+* *DELAYED_CHARGING*
+
+The channel _profile-prefs_ supports
+
+* *NO_PRESELECTION*
+* *CHARGING_WINDOW*
+
+#### Location
+
+GPS location and heading of the vehicle.
+
+* Channel Group ID is **location**
+* Available for all vehicles with built-in GPS sensor. Function can be enabled/disabled in the head unit
+* Read-only values
+
+| Channel Label | Channel ID | Type |
+|-----------------|---------------------|--------------|
+| GPS Coordinates | gps | Location |
+| Heading | heading | Number:Angle |
+
+#### Last Trip
+
+Statistic values of duration, distance and consumption of the last trip.
+
+* Channel Group ID is **last-trip**
+* Available if *Statistics* is present in *Services Supported*. See [Vehicle Properties](#properties) for further details
+* Read-only values
+* Depending on units configuration in [Thing Configuration](#thing-configuration) average values are given for 100 kilometers or miles
+
+| Channel Label | Channel ID | Type |
+|-----------------------------------------|------------------------------|---------------|
+| Last Trip Date | date | DateTime |
+| Last Trip Duration | duration | Number:Time |
+| Last Trip Distance | distance | Number:Length |
+| Distance since Charge | distance-since-charging | Number:Length |
+| Avg. Power Consumption | avg-consumption | Number:Power |
+| Avg. Power Recuperation | avg-recuperation | Number:Power |
+| Avg. Combined Consumption | avg-combined-consumption | Number:Volume |
+
+
+#### Lifetime Statistics
+
+Providing lifetime consumption values.
+
+* Channel Group ID is **lifetime**
+* Available if *Statistics* is present in *Services Supported*. See [Vehicle Properties](#properties) for further details
+* Read-only values
+* Depending on units configuration in [Thing Configuration](#thing-configuration) average values are given for 100 kilometers or miles
+
+| Channel Label | Channel ID | Type |
+|-----------------------------------------|------------------------------|---------------|
+| Total Electric Distance | total-driven-distance | Number:Length |
+| Longest 1-Charge Distance | single-longest-distance | Number:Length |
+| Avg. Power Consumption | avg-consumption | Number:Power |
+| Avg. Power Recuperation | avg-recuperation | Number:Power |
+| Avg. Combined Consumption | avg-combined-consumption | Number:Volume |
+
+
+#### Remote Services
+
+Remote control of the vehicle.
+Send a *command* to the vehicle and the *state* is reporting the execution progress.
+Only one command can be executed each time.
+Parallel execution isn't supported.
+
+* Channel Group ID is **remote**
+* Available for all commands mentioned in *Services Activated*. See [Vehicle Properties](#properties) for further details
+* Read/Write access
+
+
+| Channel Label | Channel ID | Type | Access |
+|-------------------------|---------------------|---------|--------|
+| Remote Service Command | command | String | Write |
+| Service Execution State | state | String | Read |
+
+The channel _command_ provides options
+
+* _Flash Lights_
+* _Vehicle Finder_
+* _Door Lock_
+* _Door Unlock_
+* _Horn Blow_
+* _Climate Control_
+* _Start Charging_
+* _Send Charging Profile_
+
+The channel _state_ shows the progress of the command execution in the following order
+
+1) _Initiated_
+2) _Pending_
+3) _Delivered_
+4) _Executed_
+
+#### Destinations
+
+Shows the last destinations stored in the navigation system.
+If several last destinations are stored in the navigation system the channel _name_ contains all addresses as options.
+
+* Channel Group ID is **destination**
+* Available if *LastDestinations* is present in *Services Supported*. Check [Vehicle Properties](#properties) for further details
+* Read/Write access
+
+
+| Channel Label | Channel ID | Type | Access |
+|----------------------|---------------|-----------|-------------|
+| Name | name | String | Read/Write |
+| GPS Coordinates | gps | Location | Read |
+
+
+
+#### Image
+
+Image representation of the vehicle. Size and viewport are writable and can be
+The possible values are the same mentioned in [Thing Configuration](#thing-configuration).
+
+* Channel Group ID is **image**
+* Available for all vehicles
+* Read/Write access
+
+| Channel Label | Channel ID | Type | Access |
+|----------------------------|---------------------|--------|----------|
+| Rendered Vehicle Image | png | Image | Read |
+| Image Viewport | view | String | Write |
+| Image Picture Size | size | Number | Write |
+
+## Actions
+
+Get the _Actions_ object for your vehicle using the Thing ID
+
+* bmwconnecteddrive - Binding ID, don't change!
+* bev_rex - [Thing UID](#things) of your car
+* user - Thing ID of the [Bridge](#bridge)
+* i3 - Thing ID of your car
+
+```
+ val profile = getActions("bmwconnecteddrive", "bmwconnecteddrive:bev_rex:user:i3")
+```
+
+### Charge Profile Editing
+
+Like in the Charge Profile Channels 3 Timers are provided. Replace *X* with 1, 2 or 3 to address the right timer.
+
+| Function | Parameters | Returns | Description |
+|---------------------------------------|------------------|---------------------------|------------------------------------------------------------|
+| getClimatizationEnabled | void | Boolean | Returns the enabled state of climatization |
+| setClimatizationEnabled | Boolean | void | Sets the enabled state of climatization |
+| getChargingMode | void | String | Gets the charging-mode, see valid options below |
+| setChargingMode | String | void | Sets the charging-mode, see valid options below |
+| getPreferredWindowStart | void | LocalTime | Returns the preferred charging-window start time |
+| setPreferredWindowStart | LocalTime | void | Sets the preferred charging-window start time |
+| getPreferredWindowEnd | void | LocalTime | Returns the preferred charging-window end time |
+| setPreferredWindowEnd | LocalTime | void | Sets the preferred charging-window end time |
+| getTimer*X*Enabled | void | Boolean | Returns the enabled state of timer*X* |
+| setTimer*X*Enabled | Boolean | void | Returns the enabled state of timer*X* |
+| getTimer*X*Departure | void | LocalTime | Returns the departure time of timer*X* |
+| setTimer*X*Departure | LocalTime | void | Sets the timer*X* departure time |
+| getTimer*X*Days | void | Set<DayOfWeek> | Returns the days of week timer*X* is enabled for |
+| setTimer*X*Days | Set<DayOfWeek> | void | sets the days of week timer*X* is enabled for |
+| getOverrideTimerEnabled | void | Boolean | Returns the enabled state of override timer |
+| setOverrideTimerEnabled | Boolean | void | Sets the enabled state of override timer |
+| getOverrideTimerDeparture | void | LocalTime | Returns the departure time of override timer |
+| setOverrideTimerDeparture | LocalTime | void | Sets the override timer departure time |
+| getOverrideTimerDays | void | Set<DayOfWeek> | Returns the days of week the overrideTimer is enabled for |
+| setOverrideTimerDays | Set<DayOfWeek> | void | Sets the days of week the overrideTimer is enabled for |
+| cancelEditChargeProfile | void | void | Cancel current edit of charging profile |
+| sendChargeProfile | void | void | Sends the charging profile to the vehicle |
+
+Values for valid charging mode get/set
+
+* *IMMEDIATE_CHARGING*
+* *DELAYED_CHARGING*
+
+
+## Further Descriptions
+
+### Dynamic Data
+
+<img align="right" src="./doc/ServiceOptions.png" width="400" height="350"/>
+
+There are 3 occurrences of dynamic data delivered
+
+* Upcoming Services delivered in group [Services](#services)
+* Check Control Messages delivered in group [Check Control](#check-control)
+* Last Destinations delivered in group [Destinations](#destinations)
+
+The channel id _name_ shows the first element as default.
+All other possibilities are attached as options.
+The picture on the right shows the _Service Name_ item and all four possible options.
+Select the desired service and the corresponding _Service Date & Milage_ will be shown.
+
+### TroubleShooting
+
+BMW has a high range of vehicles supported by ConnectedDrive.
+In case of any issues with this binding help to resolve it!
+Please perform the following steps:
+
+* Can you [log into ConnectedDrive](https://www.bmw-connecteddrive.com/country-region-select/country-region-selection.html) with your credentials? Please note this isn't the BMW Customer portal - it's the ConnectedDrive portal
+* Is the vehicle listed in your account? There's a one-to-one relation from user to vehicle
+
+If the access to the portal is working and the vehicle is listed some debug data is needed in order to identify the issue.
+
+#### Generate Debug Fingerprint
+
+If you checked the above pre-conditions you need to get the debug fingerprint from the logs.
+First [enable debug logging](https://www.openhab.org/docs/administration/logging.html#defining-what-to-log) for the binding.
+
+```
+log:set DEBUG org.openhab.binding.bmwconnecteddrive
+```
+
+The debug fingerprint is generated immediately after the vehicle thing is initialized the first time, e.g. after openHAB startup.
+To force a new fingerprint disable the thing shortly and enable it again.
+Personal data is eliminated from the log entries so it should be possible to share them in public.
+Data like
+
+* Dealer Properties
+* Vehicle Identification Number (VIN)
+* Location latitude / longitude
+
+are anonymized.
+You'll find the fingerprint in the logs with the command
+
+```
+grep "Troubleshoot Fingerprint Data" openhab.log
+```
+
+After the corresponding fingerprint is generated please [follow the instructions to raise an issue](https://community.openhab.org/t/how-to-file-an-issue/68464) and attach the fingerprint data!
+Your feedback is highly appreciated!
+
+
+### Range vs Range Radius
+
+<img align="right" src="./doc/range-radius.png" width="400" height="350"/>
+
+You will observe differences in the vehicle range and range radius values.
+While range is indicating the possible distance to be driven on roads the range radius indicates the reachable range on the map.
+
+The right picture shows the distance between Kassel and Frankfurt in Germany.
+While the air-line distance is ~145 kilometer the route distance is ~192 kilometer.
+So range value is the normal remaining range while the range radius values can be used e.g. on [Mapview](https://www.openhab.org/docs/configuration/sitemaps.html#element-type-mapview) to indicate the reachable range on map.
+Please note this is just an indicator of the effective range.
+Especially for electric vehicles it depends on many factors like driving style and usage of electric consumers.
+
+## Full Example
+
+The example is based on a BMW i3 with range extender (REX).
+Exchange the three configuration parameters in the Things section
+
+* YOUR_USERNAME - with your ConnectedDrive login username
+* YOUR_PASSWORD - with your ConnectedDrive password credentials
+* VEHICLE_VIN - the vehicle identification number
+
+In addition search for all occurrences of *i3* and replace it with your Vehicle Identification like *x3* or *535d* and you're ready to go!
+
+### Things File
+
+```
+Bridge bmwconnecteddrive:account:user "BMW ConnectedDrive Account" [userName="YOUR_USERNAME",password="YOUR_PASSWORD",region="ROW"] {
+ Thing bev_rex i3 "BMW i3 94h REX" [ vin="VEHICLE_VIN",units="AUTODETECT",imageSize=600,imageViewport="FRONT",refreshInterval=5]
+}
+```
+
+### Items File
+
+```
+Number:Length i3Mileage "Odometer [%d %unit%]" <line> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:range#mileage" }
+Number:Length i3Range "Range [%d %unit%]" <motion> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:range#hybrid"}
+Number:Length i3RangeElectric "Electric Range [%d %unit%]" <motion> (i3,long) {channel="bmwconnecteddrive:bev_rex:user:i3:range#electric"}
+Number:Length i3RangeFuel "Fuel Range [%d %unit%]" <motion> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:range#fuel"}
+Number:Dimensionless i3BatterySoc "Battery Charge [%.1f %%]" <battery> (i3,long) {channel="bmwconnecteddrive:bev_rex:user:i3:range#soc"}
+Number:Volume i3Fuel "Fuel [%.1f %unit%]" <oil> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:range#remaining-fuel"}
+Number:Length i3RadiusElectric "Electric Radius [%d %unit%]" <zoom> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:range#radius-electric" }
+Number:Length i3RadiusHybrid "Hybrid Radius [%d %unit%]" <zoom> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:range#radius-hybrid" }
+
+String i3DoorStatus "Door Status [%s]" <lock> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:status#doors" }
+String i3WindowStatus "Window Status [%s]" <lock> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:status#windows" }
+String i3LockStatus "Lock Status [%s]" <lock> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:status#lock" }
+DateTime i3NextServiceDate "Next Service Date [%1$tb %1$tY]" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:status#service-date" }
+String i3NextServiceMileage "Next Service Mileage [%d %unit%]" <line> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:status#service-mileage" }
+String i3CheckControl "Check Control [%s]" <error> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:status#check-control" }
+String i3ChargingStatus "Charging [%s]" <energy> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:status#charge" }
+DateTime i3LastUpdate "Update [%1$tA, %1$td.%1$tm. %1$tH:%1$tM]" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:status#last-update"}
+
+DateTime i3TripDateTime "Trip Date [%1$tA, %1$td.%1$tm. %1$tH:%1$tM]" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:last-trip#date"}
+Number:Time i3TripDuration "Trip Duration [%d %unit%]" <time> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:last-trip#duration"}
+Number:Length i3TripDistance "Distance [%d %unit%]" <line> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:last-trip#distance" }
+Number:Length i3TripDistanceSinceCharge "Distance since last Charge [%d %unit%]" <line> (i3,long) {channel="bmwconnecteddrive:bev_rex:user:i3:last-trip#distance-since-charging" }
+Number:Energy i3AvgTripConsumption "Average Consumption [%.1f %unit%]" <energy> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:last-trip#avg-consumption" }
+Number:Volume i3AvgTripCombined "Average Combined Consumption [%.1f %unit%]" <oil> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:last-trip#avg-combined-consumption" }
+Number:Energy i3AvgTripRecuperation "Average Recuperation [%.1f %unit%]" <energy> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:last-trip#avg-recuperation" }
+
+Number:Length i3TotalElectric "Electric Distance Driven [%d %unit%]" <line> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:lifetime#total-driven-distance" }
+Number:Length i3LongestEVTrip "Longest Electric Trip [%d %unit%]" <line> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:lifetime#single-longest-distance" }
+Number:Energy i3AvgConsumption "Average Consumption [%.1f %unit%]" <energy> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:lifetime#avg-consumption" }
+Number:Volume i3AvgCombined "Average Combined Consumption [%.1f %unit%]" <oil> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:lifetime#avg-combined-consumption" }
+Number:Energy i3AvgRecuperation "Average Recuperation [%.1f %unit%]" <energy> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:lifetime#avg-recuperation" }
+
+Location i3Location "Location [%s]" <zoom> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:location#gps" }
+Number:Angle i3Heading "Heading [%.1f %unit%]" <zoom> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:location#heading" }
+
+String i3RemoteCommand "Command [%s]" <switch> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:remote#command" }
+String i3RemoteState "Remote Execution State [%s]" <status> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:remote#state" }
+
+String i3DriverDoor "Driver Door [%s]" <lock> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:doors#driver-front" }
+String i3DriverDoorRear "Driver Door Rear [%s]" <lock> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:doors#driver-rear" }
+String i3PassengerDoor "Passenger Door [%s]" <lock> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:doors#passenger-front" }
+String i3PassengerDoorRear "Passenger Door Rear [%s]" <lock> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:doors#passenger-rear" }
+String i3Hood "Hood [%s]" <lock> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:doors#hood" }
+String i3Trunk "Trunk [%s]" <lock> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:doors#trunk" }
+String i3DriverWindow "Driver Window [%s]" <lock> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:doors#win-driver-front" }
+String i3DriverWindowRear "Driver Window Rear [%s]" <lock> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:doors#win-driver-rear" }
+String i3PassengerWindow "Passenger Window [%s]" <lock> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:doors#win-passenger-front" }
+String i3PassengerWindowRear "Passenger Window Rear [%s]" <lock> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:doors#win-passenger-rear" }
+String i3RearWindow "Rear Window [%s]" <lock> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:doors#win-rear" }
+String i3Sunroof "Sunroof [%s]" <lock> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:doors#sunroof" }
+
+String i3ServiceName "Service Name [%s]" <text> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:service#name" }
+String i3ServiceDetails "Service Details [%s]" <text> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:service#details" }
+Number:Length i3ServiceMileage "Service Mileage [%d %unit%]" <line> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:service#mileage" }
+DateTime i3ServiceDate "Service Date [%1$tb %1$tY]" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:service#date" }
+
+String i3CCName "CheckControl Name [%s]" <text> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:check#name" }
+String i3CCDetails "CheckControl Details [%s]" <text> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:check#details" }
+Number:Length i3CCMileage "CheckControl Mileage [%d %unit%]" <line> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:check#mileage" }
+
+String i3DestName "Destination [%s]" <house> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:destination#name" }
+Location i3DestLocation "GPS [%s]" <zoom> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:destination#gps" }
+
+Switch i3ChargeProfileClimate "Charge Profile Climatization" <temperature> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#profile-climate" }
+String i3ChargeProfileMode "Charge Profile Mode [%s]" <energy> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#profile-mode" }
+DateTime i3ChargeWindowStart "Charge Window Start [%1$tH:%1$tM]" <time> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#window-start" }
+Number i3ChargeWindowStartHour "Charge Window Start Hour [%d]" <time> (i3)
+Number i3ChargeWindowStartMinute "Charge Window Start Minute [%d]" <time> (i3)
+DateTime i3ChargeWindowEnd "Charge Window End [%1$tH:%1$tM]" <time> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#window-end" }
+Number i3ChargeWindowEndHour "Charge Window End Hour [%d]" <time> (i3)
+Number i3ChargeWindowEndMinute "Charge Window End Minute [%d]" <time> (i3)
+DateTime i3Timer1Departure "Timer 1 Departure [%1$tH:%1$tM]" <time> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-departure" }
+Number i3Timer1DepartureHour "Timer 1 Departure Hour [%d]" <time> (i3)
+Number i3Timer1DepartureMinute "Timer 1 Departure Minute [%d]" <time> (i3)
+String i3Timer1Days "Timer 1 Days [%s]" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-days" }
+Switch i3Timer1DayMon "Timer 1 Monday" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-day-mon" }
+Switch i3Timer1DayTue "Timer 1 Tuesday" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-day-tue" }
+Switch i3Timer1DayWed "Timer 1 Wednesday" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-day-wed" }
+Switch i3Timer1DayThu "Timer 1 Thursday" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-day-thu" }
+Switch i3Timer1DayFri "Timer 1 Friday" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-day-fri" }
+Switch i3Timer1DaySat "Timer 1 Saturday" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-day-sat" }
+Switch i3Timer1DaySun "Timer 1 Sunday" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-day-sun" }
+Switch i3Timer1Enabled "Timer 1 Enabled" <switch> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-enabled" }
+DateTime i3Timer2Departure "Timer 2 Departure [%1$tH:%1$tM]" <time> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-departure" }
+Number i3Timer2DepartureHour "Timer 2 Departure Hour [%d]" <time> (i3)
+Number i3Timer2DepartureMinute "Timer 2 Departure Minute [%d]" <time> (i3)
+String i3Timer2Days "Timer 2 Days [%s]" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-days" }
+Switch i3Timer2DayMon "Timer 2 Monday" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-day-mon" }
+Switch i3Timer2DayTue "Timer 2 Tuesday" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-day-tue" }
+Switch i3Timer2DayWed "Timer 2 Wednesday" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-day-wed" }
+Switch i3Timer2DayThu "Timer 2 Thursday" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-day-thu" }
+Switch i3Timer2DayFri "Timer 2 Friday" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-day-fri" }
+Switch i3Timer2DaySat "Timer 2 Saturday" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-day-sat" }
+Switch i3Timer2DaySun "Timer 2 Sunday" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-day-sun" }
+Switch i3Timer2Enabled "Timer 2 Enabled" <switch> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-enabled" }
+DateTime i3Timer3Departure "Timer 3 Departure [%1$tH:%1$tM]" <time> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-departure" }
+Number i3Timer3DepartureHour "Timer 3 Departure Hour [%d]" <time> (i3)
+Number i3Timer3DepartureMinute "Timer 3 Departure Minute [%d]" <time> (i3)
+String i3Timer3Days "Timer 3 Days [%s]" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-days" }
+Switch i3Timer3DayMon "Timer 3 Monday" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-day-mon" }
+Switch i3Timer3DayTue "Timer 3 Tuesday" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-day-tue" }
+Switch i3Timer3DayWed "Timer 3 Wednesday" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-day-wed" }
+Switch i3Timer3DayThu "Timer 3 Thursday" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-day-thu" }
+Switch i3Timer3DayFri "Timer 3 Friday" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-day-fri" }
+Switch i3Timer3DaySat "Timer 3 Saturday" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-day-sat" }
+Switch i3Timer3DaySun "Timer 3 Sunday" <calendar> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-day-sun" }
+Switch i3Timer3Enabled "Timer 3 Enabled" <switch> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-enabled" }
+Switch i3OverrideEnabled "Override Timer Enabled" <switch> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#override-enabled"}
+DateTime i3OverrideDeparture "Override Timer Departure [%1$tH:%1$tM]" <time> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:charge#override-departure" }
+Number i3OverrideDepartureHour "Override Timer Departure Hour [%d]" <time> (i3)
+Number i3OverrideDepartureMinute "Override Timer Departure Minute [%d]" <time> (i3)
+
+Image i3Image "Image" (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:image#png" }
+String i3ImageViewport "Image Viewport [%s]" <zoom> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:image#view" }
+Number i3ImageSize "Image Size [%d]" <zoom> (i3) {channel="bmwconnecteddrive:bev_rex:user:i3:image#size" }
+```
+
+### Sitemap File
+
+```
+sitemap BMW label="BMW" {
+ Frame label="BMW i3" {
+ Image item=i3Image
+
+ }
+ Frame label="Range" {
+ Text item=i3Mileage
+ Text item=i3Range
+ Text item=i3RangeElectric
+ Text item=i3RangeFuel
+ Text item=i3BatterySoc
+ Text item=i3Fuel
+ Text item=i3RadiusElectric
+ Text item=i3RadiusHybrid
+ }
+ Frame label="Status" {
+ Text item=i3DoorStatus
+ Text item=i3WindowStatus
+ Text item=i3LockStatus
+ Text item=i3NextServiceDate
+ Text item=i3NextServiceMileage
+ Text item=i3CheckControl
+ Text item=i3ChargingStatus
+ Text item=i3LastUpdate
+ }
+ Frame label="Remote Services" {
+ Selection item=i3RemoteCommand
+ Text item=i3RemoteState
+ }
+ Frame label="Last Trip" {
+ Text item=i3TripDateTime
+ Text item=i3TripDuration
+ Text item=i3TripDistance
+ Text item=i3TripDistanceSinceCharge
+ Text item=i3AvgTripConsumption
+ Text item=i3AvgTripRecuperation
+ Text item=i3AvgTripCombined
+ }
+ Frame label="Lifetime" {
+ Text item=i3TotalElectric
+ Text item=i3LongestEVTrip
+ Text item=i3AvgConsumption
+ Text item=i3AvgRecuperation
+ Text item=i3AvgCombined
+ }
+ Frame label="Services" {
+ Text item=i3ServiceName
+ Text item=i3ServiceMileage
+ Text item=i3ServiceDate
+ }
+ Frame label="CheckControl" {
+ Text item=i3CCName
+ Text item=i3CCMileage
+ }
+ Frame label="Door Details" {
+ Text item=i3DriverDoor visibility=[i3DriverDoor!="INVALID"]
+ Text item=i3DriverDoorRear visibility=[i3DriverDoorRear!="INVALID"]
+ Text item=i3PassengerDoor visibility=[i3PassengerDoor!="INVALID"]
+ Text item=i3PassengerDoorRear visibility=[i3PassengerDoorRear!="INVALID"]
+ Text item=i3Hood visibility=[i3Hood!="INVALID"]
+ Text item=i3Trunk visibility=[i3Trunk!="INVALID"]
+ Text item=i3DriverWindow visibility=[i3DriverWindow!="INVALID"]
+ Text item=i3DriverWindowRear visibility=[i3DriverWindowRear!="INVALID"]
+ Text item=i3PassengerWindow visibility=[i3PassengerWindow!="INVALID"]
+ Text item=i3PassengerWindowRear visibility=[i3PassengerWindowRear!="INVALID"]
+ Text item=i3RearWindow visibility=[i3RearWindow!="INVALID"]
+ Text item=i3Sunroof visibility=[i3Sunroof!="INVALID"]
+ }
+ Frame label="Location" {
+ Text item=i3Location
+ Text item=i3Heading
+ }
+ Frame label="Charge Profile" {
+ Switch item=i3ChargeProfileClimate
+ Selection item=i3ChargeProfileMode
+ Text item=i3ChargeWindowStart
+ Setpoint item=i3ChargeWindowStartHour maxValue=23 step=1 icon="time"
+ Setpoint item=i3ChargeWindowStartMinute maxValue=55 step=5 icon="time"
+ Text item=i3ChargeWindowEnd
+ Setpoint item=i3ChargeWindowEndHour maxValue=23 step=1 icon="time"
+ Setpoint item=i3ChargeWindowEndMinute maxValue=55 step=5 icon="time"
+ Text item=i3Timer1Departure
+ Setpoint item=i3Timer1DepartureHour maxValue=23 step=1 icon="time"
+ Setpoint item=i3Timer1DepartureMinute maxValue=55 step=5 icon="time"
+ Text item=i3Timer1Days
+ Switch item=i3Timer1DayMon
+ Switch item=i3Timer1DayTue
+ Switch item=i3Timer1DayWed
+ Switch item=i3Timer1DayThu
+ Switch item=i3Timer1DayFri
+ Switch item=i3Timer1DaySat
+ Switch item=i3Timer1DaySun
+ Switch item=i3Timer1Enabled
+ Text item=i3Timer2Departure
+ Setpoint item=i3Timer2DepartureHour maxValue=23 step=1 icon="time"
+ Setpoint item=i3Timer2DepartureMinute maxValue=55 step=5 icon="time"
+ Text item=i3Timer2Days
+ Switch item=i3Timer2DayMon
+ Switch item=i3Timer2DayTue
+ Switch item=i3Timer2DayWed
+ Switch item=i3Timer2DayThu
+ Switch item=i3Timer2DayFri
+ Switch item=i3Timer2DaySat
+ Switch item=i3Timer2DaySun
+ Switch item=i3Timer2Enabled
+ Text item=i3Timer3Departure
+ Setpoint item=i3Timer3DepartureHour maxValue=23 step=1 icon="time"
+ Setpoint item=i3Timer3DepartureMinute maxValue=55 step=5 icon="time"
+ Text item=i3Timer3Days
+ Switch item=i3Timer3DayMon
+ Switch item=i3Timer3DayTue
+ Switch item=i3Timer3DayWed
+ Switch item=i3Timer3DayThu
+ Switch item=i3Timer3DayFri
+ Switch item=i3Timer3DaySat
+ Switch item=i3Timer3DaySun
+ Switch item=i3Timer3Enabled
+ Switch item=i3OverrideEnabled
+ Text item=i3OverrideDeparture
+ Setpoint item=i3OverrideDepartureHour maxValue=23 step=1 icon="time"
+ Setpoint item=i3OverrideDepartureMinute maxValue=55 step=5 icon="time"
+ }
+ Frame label="Last Destinations" {
+ Text item=i3DestName
+ Text item=i3DestLocation
+ }
+ Frame label="Image Properties" {
+ Text item=i3ImageViewport
+ Text item=i3ImageSize
+ }
+}
+```
+
+### Rules File
+
+```
+rule "i3ChargeWindowStartSetpoint"
+when
+ Item i3ChargeWindowStartMinute changed or
+ Item i3ChargeWindowStartHour changed
+then
+ val hour = (i3ChargeWindowStartHour.state as Number).intValue
+ val minute = (i3ChargeWindowStartMinute.state as Number).intValue
+ val time = (i3ChargeWindowStart.state as DateTimeType).zonedDateTime
+ i3ChargeWindowStart.sendCommand(new DateTimeType(time.withHour(hour).withMinute(minute)))
+end
+
+rule "i3ChargeWindowStart"
+when
+ Item i3ChargeWindowStart changed
+then
+ val time = (i3ChargeWindowStart.state as DateTimeType).zonedDateTime
+ i3ChargeWindowStartMinute.sendCommand(time.minute)
+ i3ChargeWindowStartHour.sendCommand(time.hour)
+end
+
+rule "i3ChargeWindowEndSetpoint"
+when
+ Item i3ChargeWindowEndMinute changed or
+ Item i3ChargeWindowEndHour changed
+then
+ val hour = (i3ChargeWindowEndHour.state as Number).intValue
+ val minute = (i3ChargeWindowEndMinute.state as Number).intValue
+ val time = (i3ChargeWindowEnd.state as DateTimeType).zonedDateTime
+ i3ChargeWindowEnd.sendCommand(new DateTimeType(time.withHour(hour).withMinute(minute)))
+end
+
+rule "i3ChargeWindowEnd"
+when
+ Item i3ChargeWindowEnd changed
+then
+ val time = (i3ChargeWindowEnd.state as DateTimeType).zonedDateTime
+ i3ChargeWindowEndMinute.sendCommand(time.minute)
+ i3ChargeWindowEndHour.sendCommand(time.hour)
+end
+
+rule "i3Timer1DepartureSetpoint"
+when
+ Item i3Timer1DepartureMinute changed or
+ Item i3Timer1DepartureHour changed
+then
+ val hour = (i3Timer1DepartureHour.state as Number).intValue
+ val minute = (i3Timer1DepartureMinute.state as Number).intValue
+ val time = (i3Timer1Departure.state as DateTimeType).zonedDateTime
+ i3Timer1Departure.sendCommand(new DateTimeType(time.withHour(hour).withMinute(minute)))
+end
+
+rule "i3Timer1Departure"
+when
+ Item i3Timer1Departure changed
+then
+ val time = (i3Timer1Departure.state as DateTimeType).zonedDateTime
+ i3Timer1DepartureMinute.sendCommand(time.minute)
+ i3Timer1DepartureHour.sendCommand(time.hour)
+end
+
+rule "i3Timer2DepartureSetpoint"
+when
+ Item i3Timer2DepartureMinute changed or
+ Item i3Timer2DepartureHour changed
+then
+ val hour = (i3Timer2DepartureHour.state as Number).intValue
+ val minute = (i3Timer2DepartureMinute.state as Number).intValue
+ val time = (i3Timer2Departure.state as DateTimeType).zonedDateTime
+ i3Timer2Departure.sendCommand(new DateTimeType(time.withHour(hour).withMinute(minute)))
+end
+
+rule "i3Timer2Departure"
+when
+ Item i3Timer2Departure changed
+then
+ val time = (i3Timer2Departure.state as DateTimeType).zonedDateTime
+ i3Timer2DepartureMinute.sendCommand(time.minute)
+ i3Timer2DepartureHour.sendCommand(time.hour)
+end
+
+rule "i3Timer3DepartureSetpoint"
+when
+ Item i3Timer3DepartureMinute changed or
+ Item i3Timer3DepartureHour changed
+then
+ val hour = (i3Timer3DepartureHour.state as Number).intValue
+ val minute = (i3Timer3DepartureMinute.state as Number).intValue
+ val time = (i3Timer3Departure.state as DateTimeType).zonedDateTime
+ i3Timer3Departure.sendCommand(new DateTimeType(time.withHour(hour).withMinute(minute)))
+end
+
+rule "i3Timer3Departure"
+when
+ Item i3Timer3Departure changed
+then
+ val time = (i3Timer3Departure.state as DateTimeType).zonedDateTime
+ i3Timer3DepartureMinute.sendCommand(time.minute)
+ i3Timer3DepartureHour.sendCommand(time.hour)
+end
+
+rule "i3OverrideDepartureSetpoint"
+when
+ Item i3OverrideDepartureMinute changed or
+ Item i3OverrideDepartureHour changed
+then
+ val hour = (i3OverrideDepartureHour.state as Number).intValue
+ val minute = (i3OverrideDepartureMinute.state as Number).intValue
+ val time = (i3OverrideDeparture.state as DateTimeType).zonedDateTime
+ i3OverrideDeparture.sendCommand(new DateTimeType(time.withHour(hour).withMinute(minute)))
+end
+
+rule "i3OverrideDeparture"
+when
+ Item i3OverrideDeparture changed
+then
+ val time = (i3OverrideDeparture.state as DateTimeType).zonedDateTime
+ i3OverrideDepartureMinute.sendCommand(time.minute)
+ i3OverrideDepartureHour.sendCommand(time.hour)
+end
+```
+
+### Action example
+
+```
+ val profile = getActions("bmwconnecteddrive", "bmwconnecteddrive:bev_rex:user:i3")
+ val now = ZonedDateTime.now.toLocalTime
+ profile.setChargingMode("DELAYED_CHARGING")
+ profile.setTimer1Departure(now.minusHours(2))
+ profile.setTimer1Days(java.util.Set())
+ profile.setTimer1Enabled(true)
+ profile.setTimer2Enabled(false)
+ profile.setTimer3Enabled(false)
+ profile.setPreferredWindowStart(now.minusHours(6))
+ profile.setPreferredWindowEnd(now.minusHours(2))
+ profile.sendChargeProfile()
+```
+
+## Credits
+
+This work is based on the project of [Bimmer Connected](https://github.com/bimmerconnected/bimmer_connected).
+Also a [manual installation based on python](https://community.openhab.org/t/script-to-access-the-bmw-connecteddrive-portal-via-oh/37345) was already available for openHAB.
+This binding is basically a port to openHAB based on these concept works!
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.openhab.addons.bundles</groupId>
+ <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+ <version>3.1.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>org.openhab.binding.bmwconnecteddrive</artifactId>
+
+ <name>openHAB Add-ons :: Bundles :: BMWConnectedDrive Binding</name>
+
+</project>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.bmwconnecteddrive-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+ <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${project.version}/xml/features</repository>
+
+ <feature name="openhab-binding-bmwconnecteddrive" description="BMWConnectedDrive Binding" version="${project.version}">
+ <feature>openhab-runtime-base</feature>
+ <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bmwconnecteddrive/${project.version}</bundle>
+ </feature>
+</features>
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+/**
+ * The {@link ConnectedDriveConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class ConnectedDriveConfiguration {
+
+ /**
+ * Depending on the location the correct server needs to be called
+ */
+ public String region = Constants.EMPTY;
+
+ /**
+ * BMW Connected Drive Username
+ */
+ public String userName = Constants.EMPTY;
+
+ /**
+ * BMW Connected Drive Password
+ */
+ public String password = Constants.EMPTY;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link ConnectedDriveConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - edit & send of charge profile
+ */
+@NonNullByDefault
+public class ConnectedDriveConstants {
+
+ private static final String BINDING_ID = "bmwconnecteddrive";
+
+ // Units
+ public static final String UNITS_AUTODETECT = "AUTODETECT";
+ public static final String UNITS_IMPERIAL = "IMPERIAL";
+ public static final String UNITS_METRIC = "METRIC";
+
+ public static final String VIN = "vin";
+
+ public static final int DEFAULT_IMAGE_SIZE_PX = 1024;
+ public static final int DEFAULT_REFRESH_INTERVAL_MINUTES = 5;
+ public static final String DEFAULT_IMAGE_VIEWPORT = "FRONT";
+
+ // See constants from bimmer-connected
+ // https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/vehicle.py
+ public enum VehicleType {
+ CONVENTIONAL("conv"),
+ PLUGIN_HYBRID("phev"),
+ ELECTRIC_REX("bev_rex"),
+ ELECTRIC("bev");
+
+ private final String type;
+
+ VehicleType(String s) {
+ type = s;
+ }
+
+ @Override
+ public String toString() {
+ return type;
+ }
+ }
+
+ public enum ChargingMode {
+ IMMEDIATE_CHARGING,
+ DELAYED_CHARGING
+ }
+
+ public enum ChargingPreference {
+ NO_PRESELECTION,
+ CHARGING_WINDOW
+ }
+
+ public static final Set<String> FUEL_VEHICLES = Set.of(VehicleType.CONVENTIONAL.toString(),
+ VehicleType.PLUGIN_HYBRID.toString(), VehicleType.ELECTRIC_REX.toString());
+ public static final Set<String> ELECTRIC_VEHICLES = Set.of(VehicleType.ELECTRIC.toString(),
+ VehicleType.PLUGIN_HYBRID.toString(), VehicleType.ELECTRIC_REX.toString());
+
+ // Countries with Mileage display
+ public static final Set<String> IMPERIAL_COUNTRIES = Set.of("US", "GB");
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID THING_TYPE_CONNECTED_DRIVE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account");
+ public static final ThingTypeUID THING_TYPE_CONV = new ThingTypeUID(BINDING_ID,
+ VehicleType.CONVENTIONAL.toString());
+ public static final ThingTypeUID THING_TYPE_PHEV = new ThingTypeUID(BINDING_ID,
+ VehicleType.PLUGIN_HYBRID.toString());
+ public static final ThingTypeUID THING_TYPE_BEV_REX = new ThingTypeUID(BINDING_ID,
+ VehicleType.ELECTRIC_REX.toString());
+ public static final ThingTypeUID THING_TYPE_BEV = new ThingTypeUID(BINDING_ID, VehicleType.ELECTRIC.toString());
+ public static final Set<ThingTypeUID> SUPPORTED_THING_SET = Set.of(THING_TYPE_CONNECTED_DRIVE_ACCOUNT,
+ THING_TYPE_CONV, THING_TYPE_PHEV, THING_TYPE_BEV_REX, THING_TYPE_BEV);
+
+ // Thing Group definitions
+ public static final String CHANNEL_GROUP_STATUS = "status";
+ public static final String CHANNEL_GROUP_SERVICE = "service";
+ public static final String CHANNEL_GROUP_CHECK_CONTROL = "check";
+ public static final String CHANNEL_GROUP_DOORS = "doors";
+ public static final String CHANNEL_GROUP_RANGE = "range";
+ public static final String CHANNEL_GROUP_LOCATION = "location";
+ public static final String CHANNEL_GROUP_LAST_TRIP = "last-trip";
+ public static final String CHANNEL_GROUP_LIFETIME = "lifetime";
+ public static final String CHANNEL_GROUP_REMOTE = "remote";
+ public static final String CHANNEL_GROUP_CHARGE = "charge";
+ public static final String CHANNEL_GROUP_VEHICLE_IMAGE = "image";
+ public static final String CHANNEL_GROUP_DESTINATION = "destination";
+
+ // Generic Constants for several groups
+ public static final String NAME = "name";
+ public static final String DETAILS = "details";
+ public static final String DATE = "date";
+ public static final String MILEAGE = "mileage";
+ public static final String GPS = "gps";
+ public static final String HEADING = "heading";
+
+ // Status
+ public static final String DOORS = "doors";
+ public static final String WINDOWS = "windows";
+ public static final String LOCK = "lock";
+ public static final String SERVICE_DATE = "service-date";
+ public static final String SERVICE_MILEAGE = "service-mileage";
+ public static final String CHECK_CONTROL = "check-control";
+ public static final String CHARGE_STATUS = "charge";
+ public static final String CHARGE_END_REASON = "reason";
+ public static final String CHARGE_REMAINING = "remaining";
+ public static final String LAST_UPDATE = "last-update";
+
+ // Door Details
+ public static final String DOOR_DRIVER_FRONT = "driver-front";
+ public static final String DOOR_DRIVER_REAR = "driver-rear";
+ public static final String DOOR_PASSENGER_FRONT = "passenger-front";
+ public static final String DOOR_PASSENGER_REAR = "passenger-rear";
+ public static final String HOOD = "hood";
+ public static final String TRUNK = "trunk";
+ public static final String WINDOW_DOOR_DRIVER_FRONT = "win-driver-front";
+ public static final String WINDOW_DOOR_DRIVER_REAR = "win-driver-rear";
+ public static final String WINDOW_DOOR_PASSENGER_FRONT = "win-passenger-front";
+ public static final String WINDOW_DOOR_PASSENGER_REAR = "win-passenger-rear";
+ public static final String WINDOW_REAR = "win-rear";
+ public static final String SUNROOF = "sunroof";
+
+ // Charge Profile
+ public static final String CHARGE_PROFILE_CLIMATE = "profile-climate";
+ public static final String CHARGE_PROFILE_MODE = "profile-mode";
+ public static final String CHARGE_PROFILE_PREFERENCE = "profile-prefs";
+ public static final String CHARGE_WINDOW_START = "window-start";
+ public static final String CHARGE_WINDOW_END = "window-end";
+ public static final String CHARGE_TIMER1 = "timer1";
+ public static final String CHARGE_TIMER2 = "timer2";
+ public static final String CHARGE_TIMER3 = "timer3";
+ public static final String CHARGE_OVERRIDE = "override";
+ public static final String CHARGE_DEPARTURE = "-departure";
+ public static final String CHARGE_ENABLED = "-enabled";
+ public static final String CHARGE_DAYS = "-days";
+ public static final String CHARGE_DAY_MON = "-day-mon";
+ public static final String CHARGE_DAY_TUE = "-day-tue";
+ public static final String CHARGE_DAY_WED = "-day-wed";
+ public static final String CHARGE_DAY_THU = "-day-thu";
+ public static final String CHARGE_DAY_FRI = "-day-fri";
+ public static final String CHARGE_DAY_SAT = "-day-sat";
+ public static final String CHARGE_DAY_SUN = "-day-sun";
+
+ // Range
+ public static final String RANGE_HYBRID = "hybrid";
+ public static final String RANGE_ELECTRIC = "electric";
+ public static final String SOC = "soc";
+ public static final String RANGE_FUEL = "fuel";
+ public static final String REMAINING_FUEL = "remaining-fuel";
+ public static final String RANGE_RADIUS_ELECTRIC = "radius-electric";
+ public static final String RANGE_RADIUS_FUEL = "radius-fuel";
+ public static final String RANGE_RADIUS_HYBRID = "radius-hybrid";
+
+ // Last Trip
+ public static final String DURATION = "duration";
+ public static final String DISTANCE = "distance";
+ public static final String DISTANCE_SINCE_CHARGING = "distance-since-charging";
+ public static final String AVG_CONSUMPTION = "avg-consumption";
+ public static final String AVG_COMBINED_CONSUMPTION = "avg-combined-consumption";
+ public static final String AVG_RECUPERATION = "avg-recuperation";
+
+ // Lifetime + Average Consumptions
+ public static final String TOTAL_DRIVEN_DISTANCE = "total-driven-distance";
+ public static final String SINGLE_LONGEST_DISTANCE = "single-longest-distance";
+
+ // Image
+ public static final String IMAGE_FORMAT = "png";
+ public static final String IMAGE_VIEWPORT = "view";
+ public static final String IMAGE_SIZE = "size";
+
+ // Remote Services
+ public static final String REMOTE_SERVICE_LIGHT_FLASH = "light";
+ public static final String REMOTE_SERVICE_VEHICLE_FINDER = "finder";
+ public static final String REMOTE_SERVICE_DOOR_LOCK = "lock";
+ public static final String REMOTE_SERVICE_DOOR_UNLOCK = "unlock";
+ public static final String REMOTE_SERVICE_HORN = "horn";
+ public static final String REMOTE_SERVICE_AIR_CONDITIONING = "climate";
+ public static final String REMOTE_SERVICE_CHARGE_NOW = "charge-now";
+ public static final String REMOTE_SERVICE_CHARGING_CONTROL = "charge-control";
+ public static final String REMOTE_SERVICE_COMMAND = "command";
+ public static final String REMOTE_STATE = "state";
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.BMWConnectedDriveOptionProvider;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.ConnectedDriveBridgeHandler;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.VehicleHandler;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.core.i18n.LocaleProvider;
+import org.openhab.core.i18n.TimeZoneProvider;
+import org.openhab.core.io.net.http.HttpClientFactory;
+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.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link ConnectedDriveHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.bmwconnecteddrive", service = ThingHandlerFactory.class)
+public class ConnectedDriveHandlerFactory extends BaseThingHandlerFactory {
+
+ private final HttpClientFactory httpClientFactory;
+ private final BMWConnectedDriveOptionProvider optionProvider;
+ private boolean imperial = false;
+
+ @Activate
+ public ConnectedDriveHandlerFactory(final @Reference HttpClientFactory hcf,
+ final @Reference BMWConnectedDriveOptionProvider op, final @Reference LocaleProvider lp,
+ final @Reference TimeZoneProvider timeZoneProvider) {
+ httpClientFactory = hcf;
+ optionProvider = op;
+ imperial = IMPERIAL_COUNTRIES.contains(lp.getLocale().getCountry());
+ Converter.setTimeZoneProvider(timeZoneProvider);
+ }
+
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_SET.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+ if (THING_TYPE_CONNECTED_DRIVE_ACCOUNT.equals(thingTypeUID)) {
+ return new ConnectedDriveBridgeHandler((Bridge) thing, httpClientFactory);
+ } else if (SUPPORTED_THING_SET.contains(thingTypeUID)) {
+ VehicleHandler vh = new VehicleHandler(thing, optionProvider, thingTypeUID.getId(), imperial);
+ return vh;
+ }
+ return null;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+/**
+ * The {@link VehicleConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class VehicleConfiguration {
+ /**
+ * Vehicle Identification Number (VIN)
+ */
+ public String vin = Constants.EMPTY;
+
+ /**
+ * Data refresh rate in minutes
+ */
+ public int refreshInterval = ConnectedDriveConstants.DEFAULT_REFRESH_INTERVAL_MINUTES;
+
+ /**
+ * Either Auto Detect Miles units (UK & US) or select Format directly
+ * <option value="AUTODETECT">Auto Detect</option>
+ * <option value="METRIC">Metric</option>
+ * <option value="IMPERIAL">Imperial</option>
+ */
+ public String units = ConnectedDriveConstants.UNITS_AUTODETECT;
+
+ /**
+ * image size - width & length (square)
+ */
+ public int imageSize = ConnectedDriveConstants.DEFAULT_IMAGE_SIZE_PX;
+
+ /**
+ * image viewport defined as options in thing xml
+ * <option value="FRONT">Front</option>
+ * <option value="REAR">Rear</option>
+ * <option value="SIDE">Slide</option>
+ * <option value="DASHBOARD">Dashboard</option>
+ * <option value="DRIVERDOOR">Driver Door</option>
+ */
+ public String imageViewport = ConnectedDriveConstants.DEFAULT_IMAGE_VIEWPORT;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.action;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper.ProfileKey.*;
+
+import java.time.DayOfWeek;
+import java.time.LocalTime;
+import java.util.Optional;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.VehicleHandler;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper.ProfileKey;
+import org.openhab.core.automation.annotation.ActionInput;
+import org.openhab.core.automation.annotation.ActionOutput;
+import org.openhab.core.automation.annotation.RuleAction;
+import org.openhab.core.thing.binding.ThingActions;
+import org.openhab.core.thing.binding.ThingActionsScope;
+import org.openhab.core.thing.binding.ThingHandler;
+
+/**
+ * The {@link BMWConnectedDriveActions} provides actions for VehicleHandler
+ *
+ * @author Norbert Truchsess - Initial contribution
+ */
+@ThingActionsScope(name = "bmwconnecteddrive")
+@NonNullByDefault
+public class BMWConnectedDriveActions implements ThingActions {
+
+ private Optional<VehicleHandler> handler = Optional.empty();
+
+ private Optional<ChargeProfileWrapper> profile = Optional.empty();
+
+ @RuleAction(label = "getTimer1Departure", description = "returns the departure time of timer1")
+ public @ActionOutput(name = "time", type = "java.util.Optional<java.time.LocalTime>") Optional<LocalTime> getTimer1Departure() {
+ return getTime(TIMER1);
+ }
+
+ @RuleAction(label = "setTimer1Departure", description = "sets the timer1 departure time")
+ public void setTimer1Departure(@ActionInput(name = "time", type = "java.time.LocalTime") @Nullable LocalTime time) {
+ setTime(TIMER1, time);
+ }
+
+ @RuleAction(label = "getTimer1Enabled", description = "returns the enabled state of timer1")
+ public @ActionOutput(name = "enabled", type = "java.util.Optional<java.lang.Boolean>") Optional<Boolean> getTimer1Enabled() {
+ return getEnabled(TIMER1);
+ }
+
+ @RuleAction(label = "setTimer1Enabled", description = "sets the enabled state of timer1")
+ public void setTimer1Enabled(@ActionInput(name = "enabled", type = "java.lang.Boolean") @Nullable Boolean enabled) {
+ setEnabled(TIMER1, enabled);
+ }
+
+ @RuleAction(label = "getTimer2Departure", description = "returns the departure time of timer2")
+ public @ActionOutput(name = "time", type = "java.util.Optional<java.time.LocalTime>") Optional<LocalTime> getTimer2Departure() {
+ return getTime(TIMER2);
+ }
+
+ @RuleAction(label = "setTimer2Departure", description = "sets the timer2 departure time")
+ public void setTimer2Departure(@ActionInput(name = "time", type = "java.time.LocalTime") @Nullable LocalTime time) {
+ setTime(TIMER2, time);
+ }
+
+ @RuleAction(label = "getTimer2Enabled", description = "returns the enabled state of timer2")
+ public @ActionOutput(name = "enabled", type = "java.util.Optional<java.lang.Boolean>") Optional<Boolean> getTimer2Enabled() {
+ return getEnabled(TIMER2);
+ }
+
+ @RuleAction(label = "setTimer2Enabled", description = "sets the enabled state of timer2")
+ public void setTimer2Enabled(@ActionInput(name = "enabled", type = "java.lang.Boolean") @Nullable Boolean enabled) {
+ setEnabled(TIMER2, enabled);
+ }
+
+ @RuleAction(label = "getTimer3Departure", description = "returns the departure time of timer3")
+ public @ActionOutput(name = "time", type = "java.util.Optional<java.time.LocalTime>") Optional<LocalTime> getTimer3Departure() {
+ return getTime(TIMER3);
+ }
+
+ @RuleAction(label = "setTimer3Departure", description = "sets the timer3 departure time")
+ public void setTimer3Departure(@ActionInput(name = "time", type = "java.time.LocalTime") @Nullable LocalTime time) {
+ setTime(TIMER3, time);
+ }
+
+ @RuleAction(label = "getTimer3Enabled", description = "returns the enabled state of timer3")
+ public @ActionOutput(name = "enabled", type = "java.util.Optional<java.lang.Boolean>") Optional<Boolean> getTimer3Enabled() {
+ return getEnabled(TIMER3);
+ }
+
+ @RuleAction(label = "setTimer3Enabled", description = "sets the enabled state of timer3")
+ public void setTimer3Enabled(@ActionInput(name = "enabled", type = "java.lang.Boolean") @Nullable Boolean enabled) {
+ setEnabled(TIMER3, enabled);
+ }
+
+ @RuleAction(label = "getOverrideTimerDeparture", description = "returns the departure time of overrideTimer")
+ public @ActionOutput(name = "time", type = "java.util.Optional<java.time.LocalTime>") Optional<LocalTime> getOverrideTimerDeparture() {
+ return getTime(OVERRIDE);
+ }
+
+ @RuleAction(label = "setOverrideTimerDeparture", description = "sets the overrideTimer departure time")
+ public void setOverrideTimerDeparture(
+ @ActionInput(name = "time", type = "java.time.LocalTime") @Nullable LocalTime time) {
+ setTime(OVERRIDE, time);
+ }
+
+ @RuleAction(label = "getOverrideTimerEnabled", description = "returns the enabled state of overrideTimer")
+ public @ActionOutput(name = "enabled", type = "java.util.Optional<java.lang.Boolean>") Optional<Boolean> getOverrideTimerEnabled() {
+ return getEnabled(OVERRIDE);
+ }
+
+ @RuleAction(label = "setOverrideTimerEnabled", description = "sets the enabled state of overrideTimer")
+ public void setOverrideTimerEnabled(
+ @ActionInput(name = "enabled", type = "java.lang.Boolean") @Nullable Boolean enabled) {
+ setEnabled(OVERRIDE, enabled);
+ }
+
+ @RuleAction(label = "getPreferredWindowStart", description = "returns the preferred charging-window start time")
+ public @ActionOutput(name = "time", type = "java.util.Optional<java.time.LocalTime>") Optional<LocalTime> getPreferredWindowStart() {
+ return getTime(WINDOWSTART);
+ }
+
+ @RuleAction(label = "setPreferredWindowStart", description = "sets the preferred charging-window start time")
+ public void setPreferredWindowStart(
+ @ActionInput(name = "time", type = "java.time.LocalTime") @Nullable LocalTime time) {
+ setTime(WINDOWSTART, time);
+ }
+
+ @RuleAction(label = "getPreferredWindowEnd", description = "returns the preferred charging-window end time")
+ public @ActionOutput(name = "time", type = "java.util.Optional<java.time.LocalTime>") Optional<LocalTime> getPreferredWindowEnd() {
+ return getTime(WINDOWEND);
+ }
+
+ @RuleAction(label = "setPreferredWindowEnd", description = "sets the preferred charging-window end time")
+ public void setPreferredWindowEnd(
+ @ActionInput(name = "time", type = "java.time.LocalTime") @Nullable LocalTime time) {
+ setTime(WINDOWEND, time);
+ }
+
+ @RuleAction(label = "getClimatizationEnabled", description = "returns the enabled state of climatization")
+ public @ActionOutput(name = "enabled", type = "java.util.Optional<java.lang.Boolean>") Optional<Boolean> getClimatizationEnabled() {
+ return getEnabled(CLIMATE);
+ }
+
+ @RuleAction(label = "setClimatizationEnabled", description = "sets the enabled state of climatization")
+ public void setClimatizationEnabled(
+ @ActionInput(name = "enabled", type = "java.lang.Boolean") @Nullable Boolean enabled) {
+ setEnabled(CLIMATE, enabled);
+ }
+
+ @RuleAction(label = "getChargingMode", description = "gets the charging-mode")
+ public @ActionOutput(name = "mode", type = "java.util.Optional<java.lang.String>") Optional<String> getChargingMode() {
+ return getProfile().map(profile -> profile.getMode());
+ }
+
+ @RuleAction(label = "setChargingMode", description = "sets the charging-mode")
+ public void setChargingMode(@ActionInput(name = "mode", type = "java.lang.String") @Nullable String mode) {
+ getProfile().ifPresent(profile -> profile.setMode(mode));
+ }
+
+ @RuleAction(label = "getTimer1Days", description = "returns the days of week timer1 is enabled for")
+ public @ActionOutput(name = "days", type = "java.util.Optional<java.util.Set<java.time.DayOfWeek>>") Optional<Set<DayOfWeek>> getTimer1Days() {
+ return getDays(TIMER1);
+ }
+
+ @RuleAction(label = "setTimer1Days", description = "sets the days of week timer1 is enabled for")
+ public void setTimer1Days(
+ @ActionInput(name = "days", type = "java.util.Set<java.time.DayOfWeek>") @Nullable Set<DayOfWeek> days) {
+ setDays(TIMER1, days);
+ }
+
+ @RuleAction(label = "getTimer2Days", description = "returns the days of week timer2 is enabled for")
+ public @ActionOutput(name = "days", type = "java.util.Optional<java.util.Set<java.time.DayOfWeek>>") Optional<Set<DayOfWeek>> getTimer2Days() {
+ return getDays(TIMER2);
+ }
+
+ @RuleAction(label = "setTimer2Days", description = "sets the days of week timer2 is enabled for")
+ public void setTimer2Days(
+ @ActionInput(name = "days", type = "java.util.Set<java.time.DayOfWeek>") @Nullable Set<DayOfWeek> days) {
+ setDays(TIMER2, days);
+ }
+
+ @RuleAction(label = "getTimer3Days", description = "returns the days of week timer3 is enabled for")
+ public @ActionOutput(name = "days", type = "java.util.Optional<java.util.Set<java.time.DayOfWeek>>") Optional<Set<DayOfWeek>> getTimer3Days() {
+ return getDays(TIMER3);
+ }
+
+ @RuleAction(label = "setTimer3Days", description = "sets the days of week timer3 is enabled for")
+ public void setTimer3Days(
+ @ActionInput(name = "days", type = "java.util.Set<java.time.DayOfWeek>") @Nullable Set<DayOfWeek> days) {
+ setDays(TIMER3, days);
+ }
+
+ @RuleAction(label = "getOverrideTimerDays", description = "returns the days of week the overrideTimer is enabled for")
+ public @ActionOutput(name = "days", type = "java.util.Optional<java.util.Set<java.time.DayOfWeek>>") Optional<Set<DayOfWeek>> getOverrideTimerDays() {
+ return getDays(OVERRIDE);
+ }
+
+ @RuleAction(label = "setOverrideTimerDays", description = "sets the days of week the overrideTimer is enabled for")
+ public void setOverrideTimerDays(
+ @ActionInput(name = "days", type = "java.util.Set<java.time.DayOfWeek>") @Nullable Set<DayOfWeek> days) {
+ setDays(OVERRIDE, days);
+ }
+
+ @RuleAction(label = "sendChargeProfile", description = "sends the charging profile to the vehicle")
+ public void sendChargeProfile() {
+ handler.ifPresent(handle -> handle.sendChargeProfile(getProfile()));
+ }
+
+ @RuleAction(label = "cancel", description = "cancel current edit of charging profile")
+ public void cancelEditChargeProfile() {
+ profile = Optional.empty();
+ }
+
+ public static Optional<LocalTime> getTimer1Departure(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getTimer1Departure();
+ }
+
+ public static void setTimer1Departure(ThingActions actions, @Nullable LocalTime time) {
+ ((BMWConnectedDriveActions) actions).setTimer1Departure(time);
+ }
+
+ public static Optional<Boolean> getTimer1Enabled(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getTimer1Enabled();
+ }
+
+ public static void setTimer1Enabled(ThingActions actions, @Nullable Boolean enabled) {
+ ((BMWConnectedDriveActions) actions).setTimer1Enabled(enabled);
+ }
+
+ public static Optional<LocalTime> getTimer2Departure(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getTimer2Departure();
+ }
+
+ public static void setTimer2Departure(ThingActions actions, @Nullable LocalTime time) {
+ ((BMWConnectedDriveActions) actions).setTimer2Departure(time);
+ }
+
+ public static Optional<Boolean> getTimer2Enabled(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getTimer2Enabled();
+ }
+
+ public static void setTimer2Enabled(ThingActions actions, @Nullable Boolean enabled) {
+ ((BMWConnectedDriveActions) actions).setTimer2Enabled(enabled);
+ }
+
+ public static Optional<LocalTime> getTimer3Departure(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getTimer3Departure();
+ }
+
+ public static void setTimer3Departure(ThingActions actions, @Nullable LocalTime time) {
+ ((BMWConnectedDriveActions) actions).setTimer3Departure(time);
+ }
+
+ public static Optional<Boolean> getTimer3Enabled(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getTimer3Enabled();
+ }
+
+ public static void setTimer3Enabled(ThingActions actions, @Nullable Boolean enabled) {
+ ((BMWConnectedDriveActions) actions).setTimer3Enabled(enabled);
+ }
+
+ public static Optional<LocalTime> getOverrideTimerDeparture(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getOverrideTimerDeparture();
+ }
+
+ public static void setOverrideTimerDeparture(ThingActions actions, @Nullable LocalTime time) {
+ ((BMWConnectedDriveActions) actions).setOverrideTimerDeparture(time);
+ }
+
+ public static Optional<Boolean> getOverrideTimerEnabled(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getOverrideTimerEnabled();
+ }
+
+ public static void setOverrideTimerEnabled(ThingActions actions, @Nullable Boolean enabled) {
+ ((BMWConnectedDriveActions) actions).setOverrideTimerEnabled(enabled);
+ }
+
+ public static Optional<LocalTime> getPreferredWindowStart(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getPreferredWindowStart();
+ }
+
+ public static void setPreferredWindowStart(ThingActions actions, @Nullable LocalTime time) {
+ ((BMWConnectedDriveActions) actions).setPreferredWindowStart(time);
+ }
+
+ public static Optional<LocalTime> getPreferredWindowEnd(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getPreferredWindowEnd();
+ }
+
+ public static void setPreferredWindowEnd(ThingActions actions, @Nullable LocalTime time) {
+ ((BMWConnectedDriveActions) actions).setPreferredWindowEnd(time);
+ }
+
+ public static Optional<Boolean> getClimatizationEnabled(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getClimatizationEnabled();
+ }
+
+ public static void setClimatizationEnabled(ThingActions actions, @Nullable Boolean enabled) {
+ ((BMWConnectedDriveActions) actions).setClimatizationEnabled(enabled);
+ }
+
+ public static Optional<String> getChargingMode(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getChargingMode();
+ }
+
+ public static void setChargingMode(ThingActions actions, @Nullable String mode) {
+ ((BMWConnectedDriveActions) actions).setChargingMode(mode);
+ }
+
+ public static Optional<Set<DayOfWeek>> getTimer1Days(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getTimer1Days();
+ }
+
+ public static void setTimer1Days(ThingActions actions, @Nullable Set<DayOfWeek> days) {
+ ((BMWConnectedDriveActions) actions).setTimer1Days(days);
+ }
+
+ public static Optional<Set<DayOfWeek>> getTimer2Days(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getTimer2Days();
+ }
+
+ public static void setTimer2Days(ThingActions actions, @Nullable Set<DayOfWeek> days) {
+ ((BMWConnectedDriveActions) actions).setTimer2Days(days);
+ }
+
+ public static Optional<Set<DayOfWeek>> getTimer3Days(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getTimer3Days();
+ }
+
+ public static void setTimer3Days(ThingActions actions, @Nullable Set<DayOfWeek> days) {
+ ((BMWConnectedDriveActions) actions).setTimer3Days(days);
+ }
+
+ public static Optional<Set<DayOfWeek>> getOverrideTimerDays(ThingActions actions) {
+ return ((BMWConnectedDriveActions) actions).getOverrideTimerDays();
+ }
+
+ public static void setOverrideTimerDays(ThingActions actions, @Nullable Set<DayOfWeek> days) {
+ ((BMWConnectedDriveActions) actions).setOverrideTimerDays(days);
+ }
+
+ public static void sendChargeProfile(ThingActions actions) {
+ ((BMWConnectedDriveActions) actions).sendChargeProfile();
+ }
+
+ public static void cancelEditChargeProfile(ThingActions actions) {
+ ((BMWConnectedDriveActions) actions).cancelEditChargeProfile();
+ }
+
+ @Override
+ public void setThingHandler(@Nullable ThingHandler handler) {
+ if (handler instanceof VehicleHandler) {
+ this.handler = Optional.of((VehicleHandler) handler);
+ }
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return handler.get();
+ }
+
+ private Optional<ChargeProfileWrapper> getProfile() {
+ if (profile.isEmpty()) {
+ profile = handler.flatMap(handle -> handle.getChargeProfileWrapper());
+ }
+ return profile;
+ }
+
+ private Optional<LocalTime> getTime(ProfileKey key) {
+ return getProfile().map(profile -> profile.getTime(key));
+ }
+
+ private void setTime(ProfileKey key, @Nullable LocalTime time) {
+ getProfile().ifPresent(profile -> profile.setTime(key, time));
+ }
+
+ private Optional<Boolean> getEnabled(ProfileKey key) {
+ return getProfile().map(profile -> profile.isEnabled(key));
+ }
+
+ private void setEnabled(ProfileKey key, @Nullable Boolean enabled) {
+ getProfile().ifPresent(profile -> profile.setEnabled(key, enabled));
+ }
+
+ private Optional<Set<DayOfWeek>> getDays(ProfileKey key) {
+ return getProfile().map(profile -> profile.getDays(key));
+ }
+
+ private void setDays(ProfileKey key, @Nullable Set<DayOfWeek> days) {
+ getProfile().ifPresent(profile -> {
+ profile.setDays(key, days);
+ });
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.discovery;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.SUPPORTED_THING_SET;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.discovery.VehiclesContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.ConnectedDriveBridgeHandler;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link VehicleDiscovery} requests data from ConnectedDrive and is identifying the Vehicles after response
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class VehicleDiscovery extends AbstractDiscoveryService implements DiscoveryService, ThingHandlerService {
+
+ private final Logger logger = LoggerFactory.getLogger(VehicleDiscovery.class);
+ private static final int DISCOVERY_TIMEOUT = 10;
+ private Optional<ConnectedDriveBridgeHandler> bridgeHandler = Optional.empty();
+
+ public VehicleDiscovery() {
+ super(SUPPORTED_THING_SET, DISCOVERY_TIMEOUT, false);
+ }
+
+ public void onResponse(VehiclesContainer container) {
+ bridgeHandler.ifPresent(bridge -> {
+ final ThingUID bridgeUID = bridge.getThing().getUID();
+ container.vehicles.forEach(vehicle -> {
+ // the DriveTrain field in the delivered json is defining the Vehicle Type
+ String vehicleType = vehicle.driveTrain.toLowerCase();
+ SUPPORTED_THING_SET.forEach(entry -> {
+ if (entry.getId().equals(vehicleType)) {
+ ThingUID uid = new ThingUID(entry, vehicle.vin, bridgeUID.getId());
+ Map<String, String> properties = new HashMap<>();
+ // Dealer
+ if (vehicle.dealer != null) {
+ properties.put("dealer", vehicle.dealer.name);
+ properties.put("dealerAddress", vehicle.dealer.street + " " + vehicle.dealer.country + " "
+ + vehicle.dealer.postalCode + " " + vehicle.dealer.city);
+ properties.put("dealerPhone", vehicle.dealer.phone);
+ }
+
+ // Services & Support
+ properties.put("servicesActivated", getObject(vehicle, Constants.ACTIVATED));
+ String servicesSupported = getObject(vehicle, Constants.SUPPORTED);
+ String servicesNotSupported = getObject(vehicle, Constants.NOT_SUPPORTED);
+ if (vehicle.statisticsAvailable) {
+ servicesSupported += Constants.STATISTICS;
+ } else {
+ servicesNotSupported += Constants.STATISTICS;
+ }
+ properties.put(Constants.SERVICES_SUPPORTED, servicesSupported);
+ properties.put("servicesNotSupported", servicesNotSupported);
+ properties.put("supportBreakdownNumber", vehicle.breakdownNumber);
+
+ // Vehicle Properties
+ if (vehicle.supportedChargingModes != null) {
+ properties.put("vehicleChargeModes",
+ String.join(Constants.SPACE, vehicle.supportedChargingModes));
+ }
+ if (vehicle.hasAlarmSystem) {
+ properties.put("vehicleAlarmSystem", "Available");
+ } else {
+ properties.put("vehicleAlarmSystem", "Not Available");
+ }
+ properties.put("vehicleBrand", vehicle.brand);
+ properties.put("vehicleBodytype", vehicle.bodytype);
+ properties.put("vehicleColor", vehicle.color);
+ properties.put("vehicleConstructionYear", Short.toString(vehicle.yearOfConstruction));
+ properties.put("vehicleDriveTrain", vehicle.driveTrain);
+ properties.put("vehicleModel", vehicle.model);
+ if (vehicle.chargingControl != null) {
+ properties.put("vehicleChargeControl", Converter.toTitleCase(vehicle.model));
+ }
+
+ // Update Properties for already created Things
+ bridge.getThing().getThings().forEach(vehicleThing -> {
+ Configuration c = vehicleThing.getConfiguration();
+ if (c.containsKey(ConnectedDriveConstants.VIN)) {
+ String thingVIN = c.get(ConnectedDriveConstants.VIN).toString();
+ if (vehicle.vin.equals(thingVIN)) {
+ vehicleThing.setProperties(properties);
+ }
+ }
+ });
+
+ // Properties needed for functional THing
+ properties.put(ConnectedDriveConstants.VIN, vehicle.vin);
+ properties.put("refreshInterval",
+ Integer.toString(ConnectedDriveConstants.DEFAULT_REFRESH_INTERVAL_MINUTES));
+ properties.put("units", ConnectedDriveConstants.UNITS_AUTODETECT);
+ properties.put("imageSize", Integer.toString(ConnectedDriveConstants.DEFAULT_IMAGE_SIZE_PX));
+ properties.put("imageViewport", ConnectedDriveConstants.DEFAULT_IMAGE_VIEWPORT);
+
+ String vehicleLabel = vehicle.brand + " " + vehicle.model;
+ Map<String, Object> convertedProperties = new HashMap<String, Object>(properties);
+ thingDiscovered(DiscoveryResultBuilder.create(uid).withBridge(bridgeUID)
+ .withRepresentationProperty(ConnectedDriveConstants.VIN).withLabel(vehicleLabel)
+ .withProperties(convertedProperties).build());
+ }
+ });
+ });
+ });
+ };
+
+ /**
+ * Get all field names from a DTO with a specific value
+ * Used to get e.g. all services which are "ACTIVATED"
+ *
+ * @param DTO Object
+ * @param compare String which needs to map with the value
+ * @return String with all field names matching this value separated with Spaces
+ */
+ public String getObject(Object dto, String compare) {
+ StringBuilder buf = new StringBuilder();
+ for (Field field : dto.getClass().getDeclaredFields()) {
+ try {
+ Object value = field.get(dto);
+ if (compare.equals(value)) {
+ buf.append(Converter.capitalizeFirst(field.getName()) + Constants.SPACE);
+ }
+ } catch (IllegalArgumentException | IllegalAccessException e) {
+ logger.debug("Field {} not found {}", compare, e.getMessage());
+ }
+ }
+ return buf.toString();
+ }
+
+ @Override
+ public void setThingHandler(ThingHandler handler) {
+ if (handler instanceof ConnectedDriveBridgeHandler) {
+ bridgeHandler = Optional.of((ConnectedDriveBridgeHandler) handler);
+ bridgeHandler.get().setDiscoveryService(this);
+ }
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return bridgeHandler.orElse(null);
+ }
+
+ @Override
+ protected void startScan() {
+ bridgeHandler.ifPresent(ConnectedDriveBridgeHandler::requestVehicles);
+ }
+
+ @Override
+ public void deactivate() {
+ super.deactivate();
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.utils.Constants.*;
+
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+
+/**
+ * The {@link Destination} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Destination {
+ public float lat;
+ public float lon;
+ public String country;
+ public String city;
+ public String street;
+ public String streetNumber;
+ public String type;
+ public String createdAt;
+
+ public String getAddress() {
+ StringBuilder buf = new StringBuilder();
+ if (street != null) {
+ buf.append(street);
+ if (streetNumber != null) {
+ buf.append(SPACE).append(streetNumber);
+ }
+ }
+ if (city != null) {
+ if (buf.length() > 0) {
+ buf.append(COMMA).append(SPACE).append(city);
+ } else {
+ buf.append(city);
+ }
+ }
+ if (buf.length() == 0) {
+ return UNDEF;
+ } else {
+ return Converter.toTitleCase(buf.toString());
+ }
+ }
+
+ public String getCoordinates() {
+ return lat + Constants.COMMA + lon;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto;
+
+import java.util.List;
+
+/**
+ * The {@link DestinationContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class DestinationContainer {
+ public List<Destination> destinations;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto;
+
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+
+/**
+ * The {@link NetworkError} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class NetworkError {
+ public String url;
+ public int status;
+ public String reason;
+ public String params;
+
+ @Override
+ public String toString() {
+ return new StringBuilder(url).append(Constants.HYPHEN).append(status).append(Constants.HYPHEN).append(reason)
+ .append(params).toString();
+ }
+
+ public String toJson() {
+ return Converter.getGson().toJson(this);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.auth;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link Timer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class AuthResponse {
+ @SerializedName("access_token")
+ public String accessToken;
+ @SerializedName("token_type")
+ public String tokenType;
+ @SerializedName("expires_in")
+ public int expiresIn;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.charge;
+
+/**
+ * The {@link ChargeProfile} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - edit & send of charge profile
+ */
+public class ChargeProfile {
+ public WeeklyPlanner weeklyPlanner;
+ public WeeklyPlanner twoTimesTimer;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.charge;
+
+/**
+ * The {@link ChargingWindow} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class ChargingWindow {
+ public String startTime;// ":"11:00",
+ public String endTime;// ":"17:00"}}
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.charge;
+
+import java.util.List;
+
+/**
+ * The {@link Timer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - edit & send of charge profile
+ */
+public class Timer {
+ public String departureTime;// ": "05:00",
+ public Boolean timerEnabled;// ": false,
+ public List<String> weekdays;
+ /**
+ * "MONDAY",
+ * "TUESDAY",
+ * "WEDNESDAY",
+ * "THURSDAY",
+ * "FRIDAY"
+ * ] '
+ */
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.charge;
+
+/**
+ * The {@link WeeklyPlanner} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - edit & send of charge profile
+ */
+public class WeeklyPlanner {
+ public Boolean climatizationEnabled; // ": true,
+ public String chargingMode;// ": "IMMEDIATE_CHARGING",
+ public String chargingPreferences; // ": "CHARGING_WINDOW",
+ public Timer timer1; // : {
+ public Timer timer2;// ": {
+ public Timer timer3;// ":{"departureTime":"00:00","timerEnabled":false,"weekdays":[]},"
+ public Timer overrideTimer;// ":{"departureTime":"12:00","timerEnabled":false,"weekdays":["SATURDAY"]},"
+ public ChargingWindow preferredChargingWindow;// ":{"startTime":"11:00","endTime":"17:00"}}
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.compat;
+
+/**
+ * The {@link CBSMessageCompat} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CBSMessageCompat {
+ public String description; // "Nächster Wechsel spätestens zum angegebenen Termin.",
+ public String text; // "Bremsflüssigkeit",
+ public int id; // 3,
+ public String status; // "OK",
+ public String messageType; // "CBS",
+ public String date; // "2021-11"
+ public int unitOfLengthRemaining; // ": "2000"
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.compat;
+
+/**
+ * The {@link CCMMessageCompat} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CCMMessageCompat {
+ public String text;// "Laden nicht möglich"
+ public int id;// 804,
+ public String status;// "NULL",
+ public String messageType;// "CCM",
+ public int unitOfLengthRemaining = -1; // "18312"
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.compat;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link VehicleAttributes} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class VehicleAttributes {
+ // Windows & Doors
+ @SerializedName("door_driver_front")
+ public String doorDriverFront;// "CLOSED",
+ @SerializedName("door_driver_rear")
+ public String doorDriverRear;// "CLOSED",
+ @SerializedName("door_passenger_front")
+ public String doorPassengerFront;// "CLOSED",
+ @SerializedName("door_passenger_rear")
+ public String doorPassengerRear;// "CLOSED",
+ @SerializedName("hood_state")
+ public String hoodState;// "CLOSED",
+ @SerializedName("trunk_state")
+ public String trunkState;// "CLOSED",
+ @SerializedName("window_driver_front")
+ public String winDriverFront;// "CLOSED",
+ @SerializedName("window_driver_rear")
+ public String winDriverRear;// "CLOSED",
+ @SerializedName("window_passenger_front")
+ public String winPassengerFront;// "CLOSED",
+ @SerializedName("window_passenger_rear")
+ public String winPassengerRear;// "CLOSED",
+ @SerializedName("sunroof_state")
+ public String sunroofState;// "CLOSED",
+ @SerializedName("door_lock_state")
+ public String doorLockState;// "SECURED",
+ public String shdStatusUnified;// "CLOSED",
+
+ // Charge Status
+ public String chargingHVStatus;// "INVALID",
+ public String lastChargingEndReason;// "CHARGING_GOAL_REACHED",
+ public String connectorStatus;// "DISCONNECTED",
+ public String chargingLogicCurrentlyActive;// "NOT_CHARGING",
+ public String chargeNowAllowed;// "NOT_ALLOWED",
+ @SerializedName("charging_status")
+ public String chargingStatus;// "NOCHARGING",
+ public String lastChargingEndResult;// "SUCCESS",
+ public String chargingSystemStatus;// "NOCHARGING",
+ public String lastUpdateReason;// "VEHCSHUTDOWN_SECURED"
+
+ // Range
+ public int mileage;// "17236",
+ public double beMaxRangeElectric;// "209.0",
+ public double beMaxRangeElectricKm;// "209.0",
+ public double beRemainingRangeElectric;// "179.0",
+ public double beRemainingRangeElectricKm;// "179.0",
+ public double beMaxRangeElectricMile;// "129.0",
+ public double beRemainingRangeElectricMile;// "111.0",
+ public double beRemainingRangeFuelKm;// "67.0",
+ public double beRemainingRangeFuelMile;// "41.0",
+ public double beRemainingRangeFuel;// "67.0",
+ @SerializedName("kombi_current_remaining_range_fuel")
+ public double kombiRemainingRangeFuel;// "67.0",
+
+ public double chargingLevelHv;// "89.0",
+ @SerializedName("soc_hv_percent")
+ public double socHvPercent;// "82.6",
+ @SerializedName("remaining_fuel")
+ public double remainingFuel;// "4",
+ public double fuelPercent;// "47",
+
+ // Last Status update
+ public String updateTime;// "22.08.2020 12:55:46 UTC",
+ @SerializedName("updateTime_converted")
+ public String updateTimeConverted;// "22.08.2020 13:55",
+ @SerializedName("updateTime_converted_date")
+ public String updateTimeConvertedDate;// "22.08.2020",
+ @SerializedName("updateTime_converted_time")
+ public String updateTimeConvertedTime;// "13:55",
+ @SerializedName("updateTime_converted_timestamp")
+ public String updateTimeConvertedTimestamp;// "1598104546000",
+
+ // Last Trip Update
+ @SerializedName("Segment_LastTrip_time_segment_end")
+ public String lastTripEnd;// "22.08.2020 14:52:00 UTC",
+ @SerializedName("Segment_LastTrip_time_segment_end_formatted")
+ public String lastTripEndFormatted;// "22.08.2020 14:52",
+ @SerializedName("Segment_LastTrip_time_segment_end_formatted_date")
+ public String lastTripEndFormattedDate;// "22.08.2020",
+ @SerializedName("Segment_LastTrip_time_segment_end_formatted_time")
+ public String lastTripEndFormattedTime;// "14:52",
+
+ // Location
+ @SerializedName("gps_lat")
+ public float gpsLat;// "43.21",
+ @SerializedName("gps_lng")
+ public float gpsLon;// "8.765",
+ public int heading;// "41",
+
+ public String unitOfLength;// "km",
+ public String unitOfEnergy;// "kWh",
+ @SerializedName("vehicle_tracking")
+ public String vehicleTracking;// "1",
+ @SerializedName("head_unit_pu_software")
+ public String headunitSoftware;// "07/16",
+ @SerializedName("check_control_messages")
+ public String checkControlMessages;// "",
+ @SerializedName("sunroof_position")
+ public String sunroofPosition;// "0",
+ @SerializedName("single_immediate_charging")
+ public String singleImmediateCharging;// "isUnused",
+ public String unitOfCombustionConsumption;// "l/100km",
+ @SerializedName("Segment_LastTrip_ratio_electric_driven_distance")
+ public String lastTripElectricRation;// "100",
+ @SerializedName("condition_based_services")
+ public String conditionBasedServices;// "00003,OK,2021-11,;00017,OK,2021-11,;00001,OK,2021-11,;00032,OK,2021-11,",
+ @SerializedName("charging_inductive_positioning")
+ public String chargingInductivePositioning;// "not_positioned",
+ @SerializedName("lsc_trigger")
+ public String lscTrigger;// "VEHCSHUTDOWN_SECURED",
+ @SerializedName("lights_parking")
+ public String lightsParking;// "OFF",
+ public String prognosisWhileChargingStatus;// "NOT_NEEDED",
+ @SerializedName("head_unit")
+ public String headunit;// "EntryNav",
+ @SerializedName("battery_size_max")
+ public String batterySizeMax;// "33200",
+ @SerializedName("charging_connection_type")
+ public String chargingConnectionType;// "CONDUCTIVE",
+ public String unitOfElectricConsumption;// "kWh/100km",
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.compat;
+
+/**
+ * The {@link VehicleAttributesContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class VehicleAttributesContainer {
+ public VehicleAttributes attributesMap;
+ public VehicleMessages vehicleMessages;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.compat;
+
+import java.util.List;
+
+/**
+ * The {@link VehicleMessages} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @param <CBSMessage>
+ */
+public class VehicleMessages {
+ public List<CCMMessageCompat> ccmMessages;
+ public List<CBSMessageCompat> cbsMessages;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.discovery;
+
+/**
+ * The {@link Dealer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Dealer {
+ public String name;
+ public String street;
+ public String postalCode;
+ public String city;
+ public String country;
+ public String phone;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.discovery;
+
+import java.util.List;
+
+/**
+ * The {@link Vehicle} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Vehicle {
+ public String vin;
+ public String model;
+ public String driveTrain;
+ public String brand;
+ public short yearOfConstruction;
+ public String bodytype;
+ public String color;
+ public boolean statisticsCommunityEnabled;
+ public boolean statisticsAvailable;
+ public boolean hasAlarmSystem;
+ public Dealer dealer;
+ public String breakdownNumber;
+ public List<String> supportedChargingModes;
+ public String chargingControl;// ": "WEEKLY_PLANNER",
+
+ // Remote Services
+ public String vehicleFinder; // ACTIVATED
+ public String hornBlow; // ACTIVATED
+ public String lightFlash; // ACTIVATED
+ public String doorLock; // ACTIVATED
+ public String doorUnlock; // ACTIVATED
+ public String climateNow; // ACTIVATED
+ public String sendPoi; // ACTIVATED
+
+ public String remote360; // SUPPORTED
+ public String climateControl; // SUPPORTED
+ public String chargeNow; // SUPPORTED
+ public String lastDestinations; // SUPPORTED
+ public String carCloud; // SUPPORTED
+ public String remoteSoftwareUpgrade; // SUPPORTED
+
+ public String climateNowRES;// ": "NOT_SUPPORTED",
+ public String climateControlRES;// ": "NOT_SUPPORTED",
+ public String smartSolution;// ": "NOT_SUPPORTED",
+ public String ipa;// ": "NOT_SUPPORTED",
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.discovery;
+
+import java.util.List;
+
+/**
+ * The {@link VehiclesContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class VehiclesContainer {
+ public List<Vehicle> vehicles;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.remote;
+
+/**
+ * The {@link ExecutionStatus} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class ExecutionStatus {
+ public String serviceType;// ": "DOOR_UNLOCK",
+ public String status;// ": "EXECUTED",
+ public String eventId;// ": "5639303536333926DA7B9400@bmw.de",
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.remote;
+
+/**
+ * The {@link ExecutionStatusContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class ExecutionStatusContainer {
+ public ExecutionStatus executionStatus;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.statistics;
+
+/**
+ * The {@link AllTrips} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class AllTrips {
+ public CommunityPowerEntry avgElectricConsumption;
+ public CommunityPowerEntry avgRecuperation;
+ public CommunityChargeCycleEntry chargecycleRange;
+ public CommunityEletricDistanceEntry totalElectricDistance;
+ public CommunityPowerEntry avgCombinedConsumption;
+ public float savedCO2;// ":461.083,"
+ public float savedCO2greenEnergy;// ":2712.255,"
+ public float totalSavedFuel;// ":0,"
+ public String resetDate;// ":"2020-08-24T14:40:40+0000","
+ public int batterySizeMax;// ":33200
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.statistics;
+
+/**
+ * The {@link AllTripsContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class AllTripsContainer {
+ public AllTrips allTrips;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.statistics;
+
+/**
+ * The {@link CommunityChargeCycleEntry} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CommunityChargeCycleEntry {
+ public float communityAverage;// ": 194.21,
+ public float communityHigh;// ": 270,
+ public float userAverage;// ": 57.3,
+ public float userHigh;// ": 185.48,
+ public float userCurrentChargeCycle;// ": 68
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.statistics;
+
+/**
+ * The {@link CommunityEletricDistanceEntry} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CommunityEletricDistanceEntry {
+ public float communityLow;// ": 19,
+ public float communityAverage;// ": 40850.56,
+ public float communityHigh;// ": 193006,
+ public float userTotal;// ": 16629.4
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.statistics;
+
+/**
+ * The {@link CommunityPowerEntry} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CommunityPowerEntry {
+ public float communityLow;// ": 11.05,
+ public float communityAverage;// ": 16.28,
+ public float communityHigh;// ": 21.99,
+ public float userAverage;// ": 16.46
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.statistics;
+
+/**
+ * The {@link LastTrip} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class LastTrip {
+ public float efficiencyValue;// ": 0.98,
+ public float totalDistance;// ": 2,
+ public float electricDistance;// ": 2,
+ public float avgElectricConsumption;// ": 7,
+ public float avgRecuperation;// ": 6,
+ public float drivingModeValue;// ": 0.87,
+ public float accelerationValue;// ": 0.99,
+ public float anticipationValue;// ": 0.99,
+ public float totalConsumptionValue;// ": 1.25,
+ public float auxiliaryConsumptionValue;// ": 0.78,
+ public float avgCombinedConsumption;// ": 0,
+ public float electricDistanceRatio;// ": 100,
+ public float savedFuel;// ": 0,
+ public String date;// ": "2020-08-24T17:55:00+0000",
+ public float duration;// ": 5
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.statistics;
+
+/**
+ * The {@link LastTripContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class LastTripContainer {
+ public LastTrip lastTrip;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.status;
+
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+
+/**
+ * The {@link CBSMessage} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CBSMessage {
+ public String cbsType;// ": "BRAKE_FLUID",
+ public String cbsState;// ": "OK",
+ public String cbsDueDate;// ": "2021-11",
+ public String cbsDescription;// ": "Next change due at the latest by the stated date."
+ public int cbsRemainingMileage = -1; // 46000
+
+ public String cbsTypeConverted = null;
+ public String cbsDescriptionConverted = null;
+
+ public String getDueDate() {
+ if (cbsDueDate == null) {
+ return Constants.NULL_DATE;
+ } else {
+ return cbsDueDate + Constants.UTC_APPENDIX;
+ }
+ }
+
+ public String getType() {
+ if (cbsTypeConverted == null) {
+ if (cbsType == null) {
+ cbsTypeConverted = Constants.INVALID;
+ } else {
+ cbsTypeConverted = Converter.toTitleCase(cbsType);
+ }
+ }
+ return cbsTypeConverted;
+ }
+
+ public String getDescription() {
+ if (cbsDescriptionConverted == null) {
+ if (cbsDescription == null) {
+ cbsDescriptionConverted = Constants.INVALID;
+ } else {
+ cbsDescriptionConverted = cbsDescription;
+ }
+ }
+ return cbsDescriptionConverted;
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder(cbsDueDate).append(Constants.HYPHEN).append(cbsRemainingMileage)
+ .append(Constants.HYPHEN).append(cbsType).toString();
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.status;
+
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+/**
+ * The {@link CCMMessage} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CCMMessage {
+ // if necessary. Perform reset after adjustment. See Owner's Handbook for further
+ // information.",
+ public String ccmDescriptionShort = Constants.INVALID;// ": "Tyre pressure notification",
+ public String ccmDescriptionLong = Constants.INVALID;// ": "You can continue driving. Check tyre pressure when tyres
+ // are cold and adjust
+ public int ccmId = -1;// ": 955,
+ public int ccmMileage = -1;// ": 41544
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.status;
+
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+/**
+ * The {@link Doors} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Doors {
+ public String doorDriverFront = Constants.UNDEF;// ": "CLOSED",
+ public String doorDriverRear = Constants.UNDEF;// ": "CLOSED",
+ public String doorPassengerFront = Constants.UNDEF;// ": "CLOSED",
+ public String doorPassengerRear = Constants.UNDEF;// ": "CLOSED",
+ public String trunk = Constants.UNDEF;// ": "CLOSED",
+ public String hood = Constants.UNDEF;// ": "CLOSED",
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.status;
+
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+/**
+ * The {@link Position} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Position {
+ public float lat;// ": 46.55605,
+ public float lon;// ": 10.495669,
+ public int heading;// ": 219,
+ public String status;// ": "OK"
+
+ public String getCoordinates() {
+ return new StringBuilder(Float.toString(lat)).append(Constants.COMMA).append(Float.toString(lon)).toString();
+ }
+
+ @Override
+ public String toString() {
+ return getCoordinates();
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.status;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link VehicleStatus} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class VehicleStatus {
+ public int mileage = Constants.INT_UNDEF;// ": 17273,
+ public double remainingFuel = Constants.INT_UNDEF;// ": 4,
+ public double remainingRangeElectric = Constants.INT_UNDEF;// ": 148,
+ public double remainingRangeElectricMls;// ": 91,
+ public double remainingRangeFuel = Constants.INT_UNDEF;// ": 70,"
+ public double remainingRangeFuelMls;// ":43,"
+ public double maxRangeElectric = Constants.INT_UNDEF;// ":216,"
+ public double maxRangeElectricMls;// ":134,"
+ public double maxFuel;// ":8.5,
+ public double chargingLevelHv;// ":71,
+ public String vin;// : "ANONYMOUS",
+ public String updateReason;// ": "VEHICLE_SHUTDOWN_SECURED",
+ public String updateTime;// ": "2020-08-24 T15:55:32+0000",
+ public String doorDriverFront = Constants.UNDEF;// ": "CLOSED",
+ public String doorDriverRear = Constants.UNDEF;// ": "CLOSED",
+ public String doorPassengerFront = Constants.UNDEF;// ": "CLOSED",
+ public String doorPassengerRear = Constants.UNDEF;// ": "CLOSED",
+ public String windowDriverFront = Constants.UNDEF;// ": "CLOSED",
+ public String windowDriverRear = Constants.UNDEF;// ": "CLOSED",
+ public String windowPassengerFront = Constants.UNDEF;// ": "CLOSED",
+ public String windowPassengerRear = Constants.UNDEF;// ": "CLOSED",
+ public String sunroof = Constants.UNDEF;// ": "CLOSED",
+ public String trunk = Constants.UNDEF;// ": "CLOSED",
+ public String rearWindow = Constants.UNDEF;// ": "INVALID",
+ public String hood = Constants.UNDEF;// ": "CLOSED",
+ public String doorLockState;// ": "SECURED",
+ public String parkingLight;// ": "OFF",
+ public String positionLight;// ": "ON",
+ public String connectionStatus;// ": "DISCONNECTED",
+ public String chargingStatus;// ": "INVALID","
+ public String lastChargingEndReason;// ": "CHARGING_GOAL_REACHED",
+ public String lastChargingEndResult;// ": "SUCCESS","
+ public Double chargingTimeRemaining;// ": "45",
+ public Position position;
+ public String internalDataTimeUTC;// ": "2020-08-24 T15:55:32",
+ public boolean singleImmediateCharging;// ":false,
+ public String chargingConnectionType;// ": "CONDUCTIVE",
+ public String chargingInductivePositioning;// ": "NOT_POSITIONED",
+ public String vehicleCountry;// ": "DE","+"
+ @SerializedName("DCS_CCH_Activation")
+ public String dcsCchActivation;// ": "NA",
+ @SerializedName("DCS_CCH_Ongoing")
+ public boolean dcsCchOngoing;// ":false
+ public List<CCMMessage> checkControlMessages = new ArrayList<CCMMessage>();// ":[],
+ public List<CBSMessage> cbsData = new ArrayList<CBSMessage>();
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.status;
+
+/**
+ * The {@link VehicleStatusContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class VehicleStatusContainer {
+ public VehicleStatus vehicleStatus;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.status;
+
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+/**
+ * The {@link Windows} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Windows {
+ public String windowDriverFront = Constants.UNDEF;// ": "CLOSED",
+ public String windowDriverRear = Constants.UNDEF;// ": "CLOSED",
+ public String windowPassengerFront = Constants.UNDEF;// ": "CLOSED",
+ public String windowPassengerRear = Constants.UNDEF;// ": "CLOSED",
+ public String sunroof = Constants.UNDEF;// ": "CLOSED",
+ public String rearWindow = Constants.UNDEF;// ": "INVALID",
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
+import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
+import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
+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
+ */
+@Component(service = { DynamicStateDescriptionProvider.class, BMWConnectedDriveOptionProvider.class })
+@NonNullByDefault
+public class BMWConnectedDriveOptionProvider extends BaseDynamicStateDescriptionProvider {
+
+ @Reference
+ protected void setChannelTypeI18nLocalizationService(
+ final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
+ this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
+ }
+
+ protected void unsetChannelTypeI18nLocalizationService(
+ final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
+ this.channelTypeI18nLocalizationService = null;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link ByteResponseCallback} Interface for all raw byte results from ASYNC REST API
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public interface ByteResponseCallback extends ResponseCallback {
+
+ public void onResponse(byte[] result);
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.utils.Constants.ANONYMOUS;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Optional;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConfiguration;
+import org.openhab.binding.bmwconnecteddrive.internal.discovery.VehicleDiscovery;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.discovery.Dealer;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.discovery.VehiclesContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.BimmerConstants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.core.io.net.http.HttpClientFactory;
+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.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonParseException;
+
+/**
+ * The {@link ConnectedDriveBridgeHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class ConnectedDriveBridgeHandler extends BaseBridgeHandler implements StringResponseCallback {
+ private final Logger logger = LoggerFactory.getLogger(ConnectedDriveBridgeHandler.class);
+ private HttpClientFactory httpClientFactory;
+ private Optional<VehicleDiscovery> discoveryService = Optional.empty();
+ private Optional<ConnectedDriveProxy> proxy = Optional.empty();
+ private Optional<ScheduledFuture<?>> initializerJob = Optional.empty();
+ private Optional<String> troubleshootFingerprint = Optional.empty();
+
+ public ConnectedDriveBridgeHandler(Bridge bridge, HttpClientFactory hcf) {
+ super(bridge);
+ httpClientFactory = hcf;
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ // no commands available
+ }
+
+ @Override
+ public void initialize() {
+ troubleshootFingerprint = Optional.empty();
+ updateStatus(ThingStatus.UNKNOWN);
+ ConnectedDriveConfiguration config = getConfigAs(ConnectedDriveConfiguration.class);
+ if (!checkConfiguration(config)) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
+ } else {
+ proxy = Optional.of(new ConnectedDriveProxy(httpClientFactory, config));
+ // give the system some time to create all predefined Vehicles
+ // check with API call if bridge is online
+ initializerJob = Optional.of(scheduler.schedule(this::requestVehicles, 5, TimeUnit.SECONDS));
+ }
+ }
+
+ public static boolean checkConfiguration(ConnectedDriveConfiguration config) {
+ if (Constants.EMPTY.equals(config.userName) || Constants.EMPTY.equals(config.password)) {
+ return false;
+ } else if (BimmerConstants.AUTH_SERVER_MAP.containsKey(config.region)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void dispose() {
+ initializerJob.ifPresent(job -> job.cancel(true));
+ }
+
+ public void requestVehicles() {
+ proxy.ifPresent(prox -> prox.requestVehicles(this));
+ }
+
+ public String getDiscoveryFingerprint() {
+ return troubleshootFingerprint.map(fingerprint -> {
+ VehiclesContainer container = null;
+ try {
+ container = Converter.getGson().fromJson(fingerprint, VehiclesContainer.class);
+ if (container != null) {
+ if (container.vehicles != null) {
+ if (container.vehicles.isEmpty()) {
+ return Constants.EMPTY_JSON;
+ } else {
+ container.vehicles.forEach(entry -> {
+ entry.vin = ANONYMOUS;
+ entry.breakdownNumber = ANONYMOUS;
+ if (entry.dealer != null) {
+ Dealer d = entry.dealer;
+ d.city = ANONYMOUS;
+ d.country = ANONYMOUS;
+ d.name = ANONYMOUS;
+ d.phone = ANONYMOUS;
+ d.postalCode = ANONYMOUS;
+ d.street = ANONYMOUS;
+ }
+ });
+ return Converter.getGson().toJson(container);
+ }
+ }
+ }
+ } catch (JsonParseException jpe) {
+ logger.debug("Cannot parse fingerprint {}", jpe.getMessage());
+ }
+ // Not a VehiclesContainer or Vehicles is empty so deliver fingerprint as it is
+ return fingerprint;
+ }).orElse(Constants.INVALID);
+ }
+
+ private void logFingerPrint() {
+ logger.debug("###### Discovery Troubleshoot Fingerprint Data - BEGIN ######");
+ logger.debug("### Discovery Result ###");
+ logger.debug("{}", getDiscoveryFingerprint());
+ logger.debug("###### Discovery Troubleshoot Fingerprint Data - END ######");
+ }
+
+ /**
+ * There's only the Vehicles response available
+ */
+ @Override
+ public void onResponse(@Nullable String response) {
+ boolean firstResponse = troubleshootFingerprint.isEmpty();
+ if (response != null) {
+ updateStatus(ThingStatus.ONLINE);
+ troubleshootFingerprint = discoveryService.map(discovery -> {
+ try {
+ VehiclesContainer container = Converter.getGson().fromJson(response, VehiclesContainer.class);
+ if (container != null) {
+ if (container.vehicles != null) {
+ discovery.onResponse(container);
+ container.vehicles.forEach(entry -> {
+ entry.vin = ANONYMOUS;
+ entry.breakdownNumber = ANONYMOUS;
+ if (entry.dealer != null) {
+ Dealer d = entry.dealer;
+ d.city = ANONYMOUS;
+ d.country = ANONYMOUS;
+ d.name = ANONYMOUS;
+ d.phone = ANONYMOUS;
+ d.postalCode = ANONYMOUS;
+ d.street = ANONYMOUS;
+ }
+ });
+ }
+ return Converter.getGson().toJson(container);
+ }
+ } catch (JsonParseException jpe) {
+ logger.debug("Fingerprint parse exception {}", jpe.getMessage());
+ }
+ // Unparseable or not a VehiclesContainer:
+ return response;
+ });
+ } else {
+ troubleshootFingerprint = Optional.of(Constants.EMPTY_JSON);
+ }
+ if (firstResponse) {
+ logFingerPrint();
+ }
+ }
+
+ @Override
+ public void onError(NetworkError error) {
+ boolean firstResponse = troubleshootFingerprint.isEmpty();
+ troubleshootFingerprint = Optional.of(error.toJson());
+ if (firstResponse) {
+ logFingerPrint();
+ }
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error.reason);
+ }
+
+ @Override
+ public Collection<Class<? extends ThingHandlerService>> getServices() {
+ return Collections.singleton(VehicleDiscovery.class);
+ }
+
+ public Optional<ConnectedDriveProxy> getProxy() {
+ return proxy;
+ }
+
+ public void setDiscoveryService(VehicleDiscovery discoveryService) {
+ this.discoveryService = Optional.of(discoveryService);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.utils.HTTPConstants.*;
+
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+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.client.api.Result;
+import org.eclipse.jetty.client.util.BufferingResponseListener;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.util.MultiMap;
+import org.eclipse.jetty.util.UrlEncoded;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConfiguration;
+import org.openhab.binding.bmwconnecteddrive.internal.VehicleConfiguration;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.auth.AuthResponse;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.simulation.Injector;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.BimmerConstants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ImageProperties;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link ConnectedDriveProxy} This class holds the important constants for the BMW Connected Drive Authorization.
+ * They
+ * are taken from the Bimmercode from github {@link https://github.com/bimmerconnected/bimmer_connected}
+ * File defining these constants
+ * {@link https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/account.py}
+ * https://customer.bmwgroup.com/one/app/oauth.js
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - edit & send of charge profile
+ */
+@NonNullByDefault
+public class ConnectedDriveProxy {
+ private final Logger logger = LoggerFactory.getLogger(ConnectedDriveProxy.class);
+ private final Token token = new Token();
+ private final HttpClient httpClient;
+ private final HttpClient authHttpClient;
+ private final String legacyAuthUri;
+ private final ConnectedDriveConfiguration configuration;
+
+ /**
+ * URLs taken from https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/const.py
+ */
+ final String baseUrl;
+ final String vehicleUrl;
+ final String legacyUrl;
+ final String vehicleStatusAPI = "/status";
+ final String lastTripAPI = "/statistics/lastTrip";
+ final String allTripsAPI = "/statistics/allTrips";
+ final String chargeAPI = "/chargingprofile";
+ final String destinationAPI = "/destinations";
+ final String imageAPI = "/image";
+ final String rangeMapAPI = "/rangemap";
+ final String serviceExecutionAPI = "/executeService";
+ final String serviceExecutionStateAPI = "/serviceExecutionStatus";
+
+ public ConnectedDriveProxy(HttpClientFactory httpClientFactory, ConnectedDriveConfiguration config) {
+ httpClient = httpClientFactory.getCommonHttpClient();
+ authHttpClient = httpClientFactory.createHttpClient(AUTH_HTTP_CLIENT_NAME);
+ authHttpClient.setFollowRedirects(false);
+ configuration = config;
+
+ final StringBuilder legacyAuth = new StringBuilder();
+ legacyAuth.append("https://");
+ legacyAuth.append(BimmerConstants.AUTH_SERVER_MAP.get(configuration.region));
+ legacyAuth.append(BimmerConstants.OAUTH_ENDPOINT);
+ legacyAuthUri = legacyAuth.toString();
+ vehicleUrl = "https://" + getRegionServer() + "/webapi/v1/user/vehicles";
+ baseUrl = vehicleUrl + "/";
+ legacyUrl = "https://" + getRegionServer() + "/api/vehicle/dynamic/v1/";
+ }
+
+ private synchronized void call(final String url, final boolean post, final @Nullable MultiMap<String> params,
+ final ResponseCallback callback) {
+ // only executed in "simulation mode"
+ // SimulationTest.testSimulationOff() assures Injector is off when releasing
+ if (Injector.isActive()) {
+ if (url.equals(baseUrl)) {
+ ((StringResponseCallback) callback).onResponse(Injector.getDiscovery());
+ } else if (url.endsWith(vehicleStatusAPI)) {
+ ((StringResponseCallback) callback).onResponse(Injector.getStatus());
+ } else {
+ logger.debug("Simulation of {} not supported", url);
+ }
+ return;
+ }
+ final Request req;
+ final String encoded = params == null || params.isEmpty() ? null
+ : UrlEncoded.encode(params, StandardCharsets.UTF_8, false);
+ final String completeUrl;
+
+ if (post) {
+ completeUrl = url;
+ req = httpClient.POST(url);
+ if (encoded != null) {
+ req.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, encoded, StandardCharsets.UTF_8));
+ }
+ } else {
+ completeUrl = encoded == null ? url : url + Constants.QUESTION + encoded;
+ req = httpClient.newRequest(completeUrl);
+ }
+ req.header(HttpHeader.AUTHORIZATION, getToken().getBearerToken());
+ req.header(HttpHeader.REFERER, BimmerConstants.REFERER_URL);
+
+ req.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(new BufferingResponseListener() {
+ @NonNullByDefault({})
+ @Override
+ public void onComplete(Result result) {
+ if (result.getResponse().getStatus() != 200) {
+ NetworkError error = new NetworkError();
+ error.url = completeUrl;
+ error.status = result.getResponse().getStatus();
+ if (result.getResponse().getReason() != null) {
+ error.reason = result.getResponse().getReason();
+ } else {
+ error.reason = result.getFailure().getMessage();
+ }
+ error.params = result.getRequest().getParams().toString();
+ logger.debug("HTTP Error {}", error.toString());
+ callback.onError(error);
+ } else {
+ if (callback instanceof StringResponseCallback) {
+ ((StringResponseCallback) callback).onResponse(getContentAsString());
+ } else if (callback instanceof ByteResponseCallback) {
+ ((ByteResponseCallback) callback).onResponse(getContent());
+ } else {
+ logger.error("unexpected reponse type {}", callback.getClass().getName());
+ }
+ }
+ }
+ });
+ }
+
+ public void get(String url, @Nullable MultiMap<String> params, ResponseCallback callback) {
+ call(url, false, params, callback);
+ }
+
+ public void post(String url, @Nullable MultiMap<String> params, ResponseCallback callback) {
+ call(url, true, params, callback);
+ }
+
+ public void requestVehicles(StringResponseCallback callback) {
+ get(vehicleUrl, null, callback);
+ }
+
+ public void requestVehcileStatus(VehicleConfiguration config, StringResponseCallback callback) {
+ get(baseUrl + config.vin + vehicleStatusAPI, null, callback);
+ }
+
+ public void requestLegacyVehcileStatus(VehicleConfiguration config, StringResponseCallback callback) {
+ // see https://github.com/jupe76/bmwcdapi/search?q=dynamic%2Fv1
+ get(legacyUrl + config.vin + "?offset=-60", null, callback);
+ }
+
+ public void requestLastTrip(VehicleConfiguration config, StringResponseCallback callback) {
+ get(baseUrl + config.vin + lastTripAPI, null, callback);
+ }
+
+ public void requestAllTrips(VehicleConfiguration config, StringResponseCallback callback) {
+ get(baseUrl + config.vin + allTripsAPI, null, callback);
+ }
+
+ public void requestChargingProfile(VehicleConfiguration config, StringResponseCallback callback) {
+ get(baseUrl + config.vin + chargeAPI, null, callback);
+ }
+
+ public void requestDestinations(VehicleConfiguration config, StringResponseCallback callback) {
+ get(baseUrl + config.vin + destinationAPI, null, callback);
+ }
+
+ public void requestRangeMap(VehicleConfiguration config, @Nullable MultiMap<String> params,
+ StringResponseCallback callback) {
+ get(baseUrl + config.vin + rangeMapAPI, params, callback);
+ }
+
+ public void requestImage(VehicleConfiguration config, ImageProperties props, ByteResponseCallback callback) {
+ final String localImageUrl = baseUrl + config.vin + imageAPI;
+ final MultiMap<String> dataMap = new MultiMap<String>();
+ dataMap.add("width", Integer.toString(props.size));
+ dataMap.add("height", Integer.toString(props.size));
+ dataMap.add("view", props.viewport);
+ get(localImageUrl, dataMap, callback);
+ }
+
+ private String getRegionServer() {
+ final String retVal = BimmerConstants.SERVER_MAP.get(configuration.region);
+ return retVal == null ? Constants.INVALID : retVal;
+ }
+
+ private String getAuthorizationValue() {
+ final String retVal = BimmerConstants.AUTHORIZATION_VALUE_MAP.get(configuration.region);
+ return retVal == null ? Constants.INVALID : retVal;
+ }
+
+ RemoteServiceHandler getRemoteServiceHandler(VehicleHandler vehicleHandler) {
+ return new RemoteServiceHandler(vehicleHandler, this);
+ }
+
+ // Token handling
+
+ /**
+ * Gets new token if old one is expired or invalid. In case of error the token remains.
+ * So if token refresh fails the corresponding requests will also fail and update the
+ * Thing status accordingly.
+ *
+ * @return token
+ */
+ public Token getToken() {
+ if (token.isExpired() || !token.isValid()) {
+ updateToken();
+ }
+ return token;
+ }
+
+ /**
+ * Authorize at BMW Connected Drive Portal and get Token
+ *
+ * @return
+ */
+ private synchronized void updateToken() {
+ if (!authHttpClient.isStarted()) {
+ try {
+ authHttpClient.start();
+ } catch (Exception e) {
+ logger.warn("Auth Http Client cannot be started {}", e.getMessage());
+ return;
+ }
+ }
+
+ final Request req = authHttpClient.POST(legacyAuthUri);
+ req.header(HttpHeader.CONNECTION, KEEP_ALIVE);
+ req.header(HttpHeader.HOST, getRegionServer());
+ req.header(HttpHeader.AUTHORIZATION, getAuthorizationValue());
+ req.header(CREDENTIALS, BimmerConstants.CREDENTIAL_VALUES);
+ req.header(HttpHeader.REFERER, BimmerConstants.REFERER_URL);
+
+ final MultiMap<String> dataMap = new MultiMap<String>();
+ dataMap.add("grant_type", "password");
+ dataMap.add(SCOPE, BimmerConstants.SCOPE_VALUES);
+ dataMap.add(USERNAME, configuration.userName);
+ dataMap.add(PASSWORD, configuration.password);
+ req.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
+ UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
+ try {
+ ContentResponse contentResponse = req.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send();
+ // Status needs to be 302 - Response is stored in Header
+ if (contentResponse.getStatus() == 302) {
+ final HttpFields fields = contentResponse.getHeaders();
+ final HttpField field = fields.getField(HttpHeader.LOCATION);
+ tokenFromUrl(field.getValue());
+ } else if (contentResponse.getStatus() == 200) {
+ final String stringContent = contentResponse.getContentAsString();
+ if (stringContent != null && !stringContent.isEmpty()) {
+ try {
+ final AuthResponse authResponse = Converter.getGson().fromJson(stringContent,
+ AuthResponse.class);
+ if (authResponse != null) {
+ token.setToken(authResponse.accessToken);
+ token.setType(authResponse.tokenType);
+ token.setExpiration(authResponse.expiresIn);
+ } else {
+ logger.debug("not an Authorization response: {}", stringContent);
+ }
+ } catch (JsonSyntaxException jse) {
+ logger.debug("Authorization response unparsable: {}", stringContent);
+ }
+ } else {
+ logger.debug("Authorization response has no content");
+ }
+ } else {
+ logger.debug("Authorization status {} reason {}", contentResponse.getStatus(),
+ contentResponse.getReason());
+ }
+ } catch (InterruptedException | ExecutionException | TimeoutException e) {
+ logger.debug("Authorization exception: {}", e.getMessage());
+ }
+ }
+
+ void tokenFromUrl(String encodedUrl) {
+ final MultiMap<String> tokenMap = new MultiMap<String>();
+ UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII);
+ tokenMap.forEach((key, value) -> {
+ if (value.size() > 0) {
+ String val = value.get(0);
+ if (key.endsWith(ACCESS_TOKEN)) {
+ token.setToken(val.toString());
+ } else if (key.equals(EXPIRES_IN)) {
+ token.setExpiration(Integer.parseInt(val.toString()));
+ } else if (key.equals(TOKEN_TYPE)) {
+ token.setType(val.toString());
+ }
+ }
+ });
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
+
+import java.util.Optional;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.util.MultiMap;
+import org.openhab.binding.bmwconnecteddrive.internal.VehicleConfiguration;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.remote.ExecutionStatusContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.HTTPConstants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link RemoteServiceHandler} handles executions of remote services towards your Vehicle
+ *
+ * @see https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/remote_services.py
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - edit & send of charge profile
+ */
+@NonNullByDefault
+public class RemoteServiceHandler implements StringResponseCallback {
+ private final Logger logger = LoggerFactory.getLogger(RemoteServiceHandler.class);
+
+ private static final String SERVICE_TYPE = "serviceType";
+ private static final String DATA = "data";
+ // after 6 retries the state update will give up
+ private static final int GIVEUP_COUNTER = 6;
+ private static final int STATE_UPDATE_SEC = HTTPConstants.HTTP_TIMEOUT_SEC + 1; // regular timeout + 1sec
+
+ private final ConnectedDriveProxy proxy;
+ private final VehicleHandler handler;
+ private final String serviceExecutionAPI;
+ private final String serviceExecutionStateAPI;
+
+ private int counter = 0;
+ private Optional<ScheduledFuture<?>> stateJob = Optional.empty();
+ private Optional<String> serviceExecuting = Optional.empty();
+
+ public enum ExecutionState {
+ READY,
+ INITIATED,
+ PENDING,
+ DELIVERED,
+ EXECUTED,
+ ERROR,
+ }
+
+ public enum RemoteService {
+ LIGHT_FLASH(REMOTE_SERVICE_LIGHT_FLASH, "Flash Lights"),
+ VEHICLE_FINDER(REMOTE_SERVICE_VEHICLE_FINDER, "Vehicle Finder"),
+ DOOR_LOCK(REMOTE_SERVICE_DOOR_LOCK, "Door Lock"),
+ DOOR_UNLOCK(REMOTE_SERVICE_DOOR_UNLOCK, "Door Unlock"),
+ HORN_BLOW(REMOTE_SERVICE_HORN, "Horn Blow"),
+ CLIMATE_NOW(REMOTE_SERVICE_AIR_CONDITIONING, "Climate Control"),
+ CHARGE_NOW(REMOTE_SERVICE_CHARGE_NOW, "Start Charging"),
+ CHARGING_CONTROL(REMOTE_SERVICE_CHARGING_CONTROL, "Send Charging Profile");
+
+ private final String command;
+ private final String label;
+
+ RemoteService(final String command, final String label) {
+ this.command = command;
+ this.label = label;
+ }
+
+ public String getCommand() {
+ return command;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+ }
+
+ public RemoteServiceHandler(VehicleHandler vehicleHandler, ConnectedDriveProxy connectedDriveProxy) {
+ handler = vehicleHandler;
+ proxy = connectedDriveProxy;
+ final VehicleConfiguration config = handler.getConfiguration().get();
+ serviceExecutionAPI = proxy.baseUrl + config.vin + proxy.serviceExecutionAPI;
+ serviceExecutionStateAPI = proxy.baseUrl + config.vin + proxy.serviceExecutionStateAPI;
+ }
+
+ boolean execute(RemoteService service, String... data) {
+ synchronized (this) {
+ if (serviceExecuting.isPresent()) {
+ // only one service executing
+ return false;
+ }
+ serviceExecuting = Optional.of(service.name());
+ }
+ final MultiMap<String> dataMap = new MultiMap<String>();
+ dataMap.add(SERVICE_TYPE, service.name());
+ if (data.length > 0) {
+ dataMap.add(DATA, data[0]);
+ }
+ proxy.post(serviceExecutionAPI, dataMap, this);
+ return true;
+ }
+
+ public void getState() {
+ synchronized (this) {
+ serviceExecuting.ifPresentOrElse(service -> {
+ if (counter >= GIVEUP_COUNTER) {
+ logger.warn("Giving up updating state for {} after {} times", service, GIVEUP_COUNTER);
+ reset();
+ // immediately refresh data
+ handler.getData();
+ }
+ counter++;
+ final MultiMap<String> dataMap = new MultiMap<String>();
+ dataMap.add(SERVICE_TYPE, service);
+ proxy.get(serviceExecutionStateAPI, dataMap, this);
+ }, () -> {
+ logger.warn("No Service executed to get state");
+ });
+ stateJob = Optional.empty();
+ }
+ }
+
+ @Override
+ public void onResponse(@Nullable String result) {
+ if (result != null) {
+ try {
+ ExecutionStatusContainer esc = Converter.getGson().fromJson(result, ExecutionStatusContainer.class);
+ if (esc != null && esc.executionStatus != null) {
+ String status = esc.executionStatus.status;
+ synchronized (this) {
+ handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), status);
+ if (ExecutionState.EXECUTED.name().equals(status)) {
+ // refresh loop ends - update of status handled in the normal refreshInterval. Earlier
+ // update doesn't show better results!
+ reset();
+ return;
+ }
+ }
+ }
+ } catch (JsonSyntaxException jse) {
+ logger.debug("RemoteService response is unparseable: {} {}", result, jse.getMessage());
+ }
+ }
+ // schedule even if no result is present until retries exceeded
+ synchronized (this) {
+ stateJob.ifPresent(job -> {
+ if (!job.isDone()) {
+ job.cancel(true);
+ }
+ });
+ stateJob = Optional.of(handler.getScheduler().schedule(this::getState, STATE_UPDATE_SEC, TimeUnit.SECONDS));
+ }
+ }
+
+ @Override
+ public void onError(NetworkError error) {
+ synchronized (this) {
+ handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null),
+ ExecutionState.ERROR.name() + Constants.SPACE + Integer.toString(error.status));
+ reset();
+ }
+ }
+
+ private void reset() {
+ serviceExecuting = Optional.empty();
+ counter = 0;
+ }
+
+ public void cancel() {
+ synchronized (this) {
+ stateJob.ifPresent(action -> {
+ if (!action.isDone()) {
+ action.cancel(true);
+ }
+ stateJob = Optional.empty();
+ });
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
+
+/**
+ * The {@link ResponseCallback} Marker Interface for all ASYNC REST API callbacks
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public interface ResponseCallback {
+ public void onError(NetworkError error);
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link StringResponseCallback} Interface for all String results from ASYNC REST API
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public interface StringResponseCallback extends ResponseCallback {
+
+ public void onResponse(@Nullable String result);
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+/**
+ * The {@link Token} BMW ConnectedDrive Token storage
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class Token {
+ private String token = Constants.EMPTY;
+ private String tokenType = Constants.EMPTY;
+ private long expiration = 0;
+
+ public String getBearerToken() {
+ return new StringBuilder(tokenType).append(Constants.SPACE).append(token).toString();
+ }
+
+ public void setToken(String token) {
+ this.token = token;
+ }
+
+ public void setExpiration(int expiration) {
+ this.expiration = System.currentTimeMillis() / 1000 + expiration;
+ }
+
+ /**
+ * @return true if Token expires in less than 1 second
+ */
+ public boolean isExpired() {
+ return (expiration - System.currentTimeMillis() / 1000) < 1;
+ }
+
+ public void setType(String type) {
+ tokenType = type;
+ }
+
+ public boolean isValid() {
+ return (!token.equals(Constants.EMPTY) && !tokenType.equals(Constants.EMPTY) && expiration > 0);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
+
+import java.time.DayOfWeek;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+import javax.measure.quantity.Length;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.VehicleType;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.Destination;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.AllTrips;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.LastTrip;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.CBSMessage;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.CCMMessage;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.Doors;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.Position;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatus;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.Windows;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileUtils;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileUtils.TimedChannel;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper.ProfileKey;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.RemoteServiceUtils;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.VehicleStatusUtils;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PointType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.ImperialUnits;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.State;
+import org.openhab.core.types.StateOption;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link VehicleChannelHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - edit & send of charge profile
+ */
+@NonNullByDefault
+public abstract class VehicleChannelHandler extends BaseThingHandler {
+ protected final Logger logger = LoggerFactory.getLogger(VehicleChannelHandler.class);
+ protected boolean imperial = false;
+ protected boolean hasFuel = false;
+ protected boolean isElectric = false;
+ protected boolean isHybrid = false;
+
+ // List Interfaces
+ protected List<CBSMessage> serviceList = new ArrayList<CBSMessage>();
+ protected String selectedService = Constants.UNDEF;
+ protected List<CCMMessage> checkControlList = new ArrayList<CCMMessage>();
+ protected String selectedCC = Constants.UNDEF;
+ protected List<Destination> destinationList = new ArrayList<Destination>();
+ protected String selectedDestination = Constants.UNDEF;
+
+ protected BMWConnectedDriveOptionProvider optionProvider;
+
+ // Data Caches
+ protected Optional<String> vehicleStatusCache = Optional.empty();
+ protected Optional<String> lastTripCache = Optional.empty();
+ protected Optional<String> allTripsCache = Optional.empty();
+ protected Optional<String> chargeProfileCache = Optional.empty();
+ protected Optional<String> rangeMapCache = Optional.empty();
+ protected Optional<String> destinationCache = Optional.empty();
+ protected Optional<byte[]> imageCache = Optional.empty();
+
+ public VehicleChannelHandler(Thing thing, BMWConnectedDriveOptionProvider op, String type, boolean imperial) {
+ super(thing);
+ optionProvider = op;
+
+ this.imperial = imperial;
+ hasFuel = type.equals(VehicleType.CONVENTIONAL.toString()) || type.equals(VehicleType.PLUGIN_HYBRID.toString())
+ || type.equals(VehicleType.ELECTRIC_REX.toString());
+ isElectric = type.equals(VehicleType.PLUGIN_HYBRID.toString())
+ || type.equals(VehicleType.ELECTRIC_REX.toString()) || type.equals(VehicleType.ELECTRIC.toString());
+ isHybrid = hasFuel && isElectric;
+
+ setOptions(CHANNEL_GROUP_REMOTE, REMOTE_SERVICE_COMMAND, RemoteServiceUtils.getOptions(isElectric));
+ }
+
+ private void setOptions(final String group, final String id, List<StateOption> options) {
+ optionProvider.setStateOptions(new ChannelUID(thing.getUID(), group, id), options);
+ }
+
+ protected void updateChannel(final String group, final String id, final State state) {
+ updateState(new ChannelUID(thing.getUID(), group, id), state);
+ }
+
+ protected void updateCheckControls(List<CCMMessage> ccl) {
+ if (ccl.size() == 0) {
+ // No Check Control available - show not active
+ CCMMessage ccm = new CCMMessage();
+ ccm.ccmDescriptionLong = Constants.NO_ENTRIES;
+ ccm.ccmDescriptionShort = Constants.NO_ENTRIES;
+ ccm.ccmId = -1;
+ ccm.ccmMileage = -1;
+ ccl.add(ccm);
+ }
+
+ // add all elements to options
+ checkControlList = ccl;
+ List<StateOption> ccmDescriptionOptions = new ArrayList<>();
+ List<StateOption> ccmDetailsOptions = new ArrayList<>();
+ List<StateOption> ccmMileageOptions = new ArrayList<>();
+ boolean isSelectedElementIn = false;
+ int index = 0;
+ for (CCMMessage ccEntry : checkControlList) {
+ ccmDescriptionOptions.add(new StateOption(Integer.toString(index), ccEntry.ccmDescriptionShort));
+ ccmDetailsOptions.add(new StateOption(Integer.toString(index), ccEntry.ccmDescriptionLong));
+ ccmMileageOptions.add(new StateOption(Integer.toString(index), Integer.toString(ccEntry.ccmMileage)));
+ if (selectedCC.equals(ccEntry.ccmDescriptionShort)) {
+ isSelectedElementIn = true;
+ }
+ index++;
+ }
+ setOptions(CHANNEL_GROUP_CHECK_CONTROL, NAME, ccmDescriptionOptions);
+ setOptions(CHANNEL_GROUP_CHECK_CONTROL, DETAILS, ccmDetailsOptions);
+ setOptions(CHANNEL_GROUP_CHECK_CONTROL, MILEAGE, ccmMileageOptions);
+
+ // if current selected item isn't anymore in the list select first entry
+ if (!isSelectedElementIn) {
+ selectCheckControl(0);
+ }
+ }
+
+ protected void selectCheckControl(int index) {
+ if (index >= 0 && index < checkControlList.size()) {
+ CCMMessage ccEntry = checkControlList.get(index);
+ selectedCC = ccEntry.ccmDescriptionShort;
+ updateChannel(CHANNEL_GROUP_CHECK_CONTROL, NAME, StringType.valueOf(ccEntry.ccmDescriptionShort));
+ updateChannel(CHANNEL_GROUP_CHECK_CONTROL, DETAILS, StringType.valueOf(ccEntry.ccmDescriptionLong));
+ updateChannel(CHANNEL_GROUP_CHECK_CONTROL, MILEAGE, QuantityType.valueOf(
+ Converter.round(ccEntry.ccmMileage), imperial ? ImperialUnits.MILE : Constants.KILOMETRE_UNIT));
+ }
+ }
+
+ protected void updateServices(List<CBSMessage> sl) {
+ // if list is empty add "undefined" element
+ if (sl.size() == 0) {
+ CBSMessage cbsm = new CBSMessage();
+ cbsm.cbsType = Constants.NO_ENTRIES;
+ cbsm.cbsDescription = Constants.NO_ENTRIES;
+ sl.add(cbsm);
+ }
+
+ // add all elements to options
+ serviceList = sl;
+ List<StateOption> serviceNameOptions = new ArrayList<>();
+ List<StateOption> serviceDetailsOptions = new ArrayList<>();
+ List<StateOption> serviceDateOptions = new ArrayList<>();
+ List<StateOption> serviceMileageOptions = new ArrayList<>();
+ boolean isSelectedElementIn = false;
+ int index = 0;
+ for (CBSMessage serviceEntry : serviceList) {
+ // create StateOption with "value = list index" and "label = human readable string"
+ serviceNameOptions.add(new StateOption(Integer.toString(index), serviceEntry.getType()));
+ serviceDetailsOptions.add(new StateOption(Integer.toString(index), serviceEntry.getDescription()));
+ serviceDateOptions.add(new StateOption(Integer.toString(index), serviceEntry.getDueDate()));
+ serviceMileageOptions
+ .add(new StateOption(Integer.toString(index), Integer.toString(serviceEntry.cbsRemainingMileage)));
+ if (selectedService.equals(serviceEntry.getType())) {
+ isSelectedElementIn = true;
+ }
+ index++;
+ }
+ setOptions(CHANNEL_GROUP_SERVICE, NAME, serviceNameOptions);
+ setOptions(CHANNEL_GROUP_SERVICE, DETAILS, serviceDetailsOptions);
+ setOptions(CHANNEL_GROUP_SERVICE, DATE, serviceDateOptions);
+ setOptions(CHANNEL_GROUP_SERVICE, MILEAGE, serviceMileageOptions);
+
+ // if current selected item isn't anymore in the list select first entry
+ if (!isSelectedElementIn) {
+ selectService(0);
+ }
+ }
+
+ protected void selectService(int index) {
+ if (index >= 0 && index < serviceList.size()) {
+ CBSMessage serviceEntry = serviceList.get(index);
+ selectedService = serviceEntry.cbsType;
+ updateChannel(CHANNEL_GROUP_SERVICE, NAME,
+ StringType.valueOf(Converter.toTitleCase(serviceEntry.getType())));
+ updateChannel(CHANNEL_GROUP_SERVICE, DETAILS,
+ StringType.valueOf(Converter.toTitleCase(serviceEntry.getDescription())));
+ updateChannel(CHANNEL_GROUP_SERVICE, DATE,
+ DateTimeType.valueOf(Converter.getLocalDateTime(serviceEntry.getDueDate())));
+ updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE,
+ QuantityType.valueOf(Converter.round(serviceEntry.cbsRemainingMileage),
+ imperial ? ImperialUnits.MILE : Constants.KILOMETRE_UNIT));
+ }
+ }
+
+ protected void updateDestinations(List<Destination> dl) {
+ // if list is empty add "undefined" element
+ if (dl.size() == 0) {
+ Destination dest = new Destination();
+ dest.city = Constants.NO_ENTRIES;
+ dest.lat = -1;
+ dest.lon = -1;
+ dl.add(dest);
+ }
+
+ // add all elements to options
+ destinationList = dl;
+ List<StateOption> destinationNameOptions = new ArrayList<>();
+ List<StateOption> destinationGPSOptions = new ArrayList<>();
+ boolean isSelectedElementIn = false;
+ int index = 0;
+ for (Destination destination : destinationList) {
+ destinationNameOptions.add(new StateOption(Integer.toString(index), destination.getAddress()));
+ destinationGPSOptions.add(new StateOption(Integer.toString(index), destination.getCoordinates()));
+ if (selectedDestination.equals(destination.getAddress())) {
+ isSelectedElementIn = true;
+ }
+ index++;
+ }
+ setOptions(CHANNEL_GROUP_DESTINATION, NAME, destinationNameOptions);
+ setOptions(CHANNEL_GROUP_DESTINATION, GPS, destinationGPSOptions);
+
+ // if current selected item isn't anymore in the list select first entry
+ if (!isSelectedElementIn) {
+ selectDestination(0);
+ }
+ }
+
+ protected void selectDestination(int index) {
+ if (index >= 0 && index < destinationList.size()) {
+ Destination destinationEntry = destinationList.get(index);
+ // update selected Item
+ selectedDestination = destinationEntry.getAddress();
+ // update coordinates according to new set location
+ updateChannel(CHANNEL_GROUP_DESTINATION, NAME, StringType.valueOf(destinationEntry.getAddress()));
+ updateChannel(CHANNEL_GROUP_DESTINATION, GPS, PointType.valueOf(destinationEntry.getCoordinates()));
+ }
+ }
+
+ protected void updateAllTrips(AllTrips allTrips) {
+ QuantityType<Length> qtTotalElectric = QuantityType
+ .valueOf(Converter.round(allTrips.totalElectricDistance.userTotal), Constants.KILOMETRE_UNIT);
+ QuantityType<Length> qtLongestElectricRange = QuantityType
+ .valueOf(Converter.round(allTrips.chargecycleRange.userHigh), Constants.KILOMETRE_UNIT);
+ QuantityType<Length> qtDistanceSinceCharge = QuantityType
+ .valueOf(Converter.round(allTrips.chargecycleRange.userCurrentChargeCycle), Constants.KILOMETRE_UNIT);
+
+ updateChannel(CHANNEL_GROUP_LIFETIME, TOTAL_DRIVEN_DISTANCE,
+ imperial ? Converter.getMiles(qtTotalElectric) : qtTotalElectric);
+ updateChannel(CHANNEL_GROUP_LIFETIME, SINGLE_LONGEST_DISTANCE,
+ imperial ? Converter.getMiles(qtLongestElectricRange) : qtLongestElectricRange);
+ updateChannel(CHANNEL_GROUP_LAST_TRIP, DISTANCE_SINCE_CHARGING,
+ imperial ? Converter.getMiles(qtDistanceSinceCharge) : qtDistanceSinceCharge);
+
+ // Conversion from kwh/100km to kwh/10mi has to be done manually
+ double avgConsumotion = imperial ? allTrips.avgElectricConsumption.userAverage * Converter.MILES_TO_KM_RATIO
+ : allTrips.avgElectricConsumption.userAverage;
+ double avgCombinedConsumption = imperial
+ ? allTrips.avgCombinedConsumption.userAverage * Converter.MILES_TO_KM_RATIO
+ : allTrips.avgCombinedConsumption.userAverage;
+ double avgRecuperation = imperial ? allTrips.avgRecuperation.userAverage * Converter.MILES_TO_KM_RATIO
+ : allTrips.avgRecuperation.userAverage;
+
+ updateChannel(CHANNEL_GROUP_LIFETIME, AVG_CONSUMPTION,
+ QuantityType.valueOf(Converter.round(avgConsumotion), Units.KILOWATT_HOUR));
+ updateChannel(CHANNEL_GROUP_LIFETIME, AVG_COMBINED_CONSUMPTION,
+ QuantityType.valueOf(Converter.round(avgCombinedConsumption), Units.LITRE));
+ updateChannel(CHANNEL_GROUP_LIFETIME, AVG_RECUPERATION,
+ QuantityType.valueOf(Converter.round(avgRecuperation), Units.KILOWATT_HOUR));
+ }
+
+ protected void updateLastTrip(LastTrip trip) {
+ // Whyever the Last Trip DateTime is delivered without offest - so LocalTime
+ updateChannel(CHANNEL_GROUP_LAST_TRIP, DATE,
+ DateTimeType.valueOf(Converter.getLocalDateTimeWithoutOffest(trip.date)));
+ updateChannel(CHANNEL_GROUP_LAST_TRIP, DURATION, QuantityType.valueOf(trip.duration, Units.MINUTE));
+
+ QuantityType<Length> qtTotalDistance = QuantityType.valueOf(Converter.round(trip.totalDistance),
+ Constants.KILOMETRE_UNIT);
+ updateChannel(CHANNEL_GROUP_LAST_TRIP, DISTANCE,
+ imperial ? Converter.getMiles(qtTotalDistance) : qtTotalDistance);
+
+ // Conversion from kwh/100km to kwh/10mi has to be done manually
+ double avgConsumtption = imperial ? trip.avgElectricConsumption * Converter.MILES_TO_KM_RATIO
+ : trip.avgElectricConsumption;
+ double avgCombinedConsumption = imperial ? trip.avgCombinedConsumption * Converter.MILES_TO_KM_RATIO
+ : trip.avgCombinedConsumption;
+ double avgRecuperation = imperial ? trip.avgRecuperation * Converter.MILES_TO_KM_RATIO : trip.avgRecuperation;
+
+ updateChannel(CHANNEL_GROUP_LAST_TRIP, AVG_CONSUMPTION,
+ QuantityType.valueOf(Converter.round(avgConsumtption), Units.KILOWATT_HOUR));
+ updateChannel(CHANNEL_GROUP_LAST_TRIP, AVG_COMBINED_CONSUMPTION,
+ QuantityType.valueOf(Converter.round(avgCombinedConsumption), Units.LITRE));
+ updateChannel(CHANNEL_GROUP_LAST_TRIP, AVG_RECUPERATION,
+ QuantityType.valueOf(Converter.round(avgRecuperation), Units.KILOWATT_HOUR));
+ }
+
+ protected void updateChargeProfileFromContent(String content) {
+ ChargeProfileWrapper.fromJson(content).ifPresent(this::updateChargeProfile);
+ }
+
+ protected void updateChargeProfile(ChargeProfileWrapper wrapper) {
+ updateChannel(CHANNEL_GROUP_CHARGE, CHARGE_PROFILE_PREFERENCE,
+ StringType.valueOf(Converter.toTitleCase(wrapper.getPreference())));
+ updateChannel(CHANNEL_GROUP_CHARGE, CHARGE_PROFILE_MODE,
+ StringType.valueOf(Converter.toTitleCase(wrapper.getMode())));
+ final Boolean climate = wrapper.isEnabled(ProfileKey.CLIMATE);
+ updateChannel(CHANNEL_GROUP_CHARGE, CHARGE_PROFILE_CLIMATE,
+ climate == null ? UnDefType.UNDEF : OnOffType.from(climate));
+ updateTimedState(wrapper, ProfileKey.WINDOWSTART);
+ updateTimedState(wrapper, ProfileKey.WINDOWEND);
+ updateTimedState(wrapper, ProfileKey.TIMER1);
+ updateTimedState(wrapper, ProfileKey.TIMER2);
+ updateTimedState(wrapper, ProfileKey.TIMER3);
+ updateTimedState(wrapper, ProfileKey.OVERRIDE);
+ }
+
+ protected void updateTimedState(ChargeProfileWrapper profile, ProfileKey key) {
+ final TimedChannel timed = ChargeProfileUtils.getTimedChannel(key);
+ if (timed != null) {
+ final LocalTime time = profile.getTime(key);
+ updateChannel(CHANNEL_GROUP_CHARGE, timed.time, time == null ? UnDefType.UNDEF
+ : new DateTimeType(ZonedDateTime.of(Constants.EPOCH_DAY, time, ZoneId.systemDefault())));
+ if (timed.timer != null) {
+ final Boolean enabled = profile.isEnabled(key);
+ updateChannel(CHANNEL_GROUP_CHARGE, timed.timer + CHARGE_ENABLED,
+ enabled == null ? UnDefType.UNDEF : OnOffType.from(enabled));
+ if (timed.hasDays) {
+ final Set<DayOfWeek> days = profile.getDays(key);
+ updateChannel(CHANNEL_GROUP_CHARGE, timed.timer + CHARGE_DAYS,
+ days == null ? UnDefType.UNDEF : StringType.valueOf(ChargeProfileUtils.formatDays(days)));
+ EnumSet.allOf(DayOfWeek.class).forEach(day -> {
+ updateChannel(CHANNEL_GROUP_CHARGE, timed.timer + ChargeProfileUtils.getDaysChannel(day),
+ days == null ? UnDefType.UNDEF : OnOffType.from(days.contains(day)));
+ });
+ }
+ }
+ }
+ }
+
+ protected void updateDoors(Doors doorState) {
+ updateChannel(CHANNEL_GROUP_DOORS, DOOR_DRIVER_FRONT,
+ StringType.valueOf(Converter.toTitleCase(doorState.doorDriverFront)));
+ updateChannel(CHANNEL_GROUP_DOORS, DOOR_DRIVER_REAR,
+ StringType.valueOf(Converter.toTitleCase(doorState.doorDriverRear)));
+ updateChannel(CHANNEL_GROUP_DOORS, DOOR_PASSENGER_FRONT,
+ StringType.valueOf(Converter.toTitleCase(doorState.doorPassengerFront)));
+ updateChannel(CHANNEL_GROUP_DOORS, DOOR_PASSENGER_REAR,
+ StringType.valueOf(Converter.toTitleCase(doorState.doorPassengerRear)));
+ updateChannel(CHANNEL_GROUP_DOORS, TRUNK, StringType.valueOf(Converter.toTitleCase(doorState.trunk)));
+ updateChannel(CHANNEL_GROUP_DOORS, HOOD, StringType.valueOf(Converter.toTitleCase(doorState.hood)));
+ }
+
+ protected void updateWindows(Windows windowState) {
+ updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_DRIVER_FRONT,
+ StringType.valueOf(Converter.toTitleCase(windowState.windowDriverFront)));
+ updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_DRIVER_REAR,
+ StringType.valueOf(Converter.toTitleCase(windowState.windowDriverRear)));
+ updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_PASSENGER_FRONT,
+ StringType.valueOf(Converter.toTitleCase(windowState.windowPassengerFront)));
+ updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_PASSENGER_REAR,
+ StringType.valueOf(Converter.toTitleCase(windowState.windowPassengerRear)));
+ updateChannel(CHANNEL_GROUP_DOORS, WINDOW_REAR,
+ StringType.valueOf(Converter.toTitleCase(windowState.rearWindow)));
+ updateChannel(CHANNEL_GROUP_DOORS, SUNROOF, StringType.valueOf(Converter.toTitleCase(windowState.sunroof)));
+ }
+
+ protected void updatePosition(Position pos) {
+ updateChannel(CHANNEL_GROUP_LOCATION, GPS, PointType.valueOf(pos.getCoordinates()));
+ updateChannel(CHANNEL_GROUP_LOCATION, HEADING, QuantityType.valueOf(pos.heading, Units.DEGREE_ANGLE));
+ }
+
+ protected void updateVehicleStatus(VehicleStatus vStatus) {
+ // Vehicle Status
+ updateChannel(CHANNEL_GROUP_STATUS, LOCK, StringType.valueOf(Converter.toTitleCase(vStatus.doorLockState)));
+
+ // Service Updates
+ updateChannel(CHANNEL_GROUP_STATUS, SERVICE_DATE,
+ DateTimeType.valueOf(Converter.getLocalDateTime(VehicleStatusUtils.getNextServiceDate(vStatus))));
+
+ updateChannel(CHANNEL_GROUP_STATUS, SERVICE_MILEAGE,
+ QuantityType.valueOf(Converter.round(VehicleStatusUtils.getNextServiceMileage(vStatus)),
+ imperial ? ImperialUnits.MILE : Constants.KILOMETRE_UNIT));
+ // CheckControl Active?
+ updateChannel(CHANNEL_GROUP_STATUS, CHECK_CONTROL,
+ StringType.valueOf(Converter.toTitleCase(VehicleStatusUtils.checkControlActive(vStatus))));
+ // last update Time
+ updateChannel(CHANNEL_GROUP_STATUS, LAST_UPDATE,
+ DateTimeType.valueOf(Converter.getLocalDateTime(VehicleStatusUtils.getUpdateTime(vStatus))));
+
+ Doors doorState = null;
+ try {
+ doorState = Converter.getGson().fromJson(Converter.getGson().toJson(vStatus), Doors.class);
+ } catch (JsonSyntaxException jse) {
+ logger.debug("Doors parse exception {}", jse.getMessage());
+ }
+ if (doorState != null) {
+ updateChannel(CHANNEL_GROUP_STATUS, DOORS, StringType.valueOf(VehicleStatusUtils.checkClosed(doorState)));
+ updateDoors(doorState);
+ }
+ Windows windowState = null;
+ try {
+ windowState = Converter.getGson().fromJson(Converter.getGson().toJson(vStatus), Windows.class);
+ } catch (JsonSyntaxException jse) {
+ logger.debug("Windows parse exception {}", jse.getMessage());
+ }
+ if (windowState != null) {
+ updateChannel(CHANNEL_GROUP_STATUS, WINDOWS,
+ StringType.valueOf(VehicleStatusUtils.checkClosed(windowState)));
+ updateWindows(windowState);
+ }
+
+ // Range values
+ // based on unit of length decide if range shall be reported in km or miles
+ float totalRange = 0;
+ if (isElectric) {
+ totalRange += vStatus.remainingRangeElectric;
+ QuantityType<Length> qtElectricRange = QuantityType.valueOf(vStatus.remainingRangeElectric,
+ Constants.KILOMETRE_UNIT);
+ QuantityType<Length> qtElectricRadius = QuantityType
+ .valueOf(Converter.guessRangeRadius(vStatus.remainingRangeElectric), Constants.KILOMETRE_UNIT);
+
+ updateChannel(CHANNEL_GROUP_RANGE, RANGE_ELECTRIC,
+ imperial ? Converter.getMiles(qtElectricRange) : qtElectricRange);
+ updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_ELECTRIC,
+ imperial ? Converter.getMiles(qtElectricRadius) : qtElectricRadius);
+ }
+ if (hasFuel) {
+ totalRange += vStatus.remainingRangeFuel;
+ QuantityType<Length> qtFuelRange = QuantityType.valueOf(vStatus.remainingRangeFuel,
+ Constants.KILOMETRE_UNIT);
+ QuantityType<Length> qtFuelRadius = QuantityType
+ .valueOf(Converter.guessRangeRadius(vStatus.remainingRangeFuel), Constants.KILOMETRE_UNIT);
+
+ updateChannel(CHANNEL_GROUP_RANGE, RANGE_FUEL, imperial ? Converter.getMiles(qtFuelRange) : qtFuelRange);
+ updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_FUEL,
+ imperial ? Converter.getMiles(qtFuelRadius) : qtFuelRadius);
+ }
+ if (isHybrid) {
+ QuantityType<Length> qtHybridRange = QuantityType.valueOf(totalRange, Constants.KILOMETRE_UNIT);
+ QuantityType<Length> qtHybridRadius = QuantityType.valueOf(Converter.guessRangeRadius(totalRange),
+ Constants.KILOMETRE_UNIT);
+ updateChannel(CHANNEL_GROUP_RANGE, RANGE_HYBRID,
+ imperial ? Converter.getMiles(qtHybridRange) : qtHybridRange);
+ updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_HYBRID,
+ imperial ? Converter.getMiles(qtHybridRadius) : qtHybridRadius);
+ }
+
+ updateChannel(CHANNEL_GROUP_RANGE, MILEAGE,
+ QuantityType.valueOf(vStatus.mileage, imperial ? ImperialUnits.MILE : Constants.KILOMETRE_UNIT));
+ if (isElectric) {
+ updateChannel(CHANNEL_GROUP_RANGE, SOC, QuantityType.valueOf(vStatus.chargingLevelHv, Units.PERCENT));
+ }
+ if (hasFuel) {
+ updateChannel(CHANNEL_GROUP_RANGE, REMAINING_FUEL,
+ QuantityType.valueOf(vStatus.remainingFuel, Units.LITRE));
+ }
+
+ // Charge Values
+ if (isElectric) {
+ if (vStatus.chargingStatus != null) {
+ if (Constants.INVALID.equals(vStatus.chargingStatus)) {
+ updateChannel(CHANNEL_GROUP_STATUS, CHARGE_STATUS,
+ StringType.valueOf(Converter.toTitleCase(vStatus.lastChargingEndReason)));
+ } else {
+ // State INVALID is somehow misleading. Instead show the Last Charging End Reason
+ updateChannel(CHANNEL_GROUP_STATUS, CHARGE_STATUS,
+ StringType.valueOf(Converter.toTitleCase(vStatus.chargingStatus)));
+ }
+ } else {
+ updateChannel(CHANNEL_GROUP_STATUS, CHARGE_STATUS, UnDefType.NULL);
+ }
+ if (vStatus.chargingTimeRemaining != null) {
+ try {
+ updateChannel(CHANNEL_GROUP_STATUS, CHARGE_REMAINING,
+ QuantityType.valueOf(vStatus.chargingTimeRemaining, Units.MINUTE));
+ } catch (NumberFormatException nfe) {
+ updateChannel(CHANNEL_GROUP_STATUS, CHARGE_REMAINING, UnDefType.UNDEF);
+ }
+ } else {
+ updateChannel(CHANNEL_GROUP_STATUS, CHARGE_REMAINING, UnDefType.NULL);
+ }
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.VehicleConfiguration;
+import org.openhab.binding.bmwconnecteddrive.internal.action.BMWConnectedDriveActions;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.DestinationContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.compat.VehicleAttributesContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.AllTrips;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.AllTripsContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.LastTrip;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.LastTripContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatus;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatusContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.RemoteServiceHandler.ExecutionState;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.RemoteServiceHandler.RemoteService;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileUtils;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileUtils.ChargeKeyDay;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper.ProfileKey;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ImageProperties;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.RemoteServiceUtils;
+import org.openhab.core.io.net.http.HttpUtil;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.RawType;
+import org.openhab.core.library.types.StringType;
+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.BridgeHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link VehicleHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - edit & send charge profile
+ */
+@NonNullByDefault
+public class VehicleHandler extends VehicleChannelHandler {
+ private int legacyMode = Constants.INT_UNDEF; // switch to legacy API in case of 404 Errors
+
+ private Optional<ConnectedDriveProxy> proxy = Optional.empty();
+ private Optional<RemoteServiceHandler> remote = Optional.empty();
+ private Optional<VehicleConfiguration> configuration = Optional.empty();
+ private Optional<ConnectedDriveBridgeHandler> bridgeHandler = Optional.empty();
+ private Optional<ScheduledFuture<?>> refreshJob = Optional.empty();
+ private Optional<ScheduledFuture<?>> editTimeout = Optional.empty();
+ private Optional<List<ResponseCallback>> callbackCounter = Optional.empty();
+
+ private ImageProperties imageProperties = new ImageProperties();
+ VehicleStatusCallback vehicleStatusCallback = new VehicleStatusCallback();
+ StringResponseCallback oldVehicleStatusCallback = new LegacyVehicleStatusCallback();
+ StringResponseCallback lastTripCallback = new LastTripCallback();
+ StringResponseCallback allTripsCallback = new AllTripsCallback();
+ StringResponseCallback chargeProfileCallback = new ChargeProfilesCallback();
+ StringResponseCallback rangeMapCallback = new RangeMapCallback();
+ DestinationsCallback destinationCallback = new DestinationsCallback();
+ ByteResponseCallback imageCallback = new ImageCallback();
+
+ private Optional<ChargeProfileWrapper> chargeProfileEdit = Optional.empty();
+ private Optional<String> chargeProfileSent = Optional.empty();
+
+ public VehicleHandler(Thing thing, BMWConnectedDriveOptionProvider op, String type, boolean imperial) {
+ super(thing, op, type, imperial);
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ String group = channelUID.getGroupId();
+
+ // Refresh of Channels with cached values
+ if (command instanceof RefreshType) {
+ if (CHANNEL_GROUP_LAST_TRIP.equals(group)) {
+ lastTripCache.ifPresent(lastTrip -> lastTripCallback.onResponse(lastTrip));
+ } else if (CHANNEL_GROUP_LIFETIME.equals(group)) {
+ allTripsCache.ifPresent(allTrips -> allTripsCallback.onResponse(allTrips));
+ } else if (CHANNEL_GROUP_DESTINATION.equals(group)) {
+ destinationCache.ifPresent(destination -> destinationCallback.onResponse(destination));
+ } else if (CHANNEL_GROUP_STATUS.equals(group)) {
+ vehicleStatusCache.ifPresent(vehicleStatus -> vehicleStatusCallback.onResponse(vehicleStatus));
+ } else if (CHANNEL_GROUP_CHARGE.equals(group)) {
+ chargeProfileEdit.ifPresentOrElse(this::updateChargeProfile,
+ () -> chargeProfileCache.ifPresent(this::updateChargeProfileFromContent));
+ } else if (CHANNEL_GROUP_VEHICLE_IMAGE.equals(group)) {
+ imageCache.ifPresent(image -> imageCallback.onResponse(image));
+ }
+ // Check for Channel Group and corresponding Actions
+ } else if (CHANNEL_GROUP_REMOTE.equals(group)) {
+ // Executing Remote Services
+ if (command instanceof StringType) {
+ String serviceCommand = ((StringType) command).toFullString();
+ remote.ifPresent(remot -> {
+ switch (serviceCommand) {
+ case REMOTE_SERVICE_LIGHT_FLASH:
+ case REMOTE_SERVICE_AIR_CONDITIONING:
+ case REMOTE_SERVICE_DOOR_LOCK:
+ case REMOTE_SERVICE_DOOR_UNLOCK:
+ case REMOTE_SERVICE_HORN:
+ case REMOTE_SERVICE_VEHICLE_FINDER:
+ case REMOTE_SERVICE_CHARGE_NOW:
+ RemoteServiceUtils.getRemoteService(serviceCommand)
+ .ifPresentOrElse(service -> remot.execute(service), () -> {
+ logger.debug("Remote service execution {} unknown", serviceCommand);
+ });
+ break;
+ case REMOTE_SERVICE_CHARGING_CONTROL:
+ sendChargeProfile(chargeProfileEdit);
+ break;
+ default:
+ logger.debug("Remote service execution {} unknown", serviceCommand);
+ break;
+ }
+ });
+ }
+ } else if (CHANNEL_GROUP_VEHICLE_IMAGE.equals(group)) {
+ // Image Change
+ configuration.ifPresent(config -> {
+ if (command instanceof StringType) {
+ if (channelUID.getIdWithoutGroup().equals(IMAGE_VIEWPORT)) {
+ String newViewport = command.toString();
+ synchronized (imageProperties) {
+ if (!imageProperties.viewport.equals(newViewport)) {
+ imageProperties = new ImageProperties(newViewport, imageProperties.size);
+ imageCache = Optional.empty();
+ proxy.ifPresent(prox -> prox.requestImage(config, imageProperties, imageCallback));
+ }
+ }
+ updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, StringType.valueOf(newViewport));
+ }
+ }
+ if (command instanceof DecimalType) {
+ if (command instanceof DecimalType) {
+ int newImageSize = ((DecimalType) command).intValue();
+ if (channelUID.getIdWithoutGroup().equals(IMAGE_SIZE)) {
+ synchronized (imageProperties) {
+ if (imageProperties.size != newImageSize) {
+ imageProperties = new ImageProperties(imageProperties.viewport, newImageSize);
+ imageCache = Optional.empty();
+ proxy.ifPresent(prox -> prox.requestImage(config, imageProperties, imageCallback));
+ }
+ }
+ }
+ updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_SIZE, new DecimalType(newImageSize));
+ }
+ }
+ });
+ } else if (CHANNEL_GROUP_DESTINATION.equals(group)) {
+ if (command instanceof StringType) {
+ int index = Converter.getIndex(command.toFullString());
+ if (index != -1) {
+ selectDestination(index);
+ } else {
+ logger.debug("Cannot select Destination index {}", command.toFullString());
+ }
+ }
+ } else if (CHANNEL_GROUP_SERVICE.equals(group)) {
+ if (command instanceof StringType) {
+ int index = Converter.getIndex(command.toFullString());
+ if (index != -1) {
+ selectService(index);
+ } else {
+ logger.debug("Cannot select Service index {}", command.toFullString());
+ }
+ }
+ } else if (CHANNEL_GROUP_CHECK_CONTROL.equals(group)) {
+ if (command instanceof StringType) {
+ int index = Converter.getIndex(command.toFullString());
+ if (index != -1) {
+ selectCheckControl(index);
+ } else {
+ logger.debug("Cannot select CheckControl index {}", command.toFullString());
+ }
+ }
+ } else if (CHANNEL_GROUP_CHARGE.equals(group)) {
+ handleChargeProfileCommand(channelUID, command);
+ }
+ }
+
+ @Override
+ public void initialize() {
+ callbackCounter = Optional.of(new ArrayList<ResponseCallback>());
+ updateStatus(ThingStatus.UNKNOWN);
+ final VehicleConfiguration config = getConfigAs(VehicleConfiguration.class);
+ configuration = Optional.of(config);
+ Bridge bridge = getBridge();
+ if (bridge != null) {
+ BridgeHandler handler = bridge.getHandler();
+ if (handler != null) {
+ bridgeHandler = Optional.of(((ConnectedDriveBridgeHandler) handler));
+ proxy = ((ConnectedDriveBridgeHandler) handler).getProxy();
+ remote = proxy.map(prox -> prox.getRemoteServiceHandler(this));
+ } else {
+ logger.debug("Bridge Handler null");
+ }
+ } else {
+ logger.debug("Bridge null");
+ }
+
+ // get Image after init with config values
+ synchronized (imageProperties) {
+ imageProperties = new ImageProperties(config.imageViewport, config.imageSize);
+ }
+ updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, StringType.valueOf((config.imageViewport)));
+ updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_SIZE, new DecimalType((config.imageSize)));
+
+ // check imperial setting is different to AutoDetect
+ if (!UNITS_AUTODETECT.equals(config.units)) {
+ imperial = UNITS_IMPERIAL.equals(config.units);
+ }
+
+ // start update schedule
+ startSchedule(config.refreshInterval);
+ }
+
+ 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));
+ editTimeout.ifPresent(job -> job.cancel(true));
+ remote.ifPresent(RemoteServiceHandler::cancel);
+ }
+
+ public void getData() {
+ proxy.ifPresentOrElse(prox -> {
+ configuration.ifPresentOrElse(config -> {
+ if (legacyMode == 1) {
+ prox.requestLegacyVehcileStatus(config, oldVehicleStatusCallback);
+ } else {
+ prox.requestVehcileStatus(config, vehicleStatusCallback);
+ }
+ addCallback(vehicleStatusCallback);
+ if (isSupported(Constants.STATISTICS)) {
+ prox.requestLastTrip(config, lastTripCallback);
+ prox.requestAllTrips(config, allTripsCallback);
+ addCallback(lastTripCallback);
+ addCallback(allTripsCallback);
+ }
+ if (isSupported(Constants.LAST_DESTINATIONS)) {
+ prox.requestDestinations(config, destinationCallback);
+ addCallback(destinationCallback);
+ }
+ if (isElectric) {
+ prox.requestChargingProfile(config, chargeProfileCallback);
+ addCallback(chargeProfileCallback);
+ }
+ synchronized (imageProperties) {
+ if (!imageCache.isPresent() && !imageProperties.failLimitReached()) {
+ prox.requestImage(config, imageProperties, imageCallback);
+ addCallback(imageCallback);
+ }
+ }
+ }, () -> {
+ logger.warn("ConnectedDrive Configuration isn't present");
+ });
+ }, () -> {
+ logger.warn("ConnectedDrive Proxy isn't present");
+ });
+ }
+
+ private synchronized void addCallback(ResponseCallback rc) {
+ callbackCounter.ifPresent(counter -> counter.add(rc));
+ }
+
+ private synchronized void removeCallback(ResponseCallback rc) {
+ callbackCounter.ifPresent(counter -> {
+ counter.remove(rc);
+ // all necessary callbacks received => print and set to empty
+ if (counter.isEmpty()) {
+ logFingerPrint();
+ callbackCounter = Optional.empty();
+ }
+ });
+ }
+
+ private void logFingerPrint() {
+ final String vin = configuration.map(config -> config.vin).orElse("");
+ logger.debug("###### Vehicle Troubleshoot Fingerprint Data - BEGIN ######");
+ logger.debug("### Discovery Result ###");
+ bridgeHandler.ifPresent(handler -> {
+ logger.debug("{}", handler.getDiscoveryFingerprint());
+ });
+ vehicleStatusCache.ifPresentOrElse(vehicleStatus -> {
+ logger.debug("### Vehicle Status ###");
+
+ // Anonymous data for VIN and Position
+ try {
+ VehicleStatusContainer container = Converter.getGson().fromJson(vehicleStatus,
+ VehicleStatusContainer.class);
+ if (container != null) {
+ VehicleStatus status = container.vehicleStatus;
+ if (status != null) {
+ status.vin = Constants.ANONYMOUS;
+ if (status.position != null) {
+ status.position.lat = -1;
+ status.position.lon = -1;
+ status.position.heading = -1;
+ }
+ }
+ }
+ logger.debug("{}", Converter.getGson().toJson(container));
+ } catch (JsonSyntaxException jse) {
+ logger.debug("{}", jse.getMessage());
+ }
+ }, () -> {
+ logger.debug("### Vehicle Status Empty ###");
+ });
+ lastTripCache.ifPresentOrElse(lastTrip -> {
+ logger.debug("### Last Trip ###");
+ logger.debug("{}", lastTrip.replaceAll(vin, Constants.ANONYMOUS));
+ }, () -> {
+ logger.debug("### Last Trip Empty ###");
+ });
+ allTripsCache.ifPresentOrElse(allTrips -> {
+ logger.debug("### All Trips ###");
+ logger.debug("{}", allTrips.replaceAll(vin, Constants.ANONYMOUS));
+ }, () -> {
+ logger.debug("### All Trips Empty ###");
+ });
+ if (isElectric) {
+ chargeProfileCache.ifPresentOrElse(chargeProfile -> {
+ logger.debug("### Charge Profile ###");
+ logger.debug("{}", chargeProfile.replaceAll(vin, Constants.ANONYMOUS));
+ }, () -> {
+ logger.debug("### Charge Profile Empty ###");
+ });
+ }
+ destinationCache.ifPresentOrElse(destination -> {
+ logger.debug("### Charge Profile ###");
+ try {
+ DestinationContainer container = Converter.getGson().fromJson(destination, DestinationContainer.class);
+ if (container != null) {
+ if (container.destinations != null) {
+ container.destinations.forEach(entry -> {
+ entry.lat = 0;
+ entry.lon = 0;
+ entry.city = Constants.ANONYMOUS;
+ entry.street = Constants.ANONYMOUS;
+ entry.streetNumber = Constants.ANONYMOUS;
+ entry.country = Constants.ANONYMOUS;
+ });
+ logger.debug("{}", Converter.getGson().toJson(container));
+ }
+ } else {
+ logger.debug("### Destinations Empty ###");
+ }
+ } catch (JsonSyntaxException jse) {
+ logger.debug("{}", jse.getMessage());
+ }
+ }, () -> {
+ logger.debug("### Charge Profile Empty ###");
+ });
+ rangeMapCache.ifPresentOrElse(rangeMap -> {
+ logger.debug("### Range Map ###");
+ logger.debug("{}", rangeMap.replaceAll(vin, Constants.ANONYMOUS));
+ }, () -> {
+ logger.debug("### Range Map Empty ###");
+ });
+ logger.debug("###### Vehicle Troubleshoot Fingerprint Data - END ######");
+ }
+
+ /**
+ * Don't stress ConnectedDrive with unnecessary requests. One call at the beginning is done to check the response.
+ * After cache has e.g. a proper error response it will be shown in the fingerprint
+ *
+ * @return
+ */
+ private boolean isSupported(String service) {
+ final String services = thing.getProperties().get(Constants.SERVICES_SUPPORTED);
+ if (services != null) {
+ if (services.contains(service)) {
+ return true;
+ }
+ }
+ // if cache is empty give it a try one time to collected Troubleshoot data
+ return lastTripCache.isEmpty() || allTripsCache.isEmpty() || destinationCache.isEmpty();
+ }
+
+ public void updateRemoteExecutionStatus(@Nullable String service, @Nullable String status) {
+ if (RemoteService.CHARGING_CONTROL.toString().equals(service)
+ && ExecutionState.EXECUTED.name().equals(status)) {
+ saveChargeProfileSent();
+ }
+ updateChannel(CHANNEL_GROUP_REMOTE, REMOTE_STATE, StringType
+ .valueOf(Converter.toTitleCase((service == null ? "-" : service) + Constants.SPACE + status)));
+ }
+
+ public Optional<VehicleConfiguration> getConfiguration() {
+ return configuration;
+ }
+
+ public ScheduledExecutorService getScheduler() {
+ return scheduler;
+ }
+
+ /**
+ * Callbacks for ConnectedDrive Portal
+ *
+ * @author Bernd Weymann
+ *
+ */
+ public class ChargeProfilesCallback implements StringResponseCallback {
+ @Override
+ public void onResponse(@Nullable String content) {
+ if (content != null) {
+ chargeProfileCache = Optional.of(content);
+ if (chargeProfileEdit.isEmpty()) {
+ updateChargeProfileFromContent(content);
+ }
+ }
+ removeCallback(this);
+ }
+
+ /**
+ * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
+ */
+ @Override
+ public void onError(NetworkError error) {
+ logger.debug("{}", error.toString());
+ chargeProfileCache = Optional.of(Converter.getGson().toJson(error));
+ removeCallback(this);
+ }
+ }
+
+ public class RangeMapCallback implements StringResponseCallback {
+ @Override
+ public void onResponse(@Nullable String content) {
+ rangeMapCache = Optional.ofNullable(content);
+ removeCallback(this);
+ }
+
+ /**
+ * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
+ */
+ @Override
+ public void onError(NetworkError error) {
+ logger.debug("{}", error.toString());
+ rangeMapCache = Optional.of(Converter.getGson().toJson(error));
+ removeCallback(this);
+ }
+ }
+
+ public class DestinationsCallback implements StringResponseCallback {
+
+ @Override
+ public void onResponse(@Nullable String content) {
+ destinationCache = Optional.ofNullable(content);
+ if (content != null) {
+ try {
+ DestinationContainer dc = Converter.getGson().fromJson(content, DestinationContainer.class);
+ if (dc != null && dc.destinations != null) {
+ updateDestinations(dc.destinations);
+ }
+ } catch (JsonSyntaxException jse) {
+ logger.debug("{}", jse.getMessage());
+ }
+ }
+ removeCallback(this);
+ }
+
+ /**
+ * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
+ */
+ @Override
+ public void onError(NetworkError error) {
+ logger.debug("{}", error.toString());
+ destinationCache = Optional.of(Converter.getGson().toJson(error));
+ removeCallback(this);
+ }
+ }
+
+ public class ImageCallback implements ByteResponseCallback {
+ @Override
+ public void onResponse(byte[] content) {
+ if (content.length > 0) {
+ imageCache = Optional.of(content);
+ String contentType = HttpUtil.guessContentTypeFromData(content);
+ updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_FORMAT, new RawType(content, contentType));
+ } else {
+ synchronized (imageProperties) {
+ imageProperties.failed();
+ }
+ }
+ removeCallback(this);
+ }
+
+ /**
+ * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
+ */
+ @Override
+ public void onError(NetworkError error) {
+ logger.debug("{}", error.toString());
+ synchronized (imageProperties) {
+ imageProperties.failed();
+ }
+ removeCallback(this);
+ }
+ }
+
+ public class AllTripsCallback implements StringResponseCallback {
+ @Override
+ public void onResponse(@Nullable String content) {
+ if (content != null) {
+ allTripsCache = Optional.of(content);
+ try {
+ AllTripsContainer atc = Converter.getGson().fromJson(content, AllTripsContainer.class);
+ if (atc != null) {
+ AllTrips at = atc.allTrips;
+ if (at != null) {
+ updateAllTrips(at);
+ }
+ }
+ } catch (JsonSyntaxException jse) {
+ logger.debug("{}", jse.getMessage());
+ }
+ }
+ removeCallback(this);
+ }
+
+ /**
+ * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
+ */
+ @Override
+ public void onError(NetworkError error) {
+ logger.debug("{}", error.toString());
+ allTripsCache = Optional.of(Converter.getGson().toJson(error));
+ removeCallback(this);
+ }
+ }
+
+ public class LastTripCallback implements StringResponseCallback {
+ @Override
+ public void onResponse(@Nullable String content) {
+ if (content != null) {
+ lastTripCache = Optional.of(content);
+ try {
+ LastTripContainer lt = Converter.getGson().fromJson(content, LastTripContainer.class);
+ if (lt != null) {
+ LastTrip trip = lt.lastTrip;
+ if (trip != null) {
+ updateLastTrip(trip);
+ }
+ }
+ } catch (JsonSyntaxException jse) {
+ logger.debug("{}", jse.getMessage());
+ }
+ }
+ removeCallback(this);
+ }
+
+ /**
+ * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
+ */
+ @Override
+ public void onError(NetworkError error) {
+ logger.debug("{}", error.toString());
+ lastTripCache = Optional.of(Converter.getGson().toJson(error));
+ removeCallback(this);
+ }
+ }
+
+ /**
+ * The VehicleStatus is supported by all Vehicle Types so it's used to reflect the Thing Status
+ */
+ public class VehicleStatusCallback implements StringResponseCallback {
+ @Override
+ public void onResponse(@Nullable String content) {
+ if (content != null) {
+ // switch to non legacy mode
+ legacyMode = 0;
+ updateStatus(ThingStatus.ONLINE);
+ vehicleStatusCache = Optional.of(content);
+ try {
+ VehicleStatusContainer status = Converter.getGson().fromJson(content, VehicleStatusContainer.class);
+ if (status != null) {
+ VehicleStatus vStatus = status.vehicleStatus;
+ if (vStatus == null) {
+ return;
+ }
+ updateVehicleStatus(vStatus);
+ updateCheckControls(vStatus.checkControlMessages);
+ updateServices(vStatus.cbsData);
+ updatePosition(vStatus.position);
+ }
+ } catch (JsonSyntaxException jse) {
+ logger.debug("{}", jse.getMessage());
+ }
+ }
+ removeCallback(this);
+ }
+
+ @Override
+ public void onError(NetworkError error) {
+ logger.debug("{}", error.toString());
+ // only if legacyMode isn't set yet try legacy API
+ if (error.status != 200 && legacyMode == Constants.INT_UNDEF) {
+ logger.debug("VehicleStatus not found - try legacy API");
+ proxy.get().requestLegacyVehcileStatus(configuration.get(), oldVehicleStatusCallback);
+ }
+ vehicleStatusCache = Optional.of(Converter.getGson().toJson(error));
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error.reason);
+ removeCallback(this);
+ }
+ }
+
+ /**
+ * Fallback API if origin isn't supported.
+ * This comes from the Community Discussion where a Vehicle from 2015 answered with "404"
+ * https://community.openhab.org/t/bmw-connecteddrive-binding/105124
+ *
+ * Selection of API was discussed here
+ * https://community.openhab.org/t/bmw-connecteddrive-bmw-i3/103876
+ *
+ * I figured out that only one API was working for this Vehicle. So this backward compatible Callback is introduced.
+ * The delivered data is converted into the origin dto object so no changes in previous functional code needed
+ */
+ public class LegacyVehicleStatusCallback implements StringResponseCallback {
+ @Override
+ public void onResponse(@Nullable String content) {
+ if (content != null) {
+ try {
+ VehicleAttributesContainer vac = Converter.getGson().fromJson(content,
+ VehicleAttributesContainer.class);
+ vehicleStatusCallback.onResponse(Converter.transformLegacyStatus(vac));
+ legacyMode = 1;
+ logger.debug("VehicleStatus switched to legacy mode");
+ } catch (JsonSyntaxException jse) {
+ logger.debug("{}", jse.getMessage());
+ }
+ }
+ }
+
+ @Override
+ public void onError(NetworkError error) {
+ logger.debug("{}", error.toString());
+ vehicleStatusCallback.onError(error);
+ }
+ }
+
+ private void handleChargeProfileCommand(ChannelUID channelUID, Command command) {
+ if (chargeProfileEdit.isEmpty()) {
+ chargeProfileEdit = getChargeProfileWrapper();
+ }
+
+ chargeProfileEdit.ifPresent(profile -> {
+
+ boolean processed = false;
+
+ final String id = channelUID.getIdWithoutGroup();
+
+ if (command instanceof StringType) {
+ final String stringCommand = ((StringType) command).toFullString();
+ switch (id) {
+ case CHARGE_PROFILE_PREFERENCE:
+ profile.setPreference(stringCommand);
+ updateChannel(CHANNEL_GROUP_CHARGE, CHARGE_PROFILE_PREFERENCE,
+ StringType.valueOf(Converter.toTitleCase(profile.getPreference())));
+ processed = true;
+ break;
+ case CHARGE_PROFILE_MODE:
+ profile.setMode(stringCommand);
+ updateChannel(CHANNEL_GROUP_CHARGE, CHARGE_PROFILE_MODE,
+ StringType.valueOf(Converter.toTitleCase(profile.getMode())));
+ processed = true;
+ break;
+ default:
+ break;
+ }
+ } else if (command instanceof OnOffType) {
+ final ProfileKey enableKey = ChargeProfileUtils.getEnableKey(id);
+ if (enableKey != null) {
+ profile.setEnabled(enableKey, OnOffType.ON.equals(command));
+ updateTimedState(profile, enableKey);
+ processed = true;
+ } else {
+ final ChargeKeyDay chargeKeyDay = ChargeProfileUtils.getKeyDay(id);
+ if (chargeKeyDay != null) {
+ profile.setDayEnabled(chargeKeyDay.key, chargeKeyDay.day, OnOffType.ON.equals(command));
+ updateTimedState(profile, chargeKeyDay.key);
+ processed = true;
+ }
+ }
+ } else if (command instanceof DateTimeType) {
+ DateTimeType dtt = (DateTimeType) command;
+ logger.debug("Accept {} for ID {}", dtt.toFullString(), id);
+ final ProfileKey key = ChargeProfileUtils.getTimeKey(id);
+ if (key != null) {
+ profile.setTime(key, dtt.getZonedDateTime().toLocalTime());
+ updateTimedState(profile, key);
+ processed = true;
+ }
+ }
+
+ if (processed) {
+ // cancel current timer and add another 5 mins - valid for each edit
+ editTimeout.ifPresent(timeout -> timeout.cancel(true));
+ // start edit timer with 5 min timeout
+ editTimeout = Optional.of(scheduler.schedule(() -> {
+ editTimeout = Optional.empty();
+ chargeProfileEdit = Optional.empty();
+ chargeProfileCache.ifPresent(this::updateChargeProfileFromContent);
+ }, 5, TimeUnit.MINUTES));
+ } else {
+ logger.debug("unexpected command {} not processed", command.toFullString());
+ }
+ });
+ }
+
+ private void saveChargeProfileSent() {
+ editTimeout.ifPresent(timeout -> {
+ timeout.cancel(true);
+ editTimeout = Optional.empty();
+ });
+ chargeProfileSent.ifPresent(sent -> {
+ chargeProfileCache = Optional.of(sent);
+ chargeProfileSent = Optional.empty();
+ chargeProfileEdit = Optional.empty();
+ chargeProfileCache.ifPresent(this::updateChargeProfileFromContent);
+ });
+ }
+
+ @Override
+ public Collection<Class<? extends ThingHandlerService>> getServices() {
+ return Set.of(BMWConnectedDriveActions.class);
+ }
+
+ public Optional<ChargeProfileWrapper> getChargeProfileWrapper() {
+ return chargeProfileCache.flatMap(cache -> {
+ return ChargeProfileWrapper.fromJson(cache).map(wrapper -> {
+ return wrapper;
+ }).or(() -> {
+ logger.debug("cannot parse charging profile: {}", cache);
+ return Optional.empty();
+ });
+ }).or(() -> {
+ logger.debug("No ChargeProfile recieved so far - cannot start editing");
+ return Optional.empty();
+ });
+ }
+
+ public void sendChargeProfile(Optional<ChargeProfileWrapper> profile) {
+ profile.map(profil -> profil.getJson()).ifPresent(json -> {
+ logger.debug("sending charging profile: {}", json);
+ chargeProfileSent = Optional.of(json);
+ remote.ifPresent(rem -> rem.execute(RemoteService.CHARGING_CONTROL, json));
+ });
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler.simulation;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link Injector} Simulates feedback of the ConnectedDrive Portal
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class Injector {
+ private static boolean active = false;
+
+ // copy discovery json here
+ private static String discovery = "";
+
+ // copy vehicle status json here
+ private static String status = "";
+
+ public static boolean isActive() {
+ return active;
+ }
+
+ public static String getDiscovery() {
+ return discovery;
+ }
+
+ public static String getStatus() {
+ return status;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.utils;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link BimmerConstants} This class holds the important constants for the BMW Connected Drive Authorization. They
+ * are taken from the Bimmercode from github {@link https://github.com/bimmerconnected/bimmer_connected}
+ * File defining these constants
+ * {@link https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/account.py}
+ * https://customer.bmwgroup.com/one/app/oauth.js
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class BimmerConstants {
+
+ // https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/country_selector.py
+ public static final String REGION_NORTH_AMERICA = "NORTH_AMERICA";
+ public static final String REGION_CHINA = "CHINA";
+ public static final String REGION_ROW = "ROW";
+
+ // https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/country_selector.py
+ public static final String AUTH_SERVER_NORTH_AMERICA = "b2vapi.bmwgroup.us/gcdm";
+ public static final String AUTH_SERVER_CHINA = "b2vapi.bmwgroup.cn/gcdm";
+ public static final String AUTH_SERVER_ROW = "b2vapi.bmwgroup.com/gcdm";
+ public static final Map<String, String> AUTH_SERVER_MAP = Map.of(REGION_NORTH_AMERICA, AUTH_SERVER_NORTH_AMERICA,
+ REGION_CHINA, AUTH_SERVER_CHINA, REGION_ROW, AUTH_SERVER_ROW);
+
+ public static final String OAUTH_ENDPOINT = "/oauth/token";
+
+ public static final String SERVER_NORTH_AMERICA = "b2vapi.bmwgroup.us";
+ public static final String SERVER_CHINA = "b2vapi.bmwgroup.cn:8592";
+ public static final String SERVER_ROW = "b2vapi.bmwgroup.com";
+ public static final Map<String, String> SERVER_MAP = Map.of(REGION_NORTH_AMERICA, SERVER_NORTH_AMERICA,
+ REGION_CHINA, SERVER_CHINA, REGION_ROW, SERVER_ROW);
+
+ // see https://github.com/bimmerconnected/bimmer_connected/pull/252/files
+ public static final Map<String, String> AUTHORIZATION_VALUE_MAP = Map.of(REGION_NORTH_AMERICA,
+ "Basic ZDc2NmI1MzctYTY1NC00Y2JkLWEzZGMtMGNhNTY3MmQ3ZjhkOjE1ZjY5N2Y2LWE1ZDUtNGNhZC05OWQ5LTNhMTViYzdmMzk3Mw==",
+ REGION_CHINA,
+ "Basic blF2NkNxdHhKdVhXUDc0eGYzQ0p3VUVQOjF6REh4NnVuNGNEanliTEVOTjNreWZ1bVgya0VZaWdXUGNRcGR2RFJwSUJrN3JPSg==",
+ REGION_ROW,
+ "Basic ZDc2NmI1MzctYTY1NC00Y2JkLWEzZGMtMGNhNTY3MmQ3ZjhkOjE1ZjY5N2Y2LWE1ZDUtNGNhZC05OWQ5LTNhMTViYzdmMzk3Mw==");
+
+ public static final String CREDENTIAL_VALUES = "nQv6CqtxJuXWP74xf3CJwUEP:1zDHx6un4cDjybLENN3kyfumX2kEYigWPcQpdvDRpIBk7rOJ";
+ public static final String REDIRECT_URI_VALUE = "https://www.bmw-connecteddrive.com/app/static/external-dispatch.html";
+ public static final String SCOPE_VALUES = "authenticate_user vehicle_data remote_services";
+
+ public static final String LEGACY_CREDENTIAL_VALUES = "nQv6CqtxJuXWP74xf3CJwUEP:1zDHx6un4cDjybLENN3kyfumX2kEYigWPcQpdvDRpIBk7rOJ";
+ public static final String REFERER_URL = "https://www.bmw-connecteddrive.de/app/index.html";
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.utils;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
+
+import java.time.DayOfWeek;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper.ProfileKey;
+
+/**
+ * The {@link ChargeProfileUtils} utility functions for charging profiles
+ *
+ * @author Norbert Truchsess - initial contribution
+ */
+@NonNullByDefault
+public class ChargeProfileUtils {
+
+ // Charging
+ public static class TimedChannel {
+ public final String time;
+ public final @Nullable String timer;
+ public final boolean hasDays;
+
+ TimedChannel(final String time, @Nullable final String timer, final boolean hasDays) {
+ this.time = time;
+ this.timer = timer;
+ this.hasDays = hasDays;
+ }
+ }
+
+ @SuppressWarnings("serial")
+ private static final Map<ProfileKey, TimedChannel> TIMED_CHANNELS = new HashMap<>() {
+ {
+ put(ProfileKey.WINDOWSTART, new TimedChannel(CHARGE_WINDOW_START, null, false));
+ put(ProfileKey.WINDOWEND, new TimedChannel(CHARGE_WINDOW_END, null, false));
+ put(ProfileKey.TIMER1, new TimedChannel(CHARGE_TIMER1 + CHARGE_DEPARTURE, CHARGE_TIMER1, true));
+ put(ProfileKey.TIMER2, new TimedChannel(CHARGE_TIMER2 + CHARGE_DEPARTURE, CHARGE_TIMER2, true));
+ put(ProfileKey.TIMER3, new TimedChannel(CHARGE_TIMER3 + CHARGE_DEPARTURE, CHARGE_TIMER3, true));
+ put(ProfileKey.OVERRIDE, new TimedChannel(CHARGE_OVERRIDE + CHARGE_DEPARTURE, CHARGE_OVERRIDE, false));
+ }
+ };
+
+ @SuppressWarnings("serial")
+ private static final Map<DayOfWeek, String> DAY_CHANNELS = new HashMap<>() {
+ {
+ put(DayOfWeek.MONDAY, CHARGE_DAY_MON);
+ put(DayOfWeek.TUESDAY, CHARGE_DAY_TUE);
+ put(DayOfWeek.WEDNESDAY, CHARGE_DAY_WED);
+ put(DayOfWeek.THURSDAY, CHARGE_DAY_THU);
+ put(DayOfWeek.FRIDAY, CHARGE_DAY_FRI);
+ put(DayOfWeek.SATURDAY, CHARGE_DAY_SAT);
+ put(DayOfWeek.SUNDAY, CHARGE_DAY_SUN);
+ }
+ };
+
+ public static class ChargeKeyDay {
+ public final ProfileKey key;
+ public final DayOfWeek day;
+
+ ChargeKeyDay(final ProfileKey key, final DayOfWeek day) {
+ this.key = key;
+ this.day = day;
+ }
+ }
+
+ @SuppressWarnings("serial")
+ private static final Map<String, ProfileKey> CHARGE_ENABLED_CHANNEL_KEYS = new HashMap<>() {
+ {
+ TIMED_CHANNELS.forEach((key, channel) -> {
+ put(channel.timer + CHARGE_ENABLED, key);
+ });
+ put(CHARGE_PROFILE_CLIMATE, ProfileKey.CLIMATE);
+ }
+ };
+
+ @SuppressWarnings("serial")
+ private static final Map<String, ProfileKey> CHARGE_TIME_CHANNEL_KEYS = new HashMap<>() {
+ {
+ TIMED_CHANNELS.forEach((key, channel) -> {
+ put(channel.time, key);
+ });
+ }
+ };
+
+ @SuppressWarnings("serial")
+ private static final Map<String, ChargeKeyDay> CHARGE_DAYS_CHANNEL_KEYS = new HashMap<>() {
+ {
+ DAY_CHANNELS.forEach((dayOfWeek, dayChannel) -> {
+ put(CHARGE_TIMER1 + dayChannel, new ChargeKeyDay(ProfileKey.TIMER1, dayOfWeek));
+ put(CHARGE_TIMER2 + dayChannel, new ChargeKeyDay(ProfileKey.TIMER2, dayOfWeek));
+ put(CHARGE_TIMER3 + dayChannel, new ChargeKeyDay(ProfileKey.TIMER3, dayOfWeek));
+ });
+ }
+ };
+
+ public static @Nullable TimedChannel getTimedChannel(ProfileKey key) {
+ return TIMED_CHANNELS.get(key);
+ }
+
+ public static @Nullable String getDaysChannel(DayOfWeek day) {
+ return DAY_CHANNELS.get(day);
+ }
+
+ public static @Nullable ProfileKey getEnableKey(final String id) {
+ return CHARGE_ENABLED_CHANNEL_KEYS.get(id);
+ }
+
+ public static @Nullable ChargeKeyDay getKeyDay(final String id) {
+ return CHARGE_DAYS_CHANNEL_KEYS.get(id);
+ }
+
+ public static @Nullable ProfileKey getTimeKey(final String id) {
+ return CHARGE_TIME_CHANNEL_KEYS.get(id);
+ }
+
+ public static String formatDays(final Set<DayOfWeek> weekdays) {
+ return weekdays.stream().map(day -> Constants.DAYS.get(day)).collect(Collectors.joining(Constants.COMMA));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.utils;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper.ProfileKey.*;
+import static org.openhab.binding.bmwconnecteddrive.internal.utils.Constants.*;
+
+import java.time.DayOfWeek;
+import java.time.LocalTime;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.ChargingMode;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.ChargingPreference;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.charge.ChargeProfile;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.charge.ChargingWindow;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.charge.Timer;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.charge.WeeklyPlanner;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link ChargeProfileWrapper} Wrapper for ChargeProfiles
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - add ChargeProfileActions
+ */
+@NonNullByDefault
+public class ChargeProfileWrapper {
+ private static final Logger LOGGER = LoggerFactory.getLogger(ChargeProfileWrapper.class);
+
+ public enum ProfileType {
+ WEEKLY,
+ TWO_TIMES,
+ EMPTY
+ }
+
+ public enum ProfileKey {
+ CLIMATE,
+ TIMER1,
+ TIMER2,
+ TIMER3,
+ TIMER4,
+ OVERRIDE,
+ WINDOWSTART,
+ WINDOWEND
+ }
+
+ protected final ProfileType type;
+
+ private Optional<ChargingMode> mode = Optional.empty();
+ private Optional<ChargingPreference> preference = Optional.empty();
+
+ private final Map<ProfileKey, Boolean> enabled = new HashMap<>();
+ private final Map<ProfileKey, LocalTime> times = new HashMap<>();
+ private final Map<ProfileKey, Set<DayOfWeek>> daysOfWeek = new HashMap<>();
+
+ public static Optional<ChargeProfileWrapper> fromJson(final String content) {
+ try {
+ final ChargeProfile cp = Converter.getGson().fromJson(content, ChargeProfile.class);
+ if (cp != null) {
+ return Optional.of(new ChargeProfileWrapper(cp));
+ }
+ } catch (JsonSyntaxException jse) {
+ LOGGER.debug("ChargeProfile unparsable: {}", content);
+ }
+ return Optional.empty();
+ }
+
+ private ChargeProfileWrapper(final ChargeProfile profile) {
+ final WeeklyPlanner planner;
+
+ if (profile.weeklyPlanner != null) {
+ type = ProfileType.WEEKLY;
+ planner = profile.weeklyPlanner;
+ } else if (profile.twoTimesTimer != null) {
+ type = ProfileType.TWO_TIMES;
+ planner = profile.twoTimesTimer;
+ // timer days not supported
+ } else {
+ type = ProfileType.EMPTY;
+ return;
+ }
+
+ setPreference(planner.chargingPreferences);
+ setMode(planner.chargingMode);
+
+ setEnabled(CLIMATE, planner.climatizationEnabled);
+
+ addTimer(TIMER1, planner.timer1);
+ addTimer(TIMER2, planner.timer2);
+
+ if (planner.preferredChargingWindow != null) {
+ addTime(WINDOWSTART, planner.preferredChargingWindow.startTime);
+ addTime(WINDOWEND, planner.preferredChargingWindow.endTime);
+ } else {
+ preference.ifPresent(pref -> {
+ if (ChargingPreference.CHARGING_WINDOW.equals(pref)) {
+ addTime(WINDOWSTART, null);
+ addTime(WINDOWEND, null);
+ }
+ });
+ }
+
+ if (isWeekly()) {
+ addTimer(TIMER3, planner.timer3);
+ addTimer(OVERRIDE, planner.overrideTimer);
+ }
+ }
+
+ public @Nullable Boolean isEnabled(final ProfileKey key) {
+ return enabled.get(key);
+ }
+
+ public void setEnabled(final ProfileKey key, @Nullable final Boolean enabled) {
+ if (enabled == null) {
+ this.enabled.remove(key);
+ } else {
+ this.enabled.put(key, enabled);
+ }
+ }
+
+ public @Nullable String getMode() {
+ return mode.map(m -> m.name()).orElse(null);
+ }
+
+ public void setMode(final @Nullable String mode) {
+ if (mode != null) {
+ try {
+ this.mode = Optional.of(ChargingMode.valueOf(mode));
+ return;
+ } catch (IllegalArgumentException iae) {
+ LOGGER.warn("unexpected value for chargingMode: {}", mode);
+ }
+ }
+ this.mode = Optional.empty();
+ }
+
+ public @Nullable String getPreference() {
+ return preference.map(pref -> pref.name()).orElse(null);
+ }
+
+ public void setPreference(final @Nullable String preference) {
+ if (preference != null) {
+ try {
+ this.preference = Optional.of(ChargingPreference.valueOf(preference));
+ return;
+ } catch (IllegalArgumentException iae) {
+ LOGGER.warn("unexpected value for chargingPreference: {}", preference);
+ }
+ }
+ this.preference = Optional.empty();
+ }
+
+ public @Nullable Set<DayOfWeek> getDays(final ProfileKey key) {
+ return daysOfWeek.get(key);
+ }
+
+ public void setDays(final ProfileKey key, final @Nullable Set<DayOfWeek> days) {
+ if (days == null) {
+ daysOfWeek.remove(key);
+ } else {
+ daysOfWeek.put(key, days);
+ }
+ }
+
+ public void setDayEnabled(final ProfileKey key, final DayOfWeek day, final boolean enabled) {
+ final Set<DayOfWeek> days = daysOfWeek.get(key);
+ if (days == null) {
+ daysOfWeek.put(key, enabled ? EnumSet.of(day) : EnumSet.noneOf(DayOfWeek.class));
+ } else {
+ if (enabled) {
+ days.add(day);
+ } else {
+ days.remove(day);
+ }
+ }
+ }
+
+ public @Nullable LocalTime getTime(final ProfileKey key) {
+ return times.get(key);
+ }
+
+ public void setTime(final ProfileKey key, @Nullable LocalTime time) {
+ if (time == null) {
+ times.remove(key);
+ } else {
+ times.put(key, time);
+ }
+ }
+
+ public String getJson() {
+ final ChargeProfile profile = new ChargeProfile();
+ final WeeklyPlanner planner = new WeeklyPlanner();
+
+ preference.ifPresent(pref -> planner.chargingPreferences = pref.name());
+ planner.climatizationEnabled = isEnabled(CLIMATE);
+ preference.ifPresent(pref -> {
+ if (ChargingPreference.CHARGING_WINDOW.equals(pref)) {
+ planner.chargingMode = getMode();
+ final LocalTime start = getTime(WINDOWSTART);
+ final LocalTime end = getTime(WINDOWEND);
+ if (start != null || end != null) {
+ planner.preferredChargingWindow = new ChargingWindow();
+ planner.preferredChargingWindow.startTime = start == null ? null : start.format(TIME_FORMATER);
+ planner.preferredChargingWindow.endTime = end == null ? null : end.format(TIME_FORMATER);
+ }
+ }
+ });
+ planner.timer1 = getTimer(TIMER1);
+ planner.timer2 = getTimer(TIMER2);
+ if (isWeekly()) {
+ planner.timer3 = getTimer(TIMER3);
+ planner.overrideTimer = getTimer(OVERRIDE);
+ profile.weeklyPlanner = planner;
+ } else if (isTwoTimes()) {
+ profile.twoTimesTimer = planner;
+ }
+ return Converter.getGson().toJson(profile);
+ }
+
+ private void addTime(final ProfileKey key, @Nullable final String time) {
+ try {
+ times.put(key, time == null ? NULL_LOCAL_TIME : LocalTime.parse(time, TIME_FORMATER));
+ } catch (DateTimeParseException dtpe) {
+ LOGGER.warn("unexpected value for {} time: {}", key.name(), time);
+ }
+ }
+
+ private void addTimer(final ProfileKey key, @Nullable final Timer timer) {
+ if (timer == null) {
+ enabled.put(key, false);
+ addTime(key, null);
+ if (isWeekly()) {
+ daysOfWeek.put(key, EnumSet.noneOf(DayOfWeek.class));
+ }
+ } else {
+ enabled.put(key, timer.timerEnabled);
+ addTime(key, timer.departureTime);
+ if (isWeekly()) {
+ final EnumSet<DayOfWeek> daySet = EnumSet.noneOf(DayOfWeek.class);
+ if (timer.weekdays != null) {
+ for (String day : timer.weekdays) {
+ try {
+ daySet.add(DayOfWeek.valueOf(day));
+ } catch (IllegalArgumentException iae) {
+ LOGGER.warn("unexpected value for {} day: {}", key.name(), day);
+ }
+ }
+ }
+ daysOfWeek.put(key, daySet);
+ }
+ }
+ }
+
+ private @Nullable Timer getTimer(final ProfileKey key) {
+ final Timer timer = new Timer();
+ timer.timerEnabled = enabled.get(key);
+ final LocalTime time = times.get(key);
+ timer.departureTime = time == null ? null : time.format(TIME_FORMATER);
+ if (isWeekly()) {
+ final Set<DayOfWeek> days = daysOfWeek.get(key);
+ if (days != null) {
+ timer.weekdays = new ArrayList<>();
+ for (DayOfWeek day : days) {
+ timer.weekdays.add(day.name());
+ }
+ }
+ }
+ return timer.timerEnabled == null && timer.departureTime == null && timer.weekdays == null ? null : timer;
+ }
+
+ private boolean isWeekly() {
+ return ProfileType.WEEKLY.equals(type);
+ }
+
+ private boolean isTwoTimes() {
+ return ProfileType.TWO_TIMES.equals(type);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.utils;
+
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.Map;
+
+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.types.UnDefType;
+
+/**
+ * The {@link Constants} General Constant Definitions
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - contributor
+ */
+@NonNullByDefault
+public class Constants {
+ // For Vehicle Status
+ public static final String OK = "Ok";
+ public static final String ACTIVE = "Active";
+ public static final String NOT_ACTIVE = "Not Active";
+ public static final String NO_ENTRIES = "No Entries";
+ public static final String OPEN = "Open";
+ public static final String INVALID = "Invalid";
+ public static final String CLOSED = "Closed";
+ public static final String INTERMEDIATE = "Intermediate";
+ public static final String UNDEF = UnDefType.UNDEF.toFullString();
+ public static final String UTC_APPENDIX = "-01T12:00:00";
+ public static final String NULL_DATE = "1900-01-01T00:00:00";
+ public static final String NULL_TIME = "00:00";
+ public static final int INT_UNDEF = -1;
+ public static final Unit<Length> KILOMETRE_UNIT = MetricPrefix.KILO(SIUnits.METRE);
+
+ // Services to query
+ public static final String SERVICES_SUPPORTED = "servicesSupported";
+ public static final String STATISTICS = "Statistics";
+ public static final String LAST_DESTINATIONS = "LastDestinations";
+
+ // Services in Discovery
+ public static final String ACTIVATED = "ACTIVATED";
+ public static final String SUPPORTED = "SUPPORTED";
+ public static final String NOT_SUPPORTED = "NOT_SUPPORTED";
+
+ // General Constants for String concatenation
+ public static final String NULL = "null";
+ public static final String SPACE = " ";
+ public static final String UNDERLINE = "_";
+ public static final String HYPHEN = " - ";
+ public static final String PLUS = "+";
+ public static final String EMPTY = "";
+ public static final String COMMA = ",";
+ public static final String QUESTION = "?";
+ public static final String COLON = ":";
+
+ public static final String ANONYMOUS = "Anonymous";
+ public static final int MILES_TO_FEET_FACTOR = 5280;
+ public static final String EMPTY_JSON = "{}";
+
+ // Time Constants for DateTime channels
+ public static final LocalDate EPOCH_DAY = LocalDate.ofEpochDay(0);
+ public static final DateTimeFormatter TIME_FORMATER = DateTimeFormatter.ofPattern("HH:mm");
+ public static final LocalTime NULL_LOCAL_TIME = LocalTime.parse(NULL_TIME, TIME_FORMATER);
+
+ @SuppressWarnings("serial")
+ public static final Map<DayOfWeek, String> DAYS = new HashMap<>() {
+ {
+ put(DayOfWeek.MONDAY, "Mon");
+ put(DayOfWeek.TUESDAY, "Tue");
+ put(DayOfWeek.WEDNESDAY, "Wed");
+ put(DayOfWeek.THURSDAY, "Thu");
+ put(DayOfWeek.FRIDAY, "Fri");
+ put(DayOfWeek.SATURDAY, "Sat");
+ put(DayOfWeek.SUNDAY, "Sun");
+ }
+ };
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.utils;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import javax.measure.quantity.Length;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.compat.VehicleAttributes;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.compat.VehicleAttributesContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.compat.VehicleMessages;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.CBSMessage;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.CCMMessage;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.Position;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatus;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatusContainer;
+import org.openhab.core.i18n.TimeZoneProvider;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.ImperialUnits;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link Converter} Conversion Helpers
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class Converter {
+ public static final Logger LOGGER = LoggerFactory.getLogger(Converter.class);
+
+ public static final DateTimeFormatter SERVICE_DATE_INPUT_PATTERN = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+ public static final DateTimeFormatter SERVICE_DATE_OUTPUT_PATTERN = DateTimeFormatter.ofPattern("MMM yyyy");
+
+ public static final String LOCAL_DATE_INPUT_PATTERN_STRING = "dd.MM.yyyy HH:mm";
+ public static final DateTimeFormatter LOCAL_DATE_INPUT_PATTERN = DateTimeFormatter
+ .ofPattern(LOCAL_DATE_INPUT_PATTERN_STRING);
+
+ 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);
+
+ public static final String DATE_INPUT_ZONE_PATTERN_STRING = "yyyy-MM-dd'T'HH:mm:ssZ";
+ public static final DateTimeFormatter DATE_INPUT_ZONE_PATTERN = DateTimeFormatter
+ .ofPattern(DATE_INPUT_ZONE_PATTERN_STRING);
+
+ public static final DateTimeFormatter DATE_OUTPUT_PATTERN = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
+
+ private static final Gson GSON = new Gson();
+ private static final double SCALE = 10;
+ public static final double MILES_TO_KM_RATIO = 1.60934;
+ private static final String SPLIT_HYPHEN = "-";
+ private static final String SPLIT_BRACKET = "\\(";
+
+ public static Optional<TimeZoneProvider> timeZoneProvider = Optional.empty();
+
+ public static double round(double value) {
+ return Math.round(value * SCALE) / SCALE;
+ }
+
+ public static String getLocalDateTimeWithoutOffest(@Nullable String input) {
+ if (input == null) {
+ return Constants.NULL_DATE;
+ }
+ LocalDateTime ldt;
+ if (input.contains(Constants.PLUS)) {
+ ldt = LocalDateTime.parse(input, Converter.DATE_INPUT_ZONE_PATTERN);
+ } else {
+ ldt = LocalDateTime.parse(input, Converter.DATE_INPUT_PATTERN);
+ }
+ return ldt.format(Converter.DATE_INPUT_PATTERN);
+ }
+
+ public static String getLocalDateTime(@Nullable String input) {
+ if (input == null) {
+ return Constants.NULL_DATE;
+ }
+
+ LocalDateTime ldt;
+ if (input.contains(Constants.PLUS)) {
+ ldt = LocalDateTime.parse(input, Converter.DATE_INPUT_ZONE_PATTERN);
+ } else {
+ try {
+ ldt = LocalDateTime.parse(input, Converter.DATE_INPUT_PATTERN);
+ } catch (DateTimeParseException dtpe) {
+ ldt = LocalDateTime.parse(input, Converter.LOCAL_DATE_INPUT_PATTERN);
+ }
+ }
+ ZonedDateTime zdtUTC = ldt.atZone(ZoneId.of("UTC"));
+ ZonedDateTime zdtLZ;
+ zdtLZ = zdtUTC.withZoneSameInstant(ZoneId.systemDefault());
+ if (timeZoneProvider.isPresent()) {
+ zdtLZ = zdtUTC.withZoneSameInstant(timeZoneProvider.get().getTimeZone());
+ } else {
+ zdtLZ = zdtUTC.withZoneSameInstant(ZoneId.systemDefault());
+ }
+ return zdtLZ.format(Converter.DATE_INPUT_PATTERN);
+ }
+
+ public static void setTimeZoneProvider(TimeZoneProvider tzp) {
+ timeZoneProvider = Optional.of(tzp);
+ }
+
+ public static String toTitleCase(@Nullable String input) {
+ if (input == null) {
+ return toTitleCase(Constants.UNDEF);
+ } else {
+ String lower = input.replaceAll(Constants.UNDERLINE, Constants.SPACE).toLowerCase();
+ String converted = toTitleCase(lower, Constants.SPACE);
+ converted = toTitleCase(converted, SPLIT_HYPHEN);
+ converted = toTitleCase(converted, SPLIT_BRACKET);
+ return converted;
+ }
+ }
+
+ private static String toTitleCase(String input, String splitter) {
+ String[] arr = input.split(splitter);
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < arr.length; i++) {
+ if (i > 0) {
+ sb.append(splitter.replaceAll("\\\\", Constants.EMPTY));
+ }
+ sb.append(Character.toUpperCase(arr[i].charAt(0))).append(arr[i].substring(1));
+ }
+ return sb.toString().trim();
+ }
+
+ public static String capitalizeFirst(String str) {
+ return str.substring(0, 1).toUpperCase() + str.substring(1);
+ }
+
+ public static Gson getGson() {
+ return GSON;
+ }
+
+ /**
+ * Measure distance between 2 coordinates
+ *
+ * @param sourceLatitude
+ * @param sourceLongitude
+ * @param destinationLatitude
+ * @param destinationLongitude
+ * @return distance
+ */
+ public static double measureDistance(double sourceLatitude, double sourceLongitude, double destinationLatitude,
+ double destinationLongitude) {
+ double earthRadius = 6378.137; // Radius of earth in KM
+ double dLat = destinationLatitude * Math.PI / 180 - sourceLatitude * Math.PI / 180;
+ double dLon = destinationLongitude * Math.PI / 180 - sourceLongitude * Math.PI / 180;
+ double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(sourceLatitude * Math.PI / 180)
+ * Math.cos(destinationLatitude * Math.PI / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
+ double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+ return earthRadius * c;
+ }
+
+ /**
+ * 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
+ *
+ * In future it's foreseen to replace this with BMW RangeMap Service which isn't running at the moment.
+ *
+ * @param range
+ * @return mapping from air-line distance to "real road" distance
+ */
+ public static double guessRangeRadius(double range) {
+ return range * 0.8;
+ }
+
+ public static State getMiles(QuantityType<Length> qtLength) {
+ if (qtLength.intValue() == -1) {
+ return UnDefType.UNDEF;
+ }
+ QuantityType<Length> qt = qtLength.toUnit(ImperialUnits.MILE);
+ if (qt != null) {
+ return qt;
+ } else {
+ LOGGER.debug("Cannot convert {} to miles", qt);
+ return UnDefType.UNDEF;
+ }
+ }
+
+ public static int getIndex(String fullString) {
+ int index = -1;
+ try {
+ index = Integer.parseInt(fullString);
+ } catch (NumberFormatException nfe) {
+ }
+ return index;
+ }
+
+ public static String transformLegacyStatus(@Nullable VehicleAttributesContainer vac) {
+ if (vac != null) {
+ if (vac.attributesMap != null && vac.vehicleMessages != null) {
+ VehicleAttributes attributesMap = vac.attributesMap;
+ VehicleMessages vehicleMessages = vac.vehicleMessages;
+ // create target objects
+ VehicleStatusContainer vsc = new VehicleStatusContainer();
+ VehicleStatus vs = new VehicleStatus();
+ vsc.vehicleStatus = vs;
+
+ vs.mileage = attributesMap.mileage;
+ vs.doorLockState = attributesMap.doorLockState;
+
+ vs.doorDriverFront = attributesMap.doorDriverFront;
+ vs.doorDriverRear = attributesMap.doorDriverRear;
+ vs.doorPassengerFront = attributesMap.doorPassengerFront;
+ vs.doorPassengerRear = attributesMap.doorPassengerRear;
+ vs.hood = attributesMap.hoodState;
+ vs.trunk = attributesMap.trunkState;
+
+ vs.windowDriverFront = attributesMap.winDriverFront;
+ vs.windowDriverRear = attributesMap.winDriverRear;
+ vs.windowPassengerFront = attributesMap.winPassengerFront;
+ vs.windowPassengerRear = attributesMap.winPassengerRear;
+ vs.sunroof = attributesMap.sunroofState;
+
+ vs.remainingFuel = attributesMap.remainingFuel;
+ vs.remainingRangeElectric = attributesMap.beRemainingRangeElectricKm;
+ vs.remainingRangeElectricMls = attributesMap.beRemainingRangeElectricMile;
+ vs.remainingRangeFuel = attributesMap.beRemainingRangeFuelKm;
+ vs.remainingRangeFuelMls = attributesMap.beRemainingRangeFuelMile;
+ vs.remainingFuel = attributesMap.remainingFuel;
+ vs.chargingLevelHv = attributesMap.chargingLevelHv;
+ vs.chargingStatus = attributesMap.chargingHVStatus;
+ vs.lastChargingEndReason = attributesMap.lastChargingEndReason;
+
+ vs.updateTime = attributesMap.updateTimeConverted;
+
+ Position p = new Position();
+ p.lat = attributesMap.gpsLat;
+ p.lon = attributesMap.gpsLon;
+ p.heading = attributesMap.heading;
+ vs.position = p;
+
+ final List<CCMMessage> ccml = new ArrayList<CCMMessage>();
+ if (vehicleMessages != null) {
+ if (vehicleMessages.ccmMessages != null) {
+ vehicleMessages.ccmMessages.forEach(entry -> {
+ CCMMessage ccmM = new CCMMessage();
+ ccmM.ccmDescriptionShort = entry.text;
+ ccmM.ccmDescriptionLong = Constants.HYPHEN;
+ ccmM.ccmMileage = entry.unitOfLengthRemaining;
+ ccml.add(ccmM);
+ });
+ }
+ }
+ vs.checkControlMessages = ccml;
+
+ final List<CBSMessage> cbsl = new ArrayList<CBSMessage>();
+ if (vehicleMessages != null) {
+ if (vehicleMessages.cbsMessages != null) {
+ vehicleMessages.cbsMessages.forEach(entry -> {
+ CBSMessage cbsm = new CBSMessage();
+ cbsm.cbsType = entry.text;
+ cbsm.cbsDescription = entry.description;
+ cbsm.cbsDueDate = entry.date;
+ cbsm.cbsRemainingMileage = entry.unitOfLengthRemaining;
+ cbsl.add(cbsm);
+ });
+ }
+ }
+ vs.cbsData = cbsl;
+ return Converter.getGson().toJson(vsc);
+ }
+ }
+ return Constants.EMPTY_JSON;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.utils;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link HTTPConstants} class contains fields mapping thing configuration parameters.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class HTTPConstants {
+ public static final int HTTP_TIMEOUT_SEC = 10;
+
+ public static final String AUTH_HTTP_CLIENT_NAME = "AuthHttpClient";
+ public static final String CONTENT_TYPE_URL_ENCODED = "application/x-www-form-urlencoded";
+ public static final String CONTENT_TYPE_JSON = "application/json";
+ public static final String KEEP_ALIVE = "Keep-Alive";
+ public static final String CLIENT_ID = "client_id";
+ public static final String RESPONSE_TYPE = "response_type";
+ public static final String TOKEN = "token";
+ public static final String REDIRECT_URI = "redirect_uri";
+ public static final String SCOPE = "scope";
+ public static final String CREDENTIALS = "Credentials";
+ public static final String USERNAME = "username";
+ public static final String PASSWORD = "password";
+ public static final String CONTENT_LENGTH = "Content-Length";
+
+ public static final String ACCESS_TOKEN = "access_token";
+ public static final String TOKEN_TYPE = "token_type";
+ public static final String EXPIRES_IN = "expires_in";
+ public static final String CHUNKED = "chunked";
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.utils;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link ImageProperties} Properties of current Vehicle Image
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class ImageProperties {
+ public static final int RETRY_COUNTER = 5;
+ public int failCounter = 0;
+ public String viewport = Constants.EMPTY;
+ public int size = -1;
+
+ public ImageProperties(String viewport, int size) {
+ this.viewport = viewport;
+ this.size = size;
+ }
+
+ public ImageProperties() {
+ }
+
+ public void failed() {
+ failCounter++;
+ }
+
+ public boolean failLimitReached() {
+ return failCounter > RETRY_COUNTER;
+ }
+
+ @Override
+ public String toString() {
+ return viewport + size;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.utils;
+
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.RemoteServiceHandler.RemoteService;
+import org.openhab.core.types.StateOption;
+
+/**
+ * Helper class for Remote Service Commands
+ *
+ * @author Norbert Truchsess - Initial contribution
+ */
+@NonNullByDefault
+public class RemoteServiceUtils {
+
+ private static final Map<String, RemoteService> COMMAND_SERVICES = Stream.of(RemoteService.values())
+ .collect(Collectors.toUnmodifiableMap(RemoteService::getCommand, service -> service));
+
+ private static final Set<RemoteService> ELECTRIC_SERVICES = EnumSet.of(RemoteService.CHARGE_NOW,
+ RemoteService.CHARGING_CONTROL);
+
+ public static Optional<RemoteService> getRemoteService(final String command) {
+ return Optional.ofNullable(COMMAND_SERVICES.get(command));
+ }
+
+ public static List<StateOption> getOptions(final boolean isElectric) {
+ return Stream.of(RemoteService.values())
+ .filter(service -> isElectric ? true : !ELECTRIC_SERVICES.contains(service))
+ .map(service -> new StateOption(service.getCommand(), service.getLabel()))
+ .collect(Collectors.toUnmodifiableList());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.utils;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.utils.Constants.*;
+
+import java.lang.reflect.Field;
+import java.time.LocalDateTime;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.CBSMessage;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatus;
+
+/**
+ * The {@link VehicleStatusUtils} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class VehicleStatusUtils {
+
+ public static String getNextServiceDate(VehicleStatus vStatus) {
+ if (vStatus.cbsData == null) {
+ return Constants.NULL_DATE;
+ }
+ if (vStatus.cbsData.isEmpty()) {
+ return Constants.NULL_DATE;
+ } else {
+ LocalDateTime farFuture = LocalDateTime.now().plusYears(100);
+ LocalDateTime serviceDate = farFuture;
+ for (int i = 0; i < vStatus.cbsData.size(); i++) {
+ CBSMessage entry = vStatus.cbsData.get(i);
+ if (entry.cbsDueDate != null) {
+ LocalDateTime d = LocalDateTime.parse(entry.cbsDueDate + Constants.UTC_APPENDIX);
+ if (d.isBefore(serviceDate)) {
+ serviceDate = d;
+ }
+ }
+ }
+ if (serviceDate.equals(farFuture)) {
+ return Constants.NULL_DATE;
+ } else {
+ return serviceDate.format(Converter.DATE_INPUT_PATTERN);
+ }
+ }
+ }
+
+ public static int getNextServiceMileage(VehicleStatus vStatus) {
+ if (vStatus.cbsData == null) {
+ return -1;
+ }
+ if (vStatus.cbsData.isEmpty()) {
+ return -1;
+ } else {
+ int serviceMileage = Integer.MAX_VALUE;
+ for (int i = 0; i < vStatus.cbsData.size(); i++) {
+ CBSMessage entry = vStatus.cbsData.get(i);
+ if (entry.cbsRemainingMileage != -1) {
+ if (entry.cbsRemainingMileage < serviceMileage) {
+ serviceMileage = entry.cbsRemainingMileage;
+ }
+ }
+ }
+ if (serviceMileage != Integer.MAX_VALUE) {
+ return serviceMileage;
+ } else {
+ return -1;
+ }
+ }
+ }
+
+ public static String checkControlActive(VehicleStatus vStatus) {
+ if (vStatus.checkControlMessages == null) {
+ return UNDEF;
+ }
+ if (vStatus.checkControlMessages.isEmpty()) {
+ return NOT_ACTIVE;
+ } else {
+ return ACTIVE;
+ }
+ }
+
+ public static String getUpdateTime(VehicleStatus vStatus) {
+ if (vStatus.internalDataTimeUTC != null) {
+ return vStatus.internalDataTimeUTC;
+ } else if (vStatus.updateTime != null) {
+ return vStatus.updateTime;
+ } else {
+ return Constants.NULL_DATE;
+ }
+ }
+
+ /**
+ * Check for certain Windows or Doors DTO object the "Closed" Status
+ * INVALID values will be ignored
+ *
+ * @param dto
+ * @return Closed if all "Closed", "Open" otherwise
+ */
+ public static String checkClosed(Object dto) {
+ String overallState = Constants.UNDEF;
+ for (Field field : dto.getClass().getDeclaredFields()) {
+ try {
+ Object d = field.get(dto);
+ if (d != null) {
+ String state = d.toString();
+ // skip invalid entries - they don't apply to this Vehicle
+ if (!state.equalsIgnoreCase(INVALID)) {
+ if (state.equalsIgnoreCase(OPEN)) {
+ overallState = OPEN;
+ // stop searching for more open items - overall Doors / Windows are OPEN
+ break;
+ } else if (state.equalsIgnoreCase(INTERMEDIATE)) {
+ if (!overallState.equalsIgnoreCase(OPEN)) {
+ overallState = INTERMEDIATE;
+ // continue searching - maybe another Door / Window is OPEN
+ }
+ } else if (state.equalsIgnoreCase(CLOSED)) {
+ // at least one valid object needs to be found in order to reply "CLOSED"
+ if (overallState.equalsIgnoreCase(UNDEF)) {
+ overallState = CLOSED;
+ }
+ }
+ }
+ }
+ } catch (IllegalArgumentException | IllegalAccessException e) {
+ }
+ }
+ return Converter.toTitleCase(overallState);
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="bmwconnecteddrive" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+ <name>BMW ConnectedDrive</name>
+ <description>Provides access to your Vehicle Data via BMW Connected Drive Portal</description>
+
+</binding:binding>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+ <config-description uri="thing-type:bmwconnecteddrive:bridge">
+ <parameter name="userName" type="text" required="true">
+ <label>Username</label>
+ <description>BMW Connected Drive Username</description>
+ </parameter>
+ <parameter name="password" type="text" required="true">
+ <label>Password</label>
+ <description>BMW Connected Drive Password</description>
+ <context>password</context>
+ </parameter>
+ <parameter name="region" type="text" required="true">
+ <label>Region</label>
+ <description>Select Region in order to connect to the appropriate BMW Server</description>
+ <options>
+ <option value="NORTH_AMERICA">North America</option>
+ <option value="CHINA">China</option>
+ <option value="ROW">Rest of the World</option>
+ </options>
+ <default>ROW</default>
+ </parameter>
+ </config-description>
+</config-description:config-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+ <config-description uri="thing-type:bmwconnecteddrive:vehicle">
+ <parameter name="vin" type="text" required="true">
+ <label>Vehicle Identification Number (VIN)</label>
+ <description>Unique VIN given by BMW</description>
+ </parameter>
+ <parameter name="refreshInterval" type="integer" min="1" unit="min" required="true">
+ <label>Refresh Interval</label>
+ <description>Data Refresh Rate for your Vehicle data</description>
+ <default>5</default>
+ </parameter>
+ <parameter name="units" type="text">
+ <label>Unit Selection</label>
+ <description>Units are selected via auto-detection but you can overrule</description>
+ <options>
+ <option value="AUTODETECT">Auto Detect</option>
+ <option value="METRIC">Metric</option>
+ <option value="IMPERIAL">Imperial</option>
+ </options>
+ <default>AUTODETECT</default>
+ </parameter>
+ <parameter name="imageSize" type="integer">
+ <label>Image Picture Size</label>
+ <description>Vehicle Image size for width and length</description>
+ <default>1024</default>
+ </parameter>
+ <parameter name="imageViewport" type="text">
+ <label>Image Viewport</label>
+ <description>Viewport for Vehicle Image</description>
+ <options>
+ <option value="FRONT">Front View</option>
+ <option value="REAR">Rear View</option>
+ <option value="SIDE">Side View</option>
+ <option value="DASHBOARD">Dashboard View</option>
+ <option value="DRIVERDOOR">Driver Door View</option>
+ </options>
+ <default>FRONT</default>
+ </parameter>
+ </config-description>
+</config-description:config-descriptions>
--- /dev/null
+# Binding
+binding.bmwconnecteddrive.name = BMW ConnectedDrive
+binding.bmwconnecteddrive.description = Zeigt die Fahrzeugdaten über das BMW ConnectedDrive Portal
+
+# bridge types
+thing-type.bmwconnecteddrive.account.label = BMW ConnectedDrive Benutzerkonto
+thing-type.bmwconnecteddrive.account.description = Zugriff auf das BMW ConnectedDrive Portal für einen Benutzer
+thing-type.config.bmwconnecteddrive.account.userName = Benutzername für das ConnectedDrive Portal
+thing-type.config.bmwconnecteddrive.account.password = Passwort für das ConnectedDrive Portal
+thing-type.config.bmwconnecteddrive.account.region = Auswahl Ihrer Region zur Verbindung mit dem korrekten BMW Server
+thing-type.config.bmwconnecteddrive.account.region.option.NORTH_AMERICA = Nordamerika
+thing-type.config.bmwconnecteddrive.account.region.option.CHINA = China
+thing-type.config.bmwconnecteddrive.account.region.option.ROW = Rest der Welt
+
+# thing types
+thing-type.bmwconnecteddrive.bev_rex.label = Elektrofahrzeug mit REX
+thing-type.bmwconnecteddrive.bev_rex.description = Elektrofahrzeug mit Range Extender (bev_rex)
+thing-type.config.bmwconnecteddrive.bev_rex.vin.label = Fahrzeug Identifikationsnummer (VIN)
+thing-type.config.bmwconnecteddrive.bev_rex.vin.description = VIN des Fahrzeugs
+thing-type.config.bmwconnecteddrive.bev_rex.refreshInterval.label = Datenaktualisierung in Minuten
+thing-type.config.bmwconnecteddrive.bev_rex.refreshInterval.description = Rate der Datenaktualisierung Ihres Fahrzeugs
+thing-type.config.bmwconnecteddrive.bev_rex.units.label = Einheiten
+thing-type.config.bmwconnecteddrive.bev_rex.units.description = Automatische oder direkte Auswahl der Einheiten
+thing-type.config.bmwconnecteddrive.bev_rex.units.option.AUTODETECT = Automatische Auswahl
+thing-type.config.bmwconnecteddrive.bev_rex.units.option.IMPERIAL = Angloamerikanisches System
+thing-type.config.bmwconnecteddrive.bev_rex.units.option.METRIC = Metrisches System
+thing-type.config.bmwconnecteddrive.bev_rex.imageSize.label = Bildgröße
+thing-type.config.bmwconnecteddrive.bev_rex.imageSize.description = Bildgröße des Fahrzeugs für Länge und Breite
+thing-type.config.bmwconnecteddrive.bev_rex.imageViewport.label = Bild Ansicht
+thing-type.config.bmwconnecteddrive.bev_rex.imageViewport.description = Ansicht des Fahrzeugs
+thing-type.config.bmwconnecteddrive.bev_rex.imageViewport.option.FRONT = Vorderansicht
+thing-type.config.bmwconnecteddrive.bev_rex.imageViewport.option.REAR = Rückansicht
+thing-type.config.bmwconnecteddrive.bev_rex.imageViewport.option.SIDE = Seitenansicht
+thing-type.config.bmwconnecteddrive.bev_rex.imageViewport.option.DASHBOARD = Innenansicht Armaturen
+thing-type.config.bmwconnecteddrive.bev_rex.imageViewport.option.DRIVERDOOR = Seitenansicht Fahrertür
+
+thing-type.bmwconnecteddrive.bev.label = Elektrofahrzeug
+thing-type.bmwconnecteddrive.bev.description = Batterieelektrisches Fahrzeug (bev)
+thing-type.config.bmwconnecteddrive.bev.vin.label = Fahrzeug Identifikationsnummer (VIN)
+thing-type.config.bmwconnecteddrive.bev.vin.description = VIN des Fahrzeugs
+thing-type.config.bmwconnecteddrive.bev.refreshInterval.label = Datenaktualisierung in Minuten
+thing-type.config.bmwconnecteddrive.bev.refreshInterval.description = Rate der Datenaktualisierung Ihres Fahrzeugs
+thing-type.config.bmwconnecteddrive.bev.units.label = Einheiten
+thing-type.config.bmwconnecteddrive.bev.units.description = Automatische oder direkte Auswahl der Einheiten
+thing-type.config.bmwconnecteddrive.bev.units.option.AUTODETECT = Automatische Auswahl
+thing-type.config.bmwconnecteddrive.bev.units.option.IMPERIAL = Angloamerikanisches System
+thing-type.config.bmwconnecteddrive.bev.units.option.METRIC = Metrisches System
+thing-type.config.bmwconnecteddrive.bev.imageSize.label = Bildgröße
+thing-type.config.bmwconnecteddrive.bev.imageSize.description = Bildgröße des Fahrzeugs für Länge und Breite
+thing-type.config.bmwconnecteddrive.bev.imageViewport.label = Bild Ansicht
+thing-type.config.bmwconnecteddrive.bev.imageViewport.description = Ansicht des Fahrzeugs
+thing-type.config.bmwconnecteddrive.bev.imageViewport.option.FRONT = Vorderansicht
+thing-type.config.bmwconnecteddrive.bev.imageViewport.option.REAR = Rückansicht
+thing-type.config.bmwconnecteddrive.bev.imageViewport.option.SIDE = Seitenansicht
+thing-type.config.bmwconnecteddrive.bev.imageViewport.option.DASHBOARD = Innenansicht Armaturen
+thing-type.config.bmwconnecteddrive.bev.imageViewport.option.DRIVERDOOR = Seitenansicht Fahrertür
+
+thing-type.bmwconnecteddrive.phev.label = Plug-in-Hybrid Elektrofahrzeug
+thing-type.bmwconnecteddrive.phev.description = Konventionelles Fahrzeug mit Elektromotor (phev)
+thing-type.config.bmwconnecteddrive.phev.vin.label = Fahrzeug Identifikationsnummer (VIN)
+thing-type.config.bmwconnecteddrive.phev.vin.description = VIN des Fahrzeugs
+thing-type.config.bmwconnecteddrive.phev.refreshInterval.label = Datenaktualisierung in Minuten
+thing-type.config.bmwconnecteddrive.phev.refreshInterval.description = Rate der Datenaktualisierung Ihres Fahrzeugs
+thing-type.config.bmwconnecteddrive.phev.units.label = Einheiten
+thing-type.config.bmwconnecteddrive.phev.units.description = Automatische oder direkte Auswahl der Einheiten
+thing-type.config.bmwconnecteddrive.phev.units.option.AUTODETECT = Automatische Auswahl
+thing-type.config.bmwconnecteddrive.phev.units.option.IMPERIAL = Angloamerikanisches System
+thing-type.config.bmwconnecteddrive.phev.units.option.METRIC = Metrisches System
+thing-type.config.bmwconnecteddrive.phev.imageSize.label = Bildgröße
+thing-type.config.bmwconnecteddrive.phev.imageSize.description = Bildgröße des Fahrzeugs für Länge und Breite
+thing-type.config.bmwconnecteddrive.phev.imageViewport.label = Bild Ansicht
+thing-type.config.bmwconnecteddrive.phev.imageViewport.description = Ansicht des Fahrzeugs
+thing-type.config.bmwconnecteddrive.phev.imageViewport.option.FRONT = Vorderansicht
+thing-type.config.bmwconnecteddrive.phev.imageViewport.option.REAR = Rückansicht
+thing-type.config.bmwconnecteddrive.phev.imageViewport.option.SIDE = Seitenansicht
+thing-type.config.bmwconnecteddrive.phev.imageViewport.option.DASHBOARD = Innenansicht Armaturen
+thing-type.config.bmwconnecteddrive.phev.imageViewport.option.DRIVERDOOR = Seitenansicht Fahrertür
+
+thing-type.bmwconnecteddrive.conv.label = Konventionelles Fahrzeug
+thing-type.bmwconnecteddrive.conv.description = Konventionelles Benzin/Diesel Fahrzeug (conv)
+thing-type.config.bmwconnecteddrive.conv.vin.label = Fahrzeug Identifikationsnummer (VIN)
+thing-type.config.bmwconnecteddrive.conv.vin.description = VIN des Fahrzeugs
+thing-type.config.bmwconnecteddrive.conv.refreshInterval.label = Datenaktualisierung in Minuten
+thing-type.config.bmwconnecteddrive.conv.refreshInterval.description = Rate der Datenaktualisierung Ihres Fahrzeugs
+thing-type.config.bmwconnecteddrive.conv.units.label = Einheiten
+thing-type.config.bmwconnecteddrive.conv.units.description = Automatische oder direkte Auswahl der Einheiten
+thing-type.config.bmwconnecteddrive.conv.units.option.AUTODETECT = Automatische Auswahl
+thing-type.config.bmwconnecteddrive.conv.units.option.IMPERIAL = Angloamerikanisches System
+thing-type.config.bmwconnecteddrive.conv.units.option.METRIC = Metrisches System
+thing-type.config.bmwconnecteddrive.conv.imageSize.label = Bildgröße
+thing-type.config.bmwconnecteddrive.conv.imageSize.description = Bildgröße des Fahrzeugs für Länge und Breite
+thing-type.config.bmwconnecteddrive.conv.imageViewport.label = Bild Ansicht
+thing-type.config.bmwconnecteddrive.conv.imageViewport.description = Ansicht des Fahrzeugs
+thing-type.config.bmwconnecteddrive.conv.imageViewport.option.FRONT = Vorderansicht
+thing-type.config.bmwconnecteddrive.conv.imageViewport.option.REAR = Rückansicht
+thing-type.config.bmwconnecteddrive.conv.imageViewport.option.SIDE = Seitenansicht
+thing-type.config.bmwconnecteddrive.conv.imageViewport.option.DASHBOARD = Innenansicht Armaturen
+thing-type.config.bmwconnecteddrive.conv.imageViewport.option.DRIVERDOOR = Seitenansicht Fahrertür
+
+# Channel Groups
+channel-group-type.bmwconnecteddrive.charge-values.label = Elektrisches Laden
+channel-group-type.bmwconnecteddrive.charge-values.description = Ladezustand und Ladeprofile des Fahrzeugs
+channel-group-type.bmwconnecteddrive.ev-lifetime-values.label = Gesamtlaufzeit Statistik
+channel-group-type.bmwconnecteddrive.ev-lifetime-values.description = Verbrauchswerte und zurückgelegte Strecken über die Fahrzeug-Gesamtlaufzeit
+channel-group-type.bmwconnecteddrive.hybrid-lifetime-values.label = Gesamtlaufzeit Statistik
+channel-group-type.bmwconnecteddrive.hybrid-lifetime-values.description = Verbrauchswerte und zurückgelegte Strecken über die Fahrzeug-Gesamtlaufzeit
+channel-group-type.bmwconnecteddrive.ev-last-trip-values.label = Statistik der letzten Fahrt
+channel-group-type.bmwconnecteddrive.ev-last-trip-values.description = Verbrauchswerte und zurück gelegte Strecke der letzten Fahrt
+channel-group-type.bmwconnecteddrive.hybrid-last-trip-values.label = Statistik der letzten Fahrt
+channel-group-type.bmwconnecteddrive.hybrid-last-trip-values.description = Verbrauchswerte und zurück gelegte Strecke der letzten Fahrt
+channel-group-type.bmwconnecteddrive.ev-range-values.label = Elektrische Reichweite
+channel-group-type.bmwconnecteddrive.ev-range-values.description = Tachostand, Reichweiten und Ladestand des Fahrzeugs
+channel-group-type.bmwconnecteddrive.check-control-values.label = Warnungen
+channel-group-type.bmwconnecteddrive.check-control-values.description = Aktuelle Warungen des Fahrzeugs
+channel-group-type.bmwconnecteddrive.service-values.label = Wartung
+channel-group-type.bmwconnecteddrive.service-values.description = Zukünftige Wartungstermine des Fahrzeugs
+channel-group-type.bmwconnecteddrive.conv-range-values.label = Reichweite
+channel-group-type.bmwconnecteddrive.conv-range-values.description = Tachostand, Reichweite und Tankfüllung des Fahrzeugs
+channel-group-type.bmwconnecteddrive.hybrid-range-values.label = Hybride Reichweite
+channel-group-type.bmwconnecteddrive.hybrid-range-values.description = Tachostand, Reichweite, Ladezustand und Tankfüllung des Fahrzeugs
+channel-group-type.bmwconnecteddrive.image-values.label = Fahrzeug Bild
+channel-group-type.bmwconnecteddrive.image-values.description = Bild des Fahrzeug basierend auf der Ansicht in der Konfiguration
+channel-group-type.bmwconnecteddrive.remote-services.label = Fahrzeug Fernsteuerung
+channel-group-type.bmwconnecteddrive.remote-services.description = Fernsteuerung des Fahrzeugs über den BMW Server wie Türen schließen / öffnen, Klimasteuerung und mehr
+channel-group-type.bmwconnecteddrive.vehicle-status.label = Fahrzeug Zustand
+channel-group-type.bmwconnecteddrive.vehicle-status.description = Zustand des Fahrzeugs über Türen, Fenster, abgeschlossen, anstehende Wartung und aktive Warnungen
+channel-group-type.bmwconnecteddrive.ev-vehicle-status.label = Fahrzeug Zustand
+channel-group-type.bmwconnecteddrive.ev-vehicle-status.description = Zustand des Fahrzeugs über Türen, Fenster, abgeschlossen, anstehende Wartung und aktive Warnungen
+channel-group-type.bmwconnecteddrive.location-values.label = Fahrzeug Standort
+channel-group-type.bmwconnecteddrive.location-values.description = Koordinaten und Ausrichtung des Fahrzeugs
+channel-group-type.bmwconnecteddrive.destination-values.label = Ziele
+channel-group-type.bmwconnecteddrive.destination-values.description = Zeigt die gespeicherten Ziele des Fahrzeugs
+channel-group-type.bmwconnecteddrive.troubleshoot-control.label = Fehlerbehebung
+channel-group-type.bmwconnecteddrive.troubleshoot-control.description = Generiert Daten zur Fehlerbehebung eines Problems
+channel-group-type.bmwconnecteddrive.door-values.label = Details aller Türen
+channel-group-type.bmwconnecteddrive.door-values.description = Zeigt die Details der Türen und Fenster des Fahrzeugs
+
+# Channel Types
+channel-type.bmwconnecteddrive.doors-channel.label = Gesamtzustand der Türen
+channel-type.bmwconnecteddrive.windows-channel.label = Gesamtzustand der Fenster
+channel-type.bmwconnecteddrive.lock-channel.label = Fahrzeug Abgeschlossen
+channel-type.bmwconnecteddrive.next-service-date-channel.label = Nächster Service Termin
+channel-type.bmwconnecteddrive.next-service-mileage-channel.label = Nächster Service in Kilometern
+channel-type.bmwconnecteddrive.check-control-channel.label = Warnung Aktiv
+channel-type.bmwconnecteddrive.charging-status-channel.label = Ladezustand
+channel-type.bmwconnecteddrive.charging-remaining-channel.label = Verbleibende Ladezeit
+channel-type.bmwconnecteddrive.last-update-channel.label = Letzte Aktualisierung
+
+channel-type.bmwconnecteddrive.driver-front-channel.label = Fahrertür
+channel-type.bmwconnecteddrive.driver-rear-channel.label = Fahrertür Hinten
+channel-type.bmwconnecteddrive.passenger-front-channel.label = Beifahrertür
+channel-type.bmwconnecteddrive.passenger-rear-channel.label = Beifahrertür Hinten
+channel-type.bmwconnecteddrive.hood-channel.label = Frontklappe
+channel-type.bmwconnecteddrive.trunk-channel.label = Heckklappe
+channel-type.bmwconnecteddrive.window-driver-front-channel.label = Fahrertür Fenster
+channel-type.bmwconnecteddrive.window-driver-rear-channel.label = Fahrertür Hinten Fenster
+channel-type.bmwconnecteddrive.window-passenger-front-channel.label = Beifahrertür Fenster
+channel-type.bmwconnecteddrive.window-passenger-rear-channel.label = Beifahrertür Hinten Fenster
+channel-type.bmwconnecteddrive.window-rear-channel.label = Heckfenster
+channel-type.bmwconnecteddrive.sunroof-channel.label = Schiebedach
+
+channel-type.bmwconnecteddrive.mileage-channel.label = Tachostand
+channel-type.bmwconnecteddrive.range-hybrid-channel.label = Hybride Reichweite
+channel-type.bmwconnecteddrive.range-electric-channel.label = Elektrische Reichweite
+channel-type.bmwconnecteddrive.soc-channel.label = Batterie Ladestand
+channel-type.bmwconnecteddrive.range-fuel-channel.label = Verbrenner Reichweite
+channel-type.bmwconnecteddrive.remaining-fuel-channel.label = Tankstand
+channel-type.bmwconnecteddrive.range-radius-electric-channel.label = Elektrischer Reichweiten-Radius
+channel-type.bmwconnecteddrive.range-radius-fuel-channel.label = Verbrenner Reichweiten-Radius
+channel-type.bmwconnecteddrive.range-radius-hybrid-channel.label = Hybrider Reichweiten-Radius
+
+channel-type.bmwconnecteddrive.service-name-channel.label = Service
+channel-type.bmwconnecteddrive.service-details-channel.label = Service Details
+channel-type.bmwconnecteddrive.service-date-channel.label = Service Termin
+channel-type.bmwconnecteddrive.service-mileage-channel.label = Service in Kilometern
+
+channel-type.bmwconnecteddrive.checkcontrol-name-channel.label = Warnung
+channel-type.bmwconnecteddrive.checkcontrol-details-channel.label = Warnung Details
+channel-type.bmwconnecteddrive.checkcontrol-mileage-channel.label = Warnung bei Kilometer
+
+channel-type.bmwconnecteddrive.profile-climate-channel.label = Klimatisierung bei Abfahrt
+channel-type.bmwconnecteddrive.profile-mode-channel.label = Ladeprofil
+channel-type.bmwconnecteddrive.profile-mode-channel.option.IMMEDIATE_CHARGING = Sofortiges Laden
+channel-type.bmwconnecteddrive.profile-mode-channel.option.DELAYED_CHARGING = Laden im Zeitfenster
+channel-type.bmwconnecteddrive.profile-prefs-channel.label = Ladeprofil Präferenz
+channel-type.bmwconnecteddrive.profile-prefs-channel.option.NO_PRESELECTION = Keine Präferenz
+channel-type.bmwconnecteddrive.profile-prefs-channel.option.Charging Window = Zeitfenster
+channel-type.bmwconnecteddrive.window-start-channel.label = Ladefenster Startzeit
+channel-type.bmwconnecteddrive.window-start-hour-channel.label = Ladefenster Startzeit Stunde
+channel-type.bmwconnecteddrive.window-start-minute-channel.label = Ladefenster Startzeit Minute
+channel-type.bmwconnecteddrive.window-end-channel.label = Ladefenster Endzeit
+channel-type.bmwconnecteddrive.window-end-hour-channel.label = Ladefenster Endzeit Stunde
+channel-type.bmwconnecteddrive.window-end-minute-channel.label = Ladefenster Endzeit Minute
+channel-type.bmwconnecteddrive.timer1-enabled-channel.label = Zeitprofil 1 - Aktiviert
+channel-type.bmwconnecteddrive.timer1-departure-channel.label = Zeitprofil 1 - Abfahrtszeit
+channel-type.bmwconnecteddrive.timer1-departure-hour-channel.label = Zeitprofil 1 - Abfahrtszeit Stunde
+channel-type.bmwconnecteddrive.timer1-departure-minute-channel.label = Zeitprofil 1 - Abfahrtszeit Minute
+channel-type.bmwconnecteddrive.timer1-days-channel.label = Zeitprofil 1 - Tage
+channel-type.bmwconnecteddrive.timer1-day-mon-channel.label = Zeitprofil 1 - Montag
+channel-type.bmwconnecteddrive.timer1-day-tue-channel.label = Zeitprofil 1 - Dienstag
+channel-type.bmwconnecteddrive.timer1-day-wed-channel.label = Zeitprofil 1 - Mittwoch
+channel-type.bmwconnecteddrive.timer1-day-thu-channel.label = Zeitprofil 1 - Donnerstag
+channel-type.bmwconnecteddrive.timer1-day-fri-channel.label = Zeitprofil 1 - Freitag
+channel-type.bmwconnecteddrive.timer1-day-sat-channel.label = Zeitprofil 1 - Samstag
+channel-type.bmwconnecteddrive.timer1-day-sun-channel.label = Zeitprofil 1 - Sonntag
+channel-type.bmwconnecteddrive.timer2-enabled-channel.label = Zeitprofil 2 - Aktiviert
+channel-type.bmwconnecteddrive.timer2-departure-channel.label = Zeitprofil 2 - Abfahrtszeit
+channel-type.bmwconnecteddrive.timer2-departure-hour-channel.label = Zeitprofil 2 - Abfahrtszeit Stunde
+channel-type.bmwconnecteddrive.timer2-departure-minute-channel.label = Zeitprofil 2 - Abfahrtszeit Minute
+channel-type.bmwconnecteddrive.timer2-days-channel.label = Zeitprofil 2 - Tage
+channel-type.bmwconnecteddrive.timer2-day-mon-channel.label = Zeitprofil 2 - Montag
+channel-type.bmwconnecteddrive.timer2-day-tue-channel.label = Zeitprofil 2 - Dienstag
+channel-type.bmwconnecteddrive.timer2-day-wed-channel.label = Zeitprofil 2 - Mittwoch
+channel-type.bmwconnecteddrive.timer2-day-thu-channel.label = Zeitprofil 2 - Donnerstag
+channel-type.bmwconnecteddrive.timer2-day-fri-channel.label = Zeitprofil 2 - Freitag
+channel-type.bmwconnecteddrive.timer2-day-sat-channel.label = Zeitprofil 2 - Samstag
+channel-type.bmwconnecteddrive.timer2-day-sun-channel.label = Zeitprofil 2 - Sonnatg
+channel-type.bmwconnecteddrive.timer3-enabled-channel.label = Zeitprofil 3 - Aktiviert
+channel-type.bmwconnecteddrive.timer3-departure-channel.label = Zeitprofil 3 - Abfahrtszeit
+channel-type.bmwconnecteddrive.timer3-departure-hour-channel.label = Zeitprofil 3 - Abfahrtszeit Stunde
+channel-type.bmwconnecteddrive.timer3-departure-minute-channel.label = Zeitprofil 3 - Abfahrtszeit Minute
+channel-type.bmwconnecteddrive.timer3-days-channel.label = Zeitprofil 3 - Tage
+channel-type.bmwconnecteddrive.timer3-day-mon-channel.label = Zeitprofil 3 - Montag
+channel-type.bmwconnecteddrive.timer3-day-tue-channel.label = Zeitprofil 3 - Dienstag
+channel-type.bmwconnecteddrive.timer3-day-wed-channel.label = Zeitprofil 3 - Mittwoch
+channel-type.bmwconnecteddrive.timer3-day-thu-channel.label = Zeitprofil 3 - Donnerstag
+channel-type.bmwconnecteddrive.timer3-day-fri-channel.label = Zeitprofil 3 - Freitag
+channel-type.bmwconnecteddrive.timer3-day-sat-channel.label = Zeitprofil 3 - Samstag
+channel-type.bmwconnecteddrive.timer3-day-sun-channel.label = Zeitprofil 3 - Sonntag
+channel-type.bmwconnecteddrive.override-departure-channel.label = Einmaliges Zeitprofil - Abfahrtszeit
+channel-type.bmwconnecteddrive.override-departure-hour-channel.label = Einmaliges Zeitprofil - Abfahrtszeit Stunde
+channel-type.bmwconnecteddrive.override-departure-minute-channel.label = Einmaliges Zeitprofil - Abfahrtszeit Minute
+channel-type.bmwconnecteddrive.override-enabled-channel.label = Einmaliges Zeitprofil - Aktiviert
+
+channel-type.bmwconnecteddrive.destination-name-channel.label = Zieladresse
+channel-type.bmwconnecteddrive.destination-gps-channel.label = Zielkoordinaten
+
+channel-type.bmwconnecteddrive.gps-channel.label = Koordinaten
+channel-type.bmwconnecteddrive.heading-channel.label = Ausrichtung
+
+channel-type.bmwconnecteddrive.trip-date-time-channel.label = Datum
+channel-type.bmwconnecteddrive.trip-duration-channel.label = Dauer
+channel-type.bmwconnecteddrive.distance-channel.label = Distanz
+channel-type.bmwconnecteddrive.distance-since-charging-channel.label = Strecke seit Ladung
+channel-type.bmwconnecteddrive.average-consumption-channel.label = Elektrischer Verbrauch
+channel-type.bmwconnecteddrive.average-consumption-channel.description = Elektrischer Durchnittsverbaruch über 100 km/mi
+channel-type.bmwconnecteddrive.average-combined-consumption-channel.label = Kombinierter Verbrauch
+channel-type.bmwconnecteddrive.average-combined-consumption-channel.description = Kombinierter Durchnittsverbaruch in Liter über 100 km/mi
+channel-type.bmwconnecteddrive.average-recuperation-channel.label = Rekuperation Durchschnitt
+channel-type.bmwconnecteddrive.average-recuperation-channel.description = Durchschnittliche Rekuperation über 100 km/mi
+channel-type.bmwconnecteddrive.total-driven-distance-channel.label = Elektrisch gefahrene Distanz
+channel-type.bmwconnecteddrive.single-longest-distance-channel.label = Längste Fahrt mit einer Ladung
+
+channel-type.bmwconnecteddrive.remote-command-channel.label = Kommando Auswahl
+channel-type.bmwconnecteddrive.remote-command-channel.option.light = Lichthupe Ausführen
+channel-type.bmwconnecteddrive.remote-command-channel.option.finder = Fahrzeug Lokalisieren
+channel-type.bmwconnecteddrive.remote-command-channel.option.lock = Fahrzeug Abschließen
+channel-type.bmwconnecteddrive.remote-command-channel.option.unlock = Fahrzug Aufschließen
+channel-type.bmwconnecteddrive.remote-command-channel.option.horn = Hupe Aktivieren
+channel-type.bmwconnecteddrive.remote-command-channel.option.climate = Klimatisierung Ausführen
+channel-type.bmwconnecteddrive.remote-state-channel.label = Ausführungszustand
+
+
+channel-type.bmwconnecteddrive.png-channel.label = Fahrzeug Bild
+channel-type.bmwconnecteddrive.image-view-channel.label = Fahrzeug Ansicht
+channel-type.bmwconnecteddrive.image-size-channel.label = Fahrzeug Bildgröße
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <bridge-type id="account">
+ <label>BMW ConnectedDrive Account</label>
+ <description>Access to BMW ConnectedDrive Portal for a specific user</description>
+ <config-description-ref uri="thing-type:bmwconnecteddrive:bridge"/>
+ </bridge-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-group-type id="charge-values">
+ <label>Electric Charging</label>
+ <description>Charge Profiles of Vehicle</description>
+ <channels>
+ <channel id="profile-climate" typeId="profile-climate-channel"/>
+ <channel id="profile-mode" typeId="profile-mode-channel"/>
+ <channel id="profile-prefs" typeId="profile-prefs-channel"/>
+ <channel id="window-start" typeId="window-start-channel"/>
+ <channel id="window-end" typeId="window-end-channel"/>
+ <channel id="timer1-departure" typeId="timer1-departure-channel"/>
+ <channel id="timer1-days" typeId="timer1-days-channel"/>
+ <channel id="timer1-day-mon" typeId="timer1-day-mon-channel"/>
+ <channel id="timer1-day-tue" typeId="timer1-day-tue-channel"/>
+ <channel id="timer1-day-wed" typeId="timer1-day-wed-channel"/>
+ <channel id="timer1-day-thu" typeId="timer1-day-thu-channel"/>
+ <channel id="timer1-day-fri" typeId="timer1-day-fri-channel"/>
+ <channel id="timer1-day-sat" typeId="timer1-day-sat-channel"/>
+ <channel id="timer1-day-sun" typeId="timer1-day-sun-channel"/>
+ <channel id="timer1-enabled" typeId="timer1-enabled-channel"/>
+ <channel id="timer2-departure" typeId="timer2-departure-channel"/>
+ <channel id="timer2-days" typeId="timer2-days-channel"/>
+ <channel id="timer2-day-mon" typeId="timer2-day-mon-channel"/>
+ <channel id="timer2-day-tue" typeId="timer2-day-tue-channel"/>
+ <channel id="timer2-day-wed" typeId="timer2-day-wed-channel"/>
+ <channel id="timer2-day-thu" typeId="timer2-day-thu-channel"/>
+ <channel id="timer2-day-fri" typeId="timer2-day-fri-channel"/>
+ <channel id="timer2-day-sat" typeId="timer2-day-sat-channel"/>
+ <channel id="timer2-day-sun" typeId="timer2-day-sun-channel"/>
+ <channel id="timer2-enabled" typeId="timer2-enabled-channel"/>
+ <channel id="timer3-departure" typeId="timer3-departure-channel"/>
+ <channel id="timer3-days" typeId="timer3-days-channel"/>
+ <channel id="timer3-day-mon" typeId="timer3-day-mon-channel"/>
+ <channel id="timer3-day-tue" typeId="timer3-day-tue-channel"/>
+ <channel id="timer3-day-wed" typeId="timer3-day-wed-channel"/>
+ <channel id="timer3-day-thu" typeId="timer3-day-thu-channel"/>
+ <channel id="timer3-day-fri" typeId="timer3-day-fri-channel"/>
+ <channel id="timer3-day-sat" typeId="timer3-day-sat-channel"/>
+ <channel id="timer3-day-sun" typeId="timer3-day-sun-channel"/>
+ <channel id="timer3-enabled" typeId="timer3-enabled-channel"/>
+ <channel id="override-departure" typeId="override-departure-channel"/>
+ <channel id="override-enabled" typeId="override-enabled-channel"/>
+ </channels>
+ </channel-group-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-type id="profile-climate-channel">
+ <item-type>Switch</item-type>
+ <label>A/C at Departure Time</label>
+ </channel-type>
+ <channel-type id="profile-mode-channel">
+ <item-type>String</item-type>
+ <label>Charge Mode</label>
+ <description>Mode for selecting immediate or delyed charging</description>
+ <command>
+ <options>
+ <option value="IMMEDIATE_CHARGING">Immediate Charging</option>
+ <option value="DELAYED_CHARGING">Prefer Charging in Charging Window</option>
+ </options>
+ </command>
+ </channel-type>
+ <channel-type id="profile-prefs-channel">
+ <item-type>String</item-type>
+ <label>Charge Preferences</label>
+ <description>Preferences for delayed charging</description>
+ <command>
+ <options>
+ <option value="NO_PRESELECTION">No Preference</option>
+ <option value="CHARGING_WINDOW">Charging Window</option>
+ </options>
+ </command>
+ </channel-type>
+ <channel-type id="window-start-channel">
+ <item-type>DateTime</item-type>
+ <label>Window Start Time</label>
+ <description>Start time of charging window</description>
+ <state pattern="%1$tH:%1$tM" readOnly="false"/>
+ </channel-type>
+ <channel-type id="window-end-channel">
+ <item-type>DateTime</item-type>
+ <label>Window End Time</label>
+ <description>End time of charging window</description>
+ <state pattern="%1$tH:%1$tM" readOnly="false"/>
+ </channel-type>
+ <channel-type id="timer1-departure-channel">
+ <item-type>DateTime</item-type>
+ <label>T1 Departure Time</label>
+ <description>Departure time for regular schedule timer 1</description>
+ <state pattern="%1$tH:%1$tM" readOnly="false"/>
+ </channel-type>
+ <channel-type id="timer1-days-channel">
+ <item-type>String</item-type>
+ <label>T1 Days</label>
+ <description>Days scheduled for timer 1</description>
+ </channel-type>
+ <channel-type id="timer1-day-mon-channel">
+ <item-type>Switch</item-type>
+ <label>T1 Monday</label>
+ <description>Monday scheduled for timer 1</description>
+ </channel-type>
+ <channel-type id="timer1-day-tue-channel">
+ <item-type>Switch</item-type>
+ <label>T1 Tuesday</label>
+ <description>Tuesday scheduled for timer 1</description>
+ </channel-type>
+ <channel-type id="timer1-day-wed-channel">
+ <item-type>Switch</item-type>
+ <label>T1 Wednesday</label>
+ <description>Wednesday scheduled for timer 1</description>
+ </channel-type>
+ <channel-type id="timer1-day-thu-channel">
+ <item-type>Switch</item-type>
+ <label>T1 Thursday</label>
+ <description>Thursday scheduled for timer 1</description>
+ </channel-type>
+ <channel-type id="timer1-day-fri-channel">
+ <item-type>Switch</item-type>
+ <label>T1 Friday</label>
+ <description>Friday scheduled for timer 1</description>
+ </channel-type>
+ <channel-type id="timer1-day-sat-channel">
+ <item-type>Switch</item-type>
+ <label>T1 Saturday</label>
+ <description>Saturday scheduled for timer 1</description>
+ </channel-type>
+ <channel-type id="timer1-day-sun-channel">
+ <item-type>Switch</item-type>
+ <label>T1 Sunday</label>
+ <description>Sunday scheduled for timer 1</description>
+ </channel-type>
+ <channel-type id="timer1-enabled-channel">
+ <item-type>Switch</item-type>
+ <label>T1 Enabled</label>
+ <description>Timer 1 enabled</description>
+ </channel-type>
+ <channel-type id="timer2-departure-channel">
+ <item-type>DateTime</item-type>
+ <label>T2 Departure Time</label>
+ <description>Departure time for regular schedule timer 2</description>
+ <state pattern="%1$tH:%1$tM" readOnly="false"/>
+ </channel-type>
+ <channel-type id="timer2-days-channel">
+ <item-type>String</item-type>
+ <label>T2 Days</label>
+ <description>Days scheduled for timer 2</description>
+ </channel-type>
+ <channel-type id="timer2-day-mon-channel">
+ <item-type>Switch</item-type>
+ <label>T2 Monday</label>
+ <description>Monday scheduled for timer 2</description>
+ </channel-type>
+ <channel-type id="timer2-day-tue-channel">
+ <item-type>Switch</item-type>
+ <label>T2 Tuesday</label>
+ <description>Tuesday scheduled for timer 2</description>
+ </channel-type>
+ <channel-type id="timer2-day-wed-channel">
+ <item-type>Switch</item-type>
+ <label>T2 Wednesday</label>
+ <description>Wednesday scheduled for timer 2</description>
+ </channel-type>
+ <channel-type id="timer2-day-thu-channel">
+ <item-type>Switch</item-type>
+ <label>T2 Thursday</label>
+ <description>Thursday scheduled for timer 2</description>
+ </channel-type>
+ <channel-type id="timer2-day-fri-channel">
+ <item-type>Switch</item-type>
+ <label>T2 Friday</label>
+ <description>Friday scheduled for timer 2</description>
+ </channel-type>
+ <channel-type id="timer2-day-sat-channel">
+ <item-type>Switch</item-type>
+ <label>T2 Saturday</label>
+ <description>Saturday scheduled for timer 2</description>
+ </channel-type>
+ <channel-type id="timer2-day-sun-channel">
+ <item-type>Switch</item-type>
+ <label>T2 Sunday</label>
+ <description>Sunday scheduled for timer 2</description>
+ </channel-type>
+ <channel-type id="timer2-enabled-channel">
+ <item-type>Switch</item-type>
+ <label>T2 Enabled</label>
+ <description>Timer 2 enabled</description>
+ </channel-type>
+ <channel-type id="timer3-departure-channel">
+ <item-type>DateTime</item-type>
+ <label>T3 Departure Time</label>
+ <description>Departure time for regular schedule timer 3</description>
+ <state pattern="%1$tH:%1$tM" readOnly="false"/>
+ </channel-type>
+ <channel-type id="timer3-days-channel">
+ <item-type>String</item-type>
+ <label>T3 Days</label>
+ <description>Days scheduled for timer 3</description>
+ </channel-type>
+ <channel-type id="timer3-day-mon-channel">
+ <item-type>Switch</item-type>
+ <label>T3 Monday</label>
+ <description>Monday scheduled for timer 3</description>
+ </channel-type>
+ <channel-type id="timer3-day-tue-channel">
+ <item-type>Switch</item-type>
+ <label>T3 Tuesday</label>
+ <description>Tuesday scheduled for timer 3</description>
+ </channel-type>
+ <channel-type id="timer3-day-wed-channel">
+ <item-type>Switch</item-type>
+ <label>T3 Wednesday</label>
+ <description>Wednesday scheduled for timer 3</description>
+ </channel-type>
+ <channel-type id="timer3-day-thu-channel">
+ <item-type>Switch</item-type>
+ <label>T3 Thursday</label>
+ <description>Thursday scheduled for timer 3</description>
+ </channel-type>
+ <channel-type id="timer3-day-fri-channel">
+ <item-type>Switch</item-type>
+ <label>T3 Friday</label>
+ <description>Friday scheduled for timer 3</description>
+ </channel-type>
+ <channel-type id="timer3-day-sat-channel">
+ <item-type>Switch</item-type>
+ <label>T3 Saturday</label>
+ <description>Saturday scheduled for timer 3</description>
+ </channel-type>
+ <channel-type id="timer3-day-sun-channel">
+ <item-type>Switch</item-type>
+ <label>T3 Sunday</label>
+ <description>Sunday scheduled for timer 3</description>
+ </channel-type>
+ <channel-type id="timer3-enabled-channel">
+ <item-type>Switch</item-type>
+ <label>T3 Enabled</label>
+ <description>Timer 3 enabled</description>
+ </channel-type>
+ <channel-type id="override-departure-channel">
+ <item-type>DateTime</item-type>
+ <label>OT Departure Time</label>
+ <description>Departure time for override timer</description>
+ <state pattern="%1$tH:%1$tM" readOnly="false"/>
+ </channel-type>
+ <channel-type id="override-enabled-channel">
+ <item-type>Switch</item-type>
+ <label>OT Enabled</label>
+ <description>Override timer enabled</description>
+ </channel-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-type id="checkcontrol-name-channel">
+ <item-type>String</item-type>
+ <label>CheckControl Description</label>
+ </channel-type>
+ <channel-type id="checkcontrol-details-channel">
+ <item-type>String</item-type>
+ <label>CheckControl Details</label>
+ </channel-type>
+ <channel-type id="checkcontrol-mileage-channel">
+ <item-type>Number:Length</item-type>
+ <label>Mileage Occurrence</label>
+ <state pattern="%d %unit%"/>
+ </channel-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-group-type id="check-control-values">
+ <label>Check Control Messages</label>
+ <description>Show the current active CheckControl Messages</description>
+ <channels>
+ <channel id="name" typeId="checkcontrol-name-channel"/>
+ <channel id="details" typeId="checkcontrol-details-channel"/>
+ <channel id="mileage" typeId="checkcontrol-mileage-channel"/>
+ </channels>
+ </channel-group-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-group-type id="conv-range-values">
+ <label>Range Data</label>
+ <description>Provides Mileage, remaining range and fuel level values</description>
+ <channels>
+ <channel id="mileage" typeId="mileage-channel"/>
+ <channel id="fuel" typeId="range-fuel-channel"/>
+ <channel id="remaining-fuel" typeId="remaining-fuel-channel"/>
+ <channel id="radius-fuel" typeId="range-radius-fuel-channel"/>
+ </channels>
+ </channel-group-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-type id="destination-name-channel">
+ <item-type>String</item-type>
+ <label>Name</label>
+ </channel-type>
+ <channel-type id="destination-gps-channel">
+ <item-type>Location</item-type>
+ <label>GPS Coordinates</label>
+ </channel-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-group-type id="destination-values">
+ <label>Destination List</label>
+ <description>Shows Your Destinations in a List</description>
+ <channels>
+ <channel id="name" typeId="destination-name-channel"/>
+ <channel id="gps" typeId="destination-gps-channel"/>
+ </channels>
+ </channel-group-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-type id="driver-front-channel">
+ <item-type>String</item-type>
+ <label>Driver Door</label>
+ <state readOnly="true"/>
+ </channel-type>
+ <channel-type id="driver-rear-channel">
+ <item-type>String</item-type>
+ <label>Driver Door Rear</label>
+ <state readOnly="true"/>
+ </channel-type>
+ <channel-type id="passenger-front-channel">
+ <item-type>String</item-type>
+ <label>Passenger Door</label>
+ <state readOnly="true"/>
+ </channel-type>
+ <channel-type id="passenger-rear-channel">
+ <item-type>String</item-type>
+ <label>Passenger Door Rear</label>
+ <state readOnly="true"/>
+ </channel-type>
+ <channel-type id="hood-channel">
+ <item-type>String</item-type>
+ <label>Hood</label>
+ <state readOnly="true"/>
+ </channel-type>
+ <channel-type id="trunk-channel">
+ <item-type>String</item-type>
+ <label>Trunk</label>
+ <state readOnly="true"/>
+ </channel-type>
+ <channel-type id="window-driver-front-channel">
+ <item-type>String</item-type>
+ <label>Driver Window</label>
+ <state readOnly="true"/>
+ </channel-type>
+ <channel-type id="window-driver-rear-channel">
+ <item-type>String</item-type>
+ <label>Driver Rear Window</label>
+ <state readOnly="true"/>
+ </channel-type>
+ <channel-type id="window-passenger-front-channel">
+ <item-type>String</item-type>
+ <label>Passenger Window</label>
+ <state readOnly="true"/>
+ </channel-type>
+ <channel-type id="window-passenger-rear-channel">
+ <item-type>String</item-type>
+ <label>Passenger Rear Window</label>
+ <state readOnly="true"/>
+ </channel-type>
+ <channel-type id="window-rear-channel">
+ <item-type>String</item-type>
+ <label>Rear Window</label>
+ <state readOnly="true"/>
+ </channel-type>
+ <channel-type id="sunroof-channel">
+ <item-type>String</item-type>
+ <label>Sunroof</label>
+ <state readOnly="true"/>
+ </channel-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-group-type id="door-values">
+ <label>Detailed Door Status</label>
+ <description>Detailed Status of all Doors and Windows</description>
+ <channels>
+ <channel id="driver-front" typeId="driver-front-channel"/>
+ <channel id="driver-rear" typeId="driver-rear-channel"/>
+ <channel id="passenger-front" typeId="passenger-front-channel"/>
+ <channel id="passenger-rear" typeId="passenger-rear-channel"/>
+ <channel id="hood" typeId="hood-channel"/>
+ <channel id="trunk" typeId="trunk-channel"/>
+ <channel id="win-driver-front" typeId="window-driver-front-channel"/>
+ <channel id="win-driver-rear" typeId="window-driver-rear-channel"/>
+ <channel id="win-passenger-front" typeId="window-passenger-front-channel"/>
+ <channel id="win-passenger-rear" typeId="window-passenger-rear-channel"/>
+ <channel id="win-rear" typeId="window-rear-channel"/>
+ <channel id="sunroof" typeId="sunroof-channel"/>
+ </channels>
+ </channel-group-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-group-type id="ev-last-trip-values">
+ <label>Last Trip Statistics</label>
+ <description>EV Consumption Values and Distances for the Last Trip</description>
+ <channels>
+ <channel id="date" typeId="trip-date-time-channel"/>
+ <channel id="duration" typeId="trip-duration-channel"/>
+ <channel id="distance" typeId="distance-channel"/>
+ <channel id="distance-since-charging" typeId="distance-since-charging-channel"/>
+ <channel id="avg-consumption" typeId="average-consumption-channel"/>
+ <channel id="avg-recuperation" typeId="average-recuperation-channel"/>
+ </channels>
+ </channel-group-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-group-type id="ev-lifetime-values">
+ <label>Lifetime Statistics</label>
+ <description>Consumption Values and Distances over Lifetime</description>
+ <channels>
+ <channel id="avg-consumption" typeId="average-consumption-channel"/>
+ <channel id="avg-recuperation" typeId="average-recuperation-channel"/>
+ <channel id="total-driven-distance" typeId="total-driven-distance-channel"/>
+ <channel id="single-longest-distance" typeId="single-longest-distance-channel"/>
+ </channels>
+ </channel-group-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-group-type id="ev-range-values">
+ <label>Electric Range Data</label>
+ <description>Provides Mileage, remaining range and charge level values</description>
+ <channels>
+ <channel id="mileage" typeId="mileage-channel"/>
+ <channel id="electric" typeId="range-electric-channel"/>
+ <channel id="soc" typeId="soc-channel"/>
+ <channel id="radius-electric" typeId="range-radius-electric-channel"/>
+ </channels>
+ </channel-group-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-group-type id="ev-vehicle-status">
+ <label>Vehicle Status</label>
+ <description>Provides Status of Doors, Windows, Lock State, Service and Check Control Messages</description>
+ <channels>
+ <channel id="doors" typeId="doors-channel"/>
+ <channel id="windows" typeId="windows-channel"/>
+ <channel id="lock" typeId="lock-channel"/>
+ <channel id="service-date" typeId="next-service-date-channel"/>
+ <channel id="service-mileage" typeId="next-service-mileage-channel"/>
+ <channel id="check-control" typeId="check-control-channel"/>
+ <channel id="charge" typeId="charging-status-channel"/>
+ <channel id="remaining" typeId="charging-remaining-channel"/>
+ <channel id="last-update" typeId="last-update-channel"/>
+ </channels>
+ </channel-group-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-group-type id="hybrid-last-trip-values">
+ <label>Last Trip Statistics</label>
+ <description>Hybrid Consumption Values and Distances for the Last Trip</description>
+ <channels>
+ <channel id="date" typeId="trip-date-time-channel"/>
+ <channel id="duration" typeId="trip-duration-channel"/>
+ <channel id="distance" typeId="distance-channel"/>
+ <channel id="distance-since-charging" typeId="distance-since-charging-channel"/>
+ <channel id="avg-consumption" typeId="average-consumption-channel"/>
+ <channel id="avg-combined-consumption" typeId="average-combined-consumption-channel"/>
+ <channel id="avg-recuperation" typeId="average-recuperation-channel"/>
+ </channels>
+ </channel-group-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-group-type id="hybrid-lifetime-values">
+ <label>Lifetime Statistics</label>
+ <description>Consumption Values and Distances over Lifetime</description>
+ <channels>
+ <channel id="avg-consumption" typeId="average-consumption-channel"/>
+ <channel id="avg-combined-consumption" typeId="average-combined-consumption-channel"/>
+ <channel id="avg-recuperation" typeId="average-recuperation-channel"/>
+ <channel id="total-driven-distance" typeId="total-driven-distance-channel"/>
+ <channel id="single-longest-distance" typeId="single-longest-distance-channel"/>
+ </channels>
+ </channel-group-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-group-type id="hybrid-range-values">
+ <label>Hybrid Range Data</label>
+ <description>Provides Mileage, remaining range and fuel and charge level values</description>
+ <channels>
+ <channel id="mileage" typeId="mileage-channel"/>
+ <channel id="hybrid" typeId="range-hybrid-channel"/>
+ <channel id="electric" typeId="range-electric-channel"/>
+ <channel id="soc" typeId="soc-channel"/>
+ <channel id="fuel" typeId="range-fuel-channel"/>
+ <channel id="remaining-fuel" typeId="remaining-fuel-channel"/>
+ <channel id="radius-electric" typeId="range-radius-electric-channel"/>
+ <channel id="radius-hybrid" typeId="range-radius-hybrid-channel"/>
+ </channels>
+ </channel-group-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-group-type id="image-values">
+ <label>Vehicle Image</label>
+ <description>Provides an Image of your Vehicle</description>
+ <channels>
+ <channel id="png" typeId="png-channel"/>
+ <channel id="size" typeId="image-size-channel"/>
+ <channel id="view" typeId="image-view-channel"/>
+ </channels>
+ </channel-group-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-type id="png-channel">
+ <item-type>Image</item-type>
+ <label>Rendered Vehicle Image</label>
+ <state readOnly="true"/>
+ </channel-type>
+ <channel-type id="image-view-channel">
+ <item-type>String</item-type>
+ <label>Image Viewport</label>
+ <command>
+ <options>
+ <option value="FRONT">Front View</option>
+ <option value="REAR">Rear View</option>
+ <option value="SIDE">Side View</option>
+ <option value="DASHBOARD">Dashboard View</option>
+ <option value="DRIVERDOOR">Driver Door View</option>
+ </options>
+ </command>
+ </channel-type>
+ <channel-type id="image-size-channel">
+ <item-type>Number</item-type>
+ <label>Image Size</label>
+ </channel-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-type id="trip-date-time-channel">
+ <item-type>DateTime</item-type>
+ <label>Date and Time</label>
+ <state pattern="%1$tA, %1$td.%1$tm. %1$tH:%1$tM" readOnly="true"/>
+ </channel-type>
+ <channel-type id="trip-duration-channel">
+ <item-type>Number:Time</item-type>
+ <label>Last Trip Duration</label>
+ <state pattern="%d %unit%" readOnly="true"/>
+ </channel-type>
+ <channel-type id="distance-channel">
+ <item-type>Number:Length</item-type>
+ <label>Last Trip Distance</label>
+ <state pattern="%d %unit%" readOnly="true"/>
+ </channel-type>
+ <channel-type id="distance-since-charging-channel">
+ <item-type>Number:Length</item-type>
+ <label>Distance since Charge</label>
+ <description>Total distance driven since last charging</description>
+ <state pattern="%d %unit%" readOnly="true"/>
+ </channel-type>
+ <channel-type id="average-consumption-channel">
+ <item-type>Number:Energy</item-type>
+ <label>Avg. Power Consumption</label>
+ <description>Average electric power consumption per 100 km/mi</description>
+ <state pattern="%.1f %unit%" readOnly="true"/>
+ </channel-type>
+ <channel-type id="average-combined-consumption-channel">
+ <item-type>Number:Volume</item-type>
+ <label>Avg. Combined Consumption</label>
+ <description>Average combined consumption in liter per 100 km/mi</description>
+ <state pattern="%.1f %unit%" readOnly="true"/>
+ </channel-type>
+ <channel-type id="average-recuperation-channel">
+ <item-type>Number:Energy</item-type>
+ <label>Avg. Recuperation</label>
+ <description>Average electric recuperation per 100 km/mi</description>
+ <state pattern="%.1f %unit%" readOnly="true"/>
+ </channel-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-type id="total-driven-distance-channel">
+ <item-type>Number:Length</item-type>
+ <label>Total Electric Distance</label>
+ <state pattern="%.0f %unit%" readOnly="true"/>
+ </channel-type>
+ <channel-type id="single-longest-distance-channel">
+ <item-type>Number:Length</item-type>
+ <label>Longest 1-Charge Distance</label>
+ <state pattern="%.0f %unit%" readOnly="true"/>
+ </channel-type>
+ <channel-type id="average-consumption-channel">
+ <item-type>Number:Energy</item-type>
+ <label>Avg. Power Consumption</label>
+ <description>Average Combined Consumption electric power consumption per 100 km/mi</description>
+ <state pattern="%.1f %unit%" readOnly="true"/>
+ </channel-type>
+ <channel-type id="average-recuperation-channel">
+ <item-type>Number:Energy</item-type>
+ <label>Avg. Combined Consumption Recuperation</label>
+ <description>Average electric recuperation per 100 km/mi</description>
+ <state pattern="%.1f %unit%" readOnly="true"/>
+ </channel-type>
+ <channel-type id="average-combined-consumption-channel">
+ <item-type>Number:Volume</item-type>
+ <label>Avg. Combined Consumption</label>
+ <description>Average combined consumption in liter per 100 km/mi</description>
+ <state pattern="%.1f %unit%" readOnly="true"/>
+ </channel-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-group-type id="location-values">
+ <label>Vehicle Location</label>
+ <description>Coordinates and Heading of the Vehcile</description>
+ <channels>
+ <channel id="gps" typeId="gps-channel"/>
+ <channel id="heading" typeId="heading-channel"/>
+ </channels>
+ </channel-group-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-type id="gps-channel">
+ <item-type>Location</item-type>
+ <label>GPS Coordinates</label>
+ </channel-type>
+ <channel-type id="heading-channel">
+ <item-type>Number:Angle</item-type>
+ <label>Heading Angle</label>
+ <state pattern="%d %unit%" readOnly="true"/>
+ </channel-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-type id="mileage-channel">
+ <item-type>Number:Length</item-type>
+ <label>Total Distance Driven</label>
+ <state pattern="%d %unit%" readOnly="true"/>
+ </channel-type>
+ <channel-type id="range-electric-channel">
+ <item-type>Number:Length</item-type>
+ <label>Electric Range</label>
+ <state pattern="%d %unit%" readOnly="true"/>
+ </channel-type>
+ <channel-type id="range-fuel-channel">
+ <item-type>Number:Length</item-type>
+ <label>Fuel Range</label>
+ <state pattern="%d %unit%" readOnly="true"/>
+ </channel-type>
+ <channel-type id="range-hybrid-channel">
+ <item-type>Number:Length</item-type>
+ <label>Hybrid Range</label>
+ <state pattern="%d %unit%" readOnly="true"/>
+ </channel-type>
+ <channel-type id="soc-channel">
+ <item-type>Number:Dimensionless</item-type>
+ <label>Battery Charge Level</label>
+ <state pattern="%d %unit%" readOnly="true"/>
+ </channel-type>
+ <channel-type id="remaining-fuel-channel">
+ <item-type>Number:Volume</item-type>
+ <label>Remaining Fuel</label>
+ <state pattern="%d %unit%" readOnly="true"/>
+ </channel-type>
+ <channel-type id="range-radius-electric-channel">
+ <item-type>Number:Length</item-type>
+ <label>Electric Range Radius</label>
+ <state pattern="%.0f %unit%" readOnly="true"/>
+ </channel-type>
+ <channel-type id="range-radius-fuel-channel">
+ <item-type>Number:Length</item-type>
+ <label>Fuel Range Radius</label>
+ <state pattern="%.0f %unit%" readOnly="true"/>
+ </channel-type>
+ <channel-type id="range-radius-hybrid-channel">
+ <item-type>Number:Length</item-type>
+ <label>Hybrid Range Radius</label>
+ <state pattern="%.0f %unit%" readOnly="true"/>
+ </channel-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-group-type id="remote-services">
+ <label>Remote Services</label>
+ <description>Services can be executed via BMW Server like Door lock/unlock, Air Conditioning and more</description>
+ <channels>
+ <channel id="command" typeId="remote-command-channel"/>
+ <channel id="state" typeId="remote-state-channel"/>
+ </channels>
+ </channel-group-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-type id="remote-command-channel">
+ <item-type>String</item-type>
+ <label>Remote Command</label>
+ </channel-type>
+ <channel-type id="remote-state-channel">
+ <item-type>String</item-type>
+ <label>Service Execution State</label>
+ <state readOnly="true"/>
+ </channel-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-type id="service-name-channel">
+ <item-type>String</item-type>
+ <label>Service Name</label>
+ </channel-type>
+ <channel-type id="service-details-channel">
+ <item-type>String</item-type>
+ <label>Service Details</label>
+ </channel-type>
+ <channel-type id="service-date-channel">
+ <item-type>DateTime</item-type>
+ <label>Service Date</label>
+ <state pattern="%1$tb %1$tY"/>
+ </channel-type>
+ <channel-type id="service-mileage-channel">
+ <item-type>Number:Length</item-type>
+ <label>Mileage till Service</label>
+ <state pattern="%d %unit%"/>
+ </channel-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-group-type id="service-values">
+ <label>Vehicle Services</label>
+ <description>All future Service schedules</description>
+ <channels>
+ <channel id="name" typeId="service-name-channel"/>
+ <channel id="details" typeId="service-details-channel"/>
+ <channel id="date" typeId="service-date-channel"/>
+ <channel id="mileage" typeId="service-mileage-channel"/>
+ </channels>
+ </channel-group-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <thing-type id="bev">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="account"/>
+ </supported-bridge-type-refs>
+
+ <label>Electric Vehicle</label>
+ <description>Battery Electric Vehicle (BEV)</description>
+
+ <channel-groups>
+ <channel-group id="status" typeId="ev-vehicle-status"/>
+ <channel-group id="doors" typeId="door-values"/>
+ <channel-group id="range" typeId="ev-range-values"/>
+ <channel-group id="check" typeId="check-control-values"/>
+ <channel-group id="service" typeId="service-values"/>
+ <channel-group id="location" typeId="location-values"/>
+ <channel-group id="remote" typeId="remote-services"/>
+ <channel-group id="charge" typeId="charge-values"/>
+ <channel-group id="last-trip" typeId="ev-last-trip-values"/>
+ <channel-group id="lifetime" typeId="ev-lifetime-values"/>
+ <channel-group id="destination" typeId="destination-values"/>
+ <channel-group id="image" typeId="image-values"/>
+ </channel-groups>
+
+ <representation-property>vin</representation-property>
+
+ <config-description-ref uri="thing-type:bmwconnecteddrive:vehicle"/>
+ </thing-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <thing-type id="bev_rex">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="account"/>
+ </supported-bridge-type-refs>
+
+ <label>Electric Vehicle with REX</label>
+ <description>Battery Electric Vehicle with Range Extender (BEV_REX)</description>
+
+ <channel-groups>
+ <channel-group id="status" typeId="ev-vehicle-status"/>
+ <channel-group id="doors" typeId="door-values"/>
+ <channel-group id="range" typeId="hybrid-range-values"/>
+ <channel-group id="check" typeId="check-control-values"/>
+ <channel-group id="service" typeId="service-values"/>
+ <channel-group id="location" typeId="location-values"/>
+ <channel-group id="remote" typeId="remote-services"/>
+ <channel-group id="charge" typeId="charge-values"/>
+ <channel-group id="last-trip" typeId="hybrid-last-trip-values"/>
+ <channel-group id="lifetime" typeId="hybrid-lifetime-values"/>
+ <channel-group id="destination" typeId="destination-values"/>
+ <channel-group id="image" typeId="image-values"/>
+ </channel-groups>
+
+ <representation-property>vin</representation-property>
+
+ <config-description-ref uri="thing-type:bmwconnecteddrive:vehicle"/>
+ </thing-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <thing-type id="conv">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="account"/>
+ </supported-bridge-type-refs>
+
+ <label>Conventional Vehicle</label>
+ <description>Conventional Fuel Vehicle (CONV)</description>
+
+ <channel-groups>
+ <channel-group id="status" typeId="vehicle-status"/>
+ <channel-group id="doors" typeId="door-values"/>
+ <channel-group id="range" typeId="conv-range-values"/>
+ <channel-group id="check" typeId="check-control-values"/>
+ <channel-group id="service" typeId="service-values"/>
+ <channel-group id="location" typeId="location-values"/>
+ <channel-group id="remote" typeId="remote-services"/>
+ <channel-group id="destination" typeId="destination-values"/>
+ <channel-group id="image" typeId="image-values"/>
+ </channel-groups>
+
+ <representation-property>vin</representation-property>
+
+ <config-description-ref uri="thing-type:bmwconnecteddrive:vehicle"/>
+ </thing-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <thing-type id="phev">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="account"/>
+ </supported-bridge-type-refs>
+
+ <label>Plug-In-Hybrid Electric Vehicle</label>
+ <description>Conventional Fuel Vehicle with supporting Electric Engine (PHEV)</description>
+
+ <channel-groups>
+ <channel-group id="status" typeId="ev-vehicle-status"/>
+ <channel-group id="doors" typeId="door-values"/>
+ <channel-group id="range" typeId="hybrid-range-values"/>
+ <channel-group id="check" typeId="check-control-values"/>
+ <channel-group id="service" typeId="service-values"/>
+ <channel-group id="location" typeId="location-values"/>
+ <channel-group id="remote" typeId="remote-services"/>
+ <channel-group id="charge" typeId="charge-values"/>
+ <channel-group id="last-trip" typeId="hybrid-last-trip-values"/>
+ <channel-group id="lifetime" typeId="hybrid-lifetime-values"/>
+ <channel-group id="destination" typeId="destination-values"/>
+ <channel-group id="image" typeId="image-values"/>
+ </channel-groups>
+
+ <representation-property>vin</representation-property>
+
+ <config-description-ref uri="thing-type:bmwconnecteddrive:vehicle"/>
+ </thing-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-type id="doors-channel">
+ <item-type>String</item-type>
+ <label>Overall Door Status</label>
+ <state readOnly="true"/>
+ </channel-type>
+ <channel-type id="windows-channel">
+ <item-type>String</item-type>
+ <label>Overall Window Status</label>
+ <state readOnly="true"/>
+ </channel-type>
+ <channel-type id="lock-channel">
+ <item-type>String</item-type>
+ <label>Doors Locked</label>
+ <state readOnly="true"/>
+ </channel-type>
+ <channel-type id="next-service-date-channel">
+ <item-type>DateTime</item-type>
+ <label>Next Service Date</label>
+ <state pattern="%1$tb %1$tY" readOnly="true"/>
+ </channel-type>
+ <channel-type id="next-service-mileage-channel">
+ <item-type>Number:Length</item-type>
+ <label>Mileage Till Next Service</label>
+ <state pattern="%d %unit%" readOnly="true"/>
+ </channel-type>
+ <channel-type id="check-control-channel">
+ <item-type>String</item-type>
+ <label>Check Control</label>
+ <state readOnly="true"/>
+ </channel-type>
+ <channel-type id="charging-status-channel">
+ <item-type>String</item-type>
+ <label>Charging Status</label>
+ <state readOnly="true"/>
+ </channel-type>
+ <channel-type id="charging-remaining-channel">
+ <item-type>Number:Time</item-type>
+ <label>Remaining Charging Time</label>
+ <state pattern="%d %unit%" readOnly="true"/>
+ </channel-type>
+ <channel-type id="last-update-channel">
+ <item-type>DateTime</item-type>
+ <label>Last Status Timestamp</label>
+ <state pattern="%1$tA, %1$td.%1$tm. %1$tH:%1$tM" readOnly="true"/>
+ </channel-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <channel-group-type id="vehicle-status">
+ <label>Vehicle Status</label>
+ <description>Provides Status of Doors, Windows, Lock State, Service and Check Control Messages</description>
+ <channels>
+ <channel id="doors" typeId="doors-channel"/>
+ <channel id="windows" typeId="windows-channel"/>
+ <channel id="lock" typeId="lock-channel"/>
+ <channel id="service-date" typeId="next-service-date-channel"/>
+ <channel id="service-mileage" typeId="next-service-mileage-channel"/>
+ <channel id="check-control" typeId="check-control-channel"/>
+ <channel id="last-update" typeId="last-update-channel"/>
+ </channels>
+ </channel-group-type>
+</thing:thing-descriptions>
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.discovery;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.discovery.VehiclesContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.ConnectedDriveBridgeHandler;
+import org.openhab.binding.bmwconnecteddrive.internal.util.FileReader;
+import org.openhab.core.config.discovery.DiscoveryListener;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ThingUID;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link DiscoveryTest} Test Discovery Results
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class DiscoveryTest {
+ private final Logger logger = LoggerFactory.getLogger(DiscoveryTest.class);
+ private static final Gson GSON = new Gson();
+ private static final int DISCOVERY_VEHICLES = 9;
+
+ @Test
+ public void testDiscovery() {
+ String content = FileReader.readFileInString("src/test/resources/webapi/connected-drive-account-info.json");
+ ConnectedDriveBridgeHandler bh = mock(ConnectedDriveBridgeHandler.class);
+ Bridge b = mock(Bridge.class);
+ when(bh.getThing()).thenReturn(b);
+ when(b.getUID()).thenReturn(new ThingUID("bmwconnecteddrive", "account", "abc"));
+ VehicleDiscovery discovery = new VehicleDiscovery();
+ discovery.setThingHandler(bh);
+ DiscoveryListener listener = mock(DiscoveryListener.class);
+ discovery.addDiscoveryListener(listener);
+ VehiclesContainer container = GSON.fromJson(content, VehiclesContainer.class);
+ ArgumentCaptor<DiscoveryResult> discoveries = ArgumentCaptor.forClass(DiscoveryResult.class);
+ ArgumentCaptor<DiscoveryService> services = ArgumentCaptor.forClass(DiscoveryService.class);
+ if (container != null) {
+ discovery.onResponse(container);
+ verify(listener, times(1)).thingDiscovered(services.capture(), discoveries.capture());
+ List<DiscoveryResult> results = discoveries.getAllValues();
+ assertEquals(1, results.size(), "Found Vehicles");
+ DiscoveryResult result = results.get(0);
+ assertEquals("bmwconnecteddrive:bev_rex:abc:MY_REAL_VIN", result.getThingUID().getAsString(), "Thing UID");
+ } else {
+ assertTrue(false);
+ }
+ }
+
+ @Test
+ public void testBimmerDiscovery() {
+ String content = FileReader.readFileInString("src/test/resources/responses/vehicles.json");
+ ConnectedDriveBridgeHandler bh = mock(ConnectedDriveBridgeHandler.class);
+ Bridge b = mock(Bridge.class);
+ when(bh.getThing()).thenReturn(b);
+ when(b.getUID()).thenReturn(new ThingUID("bmwconnecteddrive", "account", "abc"));
+ VehicleDiscovery discovery = new VehicleDiscovery();
+ discovery.setThingHandler(bh);
+ DiscoveryListener listener = mock(DiscoveryListener.class);
+ discovery.addDiscoveryListener(listener);
+ VehiclesContainer container = GSON.fromJson(content, VehiclesContainer.class);
+ ArgumentCaptor<DiscoveryResult> discoveries = ArgumentCaptor.forClass(DiscoveryResult.class);
+ ArgumentCaptor<DiscoveryService> services = ArgumentCaptor.forClass(DiscoveryService.class);
+
+ if (container != null) {
+ discovery.onResponse(container);
+ verify(listener, times(DISCOVERY_VEHICLES)).thingDiscovered(services.capture(), discoveries.capture());
+ List<DiscoveryResult> results = discoveries.getAllValues();
+ results.forEach(entry -> {
+ logger.info("{}", entry.toString());
+ });
+ } else {
+ assertTrue(false);
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.charge.ChargeProfile;
+import org.openhab.binding.bmwconnecteddrive.internal.util.FileReader;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link ChargeProfileTest} Test json responses from ConnectedDrive Portal
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class ChargeProfileTest {
+ private static final Gson GSON = new Gson();
+
+ @Test
+ public void testChargeProfile() {
+ String resource1 = FileReader.readFileInString("src/test/resources/webapi/charging-profile.json");
+ ChargeProfile cp = GSON.fromJson(resource1, ChargeProfile.class);
+ assertNotNull(cp.weeklyPlanner);
+ assertNotNull(cp.weeklyPlanner.timer1);
+ assertFalse(cp.weeklyPlanner.timer1.timerEnabled);
+ assertNotNull(cp.weeklyPlanner.timer1.weekdays);
+ assertEquals(5, cp.weeklyPlanner.timer1.weekdays.size(), "Days");
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.discovery.Vehicle;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.discovery.VehiclesContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.util.FileReader;
+import org.openhab.core.thing.ThingTypeUID;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link ConnectedDriveTest} Test json responses from ConnectedDrive Portal
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class ConnectedDriveTest {
+ private static final Gson GSON = new Gson();
+
+ @Test
+ public void testUserInfo() {
+ String resource1 = FileReader.readFileInString("src/test/resources/webapi/connected-drive-account-info.json");
+ VehiclesContainer container = GSON.fromJson(resource1, VehiclesContainer.class);
+ List<Vehicle> vehicles = container.vehicles;
+ assertEquals(1, vehicles.size(), "Number of Vehicles");
+ Vehicle v = vehicles.get(0);
+ assertEquals("MY_REAL_VIN", v.vin, "VIN");
+ assertEquals("i3 94 (+ REX)", v.model, "Model");
+ assertEquals("BEV_REX", v.driveTrain, "DriveTrain");
+ assertEquals("BMW_I", v.brand, "Brand");
+ assertEquals(2017, v.yearOfConstruction, "Year of Construction");
+ }
+
+ @Test
+ public void testChannelUID() {
+ ThingTypeUID thingTypePHEV = new ThingTypeUID("bmwconnecteddrive", "plugin-hybrid-vehicle");
+ assertEquals("plugin-hybrid-vehicle", thingTypePHEV.getId(), "Vehicle Type");
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.bmwconnecteddrive.internal.util.FileReader;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link DestinationTest} Test json responses from ConnectedDrive Portal
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class DestinationTest {
+ private final Logger logger = LoggerFactory.getLogger(DestinationTest.class);
+ private static final Gson GSON = new Gson();
+
+ @Test
+ public void testDestinations() {
+ String resource1 = FileReader.readFileInString("src/test/resources/webapi/destinations.json");
+ DestinationContainer container = GSON.fromJson(resource1, DestinationContainer.class);
+ List<Destination> destinations = container.destinations;
+ assertEquals(9, destinations.size(), "Number of Vehicles");
+ destinations.forEach(entry -> {
+ logger.info(entry.getAddress());
+ assertFalse(entry.getAddress().contains(Constants.NULL), "No Null contained");
+ });
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.LastTrip;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.LastTripContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.util.FileReader;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link LastTripTest} Test json responses from ConnectedDrive Portal
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class LastTripTest {
+ private static final Gson GSON = new Gson();
+
+ @Test
+ public void testUserInfo() {
+ String content = FileReader.readFileInString("src/test/resources/webapi/last-trip.json");
+ LastTripContainer lt = GSON.fromJson(content, LastTripContainer.class);
+ LastTrip trip = lt.lastTrip;
+ assertNotNull(trip);
+ assertEquals(2.0, trip.totalDistance, 0.01, "Distance Driven");
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.measure.Unit;
+import javax.measure.quantity.Length;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.VehicleType;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.AllTrips;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.AllTripsContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.ImperialUnits;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.types.State;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link LifetimeWrapper} Test json responses from ConnectedDrive Portal
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class LifetimeWrapper {
+ private static final Gson GSON = new Gson();
+ private static final Unit<Length> MILES = ImperialUnits.MILE;
+
+ private AllTrips allTrips;
+ private boolean imperial;
+ private boolean isElectric;
+ private boolean hasFuel;
+ private boolean isHybrid;
+
+ private Map<String, State> specialHandlingMap = new HashMap<String, State>();
+
+ public LifetimeWrapper(String type, boolean imperial, String statusJson) {
+ this.imperial = imperial;
+ hasFuel = type.equals(VehicleType.CONVENTIONAL.toString()) || type.equals(VehicleType.PLUGIN_HYBRID.toString())
+ || type.equals(VehicleType.ELECTRIC_REX.toString());
+ isElectric = type.equals(VehicleType.PLUGIN_HYBRID.toString())
+ || type.equals(VehicleType.ELECTRIC_REX.toString()) || type.equals(VehicleType.ELECTRIC.toString());
+ isHybrid = hasFuel && isElectric;
+ AllTripsContainer container = GSON.fromJson(statusJson, AllTripsContainer.class);
+ assertNotNull(container);
+ assertNotNull(container.allTrips);
+ allTrips = container.allTrips;
+ }
+
+ /**
+ * Test results auctomatically against json values
+ *
+ * @param channels
+ * @param states
+ * @return
+ */
+ public boolean checkResults(@Nullable List<ChannelUID> channels, @Nullable List<State> states) {
+ assertNotNull(channels);
+ assertNotNull(states);
+ assertTrue(channels.size() == states.size(), "Same list sizes ");
+ for (int i = 0; i < channels.size(); i++) {
+ checkResult(channels.get(i), states.get(i));
+ }
+ return true;
+ }
+
+ /**
+ * Add a specific check for a value e.g. hard coded "Upcoming Service" in order to check the right ordering
+ *
+ * @param specialHand
+ * @return
+ */
+ public LifetimeWrapper append(Map<String, State> compareMap) {
+ specialHandlingMap.putAll(compareMap);
+ return this;
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ private void checkResult(ChannelUID channelUID, State state) {
+ String cUid = channelUID.getIdWithoutGroup();
+ QuantityType<Length> qt;
+ switch (cUid) {
+ case DISTANCE_SINCE_CHARGING:
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ if (imperial) {
+ assertEquals(MILES, qt.getUnit(), "Miles");
+ assertEquals(allTrips.chargecycleRange.userCurrentChargeCycle / Converter.MILES_TO_KM_RATIO,
+ qt.floatValue(), 0.1, "Distance since charging");
+ } else {
+ assertEquals(Constants.KILOMETRE_UNIT, qt.getUnit(), "KM");
+ assertEquals(allTrips.chargecycleRange.userCurrentChargeCycle, qt.floatValue(), 0.1,
+ "Distance since charging");
+ }
+ break;
+ case SINGLE_LONGEST_DISTANCE:
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ if (imperial) {
+ assertEquals(MILES, qt.getUnit(), "Miles");
+ assertEquals(allTrips.chargecycleRange.userHigh / Converter.MILES_TO_KM_RATIO, qt.floatValue(), 0.1,
+ "Longest Distance");
+ } else {
+ assertEquals(Constants.KILOMETRE_UNIT, qt.getUnit(), "KM");
+ assertEquals(allTrips.chargecycleRange.userHigh, qt.floatValue(), 0.1, "Longest Distance");
+ }
+ break;
+ case TOTAL_DRIVEN_DISTANCE:
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ if (imperial) {
+ assertEquals(MILES, qt.getUnit(), "Miles");
+ assertEquals(allTrips.totalElectricDistance.userTotal / Converter.MILES_TO_KM_RATIO,
+ qt.floatValue(), 0.1, "Total Electric Distance");
+ } else {
+ assertEquals(Constants.KILOMETRE_UNIT, qt.getUnit(), "KM");
+ assertEquals(allTrips.totalElectricDistance.userTotal, qt.floatValue(), 0.1,
+ "Total Electric Distance");
+ }
+ break;
+ case AVG_CONSUMPTION:
+ assertTrue(isElectric, "Is Electric");
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ assertEquals(Units.KILOWATT_HOUR, qt.getUnit(), "kw/h");
+ if (imperial) {
+ assertEquals(allTrips.avgElectricConsumption.userAverage * Converter.MILES_TO_KM_RATIO,
+ qt.floatValue(), 0.1, "Avg Consumption");
+ } else {
+ assertEquals(allTrips.avgElectricConsumption.userAverage, qt.floatValue(), 0.1, "Avg Consumption");
+ }
+ break;
+ case AVG_RECUPERATION:
+ assertTrue(isElectric, "Is Electric");
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ assertEquals(Units.KILOWATT_HOUR, qt.getUnit(), "kw/h");
+ if (imperial) {
+ assertEquals(allTrips.avgRecuperation.userAverage * Converter.MILES_TO_KM_RATIO, qt.floatValue(),
+ 0.1, "Avg Recuperation");
+ } else {
+ assertEquals(allTrips.avgRecuperation.userAverage, qt.floatValue(), 0.1, "Avg Recuperation");
+ }
+ break;
+ case AVG_COMBINED_CONSUMPTION:
+ assertTrue(isHybrid, "Is Hybrid");
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ assertEquals(Units.LITRE, qt.getUnit(), "Liter");
+ if (imperial) {
+ assertEquals(allTrips.avgCombinedConsumption.userAverage * Converter.MILES_TO_KM_RATIO,
+ qt.floatValue(), 0.1, "Avg Combined Consumption");
+ } else {
+ assertEquals(allTrips.avgCombinedConsumption.userAverage, qt.floatValue(), 0.1,
+ "Avg Combined Consumption");
+ }
+ break;
+ default:
+ // fail in case of unknown update
+ assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+ break;
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.remote.ExecutionStatus;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.remote.ExecutionStatusContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.RemoteServiceHandler.ExecutionState;
+import org.openhab.binding.bmwconnecteddrive.internal.util.FileReader;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link RemoteStatusTest} Test json responses from ConnectedDrive Portal
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class RemoteStatusTest {
+ private static final Gson GSON = new Gson();
+
+ @Test
+ public void testStatus() {
+ String resource1 = FileReader.readFileInString("src/test/resources/webapi/remote-services/pending.json");
+ ExecutionStatusContainer esc = GSON.fromJson(resource1, ExecutionStatusContainer.class);
+ ExecutionStatus execStatus = esc.executionStatus;
+ assertEquals(ExecutionState.PENDING.name(), execStatus.status, "Status");
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.measure.Unit;
+import javax.measure.quantity.Length;
+import javax.measure.quantity.Time;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.VehicleType;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.Doors;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatus;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatusContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.Windows;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.VehicleStatusUtils;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.PointType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.ImperialUnits;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link StatusWrapper} Test json responses from ConnectedDrive Portal
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class StatusWrapper {
+ private static final Gson GSON = new Gson();
+ private static final Unit<Length> KILOMETRE = Constants.KILOMETRE_UNIT;
+ private static final double ALLOWED_MILE_CONVERSION_DEVIATION = 1.5;
+ private static final double ALLOWED_KM_ROUND_DEVIATION = 0.1;
+
+ private VehicleStatus vStatus;
+ private boolean imperial;
+ private boolean isElectric;
+ private boolean hasFuel;
+ private boolean isHybrid;
+
+ private Map<String, State> specialHandlingMap = new HashMap<String, State>();
+
+ public StatusWrapper(String type, boolean imperial, String statusJson) {
+ this.imperial = imperial;
+ hasFuel = type.equals(VehicleType.CONVENTIONAL.toString()) || type.equals(VehicleType.PLUGIN_HYBRID.toString())
+ || type.equals(VehicleType.ELECTRIC_REX.toString());
+ isElectric = type.equals(VehicleType.PLUGIN_HYBRID.toString())
+ || type.equals(VehicleType.ELECTRIC_REX.toString()) || type.equals(VehicleType.ELECTRIC.toString());
+ isHybrid = hasFuel && isElectric;
+ VehicleStatusContainer container = GSON.fromJson(statusJson, VehicleStatusContainer.class);
+ assertNotNull(container);
+ assertNotNull(container.vehicleStatus);
+ vStatus = container.vehicleStatus;
+ }
+
+ /**
+ * Test results auctomatically against json values
+ *
+ * @param channels
+ * @param states
+ * @return
+ */
+ public boolean checkResults(@Nullable List<ChannelUID> channels, @Nullable List<State> states) {
+ assertNotNull(channels);
+ assertNotNull(states);
+ assertTrue(channels.size() == states.size(), "Same list sizes");
+ for (int i = 0; i < channels.size(); i++) {
+ checkResult(channels.get(i), states.get(i));
+ }
+ return true;
+ }
+
+ /**
+ * Add a specific check for a value e.g. hard coded "Upcoming Service" in order to check the right ordering
+ *
+ * @param specialHand
+ * @return
+ */
+ public StatusWrapper append(Map<String, State> compareMap) {
+ specialHandlingMap.putAll(compareMap);
+ return this;
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ private void checkResult(ChannelUID channelUID, State state) {
+ String cUid = channelUID.getIdWithoutGroup();
+ String gUid = channelUID.getGroupId();
+ QuantityType<Length> qt;
+ QuantityType<Time> qtt;
+ StringType st;
+ StringType wanted;
+ DateTimeType dtt;
+ PointType pt;
+ switch (cUid) {
+ case MILEAGE:
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ if (imperial) {
+ assertEquals(ImperialUnits.MILE, qt.getUnit(), "Miles");
+ } else {
+ assertEquals(KILOMETRE, qt.getUnit(), "KM");
+ }
+ switch (gUid) {
+ case CHANNEL_GROUP_RANGE:
+ assertEquals(qt.intValue(), vStatus.mileage, "Mileage");
+ break;
+ case CHANNEL_GROUP_SERVICE:
+ if (vStatus.cbsData.isEmpty()) {
+ assertEquals(qt.intValue(), -1, "Service Mileage");
+ } else {
+ assertEquals(qt.intValue(), vStatus.cbsData.get(0).cbsRemainingMileage, "Service Mileage");
+ }
+ break;
+ case CHANNEL_GROUP_CHECK_CONTROL:
+ if (vStatus.checkControlMessages.isEmpty()) {
+ assertEquals(qt.intValue(), -1, "CheckControl Mileage");
+ } else {
+ assertEquals(qt.intValue(), vStatus.checkControlMessages.get(0).ccmMileage,
+ "CheckControl Mileage");
+ }
+ break;
+ default:
+ assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+ break;
+ }
+ break;
+ case RANGE_ELECTRIC:
+ assertTrue(isElectric, "Is Eelctric");
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ if (imperial) {
+ assertEquals(ImperialUnits.MILE, qt.getUnit(), "Miles");
+ assertEquals(Converter.round(qt.floatValue()), Converter.round(vStatus.remainingRangeElectricMls),
+ ALLOWED_MILE_CONVERSION_DEVIATION, "Mileage");
+ } else {
+ assertEquals(KILOMETRE, qt.getUnit(), "KM");
+ assertEquals(Converter.round(qt.floatValue()), Converter.round(vStatus.remainingRangeElectric),
+ ALLOWED_KM_ROUND_DEVIATION, "Mileage");
+ }
+ break;
+ case RANGE_FUEL:
+ assertTrue(hasFuel, "Has Fuel");
+ if (!(state instanceof UnDefType)) {
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ if (imperial) {
+ assertEquals(ImperialUnits.MILE, qt.getUnit(), "Miles");
+ assertEquals(Converter.round(qt.floatValue()), Converter.round(vStatus.remainingRangeFuelMls),
+ ALLOWED_MILE_CONVERSION_DEVIATION, "Mileage");
+ } else {
+ assertEquals(KILOMETRE, qt.getUnit(), "KM");
+ assertEquals(Converter.round(qt.floatValue()), Converter.round(vStatus.remainingRangeFuel),
+ ALLOWED_KM_ROUND_DEVIATION, "Mileage");
+ }
+ }
+ break;
+ case RANGE_HYBRID:
+ assertTrue(isHybrid, "Is Hybrid");
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ if (imperial) {
+ assertEquals(ImperialUnits.MILE, qt.getUnit(), "Miles");
+ assertEquals(Converter.round(qt.floatValue()),
+ Converter.round(vStatus.remainingRangeElectricMls + vStatus.remainingRangeFuelMls),
+ ALLOWED_MILE_CONVERSION_DEVIATION, "Mileage");
+ } else {
+ assertEquals(KILOMETRE, qt.getUnit(), "KM");
+ assertEquals(Converter.round(qt.floatValue()),
+ Converter.round(vStatus.remainingRangeElectric + vStatus.remainingRangeFuel),
+ ALLOWED_KM_ROUND_DEVIATION, "Mileage");
+ }
+ break;
+ case REMAINING_FUEL:
+ assertTrue(hasFuel, "Has Fuel");
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ assertEquals(Units.LITRE, qt.getUnit(), "Liter Unit");
+ assertEquals(Converter.round(vStatus.remainingFuel), Converter.round(qt.floatValue()), 0.01,
+ "Fuel Level");
+ break;
+ case SOC:
+ assertTrue(isElectric, "Is Eelctric");
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ assertEquals(Units.PERCENT, qt.getUnit(), "Percent");
+ assertEquals(Converter.round(vStatus.chargingLevelHv), Converter.round(qt.floatValue()), 0.01,
+ "Charge Level");
+ break;
+ case LOCK:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ assertEquals(Converter.toTitleCase(vStatus.doorLockState), st.toString(), "Vehicle locked");
+ break;
+ case DOORS:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ Doors doorState = GSON.fromJson(GSON.toJson(vStatus), Doors.class);
+ if (doorState != null) {
+ assertEquals(VehicleStatusUtils.checkClosed(doorState), st.toString(), "Doors Closed");
+ } else {
+ assertTrue(false);
+ }
+
+ break;
+ case WINDOWS:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ Windows windowState = GSON.fromJson(GSON.toJson(vStatus), Windows.class);
+ if (windowState != null) {
+ if (specialHandlingMap.containsKey(WINDOWS)) {
+ assertEquals(specialHandlingMap.get(WINDOWS).toString(), st.toString(), "Windows");
+ } else {
+ assertEquals(VehicleStatusUtils.checkClosed(windowState), st.toString(), "Windows");
+ }
+ } else {
+ assertTrue(false);
+ }
+
+ break;
+ case CHECK_CONTROL:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ if (specialHandlingMap.containsKey(CHECK_CONTROL)) {
+ assertEquals(specialHandlingMap.get(CHECK_CONTROL).toString(), st.toString(), "Check Control");
+ } else {
+ assertEquals(Converter.toTitleCase(VehicleStatusUtils.checkControlActive(vStatus)), st.toString(),
+ "Check Control");
+ }
+ break;
+ case CHARGE_STATUS:
+ assertTrue(isElectric, "Is Electric");
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ if (vStatus.chargingStatus.contentEquals(Constants.INVALID)) {
+ assertEquals(Converter.toTitleCase(vStatus.lastChargingEndReason), st.toString(), "Charge Status");
+ } else {
+ assertEquals(Converter.toTitleCase(vStatus.chargingStatus), st.toString(), "Charge Status");
+ }
+ break;
+ case CHARGE_REMAINING:
+ assertTrue(isElectric, "Is Electric");
+ if (vStatus.chargingTimeRemaining == null) {
+ assertTrue(state instanceof UnDefType, "expected UndefType");
+ } else {
+ assertTrue(state instanceof QuantityType);
+ qtt = ((QuantityType) state);
+ assertEquals(qtt.doubleValue(), vStatus.chargingTimeRemaining);
+ assertEquals(Units.MINUTE, qtt.getUnit(), "Minutes");
+ }
+ break;
+ case LAST_UPDATE:
+ assertTrue(state instanceof DateTimeType);
+ dtt = (DateTimeType) state;
+ DateTimeType expected = DateTimeType
+ .valueOf(Converter.getLocalDateTime(VehicleStatusUtils.getUpdateTime(vStatus)));
+ assertEquals(expected.toString(), dtt.toString(), "Last Update");
+ break;
+ case GPS:
+ assertTrue(state instanceof PointType);
+ pt = (PointType) state;
+ assertNotNull(vStatus.position);
+ assertEquals(vStatus.position.getCoordinates(), pt.toString(), "Coordinates");
+ break;
+ case HEADING:
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ assertEquals(Units.DEGREE_ANGLE, qt.getUnit(), "Angle Unit");
+ assertNotNull(vStatus.position);
+ assertEquals(vStatus.position.heading, qt.intValue(), 0.01, "Heading");
+ break;
+ case RANGE_RADIUS_ELECTRIC:
+ assertTrue(state instanceof QuantityType);
+ assertTrue(isElectric);
+ qt = (QuantityType) state;
+ if (imperial) {
+ assertEquals(Converter.guessRangeRadius(vStatus.remainingRangeElectricMls), qt.floatValue(), 1,
+ "Range Radius Electric mi");
+ } else {
+ assertEquals(Converter.guessRangeRadius(vStatus.remainingRangeElectric), qt.floatValue(), 0.1,
+ "Range Radius Electric km");
+ }
+ break;
+ case RANGE_RADIUS_FUEL:
+ assertTrue(state instanceof QuantityType);
+ assertTrue(hasFuel);
+ qt = (QuantityType) state;
+ if (imperial) {
+ assertEquals(Converter.guessRangeRadius(vStatus.remainingRangeFuelMls), qt.floatValue(), 1,
+ "Range Radius Fuel mi");
+ } else {
+ assertEquals(Converter.guessRangeRadius(vStatus.remainingRangeFuel), qt.floatValue(), 0.1,
+ "Range Radius Fuel km");
+ }
+ break;
+ case RANGE_RADIUS_HYBRID:
+ assertTrue(state instanceof QuantityType);
+ assertTrue(isHybrid);
+ qt = (QuantityType) state;
+ if (imperial) {
+ assertEquals(
+ Converter.guessRangeRadius(
+ vStatus.remainingRangeElectricMls + vStatus.remainingRangeFuelMls),
+ qt.floatValue(), ALLOWED_MILE_CONVERSION_DEVIATION, "Range Radius Hybrid mi");
+ } else {
+ assertEquals(
+ Converter.guessRangeRadius(vStatus.remainingRangeElectric + vStatus.remainingRangeFuel),
+ qt.floatValue(), ALLOWED_KM_ROUND_DEVIATION, "Range Radius Hybrid km");
+ }
+ break;
+ case DOOR_DRIVER_FRONT:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ wanted = StringType.valueOf(Converter.toTitleCase(vStatus.doorDriverFront));
+ assertEquals(wanted.toString(), st.toString(), "Door");
+ break;
+ case DOOR_DRIVER_REAR:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ wanted = StringType.valueOf(Converter.toTitleCase(vStatus.doorDriverRear));
+ assertEquals(wanted.toString(), st.toString(), "Door");
+ break;
+ case DOOR_PASSENGER_FRONT:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ wanted = StringType.valueOf(Converter.toTitleCase(vStatus.doorPassengerFront));
+ assertEquals(wanted.toString(), st.toString(), "Door");
+ break;
+ case DOOR_PASSENGER_REAR:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ wanted = StringType.valueOf(Converter.toTitleCase(vStatus.doorPassengerRear));
+ assertEquals(wanted.toString(), st.toString(), "Door");
+ break;
+ case TRUNK:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ wanted = StringType.valueOf(Converter.toTitleCase(vStatus.trunk));
+ assertEquals(wanted.toString(), st.toString(), "Door");
+ break;
+ case HOOD:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ wanted = StringType.valueOf(Converter.toTitleCase(vStatus.hood));
+ assertEquals(wanted.toString(), st.toString(), "Door");
+ break;
+ case WINDOW_DOOR_DRIVER_FRONT:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ wanted = StringType.valueOf(Converter.toTitleCase(vStatus.windowDriverFront));
+ assertEquals(wanted.toString(), st.toString(), "Window");
+ break;
+ case WINDOW_DOOR_DRIVER_REAR:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ wanted = StringType.valueOf(Converter.toTitleCase(vStatus.windowDriverRear));
+ assertEquals(wanted.toString(), st.toString(), "Window");
+ break;
+ case WINDOW_DOOR_PASSENGER_FRONT:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ wanted = StringType.valueOf(Converter.toTitleCase(vStatus.windowPassengerFront));
+ assertEquals(wanted.toString(), st.toString(), "Window");
+ break;
+ case WINDOW_DOOR_PASSENGER_REAR:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ wanted = StringType.valueOf(Converter.toTitleCase(vStatus.windowPassengerRear));
+ assertEquals(wanted.toString(), st.toString(), "Window");
+ break;
+ case WINDOW_REAR:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ wanted = StringType.valueOf(Converter.toTitleCase(vStatus.rearWindow));
+ assertEquals(wanted.toString(), st.toString(), "Window");
+ break;
+ case SUNROOF:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ wanted = StringType.valueOf(Converter.toTitleCase(vStatus.sunroof));
+ assertEquals(wanted.toString(), st.toString(), "Window");
+ break;
+ case SERVICE_DATE:
+ assertTrue(state instanceof DateTimeType);
+ dtt = (DateTimeType) state;
+ if (gUid.contentEquals(CHANNEL_GROUP_STATUS)) {
+ if (specialHandlingMap.containsKey(SERVICE_DATE)) {
+ assertEquals(specialHandlingMap.get(SERVICE_DATE).toString(), dtt.toString(), "Next Service");
+ } else {
+ String dueDateString = VehicleStatusUtils.getNextServiceDate(vStatus);
+ DateTimeType expectedDTT = DateTimeType.valueOf(Converter.getLocalDateTime(dueDateString));
+ assertEquals(expectedDTT.toString(), dtt.toString(), "Next Service");
+ }
+ } else if (gUid.equals(CHANNEL_GROUP_SERVICE)) {
+ String dueDateString = vStatus.cbsData.get(0).getDueDate();
+ DateTimeType expectedDTT = DateTimeType.valueOf(Converter.getLocalDateTime(dueDateString));
+ assertEquals(expectedDTT.toString(), dtt.toString(), "First Service Date");
+ }
+ break;
+ case SERVICE_MILEAGE:
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ if (gUid.contentEquals(CHANNEL_GROUP_STATUS)) {
+ if (imperial) {
+ assertEquals(ImperialUnits.MILE, qt.getUnit(), "Next Service Miles");
+ assertEquals(VehicleStatusUtils.getNextServiceMileage(vStatus), qt.intValue(), "Mileage");
+ } else {
+ assertEquals(KILOMETRE, qt.getUnit(), "Next Service KM");
+ assertEquals(VehicleStatusUtils.getNextServiceMileage(vStatus), qt.intValue(), "Mileage");
+ }
+ } else if (gUid.equals(CHANNEL_GROUP_SERVICE)) {
+ if (imperial) {
+ assertEquals(ImperialUnits.MILE, qt.getUnit(), "First Service Miles");
+ assertEquals(vStatus.cbsData.get(0).cbsRemainingMileage, qt.intValue(),
+ "First Service Mileage");
+ } else {
+ assertEquals(KILOMETRE, qt.getUnit(), "First Service KM");
+ assertEquals(vStatus.cbsData.get(0).cbsRemainingMileage, qt.intValue(),
+ "First Service Mileage");
+ }
+ }
+ break;
+ case NAME:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ switch (gUid) {
+ case CHANNEL_GROUP_SERVICE:
+ wanted = StringType.valueOf(Converter.toTitleCase(Constants.NO_ENTRIES));
+ if (!vStatus.cbsData.isEmpty()) {
+ wanted = StringType.valueOf(Converter.toTitleCase(vStatus.cbsData.get(0).getType()));
+ }
+ assertEquals(wanted.toString(), st.toString(), "Service Name");
+ break;
+ case CHANNEL_GROUP_CHECK_CONTROL:
+ wanted = StringType.valueOf(Constants.NO_ENTRIES);
+ if (!vStatus.checkControlMessages.isEmpty()) {
+ wanted = StringType.valueOf(vStatus.checkControlMessages.get(0).ccmDescriptionShort);
+ }
+ assertEquals(wanted.toString(), st.toString(), "CheckControl Name");
+ break;
+ default:
+ assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+ break;
+ }
+ break;
+ case DETAILS:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ switch (gUid) {
+ case CHANNEL_GROUP_SERVICE:
+ wanted = StringType.valueOf(Converter.toTitleCase(Constants.NO_ENTRIES));
+ if (!vStatus.cbsData.isEmpty()) {
+ wanted = StringType.valueOf(Converter.toTitleCase(vStatus.cbsData.get(0).getDescription()));
+ }
+ assertEquals(wanted.toString(), st.toString(), "Service Details");
+ break;
+ case CHANNEL_GROUP_CHECK_CONTROL:
+ wanted = StringType.valueOf(Constants.NO_ENTRIES);
+ if (!vStatus.checkControlMessages.isEmpty()) {
+ wanted = StringType.valueOf(vStatus.checkControlMessages.get(0).ccmDescriptionLong);
+ }
+ assertEquals(wanted.toString(), st.toString(), "CheckControl Details");
+ break;
+ default:
+ assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+ break;
+ }
+ break;
+ case DATE:
+ assertTrue(state instanceof DateTimeType);
+ dtt = (DateTimeType) state;
+ switch (gUid) {
+ case CHANNEL_GROUP_SERVICE:
+ String dueDateString = Constants.NULL_DATE;
+ if (!vStatus.cbsData.isEmpty()) {
+ dueDateString = vStatus.cbsData.get(0).getDueDate();
+ }
+ DateTimeType expectedDTT = DateTimeType.valueOf(Converter.getLocalDateTime(dueDateString));
+ assertEquals(expectedDTT.toString(), dtt.toString(), "ServiceSate");
+ break;
+ default:
+ assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+ break;
+ }
+ break;
+ default:
+ // fail in case of unknown update
+ assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+ break;
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.measure.Unit;
+import javax.measure.quantity.Length;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.VehicleType;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.LastTrip;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.LastTripContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.ImperialUnits;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.types.State;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link TripWrapper} Test json responses from ConnectedDrive Portal
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class TripWrapper {
+ private static final Gson GSON = new Gson();
+ private static final Unit<Length> MILES = ImperialUnits.MILE;
+
+ private LastTrip lastTrip;
+ private boolean imperial;
+ private boolean isElectric;
+ private boolean hasFuel;
+ private boolean isHybrid;
+
+ private Map<String, State> specialHandlingMap = new HashMap<String, State>();
+
+ public TripWrapper(String type, boolean imperial, String statusJson) {
+ this.imperial = imperial;
+ hasFuel = type.equals(VehicleType.CONVENTIONAL.toString()) || type.equals(VehicleType.PLUGIN_HYBRID.toString())
+ || type.equals(VehicleType.ELECTRIC_REX.toString());
+ isElectric = type.equals(VehicleType.PLUGIN_HYBRID.toString())
+ || type.equals(VehicleType.ELECTRIC_REX.toString()) || type.equals(VehicleType.ELECTRIC.toString());
+ isHybrid = hasFuel && isElectric;
+ LastTripContainer container = GSON.fromJson(statusJson, LastTripContainer.class);
+ assertNotNull(container);
+ assertNotNull(container.lastTrip);
+ lastTrip = container.lastTrip;
+ }
+
+ /**
+ * Test results auctomatically against json values
+ *
+ * @param channels
+ * @param states
+ * @return
+ */
+ public boolean checkResults(@Nullable List<ChannelUID> channels, @Nullable List<State> states) {
+ assertNotNull(channels);
+ assertNotNull(states);
+ assertTrue(channels.size() == states.size(), "Same list sizes");
+ for (int i = 0; i < channels.size(); i++) {
+ checkResult(channels.get(i), states.get(i));
+ }
+ return true;
+ }
+
+ /**
+ * Add a specific check for a value e.g. hard coded "Upcoming Service" in order to check the right ordering
+ *
+ * @param specialHand
+ * @return
+ */
+ public TripWrapper append(Map<String, State> compareMap) {
+ specialHandlingMap.putAll(compareMap);
+ return this;
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ private void checkResult(ChannelUID channelUID, State state) {
+ String cUid = channelUID.getIdWithoutGroup();
+ QuantityType<Length> qt;
+ DateTimeType dtt;
+ switch (cUid) {
+ case DATE:
+ assertTrue(state instanceof DateTimeType);
+ dtt = ((DateTimeType) state);
+ DateTimeType expected = DateTimeType.valueOf(Converter.getLocalDateTimeWithoutOffest(lastTrip.date));
+ assertEquals(expected.toString(), dtt.toString(), "Trip Date");
+ break;
+ case DURATION:
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ assertEquals(Units.MINUTE, qt.getUnit(), "Minute");
+ assertEquals(lastTrip.duration, qt.floatValue(), 0.1, "Duration");
+ break;
+ case DISTANCE:
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ if (imperial) {
+ assertEquals(MILES, qt.getUnit(), "Miles");
+ assertEquals(lastTrip.totalDistance / Converter.MILES_TO_KM_RATIO, qt.floatValue(), 0.1,
+ "Distance");
+
+ } else {
+ assertEquals(Constants.KILOMETRE_UNIT, qt.getUnit(), "KM");
+ assertEquals(lastTrip.totalDistance, qt.floatValue(), 0.1, "Distance");
+ }
+ break;
+ case AVG_CONSUMPTION:
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ assertEquals(Units.KILOWATT_HOUR, qt.getUnit(), "kw/h");
+ if (imperial) {
+ assertEquals(lastTrip.avgElectricConsumption * Converter.MILES_TO_KM_RATIO, qt.floatValue(), 0.1,
+ "Avg Consumption");
+ } else {
+ assertEquals(lastTrip.avgElectricConsumption, qt.floatValue(), 0.1, "Avg Consumption");
+ }
+ break;
+ case AVG_COMBINED_CONSUMPTION:
+ assertTrue(isHybrid, "Is Hybrid");
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ assertEquals(Units.LITRE, qt.getUnit(), "Liter");
+ if (imperial) {
+ assertEquals(Converter.round(lastTrip.avgCombinedConsumption * Converter.MILES_TO_KM_RATIO),
+ qt.floatValue(), 0.01, "Percent");
+ } else {
+ assertEquals(Converter.round(lastTrip.avgCombinedConsumption), qt.floatValue(), 0.01, "Percent");
+ }
+ break;
+ case AVG_RECUPERATION:
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ assertEquals(Units.KILOWATT_HOUR, qt.getUnit(), "kw/h");
+ if (imperial) {
+ assertEquals(lastTrip.avgRecuperation * Converter.MILES_TO_KM_RATIO, qt.floatValue(), 0.1,
+ "Avg Recuperation");
+ } else {
+ assertEquals(lastTrip.avgRecuperation, qt.floatValue(), 0.1, "Avg Recuperation");
+ }
+ break;
+ default:
+ // fail in case of unknown update
+ assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+ break;
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.compat.VehicleAttributesContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.CBSMessage;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.Position;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatus;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatusContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.util.FileReader;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link VehicleStatusTest} Test json responses from ConnectedDrive Portal
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class VehicleStatusTest {
+ private static final Gson GSON = new Gson();
+
+ @Test
+ public void testBevRexValues() {
+ String resource1 = FileReader.readFileInString("src/test/resources/webapi/vehicle-status.json");
+ VehicleStatusContainer status = GSON.fromJson(resource1, VehicleStatusContainer.class);
+ VehicleStatus vStatus = status.vehicleStatus;
+ assertEquals(17273.0, vStatus.mileage, 0.1, "Mileage");
+ Position p = vStatus.position;
+ assertEquals(219, p.heading, "Heading");
+
+ assertEquals("NA", vStatus.dcsCchActivation, "DCS Activation");
+ assertEquals(false, vStatus.dcsCchOngoing, "DCS Ongoing");
+ }
+
+ @Test
+ public void testServices() {
+ String resource1 = FileReader.readFileInString("src/test/resources/webapi/vehicle-status.json");
+ VehicleStatusContainer status = GSON.fromJson(resource1, VehicleStatusContainer.class);
+ VehicleStatus vStatus = status.vehicleStatus;
+ List<CBSMessage> services = vStatus.cbsData;
+ CBSMessage message = services.get(0);
+ assertEquals(15345, message.cbsRemainingMileage, "Service Mileage");
+ message = services.get(1);
+ assertEquals(-1, message.cbsRemainingMileage, "Service Mileage ");
+ }
+
+ @Test
+ public void testCompatibility() {
+ String resource = FileReader.readFileInString("src/test/resources/api/vehicle/vehicle-ccm.json");
+ VehicleAttributesContainer vac = GSON.fromJson(resource, VehicleAttributesContainer.class);
+ assertEquals("Laden nicht möglich", vac.vehicleMessages.ccmMessages.get(0).text, "CCM");
+ // Time Test to be removed - different Machines = different Time Zones
+ // VehicleStatusContainer vsc = GSON.fromJson(vac.transform(), VehicleStatusContainer.class);
+ // assertEquals("27.09.2020 13:18", vsc.vehicleStatus.getUpdateTime(), "UTC DateTime");
+ // String ldt = Converter.getLocalDateTime(vsc.vehicleStatus.getUpdateTime());
+ // assertEquals("2020-09-27T15:18:00", ldt.toString(), "Local DateTime");
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.VehicleType;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.LifetimeWrapper;
+import org.openhab.binding.bmwconnecteddrive.internal.util.FileReader;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandlerCallback;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link AllTripTests} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class AllTripTests {
+ private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
+
+ private static final int HYBRID_CALL_TIMES = 6;
+
+ @Nullable
+ ArgumentCaptor<ChannelUID> channelCaptor;
+ @Nullable
+ ArgumentCaptor<State> stateCaptor;
+ @Nullable
+ ThingHandlerCallback tc;
+ @Nullable
+ VehicleHandler cch;
+ @Nullable
+ List<ChannelUID> allChannels;
+ @Nullable
+ List<State> allStates;
+ String driveTrain = Constants.EMPTY;
+ boolean imperial;
+
+ /**
+ * Prepare environment for Vehicle Status Updates
+ */
+ public void setup(String type, boolean imperial) {
+ driveTrain = type;
+ this.imperial = imperial;
+ Thing thing = mock(Thing.class);
+ when(thing.getUID()).thenReturn(new ThingUID("testbinding", "test"));
+ BMWConnectedDriveOptionProvider op = mock(BMWConnectedDriveOptionProvider.class);
+ cch = new VehicleHandler(thing, op, type, imperial);
+ tc = mock(ThingHandlerCallback.class);
+ cch.setCallback(tc);
+ channelCaptor = ArgumentCaptor.forClass(ChannelUID.class);
+ stateCaptor = ArgumentCaptor.forClass(State.class);
+ }
+
+ private boolean testTrip(String statusContent, int callbacksExpected, Optional<Map<String, State>> concreteChecks) {
+ assertNotNull(statusContent);
+ cch.allTripsCallback.onResponse(statusContent);
+ verify(tc, times(callbacksExpected)).stateUpdated(channelCaptor.capture(), stateCaptor.capture());
+ allChannels = channelCaptor.getAllValues();
+ allStates = stateCaptor.getAllValues();
+
+ assertNotNull(driveTrain);
+ LifetimeWrapper checker = new LifetimeWrapper(driveTrain, imperial, statusContent);
+ trace();
+ if (concreteChecks.isPresent()) {
+ return checker.append(concreteChecks.get()).checkResults(allChannels, allStates);
+ } else {
+ return checker.checkResults(allChannels, allStates);
+ }
+ }
+
+ private void trace() {
+ for (int i = 0; i < allChannels.size(); i++) {
+ logger.info("Channel {} {}", allChannels.get(i), allStates.get(i));
+ }
+ }
+
+ @Test
+ public void testi3Rex() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.ELECTRIC_REX.toString(), false);
+ String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/all-trips.json");
+ assertTrue(testTrip(content, HYBRID_CALL_TIMES, Optional.empty()));
+ }
+
+ @Test
+ public void test530E() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.PLUGIN_HYBRID.toString(), false);
+ String content = FileReader.readFileInString("src/test/resources/responses/530E/all-trips.json");
+ assertTrue(testTrip(content, HYBRID_CALL_TIMES, Optional.empty()));
+ }
+
+ @Test
+ public void testi3RexImperial() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.ELECTRIC_REX.toString(), true);
+ String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/all-trips.json");
+ assertTrue(testTrip(content, HYBRID_CALL_TIMES, Optional.empty()));
+ }
+
+ @Test
+ public void test530EImperial() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.PLUGIN_HYBRID.toString(), true);
+ String content = FileReader.readFileInString("src/test/resources/responses/530E/all-trips.json");
+ assertTrue(testTrip(content, HYBRID_CALL_TIMES, Optional.empty()));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.*;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConfiguration;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.BimmerConstants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.HTTPConstants;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link AuthTest} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class AuthTest {
+ private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
+
+ @Test
+ public void testAuthServerMap() {
+ Map<String, String> authServers = BimmerConstants.AUTH_SERVER_MAP;
+ assertEquals(3, authServers.size(), "Number of Servers");
+ Map<String, String> api = BimmerConstants.SERVER_MAP;
+ assertEquals(3, api.size(), "Number of Servers");
+ }
+
+ @Test
+ public void testTokenDecoding() {
+ String headerValue = "https://www.bmw-connecteddrive.com/app/static/external-dispatch.html#access_token=SfXKgkEXeeFJkVqdD4XMmfUU224MRuyh&token_type=Bearer&expires_in=7199";
+ HttpClientFactory hcf = mock(HttpClientFactory.class);
+ when(hcf.getCommonHttpClient()).thenReturn(mock(HttpClient.class));
+ when(hcf.createHttpClient(HTTPConstants.AUTH_HTTP_CLIENT_NAME)).thenReturn(mock(HttpClient.class));
+ ConnectedDriveConfiguration config = new ConnectedDriveConfiguration();
+ config.region = BimmerConstants.REGION_ROW;
+ ConnectedDriveProxy dcp = new ConnectedDriveProxy(hcf, config);
+ dcp.tokenFromUrl(headerValue);
+ Token t = dcp.getToken();
+ assertEquals("Bearer SfXKgkEXeeFJkVqdD4XMmfUU224MRuyh", t.getBearerToken(), "Token");
+ }
+
+ public void testRealTokenUpdate() {
+ ConnectedDriveConfiguration config = new ConnectedDriveConfiguration();
+ config.region = BimmerConstants.REGION_ROW;
+ config.userName = "bla";
+ config.password = "blub";
+ HttpClientFactory hcf = mock(HttpClientFactory.class);
+ when(hcf.getCommonHttpClient()).thenReturn(mock(HttpClient.class));
+ when(hcf.createHttpClient(HTTPConstants.AUTH_HTTP_CLIENT_NAME)).thenReturn(mock(HttpClient.class));
+ ConnectedDriveProxy dcp = new ConnectedDriveProxy(hcf, config);
+ Token t = dcp.getToken();
+ logger.info("Token {}", t.getBearerToken());
+ logger.info("Expires {}", t.isExpired());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.*;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.VehicleType;
+import org.openhab.binding.bmwconnecteddrive.internal.util.FileReader;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandlerCallback;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link ChargeProfileTest} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class ChargeProfileTest {
+ private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
+
+ private static final int PROFILE_CALLBACK_NUMBER = 37;
+
+ @Nullable
+ ArgumentCaptor<ChannelUID> channelCaptor;
+ @Nullable
+ ArgumentCaptor<State> stateCaptor;
+ @Nullable
+ ThingHandlerCallback tc;
+ @Nullable
+ VehicleHandler cch;
+ @Nullable
+ List<ChannelUID> allChannels;
+ @Nullable
+ List<State> allStates;
+ String driveTrain = Constants.EMPTY;
+ boolean imperial;
+
+ /**
+ * Prepare environment for Vehicle Status Updates
+ */
+ public void setup(String type, boolean imperial) {
+ driveTrain = type;
+ this.imperial = imperial;
+ Thing thing = mock(Thing.class);
+ when(thing.getUID()).thenReturn(new ThingUID("testbinding", "test"));
+ BMWConnectedDriveOptionProvider op = mock(BMWConnectedDriveOptionProvider.class);
+ cch = new VehicleHandler(thing, op, type, imperial);
+ tc = mock(ThingHandlerCallback.class);
+ cch.setCallback(tc);
+ channelCaptor = ArgumentCaptor.forClass(ChannelUID.class);
+ stateCaptor = ArgumentCaptor.forClass(State.class);
+ }
+
+ private boolean testProfile(String statusContent, int callbacksExpected) {
+ assertNotNull(statusContent);
+
+ cch.chargeProfileCallback.onResponse(statusContent);
+ verify(tc, times(callbacksExpected)).stateUpdated(channelCaptor.capture(), stateCaptor.capture());
+ allChannels = channelCaptor.getAllValues();
+ allStates = stateCaptor.getAllValues();
+
+ assertNotNull(driveTrain);
+ trace();
+ return true;
+ }
+
+ private void trace() {
+ for (int i = 0; i < allChannels.size(); i++) {
+ logger.info("Channel {} {}", allChannels.get(i), allStates.get(i));
+ }
+ }
+
+ /**
+ * Channel testbinding::test:charge#profile-climate ON
+ * Channel testbinding::test:charge#profile-mode IMMEDIATE_CHARGING
+ * Channel testbinding::test:charge#window-start 11:00
+ * Channel testbinding::test:charge#window-end 17:00
+ * Channel testbinding::test:charge#timer1-departure 05:00
+ * Channel testbinding::test:charge#timer1-enabled OFF
+ * Channel testbinding::test:charge#timer1-days MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY
+ * Channel testbinding::test:charge#timer2-departure 12:00
+ * Channel testbinding::test:charge#timer2-enabled ON
+ * Channel testbinding::test:charge#timer2-days SATURDAY
+ * Channel testbinding::test:charge#timer3-departure 00:00
+ * Channel testbinding::test:charge#timer3-enabled OFF
+ * Channel testbinding::test:charge#timer3-days
+ */
+ @Test
+ public void testChargingProfile() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.ELECTRIC_REX.toString(), false);
+ String content = FileReader.readFileInString("src/test/resources/webapi/charging-profile.json");
+ testProfile(content, PROFILE_CALLBACK_NUMBER);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConfiguration;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.BimmerConstants;
+
+/**
+ * The {@link ConfigurationTest} test different configurations
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class ConfigurationTest {
+
+ @Test
+ public void testAuthServerMap() {
+ ConnectedDriveConfiguration cdc = new ConnectedDriveConfiguration();
+ assertFalse(ConnectedDriveBridgeHandler.checkConfiguration(cdc));
+ cdc.userName = "a";
+ assertFalse(ConnectedDriveBridgeHandler.checkConfiguration(cdc));
+ cdc.password = "b";
+ assertFalse(ConnectedDriveBridgeHandler.checkConfiguration(cdc));
+ cdc.region = "c";
+ assertFalse(ConnectedDriveBridgeHandler.checkConfiguration(cdc));
+ cdc.region = BimmerConstants.REGION_NORTH_AMERICA;
+ assertTrue(ConnectedDriveBridgeHandler.checkConfiguration(cdc));
+ cdc.region = BimmerConstants.REGION_ROW;
+ assertTrue(ConnectedDriveBridgeHandler.checkConfiguration(cdc));
+ cdc.region = BimmerConstants.REGION_CHINA;
+ assertTrue(ConnectedDriveBridgeHandler.checkConfiguration(cdc));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import static org.mockito.Mockito.*;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandlerCallback;
+import org.openhab.core.types.State;
+
+/**
+ * The {@link ErrorResponseTest} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class ErrorResponseTest {
+ @Nullable
+ ArgumentCaptor<ChannelUID> channelCaptor;
+ @Nullable
+ ArgumentCaptor<State> stateCaptor;
+ @Nullable
+ ThingHandlerCallback tc;
+ @Nullable
+ VehicleHandler cch;
+ @Nullable
+ List<ChannelUID> allChannels;
+ @Nullable
+ List<State> allStates;
+ @Nullable
+ String driveTrain;
+ boolean imperial;
+
+ /**
+ * Prepare environment for Vehicle Status Updates
+ */
+ public void setup(String type, boolean imperial) {
+ driveTrain = type;
+ this.imperial = imperial;
+ Thing thing = mock(Thing.class);
+ when(thing.getUID()).thenReturn(new ThingUID("testbinding", "test"));
+ BMWConnectedDriveOptionProvider op = mock(BMWConnectedDriveOptionProvider.class);
+ cch = new VehicleHandler(thing, op, type, imperial);
+ tc = mock(ThingHandlerCallback.class);
+ cch.setCallback(tc);
+ channelCaptor = ArgumentCaptor.forClass(ChannelUID.class);
+ stateCaptor = ArgumentCaptor.forClass(State.class);
+ }
+
+ @Test
+ public void testErrorResponseCallbacks() {
+ String error = "{\"error\":true,\"reason\":\"offline\"}";
+ setup("BEV", false);
+ cch.vehicleStatusCallback.onResponse(error);
+ cch.allTripsCallback.onResponse(error);
+ cch.lastTripCallback.onResponse(error);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
+import org.openhab.binding.bmwconnecteddrive.internal.util.FileReader;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ThingUID;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link FingerprintTest} Test Discovery Results
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class FingerprintTest {
+ private final Logger logger = LoggerFactory.getLogger(FingerprintTest.class);
+
+ public void testDiscoveryFingerprint() {
+ Bridge b = mock(Bridge.class);
+ when(b.getUID()).thenReturn(new ThingUID("bmwconnecteddrive", "account", "user"));
+ HttpClientFactory hcf = mock(HttpClientFactory.class);
+ ConnectedDriveBridgeHandler bh = new ConnectedDriveBridgeHandler(b, hcf);
+ // when(bh.getThing()).thenReturn(b);
+
+ bh.onResponse(Constants.EMPTY_JSON);
+ assertEquals(Constants.EMPTY_JSON, bh.getDiscoveryFingerprint(), "Empty Response");
+
+ bh.onResponse(null);
+ assertEquals(Constants.EMPTY_JSON, bh.getDiscoveryFingerprint(), "Empty Response");
+
+ String content = FileReader.readFileInString("src/test/resources/webapi/connected-drive-account-info.json");
+ bh.onResponse(content);
+ String fingerprint = bh.getDiscoveryFingerprint();
+ logger.info("{}", fingerprint);
+ assertFalse(fingerprint.contains("My Real"), "Anonymous Fingerprint");
+ assertFalse(fingerprint.contains("MY_REAL_VIN"), "Anonymous Fingerprint");
+
+ NetworkError err = new NetworkError();
+ err.url = "Some URL";
+ err.status = 500;
+ err.reason = "Internal Server Error";
+ bh.onError(err);
+ assertEquals(err.toJson(), bh.getDiscoveryFingerprint(), "Empty Response");
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.VehicleType;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.TripWrapper;
+import org.openhab.binding.bmwconnecteddrive.internal.util.FileReader;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandlerCallback;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link LastTripTests} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class LastTripTests {
+ private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
+
+ private static final int HYBRID_CALL_TIMES = 6;
+
+ @Nullable
+ ArgumentCaptor<ChannelUID> channelCaptor;
+ @Nullable
+ ArgumentCaptor<State> stateCaptor;
+ @Nullable
+ ThingHandlerCallback tc;
+ @Nullable
+ VehicleHandler cch;
+ @Nullable
+ List<ChannelUID> allChannels;
+ @Nullable
+ List<State> allStates;
+ String driveTrain = Constants.EMPTY;
+ boolean imperial;
+
+ /**
+ * Prepare environment for Vehicle Status Updates
+ */
+ public void setup(String type, boolean imperial) {
+ driveTrain = type;
+ this.imperial = imperial;
+ Thing thing = mock(Thing.class);
+ when(thing.getUID()).thenReturn(new ThingUID("testbinding", "test"));
+ BMWConnectedDriveOptionProvider op = mock(BMWConnectedDriveOptionProvider.class);
+ cch = new VehicleHandler(thing, op, type, imperial);
+ tc = mock(ThingHandlerCallback.class);
+ cch.setCallback(tc);
+ channelCaptor = ArgumentCaptor.forClass(ChannelUID.class);
+ stateCaptor = ArgumentCaptor.forClass(State.class);
+ }
+
+ private boolean testTrip(String statusContent, int callbacksExpected, Optional<Map<String, State>> concreteChecks) {
+ assertNotNull(statusContent);
+ cch.lastTripCallback.onResponse(statusContent);
+ verify(tc, times(callbacksExpected)).stateUpdated(channelCaptor.capture(), stateCaptor.capture());
+ allChannels = channelCaptor.getAllValues();
+ allStates = stateCaptor.getAllValues();
+
+ assertNotNull(driveTrain);
+ TripWrapper checker = new TripWrapper(driveTrain, imperial, statusContent);
+ trace();
+ if (concreteChecks.isPresent()) {
+ return checker.append(concreteChecks.get()).checkResults(allChannels, allStates);
+ } else {
+ return checker.checkResults(allChannels, allStates);
+ }
+ }
+
+ private void trace() {
+ for (int i = 0; i < allChannels.size(); i++) {
+ logger.info("Channel {} {}", allChannels.get(i), allStates.get(i));
+ }
+ }
+
+ @Test
+ public void testi3Rex() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.ELECTRIC_REX.toString(), false);
+ String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/last-trip.json");
+ assertTrue(testTrip(content, HYBRID_CALL_TIMES, Optional.empty()));
+ }
+
+ @Test
+ public void test530E() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.PLUGIN_HYBRID.toString(), false);
+ String content = FileReader.readFileInString("src/test/resources/responses/530E/last-trip.json");
+ assertTrue(testTrip(content, HYBRID_CALL_TIMES, Optional.empty()));
+ }
+
+ @Test
+ public void testi3RexImperial() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.ELECTRIC_REX.toString(), true);
+ String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/last-trip.json");
+ assertTrue(testTrip(content, HYBRID_CALL_TIMES, Optional.empty()));
+ }
+
+ @Test
+ public void test530EImperial() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.PLUGIN_HYBRID.toString(), true);
+ String content = FileReader.readFileInString("src/test/resources/responses/530E/last-trip.json");
+ assertTrue(testTrip(content, HYBRID_CALL_TIMES, Optional.empty()));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.simulation.Injector;
+
+/**
+ * The {@link SimulationTest} Assures simulation is off
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class SimulationTest {
+
+ @Test
+ public void testSimulationOff() {
+ assertFalse(Injector.isActive(), "Simulation off");
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.VehicleType;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.StatusWrapper;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.compat.VehicleAttributesContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.util.FileReader;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandlerCallback;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link VehicleTests} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class VehicleTests {
+ private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
+
+ private static final int STATUS_ELECTRIC = 9;
+ private static final int STATUS_CONV = 7;
+ private static final int RANGE_HYBRID = 9;
+ private static final int RANGE_CONV = 4;
+ private static final int RANGE_ELECTRIC = 4;
+ private static final int DOORS = 12;
+ private static final int CHECK_EMPTY = 3;
+ private static final int CHECK_AVAILABLE = 3;
+ private static final int SERVICE_AVAILABLE = 4;
+ private static final int SERVICE_EMPTY = 4;
+ private static final int POSITION = 2;
+
+ @Nullable
+ ArgumentCaptor<ChannelUID> channelCaptor;
+ @Nullable
+ ArgumentCaptor<State> stateCaptor;
+ @Nullable
+ ThingHandlerCallback tc;
+ @Nullable
+ VehicleHandler cch;
+ @Nullable
+ List<ChannelUID> allChannels;
+ @Nullable
+ List<State> allStates;
+ String driveTrain = Constants.EMPTY;
+ boolean imperial;
+
+ /**
+ * Prepare environment for Vehicle Status Updates
+ */
+ public void setup(String type, boolean imperial) {
+ driveTrain = type;
+ this.imperial = imperial;
+ Thing thing = mock(Thing.class);
+ when(thing.getUID()).thenReturn(new ThingUID("testbinding", "test"));
+ BMWConnectedDriveOptionProvider op = mock(BMWConnectedDriveOptionProvider.class);
+ cch = new VehicleHandler(thing, op, type, imperial);
+ tc = mock(ThingHandlerCallback.class);
+ cch.setCallback(tc);
+ channelCaptor = ArgumentCaptor.forClass(ChannelUID.class);
+ stateCaptor = ArgumentCaptor.forClass(State.class);
+ }
+
+ private boolean testVehicle(String statusContent, int callbacksExpected,
+ Optional<Map<String, State>> concreteChecks) {
+ assertNotNull(statusContent);
+ cch.vehicleStatusCallback.onResponse(statusContent);
+ verify(tc, times(callbacksExpected)).stateUpdated(channelCaptor.capture(), stateCaptor.capture());
+ allChannels = channelCaptor.getAllValues();
+ allStates = stateCaptor.getAllValues();
+
+ assertNotNull(driveTrain);
+ StatusWrapper checker = new StatusWrapper(driveTrain, imperial, statusContent);
+ trace();
+ if (concreteChecks.isPresent()) {
+ return checker.append(concreteChecks.get()).checkResults(allChannels, allStates);
+ } else {
+ return checker.checkResults(allChannels, allStates);
+ }
+ }
+
+ private void trace() {
+ for (int i = 0; i < allChannels.size(); i++) {
+ logger.info("Channel {} {}", allChannels.get(i), allStates.get(i));
+ }
+ }
+
+ /**
+ * Test various Vehicles from users which delivered their fingerprint.
+ * The tests are checking the chain from "JSON to Channel update".
+ * Checks are done in an automated way cross checking the data from JSON and data delivered via Channel.
+ * Also important the updates are counted in order to check if code changes are affecting Channel Updates.
+ *
+ * With the given output the updated Channels are visible.
+ * Example:
+ *
+ * testi3Rex
+ * [main] INFO org.eclipse.jetty.util.log - Logging initialized @1731ms
+ * Channel testbinding::test:status#lock Secured
+ * Channel testbinding::test:status#service-date 2021-11-01T13:00:00.000+0100
+ * Channel testbinding::test:status#service-mileage -1.0 km
+ * Channel testbinding::test:status#check-control Not Active
+ * Channel testbinding::test:status#last-update 2020-08-24T17:55:32.000+0200
+ * Channel testbinding::test:status#doors CLOSED
+ * Channel testbinding::test:status#windows CLOSED
+ * Channel testbinding::test:doors#driver-front CLOSED
+ * Channel testbinding::test:doors#driver-rear CLOSED
+ * Channel testbinding::test:doors#passenger-front CLOSED
+ * Channel testbinding::test:doors#passenger-rear CLOSED
+ * Channel testbinding::test:doors#trunk CLOSED
+ * Channel testbinding::test:doors#hood CLOSED
+ * Channel testbinding::test:doors#window-driver-front CLOSED
+ * Channel testbinding::test:doors#window-driver-rear CLOSED
+ * Channel testbinding::test:doors#window-passenger-front CLOSED
+ * Channel testbinding::test:doors#window-passenger-rear CLOSED
+ * Channel testbinding::test:doors#window-rear INVALID
+ * Channel testbinding::test:doors#sunroof CLOSED
+ * Channel testbinding::test:range#mileage 17273.0 km
+ * Channel testbinding::test:range#electric 148.0 km
+ * Channel testbinding::test:range#radius-electric 118.4 km
+ * Channel testbinding::test:range#fuel 70.0 km
+ * Channel testbinding::test:range#radius-fuel 56.0 km
+ * Channel testbinding::test:range#hybrid 218.0 km
+ * Channel testbinding::test:range#radius-hybrid 174.4 km
+ * Channel testbinding::test:range#soc 71.0 %
+ * Channel testbinding::test:range#remaining-fuel 4.0 l
+ * Channel testbinding::test:status#charge Charging Goal Reached
+ * Channel testbinding::test:check#size 0
+ * Channel testbinding::test:check#name INVALID
+ * Channel testbinding::test:check#mileage -1.0 km
+ * Channel testbinding::test:check#index -1
+ * Channel testbinding::test:service#size 4
+ * Channel testbinding::test:service#name Brake Fluid
+ * Channel testbinding::test:service#date 2021-11-01T13:00:00.000+0100
+ * Channel testbinding::test:service#mileage 15345.0 km
+ * Channel testbinding::test:service#index 0
+ * Channel testbinding::test:location#latitude 50.55604934692383
+ * Channel testbinding::test:location#longitude 8.4956693649292
+ * Channel testbinding::test:location#heading 219.0 °
+ *
+ */
+
+ @Test
+ public void testi3Rex() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.ELECTRIC_REX.toString(), false);
+ String content = FileReader.readFileInString("src/test/resources/webapi/vehicle-status.json");
+ assertTrue(testVehicle(content,
+ STATUS_ELECTRIC + RANGE_HYBRID + DOORS + CHECK_EMPTY + SERVICE_AVAILABLE + POSITION, Optional.empty()));
+ }
+
+ @Test
+ public void testi3RexMiles() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.ELECTRIC_REX.toString(), true);
+ String content = FileReader.readFileInString("src/test/resources/webapi/vehicle-status.json");
+ // assertTrue(testVehicle(content, HYBRID_CALL_TIMES + LIST_UPDATES, Optional.empty()));
+ assertTrue(testVehicle(content,
+ STATUS_ELECTRIC + RANGE_HYBRID + DOORS + CHECK_EMPTY + SERVICE_AVAILABLE + POSITION, Optional.empty()));
+ }
+
+ @Test
+ public void testF15() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.CONVENTIONAL.toString(), false);
+ String content = FileReader.readFileInString("src/test/resources/responses/F15/status.json");
+ // Check earliest Service by hard
+ Map<String, State> m = new HashMap<String, State>();
+ // Don>'t test on concrete timestamp - it's is different on each machine
+ // Check for cbsType which is "Oil" instead
+ // m.put(ConnectedDriveConstants.SERVICE_DATE, DateTimeType.valueOf("2018-06-01T14:00:00.000+0200"));
+ m.put(ConnectedDriveConstants.NAME, StringType.valueOf("Oil"));
+ assertTrue(testVehicle(content, STATUS_CONV + DOORS + RANGE_CONV + POSITION + SERVICE_AVAILABLE + CHECK_EMPTY,
+ Optional.of(m)));
+ }
+
+ @Test
+ public void testF15Miles() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.CONVENTIONAL.toString(), true);
+ String content = FileReader.readFileInString("src/test/resources/responses/F15/status.json");
+ // Check earliest Service by hard
+ Map<String, State> m = new HashMap<String, State>();
+ // Don>'t test on concrete timestamp - it's idfferent on each machine
+ // Check for cbsType which is "Oil" instead
+ // m.put(ConnectedDriveConstants.SERVICE_DATE, DateTimeType.valueOf("2018-06-01T14:00:00.000+0200"));
+ m.put(ConnectedDriveConstants.NAME, StringType.valueOf("Oil"));
+ assertTrue(testVehicle(content, STATUS_CONV + DOORS + RANGE_CONV + POSITION + SERVICE_AVAILABLE + CHECK_EMPTY,
+ Optional.of(m)));
+ }
+
+ @Test
+ public void testF31() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.CONVENTIONAL.toString(), false);
+ String content = FileReader.readFileInString("src/test/resources/responses/F31/status.json");
+ assertTrue(testVehicle(content, STATUS_CONV + DOORS + RANGE_CONV + POSITION + SERVICE_AVAILABLE + CHECK_EMPTY,
+ Optional.empty()));
+ }
+
+ @Test
+ public void testF31Miles() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.CONVENTIONAL.toString(), true);
+ String content = FileReader.readFileInString("src/test/resources/responses/F31/status.json");
+ assertTrue(testVehicle(content, STATUS_CONV + DOORS + RANGE_CONV + POSITION + SERVICE_AVAILABLE + CHECK_EMPTY,
+ Optional.empty()));
+ }
+
+ @Test
+ public void testF35() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.CONVENTIONAL.toString(), false);
+ String content = FileReader.readFileInString("src/test/resources/responses/F35/status.json");
+ assertTrue(testVehicle(content, STATUS_CONV + DOORS + RANGE_CONV + POSITION + SERVICE_EMPTY + CHECK_EMPTY,
+ Optional.empty()));
+ }
+
+ @Test
+ public void testF35Miles() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.CONVENTIONAL.toString(), true);
+ String content = FileReader.readFileInString("src/test/resources/responses/F35/status.json");
+ assertTrue(testVehicle(content, STATUS_CONV + DOORS + RANGE_CONV + POSITION + SERVICE_EMPTY + CHECK_EMPTY,
+ Optional.empty()));
+ }
+
+ @Test
+ public void testF45() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.CONVENTIONAL.toString(), false);
+ String content = FileReader.readFileInString("src/test/resources/responses/F45/status.json");
+ // assertTrue(testVehicle(content, 27, Optional.empty()));
+ assertTrue(testVehicle(content, STATUS_CONV + DOORS + RANGE_CONV + SERVICE_EMPTY + CHECK_EMPTY + POSITION,
+ Optional.empty()));
+ }
+
+ @Test
+ public void testF45Miles() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.CONVENTIONAL.toString(), true);
+ String content = FileReader.readFileInString("src/test/resources/responses/F45/status.json");
+ assertTrue(testVehicle(content, STATUS_CONV + DOORS + RANGE_CONV + SERVICE_EMPTY + CHECK_EMPTY + POSITION,
+ Optional.empty()));
+ }
+
+ @Test
+ public void testF48() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.CONVENTIONAL.toString(), false);
+ String content = FileReader.readFileInString("src/test/resources/responses/F48/status.json");
+ assertTrue(testVehicle(content,
+ STATUS_CONV + DOORS + RANGE_CONV + SERVICE_AVAILABLE + CHECK_AVAILABLE + POSITION, Optional.empty()));
+ }
+
+ @Test
+ public void testF48Miles() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.CONVENTIONAL.toString(), true);
+ String content = FileReader.readFileInString("src/test/resources/responses/F48/status.json");
+ assertTrue(testVehicle(content,
+ STATUS_CONV + DOORS + RANGE_CONV + SERVICE_AVAILABLE + CHECK_AVAILABLE + POSITION, Optional.empty()));
+ }
+
+ @Test
+ public void testG31NBTEvo() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.CONVENTIONAL.toString(), false);
+ String content = FileReader.readFileInString("src/test/resources/responses/G31_NBTevo/status.json");
+ // assertTrue(testVehicle(content, 27, Optional.empty()));
+ assertTrue(testVehicle(content, STATUS_CONV + DOORS + RANGE_CONV + SERVICE_AVAILABLE + CHECK_EMPTY + POSITION,
+ Optional.empty()));
+ }
+
+ @Test
+ public void testG31NBTEvoMiles() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.CONVENTIONAL.toString(), true);
+ String content = FileReader.readFileInString("src/test/resources/responses/G31_NBTevo/status.json");
+ assertTrue(testVehicle(content, STATUS_CONV + DOORS + RANGE_CONV + SERVICE_AVAILABLE + CHECK_EMPTY + POSITION,
+ Optional.empty()));
+ }
+
+ @Test
+ public void testI01NoRex() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.ELECTRIC.toString(), false);
+ String content = FileReader.readFileInString("src/test/resources/responses/I01_NOREX/status.json");
+ assertTrue(testVehicle(content,
+ STATUS_ELECTRIC + DOORS + RANGE_ELECTRIC + SERVICE_AVAILABLE + CHECK_EMPTY + POSITION,
+ Optional.empty()));
+ }
+
+ @Test
+ public void testI01NoRexMiles() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.ELECTRIC.toString(), true);
+ String content = FileReader.readFileInString("src/test/resources/responses/I01_NOREX/status.json");
+ assertTrue(testVehicle(content,
+ STATUS_ELECTRIC + DOORS + RANGE_ELECTRIC + SERVICE_AVAILABLE + CHECK_EMPTY + POSITION,
+ Optional.empty()));
+ }
+
+ @Test
+ public void testI01Rex() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.ELECTRIC_REX.toString(), false);
+ String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/status.json");
+ assertTrue(testVehicle(content,
+ STATUS_ELECTRIC + DOORS + RANGE_HYBRID + SERVICE_AVAILABLE + CHECK_EMPTY + POSITION, Optional.empty()));
+ }
+
+ @Test
+ public void testI01RexMiles() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.ELECTRIC_REX.toString(), true);
+ String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/status.json");
+ assertTrue(testVehicle(content,
+ STATUS_ELECTRIC + DOORS + RANGE_HYBRID + SERVICE_AVAILABLE + CHECK_EMPTY + POSITION, Optional.empty()));
+ }
+
+ @Test
+ public void test318iF31() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.CONVENTIONAL.toString(), false);
+ String content = FileReader.readFileInString("src/test/resources/responses/F31/status-318i.json");
+ Map<String, State> m = new HashMap<String, State>();
+ m.put(ConnectedDriveConstants.WINDOWS, StringType.valueOf(Constants.INTERMEDIATE));
+ assertTrue(testVehicle(content, STATUS_CONV + DOORS + RANGE_CONV + SERVICE_AVAILABLE + CHECK_EMPTY + POSITION,
+ Optional.empty()));
+ }
+
+ @Test
+ public void test318iF31Miles() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.CONVENTIONAL.toString(), true);
+ String content = FileReader.readFileInString("src/test/resources/responses/F31/status-318i.json");
+ Map<String, State> m = new HashMap<String, State>();
+ m.put(ConnectedDriveConstants.WINDOWS, StringType.valueOf(Constants.INTERMEDIATE));
+ assertTrue(testVehicle(content, STATUS_CONV + DOORS + RANGE_CONV + SERVICE_AVAILABLE + CHECK_EMPTY + POSITION,
+ Optional.empty()));
+ }
+
+ @Test
+ public void testI01RexCompat() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.ELECTRIC_REX.toString(), false);
+ String content = FileReader.readFileInString("src/test/resources/api/vehicle/vehicle-ccm.json");
+ VehicleAttributesContainer vac = Converter.getGson().fromJson(content, VehicleAttributesContainer.class);
+ assertTrue(testVehicle(Converter.transformLegacyStatus(vac),
+ STATUS_ELECTRIC + DOORS + RANGE_HYBRID + SERVICE_AVAILABLE + CHECK_AVAILABLE + POSITION,
+ Optional.empty()));
+ }
+
+ @Test
+ public void testI01RexMilesCompat() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.ELECTRIC_REX.toString(), true);
+ String content = FileReader.readFileInString("src/test/resources/api/vehicle/vehicle-ccm.json");
+ VehicleAttributesContainer vac = Converter.getGson().fromJson(content, VehicleAttributesContainer.class);
+ assertTrue(testVehicle(Converter.transformLegacyStatus(vac),
+ STATUS_ELECTRIC + DOORS + RANGE_HYBRID + SERVICE_AVAILABLE + CHECK_AVAILABLE + POSITION,
+ Optional.empty()));
+ }
+
+ @Test
+ public void testF11Compat() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.CONVENTIONAL.toString(), false);
+ String content = FileReader.readFileInString("src/test/resources/responses/F11/vehicle-status.json");
+ VehicleAttributesContainer vac = Converter.getGson().fromJson(content, VehicleAttributesContainer.class);
+ assertTrue(testVehicle(Converter.transformLegacyStatus(vac),
+ STATUS_CONV + DOORS + RANGE_CONV + SERVICE_AVAILABLE + CHECK_EMPTY + POSITION, Optional.empty()));
+ }
+
+ @Test
+ public void testF11MilesCompat() {
+ logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+ setup(VehicleType.CONVENTIONAL.toString(), true);
+ String content = FileReader.readFileInString("src/test/resources/responses/F11/vehicle-status.json");
+ VehicleAttributesContainer vac = Converter.getGson().fromJson(content, VehicleAttributesContainer.class);
+ assertTrue(testVehicle(Converter.transformLegacyStatus(vac),
+ STATUS_CONV + DOORS + RANGE_CONV + SERVICE_AVAILABLE + CHECK_EMPTY + POSITION, Optional.empty()));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.util;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+/**
+ * The {@link FileReader} Helper Util to read test resource files
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class FileReader {
+
+ public static String readFileInString(String filename) {
+ try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(filename), "UTF-8"));) {
+ StringBuilder buf = new StringBuilder();
+ String sCurrentLine;
+
+ while ((sCurrentLine = br.readLine()) != null) {
+ buf.append(sCurrentLine);
+ }
+ return buf.toString();
+ } catch (IOException e) {
+ // fail if file cannot be read
+ assertTrue(false);
+ }
+ return Constants.UNDEF;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.util;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.Locale;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatus;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatusContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.core.library.types.DateTimeType;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link LocaleTest} is testing locale settings
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class LocaleTest {
+ private static final Gson GSON = new Gson();
+
+ @Test
+ public void languageTest() {
+ assertTrue(ConnectedDriveConstants.IMPERIAL_COUNTRIES.contains(Locale.UK.getCountry()), "United Kingdom");
+ assertTrue(ConnectedDriveConstants.IMPERIAL_COUNTRIES.contains(Locale.US.getCountry()), "United States");
+ assertFalse(ConnectedDriveConstants.IMPERIAL_COUNTRIES.contains(Locale.FRANCE.getCountry()), "France");
+ assertFalse(ConnectedDriveConstants.IMPERIAL_COUNTRIES.contains(Locale.GERMAN.getCountry()), "Germany");
+ }
+
+ public void testTimeUTCToLocaleTime() {
+ String resource1 = FileReader.readFileInString("src/test/resources/webapi/vehicle-status.json");
+ VehicleStatusContainer status = GSON.fromJson(resource1, VehicleStatusContainer.class);
+ VehicleStatus vStatus = status.vehicleStatus;
+
+ String inputTime = vStatus.internalDataTimeUTC;
+ String localeTime = Converter.getLocalDateTime(inputTime);
+ String dateTimeType = DateTimeType.valueOf(localeTime).toString();
+ assertEquals("2020-08-24T15:55:32", inputTime, "Input DateTime");
+ assertEquals("2020-08-24T17:55:32", localeTime, "Output DateTime");
+ assertEquals("2020-08-24T17:55:32.000+0200", dateTimeType, "DateTimeType Value");
+
+ inputTime = vStatus.updateTime;
+ localeTime = Converter.getLocalDateTime(inputTime);
+ dateTimeType = DateTimeType.valueOf(localeTime).toString();
+ assertEquals("2020-08-24T15:55:32+0000", inputTime, "Input DateTime");
+ assertEquals("2020-08-24T17:55:32", localeTime, "Output DateTime");
+ assertEquals("2020-08-24T17:55:32.000+0200", dateTimeType, "DateTimeType Value");
+
+ inputTime = vStatus.updateTime;
+ localeTime = Converter.getLocalDateTimeWithoutOffest(inputTime);
+ dateTimeType = DateTimeType.valueOf(localeTime).toString();
+ assertEquals("2020-08-24T15:55:32+0000", inputTime, "Input DateTime");
+ assertEquals("2020-08-24T15:55:32", localeTime, "Output DateTime");
+ assertEquals("2020-08-24T15:55:32.000+0200", dateTimeType, "DateTimeType Value");
+ }
+
+ @Test
+ public void testDistance() {
+ double lat = 45.678;
+ double lon = 8.765;
+ double distance = 0.005;
+ double dist = Converter.measureDistance(lat, lon, lat + distance, lon + distance);
+ assertTrue(dist < 1, "Distance below 1 km");
+ }
+}
--- /dev/null
+{
+ "communitySwitch": false,
+ "modelType": "I3",
+ "scoreList": [
+ {
+ "attrName": "AVERAGE_ELECTRIC_CONSUMPTION",
+ "attrUnit": "KWH_PER_100KM",
+ "minValue": 0.0,
+ "maxValue": 40.0,
+ "lifeTime": 16.5
+ },
+ {
+ "attrName": "AVERAGE_RECUPERATED_ENERGY_PER_100_KM",
+ "attrUnit": "KWH_PER_100KM",
+ "minValue": 0.0,
+ "maxValue": 20.0,
+ "lifeTime": 4.5
+ },
+ {
+ "attrName": "CUMULATED_ELECTRIC_DRIVEN_DISTANCE",
+ "attrUnit": "KM",
+ "minValue": 0.0,
+ "maxValue": 16593.4,
+ "lifeTime": 16592.4
+ },
+ {
+ "attrName": "LONGEST_DISTANCE_WITHOUT_CHARGING",
+ "attrUnit": "KM",
+ "minValue": 0.0,
+ "maxValue": 270.0,
+ "lifeTime": 185.5
+ }
+ ],
+ "lastTripList": [
+ {
+ "name": "LASTTRIP_DELTA_KM",
+ "unit": "KM",
+ "lastTrip": "2.0"
+ },
+ {
+ "name": "ACTUAL_DISTANCE_WITHOUT_CHARGING",
+ "unit": "KM",
+ "lastTrip": "31.0"
+ },
+ {
+ "name": "AVERAGE_ELECTRIC_CONSUMPTION",
+ "unit": "KWH_PER_100KM",
+ "lastTrip": "14.5"
+ },
+ {
+ "name": "AVERAGE_RECUPERATED_ENERGY_PER_100_KM",
+ "unit": "KWH_PER_100KM",
+ "lastTrip": "8.0"
+ },
+ {
+ "name": "CUMULATED_ELECTRIC_DRIVEN_DISTANCE",
+ "unit": "KM",
+ "lastTrip": "16592.4"
+ }
+ ],
+ "lifeTimeList": [],
+ "efficiencyQuotient": 44,
+ "characteristicList": [
+ {
+ "characteristic": "TOTAL_CONSUMPTION",
+ "quantity": 2
+ },
+ {
+ "characteristic": "AUXILIARY_CONSUMPTION",
+ "quantity": 2
+ },
+ {
+ "characteristic": "DRIVING_MODE",
+ "quantity": 0
+ },
+ {
+ "characteristic": "ACCELERATION",
+ "quantity": 3
+ },
+ {
+ "characteristic": "ANTICIPATION",
+ "quantity": 3
+ }
+ ]
+}
\ No newline at end of file
--- /dev/null
+{
+ "latitude": 56.789,
+ "longitude": 8.765,
+ "isoCountryCode": "DEU",
+ "auxPowerRegular": 1.4,
+ "auxPowerEcoPro": 1.2,
+ "auxPowerEcoProPlus": 0.4,
+ "soc": 25.952999114990234,
+ "pendingUpdate": false,
+ "vehicleTracking": true,
+ "socmax": 29.84
+}
\ No newline at end of file
--- /dev/null
+{
+ "attributesMap": {
+ "unitOfLength": "km",
+ "sunroof_state": "CLOSED",
+ "chargingLogicCurrentlyActive": "NOT_CHARGING",
+ "vehicle_tracking": "1",
+ "chargeNowAllowed": "NOT_ALLOWED",
+ "updateTime_converted": "16.08.2020 15:34",
+ "door_driver_rear": "CLOSED",
+ "head_unit_pu_software": "07/16",
+ "beMaxRangeElectricKm": "203.0",
+ "door_passenger_rear": "CLOSED",
+ "beRemainingRangeFuelKm": "62.0",
+ "Segment_LastTrip_time_segment_end_formatted_date": "15.08.2020",
+ "door_driver_front": "CLOSED",
+ "shdStatusUnified": "CLOSED",
+ "hood_state": "CLOSED",
+ "charging_status": "CHARGINGACTIVE",
+ "kombi_current_remaining_range_fuel": "62.0",
+ "beMaxRangeElectric": "203.0",
+ "window_driver_rear": "CLOSED",
+ "beRemainingRangeElectricKm": "203.0",
+ "mileage": "17044",
+ "Segment_LastTrip_time_segment_end_formatted_time": "19:11",
+ "beMaxRangeElectricMile": "126.0",
+ "Segment_LastTrip_time_segment_end_formatted": "15.08.2020 19:11",
+ "lastChargingEndResult": "UNKNOWN",
+ "unitOfEnergy": "kWh",
+ "beRemainingRangeElectric": "203.0",
+ "sunroof_position": "0",
+ "soc_hv_percent": "65.4",
+ "single_immediate_charging": "isUnused",
+ "updateTime_converted_time": "15:34",
+ "chargingHVStatus": "FINISHED_FULLY_CHARGED",
+ "connectorStatus": "CONNECTED",
+ "chargingLevelHv": "100.0",
+ "chargingSystemStatus": "CHARGINGACTIVE",
+ "fuelPercent": "47",
+ "unitOfCombustionConsumption": "l/100km",
+ "gps_lat": "xxx",
+ "window_driver_front": "CLOSED",
+ "Segment_LastTrip_ratio_electric_driven_distance": "100",
+ "gps_lng": "xxx",
+ "condition_based_services": "00003,OK,2021-11,;00017,OK,2021-11,;00001,OK,2021-11,;00032,OK,2021-11,",
+ "window_passenger_front": "CLOSED",
+ "window_passenger_rear": "CLOSED",
+ "lastChargingEndReason": "UNKNOWN",
+ "updateTime_converted_date": "16.08.2020",
+ "beRemainingRangeFuelMile": "38.0",
+ "beRemainingRangeFuel": "62.0",
+ "door_passenger_front": "CLOSED",
+ "updateTime_converted_timestamp": "1597592093000",
+ "remaining_fuel": "4",
+ "charging_inductive_positioning": "not_positioned",
+ "heading": "221",
+ "lsc_trigger": "DOORSTATECHANGED",
+ "lights_parking": "OFF",
+ "door_lock_state": "UNLOCKED",
+ "updateTime": "16.08.2020 14:34:53 UTC",
+ "prognosisWhileChargingStatus": "IS_PERFORMED",
+ "head_unit": "EntryNav",
+ "trunk_state": "CLOSED",
+ "battery_size_max": "33200",
+ "charging_connection_type": "CONDUCTIVE",
+ "beRemainingRangeElectricMile": "126.0",
+ "unitOfElectricConsumption": "kWh/100km",
+ "Segment_LastTrip_time_segment_end": "15.08.2020 19:11:00 UTC",
+ "lastUpdateReason": "DOORSTATECHANGED"
+ },
+ "vehicleMessages": {
+ "ccmMessages": [],
+ "cbsMessages": [
+ {
+ "description": "Nächster Wechsel spätestens zum angegebenen Termin.",
+ "text": "Bremsflüssigkeit",
+ "id": 3,
+ "status": "OK",
+ "messageType": "CBS",
+ "date": "2021-11"
+ },
+ {
+ "description": "Nächste Sichtprüfung nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+ "text": "Fahrzeug-Check",
+ "id": 17,
+ "status": "OK",
+ "messageType": "CBS",
+ "date": "2021-11"
+ },
+ {
+ "description": "Nächster Wechsel nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+ "text": "Motoröl",
+ "id": 1,
+ "status": "OK",
+ "messageType": "CBS",
+ "date": "2021-11"
+ },
+ {
+ "description": "Nächste gesetzliche Fahrzeuguntersuchung zum angegebenen Termin.",
+ "text": "§ Fahrzeuguntersuchung",
+ "id": 32,
+ "status": "OK",
+ "messageType": "CBS",
+ "date": "2021-11"
+ }
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "attributesMap": {
+ "unitOfLength": "km",
+ "sunroof_state": "CLOSED",
+ "chargingLogicCurrentlyActive": "NOT_CHARGING",
+ "vehicle_tracking": "1",
+ "chargeNowAllowed": "ALLOWED",
+ "updateTime_converted": "27.09.2020 13:18",
+ "door_driver_rear": "CLOSED",
+ "head_unit_pu_software": "07/16",
+ "beMaxRangeElectricKm": "203.0",
+ "door_passenger_rear": "CLOSED",
+ "beRemainingRangeFuelKm": "65.0",
+ "Segment_LastTrip_time_segment_end_formatted_date": "27.09.2020",
+ "door_driver_front": "CLOSED",
+ "shdStatusUnified": "CLOSED",
+ "hood_state": "CLOSED",
+ "charging_status": "CHARGINGERROR",
+ "kombi_current_remaining_range_fuel": "65.0",
+ "beMaxRangeElectric": "203.0",
+ "window_driver_rear": "CLOSED",
+ "beRemainingRangeElectricKm": "100.0",
+ "mileage": "18313",
+ "Segment_LastTrip_time_segment_end_formatted_time": "13:24",
+ "beMaxRangeElectricMile": "126.0",
+ "Segment_LastTrip_time_segment_end_formatted": "27.09.2020 13:24",
+ "lastChargingEndResult": "FAILED",
+ "check_control_messages": "00804,18312",
+ "unitOfEnergy": "kWh",
+ "beRemainingRangeElectric": "100.0",
+ "sunroof_position": "0",
+ "soc_hv_percent": "51.6",
+ "single_immediate_charging": "isUnused",
+ "updateTime_converted_time": "13:18",
+ "chargingHVStatus": "ERROR",
+ "connectorStatus": "CONNECTED",
+ "chargingLevelHv": "51.0",
+ "chargingSystemStatus": "CHARGINGERROR",
+ "fuelPercent": "47",
+ "unitOfCombustionConsumption": "l/100km",
+ "gps_lat": "56.789",
+ "window_driver_front": "CLOSED",
+ "Segment_LastTrip_ratio_electric_driven_distance": "100",
+ "gps_lng": "8.765",
+ "condition_based_services": "00003,OK,2021-11,;00017,OK,2021-11,;00001,OK,2021-11,;00032,OK,2021-11,",
+ "window_passenger_front": "CLOSED",
+ "targetSoc": "100.0",
+ "window_passenger_rear": "CLOSED",
+ "lastChargingEndReason": "POWERGRID_FAILED",
+ "updateTime_converted_date": "27.09.2020",
+ "beRemainingRangeFuelMile": "40.0",
+ "beRemainingRangeFuel": "65.0",
+ "door_passenger_front": "CLOSED",
+ "updateTime_converted_timestamp": "1601212738000",
+ "remaining_fuel": "4",
+ "charging_inductive_positioning": "not_positioned",
+ "heading": "39",
+ "lsc_trigger": "CHARGINGINTERRUPTED",
+ "lights_parking": "OFF",
+ "door_lock_state": "SECURED",
+ "updateTime": "27.09.2020 13:18:58 UTC",
+ "prognosisWhileChargingStatus": "NOT_NEEDED",
+ "head_unit": "EntryNav",
+ "trunk_state": "CLOSED",
+ "battery_size_max": "33200",
+ "charging_connection_type": "CONDUCTIVE",
+ "beRemainingRangeElectricMile": "62.0",
+ "unitOfElectricConsumption": "kWh/100km",
+ "Segment_LastTrip_time_segment_end": "27.09.2020 13:24:00 UTC",
+ "lastUpdateReason": "CHARGINGINTERRUPTED"
+ },
+ "vehicleMessages": {
+ "ccmMessages": [
+ {
+ "text": "Laden nicht möglich",
+ "id": 804,
+ "status": "NULL",
+ "messageType": "CCM",
+ "unitOfLengthRemaining": "18312"
+ }
+ ],
+ "cbsMessages": [
+ {
+ "description": "Nächster Wechsel spätestens zum angegebenen Termin.",
+ "text": "Bremsflüssigkeit",
+ "id": 3,
+ "status": "OK",
+ "messageType": "CBS",
+ "date": "2021-11"
+ },
+ {
+ "description": "Nächste Sichtprüfung nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+ "text": "Fahrzeug-Check",
+ "id": 17,
+ "status": "OK",
+ "messageType": "CBS",
+ "date": "2021-11"
+ },
+ {
+ "description": "Nächster Wechsel nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+ "text": "Motoröl",
+ "id": 1,
+ "status": "OK",
+ "messageType": "CBS",
+ "date": "2021-11"
+ },
+ {
+ "description": "Nächste gesetzliche Fahrzeuguntersuchung zum angegebenen Termin.",
+ "text": "§ Fahrzeuguntersuchung",
+ "id": 32,
+ "status": "OK",
+ "messageType": "CBS",
+ "date": "2021-11"
+ }
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "attributesMap": {
+ "unitOfLength": "km",
+ "sunroof_state": "CLOSED",
+ "chargingLogicCurrentlyActive": "NOT_CHARGING",
+ "vehicle_tracking": "1",
+ "chargeNowAllowed": "NOT_ALLOWED",
+ "updateTime_converted": "22.08.2020 13:55",
+ "door_driver_rear": "CLOSED",
+ "head_unit_pu_software": "07/16",
+ "beMaxRangeElectricKm": "209.0",
+ "door_passenger_rear": "CLOSED",
+ "beRemainingRangeFuelKm": "67.0",
+ "Segment_LastTrip_time_segment_end_formatted_date": "22.08.2020",
+ "door_driver_front": "CLOSED",
+ "shdStatusUnified": "CLOSED",
+ "hood_state": "CLOSED",
+ "charging_status": "NOCHARGING",
+ "kombi_current_remaining_range_fuel": "67.0",
+ "beMaxRangeElectric": "209.0",
+ "window_driver_rear": "CLOSED",
+ "beRemainingRangeElectricKm": "179.0",
+ "mileage": "17236",
+ "Segment_LastTrip_time_segment_end_formatted_time": "14:52",
+ "beMaxRangeElectricMile": "129.0",
+ "Segment_LastTrip_time_segment_end_formatted": "22.08.2020 14:52",
+ "lastChargingEndResult": "SUCCESS",
+ "check_control_messages": "",
+ "unitOfEnergy": "kWh",
+ "beRemainingRangeElectric": "179.0",
+ "sunroof_position": "0",
+ "soc_hv_percent": "82.6",
+ "single_immediate_charging": "isUnused",
+ "updateTime_converted_time": "13:55",
+ "chargingHVStatus": "INVALID",
+ "connectorStatus": "DISCONNECTED",
+ "chargingLevelHv": "89.0",
+ "chargingSystemStatus": "NOCHARGING",
+ "fuelPercent": "47",
+ "unitOfCombustionConsumption": "l/100km",
+ "gps_lat": "56.789",
+ "window_driver_front": "CLOSED",
+ "Segment_LastTrip_ratio_electric_driven_distance": "100",
+ "gps_lng": "8.765",
+ "condition_based_services": "00003,OK,2021-11,;00017,OK,2021-11,;00001,OK,2021-11,;00032,OK,2021-11,",
+ "window_passenger_front": "CLOSED",
+ "window_passenger_rear": "CLOSED",
+ "lastChargingEndReason": "CHARGING_GOAL_REACHED",
+ "updateTime_converted_date": "22.08.2020",
+ "beRemainingRangeFuelMile": "41.0",
+ "beRemainingRangeFuel": "67.0",
+ "door_passenger_front": "CLOSED",
+ "updateTime_converted_timestamp": "1598104546000",
+ "remaining_fuel": "4",
+ "charging_inductive_positioning": "not_positioned",
+ "heading": "41",
+ "lsc_trigger": "VEHCSHUTDOWN_SECURED",
+ "lights_parking": "OFF",
+ "door_lock_state": "SECURED",
+ "updateTime": "22.08.2020 12:55:46 UTC",
+ "prognosisWhileChargingStatus": "NOT_NEEDED",
+ "head_unit": "EntryNav",
+ "trunk_state": "CLOSED",
+ "battery_size_max": "33200",
+ "charging_connection_type": "CONDUCTIVE",
+ "beRemainingRangeElectricMile": "111.0",
+ "unitOfElectricConsumption": "kWh/100km",
+ "Segment_LastTrip_time_segment_end": "22.08.2020 14:52:00 UTC",
+ "lastUpdateReason": "VEHCSHUTDOWN_SECURED"
+ },
+ "vehicleMessages": {
+ "ccmMessages": [],
+ "cbsMessages": [
+ {
+ "description": "Nächster Wechsel spätestens zum angegebenen Termin.",
+ "text": "Bremsflüssigkeit",
+ "id": 3,
+ "status": "OK",
+ "messageType": "CBS",
+ "date": "2021-11"
+ },
+ {
+ "description": "Nächste Sichtprüfung nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+ "text": "Fahrzeug-Check",
+ "id": 17,
+ "status": "OK",
+ "messageType": "CBS",
+ "date": "2021-11"
+ },
+ {
+ "description": "Nächster Wechsel nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+ "text": "Motoröl",
+ "id": 1,
+ "status": "OK",
+ "messageType": "CBS",
+ "date": "2021-11"
+ },
+ {
+ "description": "Nächste gesetzliche Fahrzeuguntersuchung zum angegebenen Termin.",
+ "text": "§ Fahrzeuguntersuchung",
+ "id": 32,
+ "status": "OK",
+ "messageType": "CBS",
+ "date": "2021-11"
+ }
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "vehicleStatus": {
+ "vin": "ANONYMOUS",
+ "mileage": 17131,
+ "updateReason": "VEHICLE_SECURED",
+ "updateTime": "2020-08-18T17:54:12+0000",
+ "doorDriverFront": "CLOSED",
+ "doorDriverRear": "CLOSED",
+ "doorPassengerFront": "CLOSED",
+ "doorPassengerRear": "CLOSED",
+ "windowDriverFront": "CLOSED",
+ "windowDriverRear": "CLOSED",
+ "windowPassengerFront": "CLOSED",
+ "windowPassengerRear": "CLOSED",
+ "sunroof": "CLOSED",
+ "trunk": "CLOSED",
+ "rearWindow": "INVALID",
+ "hood": "CLOSED",
+ "doorLockState": "SECURED",
+ "parkingLight": "OFF",
+ "positionLight": "ON",
+ "remainingFuel": 4,
+ "remainingRangeElectric": 184,
+ "remainingRangeElectricMls": 114,
+ "remainingRangeFuel": 72,
+ "remainingRangeFuelMls": 44,
+ "maxRangeElectric": 224,
+ "maxRangeElectricMls": 139,
+ "maxFuel": 8.5,
+ "connectionStatus": "DISCONNECTED",
+ "chargingStatus": "INVALID",
+ "chargingLevelHv": 84,
+ "lastChargingEndReason": "END_REQUESTED_BY_DRIVER",
+ "lastChargingEndResult": "SUCCESS",
+ "position": {
+ "lat": 56.789,
+ "lon": 8.765,
+ "heading": 41,
+ "status": "OK"
+ },
+ "internalDataTimeUTC": "2020-08-18T17:54:12",
+ "singleImmediateCharging": false,
+ "chargingConnectionType": "CONDUCTIVE",
+ "chargingInductivePositioning": "NOT_POSITIONED",
+ "vehicleCountry": "DE",
+ "checkControlMessages": [],
+ "cbsData": [
+ {
+ "cbsType": "BRAKE_FLUID",
+ "cbsState": "OK",
+ "cbsDueDate": "2021-11",
+ "cbsDescription": "Next change due at the latest by the stated date."
+ },
+ {
+ "cbsType": "VEHICLE_CHECK",
+ "cbsState": "OK",
+ "cbsDueDate": "2021-11",
+ "cbsDescription": "Next visual inspection due when the stated distance has been covered or by the stated date."
+ },
+ {
+ "cbsType": "OIL",
+ "cbsState": "OK",
+ "cbsDueDate": "2021-11",
+ "cbsDescription": "Next change due when the stated distance has been covered or by the specified date."
+ },
+ {
+ "cbsType": "VEHICLE_TUV",
+ "cbsState": "OK",
+ "cbsDueDate": "2021-11",
+ "cbsDescription": "Next statutory vehicle inspection due by the stated date."
+ }
+ ],
+ "DCS_CCH_Activation": "NA",
+ "DCS_CCH_Ongoing": false
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "allTrips": {
+ "avgElectricConsumption": {
+ "communityLow": -4.74,
+ "communityAverage": 5.8,
+ "communityHigh": 21.04,
+ "userAverage": 6.73
+ },
+ "avgRecuperation": {
+ "communityLow": 1.27,
+ "communityAverage": 5.29,
+ "communityHigh": 19.77,
+ "userAverage": 5.1
+ },
+ "chargecycleRange": {
+ "communityAverage": 11.04,
+ "communityHigh": 169.59,
+ "userAverage": 23.74,
+ "userHigh": 3.62,
+ "userCurrentChargeCycle": 28.06
+ },
+ "totalElectricDistance": {
+ "communityLow": 20.33,
+ "communityAverage": 16575.07,
+ "communityHigh": 63175.2,
+ "userTotal": 20102.5
+ },
+ "avgCombinedConsumption": {
+ "communityLow": 2.45,
+ "communityAverage": 6.08,
+ "communityHigh": 10.86,
+ "userAverage": 4.93
+ },
+ "savedCO2": 557.382,
+ "savedCO2greenEnergy": 3278.717,
+ "totalSavedFuel": 1156.8,
+ "resetDate": "2020-09-19T12:54:20+0000",
+ "batterySizeMax": 9140
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "weeklyPlanner": {
+ "climatizationEnabled": true,
+ "chargingMode": "IMMEDIATE_CHARGING",
+ "chargingPreferences": "CHARGING_WINDOW",
+ "timer1": {
+ "departureTime": "07:10",
+ "timerEnabled": true,
+ "weekdays": [
+ "MONDAY",
+ "TUESDAY",
+ "WEDNESDAY",
+ "THURSDAY",
+ "FRIDAY"
+ ]
+ },
+ "timer2": {
+ "departureTime": "09:00",
+ "timerEnabled": true,
+ "weekdays": [
+ "SATURDAY",
+ "SUNDAY"
+ ]
+ },
+ "timer3": {
+ "departureTime": "00:00",
+ "timerEnabled": false,
+ "weekdays": []
+ },
+ "overrideTimer": {
+ "departureTime": "09:00",
+ "timerEnabled": false,
+ "weekdays": [
+ "SUNDAY"
+ ]
+ },
+ "preferredChargingWindow": {
+ "startTime": "00:00",
+ "endTime": "00:00"
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "vehicles": [
+ {
+ "vin": "ANONYMOUS",
+ "model": "530e iPerformance",
+ "driveTrain": "PHEV",
+ "brand": "BMW",
+ "yearOfConstruction": 2018,
+ "bodytype": "G30",
+ "color": "SCHWARZ II",
+ "statisticsCommunityEnabled": false,
+ "statisticsAvailable": true,
+ "hasAlarmSystem": true,
+ "dealer": {
+ "name": "ANONYMOUS",
+ "street": "ANONYMOUS",
+ "postalCode": "ANONYMOUS",
+ "city": "ANONYMOUS",
+ "country": "ANONYMOUS",
+ "phone": "ANONYMOUS"
+ },
+ "breakdownNumber": "ANONYMOUS",
+ "supportedChargingModes": [
+ "AC_LOW"
+ ],
+ "chargingControl": "WEEKLY_PLANNER",
+ "vehicleFinder": "ACTIVATED",
+ "hornBlow": "ACTIVATED",
+ "lightFlash": "ACTIVATED",
+ "doorLock": "ACTIVATED",
+ "doorUnlock": "ACTIVATED",
+ "climateNow": "ACTIVATED",
+ "sendPoi": "ACTIVATED",
+ "remote360": "NOT_SUPPORTED",
+ "climateControl": "NOT_SUPPORTED",
+ "chargeNow": "NOT_SUPPORTED",
+ "lastDestinations": "NOT_SUPPORTED",
+ "carCloud": "ACTIVATED",
+ "remoteSoftwareUpgrade": "NOT_SUPPORTED",
+ "climateNowRES": "NOT_SUPPORTED",
+ "climateControlRES": "NOT_SUPPORTED",
+ "smartSolution": "NOT_SUPPORTED",
+ "ipa": "NOT_SUPPORTED"
+ }
+ ]
+}
\ No newline at end of file
--- /dev/null
+{
+ "lastTrip": {
+ "efficiencyValue": 1,
+ "totalDistance": 2,
+ "electricDistance": 2,
+ "avgElectricConsumption": 9,
+ "avgRecuperation": 9,
+ "drivingModeValue": 0.5,
+ "totalConsumptionValue": 1.25,
+ "avgCombinedConsumption": 0,
+ "electricDistanceRatio": 100,
+ "savedFuel": 1.53,
+ "date": "2020-09-19T16:03:00+0000",
+ "duration": 3,
+ "chargingBehaviorValue": 1.25,
+ "electricDistanceShareValue": 1.25
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "vehicleStatus": {
+ "mileage": 47035,
+ "remainingFuel": 18.0,
+ "remainingRangeElectric": 3.0,
+ "remainingRangeElectricMls": 1.0,
+ "remainingRangeFuel": 249.0,
+ "remainingRangeFuelMls": 154.0,
+ "maxRangeElectric": 32.0,
+ "maxRangeElectricMls": 19.0,
+ "maxFuel": 0.0,
+ "chargingLevelHv": 17.0,
+ "vin": "ANONYMOUS",
+ "updateReason": "VEHICLE_SHUTDOWN_SECURED",
+ "updateTime": "2020-09-19T14:04:25+0000",
+ "doorDriverFront": "CLOSED",
+ "doorDriverRear": "CLOSED",
+ "doorPassengerFront": "CLOSED",
+ "doorPassengerRear": "CLOSED",
+ "windowDriverFront": "CLOSED",
+ "windowDriverRear": "CLOSED",
+ "windowPassengerFront": "CLOSED",
+ "windowPassengerRear": "CLOSED",
+ "trunk": "CLOSED",
+ "rearWindow": "INVALID",
+ "hood": "CLOSED",
+ "doorLockState": "SECURED",
+ "parkingLight": "OFF",
+ "positionLight": "OFF",
+ "connectionStatus": "DISCONNECTED",
+ "chargingStatus": "INVALID",
+ "lastChargingEndReason": "CHARGING_GOAL_REACHED",
+ "lastChargingEndResult": "SUCCESS",
+ "position": {
+ "lat": -1.0,
+ "lon": -1.0,
+ "heading": -1,
+ "status": "OK"
+ },
+ "internalDataTimeUTC": "2020-09-19T14:04:25",
+ "singleImmediateCharging": false,
+ "chargingConnectionType": "CONDUCTIVE",
+ "chargingInductivePositioning": "NOT_POSITIONED",
+ "vehicleCountry": "SE",
+ "DCS_CCH_Activation": "NA",
+ "DCS_CCH_Ongoing": false,
+ "checkControlMessages": [],
+ "cbsData": [
+ {
+ "cbsType": "OIL",
+ "cbsState": "OK",
+ "cbsDueDate": "2022-01",
+ "cbsDescription": "Next service due when the stated distance has been covered or by the specified date.",
+ "cbsRemainingMileage": 19000
+ },
+ {
+ "cbsType": "VEHICLE_CHECK",
+ "cbsState": "OK",
+ "cbsDueDate": "2022-01",
+ "cbsDescription": "Next visual inspection due when the stated distance has been covered or by the stated date.",
+ "cbsRemainingMileage": 19000
+ },
+ {
+ "cbsType": "BRAKE_FLUID",
+ "cbsState": "OK",
+ "cbsDueDate": "2021-11",
+ "cbsDescription": "Next change due at the latest by the stated date.",
+ "cbsRemainingMileage": 0
+ }
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "attributesMap": {
+ "remaining_fuel": "34",
+ "unitOfCombustionConsumption": "l/100km",
+ "unitOfLength": "km",
+ "vehicle_tracking": "0",
+ "unitOfEnergy": "kWh",
+ "head_unit_pu_software": "07/14",
+ "condition_based_services": "00001,PENDING,2021-08,2000;00100,OK,2023-08,23000;00003,OK,2021-08,",
+ "head_unit": "NBT",
+ "unitOfElectricConsumption": "kWh/100km",
+ "lastUpdateReason": "Error",
+ "mileage": "113930"
+ },
+ "vehicleMessages": {
+ "ccmMessages": [],
+ "cbsMessages": [
+ {
+ "description": "Onderhoud binnenkort nodig. Maak a.u.b. een afspraak met uw Servicepartner.",
+ "text": "Motorolie",
+ "id": 1,
+ "status": "PENDING",
+ "messageType": "CBS",
+ "date": "2021-08",
+ "unitOfLengthRemaining": "2000"
+ },
+ {
+ "description": "Volgende optische controle uiterlijk na afloop van de aangegeven afstand of uiterlijk op het aangegeven tijdstip",
+ "text": "Voertuig check",
+ "id": 100,
+ "status": "OK",
+ "messageType": "CBS",
+ "date": "2023-08",
+ "unitOfLengthRemaining": "23000"
+ },
+ {
+ "description": "Volgende vervanging uiterlijk op het aangegeven tijdstip.",
+ "text": "Remvloeistof",
+ "id": 3,
+ "status": "OK",
+ "messageType": "CBS",
+ "date": "2021-08"
+ }
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "vehicleStatus" : {
+ "doorPassengerFront" : "CLOSED",
+ "cbsData" : [ {
+ "cbsState" : "OK",
+ "cbsRemainingMileage" : 47000,
+ "cbsDescription" : "Next change due at the latest when the stated distance has been covered.",
+ "cbsType" : "BRAKE_PADS_FRONT"
+ }, {
+ "cbsState" : "OK",
+ "cbsRemainingMileage" : 46000,
+ "cbsDescription" : "Next change due at the latest when the stated distance has been covered.",
+ "cbsType" : "BRAKE_PADS_REAR"
+ }, {
+ "cbsDueDate" : "2018-06",
+ "cbsState" : "OK",
+ "cbsRemainingMileage" : 12000,
+ "cbsDescription" : "Next service due when the stated distance has been covered or by the specified date.",
+ "cbsType" : "OIL"
+ }, {
+ "cbsDueDate" : "2021-06",
+ "cbsState" : "OK",
+ "cbsRemainingMileage" : 50000,
+ "cbsDescription" : "Next visual inspection due when the stated distance has been covered or by the stated date.",
+ "cbsType" : "VEHICLE_CHECK"
+ }, {
+ "cbsDueDate" : "2020-04",
+ "cbsState" : "OK",
+ "cbsDescription" : "Next change due at the latest by the stated date.",
+ "cbsType" : "BRAKE_FLUID"
+ } ],
+ "windowDriverFront" : "CLOSED",
+ "DCS_CCH_Ongoing" : false,
+ "updateReason" : "VEHICLE_SHUTDOWN",
+ "rearWindow" : "INVALID",
+ "remainingRangeFuelMls" : 154,
+ "DCS_CCH_Activation" : "NA",
+ "singleImmediateCharging" : false,
+ "positionLight" : "OFF",
+ "vin" : "F15_VIN",
+ "doorDriverFront" : "CLOSED",
+ "mileage" : 1629,
+ "checkControlMessages" : [ ],
+ "parkingLight" : "OFF",
+ "windowDriverRear" : "CLOSED",
+ "steering" : "LH",
+ "updateTime" : "2018-03-08T22:21:39-0500",
+ "remainingFuel" : 30,
+ "windowPassengerRear" : "CLOSED",
+ "trunk" : "CLOSED",
+ "hood" : "CLOSED",
+ "internalDataTimeUTC" : "2018-03-09T03:21:39",
+ "windowPassengerFront" : "CLOSED",
+ "doorLockState" : "UNLOCKED",
+ "doorPassengerRear" : "CLOSED",
+ "remainingRangeFuel" : 249,
+ "doorDriverRear" : "CLOSED",
+ "sunroof" : "CLOSED",
+ "position" : {
+ "heading" : 11,
+ "lon" : -23.123,
+ "lat" : 23.123,
+ "status" : "OK"
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "vehicleStatus": {
+ "mileage": 26112,
+ "remainingFuel": 55.0,
+ "remainingRangeElectric": 0.0,
+ "remainingRangeElectricMls": 0.0,
+ "remainingRangeFuel": 879.0,
+ "remainingRangeFuelMls": 546.0,
+ "maxRangeElectric": 0.0,
+ "maxRangeElectricMls": 0.0,
+ "maxFuel": 0.0,
+ "chargingLevelHv": 0.0,
+ "vin": "ANONYMOUS",
+ "updateReason": "DOOR_STATE_CHANGED",
+ "updateTime": "2020-09-28T13:17:20+0000",
+ "doorDriverFront": "CLOSED",
+ "doorDriverRear": "CLOSED",
+ "doorPassengerFront": "CLOSED",
+ "doorPassengerRear": "CLOSED",
+ "windowDriverFront": "CLOSED",
+ "windowDriverRear": "CLOSED",
+ "windowPassengerFront": "CLOSED",
+ "windowPassengerRear": "CLOSED",
+ "sunroof": "UNKOWN",
+ "trunk": "CLOSED",
+ "rearWindow": "CLOSED",
+ "hood": "CLOSED",
+ "doorLockState": "SECURED",
+ "parkingLight": "OFF",
+ "positionLight": "OFF",
+ "position": {
+ "lat": -1.0,
+ "lon": -1.0,
+ "heading": -1,
+ "status": "OK"
+ },
+ "internalDataTimeUTC": "2020-09-28T13:16:36",
+ "singleImmediateCharging": false,
+ "vehicleCountry": "NL",
+ "DCS_CCH_Activation": "NA",
+ "DCS_CCH_Ongoing": false,
+ "checkControlMessages": [],
+ "cbsData": [
+ {
+ "cbsType": "OIL",
+ "cbsState": "OK",
+ "cbsDueDate": "2022-07",
+ "cbsDescription": "Next service due when the stated distance has been covered or by the specified date.",
+ "cbsRemainingMileage": 28000
+ },
+ {
+ "cbsType": "VEHICLE_CHECK",
+ "cbsState": "OK",
+ "cbsDueDate": "2022-07",
+ "cbsDescription": "Next visual inspection due when the stated distance has been covered or by the stated date.",
+ "cbsRemainingMileage": 28000
+ },
+ {
+ "cbsType": "BRAKE_FLUID",
+ "cbsState": "OK",
+ "cbsDueDate": "2022-01",
+ "cbsDescription": "Next change due at the latest by the stated date.",
+ "cbsRemainingMileage": -1
+ }
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "DCS_CCH_Activation": "NA",
+ "DCS_CCH_Ongoing": false,
+ "vehicleStatus": {
+ "position": {
+ "lat": 12.3456,
+ "lon": 34.5678,
+ "status": "OK"
+ },
+ "steering": "RH",
+ "updateTime": "2018-07-25T16:02:04+0200",
+ "vin": "F31_VIN"
+ }
+}
--- /dev/null
+{
+ "vehicleStatus": {
+ "DCS_CCH_Activation": "NA",
+ "DCS_CCH_Ongoing": false,
+ "position": {
+ "status": "DRIVER_DISABLED"
+ },
+ "updateTime": "2018-07-16T21:47:46+0000",
+ "vehicleCountry": "CN",
+ "vin": "some_vin"
+ }
+}
--- /dev/null
+{
+ "vehicleStatus": {
+ "position": {
+ "lat": 12.3456,
+ "lon": 34.5678,
+ "status": "OK"
+ },
+ "steering": "LH",
+ "updateTime": "2018-04-06T13:56:47+0200",
+ "vin": "F45_vin"
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "vehicleStatus" : {
+ "doorPassengerFront" : "CLOSED",
+ "cbsData" : [ {
+ "cbsDueDate" : "2019-07",
+ "cbsState" : "OK",
+ "cbsRemainingMileage" : 9000,
+ "cbsDescription" : "Next service due when the stated distance has been covered or by the specified date.",
+ "cbsType" : "OIL"
+ }, {
+ "cbsDueDate" : "2021-07",
+ "cbsState" : "OK",
+ "cbsRemainingMileage" : 39000,
+ "cbsDescription" : "Next visual inspection due when the stated distance has been covered or by the stated date.",
+ "cbsType" : "VEHICLE_CHECK"
+ }, {
+ "cbsDueDate" : "2020-07",
+ "cbsState" : "OK",
+ "cbsDescription" : "Next change due at the latest by the stated date.",
+ "cbsType" : "BRAKE_FLUID"
+ } ],
+ "windowDriverFront" : "CLOSED",
+ "DCS_CCH_Ongoing" : false,
+ "updateReason" : "DOOR_STATE_CHANGED",
+ "rearWindow" : "INVALID",
+ "remainingRangeFuelMls" : 366,
+ "DCS_CCH_Activation" : "NA",
+ "singleImmediateCharging" : false,
+ "positionLight" : "OFF",
+ "vin" : "F48_VIN",
+ "doorDriverFront" : "CLOSED",
+ "mileage" : 21529,
+ "checkControlMessages": [
+ {
+ "ccmDescriptionLong": "You can continue driving. Check tyre pressure when tyres are cold and adjust if necessary. Perform reset after adjustment. See Owner's Handbook for further information.",
+ "ccmDescriptionShort": "Tyre pressure notification",
+ "ccmId": 955,
+ "ccmMileage": 41544
+ }
+ ],
+ "parkingLight" : "OFF",
+ "windowDriverRear" : "CLOSED",
+ "steering" : "LH",
+ "updateTime" : "2018-03-10T19:35:30+0100",
+ "remainingFuel" : 39,
+ "windowPassengerRear" : "CLOSED",
+ "trunk" : "CLOSED",
+ "hood" : "CLOSED",
+ "internalDataTimeUTC" : "2018-03-10T18:35:30",
+ "windowPassengerFront" : "CLOSED",
+ "doorLockState" : "SECURED",
+ "doorPassengerRear" : "CLOSED",
+ "remainingRangeFuel" : 590,
+ "doorDriverRear" : "CLOSED",
+ "position" : {
+ "heading" : 141,
+ "lon" : 10.1010101,
+ "lat" : 50.505050,
+ "status" : "OK"
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "access_token": "some_token_string",
+ "token_type": "Bearer",
+ "expires_in": 28799,
+ "refresh_token": "another_token_string",
+ "scope": "authenticate_user vehicle_data remote_services"
+}
\ No newline at end of file
--- /dev/null
+{
+ "executionStatus" : {
+ "serviceType" : "LIGHT_FLASH",
+ "eventId" : "424C39333232312237B3E900@bmw.de",
+ "extendedStatus" : {
+ "result" : "STATUS_UNKNOWN"
+ },
+ "status" : "DELIVERED"
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "executionStatus" : {
+ "serviceType" : "LIGHT_FLASH",
+ "eventId" : "424C39333232312237B3E900@bmw.de",
+ "extendedStatus" : {
+ "result" : "STATUS_CHANGED",
+ "ignitionOnStatus" : "false"
+ },
+ "status" : "EXECUTED"
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "executionStatus" : {
+ "serviceType" : "LIGHT_FLASH",
+ "eventId" : "424C39333232312237B3E900@bmw.de",
+ "status" : "INITIATED"
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "executionStatus" : {
+ "serviceType" : "LIGHT_FLASH",
+ "eventId" : "424C39333232312237B3E900@bmw.de",
+ "extendedStatus" : {
+ "result" : "STATUS_UNKNOWN"
+ },
+ "status" : "PENDING"
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "vehicleStatus" : {
+ "doorPassengerFront" : "CLOSED",
+ "cbsData" : [ {
+ "cbsDueDate" : "2020-01",
+ "cbsState" : "OK",
+ "cbsRemainingMileage" : 25000,
+ "cbsDescription" : "Next service due when the stated distance has been covered or by the specified date.",
+ "cbsType" : "OIL"
+ }, {
+ "cbsDueDate" : "2022-01",
+ "cbsState" : "OK",
+ "cbsRemainingMileage" : 60000,
+ "cbsDescription" : "Next visual inspection due when the stated distance has been covered or by the stated date.",
+ "cbsType" : "VEHICLE_CHECK"
+ }, {
+ "cbsDueDate" : "2021-01",
+ "cbsState" : "OK",
+ "cbsDescription" : "Next change due at the latest by the stated date.",
+ "cbsType" : "BRAKE_FLUID"
+ } ],
+ "windowDriverFront" : "CLOSED",
+ "DCS_CCH_Ongoing" : false,
+ "updateReason" : "VEHICLE_SHUTDOWN",
+ "rearWindow" : "CLOSED",
+ "remainingRangeFuelMls" : 199,
+ "DCS_CCH_Activation" : "NA",
+ "singleImmediateCharging" : false,
+ "positionLight" : "OFF",
+ "vin" : "G31_NBTevo_VIN",
+ "doorDriverFront" : "CLOSED",
+ "mileage" : 4126,
+ "checkControlMessages" : [ ],
+ "parkingLight" : "OFF",
+ "windowDriverRear" : "CLOSED",
+ "steering" : "LH",
+ "updateTime" : "2018-03-10T11:39:41+0100",
+ "remainingFuel" : 33,
+ "windowPassengerRear" : "CLOSED",
+ "trunk" : "CLOSED",
+ "hood" : "CLOSED",
+ "internalDataTimeUTC" : "2018-03-10T10:39:41",
+ "windowPassengerFront" : "CLOSED",
+ "doorLockState" : "SECURED",
+ "doorPassengerRear" : "CLOSED",
+ "remainingRangeFuel" : 321,
+ "doorDriverRear" : "CLOSED",
+ "position" : {
+ "heading" : 174,
+ "lon" : 10.1010,
+ "lat" : 50.5050,
+ "status" : "OK"
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "vehicleStatus" : {
+ "doorPassengerFront" : "CLOSED",
+ "cbsData" : [ {
+ "cbsDueDate" : "2020-01",
+ "cbsState" : "OK",
+ "cbsRemainingMileage" : 25000,
+ "cbsDescription" : "Next service due when the stated distance has been covered or by the specified date.",
+ "cbsType" : "OIL"
+ }, {
+ "cbsDueDate" : "2022-01",
+ "cbsState" : "OK",
+ "cbsRemainingMileage" : 60000,
+ "cbsDescription" : "Next visual inspection due when the stated distance has been covered or by the stated date.",
+ "cbsType" : "VEHICLE_CHECK"
+ }, {
+ "cbsDueDate" : "2021-01",
+ "cbsState" : "OK",
+ "cbsDescription" : "Next change due at the latest by the stated date.",
+ "cbsType" : "BRAKE_FLUID"
+ } ],
+ "windowDriverFront" : "CLOSED",
+ "DCS_CCH_Ongoing" : false,
+ "updateReason" : "VEHICLE_SHUTDOWN_SECURED",
+ "rearWindow" : "CLOSED",
+ "remainingRangeFuelMls" : 187,
+ "DCS_CCH_Activation" : "NA",
+ "singleImmediateCharging" : false,
+ "positionLight" : "OFF",
+ "vin" : "G31_VIN",
+ "doorDriverFront" : "CLOSED",
+ "mileage" : 4134,
+ "checkControlMessages" : [ ],
+ "parkingLight" : "OFF",
+ "windowDriverRear" : "CLOSED",
+ "steering" : "LH",
+ "updateTime" : "2018-03-12T08:56:16+0100",
+ "remainingFuel" : 32,
+ "windowPassengerRear" : "CLOSED",
+ "trunk" : "CLOSED",
+ "hood" : "CLOSED",
+ "internalDataTimeUTC" : "2018-03-12T07:56:16",
+ "windowPassengerFront" : "CLOSED",
+ "doorLockState" : "SECURED",
+ "doorPassengerRear" : "CLOSED",
+ "remainingRangeFuel" : 302,
+ "doorDriverRear" : "CLOSED",
+ "position" : {
+ "status" : "DRIVER_DISABLED"
+ }
+ }
+}
--- /dev/null
+{
+ "vehicleStatus": {
+ "doorPassengerFront": "CLOSED",
+ "cbsData": [
+ {
+ "cbsDueDate": "2018-11",
+ "cbsState": "OK",
+ "cbsDescription": "Next change due at the latest by the stated date.",
+ "cbsType": "BRAKE_FLUID"
+ },
+ {
+ "cbsDueDate": "2018-11",
+ "cbsState": "OK",
+ "cbsDescription": "Next visual inspection due when the stated distance has been covered or by the stated date.",
+ "cbsType": "VEHICLE_CHECK"
+ }
+ ],
+ "windowDriverFront": "CLOSED",
+ "DCS_CCH_Ongoing": false,
+ "updateReason": "VEHICLE_SHUTDOWN_SECURED",
+ "rearWindow": "INVALID",
+ "DCS_CCH_Activation": "NA",
+ "chargingInductivePositioning": "NOT_POSITIONED",
+ "remainingRangeElectric": 53,
+ "singleImmediateCharging": false,
+ "maxRangeElectric": 103,
+ "chargingConnectionType": "CONDUCTIVE",
+ "positionLight": "OFF",
+ "lastChargingEndReason": "END_REQUESTED_BY_DRIVER",
+ "chargingStatus": "INVALID",
+ "vin": "I01_NOREX_VIN",
+ "doorDriverFront": "CLOSED",
+ "mileage": 16059,
+ "checkControlMessages": [],
+ "parkingLight": "OFF",
+ "windowDriverRear": "CLOSED",
+ "remainingRangeElectricMls": 32,
+ "lastChargingEndResult": "SUCCESS",
+ "steering": "RH",
+ "updateTime": "2018-03-15T15:44:30+0100",
+ "remainingFuel": 0,
+ "windowPassengerRear": "CLOSED",
+ "trunk": "CLOSED",
+ "hood": "CLOSED",
+ "internalDataTimeUTC": "2018-03-15T14:44:30",
+ "maxRangeElectricMls": 64,
+ "windowPassengerFront": "CLOSED",
+ "doorLockState": "SECURED",
+ "doorPassengerRear": "CLOSED",
+ "doorDriverRear": "CLOSED",
+ "sunroof": "CLOSED",
+ "connectionStatus": "DISCONNECTED",
+ "chargingLevelHv": 58,
+ "position": {
+ "heading": 123,
+ "lon": 12.3456,
+ "lat": -65.4321,
+ "status": "OK"
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "allTrips": {
+ "avgElectricConsumption": {
+ "communityLow": 11.05,
+ "communityAverage": 16.28,
+ "communityHigh": 21.99,
+ "userAverage": 16.46
+ },
+ "avgRecuperation": {
+ "communityLow": 0.47,
+ "communityAverage": 3.37,
+ "communityHigh": 11.51,
+ "userAverage": 4.53
+ },
+ "chargecycleRange": {
+ "communityAverage": 194.21,
+ "communityHigh": 270,
+ "userAverage": 57.3,
+ "userHigh": 185.48,
+ "userCurrentChargeCycle": 68
+ },
+ "totalElectricDistance": {
+ "communityLow": 19,
+ "communityAverage": 40850.56,
+ "communityHigh": 193006,
+ "userTotal": 16629.4
+ },
+ "avgCombinedConsumption": {
+ "communityLow": 0,
+ "communityAverage": 0.92,
+ "communityHigh": 4.44,
+ "userAverage": 0.64
+ },
+ "savedCO2": 461.083,
+ "savedCO2greenEnergy": 2712.255,
+ "totalSavedFuel": 0,
+ "resetDate": "2020-08-24T14:40:40+0000",
+ "batterySizeMax": 33200
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "weeklyPlanner": {
+ "climatizationEnabled": true,
+ "chargingMode": "IMMEDIATE_CHARGING",
+ "chargingPreferences": "CHARGING_WINDOW",
+ "timer1": {
+ "departureTime": "05:00",
+ "timerEnabled": false,
+ "weekdays": [
+ "MONDAY",
+ "TUESDAY",
+ "WEDNESDAY",
+ "THURSDAY",
+ "FRIDAY"
+ ]
+ },
+ "timer2": {
+ "departureTime": "12:00",
+ "timerEnabled": true,
+ "weekdays": [
+ "SATURDAY"
+ ]
+ },
+ "timer3": {
+ "departureTime": "00:00",
+ "timerEnabled": false,
+ "weekdays": []
+ },
+ "overrideTimer": {
+ "departureTime": "12:00",
+ "timerEnabled": false,
+ "weekdays": [
+ "SATURDAY"
+ ]
+ },
+ "preferredChargingWindow": {
+ "startTime": "11:00",
+ "endTime": "17:00"
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "vehicles": [
+ {
+ "vin": "MY_REAL_VIN",
+ "model": "i3 94 (+ REX)",
+ "bodytype": "I01",
+ "driveTrain": "BEV_REX",
+ "color": "CAPPARISWEISS MIT AKZENT BMW I BLAU",
+ "colorCode": "B85",
+ "brand": "BMW_I",
+ "yearOfConstruction": 2017,
+ "statisticsCommunityEnabled": false,
+ "statisticsAvailable": true,
+ "hub": "HUB_ECE",
+ "hasAlarmSystem": true,
+ "dealer": {
+ "name": "My Real Dealer Name",
+ "street": "My Real Dealer Address",
+ "postalCode": "4711",
+ "city": "My Real Dealer City",
+ "country": "DE",
+ "phone": "My Real Dealer Phone"
+ },
+ "breakdownNumber": "Real Phone Number",
+ "countryCode": "V6",
+ "egoVehiclePath": "",
+ "chargingUpdateMode": "NORMAL_PROGNOSE_BASED",
+ "steering": "LH",
+ "vehicleFinderRestriction": "NONE",
+ "hmiVersion": "ID4",
+ "a4a": "USB_ONLY",
+ "vehicleFinder": "ACTIVATED",
+ "remote360": "NOT_SUPPORTED",
+ "hornBlow": "ACTIVATED",
+ "lightFlash": "ACTIVATED",
+ "doorLock": "ACTIVATED",
+ "doorUnlock": "ACTIVATED",
+ "climateControl": "NOT_SUPPORTED",
+ "climateNow": "ACTIVATED",
+ "climateNowRES": "NOT_SUPPORTED",
+ "climateControlRES": "NOT_SUPPORTED",
+ "chargingControl": "WEEKLY_PLANNER",
+ "chargeNow": "NOT_SUPPORTED",
+ "sendPoi": "ACTIVATED",
+ "rangeMap": "RANGE_CIRCLE",
+ "lastDestinations": "SUPPORTED",
+ "intermodalRouting": "NOT_AVAILABLE",
+ "climateFunction": "AIRCONDITIONING",
+ "onlineSearchMode": "MAP",
+ "onlineSearchProvider": "GOOGLE",
+ "smartSolution": "NOT_SUPPORTED",
+ "carCloud": "NOT_SUPPORTED",
+ "supportedChargingModes": [
+ "AC_LOW",
+ "DC"
+ ],
+ "lscType": "I_LSC_IMM",
+ "ipa": "NOT_SUPPORTED",
+ "puStep": "1119",
+ "remoteSoftwareUpgrade": "NOT_SUPPORTED"
+ }
+ ]
+}
\ No newline at end of file
--- /dev/null
+{
+ "lastTrip": {
+ "efficiencyValue": 0.98,
+ "totalDistance": 2,
+ "electricDistance": 2,
+ "avgElectricConsumption": 7,
+ "avgRecuperation": 6,
+ "drivingModeValue": 0.87,
+ "totalConsumptionValue": 1.25,
+ "avgCombinedConsumption": 0,
+ "electricDistanceRatio": 100,
+ "savedFuel": 0,
+ "date": "2020-08-24T17:55:00+0000",
+ "duration": 5,
+ "auxiliaryConsumptionValue": 0.78,
+ "anticipationValue": 0.99,
+ "accelerationValue": 0.99
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "vehicleStatus" : {
+ "doorPassengerFront" : "CLOSED",
+ "cbsData" : [ {
+ "cbsDueDate" : "2018-05",
+ "cbsState" : "OK",
+ "cbsDescription" : "Next change due at the latest by the stated date.",
+ "cbsType" : "BRAKE_FLUID"
+ }, {
+ "cbsDueDate" : "2018-05",
+ "cbsState" : "OK",
+ "cbsDescription" : "Next visual inspection due when the stated distance has been covered or by the stated date.",
+ "cbsType" : "VEHICLE_CHECK"
+ } ],
+ "windowDriverFront" : "CLOSED",
+ "DCS_CCH_Ongoing" : false,
+ "updateReason" : "VEHICLE_SHUTDOWN_SECURED",
+ "rearWindow" : "INVALID",
+ "remainingRangeFuelMls" : 65,
+ "DCS_CCH_Activation" : "NA",
+ "chargingInductivePositioning" : "NOT_POSITIONED",
+ "remainingRangeElectric" : 48,
+ "singleImmediateCharging" : false,
+ "maxRangeElectric" : 94,
+ "chargingConnectionType" : "CONDUCTIVE",
+ "positionLight" : "OFF",
+ "lastChargingEndReason" : "UNKNOWN",
+ "chargingStatus" : "CHARGING",
+ "vin" : "I01_VIN",
+ "doorDriverFront" : "CLOSED",
+ "mileage" : 38807,
+ "chargingTimeRemaining" : 332,
+ "checkControlMessages" : [ ],
+ "parkingLight" : "OFF",
+ "windowDriverRear" : "CLOSED",
+ "remainingRangeElectricMls" : 30,
+ "lastChargingEndResult" : "UNKNOWN",
+ "steering" : "LH",
+ "updateTime" : "2018-03-12T08:38:57+0100",
+ "remainingFuel" : 8,
+ "windowPassengerRear" : "CLOSED",
+ "trunk" : "CLOSED",
+ "hood" : "CLOSED",
+ "internalDataTimeUTC" : "2018-03-12T06:21:01",
+ "maxRangeElectricMls" : 58,
+ "maxFuel" : 8,
+ "windowPassengerFront" : "CLOSED",
+ "doorLockState" : "SECURED",
+ "doorPassengerRear" : "CLOSED",
+ "remainingRangeFuel" : 106,
+ "doorDriverRear" : "CLOSED",
+ "connectionStatus" : "CONNECTED",
+ "chargingLevelHv" : 54,
+ "position" : {
+ "heading" : 356,
+ "lon" : 15.00000,
+ "lat" : 58.000000,
+ "status" : "OK"
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "vehicles": [
+ {
+ "vehicleFinderRestriction": "NONE",
+ "doorUnlock": "ACTIVATED",
+ "color": "SOPHISTOGRAU BRILLANTEFFEKT METALLI",
+ "onlineSearchMode": "MAP",
+ "breakdownNumber": "+4912345678",
+ "smartSolution": "NOT_SUPPORTED",
+ "yearOfConstruction": 2018,
+ "driveTrain": "CONV",
+ "rangeMap": "NOT_SUPPORTED",
+ "vehicleFinder": "ACTIVATED",
+ "a4a": "BLUETOOTH",
+ "hornBlow": "ACTIVATED",
+ "chargeNow": "NOT_SUPPORTED",
+ "countryCode": "V6",
+ "bodytype": "G31",
+ "lightFlash": "ACTIVATED",
+ "vin": "G31_NBTevo_VIN",
+ "model": "530i xDrive",
+ "ipa": "NOT_SUPPORTED",
+ "brand": "BMW",
+ "climateControl": "DEPARTURE_TIMER",
+ "sendPoi": "ACTIVATED",
+ "lastDestinations": "NOT_SUPPORTED",
+ "doorLock": "ACTIVATED",
+ "chargingControl": "NOT_SUPPORTED",
+ "onlineSearchProvider": "GOOGLE",
+ "steering": "LH",
+ "intermodalRouting": "NOT_AVAILABLE",
+ "lscType": "LSC_BASIS",
+ "hasAlarmSystem": true,
+ "statisticsCommunityEnabled": false,
+ "climateNow": "ACTIVATED",
+ "fuelType": "PETROL",
+ "hub": "HUB_ECE",
+ "remote360": "NOT_SUPPORTED",
+ "hmiVersion": "ID5",
+ "climateFunction": "PARK_HEATING",
+ "colorCode": "A90",
+ "carCloud": "ACTIVATED",
+ "statisticsAvailable": false
+ },
+ {
+ "vehicleFinderRestriction": "NONE",
+ "doorUnlock": "ACTIVATED",
+ "color": "ALPINWEISS III",
+ "onlineSearchMode": "MAP",
+ "breakdownNumber": "+4989358111111",
+ "smartSolution": "NOT_SUPPORTED",
+ "yearOfConstruction": 2017,
+ "driveTrain": "CONV",
+ "rangeMap": "NOT_SUPPORTED",
+ "vehicleFinder": "ACTIVATED",
+ "a4a": "BLUETOOTH",
+ "hornBlow": "ACTIVATED",
+ "chargeNow": "NOT_SUPPORTED",
+ "countryCode": "V1-NL",
+ "bodytype": "F48",
+ "lightFlash": "ACTIVATED",
+ "vin": "F48_VIN",
+ "model": "X1 sDrive18i",
+ "ipa": "NOT_SUPPORTED",
+ "brand": "BMW",
+ "climateControl": "START_TIMER",
+ "sendPoi": "ACTIVATED",
+ "lastDestinations": "NOT_SUPPORTED",
+ "doorLock": "ACTIVATED",
+ "chargingControl": "NOT_SUPPORTED",
+ "onlineSearchProvider": "GOOGLE",
+ "steering": "LH",
+ "intermodalRouting": "NOT_AVAILABLE",
+ "lscType": "LSC_BASIS",
+ "hasAlarmSystem": true,
+ "statisticsCommunityEnabled": false,
+ "climateNow": "ACTIVATED",
+ "fuelType": "PETROL",
+ "hub": "HUB_ECE",
+ "remote360": "NOT_SUPPORTED",
+ "hmiVersion": "ID5",
+ "dealer": {
+ "country": "NL",
+ "city": "city",
+ "phone": "phone",
+ "street": "street",
+ "postalCode": "1234 AB",
+ "name": "BMW dealer"
+ },
+ "climateFunction": "VENTILATION",
+ "colorCode": "300",
+ "carCloud": "ACTIVATED",
+ "statisticsAvailable": false
+ },
+ {
+ "vehicleFinderRestriction": "NONE",
+ "doorUnlock": "ACTIVATED",
+ "color": "ARRAVANIGRAU/BMW i BLAU",
+ "onlineSearchMode": "MAP",
+ "breakdownNumber": "+4989358957103",
+ "smartSolution": "NOT_SUPPORTED",
+ "yearOfConstruction": 2014,
+ "driveTrain": "BEV_REX",
+ "rangeMap": "RANGE_CIRCLE",
+ "vehicleFinder": "ACTIVATED",
+ "a4a": "USB_ONLY",
+ "hornBlow": "ACTIVATED",
+ "chargeNow": "NOT_SUPPORTED",
+ "licensePlate": "ABC000",
+ "countryCode": "V1-R1-SE",
+ "bodytype": "I01",
+ "lightFlash": "ACTIVATED",
+ "vin": "I01_VIN",
+ "model": "i3 (+ REX)",
+ "ipa": "NOT_SUPPORTED",
+ "brand": "BMW_I",
+ "climateControl": "NOT_SUPPORTED",
+ "sendPoi": "ACTIVATED",
+ "lastDestinations": "SUPPORTED",
+ "doorLock": "ACTIVATED",
+ "chargingControl": "WEEKLY_PLANNER",
+ "onlineSearchProvider": "GOOGLE",
+ "steering": "LH",
+ "intermodalRouting": "NOT_AVAILABLE",
+ "supportedChargingModes": [
+ "AC_LOW",
+ "AC_HIGH",
+ "DC"
+ ],
+ "lscType": "I_LSC_IMM",
+ "hasAlarmSystem": false,
+ "statisticsCommunityEnabled": false,
+ "climateNow": "ACTIVATED",
+ "hub": "HUB_ECE",
+ "remote360": "NOT_SUPPORTED",
+ "hmiVersion": "ID4",
+ "dealer": {
+ "country": "SE",
+ "city": "Solna",
+ "phone": "+46 8 7353900",
+ "street": "G�rdsv�gen 9-11",
+ "postalCode": "169 70",
+ "name": "Bavaria Sverige Bil AB/ Solna"
+ },
+ "climateFunction": "AIRCONDITIONING",
+ "colorCode": "B74",
+ "carCloud": "NOT_SUPPORTED",
+ "statisticsAvailable": true
+ },
+ {
+ "vehicleFinderRestriction": "NONE",
+ "doorUnlock": "NOT_SUPPORTED",
+ "color": "BLACK SAPPHIRE METALLIC",
+ "onlineSearchMode": "MAP",
+ "breakdownNumber": "+4989358957103",
+ "smartSolution": "NOT_SUPPORTED",
+ "yearOfConstruction": 2017,
+ "driveTrain": "CONV",
+ "rangeMap": "NOT_SUPPORTED",
+ "vehicleFinder": "ACTIVATED",
+ "a4a": "BLUETOOTH",
+ "hornBlow": "ACTIVATED",
+ "chargeNow": "NOT_SUPPORTED",
+ "countryCode": "V2-CA",
+ "bodytype": "F15",
+ "lightFlash": "ACTIVATED",
+ "vin": "F15_VIN",
+ "model": "X5 xDrive35i",
+ "ipa": "NOT_SUPPORTED",
+ "brand": "BMW",
+ "climateControl": "DEPARTURE_TIMER",
+ "sendPoi": "ACTIVATED",
+ "lastDestinations": "NOT_SUPPORTED",
+ "doorLock": "ACTIVATED",
+ "chargingControl": "NOT_SUPPORTED",
+ "steering": "LH",
+ "intermodalRouting": "NOT_AVAILABLE",
+ "lscType": "LSC_BASIS",
+ "hasAlarmSystem": true,
+ "statisticsCommunityEnabled": false,
+ "climateNow": "ACTIVATED",
+ "fuelType": "PETROL",
+ "hub": "HUB_US",
+ "remote360": "NOT_SUPPORTED",
+ "hmiVersion": "ID5",
+ "dealer": {
+ "country": "CA",
+ "city": "Ottawa",
+ "phone": "1-866-599-4999",
+ "street": "85 Wellington St",
+ "postalCode": "K1A 1A1",
+ "name": "Parliament of Canada"
+ },
+ "climateFunction": "VENTILATION",
+ "colorCode": "475",
+ "carCloud": "NOT_SUPPORTED",
+ "statisticsAvailable": false
+ },
+ {
+ "vehicleFinderRestriction": "NONE",
+ "doorUnlock": "ACTIVATED",
+ "color": "SOLARORANGE MET. M. AKZENT FROZEN G",
+ "onlineSearchMode": "MAP",
+ "breakdownNumber": "+4989358957103",
+ "smartSolution": "NOT_SUPPORTED",
+ "yearOfConstruction": 2014,
+ "driveTrain": "BEV",
+ "rangeMap": "RANGE_CIRCLE",
+ "vehicleFinder": "ACTIVATED",
+ "a4a": "USB_ONLY",
+ "hornBlow": "ACTIVATED",
+ "chargeNow": "NOT_SUPPORTED",
+ "licensePlate": "HIDDEN",
+ "countryCode": "B3-ZA",
+ "bodytype": "I01",
+ "lightFlash": "ACTIVATED",
+ "vin": "I01_NOREX_VIN",
+ "model": "i3",
+ "ipa": "NOT_SUPPORTED",
+ "brand": "BMW_I",
+ "climateControl": "NOT_SUPPORTED",
+ "sendPoi": "ACTIVATED",
+ "lastDestinations": "SUPPORTED",
+ "doorLock": "ACTIVATED",
+ "chargingControl": "WEEKLY_PLANNER",
+ "onlineSearchProvider": "GOOGLE",
+ "steering": "RH",
+ "intermodalRouting": "NOT_AVAILABLE",
+ "supportedChargingModes": [
+ "AC_LOW",
+ "AC_HIGH"
+ ],
+ "lscType": "I_LSC_IMM",
+ "hasAlarmSystem": true,
+ "statisticsCommunityEnabled": true,
+ "climateNow": "ACTIVATED",
+ "hub": "HUB_ECE",
+ "remote360": "NOT_SUPPORTED",
+ "hmiVersion": "ID4",
+ "dealer": {
+ "country": "ZA",
+ "city": "Midrand",
+ "phone": "+27 12 522 3000",
+ "street": "1 Bavaria Avenue",
+ "postalCode": "1685",
+ "name": "BMW (South Africa) (Pty) Ltd. ZA"
+ },
+ "climateFunction": "AIRCONDITIONING",
+ "colorCode": "B78",
+ "carCloud": "NOT_SUPPORTED",
+ "statisticsAvailable": true
+ },
+ {
+ "a4a": "USB_ONLY",
+ "bodytype": "F45",
+ "brand": "BMW",
+ "breakdownNumber": "+4989358957103",
+ "carCloud": "NOT_SUPPORTED",
+ "chargeNow": "NOT_SUPPORTED",
+ "chargingControl": "NOT_SUPPORTED",
+ "climateControl": "START_TIMER",
+ "climateFunction": "VENTILATION",
+ "climateNow": "ACTIVATED",
+ "color": "MEDITERRANBLAU METALLIC",
+ "colorCode": "C10",
+ "countryCode": "V1-ES",
+ "dealer": {
+ "city": "Madrid",
+ "country": "ES",
+ "name": "BMW Iberica S.A.",
+ "phone": "+34 913350505",
+ "postalCode": "28050",
+ "street": "Avenida de Burgos ,118"
+ },
+ "doorLock": "ACTIVATED",
+ "doorUnlock": "ACTIVATED",
+ "driveTrain": "CONV",
+ "fuelType": "PETROL",
+ "hasAlarmSystem": false,
+ "hmiVersion": "ID4",
+ "hornBlow": "ACTIVATED",
+ "hub": "HUB_ECE",
+ "intermodalRouting": "NOT_AVAILABLE",
+ "ipa": "NOT_SUPPORTED",
+ "lastDestinations": "NOT_SUPPORTED",
+ "licensePlate": "some_license_plate",
+ "lightFlash": "ACTIVATED",
+ "lscType": "NOT_SUPPORTED",
+ "model": "225i",
+ "onlineSearchMode": "MAP",
+ "onlineSearchProvider": "GOOGLE",
+ "rangeMap": "NOT_SUPPORTED",
+ "remote360": "NOT_SUPPORTED",
+ "sendPoi": "ACTIVATED",
+ "smartSolution": "NOT_SUPPORTED",
+ "statisticsAvailable": false,
+ "statisticsCommunityEnabled": false,
+ "steering": "LH",
+ "vehicleFinder": "ACTIVATED",
+ "vehicleFinderRestriction": "NONE",
+ "vin": "F45_VIN",
+ "yearOfConstruction": 2016
+ },
+ {
+ "a4a": "USB_ONLY",
+ "bodytype": "F31",
+ "brand": "BMW",
+ "breakdownNumber": "+4989358957103",
+ "carCloud": "NOT_SUPPORTED",
+ "chargeNow": "NOT_SUPPORTED",
+ "chargingControl": "NOT_SUPPORTED",
+ "climateControl": "START_TIMER",
+ "climateControlRES": "NOT_SUPPORTED",
+ "climateFunction": "VENTILATION",
+ "climateNow": "ACTIVATED",
+ "climateNowRES": "NOT_SUPPORTED",
+ "color": "PLATINSILBER METALLIC",
+ "colorCode": "C08",
+ "countryCode": "V1-UK",
+ "dealer": {
+ "city": "Farnborough",
+ "country": "GB",
+ "name": "BMW (UK) Ltd. ICS - DIRECT SUPPLY",
+ "phone": "+44 1252 920000",
+ "postalCode": "GU14 0FB",
+ "street": "Summit ONE"
+ },
+ "doorLock": "ACTIVATED",
+ "doorUnlock": "ACTIVATED",
+ "driveTrain": "CONV",
+ "fuelType": "DIESEL",
+ "hasAlarmSystem": true,
+ "hmiVersion": "ID4",
+ "hornBlow": "NOT_SUPPORTED",
+ "hub": "HUB_ECE",
+ "intermodalRouting": "NOT_AVAILABLE",
+ "ipa": "NOT_SUPPORTED",
+ "lastDestinations": "NOT_SUPPORTED",
+ "licensePlate": "some_license_plate",
+ "lightFlash": "ACTIVATED",
+ "lscType": "NOT_SUPPORTED",
+ "model": "320d",
+ "onlineSearchMode": "MAP",
+ "onlineSearchProvider": "GOOGLE",
+ "rangeMap": "NOT_SUPPORTED",
+ "remote360": "NOT_SUPPORTED",
+ "remoteSoftwareUpgrade": "NOT_SUPPORTED",
+ "sendPoi": "ACTIVATED",
+ "smartSolution": "NOT_SUPPORTED",
+ "statisticsAvailable": false,
+ "statisticsCommunityEnabled": false,
+ "steering": "RH",
+ "vehicleFinder": "ACTIVATED",
+ "vehicleFinderRestriction": "NONE",
+ "vin": "F31_VIN",
+ "yearOfConstruction": 2015
+ },
+ {
+ "a4a": "NOT_SUPPORTED",
+ "bodytype": "F35",
+ "brand": "BMW",
+ "breakdownNumber": "+4989358957103",
+ "carCloud": "NOT_SUPPORTED",
+ "chargeNow": "NOT_SUPPORTED",
+ "chargingControl": "NOT_SUPPORTED",
+ "climateControl": "NOT_SUPPORTED",
+ "climateControlRES": "NOT_SUPPORTED",
+ "climateFunction": "VENTILATION",
+ "climateNow": "NOT_SUPPORTED",
+ "climateNowRES": "NOT_SUPPORTED",
+ "color": "MINERALWEISS METALLIC",
+ "colorCode": "A96",
+ "countryCode": "V5-CN",
+ "dealer": {
+ "city": "Beijing",
+ "country": "CN",
+ "name": "Beijing Baozen Baiwang Automotive Sales Co., Ltd.",
+ "phone": "+86 10 62826789",
+ "postalCode": "100094",
+ "street": "F2 Baiwang Green Valley"
+ },
+ "doorLock": "NOT_SUPPORTED",
+ "doorUnlock": "NOT_SUPPORTED",
+ "driveTrain": "CONV",
+ "fuelType": "PETROL",
+ "hasAlarmSystem": false,
+ "hmiVersion": "ID5",
+ "hornBlow": "NOT_SUPPORTED",
+ "hub": "HUB_CN",
+ "intermodalRouting": "NOT_AVAILABLE",
+ "ipa": "NOT_SUPPORTED",
+ "lastDestinations": "NOT_SUPPORTED",
+ "licensePlate": "some_license_plate",
+ "lightFlash": "NOT_SUPPORTED",
+ "lscType": "NOT_SUPPORTED",
+ "model": "328Li",
+ "onlineSearchMode": "MAP",
+ "rangeMap": "NOT_SUPPORTED",
+ "remote360": "NOT_SUPPORTED",
+ "remoteSoftwareUpgrade": "NOT_SUPPORTED",
+ "sendPoi": "NOT_SUPPORTED",
+ "smartSolution": "NOT_SUPPORTED",
+ "statisticsAvailable": false,
+ "statisticsCommunityEnabled": false,
+ "steering": "LH",
+ "vehicleFinder": "NOT_SUPPORTED",
+ "vin": "F31_VIN",
+ "yearOfConstruction": 2015
+ },
+ {
+ "vin": "ANONYMOUS",
+ "model": "318i",
+ "driveTrain": "CONV",
+ "brand": "BMW",
+ "yearOfConstruction": 2019,
+ "bodytype": "F31",
+ "color": "MINERALGRAU METALLIC",
+ "statisticsCommunityEnabled": false,
+ "statisticsAvailable": false,
+ "hasAlarmSystem": true,
+ "dealer": {
+ "name": "ANONYMOUS",
+ "street": "ANONYMOUS",
+ "postalCode": "ANONYMOUS",
+ "city": "ANONYMOUS",
+ "country": "ANONYMOUS",
+ "phone": "ANONYMOUS"
+ },
+ "breakdownNumber": "ANONYMOUS",
+ "chargingControl": "NOT_SUPPORTED",
+ "vehicleFinder": "ACTIVATED",
+ "hornBlow": "ACTIVATED",
+ "lightFlash": "ACTIVATED",
+ "doorLock": "ACTIVATED",
+ "doorUnlock": "ACTIVATED",
+ "climateNow": "ACTIVATED",
+ "sendPoi": "ACTIVATED",
+ "remote360": "NOT_SUPPORTED",
+ "climateControl": "START_TIMER",
+ "chargeNow": "NOT_SUPPORTED",
+ "lastDestinations": "NOT_SUPPORTED",
+ "carCloud": "ACTIVATED",
+ "remoteSoftwareUpgrade": "NOT_SUPPORTED",
+ "climateNowRES": "NOT_SUPPORTED",
+ "climateControlRES": "NOT_SUPPORTED",
+ "smartSolution": "NOT_SUPPORTED",
+ "ipa": "NOT_SUPPORTED"
+ }
+ ]
+}
--- /dev/null
+{
+ "allTrips": {
+ "avgElectricConsumption": {
+ "communityLow": 11.05,
+ "communityAverage": 16.28,
+ "communityHigh": 21.99,
+ "userAverage": 16.46
+ },
+ "avgRecuperation": {
+ "communityLow": 0.47,
+ "communityAverage": 3.37,
+ "communityHigh": 11.51,
+ "userAverage": 4.53
+ },
+ "chargecycleRange": {
+ "communityAverage": 194.21,
+ "communityHigh": 270,
+ "userAverage": 57.3,
+ "userHigh": 185.48,
+ "userCurrentChargeCycle": 68
+ },
+ "totalElectricDistance": {
+ "communityLow": 19,
+ "communityAverage": 40850.56,
+ "communityHigh": 193006,
+ "userTotal": 16629.4
+ },
+ "avgCombinedConsumption": {
+ "communityLow": 0,
+ "communityAverage": 0.92,
+ "communityHigh": 4.44,
+ "userAverage": 0.64
+ },
+ "savedCO2": 461.083,
+ "savedCO2greenEnergy": 2712.255,
+ "totalSavedFuel": 0,
+ "resetDate": "2020-08-24T14:40:40+0000",
+ "batterySizeMax": 33200
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "weeklyPlanner": {
+ "climatizationEnabled": true,
+ "chargingMode": "IMMEDIATE_CHARGING",
+ "chargingPreferences": "CHARGING_WINDOW",
+ "timer1": {
+ "departureTime": "05:00",
+ "timerEnabled": false,
+ "weekdays": [
+ "MONDAY",
+ "TUESDAY",
+ "WEDNESDAY",
+ "THURSDAY",
+ "FRIDAY"
+ ]
+ },
+ "timer2": {
+ "departureTime": "12:00",
+ "timerEnabled": true,
+ "weekdays": [
+ "SATURDAY"
+ ]
+ },
+ "timer3": {
+ "departureTime": "00:00",
+ "timerEnabled": false,
+ "weekdays": []
+ },
+ "overrideTimer": {
+ "departureTime": "12:00",
+ "timerEnabled": false,
+ "weekdays": [
+ "SATURDAY"
+ ]
+ },
+ "preferredChargingWindow": {
+ "startTime": "11:00",
+ "endTime": "17:00"
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "vehicles": [
+ {
+ "vin": "MY_REAL_VIN",
+ "model": "i3 94 (+ REX)",
+ "bodytype": "I01",
+ "driveTrain": "BEV_REX",
+ "color": "CAPPARISWEISS MIT AKZENT BMW I BLAU",
+ "colorCode": "B85",
+ "brand": "BMW_I",
+ "yearOfConstruction": 2017,
+ "statisticsCommunityEnabled": false,
+ "statisticsAvailable": true,
+ "hub": "HUB_ECE",
+ "hasAlarmSystem": true,
+ "dealer": {
+ "name": "My Real Dealer Name",
+ "street": "My Real Dealer Address",
+ "postalCode": "4711",
+ "city": "My Real Dealer City",
+ "country": "DE",
+ "phone": "My Real Dealer Phone"
+ },
+ "breakdownNumber": "Real Phone Number",
+ "countryCode": "V6",
+ "egoVehiclePath": "",
+ "chargingUpdateMode": "NORMAL_PROGNOSE_BASED",
+ "steering": "LH",
+ "vehicleFinderRestriction": "NONE",
+ "hmiVersion": "ID4",
+ "a4a": "USB_ONLY",
+ "vehicleFinder": "ACTIVATED",
+ "remote360": "NOT_SUPPORTED",
+ "hornBlow": "ACTIVATED",
+ "lightFlash": "ACTIVATED",
+ "doorLock": "ACTIVATED",
+ "doorUnlock": "ACTIVATED",
+ "climateControl": "NOT_SUPPORTED",
+ "climateNow": "ACTIVATED",
+ "climateNowRES": "NOT_SUPPORTED",
+ "climateControlRES": "NOT_SUPPORTED",
+ "chargingControl": "WEEKLY_PLANNER",
+ "chargeNow": "NOT_SUPPORTED",
+ "sendPoi": "ACTIVATED",
+ "rangeMap": "RANGE_CIRCLE",
+ "lastDestinations": "SUPPORTED",
+ "intermodalRouting": "NOT_AVAILABLE",
+ "climateFunction": "AIRCONDITIONING",
+ "onlineSearchMode": "MAP",
+ "onlineSearchProvider": "GOOGLE",
+ "smartSolution": "NOT_SUPPORTED",
+ "carCloud": "NOT_SUPPORTED",
+ "supportedChargingModes": [
+ "AC_LOW",
+ "DC"
+ ],
+ "lscType": "I_LSC_IMM",
+ "ipa": "NOT_SUPPORTED",
+ "puStep": "1119",
+ "remoteSoftwareUpgrade": "NOT_SUPPORTED"
+ }
+ ]
+}
\ No newline at end of file
--- /dev/null
+{
+ "destinations": [
+ {
+ "lat": 47.11,
+ "lon": 47.11,
+ "country": "DEUTSCHLAND",
+ "city": "My Real City",
+ "street": "My Real Street",
+ "streetNumber": "My Real Number",
+ "type": "DESTINATION",
+ "createdAt": "2020-08-16T12:52:58+0000"
+ },
+ {
+ "lat": 47.11,
+ "lon": 47.11,
+ "country": "DEUTSCHLAND",
+ "city": "My Real City",
+ "street": "My Real Street",
+ "streetNumber": "My Real Number",
+ "type": "DESTINATION",
+ "createdAt": "2020-08-12T17:03:35+0000"
+ },
+ {
+ "lat": 47.11,
+ "lon": 47.11,
+ "country": "DEUTSCHLAND",
+ "city": "My Real City",
+ "street": "My Real Street",
+ "streetNumber": "My Real Number",
+ "type": "DESTINATION",
+ "createdAt": "2020-08-03T08:15:20+0000"
+ },
+ {
+ "lat": 47.11,
+ "lon": 47.11,
+ "country": "DEUTSCHLAND",
+ "city": "My Real City",
+ "street": "My Real Street",
+ "streetNumber": "My Real Number",
+ "type": "DESTINATION",
+ "createdAt": "2020-07-31T13:09:15+0000"
+ },
+ {
+ "lat": 47.11,
+ "lon": 47.11,
+ "country": "DEUTSCHLAND",
+ "city": "My Real City",
+ "street": "My Real Street",
+ "streetNumber": "My Real Number",
+ "type": "DESTINATION",
+ "createdAt": "2020-07-25T11:20:18+0000"
+ },
+ {
+ "lat": 47.11,
+ "lon": 47.11,
+ "country": "DEUTSCHLAND",
+ "city": "My Real City",
+ "street": "My Real Street",
+ "streetNumber": "My Real Number",
+ "type": "DESTINATION",
+ "createdAt": "2020-07-18T11:22:37+0000"
+ },
+ {
+ "lat": 47.11,
+ "lon": 47.11,
+ "country": "DEUTSCHLAND",
+ "city": "My Real City",
+ "street": "My Real Street",
+ "streetNumber": "My Real Number",
+ "type": "DESTINATION",
+ "createdAt": "2020-02-08T11:06:52+0000"
+ },
+ {
+ "lat": 47.11,
+ "lon": 47.11,
+ "country": "DEUTSCHLAND",
+ "city": "My Real City",
+ "street": "My Real Street",
+ "streetNumber": "My Real Number",
+ "type": "DESTINATION",
+ "createdAt": "2020-02-02T14:07:54+0000"
+ },
+ {
+ "lat": 47.11,
+ "lon": 47.11,
+ "country": "DEUTSCHLAND",
+ "city": "My Real City",
+ "street": "My Real Street",
+ "streetNumber": "My Real Number",
+ "type": "DESTINATION",
+ "createdAt": "2020-02-02T13:24:36+0000"
+ }
+ ]
+}
\ No newline at end of file
--- /dev/null
+{
+ "lastTrip": {
+ "efficiencyValue": 0.98,
+ "totalDistance": 2,
+ "electricDistance": 2,
+ "avgElectricConsumption": 7,
+ "avgRecuperation": 6,
+ "drivingModeValue": 0.87,
+ "totalConsumptionValue": 1.25,
+ "avgCombinedConsumption": 0,
+ "electricDistanceRatio": 100,
+ "savedFuel": 0,
+ "date": "2020-08-24T17:55:00+0000",
+ "duration": 5,
+ "auxiliaryConsumptionValue": 0.78,
+ "anticipationValue": 0.99,
+ "accelerationValue": 0.99
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "executionStatus": {
+ "serviceType": "DOOR_UNLOCK",
+ "status": "DELIVERED",
+ "eventId": "5639303536333926DA7B9400@bmw.de",
+ "extendedStatus": {
+ "result": "STATUS_UNKNOWN"
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "executionStatus": {
+ "serviceType": "DOOR_UNLOCK",
+ "status": "EXECUTED",
+ "eventId": "5639303536333926DA7B9400@bmw.de",
+ "extendedStatus": {
+ "newDoorStatus": "INVALID",
+ "oldDoorStatus": "INVALID",
+ "result": "STATUS_NOT_CHANGED"
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "executionStatus": {
+ "serviceType": "DOOR_UNLOCK",
+ "status": "PENDING",
+ "eventId": "5639303536333926DA7B9400@bmw.de",
+ "extendedStatus": {
+ "result": "STATUS_UNKNOWN"
+ }
+ }
+}
--- /dev/null
+{
+ "vehicleStatus": {
+ "mileage": 18390,
+ "remainingFuel": 4.0,
+ "remainingRangeElectric": 67.0,
+ "remainingRangeElectricMls": 41.0,
+ "remainingRangeFuel": 59.0,
+ "remainingRangeFuelMls": 36.0,
+ "maxRangeElectric": 211.0,
+ "maxRangeElectricMls": 131.0,
+ "maxFuel": 8.5,
+ "chargingLevelHv": 35.0,
+ "vin": "ANONYMOUS",
+ "updateReason": "VEHICLE_SHUTDOWN_SECURED",
+ "updateTime": "2020-09-29T12:52:12+0000",
+ "doorDriverFront": "CLOSED",
+ "doorDriverRear": "CLOSED",
+ "doorPassengerFront": "CLOSED",
+ "doorPassengerRear": "CLOSED",
+ "windowDriverFront": "CLOSED",
+ "windowDriverRear": "CLOSED",
+ "windowPassengerFront": "CLOSED",
+ "windowPassengerRear": "CLOSED",
+ "sunroof": "CLOSED",
+ "trunk": "CLOSED",
+ "rearWindow": "INVALID",
+ "hood": "CLOSED",
+ "doorLockState": "SECURED",
+ "parkingLight": "OFF",
+ "positionLight": "ON",
+ "connectionStatus": "DISCONNECTED",
+ "chargingStatus": "INVALID",
+ "lastChargingEndReason": "END_REQUESTED_BY_DRIVER",
+ "lastChargingEndResult": "SUCCESS",
+ "position": {
+ "lat": -1.0,
+ "lon": -1.0,
+ "heading": -1,
+ "status": "OK"
+ },
+ "internalDataTimeUTC": "2020-09-29T12:52:12",
+ "singleImmediateCharging": false,
+ "chargingConnectionType": "CONDUCTIVE",
+ "chargingInductivePositioning": "NOT_POSITIONED",
+ "vehicleCountry": "DE",
+ "DCS_CCH_Activation": "NA",
+ "DCS_CCH_Ongoing": false,
+ "checkControlMessages": [
+ {
+ "ccmDescriptionLong": "Check tyre pressure and correct if necessary. Afterwards, perform Tyre Pressure Monitor (RDC) reset. See Owner\u0027s Handbook for more information.",
+ "ccmDescriptionShort": "Check tyre pressure. Initialise RDC",
+ "ccmId": 142,
+ "ccmMileage": 18384
+ }
+ ],
+ "cbsData": [
+ {
+ "cbsType": "BRAKE_FLUID",
+ "cbsState": "OK",
+ "cbsDueDate": "2021-11",
+ "cbsDescription": "Next change due at the latest by the stated date.",
+ "cbsRemainingMileage": -1
+ },
+ {
+ "cbsType": "VEHICLE_CHECK",
+ "cbsState": "OK",
+ "cbsDueDate": "2021-11",
+ "cbsDescription": "Next visual inspection due when the stated distance has been covered or by the stated date.",
+ "cbsRemainingMileage": -1
+ },
+ {
+ "cbsType": "OIL",
+ "cbsState": "OK",
+ "cbsDueDate": "2021-11",
+ "cbsDescription": "Next change due when the stated distance has been covered or by the specified date.",
+ "cbsRemainingMileage": -1
+ },
+ {
+ "cbsType": "VEHICLE_TUV",
+ "cbsState": "OK",
+ "cbsDueDate": "2021-11",
+ "cbsDescription": "Next statutory vehicle inspection due by the stated date.",
+ "cbsRemainingMileage": -1
+ }
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "vehicleStatus": {
+ "vin": "ANONYMOUS",
+ "mileage": 17307,
+ "updateReason": "CHARGING_STARTED",
+ "updateTime": "2020-08-25T15:37:16+0000",
+ "doorDriverFront": "CLOSED",
+ "doorDriverRear": "CLOSED",
+ "doorPassengerFront": "CLOSED",
+ "doorPassengerRear": "CLOSED",
+ "windowDriverFront": "CLOSED",
+ "windowDriverRear": "CLOSED",
+ "windowPassengerFront": "CLOSED",
+ "windowPassengerRear": "CLOSED",
+ "sunroof": "CLOSED",
+ "trunk": "CLOSED",
+ "rearWindow": "INVALID",
+ "hood": "CLOSED",
+ "doorLockState": "SECURED",
+ "parkingLight": "OFF",
+ "positionLight": "ON",
+ "remainingFuel": 4,
+ "remainingRangeElectric": 182,
+ "remainingRangeElectricMls": 113,
+ "remainingRangeFuel": 69,
+ "remainingRangeFuelMls": 42,
+ "maxRangeElectric": 213,
+ "maxRangeElectricMls": 132,
+ "maxFuel": 8.5,
+ "connectionStatus": "CONNECTED",
+ "chargingStatus": "CHARGING",
+ "chargingTimeRemaining": 76,
+ "chargingLevelHv": 86,
+ "lastChargingEndReason": "UNKNOWN",
+ "lastChargingEndResult": "UNKNOWN",
+ "position": {
+ "lat": 56.789,
+ "lon": 8.765,
+ "heading": 41,
+ "status": "OK"
+ },
+ "internalDataTimeUTC": "2020-08-25T12:57:59",
+ "singleImmediateCharging": false,
+ "chargingConnectionType": "CONDUCTIVE",
+ "chargingInductivePositioning": "NOT_POSITIONED",
+ "vehicleCountry": "DE",
+ "checkControlMessages": [],
+ "cbsData": [
+ {
+ "cbsType": "BRAKE_FLUID",
+ "cbsState": "OK",
+ "cbsDueDate": "2021-11",
+ "cbsDescription": "Next change due at the latest by the stated date."
+ },
+ {
+ "cbsType": "VEHICLE_CHECK",
+ "cbsState": "OK",
+ "cbsDueDate": "2021-11",
+ "cbsDescription": "Next visual inspection due when the stated distance has been covered or by the stated date."
+ },
+ {
+ "cbsType": "OIL",
+ "cbsState": "OK",
+ "cbsDueDate": "2021-11",
+ "cbsDescription": "Next change due when the stated distance has been covered or by the specified date."
+ },
+ {
+ "cbsType": "VEHICLE_TUV",
+ "cbsState": "OK",
+ "cbsDueDate": "2021-11",
+ "cbsDescription": "Next statutory vehicle inspection due by the stated date."
+ }
+ ],
+ "DCS_CCH_Activation": "NA",
+ "DCS_CCH_Ongoing": false
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "vehicleStatus": {
+ "vin": "ANONYMOUS",
+ "mileage": 17273,
+ "updateReason": "VEHICLE_SHUTDOWN_SECURED",
+ "updateTime": "2020-08-24T15:55:32+0000",
+ "doorDriverFront": "CLOSED",
+ "doorDriverRear": "CLOSED",
+ "doorPassengerFront": "CLOSED",
+ "doorPassengerRear": "CLOSED",
+ "windowDriverFront": "CLOSED",
+ "windowDriverRear": "CLOSED",
+ "windowPassengerFront": "CLOSED",
+ "windowPassengerRear": "CLOSED",
+ "sunroof": "CLOSED",
+ "trunk": "CLOSED",
+ "rearWindow": "INVALID",
+ "hood": "CLOSED",
+ "doorLockState": "SECURED",
+ "parkingLight": "OFF",
+ "positionLight": "ON",
+ "remainingFuel": 4,
+ "remainingRangeElectric": 148,
+ "remainingRangeElectricMls": 91,
+ "remainingRangeFuel": 70,
+ "remainingRangeFuelMls": 43,
+ "maxRangeElectric": 216,
+ "maxRangeElectricMls": 134,
+ "maxFuel": 8.5,
+ "connectionStatus": "DISCONNECTED",
+ "chargingStatus": "INVALID",
+ "chargingLevelHv": 71,
+ "lastChargingEndReason": "CHARGING_GOAL_REACHED",
+ "lastChargingEndResult": "SUCCESS",
+ "position": {
+ "lat": 56.789,
+ "lon": 8.765,
+ "heading": 219,
+ "status": "OK"
+ },
+ "internalDataTimeUTC": "2020-08-24T15:55:32",
+ "singleImmediateCharging": false,
+ "chargingConnectionType": "CONDUCTIVE",
+ "chargingInductivePositioning": "NOT_POSITIONED",
+ "vehicleCountry": "DE",
+ "checkControlMessages": [],
+ "cbsData": [
+ {
+ "cbsType": "BRAKE_FLUID",
+ "cbsState": "OK",
+ "cbsDueDate": "2021-11",
+ "cbsDescription": "Next change due at the latest by the stated date.",
+ "cbsRemainingMileage": 15345
+ },
+ {
+ "cbsType": "VEHICLE_CHECK",
+ "cbsState": "OK",
+ "cbsDueDate": "2021-11",
+ "cbsDescription": "Next visual inspection due when the stated distance has been covered or by the stated date."
+ },
+ {
+ "cbsType": "OIL",
+ "cbsState": "OK",
+ "cbsDueDate": "2021-11",
+ "cbsDescription": "Next change due when the stated distance has been covered or by the specified date."
+ },
+ {
+ "cbsType": "VEHICLE_TUV",
+ "cbsState": "OK",
+ "cbsDueDate": "2021-11",
+ "cbsDescription": "Next statutory vehicle inspection due by the stated date."
+ }
+ ],
+ "DCS_CCH_Activation": "NA",
+ "DCS_CCH_Ongoing": false
+ }
+}
\ No newline at end of file
<module>org.openhab.binding.bluetooth.govee</module>
<module>org.openhab.binding.bluetooth.roaming</module>
<module>org.openhab.binding.bluetooth.ruuvitag</module>
+ <module>org.openhab.binding.bmwconnecteddrive</module>
<module>org.openhab.binding.boschindego</module>
<module>org.openhab.binding.boschshc</module>
<module>org.openhab.binding.bosesoundtouch</module>