]> git.basschouten.com Git - openhab-addons.git/commitdiff
[mybmw] new binding contribution (#12006)
authorBernd Weymann <bernd.weymann@gmail.com>
Mon, 25 Apr 2022 06:12:55 +0000 (08:12 +0200)
committerGitHub <noreply@github.com>
Mon, 25 Apr 2022 06:12:55 +0000 (08:12 +0200)
* solve pom.xml conflict

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* authorization working

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* Fully Authorization integration & cleanup

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* make project compilable

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* make code compilable & buildable

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* fix discovery test

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* fix property test

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* fix unit tests

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* unit tests fixed

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* solve checkstyle high & medium

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* include all status update channel calls

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* correct Closed/Connected/Locked states

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* add charge statistics

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* add ChargingProfile channels

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* add charging sessions

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* add discovery properties

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* provide general check-control info

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* add test data for different vehicles

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* fix some checkstyle

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* add more vehicles to unit test

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* add F11 test

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* add service mileage

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* add check controls

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* add fingerprint mechanism

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* cleanup channels after rework

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* bugfixe requesting vehicles

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* fix brand handling

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* anonymous fingerprint

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* add charge statistics channels

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* add tire channels

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* remove range max channels

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* resolve last checkstyle issues

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* fix chrge profile updates

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* fix remote service execution

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* fix image handling

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* adapt service & checkcontrol handling

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* bugfix session selection

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* rework km / mi handling

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* introduce fallbacks for range calculations

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* unit tests for all available vehicle fingerprints

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* bugfixes during creation of HMI and translations

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* bugfix translation and language selection

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* introduce climate-now start / stop

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* bugfixes translation

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* add all fuelindicator fields

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* correct remote service ids, commands and translations

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* cleanup remote requsts and responses

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* add remote response examples

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* rework command options

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* remove unused timezoneprovider

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* locale language debugging

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* fix range value calculation

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* introduce charge-info channel

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* localize charge info string

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* smaller charge status fixes

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* 1st readme adaptions plus corresponding bugfixes

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* bugfix date time conversion

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* readme channel group update

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* add motion status

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* raw test anonymous fingerprint

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* revert motion status

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* bugfixes todo sections

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* china login

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* remove unnecessary info logging

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* spell check and example update

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* resolve codeowner conflicts

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* delete rex responses

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* bugfix linux characters

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* bugfix unit test with static time comparison

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* Copyright header adaption

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* add raw data channel

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* add address channel

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* add motion channel

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* add mild hybrid vehicle support

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* review comment corrections

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* correct review comments

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
* resolve bom conflict

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
194 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.mybmw/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/README.md [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/doc/AwayImage.png [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/doc/CarStatusImages.png [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/doc/ChargingImage.png [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/doc/CheckControlImage.png [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/doc/DiscoveryScan.png [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/doc/RawData.png [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/doc/RawDataItems.png [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/doc/SessionOptions.png [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/doc/UnlockedImage.png [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/doc/panel.png [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/doc/properties.png [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/doc/range-radius.png [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/doc/vehicle-properties.png [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/VehicleConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/AuthQueryResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/AuthResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaAccessToken.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaPublicKey.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaPublicKeyResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaTokenExpiration.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaTokenResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeProfile.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSession.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSessions.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSessionsContainer.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeStatistics.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeStatisticsContainer.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSettings.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingWindow.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/Time.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/Timer.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/network/NetworkError.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Address.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/CBS.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/CCM.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/ChargingState.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Coordinates.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Distance.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Doors.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/DoorsWindows.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/FuelLevel.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Location.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Properties.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Range.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Tire.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/TireStatus.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Tires.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Windows.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/remote/ExecutionError.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/remote/ExecutionStatusContainer.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/CBSMessage.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/CCMMessage.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/DoorWindow.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/FuelIndicator.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Issues.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Mileage.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Status.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Capabilities.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/RemoteService.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Vehicle.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/ByteResponseCallback.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWCommandOptionProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWProxy.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/ResponseCallback.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/StringResponseCallback.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/Token.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleChannelHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/simulation/Injector.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/BimmerConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargeProfileUtils.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargeProfileWrapper.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/Constants.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/Converter.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/HTTPConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ImageProperties.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/RemoteServiceUtils.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/VehicleStatusUtils.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/config/bridge-config.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/config/thing-config.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/bridge-connected-drive.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/charge-statistics-channel-groups.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/charge-statistics-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/check-control-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/check-control-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/conv-range-channel-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/door-status-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/doors-status-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/ev-range-channel-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/ev-vehicle-status-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/hybrid-range-channel-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/image-channel-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/image-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/location-channel-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/location-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/profile-channel-groups.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/profile-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/range-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/remote-services-channel-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/remote-services-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/service-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/service-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/session-channel-groups.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/session-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-bev.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-bev_rex.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-conv.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-phev.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/tires-channel-groups.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/tires-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/discovery/DiscoveryTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/ChargeProfileTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/ChargeStatisticWrapper.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/StatusWrapper.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/VehiclePropertiesTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/VehicleStatusTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/AuthTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/ChargeStatisticsTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/ConfigurationTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/ErrorResponseTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/SimulationTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/VehicleTests.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/util/FileReader.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/util/LocaleTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/530e/vehicles.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/F11/vehicles_v2_bmw_0.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/F31/vehicles_v2_bmw_0.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/F44/vehicles_v2_bmw_0.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/F45/vehicles_v2_bmw_0.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/F48/vehicles_v2_bmw_0.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/G01/vehicles_v2_bmw_0.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/G05/vehicles_v2_bmw_0.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/G08/vehicles_v2_bmw_0.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/340i.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/charging-sessions_0.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/charging-statistics_0.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/json_export.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/vehicles_v2_bmw_0.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/G30/charging-sessions_0.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/G30/charging-statistics_0.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/G30/vehicles_v2_bmw_0.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_NOREX/charging-sessions_0.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_NOREX/charging-statistics_0.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_NOREX/vehicles_v2_bmw_0.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charge-sessions.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charge-statistics-de.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charge-statistics-en.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charging-statistics_1.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicle-charging.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicle-fully-charged.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicles-de.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicles.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicles_v2_bmw_0.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/TwoVehicles/anonymous-raw.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/TwoVehicles/f11-raw.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/TwoVehicles/two-vehicles.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/OAuthConfig_ROW_response.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/OAuth_Login_Response.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/auth_cn_login_error.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/auth_cn_login_pwd.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/auth_cn_publickey.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/auth_error_internal_error.txt [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/auth_error_wrong_password.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/auth_token.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/authorization_response.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/china-key.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/oauth_config.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/chargingprofile/two-weeks-timer.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/chargingprofile/weekly-planner-t2-active.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/fingerprint-raw.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/fingerprint.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/remote_services/eadrax_service_delivered.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/remote_services/eadrax_service_eventposition.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/remote_services/eadrax_service_executed.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/remote_services/eadrax_service_initiated.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/remote_services/eadrax_service_pending.json [new file with mode: 0644]
bundles/org.openhab.binding.mybmw/src/test/resources/responses/remote_services/service-error.json [new file with mode: 0644]
bundles/pom.xml

index ee0088abd55978b6bcae2aa83076f98df48b169e..2250a960fa9e37cc87c38b467cd537ebb22ad4e7 100644 (file)
 /bundles/org.openhab.binding.mqtt.homeassistant/ @davidgraeff @antroids
 /bundles/org.openhab.binding.mqtt.homie/ @davidgraeff
 /bundles/org.openhab.binding.mycroft/ @dalgwen
+/bundles/org.openhab.binding.mybmw/ @weymann @ntruchsess
 /bundles/org.openhab.binding.myq/ @digitaldan
 /bundles/org.openhab.binding.mystrom/ @pail23
 /bundles/org.openhab.binding.nanoleaf/ @raepple @stefan-hoehn
index 87c62166da0afa9c4157f81321c111e2573fa3e2..7176056bb0d916457db4325f101535c4a5a9d5ac 100644 (file)
       <artifactId>org.openhab.binding.mycroft</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.mybmw</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.myq</artifactId>
diff --git a/bundles/org.openhab.binding.mybmw/NOTICE b/bundles/org.openhab.binding.mybmw/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.mybmw/README.md b/bundles/org.openhab.binding.mybmw/README.md
new file mode 100644 (file)
index 0000000..9ee7020
--- /dev/null
@@ -0,0 +1,846 @@
+# MyBMW Binding
+
+The binding provides access like [MyBMW App](https://www.bmw.com/en/footer/mybmw-app.html) to 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 information.
+Check for each group if it's supported by your vehicle.
+
+Please note **this isn't a real-time binding**. 
+If a door is opened the state isn't transmitted and changed immediately. 
+It's not a flaw in the binding itself because the state in BMW's own MyBMW App is also updated with some delay. 
+
+## Supported Things
+
+### Bridge
+
+The bridge establishes the connection between BMW API and openHAB.
+
+| Name                       | Bridge Type ID | Description                              |
+|----------------------------|----------------|------------------------------------------|
+| MyBMW Account              | `account`      | Access to BMW API for a specific user    |
+
+
+### Things
+
+Four different vehicle types are provided. 
+They differ in the supported channel groups & channels. 
+Conventional Fuel Vehicles don't provide e.g. _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`         | Vehicle with electric drive train                                   |
+| BMW Electric Vehicle with REX       | `bev_rex`     | Vehicle with electric drive train plus fuel powered range extender  |
+| BMW Plug-In-Hybrid Electric Vehicle | `phev`        | Vehicle with combustion and electric drive train                    |
+| BMW Conventional Vehicle            | `conv`        | Vehicle with combustion drive train                                 |
+
+#### Properties
+
+<img align="right" src="./doc/vehicle-properties.png" width="500" height="350"/>
+
+For each vehicle properties are available. 
+Basic information is given regarding
+
+* Vehicle properties like model type, drive train and construction year
+* Which services are available / not available
+
+In the right picture can see in *remoteServicesEnabled* e.g. the *Door Lock* and *Door Unlock* services are mentioned. 
+This ensures channel group [Remote Services](#remote-services) is supporting door lock and unlock remote control.
+
+In *Services Supported* the entry *ChargingHistory* is mentioned.
+So it's valid to connect channel group [Charge Sessions](#charge-sessions) in order to display your last charging sessions.
+
+| Property Key           | Property Value      |  Supported Channel Groups    |
+|------------------------|---------------------|------------------------------|
+| servicesSupported      | ChargingHistory     | session                      |
+| remoteServicesEnabled  | _list of services_  | remote                       |
+
+
+## Discovery
+
+Auto discovery is starting after the bridge 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    | MyBMW Username                                                     |
+| password        | text    | MyBMW 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)
+
+
+#### Advanced Configuration
+
+| Parameter       | Type    | Description                                             |           
+|-----------------|---------|---------------------------------------------------------|
+| language        | text    | Channel data can be returned in the desired language    |
+
+Language is predefined as *AUTODETECT*.
+Some textual descriptions, date and times are delivered based on your local language.
+You can overwrite this setting with lowercase 2-letter [language code reagrding ISO 639](https://www.oracle.com/java/technologies/javase/jdk8-jre8-suported-locales.html)
+So if want your UI in english language place *en* as desired language.
+
+### Thing Configuration
+
+Same configuration is needed for all things
+
+| Parameter       | Type    | Description                           |           
+|-----------------|---------|---------------------------------------|
+| vin             | text    | Vehicle Identification Number (VIN)   |
+| refreshInterval | integer | Refresh Interval in Minutes           |
+
+
+#### Advanced Configuration
+
+| Parameter       | Type    | Description                       |           
+|-----------------|---------|-----------------------------------|
+| vehicleBrand    | text    | Vehicle Brand like BMW or Mini    |
+
+The _vehicleBrand_ is automatically obtained by the discovery service and shall not be changed.
+If thing is defined manually via *.things file following brands are supported
+
+* BMW
+* MINI
+
+
+## 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 
+
+| Channel Group ID                 | Description                                       | conv | phev | bev_rex | bev |
+|----------------------------------|---------------------------------------------------|------|------|---------|-----|
+| [status](#vehicle-status)        | Overall vehicle status                            |  X   |  X   |    X    |  X  |
+| [range](#range-data)             | Provides mileage, range and charge / fuel levels  |  X   |  X   |    X    |  X  |
+| [doors](#doors-details)          | Detials of all doors and windows                  |  X   |  X   |    X    |  X  |
+| [check](#check-control)          | Shows current active CheckControl messages        |  X   |  X   |    X    |  X  |
+| [service](#services)             | Future vehicle service schedules                  |  X   |  X   |    X    |  X  |
+| [location](#location)            | Coordinates and heading of the vehicle            |  X   |  X   |    X    |  X  |
+| [remote](#remote-services)       | Remote control of the vehicle                     |  X   |  X   |    X    |  X  |
+| [profile](#charge-profile)       | Scheduled charging profiles of vehicle            |      |  X   |    X    |  X  |
+| [statistic](#charge-statistics)  | Charging statistics of current month              |      |  X   |    X    |  X  |
+| [session](#charge-sessions)      | Past charging sessions                            |      |  X   |    X    |  X  |
+| [tires](#tire-pressure)          | Current and wanted pressure for all tires         |  X   |  X   |    X    |  X  |
+| [image](#image)                  | Provides an image of your vehicle                 |  X   |  X   |    X    |  X  |
+
+
+#### 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                                    | conv | phev | bev_rex | bev |
+|---------------------------|---------------------|---------------|------------------------------------------------|------|------|---------|-----|
+| Overall Door Status       | doors               | String        | Combined status for all doors                  |  X   |  X   |    X    |  X  |
+| Overall Window Status     | windows             | String        | Combined status for all windows                |  X   |  X   |    X    |  X  |
+| Doors Locked              | lock                | String        | Status if vehicle is secured                   |  X   |  X   |    X    |  X  |
+| Next Service Date         | service-date        | DateTime      | Date of next upcoming service                  |  X   |  X   |    X    |  X  |
+| Mileage till Next Service | service-mileage     | Number:Length | Mileage till upcoming service                  |  X   |  X   |    X    |  X  |
+| Check Control             | check-control       | String        | Presence of active warning messages            |  X   |  X   |    X    |  X  |
+| Plug Connection Status    | plug-connection     | String        | Plug is _Connected_ or _Not connected_         |      |  X   |    X    |  X  |
+| Charging Status           | charge              | String        | Current charging status                        |      |  X   |    X    |  X  |
+| Charging Information      | charge-info         | String        | Information regarding current charging session |      |  X   |    X    |  X  |
+| Motion Status             | motion              | Switch        | Driving state - depends on vehicle hardware    |  X   |  X   |    X    |  X  |
+| Last Status Timestamp     | last-update         | DateTime      | Date and time of last status update            |  X   |  X   |    X    |  X  |
+
+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
+
+Localized String of current active warnings.
+Examples:
+
+* No Issues
+* Multiple Issues
+
+Charging Status values
+
+* _Not Charging_
+* _Charging_
+* _Plugged In_
+* _Fully Charged_
+
+Charging Information values
+Localized String of current active charging session
+Examples
+
+* 100% at ~00:43
+* Starts at ~09:00
+
+##### Vehicle Status Raw Data
+
+The _raw data channel_ is marked as _advanced_ and isn't shown by default.
+Target are advanced users to derive even more data out of BMW API replies.
+As the replies are formatted as JSON use the [JsonPath Transformation Service](https://www.openhab.org/addons/transformations/jsonpath/) to extract data for an item,   
+
+| Channel Label             | Channel ID          | Type          | Description                                    |
+|---------------------------|---------------------|---------------|------------------------------------------------|
+| Raw Data                  | raw                 | String        | Unfiltered JSON String of vehicle data         |
+
+<img align="right" src="./doc/RawData.png" width="400" height="125"/>
+
+Examples:
+
+_Country ISO Code_
+
+```
+$.properties.originCountryISO
+```
+
+_Drivers Guide URL_
+
+```
+$.driverGuideInfo.androidStoreUrl
+```
+
+#### 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    |     |
+| Electric 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    |     | 
+
+
+#### 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
+
+
+#### Check Control
+
+Group for all current active Check Control 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     |
+|---------------------------------|---------------------|----------------|------------|
+| Check Control Description       | name                | String         | Read/Write |
+| Check Control Details           | details             | String         | Read       |
+| Severity Level                  | severity            | String         | Read       |
+
+Severity Levels
+
+* Ok
+* Low
+* Medium
+
+
+#### 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                | DateTime       | Read       |
+| Mileage till Service           | mileage             | Number:Length  | Read       |
+
+
+#### 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 | 
+| Address         | address             | String       | 
+
+
+#### 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-low_
+* _climate-now-start_
+* _climate-now-stop_
+
+The channel _state_ shows the progress of the command execution in the following order
+
+1) _initiated_ 
+2) _pending_
+3) _delivered_
+4) _executed_
+
+
+#### Charge Profile
+
+Charging options with date and time for preferred time windows and charging modes.
+
+* Channel Group ID is **profile**
+* Available for electric and hybrid vehicles
+* Read access for UI. 
+* There are 4 timers *T1, T2, T3 and T4* available. Replace *X* with number 1,2 or 3 to target the correct timer
+
+| Channel Label              | Channel ID                | Type     | 
+|----------------------------|---------------------------|----------| 
+| Charge Mode                | mode                      | String   | 
+| Charge Preferences         | prefs                     | String   | 
+| Charging Plan              | control                   | String   | 
+| SoC Target                 | target                    | String   | 
+| Charging Energy Limited    | limit                     | Switch   | 
+| Window Start Time          | window-start              | DateTime | 
+| Window End Time            | window-end                | DateTime | 
+| A/C at Departure           | climate                   | Switch   | 
+| T*X* Enabled               | timer*X*-enabled          | Switch   | 
+| T*X* Departure Time        | timer*X*-departure        | DateTime | 
+| T*X* Monday                | timer*X*-day-mon          | Switch   | 
+| T*X* Tuesday               | timer*X*-day-tue          | Switch   | 
+| T*X* Wednesday             | timer*X*-day-wed          | Switch   | 
+| T*X* Thursday              | timer*X*-day-thu          | Switch   | 
+| T*X* Friday                | timer*X*-day-fri          | Switch   | 
+| T*X* Saturday              | timer*X*-day-sat          | Switch   | 
+| T*X* Sunday                | timer*X*-day-sun          | Switch   | 
+
+The channel _profile-mode_ supports
+
+* *immediateCharging*
+* *delayedCharging*
+
+The channel _profile-prefs_ supports
+
+* *noPreSelection*
+* *chargingWindow*
+
+
+#### Charge Statistics
+
+Shows charge statistics of the current month
+
+* Channel Group ID is **statistic**
+* Available for electric and hybrid vehicles 
+* Read-only values
+| Channel Label              | Channel ID              | Type           | 
+|----------------------------|-------------------------|----------------|
+| Charge Statistic Month     | title                   | String         |
+| Energy Charged             | energy                  | Number:Energy  |
+| Charge Sessions            | sessions                | Number         |
+
+
+#### Charge Sessions
+
+Group for past charging sessions.
+If more than one message is active the channel _name_ contains all active messages as options.
+
+* Channel Group ID is **session**
+* Available for electric and hybrid vehicles
+* Read-only values
+
+| Channel Label                   | Channel ID   | Type     |
+|---------------------------------|--------------|----------|
+| Session Title                   | title        | String   |
+| Session Details                 | subtitle     | String   |
+| Charged Energy in Session       | energy       | String   |
+| Issues during Session           | issue        | String   |
+| Session Status                  | status       | String   |
+
+
+#### Tire Pressure
+
+Current and target tire pressure values
+
+* Channel Group ID is **tires**
+* Available for all vehicles if corresponding sensors are built-in 
+* Read-only values
+| Channel Label              | Channel ID              | Type             | 
+|----------------------------|-------------------------|------------------|
+| Front Left                 | fl-current              | Number:Pressure  |
+| Front Left Target          | fl-target               | Number:Pressure  |
+| Front Right                | fr-current              | Number:Pressure  |
+| Front Right Target         | fr-target               | Number:Pressure  |
+| Rear Left                  | rl-current              | Number:Pressure  |
+| Rear Left Target           | rl-target               | Number:Pressure  |
+| Rear Right                 | rr-current              | Number:Pressure  |
+| Rear Right Target          | rr-target               | Number:Pressure  |
+
+
+#### Image
+
+Image representation of the vehicle. 
+
+* 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    |
+
+Possible view ports:
+
+* _VehicleStatus_ Front Side View
+* _VehicleInfo_ Front View
+* _ChargingHistory_ Side View
+* _Default_ Front Side View
+
+
+## Further Descriptions
+
+### Dynamic Data
+
+<img align="right" src="./doc/SessionOptions.png" width="400" height="250"/>
+
+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)
+* Charging Session data delivered in group [Charge Sessions](#charge-sessions)
+
+The channel id _name_ shows the first element as default. 
+All other possibilities are attached as options. 
+The picture on the right shows the _Session Title_ item and 3 possible options. 
+Select the desired service and the corresponding Charge Session with _Energy Charged_, _Session Status_ and _Session Issues_ will be shown.  
+
+### TroubleShooting
+
+BMW has a high range of vehicles supported by their API.
+In case of any issues with this binding help to resolve it! 
+Please perform the following steps:
+
+* Can you log into MyBMW App with your credentials? 
+* Is the vehicle listed in your account? 
+* Is the [MyBMW Brige](#bridge) status _Online_?
+
+If these preconditions are fulfilled proceed with the fingerprint generation. 
+
+#### Generate Debug Fingerprint
+
+<img align="right" src="./doc/DiscoveryScan.png" width="400" height="350"/>
+
+
+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.mybmw
+```
+
+The debug fingerprint is generated every time the discovery is executed.
+To force a new fingerprint perform a _Scan_ for MyBMW things.
+Personal data is eliminated from the log entries so it should be possible to share them in public.
+Data like
+
+* Vehicle Identification Number (VIN)
+* Location data
+
+are anonymized.
+You'll find the fingerprint in the logs with the command
+
+```
+grep "Discovery 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 kilometers the route distance is 192 kilometers.
+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/ui/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 configuration parameters in the Things section
+
+* 4711 - any id you want
+* YOUR_USERNAME - with your MyBMW login username
+* YOUR_PASSWORD - with your MyBMW 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 mybmw:account:4711   "MyBMW Account" [userName="YOUR_USERNAME",password="YOUR_PASSWORD",region="ROW"] {
+         Thing bev_rex i3       "BMW i3 94h REX"                [ vin="VEHICLE_VIN",refreshInterval=5,vehicleBrand="BMW"]
+}
+```
+
+### Items File
+
+```
+Number:Length           i3Mileage                 "Odometer [%d %unit%]"                        <line>          (i3)        {channel="mybmw:bev_rex:4711:i3:range#mileage" }                                                                           
+Number:Length           i3Range                   "Range [%d %unit%]"                           <motion>        (i3)        {channel="mybmw:bev_rex:4711:i3:range#hybrid"}
+Number:Length           i3RangeElectric           "Electric Range [%d %unit%]"                  <motion>        (i3,long)   {channel="mybmw:bev_rex:4711:i3:range#electric"}   
+Number:Length           i3RangeFuel               "Fuel Range [%d %unit%]"                      <motion>        (i3)        {channel="mybmw:bev_rex:4711:i3:range#fuel"}
+Number:Dimensionless    i3BatterySoc              "Battery Charge [%.1f %%]"                    <battery>       (i3,long)   {channel="mybmw:bev_rex:4711:i3:range#soc"}
+Number:Volume           i3Fuel                    "Fuel [%.1f %unit%]"                          <oil>           (i3)        {channel="mybmw:bev_rex:4711:i3:range#remaining-fuel"}
+Number:Length           i3RadiusElectric          "Electric Radius [%d %unit%]"                 <zoom>          (i3)        {channel="mybmw:bev_rex:4711:i3:range#radius-electric" }
+Number:Length           i3RadiusFuel              "Fuel Radius [%d %unit%]"                     <zoom>          (i3)        {channel="mybmw:bev_rex:4711:i3:range#radius-fuel" }
+Number:Length           i3RadiusHybrid            "Hybrid Radius [%d %unit%]"                   <zoom>          (i3)        {channel="mybmw:bev_rex:4711:i3:range#radius-hybrid" }
+
+String                  i3DoorStatus              "Door Status [%s]"                            <lock>          (i3)        {channel="mybmw:bev_rex:4711:i3:status#doors" }
+String                  i3WindowStatus            "Window Status [%s]"                          <lock>          (i3)        {channel="mybmw:bev_rex:4711:i3:status#windows" }
+String                  i3LockStatus              "Lock Status [%s]"                            <lock>          (i3)        {channel="mybmw:bev_rex:4711:i3:status#lock" }
+DateTime                i3NextServiceDate         "Next Service Date [%1$tb %1$tY]"             <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:status#service-date" }
+String                  i3NextServiceMileage      "Next Service Mileage [%d %unit%]"            <line>          (i3)        {channel="mybmw:bev_rex:4711:i3:status#service-mileage" }
+String                  i3CheckControl            "Check Control [%s]"                          <error>         (i3)        {channel="mybmw:bev_rex:4711:i3:status#check-control" }
+String                  i3PlugConnection          "Plug [%s]"                                   <energy>        (i3)        {channel="mybmw:bev_rex:4711:i3:status#plug-connection" } 
+String                  i3ChargingStatus          "[%s]"                                        <energy>        (i3)        {channel="mybmw:bev_rex:4711:i3:status#charge" } 
+String                  i3ChargingInfo            "[%s]"                                        <energy>        (i3)        {channel="mybmw:bev_rex:4711:i3:status#charge-info" } 
+DateTime                i3LastUpdate              "Update [%1$tA, %1$td.%1$tm. %1$tH:%1$tM]"    <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:status#last-update"}
+
+Location                i3Location                "Location  [%s]"                              <zoom>          (i3)        {channel="mybmw:bev_rex:4711:i3:location#gps" }                                                                           
+Number:Angle            i3Heading                 "Heading [%.1f %unit%]"                       <zoom>          (i3)        {channel="mybmw:bev_rex:4711:i3:location#heading" }  
+
+String                  i3RemoteCommand           "Command [%s]"                                <switch>        (i3)        {channel="mybmw:bev_rex:4711:i3:remote#command" } 
+String                  i3RemoteState             "Remote Execution State [%s]"                 <status>        (i3)        {channel="mybmw:bev_rex:4711:i3:remote#state" } 
+
+String                  i3DriverDoor              "Driver Door [%s]"                            <lock>          (i3)        {channel="mybmw:bev_rex:4711:i3:doors#driver-front" }
+String                  i3DriverDoorRear          "Driver Door Rear [%s]"                       <lock>          (i3)        {channel="mybmw:bev_rex:4711:i3:doors#driver-rear" }
+String                  i3PassengerDoor           "Passenger Door [%s]"                         <lock>          (i3)        {channel="mybmw:bev_rex:4711:i3:doors#passenger-front" }
+String                  i3PassengerDoorRear       "Passenger Door Rear [%s]"                    <lock>          (i3)        {channel="mybmw:bev_rex:4711:i3:doors#passenger-rear" }
+String                  i3Hood                    "Hood [%s]"                                   <lock>          (i3)        {channel="mybmw:bev_rex:4711:i3:doors#hood" }
+String                  i3Trunk                   "Trunk [%s]"                                  <lock>          (i3)        {channel="mybmw:bev_rex:4711:i3:doors#trunk" }
+String                  i3DriverWindow            "Driver Window [%s]"                          <lock>          (i3)        {channel="mybmw:bev_rex:4711:i3:doors#win-driver-front" }
+String                  i3DriverWindowRear        "Driver Window Rear [%s]"                     <lock>          (i3)        {channel="mybmw:bev_rex:4711:i3:doors#win-driver-rear" }
+String                  i3PassengerWindow         "Passenger Window [%s]"                       <lock>          (i3)        {channel="mybmw:bev_rex:4711:i3:doors#win-passenger-front" }
+String                  i3PassengerWindowRear     "Passenger Window Rear [%s]"                  <lock>          (i3)        {channel="mybmw:bev_rex:4711:i3:doors#win-passenger-rear" }
+String                  i3RearWindow              "Rear Window [%s]"                            <lock>          (i3)        {channel="mybmw:bev_rex:4711:i3:doors#win-rear" }
+String                  i3Sunroof                 "Sunroof [%s]"                                <lock>          (i3)        {channel="mybmw:bev_rex:4711:i3:doors#sunroof" }
+
+String                  i3ServiceName             "Service Name [%s]"                           <text>          (i3)        {channel="mybmw:bev_rex:4711:i3:service#name" }
+String                  i3ServiceDetails          "Service Details [%s]"                        <text>          (i3)        {channel="mybmw:bev_rex:4711:i3:service#details" }
+Number:Length           i3ServiceMileage          "Service Mileage [%d %unit%]"                 <line>          (i3)        {channel="mybmw:bev_rex:4711:i3:service#mileage" }
+DateTime                i3ServiceDate             "Service Date [%1$tb %1$tY]"                  <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:service#date" }
+
+String                  i3CCName                  "CheckControl Name [%s]"                      <text>          (i3)        {channel="mybmw:bev_rex:4711:i3:check#name" }
+String                  i3CCDetails               "CheckControl Details [%s]"                   <text>          (i3)        {channel="mybmw:bev_rex:4711:i3:check#details" }
+String                  i3CCSeverity              "CheckControl Severity [%s]"                  <line>          (i3)        {channel="mybmw:bev_rex:4711:i3:check#severity" }
+Switch                  i3ChargeProfileClimate    "Charge Profile Climatization"                <temperature>   (i3)        {channel="mybmw:bev_rex:4711:i3:profile#climate" }  
+String                  i3ChargeProfileMode       "Charge Profile Mode [%s]"                    <energy>        (i3)        {channel="mybmw:bev_rex:4711:i3:profile#mode" } 
+String                  i3ChargeProfilePrefs      "Charge Profile Preference [%s]"              <energy>        (i3)        {channel="mybmw:bev_rex:4711:i3:profile#prefs" } 
+String                  i3ChargeProfileCtrl       "Charge Profile Control [%s]"                 <energy>        (i3)        {channel="mybmw:bev_rex:4711:i3:profile#control" } 
+Number                  i3ChargeProfileTarget     "Charge Profile SoC Target [%s]"              <energy>        (i3)        {channel="mybmw:bev_rex:4711:i3:profile#target" } 
+Switch                  i3ChargeProfileLimit      "Charge Profile limited"                      <energy>        (i3)        {channel="mybmw:bev_rex:4711:i3:profile#limit" } 
+DateTime                i3ChargeWindowStart       "Charge Window Start [%1$tH:%1$tM]"           <time>          (i3)        {channel="mybmw:bev_rex:4711:i3:profile#window-start" } 
+DateTime                i3ChargeWindowEnd         "Charge Window End [%1$tH:%1$tM]"             <time>          (i3)        {channel="mybmw:bev_rex:4711:i3:profile#window-end" } 
+DateTime                i3Timer1Departure         "Timer 1 Departure [%1$tH:%1$tM]"             <time>          (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer1-departure" } 
+String                  i3Timer1Days              "Timer 1 Days [%s]"                           <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer1-days" } 
+Switch                  i3Timer1DayMon            "Timer 1 Monday"                              <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer1-day-mon" } 
+Switch                  i3Timer1DayTue            "Timer 1 Tuesday"                             <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer1-day-tue" } 
+Switch                  i3Timer1DayWed            "Timer 1 Wednesday"                           <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer1-day-wed" } 
+Switch                  i3Timer1DayThu            "Timer 1 Thursday"                            <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer1-day-thu" } 
+Switch                  i3Timer1DayFri            "Timer 1 Friday"                              <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer1-day-fri" } 
+Switch                  i3Timer1DaySat            "Timer 1 Saturday"                            <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer1-day-sat" } 
+Switch                  i3Timer1DaySun            "Timer 1 Sunday"                              <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer1-day-sun" } 
+Switch                  i3Timer1Enabled           "Timer 1 Enabled"                             <switch>        (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer1-enabled" }  
+DateTime                i3Timer2Departure         "Timer 2 Departure [%1$tH:%1$tM]"             <time>          (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer2-departure" } 
+Switch                  i3Timer2DayMon            "Timer 2 Monday"                              <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer2-day-mon" } 
+Switch                  i3Timer2DayTue            "Timer 2 Tuesday"                             <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer2-day-tue" } 
+Switch                  i3Timer2DayWed            "Timer 2 Wednesday"                           <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer2-day-wed" } 
+Switch                  i3Timer2DayThu            "Timer 2 Thursday"                            <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer2-day-thu" } 
+Switch                  i3Timer2DayFri            "Timer 2 Friday"                              <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer2-day-fri" } 
+Switch                  i3Timer2DaySat            "Timer 2 Saturday"                            <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer2-day-sat" } 
+Switch                  i3Timer2DaySun            "Timer 2 Sunday"                              <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer2-day-sun" } 
+Switch                  i3Timer2Enabled           "Timer 2 Enabled"                             <switch>        (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer2-enabled" }  
+DateTime                i3Timer3Departure         "Timer 3 Departure [%1$tH:%1$tM]"             <time>          (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer3-departure" } 
+Switch                  i3Timer3DayMon            "Timer 3 Monday"                              <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer3-day-mon" } 
+Switch                  i3Timer3DayTue            "Timer 3 Tuesday"                             <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer3-day-tue" } 
+Switch                  i3Timer3DayWed            "Timer 3 Wednesday"                           <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer3-day-wed" } 
+Switch                  i3Timer3DayThu            "Timer 3 Thursday"                            <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer3-day-thu" } 
+Switch                  i3Timer3DayFri            "Timer 3 Friday"                              <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer3-day-fri" } 
+Switch                  i3Timer3DaySat            "Timer 3 Saturday"                            <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer3-day-sat" } 
+Switch                  i3Timer3DaySun            "Timer 3 Sunday"                              <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer3-day-sun" } 
+Switch                  i3Timer3Enabled           "Timer 3 Enabled"                             <switch>        (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer3-enabled" }
+DateTime                i3Timer4Departure         "Timer 4 Departure [%1$tH:%1$tM]"             <time>          (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer4-departure" } 
+Switch                  i3Timer4DayMon            "Timer 4 Monday"                              <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer4-day-mon" } 
+Switch                  i3Timer4DayTue            "Timer 4 Tuesday"                             <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer4-day-tue" } 
+Switch                  i3Timer4DayWed            "Timer 4 Wednesday"                           <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer4-day-wed" } 
+Switch                  i3Timer4DayThu            "Timer 4 Thursday"                            <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer4-day-thu" } 
+Switch                  i3Timer4DayFri            "Timer 4 Friday"                              <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer4-day-fri" } 
+Switch                  i3Timer4DaySat            "Timer 4 Saturday"                            <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer4-day-sat" } 
+Switch                  i3Timer4DaySun            "Timer 4 Sunday"                              <calendar>      (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer4-day-sun" } 
+Switch                  i3Timer4Enabled           "Timer 4 Enabled"                             <switch>        (i3)        {channel="mybmw:bev_rex:4711:i3:profile#timer4-enabled" }
+
+String                  i3StatisticsTitle         "[%s]"                                        <text>          (i3)        {channel="mybmw:bev_rex:4711:i3:statistic#title" }
+Number:Energy           i3StatisticsEnergy        "Charged [%d %unit%]"                         <energy>        (i3)        {channel="mybmw:bev_rex:4711:i3:statistic#energy" }
+Number                  i3StatisticsSessions      "Sessions [%d]"                               <line>          (i3)        {channel="mybmw:bev_rex:4711:i3:statistic#sessions" }
+
+String                  i3SessionTitle            "[%s]"                                        <text>          (i3)        {channel="mybmw:bev_rex:4711:i3:session#title" }
+String                  i3SessionDetails          "[%s]"                                        <text>          (i3)        {channel="mybmw:bev_rex:4711:i3:session#subtitle" }
+String                  i3SessionCharged          "Energy Charged [%s]"                         <energy>        (i3)        {channel="mybmw:bev_rex:4711:i3:session#energy" }
+String                  i3SessionProblems         "Problems [%s]"                               <error>         (i3)        {channel="mybmw:bev_rex:4711:i3:session#issue" }
+String                  i3SessionStatus           "Session status [%s]"                         <text>          (i3)        {channel="mybmw:bev_rex:4711:i3:session#status" }
+
+Number:Pressure         i3TireFLCurrent           "Tire Front Left [%.1f %unit%]"               <text>          (i3)        {channel="mybmw:bev_rex:4711:i3:tires#fl-current" }
+Number:Pressure         i3TireFLTarget            "Tire Front Left Target [%.1f %unit%]"        <text>          (i3)        {channel="mybmw:bev_rex:4711:i3:tires#fl-target" }
+Number:Pressure         i3TireFRCurrent           "Tire Front Right [%.1f %unit%]"              <text>          (i3)        {channel="mybmw:bev_rex:4711:i3:tires#fr-current" }
+Number:Pressure         i3TireFRTarget            "Tire Front Right Target [%.1f %unit%]"       <text>          (i3)        {channel="mybmw:bev_rex:4711:i3:tires#fr-target" }
+Number:Pressure         i3TireRLCurrent           "Tire Rear Left [%.1f %unit%]"                <text>          (i3)        {channel="mybmw:bev_rex:4711:i3:tires#rl-current" }
+Number:Pressure         i3TireRLTarget            "Tire Rear Left Target [%.1f %unit%]"         <text>          (i3)        {channel="mybmw:bev_rex:4711:i3:tires#rl-target" }
+Number:Pressure         i3TireRRCurrent           "Tire Rear Right [%.1f %unit%]"               <text>          (i3)        {channel="mybmw:bev_rex:4711:i3:tires#rr-current" }
+Number:Pressure         i3TireRRTarget            "Tire Rear Right Target [%.1f %unit%]"        <text>          (i3)        {channel="mybmw:bev_rex:4711:i3:tires#rr-target" }
+
+Image                   i3Image                   "Image"                                                       (i3)        {channel="mybmw:bev_rex:4711:i3:image#png" }  
+String                  i3ImageViewport           "Image Viewport [%s]"                         <zoom>          (i3)        {channel="mybmw:bev_rex:4711:i3:image#view" }  
+```
+
+### Sitemap File
+
+```
+sitemap BMW label="BMW" {
+  Frame label="BMW i3" {
+    Image  item=i3Image  
+                       
+  } 
+  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="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="Remote Services" {
+    Selection item=i3RemoteCommand              
+    Text      item=i3RemoteState              
+  }
+  Frame label="Services" {
+    Selection    item=i3ServiceName          
+    Text         item=i3ServiceDetails          
+    Text         item=i3ServiceMileage          
+    Text         item=i3ServiceDate          
+  }
+  Frame label="CheckControl" {
+    Selection    item=i3CCName          
+    Text         item=i3CCDetails          
+    Text         item=i3CCSeverity          
+  }
+  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        
+    Text      item=i3ChargeWindowEnd          
+    Text      item=i3Timer1Departure          
+    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          
+    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          
+    Switch    item=i3Timer3DayMon            
+    Switch    item=i3Timer3DayTue            
+    Switch    item=i3Timer3DayWed            
+    Switch    item=i3Timer3DayThu            
+    Switch    item=i3Timer3DayFri            
+    Switch    item=i3Timer3DaySat            
+    Switch    item=i3Timer3DaySun            
+    Switch    item=i3Timer3Enabled            
+    Text      item=i3Timer4Departure          
+    Switch    item=i3Timer4DayMon            
+    Switch    item=i3Timer4DayTue            
+    Switch    item=i3Timer4DayWed            
+    Switch    item=i3Timer4DayThu            
+    Switch    item=i3Timer4DayFri            
+    Switch    item=i3Timer4DaySat            
+    Switch    item=i3Timer4DaySun            
+    Switch    item=i3Timer4Enabled            
+  } 
+  Frame label="Charge Statistics" {
+    Text    item=i3StatisticsTitle          
+    Text    item=i3StatisticsEnergy             
+    Text    item=i3StatisticsSessions          
+  }
+
+  Frame label="Charge Sessions" {
+    Selection    item=i3SessionTitle          
+    Text         item=i3SessionDetails             
+    Text         item=i3SessionCharged          
+    Text         item=i3SessionProblems             
+    Text         item=i3SessionStatus          
+  }
+  Frame label="Tires" {
+    Text    item=i3TireFLCurrent          
+    Text    item=i3TireFLTarget             
+    Text    item=i3TireFRCurrent          
+    Text    item=i3TireFRTarget             
+    Text    item=i3TireRLCurrent          
+    Text    item=i3TireRLTarget             
+    Text    item=i3TireRRCurrent          
+    Text    item=i3TireRRTarget             
+  }
+  Frame label="Image Properties" {
+    Selection    item=i3ImageViewport
+  } 
+}
+```
+
+## Credits
+
+This work is based on the project of [Bimmer Connected](https://github.com/bimmerconnected/bimmer_connected). 
+
diff --git a/bundles/org.openhab.binding.mybmw/doc/AwayImage.png b/bundles/org.openhab.binding.mybmw/doc/AwayImage.png
new file mode 100644 (file)
index 0000000..24dd33c
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/AwayImage.png differ
diff --git a/bundles/org.openhab.binding.mybmw/doc/CarStatusImages.png b/bundles/org.openhab.binding.mybmw/doc/CarStatusImages.png
new file mode 100644 (file)
index 0000000..b198ede
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/CarStatusImages.png differ
diff --git a/bundles/org.openhab.binding.mybmw/doc/ChargingImage.png b/bundles/org.openhab.binding.mybmw/doc/ChargingImage.png
new file mode 100644 (file)
index 0000000..09a132f
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/ChargingImage.png differ
diff --git a/bundles/org.openhab.binding.mybmw/doc/CheckControlImage.png b/bundles/org.openhab.binding.mybmw/doc/CheckControlImage.png
new file mode 100644 (file)
index 0000000..4f8aa93
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/CheckControlImage.png differ
diff --git a/bundles/org.openhab.binding.mybmw/doc/DiscoveryScan.png b/bundles/org.openhab.binding.mybmw/doc/DiscoveryScan.png
new file mode 100644 (file)
index 0000000..bba0f19
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/DiscoveryScan.png differ
diff --git a/bundles/org.openhab.binding.mybmw/doc/RawData.png b/bundles/org.openhab.binding.mybmw/doc/RawData.png
new file mode 100644 (file)
index 0000000..c474ef7
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/RawData.png differ
diff --git a/bundles/org.openhab.binding.mybmw/doc/RawDataItems.png b/bundles/org.openhab.binding.mybmw/doc/RawDataItems.png
new file mode 100644 (file)
index 0000000..3a5a140
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/RawDataItems.png differ
diff --git a/bundles/org.openhab.binding.mybmw/doc/SessionOptions.png b/bundles/org.openhab.binding.mybmw/doc/SessionOptions.png
new file mode 100644 (file)
index 0000000..87138ba
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/SessionOptions.png differ
diff --git a/bundles/org.openhab.binding.mybmw/doc/UnlockedImage.png b/bundles/org.openhab.binding.mybmw/doc/UnlockedImage.png
new file mode 100644 (file)
index 0000000..7518330
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/UnlockedImage.png differ
diff --git a/bundles/org.openhab.binding.mybmw/doc/panel.png b/bundles/org.openhab.binding.mybmw/doc/panel.png
new file mode 100644 (file)
index 0000000..efc3a61
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/panel.png differ
diff --git a/bundles/org.openhab.binding.mybmw/doc/properties.png b/bundles/org.openhab.binding.mybmw/doc/properties.png
new file mode 100644 (file)
index 0000000..ce2a263
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/properties.png differ
diff --git a/bundles/org.openhab.binding.mybmw/doc/range-radius.png b/bundles/org.openhab.binding.mybmw/doc/range-radius.png
new file mode 100644 (file)
index 0000000..21fc8fb
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/range-radius.png differ
diff --git a/bundles/org.openhab.binding.mybmw/doc/vehicle-properties.png b/bundles/org.openhab.binding.mybmw/doc/vehicle-properties.png
new file mode 100644 (file)
index 0000000..7c96844
Binary files /dev/null and b/bundles/org.openhab.binding.mybmw/doc/vehicle-properties.png differ
diff --git a/bundles/org.openhab.binding.mybmw/pom.xml b/bundles/org.openhab.binding.mybmw/pom.xml
new file mode 100644 (file)
index 0000000..794a20d
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  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.3.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.mybmw</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: MyBMW Binding</name>
+
+</project>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/feature/feature.xml b/bundles/org.openhab.binding.mybmw/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..983d7b5
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.mybmw-${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-mybmw" description="MyBMW Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.mybmw/${project.version}</bundle>
+       </feature>
+</features>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConfiguration.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConfiguration.java
new file mode 100644 (file)
index 0000000..107ce19
--- /dev/null
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mybmw.internal.utils.Constants;
+
+/**
+ * The {@link MyBMWConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class MyBMWConfiguration {
+
+    /**
+     * Depending on the location the correct server needs to be called
+     */
+    public String region = Constants.EMPTY;
+
+    /**
+     * MyBMW App Username
+     */
+    public String userName = Constants.EMPTY;
+
+    /**
+     * MyBMW App Password
+     */
+    public String password = Constants.EMPTY;
+
+    /**
+     * Preferred Locale language
+     */
+    public String language = Constants.LANGUAGE_AUTODETECT;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConstants.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConstants.java
new file mode 100644 (file)
index 0000000..b26cc0a
--- /dev/null
@@ -0,0 +1,204 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link MyBMWConstants} 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 MyBMWConstants {
+
+    private static final String BINDING_ID = "mybmw";
+
+    public static final String VIN = "vin";
+
+    public static final int DEFAULT_IMAGE_SIZE_PX = 1024;
+    public static final int DEFAULT_REFRESH_INTERVAL_MINUTES = 5;
+
+    // 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"),
+        MILD_HYBRID("hybrid"),
+        ELECTRIC_REX("bev_rex"),
+        ELECTRIC("bev"),
+        UNKNOWN("unknown");
+
+        private final String type;
+
+        VehicleType(String s) {
+            type = s;
+        }
+
+        @Override
+        public String toString() {
+            return type;
+        }
+    }
+
+    public enum ChargingMode {
+        immediateCharging,
+        delayedCharging
+    }
+
+    public enum ChargingPreference {
+        noPreSelection,
+        chargingWindow
+    }
+
+    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());
+
+    // 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_REMOTE = "remote";
+    public static final String CHANNEL_GROUP_CHARGE_PROFILE = "profile";
+    public static final String CHANNEL_GROUP_CHARGE_STATISTICS = "statistic";
+    public static final String CHANNEL_GROUP_CHARGE_SESSION = "session";
+    public static final String CHANNEL_GROUP_TIRES = "tires";
+    public static final String CHANNEL_GROUP_VEHICLE_IMAGE = "image";
+
+    // Charge Statistics & Sessions
+    public static final String SESSIONS = "sessions";
+    public static final String ENERGY = "energy";
+    public static final String TITLE = "title";
+    public static final String SUBTITLE = "subtitle";
+    public static final String ISSUE = "issue";
+    public static final String STATUS = "status";
+
+    // Generic Constants for several groups
+    public static final String NAME = "name";
+    public static final String DETAILS = "details";
+    public static final String SEVERITY = "severity";
+    public static final String DATE = "date";
+    public static final String MILEAGE = "mileage";
+    public static final String GPS = "gps";
+    public static final String HEADING = "heading";
+    public static final String ADDRESS = "address";
+
+    // 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 PLUG_CONNECTION = "plug-connection";
+    public static final String CHARGE_STATUS = "charge";
+    public static final String CHARGE_INFO = "charge-info";
+    public static final String MOTION = "motion";
+    public static final String LAST_UPDATE = "last-update";
+    public static final String RAW = "raw";
+
+    // 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 = "climate";
+    public static final String CHARGE_PROFILE_MODE = "mode";
+    public static final String CHARGE_PROFILE_PREFERENCE = "prefs";
+    public static final String CHARGE_PROFILE_CONTROL = "control";
+    public static final String CHARGE_PROFILE_TARGET = "target";
+    public static final String CHARGE_PROFILE_LIMIT = "limit";
+    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_TIMER4 = "timer4";
+    public static final String CHARGE_DEPARTURE = "-departure";
+    public static final String CHARGE_ENABLED = "-enabled";
+    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_ELECTRIC = "electric";
+    public static final String RANGE_RADIUS_ELECTRIC = "radius-electric";
+    public static final String RANGE_FUEL = "fuel";
+    public static final String RANGE_RADIUS_FUEL = "radius-fuel";
+    public static final String RANGE_HYBRID = "hybrid";
+    public static final String RANGE_RADIUS_HYBRID = "radius-hybrid";
+    public static final String REMAINING_FUEL = "remaining-fuel";
+    public static final String SOC = "soc";
+
+    // Image
+    public static final String IMAGE_FORMAT = "png";
+    public static final String IMAGE_VIEWPORT = "view";
+
+    // Remote Services
+    public static final String REMOTE_SERVICE_LIGHT_FLASH = "light-flash";
+    public static final String REMOTE_SERVICE_VEHICLE_FINDER = "vehicle-finder";
+    public static final String REMOTE_SERVICE_DOOR_LOCK = "door-lock";
+    public static final String REMOTE_SERVICE_DOOR_UNLOCK = "door-unlock";
+    public static final String REMOTE_SERVICE_HORN = "horn-blow";
+    public static final String REMOTE_SERVICE_AIR_CONDITIONING_START = "climate-now-start";
+    public static final String REMOTE_SERVICE_AIR_CONDITIONING_STOP = "climate-now-stop";
+
+    public static final String REMOTE_SERVICE_COMMAND = "command";
+    public static final String REMOTE_STATE = "state";
+
+    // TIRES
+    public static final String FRONT_LEFT_CURRENT = "fl-current";
+    public static final String FRONT_LEFT_TARGET = "fl-target";
+    public static final String FRONT_RIGHT_CURRENT = "fr-current";
+    public static final String FRONT_RIGHT_TARGET = "fr-target";
+    public static final String REAR_LEFT_CURRENT = "rl-current";
+    public static final String REAR_LEFT_TARGET = "rl-target";
+    public static final String REAR_RIGHT_CURRENT = "rr-current";
+    public static final String REAR_RIGHT_TARGET = "rr-target";
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWHandlerFactory.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWHandlerFactory.java
new file mode 100644 (file)
index 0000000..902d51e
--- /dev/null
@@ -0,0 +1,71 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal;
+
+import static org.openhab.binding.mybmw.internal.MyBMWConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mybmw.internal.handler.MyBMWBridgeHandler;
+import org.openhab.binding.mybmw.internal.handler.MyBMWCommandOptionProvider;
+import org.openhab.binding.mybmw.internal.handler.VehicleHandler;
+import org.openhab.core.i18n.LocaleProvider;
+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 MyBMWHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.mybmw", service = ThingHandlerFactory.class)
+public class MyBMWHandlerFactory extends BaseThingHandlerFactory {
+    private final HttpClientFactory httpClientFactory;
+    private final MyBMWCommandOptionProvider commandOptionProvider;
+    private String localeLanguage;
+
+    @Activate
+    public MyBMWHandlerFactory(final @Reference HttpClientFactory hcf, final @Reference MyBMWCommandOptionProvider cop,
+            final @Reference LocaleProvider lp) {
+        httpClientFactory = hcf;
+        commandOptionProvider = cop;
+        localeLanguage = lp.getLocale().getLanguage().toLowerCase();
+    }
+
+    @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 MyBMWBridgeHandler((Bridge) thing, httpClientFactory, localeLanguage);
+        } else if (SUPPORTED_THING_SET.contains(thingTypeUID)) {
+            VehicleHandler vh = new VehicleHandler(thing, commandOptionProvider, thingTypeUID.getId());
+            return vh;
+        }
+        return null;
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/VehicleConfiguration.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/VehicleConfiguration.java
new file mode 100644 (file)
index 0000000..6d8d04a
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mybmw.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;
+
+    /**
+     * Vehicle brand
+     * - bmw
+     * - mini
+     */
+    public String vehicleBrand = Constants.EMPTY;
+
+    /**
+     * Data refresh rate in minutes
+     */
+    public int refreshInterval = MyBMWConstants.DEFAULT_REFRESH_INTERVAL_MINUTES;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java
new file mode 100644 (file)
index 0000000..9c6d80c
--- /dev/null
@@ -0,0 +1,223 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.discovery;
+
+import static org.openhab.binding.mybmw.internal.MyBMWConstants.SUPPORTED_THING_SET;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+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.openhab.binding.mybmw.internal.MyBMWConstants;
+import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
+import org.openhab.binding.mybmw.internal.handler.MyBMWBridgeHandler;
+import org.openhab.binding.mybmw.internal.handler.RemoteServiceHandler;
+import org.openhab.binding.mybmw.internal.utils.Constants;
+import org.openhab.binding.mybmw.internal.utils.VehicleStatusUtils;
+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 BMW API and is identifying the Vehicles after response
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class VehicleDiscovery extends AbstractDiscoveryService implements DiscoveryService, ThingHandlerService {
+    private static final Logger LOGGER = LoggerFactory.getLogger(VehicleDiscovery.class);
+    public static final String SUPPORTED_SUFFIX = "Supported";
+    public static final String ENABLE_SUFFIX = "Enable";
+    public static final String ENABLED_SUFFIX = "Enabled";
+    private static final int DISCOVERY_TIMEOUT = 10;
+    private Optional<MyBMWBridgeHandler> bridgeHandler = Optional.empty();
+
+    public VehicleDiscovery() {
+        super(SUPPORTED_THING_SET, DISCOVERY_TIMEOUT, false);
+    }
+
+    public void onResponse(List<Vehicle> vehicleList) {
+        bridgeHandler.ifPresent(bridge -> {
+            final ThingUID bridgeUID = bridge.getThing().getUID();
+            vehicleList.forEach(vehicle -> {
+                // the DriveTrain field in the delivered json is defining the Vehicle Type
+                String vehicleType = VehicleStatusUtils.vehicleType(vehicle.driveTrain, vehicle.model).toString();
+                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<>();
+                        // Vehicle Properties
+                        properties.put("vehicleModel", vehicle.model);
+                        properties.put("vehicleDriveTrain", vehicle.driveTrain);
+                        properties.put("vehicleConstructionYear", Integer.toString(vehicle.year));
+                        properties.put("vehicleBodytype", vehicle.bodyType);
+
+                        properties.put("servicesSupported", getServices(vehicle, SUPPORTED_SUFFIX, true));
+                        properties.put("servicesUnsupported", getServices(vehicle, SUPPORTED_SUFFIX, false));
+                        String servicesEnabled = getServices(vehicle, ENABLED_SUFFIX, true) + Constants.SEMICOLON
+                                + getServices(vehicle, ENABLE_SUFFIX, true);
+                        properties.put("servicesEnabled", servicesEnabled.trim());
+                        String servicesDisabled = getServices(vehicle, ENABLED_SUFFIX, false) + Constants.SEMICOLON
+                                + getServices(vehicle, ENABLE_SUFFIX, false);
+                        properties.put("servicesDisabled", servicesDisabled.trim());
+
+                        // For RemoteServices we need to do it step-by-step
+                        StringBuffer remoteServicesEnabled = new StringBuffer();
+                        StringBuffer remoteServicesDisabled = new StringBuffer();
+                        if (vehicle.capabilities.lock.isEnabled) {
+                            remoteServicesEnabled.append(
+                                    RemoteServiceHandler.RemoteService.DOOR_LOCK.getLabel() + Constants.SEMICOLON);
+                        } else {
+                            remoteServicesDisabled.append(
+                                    RemoteServiceHandler.RemoteService.DOOR_LOCK.getLabel() + Constants.SEMICOLON);
+                        }
+                        if (vehicle.capabilities.unlock.isEnabled) {
+                            remoteServicesEnabled.append(
+                                    RemoteServiceHandler.RemoteService.DOOR_UNLOCK.getLabel() + Constants.SEMICOLON);
+                        } else {
+                            remoteServicesDisabled.append(
+                                    RemoteServiceHandler.RemoteService.DOOR_UNLOCK.getLabel() + Constants.SEMICOLON);
+                        }
+                        if (vehicle.capabilities.lights.isEnabled) {
+                            remoteServicesEnabled.append(
+                                    RemoteServiceHandler.RemoteService.LIGHT_FLASH.getLabel() + Constants.SEMICOLON);
+                        } else {
+                            remoteServicesDisabled.append(
+                                    RemoteServiceHandler.RemoteService.LIGHT_FLASH.getLabel() + Constants.SEMICOLON);
+                        }
+                        if (vehicle.capabilities.horn.isEnabled) {
+                            remoteServicesEnabled.append(
+                                    RemoteServiceHandler.RemoteService.HORN_BLOW.getLabel() + Constants.SEMICOLON);
+                        } else {
+                            remoteServicesDisabled.append(
+                                    RemoteServiceHandler.RemoteService.HORN_BLOW.getLabel() + Constants.SEMICOLON);
+                        }
+                        if (vehicle.capabilities.vehicleFinder.isEnabled) {
+                            remoteServicesEnabled.append(
+                                    RemoteServiceHandler.RemoteService.VEHICLE_FINDER.getLabel() + Constants.SEMICOLON);
+                        } else {
+                            remoteServicesDisabled.append(
+                                    RemoteServiceHandler.RemoteService.VEHICLE_FINDER.getLabel() + Constants.SEMICOLON);
+                        }
+                        if (vehicle.capabilities.climateNow.isEnabled) {
+                            remoteServicesEnabled.append(RemoteServiceHandler.RemoteService.CLIMATE_NOW_START.getLabel()
+                                    + Constants.SEMICOLON);
+                        } else {
+                            remoteServicesDisabled
+                                    .append(RemoteServiceHandler.RemoteService.CLIMATE_NOW_START.getLabel()
+                                            + Constants.SEMICOLON);
+                        }
+                        properties.put("remoteServicesEnabled", remoteServicesEnabled.toString().trim());
+                        properties.put("remoteServicesDisabled", remoteServicesDisabled.toString().trim());
+
+                        // Update Properties for already created Things
+                        bridge.getThing().getThings().forEach(vehicleThing -> {
+                            Configuration c = vehicleThing.getConfiguration();
+                            if (c.containsKey(MyBMWConstants.VIN)) {
+                                String thingVIN = c.get(MyBMWConstants.VIN).toString();
+                                if (vehicle.vin.equals(thingVIN)) {
+                                    vehicleThing.setProperties(properties);
+                                }
+                            }
+                        });
+
+                        // Properties needed for functional Thing
+                        properties.put(MyBMWConstants.VIN, vehicle.vin);
+                        properties.put("vehicleBrand", vehicle.brand);
+                        properties.put("refreshInterval",
+                                Integer.toString(MyBMWConstants.DEFAULT_REFRESH_INTERVAL_MINUTES));
+
+                        String vehicleLabel = vehicle.brand + " " + vehicle.model;
+                        Map<String, Object> convertedProperties = new HashMap<String, Object>(properties);
+                        thingDiscovered(DiscoveryResultBuilder.create(uid).withBridge(bridgeUID)
+                                .withRepresentationProperty(MyBMWConstants.VIN).withLabel(vehicleLabel)
+                                .withProperties(convertedProperties).build());
+                    }
+                });
+            });
+        });
+    }
+
+    @Override
+    public void setThingHandler(ThingHandler handler) {
+        if (handler instanceof MyBMWBridgeHandler) {
+            bridgeHandler = Optional.of((MyBMWBridgeHandler) handler);
+            bridgeHandler.get().setDiscoveryService(this);
+        }
+    }
+
+    @Override
+    public @Nullable ThingHandler getThingHandler() {
+        return bridgeHandler.orElse(null);
+    }
+
+    @Override
+    protected void startScan() {
+        bridgeHandler.ifPresent(MyBMWBridgeHandler::requestVehicles);
+    }
+
+    @Override
+    public void deactivate() {
+        super.deactivate();
+    }
+
+    public static String getServices(Vehicle vehicle, String suffix, boolean enabled) {
+        StringBuffer sb = new StringBuffer();
+        List<String> l = getObject(vehicle.capabilities, enabled);
+        for (String capEntry : l) {
+            // remove "is" prefix
+            String cut = capEntry.substring(2);
+            if (cut.endsWith(suffix)) {
+                if (sb.length() > 0) {
+                    sb.append(Constants.SEMICOLON);
+                }
+                sb.append(cut.substring(0, cut.length() - suffix.length()));
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 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 static List<String> getObject(Object dto, Object compare) {
+        List<String> l = new ArrayList<String>();
+        for (Field field : dto.getClass().getDeclaredFields()) {
+            try {
+                Object value = field.get(dto);
+                if (compare.equals(value)) {
+                    l.add(field.getName());
+                }
+            } catch (IllegalArgumentException | IllegalAccessException e) {
+                LOGGER.debug("Field {} not found {}", compare, e.getMessage());
+            }
+        }
+        return l;
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/AuthQueryResponse.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/AuthQueryResponse.java
new file mode 100644 (file)
index 0000000..faee2f0
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.auth;
+
+import java.util.List;
+
+/**
+ * The {@link AuthQueryResponse} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class AuthQueryResponse {
+    public String clientName;// ": "mybmwapp",
+    public String clientSecret;// ": "c0e3393d-70a2-4f6f-9d3c-8530af64d552",
+    public String clientId;// ": "31c357a0-7a1d-4590-aa99-33b97244d048",
+    public String gcdmBaseUrl;// ": "https://customer.bmwgroup.com",
+    public String returnUrl;// ": "com.bmw.connected://oauth",
+    public String brand;// ": "bmw",
+    public String language;// ": "en",
+    public String country;// ": "US",
+    public String authorizationEndpoint;// ": "https://customer.bmwgroup.com/oneid/login",
+    public String tokenEndpoint;// ": "https://customer.bmwgroup.com/gcdm/oauth/token",
+    public List<String> scopes;// ;": [
+    // "openid",
+    // "profile",
+    // "email",
+    // "offline_access",
+    // "smacc",
+    // "vehicle_data",
+    // "perseus",
+    // "dlm",
+    // "svds",
+    // "cesim",
+    // "vsapi",
+    // "remote_services",
+    // "fupo",
+    // "authenticate_user"
+    // ],
+    public List<String> promptValues; // ": ["login"]
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/AuthResponse.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/AuthResponse.java
new file mode 100644 (file)
index 0000000..8206157
--- /dev/null
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.auth;
+
+import org.openhab.binding.mybmw.internal.utils.Constants;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link AuthResponse} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class AuthResponse {
+    @SerializedName("access_token")
+    public String accessToken = Constants.EMPTY;
+    @SerializedName("token_type")
+    public String tokenType = Constants.EMPTY;
+    @SerializedName("expires_in")
+    public int expiresIn = -1;
+
+    @Override
+    public String toString() {
+        return "Token " + accessToken + " type " + tokenType + " expires in " + expiresIn;
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaAccessToken.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaAccessToken.java
new file mode 100644 (file)
index 0000000..2ffb191
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.auth;
+
+import org.openhab.binding.mybmw.internal.utils.Constants;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link ChinaAccessToken} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class ChinaAccessToken {
+    @SerializedName("access_token")
+    public String accessToken = Constants.EMPTY;
+    @SerializedName("token_type")
+    public String tokenType = Constants.EMPTY;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaPublicKey.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaPublicKey.java
new file mode 100644 (file)
index 0000000..ff76b0a
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.auth;
+
+/**
+ * The {@link ChinaPublicKey} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class ChinaPublicKey {
+    public String value;// ": "-----BEGIN PUBLIC
+                        // KEY-----\r\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCteEZFIGa2z5cj7sAmX40y8/ige01T2r+VUzkMshAYwotZFvrVWZLQ6W9+ltvINJoRfZEZkmdP2lsidhqj1H1+RWyC78ear7Fm6xd9Gp9LnKtVVBJRM/9cBRg0AGiTJ7IO/x6MpKkBxxHmProFqPI40hueunV85RlaPBrjZVNIpQIDAQAB\r\n-----END
+                        // PUBLIC KEY-----",
+    public String expires;// ": "3600"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaPublicKeyResponse.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaPublicKeyResponse.java
new file mode 100644 (file)
index 0000000..1bb2cc9
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.auth;
+
+/**
+ * The {@link ChinaPublicKeyResponse} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class ChinaPublicKeyResponse {
+    public ChinaPublicKey data;
+    public int code;// ":200,
+    public String error;// ":false,
+    public String description;// ":"ok"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaTokenExpiration.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaTokenExpiration.java
new file mode 100644 (file)
index 0000000..fc00a4a
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.auth;
+
+/**
+ * The {@link ChinaTokenExpiration} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class ChinaTokenExpiration {
+    public String jti;// ":"DUMMY$1$A$1637707916782",
+    public long nbf;// ":1637707916,
+    public long exp;// ":1637711216,
+    public long iat;// ":1637707916}
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaTokenResponse.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/ChinaTokenResponse.java
new file mode 100644 (file)
index 0000000..aad71b6
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.auth;
+
+/**
+ * The {@link ChinaTokenResponse} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class ChinaTokenResponse {
+    public ChinaAccessToken data;
+    public int code;// ":200,
+    public String error;// ":false,
+    public String description;// ":"ok"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeProfile.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeProfile.java
new file mode 100644 (file)
index 0000000..4cc9722
--- /dev/null
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.charge;
+
+import java.util.List;
+
+/**
+ * The {@link ChargeProfile} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - edit & send of charge profile
+ */
+public class ChargeProfile {
+    public static final Timer INVALID_TIMER = new Timer();
+
+    public ChargingWindow reductionOfChargeCurrent;
+    public String chargingMode;// ": "immediateCharging",
+    public String chargingPreference;// ": "chargingWindow",
+    public String chargingControlType;// ": "weeklyPlanner",
+    public List<Timer> departureTimes;
+    public boolean climatisationOn;// ": false,
+    public ChargingSettings chargingSettings;
+
+    public Timer getTimerId(int id) {
+        if (departureTimes != null) {
+            for (Timer t : departureTimes) {
+                if (t.id == id) {
+                    return t;
+                }
+            }
+        }
+        return INVALID_TIMER;
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSession.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSession.java
new file mode 100644 (file)
index 0000000..1a7b4af
--- /dev/null
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.charge;
+
+/**
+ * The {@link ChargeSession} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class ChargeSession {
+    public String id;// ": "2021-12-26T16:57:20Z_128fa4af",
+    public String title;// ": "Gestern 17:57",
+    public String subtitle;// ": "Uferstraße 4B • 7h 45min • -- EUR",
+    public String energyCharged;// ": "~ 31 kWh",
+    public String sessionStatus;// ": "FINISHED",
+    public String issues;// ": "2 Probleme",
+    public String isPublic;// ": false
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSessions.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSessions.java
new file mode 100644 (file)
index 0000000..77862a3
--- /dev/null
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.charge;
+
+import java.util.List;
+
+/**
+ * The {@link ChargeSessions} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class ChargeSessions {
+    public String total;// ": "~ 218 kWh",
+    public String numberOfSessions;// ": "17",
+    public String chargingListState;// ": "HAS_SESSIONS",
+    public List<ChargeSession> sessions;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSessionsContainer.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSessionsContainer.java
new file mode 100644 (file)
index 0000000..5e56493
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.charge;
+
+/**
+ * The {@link ChargeSessionsContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class ChargeSessionsContainer {
+    public Object paginationInfo;
+    public ChargeSessions chargingSessions;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeStatistics.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeStatistics.java
new file mode 100644 (file)
index 0000000..c618530
--- /dev/null
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.charge;
+
+/**
+ * The {@link ChargeStatistics} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class ChargeStatistics {
+    public int totalEnergyCharged;// ": 173,
+    public String totalEnergyChargedSemantics;// ": "Insgesamt circa 173 Kilowattstunden geladen",
+    public String symbol;// ": "~",
+    public int numberOfChargingSessions;// ": 13,
+    public String numberOfChargingSessionsSemantics;// ": "13 Ladevorgänge"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeStatisticsContainer.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeStatisticsContainer.java
new file mode 100644 (file)
index 0000000..d737acb
--- /dev/null
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.charge;
+
+/**
+ * The {@link ChargeStatisticsContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class ChargeStatisticsContainer {
+    public String description;// ": "Dezember 2021",
+    public String optStateType;// ": "OPT_IN_WITH_SESSIONS",
+    public ChargeStatistics statistics;// ": {
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSettings.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSettings.java
new file mode 100644 (file)
index 0000000..cb2ea46
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.charge;
+
+/**
+ * The {@link ChargingSettings} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class ChargingSettings {
+    public int targetSoc;// ": 100,
+    public boolean isAcCurrentLimitActive;// ": false,
+    public String hospitality;// ": "NO_ACTION",
+    public String idcc;// ": "NO_ACTION"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingWindow.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingWindow.java
new file mode 100644 (file)
index 0000000..9e94bb9
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.charge;
+
+/**
+ * The {@link ChargingWindow} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class ChargingWindow {
+    public Time start;
+    public Time end;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/Time.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/Time.java
new file mode 100644 (file)
index 0000000..ebd65a9
--- /dev/null
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.charge;
+
+import org.openhab.binding.mybmw.internal.utils.Converter;
+
+/**
+ * The {@link Time} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - edit & send of charge profile
+ */
+public class Time {
+    public int hour;// ": 11,
+    public int minute;// ": 0
+
+    @Override
+    public String toString() {
+        return Converter.getTime(this);
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/Timer.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/Timer.java
new file mode 100644 (file)
index 0000000..066da9f
--- /dev/null
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.charge;
+
+import java.util.List;
+
+import org.openhab.binding.mybmw.internal.utils.Constants;
+
+/**
+ * The {@link Timer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - edit & send of charge profile
+ */
+public class Timer {
+    public int id = -1;// ": 1,
+    public String action;// ": "deactivate",
+    public Time timeStamp;
+    public List<String> timerWeekDays;
+
+    @Override
+    public String toString() {
+        return id + Constants.COLON + action + Constants.COLON + timeStamp + Constants.COLON + timerWeekDays;
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/network/NetworkError.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/network/NetworkError.java
new file mode 100644 (file)
index 0000000..c374e71
--- /dev/null
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.network;
+
+import org.openhab.binding.mybmw.internal.utils.Constants;
+import org.openhab.binding.mybmw.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);
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Address.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Address.java
new file mode 100644 (file)
index 0000000..343754d
--- /dev/null
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.properties;
+
+/**
+ * The {@link Address} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Address {
+    public String formatted;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/CBS.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/CBS.java
new file mode 100644 (file)
index 0000000..c5d4744
--- /dev/null
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.properties;
+
+import org.openhab.binding.mybmw.internal.utils.Constants;
+
+/**
+ * The {@link CBS} Data Transfer Object ConditionBasedService
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CBS {
+    public String type = Constants.NO_ENTRIES;// ": "BRAKE_FLUID",
+    public String status = Constants.NO_ENTRIES;// ": "OK",
+    public String dateTime;// ": "2023-11-01T00:00:00.000Z"
+    public Distance distance;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/CCM.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/CCM.java
new file mode 100644 (file)
index 0000000..ab2517d
--- /dev/null
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.properties;
+
+/**
+ * The {@link CCM} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CCM {
+    // [todo] [todo] definition currently unknown
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/ChargingState.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/ChargingState.java
new file mode 100644 (file)
index 0000000..a16c217
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.properties;
+
+/**
+ * The {@link ChargingState} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class ChargingState {
+    public int chargePercentage;// ": 74,
+    public String state;// ": "NOT_CHARGING",
+    public String type;// ": "NOT_AVAILABLE",
+    public boolean isChargerConnected;// ": false
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Coordinates.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Coordinates.java
new file mode 100644 (file)
index 0000000..9e74e3e
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.properties;
+
+/**
+ * The {@link Coordinates} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Coordinates {
+    public double latitude;// ": 50.556049,
+    public double longitude;// ": 8.495669
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Distance.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Distance.java
new file mode 100644 (file)
index 0000000..966003a
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.properties;
+
+/**
+ * The {@link Distance} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Distance {
+    public int value;// ": 31,
+    public String units;// ": "KILOMETERS"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Doors.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Doors.java
new file mode 100644 (file)
index 0000000..273e983
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.properties;
+
+/**
+ * The {@link Doors} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Doors {
+    public String driverFront;// ": "CLOSED",
+    public String driverRear;// ": "CLOSED",
+    public String passengerFront;// ": "CLOSED",
+    public String passengerRear;// ": "CLOSED"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/DoorsWindows.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/DoorsWindows.java
new file mode 100644 (file)
index 0000000..bccc905
--- /dev/null
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.properties;
+
+/**
+ * The {@link DoorsWindows} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class DoorsWindows {
+    public Doors doors;
+    public Windows windows;
+    public String trunk;// ": "CLOSED",
+    public String hood;// ": "CLOSED",
+    public String moonroof;// ": "CLOSED"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/FuelLevel.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/FuelLevel.java
new file mode 100644 (file)
index 0000000..60c039e
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.properties;
+
+/**
+ * The {@link FuelLevel} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class FuelLevel {
+    public int value;// ": 4,
+    public String units;// ": "LITERS"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Location.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Location.java
new file mode 100644 (file)
index 0000000..e19decf
--- /dev/null
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.properties;
+
+/**
+ * The {@link Location} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Location {
+    public Coordinates coordinates;
+    public Address address;
+    public int heading;// ": 222
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Properties.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Properties.java
new file mode 100644 (file)
index 0000000..0198e40
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.properties;
+
+import java.util.List;
+
+/**
+ * The {@link Properties} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Properties {
+    public String lastUpdatedAt;// ": "2021-12-21T16:46:02Z",
+    public boolean inMotion;// ": false,
+    public boolean areDoorsLocked;// ": true,
+    public String originCountryISO;// ": "DE",
+    public boolean areDoorsClosed;// ": true,
+    public boolean areDoorsOpen;// ": false,
+    public boolean areWindowsClosed;// ": true,
+    public DoorsWindows doorsAndWindows;// ":
+    public boolean isServiceRequired;// ":false
+    public FuelLevel fuelLevel;
+    public ChargingState chargingState;// ":
+    public Range combustionRange;
+    public Range combinedRange;
+    public Range electricRange;
+    public Range electricRangeAndStatus;
+    public List<CCM> checkControlMessages;
+    public List<CBS> serviceRequired;
+    public Location vehicleLocation;
+    public Tires tires;
+    // "climateControl":{} [todo] definition currently unknown
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Range.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Range.java
new file mode 100644 (file)
index 0000000..354e642
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.properties;
+
+/**
+ * The {@link Range} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Range {
+    public int chargePercentage;
+    public Distance distance;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Tire.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Tire.java
new file mode 100644 (file)
index 0000000..60aabdf
--- /dev/null
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.properties;
+
+/**
+ * The {@link Tire} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Tire {
+    public TireStatus status;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/TireStatus.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/TireStatus.java
new file mode 100644 (file)
index 0000000..9cdef21
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.properties;
+
+/**
+ * The {@link TireStatus} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class TireStatus {
+    public double currentPressure;// ": 220,
+    public String localizedCurrentPressure;// ": "2.2 bar",
+    public String localizedTargetPressure;// ": "2.3 bar",
+    public double targetPressure;// ": 230
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Tires.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Tires.java
new file mode 100644 (file)
index 0000000..46a87be
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.properties;
+
+/**
+ * The {@link Tires} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Tires {
+    public Tire frontLeft;
+    public Tire frontRight;
+    public Tire rearLeft;
+    public Tire rearRight;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Windows.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Windows.java
new file mode 100644 (file)
index 0000000..322d315
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.properties;
+
+/**
+ * The {@link Windows} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Windows {
+    public String driverFront;// ": "CLOSED",
+    public String driverRear;// ": "CLOSED",
+    public String passengerFront;// ": "CLOSED",
+    public String passengerRear;// ": "CLOSED"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/remote/ExecutionError.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/remote/ExecutionError.java
new file mode 100644 (file)
index 0000000..f402c53
--- /dev/null
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.remote;
+
+/**
+ * The {@link ExecutionError} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class ExecutionError {
+    public String title;// ": "Etwas ist schiefgelaufen",
+    public String description;// ": "Die folgenden Einschränkungen verbieten die Ausführung von Remote Services: Aus
+                              // Sicherheitsgründen sind Remote Services nicht verfügbar, wenn die Fahrbereitschaft
+                              // eingeschaltet ist. Remote Services können nur mit einem ausreichenden Ladezustand
+                              // durchgeführt werden. Die Remote Services „Verriegeln“ und „Entriegeln“ können nur
+                              // ausgeführt werden, wenn die Fahrertür geschlossen und der Türstatus bekannt ist.",
+    public String presentationType;// ": "PAGE",
+    public int iconId;// ": 60217,
+    public boolean isRetriable;// ": true,
+    public String errorDetails;// ": "NACK"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/remote/ExecutionStatusContainer.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/remote/ExecutionStatusContainer.java
new file mode 100644 (file)
index 0000000..919f53a
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.remote;
+
+/**
+ * The {@link ExecutionStatusContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class ExecutionStatusContainer {
+    public String eventId;
+    public String creationTime;
+    public String eventStatus;
+    public ExecutionError errorDetails;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/CBSMessage.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/CBSMessage.java
new file mode 100644 (file)
index 0000000..6e8cafe
--- /dev/null
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.status;
+
+/**
+ * The {@link CBSMessage} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CBSMessage {
+    public String id;// ": "BrakeFluid",
+    public String title;// ": "Brake fluid",
+    public int iconId;// ": 60223,
+    public String longDescription;// ": "Next service due by the specified date.",
+    public String subtitle;// ": "Due in November 2023",
+    public String criticalness;// ": "nonCritical"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/CCMMessage.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/CCMMessage.java
new file mode 100644 (file)
index 0000000..818f1cf
--- /dev/null
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.status;
+
+import org.openhab.binding.mybmw.internal.utils.Constants;
+
+/**
+ * The {@link CCMMessage} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CCMMessage {
+    public String criticalness;// ": "semiCritical",
+    public int iconId;// ": 60217,
+    public String state = Constants.NO_ENTRIES;// ": "Medium",
+    public String title = Constants.NO_ENTRIES;// ": "Battery discharged: Start engine"
+    public String id;// ": "229",
+    public String longDescription = Constants.NO_ENTRIES;// ": "Charge by driving for longer periods or use external
+                                                         // charger. Functions requiring battery will be switched off.
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/DoorWindow.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/DoorWindow.java
new file mode 100644 (file)
index 0000000..37b10a1
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.status;
+
+/**
+ * The {@link DoorWindow} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class DoorWindow {
+    public int iconId;// ": 59757,
+    public String title;// ": "Lock status",
+    public String state;// ": "Locked",
+    public String criticalness;// ": "nonCritical"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/FuelIndicator.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/FuelIndicator.java
new file mode 100644 (file)
index 0000000..4a823ba
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.status;
+
+/**
+ * The {@link FuelIndicator} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class FuelIndicator {
+    public int mainBarValue;// ": 74,
+    public String rangeUnits;// ": "km",
+    public String rangeValue;// ": "76",
+    public String levelUnits;// ": "%",
+    public String levelValue;// ": "74",
+
+    public int secondaryBarValue;// ": 0,
+    public int infoIconId;// ": 59694,
+    public int rangeIconId;// ": 59683,
+    public int levelIconId;// ": 59694,
+    public boolean showsBar;// ": true,
+    public boolean showBarGoal;// ": false,
+    public String barType;// ": null,
+    public String infoLabel;// ": "State of Charge",
+    public boolean isInaccurate;// ": false,
+    public boolean isCircleIcon;// ": false,
+    public String iconOpacity;// ": "high",
+    public String chargingType;// ": null,
+    public String chargingStatusType;// ": "DEFAULT",
+    public String chargingStatusIndicatorType;// ": "DEFAULT"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Issues.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Issues.java
new file mode 100644 (file)
index 0000000..7b7d5fe
--- /dev/null
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.status;
+
+/**
+ * The {@link Issues} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Issues {
+    // [todo] definition currently unknown
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Mileage.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Mileage.java
new file mode 100644 (file)
index 0000000..1c6f9ce
--- /dev/null
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.status;
+
+/**
+ * The {@link Mileage} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Mileage {
+    public int mileage;// ": 31537,
+    public String units;// ": "km",
+    public String formattedMileage;// ": "31537"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Status.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Status.java
new file mode 100644 (file)
index 0000000..903b451
--- /dev/null
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.status;
+
+import java.util.List;
+
+import org.openhab.binding.mybmw.internal.dto.charge.ChargeProfile;
+
+/**
+ * The {@link Status} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Status {
+    public String lastUpdatedAt;// ": "2021-12-21T16:46:02Z",
+    public Mileage currentMileage;
+    public Issues issues;
+    public String doorsGeneralState;// ":"Locked",
+    public String checkControlMessagesGeneralState;// ":"No Issues",
+    public List<DoorWindow> doorsAndWindows;// ":[
+    public List<CCMMessage> checkControlMessages;//
+    public List<CBSMessage> requiredServices;//
+    // "recallMessages":[],
+    // "recallExternalUrl":null,
+    public List<FuelIndicator> fuelIndicators;
+    public String timestampMessage;// ":"Updated from vehicle 12/21/2021 05:46 PM",
+    public ChargeProfile chargingProfile;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Capabilities.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Capabilities.java
new file mode 100644 (file)
index 0000000..e2bf782
--- /dev/null
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.vehicle;
+
+/**
+ * The {@link Capabilities} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+
+public class Capabilities {
+    public boolean isRemoteServicesBookingRequired;
+    public boolean isRemoteServicesActivationRequired;
+    public boolean isRemoteHistorySupported;
+    public boolean canRemoteHistoryBeDeleted;
+    public boolean isChargingHistorySupported;
+    public boolean isScanAndChargeSupported;
+    public boolean isDCSContractManagementSupported;
+    public boolean isBmwChargingSupported;
+    public boolean isMiniChargingSupported;
+    public boolean isChargeNowForBusinessSupported;
+    public boolean isDataPrivacyEnabled;
+    public boolean isChargingPlanSupported;
+    public boolean isChargingPowerLimitEnable;
+    public boolean isChargingTargetSocEnable;
+    public boolean isChargingLoudnessEnable;
+    public boolean isChargingSettingsEnabled;
+    public boolean isChargingHospitalityEnabled;
+    public boolean isEvGoChargingSupported;
+    public boolean isFindChargingEnabled;
+    public boolean isCustomerEsimSupported;
+    public boolean isCarSharingSupported;
+    public boolean isEasyChargeSupported;
+
+    public RemoteService lock;
+    public RemoteService unlock;
+    public RemoteService lights;
+    public RemoteService horn;
+    public RemoteService vehicleFinder;
+    public RemoteService sendPoi;
+    public RemoteService climateNow;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/RemoteService.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/RemoteService.java
new file mode 100644 (file)
index 0000000..8ad719c
--- /dev/null
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.vehicle;
+
+/**
+ * The {@link RemoteService} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class RemoteService {
+    public boolean isEnabled;// ": true,
+    public boolean isPinAuthenticationRequired;// ": false,
+    public String executionMessage;// ": "Lock your vehicle now? Remote functions may take a few seconds."
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Vehicle.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Vehicle.java
new file mode 100644 (file)
index 0000000..292728f
--- /dev/null
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto.vehicle;
+
+import org.openhab.binding.mybmw.internal.dto.properties.Properties;
+import org.openhab.binding.mybmw.internal.dto.status.Status;
+
+/**
+ * The {@link Vehicle} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Vehicle {
+    public String vin;// ": "WBY1Z81040V905639",
+    public String model;// ": "i3 94 (+ REX)",
+    public int year;// ": 2017,
+    public String brand;// ": "BMW",
+    public String headUnit;// ": "ID5",
+    public boolean isLscSupported;// ": true,
+    public String driveTrain;// ": "ELECTRIC",
+    public String puStep;// ": "0321",
+    public String iStep;// ": "I001-21-03-530",
+    public String telematicsUnit;// ": "TCB1",
+    public String hmiVersion;// ": "ID4",
+    public String bodyType;// ": "I01",
+    public String a4aType;// ": "USB_ONLY",
+    public String exFactoryPUStep;// ": "0717",
+    public String exFactoryILevel;// ": "I001-17-07-500"
+    public Capabilities capabilities;
+    // "connectedDriveServices": [] currently no clue how to resolve,
+    public Properties properties;
+    public boolean isMappingPending;// ":false,"
+    public boolean isMappingUnconfirmed;// ":false,
+    public Status status;
+    public boolean valid = false;
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/ByteResponseCallback.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/ByteResponseCallback.java
new file mode 100644 (file)
index 0000000..24a6309
--- /dev/null
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.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);
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java
new file mode 100644 (file)
index 0000000..06c0f6b
--- /dev/null
@@ -0,0 +1,141 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.handler;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+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.mybmw.internal.MyBMWConfiguration;
+import org.openhab.binding.mybmw.internal.discovery.VehicleDiscovery;
+import org.openhab.binding.mybmw.internal.dto.network.NetworkError;
+import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
+import org.openhab.binding.mybmw.internal.utils.BimmerConstants;
+import org.openhab.binding.mybmw.internal.utils.Constants;
+import org.openhab.binding.mybmw.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;
+
+/**
+ * The {@link MyBMWBridgeHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class MyBMWBridgeHandler extends BaseBridgeHandler implements StringResponseCallback {
+    private final Logger logger = LoggerFactory.getLogger(MyBMWBridgeHandler.class);
+    private HttpClientFactory httpClientFactory;
+    private Optional<VehicleDiscovery> discoveryService = Optional.empty();
+    private Optional<MyBMWProxy> proxy = Optional.empty();
+    private Optional<ScheduledFuture<?>> initializerJob = Optional.empty();
+    private Optional<String> troubleshootFingerprint = Optional.empty();
+    private String localeLanguage;
+
+    public MyBMWBridgeHandler(Bridge bridge, HttpClientFactory hcf, String language) {
+        super(bridge);
+        httpClientFactory = hcf;
+        localeLanguage = language;
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        // no commands available
+    }
+
+    @Override
+    public void initialize() {
+        troubleshootFingerprint = Optional.empty();
+        updateStatus(ThingStatus.UNKNOWN);
+        MyBMWConfiguration config = getConfigAs(MyBMWConfiguration.class);
+        if (config.language.equals(Constants.LANGUAGE_AUTODETECT)) {
+            config.language = localeLanguage;
+        }
+        if (!checkConfiguration(config)) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
+        } else {
+            proxy = Optional.of(new MyBMWProxy(httpClientFactory, config));
+            initializerJob = Optional.of(scheduler.schedule(this::requestVehicles, 2, TimeUnit.SECONDS));
+        }
+    }
+
+    public static boolean checkConfiguration(MyBMWConfiguration config) {
+        if (Constants.EMPTY.equals(config.userName) || Constants.EMPTY.equals(config.password)) {
+            return false;
+        } else {
+            return BimmerConstants.EADRAX_SERVER_MAP.containsKey(config.region);
+        }
+    }
+
+    @Override
+    public void dispose() {
+        initializerJob.ifPresent(job -> job.cancel(true));
+    }
+
+    public void requestVehicles() {
+        proxy.ifPresent(prox -> prox.requestVehicles(this));
+    }
+
+    private void logFingerPrint() {
+        logger.debug("###### Discovery Fingerprint Data - BEGIN ######");
+        logger.debug("{}", troubleshootFingerprint.get());
+        logger.debug("###### Discovery Fingerprint Data - END ######");
+    }
+
+    /**
+     * Response for vehicle request
+     */
+    @Override
+    public synchronized void onResponse(@Nullable String response) {
+        if (response != null) {
+            updateStatus(ThingStatus.ONLINE);
+            List<Vehicle> vehicleList = Converter.getVehicleList(response);
+            discoveryService.get().onResponse(vehicleList);
+            troubleshootFingerprint = Optional.of(Converter.anonymousFingerprint(response));
+            logFingerPrint();
+        }
+    }
+
+    @Override
+    public void onError(NetworkError error) {
+        troubleshootFingerprint = Optional.of(error.toJson());
+        logFingerPrint();
+        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error.reason);
+    }
+
+    @Override
+    public Collection<Class<? extends ThingHandlerService>> getServices() {
+        return Collections.singleton(VehicleDiscovery.class);
+    }
+
+    public Optional<MyBMWProxy> getProxy() {
+        return proxy;
+    }
+
+    public void setDiscoveryService(VehicleDiscovery discoveryService) {
+        this.discoveryService = Optional.of(discoveryService);
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWCommandOptionProvider.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWCommandOptionProvider.java
new file mode 100644 (file)
index 0000000..d922dd3
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.events.EventPublisher;
+import org.openhab.core.thing.binding.BaseDynamicCommandDescriptionProvider;
+import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
+import org.openhab.core.thing.link.ItemChannelLinkRegistry;
+import org.openhab.core.thing.type.DynamicCommandDescriptionProvider;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * Dynamic provider of command options while leaving other state description fields as original.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = { DynamicCommandDescriptionProvider.class, MyBMWCommandOptionProvider.class })
+public class MyBMWCommandOptionProvider extends BaseDynamicCommandDescriptionProvider {
+    @Activate
+    public MyBMWCommandOptionProvider(final @Reference EventPublisher eventPublisher, //
+            final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry, //
+            final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
+        this.eventPublisher = eventPublisher;
+        this.itemChannelLinkRegistry = itemChannelLinkRegistry;
+        this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWProxy.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWProxy.java
new file mode 100644 (file)
index 0000000..20a4f4c
--- /dev/null
@@ -0,0 +1,510 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.handler;
+
+import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.*;
+
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.MessageDigest;
+import java.security.PublicKey;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+import javax.crypto.Cipher;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.HttpResponseException;
+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.HttpHeader;
+import org.eclipse.jetty.util.MultiMap;
+import org.eclipse.jetty.util.UrlEncoded;
+import org.openhab.binding.mybmw.internal.MyBMWConfiguration;
+import org.openhab.binding.mybmw.internal.VehicleConfiguration;
+import org.openhab.binding.mybmw.internal.dto.auth.AuthQueryResponse;
+import org.openhab.binding.mybmw.internal.dto.auth.AuthResponse;
+import org.openhab.binding.mybmw.internal.dto.auth.ChinaPublicKeyResponse;
+import org.openhab.binding.mybmw.internal.dto.auth.ChinaTokenExpiration;
+import org.openhab.binding.mybmw.internal.dto.auth.ChinaTokenResponse;
+import org.openhab.binding.mybmw.internal.dto.network.NetworkError;
+import org.openhab.binding.mybmw.internal.handler.simulation.Injector;
+import org.openhab.binding.mybmw.internal.utils.BimmerConstants;
+import org.openhab.binding.mybmw.internal.utils.Constants;
+import org.openhab.binding.mybmw.internal.utils.Converter;
+import org.openhab.binding.mybmw.internal.utils.HTTPConstants;
+import org.openhab.binding.mybmw.internal.utils.ImageProperties;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link MyBMWProxy} 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 MyBMWProxy {
+    private final Logger logger = LoggerFactory.getLogger(MyBMWProxy.class);
+    private Optional<RemoteServiceHandler> remoteServiceHandler = Optional.empty();
+    private final Token token = new Token();
+    private final HttpClient httpClient;
+    private final MyBMWConfiguration configuration;
+
+    /**
+     * URLs taken from https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/const.py
+     */
+    final String vehicleUrl;
+    final String remoteCommandUrl;
+    final String remoteStatusUrl;
+    final String serviceExecutionAPI = "/executeService";
+    final String serviceExecutionStateAPI = "/serviceExecutionStatus";
+    final String remoteServiceEADRXstatusUrl = BimmerConstants.API_REMOTE_SERVICE_BASE_URL
+            + "eventStatus?eventId={event_id}";
+
+    public MyBMWProxy(HttpClientFactory httpClientFactory, MyBMWConfiguration config) {
+        httpClient = httpClientFactory.getCommonHttpClient();
+        configuration = config;
+
+        vehicleUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
+                + BimmerConstants.API_VEHICLES;
+
+        remoteCommandUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
+                + BimmerConstants.API_REMOTE_SERVICE_BASE_URL;
+        remoteStatusUrl = remoteCommandUrl + "eventStatus";
+    }
+
+    public synchronized void call(final String url, final boolean post, final @Nullable String encoding,
+            final @Nullable String params, final String brand, final ResponseCallback callback) {
+        // only executed in "simulation mode"
+        // SimulationTest.testSimulationOff() assures Injector is off when releasing
+        if (Injector.isActive()) {
+            if (url.equals(vehicleUrl)) {
+                ((StringResponseCallback) callback).onResponse(Injector.getDiscovery());
+            } else if (url.endsWith(vehicleUrl)) {
+                ((StringResponseCallback) callback).onResponse(Injector.getStatus());
+            } else {
+                logger.debug("Simulation of {} not supported", url);
+            }
+            return;
+        }
+
+        // return in case of unknown brand
+        String userAgent = BimmerConstants.BRAND_USER_AGENTS_MAP.get(brand.toLowerCase());
+        if (userAgent == null) {
+            logger.warn("Unknown Brand {}", brand);
+            return;
+        }
+
+        final Request req;
+        final String completeUrl;
+
+        if (post) {
+            completeUrl = url;
+            req = httpClient.POST(url);
+            if (encoding != null) {
+                req.header(HttpHeader.CONTENT_TYPE, encoding);
+                if (CONTENT_TYPE_URL_ENCODED.equals(encoding)) {
+                    req.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, params, StandardCharsets.UTF_8));
+                } else if (CONTENT_TYPE_JSON_ENCODED.equals(encoding)) {
+                    req.content(new StringContentProvider(CONTENT_TYPE_JSON_ENCODED, params, StandardCharsets.UTF_8));
+                }
+            }
+        } else {
+            completeUrl = params == null ? url : url + Constants.QUESTION + params;
+            req = httpClient.newRequest(completeUrl);
+        }
+        req.header(HttpHeader.AUTHORIZATION, getToken().getBearerToken());
+        req.header(HTTPConstants.X_USER_AGENT, userAgent);
+        req.header(HttpHeader.ACCEPT_LANGUAGE, configuration.language);
+        if (callback instanceof ByteResponseCallback) {
+            req.header(HttpHeader.ACCEPT, "image/png");
+        } else {
+            req.header(HttpHeader.ACCEPT, CONTENT_TYPE_JSON_ENCODED);
+        }
+
+        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 String coding, @Nullable String params, final String brand,
+            ResponseCallback callback) {
+        call(url, false, coding, params, brand, callback);
+    }
+
+    public void post(String url, @Nullable String coding, @Nullable String params, final String brand,
+            ResponseCallback callback) {
+        call(url, true, coding, params, brand, callback);
+    }
+
+    /**
+     * request all vehicles for one specific brand
+     *
+     * @param brand
+     * @param callback
+     */
+    public void requestVehicles(String brand, StringResponseCallback callback) {
+        // calculate necessary parameters for query
+        MultiMap<String> vehicleParams = new MultiMap<String>();
+        vehicleParams.put(BimmerConstants.TIRE_GUARD_MODE, Constants.ENABLED);
+        vehicleParams.put(BimmerConstants.APP_DATE_TIME, Long.toString(System.currentTimeMillis()));
+        vehicleParams.put(BimmerConstants.APP_TIMEZONE, Integer.toString(Converter.getOffsetMinutes()));
+        String params = UrlEncoded.encode(vehicleParams, StandardCharsets.UTF_8, false);
+        get(vehicleUrl + "?" + params, null, null, brand, callback);
+    }
+
+    /**
+     * request vehicles for all possible brands
+     *
+     * @param callback
+     */
+    public void requestVehicles(StringResponseCallback callback) {
+        BimmerConstants.ALL_BRANDS.forEach(brand -> {
+            requestVehicles(brand, callback);
+        });
+    }
+
+    public void requestImage(VehicleConfiguration config, ImageProperties props, ByteResponseCallback callback) {
+        final String localImageUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
+                + "/eadrax-ics/v3/presentation/vehicles/" + config.vin + "/images?carView=" + props.viewport;
+        get(localImageUrl, null, null, config.vehicleBrand, callback);
+    }
+
+    /**
+     * request charge statistics for electric vehicles
+     *
+     * @param callback
+     */
+    public void requestChargeStatistics(VehicleConfiguration config, StringResponseCallback callback) {
+        MultiMap<String> chargeStatisticsParams = new MultiMap<String>();
+        chargeStatisticsParams.put("vin", config.vin);
+        chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime());
+        String params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false);
+        String chargeStatisticsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
+                + "/eadrax-chs/v1/charging-statistics?" + params;
+        get(chargeStatisticsUrl, null, null, config.vehicleBrand, callback);
+    }
+
+    /**
+     * request charge statistics for electric vehicles
+     *
+     * @param callback
+     */
+    public void requestChargeSessions(VehicleConfiguration config, StringResponseCallback callback) {
+        MultiMap<String> chargeSessionsParams = new MultiMap<String>();
+        chargeSessionsParams.put("vin", "WBY1Z81040V905639");
+        chargeSessionsParams.put("maxResults", "40");
+        chargeSessionsParams.put("include_date_picker", "true");
+        String params = UrlEncoded.encode(chargeSessionsParams, StandardCharsets.UTF_8, false);
+        String chargeSessionsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
+                + "/eadrax-chs/v1/charging-sessions?" + params;
+
+        get(chargeSessionsUrl, null, null, config.vehicleBrand, callback);
+    }
+
+    RemoteServiceHandler getRemoteServiceHandler(VehicleHandler vehicleHandler) {
+        remoteServiceHandler = Optional.of(new RemoteServiceHandler(vehicleHandler, this));
+        return remoteServiceHandler.get();
+    }
+
+    // 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.isValid()) {
+            boolean tokenUpdateSuccess = false;
+            switch (configuration.region) {
+                case BimmerConstants.REGION_CHINA:
+                    tokenUpdateSuccess = updateTokenChina();
+                    break;
+                case BimmerConstants.REGION_NORTH_AMERICA:
+                    tokenUpdateSuccess = updateToken();
+                    break;
+                case BimmerConstants.REGION_ROW:
+                    tokenUpdateSuccess = updateToken();
+                    break;
+                default:
+                    logger.warn("Region {} not supported", configuration.region);
+                    break;
+            }
+            if (!tokenUpdateSuccess) {
+                logger.debug("Authorization failed!");
+            }
+        }
+        return token;
+    }
+
+    /**
+     * Everything is catched by surroundig try catch
+     * - HTTP Exceptions
+     * - JSONSyntax Exceptions
+     * - potential NullPointer Exceptions
+     *
+     * @return
+     */
+    @SuppressWarnings("null")
+    public synchronized boolean updateToken() {
+        try {
+            /*
+             * Step 1) Get basic values for further queries
+             */
+            String authValuesUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
+                    + BimmerConstants.API_OAUTH_CONFIG;
+            Request authValuesRequest = httpClient.newRequest(authValuesUrl);
+            authValuesRequest.header(ACP_SUBSCRIPTION_KEY, BimmerConstants.OCP_APIM_KEYS.get(configuration.region));
+            authValuesRequest.header(X_USER_AGENT, BimmerConstants.USER_AGENT_BMW);
+            ContentResponse authValuesResponse = authValuesRequest.send();
+            if (authValuesResponse.getStatus() != 200) {
+                throw new HttpResponseException("URL: " + authValuesRequest.getURI() + ", Error: "
+                        + authValuesResponse.getStatus() + ", Message: " + authValuesResponse.getContentAsString(),
+                        authValuesResponse);
+            }
+            AuthQueryResponse aqr = Converter.getGson().fromJson(authValuesResponse.getContentAsString(),
+                    AuthQueryResponse.class);
+
+            /*
+             * Step 2) Calculate values for base parameters
+             */
+            String verfifierBytes = Converter.getRandomString(64);
+            String codeVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(verfifierBytes.getBytes());
+            MessageDigest digest = MessageDigest.getInstance("SHA-256");
+            byte[] hash = digest.digest(codeVerifier.getBytes(StandardCharsets.UTF_8));
+            String codeChallange = Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
+            String stateBytes = Converter.getRandomString(16);
+            String state = Base64.getUrlEncoder().withoutPadding().encodeToString(stateBytes.getBytes());
+
+            MultiMap<String> baseParams = new MultiMap<String>();
+            baseParams.put(CLIENT_ID, aqr.clientId);
+            baseParams.put(RESPONSE_TYPE, CODE);
+            baseParams.put(REDIRECT_URI, aqr.returnUrl);
+            baseParams.put(STATE, state);
+            baseParams.put(NONCE, BimmerConstants.LOGIN_NONCE);
+            baseParams.put(SCOPE, String.join(Constants.SPACE, aqr.scopes));
+            baseParams.put(CODE_CHALLENGE, codeChallange);
+            baseParams.put(CODE_CHALLENGE_METHOD, "S256");
+
+            /**
+             * Step 3) Authorization with username and password
+             */
+            String loginUrl = aqr.gcdmBaseUrl + BimmerConstants.OAUTH_ENDPOINT;
+            Request loginRequest = httpClient.POST(loginUrl);
+            loginRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED);
+
+            MultiMap<String> loginParams = new MultiMap<String>(baseParams);
+            loginParams.put(GRANT_TYPE, BimmerConstants.AUTHORIZATION_CODE);
+            loginParams.put(USERNAME, configuration.userName);
+            loginParams.put(PASSWORD, configuration.password);
+            loginRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
+                    UrlEncoded.encode(loginParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
+            ContentResponse loginResponse = loginRequest.send();
+            if (loginResponse.getStatus() != 200) {
+                throw new HttpResponseException("URL: " + loginRequest.getURI() + ", Error: "
+                        + loginResponse.getStatus() + ", Message: " + loginResponse.getContentAsString(),
+                        loginResponse);
+            }
+            String authCode = getAuthCode(loginResponse.getContentAsString());
+
+            /**
+             * Step 4) Authorize with code
+             */
+            Request authRequest = httpClient.POST(loginUrl).followRedirects(false);
+            MultiMap<String> authParams = new MultiMap<String>(baseParams);
+            authParams.put(AUTHORIZATION, authCode);
+            authRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED);
+            authRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
+                    UrlEncoded.encode(authParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
+            ContentResponse authResponse = authRequest.send();
+            if (authResponse.getStatus() != 302) {
+                throw new HttpResponseException("URL: " + authRequest.getURI() + ", Error: " + authResponse.getStatus()
+                        + ", Message: " + authResponse.getContentAsString(), authResponse);
+            }
+            String code = MyBMWProxy.codeFromUrl(authResponse.getHeaders().get(HttpHeader.LOCATION));
+
+            /**
+             * Step 5) Request token
+             */
+            Request codeRequest = httpClient.POST(aqr.tokenEndpoint);
+            String basicAuth = "Basic "
+                    + Base64.getUrlEncoder().encodeToString((aqr.clientId + ":" + aqr.clientSecret).getBytes());
+            codeRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED);
+            codeRequest.header(AUTHORIZATION, basicAuth);
+
+            MultiMap<String> codeParams = new MultiMap<String>();
+            codeParams.put(CODE, code);
+            codeParams.put(CODE_VERIFIER, codeVerifier);
+            codeParams.put(REDIRECT_URI, aqr.returnUrl);
+            codeParams.put(GRANT_TYPE, BimmerConstants.AUTHORIZATION_CODE);
+            codeRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
+                    UrlEncoded.encode(codeParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
+            ContentResponse codeResponse = codeRequest.send();
+            if (codeResponse.getStatus() != 200) {
+                throw new HttpResponseException("URL: " + codeRequest.getURI() + ", Error: " + codeResponse.getStatus()
+                        + ", Message: " + codeResponse.getContentAsString(), codeResponse);
+            }
+            AuthResponse ar = Converter.getGson().fromJson(codeResponse.getContentAsString(), AuthResponse.class);
+            token.setType(ar.tokenType);
+            token.setToken(ar.accessToken);
+            token.setExpiration(ar.expiresIn);
+            return true;
+        } catch (Exception e) {
+            logger.warn("Authorization Exception: {}", e.getMessage());
+        }
+        return false;
+    }
+
+    private String getAuthCode(String response) {
+        String[] keys = response.split("&");
+        for (int i = 0; i < keys.length; i++) {
+            if (keys[i].startsWith(AUTHORIZATION)) {
+                String authCode = keys[i].split("=")[1];
+                authCode = authCode.split("\"")[0];
+                return authCode;
+            }
+        }
+        return Constants.EMPTY;
+    }
+
+    public static String codeFromUrl(String encodedUrl) {
+        final MultiMap<String> tokenMap = new MultiMap<String>();
+        UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII);
+        final StringBuilder codeFound = new StringBuilder();
+        tokenMap.forEach((key, value) -> {
+            if (value.size() > 0) {
+                String val = value.get(0);
+                if (key.endsWith(CODE)) {
+                    codeFound.append(val.toString());
+                }
+            }
+        });
+        return codeFound.toString();
+    }
+
+    @SuppressWarnings("null")
+    public synchronized boolean updateTokenChina() {
+        try {
+            /**
+             * Step 1) get public key
+             */
+            String publicKeyUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_CHINA)
+                    + BimmerConstants.CHINA_PUBLIC_KEY;
+            Request oauthQueryRequest = httpClient.newRequest(publicKeyUrl);
+            oauthQueryRequest.header(X_USER_AGENT, BimmerConstants.USER_AGENT_BMW);
+            ContentResponse publicKeyResponse = oauthQueryRequest.send();
+            if (publicKeyResponse.getStatus() != 200) {
+                throw new HttpResponseException("URL: " + oauthQueryRequest.getURI() + ", Error: "
+                        + publicKeyResponse.getStatus() + ", Message: " + publicKeyResponse.getContentAsString(),
+                        publicKeyResponse);
+            }
+            ChinaPublicKeyResponse pkr = Converter.getGson().fromJson(publicKeyResponse.getContentAsString(),
+                    ChinaPublicKeyResponse.class);
+
+            /**
+             * Step 2) Encode password with public key
+             */
+            // https://www.baeldung.com/java-read-pem-file-keys
+            String publicKeyStr = pkr.data.value;
+            String publicKeyPEM = publicKeyStr.replace("-----BEGIN PUBLIC KEY-----", "")
+                    .replaceAll(System.lineSeparator(), "").replace("-----END PUBLIC KEY-----", "").replace("\\r", "")
+                    .replace("\\n", "").trim();
+            byte[] encoded = Base64.getDecoder().decode(publicKeyPEM);
+            X509EncodedKeySpec spec = new X509EncodedKeySpec(encoded);
+            KeyFactory kf = KeyFactory.getInstance("RSA");
+            PublicKey publicKey = kf.generatePublic(spec);
+            // https://www.thexcoders.net/java-ciphers-rsa/
+            Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
+            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
+            byte[] encryptedBytes = cipher.doFinal(configuration.password.getBytes());
+            String encodedPassword = Base64.getEncoder().encodeToString(encryptedBytes);
+
+            /**
+             * Step 3) Send Auth with encoded password
+             */
+            String tokenUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_CHINA)
+                    + BimmerConstants.CHINA_LOGIN;
+            Request loginRequest = httpClient.POST(tokenUrl);
+            loginRequest.header(X_USER_AGENT, BimmerConstants.USER_AGENT_BMW);
+            String jsonContent = "{ \"mobile\":\"" + configuration.userName + "\", \"password\":\"" + encodedPassword
+                    + "\"}";
+            loginRequest.content(new StringContentProvider(jsonContent));
+            ContentResponse tokenResponse = loginRequest.send();
+            if (tokenResponse.getStatus() != 200) {
+                throw new HttpResponseException("URL: " + loginRequest.getURI() + ", Error: "
+                        + tokenResponse.getStatus() + ", Message: " + tokenResponse.getContentAsString(),
+                        tokenResponse);
+            }
+            String authCode = getAuthCode(tokenResponse.getContentAsString());
+
+            /**
+             * Step 4) Decode access token
+             */
+            ChinaTokenResponse cat = Converter.getGson().fromJson(authCode, ChinaTokenResponse.class);
+            String token = cat.data.accessToken;
+            // https://www.baeldung.com/java-jwt-token-decode
+            String[] chunks = token.split("\\.");
+            String tokenJwtDecodeStr = new String(Base64.getUrlDecoder().decode(chunks[1]));
+            ChinaTokenExpiration cte = Converter.getGson().fromJson(tokenJwtDecodeStr, ChinaTokenExpiration.class);
+            Token t = new Token();
+            t.setToken(token);
+            t.setType(cat.data.tokenType);
+            t.setExpirationTotal(cte.exp);
+            return true;
+        } catch (Exception e) {
+            logger.warn("Authorization Exception: {}", e.getMessage());
+        }
+        return false;
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceHandler.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceHandler.java
new file mode 100644 (file)
index 0000000..116733e
--- /dev/null
@@ -0,0 +1,227 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.handler;
+
+import static org.openhab.binding.mybmw.internal.MyBMWConstants.*;
+import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.CONTENT_TYPE_JSON_ENCODED;
+
+import java.nio.charset.StandardCharsets;
+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.eclipse.jetty.util.UrlEncoded;
+import org.openhab.binding.mybmw.internal.VehicleConfiguration;
+import org.openhab.binding.mybmw.internal.dto.network.NetworkError;
+import org.openhab.binding.mybmw.internal.dto.remote.ExecutionStatusContainer;
+import org.openhab.binding.mybmw.internal.utils.Constants;
+import org.openhab.binding.mybmw.internal.utils.Converter;
+import org.openhab.binding.mybmw.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 EVENT_ID = "eventId";
+    private static final String DATA = "data";
+    private static final int GIVEUP_COUNTER = 12; // after 12 retries the state update will give up
+    private static final int STATE_UPDATE_SEC = HTTPConstants.HTTP_TIMEOUT_SEC + 1; // regular timeout + 1sec
+
+    private final MyBMWProxy 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();
+    private Optional<String> executingEventId = Optional.empty();
+
+    public enum ExecutionState {
+        READY,
+        INITIATED,
+        PENDING,
+        DELIVERED,
+        EXECUTED,
+        ERROR,
+        TIMEOUT
+    }
+
+    public enum RemoteService {
+        LIGHT_FLASH("Flash Lights", REMOTE_SERVICE_LIGHT_FLASH, REMOTE_SERVICE_LIGHT_FLASH),
+        VEHICLE_FINDER("Vehicle Finder", REMOTE_SERVICE_VEHICLE_FINDER, REMOTE_SERVICE_VEHICLE_FINDER),
+        DOOR_LOCK("Door Lock", REMOTE_SERVICE_DOOR_LOCK, REMOTE_SERVICE_DOOR_LOCK),
+        DOOR_UNLOCK("Door Unlock", REMOTE_SERVICE_DOOR_UNLOCK, REMOTE_SERVICE_DOOR_UNLOCK),
+        HORN_BLOW("Horn Blow", REMOTE_SERVICE_HORN, REMOTE_SERVICE_HORN),
+        CLIMATE_NOW_START("Start Climate", REMOTE_SERVICE_AIR_CONDITIONING_START, "climate-now?action=START"),
+        CLIMATE_NOW_STOP("Stop Climate", REMOTE_SERVICE_AIR_CONDITIONING_STOP, "climate-now?action=STOP");
+
+        private final String label;
+        private final String id;
+        private final String command;
+
+        RemoteService(final String label, final String id, String command) {
+            this.label = label;
+            this.id = id;
+            this.command = command;
+        }
+
+        public String getLabel() {
+            return label;
+        }
+
+        public String getId() {
+            return id;
+        }
+
+        public String getCommand() {
+            return command;
+        }
+    }
+
+    public RemoteServiceHandler(VehicleHandler vehicleHandler, MyBMWProxy myBmwProxy) {
+        handler = vehicleHandler;
+        proxy = myBmwProxy;
+        final VehicleConfiguration config = handler.getConfiguration().get();
+        serviceExecutionAPI = proxy.remoteCommandUrl + config.vin + "/";
+        serviceExecutionStateAPI = proxy.remoteStatusUrl;
+    }
+
+    boolean execute(RemoteService service, String... data) {
+        synchronized (this) {
+            if (serviceExecuting.isPresent()) {
+                logger.debug("Execution rejected - {} still pending", serviceExecuting.get());
+                // only one service executing
+                return false;
+            }
+            serviceExecuting = Optional.of(service.getId());
+        }
+        final MultiMap<String> dataMap = new MultiMap<String>();
+        if (data.length > 0) {
+            dataMap.add(DATA, data[0]);
+            proxy.post(serviceExecutionAPI + service.getCommand(), CONTENT_TYPE_JSON_ENCODED, data[0],
+                    handler.getConfiguration().get().vehicleBrand, this);
+        } else {
+            proxy.post(serviceExecutionAPI + service.getCommand(), null, null,
+                    handler.getConfiguration().get().vehicleBrand, 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);
+                    handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null),
+                            ExecutionState.TIMEOUT.name().toLowerCase());
+                    reset();
+                    // immediately refresh data
+                    handler.getData();
+                } else {
+                    counter++;
+                    final MultiMap<String> dataMap = new MultiMap<String>();
+                    dataMap.add(EVENT_ID, executingEventId.get());
+                    final String encoded = UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false);
+                    proxy.post(serviceExecutionStateAPI + Constants.QUESTION + encoded, null, null,
+                            handler.getConfiguration().get().vehicleBrand, 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) {
+                    if (esc.eventId != null) {
+                        // service initiated - store event id for further MyBMW updates
+                        executingEventId = Optional.of(esc.eventId);
+                        handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null),
+                                ExecutionState.INITIATED.name().toLowerCase());
+                    } else if (esc.eventStatus != null) {
+                        // service status updated
+                        synchronized (this) {
+                            handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null),
+                                    esc.eventStatus.toLowerCase());
+                            if (ExecutionState.EXECUTED.name().equalsIgnoreCase(esc.eventStatus)
+                                    || ExecutionState.ERROR.name().equalsIgnoreCase(esc.eventStatus)) {
+                                // 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().toLowerCase() + Constants.SPACE + Integer.toString(error.status));
+            reset();
+        }
+    }
+
+    private void reset() {
+        serviceExecuting = Optional.empty();
+        executingEventId = Optional.empty();
+        counter = 0;
+    }
+
+    public void cancel() {
+        synchronized (this) {
+            stateJob.ifPresent(action -> {
+                if (!action.isDone()) {
+                    action.cancel(true);
+                }
+                stateJob = Optional.empty();
+            });
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/ResponseCallback.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/ResponseCallback.java
new file mode 100644 (file)
index 0000000..a64ee44
--- /dev/null
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mybmw.internal.dto.network.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);
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/StringResponseCallback.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/StringResponseCallback.java
new file mode 100644 (file)
index 0000000..630a574
--- /dev/null
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.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);
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/Token.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/Token.java
new file mode 100644 (file)
index 0000000..34e0cef
--- /dev/null
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mybmw.internal.utils.Constants;
+
+/**
+ * The {@link Token} MyBMW 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;
+    }
+
+    public void setExpirationTotal(long expiration) {
+        this.expiration = expiration;
+    }
+
+    public void setType(String type) {
+        tokenType = type;
+    }
+
+    public boolean isValid() {
+        return (!token.equals(Constants.EMPTY) && !tokenType.equals(Constants.EMPTY)
+                && (this.expiration - System.currentTimeMillis() / 1000) > 1);
+    }
+
+    @Override
+    public String toString() {
+        return tokenType + Constants.COLON + token + Constants.COLON + isValid();
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleChannelHandler.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleChannelHandler.java
new file mode 100644 (file)
index 0000000..1dec0d6
--- /dev/null
@@ -0,0 +1,456 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.handler;
+
+import static org.openhab.binding.mybmw.internal.MyBMWConstants.*;
+
+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.Unit;
+import javax.measure.quantity.Length;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mybmw.internal.MyBMWConstants.VehicleType;
+import org.openhab.binding.mybmw.internal.dto.charge.ChargeProfile;
+import org.openhab.binding.mybmw.internal.dto.charge.ChargeSession;
+import org.openhab.binding.mybmw.internal.dto.charge.ChargeStatisticsContainer;
+import org.openhab.binding.mybmw.internal.dto.charge.ChargingSettings;
+import org.openhab.binding.mybmw.internal.dto.properties.CBS;
+import org.openhab.binding.mybmw.internal.dto.properties.DoorsWindows;
+import org.openhab.binding.mybmw.internal.dto.properties.Location;
+import org.openhab.binding.mybmw.internal.dto.properties.Tires;
+import org.openhab.binding.mybmw.internal.dto.status.CCMMessage;
+import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
+import org.openhab.binding.mybmw.internal.utils.ChargeProfileUtils;
+import org.openhab.binding.mybmw.internal.utils.ChargeProfileUtils.TimedChannel;
+import org.openhab.binding.mybmw.internal.utils.ChargeProfileWrapper;
+import org.openhab.binding.mybmw.internal.utils.ChargeProfileWrapper.ProfileKey;
+import org.openhab.binding.mybmw.internal.utils.Constants;
+import org.openhab.binding.mybmw.internal.utils.Converter;
+import org.openhab.binding.mybmw.internal.utils.RemoteServiceUtils;
+import org.openhab.binding.mybmw.internal.utils.VehicleStatusUtils;
+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.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.CommandOption;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link VehicleChannelHandler} handles Channel updates
+ *
+ * @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 hasFuel = false;
+    protected boolean isElectric = false;
+    protected boolean isHybrid = false;
+
+    // List Interfaces
+    protected List<CBS> serviceList = new ArrayList<CBS>();
+    protected String selectedService = Constants.UNDEF;
+    protected List<CCMMessage> checkControlList = new ArrayList<CCMMessage>();
+    protected String selectedCC = Constants.UNDEF;
+    protected List<ChargeSession> sessionList = new ArrayList<ChargeSession>();
+    protected String selectedSession = Constants.UNDEF;
+
+    protected MyBMWCommandOptionProvider commandOptionProvider;
+
+    // Data Caches
+    protected Optional<String> vehicleStatusCache = Optional.empty();
+    protected Optional<byte[]> imageCache = Optional.empty();
+
+    public VehicleChannelHandler(Thing thing, MyBMWCommandOptionProvider cop, String type) {
+        super(thing);
+        commandOptionProvider = cop;
+
+        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<CommandOption> options) {
+        commandOptionProvider.setCommandOptions(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 updateChargeStatistics(ChargeStatisticsContainer csc) {
+        updateChannel(CHANNEL_GROUP_CHARGE_STATISTICS, TITLE, StringType.valueOf(csc.description));
+        updateChannel(CHANNEL_GROUP_CHARGE_STATISTICS, ENERGY,
+                QuantityType.valueOf(csc.statistics.totalEnergyCharged, Units.KILOWATT_HOUR));
+        updateChannel(CHANNEL_GROUP_CHARGE_STATISTICS, SESSIONS,
+                DecimalType.valueOf(Integer.toString(csc.statistics.numberOfChargingSessions)));
+    }
+
+    protected void updateVehicle(Vehicle v) {
+        updateVehicleStatus(v);
+        updateRange(v);
+        updateDoors(v.properties.doorsAndWindows);
+        updateWindows(v.properties.doorsAndWindows);
+        updatePosition(v.properties.vehicleLocation);
+        updateServices(v.properties.serviceRequired);
+        updateCheckControls(v.status.checkControlMessages);
+        updateTires(v.properties.tires);
+    }
+
+    private void updateTires(@Nullable Tires tires) {
+        if (tires == null) {
+            updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_CURRENT, UnDefType.UNDEF);
+            updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_TARGET, UnDefType.UNDEF);
+            updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_CURRENT, UnDefType.UNDEF);
+            updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_TARGET, UnDefType.UNDEF);
+            updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_CURRENT, UnDefType.UNDEF);
+            updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_TARGET, UnDefType.UNDEF);
+            updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_CURRENT, UnDefType.UNDEF);
+            updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_TARGET, UnDefType.UNDEF);
+        } else {
+            updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_CURRENT,
+                    QuantityType.valueOf(tires.frontLeft.status.currentPressure / 100, Units.BAR));
+            updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_TARGET,
+                    QuantityType.valueOf(tires.frontLeft.status.targetPressure / 100, Units.BAR));
+            updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_CURRENT,
+                    QuantityType.valueOf(tires.frontRight.status.currentPressure / 100, Units.BAR));
+            updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_TARGET,
+                    QuantityType.valueOf(tires.frontRight.status.targetPressure / 100, Units.BAR));
+            updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_CURRENT,
+                    QuantityType.valueOf(tires.rearLeft.status.currentPressure / 100, Units.BAR));
+            updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_TARGET,
+                    QuantityType.valueOf(tires.rearLeft.status.targetPressure / 100, Units.BAR));
+            updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_CURRENT,
+                    QuantityType.valueOf(tires.rearRight.status.currentPressure / 100, Units.BAR));
+            updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_TARGET,
+                    QuantityType.valueOf(tires.rearRight.status.targetPressure / 100, Units.BAR));
+        }
+    }
+
+    protected void updateVehicleStatus(Vehicle v) {
+        updateChannel(CHANNEL_GROUP_STATUS, LOCK, Converter.getLockState(v.properties.areDoorsLocked));
+        updateChannel(CHANNEL_GROUP_STATUS, SERVICE_DATE,
+                VehicleStatusUtils.getNextServiceDate(v.properties.serviceRequired));
+        updateChannel(CHANNEL_GROUP_STATUS, SERVICE_MILEAGE,
+                VehicleStatusUtils.getNextServiceMileage(v.properties.serviceRequired));
+        updateChannel(CHANNEL_GROUP_STATUS, CHECK_CONTROL,
+                StringType.valueOf(v.status.checkControlMessagesGeneralState));
+        updateChannel(CHANNEL_GROUP_STATUS, MOTION, OnOffType.from(v.properties.inMotion));
+        updateChannel(CHANNEL_GROUP_STATUS, LAST_UPDATE,
+                DateTimeType.valueOf(Converter.zonedToLocalDateTime(v.properties.lastUpdatedAt)));
+        updateChannel(CHANNEL_GROUP_STATUS, DOORS, Converter.getClosedState(v.properties.areDoorsClosed));
+        updateChannel(CHANNEL_GROUP_STATUS, WINDOWS, Converter.getClosedState(v.properties.areWindowsClosed));
+
+        if (isElectric) {
+            updateChannel(CHANNEL_GROUP_STATUS, PLUG_CONNECTION,
+                    Converter.getConnectionState(v.properties.chargingState.isChargerConnected));
+            updateChannel(CHANNEL_GROUP_STATUS, CHARGE_STATUS,
+                    StringType.valueOf(Converter.toTitleCase(VehicleStatusUtils.getChargStatus(v))));
+            updateChannel(CHANNEL_GROUP_STATUS, CHARGE_INFO,
+                    StringType.valueOf(Converter.getLocalTime(VehicleStatusUtils.getChargeInfo(v))));
+        }
+    }
+
+    protected void updateRange(Vehicle v) {
+        // get the right unit
+        Unit<Length> lengthUnit = VehicleStatusUtils.getLengthUnit(v.status.fuelIndicators);
+        if (lengthUnit == null) {
+            return;
+        }
+        if (isElectric) {
+            int rangeElectric = VehicleStatusUtils.getRange(Constants.UNIT_PRECENT_JSON, v);
+            QuantityType<Length> qtElectricRange = QuantityType.valueOf(rangeElectric, lengthUnit);
+            QuantityType<Length> qtElectricRadius = QuantityType.valueOf(Converter.guessRangeRadius(rangeElectric),
+                    lengthUnit);
+            updateChannel(CHANNEL_GROUP_RANGE, RANGE_ELECTRIC, qtElectricRange);
+            updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_ELECTRIC, qtElectricRadius);
+        }
+        if (hasFuel) {
+            int rangeFuel = VehicleStatusUtils.getRange(Constants.UNIT_LITER_JSON, v);
+            QuantityType<Length> qtFuelRange = QuantityType.valueOf(rangeFuel, lengthUnit);
+            QuantityType<Length> qtFuelRadius = QuantityType.valueOf(Converter.guessRangeRadius(rangeFuel), lengthUnit);
+            updateChannel(CHANNEL_GROUP_RANGE, RANGE_FUEL, qtFuelRange);
+            updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_FUEL, qtFuelRadius);
+        }
+        if (isHybrid) {
+            int rangeCombined = VehicleStatusUtils.getRange(Constants.PHEV, v);
+            QuantityType<Length> qtHybridRange = QuantityType.valueOf(rangeCombined, lengthUnit);
+            QuantityType<Length> qtHybridRadius = QuantityType.valueOf(Converter.guessRangeRadius(rangeCombined),
+                    lengthUnit);
+            updateChannel(CHANNEL_GROUP_RANGE, RANGE_HYBRID, qtHybridRange);
+            updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_HYBRID, qtHybridRadius);
+        }
+        if (v.status.currentMileage.mileage == Constants.INT_UNDEF) {
+            updateChannel(CHANNEL_GROUP_RANGE, MILEAGE, UnDefType.UNDEF);
+        } else {
+            updateChannel(CHANNEL_GROUP_RANGE, MILEAGE,
+                    QuantityType.valueOf(v.status.currentMileage.mileage, lengthUnit));
+        }
+        if (isElectric) {
+            updateChannel(CHANNEL_GROUP_RANGE, SOC,
+                    QuantityType.valueOf(v.properties.chargingState.chargePercentage, Units.PERCENT));
+        }
+        if (hasFuel) {
+            updateChannel(CHANNEL_GROUP_RANGE, REMAINING_FUEL,
+                    QuantityType.valueOf(v.properties.fuelLevel.value, Units.LITRE));
+        }
+    }
+
+    protected void updateCheckControls(List<CCMMessage> ccl) {
+        if (ccl.isEmpty()) {
+            // No Check Control available - show not active
+            CCMMessage ccm = new CCMMessage();
+            ccm.title = Constants.NO_ENTRIES;
+            ccm.longDescription = Constants.NO_ENTRIES;
+            ccm.state = Constants.NO_ENTRIES;
+            ccl.add(ccm);
+        }
+
+        // add all elements to options
+        checkControlList = ccl;
+        List<CommandOption> ccmDescriptionOptions = new ArrayList<>();
+        boolean isSelectedElementIn = false;
+        int index = 0;
+        for (CCMMessage ccEntry : checkControlList) {
+            ccmDescriptionOptions.add(new CommandOption(Integer.toString(index), ccEntry.title));
+            if (selectedCC.equals(ccEntry.title)) {
+                isSelectedElementIn = true;
+            }
+            index++;
+        }
+        setOptions(CHANNEL_GROUP_CHECK_CONTROL, NAME, ccmDescriptionOptions);
+
+        // 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.title;
+            updateChannel(CHANNEL_GROUP_CHECK_CONTROL, NAME, StringType.valueOf(ccEntry.title));
+            updateChannel(CHANNEL_GROUP_CHECK_CONTROL, DETAILS, StringType.valueOf(ccEntry.longDescription));
+            updateChannel(CHANNEL_GROUP_CHECK_CONTROL, SEVERITY, StringType.valueOf(ccEntry.state));
+        }
+    }
+
+    protected void updateServices(List<CBS> sl) {
+        // if list is empty add "undefined" element
+        if (sl.isEmpty()) {
+            CBS cbsm = new CBS();
+            cbsm.type = Constants.NO_ENTRIES;
+            sl.add(cbsm);
+        }
+
+        // add all elements to options
+        serviceList = sl;
+        List<CommandOption> serviceNameOptions = new ArrayList<>();
+        boolean isSelectedElementIn = false;
+        int index = 0;
+        for (CBS serviceEntry : serviceList) {
+            // create StateOption with "value = list index" and "label = human readable string"
+            serviceNameOptions.add(new CommandOption(Integer.toString(index), serviceEntry.type));
+            if (selectedService.equals(serviceEntry.type)) {
+                isSelectedElementIn = true;
+            }
+            index++;
+        }
+        setOptions(CHANNEL_GROUP_SERVICE, NAME, serviceNameOptions);
+
+        // 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()) {
+            CBS serviceEntry = serviceList.get(index);
+            selectedService = serviceEntry.type;
+            updateChannel(CHANNEL_GROUP_SERVICE, NAME, StringType.valueOf(Converter.toTitleCase(serviceEntry.type)));
+            if (serviceEntry.dateTime != null) {
+                updateChannel(CHANNEL_GROUP_SERVICE, DATE,
+                        DateTimeType.valueOf(Converter.zonedToLocalDateTime(serviceEntry.dateTime)));
+            } else {
+                updateChannel(CHANNEL_GROUP_SERVICE, DATE, UnDefType.UNDEF);
+            }
+            if (serviceEntry.distance != null) {
+                if (Constants.KILOMETERS_JSON.equals(serviceEntry.distance.units)) {
+                    updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE,
+                            QuantityType.valueOf(serviceEntry.distance.value, Constants.KILOMETRE_UNIT));
+                } else {
+                    updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE,
+                            QuantityType.valueOf(serviceEntry.distance.value, ImperialUnits.MILE));
+                }
+            } else {
+                updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE,
+                        QuantityType.valueOf(Constants.INT_UNDEF, Constants.KILOMETRE_UNIT));
+            }
+        }
+    }
+
+    protected void updateSessions(List<ChargeSession> sl) {
+        // if list is empty add "undefined" element
+        if (sl.isEmpty()) {
+            ChargeSession cs = new ChargeSession();
+            cs.title = Constants.NO_ENTRIES;
+            sl.add(cs);
+        }
+
+        // add all elements to options
+        sessionList = sl;
+        List<CommandOption> sessionNameOptions = new ArrayList<>();
+        boolean isSelectedElementIn = false;
+        int index = 0;
+        for (ChargeSession session : sessionList) {
+            // create StateOption with "value = list index" and "label = human readable string"
+            sessionNameOptions.add(new CommandOption(Integer.toString(index), session.title));
+            if (selectedService.equals(session.title)) {
+                isSelectedElementIn = true;
+            }
+            index++;
+        }
+        setOptions(CHANNEL_GROUP_CHARGE_SESSION, TITLE, sessionNameOptions);
+
+        // if current selected item isn't anymore in the list select first entry
+        if (!isSelectedElementIn) {
+            selectSession(0);
+        }
+    }
+
+    protected void selectSession(int index) {
+        if (index >= 0 && index < sessionList.size()) {
+            ChargeSession sessionEntry = sessionList.get(index);
+            selectedService = sessionEntry.title;
+            updateChannel(CHANNEL_GROUP_CHARGE_SESSION, TITLE, StringType.valueOf(sessionEntry.title));
+            updateChannel(CHANNEL_GROUP_CHARGE_SESSION, SUBTITLE, StringType.valueOf(sessionEntry.subtitle));
+            if (sessionEntry.energyCharged != null) {
+                updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ENERGY, StringType.valueOf(sessionEntry.energyCharged));
+            } else {
+                updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ENERGY, StringType.valueOf(Constants.UNDEF));
+            }
+            if (sessionEntry.issues != null) {
+                updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ISSUE, StringType.valueOf(sessionEntry.issues));
+            } else {
+                updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ISSUE, StringType.valueOf(Constants.HYPHEN));
+            }
+            updateChannel(CHANNEL_GROUP_CHARGE_SESSION, STATUS, StringType.valueOf(sessionEntry.sessionStatus));
+        }
+    }
+
+    protected void updateChargeProfile(ChargeProfile cp) {
+        ChargeProfileWrapper cpw = new ChargeProfileWrapper(cp);
+
+        updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_PREFERENCE, StringType.valueOf(cpw.getPreference()));
+        updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_MODE, StringType.valueOf(cpw.getMode()));
+        updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_CONTROL, StringType.valueOf(cpw.getControlType()));
+        ChargingSettings cs = cpw.getChargeSettings();
+        if (cs != null) {
+            updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_TARGET,
+                    DecimalType.valueOf(Integer.toString(cs.targetSoc)));
+            updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_LIMIT,
+                    OnOffType.from(cs.isAcCurrentLimitActive));
+        }
+        final Boolean climate = cpw.isEnabled(ProfileKey.CLIMATE);
+        updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_CLIMATE,
+                climate == null ? UnDefType.UNDEF : OnOffType.from(climate));
+        updateTimedState(cpw, ProfileKey.WINDOWSTART);
+        updateTimedState(cpw, ProfileKey.WINDOWEND);
+        updateTimedState(cpw, ProfileKey.TIMER1);
+        updateTimedState(cpw, ProfileKey.TIMER2);
+        updateTimedState(cpw, ProfileKey.TIMER3);
+        updateTimedState(cpw, ProfileKey.TIMER4);
+    }
+
+    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_PROFILE, timed.time,
+                    time.equals(Constants.NULL_LOCAL_TIME) ? 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_PROFILE, timed.timer + CHARGE_ENABLED,
+                        enabled == null ? UnDefType.UNDEF : OnOffType.from(enabled));
+                if (timed.hasDays) {
+                    final Set<DayOfWeek> days = profile.getDays(key);
+                    EnumSet.allOf(DayOfWeek.class).forEach(day -> {
+                        updateChannel(CHANNEL_GROUP_CHARGE_PROFILE,
+                                timed.timer + ChargeProfileUtils.getDaysChannel(day),
+                                days == null ? UnDefType.UNDEF : OnOffType.from(days.contains(day)));
+                    });
+                }
+            }
+        }
+    }
+
+    protected void updateDoors(DoorsWindows dw) {
+        updateChannel(CHANNEL_GROUP_DOORS, DOOR_DRIVER_FRONT,
+                StringType.valueOf(Converter.toTitleCase(dw.doors.driverFront)));
+        updateChannel(CHANNEL_GROUP_DOORS, DOOR_DRIVER_REAR,
+                StringType.valueOf(Converter.toTitleCase(dw.doors.driverRear)));
+        updateChannel(CHANNEL_GROUP_DOORS, DOOR_PASSENGER_FRONT,
+                StringType.valueOf(Converter.toTitleCase(dw.doors.passengerFront)));
+        updateChannel(CHANNEL_GROUP_DOORS, DOOR_PASSENGER_REAR,
+                StringType.valueOf(Converter.toTitleCase(dw.doors.passengerRear)));
+        updateChannel(CHANNEL_GROUP_DOORS, TRUNK, StringType.valueOf(Converter.toTitleCase(dw.trunk)));
+        updateChannel(CHANNEL_GROUP_DOORS, HOOD, StringType.valueOf(Converter.toTitleCase(dw.hood)));
+    }
+
+    protected void updateWindows(DoorsWindows dw) {
+        updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_DRIVER_FRONT,
+                StringType.valueOf(Converter.toTitleCase(dw.windows.driverFront)));
+        updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_DRIVER_REAR,
+                StringType.valueOf(Converter.toTitleCase(dw.windows.driverRear)));
+        updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_PASSENGER_FRONT,
+                StringType.valueOf(Converter.toTitleCase(dw.windows.passengerFront)));
+        updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_PASSENGER_REAR,
+                StringType.valueOf(Converter.toTitleCase(dw.windows.passengerRear)));
+        updateChannel(CHANNEL_GROUP_DOORS, SUNROOF, StringType.valueOf(Converter.toTitleCase(dw.moonroof)));
+    }
+
+    protected void updatePosition(Location pos) {
+        updateChannel(CHANNEL_GROUP_LOCATION, GPS, PointType
+                .valueOf(Double.toString(pos.coordinates.latitude) + "," + Double.toString(pos.coordinates.longitude)));
+        updateChannel(CHANNEL_GROUP_LOCATION, HEADING, QuantityType.valueOf(pos.heading, Units.DEGREE_ANGLE));
+        updateChannel(CHANNEL_GROUP_LOCATION, ADDRESS, StringType.valueOf(pos.address.formatted));
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java
new file mode 100644 (file)
index 0000000..c7b2da8
--- /dev/null
@@ -0,0 +1,351 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.handler;
+
+import static org.openhab.binding.mybmw.internal.MyBMWConstants.*;
+
+import java.util.Optional;
+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.mybmw.internal.VehicleConfiguration;
+import org.openhab.binding.mybmw.internal.dto.charge.ChargeSessionsContainer;
+import org.openhab.binding.mybmw.internal.dto.charge.ChargeStatisticsContainer;
+import org.openhab.binding.mybmw.internal.dto.network.NetworkError;
+import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
+import org.openhab.binding.mybmw.internal.utils.Constants;
+import org.openhab.binding.mybmw.internal.utils.Converter;
+import org.openhab.binding.mybmw.internal.utils.ImageProperties;
+import org.openhab.binding.mybmw.internal.utils.RemoteServiceUtils;
+import org.openhab.core.io.net.http.HttpUtil;
+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.types.Command;
+import org.openhab.core.types.RefreshType;
+
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link VehicleHandler} handles responses from BMW API
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - edit & send charge profile
+ */
+@NonNullByDefault
+public class VehicleHandler extends VehicleChannelHandler {
+    private Optional<MyBMWProxy> proxy = Optional.empty();
+    private Optional<RemoteServiceHandler> remote = Optional.empty();
+    public Optional<VehicleConfiguration> configuration = Optional.empty();
+    private Optional<ScheduledFuture<?>> refreshJob = Optional.empty();
+    private Optional<ScheduledFuture<?>> editTimeout = Optional.empty();
+
+    private ImageProperties imageProperties = new ImageProperties();
+    VehicleStatusCallback vehicleStatusCallback = new VehicleStatusCallback();
+    ChargeStatisticsCallback chargeStatisticsCallback = new ChargeStatisticsCallback();
+    ChargeSessionsCallback chargeSessionCallback = new ChargeSessionsCallback();
+    ByteResponseCallback imageCallback = new ImageCallback();
+
+    public VehicleHandler(Thing thing, MyBMWCommandOptionProvider cop, String driveTrain) {
+        super(thing, cop, driveTrain);
+    }
+
+    @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_STATUS.equals(group)) {
+                vehicleStatusCache.ifPresent(vehicleStatus -> vehicleStatusCallback.onResponse(vehicleStatus));
+            } 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_DOOR_LOCK:
+                        case REMOTE_SERVICE_DOOR_UNLOCK:
+                        case REMOTE_SERVICE_HORN:
+                        case REMOTE_SERVICE_VEHICLE_FINDER:
+                            RemoteServiceUtils.getRemoteService(serviceCommand)
+                                    .ifPresentOrElse(service -> remot.execute(service), () -> {
+                                        logger.debug("Remote service execution {} unknown", serviceCommand);
+                                    });
+                            break;
+                        case REMOTE_SERVICE_AIR_CONDITIONING_START:
+                            RemoteServiceUtils.getRemoteService(serviceCommand)
+                                    .ifPresentOrElse(service -> remot.execute(service), () -> {
+                                        logger.debug("Remote service execution {} unknown", serviceCommand);
+                                    });
+                            break;
+                        case REMOTE_SERVICE_AIR_CONDITIONING_STOP:
+                            RemoteServiceUtils.getRemoteService(serviceCommand)
+                                    .ifPresentOrElse(service -> remot.execute(service), () -> {
+                                        logger.debug("Remote service execution {} unknown", serviceCommand);
+                                    });
+                            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);
+                                imageCache = Optional.empty();
+                                proxy.ifPresent(prox -> prox.requestImage(config, imageProperties, imageCallback));
+                            }
+                        }
+                        updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, StringType.valueOf(newViewport));
+                    }
+                }
+            });
+        } 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_SESSION.equals(group)) {
+            if (command instanceof StringType) {
+                int index = Converter.getIndex(command.toFullString());
+                if (index != -1) {
+                    selectSession(index);
+                } else {
+                    logger.debug("Cannot select Session index {}", command.toFullString());
+                }
+            }
+        }
+    }
+
+    @Override
+    public void initialize() {
+        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) {
+                proxy = ((MyBMWBridgeHandler) handler).getProxy();
+                remote = proxy.map(prox -> prox.getRemoteServiceHandler(this));
+            } else {
+                logger.debug("Bridge Handler null");
+            }
+        } else {
+            logger.debug("Bridge null");
+        }
+
+        imageProperties = new ImageProperties();
+        updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, StringType.valueOf(imageProperties.viewport));
+
+        // 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 -> {
+                prox.requestVehicles(config.vehicleBrand, vehicleStatusCallback);
+                if (isElectric) {
+                    prox.requestChargeStatistics(config, chargeStatisticsCallback);
+                    prox.requestChargeSessions(config, chargeSessionCallback);
+                }
+                if (!imageCache.isPresent() && !imageProperties.failLimitReached()) {
+                    prox.requestImage(config, imageProperties, imageCallback);
+                }
+            }, () -> {
+                logger.warn("MyBMW Vehicle Configuration isn't present");
+            });
+        }, () -> {
+            logger.warn("MyBMWProxy isn't present");
+        });
+    }
+
+    public void updateRemoteExecutionStatus(@Nullable String service, String status) {
+        updateChannel(CHANNEL_GROUP_REMOTE, REMOTE_STATE,
+                StringType.valueOf((service == null ? "-" : service) + Constants.SPACE + status.toLowerCase()));
+    }
+
+    public Optional<VehicleConfiguration> getConfiguration() {
+        return configuration;
+    }
+
+    public ScheduledExecutorService getScheduler() {
+        return scheduler;
+    }
+
+    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();
+                }
+            }
+        }
+
+        /**
+         * 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();
+            }
+        }
+    }
+
+    /**
+     * 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) {
+                if (getConfiguration().isPresent()) {
+                    Vehicle v = Converter.getVehicle(configuration.get().vin, content);
+                    if (v.valid) {
+                        vehicleStatusCache = Optional.of(content);
+                        updateStatus(ThingStatus.ONLINE);
+                        updateChannel(CHANNEL_GROUP_STATUS, RAW,
+                                StringType.valueOf(Converter.getRawVehicleContent(configuration.get().vin, content)));
+                        updateVehicle(v);
+                        if (isElectric) {
+                            updateChargeProfile(v.status.chargingProfile);
+                        }
+                    } else {
+                        logger.debug("Vehicle {} not valid", configuration.get().vin);
+                    }
+                } else {
+                    logger.debug("configuration not present");
+                }
+            } else {
+                updateChannel(CHANNEL_GROUP_STATUS, RAW, StringType.valueOf(Constants.EMPTY_JSON));
+                logger.debug("Content not valid");
+            }
+        }
+
+        @Override
+        public void onError(NetworkError error) {
+            logger.debug("{}", error.toString());
+            vehicleStatusCache = Optional.of(Converter.getGson().toJson(error));
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error.reason);
+        }
+    }
+
+    public class ChargeStatisticsCallback implements StringResponseCallback {
+        @Override
+        public void onResponse(@Nullable String content) {
+            if (content != null) {
+                try {
+                    ChargeStatisticsContainer csc = Converter.getGson().fromJson(content,
+                            ChargeStatisticsContainer.class);
+                    if (csc != null) {
+                        updateChargeStatistics(csc);
+                    }
+                } catch (JsonSyntaxException jse) {
+                    logger.warn("{}", jse.getLocalizedMessage());
+                }
+            } else {
+                logger.debug("Content not valid");
+            }
+        }
+
+        @Override
+        public void onError(NetworkError error) {
+            logger.debug("{}", error.toString());
+        }
+    }
+
+    public class ChargeSessionsCallback implements StringResponseCallback {
+        @Override
+        public void onResponse(@Nullable String content) {
+            if (content != null) {
+                try {
+                    ChargeSessionsContainer csc = Converter.getGson().fromJson(content, ChargeSessionsContainer.class);
+                    if (csc != null) {
+                        if (csc.chargingSessions != null) {
+                            updateSessions(csc.chargingSessions.sessions);
+                        }
+                    }
+                } catch (JsonSyntaxException jse) {
+                    logger.warn("{}", jse.getLocalizedMessage());
+                }
+            } else {
+                logger.debug("Content not valid");
+            }
+        }
+
+        @Override
+        public void onError(NetworkError error) {
+            logger.debug("{}", error.toString());
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/simulation/Injector.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/simulation/Injector.java
new file mode 100644 (file)
index 0000000..c3279d5
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.handler.simulation;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link Injector} Simulates feedback of the BMW API
+ *
+ * @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;
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/BimmerConstants.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/BimmerConstants.java
new file mode 100644 (file)
index 0000000..5e19e8f
--- /dev/null
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.utils;
+
+import java.util.List;
+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 {
+
+    public static final String REGION_NORTH_AMERICA = "NORTH_AMERICA";
+    public static final String REGION_CHINA = "CHINA";
+    public static final String REGION_ROW = "ROW";
+
+    public static final String BRAND_BMW = "bmw";
+    public static final String BRAND_MINI = "mini";
+    public static final List<String> ALL_BRANDS = List.of(BRAND_BMW, BRAND_MINI);
+
+    public static final String OAUTH_ENDPOINT = "/gcdm/oauth/authenticate";
+
+    public static final String EADRAX_SERVER_NORTH_AMERICA = "cocoapi.bmwgroup.us";
+    public static final String EADRAX_SERVER_ROW = "cocoapi.bmwgroup.com";
+    public static final String EADRAX_SERVER_CHINA = "myprofile.bmw.com.cn";
+    public static final Map<String, String> EADRAX_SERVER_MAP = Map.of(REGION_NORTH_AMERICA,
+            EADRAX_SERVER_NORTH_AMERICA, REGION_CHINA, EADRAX_SERVER_CHINA, REGION_ROW, EADRAX_SERVER_ROW);
+
+    public static final String OCP_APIM_KEY_NORTH_AMERICA = "31e102f5-6f7e-7ef3-9044-ddce63891362";
+    public static final String OCP_APIM_KEY_ROW = "4f1c85a3-758f-a37d-bbb6-f8704494acfa";
+    public static final Map<String, String> OCP_APIM_KEYS = Map.of(REGION_NORTH_AMERICA, OCP_APIM_KEY_NORTH_AMERICA,
+            REGION_ROW, OCP_APIM_KEY_ROW);
+
+    public static final String CHINA_PUBLIC_KEY = "/eadrax-coas/v1/cop/publickey";
+    public static final String CHINA_LOGIN = "/eadrax-coas/v1/login/pwd";
+
+    // Http variables
+    public static final String USER_AGENT_BMW = "android(v1.07_20200330);bmw;1.7.0(11152)";
+    public static final String USER_AGENT_MINI = "android(v1.07_20200330);mini;1.7.0(11152)";
+    public static final Map<String, String> BRAND_USER_AGENTS_MAP = Map.of(BRAND_BMW, USER_AGENT_BMW, BRAND_MINI,
+            USER_AGENT_MINI);
+
+    public static final String LOGIN_NONCE = "login_nonce";
+    public static final String AUTHORIZATION_CODE = "authorization_code";
+
+    // Parameters for API Requests
+    public static final String TIRE_GUARD_MODE = "tireGuardMode";
+    public static final String APP_DATE_TIME = "appDateTime";
+    public static final String APP_TIMEZONE = "apptimezone";
+
+    // API endpoints
+    public static final String API_OAUTH_CONFIG = "/eadrax-ucs/v1/presentation/oauth/config";
+    public static final String API_VEHICLES = "/eadrax-vcs/v1/vehicles";
+    public static final String API_REMOTE_SERVICE_BASE_URL = "/eadrax-vrccs/v2/presentation/remote-commands/"; // '/{vin}/{service_type}'
+    public static final String API_POI = "/eadrax-dcs/v1/send-to-car/send-to-car";
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargeProfileUtils.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargeProfileUtils.java
new file mode 100644 (file)
index 0000000..bd474e7
--- /dev/null
@@ -0,0 +1,137 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.utils;
+
+import static org.openhab.binding.mybmw.internal.MyBMWConstants.*;
+
+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.mybmw.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.TIMER4, new TimedChannel(CHARGE_TIMER4 + CHARGE_DEPARTURE, CHARGE_TIMER4, true));
+        }
+    };
+
+    @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));
+                put(CHARGE_TIMER4 + 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));
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargeProfileWrapper.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargeProfileWrapper.java
new file mode 100644 (file)
index 0000000..c734c33
--- /dev/null
@@ -0,0 +1,303 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.utils;
+
+import static org.openhab.binding.mybmw.internal.utils.ChargeProfileWrapper.ProfileKey.*;
+import static org.openhab.binding.mybmw.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.mybmw.internal.MyBMWConstants.ChargingMode;
+import org.openhab.binding.mybmw.internal.MyBMWConstants.ChargingPreference;
+import org.openhab.binding.mybmw.internal.dto.charge.ChargeProfile;
+import org.openhab.binding.mybmw.internal.dto.charge.ChargingSettings;
+import org.openhab.binding.mybmw.internal.dto.charge.ChargingWindow;
+import org.openhab.binding.mybmw.internal.dto.charge.Time;
+import org.openhab.binding.mybmw.internal.dto.charge.Timer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 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);
+
+    private static final String CHARGING_WINDOW = "chargingWindow";
+    private static final String WEEKLY_PLANNER = "weeklyPlanner";
+    private static final String ACTIVATE = "activate";
+    private static final String DEACTIVATE = "deactivate";
+
+    public enum ProfileKey {
+        CLIMATE,
+        TIMER1,
+        TIMER2,
+        TIMER3,
+        TIMER4,
+        WINDOWSTART,
+        WINDOWEND
+    }
+
+    private Optional<ChargingMode> mode = Optional.empty();
+    private Optional<ChargingPreference> preference = Optional.empty();
+    private Optional<String> controlType = Optional.empty();
+    private Optional<ChargingSettings> chargeSettings = 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 ChargeProfileWrapper(final ChargeProfile profile) {
+        setPreference(profile.chargingPreference);
+        setMode(profile.chargingMode);
+        controlType = Optional.of(profile.chargingControlType);
+        chargeSettings = Optional.of(profile.chargingSettings);
+        setEnabled(CLIMATE, profile.climatisationOn);
+
+        addTimer(TIMER1, profile.getTimerId(1));
+        addTimer(TIMER2, profile.getTimerId(2));
+        if (profile.chargingControlType.equals(WEEKLY_PLANNER)) {
+            addTimer(TIMER3, profile.getTimerId(3));
+            addTimer(TIMER4, profile.getTimerId(4));
+        }
+
+        if (CHARGING_WINDOW.equals(profile.chargingPreference)) {
+            addTime(WINDOWSTART, profile.reductionOfChargeCurrent.start);
+            addTime(WINDOWEND, profile.reductionOfChargeCurrent.end);
+        } else {
+            preference.ifPresent(pref -> {
+                if (ChargingPreference.chargingWindow.equals(pref)) {
+                    addTime(WINDOWSTART, null);
+                    addTime(WINDOWEND, null);
+                }
+            });
+        }
+    }
+
+    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 @Nullable String getControlType() {
+        return controlType.get();
+    }
+
+    public @Nullable ChargingSettings getChargeSettings() {
+        return chargeSettings.get();
+    }
+
+    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 LocalTime getTime(final ProfileKey key) {
+        LocalTime t = times.get(key);
+        if (t != null) {
+            return t;
+        } else {
+            LOGGER.debug("Profile not valid - Key {} doesn't contain boolean value", key);
+            return Constants.NULL_LOCAL_TIME;
+        }
+    }
+
+    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();
+
+        preference.ifPresent(pref -> profile.chargingPreference = pref.name());
+        profile.chargingControlType = controlType.get();
+        Boolean enabledBool = isEnabled(CLIMATE);
+        profile.climatisationOn = enabledBool == null ? false : enabledBool;
+        preference.ifPresent(pref -> {
+            if (ChargingPreference.chargingWindow.equals(pref)) {
+                profile.chargingMode = getMode();
+                final LocalTime start = getTime(WINDOWSTART);
+                final LocalTime end = getTime(WINDOWEND);
+                if (!start.equals(Constants.NULL_LOCAL_TIME) && !end.equals(Constants.NULL_LOCAL_TIME)) {
+                    ChargingWindow cw = new ChargingWindow();
+                    profile.reductionOfChargeCurrent = cw;
+                    cw.start = new Time();
+                    cw.start.hour = start.getHour();
+                    cw.start.minute = start.getMinute();
+                    cw.end = new Time();
+                    cw.end.hour = end.getHour();
+                    cw.end.minute = end.getMinute();
+                }
+            }
+        });
+        profile.departureTimes = new ArrayList<Timer>();
+        profile.departureTimes.add(getTimer(TIMER1));
+        profile.departureTimes.add(getTimer(TIMER2));
+        if (profile.chargingControlType.equals(WEEKLY_PLANNER)) {
+            profile.departureTimes.add(getTimer(TIMER3));
+            profile.departureTimes.add(getTimer(TIMER4));
+        }
+
+        profile.chargingSettings = chargeSettings.get();
+        return Converter.getGson().toJson(profile);
+    }
+
+    private void addTime(final ProfileKey key, @Nullable final Time time) {
+        try {
+            times.put(key, time == null ? NULL_LOCAL_TIME : LocalTime.parse(Converter.getTime(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);
+            daysOfWeek.put(key, EnumSet.noneOf(DayOfWeek.class));
+        } else {
+            enabled.put(key, ACTIVATE.equals(timer.action));
+            addTime(key, timer.timeStamp);
+            final EnumSet<DayOfWeek> daySet = EnumSet.noneOf(DayOfWeek.class);
+            if (timer.timerWeekDays != null) {
+                daysOfWeek.put(key, EnumSet.noneOf(DayOfWeek.class));
+                for (String day : timer.timerWeekDays) {
+                    try {
+                        daySet.add(DayOfWeek.valueOf(day.toUpperCase()));
+                    } catch (IllegalArgumentException iae) {
+                        LOGGER.warn("unexpected value for {} day: {}", key.name(), day);
+                    }
+                    daysOfWeek.put(key, daySet);
+                }
+            }
+        }
+    }
+
+    private Timer getTimer(final ProfileKey key) {
+        final Timer timer = new Timer();
+        switch (key) {
+            case TIMER1:
+                timer.id = 1;
+                break;
+            case TIMER2:
+                timer.id = 2;
+                break;
+            case TIMER3:
+                timer.id = 3;
+                break;
+            case TIMER4:
+                timer.id = 4;
+                break;
+            default:
+                // timer id stays -1
+                break;
+        }
+        Boolean enabledBool = isEnabled(key);
+        if (enabledBool != null) {
+            timer.action = enabledBool ? ACTIVATE : DEACTIVATE;
+        } else {
+            timer.action = DEACTIVATE;
+        }
+        final LocalTime time = getTime(key);
+        if (!time.equals(Constants.NULL_LOCAL_TIME)) {
+            timer.timeStamp = new Time();
+            timer.timeStamp.hour = time.getHour();
+            timer.timeStamp.minute = time.getMinute();
+        }
+        final Set<DayOfWeek> days = daysOfWeek.get(key);
+        if (days != null) {
+            timer.timerWeekDays = new ArrayList<>();
+            for (DayOfWeek day : days) {
+                timer.timerWeekDays.add(day.name().toLowerCase());
+            }
+        }
+        return timer;
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/Constants.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/Constants.java
new file mode 100644 (file)
index 0000000..7311bb4
--- /dev/null
@@ -0,0 +1,106 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.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 NO_ENTRIES = "-";
+    public static final String OPEN = "Open";
+    public static final String CLOSED = "Closed";
+    public static final String LOCKED = "Locked";
+    public static final String UNLOCKED = "Unlocked";
+    public static final String CONNECTED = "Connected";
+    public static final String UNCONNECTED = "Not connected";
+    public static final String UNDEF = UnDefType.UNDEF.toFullString();
+    public static final String NULL_TIME = "00:00";
+    public static final String KILOMETERS_JSON = "KILOMETERS";
+    public static final String KM_JSON = "km";
+    public static final String MI_JSON = "mi";
+    public static final String UNIT_PRECENT_JSON = "%";
+    public static final String UNIT_LITER_JSON = "l";
+    public static final Unit<Length> KILOMETRE_UNIT = MetricPrefix.KILO(SIUnits.METRE);
+    public static final int INT_UNDEF = -1;
+
+    // Services in Discovery
+    public static final String ENABLED = "ENABLED";
+
+    // 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 SEMICOLON = ";";
+    public static final String TILDE = "~";
+
+    public static final String ANONYMOUS = "anonymous";
+    public static final String EMPTY_JSON = "{}";
+    public static final String LANGUAGE_AUTODETECT = "AUTODETECT";
+
+    // 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");
+        }
+    };
+
+    // Drive Train definitions from json
+    public static final String BEV = "ELECTRIC";
+    public static final String REX_EXTENSION = "(+ REX)";
+    public static final String HYBRID = "HYBRID";
+    public static final String CONV = "COMBUSTION";
+    public static final String PHEV = "PLUGIN_HYBRID";
+
+    // Carging States
+    public static final String DEFAULT = "DEFAULT";
+    public static final String NOT_CHARGING_STATE = "NOT_CHARGING";
+    public static final String CHARGING_STATE = "CHARGING";
+    public static final String PLUGGED_STATE = "PLUGGED_IN";
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/Converter.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/Converter.java
new file mode 100644 (file)
index 0000000..6f8fa6d
--- /dev/null
@@ -0,0 +1,371 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.utils;
+
+import java.lang.reflect.Type;
+import java.text.SimpleDateFormat;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.Random;
+import java.util.TimeZone;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mybmw.internal.MyBMWConstants;
+import org.openhab.binding.mybmw.internal.dto.charge.Time;
+import org.openhab.binding.mybmw.internal.dto.properties.Address;
+import org.openhab.binding.mybmw.internal.dto.properties.Coordinates;
+import org.openhab.binding.mybmw.internal.dto.properties.Distance;
+import org.openhab.binding.mybmw.internal.dto.properties.Location;
+import org.openhab.binding.mybmw.internal.dto.properties.Range;
+import org.openhab.binding.mybmw.internal.dto.status.Mileage;
+import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * 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 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 DateTimeFormatter LOCALE_ENGLISH_TIMEFORMATTER = DateTimeFormatter.ofPattern("hh:mm a",
+            Locale.ENGLISH);
+    public static final SimpleDateFormat ISO_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS");
+
+    private static final Gson GSON = new Gson();
+    private static final Vehicle INVALID_VEHICLE = new Vehicle();
+    private static final String SPLIT_HYPHEN = "-";
+    private static final String SPLIT_BRACKET = "\\(";
+    private static final String VIN_PATTERN = "\"vin\":";
+    private static final String VEHICLE_LOCATION_PATTERN = "\"vehicleLocation\":";
+    private static final String VEHICLE_LOCATION_REPLACEMENT = "\"vehicleLocation\": {\"coordinates\": {\"latitude\": 1.1,\"longitude\": 2.2},\"address\": {\"formatted\": \"anonymous\"},\"heading\": -1}";
+    private static final char OPEN_BRACKET = "{".charAt(0);
+    private static final char CLOSING_BRACKET = "}".charAt(0);
+
+    // https://www.baeldung.com/gson-list
+    public static final Type VEHICLE_LIST_TYPE = new TypeToken<ArrayList<Vehicle>>() {
+    }.getType();
+    public static int offsetMinutes = -1;
+
+    public static String zonedToLocalDateTime(String input) {
+        try {
+            ZonedDateTime d = ZonedDateTime.parse(input).withZoneSameInstant(ZoneId.systemDefault());
+            return d.toLocalDateTime().format(Converter.DATE_INPUT_PATTERN);
+        } catch (Exception e) {
+            LOGGER.debug("Unable to parse date {} - {}", input, e.getMessage());
+        }
+        return input;
+    }
+
+    public static String toTitleCase(@Nullable String input) {
+        if (input == null) {
+            return toTitleCase(Constants.UNDEF);
+        } else if (input.length() == 1) {
+            return input;
+        } 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 int guessRangeRadius(double range) {
+        return (int) (range * 0.8);
+    }
+
+    public static int getIndex(String fullString) {
+        int index = -1;
+        try {
+            index = Integer.parseInt(fullString);
+        } catch (NumberFormatException nfe) {
+        }
+        return index;
+    }
+
+    /**
+     * Returns list of found vehicles
+     * In case of errors return empty list
+     *
+     * @param json
+     * @return
+     */
+    public static List<Vehicle> getVehicleList(String json) {
+        try {
+            List<Vehicle> l = GSON.fromJson(json, VEHICLE_LIST_TYPE);
+            if (l != null) {
+                return l;
+            } else {
+                return new ArrayList<Vehicle>();
+            }
+        } catch (JsonSyntaxException e) {
+            LOGGER.warn("JsonSyntaxException {}", e.getMessage());
+            return new ArrayList<Vehicle>();
+        }
+    }
+
+    public static Vehicle getVehicle(String vin, String json) {
+        List<Vehicle> l = getVehicleList(json);
+        for (Vehicle vehicle : l) {
+            if (vin.equals(vehicle.vin)) {
+                // declare vehicle as valid
+                vehicle.valid = true;
+                return getConsistentVehcile(vehicle);
+            }
+        }
+        return INVALID_VEHICLE;
+    }
+
+    public static String getRawVehicleContent(String vin, String json) {
+        JsonArray jArr = JsonParser.parseString(json).getAsJsonArray();
+        for (int i = 0; i < jArr.size(); i++) {
+            JsonObject jo = jArr.get(i).getAsJsonObject();
+            String jsonVin = jo.getAsJsonPrimitive(MyBMWConstants.VIN).getAsString();
+            if (vin.equals(jsonVin)) {
+                return jo.toString();
+            }
+        }
+        return Constants.EMPTY_JSON;
+    }
+
+    /**
+     * ensure basic data like mileage and location data are available every time
+     *
+     * @param v
+     * @return
+     */
+    public static Vehicle getConsistentVehcile(Vehicle v) {
+        if (v.status.currentMileage == null) {
+            v.status.currentMileage = new Mileage();
+            v.status.currentMileage.mileage = -1;
+            v.status.currentMileage.units = "km";
+        }
+        if (v.properties.combustionRange == null) {
+            v.properties.combustionRange = new Range();
+            v.properties.combustionRange.distance = new Distance();
+            v.properties.combustionRange.distance.value = -1;
+            v.properties.combustionRange.distance.units = Constants.EMPTY;
+        }
+        if (v.properties.vehicleLocation == null) {
+            v.properties.vehicleLocation = new Location();
+            v.properties.vehicleLocation.heading = -1;
+            v.properties.vehicleLocation.coordinates = new Coordinates();
+            v.properties.vehicleLocation.coordinates.latitude = -1.234;
+            v.properties.vehicleLocation.coordinates.longitude = -9.876;
+            v.properties.vehicleLocation.address = new Address();
+            v.properties.vehicleLocation.address.formatted = Constants.UNDEF;
+        }
+        return v;
+    }
+
+    public static String getRandomString(int size) {
+        int leftLimit = 97; // letter 'a'
+        int rightLimit = 122; // letter 'z'
+        Random random = new Random();
+
+        String generatedString = random.ints(leftLimit, rightLimit + 1).limit(size)
+                .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString();
+
+        return generatedString;
+    }
+
+    public static State getLockState(boolean lock) {
+        if (lock) {
+            return StringType.valueOf(Constants.LOCKED);
+        } else {
+            return StringType.valueOf(Constants.UNLOCKED);
+        }
+    }
+
+    public static State getClosedState(boolean close) {
+        if (close) {
+            return StringType.valueOf(Constants.CLOSED);
+        } else {
+            return StringType.valueOf(Constants.OPEN);
+        }
+    }
+
+    public static State getConnectionState(boolean connected) {
+        if (connected) {
+            return StringType.valueOf(Constants.CONNECTED);
+        } else {
+            return StringType.valueOf(Constants.UNCONNECTED);
+        }
+    }
+
+    public static String getCurrentISOTime() {
+        Date date = new Date(System.currentTimeMillis());
+        synchronized (ISO_FORMATTER) {
+            ISO_FORMATTER.setTimeZone(TimeZone.getTimeZone("UTC"));
+            return ISO_FORMATTER.format(date);
+        }
+    }
+
+    public static String getTime(Time t) {
+        StringBuffer time = new StringBuffer();
+        if (t.hour < 10) {
+            time.append("0");
+        }
+        time.append(Integer.toString(t.hour)).append(":");
+        if (t.minute < 10) {
+            time.append("0");
+        }
+        time.append(Integer.toString(t.minute));
+        return time.toString();
+    }
+
+    public static int getOffsetMinutes() {
+        if (offsetMinutes == -1) {
+            ZoneOffset zo = ZonedDateTime.now().getOffset();
+            offsetMinutes = zo.getTotalSeconds() / 60;
+        }
+        return offsetMinutes;
+    }
+
+    public static int stringToInt(String intStr) {
+        int integer = Constants.INT_UNDEF;
+        try {
+            integer = Integer.parseInt(intStr);
+
+        } catch (Exception e) {
+            LOGGER.debug("Unable to convert range {} into int value", intStr);
+        }
+        return integer;
+    }
+
+    public static String getLocalTime(String chrageInfoLabel) {
+        String[] timeSplit = chrageInfoLabel.split(Constants.TILDE);
+        if (timeSplit.length == 2) {
+            try {
+                LocalTime timeL = LocalTime.parse(timeSplit[1].trim(), LOCALE_ENGLISH_TIMEFORMATTER);
+                return timeSplit[0] + Constants.TILDE + timeL.toString();
+            } catch (Exception e) {
+                LOGGER.debug("Unable to parse date {} - {}", timeSplit[1], e.getMessage());
+            }
+        }
+        return chrageInfoLabel;
+    }
+
+    public static String anonymousFingerprint(String raw) {
+        String anonymousFingerprintString = raw;
+        int vinStartIndex = raw.indexOf(VIN_PATTERN);
+        if (vinStartIndex != -1) {
+            String[] arr = raw.substring(vinStartIndex + VIN_PATTERN.length()).trim().split("\"");
+            String vin = arr[1].trim();
+            anonymousFingerprintString = raw.replace(vin, "anonymous");
+        }
+
+        int locationStartIndex = raw.indexOf(VEHICLE_LOCATION_PATTERN);
+        int bracketCounter = -1;
+        if (locationStartIndex != -1) {
+            int endLocationIndex = 0;
+            for (int i = locationStartIndex; i < raw.length() && bracketCounter != 0; i++) {
+                endLocationIndex = i;
+                if (raw.charAt(i) == OPEN_BRACKET) {
+                    if (bracketCounter == -1) {
+                        // start point
+                        bracketCounter = 1;
+                    } else {
+                        bracketCounter++;
+                    }
+                } else if (raw.charAt(i) == CLOSING_BRACKET) {
+                    bracketCounter--;
+                }
+            }
+            String locationReplacement = raw.substring(locationStartIndex, endLocationIndex + 1);
+            anonymousFingerprintString = anonymousFingerprintString.replace(locationReplacement,
+                    VEHICLE_LOCATION_REPLACEMENT);
+        }
+        return anonymousFingerprintString;
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/HTTPConstants.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/HTTPConstants.java
new file mode 100644 (file)
index 0000000..81a0e17
--- /dev/null
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.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 CONTENT_TYPE_URL_ENCODED = "application/x-www-form-urlencoded";
+    public static final String CONTENT_TYPE_JSON_ENCODED = "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 CODE = "code";
+    public static final String CODE_VERIFIER = "code_verifier";
+    public static final String STATE = "state";
+    public static final String NONCE = "nonce";
+    public static final String REDIRECT_URI = "redirect_uri";
+    public static final String AUTHORIZATION = "authorization";
+    public static final String GRANT_TYPE = "grant_type";
+    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 CODE_CHALLENGE = "code_challenge";
+    public static final String CODE_CHALLENGE_METHOD = "code_challenge_method";
+    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";
+
+    public static final String ACP_SUBSCRIPTION_KEY = "ocp-apim-subscription-key";
+    public static final String X_USER_AGENT = "x-user-agent";
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ImageProperties.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ImageProperties.java
new file mode 100644 (file)
index 0000000..510a89b
--- /dev/null
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.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 = "Default";
+
+    public ImageProperties(String viewport) {
+        this.viewport = viewport;
+    }
+
+    public ImageProperties() {
+    }
+
+    public void failed() {
+        failCounter++;
+    }
+
+    public boolean failLimitReached() {
+        return failCounter > RETRY_COUNTER;
+    }
+
+    @Override
+    public String toString() {
+        return viewport;
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/RemoteServiceUtils.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/RemoteServiceUtils.java
new file mode 100644 (file)
index 0000000..db2bf6e
--- /dev/null
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.utils;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mybmw.internal.handler.RemoteServiceHandler.RemoteService;
+import org.openhab.core.types.CommandOption;
+
+/**
+ * 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::getId, service -> service));
+
+    public static Optional<RemoteService> getRemoteService(final String command) {
+        return Optional.ofNullable(COMMAND_SERVICES.get(command));
+    }
+
+    public static List<CommandOption> getOptions(final boolean isElectric) {
+        return Stream.of(RemoteService.values()).map(service -> new CommandOption(service.getId(), service.getLabel()))
+                .collect(Collectors.toUnmodifiableList());
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/VehicleStatusUtils.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/VehicleStatusUtils.java
new file mode 100644 (file)
index 0000000..752827e
--- /dev/null
@@ -0,0 +1,241 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.utils;
+
+import java.time.ZonedDateTime;
+import java.util.List;
+
+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.mybmw.internal.MyBMWConstants.VehicleType;
+import org.openhab.binding.mybmw.internal.dto.properties.CBS;
+import org.openhab.binding.mybmw.internal.dto.status.FuelIndicator;
+import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
+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.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link VehicleStatusUtils} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class VehicleStatusUtils {
+    public static final Logger LOGGER = LoggerFactory.getLogger(VehicleStatusUtils.class);
+
+    public static State getNextServiceDate(List<CBS> cbsMessageList) {
+        ZonedDateTime farFuture = ZonedDateTime.now().plusYears(100);
+        ZonedDateTime serviceDate = farFuture;
+        for (CBS service : cbsMessageList) {
+            if (service.dateTime != null) {
+                ZonedDateTime d = ZonedDateTime.parse(service.dateTime);
+                if (d.isBefore(serviceDate)) {
+                    serviceDate = d;
+                } // else skip
+            }
+        }
+        if (serviceDate.equals(farFuture)) {
+            return UnDefType.UNDEF;
+        } else {
+            DateTimeType dt = DateTimeType.valueOf(serviceDate.format(Converter.DATE_INPUT_PATTERN));
+            return dt;
+        }
+    }
+
+    public static State getNextServiceMileage(List<CBS> cbsMessageList) {
+        boolean imperial = false;
+        int serviceMileage = Integer.MAX_VALUE;
+        for (CBS service : cbsMessageList) {
+            if (service.distance != null) {
+                if (service.distance.value < serviceMileage) {
+                    serviceMileage = service.distance.value;
+                    imperial = !Constants.KILOMETERS_JSON.equals(service.distance.units);
+                }
+            }
+        }
+        if (serviceMileage != Integer.MAX_VALUE) {
+            if (imperial) {
+                return QuantityType.valueOf(serviceMileage, ImperialUnits.MILE);
+            } else {
+                return QuantityType.valueOf(serviceMileage, Constants.KILOMETRE_UNIT);
+            }
+        } else {
+            return UnDefType.UNDEF;
+        }
+    }
+
+    /**
+     * calculates the mapping of thing type
+     *
+     * @param driveTrain
+     * @param model
+     * @return
+     */
+    public static VehicleType vehicleType(String driveTrain, String model) {
+        if (Constants.BEV.equals(driveTrain)) {
+            if (model.endsWith(Constants.REX_EXTENSION)) {
+                return VehicleType.ELECTRIC_REX;
+            } else {
+                return VehicleType.ELECTRIC;
+            }
+        } else if (Constants.PHEV.equals(driveTrain)) {
+            return VehicleType.PLUGIN_HYBRID;
+        } else if (Constants.CONV.equals(driveTrain) || Constants.HYBRID.equals(driveTrain)) {
+            return VehicleType.CONVENTIONAL;
+        }
+        LOGGER.warn("Unknown Vehicle Type: {} | {}", model, driveTrain);
+        return VehicleType.UNKNOWN;
+    }
+
+    public static @Nullable Unit<Length> getLengthUnit(List<FuelIndicator> indicators) {
+        Unit<Length> ret = null;
+        for (FuelIndicator fuelIndicator : indicators) {
+            String unitAbbrev = fuelIndicator.rangeUnits;
+            switch (unitAbbrev) {
+                case Constants.KM_JSON:
+                    if (ret != null) {
+                        if (!ret.equals(Constants.KILOMETRE_UNIT)) {
+                            LOGGER.debug("Ambigious Unit declarations. Found {} before {}", ret, Constants.KM_JSON);
+                        } // else - fine!
+                    } else {
+                        ret = Constants.KILOMETRE_UNIT;
+                    }
+                    break;
+                case Constants.MI_JSON:
+                    if (ret != null) {
+                        if (!ret.equals(ImperialUnits.MILE)) {
+                            LOGGER.debug("Ambigious Unit declarations. Found {} before {}", ret, Constants.MI_JSON);
+                        } // else - fine!
+                    } else {
+                        ret = ImperialUnits.MILE;
+                    }
+                    break;
+                default:
+                    LOGGER.debug("Cannot evaluate Unit for {}", unitAbbrev);
+                    break;
+            }
+        }
+        return ret;
+    }
+
+    /**
+     * The range values delivered by BMW are quite ambiguous!
+     * - status fuel indicators are missing a unique identifier
+     * - properties ranges delivering wrong values for hybrid and fuel range
+     * - properties ranges are not reflecting mi / km - every time km
+     *
+     * So getRange will try
+     * 1) fuel indicator
+     * 2) ranges from properties, except combined range
+     * 3) take a guess from fuel indicators
+     *
+     * @param unitJson
+     * @param indicators
+     * @return
+     */
+    public static int getRange(String unitJson, Vehicle vehicle) {
+        if (vehicle.status.fuelIndicators.size() == 1) {
+            return Converter.stringToInt(vehicle.status.fuelIndicators.get(0).rangeValue);
+        } else {
+            return guessRange(unitJson, vehicle);
+        }
+    }
+
+    /**
+     * Guesses the range from 3 fuelindicators
+     * - electric range calculation is correct
+     * - for the 2 other values:
+     * -- smaller one is assigned to fuel range
+     * -- bigger one is assigned to hybrid range
+     *
+     * @see VehicleStatusTest testGuessRange
+     *
+     * @param unitJson
+     * @param vehicle
+     * @return
+     */
+    public static int guessRange(String unitJson, Vehicle vehicle) {
+        int electricGuess = Constants.INT_UNDEF;
+        int fuelGuess = Constants.INT_UNDEF;
+        int hybridGuess = Constants.INT_UNDEF;
+        for (FuelIndicator fuelIndicator : vehicle.status.fuelIndicators) {
+            // electric range - this fits 100%
+            if (Constants.UNIT_PRECENT_JSON.equals(fuelIndicator.levelUnits)
+                    && fuelIndicator.chargingStatusType != null) {
+                // found electric
+                electricGuess = Converter.stringToInt(fuelIndicator.rangeValue);
+            } else {
+                if (fuelGuess == Constants.INT_UNDEF) {
+                    // fuel not set? then assume it's fuel
+                    fuelGuess = Converter.stringToInt(fuelIndicator.rangeValue);
+                } else {
+                    // fuel already guessed - take smaller value for fuel, bigger for hybrid
+                    int newGuess = Converter.stringToInt(fuelIndicator.rangeValue);
+                    hybridGuess = Math.max(fuelGuess, newGuess);
+                    fuelGuess = Math.min(fuelGuess, newGuess);
+                }
+            }
+        }
+        switch (unitJson) {
+            case Constants.UNIT_PRECENT_JSON:
+                return electricGuess;
+            case Constants.UNIT_LITER_JSON:
+                return fuelGuess;
+            case Constants.PHEV:
+                return hybridGuess;
+            default:
+                return Constants.INT_UNDEF;
+        }
+    }
+
+    public static String getChargStatus(Vehicle vehicle) {
+        FuelIndicator fi = getElectricFuelIndicator(vehicle);
+        if (fi.chargingStatusType != null) {
+            if (fi.chargingStatusType.equals(Constants.DEFAULT)) {
+                return Constants.NOT_CHARGING_STATE;
+            } else {
+                return fi.chargingStatusType;
+            }
+        }
+        return Constants.UNDEF;
+    }
+
+    public static String getChargeInfo(Vehicle vehicle) {
+        FuelIndicator fi = getElectricFuelIndicator(vehicle);
+        if (fi.chargingStatusType != null && fi.infoLabel != null) {
+            if (fi.chargingStatusType.equals(Constants.CHARGING_STATE)
+                    || fi.chargingStatusType.equals(Constants.PLUGGED_STATE)) {
+                return fi.infoLabel;
+            }
+        }
+        return Constants.HYPHEN;
+    }
+
+    private static FuelIndicator getElectricFuelIndicator(Vehicle vehicle) {
+        for (FuelIndicator fuelIndicator : vehicle.status.fuelIndicators) {
+            if (Constants.UNIT_PRECENT_JSON.equals(fuelIndicator.levelUnits)
+                    && fuelIndicator.chargingStatusType != null) {
+                return fuelIndicator;
+            }
+        }
+        return new FuelIndicator();
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644 (file)
index 0000000..0f484b3
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="mybmw" 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>MyBMW</name>
+       <description>Provides access to your Vehicle Data like MyBMW App</description>
+
+</binding:binding>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/config/bridge-config.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/config/bridge-config.xml
new file mode 100644 (file)
index 0000000..5decb67
--- /dev/null
@@ -0,0 +1,34 @@
+<?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:mybmw:bridge">
+               <parameter name="userName" type="text" required="true">
+                       <label>Username</label>
+                       <description>MyBMW Username</description>
+               </parameter>
+               <parameter name="password" type="text" required="true">
+                       <label>Password</label>
+                       <description>MyBMW 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>
+               <parameter name="language" type="text">
+                       <label>Language Settings</label>
+                       <description>Channel data can be returned in the desired language like en, de, fr ...</description>
+                       <advanced>true</advanced>
+                       <default>AUTODETECT</default>
+               </parameter>
+       </config-description>
+</config-description:config-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/config/thing-config.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/config/thing-config.xml
new file mode 100644 (file)
index 0000000..0b45eb0
--- /dev/null
@@ -0,0 +1,23 @@
+<?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:mybmw: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="vehicleBrand" type="text" required="true">
+                       <label>Brand of the Vehicle</label>
+                       <description>Vehicle brand like BMW or Mini</description>
+                       <advanced>true</advanced>
+               </parameter>
+       </config-description>
+</config-description:config-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties
new file mode 100644 (file)
index 0000000..929220e
--- /dev/null
@@ -0,0 +1,225 @@
+# Binding
+binding.mybmw.name = MyBMW
+binding.mybmw.description = Fahrzeugdaten über die MyBMW App
+
+# bridge types
+thing-type.mybmw.account.label = MyBMW Benutzerkonto
+thing-type.mybmw.account.description = Kontodaten für das BMW Benutzerkonto 
+
+# bridge config
+thing-type.config.mybmw.bridge.userName.label = Benutzername
+thing-type.config.mybmw.bridge.userName.description = Benutzername für die MyBMW App
+thing-type.config.mybmw.bridge.password.label = Passwort
+thing-type.config.mybmw.bridge.password.description = Passwort für die MyBMW App
+thing-type.config.mybmw.bridge.region.label = Region 
+thing-type.config.mybmw.bridge.region.description = Auswahl Ihrer Region 
+thing-type.config.mybmw.bridge.region.option.NORTH_AMERICA = Nordamerika 
+thing-type.config.mybmw.bridge.region.option.CHINA = China 
+thing-type.config.mybmw.bridge.region.option.ROW = Rest der Welt 
+thing-type.config.mybmw.bridge.language.label = Sprachauswahl 
+thing-type.config.mybmw.bridge.language.description = Daten werden für die gewünschte Sprache angefordert (en, de, fr ...)
+
+# thing types
+thing-type.mybmw.bev_rex.label = Elektrofahrzeug mit REX
+thing-type.mybmw.bev_rex.description = Elektrofahrzeug mit Range Extender (bev_rex)
+thing-type.mybmw.bev.label = Elektrofahrzeug
+thing-type.mybmw.bev.description = Batterieelektrisches Fahrzeug (bev)
+thing-type.mybmw.phev.label = Plug-in-Hybrid Elektrofahrzeug
+thing-type.mybmw.phev.description = Konventionelles Fahrzeug mit Elektromotor (phev)
+thing-type.mybmw.conv.label = Konventionelles Fahrzeug
+thing-type.mybmw.conv.description = Konventionelles Benzin/Diesel Fahrzeug (conv)
+
+# thing config
+thing-type.config.mybmw.vehicle.vin.label = Fahrzeug Identifikationsnummer (VIN)
+thing-type.config.mybmw.vehicle.vin.description = VIN des Fahrzeugs
+thing-type.config.mybmw.vehicle.refreshInterval.label = Datenaktualisierung in Minuten
+thing-type.config.mybmw.vehicle.refreshInterval.description = Rate der Datenaktualisierung Ihres Fahrzeugs
+thing-type.config.mybmw.vehicle.vehicleBrand.label = Marke des Fahrzeugs
+thing-type.config.mybmw.vehicle.vehicleBrand.description = Fahrzeugmarke wie z.B. BMW oder Mini.
+
+# Channel Groups
+channel-group-type.mybmw.ev-vehicle-status.label = Fahrzeug Zustand
+channel-group-type.mybmw.ev-vehicle-status.description = Gesamtzustand des Fahrzeugs
+channel-group-type.mybmw.vehicle-status.label = Fahrzeug Zustand
+channel-group-type.mybmw.vehicle-status.description = Gesamtzustand des Fahrzeugs 
+channel-group-type.mybmw.ev-range-values.label = Elektro- Reichweiten und Batterieladung
+channel-group-type.mybmw.ev-range-values.description = Tachostand, Reichweiten und Ladestand des Fahrzeugs
+channel-group-type.mybmw.hybrid-range-values.label = Hybride Reichweiten und Füllstände
+channel-group-type.mybmw.hybrid-range-values.description = Tachostand, Reichweite, Ladezustand und Tankfüllung für hybride Fahrzeuge
+channel-group-type.mybmw.conv-range-values.label = Verbrenner Reichweiten und Füllstände
+channel-group-type.mybmw.conv-range-values.description = Tachostand, Reichweite und Tankfüllung des Fahrzeugs
+channel-group-type.mybmw.door-values.label = Details aller Türen
+channel-group-type.mybmw.door-values.description = Zeigt die Details der Türen und Fenster des Fahrzeugs
+channel-group-type.mybmw.check-control-values.label = Warnungen
+channel-group-type.mybmw.check-control-values.description = Aktuelle Warnungen des Fahrzeugs
+channel-group-type.mybmw.service-values.label = Wartung
+channel-group-type.mybmw.service-values.description = Anstehende Wartungstermine des Fahrzeugs
+channel-group-type.mybmw.location-values.label = Fahrzeug Standort
+channel-group-type.mybmw.location-values.description = Koordinaten und Ausrichtung des Fahrzeugs
+channel-group-type.mybmw.remote-services.label = Fernsteuerung
+channel-group-type.mybmw.remote-services.description = Fernsteuerung des Fahrzeugs 
+channel-group-type.mybmw.profile-values.label = Elektrisches Ladeprofil
+channel-group-type.mybmw.profile-values.description = Zeitplanung der Ladevorgänge
+channel-group-type.mybmw.charge-statistic.label = Elektrische Ladestatistik
+channel-group-type.mybmw.charge-statistic.description = Statistik der Ladevorgänge im Monat
+channel-group-type.mybmw.session-values.label = Elektrische Ladevorgänge
+channel-group-type.mybmw.session-values.description = Liste der letzten Ladevorgänge
+channel-group-type.mybmw.tire-pressures.label = Reifen Luftdruck
+channel-group-type.mybmw.tire-pressures.description = Reifen Luftdruck Ist und Sollwerte
+channel-group-type.mybmw.image-values.label = Fahrzeug Bild
+channel-group-type.mybmw.image-values.description = Bild des Fahrzeug basierend auf der ausgewählten Ansicht
+
+
+
+# Channel Types
+channel-type.mybmw.doors-channel.label = Gesamtzustand der Türen
+channel-type.mybmw.windows-channel.label = Gesamtzustand der Fenster
+channel-type.mybmw.lock-channel.label = Fahrzeug Abgeschlossen
+channel-type.mybmw.next-service-date-channel.label = Nächster Service Termin
+channel-type.mybmw.next-service-mileage-channel.label = Nächster Service in Kilometern
+channel-type.mybmw.check-control-channel.label = Warnung Aktiv
+channel-type.mybmw.plug-connection-channel.label = Ladestecker
+channel-type.mybmw.charging-status-channel.label = Ladezustand
+channel-type.mybmw.charging-info-channel.label = Ladeinformationen
+channel-type.mybmw.motion-channel.label = Fahrzustand
+channel-type.mybmw.last-update-channel.label = Letzte Aktualisierung
+channel-type.mybmw.raw-channel.label = Rohdaten
+
+channel-type.mybmw.driver-front-channel.label = Fahrertür 
+channel-type.mybmw.driver-rear-channel.label = Fahrertür Hinten
+channel-type.mybmw.passenger-front-channel.label = Beifahrertür
+channel-type.mybmw.passenger-rear-channel.label = Beifahrertür Hinten
+channel-type.mybmw.hood-channel.label = Frontklappe
+channel-type.mybmw.trunk-channel.label = Heckklappe
+channel-type.mybmw.window-driver-front-channel.label = Fahrertür Fenster
+channel-type.mybmw.window-driver-rear-channel.label = Fahrertür Hinten Fenster
+channel-type.mybmw.window-passenger-front-channel.label = Beifahrertür Fenster
+channel-type.mybmw.window-passenger-rear-channel.label = Beifahrertür Hinten Fenster
+channel-type.mybmw.window-rear-channel.label = Heckfenster
+channel-type.mybmw.sunroof-channel.label = Schiebedach
+
+channel-type.mybmw.mileage-channel.label = Tachostand
+channel-type.mybmw.range-hybrid-channel.label = Hybride Reichweite
+channel-type.mybmw.range-electric-channel.label = Elektrische Reichweite 
+channel-type.mybmw.soc-channel.label = Batterie Ladestand
+channel-type.mybmw.range-fuel-channel.label = Verbrenner Reichweite
+channel-type.mybmw.remaining-fuel-channel.label = Tankstand
+channel-type.mybmw.range-radius-electric-channel.label = Elektrischer Reichweiten-Radius
+channel-type.mybmw.range-radius-fuel-channel.label =  Verbrenner Reichweiten-Radius
+channel-type.mybmw.range-radius-hybrid-channel.label = Hybrider Reichweiten-Radius
+
+channel-type.mybmw.service-name-channel.label = Service
+channel-type.mybmw.service-details-channel.label = Service Details
+channel-type.mybmw.service-date-channel.label = Service Termin
+channel-type.mybmw.service-mileage-channel.label = Service in Kilometern
+
+channel-type.mybmw.checkcontrol-name-channel.label = Warnung
+channel-type.mybmw.checkcontrol-details-channel.label = Warnung Details
+channel-type.mybmw.checkcontrol-severity-channel.label = Warnung Priorität
+
+channel-type.mybmw.profile-climate-channel.label = Klimatisierung bei Abfahrt
+channel-type.mybmw.profile-mode-channel.label = Ladeprofil
+channel-type.mybmw.profile-mode-channel.command.option.immediateCharging = Sofort Laden
+channel-type.mybmw.profile-mode-channel.command.option.delayedCharging = Ladeverzögerung
+channel-type.mybmw.profile-prefs-channel.label = Ladeprofil Präferenz
+channel-type.mybmw.profile-prefs-channel.command.option.noPreSelection = Keine Präferenz
+channel-type.mybmw.profile-prefs-channel.command.option.chargingWindow = Laden im Zeitfenster
+channel-type.mybmw.profile-control-channel.label = Ladeplan
+channel-type.mybmw.profile-control-channel.description = Ladeplan Auswahl
+channel-type.mybmw.profile-target-channel.label = Ziel Ladezustand
+channel-type.mybmw.profile-target-channel.description = Erwünschter Batterie Ladezustand
+channel-type.mybmw.profile-limit-channel.label = Ladung Limitiert
+channel-type.mybmw.profile-limit-channell.description = Limitierte Ladung aktiviert
+
+
+channel-type.mybmw.window-start-channel.label = Ladefenster Startzeit
+channel-type.mybmw.window-end-channel.label = Ladefenster Endzeit
+channel-type.mybmw.timer1-enabled-channel.label = Zeitprofil 1 - Aktiviert
+channel-type.mybmw.timer1-departure-channel.label = Zeitprofil 1 - Abfahrtszeit
+channel-type.mybmw.timer1-days-channel.label = Zeitprofil 1 - Tage
+channel-type.mybmw.timer1-day-mon-channel.label = Zeitprofil 1 - Montag
+channel-type.mybmw.timer1-day-tue-channel.label = Zeitprofil 1 - Dienstag
+channel-type.mybmw.timer1-day-wed-channel.label = Zeitprofil 1 - Mittwoch
+channel-type.mybmw.timer1-day-thu-channel.label = Zeitprofil 1 - Donnerstag
+channel-type.mybmw.timer1-day-fri-channel.label = Zeitprofil 1 - Freitag
+channel-type.mybmw.timer1-day-sat-channel.label = Zeitprofil 1 - Samstag
+channel-type.mybmw.timer1-day-sun-channel.label = Zeitprofil 1 - Sonntag
+channel-type.mybmw.timer2-enabled-channel.label = Zeitprofil 2 - Aktiviert
+channel-type.mybmw.timer2-departure-channel.label = Zeitprofil 2 - Abfahrtszeit
+channel-type.mybmw.timer2-days-channel.label = Zeitprofil 2 - Tage
+channel-type.mybmw.timer2-day-mon-channel.label = Zeitprofil 2 - Montag
+channel-type.mybmw.timer2-day-tue-channel.label = Zeitprofil 2 - Dienstag
+channel-type.mybmw.timer2-day-wed-channel.label = Zeitprofil 2 - Mittwoch
+channel-type.mybmw.timer2-day-thu-channel.label = Zeitprofil 2 - Donnerstag
+channel-type.mybmw.timer2-day-fri-channel.label = Zeitprofil 2 - Freitag
+channel-type.mybmw.timer2-day-sat-channel.label = Zeitprofil 2 - Samstag
+channel-type.mybmw.timer2-day-sun-channel.label = Zeitprofil 2 - Sonntag
+channel-type.mybmw.timer3-enabled-channel.label = Zeitprofil 3 - Aktiviert
+channel-type.mybmw.timer3-departure-channel.label = Zeitprofil 3 - Abfahrtszeit
+channel-type.mybmw.timer3-days-channel.label = Zeitprofil 3 - Tage
+channel-type.mybmw.timer3-day-mon-channel.label = Zeitprofil 3 - Montag
+channel-type.mybmw.timer3-day-tue-channel.label = Zeitprofil 3 - Dienstag
+channel-type.mybmw.timer3-day-wed-channel.label = Zeitprofil 3 - Mittwoch
+channel-type.mybmw.timer3-day-thu-channel.label = Zeitprofil 3 - Donnerstag
+channel-type.mybmw.timer3-day-fri-channel.label = Zeitprofil 3 - Freitag
+channel-type.mybmw.timer3-day-sat-channel.label = Zeitprofil 3 - Samstag
+channel-type.mybmw.timer3-day-sun-channel.label = Zeitprofil 3 - Sonntag
+channel-type.mybmw.timer4-enabled-channel.label = Zeitprofil 4 - Aktiviert
+channel-type.mybmw.timer4-departure-channel.label = Zeitprofil 4 - Abfahrtszeit
+channel-type.mybmw.timer4-days-channel.label = Zeitprofil 4 - Tage
+channel-type.mybmw.timer4-day-mon-channel.label = Zeitprofil 4 - Montag
+channel-type.mybmw.timer4-day-tue-channel.label = Zeitprofil 4 - Dienstag
+channel-type.mybmw.timer4-day-wed-channel.label = Zeitprofil 4 - Mittwoch
+channel-type.mybmw.timer4-day-thu-channel.label = Zeitprofil 4 - Donnerstag
+channel-type.mybmw.timer4-day-fri-channel.label = Zeitprofil 4 - Freitag
+channel-type.mybmw.timer4-day-sat-channel.label = Zeitprofil 4 - Samstag
+channel-type.mybmw.timer4-day-sun-channel.label = Zeitprofil 4 - Sonntag
+
+# Location
+channel-type.mybmw.gps-channel.label = Koordinaten
+channel-type.mybmw.heading-channel.label = Ausrichtung
+channel-type.mybmw.address-channel.label = Adresse
+
+#Remote
+channel-type.mybmw.remote-command-channel.label = Kommando Auswahl
+channel-type.mybmw.remote-command-channel.command.option.light-flash = Lichthupe Ausführen
+channel-type.mybmw.remote-command-channel.command.option.vehicle-finder = Fahrzeug Lokalisieren
+channel-type.mybmw.remote-command-channel.command.option.door-lock = Fahrzeug Abschließen
+channel-type.mybmw.remote-command-channel.command.option.door-unlock = Fahrzug Aufschließen
+channel-type.mybmw.remote-command-channel.command.option.horn-blow = Hupe Aktivieren
+channel-type.mybmw.remote-command-channel.command.option.climate-now-start = Klimatisierung Ausführen
+channel-type.mybmw.remote-command-channel.command.option.climate-now-stop = Klimatisierung Beenden
+channel-type.mybmw.remote-state-channel.label = Ausführungszustand
+
+# Image
+channel-type.mybmw.png-channel.label = Fahrzeug Bild 
+channel-type.mybmw.image-view-channel.label = Fahrzeug Ansicht 
+channel-type.mybmw.image-view-channel.command.option.VehicleStatus = Front Seitenansicht 
+channel-type.mybmw.image-view-channel.command.option.VehicleInfo = Frontansicht 
+channel-type.mybmw.image-view-channel.command.option.ChargingHistory = Seitenansicht 
+channel-type.mybmw.image-view-channel.command.option.Default = Standard Ansicht
+
+# Charge Sessions
+channel-type.mybmw.session-title-channel.label = Ladevorgang Beschreibung
+channel-type.mybmw.session-subtitle-channel.label = Ladevorgang Details
+channel-type.mybmw.session-energy-channel.label = Energie Geladen 
+channel-type.mybmw.session-issue-channel.label = Ladevorgang Probleme
+channel-type.mybmw.session-status-channel.label = Ladevorgang Zustand
+
+# Charge Statistcis
+channel-type.mybmw.statistic-title-channel.label = Ladestatistik Monat
+channel-type.mybmw.statistic-energy-channel.label =  Energie Geladen Monat
+channel-type.mybmw.statistic-energy-channel.description = Geladene Energie in diesem Monat
+channel-type.mybmw.statistic-sessions-channel.label = Ladevorgänge Monat
+channel-type.mybmw.statistic-sessions-channel.description = Anzahl der Ladevorgänge in diesem Monat
+
+#Tires
+channel-type.mybmw.front-left-current-channel.label  = Reifen Luftdruck Vorne Links
+channel-type.mybmw.front-left-wanted-channel.label  = Reifen Luftdruck Vorne Links Sollwert
+channel-type.mybmw.front-right-current-channel.label  = Reifen Luftdruck Vorne Rechts
+channel-type.mybmw.front-right-wanted-channel.label  = Reifen Luftdruck Vorne Rechts Sollwert
+channel-type.mybmw.rear-left-current-channel.label  = Reifen Luftdruck Hinten Links
+channel-type.mybmw.rear-left-wanted-channel.label  = Reifen Luftdruck Hinten Links Sollwert
+channel-type.mybmw.rear-right-current-channel.label  = Reifen Luftdruck Hinten Rechts
+channel-type.mybmw.rear-right-wanted-channel.label  = Reifen Luftdruck Hinten Rechts Sollwert
+
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/bridge-connected-drive.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/bridge-connected-drive.xml
new file mode 100644 (file)
index 0000000..7363192
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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>MyBMW Account</label>
+               <description>Your BMW account data</description>
+               <config-description-ref uri="thing-type:mybmw:bridge"/>
+       </bridge-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/charge-statistics-channel-groups.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/charge-statistics-channel-groups.xml
new file mode 100644 (file)
index 0000000..caa0b67
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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-statistic">
+               <label>Charging Statistics</label>
+               <description>Charging statistics of current month</description>
+               <channels>
+                       <channel id="title" typeId="statistic-title-channel"/>
+                       <channel id="energy" typeId="statistic-energy-channel"/>
+                       <channel id="sessions" typeId="statistic-sessions-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/charge-statistics-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/charge-statistics-channel-types.xml
new file mode 100644 (file)
index 0000000..806bcb0
--- /dev/null
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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="statistic-title-channel">
+               <item-type>String</item-type>
+               <label>Charge Statistic Month</label>
+       </channel-type>
+       <channel-type id="statistic-energy-channel">
+               <item-type>Number:Energy</item-type>
+               <label>Energy Charged</label>
+               <description>Total energy charged in current month</description>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="statistic-sessions-channel">
+               <item-type>Number</item-type>
+               <label>Charge Sessions</label>
+               <description>Number of charging sessions this month</description>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/check-control-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/check-control-channel-types.xml
new file mode 100644 (file)
index 0000000..48247d5
--- /dev/null
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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-severity-channel">
+               <item-type>String</item-type>
+               <label>Severity Level</label>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/check-control-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/check-control-group.xml
new file mode 100644 (file)
index 0000000..574a6ab
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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>Shows current active CheckControl messages</description>
+               <channels>
+                       <channel id="name" typeId="checkcontrol-name-channel"/>
+                       <channel id="details" typeId="checkcontrol-details-channel"/>
+                       <channel id="severity" typeId="checkcontrol-severity-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/conv-range-channel-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/conv-range-channel-group.xml
new file mode 100644 (file)
index 0000000..c3a7942
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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 and Fuel 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>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/door-status-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/door-status-channel-types.xml
new file mode 100644 (file)
index 0000000..92247ef
--- /dev/null
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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="sunroof-channel">
+               <item-type>String</item-type>
+               <label>Sunroof</label>
+               <state readOnly="true"/>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/doors-status-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/doors-status-group.xml
new file mode 100644 (file)
index 0000000..d508188
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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="sunroof" typeId="sunroof-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/ev-range-channel-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/ev-range-channel-group.xml
new file mode 100644 (file)
index 0000000..046125e
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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>Range and Charge 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="radius-electric" typeId="range-radius-electric-channel"/>
+                       <channel id="soc" typeId="soc-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/ev-vehicle-status-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/ev-vehicle-status-group.xml
new file mode 100644 (file)
index 0000000..39d9ca4
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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>Overall vehicle status</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="plug-connection" typeId="plug-connection-channel"/>
+                       <channel id="charge" typeId="charging-status-channel"/>
+                       <channel id="charge-info" typeId="charging-info-channel"/>
+                       <channel id="motion" typeId="motion-channel"/>
+                       <channel id="last-update" typeId="last-update-channel"/>
+                       <channel id="raw" typeId="raw-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/hybrid-range-channel-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/hybrid-range-channel-group.xml
new file mode 100644 (file)
index 0000000..59b413e
--- /dev/null
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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>Range, Charge / Fuel Data</label>
+               <description>>Provides mileage, remaining fuel and range data for hybrid vehicles</description>
+               <channels>
+                       <channel id="mileage" typeId="mileage-channel"/>
+                       <channel id="electric" typeId="range-electric-channel"/>
+                       <channel id="fuel" typeId="range-fuel-channel"/>
+                       <channel id="hybrid" typeId="range-hybrid-channel"/>
+                       <channel id="radius-electric" typeId="range-radius-electric-channel"/>
+                       <channel id="radius-fuel" typeId="range-radius-fuel-channel"/>
+                       <channel id="radius-hybrid" typeId="range-radius-hybrid-channel"/>
+                       <channel id="soc" typeId="soc-channel"/>
+                       <channel id="remaining-fuel" typeId="remaining-fuel-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/image-channel-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/image-channel-group.xml
new file mode 100644 (file)
index 0000000..944c3ad
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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="view" typeId="image-view-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/image-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/image-channel-types.xml
new file mode 100644 (file)
index 0000000..b758b42
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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="VehicleStatus">Front Side View</option>
+                               <option value="VehicleInfo">Front View</option>
+                               <option value="ChargingHistory">Side View</option>
+                               <option value="Default">Default View</option>
+                       </options>
+               </command>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/location-channel-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/location-channel-group.xml
new file mode 100644 (file)
index 0000000..46cbbcf
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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 vehicle</description>
+               <channels>
+                       <channel id="gps" typeId="gps-channel"/>
+                       <channel id="heading" typeId="heading-channel"/>
+                       <channel id="address" typeId="address-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/location-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/location-channel-types.xml
new file mode 100644 (file)
index 0000000..04fe7b5
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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>
+       <channel-type id="address-channel">
+               <item-type>String</item-type>
+               <label>Address</label>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/profile-channel-groups.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/profile-channel-groups.xml
new file mode 100644 (file)
index 0000000..0de16d9
--- /dev/null
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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="profile-values">
+               <label>Electric Charging Profile</label>
+               <description>Scheduled charging profiles</description>
+               <channels>
+                       <channel id="climate" typeId="profile-climate-channel"/>
+                       <channel id="mode" typeId="profile-mode-channel"/>
+                       <channel id="prefs" typeId="profile-prefs-channel"/>
+                       <channel id="control" typeId="profile-control-channel"/>
+                       <channel id="target" typeId="profile-target-channel"/>
+                       <channel id="limit" typeId="profile-limit-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-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-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-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="timer4-departure" typeId="timer4-departure-channel"/>
+                       <channel id="timer4-day-mon" typeId="timer4-day-mon-channel"/>
+                       <channel id="timer4-day-tue" typeId="timer4-day-tue-channel"/>
+                       <channel id="timer4-day-wed" typeId="timer4-day-wed-channel"/>
+                       <channel id="timer4-day-thu" typeId="timer4-day-thu-channel"/>
+                       <channel id="timer4-day-fri" typeId="timer4-day-fri-channel"/>
+                       <channel id="timer4-day-sat" typeId="timer4-day-sat-channel"/>
+                       <channel id="timer4-day-sun" typeId="timer4-day-sun-channel"/>
+                       <channel id="timer4-enabled" typeId="timer4-enabled-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/profile-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/profile-channel-types.xml
new file mode 100644 (file)
index 0000000..c677f62
--- /dev/null
@@ -0,0 +1,248 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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="immediateCharging">Immediate Charging</option>
+                               <option value="delayedCharging">Use Charging Preference</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="noPreSelection">No Selection</option>
+                               <option value="chargingWindow">Charging Window</option>
+                       </options>
+               </command>
+       </channel-type>
+       <channel-type id="profile-control-channel">
+               <item-type>String</item-type>
+               <label>Charging Plan</label>
+               <description>Charging plan selection</description>
+               <command>
+                       <options>
+                               <option value="weeklyPlanner">Weekly Schedule</option>
+                       </options>
+               </command>
+       </channel-type>
+       <channel-type id="profile-target-channel">
+               <item-type>Number</item-type>
+               <label>SOC Target</label>
+               <description>SOC charging target </description>
+       </channel-type>
+       <channel-type id="profile-limit-channel">
+               <item-type>Switch</item-type>
+               <label>Charging Energy Limited</label>
+               <description>Limited charging activated</description>
+       </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-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-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-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="timer4-departure-channel">
+               <item-type>DateTime</item-type>
+               <label>T4 Departure Time</label>
+               <description>Departure time for regular schedule timer 4</description>
+               <state pattern="%1$tH:%1$tM" readOnly="false"/>
+       </channel-type>
+       <channel-type id="timer4-day-mon-channel">
+               <item-type>Switch</item-type>
+               <label>T4 Monday</label>
+               <description>Monday scheduled for timer 4</description>
+       </channel-type>
+       <channel-type id="timer4-day-tue-channel">
+               <item-type>Switch</item-type>
+               <label>T4 Tuesday</label>
+               <description>Tuesday scheduled for timer 4</description>
+       </channel-type>
+       <channel-type id="timer4-day-wed-channel">
+               <item-type>Switch</item-type>
+               <label>T4 Wednesday</label>
+               <description>Wednesday scheduled for timer 4</description>
+       </channel-type>
+       <channel-type id="timer4-day-thu-channel">
+               <item-type>Switch</item-type>
+               <label>T4 Thursday</label>
+               <description>Thursday scheduled for timer 4</description>
+       </channel-type>
+       <channel-type id="timer4-day-fri-channel">
+               <item-type>Switch</item-type>
+               <label>T4 Friday</label>
+               <description>Friday scheduled for timer 4</description>
+       </channel-type>
+       <channel-type id="timer4-day-sat-channel">
+               <item-type>Switch</item-type>
+               <label>T4 Saturday</label>
+               <description>Saturday scheduled for timer 4</description>
+       </channel-type>
+       <channel-type id="timer4-day-sun-channel">
+               <item-type>Switch</item-type>
+               <label>T4 Sunday</label>
+               <description>Sunday scheduled for timer 4</description>
+       </channel-type>
+       <channel-type id="timer4-enabled-channel">
+               <item-type>Switch</item-type>
+               <label>T4 Enabled</label>
+               <description>Timer 4 enabled</description>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/range-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/range-channel-types.xml
new file mode 100644 (file)
index 0000000..7bcfadd
--- /dev/null
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/remote-services-channel-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/remote-services-channel-group.xml
new file mode 100644 (file)
index 0000000..0079996
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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>Remote control of the vehicle</description>
+               <channels>
+                       <channel id="command" typeId="remote-command-channel"/>
+                       <channel id="state" typeId="remote-state-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/remote-services-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/remote-services-channel-types.xml
new file mode 100644 (file)
index 0000000..b8dd159
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/service-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/service-channel-types.xml
new file mode 100644 (file)
index 0000000..827dd8a
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/service-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/service-group.xml
new file mode 100644 (file)
index 0000000..1aea1e8
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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>Future vehicle 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>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/session-channel-groups.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/session-channel-groups.xml
new file mode 100644 (file)
index 0000000..2f02d15
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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="session-values">
+               <label>Electric Charging Sessions</label>
+               <description>Past charging sessions</description>
+               <channels>
+                       <channel id="title" typeId="session-title-channel"/>
+                       <channel id="subtitle" typeId="session-subtitle-channel"/>
+                       <channel id="energy" typeId="session-energy-channel"/>
+                       <channel id="issue" typeId="session-issue-channel"/>
+                       <channel id="status" typeId="session-status-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/session-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/session-channel-types.xml
new file mode 100644 (file)
index 0000000..6df170c
--- /dev/null
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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="session-title-channel">
+               <item-type>String</item-type>
+               <label>Session Title</label>
+       </channel-type>
+       <channel-type id="session-subtitle-channel">
+               <item-type>String</item-type>
+               <label>Session Details</label>
+       </channel-type>
+       <channel-type id="session-energy-channel">
+               <item-type>String</item-type>
+               <label>Charged Energy in Session</label>
+       </channel-type>
+       <channel-type id="session-issue-channel">
+               <item-type>String</item-type>
+               <label>Issues during Session</label>
+       </channel-type>
+       <channel-type id="session-status-channel">
+               <item-type>String</item-type>
+               <label>Session Status</label>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-bev.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-bev.xml
new file mode 100644 (file)
index 0000000..593b9f5
--- /dev/null
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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="profile" typeId="profile-values"/>
+                       <channel-group id="statistic" typeId="charge-statistic"/>
+                       <channel-group id="session" typeId="session-values"/>
+                       <channel-group id="tires" typeId="tire-pressures"/>
+                       <channel-group id="image" typeId="image-values"/>
+               </channel-groups>
+
+               <representation-property>vin</representation-property>
+
+               <config-description-ref uri="thing-type:mybmw:vehicle"/>
+       </thing-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-bev_rex.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-bev_rex.xml
new file mode 100644 (file)
index 0000000..5e45f36
--- /dev/null
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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="profile" typeId="profile-values"/>
+                       <channel-group id="statistic" typeId="charge-statistic"/>
+                       <channel-group id="session" typeId="session-values"/>
+                       <channel-group id="tires" typeId="tire-pressures"/>
+                       <channel-group id="image" typeId="image-values"/>
+               </channel-groups>
+
+               <representation-property>vin</representation-property>
+
+               <config-description-ref uri="thing-type:mybmw:vehicle"/>
+       </thing-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-conv.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-conv.xml
new file mode 100644 (file)
index 0000000..b95df69
--- /dev/null
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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="tires" typeId="tire-pressures"/>
+                       <channel-group id="image" typeId="image-values"/>
+               </channel-groups>
+
+               <representation-property>vin</representation-property>
+
+               <config-description-ref uri="thing-type:mybmw:vehicle"/>
+       </thing-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-phev.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-phev.xml
new file mode 100644 (file)
index 0000000..35be350
--- /dev/null
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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="profile" typeId="profile-values"/>
+                       <channel-group id="statistic" typeId="charge-statistic"/>
+                       <channel-group id="session" typeId="session-values"/>
+                       <channel-group id="tires" typeId="tire-pressures"/>
+                       <channel-group id="image" typeId="image-values"/>
+               </channel-groups>
+
+               <representation-property>vin</representation-property>
+
+               <config-description-ref uri="thing-type:mybmw:vehicle"/>
+       </thing-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/tires-channel-groups.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/tires-channel-groups.xml
new file mode 100644 (file)
index 0000000..cb2a016
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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="tire-pressures">
+               <label>Tire Pressure</label>
+               <description>Current and wanted pressure for all tires</description>
+               <channels>
+                       <channel id="fl-current" typeId="front-left-current-channel"/>
+                       <channel id="fl-target" typeId="front-left-target-channel"/>
+                       <channel id="fr-current" typeId="front-right-current-channel"/>
+                       <channel id="fr-target" typeId="front-right-target-channel"/>
+                       <channel id="rl-current" typeId="rear-left-current-channel"/>
+                       <channel id="rl-target" typeId="rear-left-target-channel"/>
+                       <channel id="rr-current" typeId="rear-right-current-channel"/>
+                       <channel id="rr-target" typeId="rear-right-target-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/tires-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/tires-channel-types.xml
new file mode 100644 (file)
index 0000000..e403c04
--- /dev/null
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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="front-left-current-channel">
+               <item-type>Number:Pressure</item-type>
+               <label>Tire Pressure Front Left</label>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="front-left-target-channel">
+               <item-type>Number:Pressure</item-type>
+               <label>Tire Pressure Front Left Target</label>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="front-right-current-channel">
+               <item-type>Number:Pressure</item-type>
+               <label>Tire Pressure Front Right</label>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="front-right-target-channel">
+               <item-type>Number:Pressure</item-type>
+               <label>Tire Pressure Front Right Target</label>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="rear-left-current-channel">
+               <item-type>Number:Pressure</item-type>
+               <label>Tire Pressure Rear Left</label>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="rear-left-target-channel">
+               <item-type>Number:Pressure</item-type>
+               <label>Tire Pressure Rear Left Target</label>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="rear-right-current-channel">
+               <item-type>Number:Pressure</item-type>
+               <label>Tire Pressure Rear Right</label>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="rear-right-target-channel">
+               <item-type>Number:Pressure</item-type>
+               <label>Tire Pressure Rear Right Target</label>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml
new file mode 100644 (file)
index 0000000..72087c0
--- /dev/null
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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>
+       </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-info-channel">
+               <item-type>String</item-type>
+               <label>Charging Information</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="plug-connection-channel">
+               <item-type>String</item-type>
+               <label>Plug Connection Status</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="motion-channel">
+               <item-type>Switch</item-type>
+               <label>Motion Status</label>
+               <state 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>
+       <channel-type id="raw-channel" advanced="true">
+               <item-type>String</item-type>
+               <label>Raw Data</label>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-group.xml
new file mode 100644 (file)
index 0000000..0c3982a
--- /dev/null
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mybmw"
+       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>Overall vehicle status</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="motion" typeId="motion-channel"/>
+                       <channel id="last-update" typeId="last-update-channel"/>
+                       <channel id="raw" typeId="raw-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/discovery/DiscoveryTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/discovery/DiscoveryTest.java
new file mode 100644 (file)
index 0000000..0840c86
--- /dev/null
@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.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.mybmw.internal.dto.vehicle.Vehicle;
+import org.openhab.binding.mybmw.internal.handler.MyBMWBridgeHandler;
+import org.openhab.binding.mybmw.internal.util.FileReader;
+import org.openhab.binding.mybmw.internal.utils.Constants;
+import org.openhab.binding.mybmw.internal.utils.Converter;
+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.io.net.http.HttpClientFactory;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ThingUID;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+/**
+ * The {@link DiscoveryTest} Test Discovery Results
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class DiscoveryTest {
+
+    @Test
+    public void testDiscovery() {
+        String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/vehicles.json");
+        Bridge b = mock(Bridge.class);
+        MyBMWBridgeHandler bh = new MyBMWBridgeHandler(b, mock(HttpClientFactory.class), "en");
+        when(b.getUID()).thenReturn(new ThingUID("mybmw", "account", "abc"));
+        VehicleDiscovery discovery = new VehicleDiscovery();
+        discovery.setThingHandler(bh);
+        DiscoveryListener listener = mock(DiscoveryListener.class);
+        discovery.addDiscoveryListener(listener);
+        List<Vehicle> vl = Converter.getVehicleList(content);
+        assertEquals(1, vl.size(), "Vehicles found");
+        ArgumentCaptor<DiscoveryResult> discoveries = ArgumentCaptor.forClass(DiscoveryResult.class);
+        ArgumentCaptor<DiscoveryService> services = ArgumentCaptor.forClass(DiscoveryService.class);
+        bh.onResponse(content);
+        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("mybmw:bev_rex:abc:anonymous", result.getThingUID().getAsString(), "Thing UID");
+    }
+
+    @Test
+    public void testProperties() {
+        String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/vehicles.json");
+        Vehicle vehicle = Converter.getVehicle(Constants.ANONYMOUS, content);
+        String servicesSuppoertedReference = "RemoteHistory;ChargingHistory;ScanAndCharge;DCSContractManagement;BmwCharging;ChargeNowForBusiness;ChargingPlan";
+        String servicesUnsuppoertedReference = "MiniCharging;EvGoCharging;CustomerEsim;CarSharing;EasyCharge";
+        String servicesEnabledReference = "FindCharging;";
+        String servicesDisabledReference = "DataPrivacy;ChargingSettings;ChargingHospitality;ChargingPowerLimit;ChargingTargetSoc;ChargingLoudness";
+        assertEquals(servicesSuppoertedReference,
+                VehicleDiscovery.getServices(vehicle, VehicleDiscovery.SUPPORTED_SUFFIX, true), "Services supported");
+        assertEquals(servicesUnsuppoertedReference,
+                VehicleDiscovery.getServices(vehicle, VehicleDiscovery.SUPPORTED_SUFFIX, false),
+                "Services unsupported");
+
+        String servicesEnabled = VehicleDiscovery.getServices(vehicle, VehicleDiscovery.ENABLED_SUFFIX, true)
+                + Constants.SEMICOLON + VehicleDiscovery.getServices(vehicle, VehicleDiscovery.ENABLE_SUFFIX, true);
+        assertEquals(servicesEnabledReference, servicesEnabled, "Services enabled");
+        String servicesDisabled = VehicleDiscovery.getServices(vehicle, VehicleDiscovery.ENABLED_SUFFIX, false)
+                + Constants.SEMICOLON + VehicleDiscovery.getServices(vehicle, VehicleDiscovery.ENABLE_SUFFIX, false);
+        assertEquals(servicesDisabledReference, servicesDisabled, "Services disabled");
+    }
+
+    @Test
+    public void testAnonymousFingerPrint() {
+        String content = FileReader.readFileInString("src/test/resources/responses/fingerprint-raw.json");
+        String anonymous = Converter.anonymousFingerprint(content);
+        assertFalse(anonymous.contains("ABC45678"), "VIN deleted");
+
+        anonymous = Converter.anonymousFingerprint(Constants.EMPTY);
+        assertEquals(Constants.EMPTY, anonymous, "Equal Fingerprint if Empty");
+
+        anonymous = Converter.anonymousFingerprint(Constants.EMPTY_JSON);
+        assertEquals(Constants.EMPTY_JSON, anonymous, "Equal Fingerprint if Empty JSon");
+    }
+
+    @Test
+    public void testRawVehicleData() {
+        String content = FileReader.readFileInString("src/test/resources/responses/TwoVehicles/two-vehicles.json");
+        String anonymousVehicle = Converter.getRawVehicleContent("anonymous", content);
+        String contentAnon = FileReader.readFileInString("src/test/resources/responses/TwoVehicles/anonymous-raw.json");
+        // remove formatting
+        JsonObject jo = JsonParser.parseString(contentAnon).getAsJsonObject();
+        assertEquals(jo.toString(), anonymousVehicle, "Anonymous VIN raw data");
+        String contentF11 = FileReader.readFileInString("src/test/resources/responses/TwoVehicles/f11-raw.json");
+        String f11Vehicle = Converter.getRawVehicleContent("some_vin_F11", content);
+        jo = JsonParser.parseString(contentF11).getAsJsonObject();
+        assertEquals(jo.toString(), f11Vehicle, "F11 VIN raw data");
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/ChargeProfileTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/ChargeProfileTest.java
new file mode 100644 (file)
index 0000000..7cb52ad
--- /dev/null
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.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.mybmw.internal.dto.charge.ChargeProfile;
+import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
+import org.openhab.binding.mybmw.internal.util.FileReader;
+import org.openhab.binding.mybmw.internal.utils.ChargeProfileWrapper;
+import org.openhab.binding.mybmw.internal.utils.Constants;
+import org.openhab.binding.mybmw.internal.utils.Converter;
+
+/**
+ * The {@link ChargeProfileTest} is testing locale settings
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class ChargeProfileTest {
+
+    @Test
+    public void testWeeklyPlanner() {
+        String json = FileReader
+                .readFileInString("src/test/resources/responses/chargingprofile/weekly-planner-t2-active.json");
+        Vehicle v = Converter.getVehicle(Constants.ANONYMOUS, json);
+        ChargeProfile cp = v.status.chargingProfile;
+        String cpJson = Converter.getGson().toJson(cp);
+        ChargeProfileWrapper cpw = new ChargeProfileWrapper(v.status.chargingProfile);
+        assertEquals(cpJson, cpw.getJson(), "JSON comparison");
+    }
+
+    @Test
+    public void testTwoWeeksPlanner() {
+        String json = FileReader.readFileInString("src/test/resources/responses/chargingprofile/two-weeks-timer.json");
+        Vehicle v = Converter.getVehicle(Constants.ANONYMOUS, json);
+        ChargeProfile cp = v.status.chargingProfile;
+        String cpJson = Converter.getGson().toJson(cp);
+        ChargeProfileWrapper cpw = new ChargeProfileWrapper(v.status.chargingProfile);
+        assertEquals(cpJson, cpw.getJson(), "JSON comparison");
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/ChargeStatisticWrapper.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/ChargeStatisticWrapper.java
new file mode 100644 (file)
index 0000000..3dbde11
--- /dev/null
@@ -0,0 +1,107 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.openhab.binding.mybmw.internal.MyBMWConstants.*;
+
+import java.util.List;
+
+import javax.measure.quantity.Energy;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mybmw.internal.dto.charge.ChargeStatisticsContainer;
+import org.openhab.binding.mybmw.internal.utils.Converter;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.types.State;
+
+/**
+ * The {@link ChargeStatisticWrapper} tests stored fingerprint responses from BMW API
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class ChargeStatisticWrapper {
+    private ChargeStatisticsContainer chargeStatisticContainer;
+
+    public ChargeStatisticWrapper(String content) {
+        ChargeStatisticsContainer fromJson = Converter.getGson().fromJson(content, ChargeStatisticsContainer.class);
+        if (fromJson != null) {
+            chargeStatisticContainer = fromJson;
+        } else {
+            chargeStatisticContainer = new ChargeStatisticsContainer();
+        }
+    }
+
+    /**
+     * 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;
+    }
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    private void checkResult(ChannelUID channelUID, State state) {
+        String cUid = channelUID.getIdWithoutGroup();
+        String gUid = channelUID.getGroupId();
+        StringType st;
+        DecimalType dt;
+        QuantityType<Energy> qte;
+        switch (cUid) {
+            case TITLE:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                switch (gUid) {
+                    case CHANNEL_GROUP_CHARGE_STATISTICS:
+                        assertEquals(chargeStatisticContainer.description, st.toString(), "Statistics name");
+                        break;
+                    default:
+                        assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+                        break;
+                }
+                break;
+            case SESSIONS:
+                assertTrue(state instanceof DecimalType);
+                dt = ((DecimalType) state);
+                assertEquals(chargeStatisticContainer.statistics.numberOfChargingSessions, dt.intValue(),
+                        "Charge Sessions");
+                break;
+            case ENERGY:
+                assertTrue(state instanceof QuantityType);
+                qte = ((QuantityType) state);
+                assertEquals(Units.KILOWATT_HOUR, qte.getUnit(), "kwh");
+                assertEquals(chargeStatisticContainer.statistics.totalEnergyCharged, qte.intValue(), "Energy");
+                break;
+            default:
+                // fail in case of unknown update
+                assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+                break;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/StatusWrapper.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/StatusWrapper.java
new file mode 100644 (file)
index 0000000..dabcd71
--- /dev/null
@@ -0,0 +1,587 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.openhab.binding.mybmw.internal.MyBMWConstants.*;
+
+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.mybmw.internal.MyBMWConstants.VehicleType;
+import org.openhab.binding.mybmw.internal.dto.properties.CBS;
+import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
+import org.openhab.binding.mybmw.internal.utils.Constants;
+import org.openhab.binding.mybmw.internal.utils.Converter;
+import org.openhab.binding.mybmw.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.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link StatusWrapper} tests stored fingerprint responses from BMW API
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class StatusWrapper {
+    private static final Unit<Length> KILOMETRE = Constants.KILOMETRE_UNIT;
+
+    private Vehicle vehicle;
+    private boolean isElectric;
+    private boolean hasFuel;
+    private boolean isHybrid;
+
+    private Map<String, State> specialHandlingMap = new HashMap<String, State>();
+
+    public StatusWrapper(String type, String statusJson) {
+        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;
+        List<Vehicle> vl = Converter.getVehicleList(statusJson);
+        assertEquals(1, vl.size(), "Vehciles found");
+        vehicle = Converter.getConsistentVehcile(vl.get(0));
+    }
+
+    /**
+     * 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;
+        StringType st;
+        StringType wanted;
+        DateTimeType dtt;
+        PointType pt;
+        OnOffType oot;
+        Unit<Length> wantedUnit;
+        switch (cUid) {
+            case MILEAGE:
+                switch (gUid) {
+                    case CHANNEL_GROUP_RANGE:
+                        if (!state.equals(UnDefType.UNDEF)) {
+                            assertTrue(state instanceof QuantityType);
+                            qt = ((QuantityType) state);
+                            if (Constants.KM_JSON.equals(vehicle.status.currentMileage.units)) {
+                                assertEquals(KILOMETRE, qt.getUnit(), "KM");
+                            } else {
+                                assertEquals(ImperialUnits.MILE, qt.getUnit(), "Miles");
+                            }
+                            assertEquals(qt.intValue(), vehicle.status.currentMileage.mileage, "Mileage");
+                        } else {
+                            assertEquals(Constants.INT_UNDEF, vehicle.status.currentMileage.mileage,
+                                    "Mileage undefined");
+                        }
+                        break;
+                    case CHANNEL_GROUP_SERVICE:
+                        State wantedMileage = QuantityType.valueOf(Constants.INT_UNDEF, Constants.KILOMETRE_UNIT);
+                        if (!vehicle.properties.serviceRequired.isEmpty()) {
+                            if (vehicle.properties.serviceRequired.get(0).distance != null) {
+                                if (vehicle.properties.serviceRequired.get(0).distance.units
+                                        .equals(Constants.KILOMETERS_JSON)) {
+                                    wantedMileage = QuantityType.valueOf(
+                                            vehicle.properties.serviceRequired.get(0).distance.value,
+                                            Constants.KILOMETRE_UNIT);
+                                } else {
+                                    wantedMileage = QuantityType.valueOf(
+                                            vehicle.properties.serviceRequired.get(0).distance.value,
+                                            ImperialUnits.MILE);
+                                }
+                            }
+                        }
+                        assertEquals(wantedMileage, state, "Service Mileage");
+                        break;
+                    default:
+                        assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+                        break;
+                }
+                break;
+            case RANGE_ELECTRIC:
+                assertTrue(isElectric, "Is Electric");
+                assertTrue(state instanceof QuantityType);
+                qt = ((QuantityType) state);
+                wantedUnit = VehicleStatusUtils.getLengthUnit(vehicle.status.fuelIndicators);
+                assertEquals(wantedUnit, qt.getUnit());
+                assertEquals(VehicleStatusUtils.getRange(Constants.UNIT_PRECENT_JSON, vehicle), qt.intValue(),
+                        "Range Electric");
+                break;
+            case RANGE_FUEL:
+                assertTrue(hasFuel, "Has Fuel");
+                assertTrue(state instanceof QuantityType);
+                qt = ((QuantityType) state);
+                wantedUnit = VehicleStatusUtils.getLengthUnit(vehicle.status.fuelIndicators);
+                assertEquals(wantedUnit, qt.getUnit());
+                assertEquals(VehicleStatusUtils.getRange(Constants.UNIT_LITER_JSON, vehicle), qt.intValue(),
+                        "Range Combustion");
+                break;
+            case RANGE_HYBRID:
+                assertTrue(isHybrid, "Is Hybrid");
+                assertTrue(state instanceof QuantityType);
+                qt = ((QuantityType) state);
+                wantedUnit = VehicleStatusUtils.getLengthUnit(vehicle.status.fuelIndicators);
+                assertEquals(wantedUnit, qt.getUnit());
+                assertEquals(VehicleStatusUtils.getRange(Constants.PHEV, vehicle), qt.intValue(), "Range Combined");
+                break;
+            case REMAINING_FUEL:
+                assertTrue(hasFuel, "Has Fuel");
+                assertTrue(state instanceof QuantityType);
+                qt = ((QuantityType) state);
+                assertEquals(Units.LITRE, qt.getUnit(), "Liter Unit");
+                assertEquals(vehicle.properties.fuelLevel.value, qt.intValue(), "Fuel Level");
+                break;
+            case SOC:
+                assertTrue(isElectric, "Is Ee<lctric");
+                assertTrue(state instanceof QuantityType);
+                qt = ((QuantityType) state);
+                assertEquals(Units.PERCENT, qt.getUnit(), "Percent");
+                assertEquals(vehicle.properties.chargingState.chargePercentage, qt.intValue(), "Charge Level");
+                break;
+            case LOCK:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                assertEquals(Converter.getLockState(vehicle.properties.areDoorsLocked), st, "Vehicle locked");
+                break;
+            case DOORS:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                assertEquals(Converter.getClosedState(vehicle.properties.areDoorsClosed), st, "Doors Closed");
+                break;
+            case WINDOWS:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                if (specialHandlingMap.containsKey(WINDOWS)) {
+                    assertEquals(specialHandlingMap.get(WINDOWS).toString(), st.toString(), "Windows");
+                } else {
+                    assertEquals(Converter.getClosedState(vehicle.properties.areWindowsClosed), st, "Windows");
+                }
+
+                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(vehicle.status.checkControlMessagesGeneralState, st.toString(), "Check Control");
+                }
+                break;
+            case CHARGE_INFO:
+                assertTrue(isElectric, "Is Electric");
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                assertEquals(Converter.getLocalTime(VehicleStatusUtils.getChargeInfo(vehicle)), st.toString(),
+                        "Charge Info");
+                break;
+            case CHARGE_STATUS:
+                assertTrue(isElectric, "Is Electric");
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                assertEquals(Converter.toTitleCase(VehicleStatusUtils.getChargStatus(vehicle)), st.toString(),
+                        "Charge Status");
+                break;
+            case PLUG_CONNECTION:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                assertEquals(Converter.getConnectionState(vehicle.properties.chargingState.isChargerConnected), st,
+                        "Plug Connection State");
+                break;
+            case LAST_UPDATE:
+                assertTrue(state instanceof DateTimeType);
+                dtt = (DateTimeType) state;
+                DateTimeType expected = DateTimeType
+                        .valueOf(Converter.zonedToLocalDateTime(vehicle.properties.lastUpdatedAt));
+                assertEquals(expected.toString(), dtt.toString(), "Last Update");
+                break;
+            case GPS:
+                assertTrue(state instanceof PointType);
+                pt = (PointType) state;
+                assertNotNull(vehicle.properties.vehicleLocation);
+                assertEquals(
+                        PointType.valueOf(Double.toString(vehicle.properties.vehicleLocation.coordinates.latitude) + ","
+                                + Double.toString(vehicle.properties.vehicleLocation.coordinates.longitude)),
+                        pt, "Coordinates");
+                break;
+            case HEADING:
+                assertTrue(state instanceof QuantityType);
+                qt = ((QuantityType) state);
+                assertEquals(Units.DEGREE_ANGLE, qt.getUnit(), "Angle Unit");
+                assertNotNull(vehicle.properties.vehicleLocation);
+                assertEquals(vehicle.properties.vehicleLocation.heading, qt.intValue(), 0.01, "Heading");
+                break;
+            case RANGE_RADIUS_ELECTRIC:
+                assertTrue(state instanceof QuantityType);
+                assertTrue(isElectric);
+                qt = ((QuantityType) state);
+                wantedUnit = VehicleStatusUtils.getLengthUnit(vehicle.status.fuelIndicators);
+                assertEquals(wantedUnit, qt.getUnit());
+                assertEquals(
+                        Converter.guessRangeRadius(VehicleStatusUtils.getRange(Constants.UNIT_PRECENT_JSON, vehicle)),
+                        qt.intValue(), "Range Radius Electric");
+                break;
+            case RANGE_RADIUS_FUEL:
+                assertTrue(state instanceof QuantityType);
+                assertTrue(hasFuel);
+                qt = (QuantityType) state;
+                wantedUnit = VehicleStatusUtils.getLengthUnit(vehicle.status.fuelIndicators);
+                assertEquals(wantedUnit, qt.getUnit());
+                assertEquals(
+                        Converter.guessRangeRadius(VehicleStatusUtils.getRange(Constants.UNIT_LITER_JSON, vehicle)),
+                        qt.intValue(), "Range Radius Fuel");
+                break;
+            case RANGE_RADIUS_HYBRID:
+                assertTrue(state instanceof QuantityType);
+                assertTrue(isHybrid);
+                qt = (QuantityType) state;
+                wantedUnit = VehicleStatusUtils.getLengthUnit(vehicle.status.fuelIndicators);
+                assertEquals(wantedUnit, qt.getUnit());
+                assertEquals(Converter.guessRangeRadius(VehicleStatusUtils.getRange(Constants.PHEV, vehicle)),
+                        qt.intValue(), "Range Radius Combined");
+                break;
+            case DOOR_DRIVER_FRONT:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                wanted = StringType
+                        .valueOf(Converter.toTitleCase(vehicle.properties.doorsAndWindows.doors.driverFront));
+                assertEquals(wanted.toString(), st.toString(), "Door");
+                break;
+            case DOOR_DRIVER_REAR:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                wanted = StringType.valueOf(Converter.toTitleCase(vehicle.properties.doorsAndWindows.doors.driverRear));
+                assertEquals(wanted.toString(), st.toString(), "Door");
+                break;
+            case DOOR_PASSENGER_FRONT:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                wanted = StringType
+                        .valueOf(Converter.toTitleCase(vehicle.properties.doorsAndWindows.doors.passengerFront));
+                assertEquals(wanted.toString(), st.toString(), "Door");
+                break;
+            case DOOR_PASSENGER_REAR:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                wanted = StringType
+                        .valueOf(Converter.toTitleCase(vehicle.properties.doorsAndWindows.doors.passengerRear));
+                assertEquals(wanted.toString(), st.toString(), "Door");
+                break;
+            case TRUNK:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                wanted = StringType.valueOf(Converter.toTitleCase(vehicle.properties.doorsAndWindows.trunk));
+                assertEquals(wanted.toString(), st.toString(), "Door");
+                break;
+            case HOOD:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                wanted = StringType.valueOf(Converter.toTitleCase(vehicle.properties.doorsAndWindows.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(vehicle.properties.doorsAndWindows.windows.driverFront));
+                assertEquals(wanted.toString(), st.toString(), "Window");
+                break;
+            case WINDOW_DOOR_DRIVER_REAR:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                wanted = StringType
+                        .valueOf(Converter.toTitleCase(vehicle.properties.doorsAndWindows.windows.driverRear));
+                assertEquals(wanted.toString(), st.toString(), "Window");
+                break;
+            case WINDOW_DOOR_PASSENGER_FRONT:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                wanted = StringType
+                        .valueOf(Converter.toTitleCase(vehicle.properties.doorsAndWindows.windows.passengerFront));
+                assertEquals(wanted.toString(), st.toString(), "Window");
+                break;
+            case WINDOW_DOOR_PASSENGER_REAR:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                wanted = StringType
+                        .valueOf(Converter.toTitleCase(vehicle.properties.doorsAndWindows.windows.passengerRear));
+                assertEquals(wanted.toString(), st.toString(), "Window");
+                break;
+            case SUNROOF:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                wanted = StringType.valueOf(Converter.toTitleCase(vehicle.properties.doorsAndWindows.moonroof));
+                assertEquals(wanted.toString(), st.toString(), "Window");
+                break;
+            case SERVICE_DATE:
+                if (!state.equals(UnDefType.UNDEF)) {
+                    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(vehicle.properties.serviceRequired).toString();
+                            DateTimeType expectedDTT = DateTimeType.valueOf(dueDateString);
+                            assertEquals(expectedDTT.toString(), dtt.toString(), "Next Service");
+                        }
+                    } else if (gUid.equals(CHANNEL_GROUP_SERVICE)) {
+                        String dueDateString = vehicle.properties.serviceRequired.get(0).dateTime;
+                        DateTimeType expectedDTT = DateTimeType.valueOf(Converter.zonedToLocalDateTime(dueDateString));
+                        assertEquals(expectedDTT.toString(), dtt.toString(), "First Service Date");
+                    }
+                }
+                break;
+            case SERVICE_MILEAGE:
+                if (!state.equals(UnDefType.UNDEF)) {
+                    qt = ((QuantityType) state);
+                    if (gUid.contentEquals(CHANNEL_GROUP_STATUS)) {
+                        QuantityType<Length> wantedQt = (QuantityType) VehicleStatusUtils
+                                .getNextServiceMileage(vehicle.properties.serviceRequired);
+                        assertEquals(wantedQt.getUnit(), qt.getUnit(), "Next Service Miles");
+                        assertEquals(wantedQt.intValue(), qt.intValue(), "Mileage");
+                    } else if (gUid.equals(CHANNEL_GROUP_SERVICE)) {
+                        assertEquals(vehicle.properties.serviceRequired.get(0).distance.units, qt.getUnit(),
+                                "First Service Unit");
+                        assertEquals(vehicle.properties.serviceRequired.get(0).distance.value, qt.intValue(),
+                                "First Service Mileage");
+                    }
+                }
+                break;
+            case NAME:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                switch (gUid) {
+                    case CHANNEL_GROUP_SERVICE:
+                        wanted = StringType.valueOf(Constants.NO_ENTRIES);
+                        if (!vehicle.properties.serviceRequired.isEmpty()) {
+                            wanted = StringType
+                                    .valueOf(Converter.toTitleCase(vehicle.properties.serviceRequired.get(0).type));
+                        }
+                        assertEquals(wanted.toString(), st.toString(), "Service Name");
+                        break;
+                    case CHANNEL_GROUP_CHECK_CONTROL:
+                        wanted = StringType.valueOf(Constants.NO_ENTRIES);
+                        if (!vehicle.status.checkControlMessages.isEmpty()) {
+                            wanted = StringType.valueOf(vehicle.status.checkControlMessages.get(0).title);
+                        }
+                        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 (!vehicle.properties.serviceRequired.isEmpty()) {
+                            wanted = StringType
+                                    .valueOf(Converter.toTitleCase(vehicle.properties.serviceRequired.get(0).type));
+                        }
+                        assertEquals(wanted.toString(), st.toString(), "Service Details");
+                        break;
+                    case CHANNEL_GROUP_CHECK_CONTROL:
+                        wanted = StringType.valueOf(Constants.NO_ENTRIES);
+                        if (!vehicle.status.checkControlMessages.isEmpty()) {
+                            wanted = StringType.valueOf(vehicle.status.checkControlMessages.get(0).longDescription);
+                        }
+                        assertEquals(wanted.toString(), st.toString(), "CheckControl Details");
+                        break;
+                    default:
+                        assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+                        break;
+                }
+                break;
+            case SEVERITY:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                wanted = StringType.valueOf(Constants.NO_ENTRIES);
+                if (!vehicle.status.checkControlMessages.isEmpty()) {
+                    wanted = StringType.valueOf(vehicle.status.checkControlMessages.get(0).state);
+                }
+                assertEquals(wanted.toString(), st.toString(), "CheckControl Details");
+                break;
+            case DATE:
+                if (state.equals(UnDefType.UNDEF)) {
+                    for (CBS serviceEntry : vehicle.properties.serviceRequired) {
+                        assertTrue(serviceEntry.dateTime == null, "No Service Date available");
+                    }
+                } else {
+                    assertTrue(state instanceof DateTimeType);
+                    dtt = (DateTimeType) state;
+                    switch (gUid) {
+                        case CHANNEL_GROUP_SERVICE:
+                            String dueDateString = vehicle.properties.serviceRequired.get(0).dateTime;
+                            DateTimeType expectedDTT = DateTimeType
+                                    .valueOf(Converter.zonedToLocalDateTime(dueDateString));
+                            assertEquals(expectedDTT.toString(), dtt.toString(), "ServiceSate");
+                            break;
+                        default:
+                            assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+                            break;
+                    }
+                }
+                break;
+            case FRONT_LEFT_CURRENT:
+                if (vehicle.properties.tires != null) {
+                    assertTrue(state instanceof QuantityType);
+                    qt = (QuantityType) state;
+                    assertEquals(vehicle.properties.tires.frontLeft.status.currentPressure / 100, qt.doubleValue(),
+                            "Fron Left Current");
+                } else {
+                    assertTrue(state.equals(UnDefType.UNDEF));
+                }
+                break;
+            case FRONT_LEFT_TARGET:
+                if (vehicle.properties.tires != null) {
+                    assertTrue(state instanceof QuantityType);
+                    qt = (QuantityType) state;
+                    assertEquals(vehicle.properties.tires.frontLeft.status.targetPressure / 100, qt.doubleValue(),
+                            "Fron Left Current");
+                } else {
+                    assertTrue(state.equals(UnDefType.UNDEF));
+                }
+                break;
+            case FRONT_RIGHT_CURRENT:
+                if (vehicle.properties.tires != null) {
+                    assertTrue(state instanceof QuantityType);
+                    qt = (QuantityType) state;
+                    assertEquals(vehicle.properties.tires.frontRight.status.currentPressure / 100, qt.doubleValue(),
+                            "Fron Left Current");
+                } else {
+                    assertTrue(state.equals(UnDefType.UNDEF));
+                }
+                break;
+            case FRONT_RIGHT_TARGET:
+                if (vehicle.properties.tires != null) {
+                    assertTrue(state instanceof QuantityType);
+                    qt = (QuantityType) state;
+                    assertEquals(vehicle.properties.tires.frontRight.status.targetPressure / 100, qt.doubleValue(),
+                            "Fron Left Current");
+                } else {
+                    assertTrue(state.equals(UnDefType.UNDEF));
+                }
+                break;
+            case REAR_LEFT_CURRENT:
+                if (vehicle.properties.tires != null) {
+                    assertTrue(state instanceof QuantityType);
+                    qt = (QuantityType) state;
+                    assertEquals(vehicle.properties.tires.rearLeft.status.currentPressure / 100, qt.doubleValue(),
+                            "Fron Left Current");
+                } else {
+                    assertTrue(state.equals(UnDefType.UNDEF));
+                }
+                break;
+            case REAR_LEFT_TARGET:
+                if (vehicle.properties.tires != null) {
+                    assertTrue(state instanceof QuantityType);
+                    qt = (QuantityType) state;
+                    assertEquals(vehicle.properties.tires.rearLeft.status.targetPressure / 100, qt.doubleValue(),
+                            "Fron Left Current");
+                } else {
+                    assertTrue(state.equals(UnDefType.UNDEF));
+                }
+                break;
+            case REAR_RIGHT_CURRENT:
+                if (vehicle.properties.tires != null) {
+                    assertTrue(state instanceof QuantityType);
+                    qt = (QuantityType) state;
+                    assertEquals(vehicle.properties.tires.rearRight.status.currentPressure / 100, qt.doubleValue(),
+                            "Fron Left Current");
+                } else {
+                    assertTrue(state.equals(UnDefType.UNDEF));
+                }
+                break;
+            case REAR_RIGHT_TARGET:
+                if (vehicle.properties.tires != null) {
+                    assertTrue(state instanceof QuantityType);
+                    qt = (QuantityType) state;
+                    assertEquals(vehicle.properties.tires.rearRight.status.targetPressure / 100, qt.doubleValue(),
+                            "Fron Left Current");
+                } else {
+                    assertTrue(state.equals(UnDefType.UNDEF));
+                }
+                break;
+            case MOTION:
+                assertTrue(state instanceof OnOffType);
+                oot = (OnOffType) state;
+                if (vehicle.properties.inMotion) {
+                    assertEquals(oot.toFullString(), OnOffType.ON.toFullString(), "Vehicle Driving");
+                } else {
+                    assertEquals(oot.toFullString(), OnOffType.OFF.toFullString(), "Vehicle Stationary");
+                }
+                break;
+            case ADDRESS:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                assertEquals(st.toFullString(), vehicle.properties.vehicleLocation.address.formatted,
+                        "Location Address");
+                break;
+            case RAW:
+                // don't assert raw channel
+                break;
+            default:
+                if (!gUid.equals(CHANNEL_GROUP_CHARGE_PROFILE)) {
+                    // fail in case of unknown update
+                    assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+                }
+                break;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/VehiclePropertiesTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/VehiclePropertiesTest.java
new file mode 100644 (file)
index 0000000..1ce3050
--- /dev/null
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.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.mybmw.internal.dto.vehicle.Vehicle;
+import org.openhab.binding.mybmw.internal.util.FileReader;
+import org.openhab.binding.mybmw.internal.utils.Constants;
+import org.openhab.binding.mybmw.internal.utils.Converter;
+import org.openhab.binding.mybmw.internal.utils.RemoteServiceUtils;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.types.CommandOption;
+
+/**
+ * The {@link VehiclePropertiesTest} tests stored fingerprint responses from BMW API
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class VehiclePropertiesTest {
+
+    @Test
+    public void testUserInfo() {
+        String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/vehicles.json");
+        List<Vehicle> vl = Converter.getVehicleList(content);
+
+        assertEquals(1, vl.size(), "Number of Vehicles");
+        Vehicle v = vl.get(0);
+        assertEquals(Constants.ANONYMOUS, v.vin, "VIN");
+        assertEquals("i3 94 (+ REX)", v.model, "Model");
+        assertEquals(Constants.BEV, v.driveTrain, "DriveTrain");
+        assertEquals("BMW", v.brand, "Brand");
+        assertEquals(2017, v.year, "Year of Construction");
+    }
+
+    @Test
+    public void testChannelUID() {
+        ThingTypeUID thingTypePHEV = new ThingTypeUID("mybmw", "plugin-hybrid-vehicle");
+        assertEquals("plugin-hybrid-vehicle", thingTypePHEV.getId(), "Vehicle Type");
+    }
+
+    @Test
+    public void testRemoteServiceOptions() {
+        String commandReference = "[CommandOption [command=light-flash, label=Flash Lights], CommandOption [command=vehicle-finder, label=Vehicle Finder], CommandOption [command=door-lock, label=Door Lock], CommandOption [command=door-unlock, label=Door Unlock], CommandOption [command=horn-blow, label=Horn Blow], CommandOption [command=climate-now-start, label=Start Climate], CommandOption [command=climate-now-stop, label=Stop Climate]]";
+        List<CommandOption> l = RemoteServiceUtils.getOptions(true);
+        assertEquals(commandReference, l.toString(), "Commad Options");
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/VehicleStatusTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/VehicleStatusTest.java
new file mode 100644 (file)
index 0000000..80f8f1e
--- /dev/null
@@ -0,0 +1,115 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.dto;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
+import org.openhab.binding.mybmw.internal.util.FileReader;
+import org.openhab.binding.mybmw.internal.utils.Constants;
+import org.openhab.binding.mybmw.internal.utils.Converter;
+import org.openhab.binding.mybmw.internal.utils.VehicleStatusUtils;
+import org.openhab.core.library.types.DateTimeType;
+
+/**
+ * The {@link VehicleStatusTest} tests stored fingerprint responses from BMW API
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class VehicleStatusTest {
+
+    @Test
+    public void testServiceDate() {
+        String json = FileReader.readFileInString("src/test/resources/responses/I01_REX/vehicles.json");
+        Vehicle v = Converter.getVehicle(Constants.ANONYMOUS, json);
+        assertEquals(Constants.ANONYMOUS, v.vin, "VIN check");
+        assertEquals("2023-11-01T00:00",
+                ((DateTimeType) VehicleStatusUtils.getNextServiceDate(v.properties.serviceRequired)).getZonedDateTime()
+                        .toLocalDateTime().toString(),
+                "Service Date");
+
+        ZonedDateTime zdt = ZonedDateTime.parse("2021-12-21T16:46:02Z").withZoneSameInstant(ZoneId.systemDefault());
+        LocalDateTime ldt = zdt.toLocalDateTime();
+        assertEquals(ldt.format(Converter.DATE_INPUT_PATTERN),
+                Converter.zonedToLocalDateTime(v.properties.lastUpdatedAt), "Last update time");
+    }
+
+    @Test
+    public void testBevRexValues() {
+        String vehiclesJSON = FileReader.readFileInString("src/test/resources/responses/I01_REX/vehicles.json");
+        List<Vehicle> vehicleList = Converter.getVehicleList(vehiclesJSON);
+        assertEquals(1, vehicleList.size(), "Vehicles found");
+        Vehicle v = vehicleList.get(0);
+        assertEquals("BMW", v.brand, "Car brand");
+        assertEquals(true, v.properties.areDoorsClosed, "Doors Closed");
+        assertEquals(76, v.properties.electricRange.distance.value, "Electric Range");
+        assertEquals(6.789, v.properties.vehicleLocation.coordinates.longitude, 0.1, "Location lon");
+        assertEquals("immediateCharging", v.status.chargingProfile.chargingMode, "Charging Mode");
+        assertEquals(2, v.status.chargingProfile.getTimerId(2).id, "Timer ID");
+        assertEquals("[sunday]", v.status.chargingProfile.getTimerId(2).timerWeekDays.toString(), "Timer Weekdays");
+    }
+
+    @Test
+    public void testGuessRange() {
+        /**
+         * PHEV G01
+         * fuelIndicator electric unit = %
+         * fuelIndicator fuel unit = l
+         * fuelIndicator hybrid unit = null
+         */
+        String vehiclesJSON = FileReader.readFileInString("src/test/resources/responses/G01/vehicles_v2_bmw_0.json");
+        List<Vehicle> vehicleList = Converter.getVehicleList(vehiclesJSON);
+        assertEquals(1, vehicleList.size(), "Vehicles found");
+        Vehicle vehicle = vehicleList.get(0);
+        assertEquals(2, VehicleStatusUtils.getRange(Constants.UNIT_PRECENT_JSON, vehicle), "Electric Range");
+        assertEquals(437, VehicleStatusUtils.getRange(Constants.UNIT_LITER_JSON, vehicle), "Fuel Range");
+        assertEquals(439, VehicleStatusUtils.getRange(Constants.PHEV, vehicle), "Hybrid Range");
+
+        /**
+         * Electric REX I01
+         * fuelIndicator electric unit = %
+         * fuelIndicator fuel unit = null
+         * fuelIndicator hybrid unit = null
+         */
+        vehiclesJSON = FileReader.readFileInString("src/test/resources/responses/I01_REX/vehicles_v2_bmw_0.json");
+        vehicleList = Converter.getVehicleList(vehiclesJSON);
+        assertEquals(1, vehicleList.size(), "Vehicles found");
+        vehicle = vehicleList.get(0);
+        assertEquals(164, VehicleStatusUtils.getRange(Constants.UNIT_PRECENT_JSON, vehicle), "Electric Range");
+        assertEquals(64, VehicleStatusUtils.getRange(Constants.UNIT_LITER_JSON, vehicle), "Fuel Range");
+        assertEquals(228, VehicleStatusUtils.getRange(Constants.PHEV, vehicle), "Hybrid Range");
+
+        /**
+         * PHEV G05
+         * fuelIndicator electric unit = %
+         * fuelIndicator fuel unit = %
+         * fuelIndicator hybrid unit = null
+         */
+        vehiclesJSON = FileReader.readFileInString("src/test/resources/responses/G05/vehicles_v2_bmw_0.json");
+        vehicleList = Converter.getVehicleList(vehiclesJSON);
+        assertEquals(1, vehicleList.size(), "Vehicles found");
+        vehicle = vehicleList.get(0);
+        assertEquals(48, VehicleStatusUtils.getRange(Constants.UNIT_PRECENT_JSON, vehicle), "Electric Range");
+        assertEquals(418, VehicleStatusUtils.getRange(Constants.UNIT_LITER_JSON, vehicle), "Fuel Range");
+        assertEquals(466, VehicleStatusUtils.getRange(Constants.PHEV, vehicle), "Hybrid Range");
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/AuthTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/AuthTest.java
new file mode 100644 (file)
index 0000000..b735562
--- /dev/null
@@ -0,0 +1,423 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.*;
+
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.MessageDigest;
+import java.security.PublicKey;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
+
+import javax.crypto.Cipher;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+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.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.util.MultiMap;
+import org.eclipse.jetty.util.UrlEncoded;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mybmw.internal.MyBMWConfiguration;
+import org.openhab.binding.mybmw.internal.dto.auth.AuthQueryResponse;
+import org.openhab.binding.mybmw.internal.dto.auth.AuthResponse;
+import org.openhab.binding.mybmw.internal.dto.auth.ChinaPublicKeyResponse;
+import org.openhab.binding.mybmw.internal.dto.auth.ChinaTokenExpiration;
+import org.openhab.binding.mybmw.internal.dto.auth.ChinaTokenResponse;
+import org.openhab.binding.mybmw.internal.util.FileReader;
+import org.openhab.binding.mybmw.internal.utils.BimmerConstants;
+import org.openhab.binding.mybmw.internal.utils.Constants;
+import org.openhab.binding.mybmw.internal.utils.Converter;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link AuthTest} test authorization flow
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+class AuthTest {
+    private final Logger logger = LoggerFactory.getLogger(AuthTest.class);
+
+    void testAuth() {
+        String user = "usr";
+        String pwd = "pwd";
+
+        SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
+        HttpClient authHttpClient = new HttpClient(sslContextFactory);
+        try {
+            authHttpClient.start();
+            Request firstRequest = authHttpClient
+                    .newRequest("https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
+                            + "/eadrax-ucs/v1/presentation/oauth/config");
+            firstRequest.header("ocp-apim-subscription-key",
+                    BimmerConstants.OCP_APIM_KEYS.get(BimmerConstants.REGION_ROW));
+            firstRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
+
+            ContentResponse firstResponse = firstRequest.send();
+            logger.info(firstResponse.getContentAsString());
+            AuthQueryResponse aqr = Converter.getGson().fromJson(firstResponse.getContentAsString(),
+                    AuthQueryResponse.class);
+
+            // String verifier_bytes = RandomStringUtils.randomAlphanumeric(64);
+            String verifierBytes = Converter.getRandomString(64);
+            String codeVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(verifierBytes.getBytes());
+
+            MessageDigest digest = MessageDigest.getInstance("SHA-256");
+            byte[] hash = digest.digest(codeVerifier.getBytes(StandardCharsets.UTF_8));
+            String codeChallenge = Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
+
+            // String state_bytes = RandomStringUtils.randomAlphanumeric(16);
+            String stateBytes = Converter.getRandomString(16);
+            String state = Base64.getUrlEncoder().withoutPadding().encodeToString(stateBytes.getBytes());
+
+            String authUrl = aqr.gcdmBaseUrl + BimmerConstants.OAUTH_ENDPOINT;
+            logger.info(authUrl);
+            Request loginRequest = authHttpClient.POST(authUrl);
+            loginRequest.header("Content-Type", "application/x-www-form-urlencoded");
+
+            MultiMap<String> baseParams = new MultiMap<String>();
+            baseParams.put("client_id", aqr.clientId);
+            baseParams.put("response_type", "code");
+            baseParams.put("redirect_uri", aqr.returnUrl);
+            baseParams.put("state", state);
+            baseParams.put("nonce", "login_nonce");
+            baseParams.put("scope", String.join(" ", aqr.scopes));
+            baseParams.put("code_challenge", codeChallenge);
+            baseParams.put("code_challenge_method", "S256");
+
+            MultiMap<String> loginParams = new MultiMap<String>(baseParams);
+            loginParams.put("grant_type", "authorization_code");
+            loginParams.put("username", user);
+            loginParams.put("password", pwd);
+            loginRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
+                    UrlEncoded.encode(loginParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
+            ContentResponse secondResonse = loginRequest.send();
+            logger.info(secondResonse.getContentAsString());
+            String authCode = getAuthCode(secondResonse.getContentAsString());
+            logger.info(authCode);
+
+            MultiMap<String> authParams = new MultiMap<String>(baseParams);
+            authParams.put("authorization", authCode);
+            Request authRequest = authHttpClient.POST(authUrl).followRedirects(false);
+            authRequest.header("Content-Type", "application/x-www-form-urlencoded");
+            authRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
+                    UrlEncoded.encode(authParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
+            ContentResponse authResponse = authRequest.send();
+            logger.info("{}", authResponse.getHeaders());
+            logger.info("Response " + authResponse.getHeaders().get(HttpHeader.LOCATION));
+            String code = AuthTest.codeFromUrl(authResponse.getHeaders().get(HttpHeader.LOCATION));
+            logger.info("Code " + code);
+            logger.info("Auth");
+
+            logger.info(aqr.tokenEndpoint);
+            // AuthenticationStore authenticationStore = authHttpClient.getAuthenticationStore();
+            // BasicAuthentication ba = new BasicAuthentication(new URI(aqr.tokenEndpoint), Authentication.ANY_REALM,
+            // aqr.clientId, aqr.clientSecret);
+            // authenticationStore.addAuthentication(ba);
+            Request codeRequest = authHttpClient.POST(aqr.tokenEndpoint);
+            String basicAuth = "Basic "
+                    + Base64.getUrlEncoder().encodeToString((aqr.clientId + ":" + aqr.clientSecret).getBytes());
+            logger.info(basicAuth);
+            codeRequest.header("Content-Type", "application/x-www-form-urlencoded");
+            codeRequest.header(AUTHORIZATION, basicAuth);
+
+            MultiMap<String> codeParams = new MultiMap<String>();
+            codeParams.put("code", code);
+            codeParams.put("code_verifier", codeVerifier);
+            codeParams.put("redirect_uri", aqr.returnUrl);
+            codeParams.put("grant_type", "authorization_code");
+            codeRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
+                    UrlEncoded.encode(codeParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
+            ContentResponse codeResponse = codeRequest.send();
+            logger.info(codeResponse.getContentAsString());
+            AuthResponse ar = Converter.getGson().fromJson(codeResponse.getContentAsString(), AuthResponse.class);
+            Token t = new Token();
+            t.setType(ar.tokenType);
+            t.setToken(ar.accessToken);
+            t.setExpiration(ar.expiresIn);
+            logger.info(t.getBearerToken());
+
+            /**
+             * REQUEST CONTENT
+             */
+            HttpClient apiHttpClient = new HttpClient(sslContextFactory);
+            apiHttpClient.start();
+
+            MultiMap<String> vehicleParams = new MultiMap<String>();
+            vehicleParams.put("tireGuardMode", "ENABLED");
+            vehicleParams.put("appDateTime", Long.toString(System.currentTimeMillis()));
+            vehicleParams.put("apptimezone", "60");
+            // vehicleRequest.param("tireGuardMode", "ENABLED");
+            // vehicleRequest.param("appDateTime", Long.toString(System.currentTimeMillis()));
+            // vehicleRequest.param("apptimezone", "60.0");
+            // vehicleRequest.
+            // // logger.info(vehicleParams);
+            // vehicleRequest.content(new StringContentProvider(CONTENT_TYPE_JSON_ENCODED, vehicleParams.toString(),
+            // StandardCharsets.UTF_8));
+            // logger.info(vehicleRequest.getHeaders());
+            String params = UrlEncoded.encode(vehicleParams, StandardCharsets.UTF_8, false);
+
+            String vehicleUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
+                    + "/eadrax-vcs/v1/vehicles";
+            logger.info(vehicleUrl);
+            Request vehicleRequest = apiHttpClient.newRequest(vehicleUrl + "?" + params);//
+            // .param("tireGuardMode", "ENABLED")
+            // .param("appDateTime", Long.toString(System.currentTimeMillis())).param("apptimezone", "60.0");
+            // vehicleRequest.header("Content-Type", "application/x-www-form-urlencoded");
+            vehicleRequest.header(HttpHeader.AUTHORIZATION, t.getBearerToken());
+            vehicleRequest.header("accept", "application/json");
+            vehicleRequest.header("accept-language", "de");
+            vehicleRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
+            // vehicleRequest.header(HttpHeader.CONTENT_TYPE, "application/x-www-form-urlencoded");
+            // vehicleRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
+            // UrlEncoded.encode(vehicleParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
+            ContentResponse vehicleResponse = vehicleRequest.send();
+            logger.info("Vehicle Status {} {}", vehicleResponse.getStatus(), vehicleResponse.getContentAsString());
+
+            /**
+             * CHARGE STATISTICS
+             */
+            MultiMap<String> chargeStatisticsParams = new MultiMap<String>();
+            chargeStatisticsParams.put("vin", "WBY1Z81040V905639");
+            chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime());
+            params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false);
+
+            String chargeStatisticsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
+                    + "/eadrax-chs/v1/charging-statistics";
+            Request chargeStatisticsRequest = apiHttpClient.newRequest(chargeStatisticsUrl)
+                    .param("vin", "WBY1Z81040V905639").param("currentDate", Converter.getCurrentISOTime());
+            logger.info("{}", chargeStatisticsUrl);
+            // vehicleRequest.header("Content-Type", "application/x-www-form-urlencoded");
+            chargeStatisticsRequest.header(HttpHeader.AUTHORIZATION, t.getBearerToken());
+            chargeStatisticsRequest.header("accept", "application/json");
+            chargeStatisticsRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
+            chargeStatisticsRequest.header("accept-language", "de");
+
+            // MultiMap<String> chargeStatisticsParams = new MultiMap<String>();
+            // chargeStatisticsParams.put("vin", "WBY1Z81040V905639");
+            // chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime());
+            //
+            // params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false);
+            logger.info("{}", params);
+            chargeStatisticsRequest
+                    .content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, params, StandardCharsets.UTF_8));
+
+            ContentResponse chargeStatisticsResponse = chargeStatisticsRequest.send();
+            logger.info("{}", chargeStatisticsResponse.getStatus());
+            logger.info("{}", chargeStatisticsResponse.getReason());
+            logger.info("{}", chargeStatisticsResponse.getContentAsString());
+
+            /**
+             * CHARGE SESSIONS
+             */
+            MultiMap<String> chargeSessionsParams = new MultiMap<String>();
+            chargeSessionsParams.put("vin", "WBY1Z81040V905639");
+            chargeSessionsParams.put("maxResults", "40");
+            chargeSessionsParams.put("include_date_picker", "true");
+
+            params = UrlEncoded.encode(chargeSessionsParams, StandardCharsets.UTF_8, false);
+
+            String chargeSessionsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
+                    + "/eadrax-chs/v1/charging-sessions";
+            Request chargeSessionsRequest = apiHttpClient.newRequest(chargeSessionsUrl + "?" + params);
+            logger.info("{}", chargeSessionsUrl);
+            // vehicleRequest.header("Content-Type", "application/x-www-form-urlencoded");
+            chargeSessionsRequest.header(HttpHeader.AUTHORIZATION, t.getBearerToken());
+            chargeSessionsRequest.header("accept", "application/json");
+            chargeSessionsRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
+            chargeSessionsRequest.header("accept-language", "de");
+
+            // MultiMap<String> chargeStatisticsParams = new MultiMap<String>();
+            // chargeStatisticsParams.put("vin", "WBY1Z81040V905639");
+            // chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime());
+            //
+            // params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false);
+            logger.info("{}", params);
+            // chargeStatisticsRequest
+            // .content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, params, StandardCharsets.UTF_8));
+
+            ContentResponse chargeSessionsResponse = chargeSessionsRequest.send();
+            logger.info("{}", chargeSessionsResponse.getStatus());
+            logger.info("{}", chargeSessionsResponse.getReason());
+            logger.info("{}", chargeSessionsResponse.getContentAsString());
+
+            String chargingControlUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
+                    + "/eadrax-vrccs/v2/presentation/remote-commands/WBY1Z81040V905639/charging-control";
+            Request chargingControlRequest = apiHttpClient.POST(chargingControlUrl);
+            logger.info("{}", chargingControlUrl);
+            // vehicleRequest.header("Content-Type", "application/x-www-form-urlencoded");
+            chargingControlRequest.header(HttpHeader.AUTHORIZATION, t.getBearerToken());
+            chargingControlRequest.header("accept", "application/json");
+            chargingControlRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
+            chargingControlRequest.header("accept-language", "de");
+            chargingControlRequest.header("Content-Type", CONTENT_TYPE_JSON_ENCODED);
+
+            // String content = FileReader.readFileInString("src/test/resources/responses/charging-profile.json");
+            // logger.info("{}", content);
+            // ChargeProfile cpc = Converter.getGson().fromJson(content, ChargeProfile.class);
+            // String contentTranfsorm = Converter.getGson().toJson(cpc);
+            // String profile = "{chargingProfile:" + contentTranfsorm + "}";
+            // logger.info("{}", profile);
+            // chargingControlRequest
+            // .content(new StringContentProvider(CONTENT_TYPE_JSON_ENCODED, params, StandardCharsets.UTF_8));
+
+            // chargeStatisticsParams.put("vin", "WBY1Z81040V905639");
+            // chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime());
+            //
+            // params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false);
+
+            // ContentResponse chargingControlResponse = chargingControlRequest.send();
+            // logger.info("{}", chargingControlResponse.getStatus());
+            // logger.info("{}", chargingControlResponse.getReason());
+            // logger.info("{}", chargingControlResponse.getContentAsString());
+
+        } catch (Exception e) {
+            logger.error("{}", e.getMessage());
+        }
+    }
+
+    private String getAuthCode(String response) {
+        String[] keys = response.split("&");
+        for (int i = 0; i < keys.length; i++) {
+            if (keys[i].startsWith(AUTHORIZATION)) {
+                String authCode = keys[i].split("=")[1];
+                authCode = authCode.split("\"")[0];
+                return authCode;
+            }
+        }
+        return Constants.EMPTY;
+    }
+
+    public static String codeFromUrl(String encodedUrl) {
+        final MultiMap<String> tokenMap = new MultiMap<String>();
+        UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII);
+        final StringBuilder codeFound = new StringBuilder();
+        tokenMap.forEach((key, value) -> {
+            if (value.size() > 0) {
+                String val = value.get(0);
+                if (key.endsWith(CODE)) {
+                    codeFound.append(val.toString());
+                }
+            }
+        });
+        return codeFound.toString();
+    }
+
+    @Test
+    public void testJWTDeserialze() {
+        String accessTokenResponseStr = FileReader
+                .readFileInString("src/test/resources/responses/auth/auth_cn_login_pwd.json");
+        ChinaTokenResponse cat = Converter.getGson().fromJson(accessTokenResponseStr, ChinaTokenResponse.class);
+
+        // https://www.baeldung.com/java-jwt-token-decode
+        String token = cat.data.accessToken;
+        String[] chunks = token.split("\\.");
+        String tokenJwtDecodeStr = new String(Base64.getUrlDecoder().decode(chunks[1]));
+        ChinaTokenExpiration cte = Converter.getGson().fromJson(tokenJwtDecodeStr, ChinaTokenExpiration.class);
+        Token t = new Token();
+        t.setToken(token);
+        t.setType(cat.data.tokenType);
+        t.setExpirationTotal(cte.exp);
+        assertEquals(
+                "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJEVU1NWSQxJEEkMTYzNzcwNzkxNjc4MiIsIm5iZiI6MTYzNzcwNzkxNiwiZXhwIjoxNjM3NzExMjE2LCJpYXQiOjE2Mzc3MDc5MTZ9.hpi-P97W68g7avGwu9dcBRapIsaG4F8MwOdPHe6PuTA",
+                t.getBearerToken(), "Token");
+    }
+
+    public void testChina() {
+        SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
+        HttpClient authHttpClient = new HttpClient(sslContextFactory);
+        try {
+            authHttpClient.start();
+            HttpClientFactory mockHCF = mock(HttpClientFactory.class);
+            when(mockHCF.getCommonHttpClient()).thenReturn(authHttpClient);
+            MyBMWConfiguration config = new MyBMWConfiguration();
+            config.region = BimmerConstants.REGION_CHINA;
+            config.userName = "Hello User";
+            config.password = "Hello Password";
+            MyBMWProxy bmwProxy = new MyBMWProxy(mockHCF, config);
+            bmwProxy.updateTokenChina();
+        } catch (Exception e) {
+            logger.warn("Exception: " + e.getMessage());
+        }
+    }
+
+    @Test
+    public void testPublicKey() {
+        String publicKeyResponseStr = FileReader.readFileInString("src/test/resources/responses/auth/china-key.json");
+        ChinaPublicKeyResponse pkr = Converter.getGson().fromJson(publicKeyResponseStr, ChinaPublicKeyResponse.class);
+        String publicKeyStr = pkr.data.value;
+        String publicKeyPEM = publicKeyStr.replace("-----BEGIN PUBLIC KEY-----", "")
+                .replaceAll(System.lineSeparator(), "").replace("-----END PUBLIC KEY-----", "").replace("\\r", "")
+                .replace("\\n", "").trim();
+        byte[] encoded = Base64.getDecoder().decode(publicKeyPEM);
+        X509EncodedKeySpec spec = new X509EncodedKeySpec(encoded);
+        KeyFactory kf;
+        try {
+            kf = KeyFactory.getInstance("RSA");
+            PublicKey publicKey = kf.generatePublic(spec);
+            // https://www.thexcoders.net/java-ciphers-rsa/
+            Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
+            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
+            byte[] encryptedBytes = cipher.doFinal("Hello World".getBytes());
+            String encodedPassword = Base64.getEncoder().encodeToString(encryptedBytes);
+            logger.info(encodedPassword);
+        } catch (Exception e) {
+            assertTrue(false, "Excpetion: " + e.getMessage());
+        }
+    }
+
+    public void testChinaToken() {
+        SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
+        HttpClient authHttpClient = new HttpClient(sslContextFactory);
+        try {
+            authHttpClient.start();
+            String url = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_CHINA)
+                    + BimmerConstants.CHINA_PUBLIC_KEY;
+            Request oauthQueryRequest = authHttpClient.newRequest(url);
+            oauthQueryRequest.header(X_USER_AGENT, BimmerConstants.USER_AGENT_BMW);
+
+            ContentResponse publicKeyResponse = oauthQueryRequest.send();
+            ChinaPublicKeyResponse pkr = Converter.getGson().fromJson(publicKeyResponse.getContentAsString(),
+                    ChinaPublicKeyResponse.class);
+            // https://stackoverflow.com/questions/11410770/load-rsa-public-key-from-file
+
+            String publicKeyStr = pkr.data.value;
+            // String cleanPublicKeyStr = pkr.data.value.replaceAll("(\r\n|\n)", Constants.EMPTY);
+            String publicKeyPEM = publicKeyStr.replace("-----BEGIN PUBLIC KEY-----", "")
+                    .replaceAll(System.lineSeparator(), "").replace("-----END PUBLIC KEY-----", "");
+            byte[] encoded = Base64.getDecoder().decode(publicKeyPEM);
+            X509EncodedKeySpec spec = new X509EncodedKeySpec(encoded);
+
+            KeyFactory kf = KeyFactory.getInstance("RSA");
+            PublicKey publicKey = kf.generatePublic(spec);
+            // https://www.thexcoders.net/java-ciphers-rsa/
+            Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
+            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
+            byte[] encryptedBytes = cipher.doFinal("Hello World".getBytes());
+            String encodedPassword = Base64.getEncoder().encodeToString(encryptedBytes);
+            logger.info(encodedPassword);
+        } catch (Exception e) {
+            assertTrue(false, "Excpetion: " + e.getMessage());
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/ChargeStatisticsTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/ChargeStatisticsTest.java
new file mode 100644 (file)
index 0000000..0770483
--- /dev/null
@@ -0,0 +1,139 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.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.mybmw.internal.MyBMWConstants.VehicleType;
+import org.openhab.binding.mybmw.internal.VehicleConfiguration;
+import org.openhab.binding.mybmw.internal.dto.ChargeStatisticWrapper;
+import org.openhab.binding.mybmw.internal.util.FileReader;
+import org.openhab.binding.mybmw.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 ChargeStatisticsTest} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class ChargeStatisticsTest {
+    private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
+
+    private static final int EXPECTED_UPDATE_COUNT = 3;
+
+    @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"));
+        MyBMWCommandOptionProvider cop = mock(MyBMWCommandOptionProvider.class);
+        cch = new VehicleHandler(thing, cop, type);
+        VehicleConfiguration vc = new VehicleConfiguration();
+        vc.vin = Constants.ANONYMOUS;
+        Optional<VehicleConfiguration> ovc = Optional.of(vc);
+        cch.configuration = ovc;
+        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.chargeStatisticsCallback.onResponse(statusContent);
+        verify(tc, times(callbacksExpected)).stateUpdated(channelCaptor.capture(), stateCaptor.capture());
+        allChannels = channelCaptor.getAllValues();
+        allStates = stateCaptor.getAllValues();
+
+        assertNotNull(driveTrain);
+        ChargeStatisticWrapper checker = new ChargeStatisticWrapper(statusContent);
+        trace();
+        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 testI01REX() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.ELECTRIC_REX.toString(), false);
+        String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/charge-statistics-de.json");
+        assertTrue(testVehicle(content, EXPECTED_UPDATE_COUNT, Optional.empty()));
+    }
+
+    @Test
+    public void testG21() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.PLUGIN_HYBRID.toString(), false);
+        String content = FileReader.readFileInString("src/test/resources/responses/G21/charging-statistics_0.json");
+        assertTrue(testVehicle(content, EXPECTED_UPDATE_COUNT, Optional.empty()));
+    }
+
+    @Test
+    public void testG30() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.PLUGIN_HYBRID.toString(), false);
+        String content = FileReader.readFileInString("src/test/resources/responses/G30/charging-statistics_0.json");
+        assertTrue(testVehicle(content, EXPECTED_UPDATE_COUNT, 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/charging-statistics_0.json");
+        assertTrue(testVehicle(content, EXPECTED_UPDATE_COUNT, Optional.empty()));
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/ConfigurationTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/ConfigurationTest.java
new file mode 100644 (file)
index 0000000..9e81dc9
--- /dev/null
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.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.mybmw.internal.MyBMWConfiguration;
+import org.openhab.binding.mybmw.internal.utils.BimmerConstants;
+
+/**
+ * The {@link ConfigurationTest} test different configurations
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class ConfigurationTest {
+
+    @Test
+    public void testAuthServerMap() {
+        MyBMWConfiguration cdc = new MyBMWConfiguration();
+        assertFalse(MyBMWBridgeHandler.checkConfiguration(cdc));
+        cdc.userName = "a";
+        assertFalse(MyBMWBridgeHandler.checkConfiguration(cdc));
+        cdc.password = "b";
+        assertFalse(MyBMWBridgeHandler.checkConfiguration(cdc));
+        cdc.region = "c";
+        assertFalse(MyBMWBridgeHandler.checkConfiguration(cdc));
+        cdc.region = BimmerConstants.REGION_NORTH_AMERICA;
+        assertTrue(MyBMWBridgeHandler.checkConfiguration(cdc));
+        cdc.region = BimmerConstants.REGION_ROW;
+        assertTrue(MyBMWBridgeHandler.checkConfiguration(cdc));
+        cdc.region = BimmerConstants.REGION_CHINA;
+        assertTrue(MyBMWBridgeHandler.checkConfiguration(cdc));
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/ErrorResponseTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/ErrorResponseTest.java
new file mode 100644 (file)
index 0000000..50db2bd
--- /dev/null
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.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"));
+        MyBMWCommandOptionProvider cop = mock(MyBMWCommandOptionProvider.class);
+        cch = new VehicleHandler(thing, cop, type);
+        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);
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/SimulationTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/SimulationTest.java
new file mode 100644 (file)
index 0000000..29b58f1
--- /dev/null
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.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.mybmw.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");
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/VehicleTests.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/VehicleTests.java
new file mode 100644 (file)
index 0000000..80363a6
--- /dev/null
@@ -0,0 +1,340 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.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.mybmw.internal.MyBMWConstants.VehicleType;
+import org.openhab.binding.mybmw.internal.VehicleConfiguration;
+import org.openhab.binding.mybmw.internal.dto.StatusWrapper;
+import org.openhab.binding.mybmw.internal.util.FileReader;
+import org.openhab.binding.mybmw.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 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 = 12;
+    private static final int STATUS_CONV = 9;
+    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 = 11;
+    private static final int CHECK_EMPTY = 3;
+    private static final int CHECK_AVAILABLE = 3;
+    private static final int SERVICE_AVAILABLE = 3;
+    private static final int SERVICE_EMPTY = 3;
+    private static final int LOCATION = 3;
+    private static final int CHARGE_PROFILE = 44;
+    private static final int TIRES = 8;
+
+    @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;
+
+    /**
+     * Prepare environment for Vehicle Status Updates
+     */
+    public void setup(String type, String vin) {
+        driveTrain = type;
+        Thing thing = mock(Thing.class);
+        when(thing.getUID()).thenReturn(new ThingUID("testbinding", "test"));
+        MyBMWCommandOptionProvider cop = mock(MyBMWCommandOptionProvider.class);
+        cch = new VehicleHandler(thing, cop, type);
+        VehicleConfiguration vc = new VehicleConfiguration();
+        vc.vin = vin;
+        Optional<VehicleConfiguration> ovc = Optional.of(vc);
+        cch.configuration = ovc;
+        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, 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++) {
+            // change to info for debugging channel updates
+            logger.debug("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
+     * Channel testbinding::test:status#lock Locked
+     * Channel testbinding::test:status#service-date 2023-11-01T00:00:00.000+0100
+     * Channel testbinding::test:status#check-control No Issues
+     * Channel testbinding::test:status#last-update 2021-12-21T16:46:02.000+0100
+     * Channel testbinding::test:status#doors Closed
+     * Channel testbinding::test:status#windows Closed
+     * Channel testbinding::test:status#plug-connection Not connected
+     * Channel testbinding::test:status#charge Not Charging
+     * Channel testbinding::test:status#charge-type Not Available
+     * Channel testbinding::test:range#electric 76 km
+     * Channel testbinding::test:range#radius-electric 60.800000000000004 km
+     * Channel testbinding::test:range#fuel 31 km
+     * Channel testbinding::test:range#radius-fuel 24.8 km
+     * Channel testbinding::test:range#hybrid 31 km
+     * Channel testbinding::test:range#radius-hybrid 24.8 km
+     * Channel testbinding::test:range#mileage 31537 km
+     * Channel testbinding::test:range#soc 74 %
+     * Channel testbinding::test:range#remaining-fuel 4 l
+     * 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#win-driver-front Closed
+     * Channel testbinding::test:doors#win-driver-rear Undef
+     * Channel testbinding::test:doors#win-passenger-front Closed
+     * Channel testbinding::test:doors#win-passenger-rear Undef
+     * Channel testbinding::test:doors#sunroof Closed
+     * Channel testbinding::test:location#gps 1.2345,6.789
+     * Channel testbinding::test:location#heading 222 °
+     * Channel testbinding::test:service#name Brake Fluid
+     * Channel testbinding::test:service#date 2023-11-01T00:00:00.000+0100
+     * Channel testbinding::test:profile#prefs Chargingwindow
+     * Channel testbinding::test:profile#mode Immediatecharging
+     * Channel testbinding::test:profile#control Weeklyplanner
+     * Channel testbinding::test:profile#target 100
+     * Channel testbinding::test:profile#limit OFF
+     * Channel testbinding::test:profile#climate OFF
+     * Channel testbinding::test:profile#window-start 1970-01-01T11:00:00.000+0100
+     * Channel testbinding::test:profile#window-end 1970-01-01T14:30:00.000+0100
+     * Channel testbinding::test:profile#timer1-departure 1970-01-01T16:00:00.000+0100
+     * Channel testbinding::test:profile#timer1-enabled OFF
+     * Channel testbinding::test:profile#timer1-day-mon ON
+     * Channel testbinding::test:profile#timer1-day-tue ON
+     * Channel testbinding::test:profile#timer1-day-wed ON
+     * Channel testbinding::test:profile#timer1-day-thu ON
+     * Channel testbinding::test:profile#timer1-day-fri ON
+     * Channel testbinding::test:profile#timer1-day-sat ON
+     * Channel testbinding::test:profile#timer1-day-sun ON
+     * Channel testbinding::test:profile#timer2-departure 1970-01-01T12:02:00.000+0100
+     * Channel testbinding::test:profile#timer2-enabled ON
+     * Channel testbinding::test:profile#timer2-day-mon OFF
+     * Channel testbinding::test:profile#timer2-day-tue OFF
+     * Channel testbinding::test:profile#timer2-day-wed OFF
+     * Channel testbinding::test:profile#timer2-day-thu OFF
+     * Channel testbinding::test:profile#timer2-day-fri OFF
+     * Channel testbinding::test:profile#timer2-day-sat OFF
+     * Channel testbinding::test:profile#timer2-day-sun ON
+     * Channel testbinding::test:profile#timer3-departure 1970-01-01T13:03:00.000+0100
+     * Channel testbinding::test:profile#timer3-enabled OFF
+     * Channel testbinding::test:profile#timer3-day-mon OFF
+     * Channel testbinding::test:profile#timer3-day-tue OFF
+     * Channel testbinding::test:profile#timer3-day-wed OFF
+     * Channel testbinding::test:profile#timer3-day-thu OFF
+     * Channel testbinding::test:profile#timer3-day-fri OFF
+     * Channel testbinding::test:profile#timer3-day-sat ON
+     * Channel testbinding::test:profile#timer3-day-sun OFF
+     * Channel testbinding::test:profile#timer4-departure 1970-01-01T12:02:00.000+0100
+     * Channel testbinding::test:profile#timer4-enabled OFF
+     * Channel testbinding::test:profile#timer4-day-mon OFF
+     * Channel testbinding::test:profile#timer4-day-tue OFF
+     * Channel testbinding::test:profile#timer4-day-wed OFF
+     * Channel testbinding::test:profile#timer4-day-thu OFF
+     * Channel testbinding::test:profile#timer4-day-fri OFF
+     * Channel testbinding::test:profile#timer4-day-sat OFF
+     * Channel testbinding::test:profile#timer4-day-sun ON
+     */
+    @Test
+    public void testI01Rex() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.ELECTRIC_REX.toString(), Constants.ANONYMOUS);
+        String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/vehicles.json");
+        assertTrue(testVehicle(content, STATUS_ELECTRIC + RANGE_HYBRID + DOORS + LOCATION + SERVICE_AVAILABLE
+                + CHECK_EMPTY + CHARGE_PROFILE + TIRES, Optional.empty()));
+    }
+
+    @Test
+    public void testF11() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.CONVENTIONAL.toString(), "some_vin_F11");
+        String content = FileReader.readFileInString("src/test/resources/responses/F11/vehicles_v2_bmw_0.json");
+        assertTrue(testVehicle(content,
+                STATUS_CONV + DOORS + RANGE_CONV + SERVICE_AVAILABLE + CHECK_EMPTY + LOCATION + TIRES,
+                Optional.empty()));
+    }
+
+    @Test
+    public void testF31() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.CONVENTIONAL.toString(), "some_vin_F31");
+        String content = FileReader.readFileInString("src/test/resources/responses/F31/vehicles_v2_bmw_0.json");
+        assertTrue(testVehicle(content,
+                STATUS_CONV + DOORS + RANGE_CONV + SERVICE_AVAILABLE + CHECK_EMPTY + LOCATION + TIRES,
+                Optional.empty()));
+    }
+
+    @Test
+    public void testF44() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.CONVENTIONAL.toString(), "some_vin_F44");
+        String content = FileReader.readFileInString("src/test/resources/responses/F44/vehicles_v2_bmw_0.json");
+        assertTrue(testVehicle(content,
+                STATUS_CONV + DOORS + RANGE_CONV + LOCATION + SERVICE_EMPTY + CHECK_EMPTY + TIRES, Optional.empty()));
+    }
+
+    @Test
+    public void testF45() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.PLUGIN_HYBRID.toString(), "some_vin_F45");
+        String content = FileReader.readFileInString("src/test/resources/responses/F45/vehicles_v2_bmw_0.json");
+        assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_HYBRID + SERVICE_AVAILABLE + CHECK_EMPTY
+                + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty()));
+    }
+
+    @Test
+    public void testF48() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.CONVENTIONAL.toString(), "some_vin_F48");
+        String content = FileReader.readFileInString("src/test/resources/responses/F48/vehicles_v2_bmw_0.json");
+        assertTrue(testVehicle(content,
+                STATUS_CONV + DOORS + RANGE_CONV + SERVICE_AVAILABLE + CHECK_AVAILABLE + LOCATION + TIRES,
+                Optional.empty()));
+    }
+
+    @Test
+    public void testG01() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.PLUGIN_HYBRID.toString(), "some_vin_G01");
+        String content = FileReader.readFileInString("src/test/resources/responses/G01/vehicles_v2_bmw_0.json");
+        assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_HYBRID + SERVICE_AVAILABLE + CHECK_EMPTY
+                + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty()));
+    }
+
+    @Test
+    public void testG05() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.PLUGIN_HYBRID.toString(), "some_vin_G05");
+        String content = FileReader.readFileInString("src/test/resources/responses/G05/vehicles_v2_bmw_0.json");
+        assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_HYBRID + SERVICE_AVAILABLE + CHECK_EMPTY
+                + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty()));
+    }
+
+    @Test
+    public void testG08() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.ELECTRIC.toString(), "some_vin_G08");
+        String content = FileReader.readFileInString("src/test/resources/responses/G08/vehicles_v2_bmw_0.json");
+        assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_ELECTRIC + SERVICE_AVAILABLE + CHECK_EMPTY
+                + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty()));
+    }
+
+    @Test
+    public void testG21() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.PLUGIN_HYBRID.toString(), "some_vin_G21");
+        String content = FileReader.readFileInString("src/test/resources/responses/G21/vehicles_v2_bmw_0.json");
+        assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_HYBRID + SERVICE_AVAILABLE + CHECK_EMPTY
+                + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty()));
+    }
+
+    @Test
+    public void testG30() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.PLUGIN_HYBRID.toString(), "some_vin_G30");
+        String content = FileReader.readFileInString("src/test/resources/responses/G30/vehicles_v2_bmw_0.json");
+        assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_HYBRID + SERVICE_AVAILABLE + CHECK_EMPTY
+                + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty()));
+    }
+
+    @Test
+    public void testI01NoRex() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.ELECTRIC.toString(), "some_vin_I01_NOREX");
+        String content = FileReader.readFileInString("src/test/resources/responses/I01_NOREX/vehicles_v2_bmw_0.json");
+        assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_ELECTRIC + SERVICE_AVAILABLE + CHECK_EMPTY
+                + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty()));
+    }
+
+    @Test
+    public void test530e() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.PLUGIN_HYBRID.toString(), "anonymous");
+        String content = FileReader.readFileInString("src/test/resources/responses/530e/vehicles.json");
+        assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_HYBRID + SERVICE_AVAILABLE + CHECK_EMPTY
+                + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty()));
+    }
+
+    @Test
+    public void test340i() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.MILD_HYBRID.toString(), "anonymous");
+        String content = FileReader.readFileInString("src/test/resources/responses/G21/340i.json");
+        assertTrue(testVehicle(content, 38, Optional.empty()));
+        // assertTrue(testVehicle(content,
+        // STATUS_CONV + DOORS + RANGE_CONV + SERVICE_AVAILABLE + CHECK_EMPTY + LOCATION + TIRES,
+        // Optional.empty()));
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/util/FileReader.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/util/FileReader.java
new file mode 100644 (file)
index 0000000..40b4054
--- /dev/null
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+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.mybmw.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
+            assertEquals(filename, Constants.EMPTY, "Read failute " + filename);
+        }
+        return Constants.UNDEF;
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/util/LocaleTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/util/LocaleTest.java
new file mode 100644 (file)
index 0000000..ff63759
--- /dev/null
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mybmw.internal.util;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mybmw.internal.utils.Converter;
+
+/**
+ * The {@link LocaleTest} is testing locale settings
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class LocaleTest {
+
+    @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");
+    }
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/530e/vehicles.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/530e/vehicles.json
new file mode 100644 (file)
index 0000000..2d05f4f
--- /dev/null
@@ -0,0 +1,379 @@
+[{
+       "vin": "anonymous",
+       "model": "530e",
+       "year": 2021,
+       "brand": "BMW",
+       "headUnit": "MGU",
+       "isLscSupported": true,
+       "driveTrain": "PLUGIN_HYBRID",
+       "puStep": "1121",
+       "iStep": "S15A-21-11-530",
+       "telematicsUnit": "ATM02",
+       "hmiVersion": "id7",
+       "bodyType": "G30",
+       "a4aType": "NOT_SUPPORTED",
+       "exFactoryPUStep": "1121",
+       "exFactoryILevel": "S15A-21-11-530",
+       "capabilities": {
+               "isRemoteServicesBookingRequired": false,
+               "isRemoteServicesActivationRequired": false,
+               "isRemoteHistorySupported": true,
+               "canRemoteHistoryBeDeleted": false,
+               "isChargingHistorySupported": true,
+               "isScanAndChargeSupported": true,
+               "isDCSContractManagementSupported": true,
+               "isBmwChargingSupported": true,
+               "isMiniChargingSupported": false,
+               "isChargeNowForBusinessSupported": true,
+               "isDataPrivacyEnabled": false,
+               "isChargingPlanSupported": true,
+               "isChargingPowerLimitEnable": false,
+               "isChargingTargetSocEnable": false,
+               "isChargingLoudnessEnable": false,
+               "isChargingSettingsEnabled": false,
+               "isChargingHospitalityEnabled": false,
+               "isEvGoChargingSupported": false,
+               "isFindChargingEnabled": true,
+               "isCustomerEsimSupported": false,
+               "isCarSharingSupported": false,
+               "isEasyChargeSupported": false,
+               "lock": {
+                       "isEnabled": true,
+                       "isPinAuthenticationRequired": false,
+                       "executionMessage": "Jetzt Ihr Fahrzeug verriegeln? Remote-Funktionen können einige Sekunden dauern."
+               },
+               "unlock": {
+                       "isEnabled": true,
+                       "isPinAuthenticationRequired": true,
+                       "executionMessage": "Jetzt Ihr Fahrzeug entriegeln? Remote-Funktionen können einige Sekunden dauern."
+               },
+               "lights": {
+                       "isEnabled": true,
+                       "isPinAuthenticationRequired": false,
+                       "executionMessage": "Jetzt Scheinwerfer aufleuchten lassen? Remote-Funktionen können einige Sekunden dauern."
+               },
+               "horn": {
+                       "isEnabled": true,
+                       "isPinAuthenticationRequired": false,
+                       "executionMessage": "Hupen ist in vielen Ländern nur in bestimmten Situationen erlaubt. Die Verantwortung für den Einsatz und die Einhaltung der jeweils geltenden Bestimmungen liegt allein bei Ihnen als Nutzer. \n\nJetzt hupen? Remote-Funktionen können einige Sekunden dauern."
+               },
+               "vehicleFinder": {
+                       "isEnabled": true,
+                       "isPinAuthenticationRequired": false,
+                       "executionMessage": "Jetzt Ihr Fahrzeug finden? Remote-Funktionen können einige Sekunden dauern."
+               },
+               "sendPoi": {
+                       "isEnabled": true,
+                       "isPinAuthenticationRequired": false,
+                       "executionMessage": "Jetzt POI senden? Remote-Funktionen können einige Sekunden dauern."
+               },
+               "climateNow": {
+                       "isEnabled": true,
+                       "isPinAuthenticationRequired": false,
+                       "executionMessage": "Jetzt belüften? Remote-Funktionen können einige Sekunden dauern."
+               }
+       },
+       "properties": {
+               "lastUpdatedAt": "2022-01-06T15:59:07Z",
+               "inMotion": false,
+               "areDoorsLocked": false,
+               "originCountryISO": "DE",
+               "areDoorsClosed": true,
+               "areDoorsOpen": false,
+               "areWindowsClosed": true,
+               "doorsAndWindows": {
+                       "doors": {
+                               "driverFront": "CLOSED",
+                               "driverRear": "CLOSED",
+                               "passengerFront": "CLOSED",
+                               "passengerRear": "CLOSED"
+                       },
+                       "windows": {
+                               "driverFront": "CLOSED",
+                               "driverRear": "CLOSED",
+                               "passengerFront": "CLOSED",
+                               "passengerRear": "CLOSED"
+                       },
+                       "trunk": "CLOSED",
+                       "hood": "CLOSED"
+               },
+               "isServiceRequired": false,
+               "fuelLevel": {
+                       "value": 18,
+                       "units": "LITERS"
+               },
+               "chargingState": {
+                       "chargePercentage": 60,
+                       "state": "CHARGING",
+                       "type": "NOT_AVAILABLE",
+                       "isChargerConnected": true
+               },
+               "combustionRange": {
+                       "chargePercentage": 0,
+                       "distance": {
+                               "value": 251,
+                               "units": "KILOMETERS"
+                       }
+               },
+               "combinedRange": {
+                       "chargePercentage": 0,
+                       "distance": {
+                               "value": 251,
+                               "units": "KILOMETERS"
+                       }
+               },
+               "electricRange": {
+                       "chargePercentage": 0,
+                       "distance": {
+                               "value": 27,
+                               "units": "KILOMETERS"
+                       }
+               },
+               "electricRangeAndStatus": {
+                       "chargePercentage": 60,
+                       "distance": {
+                               "value": 27,
+                               "units": "KILOMETERS"
+                       }
+               },
+               "checkControlMessages": [],
+               "serviceRequired": [
+                       {
+                               "type": "OIL",
+                               "status": "OK",
+                               "dateTime": "2023-12-01T00:00:00.000Z",
+                               "distance": {
+                                       "value": 30000,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       {
+                               "type": "VEHICLE_CHECK",
+                               "status": "OK",
+                               "dateTime": "2025-12-01T00:00:00.000Z",
+                               "distance": {
+                                       "value": 60000,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       {
+                               "type": "BRAKE_FLUID",
+                               "status": "OK",
+                               "dateTime": "2024-12-01T00:00:00.000Z"
+                       },
+                       {
+                               "type": "VEHICLE_TUV",
+                               "status": "OK",
+                               "dateTime": "2024-12-01T00:00:00.000Z"
+                       }
+               ],
+               "vehicleLocation": {
+                       "coordinates": {
+                               "latitude": 1.234,
+                               "longitude": 5.678
+                       },
+                       "address": {
+                               "formatted": "anonymous"
+                       },
+                       "heading": 270
+               },
+               "tires": {
+                       "frontLeft": {
+                               "status": {
+                                       "currentPressure": 240.0,
+                                       "localizedCurrentPressure": "2,4 bar",
+                                       "localizedTargetPressure": "2,4 bar",
+                                       "targetPressure": 240.0
+                               }
+                       },
+                       "frontRight": {
+                               "status": {
+                                       "currentPressure": 240.0,
+                                       "localizedCurrentPressure": "2,4 bar",
+                                       "localizedTargetPressure": "2,4 bar",
+                                       "targetPressure": 240.0
+                               }
+                       },
+                       "rearLeft": {
+                               "status": {
+                                       "currentPressure": 270.0,
+                                       "localizedCurrentPressure": "2,7 bar",
+                                       "localizedTargetPressure": "2,8 bar",
+                                       "targetPressure": 280.0
+                               }
+                       },
+                       "rearRight": {
+                               "status": {
+                                       "currentPressure": 270.0,
+                                       "localizedCurrentPressure": "2,7 bar",
+                                       "localizedTargetPressure": "2,8 bar",
+                                       "targetPressure": 280.0
+                               }
+                       }
+               }
+       },
+       "isMappingPending": false,
+       "isMappingUnconfirmed": false,
+       "status": {
+               "lastUpdatedAt": "2022-01-06T15:59:07Z",
+               "currentMileage": {
+                       "mileage": 589,
+                       "units": "km",
+                       "formattedMileage": "589"
+               },
+               "issues": null,
+               "doorsGeneralState": "Entriegelt",
+               "checkControlMessagesGeneralState": "Keine Probleme",
+               "doorsAndWindows": [
+                       {
+                               "iconId": 59737,
+                               "title": "Verriegelungsstatus",
+                               "state": "Entriegelt",
+                               "criticalness": "nonCritical"
+                       },
+                       {
+                               "iconId": 59722,
+                               "title": "Alle Türen",
+                               "state": "Geschlossen",
+                               "criticalness": "nonCritical"
+                       },
+                       {
+                               "iconId": 59725,
+                               "title": "Alle Fenster",
+                               "state": "Geschlossen",
+                               "criticalness": "nonCritical"
+                       },
+                       {
+                               "iconId": 59706,
+                               "title": "Frontklappe",
+                               "state": "Geschlossen",
+                               "criticalness": "nonCritical"
+                       },
+                       {
+                               "iconId": 59704,
+                               "title": "Gepäckraum",
+                               "state": "Geschlossen",
+                               "criticalness": "nonCritical"
+                       }
+               ],
+               "checkControlMessages": [
+                       {
+                               "criticalness": "nonCritical",
+                               "iconId": 60117,
+                               "state": "OK",
+                               "title": "Reifen",
+                               "longDescription": "-"
+                       },
+                       {
+                               "criticalness": "nonCritical",
+                               "iconId": 60197,
+                               "state": "OK",
+                               "title": "Motoröl",
+                               "longDescription": "-"
+                       }
+               ],
+               "requiredServices": [
+                       {
+                               "id": "Oil",
+                               "title": "Motoröl",
+                               "iconId": 60197,
+                               "longDescription": "Nächster Service nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+                               "subtitle": "Fällig im Dezember 2023 oder in 30.000 km",
+                               "criticalness": "nonCritical"
+                       },
+                       {
+                               "id": "VehicleCheck",
+                               "title": "Fahrzeug-Check",
+                               "iconId": 60215,
+                               "longDescription": "Nächste Sichtprüfung nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+                               "subtitle": "Fällig im Dezember 2025 oder in 60.000 km",
+                               "criticalness": "nonCritical"
+                       },
+                       {
+                               "id": "BrakeFluid",
+                               "title": "Bremsflüssigkeit",
+                               "iconId": 60223,
+                               "longDescription": "Nächster Wechsel spätestens zum angegebenen Termin.",
+                               "subtitle": "Fällig im Dezember 2024",
+                               "criticalness": "nonCritical"
+                       },
+                       {
+                               "id": "VehicleAdmissionTest",
+                               "title": "Fahrzeuginspektion (HU)",
+                               "iconId": 60111,
+                               "longDescription": "Nächste gesetzliche Fahrzeuguntersuchung zum angegebenen Termin.",
+                               "subtitle": "Fällig im Dezember 2024",
+                               "criticalness": "nonCritical"
+                       }
+               ],
+               "fuelIndicators": [
+                       {
+                               "mainBarValue": 0,
+                               "rangeUnits": "km",
+                               "rangeValue": "251"
+                       },
+                       {
+                               "mainBarValue": 60,
+                               "rangeUnits": "km",
+                               "rangeValue": "27",
+                               "levelUnits": "%",
+                               "levelValue": "60"
+                       },
+                       {
+                               "mainBarValue": 42,
+                               "rangeUnits": "km",
+                               "rangeValue": "224",
+                               "levelUnits": "%",
+                               "levelValue": "42"
+                       }
+               ],
+               "timestampMessage": "Aktualisiert vom Fahrzeug 6.1.2022 04:59 PM",
+               "chargingProfile": {
+                       "reductionOfChargeCurrent": {
+                               "start": {
+                                       "hour": 0,
+                                       "minute": 0
+                               },
+                               "end": {
+                                       "hour": 0,
+                                       "minute": 0
+                               }
+                       },
+                       "chargingMode": "immediateCharging",
+                       "chargingPreference": "chargingWindow",
+                       "chargingControlType": "weeklyPlanner",
+                       "departureTimes": [
+                               {
+                                       "id": 1,
+                                       "action": "deactivate",
+                                       "timerWeekDays": []
+                               },
+                               {
+                                       "id": 2,
+                                       "action": "deactivate",
+                                       "timerWeekDays": []
+                               },
+                               {
+                                       "id": 3,
+                                       "action": "deactivate",
+                                       "timerWeekDays": []
+                               },
+                               {
+                                       "id": 4,
+                                       "action": "deactivate",
+                                       "timerWeekDays": []
+                               }
+                       ],
+                       "climatisationOn": true,
+                       "chargingSettings": {
+                               "targetSoc": 100,
+                               "isAcCurrentLimitActive": false,
+                               "hospitality": "NO_ACTION",
+                               "idcc": "NO_ACTION"
+                       }
+               }
+       },
+       "valid": false
+}
+]
+
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F11/vehicles_v2_bmw_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F11/vehicles_v2_bmw_0.json
new file mode 100644 (file)
index 0000000..b0a48f1
--- /dev/null
@@ -0,0 +1,279 @@
+[
+  {
+    "a4aType": "NOT_SUPPORTED",
+    "bodyType": "F11",
+    "brand": "BMW",
+    "capabilities": {
+      "canRemoteHistoryBeDeleted": false,
+      "climateNow": {
+        "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.",
+        "executionPopup": {
+          "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.",
+          "iconId": 59733,
+          "popupType": "DIALOG",
+          "primaryButtonText": "Start",
+          "secondaryButtonText": "Cancel",
+          "title": "Start Ventilation"
+        },
+        "executionStopPopup": {
+          "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.",
+          "title": "Climate control is running"
+        },
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "climateTimer": {
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false,
+        "isToggleEnabled": true,
+        "page": {
+          "description": "By setting a start time you let the vehicle know when you plan to use it.",
+          "primaryButtonText": "SEND TO VEHICLE",
+          "secondaryButtonText": "DEACTIVATE AND SEND TO VEHICLE",
+          "subtitle": "Set start time",
+          "title": "Ventilation timer"
+        },
+        "tile": {
+          "description": "Plan start time",
+          "iconId": 59774,
+          "title": "Ventilation timer"
+        }
+      },
+      "isBmwChargingSupported": false,
+      "isCarSharingSupported": false,
+      "isChargeNowForBusinessSupported": false,
+      "isChargingHistorySupported": false,
+      "isChargingHospitalityEnabled": false,
+      "isChargingLoudnessEnable": false,
+      "isChargingPlanSupported": false,
+      "isChargingPowerLimitEnable": false,
+      "isChargingSettingsEnabled": false,
+      "isChargingTargetSocEnable": false,
+      "isCustomerEsimSupported": false,
+      "isDCSContractManagementSupported": false,
+      "isDataPrivacyEnabled": false,
+      "isEasyChargeSupported": false,
+      "isEvGoChargingSupported": false,
+      "isFindChargingEnabled": false,
+      "isMiniChargingSupported": false,
+      "isRemoteHistorySupported": true,
+      "isRemoteServicesActivationRequired": false,
+      "isRemoteServicesBookingRequired": false,
+      "isScanAndChargeSupported": false,
+      "lastStateCall": {
+        "isNonLscFeatureEnabled": false,
+        "lscState": "NOT_CAPABLE"
+      },
+      "lights": {
+        "executionMessage": "Flash headlights now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "lock": {
+        "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "sendPoi": {
+        "executionMessage": "Send POI now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "unlock": {
+        "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": true
+      },
+      "vehicleFinder": {
+        "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": false,
+        "isPinAuthenticationRequired": false
+      }
+    },
+    "connectedDriveServices": [],
+    "driveTrain": "COMBUSTION",
+    "driverGuideInfo": {
+      "androidAppScheme": "com.bmwgroup.driversguide.row",
+      "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row",
+      "iosAppScheme": "bmwdriversguide:///open",
+      "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8",
+      "title": "BMW\nDriver's Guide"
+    },
+    "exFactoryILevel": "F010-12-11-503",
+    "exFactoryPUStep": "1112",
+    "headUnit": "ID5",
+    "hmiVersion": "ID4",
+    "iStep": "F010-12-11-503",
+    "isLscSupported": false,
+    "isMappingPending": false,
+    "isMappingUnconfirmed": false,
+    "model": "530d",
+    "properties": {
+      "checkControlMessages": [],
+      "climateControl": {},
+      "doorsAndWindows": {
+        "doors": {},
+        "windows": {}
+      },
+      "fuelLevel": {
+        "units": "LITERS",
+        "value": 24
+      },
+      "inMotion": false,
+      "isServiceRequired": false,
+      "lastUpdatedAt": "2021-03-10T08:02:08Z",
+      "originCountryISO": "GB",
+      "serviceRequired": [
+        {
+          "dateTime": "2022-10-01T00:00:00.000Z",
+          "status": "OK",
+          "type": "BRAKE_FLUID"
+        },
+        {
+          "dateTime": "2022-10-01T00:00:00.000Z",
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 25000
+          },
+          "status": "OK",
+          "type": "OIL"
+        },
+        {
+          "dateTime": "2024-10-01T00:00:00.000Z",
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 60000
+          },
+          "status": "OK",
+          "type": "VEHICLE_CHECK"
+        }
+      ]
+    },
+    "puStep": "1112",
+    "status": {
+      "checkControlMessages": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60197,
+          "state": "OK",
+          "title": "Engine Oil"
+        },
+        {
+          "criticalness": "semiCritical",
+          "iconId": 60217,
+          "id": "229",
+          "longDescription": "Charge by driving for longer periods or use external charger. Functions requiring battery will be switched off.",
+          "state": "Medium",
+          "title": "Battery discharged: Start engine"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60217,
+          "id": "50",
+          "longDescription": "System unable to monitor tire pressure. Check tire pressures manually. Continued driving possible. Consult service center.",
+          "state": "Low",
+          "title": "Flat Tire Monitor (FTM) inactive"
+        }
+      ],
+      "checkControlMessagesGeneralState": "Multiple Issues",
+      "doorsAndWindows": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59726,
+          "state": "Unknown",
+          "title": "All doors"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59701,
+          "state": "Unknown",
+          "title": "Left front window"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59700,
+          "state": "Unknown",
+          "title": "Right front window"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59703,
+          "state": "Unknown",
+          "title": "Left rear window"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59702,
+          "state": "Unknown",
+          "title": "Right rear window"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59721,
+          "state": "Unknown",
+          "title": "Back window"
+        }
+      ],
+      "doorsGeneralState": "Unknown",
+      "fuelIndicators": [
+        {
+          "chargingType": null,
+          "iconOpacity": "high",
+          "infoIconId": 59930,
+          "infoLabel": "Fuel Level",
+          "isCircleIcon": false,
+          "isInaccurate": true,
+          "levelIconId": 59682,
+          "levelUnits": "l",
+          "levelValue": "24",
+          "mainBarValue": 0,
+          "rangeIconId": 59681,
+          "rangeUnits": "mi",
+          "rangeValue": "- -",
+          "secondaryBarValue": 0,
+          "showsBar": false
+        }
+      ],
+      "lastUpdatedAt": "2021-03-10T08:02:08Z",
+      "recallExternalUrl": null,
+      "recallMessages": [],
+      "requiredServices": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60223,
+          "id": "BrakeFluid",
+          "longDescription": "Next service due by the specified date.",
+          "subtitle": "Due in October 2022",
+          "title": "Brake fluid"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60197,
+          "id": "Oil",
+          "longDescription": "Next service due after the specified distance or date.",
+          "subtitle": "Due in October 2022 or 15534 mi",
+          "title": "Engine oil"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60215,
+          "id": "VehicleCheck",
+          "longDescription": "Next vehicle check due after the specified distance or date.",
+          "subtitle": "Due in October 2024 or 37282 mi",
+          "title": "Vehicle check"
+        }
+      ],
+      "timestampMessage": "Updated from vehicle 3/11/2021 08:02 AM"
+    },
+    "telematicsUnit": "TCB1",
+    "themeSpecs": {
+      "vehicleStatusBackgroundColor": {
+        "blue": 158,
+        "green": 158,
+        "red": 158
+      }
+    },
+    "vin": "some_vin_F11",
+    "year": 2012
+  }
+]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F31/vehicles_v2_bmw_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F31/vehicles_v2_bmw_0.json
new file mode 100644 (file)
index 0000000..7583a8b
--- /dev/null
@@ -0,0 +1,281 @@
+[
+  {
+    "a4aType": "USB_ONLY",
+    "bodyType": "F31",
+    "brand": "BMW",
+    "capabilities": {
+      "canRemoteHistoryBeDeleted": false,
+      "climateNow": {
+        "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.",
+        "executionPopup": {
+          "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.",
+          "iconId": 59733,
+          "popupType": "DIALOG",
+          "primaryButtonText": "Start",
+          "secondaryButtonText": "Cancel",
+          "title": "Start Ventilation"
+        },
+        "executionStopPopup": {
+          "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.",
+          "title": "Climate control is running"
+        },
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "climateTimer": {
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false,
+        "isToggleEnabled": true,
+        "page": {
+          "description": "By setting a start time you let the vehicle know when you plan to use it.",
+          "primaryButtonText": "SEND TO VEHICLE",
+          "secondaryButtonText": "DEACTIVATE AND SEND TO VEHICLE",
+          "subtitle": "Set start time",
+          "title": "Ventilation timer"
+        },
+        "tile": {
+          "description": "Plan start time",
+          "iconId": 59774,
+          "title": "Ventilation timer"
+        }
+      },
+      "horn": {
+        "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "isBmwChargingSupported": false,
+      "isCarSharingSupported": false,
+      "isChargeNowForBusinessSupported": false,
+      "isChargingHistorySupported": false,
+      "isChargingHospitalityEnabled": false,
+      "isChargingLoudnessEnable": false,
+      "isChargingPlanSupported": false,
+      "isChargingPowerLimitEnable": false,
+      "isChargingSettingsEnabled": false,
+      "isChargingTargetSocEnable": false,
+      "isCustomerEsimSupported": false,
+      "isDCSContractManagementSupported": false,
+      "isDataPrivacyEnabled": false,
+      "isEasyChargeSupported": false,
+      "isEvGoChargingSupported": false,
+      "isFindChargingEnabled": false,
+      "isMiniChargingSupported": false,
+      "isRemoteHistorySupported": true,
+      "isRemoteServicesActivationRequired": false,
+      "isRemoteServicesBookingRequired": false,
+      "isScanAndChargeSupported": false,
+      "lastStateCall": {
+        "isNonLscFeatureEnabled": false,
+        "lscState": "NOT_CAPABLE"
+      },
+      "lights": {
+        "executionMessage": "Flash headlights now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "lock": {
+        "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "sendPoi": {
+        "executionMessage": "Send POI now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "unlock": {
+        "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": true
+      },
+      "vehicleFinder": {
+        "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      }
+    },
+    "connectedDriveServices": [],
+    "driveTrain": "COMBUSTION",
+    "driverGuideInfo": {
+      "androidAppScheme": "com.bmwgroup.driversguide.row",
+      "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row",
+      "iosAppScheme": "bmwdriversguide:///open",
+      "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8",
+      "title": "BMW\nDriver's Guide"
+    },
+    "exFactoryILevel": "F020-13-11-502",
+    "exFactoryPUStep": "1113",
+    "headUnit": "ID5",
+    "hmiVersion": "ID4",
+    "iStep": "F020-13-11-502",
+    "isLscSupported": false,
+    "isMappingPending": false,
+    "isMappingUnconfirmed": false,
+    "model": "320d xDrive",
+    "properties": {
+      "checkControlMessages": [],
+      "climateControl": {},
+      "doorsAndWindows": {
+        "doors": {},
+        "windows": {}
+      },
+      "fuelLevel": {
+        "units": "LITERS",
+        "value": 32
+      },
+      "inMotion": false,
+      "isServiceRequired": true,
+      "lastUpdatedAt": "2021-11-01T16:02:44Z",
+      "originCountryISO": "DE",
+      "serviceRequired": [
+        {
+          "dateTime": "2021-11-01T00:00:00.000Z",
+          "status": "PENDING",
+          "type": "BRAKE_FLUID"
+        },
+        {
+          "dateTime": "2022-07-01T00:00:00.000Z",
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 9000
+          },
+          "status": "OK",
+          "type": "OIL"
+        },
+        {
+          "dateTime": "2022-07-01T00:00:00.000Z",
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 9000
+          },
+          "status": "OK",
+          "type": "VEHICLE_CHECK"
+        },
+        {
+          "dateTime": "2023-02-01T00:00:00.000Z",
+          "status": "OK",
+          "type": "VEHICLE_TUV"
+        }
+      ]
+    },
+    "puStep": "1113",
+    "status": {
+      "checkControlMessages": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60197,
+          "state": "OK",
+          "title": "Engine Oil"
+        }
+      ],
+      "checkControlMessagesGeneralState": "No Issues",
+      "doorsAndWindows": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59726,
+          "state": "Unknown",
+          "title": "All doors"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59701,
+          "state": "Unknown",
+          "title": "Left front window"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59700,
+          "state": "Unknown",
+          "title": "Right front window"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59703,
+          "state": "Unknown",
+          "title": "Left rear window"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59702,
+          "state": "Unknown",
+          "title": "Right rear window"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59721,
+          "state": "Unknown",
+          "title": "Back window"
+        }
+      ],
+      "doorsGeneralState": "Unknown",
+      "fuelIndicators": [
+        {
+          "chargingType": null,
+          "iconOpacity": "high",
+          "infoIconId": 59930,
+          "infoLabel": "Fuel Level",
+          "isCircleIcon": false,
+          "isInaccurate": true,
+          "levelIconId": 59682,
+          "levelUnits": "l",
+          "levelValue": "32",
+          "mainBarValue": 0,
+          "rangeIconId": 59681,
+          "rangeUnits": "km",
+          "rangeValue": "- -",
+          "secondaryBarValue": 0,
+          "showsBar": false
+        }
+      ],
+      "lastUpdatedAt": "2021-11-01T16:02:44Z",
+      "recallExternalUrl": null,
+      "recallMessages": [],
+      "requiredServices": [
+        {
+          "criticalness": "semiCritical",
+          "iconId": 60223,
+          "id": "BrakeFluid",
+          "longDescription": "Service due soon. Please make an appointment with your service center.",
+          "subtitle": "Due in November 2021",
+          "title": "Brake fluid"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60197,
+          "id": "Oil",
+          "longDescription": "Next service due after the specified distance or date.",
+          "subtitle": "Due in July 2022 or 9000 km",
+          "title": "Engine oil"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60215,
+          "id": "VehicleCheck",
+          "longDescription": "Next vehicle check due after the specified distance or date.",
+          "subtitle": "Due in July 2022 or 9000 km",
+          "title": "Vehicle check"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60111,
+          "id": "VehicleAdmissionTest",
+          "longDescription": "Next state inspection due by the specified date.",
+          "subtitle": "Due in February 2023",
+          "title": "Vehicle Inspection"
+        }
+      ],
+      "timestampMessage": "Updated from vehicle 11/1/2021 05:02 PM"
+    },
+    "telematicsUnit": "TCB1",
+    "themeSpecs": {
+      "vehicleStatusBackgroundColor": {
+        "blue": 51,
+        "green": 51,
+        "red": 51
+      }
+    },
+    "vin": "some_vin_F31",
+    "year": 2013
+  }
+]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F44/vehicles_v2_bmw_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F44/vehicles_v2_bmw_0.json
new file mode 100644 (file)
index 0000000..12ea886
--- /dev/null
@@ -0,0 +1,251 @@
+[
+  {
+    "a4aType": "NOT_SUPPORTED",
+    "bodyType": "F44",
+    "brand": "BMW",
+    "capabilities": {
+      "canRemoteHistoryBeDeleted": false,
+      "horn": {
+        "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "isBmwChargingSupported": false,
+      "isCarSharingSupported": false,
+      "isChargeNowForBusinessSupported": false,
+      "isChargingHistorySupported": false,
+      "isChargingHospitalityEnabled": false,
+      "isChargingLoudnessEnable": false,
+      "isChargingPlanSupported": false,
+      "isChargingPowerLimitEnable": false,
+      "isChargingSettingsEnabled": false,
+      "isChargingTargetSocEnable": false,
+      "isCustomerEsimSupported": false,
+      "isDCSContractManagementSupported": false,
+      "isDataPrivacyEnabled": false,
+      "isEasyChargeSupported": false,
+      "isEvGoChargingSupported": false,
+      "isFindChargingEnabled": false,
+      "isMiniChargingSupported": false,
+      "isRemoteHistorySupported": true,
+      "isRemoteServicesActivationRequired": false,
+      "isRemoteServicesBookingRequired": false,
+      "isScanAndChargeSupported": false,
+      "lastStateCall": {
+        "isNonLscFeatureEnabled": false,
+        "lscState": "ACTIVATED"
+      },
+      "lights": {
+        "executionMessage": "Flash headlights now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "lock": {
+        "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "remoteSoftwareUpgrade": {
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "sendPoi": {
+        "executionMessage": "Send POI now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "unlock": {
+        "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": true
+      },
+      "vehicleFinder": {
+        "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      }
+    },
+    "connectedDriveServices": [],
+    "driveTrain": "COMBUSTION",
+    "driverGuideInfo": {
+      "androidAppScheme": "com.bmwgroup.driversguide.row",
+      "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row",
+      "iosAppScheme": "bmwdriversguide:///open",
+      "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8",
+      "title": "BMW\nDriver's Guide"
+    },
+    "exFactoryILevel": "S18A-20-11-538",
+    "exFactoryPUStep": "1120",
+    "headUnit": "MGU",
+    "hmiVersion": "id7",
+    "iStep": "S18A-21-03-550",
+    "isLscSupported": true,
+    "isMappingPending": false,
+    "isMappingUnconfirmed": false,
+    "model": "218i",
+    "properties": {
+      "areDoorsClosed": true,
+      "areDoorsLocked": true,
+      "areDoorsOpen": false,
+      "areWindowsClosed": true,
+      "checkControlMessages": [],
+      "climateControl": {
+        "activity": "INACTIVE"
+      },
+      "combustionRange": {
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 222
+        }
+      },
+      "doorsAndWindows": {
+        "doors": {
+          "driverFront": "CLOSED",
+          "driverRear": "CLOSED",
+          "passengerFront": "CLOSED",
+          "passengerRear": "CLOSED"
+        },
+        "hood": "CLOSED",
+        "trunk": "CLOSED",
+        "windows": {
+          "driverFront": "CLOSED",
+          "driverRear": "CLOSED",
+          "passengerFront": "CLOSED",
+          "passengerRear": "CLOSED"
+        }
+      },
+      "fuelLevel": {
+        "units": "LITERS",
+        "value": 16
+      },
+      "fuelPercentage": {
+        "value": 35
+      },
+      "inMotion": false,
+      "isServiceRequired": false,
+      "lastUpdatedAt": "2021-11-10T18:03:43Z",
+      "originCountryISO": "BE",
+      "serviceRequired": [
+        {
+          "dateTime": "2022-12-01T00:00:00.000Z",
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 24000
+          },
+          "status": "OK",
+          "type": "OIL"
+        },
+        {
+          "dateTime": "2023-12-01T00:00:00.000Z",
+          "status": "OK",
+          "type": "BRAKE_FLUID"
+        },
+        {
+          "dateTime": "2024-12-01T00:00:00.000Z",
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 50000
+          },
+          "status": "OK",
+          "type": "VEHICLE_CHECK"
+        }
+      ],
+      "vehicleLocation": {
+        "address": {
+          "formatted": "some_formatted_address"
+        },
+        "coordinates": {
+          "latitude": 12.3456,
+          "longitude": 34.5678
+        },
+        "heading": 123
+      }
+    },
+    "puStep": "0321",
+    "status": {
+      "checkControlMessages": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60197,
+          "state": "OK",
+          "title": "Engine Oil"
+        }
+      ],
+      "checkControlMessagesGeneralState": "No Issues",
+      "currentMileage": {
+        "formattedMileage": "3047",
+        "mileage": 3047,
+        "units": "km"
+      },
+      "doorsAndWindows": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59722,
+          "state": "Closed",
+          "title": "All doors and windows"
+        }
+      ],
+      "doorsGeneralState": "Locked",
+      "fuelIndicators": [
+        {
+          "chargingType": null,
+          "iconOpacity": "high",
+          "infoIconId": 59930,
+          "infoLabel": "Fuel Level",
+          "isCircleIcon": false,
+          "isInaccurate": false,
+          "levelIconId": 59682,
+          "levelUnits": "%",
+          "levelValue": "35",
+          "mainBarValue": 35,
+          "rangeIconId": 59681,
+          "rangeUnits": "km",
+          "rangeValue": "222",
+          "secondaryBarValue": 0,
+          "showsBar": true
+        }
+      ],
+      "issues": {},
+      "lastUpdatedAt": "2021-11-10T18:03:43Z",
+      "recallExternalUrl": null,
+      "recallMessages": [],
+      "requiredServices": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60197,
+          "id": "Oil",
+          "longDescription": "Next service due after the specified distance or date.",
+          "subtitle": "Due in December 2022 or 24000 km",
+          "title": "Engine oil"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60223,
+          "id": "BrakeFluid",
+          "longDescription": "Next service due by the specified date.",
+          "subtitle": "Due in December 2023",
+          "title": "Brake fluid"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60215,
+          "id": "VehicleCheck",
+          "longDescription": "Next vehicle check due after the specified distance or date.",
+          "subtitle": "Due in December 2024 or 50000 km",
+          "title": "Vehicle check"
+        }
+      ],
+      "timestampMessage": "Updated from vehicle 11/10/2021 07:03 PM"
+    },
+    "telematicsUnit": "ATM02",
+    "themeSpecs": {
+      "vehicleStatusBackgroundColor": {
+        "blue": 84,
+        "green": 84,
+        "red": 84
+      }
+    },
+    "vin": "some_vin_F44",
+    "year": 2020
+  }
+]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F45/vehicles_v2_bmw_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F45/vehicles_v2_bmw_0.json
new file mode 100644 (file)
index 0000000..0b7102e
--- /dev/null
@@ -0,0 +1,301 @@
+[
+    {
+      "a4aType": "USB_ONLY",
+      "bodyType": "F45",
+      "brand": "BMW",
+      "capabilities": {
+        "canRemoteHistoryBeDeleted": false,
+        "climateNow": {
+          "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.",
+          "executionPopup": {
+            "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.",
+            "iconId": 59733,
+            "popupType": "DIALOG",
+            "primaryButtonText": "Start",
+            "secondaryButtonText": "Cancel",
+            "title": "Start Climatization"
+          },
+          "executionStopPopup": {
+            "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.",
+            "title": "Climate control is running"
+          },
+          "isEnabled": true,
+          "isPinAuthenticationRequired": false
+        },
+        "isBmwChargingSupported": true,
+        "isCarSharingSupported": false,
+        "isChargeNowForBusinessSupported": false,
+        "isChargingHistorySupported": true,
+        "isChargingHospitalityEnabled": false,
+        "isChargingLoudnessEnable": false,
+        "isChargingPlanSupported": true,
+        "isChargingPowerLimitEnable": false,
+        "isChargingSettingsEnabled": false,
+        "isChargingTargetSocEnable": false,
+        "isCustomerEsimSupported": false,
+        "isDCSContractManagementSupported": true,
+        "isDataPrivacyEnabled": false,
+        "isEasyChargeSupported": false,
+        "isEvGoChargingSupported": false,
+        "isFindChargingEnabled": true,
+        "isMiniChargingSupported": false,
+        "isRemoteHistorySupported": true,
+        "isRemoteServicesActivationRequired": false,
+        "isRemoteServicesBookingRequired": false,
+        "isScanAndChargeSupported": false,
+        "lastStateCall": {
+          "isNonLscFeatureEnabled": false,
+          "lscState": "ACTIVATED"
+        },
+        "lights": {
+          "executionMessage": "Flash headlights now? Remote functions may take a few seconds.",
+          "isEnabled": true,
+          "isPinAuthenticationRequired": false
+        },
+        "lock": {
+          "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.",
+          "isEnabled": true,
+          "isPinAuthenticationRequired": false
+        },
+        "sendPoi": {
+          "executionMessage": "Send POI now? Remote functions may take a few seconds.",
+          "isEnabled": true,
+          "isPinAuthenticationRequired": false
+        },
+        "unlock": {
+          "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.",
+          "isEnabled": true,
+          "isPinAuthenticationRequired": true
+        },
+        "vehicleFinder": {
+          "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.",
+          "isEnabled": true,
+          "isPinAuthenticationRequired": false
+        }
+      },
+      "connectedDriveServices": [],
+      "driveTrain": "PLUGIN_HYBRID",
+      "driverGuideInfo": {
+        "androidAppScheme": "com.bmwgroup.driversguide.row",
+        "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row",
+        "iosAppScheme": "bmwdriversguide:///open",
+        "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8",
+        "title": "BMW\nDriver's Guide"
+      },
+      "exFactoryILevel": "F056-16-07-502",
+      "exFactoryPUStep": "0716",
+      "headUnit": "ID5",
+      "hmiVersion": "ID4",
+      "iStep": "F056-20-07-550",
+      "isLscSupported": true,
+      "isMappingPending": false,
+      "isMappingUnconfirmed": false,
+      "model": "225xe iPerformance",
+      "properties": {
+        "areDoorsClosed": true,
+        "areDoorsLocked": true,
+        "areDoorsOpen": false,
+        "areWindowsClosed": true,
+        "chargingState": {
+          "chargePercentage": 40,
+          "isChargerConnected": false,
+          "state": "NOT_CHARGING",
+          "type": "CONDUCTIVE"
+        },
+        "checkControlMessages": [],
+        "climateControl": {},
+        "combinedRange": {
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 245
+          }
+        },
+        "combustionRange": {
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 245
+          }
+        },
+        "doorsAndWindows": {
+          "doors": {
+            "driverFront": "CLOSED",
+            "driverRear": "CLOSED",
+            "passengerFront": "CLOSED",
+            "passengerRear": "CLOSED"
+          },
+          "hood": "CLOSED",
+          "trunk": "CLOSED",
+          "windows": {
+            "driverFront": "CLOSED",
+            "driverRear": "CLOSED",
+            "passengerFront": "CLOSED",
+            "passengerRear": "CLOSED"
+          }
+        },
+        "electricRange": {
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 4
+          }
+        },
+        "electricRangeAndStatus": {
+          "chargePercentage": 40,
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 4
+          }
+        },
+        "fuelLevel": {
+          "units": "LITERS",
+          "value": 20
+        },
+        "inMotion": false,
+        "isServiceRequired": false,
+        "lastUpdatedAt": "2021-11-10T18:25:38Z",
+        "originCountryISO": "GB",
+        "serviceRequired": [],
+        "vehicleLocation": {
+          "address": {
+            "formatted": "some_formatted_address"
+          },
+          "coordinates": {
+            "latitude": 12.3456,
+            "longitude": 34.5678
+          },
+          "heading": 123
+        }
+      },
+      "puStep": "0720",
+      "status": {
+        "chargingProfile": {
+          "chargingControlType": "twoWeeksTimer",
+          "chargingMode": "immediateCharging",
+          "chargingPreference": "chargingWindow",
+          "chargingSettings": {
+            "hospitality": "NO_ACTION",
+            "idcc": "NO_ACTION",
+            "isAcCurrentLimitActive": false,
+            "targetSoc": 100
+          },
+          "climatisationOn": false,
+          "departureTimes": [
+            {
+              "action": "deactivate",
+              "id": 1,
+              "timerWeekDays": []
+            },
+            {
+              "action": "deactivate",
+              "id": 2,
+              "timerWeekDays": []
+            }
+          ],
+          "reductionOfChargeCurrent": {
+            "end": {
+              "hour": 16,
+              "minute": 0
+            },
+            "start": {
+              "hour": 13,
+              "minute": 0
+            }
+          }
+        },
+        "checkControlMessages": [
+          {
+            "criticalness": "nonCritical",
+            "iconId": 60197,
+            "state": "OK",
+            "title": "Engine Oil"
+          }
+        ],
+        "checkControlMessagesGeneralState": "No Issues",
+        "currentMileage": {
+          "formattedMileage": "66720",
+          "mileage": 66720,
+          "units": "mi"
+        },
+        "doorsAndWindows": [
+          {
+            "criticalness": "nonCritical",
+            "iconId": 59722,
+            "state": "Closed",
+            "title": "All doors and windows"
+          }
+        ],
+        "doorsGeneralState": "Locked",
+        "fuelIndicators": [
+          {
+            "chargingType": null,
+            "iconOpacity": "high",
+            "infoIconId": 59691,
+            "infoLabel": "Combined Range",
+            "isCircleIcon": false,
+            "isInaccurate": false,
+            "levelIconId": null,
+            "levelUnits": null,
+            "levelValue": null,
+            "mainBarValue": 0,
+            "rangeIconId": 59691,
+            "rangeUnits": "mi",
+            "rangeValue": "152",
+            "secondaryBarValue": 0,
+            "showsBar": false
+          },
+          {
+            "barType": null,
+            "chargingStatusIndicatorType": "DEFAULT",
+            "chargingStatusType": "DEFAULT",
+            "chargingType": null,
+            "iconOpacity": "high",
+            "infoIconId": 59694,
+            "infoLabel": "State of Charge",
+            "isCircleIcon": false,
+            "isInaccurate": false,
+            "levelIconId": 59694,
+            "levelUnits": "%",
+            "levelValue": "40",
+            "mainBarValue": 40,
+            "rangeIconId": 59683,
+            "rangeUnits": "mi",
+            "rangeValue": "2",
+            "secondaryBarValue": 0,
+            "showBarGoal": false,
+            "showsBar": true
+          },
+          {
+            "chargingType": null,
+            "iconOpacity": "high",
+            "infoIconId": 59930,
+            "infoLabel": "Fuel Level",
+            "isCircleIcon": false,
+            "isInaccurate": true,
+            "levelIconId": 59682,
+            "levelUnits": "l",
+            "levelValue": "20",
+            "mainBarValue": 0,
+            "rangeIconId": 59681,
+            "rangeUnits": "mi",
+            "rangeValue": "150",
+            "secondaryBarValue": 0,
+            "showsBar": false
+          }
+        ],
+        "issues": {},
+        "lastUpdatedAt": "2021-11-10T18:25:38Z",
+        "recallExternalUrl": null,
+        "recallMessages": [],
+        "timestampMessage": "Updated from vehicle 11/11/2021 06:25 PM"
+      },
+      "telematicsUnit": "TCB1",
+      "themeSpecs": {
+        "vehicleStatusBackgroundColor": {
+          "blue": 66,
+          "green": 66,
+          "red": 66
+        }
+      },
+      "vin": "some_vin_F45",
+      "year": 2016
+    }
+  ]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F48/vehicles_v2_bmw_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F48/vehicles_v2_bmw_0.json
new file mode 100644 (file)
index 0000000..30d61fb
--- /dev/null
@@ -0,0 +1,278 @@
+[
+  {
+    "a4aType": "BLUETOOTH",
+    "bodyType": "F48",
+    "brand": "BMW",
+    "capabilities": {
+      "canRemoteHistoryBeDeleted": false,
+      "climateNow": {
+        "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.",
+        "executionPopup": {
+          "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.",
+          "iconId": 59733,
+          "popupType": "DIALOG",
+          "primaryButtonText": "Start",
+          "secondaryButtonText": "Cancel",
+          "title": "Start Ventilation"
+        },
+        "executionStopPopup": {
+          "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.",
+          "title": "Climate control is running"
+        },
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "climateTimer": {
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false,
+        "isToggleEnabled": true,
+        "page": {
+          "description": "By setting a start time you let the vehicle know when you plan to use it.",
+          "primaryButtonText": "SEND TO VEHICLE",
+          "secondaryButtonText": "DEACTIVATE AND SEND TO VEHICLE",
+          "subtitle": "Set start time",
+          "title": "Ventilation timer"
+        },
+        "tile": {
+          "description": "Plan start time",
+          "iconId": 59774,
+          "title": "Ventilation timer"
+        }
+      },
+      "horn": {
+        "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "isBmwChargingSupported": false,
+      "isCarSharingSupported": false,
+      "isChargeNowForBusinessSupported": false,
+      "isChargingHistorySupported": false,
+      "isChargingHospitalityEnabled": false,
+      "isChargingLoudnessEnable": false,
+      "isChargingPlanSupported": false,
+      "isChargingPowerLimitEnable": false,
+      "isChargingSettingsEnabled": false,
+      "isChargingTargetSocEnable": false,
+      "isCustomerEsimSupported": false,
+      "isDCSContractManagementSupported": false,
+      "isDataPrivacyEnabled": false,
+      "isEasyChargeSupported": false,
+      "isEvGoChargingSupported": false,
+      "isFindChargingEnabled": false,
+      "isMiniChargingSupported": false,
+      "isRemoteHistorySupported": true,
+      "isRemoteServicesActivationRequired": false,
+      "isRemoteServicesBookingRequired": false,
+      "isScanAndChargeSupported": false,
+      "lastStateCall": {
+        "isNonLscFeatureEnabled": false,
+        "lscState": "ACTIVATED"
+      },
+      "lights": {
+        "executionMessage": "Flash headlights now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "lock": {
+        "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "sendPoi": {
+        "executionMessage": "Send POI now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "unlock": {
+        "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": true
+      },
+      "vehicleFinder": {
+        "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      }
+    },
+    "connectedDriveServices": [
+      "WIFI_HOTSPOT_SERVICE"
+    ],
+    "driveTrain": "COMBUSTION",
+    "driverGuideInfo": {
+      "androidAppScheme": "com.bmwgroup.driversguide.row",
+      "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row",
+      "iosAppScheme": "bmwdriversguide:///open",
+      "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8",
+      "title": "BMW\nDriver's Guide"
+    },
+    "exFactoryILevel": "F056-17-07-503",
+    "exFactoryPUStep": "0717",
+    "headUnit": "ID5",
+    "hmiVersion": "ID5",
+    "iStep": "F056-18-03-541",
+    "isLscSupported": true,
+    "isMappingPending": false,
+    "isMappingUnconfirmed": false,
+    "model": "X1 sDrive18i",
+    "properties": {
+      "areDoorsClosed": true,
+      "areDoorsLocked": true,
+      "areDoorsOpen": false,
+      "areWindowsClosed": true,
+      "checkControlMessages": [],
+      "climateControl": {},
+      "combustionRange": {
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 308
+        }
+      },
+      "doorsAndWindows": {
+        "doors": {
+          "driverFront": "CLOSED",
+          "driverRear": "CLOSED",
+          "passengerFront": "CLOSED",
+          "passengerRear": "CLOSED"
+        },
+        "hood": "CLOSED",
+        "trunk": "CLOSED",
+        "windows": {
+          "driverFront": "CLOSED",
+          "driverRear": "CLOSED",
+          "passengerFront": "CLOSED",
+          "passengerRear": "CLOSED"
+        }
+      },
+      "fuelLevel": {
+        "units": "LITERS",
+        "value": 19
+      },
+      "inMotion": true,
+      "isServiceRequired": false,
+      "lastUpdatedAt": "2021-10-30T06:57:45Z",
+      "originCountryISO": "NL",
+      "serviceRequired": [
+        {
+          "dateTime": "2021-12-01T00:00:00.000Z",
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 8000
+          },
+          "status": "OK",
+          "type": "OIL"
+        },
+        {
+          "dateTime": "2021-12-01T00:00:00.000Z",
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 8000
+          },
+          "status": "OK",
+          "type": "VEHICLE_CHECK"
+        },
+        {
+          "dateTime": "2022-07-01T00:00:00.000Z",
+          "status": "OK",
+          "type": "BRAKE_FLUID"
+        }
+      ],
+      "vehicleLocation": {
+        "address": {
+          "formatted": "address, city"
+        },
+        "coordinates": {
+          "latitude": 0.0,
+          "longitude": 0.0
+        },
+        "heading": 123
+      }
+    },
+    "puStep": "0318",
+    "status": {
+      "checkControlMessages": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60197,
+          "state": "OK",
+          "title": "Engine Oil"
+        }
+      ],
+      "checkControlMessagesGeneralState": "No Issues",
+      "currentMileage": {
+        "formattedMileage": "113009",
+        "mileage": 113009,
+        "units": "km"
+      },
+      "doorsAndWindows": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59722,
+          "state": "Closed",
+          "title": "All doors and windows"
+        }
+      ],
+      "doorsGeneralState": "Locked",
+      "fuelIndicators": [
+        {
+          "chargingType": null,
+          "iconOpacity": "high",
+          "infoIconId": 59930,
+          "infoLabel": "Fuel Level",
+          "isCircleIcon": false,
+          "isInaccurate": true,
+          "levelIconId": 59682,
+          "levelUnits": "l",
+          "levelValue": "19",
+          "mainBarValue": 0,
+          "rangeIconId": 59681,
+          "rangeUnits": "km",
+          "rangeValue": "308",
+          "secondaryBarValue": 0,
+          "showsBar": false
+        }
+      ],
+      "issues": {},
+      "lastUpdatedAt": "2021-10-30T06:57:45Z",
+      "recallExternalUrl": null,
+      "recallMessages": [],
+      "requiredServices": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60197,
+          "id": "Oil",
+          "longDescription": "Next service due after the specified distance or date.",
+          "subtitle": "Due in December 2021 or 8000 km",
+          "title": "Engine oil"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60215,
+          "id": "VehicleCheck",
+          "longDescription": "Next vehicle check due after the specified distance or date.",
+          "subtitle": "Due in December 2021 or 8000 km",
+          "title": "Vehicle check"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60223,
+          "id": "BrakeFluid",
+          "longDescription": "Next service due by the specified date.",
+          "subtitle": "Due in July 2022",
+          "title": "Brake fluid"
+        }
+      ],
+      "timestampMessage": "Updated from vehicle 10/30/2021 08:57 AM"
+    },
+    "telematicsUnit": "ATM",
+    "themeSpecs": {
+      "vehicleStatusBackgroundColor": {
+        "blue": 158,
+        "green": 158,
+        "red": 158
+      }
+    },
+    "vin": "some_vin_F48",
+    "year": 2017
+  }
+]
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G01/vehicles_v2_bmw_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G01/vehicles_v2_bmw_0.json
new file mode 100644 (file)
index 0000000..e12a0ef
--- /dev/null
@@ -0,0 +1,429 @@
+[
+  {
+    "a4aType": "USB_ONLY",
+    "bodyType": "G01",
+    "brand": "BMW",
+    "capabilities": {
+      "canRemoteHistoryBeDeleted": false,
+      "climateNow": {
+        "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.",
+        "executionPopup": {
+          "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.",
+          "iconId": 59733,
+          "popupType": "DIALOG",
+          "primaryButtonText": "Start",
+          "secondaryButtonText": "Cancel",
+          "title": "Start Climatization"
+        },
+        "executionStopPopup": {
+          "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.",
+          "title": "Climate control is running"
+        },
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "horn": {
+        "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "isBmwChargingSupported": true,
+      "isCarSharingSupported": false,
+      "isChargeNowForBusinessSupported": false,
+      "isChargingHistorySupported": true,
+      "isChargingHospitalityEnabled": false,
+      "isChargingLoudnessEnable": false,
+      "isChargingPlanSupported": true,
+      "isChargingPowerLimitEnable": false,
+      "isChargingSettingsEnabled": false,
+      "isChargingTargetSocEnable": false,
+      "isCustomerEsimSupported": false,
+      "isDCSContractManagementSupported": true,
+      "isDataPrivacyEnabled": false,
+      "isEasyChargeSupported": false,
+      "isEvGoChargingSupported": false,
+      "isFindChargingEnabled": true,
+      "isMiniChargingSupported": false,
+      "isRemoteHistorySupported": true,
+      "isRemoteServicesActivationRequired": false,
+      "isRemoteServicesBookingRequired": false,
+      "isScanAndChargeSupported": false,
+      "lastStateCall": {
+        "isNonLscFeatureEnabled": false,
+        "lscState": "ACTIVATED"
+      },
+      "lights": {
+        "executionMessage": "Flash headlights now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "lock": {
+        "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "sendPoi": {
+        "executionMessage": "Send POI now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "unlock": {
+        "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": true
+      },
+      "vehicleFinder": {
+        "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      }
+    },
+    "connectedDriveServices": [],
+    "driveTrain": "PLUGIN_HYBRID",
+    "driverGuideInfo": {
+      "androidAppScheme": "com.bmwgroup.driversguide.row",
+      "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row",
+      "iosAppScheme": "bmwdriversguide:///open",
+      "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8",
+      "title": "BMW\nDriver's Guide"
+    },
+    "exFactoryILevel": "S15A-20-07-549",
+    "exFactoryPUStep": "0720",
+    "headUnit": "ID5",
+    "hmiVersion": "ID5",
+    "iStep": "S15A-20-07-549",
+    "isLscSupported": true,
+    "isMappingPending": false,
+    "isMappingUnconfirmed": false,
+    "model": "X3 xDrive30e",
+    "properties": {
+      "areDoorsClosed": true,
+      "areDoorsLocked": true,
+      "areDoorsOpen": false,
+      "areWindowsClosed": true,
+      "chargingState": {
+        "chargePercentage": 12,
+        "isChargerConnected": true,
+        "state": "CHARGING",
+        "type": "NOT_AVAILABLE"
+      },
+      "checkControlMessages": [],
+      "climateControl": {},
+      "combinedRange": {
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 439
+        }
+      },
+      "combustionRange": {
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 439
+        }
+      },
+      "doorsAndWindows": {
+        "doors": {
+          "driverFront": "CLOSED",
+          "driverRear": "CLOSED",
+          "passengerFront": "CLOSED",
+          "passengerRear": "CLOSED"
+        },
+        "hood": "CLOSED",
+        "trunk": "CLOSED",
+        "windows": {
+          "driverFront": "CLOSED",
+          "driverRear": "CLOSED",
+          "passengerFront": "CLOSED",
+          "passengerRear": "CLOSED"
+        }
+      },
+      "electricRange": {
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 2
+        }
+      },
+      "electricRangeAndStatus": {
+        "chargePercentage": 12,
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 2
+        }
+      },
+      "fuelLevel": {
+        "units": "LITERS",
+        "value": 30
+      },
+      "inMotion": false,
+      "isServiceRequired": false,
+      "lastUpdatedAt": "2021-12-13T21:06:27Z",
+      "originCountryISO": "ES",
+      "serviceRequired": [
+        {
+          "dateTime": "2024-12-01T00:00:00.000Z",
+          "status": "OK",
+          "type": "VEHICLE_TUV"
+        },
+        {
+          "dateTime": "2022-10-01T00:00:00.000Z",
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 12000
+          },
+          "status": "OK",
+          "type": "OIL"
+        },
+        {
+          "dateTime": "2024-10-01T00:00:00.000Z",
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 45000
+          },
+          "status": "OK",
+          "type": "VEHICLE_CHECK"
+        },
+        {
+          "dateTime": "2023-10-01T00:00:00.000Z",
+          "status": "OK",
+          "type": "BRAKE_FLUID"
+        }
+      ],
+      "vehicleLocation": {
+        "address": {
+          "formatted": "some_formatted_address"
+        },
+        "coordinates": {
+          "latitude": 12.3456,
+          "longitude": 34.5678
+        },
+        "heading": 123
+      }
+    },
+    "puStep": "0720",
+    "status": {
+      "chargingProfile": {
+        "chargingControlType": "weeklyPlanner",
+        "chargingMode": "delayedCharging",
+        "chargingPreference": "chargingWindow",
+        "chargingSettings": {
+          "hospitality": "NO_ACTION",
+          "idcc": "NO_ACTION",
+          "isAcCurrentLimitActive": false,
+          "targetSoc": 100
+        },
+        "climatisationOn": false,
+        "departureTimes": [
+          {
+            "action": "activate",
+            "id": 1,
+            "timeStamp": {
+              "hour": 17,
+              "minute": 0
+            },
+            "timerWeekDays": [
+              "monday",
+              "tuesday",
+              "wednesday",
+              "thursday",
+              "friday",
+              "saturday",
+              "sunday"
+            ]
+          },
+          {
+            "action": "deactivate",
+            "id": 2,
+            "timeStamp": {
+              "hour": 0,
+              "minute": 0
+            },
+            "timerWeekDays": []
+          },
+          {
+            "action": "deactivate",
+            "id": 3,
+            "timeStamp": {
+              "hour": 0,
+              "minute": 0
+            },
+            "timerWeekDays": []
+          },
+          {
+            "action": "deactivate",
+            "id": 4,
+            "timeStamp": {
+              "hour": 17,
+              "minute": 0
+            },
+            "timerWeekDays": [
+              "tuesday"
+            ]
+          }
+        ],
+        "reductionOfChargeCurrent": {
+          "end": {
+            "hour": 16,
+            "minute": 59
+          },
+          "start": {
+            "hour": 9,
+            "minute": 0
+          }
+        }
+      },
+      "checkControlMessages": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60197,
+          "state": "OK",
+          "title": "Engine Oil"
+        }
+      ],
+      "checkControlMessagesGeneralState": "No Issues",
+      "currentMileage": {
+        "formattedMileage": "21068",
+        "mileage": 21068,
+        "units": "km"
+      },
+      "doorsAndWindows": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59757,
+          "state": "Locked",
+          "title": "Lock status"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59722,
+          "state": "Closed",
+          "title": "All doors"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59725,
+          "state": "Closed",
+          "title": "All windows"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59706,
+          "state": "Closed",
+          "title": "Hood"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59704,
+          "state": "Closed",
+          "title": "Trunk"
+        }
+      ],
+      "doorsGeneralState": "Locked",
+      "fuelIndicators": [
+        {
+          "chargingType": null,
+          "iconOpacity": "high",
+          "infoIconId": 59691,
+          "infoLabel": "Combined Range",
+          "isCircleIcon": false,
+          "isInaccurate": false,
+          "levelIconId": null,
+          "levelUnits": null,
+          "levelValue": null,
+          "mainBarValue": 0,
+          "rangeIconId": 59691,
+          "rangeUnits": "km",
+          "rangeValue": "439",
+          "secondaryBarValue": 0,
+          "showsBar": false
+        },
+        {
+          "barType": null,
+          "chargingStatusIndicatorType": "PLUGGED_IN",
+          "chargingStatusType": "PLUGGED_IN",
+          "chargingType": null,
+          "iconOpacity": "high",
+          "infoIconId": 59689,
+          "infoLabel": "Starts at ~ 09:00 AM",
+          "isCircleIcon": true,
+          "isInaccurate": true,
+          "levelIconId": 59689,
+          "levelUnits": "%",
+          "levelValue": "12",
+          "mainBarValue": 12,
+          "rangeIconId": 59683,
+          "rangeUnits": "km",
+          "rangeValue": "2",
+          "secondaryBarValue": 0,
+          "showBarGoal": false,
+          "showsBar": true
+        },
+        {
+          "chargingType": null,
+          "iconOpacity": "high",
+          "infoIconId": 59930,
+          "infoLabel": "Fuel Level",
+          "isCircleIcon": false,
+          "isInaccurate": true,
+          "levelIconId": 59682,
+          "levelUnits": "l",
+          "levelValue": "30",
+          "mainBarValue": 0,
+          "rangeIconId": 59681,
+          "rangeUnits": "km",
+          "rangeValue": "437",
+          "secondaryBarValue": 0,
+          "showsBar": false
+        }
+      ],
+      "issues": {},
+      "lastUpdatedAt": "2021-12-13T21:06:27Z",
+      "recallExternalUrl": null,
+      "recallMessages": [],
+      "requiredServices": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60111,
+          "id": "VehicleAdmissionTest",
+          "longDescription": "Next state inspection due by the specified date.",
+          "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+          "title": "Vehicle Inspection"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60197,
+          "id": "Oil",
+          "longDescription": "Next service due after the specified distance or date.",
+          "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+          "title": "Engine oil"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60215,
+          "id": "VehicleCheck",
+          "longDescription": "Next vehicle check due after the specified distance or date.",
+          "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+          "title": "Vehicle check"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60223,
+          "id": "BrakeFluid",
+          "longDescription": "Next service due by the specified date.",
+          "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+          "title": "Brake fluid"
+        }
+      ],
+      "timestampMessage": "Updated from vehicle 12/13/2021 10:06 PM"
+    },
+    "telematicsUnit": "ATM",
+    "themeSpecs": {
+      "vehicleStatusBackgroundColor": {
+        "blue": 133,
+        "green": 129,
+        "red": 127
+      }
+    },
+    "vin": "some_vin_G01",
+    "year": 2020
+  }
+]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G05/vehicles_v2_bmw_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G05/vehicles_v2_bmw_0.json
new file mode 100644 (file)
index 0000000..bdcf55a
--- /dev/null
@@ -0,0 +1,401 @@
+[
+  {
+    "a4aType": "NOT_SUPPORTED",
+    "bodyType": "G05",
+    "brand": "BMW",
+    "capabilities": {
+      "canRemoteHistoryBeDeleted": false,
+      "climateNow": {
+        "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.",
+        "executionPopup": {
+          "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.",
+          "iconId": 59733,
+          "popupType": "DIALOG",
+          "primaryButtonText": "Start",
+          "secondaryButtonText": "Cancel",
+          "title": "Start Climatization"
+        },
+        "executionStopPopup": {
+          "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.",
+          "title": "Climate control is running"
+        },
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "horn": {
+        "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "isBmwChargingSupported": true,
+      "isCarSharingSupported": false,
+      "isChargeNowForBusinessSupported": false,
+      "isChargingHistorySupported": true,
+      "isChargingHospitalityEnabled": false,
+      "isChargingLoudnessEnable": false,
+      "isChargingPlanSupported": true,
+      "isChargingPowerLimitEnable": false,
+      "isChargingSettingsEnabled": false,
+      "isChargingTargetSocEnable": false,
+      "isCustomerEsimSupported": false,
+      "isDCSContractManagementSupported": true,
+      "isDataPrivacyEnabled": false,
+      "isEasyChargeSupported": false,
+      "isEvGoChargingSupported": false,
+      "isFindChargingEnabled": true,
+      "isMiniChargingSupported": false,
+      "isRemoteHistorySupported": true,
+      "isRemoteServicesActivationRequired": false,
+      "isRemoteServicesBookingRequired": false,
+      "isScanAndChargeSupported": true,
+      "lastStateCall": {
+        "isNonLscFeatureEnabled": false,
+        "lscState": "ACTIVATED"
+      },
+      "lights": {
+        "executionMessage": "Flash headlights now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "lock": {
+        "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "remote360": {
+        "isComingSoonEnabled": false,
+        "isDataPrivacyEnabled": false,
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false,
+        "isToggleEnabled": true
+      },
+      "remoteSoftwareUpgrade": {
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "sendPoi": {
+        "executionMessage": "Send POI now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "unlock": {
+        "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": true
+      },
+      "vehicleFinder": {
+        "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      }
+    },
+    "connectedDriveServices": [
+      "WIFI_HOTSPOT_SERVICE"
+    ],
+    "driveTrain": "PLUGIN_HYBRID",
+    "driverGuideInfo": {
+      "androidAppScheme": "com.bmwgroup.driversguide.row",
+      "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row",
+      "iosAppScheme": "bmwdriversguide:///open",
+      "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8",
+      "title": "BMW\nDriver's Guide"
+    },
+    "exFactoryILevel": "S18A-21-03-563",
+    "exFactoryPUStep": "0321",
+    "headUnit": "MGU",
+    "hmiVersion": "id7",
+    "iStep": "S18A-21-07-550",
+    "isLscSupported": true,
+    "isMappingPending": false,
+    "isMappingUnconfirmed": false,
+    "model": "X5 xDrive45e",
+    "properties": {
+      "areDoorsClosed": true,
+      "areDoorsLocked": true,
+      "areDoorsOpen": false,
+      "areWindowsClosed": true,
+      "chargingState": {
+        "chargePercentage": 80,
+        "isChargerConnected": true,
+        "state": "CHARGING",
+        "type": "NOT_AVAILABLE"
+      },
+      "checkControlMessages": [],
+      "climateControl": {
+        "activity": "INACTIVE"
+      },
+      "combinedRange": {
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 466
+        }
+      },
+      "combustionRange": {
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 466
+        }
+      },
+      "doorsAndWindows": {
+        "doors": {
+          "driverFront": "CLOSED",
+          "driverRear": "CLOSED",
+          "passengerFront": "CLOSED",
+          "passengerRear": "CLOSED"
+        },
+        "hood": "CLOSED",
+        "moonroof": "CLOSED",
+        "trunk": "CLOSED",
+        "windows": {
+          "driverFront": "CLOSED",
+          "driverRear": "CLOSED",
+          "passengerFront": "CLOSED",
+          "passengerRear": "CLOSED"
+        }
+      },
+      "electricRange": {
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 48
+        }
+      },
+      "electricRangeAndStatus": {
+        "chargePercentage": 80,
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 48
+        }
+      },
+      "fuelLevel": {
+        "units": "LITERS",
+        "value": 47
+      },
+      "fuelPercentage": {
+        "value": 74
+      },
+      "inMotion": false,
+      "isServiceRequired": false,
+      "lastUpdatedAt": "2021-11-10T22:21:39Z",
+      "originCountryISO": "BE",
+      "serviceRequired": [
+        {
+          "dateTime": "2023-06-01T00:00:00.000Z",
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 32000
+          },
+          "status": "OK",
+          "type": "OIL"
+        },
+        {
+          "dateTime": "2024-06-01T00:00:00.000Z",
+          "status": "OK",
+          "type": "BRAKE_FLUID"
+        },
+        {
+          "dateTime": "2025-06-01T00:00:00.000Z",
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 60000
+          },
+          "status": "OK",
+          "type": "VEHICLE_CHECK"
+        }
+      ],
+      "vehicleLocation": {
+        "address": {
+          "formatted": "some_formatted_address"
+        },
+        "coordinates": {
+          "latitude": 12.3456,
+          "longitude": 34.5678
+        },
+        "heading": 123
+      }
+    },
+    "puStep": "0721",
+    "status": {
+      "chargingProfile": {
+        "chargingControlType": "weeklyPlanner",
+        "chargingMode": "immediateCharging",
+        "chargingPreference": "noPreSelection",
+        "chargingSettings": {
+          "hospitality": "NO_ACTION",
+          "idcc": "NO_ACTION",
+          "isAcCurrentLimitActive": false,
+          "targetSoc": 100
+        },
+        "climatisationOn": true,
+        "departureTimes": [
+          {
+            "action": "deactivate",
+            "id": 1,
+            "timerWeekDays": []
+          },
+          {
+            "action": "activate",
+            "id": 2,
+            "timeStamp": {
+              "hour": 8,
+              "minute": 10
+            },
+            "timerWeekDays": [
+              "monday",
+              "tuesday",
+              "wednesday",
+              "thursday",
+              "friday"
+            ]
+          },
+          {
+            "action": "deactivate",
+            "id": 3,
+            "timerWeekDays": []
+          },
+          {
+            "action": "deactivate",
+            "id": 4,
+            "timeStamp": {
+              "hour": 8,
+              "minute": 10
+            },
+            "timerWeekDays": [
+              "thursday"
+            ]
+          }
+        ],
+        "reductionOfChargeCurrent": {
+          "end": {
+            "hour": 0,
+            "minute": 0
+          },
+          "start": {
+            "hour": 0,
+            "minute": 0
+          }
+        }
+      },
+      "checkControlMessages": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60197,
+          "state": "OK",
+          "title": "Engine Oil"
+        }
+      ],
+      "checkControlMessagesGeneralState": "No Issues",
+      "currentMileage": {
+        "formattedMileage": "2667",
+        "mileage": 2667,
+        "units": "km"
+      },
+      "doorsAndWindows": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59722,
+          "state": "Closed",
+          "title": "All doors and windows"
+        }
+      ],
+      "doorsGeneralState": "Locked",
+      "fuelIndicators": [
+        {
+          "chargingType": null,
+          "iconOpacity": "high",
+          "infoIconId": 59691,
+          "infoLabel": "Combined Range",
+          "isCircleIcon": false,
+          "isInaccurate": false,
+          "levelIconId": null,
+          "levelUnits": null,
+          "levelValue": null,
+          "mainBarValue": 0,
+          "rangeIconId": 59691,
+          "rangeUnits": "km",
+          "rangeValue": "466",
+          "secondaryBarValue": 0,
+          "showsBar": false
+        },
+        {
+          "barType": null,
+          "chargingStatusIndicatorType": "CHARGING",
+          "chargingStatusType": "CHARGING",
+          "chargingType": "charging",
+          "iconOpacity": "high",
+          "infoIconId": 59689,
+          "infoLabel": "100% at ~03:53 AM",
+          "isCircleIcon": true,
+          "isInaccurate": true,
+          "levelIconId": 59689,
+          "levelUnits": "%",
+          "levelValue": "80",
+          "mainBarValue": 80,
+          "rangeIconId": 59683,
+          "rangeUnits": "km",
+          "rangeValue": "48",
+          "secondaryBarValue": 0,
+          "showBarGoal": false,
+          "showsBar": true
+        },
+        {
+          "chargingType": null,
+          "iconOpacity": "high",
+          "infoIconId": 59930,
+          "infoLabel": "Fuel Level",
+          "isCircleIcon": false,
+          "isInaccurate": false,
+          "levelIconId": 59682,
+          "levelUnits": "%",
+          "levelValue": "74",
+          "mainBarValue": 74,
+          "rangeIconId": 59681,
+          "rangeUnits": "km",
+          "rangeValue": "418",
+          "secondaryBarValue": 0,
+          "showsBar": true
+        }
+      ],
+      "issues": {},
+      "lastUpdatedAt": "2021-11-10T22:21:39Z",
+      "recallExternalUrl": null,
+      "recallMessages": [],
+      "requiredServices": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60197,
+          "id": "Oil",
+          "longDescription": "Next service due after the specified distance or date.",
+          "subtitle": "Due in June 2023 or 32000 km",
+          "title": "Engine oil"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60223,
+          "id": "BrakeFluid",
+          "longDescription": "Next service due by the specified date.",
+          "subtitle": "Due in June 2024",
+          "title": "Brake fluid"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60215,
+          "id": "VehicleCheck",
+          "longDescription": "Next vehicle check due after the specified distance or date.",
+          "subtitle": "Due in June 2025 or 60000 km",
+          "title": "Vehicle check"
+        }
+      ],
+      "timestampMessage": "Updated from vehicle 11/10/2021 11:21 PM"
+    },
+    "telematicsUnit": "ATM02",
+    "themeSpecs": {
+      "vehicleStatusBackgroundColor": {
+        "blue": 77,
+        "green": 38,
+        "red": 31
+      }
+    },
+    "vin": "some_vin_G05",
+    "year": 2021
+  }
+]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G08/vehicles_v2_bmw_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G08/vehicles_v2_bmw_0.json
new file mode 100644 (file)
index 0000000..e0f89db
--- /dev/null
@@ -0,0 +1,401 @@
+[
+  {
+    "a4aType": "NOT_SUPPORTED",
+    "bodyType": "G08",
+    "brand": "BMW",
+    "capabilities": {
+      "canRemoteHistoryBeDeleted": false,
+      "climateNow": {
+        "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.",
+        "executionPopup": {
+          "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.",
+          "iconId": 59733,
+          "popupType": "DIALOG",
+          "primaryButtonText": "Start",
+          "secondaryButtonText": "Cancel",
+          "title": "Start Climatization"
+        },
+        "executionStopPopup": {
+          "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.",
+          "title": "Climate control is running"
+        },
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "horn": {
+        "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "isBmwChargingSupported": true,
+      "isCarSharingSupported": false,
+      "isChargeNowForBusinessSupported": false,
+      "isChargingHistorySupported": true,
+      "isChargingHospitalityEnabled": false,
+      "isChargingLoudnessEnable": false,
+      "isChargingPlanSupported": true,
+      "isChargingPowerLimitEnable": false,
+      "isChargingSettingsEnabled": false,
+      "isChargingTargetSocEnable": false,
+      "isCustomerEsimSupported": false,
+      "isDCSContractManagementSupported": true,
+      "isDataPrivacyEnabled": false,
+      "isEasyChargeSupported": false,
+      "isEvGoChargingSupported": false,
+      "isFindChargingEnabled": true,
+      "isMiniChargingSupported": false,
+      "isRemoteHistorySupported": true,
+      "isRemoteServicesActivationRequired": false,
+      "isRemoteServicesBookingRequired": false,
+      "isScanAndChargeSupported": true,
+      "lastStateCall": {
+        "isNonLscFeatureEnabled": false,
+        "lscState": "ACTIVATED"
+      },
+      "lights": {
+        "executionMessage": "Flash headlights now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "lock": {
+        "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "remote360": {
+        "isComingSoonEnabled": false,
+        "isDataPrivacyEnabled": true,
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false,
+        "isToggleEnabled": true
+      },
+      "remoteSoftwareUpgrade": {
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "sendPoi": {
+        "executionMessage": "Send POI now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "unlock": {
+        "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": true
+      },
+      "vehicleFinder": {
+        "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      }
+    },
+    "connectedDriveServices": [
+      "WIFI_HOTSPOT_SERVICE"
+    ],
+    "driveTrain": "ELECTRIC",
+    "driverGuideInfo": {
+      "androidAppScheme": "com.bmwgroup.driversguide.row",
+      "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row",
+      "iosAppScheme": "bmwdriversguide:///open",
+      "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8",
+      "title": "BMW\nDriver's Guide"
+    },
+    "exFactoryILevel": "S15C-20-11-542",
+    "exFactoryPUStep": "1120",
+    "headUnit": "MGU",
+    "hmiVersion": "id7",
+    "iStep": "S15C-20-11-542",
+    "isLscSupported": true,
+    "isMappingPending": false,
+    "isMappingUnconfirmed": false,
+    "model": "iX3",
+    "properties": {
+      "areDoorsClosed": true,
+      "areDoorsLocked": true,
+      "areDoorsOpen": false,
+      "areWindowsClosed": true,
+      "chargingState": {
+        "chargePercentage": 50,
+        "isChargerConnected": true,
+        "state": "CHARGING",
+        "type": "NOT_AVAILABLE"
+      },
+      "checkControlMessages": [],
+      "climateControl": {
+        "activity": "INACTIVE"
+      },
+      "combustionRange": {
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 186
+        }
+      },
+      "doorsAndWindows": {
+        "doors": {
+          "driverFront": "CLOSED",
+          "driverRear": "CLOSED",
+          "passengerFront": "CLOSED",
+          "passengerRear": "CLOSED"
+        },
+        "hood": "CLOSED",
+        "moonroof": "CLOSED",
+        "trunk": "CLOSED",
+        "windows": {
+          "driverFront": "CLOSED",
+          "driverRear": "CLOSED",
+          "passengerFront": "CLOSED",
+          "passengerRear": "CLOSED"
+        }
+      },
+      "electricRange": {
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 179
+        }
+      },
+      "electricRangeAndStatus": {
+        "chargePercentage": 50,
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 179
+        }
+      },
+      "fuelLevel": {
+        "units": "LITERS",
+        "value": 0
+      },
+      "fuelPercentage": {
+        "value": 0
+      },
+      "inMotion": false,
+      "isServiceRequired": false,
+      "lastUpdatedAt": "2021-11-14T15:08:24Z",
+      "originCountryISO": "NL",
+      "serviceRequired": [
+        {
+          "dateTime": "2023-06-01T00:00:00.000Z",
+          "status": "OK",
+          "type": "BRAKE_FLUID"
+        },
+        {
+          "dateTime": "2023-06-01T00:00:00.000Z",
+          "status": "OK",
+          "type": "VEHICLE_CHECK"
+        },
+        {
+          "dateTime": "2025-06-01T00:00:00.000Z",
+          "status": "OK",
+          "type": "VEHICLE_TUV"
+        }
+      ],
+      "tires": {
+        "frontLeft": {
+          "status": {
+            "currentPressure": 220,
+            "localizedCurrentPressure": "2.2 bar",
+            "localizedTargetPressure": "2.3 bar",
+            "targetPressure": 231
+          }
+        },
+        "frontRight": {
+          "status": {
+            "currentPressure": 222,
+            "localizedCurrentPressure": "2.2 bar",
+            "localizedTargetPressure": "2.3 bar",
+            "targetPressure": 233
+          }
+        },
+        "rearLeft": {
+          "status": {
+            "currentPressure": 264,
+            "localizedCurrentPressure": "2.6 bar",
+            "localizedTargetPressure": "2.6 bar",
+            "targetPressure": 265
+          }
+        },
+        "rearRight": {
+          "status": {
+            "currentPressure": 266,
+            "localizedCurrentPressure": "2.6 bar",
+            "localizedTargetPressure": "2.6 bar",
+            "targetPressure": 267
+          }
+        }
+      },
+      "vehicleLocation": {
+        "address": {
+          "formatted": "some_formatted_address"
+        },
+        "coordinates": {
+          "latitude": 12.3456,
+          "longitude": 34.5678
+        },
+        "heading": 123
+      }
+    },
+    "puStep": "1120",
+    "status": {
+      "chargingProfile": {
+        "chargingControlType": "weeklyPlanner",
+        "chargingMode": "immediateCharging",
+        "chargingPreference": "noPreSelection",
+        "chargingSettings": {
+          "hospitality": "NO_ACTION",
+          "idcc": "AUTOMATIC_INTELLIGENT",
+          "isAcCurrentLimitActive": false,
+          "targetSoc": 100
+        },
+        "climatisationOn": true,
+        "departureTimes": [
+          {
+            "action": "deactivate",
+            "id": 1,
+            "timerWeekDays": []
+          },
+          {
+            "action": "deactivate",
+            "id": 2,
+            "timerWeekDays": []
+          },
+          {
+            "action": "deactivate",
+            "id": 3,
+            "timerWeekDays": []
+          },
+          {
+            "action": "deactivate",
+            "id": 4,
+            "timerWeekDays": []
+          }
+        ],
+        "reductionOfChargeCurrent": {
+          "end": {
+            "hour": 0,
+            "minute": 0
+          },
+          "start": {
+            "hour": 0,
+            "minute": 0
+          }
+        }
+      },
+      "checkControlMessages": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60117,
+          "state": "OK",
+          "title": "Tires"
+        }
+      ],
+      "checkControlMessagesGeneralState": "No Issues",
+      "currentMileage": {
+        "formattedMileage": "9527",
+        "mileage": 9527,
+        "units": "km"
+      },
+      "doorsAndWindows": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59757,
+          "state": "Locked",
+          "title": "Lock status"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59722,
+          "state": "Closed",
+          "title": "All doors"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59725,
+          "state": "Closed",
+          "title": "All windows"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59706,
+          "state": "Closed",
+          "title": "Hood"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59704,
+          "state": "Closed",
+          "title": "Trunk"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59705,
+          "state": "Closed",
+          "title": "Sunroof"
+        }
+      ],
+      "doorsGeneralState": "Locked",
+      "fuelIndicators": [
+        {
+          "barType": null,
+          "chargingStatusIndicatorType": "CHARGING",
+          "chargingStatusType": "CHARGING",
+          "chargingType": "charging",
+          "iconOpacity": "high",
+          "infoIconId": 59689,
+          "infoLabel": "100% at ~04:01 AM",
+          "isCircleIcon": true,
+          "isInaccurate": true,
+          "levelIconId": 59689,
+          "levelUnits": "%",
+          "levelValue": "50",
+          "mainBarValue": 50,
+          "rangeIconId": 59683,
+          "rangeUnits": "km",
+          "rangeValue": "179",
+          "secondaryBarValue": 0,
+          "showBarGoal": false,
+          "showsBar": true
+        }
+      ],
+      "issues": {},
+      "lastUpdatedAt": "2021-11-14T15:08:24Z",
+      "recallExternalUrl": null,
+      "recallMessages": [],
+      "requiredServices": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60223,
+          "id": "BrakeFluid",
+          "longDescription": "Next service due by the specified date.",
+          "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+          "title": "Brake fluid"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60215,
+          "id": "VehicleCheck",
+          "longDescription": "Next vehicle check due after the specified distance or date.",
+          "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+          "title": "Vehicle check"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60111,
+          "id": "VehicleAdmissionTest",
+          "longDescription": "Next state inspection due by the specified date.",
+          "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+          "title": "Vehicle Inspection"
+        }
+      ],
+      "timestampMessage": "Updated from vehicle 11/14/2021 04:08 PM"
+    },
+    "telematicsUnit": "ATM02",
+    "themeSpecs": {
+      "vehicleStatusBackgroundColor": {
+        "blue": 173,
+        "green": 173,
+        "red": 173
+      }
+    },
+    "vin": "some_vin_G08",
+    "year": 2021
+  }
+]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/340i.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/340i.json
new file mode 100644 (file)
index 0000000..d7d11ad
--- /dev/null
@@ -0,0 +1,401 @@
+[
+    {
+        "vin": "anonymous",
+        "model": "M340i xDrive",
+        "year": 2021,
+        "brand": "BMW",
+        "headUnit": "MGU",
+        "isLscSupported": true,
+        "driveTrain": "HYBRID",
+        "puStep": "0721",
+        "iStep": "S18A-21-07-550",
+        "telematicsUnit": "ATM02",
+        "hmiVersion": "id7",
+        "bodyType": "G21",
+        "a4aType": "NOT_SUPPORTED",
+        "capabilities": {
+            "isRemoteServicesBookingRequired": false,
+            "isRemoteServicesActivationRequired": false,
+            "lock": {
+                "isEnabled": true,
+                "isPinAuthenticationRequired": false,
+                "executionMessage": "Jetzt Ihr Fahrzeug verriegeln? Remote-Funktionen können einige Sekunden dauern."
+            },
+            "unlock": {
+                "isEnabled": true,
+                "isPinAuthenticationRequired": true,
+                "executionMessage": "Jetzt Ihr Fahrzeug entriegeln? Remote-Funktionen können einige Sekunden dauern."
+            },
+            "lights": {
+                "isEnabled": true,
+                "isPinAuthenticationRequired": false,
+                "executionMessage": "Jetzt Scheinwerfer aufleuchten lassen? Remote-Funktionen können einige Sekunden dauern."
+            },
+            "horn": {
+                "isEnabled": true,
+                "isPinAuthenticationRequired": false,
+                "executionMessage": "Hupen ist in vielen Ländern nur in bestimmten Situationen erlaubt. Die Verantwortung für den Einsatz und die Einhaltung der jeweils geltenden Bestimmungen liegt allein bei Ihnen als Nutzer. \n\nJetzt hupen? Remote-Funktionen können einige Sekunden dauern."
+            },
+            "vehicleFinder": {
+                "isEnabled": true,
+                "isPinAuthenticationRequired": false,
+                "executionMessage": "Jetzt Ihr Fahrzeug finden? Remote-Funktionen können einige Sekunden dauern."
+            },
+            "speechThirdPartyAlexa": {
+                "isEnabled": true,
+                "isPinAuthenticationRequired": false,
+                "executionMessage": "Jetzt Alexa aktivieren? Remote-Funktionen können einige Sekunden dauern."
+            },
+            "sendPoi": {
+                "isEnabled": true,
+                "isPinAuthenticationRequired": false,
+                "executionMessage": "Jetzt POI senden? Remote-Funktionen können einige Sekunden dauern."
+            },
+            "lastStateCall": {
+                "isNonLscFeatureEnabled": false,
+                "lscState": "ACTIVATED"
+            },
+            "climateNow": {
+                "isEnabled": true,
+                "isPinAuthenticationRequired": false,
+                "executionMessage": "Jetzt belüften? Remote-Funktionen können einige Sekunden dauern.",
+                "executionPopup": {
+                    "executionMessage": "Jetzt belüften? Remote-Funktionen können einige Sekunden dauern.",
+                    "popupType": "DIALOG",
+                    "title": "Belüftung starten",
+                    "primaryButtonText": "Start",
+                    "secondaryButtonText": "Abbrechen",
+                    "iconId": 59733
+                },
+                "executionStopPopup": {
+                    "executionMessage": "Jetzt Klimatisierung Ihres Fahrzeugs beenden? Remote-Funktionen können einige Sekunden dauern.",
+                    "title": "Klimatisierung läuft"
+                }
+            },
+            "isRemoteHistorySupported": true,
+            "canRemoteHistoryBeDeleted": false,
+            "remoteSoftwareUpgrade": {
+                "isEnabled": true,
+                "isPinAuthenticationRequired": false
+            },
+            "climateTimer": {
+                "isEnabled": true,
+                "isPinAuthenticationRequired": false,
+                "tile": {
+                    "iconId": 59774,
+                    "title": "Belüftungstimer",
+                    "description": "Abfahrtszeit einstellen"
+                },
+                "page": {
+                    "primaryButtonText": "AN FAHRZEUG SENDEN",
+                    "secondaryButtonText": "DEAKTIVIEREN UND AN FAHRZEUG SENDEN",
+                    "title": "Belüftungstimer",
+                    "subtitle": "Abfahrtszeit einstellen",
+                    "description": "Durch das Einstellen einer Abfahrtszeit teilen Sie dem Fahrzeug mit, wann Sie es benutzen wollen."
+                },
+                "isToggleEnabled": true
+            },
+            "isChargingHistorySupported": false,
+            "isScanAndChargeSupported": false,
+            "isDCSContractManagementSupported": false,
+            "isBmwChargingSupported": false,
+            "isMiniChargingSupported": false,
+            "isChargeNowForBusinessSupported": false,
+            "isDataPrivacyEnabled": false,
+            "isChargingPlanSupported": false,
+            "isChargingPowerLimitEnable": false,
+            "isChargingTargetSocEnable": false,
+            "isChargingLoudnessEnable": false,
+            "isChargingSettingsEnabled": false,
+            "isChargingHospitalityEnabled": false,
+            "isEvGoChargingSupported": false,
+            "isFindChargingEnabled": false,
+            "isCustomerEsimSupported": false,
+            "isCarSharingSupported": false,
+            "isEasyChargeSupported": false,
+            "isSustainabilitySupported": false
+        },
+        "connectedDriveServices": [],
+        "properties": {
+            "lastUpdatedAt": "2022-03-01T07:00:27Z",
+            "inMotion": false,
+            "areDoorsLocked": true,
+            "originCountryISO": "DE",
+            "areDoorsClosed": true,
+            "areDoorsOpen": false,
+            "areWindowsClosed": true,
+            "doorsAndWindows": {
+                "doors": {
+                    "driverFront": "CLOSED",
+                    "driverRear": "CLOSED",
+                    "passengerFront": "CLOSED",
+                    "passengerRear": "CLOSED"
+                },
+                "windows": {
+                    "driverFront": "CLOSED",
+                    "driverRear": "CLOSED",
+                    "passengerFront": "CLOSED",
+                    "passengerRear": "CLOSED"
+                },
+                "trunk": "CLOSED",
+                "hood": "CLOSED",
+                "moonroof": "CLOSED"
+            },
+            "tires": {
+                "frontLeft": {
+                    "status": {
+                        "currentPressure": 280,
+                        "localizedCurrentPressure": "2,8 bar",
+                        "targetPressure": 290,
+                        "localizedTargetPressure": "2,9 bar",
+                        "wear": 0
+                    }
+                },
+                "frontRight": {
+                    "status": {
+                        "currentPressure": 280,
+                        "localizedCurrentPressure": "2,8 bar",
+                        "targetPressure": 290,
+                        "localizedTargetPressure": "2,9 bar",
+                        "wear": 0
+                    }
+                },
+                "rearLeft": {
+                    "status": {
+                        "currentPressure": 280,
+                        "localizedCurrentPressure": "2,8 bar",
+                        "targetPressure": 290,
+                        "localizedTargetPressure": "2,9 bar",
+                        "wear": 0
+                    }
+                },
+                "rearRight": {
+                    "status": {
+                        "currentPressure": 280,
+                        "localizedCurrentPressure": "2,8 bar",
+                        "targetPressure": 290,
+                        "localizedTargetPressure": "2,9 bar",
+                        "wear": 0
+                    }
+                }
+            },
+            "isServiceRequired": false,
+            "fuelLevel": {
+                "value": 36,
+                "units": "LITERS"
+            },
+            "fuelPercentage": {
+                "value": 69
+            },
+            "combustionRange": {
+                "distance": {
+                    "value": 404,
+                    "units": "KILOMETERS"
+                }
+            },
+            "checkControlMessages": [],
+            "serviceRequired": [
+                {
+                    "type": "OIL",
+                    "status": "OK",
+                    "dateTime": "2023-06-01T00:00:00.000Z",
+                    "distance": {
+                        "value": 29000,
+                        "units": "KILOMETERS"
+                    }
+                },
+                {
+                    "type": "BRAKE_FLUID",
+                    "status": "OK",
+                    "dateTime": "2024-06-01T00:00:00.000Z"
+                },
+                {
+                    "type": "VEHICLE_TUV",
+                    "status": "OK",
+                    "dateTime": "2024-08-01T00:00:00.000Z"
+                },
+                {
+                    "type": "VEHICLE_CHECK",
+                    "status": "OK",
+                    "dateTime": "2025-06-01T00:00:00.000Z",
+                    "distance": {
+                        "value": 60000,
+                        "units": "KILOMETERS"
+                    }
+                },
+                {
+                    "type": "TIRE_WEAR_FRONT",
+                    "status": "OK"
+                },
+                {
+                    "type": "TIRE_WEAR_REAR",
+                    "status": "OK"
+                }
+            ],
+            "vehicleLocation": {
+                "coordinates": {
+                    "latitude": 1.1,
+                    "longitude": 2.2
+                },
+                "address": {
+                    "formatted": "anonymous"
+                },
+                "heading": -1
+            },
+            "climateControl": {
+                "activity": "INACTIVE"
+            }
+        },
+        "isMappingPending": false,
+        "isMappingUnconfirmed": false,
+        "driverGuideInfo": {
+            "title": "BMW\nDriver's Guide",
+            "androidAppScheme": "com.bmwgroup.driversguide.row",
+            "iosAppScheme": "bmwdriversguide:///open",
+            "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row",
+            "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8"
+        },
+        "themeSpecs": {
+            "vehicleStatusBackgroundColor": {
+                "red": 40,
+                "green": 94,
+                "blue": 201
+            }
+        },
+        "status": {
+            "lastUpdatedAt": "2022-03-01T07:00:27Z",
+            "currentMileage": {
+                "mileage": 4955,
+                "units": "km",
+                "formattedMileage": "4.955"
+            },
+            "issues": null,
+            "doorsGeneralState": "Verriegelt",
+            "checkControlMessagesGeneralState": "Keine Probleme",
+            "doorsAndWindows": [
+                {
+                    "iconId": 59757,
+                    "title": "Verriegelungsstatus",
+                    "state": "Verriegelt",
+                    "criticalness": "nonCritical"
+                },
+                {
+                    "iconId": 59722,
+                    "title": "Alle Türen",
+                    "state": "Geschlossen",
+                    "criticalness": "nonCritical"
+                },
+                {
+                    "iconId": 59725,
+                    "title": "Alle Fenster",
+                    "state": "Geschlossen",
+                    "criticalness": "nonCritical"
+                },
+                {
+                    "iconId": 59706,
+                    "title": "Frontklappe",
+                    "state": "Geschlossen",
+                    "criticalness": "nonCritical"
+                },
+                {
+                    "iconId": 59704,
+                    "title": "Gepäckraum",
+                    "state": "Geschlossen",
+                    "criticalness": "nonCritical"
+                },
+                {
+                    "iconId": 59705,
+                    "title": "Glasdach",
+                    "state": "Geschlossen",
+                    "criticalness": "nonCritical"
+                }
+            ],
+            "checkControlMessages": [
+                {
+                    "criticalness": "nonCritical",
+                    "iconId": 60117,
+                    "title": "Reifen",
+                    "state": "OK"
+                },
+                {
+                    "criticalness": "nonCritical",
+                    "iconId": 60197,
+                    "title": "Motoröl",
+                    "state": "OK"
+                }
+            ],
+            "requiredServices": [
+                {
+                    "id": "Oil",
+                    "title": "Motoröl",
+                    "iconId": 60197,
+                    "longDescription": "Nächster Service nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+                    "subtitle": "Fällig im Juni 2023 oder in 29.000 km",
+                    "criticalness": "nonCritical"
+                },
+                {
+                    "id": "BrakeFluid",
+                    "title": "Bremsflüssigkeit",
+                    "iconId": 60223,
+                    "longDescription": "Nächster Wechsel spätestens zum angegebenen Termin.",
+                    "subtitle": "Fällig im Juni 2024",
+                    "criticalness": "nonCritical"
+                },
+                {
+                    "id": "VehicleAdmissionTest",
+                    "title": "Fahrzeuginspektion (HU)",
+                    "iconId": 60111,
+                    "longDescription": "Nächste gesetzliche Fahrzeuguntersuchung zum angegebenen Termin.",
+                    "subtitle": "Fällig im August 2024",
+                    "criticalness": "nonCritical"
+                },
+                {
+                    "id": "VehicleCheck",
+                    "title": "Fahrzeug-Check",
+                    "iconId": 60215,
+                    "longDescription": "Nächste Sichtprüfung zum angegebenen Termin oder nach der ggf. angegebenen Fahrstrecke.",
+                    "subtitle": "Fällig im Juni 2025 oder in 60.000 km",
+                    "criticalness": "nonCritical"
+                },
+                {
+                    "id": "TireWearFront",
+                    "title": "Reifenservice Vorderreifen",
+                    "iconId": 60447,
+                    "subtitle": "OK",
+                    "criticalness": "nonCritical"
+                },
+                {
+                    "id": "TireWearRear",
+                    "title": "Reifenservice Hinterreifen",
+                    "iconId": 60447,
+                    "subtitle": "OK",
+                    "criticalness": "nonCritical"
+                }
+            ],
+            "recallMessages": [],
+            "recallExternalUrl": null,
+            "fuelIndicators": [
+                {
+                    "secondaryBarValue": 0,
+                    "infoIconId": 59930,
+                    "infoLabel": "Tankfüllstand",
+                    "rangeIconId": 59681,
+                    "rangeUnits": "km",
+                    "rangeValue": "404",
+                    "levelIconId": 59682,
+                    "isCircleIcon": false,
+                    "iconOpacity": "high",
+                    "chargingType": null,
+                    "mainBarValue": 69,
+                    "showsBar": true,
+                    "levelUnits": "%",
+                    "levelValue": "69",
+                    "isInaccurate": false
+                }
+            ],
+            "timestampMessage": "Aktualisiert vom Fahrzeug 1.3.2022 08:00 AM"
+        },
+        "exFactoryPUStep": "0321",
+        "exFactoryILevel": "S18A-21-03-555"
+    }
+]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/charging-sessions_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/charging-sessions_0.json
new file mode 100644 (file)
index 0000000..8c5769a
--- /dev/null
@@ -0,0 +1,103 @@
+{
+  "chargingSessions": {
+    "chargingListState": "HAS_SESSIONS",
+    "numberOfSessions": "11",
+    "sessions": [
+      {
+        "energyCharged": "~ 6 kWh",
+        "id": "2021-11-14T16:42:29Z_39df2d52",
+        "isPublic": true,
+        "sessionStatus": "FINISHED",
+        "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+        "title": "Today 5:42 PM"
+      },
+      {
+        "energyCharged": "~ 11 kWh",
+        "id": "2021-11-13T18:15:24Z_39df2d52",
+        "isPublic": false,
+        "sessionStatus": "FINISHED",
+        "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+        "title": "Yesterday 7:15 PM"
+      },
+      {
+        "energyCharged": "~ 10 kWh",
+        "id": "2021-11-11T18:01:58Z_39df2d52",
+        "isPublic": true,
+        "sessionStatus": "FINISHED",
+        "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+        "title": "Thursday 7:01 PM"
+      },
+      {
+        "energyCharged": "~ 12 kWh",
+        "id": "2021-11-09T20:33:19Z_39df2d52",
+        "isPublic": false,
+        "sessionStatus": "FINISHED",
+        "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+        "title": "Tuesday 9:33 PM"
+      },
+      {
+        "energyCharged": "< 2 kWh",
+        "id": "2021-11-09T12:34:28Z_39df2d52",
+        "isPublic": true,
+        "sessionStatus": "FINISHED",
+        "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+        "title": "Tuesday 1:34 PM"
+      },
+      {
+        "energyCharged": "~ 7 kWh",
+        "id": "2021-11-08T18:31:20Z_39df2d52",
+        "isPublic": false,
+        "sessionStatus": "FINISHED",
+        "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+        "title": "Monday 7:31 PM"
+      },
+      {
+        "energyCharged": "~ 10 kWh",
+        "id": "2021-11-06T16:52:16Z_39df2d52",
+        "isPublic": false,
+        "sessionStatus": "FINISHED",
+        "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+        "title": "11/6/2021 5:52 PM"
+      },
+      {
+        "energyCharged": "~ 10 kWh",
+        "id": "2021-11-04T18:24:22Z_39df2d52",
+        "isPublic": true,
+        "sessionStatus": "FINISHED",
+        "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+        "title": "11/4/2021 7:24 PM"
+      },
+      {
+        "energyCharged": "~ 6 kWh",
+        "id": "2021-11-03T14:24:06Z_39df2d52",
+        "isPublic": false,
+        "sessionStatus": "FINISHED",
+        "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+        "title": "11/3/2021 3:24 PM"
+      },
+      {
+        "energyCharged": "~ 13 kWh",
+        "id": "2021-11-02T19:20:57Z_39df2d52",
+        "isPublic": false,
+        "sessionStatus": "FINISHED",
+        "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+        "title": "11/2/2021 8:20 PM"
+      },
+      {
+        "energyCharged": "< 2 kWh",
+        "id": "2021-11-01T15:04:09Z_39df2d52",
+        "isPublic": false,
+        "sessionStatus": "FINISHED",
+        "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+        "title": "11/1/2021 4:04 PM"
+      }
+    ],
+    "total": "~ 87 kWh"
+  },
+  "datePicker": {
+    "endDate": "2021-11-14T20:20:25Z",
+    "selectedDate": "2021-11-14T16:42:29Z",
+    "startDate": "2020-10-21T14:21:31Z"
+  },
+  "paginationInfo": {}
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/charging-statistics_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/charging-statistics_0.json
new file mode 100644 (file)
index 0000000..a595ee9
--- /dev/null
@@ -0,0 +1,11 @@
+{
+  "description": "November 2021",
+  "optStateType": "OPT_IN_WITH_SESSIONS",
+  "statistics": {
+    "numberOfChargingSessions": 11,
+    "numberOfChargingSessionsSemantics": "mobile20chsChargingSessionNumberSemantics",
+    "symbol": "~",
+    "totalEnergyCharged": 87,
+    "totalEnergyChargedSemantics": "mobile20chsApproximatelyTotalChargedSemantics"
+  }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/json_export.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/json_export.json
new file mode 100644 (file)
index 0000000..7c98cb1
--- /dev/null
@@ -0,0 +1,831 @@
+{
+    "attributes": {
+        "a4aType": "NOT_SUPPORTED",
+        "bodyType": "G21",
+        "brand": "BMW",
+        "capabilities": {
+            "canRemoteHistoryBeDeleted": false,
+            "climateNow": {
+                "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.",
+                "executionPopup": {
+                    "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.",
+                    "iconId": 59733,
+                    "popupType": "DIALOG",
+                    "primaryButtonText": "Start",
+                    "secondaryButtonText": "Cancel",
+                    "title": "Start Climatization"
+                },
+                "executionStopPopup": {
+                    "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.",
+                    "title": "Climate control is running"
+                },
+                "isEnabled": true,
+                "isPinAuthenticationRequired": false
+            },
+            "horn": {
+                "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds.",
+                "isEnabled": true,
+                "isPinAuthenticationRequired": false
+            },
+            "isBmwChargingSupported": true,
+            "isCarSharingSupported": false,
+            "isChargeNowForBusinessSupported": true,
+            "isChargingHistorySupported": true,
+            "isChargingHospitalityEnabled": false,
+            "isChargingLoudnessEnable": false,
+            "isChargingPlanSupported": true,
+            "isChargingPowerLimitEnable": false,
+            "isChargingSettingsEnabled": false,
+            "isChargingTargetSocEnable": false,
+            "isCustomerEsimSupported": false,
+            "isDCSContractManagementSupported": true,
+            "isDataPrivacyEnabled": false,
+            "isEasyChargeSupported": false,
+            "isEvGoChargingSupported": false,
+            "isFindChargingEnabled": true,
+            "isMiniChargingSupported": false,
+            "isRemoteHistorySupported": true,
+            "isRemoteServicesActivationRequired": false,
+            "isRemoteServicesBookingRequired": false,
+            "isScanAndChargeSupported": true,
+            "lastStateCall": {
+                "isNonLscFeatureEnabled": false,
+                "lscState": "ACTIVATED"
+            },
+            "lights": {
+                "executionMessage": "Flash headlights now? Remote functions may take a few seconds.",
+                "isEnabled": true,
+                "isPinAuthenticationRequired": false
+            },
+            "lock": {
+                "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.",
+                "isEnabled": true,
+                "isPinAuthenticationRequired": false
+            },
+            "remote360": {
+                "isComingSoonEnabled": false,
+                "isDataPrivacyEnabled": false,
+                "isEnabled": true,
+                "isPinAuthenticationRequired": false,
+                "isToggleEnabled": true
+            },
+            "remoteSoftwareUpgrade": {
+                "isEnabled": true,
+                "isPinAuthenticationRequired": false
+            },
+            "sendPoi": {
+                "executionMessage": "Send POI now? Remote functions may take a few seconds.",
+                "isEnabled": true,
+                "isPinAuthenticationRequired": false
+            },
+            "speechThirdPartyAlexa": {
+                "executionMessage": "Activate Alexa now? Remote functions may take a few seconds.",
+                "isEnabled": true,
+                "isPinAuthenticationRequired": false
+            },
+            "unlock": {
+                "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.",
+                "isEnabled": true,
+                "isPinAuthenticationRequired": true
+            },
+            "vehicleFinder": {
+                "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.",
+                "isEnabled": true,
+                "isPinAuthenticationRequired": false
+            }
+        },
+        "connectedDriveServices": [
+            "WIFI_HOTSPOT_SERVICE"
+        ],
+        "driveTrain": "PLUGIN_HYBRID",
+        "driverGuideInfo": {
+            "androidAppScheme": "com.bmwgroup.driversguide.row",
+            "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row",
+            "iosAppScheme": "bmwdriversguide:///open",
+            "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8",
+            "title": "BMW\nDriver's Guide"
+        },
+        "exFactoryILevel": "S18A-20-07-548",
+        "exFactoryPUStep": "0720",
+        "headUnit": "MGU",
+        "hmiVersion": "id7",
+        "iStep": "S18A-21-07-550",
+        "isLscSupported": true,
+        "isMappingPending": false,
+        "isMappingUnconfirmed": false,
+        "model": "330e xDrive",
+        "puStep": "0721",
+        "telematicsUnit": "ATM02",
+        "themeSpecs": {
+            "vehicleStatusBackgroundColor": {
+                "blue": 201,
+                "green": 94,
+                "red": 40
+            }
+        },
+        "vin": "some_vin_G21",
+        "year": 2020
+    },
+    "status": {
+        "status": {
+            "chargingProfile": {
+                "chargingControlType": "weeklyPlanner",
+                "chargingMode": "immediateCharging",
+                "chargingPreference": "noPreSelection",
+                "chargingSettings": {
+                    "hospitality": "NO_ACTION",
+                    "idcc": "NO_ACTION",
+                    "isAcCurrentLimitActive": false,
+                    "targetSoc": 100
+                },
+                "climatisationOn": false,
+                "departureTimes": [
+                    {
+                        "action": "deactivate",
+                        "id": 1,
+                        "timeStamp": {
+                            "hour": 0,
+                            "minute": 0
+                        },
+                        "timerWeekDays": []
+                    },
+                    {
+                        "action": "deactivate",
+                        "id": 2,
+                        "timeStamp": {
+                            "hour": 0,
+                            "minute": 0
+                        },
+                        "timerWeekDays": []
+                    },
+                    {
+                        "action": "deactivate",
+                        "id": 3,
+                        "timeStamp": {
+                            "hour": 0,
+                            "minute": 0
+                        },
+                        "timerWeekDays": []
+                    },
+                    {
+                        "action": "deactivate",
+                        "id": 4,
+                        "timerWeekDays": []
+                    }
+                ],
+                "reductionOfChargeCurrent": {
+                    "end": {
+                        "hour": 0,
+                        "minute": 0
+                    },
+                    "start": {
+                        "hour": 0,
+                        "minute": 0
+                    }
+                }
+            },
+            "checkControlMessages": [
+                {
+                    "criticalness": "nonCritical",
+                    "iconId": 60117,
+                    "state": "OK",
+                    "title": "Tires"
+                },
+                {
+                    "criticalness": "nonCritical",
+                    "iconId": 60197,
+                    "state": "OK",
+                    "title": "Engine Oil"
+                }
+            ],
+            "checkControlMessagesGeneralState": "No Issues",
+            "currentMileage": {
+                "formattedMileage": "27138",
+                "mileage": 27138,
+                "units": "km"
+            },
+            "doorsAndWindows": [
+                {
+                    "criticalness": "nonCritical",
+                    "iconId": 59757,
+                    "state": "Locked",
+                    "title": "Lock status"
+                },
+                {
+                    "criticalness": "nonCritical",
+                    "iconId": 59722,
+                    "state": "Closed",
+                    "title": "All doors"
+                },
+                {
+                    "criticalness": "nonCritical",
+                    "iconId": 59725,
+                    "state": "Closed",
+                    "title": "All windows"
+                },
+                {
+                    "criticalness": "nonCritical",
+                    "iconId": 59706,
+                    "state": "Closed",
+                    "title": "Hood"
+                },
+                {
+                    "criticalness": "nonCritical",
+                    "iconId": 59704,
+                    "state": "Closed",
+                    "title": "Trunk"
+                },
+                {
+                    "criticalness": "nonCritical",
+                    "iconId": 59705,
+                    "state": "Closed",
+                    "title": "Sunroof"
+                }
+            ],
+            "doorsGeneralState": "Locked",
+            "fuelIndicators": [
+                {
+                    "chargingType": null,
+                    "iconOpacity": "high",
+                    "infoIconId": 59691,
+                    "infoLabel": "Combined Range",
+                    "isCircleIcon": false,
+                    "isInaccurate": false,
+                    "levelIconId": null,
+                    "levelUnits": null,
+                    "levelValue": null,
+                    "mainBarValue": 0,
+                    "rangeIconId": 59691,
+                    "rangeUnits": "km",
+                    "rangeValue": "368",
+                    "secondaryBarValue": 0,
+                    "showsBar": false
+                },
+                {
+                    "barType": null,
+                    "chargingStatusIndicatorType": "CHARGING",
+                    "chargingStatusType": "CHARGING",
+                    "chargingType": "charging",
+                    "iconOpacity": "high",
+                    "infoIconId": 59689,
+                    "infoLabel": "100% at ~12:43 AM",
+                    "isCircleIcon": true,
+                    "isInaccurate": true,
+                    "levelIconId": 59689,
+                    "levelUnits": "%",
+                    "levelValue": "83",
+                    "mainBarValue": 83,
+                    "rangeIconId": 59683,
+                    "rangeUnits": "km",
+                    "rangeValue": "23",
+                    "secondaryBarValue": 0,
+                    "showBarGoal": false,
+                    "showsBar": true
+                },
+                {
+                    "chargingType": null,
+                    "iconOpacity": "high",
+                    "infoIconId": 59930,
+                    "infoLabel": "Fuel Level",
+                    "isCircleIcon": false,
+                    "isInaccurate": false,
+                    "levelIconId": 59682,
+                    "levelUnits": "%",
+                    "levelValue": "83",
+                    "mainBarValue": 83,
+                    "rangeIconId": 59681,
+                    "rangeUnits": "km",
+                    "rangeValue": "345",
+                    "secondaryBarValue": 0,
+                    "showsBar": true
+                }
+            ],
+            "issues": {},
+            "lastUpdatedAt": "2021-11-14T20:20:21Z",
+            "recallExternalUrl": null,
+            "recallMessages": [],
+            "requiredServices": [
+                {
+                    "criticalness": "nonCritical",
+                    "iconId": 60197,
+                    "id": "Oil",
+                    "longDescription": "Next service due after the specified distance or date.",
+                    "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+                    "title": "Engine oil"
+                },
+                {
+                    "criticalness": "nonCritical",
+                    "iconId": 60215,
+                    "id": "VehicleCheck",
+                    "longDescription": "Next vehicle check due after the specified distance or date.",
+                    "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+                    "title": "Vehicle check"
+                },
+                {
+                    "criticalness": "nonCritical",
+                    "iconId": 60447,
+                    "id": "TireWearFront",
+                    "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+                    "title": "Tire service, front tires"
+                },
+                {
+                    "criticalness": "nonCritical",
+                    "iconId": 60447,
+                    "id": "TireWearRear",
+                    "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+                    "title": "Tire service, rear tires"
+                },
+                {
+                    "criticalness": "nonCritical",
+                    "iconId": 60223,
+                    "id": "BrakeFluid",
+                    "longDescription": "Next service due by the specified date.",
+                    "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+                    "title": "Brake fluid"
+                }
+            ],
+            "timestampMessage": "Updated from vehicle 11/14/2021 09:20 PM"
+        },
+        "properties": {
+            "areDoorsClosed": true,
+            "areDoorsLocked": true,
+            "areDoorsOpen": false,
+            "areWindowsClosed": true,
+            "chargingState": {
+                "chargePercentage": 83,
+                "isChargerConnected": true,
+                "state": "CHARGING",
+                "type": "NOT_AVAILABLE"
+            },
+            "checkControlMessages": [],
+            "climateControl": {
+                "activity": "INACTIVE"
+            },
+            "combinedRange": {
+                "distance": {
+                    "units": "KILOMETERS",
+                    "value": 368
+                }
+            },
+            "combustionRange": {
+                "distance": {
+                    "units": "KILOMETERS",
+                    "value": 368
+                }
+            },
+            "doorsAndWindows": {
+                "doors": {
+                    "driverFront": "CLOSED",
+                    "driverRear": "CLOSED",
+                    "passengerFront": "CLOSED",
+                    "passengerRear": "CLOSED"
+                },
+                "hood": "CLOSED",
+                "moonroof": "CLOSED",
+                "trunk": "CLOSED",
+                "windows": {
+                    "driverFront": "CLOSED",
+                    "driverRear": "CLOSED",
+                    "passengerFront": "CLOSED",
+                    "passengerRear": "CLOSED"
+                }
+            },
+            "electricRange": {
+                "distance": {
+                    "units": "KILOMETERS",
+                    "value": 23
+                }
+            },
+            "electricRangeAndStatus": {
+                "chargePercentage": 83,
+                "distance": {
+                    "units": "KILOMETERS",
+                    "value": 23
+                }
+            },
+            "fuelLevel": {
+                "units": "LITERS",
+                "value": 29
+            },
+            "fuelPercentage": {
+                "value": 83
+            },
+            "inMotion": false,
+            "isServiceRequired": false,
+            "lastUpdatedAt": "2021-11-14T20:20:21Z",
+            "originCountryISO": "DE",
+            "serviceRequired": [
+                {
+                    "dateTime": "2022-10-01T00:00:00.000Z",
+                    "distance": {
+                        "units": "KILOMETERS",
+                        "value": 6000
+                    },
+                    "status": "OK",
+                    "type": "OIL"
+                },
+                {
+                    "dateTime": "2024-10-01T00:00:00.000Z",
+                    "distance": {
+                        "units": "KILOMETERS",
+                        "value": 39000
+                    },
+                    "status": "OK",
+                    "type": "VEHICLE_CHECK"
+                },
+                {
+                    "status": "OK",
+                    "type": "TIRE_WEAR_FRONT"
+                },
+                {
+                    "status": "OK",
+                    "type": "TIRE_WEAR_REAR"
+                },
+                {
+                    "dateTime": "2023-10-01T00:00:00.000Z",
+                    "status": "OK",
+                    "type": "BRAKE_FLUID"
+                }
+            ],
+            "tires": {
+                "frontLeft": {
+                    "details": {
+                        "dimension": "225/45 R18 95V XL",
+                        "manufacturer": "Pirelli",
+                        "manufacturingWeek": "35 / 19",
+                        "maxSpeed": "240 km/h",
+                        "mountingDate": "11/12/2021",
+                        "optimizedForOemBmw": "Yes",
+                        "partNumber": "2461777",
+                        "season": "Winter tires",
+                        "treadDesign": "SOTTOZERO 3"
+                    },
+                    "status": {
+                        "currentPressure": 260,
+                        "localizedCurrentPressure": "2.6 bar",
+                        "localizedTargetPressure": "2.7 bar",
+                        "targetPressure": 270,
+                        "wear": 0
+                    }
+                },
+                "frontRight": {
+                    "details": {
+                        "dimension": "225/45 R18 95V XL",
+                        "manufacturer": "Pirelli",
+                        "manufacturingWeek": "27 / 19",
+                        "maxSpeed": "240 km/h",
+                        "mountingDate": "11/12/2021",
+                        "optimizedForOemBmw": "Yes",
+                        "partNumber": "2461777",
+                        "season": "Winter tires",
+                        "treadDesign": "SOTTOZERO 3"
+                    },
+                    "status": {
+                        "currentPressure": 250,
+                        "localizedCurrentPressure": "2.5 bar",
+                        "localizedTargetPressure": "2.7 bar",
+                        "targetPressure": 270,
+                        "wear": 0
+                    }
+                },
+                "rearLeft": {
+                    "details": {
+                        "dimension": "255/40 R18 99V XL",
+                        "manufacturer": "Pirelli",
+                        "manufacturingWeek": "17 / 19",
+                        "maxSpeed": "240 km/h",
+                        "mountingDate": "11/12/2021",
+                        "optimizedForOemBmw": "Yes",
+                        "partNumber": "2461778",
+                        "season": "Winter tires",
+                        "treadDesign": "SOTTOZERO 3"
+                    },
+                    "status": {
+                        "currentPressure": 300,
+                        "localizedCurrentPressure": "3.0 bar",
+                        "localizedTargetPressure": "2.9 bar",
+                        "targetPressure": 290,
+                        "wear": 0
+                    }
+                },
+                "rearRight": {
+                    "details": {
+                        "dimension": "255/40 R18 99V XL",
+                        "manufacturer": "Pirelli",
+                        "manufacturingWeek": "26 / 19",
+                        "maxSpeed": "240 km/h",
+                        "mountingDate": "11/12/2021",
+                        "optimizedForOemBmw": "Yes",
+                        "partNumber": "2461778",
+                        "season": "Winter tires",
+                        "treadDesign": "SOTTOZERO 3"
+                    },
+                    "status": {
+                        "currentPressure": 250,
+                        "localizedCurrentPressure": "2.5 bar",
+                        "localizedTargetPressure": "2.9 bar",
+                        "targetPressure": 290,
+                        "wear": 0
+                    }
+                }
+            },
+            "vehicleLocation": {
+                "address": {
+                    "formatted": "some_formatted_address"
+                },
+                "coordinates": {
+                    "latitude": 12.3456,
+                    "longitude": 34.5678
+                },
+                "heading": 123
+            }
+        },
+        "all_lids_closed": true,
+        "all_windows_closed": true,
+        "are_all_cbs_ok": true,
+        "are_parking_lights_on": null,
+        "charging_end_time": "2011-11-29 00:43:00+00:00",
+        "charging_level_hv": 83,
+        "charging_start_time": null,
+        "charging_status": "CHARGING",
+        "charging_time_label": "100% at ~12:43 AM",
+        "charging_time_remaining": 3.23,
+        "check_control_messages": [],
+        "condition_based_services": [
+            {
+                "due_date": "2022-10-01 00:00:00+00:00",
+                "state": "OK",
+                "service_type": "OIL",
+                "due_distance": [
+                    6000,
+                    "KILOMETERS"
+                ],
+                "description": null
+            },
+            {
+                "due_date": "2024-10-01 00:00:00+00:00",
+                "state": "OK",
+                "service_type": "VEHICLE_CHECK",
+                "due_distance": [
+                    39000,
+                    "KILOMETERS"
+                ],
+                "description": null
+            },
+            {
+                "due_date": null,
+                "state": "OK",
+                "service_type": "TIRE_WEAR_FRONT",
+                "due_distance": null,
+                "description": null
+            },
+            {
+                "due_date": null,
+                "state": "OK",
+                "service_type": "TIRE_WEAR_REAR",
+                "due_distance": null,
+                "description": null
+            },
+            {
+                "due_date": "2023-10-01 00:00:00+00:00",
+                "state": "OK",
+                "service_type": "BRAKE_FLUID",
+                "due_distance": null,
+                "description": null
+            }
+        ],
+        "connection_status": "CONNECTED",
+        "door_lock_state": "LOCKED",
+        "fuel_indicator_count": 3,
+        "fuel_percent": 83,
+        "gps_heading": 123,
+        "gps_position": [
+            12.3456,
+            34.5678
+        ],
+        "has_check_control_messages": false,
+        "has_parking_light_state": false,
+        "is_vehicle_active": false,
+        "last_charging_end_result": null,
+        "last_update_reason": "Updated from vehicle 11/14/2021 09:20 PM",
+        "lids": [
+            {
+                "name": "hood",
+                "state": "CLOSED"
+            },
+            {
+                "name": "trunk",
+                "state": "CLOSED"
+            },
+            {
+                "name": "driverFront",
+                "state": "CLOSED"
+            },
+            {
+                "name": "driverRear",
+                "state": "CLOSED"
+            },
+            {
+                "name": "passengerFront",
+                "state": "CLOSED"
+            },
+            {
+                "name": "passengerRear",
+                "state": "CLOSED"
+            }
+        ],
+        "max_range_electric": null,
+        "mileage": [
+            27138,
+            "km"
+        ],
+        "open_lids": [],
+        "open_windows": [],
+        "parking_lights": null,
+        "remaining_fuel": [
+            29,
+            "LITERS"
+        ],
+        "remaining_range_electric": [
+            23,
+            "km"
+        ],
+        "remaining_range_fuel": [
+            345,
+            "km"
+        ],
+        "remaining_range_total": [
+            368,
+            "km"
+        ],
+        "timestamp": "2021-11-14 20:20:21+00:00",
+        "windows": [
+            {
+                "name": "driverFront",
+                "state": "CLOSED"
+            },
+            {
+                "name": "driverRear",
+                "state": "CLOSED"
+            },
+            {
+                "name": "passengerFront",
+                "state": "CLOSED"
+            },
+            {
+                "name": "passengerRear",
+                "state": "CLOSED"
+            },
+            {
+                "name": "moonroof",
+                "state": "CLOSED"
+            }
+        ]
+    },
+    "observer_latitude": null,
+    "observer_longitude": null,
+    "available_attributes": [
+        "gps_position",
+        "vin",
+        "remaining_range_total",
+        "mileage",
+        "charging_time_remaining",
+        "charging_start_time",
+        "charging_end_time",
+        "charging_time_label",
+        "charging_status",
+        "charging_level_hv",
+        "connection_status",
+        "remaining_range_electric",
+        "last_charging_end_result",
+        "remaining_fuel",
+        "remaining_range_fuel",
+        "fuel_percent",
+        "condition_based_services",
+        "check_control_messages",
+        "door_lock_state",
+        "timestamp",
+        "last_update_reason",
+        "lids",
+        "windows"
+    ],
+    "available_state_services": [
+        "status"
+    ],
+    "brand": "bmw",
+    "charging_profile": {
+        "charging_profile": {
+            "chargingControlType": "weeklyPlanner",
+            "chargingMode": "immediateCharging",
+            "chargingPreference": "noPreSelection",
+            "chargingSettings": {
+                "hospitality": "NO_ACTION",
+                "idcc": "NO_ACTION",
+                "isAcCurrentLimitActive": false,
+                "targetSoc": 100
+            },
+            "climatisationOn": false,
+            "departureTimes": [
+                {
+                    "action": "deactivate",
+                    "id": 1,
+                    "timeStamp": {
+                        "hour": 0,
+                        "minute": 0
+                    },
+                    "timerWeekDays": []
+                },
+                {
+                    "action": "deactivate",
+                    "id": 2,
+                    "timeStamp": {
+                        "hour": 0,
+                        "minute": 0
+                    },
+                    "timerWeekDays": []
+                },
+                {
+                    "action": "deactivate",
+                    "id": 3,
+                    "timeStamp": {
+                        "hour": 0,
+                        "minute": 0
+                    },
+                    "timerWeekDays": []
+                },
+                {
+                    "action": "deactivate",
+                    "id": 4,
+                    "timerWeekDays": []
+                }
+            ],
+            "reductionOfChargeCurrent": {
+                "end": {
+                    "hour": 0,
+                    "minute": 0
+                },
+                "start": {
+                    "hour": 0,
+                    "minute": 0
+                }
+            }
+        },
+        "charging_mode": "immediateCharging",
+        "charging_preferences": "noPreSelection",
+        "is_pre_entry_climatization_enabled": false,
+        "preferred_charging_window": {
+            "end_time": "00:00",
+            "start_time": "00:00"
+        },
+        "timer": {
+            "1": {
+                "action": "deactivate",
+                "start_time": "00:00",
+                "timer_id": 1,
+                "weekdays": []
+            },
+            "2": {
+                "action": "deactivate",
+                "start_time": "00:00",
+                "timer_id": 2,
+                "weekdays": []
+            },
+            "3": {
+                "action": "deactivate",
+                "start_time": "00:00",
+                "timer_id": 3,
+                "weekdays": []
+            },
+            "4": {
+                "action": "deactivate",
+                "start_time": null,
+                "timer_id": 4,
+                "weekdays": []
+            }
+        }
+    },
+    "drive_train": "PLUGIN_HYBRID",
+    "drive_train_attributes": [
+        "remaining_range_total",
+        "mileage",
+        "charging_time_remaining",
+        "charging_start_time",
+        "charging_end_time",
+        "charging_time_label",
+        "charging_status",
+        "charging_level_hv",
+        "connection_status",
+        "remaining_range_electric",
+        "last_charging_end_result",
+        "remaining_fuel",
+        "remaining_range_fuel",
+        "fuel_percent"
+    ],
+    "has_hv_battery": true,
+    "has_internal_combustion_engine": true,
+    "has_range_extender": false,
+    "has_weekly_planner_service": true,
+    "is_vehicle_tracking_enabled": true,
+    "lsc_type": "ACTIVATED",
+    "name": "330e xDrive"
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/vehicles_v2_bmw_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/vehicles_v2_bmw_0.json
new file mode 100644 (file)
index 0000000..0d4a372
--- /dev/null
@@ -0,0 +1,542 @@
+[
+  {
+    "a4aType": "NOT_SUPPORTED",
+    "bodyType": "G21",
+    "brand": "BMW",
+    "capabilities": {
+      "canRemoteHistoryBeDeleted": false,
+      "climateNow": {
+        "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.",
+        "executionPopup": {
+          "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.",
+          "iconId": 59733,
+          "popupType": "DIALOG",
+          "primaryButtonText": "Start",
+          "secondaryButtonText": "Cancel",
+          "title": "Start Climatization"
+        },
+        "executionStopPopup": {
+          "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.",
+          "title": "Climate control is running"
+        },
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "horn": {
+        "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "isBmwChargingSupported": true,
+      "isCarSharingSupported": false,
+      "isChargeNowForBusinessSupported": true,
+      "isChargingHistorySupported": true,
+      "isChargingHospitalityEnabled": false,
+      "isChargingLoudnessEnable": false,
+      "isChargingPlanSupported": true,
+      "isChargingPowerLimitEnable": false,
+      "isChargingSettingsEnabled": false,
+      "isChargingTargetSocEnable": false,
+      "isCustomerEsimSupported": false,
+      "isDCSContractManagementSupported": true,
+      "isDataPrivacyEnabled": false,
+      "isEasyChargeSupported": false,
+      "isEvGoChargingSupported": false,
+      "isFindChargingEnabled": true,
+      "isMiniChargingSupported": false,
+      "isRemoteHistorySupported": true,
+      "isRemoteServicesActivationRequired": false,
+      "isRemoteServicesBookingRequired": false,
+      "isScanAndChargeSupported": true,
+      "lastStateCall": {
+        "isNonLscFeatureEnabled": false,
+        "lscState": "ACTIVATED"
+      },
+      "lights": {
+        "executionMessage": "Flash headlights now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "lock": {
+        "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "remote360": {
+        "isComingSoonEnabled": false,
+        "isDataPrivacyEnabled": false,
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false,
+        "isToggleEnabled": true
+      },
+      "remoteSoftwareUpgrade": {
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "sendPoi": {
+        "executionMessage": "Send POI now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "speechThirdPartyAlexa": {
+        "executionMessage": "Activate Alexa now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "unlock": {
+        "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": true
+      },
+      "vehicleFinder": {
+        "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      }
+    },
+    "connectedDriveServices": [
+      "WIFI_HOTSPOT_SERVICE"
+    ],
+    "driveTrain": "PLUGIN_HYBRID",
+    "driverGuideInfo": {
+      "androidAppScheme": "com.bmwgroup.driversguide.row",
+      "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row",
+      "iosAppScheme": "bmwdriversguide:///open",
+      "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8",
+      "title": "BMW\nDriver's Guide"
+    },
+    "exFactoryILevel": "S18A-20-07-548",
+    "exFactoryPUStep": "0720",
+    "headUnit": "MGU",
+    "hmiVersion": "id7",
+    "iStep": "S18A-21-07-550",
+    "isLscSupported": true,
+    "isMappingPending": false,
+    "isMappingUnconfirmed": false,
+    "model": "330e xDrive",
+    "properties": {
+      "areDoorsClosed": true,
+      "areDoorsLocked": true,
+      "areDoorsOpen": false,
+      "areWindowsClosed": true,
+      "chargingState": {
+        "chargePercentage": 83,
+        "isChargerConnected": true,
+        "state": "CHARGING",
+        "type": "NOT_AVAILABLE"
+      },
+      "checkControlMessages": [],
+      "climateControl": {
+        "activity": "INACTIVE"
+      },
+      "combinedRange": {
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 368
+        }
+      },
+      "combustionRange": {
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 368
+        }
+      },
+      "doorsAndWindows": {
+        "doors": {
+          "driverFront": "CLOSED",
+          "driverRear": "CLOSED",
+          "passengerFront": "CLOSED",
+          "passengerRear": "CLOSED"
+        },
+        "hood": "CLOSED",
+        "moonroof": "CLOSED",
+        "trunk": "CLOSED",
+        "windows": {
+          "driverFront": "CLOSED",
+          "driverRear": "CLOSED",
+          "passengerFront": "CLOSED",
+          "passengerRear": "CLOSED"
+        }
+      },
+      "electricRange": {
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 23
+        }
+      },
+      "electricRangeAndStatus": {
+        "chargePercentage": 83,
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 23
+        }
+      },
+      "fuelLevel": {
+        "units": "LITERS",
+        "value": 29
+      },
+      "fuelPercentage": {
+        "value": 83
+      },
+      "inMotion": false,
+      "isServiceRequired": false,
+      "lastUpdatedAt": "2021-11-14T20:20:21Z",
+      "originCountryISO": "DE",
+      "serviceRequired": [
+        {
+          "dateTime": "2022-10-01T00:00:00.000Z",
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 6000
+          },
+          "status": "OK",
+          "type": "OIL"
+        },
+        {
+          "dateTime": "2024-10-01T00:00:00.000Z",
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 39000
+          },
+          "status": "OK",
+          "type": "VEHICLE_CHECK"
+        },
+        {
+          "status": "OK",
+          "type": "TIRE_WEAR_FRONT"
+        },
+        {
+          "status": "OK",
+          "type": "TIRE_WEAR_REAR"
+        },
+        {
+          "dateTime": "2023-10-01T00:00:00.000Z",
+          "status": "OK",
+          "type": "BRAKE_FLUID"
+        }
+      ],
+      "tires": {
+        "frontLeft": {
+          "details": {
+            "dimension": "225/45 R18 95V XL",
+            "manufacturer": "Pirelli",
+            "manufacturingWeek": "35 / 19",
+            "maxSpeed": "240 km/h",
+            "mountingDate": "11/12/2021",
+            "optimizedForOemBmw": "Yes",
+            "partNumber": "2461777",
+            "season": "Winter tires",
+            "treadDesign": "SOTTOZERO 3"
+          },
+          "status": {
+            "currentPressure": 260,
+            "localizedCurrentPressure": "2.6 bar",
+            "localizedTargetPressure": "2.7 bar",
+            "targetPressure": 270,
+            "wear": 0
+          }
+        },
+        "frontRight": {
+          "details": {
+            "dimension": "225/45 R18 95V XL",
+            "manufacturer": "Pirelli",
+            "manufacturingWeek": "27 / 19",
+            "maxSpeed": "240 km/h",
+            "mountingDate": "11/12/2021",
+            "optimizedForOemBmw": "Yes",
+            "partNumber": "2461777",
+            "season": "Winter tires",
+            "treadDesign": "SOTTOZERO 3"
+          },
+          "status": {
+            "currentPressure": 250,
+            "localizedCurrentPressure": "2.5 bar",
+            "localizedTargetPressure": "2.7 bar",
+            "targetPressure": 270,
+            "wear": 0
+          }
+        },
+        "rearLeft": {
+          "details": {
+            "dimension": "255/40 R18 99V XL",
+            "manufacturer": "Pirelli",
+            "manufacturingWeek": "17 / 19",
+            "maxSpeed": "240 km/h",
+            "mountingDate": "11/12/2021",
+            "optimizedForOemBmw": "Yes",
+            "partNumber": "2461778",
+            "season": "Winter tires",
+            "treadDesign": "SOTTOZERO 3"
+          },
+          "status": {
+            "currentPressure": 300,
+            "localizedCurrentPressure": "3.0 bar",
+            "localizedTargetPressure": "2.9 bar",
+            "targetPressure": 290,
+            "wear": 0
+          }
+        },
+        "rearRight": {
+          "details": {
+            "dimension": "255/40 R18 99V XL",
+            "manufacturer": "Pirelli",
+            "manufacturingWeek": "26 / 19",
+            "maxSpeed": "240 km/h",
+            "mountingDate": "11/12/2021",
+            "optimizedForOemBmw": "Yes",
+            "partNumber": "2461778",
+            "season": "Winter tires",
+            "treadDesign": "SOTTOZERO 3"
+          },
+          "status": {
+            "currentPressure": 250,
+            "localizedCurrentPressure": "2.5 bar",
+            "localizedTargetPressure": "2.9 bar",
+            "targetPressure": 290,
+            "wear": 0
+          }
+        }
+      },
+      "vehicleLocation": {
+        "address": {
+          "formatted": "some_formatted_address"
+        },
+        "coordinates": {
+          "latitude": 12.3456,
+          "longitude": 34.5678
+        },
+        "heading": 123
+      }
+    },
+    "puStep": "0721",
+    "status": {
+      "chargingProfile": {
+        "chargingControlType": "weeklyPlanner",
+        "chargingMode": "immediateCharging",
+        "chargingPreference": "noPreSelection",
+        "chargingSettings": {
+          "hospitality": "NO_ACTION",
+          "idcc": "NO_ACTION",
+          "isAcCurrentLimitActive": false,
+          "targetSoc": 100
+        },
+        "climatisationOn": false,
+        "departureTimes": [
+          {
+            "action": "deactivate",
+            "id": 1,
+            "timeStamp": {
+              "hour": 0,
+              "minute": 0
+            },
+            "timerWeekDays": []
+          },
+          {
+            "action": "deactivate",
+            "id": 2,
+            "timeStamp": {
+              "hour": 0,
+              "minute": 0
+            },
+            "timerWeekDays": []
+          },
+          {
+            "action": "deactivate",
+            "id": 3,
+            "timeStamp": {
+              "hour": 0,
+              "minute": 0
+            },
+            "timerWeekDays": []
+          },
+          {
+            "action": "deactivate",
+            "id": 4,
+            "timerWeekDays": []
+          }
+        ],
+        "reductionOfChargeCurrent": {
+          "end": {
+            "hour": 0,
+            "minute": 0
+          },
+          "start": {
+            "hour": 0,
+            "minute": 0
+          }
+        }
+      },
+      "checkControlMessages": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60117,
+          "state": "OK",
+          "title": "Tires"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60197,
+          "state": "OK",
+          "title": "Engine Oil"
+        }
+      ],
+      "checkControlMessagesGeneralState": "No Issues",
+      "currentMileage": {
+        "formattedMileage": "27138",
+        "mileage": 27138,
+        "units": "km"
+      },
+      "doorsAndWindows": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59757,
+          "state": "Locked",
+          "title": "Lock status"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59722,
+          "state": "Closed",
+          "title": "All doors"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59725,
+          "state": "Closed",
+          "title": "All windows"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59706,
+          "state": "Closed",
+          "title": "Hood"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59704,
+          "state": "Closed",
+          "title": "Trunk"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59705,
+          "state": "Closed",
+          "title": "Sunroof"
+        }
+      ],
+      "doorsGeneralState": "Locked",
+      "fuelIndicators": [
+        {
+          "chargingType": null,
+          "iconOpacity": "high",
+          "infoIconId": 59691,
+          "infoLabel": "Combined Range",
+          "isCircleIcon": false,
+          "isInaccurate": false,
+          "levelIconId": null,
+          "levelUnits": null,
+          "levelValue": null,
+          "mainBarValue": 0,
+          "rangeIconId": 59691,
+          "rangeUnits": "km",
+          "rangeValue": "368",
+          "secondaryBarValue": 0,
+          "showsBar": false
+        },
+        {
+          "barType": null,
+          "chargingStatusIndicatorType": "CHARGING",
+          "chargingStatusType": "CHARGING",
+          "chargingType": "charging",
+          "iconOpacity": "high",
+          "infoIconId": 59689,
+          "infoLabel": "100% at ~12:43 AM",
+          "isCircleIcon": true,
+          "isInaccurate": true,
+          "levelIconId": 59689,
+          "levelUnits": "%",
+          "levelValue": "83",
+          "mainBarValue": 83,
+          "rangeIconId": 59683,
+          "rangeUnits": "km",
+          "rangeValue": "23",
+          "secondaryBarValue": 0,
+          "showBarGoal": false,
+          "showsBar": true
+        },
+        {
+          "chargingType": null,
+          "iconOpacity": "high",
+          "infoIconId": 59930,
+          "infoLabel": "Fuel Level",
+          "isCircleIcon": false,
+          "isInaccurate": false,
+          "levelIconId": 59682,
+          "levelUnits": "%",
+          "levelValue": "83",
+          "mainBarValue": 83,
+          "rangeIconId": 59681,
+          "rangeUnits": "km",
+          "rangeValue": "345",
+          "secondaryBarValue": 0,
+          "showsBar": true
+        }
+      ],
+      "issues": {},
+      "lastUpdatedAt": "2021-11-14T20:20:21Z",
+      "recallExternalUrl": null,
+      "recallMessages": [],
+      "requiredServices": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60197,
+          "id": "Oil",
+          "longDescription": "Next service due after the specified distance or date.",
+          "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+          "title": "Engine oil"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60215,
+          "id": "VehicleCheck",
+          "longDescription": "Next vehicle check due after the specified distance or date.",
+          "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+          "title": "Vehicle check"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60447,
+          "id": "TireWearFront",
+          "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+          "title": "Tire service, front tires"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60447,
+          "id": "TireWearRear",
+          "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+          "title": "Tire service, rear tires"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60223,
+          "id": "BrakeFluid",
+          "longDescription": "Next service due by the specified date.",
+          "subtitle": "some_road \u2022 duration \u2022 -- EUR",
+          "title": "Brake fluid"
+        }
+      ],
+      "timestampMessage": "Updated from vehicle 11/14/2021 09:20 PM"
+    },
+    "telematicsUnit": "ATM02",
+    "themeSpecs": {
+      "vehicleStatusBackgroundColor": {
+        "blue": 201,
+        "green": 94,
+        "red": 40
+      }
+    },
+    "vin": "some_vin_G21",
+    "year": 2020
+  }
+]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G30/charging-sessions_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G30/charging-sessions_0.json
new file mode 100644 (file)
index 0000000..e28b736
--- /dev/null
@@ -0,0 +1,63 @@
+{
+  "chargingSessions": {
+    "chargingListState": "HAS_SESSIONS",
+    "numberOfSessions": "6",
+    "sessions": [
+      {
+        "energyCharged": "~ 13 kWh",
+        "id": "2021-11-10T11:24:34Z_e51ab124",
+        "isPublic": false,
+        "sessionStatus": "FINISHED",
+        "subtitle": "some_road 2022 duration 2022 -- EUR",
+        "title": "Yesterday 11:24 AM"
+      },
+      {
+        "energyCharged": "~ 11 kWh",
+        "id": "2021-11-08T16:53:02Z_e51ab124",
+        "isPublic": false,
+        "sessionStatus": "FINISHED",
+        "subtitle": "some_road 2022 duration 2022 -- EUR",
+        "title": "Monday 4:53 PM"
+      },
+      {
+        "energyCharged": "~ 12 kWh",
+        "id": "2021-11-07T13:35:27Z_e51ab124",
+        "isPublic": false,
+        "sessionStatus": "FINISHED",
+        "subtitle": "some_road 2022 duration 2022 -- EUR",
+        "title": "Sunday 1:35 PM"
+      },
+      {
+        "energyCharged": "~ 13 kWh",
+        "id": "2021-11-05T10:53:57Z_e51ab124",
+        "isPublic": false,
+        "sessionStatus": "FINISHED",
+        "subtitle": "some_road 2022 duration 2022 -- EUR",
+        "title": "Friday 10:53 AM"
+      },
+      {
+        "energyCharged": "~ 10 kWh",
+        "id": "2021-11-03T10:00:47Z_e51ab124",
+        "isPublic": false,
+        "sessionStatus": "FINISHED",
+        "subtitle": "some_road 2022 duration 2022 -- EUR",
+        "title": "11/3/2021 10:00 AM"
+      },
+      {
+        "energyCharged": "~ 12 kWh",
+        "id": "2021-11-02T12:31:45Z_e51ab124",
+        "isPublic": false,
+        "sessionStatus": "FINISHED",
+        "subtitle": "some_road 2022 duration 2022 -- EUR",
+        "title": "11/2/2021 12:31 PM"
+      }
+    ],
+    "total": "~ 71 kWh"
+  },
+  "datePicker": {
+    "endDate": "2021-11-11T09:09:49Z",
+    "selectedDate": "2021-11-10T11:24:34Z",
+    "startDate": "2021-08-16T19:31:45Z"
+  },
+  "paginationInfo": {}
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G30/charging-statistics_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G30/charging-statistics_0.json
new file mode 100644 (file)
index 0000000..7089ad3
--- /dev/null
@@ -0,0 +1,11 @@
+{
+  "description": "November 2021",
+  "optStateType": "OPT_IN_WITH_SESSIONS",
+  "statistics": {
+    "numberOfChargingSessions": 6,
+    "numberOfChargingSessionsSemantics": "mobile20chsChargingSessionNumberSemantics",
+    "symbol": "~",
+    "totalEnergyCharged": 71,
+    "totalEnergyChargedSemantics": "mobile20chsApproximatelyTotalChargedSemantics"
+  }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G30/vehicles_v2_bmw_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G30/vehicles_v2_bmw_0.json
new file mode 100644 (file)
index 0000000..fbcadbc
--- /dev/null
@@ -0,0 +1,384 @@
+[
+  {
+    "a4aType": "NOT_SUPPORTED",
+    "bodyType": "G30",
+    "brand": "BMW",
+    "capabilities": {
+      "canRemoteHistoryBeDeleted": false,
+      "climateNow": {
+        "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.",
+        "executionPopup": {
+          "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.",
+          "iconId": 59733,
+          "popupType": "DIALOG",
+          "primaryButtonText": "Start",
+          "secondaryButtonText": "Cancel",
+          "title": "Start Climatization"
+        },
+        "executionStopPopup": {
+          "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.",
+          "title": "Climate control is running"
+        },
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "isBmwChargingSupported": true,
+      "isCarSharingSupported": false,
+      "isChargeNowForBusinessSupported": false,
+      "isChargingHistorySupported": true,
+      "isChargingHospitalityEnabled": false,
+      "isChargingLoudnessEnable": false,
+      "isChargingPlanSupported": true,
+      "isChargingPowerLimitEnable": false,
+      "isChargingSettingsEnabled": false,
+      "isChargingTargetSocEnable": false,
+      "isCustomerEsimSupported": false,
+      "isDCSContractManagementSupported": true,
+      "isDataPrivacyEnabled": false,
+      "isEasyChargeSupported": false,
+      "isEvGoChargingSupported": false,
+      "isFindChargingEnabled": true,
+      "isMiniChargingSupported": false,
+      "isRemoteHistorySupported": true,
+      "isRemoteServicesActivationRequired": false,
+      "isRemoteServicesBookingRequired": false,
+      "isScanAndChargeSupported": false,
+      "lastStateCall": {
+        "isNonLscFeatureEnabled": false,
+        "lscState": "ACTIVATED"
+      },
+      "lights": {
+        "executionMessage": "Flash headlights now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "lock": {
+        "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "remote360": {
+        "isComingSoonEnabled": false,
+        "isDataPrivacyEnabled": false,
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false,
+        "isToggleEnabled": true
+      },
+      "remoteSoftwareUpgrade": {
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "sendPoi": {
+        "executionMessage": "Send POI now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "speechThirdPartyAlexa": {
+        "executionMessage": "Activate Alexa now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "unlock": {
+        "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": true
+      },
+      "vehicleFinder": {
+        "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      }
+    },
+    "connectedDriveServices": [
+      "WIFI_HOTSPOT_SERVICE"
+    ],
+    "driveTrain": "PLUGIN_HYBRID",
+    "driverGuideInfo": {
+      "androidAppScheme": "com.bmwgroup.driversguide.row",
+      "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row",
+      "iosAppScheme": "bmwdriversguide:///open",
+      "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8",
+      "title": "BMW\nDriver's Guide"
+    },
+    "exFactoryILevel": "S15A-20-07-532",
+    "exFactoryPUStep": "0720",
+    "headUnit": "MGU",
+    "hmiVersion": "id7",
+    "iStep": "S15A-21-03-550",
+    "isLscSupported": true,
+    "isMappingPending": false,
+    "isMappingUnconfirmed": false,
+    "model": "530e",
+    "properties": {
+      "areDoorsClosed": true,
+      "areDoorsLocked": true,
+      "areDoorsOpen": false,
+      "areWindowsClosed": true,
+      "chargingState": {
+        "chargePercentage": 41,
+        "isChargerConnected": false,
+        "state": "NOT_CHARGING",
+        "type": "NOT_AVAILABLE"
+      },
+      "checkControlMessages": [],
+      "climateControl": {
+        "activity": "INACTIVE"
+      },
+      "combinedRange": {
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 116
+        }
+      },
+      "combustionRange": {
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 116
+        }
+      },
+      "doorsAndWindows": {
+        "doors": {
+          "driverFront": "OPEN",
+          "driverRear": "CLOSED",
+          "passengerFront": "OPEN",
+          "passengerRear": "CLOSED"
+        },
+        "hood": "OPEN",
+        "trunk": "CLOSED",
+        "windows": {
+          "driverFront": "CLOSED",
+          "driverRear": "CLOSED",
+          "passengerFront": "OPEN",
+          "passengerRear": "CLOSED"
+        }
+      },
+      "electricRange": {
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 9
+        }
+      },
+      "electricRangeAndStatus": {
+        "chargePercentage": 41,
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 9
+        }
+      },
+      "fuelLevel": {
+        "units": "LITERS",
+        "value": 11
+      },
+      "fuelPercentage": {
+        "value": 28
+      },
+      "inMotion": false,
+      "isServiceRequired": false,
+      "lastUpdatedAt": "2021-11-11T08:58:53Z",
+      "originCountryISO": "IE",
+      "serviceRequired": [
+        {
+          "dateTime": "2022-08-01T00:00:00.000Z",
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 25000
+          },
+          "status": "OK",
+          "type": "OIL"
+        },
+        {
+          "dateTime": "2023-08-01T00:00:00.000Z",
+          "status": "OK",
+          "type": "BRAKE_FLUID"
+        },
+        {
+          "dateTime": "2024-08-01T00:00:00.000Z",
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 60000
+          },
+          "status": "OK",
+          "type": "VEHICLE_CHECK"
+        }
+      ],
+      "vehicleLocation": {
+        "address": {
+          "formatted": "some_formatted_address"
+        },
+        "coordinates": {
+          "latitude": 12.3456,
+          "longitude": 34.5678
+        },
+        "heading": 123
+      }
+    },
+    "puStep": "0321",
+    "status": {
+      "chargingProfile": {
+        "chargingControlType": "weeklyPlanner",
+        "chargingMode": "immediateCharging",
+        "chargingPreference": "noPreSelection",
+        "chargingSettings": {
+          "hospitality": "NO_ACTION",
+          "idcc": "NO_ACTION",
+          "isAcCurrentLimitActive": false,
+          "targetSoc": 100
+        },
+        "climatisationOn": true,
+        "departureTimes": [
+          {
+            "action": "deactivate",
+            "id": 1,
+            "timerWeekDays": []
+          },
+          {
+            "action": "deactivate",
+            "id": 2,
+            "timerWeekDays": []
+          },
+          {
+            "action": "deactivate",
+            "id": 3,
+            "timerWeekDays": []
+          },
+          {
+            "action": "deactivate",
+            "id": 4,
+            "timerWeekDays": []
+          }
+        ],
+        "reductionOfChargeCurrent": {
+          "end": {
+            "hour": 0,
+            "minute": 0
+          },
+          "start": {
+            "hour": 0,
+            "minute": 0
+          }
+        }
+      },
+      "checkControlMessages": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60197,
+          "state": "OK",
+          "title": "Engine Oil"
+        }
+      ],
+      "checkControlMessagesGeneralState": "No Issues",
+      "currentMileage": {
+        "formattedMileage": "7991",
+        "mileage": 7991,
+        "units": "km"
+      },
+      "doorsAndWindows": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59722,
+          "state": "Closed",
+          "title": "All doors and windows"
+        }
+      ],
+      "doorsGeneralState": "Locked",
+      "fuelIndicators": [
+        {
+          "chargingType": null,
+          "iconOpacity": "high",
+          "infoIconId": 59691,
+          "infoLabel": "Combined Range",
+          "isCircleIcon": false,
+          "isInaccurate": false,
+          "levelIconId": null,
+          "levelUnits": null,
+          "levelValue": null,
+          "mainBarValue": 0,
+          "rangeIconId": 59691,
+          "rangeUnits": "km",
+          "rangeValue": "116",
+          "secondaryBarValue": 0,
+          "showsBar": false
+        },
+        {
+          "barType": null,
+          "chargingStatusIndicatorType": "DEFAULT",
+          "chargingStatusType": "DEFAULT",
+          "chargingType": null,
+          "iconOpacity": "high",
+          "infoIconId": 59694,
+          "infoLabel": "State of Charge",
+          "isCircleIcon": false,
+          "isInaccurate": false,
+          "levelIconId": 59694,
+          "levelUnits": "%",
+          "levelValue": "41",
+          "mainBarValue": 41,
+          "rangeIconId": 59683,
+          "rangeUnits": "km",
+          "rangeValue": "9",
+          "secondaryBarValue": 0,
+          "showBarGoal": false,
+          "showsBar": true
+        },
+        {
+          "chargingType": null,
+          "iconOpacity": "high",
+          "infoIconId": 59930,
+          "infoLabel": "Fuel Level",
+          "isCircleIcon": false,
+          "isInaccurate": false,
+          "levelIconId": 59682,
+          "levelUnits": "%",
+          "levelValue": "28",
+          "mainBarValue": 28,
+          "rangeIconId": 59681,
+          "rangeUnits": "km",
+          "rangeValue": "107",
+          "secondaryBarValue": 0,
+          "showsBar": true
+        }
+      ],
+      "issues": {},
+      "lastUpdatedAt": "2021-11-11T08:58:53Z",
+      "recallExternalUrl": null,
+      "recallMessages": [],
+      "requiredServices": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60197,
+          "id": "Oil",
+          "longDescription": "Next service due after the specified distance or date.",
+          "subtitle": "Due in August 2022 or 25000 km",
+          "title": "Engine oil"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60223,
+          "id": "BrakeFluid",
+          "longDescription": "Next service due by the specified date.",
+          "subtitle": "Due in August 2023",
+          "title": "Brake fluid"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60215,
+          "id": "VehicleCheck",
+          "longDescription": "Next vehicle check due after the specified distance or date.",
+          "subtitle": "Due in August 2024 or 60000 km",
+          "title": "Vehicle check"
+        }
+      ],
+      "timestampMessage": "Updated from vehicle 11/12/2021 08:58 AM"
+    },
+    "telematicsUnit": "ATM02",
+    "themeSpecs": {
+      "vehicleStatusBackgroundColor": {
+        "blue": 158,
+        "green": 158,
+        "red": 158
+      }
+    },
+    "vin": "some_vin_G30",
+    "year": 2020
+  }
+]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_NOREX/charging-sessions_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_NOREX/charging-sessions_0.json
new file mode 100644 (file)
index 0000000..a092c63
--- /dev/null
@@ -0,0 +1,23 @@
+{
+  "chargingSessions": {
+    "chargingListState": "HAS_SESSIONS",
+    "numberOfSessions": "1",
+    "sessions": [
+      {
+        "energyCharged": "~ 36 kWh",
+        "id": "2021-11-09T18:06:18Z_some_id",
+        "isPublic": false,
+        "sessionStatus": "FINISHED",
+        "subtitle": "some_place \u2022 3h 42min \u2022 -- EUR",
+        "title": "Tuesday 7:06 PM"
+      }
+    ],
+    "total": "~ 36 kWh"
+  },
+  "datePicker": {
+    "endDate": "2021-11-11T00:44:47Z",
+    "selectedDate": "2021-11-09T18:06:18Z",
+    "startDate": "2021-08-08T15:51:27Z"
+  },
+  "paginationInfo": {}
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_NOREX/charging-statistics_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_NOREX/charging-statistics_0.json
new file mode 100644 (file)
index 0000000..8ca915a
--- /dev/null
@@ -0,0 +1,11 @@
+{
+  "description": "November 2021",
+  "optStateType": "OPT_IN_WITH_SESSIONS",
+  "statistics": {
+    "numberOfChargingSessions": 1,
+    "numberOfChargingSessionsSemantics": "mobile20chsChargingSessionNumberSemantics",
+    "symbol": "~",
+    "totalEnergyCharged": 36,
+    "totalEnergyChargedSemantics": "mobile20chsApproximatelyTotalChargedSemantics"
+  }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_NOREX/vehicles_v2_bmw_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_NOREX/vehicles_v2_bmw_0.json
new file mode 100644 (file)
index 0000000..80ba8a4
--- /dev/null
@@ -0,0 +1,316 @@
+[
+  {
+    "a4aType": "BLUETOOTH",
+    "bodyType": "I01",
+    "brand": "BMW",
+    "capabilities": {
+      "canRemoteHistoryBeDeleted": false,
+      "climateNow": {
+        "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.",
+        "executionPopup": {
+          "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.",
+          "iconId": 59733,
+          "popupType": "DIALOG",
+          "primaryButtonText": "Start",
+          "secondaryButtonText": "Cancel",
+          "title": "Start Climatization"
+        },
+        "executionStopPopup": {
+          "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.",
+          "title": "Climate control is running"
+        },
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "horn": {
+        "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "isBmwChargingSupported": true,
+      "isCarSharingSupported": false,
+      "isChargeNowForBusinessSupported": true,
+      "isChargingHistorySupported": true,
+      "isChargingHospitalityEnabled": false,
+      "isChargingLoudnessEnable": false,
+      "isChargingPlanSupported": true,
+      "isChargingPowerLimitEnable": false,
+      "isChargingSettingsEnabled": false,
+      "isChargingTargetSocEnable": false,
+      "isCustomerEsimSupported": false,
+      "isDCSContractManagementSupported": true,
+      "isDataPrivacyEnabled": false,
+      "isEasyChargeSupported": false,
+      "isEvGoChargingSupported": false,
+      "isFindChargingEnabled": true,
+      "isMiniChargingSupported": false,
+      "isRemoteHistorySupported": true,
+      "isRemoteServicesActivationRequired": false,
+      "isRemoteServicesBookingRequired": false,
+      "isScanAndChargeSupported": true,
+      "lastStateCall": {
+        "isNonLscFeatureEnabled": false,
+        "lscState": "ACTIVATED"
+      },
+      "lights": {
+        "executionMessage": "Flash headlights now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "lock": {
+        "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "sendPoi": {
+        "executionMessage": "Send POI now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "speechThirdPartyAlexa": {
+        "executionMessage": "Activate Alexa now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "unlock": {
+        "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": true
+      },
+      "vehicleFinder": {
+        "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      }
+    },
+    "connectedDriveServices": [
+      "WIFI_HOTSPOT_SERVICE"
+    ],
+    "driveTrain": "ELECTRIC",
+    "driverGuideInfo": {
+      "androidAppScheme": "com.bmwgroup.driversguide.row",
+      "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row",
+      "iosAppScheme": "bmwdriversguide:///open",
+      "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8",
+      "title": "BMW\nDriver's Guide"
+    },
+    "exFactoryILevel": "I001-20-11-520",
+    "exFactoryPUStep": "1120",
+    "headUnit": "ID5",
+    "hmiVersion": "ID5",
+    "iStep": "I001-20-11-520",
+    "isLscSupported": true,
+    "isMappingPending": false,
+    "isMappingUnconfirmed": false,
+    "model": "i3 120",
+    "properties": {
+      "areDoorsClosed": true,
+      "areDoorsLocked": true,
+      "areDoorsOpen": false,
+      "areWindowsClosed": true,
+      "chargingState": {
+        "chargePercentage": 94,
+        "isChargerConnected": false,
+        "state": "NOT_CHARGING",
+        "type": "NOT_AVAILABLE"
+      },
+      "checkControlMessages": [],
+      "climateControl": {},
+      "combustionRange": {
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 0
+        }
+      },
+      "doorsAndWindows": {
+        "doors": {
+          "driverFront": "CLOSED",
+          "driverRear": "CLOSED",
+          "passengerFront": "CLOSED",
+          "passengerRear": "CLOSED"
+        },
+        "hood": "CLOSED",
+        "trunk": "CLOSED",
+        "windows": {
+          "driverFront": "CLOSED",
+          "passengerFront": "CLOSED"
+        }
+      },
+      "electricRange": {
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 229
+        }
+      },
+      "electricRangeAndStatus": {
+        "chargePercentage": 94,
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 229
+        }
+      },
+      "fuelLevel": {
+        "units": "LITERS",
+        "value": 0
+      },
+      "inMotion": false,
+      "isServiceRequired": false,
+      "lastUpdatedAt": "2021-11-10T18:18:05Z",
+      "originCountryISO": "DE",
+      "serviceRequired": [
+        {
+          "dateTime": "2023-02-01T00:00:00.000Z",
+          "status": "OK",
+          "type": "BRAKE_FLUID"
+        },
+        {
+          "dateTime": "2023-02-01T00:00:00.000Z",
+          "status": "OK",
+          "type": "VEHICLE_CHECK"
+        },
+        {
+          "dateTime": "2024-03-01T00:00:00.000Z",
+          "status": "OK",
+          "type": "VEHICLE_TUV"
+        }
+      ],
+      "vehicleLocation": {
+        "address": {
+          "formatted": "some_formatted_address"
+        },
+        "coordinates": {
+          "latitude": 12.3456,
+          "longitude": 34.5678
+        },
+        "heading": 123
+      }
+    },
+    "puStep": "1120",
+    "status": {
+      "chargingProfile": {
+        "chargingControlType": "weeklyPlanner",
+        "chargingMode": "immediateCharging",
+        "chargingPreference": "chargingWindow",
+        "chargingSettings": {
+          "hospitality": "NO_ACTION",
+          "idcc": "NO_ACTION",
+          "isAcCurrentLimitActive": false,
+          "targetSoc": 100
+        },
+        "climatisationOn": false,
+        "departureTimes": [
+          {
+            "action": "deactivate",
+            "id": 1,
+            "timerWeekDays": []
+          },
+          {
+            "action": "deactivate",
+            "id": 2,
+            "timerWeekDays": []
+          },
+          {
+            "action": "deactivate",
+            "id": 3,
+            "timerWeekDays": []
+          },
+          {
+            "action": "deactivate",
+            "id": 4,
+            "timerWeekDays": []
+          }
+        ],
+        "reductionOfChargeCurrent": {
+          "end": {
+            "hour": 0,
+            "minute": 0
+          },
+          "start": {
+            "hour": 0,
+            "minute": 0
+          }
+        }
+      },
+      "checkControlMessages": [],
+      "checkControlMessagesGeneralState": "No Issues",
+      "currentMileage": {
+        "formattedMileage": "1250",
+        "mileage": 1250,
+        "units": "km"
+      },
+      "doorsAndWindows": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59722,
+          "state": "Closed",
+          "title": "All doors and windows"
+        }
+      ],
+      "doorsGeneralState": "Locked",
+      "fuelIndicators": [
+        {
+          "barType": null,
+          "chargingStatusIndicatorType": "DEFAULT",
+          "chargingStatusType": "DEFAULT",
+          "chargingType": null,
+          "iconOpacity": "high",
+          "infoIconId": 59694,
+          "infoLabel": "State of Charge",
+          "isCircleIcon": false,
+          "isInaccurate": false,
+          "levelIconId": 59694,
+          "levelUnits": "%",
+          "levelValue": "94",
+          "mainBarValue": 94,
+          "rangeIconId": 59683,
+          "rangeUnits": "km",
+          "rangeValue": "229",
+          "secondaryBarValue": 0,
+          "showBarGoal": false,
+          "showsBar": true
+        }
+      ],
+      "issues": {},
+      "lastUpdatedAt": "2021-11-10T18:18:05Z",
+      "recallExternalUrl": null,
+      "recallMessages": [],
+      "requiredServices": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60223,
+          "id": "BrakeFluid",
+          "longDescription": "Next service due by the specified date.",
+          "subtitle": "Due in February 2023",
+          "title": "Brake fluid"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60215,
+          "id": "VehicleCheck",
+          "longDescription": "Next vehicle check due after the specified distance or date.",
+          "subtitle": "Due in February 2023",
+          "title": "Vehicle check"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60111,
+          "id": "VehicleAdmissionTest",
+          "longDescription": "Next state inspection due by the specified date.",
+          "subtitle": "Due in March 2024",
+          "title": "Vehicle Inspection"
+        }
+      ],
+      "timestampMessage": "Updated from vehicle 11/10/2021 07:18 PM"
+    },
+    "telematicsUnit": "ATM",
+    "themeSpecs": {
+      "vehicleStatusBackgroundColor": {
+        "blue": 152,
+        "green": 154,
+        "red": 156
+      }
+    },
+    "vin": "some_vin_I01_NOREX",
+    "year": 2021
+  }
+]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charge-sessions.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charge-sessions.json
new file mode 100644 (file)
index 0000000..0ea4988
--- /dev/null
@@ -0,0 +1,156 @@
+{
+       "paginationInfo": {
+               
+       },
+       "chargingSessions": {
+               "total": "~ 218 kWh",
+               "numberOfSessions": "17",
+               "chargingListState": "HAS_SESSIONS",
+               "sessions": [
+                       {
+                               "id": "2021-12-26T16:57:20Z_128fa4af",
+                               "title": "Gestern 17:57",
+                               "subtitle": "Uferstraße 4B • 7h 45min • -- EUR",
+                               "energyCharged": "~ 31 kWh",
+                               "sessionStatus": "FINISHED",
+                               "isPublic": false
+                       },
+                       {
+                               "id": "2021-12-26T16:02:49Z_128fa4af",
+                               "title": "Gestern 17:02",
+                               "subtitle": "Uferstraße 4C • 32 min • -- EUR",
+                               "energyCharged": "~ 2 kWh",
+                               "sessionStatus": "FINISHED",
+                               "isPublic": false
+                       },
+                       {
+                               "id": "2021-12-26T09:44:36Z_128fa4af",
+                               "title": "Gestern 10:44",
+                               "subtitle": "Kelzer Weg 24 • 58 min • -- EUR",
+                               "energyCharged": "~ 2 kWh",
+                               "sessionStatus": "FINISHED",
+                               "isPublic": false
+                       },
+                       {
+                               "id": "2021-12-25T22:18:46Z_128fa4af",
+                               "title": "Samstag 23:18",
+                               "subtitle": "Kelzer Weg 24 • 3h 42min • -- EUR",
+                               "energyCharged": "~ 8 kWh",
+                               "issues": "2 Probleme",
+                               "sessionStatus": "FINISHED",
+                               "isPublic": false
+                       },
+                       {
+                               "id": "2021-12-24T11:56:09Z_128fa4af",
+                               "title": "Freitag 12:56",
+                               "subtitle": "Kelzer Weg 24 • 8h 46min • -- EUR",
+                               "energyCharged": "~ 19 kWh",
+                               "sessionStatus": "FINISHED",
+                               "isPublic": false
+                       },
+                       {
+                               "id": "2021-12-24T09:56:13Z_128fa4af",
+                               "title": "Freitag 10:56",
+                               "subtitle": "Kelzer Weg 15A • 1h 48min • -- EUR",
+                               "energyCharged": "~ 4 kWh",
+                               "sessionStatus": "FINISHED",
+                               "isPublic": false
+                       },
+                       {
+                               "id": "2021-12-23T07:55:12Z_128fa4af",
+                               "title": "Donnerstag 08:55",
+                               "subtitle": "Uferstraße 4C • 2h 55min • -- EUR",
+                               "energyCharged": "~ 21 kWh",
+                               "sessionStatus": "FINISHED",
+                               "isPublic": false
+                       },
+                       {
+                               "id": "2021-12-20T09:42:28Z_128fa4af",
+                               "title": "20.12.2021 10:42",
+                               "subtitle": "Hermannsteiner Straße 13 • 1h 14min",
+                               "energyCharged": "~ 21 kWh",
+                               "sessionStatus": "FINISHED",
+                               "isPublic": false
+                       },
+                       {
+                               "id": "2021-12-20T09:37:51Z_128fa4af",
+                               "title": "20.12.2021 10:37",
+                               "subtitle": "Hermannsteiner Straße 13 • < 1 min",
+                               "energyCharged": "< 2 kWh",
+                               "sessionStatus": "FINISHED",
+                               "isPublic": false
+                       },
+                       {
+                               "id": "2021-12-19T12:07:18Z_128fa4af",
+                               "title": "19.12.2021 13:07",
+                               "subtitle": "Uferstraße 4B • 2h 07min • -- EUR",
+                               "energyCharged": "~ 9 kWh",
+                               "issues": "1 Problem",
+                               "sessionStatus": "FINISHED",
+                               "isPublic": false
+                       },
+                       {
+                               "id": "2021-12-18T10:56:31Z_128fa4af",
+                               "title": "18.12.2021 11:56",
+                               "subtitle": "Uferstraße 4C • 41 min • -- EUR",
+                               "energyCharged": "~ 5 kWh",
+                               "sessionStatus": "FINISHED",
+                               "isPublic": false
+                       },
+                       {
+                               "id": "2021-12-16T11:08:30Z_128fa4af",
+                               "title": "16.12.2021 12:08",
+                               "subtitle": "Uferstraße 4B • 2h 07min • -- EUR",
+                               "energyCharged": "~ 9 kWh",
+                               "sessionStatus": "FINISHED",
+                               "isPublic": false
+                       },
+                       {
+                               "id": "2021-12-15T12:38:23Z_128fa4af",
+                               "title": "15.12.2021 13:38",
+                               "subtitle": "Uferstraße 4C • 1h 55min • -- EUR",
+                               "energyCharged": "~ 8 kWh",
+                               "sessionStatus": "FINISHED",
+                               "isPublic": false
+                       },
+                       {
+                               "id": "2021-12-12T10:48:16Z_128fa4af",
+                               "title": "12.12.2021 11:48",
+                               "subtitle": "Uferstraße 4C • 6h 06min • -- EUR",
+                               "energyCharged": "~ 23 kWh",
+                               "sessionStatus": "FINISHED",
+                               "isPublic": false
+                       },
+                       {
+                               "id": "2021-12-07T16:09:02Z_128fa4af",
+                               "title": "07.12.2021 17:09",
+                               "subtitle": "Uferstraße 4B • 5h 40min • -- EUR",
+                               "energyCharged": "~ 21 kWh",
+                               "issues": "1 Problem",
+                               "sessionStatus": "FINISHED",
+                               "isPublic": false
+                       },
+                       {
+                               "id": "2021-12-04T09:51:23Z_128fa4af",
+                               "title": "04.12.2021 10:51",
+                               "subtitle": "L3053 • 1h 24min",
+                               "energyCharged": "~ 22 kWh",
+                               "sessionStatus": "FINISHED",
+                               "isPublic": false
+                       },
+                       {
+                               "id": "2021-12-02T13:42:28Z_128fa4af",
+                               "title": "02.12.2021 14:42",
+                               "subtitle": "Uferstraße 4C • 2h 29min • -- EUR",
+                               "energyCharged": "~ 11 kWh",
+                               "sessionStatus": "FINISHED",
+                               "isPublic": false
+                       }
+               ]
+       },
+       "datePicker": {
+               "startDate": "2020-11-07T09:58:20Z",
+               "selectedDate": "2021-12-26T16:57:20Z",
+               "endDate": "2021-12-27T16:10:53Z"
+       }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charge-statistics-de.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charge-statistics-de.json
new file mode 100644 (file)
index 0000000..e368f22
--- /dev/null
@@ -0,0 +1,11 @@
+{
+       "description": "Dezember 2021",
+       "optStateType": "OPT_IN_WITH_SESSIONS",
+       "statistics": {
+               "totalEnergyCharged": 173,
+               "totalEnergyChargedSemantics": "Insgesamt circa 173 Kilowattstunden geladen",
+               "symbol": "~",
+               "numberOfChargingSessions": 13,
+               "numberOfChargingSessionsSemantics": "13 Ladevorgänge"
+       }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charge-statistics-en.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charge-statistics-en.json
new file mode 100644 (file)
index 0000000..a75fb1d
--- /dev/null
@@ -0,0 +1,11 @@
+{
+       "description": "December 2021",
+       "optStateType": "OPT_IN_WITH_SESSIONS",
+       "statistics": {
+               "totalEnergyCharged": 173,
+               "totalEnergyChargedSemantics": "Charged a total of approximately 173 kilowatt-hours",
+               "symbol": "~",
+               "numberOfChargingSessions": 13,
+               "numberOfChargingSessionsSemantics": "13 charging sessions"
+       }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charging-statistics_1.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charging-statistics_1.json
new file mode 100644 (file)
index 0000000..978aeca
--- /dev/null
@@ -0,0 +1,11 @@
+{
+  "description": "November 2021",
+  "optStateType": "OPT_IN_WITH_SESSIONS",
+  "statistics": {
+    "numberOfChargingSessions": 15,
+    "numberOfChargingSessionsSemantics": "mobile20chsChargingSessionNumberSemantics",
+    "symbol": "~",
+    "totalEnergyCharged": 144,
+    "totalEnergyChargedSemantics": "mobile20chsApproximatelyTotalChargedSemantics"
+  }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicle-charging.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicle-charging.json
new file mode 100644 (file)
index 0000000..4fb2461
--- /dev/null
@@ -0,0 +1,427 @@
+[
+       {
+               "vin": "anonymous",
+               "model": "i3 94 (+ REX)",
+               "year": 2017,
+               "brand": "BMW",
+               "headUnit": "ID5",
+               "isLscSupported": true,
+               "driveTrain": "ELECTRIC",
+               "puStep": "0321",
+               "iStep": "I001-21-03-530",
+               "telematicsUnit": "TCB1",
+               "hmiVersion": "ID4",
+               "bodyType": "I01",
+               "a4aType": "USB_ONLY",
+               "capabilities": {
+                       "isRemoteServicesBookingRequired": false,
+                       "isRemoteServicesActivationRequired": false,
+                       "lock": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds."
+                       },
+                       "unlock": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": true,
+                               "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds."
+                       },
+                       "lights": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Flash headlights now? Remote functions may take a few seconds."
+                       },
+                       "horn": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds."
+                       },
+                       "vehicleFinder": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Find your vehicle now? Remote functions may take a few seconds."
+                       },
+                       "sendPoi": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Send POI now? Remote functions may take a few seconds."
+                       },
+                       "lastStateCall": {
+                               "isNonLscFeatureEnabled": false,
+                               "lscState": "ACTIVATED"
+                       },
+                       "climateNow": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.",
+                               "executionPopup": {
+                                       "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.",
+                                       "popupType": "DIALOG",
+                                       "title": "Start Climatization",
+                                       "primaryButtonText": "Start",
+                                       "secondaryButtonText": "Cancel",
+                                       "iconId": 59733
+                               },
+                               "executionStopPopup": {
+                                       "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.",
+                                       "title": "Climate control is running"
+                               }
+                       },
+                       "isRemoteHistorySupported": true,
+                       "canRemoteHistoryBeDeleted": false,
+                       "isChargingHistorySupported": true,
+                       "isScanAndChargeSupported": true,
+                       "isDCSContractManagementSupported": true,
+                       "isBmwChargingSupported": true,
+                       "isMiniChargingSupported": false,
+                       "isChargeNowForBusinessSupported": true,
+                       "isDataPrivacyEnabled": false,
+                       "isChargingPlanSupported": true,
+                       "isChargingPowerLimitEnable": false,
+                       "isChargingTargetSocEnable": false,
+                       "isChargingLoudnessEnable": false,
+                       "isChargingSettingsEnabled": false,
+                       "isChargingHospitalityEnabled": false,
+                       "isEvGoChargingSupported": false,
+                       "isFindChargingEnabled": true,
+                       "isCustomerEsimSupported": false,
+                       "isCarSharingSupported": false,
+                       "isEasyChargeSupported": false
+               },
+               "connectedDriveServices": [],
+               "properties": {
+                       "lastUpdatedAt": "2021-12-25T22:29:22Z",
+                       "inMotion": false,
+                       "areDoorsLocked": true,
+                       "originCountryISO": "DE",
+                       "areDoorsClosed": true,
+                       "areDoorsOpen": false,
+                       "areWindowsClosed": true,
+                       "doorsAndWindows": {
+                               "doors": {
+                                       "driverFront": "CLOSED",
+                                       "driverRear": "CLOSED",
+                                       "passengerFront": "CLOSED",
+                                       "passengerRear": "CLOSED"
+                               },
+                               "windows": {
+                                       "driverFront": "CLOSED",
+                                       "passengerFront": "CLOSED"
+                               },
+                               "trunk": "CLOSED",
+                               "hood": "CLOSED",
+                               "moonroof": "CLOSED"
+                       },
+                       "isServiceRequired": false,
+                       "fuelLevel": {
+                               "value": 9,
+                               "units": "LITERS"
+                       },
+                       "chargingState": {
+                               "chargePercentage": 86,
+                               "state": "CHARGING",
+                               "type": "NOT_AVAILABLE",
+                               "isChargerConnected": true
+                       },
+                       "combustionRange": {
+                               "distance": {
+                                       "value": 97,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "combinedRange": {
+                               "distance": {
+                                       "value": 97,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "electricRange": {
+                               "distance": {
+                                       "value": 121,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "electricRangeAndStatus": {
+                               "chargePercentage": 86,
+                               "distance": {
+                                       "value": 121,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "checkControlMessages": [],
+                       "serviceRequired": [
+                               {
+                                       "type": "BRAKE_FLUID",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               },
+                               {
+                                       "type": "VEHICLE_CHECK",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               },
+                               {
+                                       "type": "OIL",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               },
+                               {
+                                       "type": "VEHICLE_TUV",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               }
+                       ],
+                       "vehicleLocation": {
+                               "coordinates": {
+                                       "latitude": 1.234,
+                                       "longitude": 9.876
+                               },
+                               "address": {
+                                       "formatted": "anonymous"
+                               },
+                               "heading": 73
+                       },
+                       "climateControl": {
+                               
+                       }
+               },
+               "isMappingPending": false,
+               "isMappingUnconfirmed": false,
+               "driverGuideInfo": {
+                       "title": "BMW\nDriver's Guide",
+                       "androidAppScheme": "com.bmwgroup.driversguide.row",
+                       "iosAppScheme": "bmwdriversguide:///open",
+                       "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row",
+                       "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8"
+               },
+               "themeSpecs": {
+                       "vehicleStatusBackgroundColor": {
+                               "red": 156,
+                               "green": 154,
+                               "blue": 152
+                       }
+               },
+               "status": {
+                       "lastUpdatedAt": "2021-12-25T22:29:22Z",
+                       "currentMileage": {
+                               "mileage": 31746,
+                               "units": "km",
+                               "formattedMileage": "31746"
+                       },
+                       "issues": {
+                               
+                       },
+                       "doorsGeneralState": "Locked",
+                       "checkControlMessagesGeneralState": "No Issues",
+                       "doorsAndWindows": [
+                               {
+                                       "iconId": 59757,
+                                       "title": "Lock status",
+                                       "state": "Locked",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59722,
+                                       "title": "All doors",
+                                       "state": "Closed",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59725,
+                                       "title": "All windows",
+                                       "state": "Closed",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59706,
+                                       "title": "Hood",
+                                       "state": "Closed",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59704,
+                                       "title": "Trunk",
+                                       "state": "Closed",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59705,
+                                       "title": "Sunroof",
+                                       "state": "Closed",
+                                       "criticalness": "nonCritical"
+                               }
+                       ],
+                       "checkControlMessages": [],
+                       "requiredServices": [
+                               {
+                                       "id": "BrakeFluid",
+                                       "title": "Brake fluid",
+                                       "iconId": 60223,
+                                       "longDescription": "Next service due by the specified date.",
+                                       "subtitle": "Due in November 2023",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "id": "VehicleCheck",
+                                       "title": "Vehicle check",
+                                       "iconId": 60215,
+                                       "longDescription": "Next vehicle check due after the specified distance or date.",
+                                       "subtitle": "Due in November 2023",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "id": "Oil",
+                                       "title": "Engine oil",
+                                       "iconId": 60197,
+                                       "longDescription": "Next service due after the specified distance or date.",
+                                       "subtitle": "Due in November 2023",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "id": "VehicleAdmissionTest",
+                                       "title": "Vehicle Inspection",
+                                       "iconId": 60111,
+                                       "longDescription": "Next state inspection due by the specified date.",
+                                       "subtitle": "Due in November 2023",
+                                       "criticalness": "nonCritical"
+                               }
+                       ],
+                       "recallMessages": [],
+                       "recallExternalUrl": null,
+                       "fuelIndicators": [
+                               {
+                                       "mainBarValue": 86,
+                                       "secondaryBarValue": 0,
+                                       "infoIconId": 59689,
+                                       "rangeIconId": 59683,
+                                       "rangeUnits": "km",
+                                       "rangeValue": "121",
+                                       "levelIconId": 59689,
+                                       "showsBar": true,
+                                       "levelUnits": "%",
+                                       "levelValue": "86",
+                                       "showBarGoal": false,
+                                       "barType": null,
+                                       "infoLabel": "100% at ~02:59 AM",
+                                       "isInaccurate": true,
+                                       "isCircleIcon": true,
+                                       "iconOpacity": "high",
+                                       "chargingType": "charging",
+                                       "chargingStatusType": "CHARGING",
+                                       "chargingStatusIndicatorType": "CHARGING"
+                               },
+                               {
+                                       "mainBarValue": 0,
+                                       "secondaryBarValue": 0,
+                                       "infoIconId": 59691,
+                                       "infoLabel": "Combined Range",
+                                       "rangeIconId": 59691,
+                                       "rangeUnits": "km",
+                                       "levelIconId": null,
+                                       "showsBar": false,
+                                       "levelUnits": null,
+                                       "levelValue": null,
+                                       "isInaccurate": false,
+                                       "isCircleIcon": false,
+                                       "iconOpacity": "high",
+                                       "chargingType": null,
+                                       "rangeValue": "218"
+                               },
+                               {
+                                       "mainBarValue": 0,
+                                       "secondaryBarValue": 0,
+                                       "infoIconId": 59681,
+                                       "infoLabel": "Extended Range",
+                                       "rangeIconId": null,
+                                       "rangeUnits": "km",
+                                       "rangeValue": "97",
+                                       "levelIconId": null,
+                                       "showsBar": false,
+                                       "levelUnits": null,
+                                       "levelValue": null,
+                                       "isInaccurate": false,
+                                       "isCircleIcon": false,
+                                       "iconOpacity": "high",
+                                       "chargingType": null
+                               }
+                       ],
+                       "timestampMessage": "Updated from vehicle 12/25/2021 11:29 PM",
+                       "chargingProfile": {
+                               "reductionOfChargeCurrent": {
+                                       "start": {
+                                               "hour": 11,
+                                               "minute": 0
+                                       },
+                                       "end": {
+                                               "hour": 14,
+                                               "minute": 30
+                                       }
+                               },
+                               "chargingMode": "immediateCharging",
+                               "chargingPreference": "chargingWindow",
+                               "chargingControlType": "weeklyPlanner",
+                               "departureTimes": [
+                                       {
+                                               "id": 1,
+                                               "action": "deactivate",
+                                               "timerWeekDays": [
+                                                       "monday",
+                                                       "tuesday",
+                                                       "wednesday",
+                                                       "thursday",
+                                                       "friday",
+                                                       "saturday",
+                                                       "sunday"
+                                               ],
+                                               "timeStamp": {
+                                                       "hour": 16,
+                                                       "minute": 0
+                                               }
+                                       },
+                                       {
+                                               "id": 2,
+                                               "action": "activate",
+                                               "timerWeekDays": [
+                                                       "sunday"
+                                               ],
+                                               "timeStamp": {
+                                                       "hour": 12,
+                                                       "minute": 2
+                                               }
+                                       },
+                                       {
+                                               "id": 3,
+                                               "action": "deactivate",
+                                               "timerWeekDays": [
+                                                       "saturday"
+                                               ],
+                                               "timeStamp": {
+                                                       "hour": 13,
+                                                       "minute": 3
+                                               }
+                                       },
+                                       {
+                                               "id": 4,
+                                               "action": "deactivate",
+                                               "timerWeekDays": [
+                                                       "sunday"
+                                               ],
+                                               "timeStamp": {
+                                                       "hour": 12,
+                                                       "minute": 2
+                                               }
+                                       }
+                               ],
+                               "climatisationOn": false,
+                               "chargingSettings": {
+                                       "targetSoc": 100,
+                                       "isAcCurrentLimitActive": false,
+                                       "hospitality": "NO_ACTION",
+                                       "idcc": "NO_ACTION"
+                               }
+                       }
+               },
+               "exFactoryPUStep": "0717",
+               "exFactoryILevel": "I001-17-07-500"
+       }
+]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicle-fully-charged.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicle-fully-charged.json
new file mode 100644 (file)
index 0000000..cf524eb
--- /dev/null
@@ -0,0 +1,423 @@
+ [
+    {
+        "vin": "anonymous",
+        "model": "i3 94 (+ REX)",
+        "year": 2017,
+        "brand": "BMW",
+        "headUnit": "ID5",
+        "isLscSupported": true,
+        "driveTrain": "ELECTRIC",
+        "puStep": "0321",
+        "iStep": "I001-21-03-530",
+        "telematicsUnit": "TCB1",
+        "hmiVersion": "ID4",
+        "bodyType": "I01",
+        "a4aType": "USB_ONLY",
+        "capabilities": {
+            "isRemoteServicesBookingRequired": false,
+            "isRemoteServicesActivationRequired": false,
+            "lock": {
+                "isEnabled": true,
+                "isPinAuthenticationRequired": false,
+                "executionMessage": "Jetzt Ihr Fahrzeug verriegeln? Remote-Funktionen können einige Sekunden dauern."
+            },
+            "unlock": {
+                "isEnabled": true,
+                "isPinAuthenticationRequired": true,
+                "executionMessage": "Jetzt Ihr Fahrzeug entriegeln? Remote-Funktionen können einige Sekunden dauern."
+            },
+            "lights": {
+                "isEnabled": true,
+                "isPinAuthenticationRequired": false,
+                "executionMessage": "Jetzt Scheinwerfer aufleuchten lassen? Remote-Funktionen können einige Sekunden dauern."
+            },
+            "horn": {
+                "isEnabled": true,
+                "isPinAuthenticationRequired": false,
+                "executionMessage": "Hupen ist in vielen Ländern nur in bestimmten Situationen erlaubt. Die Verantwortung für den Einsatz und die Einhaltung der jeweils geltenden Bestimmungen liegt allein bei Ihnen als Nutzer. \n\nJetzt hupen? Remote-Funktionen können einige Sekunden dauern."
+            },
+            "vehicleFinder": {
+                "isEnabled": true,
+                "isPinAuthenticationRequired": false,
+                "executionMessage": "Jetzt Ihr Fahrzeug finden? Remote-Funktionen können einige Sekunden dauern."
+            },
+            "sendPoi": {
+                "isEnabled": true,
+                "isPinAuthenticationRequired": false,
+                "executionMessage": "Jetzt POI senden? Remote-Funktionen können einige Sekunden dauern."
+            },
+            "lastStateCall": {
+                "isNonLscFeatureEnabled": false,
+                "lscState": "ACTIVATED"
+            },
+            "climateNow": {
+                "isEnabled": true,
+                "isPinAuthenticationRequired": false,
+                "executionMessage": "Jetzt belüften? Remote-Funktionen können einige Sekunden dauern.",
+                "executionPopup": {
+                    "executionMessage": "Jetzt klimatisieren? Remote-Funktionen können einige Sekunden dauern.",
+                    "popupType": "DIALOG",
+                    "title": "Klimatisierung starten",
+                    "primaryButtonText": "Start",
+                    "secondaryButtonText": "Abbrechen",
+                    "iconId": 59733
+                },
+                "executionStopPopup": {
+                    "executionMessage": "Jetzt Klimatisierung Ihres Fahrzeugs beenden? Remote-Funktionen können einige Sekunden dauern.",
+                    "title": "Klimatisierung läuft"
+                }
+            },
+            "isRemoteHistorySupported": true,
+            "canRemoteHistoryBeDeleted": false,
+            "isChargingHistorySupported": true,
+            "isScanAndChargeSupported": true,
+            "isDCSContractManagementSupported": true,
+            "isBmwChargingSupported": true,
+            "isMiniChargingSupported": false,
+            "isChargeNowForBusinessSupported": true,
+            "isDataPrivacyEnabled": false,
+            "isChargingPlanSupported": true,
+            "isChargingPowerLimitEnable": false,
+            "isChargingTargetSocEnable": false,
+            "isChargingLoudnessEnable": false,
+            "isChargingSettingsEnabled": false,
+            "isChargingHospitalityEnabled": false,
+            "isEvGoChargingSupported": false,
+            "isFindChargingEnabled": true,
+            "isCustomerEsimSupported": false,
+            "isCarSharingSupported": false,
+            "isEasyChargeSupported": false
+        },
+        "connectedDriveServices": [],
+        "properties": {
+            "lastUpdatedAt": "2022-01-04T21:04:49Z",
+            "inMotion": false,
+            "areDoorsLocked": true,
+            "originCountryISO": "DE",
+            "areDoorsClosed": true,
+            "areDoorsOpen": false,
+            "areWindowsClosed": true,
+            "doorsAndWindows": {
+                "doors": {
+                    "driverFront": "CLOSED",
+                    "driverRear": "CLOSED",
+                    "passengerFront": "CLOSED",
+                    "passengerRear": "CLOSED"
+                },
+                "windows": {
+                    "driverFront": "CLOSED",
+                    "passengerFront": "CLOSED"
+                },
+                "trunk": "CLOSED",
+                "hood": "CLOSED",
+                "moonroof": "CLOSED"
+            },
+            "isServiceRequired": false,
+            "fuelLevel": {
+                "value": 7,
+                "units": "LITERS"
+            },
+            "chargingState": {
+                "chargePercentage": 100,
+                "state": "COMPLETE",
+                "type": "NOT_AVAILABLE",
+                "isChargerConnected": true
+            },
+            "combustionRange": {
+                "distance": {
+                    "value": 90,
+                    "units": "KILOMETERS"
+                }
+            },
+            "combinedRange": {
+                "distance": {
+                    "value": 90,
+                    "units": "KILOMETERS"
+                }
+            },
+            "electricRange": {
+                "distance": {
+                    "value": 162,
+                    "units": "KILOMETERS"
+                }
+            },
+            "electricRangeAndStatus": {
+                "chargePercentage": 100,
+                "distance": {
+                    "value": 162,
+                    "units": "KILOMETERS"
+                }
+            },
+            "checkControlMessages": [],
+            "serviceRequired": [
+                {
+                    "type": "BRAKE_FLUID",
+                    "status": "OK",
+                    "dateTime": "2023-11-01T00:00:00.000Z"
+                },
+                {
+                    "type": "VEHICLE_CHECK",
+                    "status": "OK",
+                    "dateTime": "2023-11-01T00:00:00.000Z"
+                },
+                {
+                    "type": "OIL",
+                    "status": "OK",
+                    "dateTime": "2023-11-01T00:00:00.000Z"
+                },
+                {
+                    "type": "VEHICLE_TUV",
+                    "status": "OK",
+                    "dateTime": "2023-11-01T00:00:00.000Z"
+                }
+            ],
+            "vehicleLocation": {
+                "coordinates": {
+                    "latitude": 1.2345,
+                    "longitude": 9.876
+                },
+                "address": {
+                    "formatted": "anonymous"
+                },
+                "heading": 39
+            },
+            "climateControl": null
+        },
+        "isMappingPending": false,
+        "isMappingUnconfirmed": false,
+        "driverGuideInfo": {
+            "title": "BMW\nDriver's Guide",
+            "androidAppScheme": "com.bmwgroup.driversguide.row",
+            "iosAppScheme": "bmwdriversguide:///open",
+            "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row",
+            "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8"
+        },
+        "themeSpecs": {
+            "vehicleStatusBackgroundColor": {
+                "red": 156,
+                "green": 154,
+                "blue": 152
+            }
+        },
+        "status": {
+            "lastUpdatedAt": "2022-01-04T21:04:49Z",
+            "currentMileage": {
+                "mileage": 32219,
+                "units": "km",
+                "formattedMileage": "32.219"
+            },
+            "issues": null,
+            "doorsGeneralState": "Verriegelt",
+            "checkControlMessagesGeneralState": "Keine Probleme",
+            "doorsAndWindows": [
+                {
+                    "iconId": 59757,
+                    "title": "Verriegelungsstatus",
+                    "state": "Verriegelt",
+                    "criticalness": "nonCritical"
+                },
+                {
+                    "iconId": 59722,
+                    "title": "Alle Türen",
+                    "state": "Geschlossen",
+                    "criticalness": "nonCritical"
+                },
+                {
+                    "iconId": 59725,
+                    "title": "Alle Fenster",
+                    "state": "Geschlossen",
+                    "criticalness": "nonCritical"
+                },
+                {
+                    "iconId": 59706,
+                    "title": "Frontklappe",
+                    "state": "Geschlossen",
+                    "criticalness": "nonCritical"
+                },
+                {
+                    "iconId": 59704,
+                    "title": "Gepäckraum",
+                    "state": "Geschlossen",
+                    "criticalness": "nonCritical"
+                },
+                {
+                    "iconId": 59705,
+                    "title": "Glasdach",
+                    "state": "Geschlossen",
+                    "criticalness": "nonCritical"
+                }
+            ],
+            "checkControlMessages": [],
+            "requiredServices": [
+                {
+                    "id": "BrakeFluid",
+                    "title": "Bremsflüssigkeit",
+                    "iconId": 60223,
+                    "longDescription": "Nächster Wechsel spätestens zum angegebenen Termin.",
+                    "subtitle": "Fällig im November 2023",
+                    "criticalness": "nonCritical"
+                },
+                {
+                    "id": "VehicleCheck",
+                    "title": "Fahrzeug-Check",
+                    "iconId": 60215,
+                    "longDescription": "Nächste Sichtprüfung nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+                    "subtitle": "Fällig im November 2023",
+                    "criticalness": "nonCritical"
+                },
+                {
+                    "id": "Oil",
+                    "title": "Motoröl",
+                    "iconId": 60197,
+                    "longDescription": "Nächster Wechsel nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+                    "subtitle": "Fällig im November 2023",
+                    "criticalness": "nonCritical"
+                },
+                {
+                    "id": "VehicleAdmissionTest",
+                    "title": "Fahrzeuginspektion (HU)",
+                    "iconId": 60111,
+                    "longDescription": "Nächste gesetzliche Fahrzeuguntersuchung zum angegebenen Termin.",
+                    "subtitle": "Fällig im November 2023",
+                    "criticalness": "nonCritical"
+                }
+            ],
+            "recallMessages": [],
+            "recallExternalUrl": null,
+            "fuelIndicators": [
+                {
+                    "mainBarValue": 100,
+                    "secondaryBarValue": 0,
+                    "infoIconId": 59689,
+                    "rangeIconId": 59683,
+                    "rangeUnits": "km",
+                    "rangeValue": "162",
+                    "levelIconId": 59689,
+                    "showsBar": true,
+                    "levelUnits": "%",
+                    "levelValue": "100",
+                    "showBarGoal": false,
+                    "barType": null,
+                    "infoLabel": "Voll geladen",
+                    "isInaccurate": false,
+                    "isCircleIcon": true,
+                    "iconOpacity": "high",
+                    "chargingType": "charging_complete",
+                    "chargingStatusType": "FULLY_CHARGED",
+                    "chargingStatusIndicatorType": "FULLY_CHARGED"
+                },
+                {
+                    "mainBarValue": 0,
+                    "secondaryBarValue": 0,
+                    "infoIconId": 59691,
+                    "infoLabel": "Kombinierte Reichweite",
+                    "rangeIconId": 59691,
+                    "rangeUnits": "km",
+                    "levelIconId": null,
+                    "showsBar": false,
+                    "levelUnits": null,
+                    "levelValue": null,
+                    "isInaccurate": false,
+                    "isCircleIcon": false,
+                    "iconOpacity": "high",
+                    "chargingType": null,
+                    "rangeValue": "252"
+                },
+                {
+                    "mainBarValue": 0,
+                    "secondaryBarValue": 0,
+                    "infoIconId": 59681,
+                    "infoLabel": "Erweiterte Reichweite",
+                    "rangeIconId": null,
+                    "rangeUnits": "km",
+                    "rangeValue": "90",
+                    "levelIconId": null,
+                    "showsBar": false,
+                    "levelUnits": null,
+                    "levelValue": null,
+                    "isInaccurate": false,
+                    "isCircleIcon": false,
+                    "iconOpacity": "high",
+                    "chargingType": null
+                }
+            ],
+            "timestampMessage": "Aktualisiert vom Fahrzeug 4.1.2022 10:04 PM",
+            "chargingProfile": {
+                "reductionOfChargeCurrent": {
+                    "start": {
+                        "hour": 11,
+                        "minute": 0
+                    },
+                    "end": {
+                        "hour": 14,
+                        "minute": 30
+                    }
+                },
+                "chargingMode": "immediateCharging",
+                "chargingPreference": "chargingWindow",
+                "chargingControlType": "weeklyPlanner",
+                "departureTimes": [
+                    {
+                        "id": 1,
+                        "action": "deactivate",
+                        "timerWeekDays": [
+                            "monday",
+                            "tuesday",
+                            "wednesday",
+                            "thursday",
+                            "friday",
+                            "saturday",
+                            "sunday"
+                        ],
+                        "timeStamp": {
+                            "hour": 16,
+                            "minute": 0
+                        }
+                    },
+                    {
+                        "id": 2,
+                        "action": "activate",
+                        "timerWeekDays": [
+                            "sunday"
+                        ],
+                        "timeStamp": {
+                            "hour": 12,
+                            "minute": 2
+                        }
+                    },
+                    {
+                        "id": 3,
+                        "action": "deactivate",
+                        "timerWeekDays": [
+                            "saturday"
+                        ],
+                        "timeStamp": {
+                            "hour": 13,
+                            "minute": 3
+                        }
+                    },
+                    {
+                        "id": 4,
+                        "action": "deactivate",
+                        "timerWeekDays": [
+                            "sunday"
+                        ],
+                        "timeStamp": {
+                            "hour": 12,
+                            "minute": 2
+                        }
+                    }
+                ],
+                "climatisationOn": false,
+                "chargingSettings": {
+                    "targetSoc": 100,
+                    "isAcCurrentLimitActive": false,
+                    "hospitality": "NO_ACTION",
+                    "idcc": "NO_ACTION"
+                }
+            }
+        },
+        "exFactoryPUStep": "0717",
+        "exFactoryILevel": "I001-17-07-500"
+    }
+]
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicles-de.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicles-de.json
new file mode 100644 (file)
index 0000000..3be2f71
--- /dev/null
@@ -0,0 +1,427 @@
+[
+       {
+               "vin": "anonymous",
+               "model": "i3 94 (+ REX)",
+               "year": 2017,
+               "brand": "BMW",
+               "headUnit": "ID5",
+               "isLscSupported": true,
+               "driveTrain": "ELECTRIC",
+               "puStep": "0321",
+               "iStep": "I001-21-03-530",
+               "telematicsUnit": "TCB1",
+               "hmiVersion": "ID4",
+               "bodyType": "I01",
+               "a4aType": "USB_ONLY",
+               "capabilities": {
+                       "isRemoteServicesBookingRequired": false,
+                       "isRemoteServicesActivationRequired": false,
+                       "lock": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Jetzt Ihr Fahrzeug verriegeln? Remote-Funktionen können einige Sekunden dauern."
+                       },
+                       "unlock": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": true,
+                               "executionMessage": "Jetzt Ihr Fahrzeug entriegeln? Remote-Funktionen können einige Sekunden dauern."
+                       },
+                       "lights": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Jetzt Scheinwerfer aufleuchten lassen? Remote-Funktionen können einige Sekunden dauern."
+                       },
+                       "horn": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Hupen ist in vielen Ländern nur in bestimmten Situationen erlaubt. Die Verantwortung für den Einsatz und die Einhaltung der jeweils geltenden Bestimmungen liegt allein bei Ihnen als Nutzer. \n\nJetzt hupen? Remote-Funktionen können einige Sekunden dauern."
+                       },
+                       "vehicleFinder": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Jetzt Ihr Fahrzeug finden? Remote-Funktionen können einige Sekunden dauern."
+                       },
+                       "sendPoi": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Jetzt POI senden? Remote-Funktionen können einige Sekunden dauern."
+                       },
+                       "lastStateCall": {
+                               "isNonLscFeatureEnabled": false,
+                               "lscState": "ACTIVATED"
+                       },
+                       "climateNow": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Jetzt belüften? Remote-Funktionen können einige Sekunden dauern.",
+                               "executionPopup": {
+                                       "executionMessage": "Jetzt klimatisieren? Remote-Funktionen können einige Sekunden dauern.",
+                                       "popupType": "DIALOG",
+                                       "title": "Klimatisierung starten",
+                                       "primaryButtonText": "Start",
+                                       "secondaryButtonText": "Abbrechen",
+                                       "iconId": 59733
+                               },
+                               "executionStopPopup": {
+                                       "executionMessage": "Jetzt Klimatisierung Ihres Fahrzeugs beenden? Remote-Funktionen können einige Sekunden dauern.",
+                                       "title": "Klimatisierung läuft"
+                               }
+                       },
+                       "isRemoteHistorySupported": true,
+                       "canRemoteHistoryBeDeleted": false,
+                       "isChargingHistorySupported": true,
+                       "isScanAndChargeSupported": true,
+                       "isDCSContractManagementSupported": true,
+                       "isBmwChargingSupported": true,
+                       "isMiniChargingSupported": false,
+                       "isChargeNowForBusinessSupported": true,
+                       "isDataPrivacyEnabled": false,
+                       "isChargingPlanSupported": true,
+                       "isChargingPowerLimitEnable": false,
+                       "isChargingTargetSocEnable": false,
+                       "isChargingLoudnessEnable": false,
+                       "isChargingSettingsEnabled": false,
+                       "isChargingHospitalityEnabled": false,
+                       "isEvGoChargingSupported": false,
+                       "isFindChargingEnabled": true,
+                       "isCustomerEsimSupported": false,
+                       "isCarSharingSupported": false,
+                       "isEasyChargeSupported": false
+               },
+               "connectedDriveServices": [],
+               "properties": {
+                       "lastUpdatedAt": "2021-12-26T09:56:05Z",
+                       "inMotion": false,
+                       "areDoorsLocked": true,
+                       "originCountryISO": "DE",
+                       "areDoorsClosed": true,
+                       "areDoorsOpen": false,
+                       "areWindowsClosed": true,
+                       "doorsAndWindows": {
+                               "doors": {
+                                       "driverFront": "CLOSED",
+                                       "driverRear": "CLOSED",
+                                       "passengerFront": "CLOSED",
+                                       "passengerRear": "CLOSED"
+                               },
+                               "windows": {
+                                       "driverFront": "CLOSED",
+                                       "passengerFront": "CLOSED"
+                               },
+                               "trunk": "CLOSED",
+                               "hood": "CLOSED",
+                               "moonroof": "CLOSED"
+                       },
+                       "isServiceRequired": false,
+                       "fuelLevel": {
+                               "value": 9,
+                               "units": "LITERS"
+                       },
+                       "chargingState": {
+                               "chargePercentage": 100,
+                               "state": "CHARGING",
+                               "type": "NOT_AVAILABLE",
+                               "isChargerConnected": true
+                       },
+                       "combustionRange": {
+                               "distance": {
+                                       "value": 98,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "combinedRange": {
+                               "distance": {
+                                       "value": 98,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "electricRange": {
+                               "distance": {
+                                       "value": 146,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "electricRangeAndStatus": {
+                               "chargePercentage": 100,
+                               "distance": {
+                                       "value": 146,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "checkControlMessages": [],
+                       "serviceRequired": [
+                               {
+                                       "type": "BRAKE_FLUID",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               },
+                               {
+                                       "type": "VEHICLE_CHECK",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               },
+                               {
+                                       "type": "OIL",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               },
+                               {
+                                       "type": "VEHICLE_TUV",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               }
+                       ],
+                       "vehicleLocation": {
+                               "coordinates": {
+                                       "latitude": 1.234,
+                                       "longitude": 5.678
+                               },
+                               "address": {
+                                       "formatted": "where-ever"
+                               },
+                               "heading": 73
+                       },
+                       "climateControl": {
+                               
+                       }
+               },
+               "isMappingPending": false,
+               "isMappingUnconfirmed": false,
+               "driverGuideInfo": {
+                       "title": "BMW\nDriver's Guide",
+                       "androidAppScheme": "com.bmwgroup.driversguide.row",
+                       "iosAppScheme": "bmwdriversguide:///open",
+                       "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row",
+                       "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8"
+               },
+               "themeSpecs": {
+                       "vehicleStatusBackgroundColor": {
+                               "red": 156,
+                               "green": 154,
+                               "blue": 152
+                       }
+               },
+               "status": {
+                       "lastUpdatedAt": "2021-12-26T09:56:05Z",
+                       "currentMileage": {
+                               "mileage": 31746,
+                               "units": "km",
+                               "formattedMileage": "31.746"
+                       },
+                       "issues": {
+                               
+                       },
+                       "doorsGeneralState": "Verriegelt",
+                       "checkControlMessagesGeneralState": "Keine Probleme",
+                       "doorsAndWindows": [
+                               {
+                                       "iconId": 59757,
+                                       "title": "Verriegelungsstatus",
+                                       "state": "Verriegelt",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59722,
+                                       "title": "Alle Türen",
+                                       "state": "Geschlossen",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59725,
+                                       "title": "Alle Fenster",
+                                       "state": "Geschlossen",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59706,
+                                       "title": "Frontklappe",
+                                       "state": "Geschlossen",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59704,
+                                       "title": "Gepäckraum",
+                                       "state": "Geschlossen",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59705,
+                                       "title": "Glasdach",
+                                       "state": "Geschlossen",
+                                       "criticalness": "nonCritical"
+                               }
+                       ],
+                       "checkControlMessages": [],
+                       "requiredServices": [
+                               {
+                                       "id": "BrakeFluid",
+                                       "title": "Bremsflüssigkeit",
+                                       "iconId": 60223,
+                                       "longDescription": "Nächster Wechsel spätestens zum angegebenen Termin.",
+                                       "subtitle": "Fällig im November 2023",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "id": "VehicleCheck",
+                                       "title": "Fahrzeug-Check",
+                                       "iconId": 60215,
+                                       "longDescription": "Nächste Sichtprüfung nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+                                       "subtitle": "Fällig im November 2023",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "id": "Oil",
+                                       "title": "Motoröl",
+                                       "iconId": 60197,
+                                       "longDescription": "Nächster Wechsel nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+                                       "subtitle": "Fällig im November 2023",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "id": "VehicleAdmissionTest",
+                                       "title": "Fahrzeuginspektion (HU)",
+                                       "iconId": 60111,
+                                       "longDescription": "Nächste gesetzliche Fahrzeuguntersuchung zum angegebenen Termin.",
+                                       "subtitle": "Fällig im November 2023",
+                                       "criticalness": "nonCritical"
+                               }
+                       ],
+                       "recallMessages": [],
+                       "recallExternalUrl": null,
+                       "fuelIndicators": [
+                               {
+                                       "mainBarValue": 100,
+                                       "secondaryBarValue": 0,
+                                       "infoIconId": 59689,
+                                       "rangeIconId": 59683,
+                                       "rangeUnits": "km",
+                                       "rangeValue": "146",
+                                       "levelIconId": 59689,
+                                       "showsBar": true,
+                                       "levelUnits": "%",
+                                       "levelValue": "100",
+                                       "showBarGoal": false,
+                                       "barType": null,
+                                       "infoLabel": "um ~11:21 AM",
+                                       "isInaccurate": true,
+                                       "isCircleIcon": true,
+                                       "iconOpacity": "high",
+                                       "chargingType": "charging",
+                                       "chargingStatusType": "CHARGING",
+                                       "chargingStatusIndicatorType": "CHARGING"
+                               },
+                               {
+                                       "mainBarValue": 0,
+                                       "secondaryBarValue": 0,
+                                       "infoIconId": 59691,
+                                       "infoLabel": "Kombinierte Reichweite",
+                                       "rangeIconId": 59691,
+                                       "rangeUnits": "km",
+                                       "levelIconId": null,
+                                       "showsBar": false,
+                                       "levelUnits": null,
+                                       "levelValue": null,
+                                       "isInaccurate": false,
+                                       "isCircleIcon": false,
+                                       "iconOpacity": "high",
+                                       "chargingType": null,
+                                       "rangeValue": "244"
+                               },
+                               {
+                                       "mainBarValue": 0,
+                                       "secondaryBarValue": 0,
+                                       "infoIconId": 59681,
+                                       "infoLabel": "Erweiterte Reichweite",
+                                       "rangeIconId": null,
+                                       "rangeUnits": "km",
+                                       "rangeValue": "98",
+                                       "levelIconId": null,
+                                       "showsBar": false,
+                                       "levelUnits": null,
+                                       "levelValue": null,
+                                       "isInaccurate": false,
+                                       "isCircleIcon": false,
+                                       "iconOpacity": "high",
+                                       "chargingType": null
+                               }
+                       ],
+                       "timestampMessage": "Aktualisiert vom Fahrzeug 26.12.2021 10:56 AM",
+                       "chargingProfile": {
+                               "reductionOfChargeCurrent": {
+                                       "start": {
+                                               "hour": 11,
+                                               "minute": 0
+                                       },
+                                       "end": {
+                                               "hour": 14,
+                                               "minute": 30
+                                       }
+                               },
+                               "chargingMode": "immediateCharging",
+                               "chargingPreference": "chargingWindow",
+                               "chargingControlType": "weeklyPlanner",
+                               "departureTimes": [
+                                       {
+                                               "id": 1,
+                                               "action": "deactivate",
+                                               "timerWeekDays": [
+                                                       "monday",
+                                                       "tuesday",
+                                                       "wednesday",
+                                                       "thursday",
+                                                       "friday",
+                                                       "saturday",
+                                                       "sunday"
+                                               ],
+                                               "timeStamp": {
+                                                       "hour": 16,
+                                                       "minute": 0
+                                               }
+                                       },
+                                       {
+                                               "id": 2,
+                                               "action": "activate",
+                                               "timerWeekDays": [
+                                                       "sunday"
+                                               ],
+                                               "timeStamp": {
+                                                       "hour": 12,
+                                                       "minute": 2
+                                               }
+                                       },
+                                       {
+                                               "id": 3,
+                                               "action": "deactivate",
+                                               "timerWeekDays": [
+                                                       "saturday"
+                                               ],
+                                               "timeStamp": {
+                                                       "hour": 13,
+                                                       "minute": 3
+                                               }
+                                       },
+                                       {
+                                               "id": 4,
+                                               "action": "deactivate",
+                                               "timerWeekDays": [
+                                                       "sunday"
+                                               ],
+                                               "timeStamp": {
+                                                       "hour": 12,
+                                                       "minute": 2
+                                               }
+                                       }
+                               ],
+                               "climatisationOn": false,
+                               "chargingSettings": {
+                                       "targetSoc": 100,
+                                       "isAcCurrentLimitActive": false,
+                                       "hospitality": "NO_ACTION",
+                                       "idcc": "NO_ACTION"
+                               }
+                       }
+               },
+               "exFactoryPUStep": "0717",
+               "exFactoryILevel": "I001-17-07-500"
+       }
+]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicles.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicles.json
new file mode 100644 (file)
index 0000000..a822abe
--- /dev/null
@@ -0,0 +1,427 @@
+[
+       {
+               "vin": "anonymous",
+               "model": "i3 94 (+ REX)",
+               "year": 2017,
+               "brand": "BMW",
+               "headUnit": "ID5",
+               "isLscSupported": true,
+               "driveTrain": "ELECTRIC",
+               "puStep": "0321",
+               "iStep": "I001-21-03-530",
+               "telematicsUnit": "TCB1",
+               "hmiVersion": "ID4",
+               "bodyType": "I01",
+               "a4aType": "USB_ONLY",
+               "capabilities": {
+                       "isRemoteServicesBookingRequired": false,
+                       "isRemoteServicesActivationRequired": false,
+                       "lock": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds."
+                       },
+                       "unlock": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": true,
+                               "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds."
+                       },
+                       "lights": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Flash headlights now? Remote functions may take a few seconds."
+                       },
+                       "horn": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds."
+                       },
+                       "vehicleFinder": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Find your vehicle now? Remote functions may take a few seconds."
+                       },
+                       "sendPoi": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Send POI now? Remote functions may take a few seconds."
+                       },
+                       "lastStateCall": {
+                               "isNonLscFeatureEnabled": false,
+                               "lscState": "ACTIVATED"
+                       },
+                       "climateNow": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.",
+                               "executionPopup": {
+                                       "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.",
+                                       "popupType": "DIALOG",
+                                       "title": "Start Climatization",
+                                       "primaryButtonText": "Start",
+                                       "secondaryButtonText": "Cancel",
+                                       "iconId": 59733
+                               },
+                               "executionStopPopup": {
+                                       "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.",
+                                       "title": "Climate control is running"
+                               }
+                       },
+                       "isRemoteHistorySupported": true,
+                       "canRemoteHistoryBeDeleted": false,
+                       "isChargingHistorySupported": true,
+                       "isScanAndChargeSupported": true,
+                       "isDCSContractManagementSupported": true,
+                       "isBmwChargingSupported": true,
+                       "isMiniChargingSupported": false,
+                       "isChargeNowForBusinessSupported": true,
+                       "isDataPrivacyEnabled": false,
+                       "isChargingPlanSupported": true,
+                       "isChargingPowerLimitEnable": false,
+                       "isChargingTargetSocEnable": false,
+                       "isChargingLoudnessEnable": false,
+                       "isChargingSettingsEnabled": false,
+                       "isChargingHospitalityEnabled": false,
+                       "isEvGoChargingSupported": false,
+                       "isFindChargingEnabled": true,
+                       "isCustomerEsimSupported": false,
+                       "isCarSharingSupported": false,
+                       "isEasyChargeSupported": false
+               },
+               "connectedDriveServices": [],
+               "properties": {
+                       "lastUpdatedAt": "2021-12-21T16:46:02Z",
+                       "inMotion": false,
+                       "areDoorsLocked": true,
+                       "originCountryISO": "DE",
+                       "areDoorsClosed": true,
+                       "areDoorsOpen": false,
+                       "areWindowsClosed": true,
+                       "doorsAndWindows": {
+                               "doors": {
+                                       "driverFront": "CLOSED",
+                                       "driverRear": "CLOSED",
+                                       "passengerFront": "CLOSED",
+                                       "passengerRear": "CLOSED"
+                               },
+                               "windows": {
+                                       "driverFront": "CLOSED",
+                                       "passengerFront": "CLOSED"
+                               },
+                               "trunk": "CLOSED",
+                               "hood": "CLOSED",
+                               "moonroof": "CLOSED"
+                       },
+                       "isServiceRequired": false,
+                       "fuelLevel": {
+                               "value": 4,
+                               "units": "LITERS"
+                       },
+                       "chargingState": {
+                               "chargePercentage": 74,
+                               "state": "NOT_CHARGING",
+                               "type": "NOT_AVAILABLE",
+                               "isChargerConnected": false
+                       },
+                       "combustionRange": {
+                               "distance": {
+                                       "value": 31,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "combinedRange": {
+                               "distance": {
+                                       "value": 31,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "electricRange": {
+                               "distance": {
+                                       "value": 76,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "electricRangeAndStatus": {
+                               "chargePercentage": 74,
+                               "distance": {
+                                       "value": 76,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "checkControlMessages": [],
+                       "serviceRequired": [
+                               {
+                                       "type": "BRAKE_FLUID",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               },
+                               {
+                                       "type": "VEHICLE_CHECK",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               },
+                               {
+                                       "type": "OIL",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               },
+                               {
+                                       "type": "VEHICLE_TUV",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               }
+                       ],
+                       "vehicleLocation": {
+                               "coordinates": {
+                                       "latitude": 1.2345,
+                                       "longitude": 6.789
+                               },
+                               "address": {
+                                       "formatted": "anonymous"
+                               },
+                               "heading": 222
+                       },
+                       "climateControl": {
+                               
+                       }
+               },
+               "isMappingPending": false,
+               "isMappingUnconfirmed": false,
+               "driverGuideInfo": {
+                       "title": "BMW\nDriver's Guide",
+                       "androidAppScheme": "com.bmwgroup.driversguide.row",
+                       "iosAppScheme": "bmwdriversguide:///open",
+                       "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row",
+                       "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8"
+               },
+               "themeSpecs": {
+                       "vehicleStatusBackgroundColor": {
+                               "red": 156,
+                               "green": 154,
+                               "blue": 152
+                       }
+               },
+               "status": {
+                       "lastUpdatedAt": "2021-12-21T16:46:02Z",
+                       "currentMileage": {
+                               "mileage": 31537,
+                               "units": "km",
+                               "formattedMileage": "31537"
+                       },
+                       "issues": {
+                               
+                       },
+                       "doorsGeneralState": "Locked",
+                       "checkControlMessagesGeneralState": "No Issues",
+                       "doorsAndWindows": [
+                               {
+                                       "iconId": 59757,
+                                       "title": "Lock status",
+                                       "state": "Locked",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59722,
+                                       "title": "All doors",
+                                       "state": "Closed",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59725,
+                                       "title": "All windows",
+                                       "state": "Closed",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59706,
+                                       "title": "Hood",
+                                       "state": "Closed",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59704,
+                                       "title": "Trunk",
+                                       "state": "Closed",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59705,
+                                       "title": "Sunroof",
+                                       "state": "Closed",
+                                       "criticalness": "nonCritical"
+                               }
+                       ],
+                       "checkControlMessages": [],
+                       "requiredServices": [
+                               {
+                                       "id": "BrakeFluid",
+                                       "title": "Brake fluid",
+                                       "iconId": 60223,
+                                       "longDescription": "Next service due by the specified date.",
+                                       "subtitle": "Due in November 2023",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "id": "VehicleCheck",
+                                       "title": "Vehicle check",
+                                       "iconId": 60215,
+                                       "longDescription": "Next vehicle check due after the specified distance or date.",
+                                       "subtitle": "Due in November 2023",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "id": "Oil",
+                                       "title": "Engine oil",
+                                       "iconId": 60197,
+                                       "longDescription": "Next service due after the specified distance or date.",
+                                       "subtitle": "Due in November 2023",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "id": "VehicleAdmissionTest",
+                                       "title": "Vehicle Inspection",
+                                       "iconId": 60111,
+                                       "longDescription": "Next state inspection due by the specified date.",
+                                       "subtitle": "Due in November 2023",
+                                       "criticalness": "nonCritical"
+                               }
+                       ],
+                       "recallMessages": [],
+                       "recallExternalUrl": null,
+                       "fuelIndicators": [
+                               {
+                                       "mainBarValue": 74,
+                                       "secondaryBarValue": 0,
+                                       "infoIconId": 59694,
+                                       "rangeIconId": 59683,
+                                       "rangeUnits": "km",
+                                       "rangeValue": "76",
+                                       "levelIconId": 59694,
+                                       "showsBar": true,
+                                       "levelUnits": "%",
+                                       "levelValue": "74",
+                                       "showBarGoal": false,
+                                       "barType": null,
+                                       "infoLabel": "State of Charge",
+                                       "isInaccurate": false,
+                                       "isCircleIcon": false,
+                                       "iconOpacity": "high",
+                                       "chargingType": null,
+                                       "chargingStatusType": "DEFAULT",
+                                       "chargingStatusIndicatorType": "DEFAULT"
+                               },
+                               {
+                                       "mainBarValue": 0,
+                                       "secondaryBarValue": 0,
+                                       "infoIconId": 59691,
+                                       "infoLabel": "Combined Range",
+                                       "rangeIconId": 59691,
+                                       "rangeUnits": "km",
+                                       "levelIconId": null,
+                                       "showsBar": false,
+                                       "levelUnits": null,
+                                       "levelValue": null,
+                                       "isInaccurate": false,
+                                       "isCircleIcon": false,
+                                       "iconOpacity": "high",
+                                       "chargingType": null,
+                                       "rangeValue": "107"
+                               },
+                               {
+                                       "mainBarValue": 0,
+                                       "secondaryBarValue": 0,
+                                       "infoIconId": 59681,
+                                       "infoLabel": "Extended Range",
+                                       "rangeIconId": null,
+                                       "rangeUnits": "km",
+                                       "rangeValue": "31",
+                                       "levelIconId": null,
+                                       "showsBar": false,
+                                       "levelUnits": null,
+                                       "levelValue": null,
+                                       "isInaccurate": false,
+                                       "isCircleIcon": false,
+                                       "iconOpacity": "high",
+                                       "chargingType": null
+                               }
+                       ],
+                       "timestampMessage": "Updated from vehicle 12/21/2021 05:46 PM",
+                       "chargingProfile": {
+                               "reductionOfChargeCurrent": {
+                                       "start": {
+                                               "hour": 11,
+                                               "minute": 0
+                                       },
+                                       "end": {
+                                               "hour": 14,
+                                               "minute": 30
+                                       }
+                               },
+                               "chargingMode": "immediateCharging",
+                               "chargingPreference": "chargingWindow",
+                               "chargingControlType": "weeklyPlanner",
+                               "departureTimes": [
+                                       {
+                                               "id": 1,
+                                               "action": "deactivate",
+                                               "timerWeekDays": [
+                                                       "monday",
+                                                       "tuesday",
+                                                       "wednesday",
+                                                       "thursday",
+                                                       "friday",
+                                                       "saturday",
+                                                       "sunday"
+                                               ],
+                                               "timeStamp": {
+                                                       "hour": 16,
+                                                       "minute": 0
+                                               }
+                                       },
+                                       {
+                                               "id": 2,
+                                               "action": "activate",
+                                               "timerWeekDays": [
+                                                       "sunday"
+                                               ],
+                                               "timeStamp": {
+                                                       "hour": 12,
+                                                       "minute": 2
+                                               }
+                                       },
+                                       {
+                                               "id": 3,
+                                               "action": "deactivate",
+                                               "timerWeekDays": [
+                                                       "saturday"
+                                               ],
+                                               "timeStamp": {
+                                                       "hour": 13,
+                                                       "minute": 3
+                                               }
+                                       },
+                                       {
+                                               "id": 4,
+                                               "action": "deactivate",
+                                               "timerWeekDays": [
+                                                       "sunday"
+                                               ],
+                                               "timeStamp": {
+                                                       "hour": 12,
+                                                       "minute": 2
+                                               }
+                                       }
+                               ],
+                               "climatisationOn": false,
+                               "chargingSettings": {
+                                       "targetSoc": 100,
+                                       "isAcCurrentLimitActive": false,
+                                       "hospitality": "NO_ACTION",
+                                       "idcc": "NO_ACTION"
+                               }
+                       }
+               },
+               "exFactoryPUStep": "0717",
+               "exFactoryILevel": "I001-17-07-500"
+       }
+]
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicles_v2_bmw_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicles_v2_bmw_0.json
new file mode 100644 (file)
index 0000000..f8d7973
--- /dev/null
@@ -0,0 +1,387 @@
+[
+  {
+    "a4aType": "USB_ONLY",
+    "bodyType": "I01",
+    "brand": "BMW",
+    "capabilities": {
+      "canRemoteHistoryBeDeleted": false,
+      "climateNow": {
+        "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.",
+        "executionPopup": {
+          "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.",
+          "iconId": 59733,
+          "popupType": "DIALOG",
+          "primaryButtonText": "Start",
+          "secondaryButtonText": "Cancel",
+          "title": "Start Climatization"
+        },
+        "executionStopPopup": {
+          "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.",
+          "title": "Climate control is running"
+        },
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "horn": {
+        "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "isBmwChargingSupported": true,
+      "isCarSharingSupported": false,
+      "isChargeNowForBusinessSupported": false,
+      "isChargingHistorySupported": true,
+      "isChargingHospitalityEnabled": false,
+      "isChargingLoudnessEnable": false,
+      "isChargingPlanSupported": true,
+      "isChargingPowerLimitEnable": false,
+      "isChargingSettingsEnabled": false,
+      "isChargingTargetSocEnable": false,
+      "isCustomerEsimSupported": false,
+      "isDCSContractManagementSupported": true,
+      "isDataPrivacyEnabled": false,
+      "isEasyChargeSupported": false,
+      "isEvGoChargingSupported": false,
+      "isFindChargingEnabled": true,
+      "isMiniChargingSupported": false,
+      "isRemoteHistorySupported": true,
+      "isRemoteServicesActivationRequired": false,
+      "isRemoteServicesBookingRequired": false,
+      "isScanAndChargeSupported": false,
+      "lastStateCall": {
+        "isNonLscFeatureEnabled": false,
+        "lscState": "ACTIVATED"
+      },
+      "lights": {
+        "executionMessage": "Flash headlights now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "lock": {
+        "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "sendPoi": {
+        "executionMessage": "Send POI now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "unlock": {
+        "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": true
+      },
+      "vehicleFinder": {
+        "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      }
+    },
+    "connectedDriveServices": [],
+    "driveTrain": "ELECTRIC",
+    "driverGuideInfo": {
+      "androidAppScheme": "com.bmwgroup.driversguide.row",
+      "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row",
+      "iosAppScheme": "bmwdriversguide:///open",
+      "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8",
+      "title": "BMW\nDriver's Guide"
+    },
+    "exFactoryILevel": "I001-15-03-502",
+    "exFactoryPUStep": "0315",
+    "headUnit": "ID5",
+    "hmiVersion": "ID4",
+    "iStep": "I001-21-03-530",
+    "isLscSupported": true,
+    "isMappingPending": false,
+    "isMappingUnconfirmed": false,
+    "model": "i3 (+ REX)",
+    "properties": {
+      "areDoorsClosed": true,
+      "areDoorsLocked": false,
+      "areDoorsOpen": false,
+      "areWindowsClosed": true,
+      "chargingState": {
+        "chargePercentage": 100,
+        "isChargerConnected": true,
+        "state": "COMPLETE",
+        "type": "CONDUCTIVE"
+      },
+      "checkControlMessages": [],
+      "climateControl": {},
+      "combinedRange": {
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 64
+        }
+      },
+      "combustionRange": {
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 64
+        }
+      },
+      "doorsAndWindows": {
+        "doors": {
+          "driverFront": "CLOSED",
+          "driverRear": "CLOSED",
+          "passengerFront": "CLOSED",
+          "passengerRear": "CLOSED"
+        },
+        "hood": "CLOSED",
+        "moonroof": "CLOSED",
+        "trunk": "CLOSED",
+        "windows": {
+          "driverFront": "CLOSED",
+          "passengerFront": "CLOSED"
+        }
+      },
+      "electricRange": {
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 164
+        }
+      },
+      "electricRangeAndStatus": {
+        "chargePercentage": 100,
+        "distance": {
+          "units": "KILOMETERS",
+          "value": 164
+        }
+      },
+      "fuelLevel": {
+        "units": "LITERS",
+        "value": 5
+      },
+      "inMotion": false,
+      "isServiceRequired": false,
+      "lastUpdatedAt": "2021-11-11T06:49:47Z",
+      "originCountryISO": "CZ",
+      "serviceRequired": [
+        {
+          "dateTime": "2022-10-01T00:00:00.000Z",
+          "status": "OK",
+          "type": "BRAKE_FLUID"
+        },
+        {
+          "dateTime": "2023-05-01T00:00:00.000Z",
+          "status": "OK",
+          "type": "VEHICLE_CHECK"
+        },
+        {
+          "dateTime": "2023-05-01T00:00:00.000Z",
+          "status": "OK",
+          "type": "VEHICLE_TUV"
+        }
+      ],
+      "vehicleLocation": {
+        "address": {
+          "formatted": "some_formatted_address"
+        },
+        "coordinates": {
+          "latitude": 12.3456,
+          "longitude": 34.5678
+        },
+        "heading": 123
+      }
+    },
+    "puStep": "0321",
+    "status": {
+      "chargingProfile": {
+        "chargingControlType": "weeklyPlanner",
+        "chargingMode": "immediateCharging",
+        "chargingPreference": "chargingWindow",
+        "chargingSettings": {
+          "hospitality": "NO_ACTION",
+          "idcc": "NO_ACTION",
+          "isAcCurrentLimitActive": false,
+          "targetSoc": 100
+        },
+        "climatisationOn": true,
+        "departureTimes": [
+          {
+            "action": "activate",
+            "id": 1,
+            "timeStamp": {
+              "hour": 7,
+              "minute": 35
+            },
+            "timerWeekDays": [
+              "monday",
+              "tuesday",
+              "wednesday",
+              "thursday",
+              "friday"
+            ]
+          },
+          {
+            "action": "deactivate",
+            "id": 2,
+            "timeStamp": {
+              "hour": 18,
+              "minute": 0
+            },
+            "timerWeekDays": [
+              "monday",
+              "tuesday",
+              "wednesday",
+              "thursday",
+              "friday",
+              "saturday",
+              "sunday"
+            ]
+          },
+          {
+            "action": "deactivate",
+            "id": 3,
+            "timeStamp": {
+              "hour": 7,
+              "minute": 0
+            },
+            "timerWeekDays": []
+          },
+          {
+            "action": "deactivate",
+            "id": 4,
+            "timeStamp": {
+              "hour": 7,
+              "minute": 35
+            },
+            "timerWeekDays": [
+              "friday"
+            ]
+          }
+        ],
+        "reductionOfChargeCurrent": {
+          "end": {
+            "hour": 1,
+            "minute": 30
+          },
+          "start": {
+            "hour": 18,
+            "minute": 1
+          }
+        }
+      },
+      "checkControlMessages": [],
+      "checkControlMessagesGeneralState": "No Issues",
+      "currentMileage": {
+        "formattedMileage": "124462",
+        "mileage": 124462,
+        "units": "km"
+      },
+      "doorsAndWindows": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59722,
+          "state": "Closed",
+          "title": "All doors and windows"
+        }
+      ],
+      "doorsGeneralState": "Unlocked",
+      "fuelIndicators": [
+        {
+          "barType": null,
+          "chargingStatusIndicatorType": "FULLY_CHARGED",
+          "chargingStatusType": "FULLY_CHARGED",
+          "chargingType": "charging_complete",
+          "iconOpacity": "high",
+          "infoIconId": 59689,
+          "infoLabel": "Fully Charged",
+          "isCircleIcon": true,
+          "isInaccurate": false,
+          "levelIconId": 59689,
+          "levelUnits": "%",
+          "levelValue": "100",
+          "mainBarValue": 100,
+          "rangeIconId": 59683,
+          "rangeUnits": "km",
+          "rangeValue": "164",
+          "secondaryBarValue": 0,
+          "showBarGoal": false,
+          "showsBar": true
+        },
+        {
+          "chargingType": null,
+          "iconOpacity": "high",
+          "infoIconId": 59691,
+          "infoLabel": "Combined Range",
+          "isCircleIcon": false,
+          "isInaccurate": false,
+          "levelIconId": null,
+          "levelUnits": null,
+          "levelValue": null,
+          "mainBarValue": 0,
+          "rangeIconId": 59691,
+          "rangeUnits": "km",
+          "rangeValue": "228",
+          "secondaryBarValue": 0,
+          "showsBar": false
+        },
+        {
+          "chargingType": null,
+          "iconOpacity": "high",
+          "infoIconId": 59681,
+          "infoLabel": "Extended Range",
+          "isCircleIcon": false,
+          "isInaccurate": false,
+          "levelIconId": null,
+          "levelUnits": null,
+          "levelValue": null,
+          "mainBarValue": 0,
+          "rangeIconId": null,
+          "rangeUnits": "km",
+          "rangeValue": "64",
+          "secondaryBarValue": 0,
+          "showsBar": false
+        }
+      ],
+      "issues": {
+        "doorsAndWindows": {
+          "iconId": 59737,
+          "title": "Vehicle unlocked"
+        }
+      },
+      "lastUpdatedAt": "2021-11-11T06:49:47Z",
+      "recallExternalUrl": null,
+      "recallMessages": [],
+      "requiredServices": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60223,
+          "id": "BrakeFluid",
+          "longDescription": "Next service due by the specified date.",
+          "subtitle": "Due in October 2022",
+          "title": "Brake fluid"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60215,
+          "id": "VehicleCheck",
+          "longDescription": "Next vehicle check due after the specified distance or date.",
+          "subtitle": "Due in May 2023",
+          "title": "Vehicle check"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60111,
+          "id": "VehicleAdmissionTest",
+          "longDescription": "Next state inspection due by the specified date.",
+          "subtitle": "Due in May 2023",
+          "title": "Vehicle Inspection"
+        }
+      ],
+      "timestampMessage": "Updated from vehicle 11/11/2021 07:49 AM"
+    },
+    "telematicsUnit": "TCB1",
+    "themeSpecs": {
+      "vehicleStatusBackgroundColor": {
+        "blue": 86,
+        "green": 88,
+        "red": 90
+      }
+    },
+    "vin": "some_vin_I01_REX",
+    "year": 2015
+  }
+]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/TwoVehicles/anonymous-raw.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/TwoVehicles/anonymous-raw.json
new file mode 100644 (file)
index 0000000..5f092be
--- /dev/null
@@ -0,0 +1,386 @@
+    {
+       "vin": "anonymous",
+       "model": "i3 94 (+ REX)",
+       "year": 2017,
+       "brand": "BMW",
+       "headUnit": "ID5",
+       "isLscSupported": true,
+       "driveTrain": "ELECTRIC",
+       "puStep": "0321",
+       "iStep": "I001-21-03-530",
+       "telematicsUnit": "TCB1",
+       "hmiVersion": "ID4",
+       "bodyType": "I01",
+       "a4aType": "USB_ONLY",
+       "exFactoryPUStep": "0717",
+       "exFactoryILevel": "I001-17-07-500",
+       "capabilities": {
+               "isRemoteServicesBookingRequired": false,
+               "isRemoteServicesActivationRequired": false,
+               "isRemoteHistorySupported": true,
+               "canRemoteHistoryBeDeleted": false,
+               "isChargingHistorySupported": true,
+               "isScanAndChargeSupported": true,
+               "isDCSContractManagementSupported": true,
+               "isBmwChargingSupported": true,
+               "isMiniChargingSupported": false,
+               "isChargeNowForBusinessSupported": true,
+               "isDataPrivacyEnabled": false,
+               "isChargingPlanSupported": true,
+               "isChargingPowerLimitEnable": false,
+               "isChargingTargetSocEnable": false,
+               "isChargingLoudnessEnable": false,
+               "isChargingSettingsEnabled": false,
+               "isChargingHospitalityEnabled": false,
+               "isEvGoChargingSupported": false,
+               "isFindChargingEnabled": true,
+               "isCustomerEsimSupported": false,
+               "isCarSharingSupported": false,
+               "isEasyChargeSupported": false,
+               "lock": {
+                       "isEnabled": true,
+                       "isPinAuthenticationRequired": false,
+                       "executionMessage": "Jetzt Ihr Fahrzeug verriegeln? Remote-Funktionen können einige Sekunden dauern."
+               },
+               "unlock": {
+                       "isEnabled": true,
+                       "isPinAuthenticationRequired": true,
+                       "executionMessage": "Jetzt Ihr Fahrzeug entriegeln? Remote-Funktionen können einige Sekunden dauern."
+               },
+               "lights": {
+                       "isEnabled": true,
+                       "isPinAuthenticationRequired": false,
+                       "executionMessage": "Jetzt Scheinwerfer aufleuchten lassen? Remote-Funktionen können einige Sekunden dauern."
+               },
+               "horn": {
+                       "isEnabled": true,
+                       "isPinAuthenticationRequired": false,
+                       "executionMessage": "Hupen ist in vielen Ländern nur in bestimmten Situationen erlaubt. Die Verantwortung für den Einsatz und die Einhaltung der jeweils geltenden Bestimmungen liegt allein bei Ihnen als Nutzer. \n\nJetzt hupen? Remote-Funktionen können einige Sekunden dauern."
+               },
+               "vehicleFinder": {
+                       "isEnabled": true,
+                       "isPinAuthenticationRequired": false,
+                       "executionMessage": "Jetzt Ihr Fahrzeug finden? Remote-Funktionen können einige Sekunden dauern."
+               },
+               "sendPoi": {
+                       "isEnabled": true,
+                       "isPinAuthenticationRequired": false,
+                       "executionMessage": "Jetzt POI senden? Remote-Funktionen können einige Sekunden dauern."
+               },
+               "climateNow": {
+                       "isEnabled": true,
+                       "isPinAuthenticationRequired": false,
+                       "executionMessage": "Jetzt belüften? Remote-Funktionen können einige Sekunden dauern."
+               }
+       },
+       "properties": {
+               "lastUpdatedAt": "2022-01-03T18:54:57Z",
+               "inMotion": false,
+               "areDoorsLocked": true,
+               "originCountryISO": "DE",
+               "areDoorsClosed": true,
+               "areDoorsOpen": false,
+               "areWindowsClosed": true,
+               "doorsAndWindows": {
+                       "doors": {
+                               "driverFront": "CLOSED",
+                               "driverRear": "CLOSED",
+                               "passengerFront": "CLOSED",
+                               "passengerRear": "CLOSED"
+                       },
+                       "windows": {
+                               "driverFront": "CLOSED",
+                               "passengerFront": "CLOSED"
+                       },
+                       "trunk": "CLOSED",
+                       "hood": "CLOSED",
+                       "moonroof": "CLOSED"
+               },
+               "isServiceRequired": false,
+               "fuelLevel": {
+                       "value": 7,
+                       "units": "LITERS"
+               },
+               "chargingState": {
+                       "chargePercentage": 47,
+                       "state": "NOT_CHARGING",
+                       "type": "NOT_AVAILABLE",
+                       "isChargerConnected": false
+               },
+               "combustionRange": {
+                       "chargePercentage": 0,
+                       "distance": {
+                               "value": 96,
+                               "units": "KILOMETERS"
+                       }
+               },
+               "combinedRange": {
+                       "chargePercentage": 0,
+                       "distance": {
+                               "value": 96,
+                               "units": "KILOMETERS"
+                       }
+               },
+               "electricRange": {
+                       "chargePercentage": 0,
+                       "distance": {
+                               "value": 78,
+                               "units": "KILOMETERS"
+                       }
+               },
+               "electricRangeAndStatus": {
+                       "chargePercentage": 47,
+                       "distance": {
+                               "value": 78,
+                               "units": "KILOMETERS"
+                       }
+               },
+               "checkControlMessages": [],
+               "serviceRequired": [
+                       {
+                               "type": "BRAKE_FLUID",
+                               "status": "OK",
+                               "dateTime": "2023-11-01T00:00:00.000Z"
+                       },
+                       {
+                               "type": "VEHICLE_CHECK",
+                               "status": "OK",
+                               "dateTime": "2023-11-01T00:00:00.000Z"
+                       },
+                       {
+                               "type": "OIL",
+                               "status": "OK",
+                               "dateTime": "2023-11-01T00:00:00.000Z"
+                       },
+                       {
+                               "type": "VEHICLE_TUV",
+                               "status": "OK",
+                               "dateTime": "2023-11-01T00:00:00.000Z"
+                       }
+               ],
+               "vehicleLocation": {
+                       "coordinates": {
+                               "latitude": 1.234,
+                               "longitude": 9.876
+                       },
+                       "address": {
+                               "formatted": "anonymous"
+                       },
+                       "heading": 39
+               }
+       },
+       "isMappingPending": false,
+       "isMappingUnconfirmed": false,
+       "status": {
+               "lastUpdatedAt": "2022-01-03T18:54:57Z",
+               "currentMileage": {
+                       "mileage": 32179,
+                       "units": "km",
+                       "formattedMileage": "32.179"
+               },
+               "issues": null,
+               "doorsGeneralState": "Verriegelt",
+               "checkControlMessagesGeneralState": "Keine Probleme",
+               "doorsAndWindows": [
+                       {
+                               "iconId": 59757,
+                               "title": "Verriegelungsstatus",
+                               "state": "Verriegelt",
+                               "criticalness": "nonCritical"
+                       },
+                       {
+                               "iconId": 59722,
+                               "title": "Alle Türen",
+                               "state": "Geschlossen",
+                               "criticalness": "nonCritical"
+                       },
+                       {
+                               "iconId": 59725,
+                               "title": "Alle Fenster",
+                               "state": "Geschlossen",
+                               "criticalness": "nonCritical"
+                       },
+                       {
+                               "iconId": 59706,
+                               "title": "Frontklappe",
+                               "state": "Geschlossen",
+                               "criticalness": "nonCritical"
+                       },
+                       {
+                               "iconId": 59704,
+                               "title": "Gepäckraum",
+                               "state": "Geschlossen",
+                               "criticalness": "nonCritical"
+                       },
+                       {
+                               "iconId": 59705,
+                               "title": "Glasdach",
+                               "state": "Geschlossen",
+                               "criticalness": "nonCritical"
+                       }
+               ],
+               "checkControlMessages": [],
+               "requiredServices": [
+                       {
+                               "id": "BrakeFluid",
+                               "title": "Bremsflüssigkeit",
+                               "iconId": 60223,
+                               "longDescription": "Nächster Wechsel spätestens zum angegebenen Termin.",
+                               "subtitle": "Fällig im November 2023",
+                               "criticalness": "nonCritical"
+                       },
+                       {
+                               "id": "VehicleCheck",
+                               "title": "Fahrzeug-Check",
+                               "iconId": 60215,
+                               "longDescription": "Nächste Sichtprüfung nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+                               "subtitle": "Fällig im November 2023",
+                               "criticalness": "nonCritical"
+                       },
+                       {
+                               "id": "Oil",
+                               "title": "Motoröl",
+                               "iconId": 60197,
+                               "longDescription": "Nächster Wechsel nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+                               "subtitle": "Fällig im November 2023",
+                               "criticalness": "nonCritical"
+                       },
+                       {
+                               "id": "VehicleAdmissionTest",
+                               "title": "Fahrzeuginspektion (HU)",
+                               "iconId": 60111,
+                               "longDescription": "Nächste gesetzliche Fahrzeuguntersuchung zum angegebenen Termin.",
+                               "subtitle": "Fällig im November 2023",
+                               "criticalness": "nonCritical"
+                       }
+               ],
+               "fuelIndicators": [
+                       {
+                               "mainBarValue": 47,
+                               "rangeUnits": "km",
+                               "rangeValue": "78",
+                               "levelUnits": "%",
+                               "levelValue": "47",
+                               "secondaryBarValue": 0,
+                               "infoIconId": 59694,
+                               "rangeIconId": 59683,
+                               "levelIconId": 59694,
+                               "showsBar": true,
+                               "showBarGoal": false,
+                               "infoLabel": "Ladezustand",
+                               "isInaccurate": false,
+                               "isCircleIcon": false,
+                               "iconOpacity": "high",
+                               "chargingStatusType": "DEFAULT",
+                               "chargingStatusIndicatorType": "DEFAULT"
+                       },
+                       {
+                               "mainBarValue": 0,
+                               "rangeUnits": "km",
+                               "rangeValue": "174",
+                               "secondaryBarValue": 0,
+                               "infoIconId": 59691,
+                               "rangeIconId": 59691,
+                               "levelIconId": 0,
+                               "showsBar": false,
+                               "showBarGoal": false,
+                               "infoLabel": "Kombinierte Reichweite",
+                               "isInaccurate": false,
+                               "isCircleIcon": false,
+                               "iconOpacity": "high"
+                       },
+                       {
+                               "mainBarValue": 0,
+                               "rangeUnits": "km",
+                               "rangeValue": "96",
+                               "secondaryBarValue": 0,
+                               "infoIconId": 59681,
+                               "rangeIconId": 0,
+                               "levelIconId": 0,
+                               "showsBar": false,
+                               "showBarGoal": false,
+                               "infoLabel": "Erweiterte Reichweite",
+                               "isInaccurate": false,
+                               "isCircleIcon": false,
+                               "iconOpacity": "high"
+                       }
+               ],
+               "timestampMessage": "Aktualisiert vom Fahrzeug 3.1.2022 07:54 PM",
+               "chargingProfile": {
+                       "reductionOfChargeCurrent": {
+                               "start": {
+                                       "hour": 11,
+                                       "minute": 0
+                               },
+                               "end": {
+                                       "hour": 14,
+                                       "minute": 30
+                               }
+                       },
+                       "chargingMode": "immediateCharging",
+                       "chargingPreference": "chargingWindow",
+                       "chargingControlType": "weeklyPlanner",
+                       "departureTimes": [
+                               {
+                                       "id": 1,
+                                       "action": "deactivate",
+                                       "timeStamp": {
+                                               "hour": 16,
+                                               "minute": 0
+                                       },
+                                       "timerWeekDays": [
+                                               "monday",
+                                               "tuesday",
+                                               "wednesday",
+                                               "thursday",
+                                               "friday",
+                                               "saturday",
+                                               "sunday"
+                                       ]
+                               },
+                               {
+                                       "id": 2,
+                                       "action": "activate",
+                                       "timeStamp": {
+                                               "hour": 12,
+                                               "minute": 2
+                                       },
+                                       "timerWeekDays": [
+                                               "sunday"
+                                       ]
+                               },
+                               {
+                                       "id": 3,
+                                       "action": "deactivate",
+                                       "timeStamp": {
+                                               "hour": 13,
+                                               "minute": 3
+                                       },
+                                       "timerWeekDays": [
+                                               "saturday"
+                                       ]
+                               },
+                               {
+                                       "id": 4,
+                                       "action": "deactivate",
+                                       "timeStamp": {
+                                               "hour": 12,
+                                               "minute": 2
+                                       },
+                                       "timerWeekDays": [
+                                               "sunday"
+                                       ]
+                               }
+                       ],
+                       "climatisationOn": false,
+                       "chargingSettings": {
+                               "targetSoc": 100,
+                               "isAcCurrentLimitActive": false,
+                               "hospitality": "NO_ACTION",
+                               "idcc": "NO_ACTION"
+                       }
+               }
+       },
+       "valid": false
+}
+
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/TwoVehicles/f11-raw.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/TwoVehicles/f11-raw.json
new file mode 100644 (file)
index 0000000..9fb517b
--- /dev/null
@@ -0,0 +1,283 @@
+ {
+       "a4aType": "NOT_SUPPORTED",
+       "bodyType": "F11",
+       "brand": "BMW",
+       "capabilities": {
+               "canRemoteHistoryBeDeleted": false,
+               "climateNow": {
+                       "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.",
+                       "executionPopup": {
+                               "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.",
+                               "iconId": 59733,
+                               "popupType": "DIALOG",
+                               "primaryButtonText": "Start",
+                               "secondaryButtonText": "Cancel",
+                               "title": "Start Ventilation"
+                       },
+                       "executionStopPopup": {
+                               "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.",
+                               "title": "Climate control is running"
+                       },
+                       "isEnabled": true,
+                       "isPinAuthenticationRequired": false
+               },
+               "climateTimer": {
+                       "isEnabled": true,
+                       "isPinAuthenticationRequired": false,
+                       "isToggleEnabled": true,
+                       "page": {
+                               "description": "By setting a start time you let the vehicle know when you plan to use it.",
+                               "primaryButtonText": "SEND TO VEHICLE",
+                               "secondaryButtonText": "DEACTIVATE AND SEND TO VEHICLE",
+                               "subtitle": "Set start time",
+                               "title": "Ventilation timer"
+                       },
+                       "tile": {
+                               "description": "Plan start time",
+                               "iconId": 59774,
+                               "title": "Ventilation timer"
+                       }
+               },
+               "isBmwChargingSupported": false,
+               "isCarSharingSupported": false,
+               "isChargeNowForBusinessSupported": false,
+               "isChargingHistorySupported": false,
+               "isChargingHospitalityEnabled": false,
+               "isChargingLoudnessEnable": false,
+               "isChargingPlanSupported": false,
+               "isChargingPowerLimitEnable": false,
+               "isChargingSettingsEnabled": false,
+               "isChargingTargetSocEnable": false,
+               "isCustomerEsimSupported": false,
+               "isDCSContractManagementSupported": false,
+               "isDataPrivacyEnabled": false,
+               "isEasyChargeSupported": false,
+               "isEvGoChargingSupported": false,
+               "isFindChargingEnabled": false,
+               "isMiniChargingSupported": false,
+               "isRemoteHistorySupported": true,
+               "isRemoteServicesActivationRequired": false,
+               "isRemoteServicesBookingRequired": false,
+               "isScanAndChargeSupported": false,
+               "lastStateCall": {
+                       "isNonLscFeatureEnabled": false,
+                       "lscState": "NOT_CAPABLE"
+               },
+               "lights": {
+                       "executionMessage": "Flash headlights now? Remote functions may take a few seconds.",
+                       "isEnabled": true,
+                       "isPinAuthenticationRequired": false
+               },
+               "lock": {
+                       "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.",
+                       "isEnabled": true,
+                       "isPinAuthenticationRequired": false
+               },
+               "sendPoi": {
+                       "executionMessage": "Send POI now? Remote functions may take a few seconds.",
+                       "isEnabled": true,
+                       "isPinAuthenticationRequired": false
+               },
+               "unlock": {
+                       "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.",
+                       "isEnabled": true,
+                       "isPinAuthenticationRequired": true
+               },
+               "vehicleFinder": {
+                       "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.",
+                       "isEnabled": false,
+                       "isPinAuthenticationRequired": false
+               }
+       },
+       "connectedDriveServices": [],
+       "driveTrain": "COMBUSTION",
+       "driverGuideInfo": {
+               "androidAppScheme": "com.bmwgroup.driversguide.row",
+               "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row",
+               "iosAppScheme": "bmwdriversguide:///open",
+               "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8",
+               "title": "BMW\nDriver's Guide"
+       },
+       "exFactoryILevel": "F010-12-11-503",
+       "exFactoryPUStep": "1112",
+       "headUnit": "ID5",
+       "hmiVersion": "ID4",
+       "iStep": "F010-12-11-503",
+       "isLscSupported": false,
+       "isMappingPending": false,
+       "isMappingUnconfirmed": false,
+       "model": "530d",
+       "properties": {
+               "checkControlMessages": [],
+               "climateControl": {
+                       
+               },
+               "doorsAndWindows": {
+                       "doors": {
+                               
+                       },
+                       "windows": {
+                               
+                       }
+               },
+               "fuelLevel": {
+                       "units": "LITERS",
+                       "value": 24
+               },
+               "inMotion": false,
+               "isServiceRequired": false,
+               "lastUpdatedAt": "2021-03-10T08:02:08Z",
+               "originCountryISO": "GB",
+               "serviceRequired": [
+                       {
+                               "dateTime": "2022-10-01T00:00:00.000Z",
+                               "status": "OK",
+                               "type": "BRAKE_FLUID"
+                       },
+                       {
+                               "dateTime": "2022-10-01T00:00:00.000Z",
+                               "distance": {
+                                       "units": "KILOMETERS",
+                                       "value": 25000
+                               },
+                               "status": "OK",
+                               "type": "OIL"
+                       },
+                       {
+                               "dateTime": "2024-10-01T00:00:00.000Z",
+                               "distance": {
+                                       "units": "KILOMETERS",
+                                       "value": 60000
+                               },
+                               "status": "OK",
+                               "type": "VEHICLE_CHECK"
+                       }
+               ]
+       },
+       "puStep": "1112",
+       "status": {
+               "checkControlMessages": [
+                       {
+                               "criticalness": "nonCritical",
+                               "iconId": 60197,
+                               "state": "OK",
+                               "title": "Engine Oil"
+                       },
+                       {
+                               "criticalness": "semiCritical",
+                               "iconId": 60217,
+                               "id": "229",
+                               "longDescription": "Charge by driving for longer periods or use external charger. Functions requiring battery will be switched off.",
+                               "state": "Medium",
+                               "title": "Battery discharged: Start engine"
+                       },
+                       {
+                               "criticalness": "nonCritical",
+                               "iconId": 60217,
+                               "id": "50",
+                               "longDescription": "System unable to monitor tire pressure. Check tire pressures manually. Continued driving possible. Consult service center.",
+                               "state": "Low",
+                               "title": "Flat Tire Monitor (FTM) inactive"
+                       }
+               ],
+               "checkControlMessagesGeneralState": "Multiple Issues",
+               "doorsAndWindows": [
+                       {
+                               "criticalness": "nonCritical",
+                               "iconId": 59726,
+                               "state": "Unknown",
+                               "title": "All doors"
+                       },
+                       {
+                               "criticalness": "nonCritical",
+                               "iconId": 59701,
+                               "state": "Unknown",
+                               "title": "Left front window"
+                       },
+                       {
+                               "criticalness": "nonCritical",
+                               "iconId": 59700,
+                               "state": "Unknown",
+                               "title": "Right front window"
+                       },
+                       {
+                               "criticalness": "nonCritical",
+                               "iconId": 59703,
+                               "state": "Unknown",
+                               "title": "Left rear window"
+                       },
+                       {
+                               "criticalness": "nonCritical",
+                               "iconId": 59702,
+                               "state": "Unknown",
+                               "title": "Right rear window"
+                       },
+                       {
+                               "criticalness": "nonCritical",
+                               "iconId": 59721,
+                               "state": "Unknown",
+                               "title": "Back window"
+                       }
+               ],
+               "doorsGeneralState": "Unknown",
+               "fuelIndicators": [
+                       {
+                               "chargingType": null,
+                               "iconOpacity": "high",
+                               "infoIconId": 59930,
+                               "infoLabel": "Fuel Level",
+                               "isCircleIcon": false,
+                               "isInaccurate": true,
+                               "levelIconId": 59682,
+                               "levelUnits": "l",
+                               "levelValue": "24",
+                               "mainBarValue": 0,
+                               "rangeIconId": 59681,
+                               "rangeUnits": "mi",
+                               "rangeValue": "- -",
+                               "secondaryBarValue": 0,
+                               "showsBar": false
+                       }
+               ],
+               "lastUpdatedAt": "2021-03-10T08:02:08Z",
+               "recallExternalUrl": null,
+               "recallMessages": [],
+               "requiredServices": [
+                       {
+                               "criticalness": "nonCritical",
+                               "iconId": 60223,
+                               "id": "BrakeFluid",
+                               "longDescription": "Next service due by the specified date.",
+                               "subtitle": "Due in October 2022",
+                               "title": "Brake fluid"
+                       },
+                       {
+                               "criticalness": "nonCritical",
+                               "iconId": 60197,
+                               "id": "Oil",
+                               "longDescription": "Next service due after the specified distance or date.",
+                               "subtitle": "Due in October 2022 or 15534 mi",
+                               "title": "Engine oil"
+                       },
+                       {
+                               "criticalness": "nonCritical",
+                               "iconId": 60215,
+                               "id": "VehicleCheck",
+                               "longDescription": "Next vehicle check due after the specified distance or date.",
+                               "subtitle": "Due in October 2024 or 37282 mi",
+                               "title": "Vehicle check"
+                       }
+               ],
+               "timestampMessage": "Updated from vehicle 3/11/2021 08:02 AM"
+       },
+       "telematicsUnit": "TCB1",
+       "themeSpecs": {
+               "vehicleStatusBackgroundColor": {
+                       "blue": 158,
+                       "green": 158,
+                       "red": 158
+               }
+       },
+       "vin": "some_vin_F11",
+       "year": 2012
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/TwoVehicles/two-vehicles.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/TwoVehicles/two-vehicles.json
new file mode 100644 (file)
index 0000000..59cf181
--- /dev/null
@@ -0,0 +1,665 @@
+[
+       {
+               "vin": "anonymous",
+               "model": "i3 94 (+ REX)",
+               "year": 2017,
+               "brand": "BMW",
+               "headUnit": "ID5",
+               "isLscSupported": true,
+               "driveTrain": "ELECTRIC",
+               "puStep": "0321",
+               "iStep": "I001-21-03-530",
+               "telematicsUnit": "TCB1",
+               "hmiVersion": "ID4",
+               "bodyType": "I01",
+               "a4aType": "USB_ONLY",
+               "exFactoryPUStep": "0717",
+               "exFactoryILevel": "I001-17-07-500",
+               "capabilities": {
+                       "isRemoteServicesBookingRequired": false,
+                       "isRemoteServicesActivationRequired": false,
+                       "isRemoteHistorySupported": true,
+                       "canRemoteHistoryBeDeleted": false,
+                       "isChargingHistorySupported": true,
+                       "isScanAndChargeSupported": true,
+                       "isDCSContractManagementSupported": true,
+                       "isBmwChargingSupported": true,
+                       "isMiniChargingSupported": false,
+                       "isChargeNowForBusinessSupported": true,
+                       "isDataPrivacyEnabled": false,
+                       "isChargingPlanSupported": true,
+                       "isChargingPowerLimitEnable": false,
+                       "isChargingTargetSocEnable": false,
+                       "isChargingLoudnessEnable": false,
+                       "isChargingSettingsEnabled": false,
+                       "isChargingHospitalityEnabled": false,
+                       "isEvGoChargingSupported": false,
+                       "isFindChargingEnabled": true,
+                       "isCustomerEsimSupported": false,
+                       "isCarSharingSupported": false,
+                       "isEasyChargeSupported": false,
+                       "lock": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Jetzt Ihr Fahrzeug verriegeln? Remote-Funktionen können einige Sekunden dauern."
+                       },
+                       "unlock": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": true,
+                               "executionMessage": "Jetzt Ihr Fahrzeug entriegeln? Remote-Funktionen können einige Sekunden dauern."
+                       },
+                       "lights": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Jetzt Scheinwerfer aufleuchten lassen? Remote-Funktionen können einige Sekunden dauern."
+                       },
+                       "horn": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Hupen ist in vielen Ländern nur in bestimmten Situationen erlaubt. Die Verantwortung für den Einsatz und die Einhaltung der jeweils geltenden Bestimmungen liegt allein bei Ihnen als Nutzer. \n\nJetzt hupen? Remote-Funktionen können einige Sekunden dauern."
+                       },
+                       "vehicleFinder": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Jetzt Ihr Fahrzeug finden? Remote-Funktionen können einige Sekunden dauern."
+                       },
+                       "sendPoi": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Jetzt POI senden? Remote-Funktionen können einige Sekunden dauern."
+                       },
+                       "climateNow": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Jetzt belüften? Remote-Funktionen können einige Sekunden dauern."
+                       }
+               },
+               "properties": {
+                       "lastUpdatedAt": "2022-01-03T18:54:57Z",
+                       "inMotion": false,
+                       "areDoorsLocked": true,
+                       "originCountryISO": "DE",
+                       "areDoorsClosed": true,
+                       "areDoorsOpen": false,
+                       "areWindowsClosed": true,
+                       "doorsAndWindows": {
+                               "doors": {
+                                       "driverFront": "CLOSED",
+                                       "driverRear": "CLOSED",
+                                       "passengerFront": "CLOSED",
+                                       "passengerRear": "CLOSED"
+                               },
+                               "windows": {
+                                       "driverFront": "CLOSED",
+                                       "passengerFront": "CLOSED"
+                               },
+                               "trunk": "CLOSED",
+                               "hood": "CLOSED",
+                               "moonroof": "CLOSED"
+                       },
+                       "isServiceRequired": false,
+                       "fuelLevel": {
+                               "value": 7,
+                               "units": "LITERS"
+                       },
+                       "chargingState": {
+                               "chargePercentage": 47,
+                               "state": "NOT_CHARGING",
+                               "type": "NOT_AVAILABLE",
+                               "isChargerConnected": false
+                       },
+                       "combustionRange": {
+                               "chargePercentage": 0,
+                               "distance": {
+                                       "value": 96,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "combinedRange": {
+                               "chargePercentage": 0,
+                               "distance": {
+                                       "value": 96,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "electricRange": {
+                               "chargePercentage": 0,
+                               "distance": {
+                                       "value": 78,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "electricRangeAndStatus": {
+                               "chargePercentage": 47,
+                               "distance": {
+                                       "value": 78,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "checkControlMessages": [],
+                       "serviceRequired": [
+                               {
+                                       "type": "BRAKE_FLUID",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               },
+                               {
+                                       "type": "VEHICLE_CHECK",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               },
+                               {
+                                       "type": "OIL",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               },
+                               {
+                                       "type": "VEHICLE_TUV",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               }
+                       ],
+                       "vehicleLocation": {
+                               "coordinates": {
+                                       "latitude": 1.234,
+                                       "longitude": 9.876
+                               },
+                               "address": {
+                                       "formatted": "anonymous"
+                               },
+                               "heading": 39
+                       }
+               },
+               "isMappingPending": false,
+               "isMappingUnconfirmed": false,
+               "status": {
+                       "lastUpdatedAt": "2022-01-03T18:54:57Z",
+                       "currentMileage": {
+                               "mileage": 32179,
+                               "units": "km",
+                               "formattedMileage": "32.179"
+                       },
+                       "issues": null,
+                       "doorsGeneralState": "Verriegelt",
+                       "checkControlMessagesGeneralState": "Keine Probleme",
+                       "doorsAndWindows": [
+                               {
+                                       "iconId": 59757,
+                                       "title": "Verriegelungsstatus",
+                                       "state": "Verriegelt",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59722,
+                                       "title": "Alle Türen",
+                                       "state": "Geschlossen",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59725,
+                                       "title": "Alle Fenster",
+                                       "state": "Geschlossen",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59706,
+                                       "title": "Frontklappe",
+                                       "state": "Geschlossen",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59704,
+                                       "title": "Gepäckraum",
+                                       "state": "Geschlossen",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59705,
+                                       "title": "Glasdach",
+                                       "state": "Geschlossen",
+                                       "criticalness": "nonCritical"
+                               }
+                       ],
+                       "checkControlMessages": [],
+                       "requiredServices": [
+                               {
+                                       "id": "BrakeFluid",
+                                       "title": "Bremsflüssigkeit",
+                                       "iconId": 60223,
+                                       "longDescription": "Nächster Wechsel spätestens zum angegebenen Termin.",
+                                       "subtitle": "Fällig im November 2023",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "id": "VehicleCheck",
+                                       "title": "Fahrzeug-Check",
+                                       "iconId": 60215,
+                                       "longDescription": "Nächste Sichtprüfung nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+                                       "subtitle": "Fällig im November 2023",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "id": "Oil",
+                                       "title": "Motoröl",
+                                       "iconId": 60197,
+                                       "longDescription": "Nächster Wechsel nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+                                       "subtitle": "Fällig im November 2023",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "id": "VehicleAdmissionTest",
+                                       "title": "Fahrzeuginspektion (HU)",
+                                       "iconId": 60111,
+                                       "longDescription": "Nächste gesetzliche Fahrzeuguntersuchung zum angegebenen Termin.",
+                                       "subtitle": "Fällig im November 2023",
+                                       "criticalness": "nonCritical"
+                               }
+                       ],
+                       "fuelIndicators": [
+                               {
+                                       "mainBarValue": 47,
+                                       "rangeUnits": "km",
+                                       "rangeValue": "78",
+                                       "levelUnits": "%",
+                                       "levelValue": "47",
+                                       "secondaryBarValue": 0,
+                                       "infoIconId": 59694,
+                                       "rangeIconId": 59683,
+                                       "levelIconId": 59694,
+                                       "showsBar": true,
+                                       "showBarGoal": false,
+                                       "infoLabel": "Ladezustand",
+                                       "isInaccurate": false,
+                                       "isCircleIcon": false,
+                                       "iconOpacity": "high",
+                                       "chargingStatusType": "DEFAULT",
+                                       "chargingStatusIndicatorType": "DEFAULT"
+                               },
+                               {
+                                       "mainBarValue": 0,
+                                       "rangeUnits": "km",
+                                       "rangeValue": "174",
+                                       "secondaryBarValue": 0,
+                                       "infoIconId": 59691,
+                                       "rangeIconId": 59691,
+                                       "levelIconId": 0,
+                                       "showsBar": false,
+                                       "showBarGoal": false,
+                                       "infoLabel": "Kombinierte Reichweite",
+                                       "isInaccurate": false,
+                                       "isCircleIcon": false,
+                                       "iconOpacity": "high"
+                               },
+                               {
+                                       "mainBarValue": 0,
+                                       "rangeUnits": "km",
+                                       "rangeValue": "96",
+                                       "secondaryBarValue": 0,
+                                       "infoIconId": 59681,
+                                       "rangeIconId": 0,
+                                       "levelIconId": 0,
+                                       "showsBar": false,
+                                       "showBarGoal": false,
+                                       "infoLabel": "Erweiterte Reichweite",
+                                       "isInaccurate": false,
+                                       "isCircleIcon": false,
+                                       "iconOpacity": "high"
+                               }
+                       ],
+                       "timestampMessage": "Aktualisiert vom Fahrzeug 3.1.2022 07:54 PM",
+                       "chargingProfile": {
+                               "reductionOfChargeCurrent": {
+                                       "start": {
+                                               "hour": 11,
+                                               "minute": 0
+                                       },
+                                       "end": {
+                                               "hour": 14,
+                                               "minute": 30
+                                       }
+                               },
+                               "chargingMode": "immediateCharging",
+                               "chargingPreference": "chargingWindow",
+                               "chargingControlType": "weeklyPlanner",
+                               "departureTimes": [
+                                       {
+                                               "id": 1,
+                                               "action": "deactivate",
+                                               "timeStamp": {
+                                                       "hour": 16,
+                                                       "minute": 0
+                                               },
+                                               "timerWeekDays": [
+                                                       "monday",
+                                                       "tuesday",
+                                                       "wednesday",
+                                                       "thursday",
+                                                       "friday",
+                                                       "saturday",
+                                                       "sunday"
+                                               ]
+                                       },
+                                       {
+                                               "id": 2,
+                                               "action": "activate",
+                                               "timeStamp": {
+                                                       "hour": 12,
+                                                       "minute": 2
+                                               },
+                                               "timerWeekDays": [
+                                                       "sunday"
+                                               ]
+                                       },
+                                       {
+                                               "id": 3,
+                                               "action": "deactivate",
+                                               "timeStamp": {
+                                                       "hour": 13,
+                                                       "minute": 3
+                                               },
+                                               "timerWeekDays": [
+                                                       "saturday"
+                                               ]
+                                       },
+                                       {
+                                               "id": 4,
+                                               "action": "deactivate",
+                                               "timeStamp": {
+                                                       "hour": 12,
+                                                       "minute": 2
+                                               },
+                                               "timerWeekDays": [
+                                                       "sunday"
+                                               ]
+                                       }
+                               ],
+                               "climatisationOn": false,
+                               "chargingSettings": {
+                                       "targetSoc": 100,
+                                       "isAcCurrentLimitActive": false,
+                                       "hospitality": "NO_ACTION",
+                                       "idcc": "NO_ACTION"
+                               }
+                       }
+               },
+               "valid": false
+       }
+,
+  {
+    "a4aType": "NOT_SUPPORTED",
+    "bodyType": "F11",
+    "brand": "BMW",
+    "capabilities": {
+      "canRemoteHistoryBeDeleted": false,
+      "climateNow": {
+        "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.",
+        "executionPopup": {
+          "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.",
+          "iconId": 59733,
+          "popupType": "DIALOG",
+          "primaryButtonText": "Start",
+          "secondaryButtonText": "Cancel",
+          "title": "Start Ventilation"
+        },
+        "executionStopPopup": {
+          "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.",
+          "title": "Climate control is running"
+        },
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "climateTimer": {
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false,
+        "isToggleEnabled": true,
+        "page": {
+          "description": "By setting a start time you let the vehicle know when you plan to use it.",
+          "primaryButtonText": "SEND TO VEHICLE",
+          "secondaryButtonText": "DEACTIVATE AND SEND TO VEHICLE",
+          "subtitle": "Set start time",
+          "title": "Ventilation timer"
+        },
+        "tile": {
+          "description": "Plan start time",
+          "iconId": 59774,
+          "title": "Ventilation timer"
+        }
+      },
+      "isBmwChargingSupported": false,
+      "isCarSharingSupported": false,
+      "isChargeNowForBusinessSupported": false,
+      "isChargingHistorySupported": false,
+      "isChargingHospitalityEnabled": false,
+      "isChargingLoudnessEnable": false,
+      "isChargingPlanSupported": false,
+      "isChargingPowerLimitEnable": false,
+      "isChargingSettingsEnabled": false,
+      "isChargingTargetSocEnable": false,
+      "isCustomerEsimSupported": false,
+      "isDCSContractManagementSupported": false,
+      "isDataPrivacyEnabled": false,
+      "isEasyChargeSupported": false,
+      "isEvGoChargingSupported": false,
+      "isFindChargingEnabled": false,
+      "isMiniChargingSupported": false,
+      "isRemoteHistorySupported": true,
+      "isRemoteServicesActivationRequired": false,
+      "isRemoteServicesBookingRequired": false,
+      "isScanAndChargeSupported": false,
+      "lastStateCall": {
+        "isNonLscFeatureEnabled": false,
+        "lscState": "NOT_CAPABLE"
+      },
+      "lights": {
+        "executionMessage": "Flash headlights now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "lock": {
+        "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "sendPoi": {
+        "executionMessage": "Send POI now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": false
+      },
+      "unlock": {
+        "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": true,
+        "isPinAuthenticationRequired": true
+      },
+      "vehicleFinder": {
+        "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.",
+        "isEnabled": false,
+        "isPinAuthenticationRequired": false
+      }
+    },
+    "connectedDriveServices": [],
+    "driveTrain": "COMBUSTION",
+    "driverGuideInfo": {
+      "androidAppScheme": "com.bmwgroup.driversguide.row",
+      "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row",
+      "iosAppScheme": "bmwdriversguide:///open",
+      "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8",
+      "title": "BMW\nDriver's Guide"
+    },
+    "exFactoryILevel": "F010-12-11-503",
+    "exFactoryPUStep": "1112",
+    "headUnit": "ID5",
+    "hmiVersion": "ID4",
+    "iStep": "F010-12-11-503",
+    "isLscSupported": false,
+    "isMappingPending": false,
+    "isMappingUnconfirmed": false,
+    "model": "530d",
+    "properties": {
+      "checkControlMessages": [],
+      "climateControl": {},
+      "doorsAndWindows": {
+        "doors": {},
+        "windows": {}
+      },
+      "fuelLevel": {
+        "units": "LITERS",
+        "value": 24
+      },
+      "inMotion": false,
+      "isServiceRequired": false,
+      "lastUpdatedAt": "2021-03-10T08:02:08Z",
+      "originCountryISO": "GB",
+      "serviceRequired": [
+        {
+          "dateTime": "2022-10-01T00:00:00.000Z",
+          "status": "OK",
+          "type": "BRAKE_FLUID"
+        },
+        {
+          "dateTime": "2022-10-01T00:00:00.000Z",
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 25000
+          },
+          "status": "OK",
+          "type": "OIL"
+        },
+        {
+          "dateTime": "2024-10-01T00:00:00.000Z",
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 60000
+          },
+          "status": "OK",
+          "type": "VEHICLE_CHECK"
+        }
+      ]
+    },
+    "puStep": "1112",
+    "status": {
+      "checkControlMessages": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60197,
+          "state": "OK",
+          "title": "Engine Oil"
+        },
+        {
+          "criticalness": "semiCritical",
+          "iconId": 60217,
+          "id": "229",
+          "longDescription": "Charge by driving for longer periods or use external charger. Functions requiring battery will be switched off.",
+          "state": "Medium",
+          "title": "Battery discharged: Start engine"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60217,
+          "id": "50",
+          "longDescription": "System unable to monitor tire pressure. Check tire pressures manually. Continued driving possible. Consult service center.",
+          "state": "Low",
+          "title": "Flat Tire Monitor (FTM) inactive"
+        }
+      ],
+      "checkControlMessagesGeneralState": "Multiple Issues",
+      "doorsAndWindows": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59726,
+          "state": "Unknown",
+          "title": "All doors"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59701,
+          "state": "Unknown",
+          "title": "Left front window"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59700,
+          "state": "Unknown",
+          "title": "Right front window"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59703,
+          "state": "Unknown",
+          "title": "Left rear window"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59702,
+          "state": "Unknown",
+          "title": "Right rear window"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 59721,
+          "state": "Unknown",
+          "title": "Back window"
+        }
+      ],
+      "doorsGeneralState": "Unknown",
+      "fuelIndicators": [
+        {
+          "chargingType": null,
+          "iconOpacity": "high",
+          "infoIconId": 59930,
+          "infoLabel": "Fuel Level",
+          "isCircleIcon": false,
+          "isInaccurate": true,
+          "levelIconId": 59682,
+          "levelUnits": "l",
+          "levelValue": "24",
+          "mainBarValue": 0,
+          "rangeIconId": 59681,
+          "rangeUnits": "mi",
+          "rangeValue": "- -",
+          "secondaryBarValue": 0,
+          "showsBar": false
+        }
+      ],
+      "lastUpdatedAt": "2021-03-10T08:02:08Z",
+      "recallExternalUrl": null,
+      "recallMessages": [],
+      "requiredServices": [
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60223,
+          "id": "BrakeFluid",
+          "longDescription": "Next service due by the specified date.",
+          "subtitle": "Due in October 2022",
+          "title": "Brake fluid"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60197,
+          "id": "Oil",
+          "longDescription": "Next service due after the specified distance or date.",
+          "subtitle": "Due in October 2022 or 15534 mi",
+          "title": "Engine oil"
+        },
+        {
+          "criticalness": "nonCritical",
+          "iconId": 60215,
+          "id": "VehicleCheck",
+          "longDescription": "Next vehicle check due after the specified distance or date.",
+          "subtitle": "Due in October 2024 or 37282 mi",
+          "title": "Vehicle check"
+        }
+      ],
+      "timestampMessage": "Updated from vehicle 3/11/2021 08:02 AM"
+    },
+    "telematicsUnit": "TCB1",
+    "themeSpecs": {
+      "vehicleStatusBackgroundColor": {
+        "blue": 158,
+        "green": 158,
+        "red": 158
+      }
+    },
+    "vin": "some_vin_F11",
+    "year": 2012
+  }
+]
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/OAuthConfig_ROW_response.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/OAuthConfig_ROW_response.json
new file mode 100644 (file)
index 0000000..5dfdd49
--- /dev/null
@@ -0,0 +1,31 @@
+{
+       "clientName": "mybmwapp",
+       "clientSecret": "c0e3393d-70a2-4f6f-9d3c-8530af64d552",
+       "clientId": "31c357a0-7a1d-4590-aa99-33b97244d048",
+       "gcdmBaseUrl": "https://customer.bmwgroup.com",
+       "returnUrl": "com.bmw.connected://oauth",
+       "brand": "bmw",
+       "language": "en",
+       "country": "US",
+       "authorizationEndpoint": "https://customer.bmwgroup.com/oneid/login",
+       "tokenEndpoint": "https://customer.bmwgroup.com/gcdm/oauth/token",
+       "scopes": [
+               "openid",
+               "profile",
+               "email",
+               "offline_access",
+               "smacc",
+               "vehicle_data",
+               "perseus",
+               "dlm",
+               "svds",
+               "cesim",
+               "vsapi",
+               "remote_services",
+               "fupo",
+               "authenticate_user"
+       ],
+       "promptValues": [
+               "login"
+       ]
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/OAuth_Login_Response.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/OAuth_Login_Response.json
new file mode 100644 (file)
index 0000000..20c3b83
--- /dev/null
@@ -0,0 +1,3 @@
+{
+       "redirect_to": "redirect_uri=com.bmw.connected://oauth?client_id=31c357a0-7a1d-4590-aa99-33b97244d048&response_type=code&scope=openid profile email offline_access smacc vehicle_data perseus dlm svds cesim vsapi remote_services fupo authenticate_user&state=bXJXSzRyQTFmSjJ4NENrTg==&authorization=QkS8IUWigs4-0rV1krNtkpMYEB8.*AAJTSQACMDIAAlNLABx0RVVwWDlITUxhaHRHYU04UGlYZ2xybUdZRGs9AAR0eXBlAANDVFMAAlMxAAIwMQ..*"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/auth_cn_login_error.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/auth_cn_login_error.json
new file mode 100644 (file)
index 0000000..5dbfa7a
--- /dev/null
@@ -0,0 +1,7 @@
+{
+    "data": null,
+    "code": 699121,
+    "error": true,
+    "msgType": "toast",
+    "description": "系统异常,请稍后再试!"
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/auth_cn_login_pwd.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/auth_cn_login_pwd.json
new file mode 100644 (file)
index 0000000..7dee5e1
--- /dev/null
@@ -0,0 +1,12 @@
+{
+    "data": {
+      "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJEVU1NWSQxJEEkMTYzNzcwNzkxNjc4MiIsIm5iZiI6MTYzNzcwNzkxNiwiZXhwIjoxNjM3NzExMjE2LCJpYXQiOjE2Mzc3MDc5MTZ9.hpi-P97W68g7avGwu9dcBRapIsaG4F8MwOdPHe6PuTA",
+      "token_type": "Bearer",
+      "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJEVU1NWSQxJFIkMTYzNzcwNTc5NTA3NSIsIm5iZiI6MTYzNzcwNTc5NSwiZXhwIjoxNjQ1NDgwODk1LCJpYXQiOjE2Mzc3MDU3OTV9.dGVpbpfrJOo895jiU6Rk16ESYz80klfJbIX9M4KD1hQ",
+      "usid": "DUMMY",
+      "cid": "DUMMY"
+    },
+    "code": 200,
+    "error": false,
+    "description": "ok"
+  }
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/auth_cn_publickey.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/auth_cn_publickey.json
new file mode 100644 (file)
index 0000000..7f72223
--- /dev/null
@@ -0,0 +1,9 @@
+{
+    "data": {
+        "value": "-----BEGIN PUBLIC KEY-----\r\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCFXhoqJ4rSDCfWydleXnIZmiGkTl3KfPiL2H8svgJYygY2lbffv6f3bIi+Y+x3HKlriVGO2eXRLFb5knTkrGHbwwYt4Z1Fi2di4qLJBKlQk8oXu2eyFGlR/SrGo8+C3zUdShohM8Ws4FcXOs0H9sW7HX/SyCUXhMUJAxrlhsjzTwIDAQAB\r\n-----END PUBLIC KEY-----",
+        "expires": "3600"
+    },
+    "code": 200,
+    "error": false,
+    "description": "ok"
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/auth_error_internal_error.txt b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/auth_error_internal_error.txt
new file mode 100644 (file)
index 0000000..1553933
--- /dev/null
@@ -0,0 +1 @@
+INTERNAL SERVER ERROR
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/auth_error_wrong_password.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/auth_error_wrong_password.json
new file mode 100644 (file)
index 0000000..a81d310
--- /dev/null
@@ -0,0 +1,4 @@
+{
+    "error": "invalid_client",
+    "error_description": "Client authentication failed (e.g., login failure, unknown client, no client authentication included or unsupported authentication method)"
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/auth_token.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/auth_token.json
new file mode 100644 (file)
index 0000000..5561446
--- /dev/null
@@ -0,0 +1,7 @@
+{
+  "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
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/authorization_response.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/authorization_response.json
new file mode 100644 (file)
index 0000000..bf2d55b
--- /dev/null
@@ -0,0 +1,3 @@
+{
+    "redirect_to": "redirect_uri=com.bmw.connected://oauth?client_id=31c357a0-7a1d-4590-aa99-33b97244d048&response_type=code&scope=openid profile email offline_access smacc vehicle_data perseus dlm svds cesim vsapi remote_services fupo authenticate_user&state=cEG9eLAIi6Nv-aaCAniziE_B6FPoobva3qr5gukilYw&authorization=some_authorization_code"
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/china-key.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/china-key.json
new file mode 100644 (file)
index 0000000..3efc193
--- /dev/null
@@ -0,0 +1,9 @@
+{
+       "data": {
+               "value": "-----BEGIN PUBLIC KEY-----\r\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCteEZFIGa2z5cj7sAmX40y8/ige01T2r+VUzkMshAYwotZFvrVWZLQ6W9+ltvINJoRfZEZkmdP2lsidhqj1H1+RWyC78ear7Fm6xd9Gp9LnKtVVBJRM/9cBRg0AGiTJ7IO/x6MpKkBxxHmProFqPI40hueunV85RlaPBrjZVNIpQIDAQAB\r\n-----END PUBLIC KEY-----",
+               "expires": "3600"
+       },
+       "code": 200,
+       "error": false,
+       "description": "ok"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/oauth_config.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/auth/oauth_config.json
new file mode 100644 (file)
index 0000000..23c7487
--- /dev/null
@@ -0,0 +1,31 @@
+{
+    "clientName": "mybmwapp",
+    "clientSecret": "clientSecret",
+    "clientId": "clientId",
+    "gcdmBaseUrl": "https://customer.bmwgroup.com",
+    "returnUrl": "com.bmw.connected://oauth",
+    "brand": "bmw",
+    "language": "en",
+    "country": "US",
+    "authorizationEndpoint": "https://customer.bmwgroup.com/oneid/login",
+    "tokenEndpoint": "https://customer.bmwgroup.com/gcdm/oauth/token",
+    "scopes": [
+        "openid",
+        "profile",
+        "email",
+        "offline_access",
+        "smacc",
+        "vehicle_data",
+        "perseus",
+        "dlm",
+        "svds",
+        "cesim",
+        "vsapi",
+        "remote_services",
+        "fupo",
+        "authenticate_user"
+    ],
+    "promptValues": [
+        "login"
+    ]
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/chargingprofile/two-weeks-timer.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/chargingprofile/two-weeks-timer.json
new file mode 100644 (file)
index 0000000..7b49d4f
--- /dev/null
@@ -0,0 +1,301 @@
+[
+    {
+      "a4aType": "USB_ONLY",
+      "bodyType": "F45",
+      "brand": "BMW",
+      "capabilities": {
+        "canRemoteHistoryBeDeleted": false,
+        "climateNow": {
+          "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.",
+          "executionPopup": {
+            "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.",
+            "iconId": 59733,
+            "popupType": "DIALOG",
+            "primaryButtonText": "Start",
+            "secondaryButtonText": "Cancel",
+            "title": "Start Climatization"
+          },
+          "executionStopPopup": {
+            "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.",
+            "title": "Climate control is running"
+          },
+          "isEnabled": true,
+          "isPinAuthenticationRequired": false
+        },
+        "isBmwChargingSupported": true,
+        "isCarSharingSupported": false,
+        "isChargeNowForBusinessSupported": false,
+        "isChargingHistorySupported": true,
+        "isChargingHospitalityEnabled": false,
+        "isChargingLoudnessEnable": false,
+        "isChargingPlanSupported": true,
+        "isChargingPowerLimitEnable": false,
+        "isChargingSettingsEnabled": false,
+        "isChargingTargetSocEnable": false,
+        "isCustomerEsimSupported": false,
+        "isDCSContractManagementSupported": true,
+        "isDataPrivacyEnabled": false,
+        "isEasyChargeSupported": false,
+        "isEvGoChargingSupported": false,
+        "isFindChargingEnabled": true,
+        "isMiniChargingSupported": false,
+        "isRemoteHistorySupported": true,
+        "isRemoteServicesActivationRequired": false,
+        "isRemoteServicesBookingRequired": false,
+        "isScanAndChargeSupported": false,
+        "lastStateCall": {
+          "isNonLscFeatureEnabled": false,
+          "lscState": "ACTIVATED"
+        },
+        "lights": {
+          "executionMessage": "Flash headlights now? Remote functions may take a few seconds.",
+          "isEnabled": true,
+          "isPinAuthenticationRequired": false
+        },
+        "lock": {
+          "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.",
+          "isEnabled": true,
+          "isPinAuthenticationRequired": false
+        },
+        "sendPoi": {
+          "executionMessage": "Send POI now? Remote functions may take a few seconds.",
+          "isEnabled": true,
+          "isPinAuthenticationRequired": false
+        },
+        "unlock": {
+          "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.",
+          "isEnabled": true,
+          "isPinAuthenticationRequired": true
+        },
+        "vehicleFinder": {
+          "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.",
+          "isEnabled": true,
+          "isPinAuthenticationRequired": false
+        }
+      },
+      "connectedDriveServices": [],
+      "driveTrain": "PLUGIN_HYBRID",
+      "driverGuideInfo": {
+        "androidAppScheme": "com.bmwgroup.driversguide.row",
+        "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row",
+        "iosAppScheme": "bmwdriversguide:///open",
+        "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8",
+        "title": "BMW\nDriver's Guide"
+      },
+      "exFactoryILevel": "F056-16-07-502",
+      "exFactoryPUStep": "0716",
+      "headUnit": "ID5",
+      "hmiVersion": "ID4",
+      "iStep": "F056-20-07-550",
+      "isLscSupported": true,
+      "isMappingPending": false,
+      "isMappingUnconfirmed": false,
+      "model": "225xe iPerformance",
+      "properties": {
+        "areDoorsClosed": true,
+        "areDoorsLocked": true,
+        "areDoorsOpen": false,
+        "areWindowsClosed": true,
+        "chargingState": {
+          "chargePercentage": 40,
+          "isChargerConnected": false,
+          "state": "NOT_CHARGING",
+          "type": "CONDUCTIVE"
+        },
+        "checkControlMessages": [],
+        "climateControl": {},
+        "combinedRange": {
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 245
+          }
+        },
+        "combustionRange": {
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 245
+          }
+        },
+        "doorsAndWindows": {
+          "doors": {
+            "driverFront": "CLOSED",
+            "driverRear": "CLOSED",
+            "passengerFront": "CLOSED",
+            "passengerRear": "CLOSED"
+          },
+          "hood": "CLOSED",
+          "trunk": "CLOSED",
+          "windows": {
+            "driverFront": "CLOSED",
+            "driverRear": "CLOSED",
+            "passengerFront": "CLOSED",
+            "passengerRear": "CLOSED"
+          }
+        },
+        "electricRange": {
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 4
+          }
+        },
+        "electricRangeAndStatus": {
+          "chargePercentage": 40,
+          "distance": {
+            "units": "KILOMETERS",
+            "value": 4
+          }
+        },
+        "fuelLevel": {
+          "units": "LITERS",
+          "value": 20
+        },
+        "inMotion": false,
+        "isServiceRequired": false,
+        "lastUpdatedAt": "2021-11-10T18:25:38Z",
+        "originCountryISO": "GB",
+        "serviceRequired": [],
+        "vehicleLocation": {
+          "address": {
+            "formatted": "some_formatted_address"
+          },
+          "coordinates": {
+            "latitude": 12.3456,
+            "longitude": 34.5678
+          },
+          "heading": 123
+        }
+      },
+      "puStep": "0720",
+      "status": {
+        "chargingProfile": {
+          "chargingControlType": "twoWeeksTimer",
+          "chargingMode": "immediateCharging",
+          "chargingPreference": "chargingWindow",
+          "chargingSettings": {
+            "hospitality": "NO_ACTION",
+            "idcc": "NO_ACTION",
+            "isAcCurrentLimitActive": false,
+            "targetSoc": 100
+          },
+          "climatisationOn": false,
+          "departureTimes": [
+            {
+              "action": "deactivate",
+              "id": 1,
+              "timerWeekDays": []
+            },
+            {
+              "action": "deactivate",
+              "id": 2,
+              "timerWeekDays": []
+            }
+          ],
+          "reductionOfChargeCurrent": {
+            "end": {
+              "hour": 16,
+              "minute": 0
+            },
+            "start": {
+              "hour": 13,
+              "minute": 0
+            }
+          }
+        },
+        "checkControlMessages": [
+          {
+            "criticalness": "nonCritical",
+            "iconId": 60197,
+            "state": "OK",
+            "title": "Engine Oil"
+          }
+        ],
+        "checkControlMessagesGeneralState": "No Issues",
+        "currentMileage": {
+          "formattedMileage": "66720",
+          "mileage": 66720,
+          "units": "mi"
+        },
+        "doorsAndWindows": [
+          {
+            "criticalness": "nonCritical",
+            "iconId": 59722,
+            "state": "Closed",
+            "title": "All doors and windows"
+          }
+        ],
+        "doorsGeneralState": "Locked",
+        "fuelIndicators": [
+          {
+            "chargingType": null,
+            "iconOpacity": "high",
+            "infoIconId": 59691,
+            "infoLabel": "Combined Range",
+            "isCircleIcon": false,
+            "isInaccurate": false,
+            "levelIconId": null,
+            "levelUnits": null,
+            "levelValue": null,
+            "mainBarValue": 0,
+            "rangeIconId": 59691,
+            "rangeUnits": "mi",
+            "rangeValue": "152",
+            "secondaryBarValue": 0,
+            "showsBar": false
+          },
+          {
+            "barType": null,
+            "chargingStatusIndicatorType": "DEFAULT",
+            "chargingStatusType": "DEFAULT",
+            "chargingType": null,
+            "iconOpacity": "high",
+            "infoIconId": 59694,
+            "infoLabel": "State of Charge",
+            "isCircleIcon": false,
+            "isInaccurate": false,
+            "levelIconId": 59694,
+            "levelUnits": "%",
+            "levelValue": "40",
+            "mainBarValue": 40,
+            "rangeIconId": 59683,
+            "rangeUnits": "mi",
+            "rangeValue": "2",
+            "secondaryBarValue": 0,
+            "showBarGoal": false,
+            "showsBar": true
+          },
+          {
+            "chargingType": null,
+            "iconOpacity": "high",
+            "infoIconId": 59930,
+            "infoLabel": "Fuel Level",
+            "isCircleIcon": false,
+            "isInaccurate": true,
+            "levelIconId": 59682,
+            "levelUnits": "l",
+            "levelValue": "20",
+            "mainBarValue": 0,
+            "rangeIconId": 59681,
+            "rangeUnits": "mi",
+            "rangeValue": "150",
+            "secondaryBarValue": 0,
+            "showsBar": false
+          }
+        ],
+        "issues": {},
+        "lastUpdatedAt": "2021-11-10T18:25:38Z",
+        "recallExternalUrl": null,
+        "recallMessages": [],
+        "timestampMessage": "Updated from vehicle 11/11/2021 06:25 PM"
+      },
+      "telematicsUnit": "TCB1",
+      "themeSpecs": {
+        "vehicleStatusBackgroundColor": {
+          "blue": 66,
+          "green": 66,
+          "red": 66
+        }
+      },
+      "vin": "anonymous",
+      "year": 2016
+    }
+  ]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/chargingprofile/weekly-planner-t2-active.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/chargingprofile/weekly-planner-t2-active.json
new file mode 100644 (file)
index 0000000..a822abe
--- /dev/null
@@ -0,0 +1,427 @@
+[
+       {
+               "vin": "anonymous",
+               "model": "i3 94 (+ REX)",
+               "year": 2017,
+               "brand": "BMW",
+               "headUnit": "ID5",
+               "isLscSupported": true,
+               "driveTrain": "ELECTRIC",
+               "puStep": "0321",
+               "iStep": "I001-21-03-530",
+               "telematicsUnit": "TCB1",
+               "hmiVersion": "ID4",
+               "bodyType": "I01",
+               "a4aType": "USB_ONLY",
+               "capabilities": {
+                       "isRemoteServicesBookingRequired": false,
+                       "isRemoteServicesActivationRequired": false,
+                       "lock": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds."
+                       },
+                       "unlock": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": true,
+                               "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds."
+                       },
+                       "lights": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Flash headlights now? Remote functions may take a few seconds."
+                       },
+                       "horn": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds."
+                       },
+                       "vehicleFinder": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Find your vehicle now? Remote functions may take a few seconds."
+                       },
+                       "sendPoi": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Send POI now? Remote functions may take a few seconds."
+                       },
+                       "lastStateCall": {
+                               "isNonLscFeatureEnabled": false,
+                               "lscState": "ACTIVATED"
+                       },
+                       "climateNow": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.",
+                               "executionPopup": {
+                                       "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.",
+                                       "popupType": "DIALOG",
+                                       "title": "Start Climatization",
+                                       "primaryButtonText": "Start",
+                                       "secondaryButtonText": "Cancel",
+                                       "iconId": 59733
+                               },
+                               "executionStopPopup": {
+                                       "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.",
+                                       "title": "Climate control is running"
+                               }
+                       },
+                       "isRemoteHistorySupported": true,
+                       "canRemoteHistoryBeDeleted": false,
+                       "isChargingHistorySupported": true,
+                       "isScanAndChargeSupported": true,
+                       "isDCSContractManagementSupported": true,
+                       "isBmwChargingSupported": true,
+                       "isMiniChargingSupported": false,
+                       "isChargeNowForBusinessSupported": true,
+                       "isDataPrivacyEnabled": false,
+                       "isChargingPlanSupported": true,
+                       "isChargingPowerLimitEnable": false,
+                       "isChargingTargetSocEnable": false,
+                       "isChargingLoudnessEnable": false,
+                       "isChargingSettingsEnabled": false,
+                       "isChargingHospitalityEnabled": false,
+                       "isEvGoChargingSupported": false,
+                       "isFindChargingEnabled": true,
+                       "isCustomerEsimSupported": false,
+                       "isCarSharingSupported": false,
+                       "isEasyChargeSupported": false
+               },
+               "connectedDriveServices": [],
+               "properties": {
+                       "lastUpdatedAt": "2021-12-21T16:46:02Z",
+                       "inMotion": false,
+                       "areDoorsLocked": true,
+                       "originCountryISO": "DE",
+                       "areDoorsClosed": true,
+                       "areDoorsOpen": false,
+                       "areWindowsClosed": true,
+                       "doorsAndWindows": {
+                               "doors": {
+                                       "driverFront": "CLOSED",
+                                       "driverRear": "CLOSED",
+                                       "passengerFront": "CLOSED",
+                                       "passengerRear": "CLOSED"
+                               },
+                               "windows": {
+                                       "driverFront": "CLOSED",
+                                       "passengerFront": "CLOSED"
+                               },
+                               "trunk": "CLOSED",
+                               "hood": "CLOSED",
+                               "moonroof": "CLOSED"
+                       },
+                       "isServiceRequired": false,
+                       "fuelLevel": {
+                               "value": 4,
+                               "units": "LITERS"
+                       },
+                       "chargingState": {
+                               "chargePercentage": 74,
+                               "state": "NOT_CHARGING",
+                               "type": "NOT_AVAILABLE",
+                               "isChargerConnected": false
+                       },
+                       "combustionRange": {
+                               "distance": {
+                                       "value": 31,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "combinedRange": {
+                               "distance": {
+                                       "value": 31,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "electricRange": {
+                               "distance": {
+                                       "value": 76,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "electricRangeAndStatus": {
+                               "chargePercentage": 74,
+                               "distance": {
+                                       "value": 76,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "checkControlMessages": [],
+                       "serviceRequired": [
+                               {
+                                       "type": "BRAKE_FLUID",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               },
+                               {
+                                       "type": "VEHICLE_CHECK",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               },
+                               {
+                                       "type": "OIL",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               },
+                               {
+                                       "type": "VEHICLE_TUV",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               }
+                       ],
+                       "vehicleLocation": {
+                               "coordinates": {
+                                       "latitude": 1.2345,
+                                       "longitude": 6.789
+                               },
+                               "address": {
+                                       "formatted": "anonymous"
+                               },
+                               "heading": 222
+                       },
+                       "climateControl": {
+                               
+                       }
+               },
+               "isMappingPending": false,
+               "isMappingUnconfirmed": false,
+               "driverGuideInfo": {
+                       "title": "BMW\nDriver's Guide",
+                       "androidAppScheme": "com.bmwgroup.driversguide.row",
+                       "iosAppScheme": "bmwdriversguide:///open",
+                       "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row",
+                       "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8"
+               },
+               "themeSpecs": {
+                       "vehicleStatusBackgroundColor": {
+                               "red": 156,
+                               "green": 154,
+                               "blue": 152
+                       }
+               },
+               "status": {
+                       "lastUpdatedAt": "2021-12-21T16:46:02Z",
+                       "currentMileage": {
+                               "mileage": 31537,
+                               "units": "km",
+                               "formattedMileage": "31537"
+                       },
+                       "issues": {
+                               
+                       },
+                       "doorsGeneralState": "Locked",
+                       "checkControlMessagesGeneralState": "No Issues",
+                       "doorsAndWindows": [
+                               {
+                                       "iconId": 59757,
+                                       "title": "Lock status",
+                                       "state": "Locked",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59722,
+                                       "title": "All doors",
+                                       "state": "Closed",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59725,
+                                       "title": "All windows",
+                                       "state": "Closed",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59706,
+                                       "title": "Hood",
+                                       "state": "Closed",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59704,
+                                       "title": "Trunk",
+                                       "state": "Closed",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59705,
+                                       "title": "Sunroof",
+                                       "state": "Closed",
+                                       "criticalness": "nonCritical"
+                               }
+                       ],
+                       "checkControlMessages": [],
+                       "requiredServices": [
+                               {
+                                       "id": "BrakeFluid",
+                                       "title": "Brake fluid",
+                                       "iconId": 60223,
+                                       "longDescription": "Next service due by the specified date.",
+                                       "subtitle": "Due in November 2023",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "id": "VehicleCheck",
+                                       "title": "Vehicle check",
+                                       "iconId": 60215,
+                                       "longDescription": "Next vehicle check due after the specified distance or date.",
+                                       "subtitle": "Due in November 2023",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "id": "Oil",
+                                       "title": "Engine oil",
+                                       "iconId": 60197,
+                                       "longDescription": "Next service due after the specified distance or date.",
+                                       "subtitle": "Due in November 2023",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "id": "VehicleAdmissionTest",
+                                       "title": "Vehicle Inspection",
+                                       "iconId": 60111,
+                                       "longDescription": "Next state inspection due by the specified date.",
+                                       "subtitle": "Due in November 2023",
+                                       "criticalness": "nonCritical"
+                               }
+                       ],
+                       "recallMessages": [],
+                       "recallExternalUrl": null,
+                       "fuelIndicators": [
+                               {
+                                       "mainBarValue": 74,
+                                       "secondaryBarValue": 0,
+                                       "infoIconId": 59694,
+                                       "rangeIconId": 59683,
+                                       "rangeUnits": "km",
+                                       "rangeValue": "76",
+                                       "levelIconId": 59694,
+                                       "showsBar": true,
+                                       "levelUnits": "%",
+                                       "levelValue": "74",
+                                       "showBarGoal": false,
+                                       "barType": null,
+                                       "infoLabel": "State of Charge",
+                                       "isInaccurate": false,
+                                       "isCircleIcon": false,
+                                       "iconOpacity": "high",
+                                       "chargingType": null,
+                                       "chargingStatusType": "DEFAULT",
+                                       "chargingStatusIndicatorType": "DEFAULT"
+                               },
+                               {
+                                       "mainBarValue": 0,
+                                       "secondaryBarValue": 0,
+                                       "infoIconId": 59691,
+                                       "infoLabel": "Combined Range",
+                                       "rangeIconId": 59691,
+                                       "rangeUnits": "km",
+                                       "levelIconId": null,
+                                       "showsBar": false,
+                                       "levelUnits": null,
+                                       "levelValue": null,
+                                       "isInaccurate": false,
+                                       "isCircleIcon": false,
+                                       "iconOpacity": "high",
+                                       "chargingType": null,
+                                       "rangeValue": "107"
+                               },
+                               {
+                                       "mainBarValue": 0,
+                                       "secondaryBarValue": 0,
+                                       "infoIconId": 59681,
+                                       "infoLabel": "Extended Range",
+                                       "rangeIconId": null,
+                                       "rangeUnits": "km",
+                                       "rangeValue": "31",
+                                       "levelIconId": null,
+                                       "showsBar": false,
+                                       "levelUnits": null,
+                                       "levelValue": null,
+                                       "isInaccurate": false,
+                                       "isCircleIcon": false,
+                                       "iconOpacity": "high",
+                                       "chargingType": null
+                               }
+                       ],
+                       "timestampMessage": "Updated from vehicle 12/21/2021 05:46 PM",
+                       "chargingProfile": {
+                               "reductionOfChargeCurrent": {
+                                       "start": {
+                                               "hour": 11,
+                                               "minute": 0
+                                       },
+                                       "end": {
+                                               "hour": 14,
+                                               "minute": 30
+                                       }
+                               },
+                               "chargingMode": "immediateCharging",
+                               "chargingPreference": "chargingWindow",
+                               "chargingControlType": "weeklyPlanner",
+                               "departureTimes": [
+                                       {
+                                               "id": 1,
+                                               "action": "deactivate",
+                                               "timerWeekDays": [
+                                                       "monday",
+                                                       "tuesday",
+                                                       "wednesday",
+                                                       "thursday",
+                                                       "friday",
+                                                       "saturday",
+                                                       "sunday"
+                                               ],
+                                               "timeStamp": {
+                                                       "hour": 16,
+                                                       "minute": 0
+                                               }
+                                       },
+                                       {
+                                               "id": 2,
+                                               "action": "activate",
+                                               "timerWeekDays": [
+                                                       "sunday"
+                                               ],
+                                               "timeStamp": {
+                                                       "hour": 12,
+                                                       "minute": 2
+                                               }
+                                       },
+                                       {
+                                               "id": 3,
+                                               "action": "deactivate",
+                                               "timerWeekDays": [
+                                                       "saturday"
+                                               ],
+                                               "timeStamp": {
+                                                       "hour": 13,
+                                                       "minute": 3
+                                               }
+                                       },
+                                       {
+                                               "id": 4,
+                                               "action": "deactivate",
+                                               "timerWeekDays": [
+                                                       "sunday"
+                                               ],
+                                               "timeStamp": {
+                                                       "hour": 12,
+                                                       "minute": 2
+                                               }
+                                       }
+                               ],
+                               "climatisationOn": false,
+                               "chargingSettings": {
+                                       "targetSoc": 100,
+                                       "isAcCurrentLimitActive": false,
+                                       "hospitality": "NO_ACTION",
+                                       "idcc": "NO_ACTION"
+                               }
+                       }
+               },
+               "exFactoryPUStep": "0717",
+               "exFactoryILevel": "I001-17-07-500"
+       }
+]
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/fingerprint-raw.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/fingerprint-raw.json
new file mode 100644 (file)
index 0000000..3a20b4e
--- /dev/null
@@ -0,0 +1,423 @@
+ [
+       {
+               "vin": "ABC45678",
+               "model": "i3 94 (+ REX)",
+               "year": 2017,
+               "brand": "BMW",
+               "headUnit": "ID5",
+               "isLscSupported": true,
+               "driveTrain": "ELECTRIC",
+               "puStep": "0321",
+               "iStep": "I001-21-03-530",
+               "telematicsUnit": "TCB1",
+               "hmiVersion": "ID4",
+               "bodyType": "I01",
+               "a4aType": "USB_ONLY",
+               "capabilities": {
+                       "isRemoteServicesBookingRequired": false,
+                       "isRemoteServicesActivationRequired": false,
+                       "lock": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Jetzt Ihr Fahrzeug verriegeln? Remote-Funktionen können einige Sekunden dauern."
+                       },
+                       "unlock": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": true,
+                               "executionMessage": "Jetzt Ihr Fahrzeug entriegeln? Remote-Funktionen können einige Sekunden dauern."
+                       },
+                       "lights": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Jetzt Scheinwerfer aufleuchten lassen? Remote-Funktionen können einige Sekunden dauern."
+                       },
+                       "horn": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Hupen ist in vielen Ländern nur in bestimmten Situationen erlaubt. Die Verantwortung für den Einsatz und die Einhaltung der jeweils geltenden Bestimmungen liegt allein bei Ihnen als Nutzer. \n\nJetzt hupen? Remote-Funktionen können einige Sekunden dauern."
+                       },
+                       "vehicleFinder": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Jetzt Ihr Fahrzeug finden? Remote-Funktionen können einige Sekunden dauern."
+                       },
+                       "sendPoi": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Jetzt POI senden? Remote-Funktionen können einige Sekunden dauern."
+                       },
+                       "lastStateCall": {
+                               "isNonLscFeatureEnabled": false,
+                               "lscState": "ACTIVATED"
+                       },
+                       "climateNow": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Jetzt belüften? Remote-Funktionen können einige Sekunden dauern.",
+                               "executionPopup": {
+                                       "executionMessage": "Jetzt klimatisieren? Remote-Funktionen können einige Sekunden dauern.",
+                                       "popupType": "DIALOG",
+                                       "title": "Klimatisierung starten",
+                                       "primaryButtonText": "Start",
+                                       "secondaryButtonText": "Abbrechen",
+                                       "iconId": 59733
+                               },
+                               "executionStopPopup": {
+                                       "executionMessage": "Jetzt Klimatisierung Ihres Fahrzeugs beenden? Remote-Funktionen können einige Sekunden dauern.",
+                                       "title": "Klimatisierung läuft"
+                               }
+                       },
+                       "isRemoteHistorySupported": true,
+                       "canRemoteHistoryBeDeleted": false,
+                       "isChargingHistorySupported": true,
+                       "isScanAndChargeSupported": true,
+                       "isDCSContractManagementSupported": true,
+                       "isBmwChargingSupported": true,
+                       "isMiniChargingSupported": false,
+                       "isChargeNowForBusinessSupported": true,
+                       "isDataPrivacyEnabled": false,
+                       "isChargingPlanSupported": true,
+                       "isChargingPowerLimitEnable": false,
+                       "isChargingTargetSocEnable": false,
+                       "isChargingLoudnessEnable": false,
+                       "isChargingSettingsEnabled": false,
+                       "isChargingHospitalityEnabled": false,
+                       "isEvGoChargingSupported": false,
+                       "isFindChargingEnabled": true,
+                       "isCustomerEsimSupported": false,
+                       "isCarSharingSupported": false,
+                       "isEasyChargeSupported": false
+               },
+               "connectedDriveServices": [],
+               "properties": {
+                       "lastUpdatedAt": "2022-01-03T18:54:57Z",
+                       "inMotion": false,
+                       "areDoorsLocked": true,
+                       "originCountryISO": "DE",
+                       "areDoorsClosed": true,
+                       "areDoorsOpen": false,
+                       "areWindowsClosed": true,
+                       "doorsAndWindows": {
+                               "doors": {
+                                       "driverFront": "CLOSED",
+                                       "driverRear": "CLOSED",
+                                       "passengerFront": "CLOSED",
+                                       "passengerRear": "CLOSED"
+                               },
+                               "windows": {
+                                       "driverFront": "CLOSED",
+                                       "passengerFront": "CLOSED"
+                               },
+                               "trunk": "CLOSED",
+                               "hood": "CLOSED",
+                               "moonroof": "CLOSED"
+                       },
+                       "isServiceRequired": false,
+                       "fuelLevel": {
+                               "value": 7,
+                               "units": "LITERS"
+                       },
+                       "chargingState": {
+                               "chargePercentage": 47,
+                               "state": "NOT_CHARGING",
+                               "type": "NOT_AVAILABLE",
+                               "isChargerConnected": false
+                       },
+                       "combustionRange": {
+                               "distance": {
+                                       "value": 96,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "combinedRange": {
+                               "distance": {
+                                       "value": 96,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "electricRange": {
+                               "distance": {
+                                       "value": 78,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "electricRangeAndStatus": {
+                               "chargePercentage": 47,
+                               "distance": {
+                                       "value": 78,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "checkControlMessages": [],
+                       "serviceRequired": [
+                               {
+                                       "type": "BRAKE_FLUID",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               },
+                               {
+                                       "type": "VEHICLE_CHECK",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               },
+                               {
+                                       "type": "OIL",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               },
+                               {
+                                       "type": "VEHICLE_TUV",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               }
+                       ],
+                       "vehicleLocation": {
+                               "coordinates": {
+                                       "latitude": 1.234,
+                                       "longitude": 5.678
+                               },
+                               "address": {
+                                       "formatted": "Street@Home"
+                               },
+                               "heading": 39
+                       },
+                       "climateControl": null
+               },
+               "isMappingPending": false,
+               "isMappingUnconfirmed": false,
+               "driverGuideInfo": {
+                       "title": "BMW\nDriver's Guide",
+                       "androidAppScheme": "com.bmwgroup.driversguide.row",
+                       "iosAppScheme": "bmwdriversguide:///open",
+                       "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row",
+                       "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8"
+               },
+               "themeSpecs": {
+                       "vehicleStatusBackgroundColor": {
+                               "red": 156,
+                               "green": 154,
+                               "blue": 152
+                       }
+               },
+               "status": {
+                       "lastUpdatedAt": "2022-01-03T18:54:57Z",
+                       "currentMileage": {
+                               "mileage": 32179,
+                               "units": "km",
+                               "formattedMileage": "32.179"
+                       },
+                       "issues": null,
+                       "doorsGeneralState": "Verriegelt",
+                       "checkControlMessagesGeneralState": "Keine Probleme",
+                       "doorsAndWindows": [
+                               {
+                                       "iconId": 59757,
+                                       "title": "Verriegelungsstatus",
+                                       "state": "Verriegelt",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59722,
+                                       "title": "Alle Türen",
+                                       "state": "Geschlossen",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59725,
+                                       "title": "Alle Fenster",
+                                       "state": "Geschlossen",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59706,
+                                       "title": "Frontklappe",
+                                       "state": "Geschlossen",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59704,
+                                       "title": "Gepäckraum",
+                                       "state": "Geschlossen",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59705,
+                                       "title": "Glasdach",
+                                       "state": "Geschlossen",
+                                       "criticalness": "nonCritical"
+                               }
+                       ],
+                       "checkControlMessages": [],
+                       "requiredServices": [
+                               {
+                                       "id": "BrakeFluid",
+                                       "title": "Bremsflüssigkeit",
+                                       "iconId": 60223,
+                                       "longDescription": "Nächster Wechsel spätestens zum angegebenen Termin.",
+                                       "subtitle": "Fällig im November 2023",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "id": "VehicleCheck",
+                                       "title": "Fahrzeug-Check",
+                                       "iconId": 60215,
+                                       "longDescription": "Nächste Sichtprüfung nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+                                       "subtitle": "Fällig im November 2023",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "id": "Oil",
+                                       "title": "Motoröl",
+                                       "iconId": 60197,
+                                       "longDescription": "Nächster Wechsel nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+                                       "subtitle": "Fällig im November 2023",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "id": "VehicleAdmissionTest",
+                                       "title": "Fahrzeuginspektion (HU)",
+                                       "iconId": 60111,
+                                       "longDescription": "Nächste gesetzliche Fahrzeuguntersuchung zum angegebenen Termin.",
+                                       "subtitle": "Fällig im November 2023",
+                                       "criticalness": "nonCritical"
+                               }
+                       ],
+                       "recallMessages": [],
+                       "recallExternalUrl": null,
+                       "fuelIndicators": [
+                               {
+                                       "mainBarValue": 47,
+                                       "secondaryBarValue": 0,
+                                       "infoIconId": 59694,
+                                       "rangeIconId": 59683,
+                                       "rangeUnits": "km",
+                                       "rangeValue": "78",
+                                       "levelIconId": 59694,
+                                       "showsBar": true,
+                                       "levelUnits": "%",
+                                       "levelValue": "47",
+                                       "showBarGoal": false,
+                                       "barType": null,
+                                       "infoLabel": "Ladezustand",
+                                       "isInaccurate": false,
+                                       "isCircleIcon": false,
+                                       "iconOpacity": "high",
+                                       "chargingType": null,
+                                       "chargingStatusType": "DEFAULT",
+                                       "chargingStatusIndicatorType": "DEFAULT"
+                               },
+                               {
+                                       "mainBarValue": 0,
+                                       "secondaryBarValue": 0,
+                                       "infoIconId": 59691,
+                                       "infoLabel": "Kombinierte Reichweite",
+                                       "rangeIconId": 59691,
+                                       "rangeUnits": "km",
+                                       "levelIconId": null,
+                                       "showsBar": false,
+                                       "levelUnits": null,
+                                       "levelValue": null,
+                                       "isInaccurate": false,
+                                       "isCircleIcon": false,
+                                       "iconOpacity": "high",
+                                       "chargingType": null,
+                                       "rangeValue": "174"
+                               },
+                               {
+                                       "mainBarValue": 0,
+                                       "secondaryBarValue": 0,
+                                       "infoIconId": 59681,
+                                       "infoLabel": "Erweiterte Reichweite",
+                                       "rangeIconId": null,
+                                       "rangeUnits": "km",
+                                       "rangeValue": "96",
+                                       "levelIconId": null,
+                                       "showsBar": false,
+                                       "levelUnits": null,
+                                       "levelValue": null,
+                                       "isInaccurate": false,
+                                       "isCircleIcon": false,
+                                       "iconOpacity": "high",
+                                       "chargingType": null
+                               }
+                       ],
+                       "timestampMessage": "Aktualisiert vom Fahrzeug 3.1.2022 07:54 PM",
+                       "chargingProfile": {
+                               "reductionOfChargeCurrent": {
+                                       "start": {
+                                               "hour": 11,
+                                               "minute": 0
+                                       },
+                                       "end": {
+                                               "hour": 14,
+                                               "minute": 30
+                                       }
+                               },
+                               "chargingMode": "immediateCharging",
+                               "chargingPreference": "chargingWindow",
+                               "chargingControlType": "weeklyPlanner",
+                               "departureTimes": [
+                                       {
+                                               "id": 1,
+                                               "action": "deactivate",
+                                               "timerWeekDays": [
+                                                       "monday",
+                                                       "tuesday",
+                                                       "wednesday",
+                                                       "thursday",
+                                                       "friday",
+                                                       "saturday",
+                                                       "sunday"
+                                               ],
+                                               "timeStamp": {
+                                                       "hour": 16,
+                                                       "minute": 0
+                                               }
+                                       },
+                                       {
+                                               "id": 2,
+                                               "action": "activate",
+                                               "timerWeekDays": [
+                                                       "sunday"
+                                               ],
+                                               "timeStamp": {
+                                                       "hour": 12,
+                                                       "minute": 2
+                                               }
+                                       },
+                                       {
+                                               "id": 3,
+                                               "action": "deactivate",
+                                               "timerWeekDays": [
+                                                       "saturday"
+                                               ],
+                                               "timeStamp": {
+                                                       "hour": 13,
+                                                       "minute": 3
+                                               }
+                                       },
+                                       {
+                                               "id": 4,
+                                               "action": "deactivate",
+                                               "timerWeekDays": [
+                                                       "sunday"
+                                               ],
+                                               "timeStamp": {
+                                                       "hour": 12,
+                                                       "minute": 2
+                                               }
+                                       }
+                               ],
+                               "climatisationOn": false,
+                               "chargingSettings": {
+                                       "targetSoc": 100,
+                                       "isAcCurrentLimitActive": false,
+                                       "hospitality": "NO_ACTION",
+                                       "idcc": "NO_ACTION"
+                               }
+                       }
+               },
+               "exFactoryPUStep": "0717",
+               "exFactoryILevel": "I001-17-07-500"
+       }
+]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/fingerprint.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/fingerprint.json
new file mode 100644 (file)
index 0000000..f839e35
--- /dev/null
@@ -0,0 +1,387 @@
+[
+       {
+               "vin": "anonymous",
+               "model": "i3 94 (+ REX)",
+               "year": 2017,
+               "brand": "BMW",
+               "headUnit": "ID5",
+               "isLscSupported": true,
+               "driveTrain": "ELECTRIC",
+               "puStep": "0321",
+               "iStep": "I001-21-03-530",
+               "telematicsUnit": "TCB1",
+               "hmiVersion": "ID4",
+               "bodyType": "I01",
+               "a4aType": "USB_ONLY",
+               "exFactoryPUStep": "0717",
+               "exFactoryILevel": "I001-17-07-500",
+               "capabilities": {
+                       "isRemoteServicesBookingRequired": false,
+                       "isRemoteServicesActivationRequired": false,
+                       "isRemoteHistorySupported": true,
+                       "canRemoteHistoryBeDeleted": false,
+                       "isChargingHistorySupported": true,
+                       "isScanAndChargeSupported": true,
+                       "isDCSContractManagementSupported": true,
+                       "isBmwChargingSupported": true,
+                       "isMiniChargingSupported": false,
+                       "isChargeNowForBusinessSupported": true,
+                       "isDataPrivacyEnabled": false,
+                       "isChargingPlanSupported": true,
+                       "isChargingPowerLimitEnable": false,
+                       "isChargingTargetSocEnable": false,
+                       "isChargingLoudnessEnable": false,
+                       "isChargingSettingsEnabled": false,
+                       "isChargingHospitalityEnabled": false,
+                       "isEvGoChargingSupported": false,
+                       "isFindChargingEnabled": true,
+                       "isCustomerEsimSupported": false,
+                       "isCarSharingSupported": false,
+                       "isEasyChargeSupported": false,
+                       "lock": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Jetzt Ihr Fahrzeug verriegeln? Remote-Funktionen können einige Sekunden dauern."
+                       },
+                       "unlock": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": true,
+                               "executionMessage": "Jetzt Ihr Fahrzeug entriegeln? Remote-Funktionen können einige Sekunden dauern."
+                       },
+                       "lights": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Jetzt Scheinwerfer aufleuchten lassen? Remote-Funktionen können einige Sekunden dauern."
+                       },
+                       "horn": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Hupen ist in vielen Ländern nur in bestimmten Situationen erlaubt. Die Verantwortung für den Einsatz und die Einhaltung der jeweils geltenden Bestimmungen liegt allein bei Ihnen als Nutzer. \n\nJetzt hupen? Remote-Funktionen können einige Sekunden dauern."
+                       },
+                       "vehicleFinder": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Jetzt Ihr Fahrzeug finden? Remote-Funktionen können einige Sekunden dauern."
+                       },
+                       "sendPoi": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Jetzt POI senden? Remote-Funktionen können einige Sekunden dauern."
+                       },
+                       "climateNow": {
+                               "isEnabled": true,
+                               "isPinAuthenticationRequired": false,
+                               "executionMessage": "Jetzt belüften? Remote-Funktionen können einige Sekunden dauern."
+                       }
+               },
+               "properties": {
+                       "lastUpdatedAt": "2022-01-03T18:54:57Z",
+                       "inMotion": false,
+                       "areDoorsLocked": true,
+                       "originCountryISO": "DE",
+                       "areDoorsClosed": true,
+                       "areDoorsOpen": false,
+                       "areWindowsClosed": true,
+                       "doorsAndWindows": {
+                               "doors": {
+                                       "driverFront": "CLOSED",
+                                       "driverRear": "CLOSED",
+                                       "passengerFront": "CLOSED",
+                                       "passengerRear": "CLOSED"
+                               },
+                               "windows": {
+                                       "driverFront": "CLOSED",
+                                       "passengerFront": "CLOSED"
+                               },
+                               "trunk": "CLOSED",
+                               "hood": "CLOSED",
+                               "moonroof": "CLOSED"
+                       },
+                       "isServiceRequired": false,
+                       "fuelLevel": {
+                               "value": 7,
+                               "units": "LITERS"
+                       },
+                       "chargingState": {
+                               "chargePercentage": 47,
+                               "state": "NOT_CHARGING",
+                               "type": "NOT_AVAILABLE",
+                               "isChargerConnected": false
+                       },
+                       "combustionRange": {
+                               "chargePercentage": 0,
+                               "distance": {
+                                       "value": 96,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "combinedRange": {
+                               "chargePercentage": 0,
+                               "distance": {
+                                       "value": 96,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "electricRange": {
+                               "chargePercentage": 0,
+                               "distance": {
+                                       "value": 78,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "electricRangeAndStatus": {
+                               "chargePercentage": 47,
+                               "distance": {
+                                       "value": 78,
+                                       "units": "KILOMETERS"
+                               }
+                       },
+                       "checkControlMessages": [],
+                       "serviceRequired": [
+                               {
+                                       "type": "BRAKE_FLUID",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               },
+                               {
+                                       "type": "VEHICLE_CHECK",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               },
+                               {
+                                       "type": "OIL",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               },
+                               {
+                                       "type": "VEHICLE_TUV",
+                                       "status": "OK",
+                                       "dateTime": "2023-11-01T00:00:00.000Z"
+                               }
+                       ],
+                       "vehicleLocation": {
+                               "coordinates": {
+                                       "latitude": 1.234,
+                                       "longitude": 9.876
+                               },
+                               "address": {
+                                       "formatted": "anonymous"
+                               },
+                               "heading": 39
+                       }
+               },
+               "isMappingPending": false,
+               "isMappingUnconfirmed": false,
+               "status": {
+                       "lastUpdatedAt": "2022-01-03T18:54:57Z",
+                       "currentMileage": {
+                               "mileage": 32179,
+                               "units": "km",
+                               "formattedMileage": "32.179"
+                       },
+                       "issues": null,
+                       "doorsGeneralState": "Verriegelt",
+                       "checkControlMessagesGeneralState": "Keine Probleme",
+                       "doorsAndWindows": [
+                               {
+                                       "iconId": 59757,
+                                       "title": "Verriegelungsstatus",
+                                       "state": "Verriegelt",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59722,
+                                       "title": "Alle Türen",
+                                       "state": "Geschlossen",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59725,
+                                       "title": "Alle Fenster",
+                                       "state": "Geschlossen",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59706,
+                                       "title": "Frontklappe",
+                                       "state": "Geschlossen",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59704,
+                                       "title": "Gepäckraum",
+                                       "state": "Geschlossen",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "iconId": 59705,
+                                       "title": "Glasdach",
+                                       "state": "Geschlossen",
+                                       "criticalness": "nonCritical"
+                               }
+                       ],
+                       "checkControlMessages": [],
+                       "requiredServices": [
+                               {
+                                       "id": "BrakeFluid",
+                                       "title": "Bremsflüssigkeit",
+                                       "iconId": 60223,
+                                       "longDescription": "Nächster Wechsel spätestens zum angegebenen Termin.",
+                                       "subtitle": "Fällig im November 2023",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "id": "VehicleCheck",
+                                       "title": "Fahrzeug-Check",
+                                       "iconId": 60215,
+                                       "longDescription": "Nächste Sichtprüfung nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+                                       "subtitle": "Fällig im November 2023",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "id": "Oil",
+                                       "title": "Motoröl",
+                                       "iconId": 60197,
+                                       "longDescription": "Nächster Wechsel nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+                                       "subtitle": "Fällig im November 2023",
+                                       "criticalness": "nonCritical"
+                               },
+                               {
+                                       "id": "VehicleAdmissionTest",
+                                       "title": "Fahrzeuginspektion (HU)",
+                                       "iconId": 60111,
+                                       "longDescription": "Nächste gesetzliche Fahrzeuguntersuchung zum angegebenen Termin.",
+                                       "subtitle": "Fällig im November 2023",
+                                       "criticalness": "nonCritical"
+                               }
+                       ],
+                       "fuelIndicators": [
+                               {
+                                       "mainBarValue": 47,
+                                       "rangeUnits": "km",
+                                       "rangeValue": "78",
+                                       "levelUnits": "%",
+                                       "levelValue": "47",
+                                       "secondaryBarValue": 0,
+                                       "infoIconId": 59694,
+                                       "rangeIconId": 59683,
+                                       "levelIconId": 59694,
+                                       "showsBar": true,
+                                       "showBarGoal": false,
+                                       "infoLabel": "Ladezustand",
+                                       "isInaccurate": false,
+                                       "isCircleIcon": false,
+                                       "iconOpacity": "high",
+                                       "chargingStatusType": "DEFAULT",
+                                       "chargingStatusIndicatorType": "DEFAULT"
+                               },
+                               {
+                                       "mainBarValue": 0,
+                                       "rangeUnits": "km",
+                                       "rangeValue": "174",
+                                       "secondaryBarValue": 0,
+                                       "infoIconId": 59691,
+                                       "rangeIconId": 59691,
+                                       "levelIconId": 0,
+                                       "showsBar": false,
+                                       "showBarGoal": false,
+                                       "infoLabel": "Kombinierte Reichweite",
+                                       "isInaccurate": false,
+                                       "isCircleIcon": false,
+                                       "iconOpacity": "high"
+                               },
+                               {
+                                       "mainBarValue": 0,
+                                       "rangeUnits": "km",
+                                       "rangeValue": "96",
+                                       "secondaryBarValue": 0,
+                                       "infoIconId": 59681,
+                                       "rangeIconId": 0,
+                                       "levelIconId": 0,
+                                       "showsBar": false,
+                                       "showBarGoal": false,
+                                       "infoLabel": "Erweiterte Reichweite",
+                                       "isInaccurate": false,
+                                       "isCircleIcon": false,
+                                       "iconOpacity": "high"
+                               }
+                       ],
+                       "timestampMessage": "Aktualisiert vom Fahrzeug 3.1.2022 07:54 PM",
+                       "chargingProfile": {
+                               "reductionOfChargeCurrent": {
+                                       "start": {
+                                               "hour": 11,
+                                               "minute": 0
+                                       },
+                                       "end": {
+                                               "hour": 14,
+                                               "minute": 30
+                                       }
+                               },
+                               "chargingMode": "immediateCharging",
+                               "chargingPreference": "chargingWindow",
+                               "chargingControlType": "weeklyPlanner",
+                               "departureTimes": [
+                                       {
+                                               "id": 1,
+                                               "action": "deactivate",
+                                               "timeStamp": {
+                                                       "hour": 16,
+                                                       "minute": 0
+                                               },
+                                               "timerWeekDays": [
+                                                       "monday",
+                                                       "tuesday",
+                                                       "wednesday",
+                                                       "thursday",
+                                                       "friday",
+                                                       "saturday",
+                                                       "sunday"
+                                               ]
+                                       },
+                                       {
+                                               "id": 2,
+                                               "action": "activate",
+                                               "timeStamp": {
+                                                       "hour": 12,
+                                                       "minute": 2
+                                               },
+                                               "timerWeekDays": [
+                                                       "sunday"
+                                               ]
+                                       },
+                                       {
+                                               "id": 3,
+                                               "action": "deactivate",
+                                               "timeStamp": {
+                                                       "hour": 13,
+                                                       "minute": 3
+                                               },
+                                               "timerWeekDays": [
+                                                       "saturday"
+                                               ]
+                                       },
+                                       {
+                                               "id": 4,
+                                               "action": "deactivate",
+                                               "timeStamp": {
+                                                       "hour": 12,
+                                                       "minute": 2
+                                               },
+                                               "timerWeekDays": [
+                                                       "sunday"
+                                               ]
+                                       }
+                               ],
+                               "climatisationOn": false,
+                               "chargingSettings": {
+                                       "targetSoc": 100,
+                                       "isAcCurrentLimitActive": false,
+                                       "hospitality": "NO_ACTION",
+                                       "idcc": "NO_ACTION"
+                               }
+                       }
+               },
+               "valid": false
+       }
+]
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/remote_services/eadrax_service_delivered.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/remote_services/eadrax_service_delivered.json
new file mode 100644 (file)
index 0000000..dc879cb
--- /dev/null
@@ -0,0 +1,3 @@
+{
+    "eventStatus": "DELIVERED"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/remote_services/eadrax_service_eventposition.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/remote_services/eadrax_service_eventposition.json
new file mode 100644 (file)
index 0000000..c737d60
--- /dev/null
@@ -0,0 +1,12 @@
+{
+    "positionData": {
+        "status": "OK",
+        "position": {
+            "latitude": 123.456,
+            "longitude": 34.5678,
+            "formattedAddress": "some_formatted_address",
+            "heading": 121
+        }
+    },
+    "errorDetails": null
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/remote_services/eadrax_service_executed.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/remote_services/eadrax_service_executed.json
new file mode 100644 (file)
index 0000000..c2bd029
--- /dev/null
@@ -0,0 +1,3 @@
+{
+    "eventStatus": "EXECUTED"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/remote_services/eadrax_service_initiated.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/remote_services/eadrax_service_initiated.json
new file mode 100644 (file)
index 0000000..6aa071e
--- /dev/null
@@ -0,0 +1,4 @@
+{
+    "creationTime": "2021-08-14T20:58:36.403Z",
+    "eventId": "009b794b-70e9-457f-a363-2e82382d300d"
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/remote_services/eadrax_service_pending.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/remote_services/eadrax_service_pending.json
new file mode 100644 (file)
index 0000000..e4df13c
--- /dev/null
@@ -0,0 +1,3 @@
+{
+    "eventStatus": "PENDING"
+}
diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/remote_services/service-error.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/remote_services/service-error.json
new file mode 100644 (file)
index 0000000..3e645c5
--- /dev/null
@@ -0,0 +1,11 @@
+{
+       "eventStatus": "ERROR",
+       "errorDetails": {
+               "title": "Etwas ist schiefgelaufen",
+               "description": "Die folgenden Einschränkungen verbieten die Ausführung von Remote Services: Aus Sicherheitsgründen sind Remote Services nicht verfügbar, wenn die Fahrbereitschaft eingeschaltet ist. Remote Services können nur mit einem ausreichenden Ladezustand durchgeführt werden. Die Remote Services „Verriegeln“ und „Entriegeln“ können nur ausgeführt werden, wenn die Fahrertür geschlossen und der Türstatus bekannt ist.",
+               "presentationType": "PAGE",
+               "iconId": 60217,
+               "isRetriable": true,
+               "errorDetails": "NACK"
+       }
+}
index 4e405b792f82a845763378869fe46379178c46fc..503073988da6c89da1471167572ae38b4e1b7fff 100644 (file)
     <module>org.openhab.binding.mqtt.generic</module>
     <module>org.openhab.binding.mqtt.homeassistant</module>
     <module>org.openhab.binding.mqtt.homie</module>
+    <module>org.openhab.binding.mybmw</module>
     <module>org.openhab.binding.mycroft</module>
     <module>org.openhab.binding.myq</module>
     <module>org.openhab.binding.mystrom</module>