]> git.basschouten.com Git - openhab-addons.git/commitdiff
[BMW ConnectedDrive] initial contribution (#8576)
authorBernd Weymann <bernd.weymann@gmail.com>
Sat, 1 May 2021 16:14:54 +0000 (18:14 +0200)
committerGitHub <noreply@github.com>
Sat, 1 May 2021 16:14:54 +0000 (18:14 +0200)
Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
Signed-off-by: Norbert Truchsess <norbert.truchsess@t-online.de>
176 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.bmwconnecteddrive/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/README.md [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/doc/AwayImage.png [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/doc/CarStatusImages.png [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/doc/ChargingImage.png [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/doc/CheckControlImage.png [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/doc/ServiceOptions.png [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/doc/UnlockedImage.png [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/doc/panel.png [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/doc/properties.png [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/doc/range-radius.png [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/VehicleConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/action/BMWConnectedDriveActions.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/discovery/VehicleDiscovery.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/Destination.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/DestinationContainer.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/NetworkError.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/auth/AuthResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/charge/ChargeProfile.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/charge/ChargingWindow.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/charge/Timer.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/charge/WeeklyPlanner.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/CBSMessageCompat.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/CCMMessageCompat.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/VehicleAttributes.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/VehicleAttributesContainer.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/VehicleMessages.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/discovery/Dealer.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/discovery/Vehicle.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/discovery/VehiclesContainer.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/remote/ExecutionStatus.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/remote/ExecutionStatusContainer.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/AllTrips.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/AllTripsContainer.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/CommunityChargeCycleEntry.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/CommunityEletricDistanceEntry.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/CommunityPowerEntry.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/LastTrip.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/LastTripContainer.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/CBSMessage.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/CCMMessage.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/Doors.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/Position.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/VehicleStatus.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/VehicleStatusContainer.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/Windows.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/BMWConnectedDriveOptionProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ByteResponseCallback.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveBridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/RemoteServiceHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ResponseCallback.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/StringResponseCallback.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/Token.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleChannelHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/simulation/Injector.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/BimmerConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/ChargeProfileUtils.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/ChargeProfileWrapper.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/Constants.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/Converter.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/HTTPConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/ImageProperties.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/RemoteServiceUtils.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/VehicleStatusUtils.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/config/bridge-config.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/config/thing-config.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/i18n/bmwconnecteddrive_de.properties [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/bridge-connected-drive.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/charge-channel-groups.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/charge-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/check-control-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/check-control-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/conv-range-channel-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/destination-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/destination-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/door-status-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/doors-status-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-last-trip-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-lifetime-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-range-channel-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-vehicle-status-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/hybrid-last-trip-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/hybrid-lifetime-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/hybrid-range-channel-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/image-channel-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/image-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/last-trip-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/lifetime-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/location-channel-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/location-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/range-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/remote-services-channel-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/remote-services-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/service-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/service-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/thing-bev.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/thing-bev_rex.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/thing-conv.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/thing-phev.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/vehicle-status-group.xml [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/discovery/DiscoveryTest.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/ChargeProfileTest.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/ConnectedDriveTest.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/DestinationTest.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/LastTripTest.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/LifetimeWrapper.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/RemoteStatusTest.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/StatusWrapper.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/TripWrapper.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/VehicleStatusTest.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AllTripTests.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthTest.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ChargeProfileTest.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConfigurationTest.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ErrorResponseTest.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/FingerprintTest.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/LastTripTests.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/SimulationTest.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleTests.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/util/FileReader.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/util/LocaleTest.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/api/vehicle/efficiency.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/api/vehicle/navigation.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/api/vehicle/test.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/api/vehicle/vehicle-ccm.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/api/vehicle/vehicle.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/api/vehicle/webapi-status.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/530E/all-trips.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/530E/charge-profile.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/530E/discovery.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/530E/last-trip.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/530E/status.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/F11/vehicle-status.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/F15/status.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/F31/status-318i.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/F31/status.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/F35/status.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/F45/status.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/F48/status.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/G31_NBTevo/auth_response.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/G31_NBTevo/flash_delivered.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/G31_NBTevo/flash_executed.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/G31_NBTevo/flash_initiated.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/G31_NBTevo/flash_pending.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/G31_NBTevo/status.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/G31_NBTevo/status_position_disabled.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/I01_NOREX/status.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/I01_REX/all-trips.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/I01_REX/charge-profile.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/I01_REX/discovery.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/I01_REX/last-trip.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/I01_REX/status.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/vehicles.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/all-trips.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/charging-profile.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/connected-drive-account-info.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/destinations.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/last-trip.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/remote-services/delivered.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/remote-services/executed.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/remote-services/pending.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/vehicle-status-ccm-tyre.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/vehicle-status-charging.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/vehicle-status.json [new file with mode: 0644]
bundles/pom.xml

index 9f5136c9d11c7b4322901908f845a4cfc4fee22c..3a9dcf65fe61dcd4038022c9b695d031a860615e 100644 (file)
@@ -37,6 +37,7 @@
 /bundles/org.openhab.binding.bluetooth.govee/ @cpmeister
 /bundles/org.openhab.binding.bluetooth.roaming/ @cpmeister
 /bundles/org.openhab.binding.bluetooth.ruuvitag/ @ssalonen
+/bundles/org.openhab.binding.bmwconnecteddrive/ @weymann @ntruchsess
 /bundles/org.openhab.binding.boschindego/ @jofleck
 /bundles/org.openhab.binding.boschshc/ @stefan-kaestle @coeing @GerdZanker
 /bundles/org.openhab.binding.bosesoundtouch/ @marvkis @tratho
index c6d486fce12e548b539ff979f07d8b3668cf7468..82397f5e9ebae07d9712e8741d87b1dfc3275de8 100644 (file)
       <artifactId>org.openhab.binding.bluetooth.ruuvitag</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.bmwconnecteddrive</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.boschindego</artifactId>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/NOTICE b/bundles/org.openhab.binding.bmwconnecteddrive/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.bmwconnecteddrive/README.md b/bundles/org.openhab.binding.bmwconnecteddrive/README.md
new file mode 100644 (file)
index 0000000..2ed6771
--- /dev/null
@@ -0,0 +1,964 @@
+# BMW ConnectedDrive Binding
+
+The binding provides a connection between [BMW's ConnectedDrive Portal](https://www.bmw-connecteddrive.com/country-region-select/country-region-selection.html) and openHAB.
+All vehicles connected to an account will be detected by the discovery with the correct type 
+
+* Conventional Fuel Vehicle
+* Plugin-Hybrid Electrical Vehicle 
+* Battery Electric Vehicle with Range Extender
+* Battery Electric Vehicle 
+
+In addition properties are attached with information and services provided by this vehicle.
+The provided data depends on 
+
+1. the [Thing Type](#things) and 
+2. the [Properties](#properties) mentioned in Services
+
+Different channel groups are clustering all informations.
+Check for each group if it's supported for this Vehicle.
+
+Please note **this isn't a real-time binding**. 
+If a door is opened the state isn't transmitted and changed immediately. 
+This isn't a flaw in the binding itself because the state in BMW's own ConnectedDrive App is also updated with some delay. 
+
+## Supported Things
+
+### Bridge
+
+The bridge establishes the connection between BMW's ConnectedDrive Portal and openHAB.
+
+| Name                       | Bridge Type ID | Description                                                |
+|----------------------------|----------------|------------------------------------------------------------|
+| BMW ConnectedDrive Account | `account`      | Access to BMW ConnectedDrive Portal for a specific user    |
+
+
+### Things
+
+Four different vehicle types are provided. 
+They differ in the supported channel groups & channels. 
+Conventional Fuel Vehicles have no _Charging Profile_, Electric Vehicles don't provide a _Fuel Range_. 
+For hybrid vehicles in addition to _Fuel and Electric Range_ the _Hybrid Range_ is shown.
+| Name                                | Thing Type ID | Supported Channel Groups                               |
+|-------------------------------------|---------------|--------------------------------------------------------|
+| BMW Electric Vehicle                | `bev`         | status, range, location, service, check, charge, image |
+| BMW Electric Vehicle with REX       | `bev_rex`     | status, range, location, service, check, charge, image |
+| BMW Plug-In-Hybrid Electric Vehicle | `phev`        | status, range, location, service, check, charge, image |
+| BMW Conventional Vehicle            | `conv`        | status, range, location, service, check, image         |
+
+#### Properties
+
+<img align="right" src="./doc/properties.png" width="500" height="225"/>
+
+For each vehicle properties are available. 
+Basically 3 types of information are registered as properties
+
+* Informations regarding your dealer with address and phone number
+* Which services are available / not available
+* Vehicle properties like color, model type, drive train and construction year
+
+In the right picture can see in *Services Activated* e.g. the *DoorLock* and *DoorUnlock* services are mentioned. 
+This ensures channel group [Remote Services](#remote-services) is supporting door lock and unlock remote control.
+
+In *Services Supported* the entry *LastDestination* is mentioned.
+So it's valid to connect channel group [Last Destinations](#destinations) in order to display and select the last navigation destinations.
+
+| Property Key       | Property Value      |  Supported Channel Groups    |
+|--------------------|---------------------|------------------------------|
+| servicesSupported  | Statistics          | last-trip, lifetime          |
+| servicesSupported  | LastDestinations    | destinations                 |
+| servicesActivated  | _list of services_  | remote                       |
+
+
+## Discovery
+
+Auto discovery is starting after the bridge towards BMW's ConnectedDrive is created. 
+A list of your registered vehicles is queried and all found things are added in the inbox.
+Unique identifier is the *Vehicle Identification Number* (VIN). 
+If a thing is already declared in a  _.things_ configuration, discovery won't highlight it again.
+Properties will be attached to predefined vehicles if the VIN is matching.
+
+## Configuration
+
+### Bridge Configuration
+
+| Parameter       | Type    | Description                                                        |           
+|-----------------|---------|--------------------------------------------------------------------|
+| userName        | text    | BMW ConnectedDrive Username                                       |
+| password        | text    | BMW ConnectedDrive Password                                       |
+| region          | text    | Select region in order to connect to the appropriate BMW server.   |
+
+The region Configuration has 3 different options
+
+* _NORTH_AMERICA_
+* _CHINA_
+* _ROW_  (Rest of World)
+
+### Thing Configuration
+
+Same configuration is needed for all things
+
+| Parameter       | Type    | Description                           |           
+|-----------------|---------|---------------------------------------|
+| vin             | text    | Vehicle Identification Number (VIN)   |
+| refreshInterval | integer | Refresh Interval in Minutes           |
+| units           | text    | Unit Selection. See below.            |
+| imageSize       | integer | Image Size                            |
+| imageViewport   | text    | Image Viewport                        |
+
+The unit configuration has 3 options
+
+* _AUTODETECT_ selects miles for US & UK, kilometer otherwise
+* _METRIC_ selects directly kilometers
+* _IMPERIAL_ selects directly miles
+
+The _imageVieport_ allows to show the vehicle from different angels.
+Possible options are 
+
+* _FRONT_
+* _REAR_
+* _SIDE_
+* _DASHBOARD_
+* _DRIVERDOOR_
+
+## Channels
+
+There are many channels available for each vehicle. 
+For better overview they are clustered in different channel groups.
+They differ for each vehicle type, build-in sensors and activated services.
+
+
+### Thing Channel Groups 
+
+#### Vehicle Status
+
+Reflects overall status of the vehicle.
+
+* Channel Group ID is **status**
+* Available for all vehicles
+* Read-only values
+
+| Channel Label             | Channel ID          | Type          | Description                                    |
+|---------------------------|---------------------|---------------|------------------------------------------------|
+| Overall Door Status       | doors               | String        | Combined status for all doors                  |
+| Overall Window Status     | windows             | String        | Combined status for all windows                |
+| Doors Locked              | lock                | String        | Status if doors are locked or unlocked         |
+| Next Service Date         | service-date        | DateTime      | Date of upcoming service                       |
+| Mileage till Next Service | service-mileage     | Number:Length | Mileage till upcoming service                  |
+| Check Control             | check-control       | String        | Presence of active warning messages            |
+| Charging Status           | charge              | String        | Only available for phev, bev_rex and bev       |
+| Last Status Timestamp     | last-update         | DateTime      | Date and time of last status update            |
+
+Overall Door Status values
+
+* _Closed_ - all doors closed
+* _Open_ - at least one door is open
+* _Undef_ - no door data delivered at all
+
+Overall Windows Status values
+
+* _Closed_ - all windows closed
+* _Open_ - at least one window is completely open
+* _Intermediate_ - at least one window is partially open
+* _Undef_ - no window data delivered at all
+
+Check Control values
+
+* _Active_ - at least one warning message is active
+* _Not Active_ - no warning message is active
+* _Undef_ - no data for warnings delivered
+
+Charging Status values
+
+* _Charging_
+* _Error_
+* _Finished Fully Charged_
+* _Finished Not Full_
+* _Invalid_
+* _Not Charging_
+* _Charging Goal reached_
+* _Waiting For Charging_
+
+#### Services
+
+Group for all upcoming services with description, service date and/or service mileage.
+If more than one service is scheduled in the future the channel _name_ contains all future services as options.
+
+* Channel Group ID is **service**
+* Available for all vehicles
+* Read/Write access
+
+| Channel Label                  | Channel ID          | Type           | Access     |
+|--------------------------------|---------------------|----------------|------------|
+| Service Name                   | name                | String         | Read/Write |
+| Service Details                | details             | String         | Read       |
+| Service Date                   | date                | Number         | Read       |
+| Mileage till Service           | mileage             | Number:Length  | Read       |
+
+#### Check Control
+
+Group for all current active CheckControl messages.
+If more than one message is active the channel _name_ contains all active messages as options.
+
+* Channel Group ID is **check**
+* Available for all vehicles
+* Read/Write access
+
+| Channel Label                   | Channel ID          | Type           | Access     |
+|---------------------------------|---------------------|----------------|------------|
+| CheckControl Description        | name                | String         | Read/Write |
+| CheckControl Details            | details             | String         | Read       |
+| Mileage Occurrence              | mileage             | Number:Length  | Read       |
+
+#### Doors Details
+
+Detailed status of all doors and windows.
+
+* Channel Group ID is **doors**
+* Available for all vehicles if corresponding sensors are built-in 
+* Read-only values
+| Channel Label              | Channel ID              | Type          | 
+|----------------------------|-------------------------|---------------|
+| Driver Door                | driver-front            | String        |
+| Driver Door Rear           | driver-rear             | String        |
+| Passenger Door             | passenger-front         | String        |
+| Passenger Door Rear        | passenger-rear          | String        |
+| Trunk                      | trunk                   | String        |
+| Hood                       | hood                    | String        |
+| Driver Window              | win-driver-front        | String        |
+| Driver Rear Window         | win-driver-rear         | String        |
+| Passenger Window           | win-passenger-front     | String        |
+| Passenger Rear Window      | win-passenger-rear      | String        |
+| Rear Window                | win-rear                | String        |
+| Sunroof                    | sunroof                 | String        |
+
+Possible states
+
+* _Undef_ - no status data available
+* _Invalid_ - this door / window isn't applicable for this vehicle
+* _Closed_ - the door / window is closed
+* _Open_ - the door / window is open
+* _Intermediate_ - window in intermediate position, not applicable for doors
+
+#### Range Data
+
+Based on vehicle type some channels are present or not. 
+Conventional fuel vehicles don't provide *Electric Range* and battery electric vehicles don't show *Fuel Range*.
+Hybrid vehicles have both and in addition *Hybrid Range*.
+See description [Range vs Range Radius](#range-vs-range-radius) to get more information.
+
+* Channel Group ID is **range**
+* Availability according to table
+* Read-only values
+
+| Channel Label         | Channel ID            | Type                 | conv | phev | bev_rex | bev |
+|-----------------------|-----------------------|----------------------|------|------|---------|-----|
+| Mileage               | mileage               | Number:Length        |  X   |  X   |    X    |  X  |
+| Fuel Range            | range-fuel            | Number:Length        |  X   |  X   |    X    |     |
+| Battery Range         | range-electric        | Number:Length        |      |  X   |    X    |  X  | 
+| Hybrid Range          | range-hybrid          | Number:Length        |      |  X   |    X    |     | 
+| Battery Charge Level  | soc                   | Number:Dimensionless |      |  X   |    X    |  X  |
+| Remaining Fuel        | remaining-fuel        | Number:Volume        |  X   |  X   |    X    |     | 
+| Fuel Range Radius     | range-radius-fuel     | Number:Length        |  X   |  X   |    X    |     | 
+| Electric Range Radius | range-radius-electric | Number:Length        |      |  X   |    X    |  X  | 
+| Hybrid Range Radius   | range-radius-hybrid   | Number:Length        |      |  X   |    X    |     | 
+
+
+#### Charge Profile
+
+Charging options with date and time for preferred time windows and charging modes.
+
+* Channel Group ID is **charge**
+* Available for electric and hybrid vehicles
+* Read/Write access for UI. Use [Charge Profile Editing Action](#charge-profile-editing) in rules
+* There are 3 timers *T1, T2 and T3* available. Replace *X* with number 1,2 or 3 to target the correct timer
+* Additional override Timer *OT* defines a single departure besides the 3 predefined schedule timers  
+
+| Channel Label              | Channel Group ID | Channel ID                | Type     | 
+|----------------------------|------------------|---------------------------|----------| 
+| Charge Mode                | charge           | profile-mode              | String   | 
+| Charge Preferences         | charge           | profile-prefs             | String   | 
+| Window Start Time          | charge           | window-start              | DateTime | 
+| Window End Time            | charge           | window-end                | DateTime | 
+| A/C at Departure           | charge           | profile-climate           | Switch   | 
+| T*X* Enabled               | charge           | timer*X*-enabled          | Switch   | 
+| T*X* Departure Time        | charge           | timer*X*-departure        | DateTime | 
+| T*X* Days                  | charge           | timer*X*-days             | String   | 
+| T*X* Monday                | charge           | timer*X*-day-mon          | Switch   | 
+| T*X* Tuesday               | charge           | timer*X*-day-tue          | Switch   | 
+| T*X* Wednesday             | charge           | timer*X*-day-wed          | Switch   | 
+| T*X* Thursday              | charge           | timer*X*-day-thu          | Switch   | 
+| T*X* Friday                | charge           | timer*X*-day-fri          | Switch   | 
+| T*X* Saturday              | charge           | timer*X*-day-sat          | Switch   | 
+| T*X* Sunday                | charge           | timer*X*-day-sun          | Switch   | 
+| OT Enabled                 | charge           | override-enabled          | Switch   | 
+| OT Departure Time          | charge           | override-departure        | DateTime | 
+
+The channel _profile-mode_ supports
+
+* *IMMEDIATE_CHARGING*
+* *DELAYED_CHARGING*
+
+The channel _profile-prefs_ supports
+
+* *NO_PRESELECTION*
+* *CHARGING_WINDOW*
+
+#### Location
+
+GPS location and heading of the vehicle.
+
+* Channel Group ID is **location**
+* Available for all vehicles with built-in GPS sensor. Function can be enabled/disabled in the head unit
+* Read-only values
+
+| Channel Label   | Channel ID          | Type         | 
+|-----------------|---------------------|--------------|
+| GPS Coordinates | gps                 | Location     | 
+| Heading         | heading             | Number:Angle | 
+
+#### Last Trip
+
+Statistic values of duration, distance and consumption of the last trip.
+
+* Channel Group ID is **last-trip**
+* Available if *Statistics* is present in *Services Supported*. See [Vehicle Properties](#properties) for further details
+* Read-only values
+* Depending on units configuration in [Thing Configuration](#thing-configuration) average values are given for 100 kilometers or miles
+
+| Channel Label                           | Channel ID                   | Type          |
+|-----------------------------------------|------------------------------|---------------|
+| Last Trip Date                          | date                         | DateTime      |
+| Last Trip Duration                      | duration                     | Number:Time   |
+| Last Trip Distance                      | distance                     | Number:Length |
+| Distance since Charge                   | distance-since-charging      | Number:Length |
+| Avg. Power Consumption                  | avg-consumption              | Number:Power  |
+| Avg. Power Recuperation                 | avg-recuperation             | Number:Power  |
+| Avg. Combined Consumption               | avg-combined-consumption     | Number:Volume |
+
+
+#### Lifetime Statistics
+
+Providing lifetime consumption values.
+
+* Channel Group ID is **lifetime**
+* Available if *Statistics* is present in *Services Supported*. See [Vehicle Properties](#properties) for further details
+* Read-only values
+* Depending on units configuration in [Thing Configuration](#thing-configuration) average values are given for 100 kilometers or miles
+
+| Channel Label                           | Channel ID                   | Type          | 
+|-----------------------------------------|------------------------------|---------------|
+| Total Electric Distance                 | total-driven-distance        | Number:Length |
+| Longest 1-Charge Distance               | single-longest-distance      | Number:Length |
+| Avg. Power Consumption                  | avg-consumption              | Number:Power  |
+| Avg. Power Recuperation                 | avg-recuperation             | Number:Power  |
+| Avg. Combined Consumption               | avg-combined-consumption     | Number:Volume |
+
+
+#### Remote Services
+
+Remote control of the vehicle. 
+Send a *command* to the vehicle and the *state* is reporting the execution progress.
+Only one command can be executed each time.
+Parallel execution isn't supported.
+
+* Channel Group ID is **remote**
+* Available for all commands mentioned in *Services Activated*. See [Vehicle Properties](#properties) for further details
+* Read/Write access
+
+
+| Channel Label           | Channel ID          | Type    | Access |
+|-------------------------|---------------------|---------|--------|
+| Remote Service Command  | command             | String  | Write  |
+| Service Execution State | state               | String  | Read   |
+
+The channel _command_ provides options
+
+* _Flash Lights_
+* _Vehicle Finder_
+* _Door Lock_
+* _Door Unlock_
+* _Horn Blow_
+* _Climate Control_
+* _Start Charging_
+* _Send Charging Profile_
+
+The channel _state_ shows the progress of the command execution in the following order
+
+1) _Initiated_ 
+2) _Pending_
+3) _Delivered_
+4) _Executed_
+
+#### Destinations
+
+Shows the last destinations stored in the navigation system.
+If several last destinations are stored in the navigation system the channel _name_ contains all addresses as options.
+
+* Channel Group ID is **destination**
+* Available if *LastDestinations* is present in *Services Supported*. Check [Vehicle Properties](#properties) for further details
+* Read/Write access
+
+
+| Channel Label        | Channel ID    | Type      | Access      |
+|----------------------|---------------|-----------|-------------|
+| Name                 | name          | String    | Read/Write  |
+| GPS Coordinates      | gps           | Location  | Read        |
+
+
+
+#### Image
+
+Image representation of the vehicle. Size and viewport are writable and can be 
+The possible values are the same mentioned in [Thing Configuration](#thing-configuration).
+
+* Channel Group ID is **image**
+* Available for all vehicles
+* Read/Write access
+
+| Channel Label              | Channel ID          | Type   |  Access  |
+|----------------------------|---------------------|--------|----------|
+| Rendered Vehicle Image     | png                 | Image  | Read     |
+| Image Viewport             | view                | String | Write    |
+| Image Picture Size         | size                | Number | Write    |
+
+## Actions
+
+Get the _Actions_ object for your vehicle using the Thing ID
+
+* bmwconnecteddrive - Binding ID, don't change!
+* bev_rex - [Thing UID](#things) of your car
+* user - Thing ID of the [Bridge](#bridge)
+* i3 - Thing ID of your car
+
+```
+  val profile = getActions("bmwconnecteddrive", "bmwconnecteddrive:bev_rex:user:i3")
+```
+
+### Charge Profile Editing
+
+Like in the Charge Profile Channels 3 Timers are provided. Replace *X* with 1, 2 or 3 to address the right timer.
+
+| Function                              | Parameters       | Returns                   | Description                                                | 
+|---------------------------------------|------------------|---------------------------|------------------------------------------------------------| 
+| getClimatizationEnabled               | void             | Boolean                   | Returns the enabled state of climatization                 | 
+| setClimatizationEnabled               | Boolean          | void                      | Sets the enabled state of climatization                    | 
+| getChargingMode                       | void             | String                    | Gets the charging-mode, see valid options below            | 
+| setChargingMode                       | String           | void                      | Sets the charging-mode, see valid options below            | 
+| getPreferredWindowStart               | void             | LocalTime                 | Returns the preferred charging-window start time           | 
+| setPreferredWindowStart               | LocalTime        | void                      | Sets the preferred charging-window start time              | 
+| getPreferredWindowEnd                 | void             | LocalTime                 | Returns the preferred charging-window end time             | 
+| setPreferredWindowEnd                 | LocalTime        | void                      | Sets the preferred charging-window end time                | 
+| getTimer*X*Enabled                    | void             | Boolean                   | Returns the enabled state of timer*X*                      | 
+| setTimer*X*Enabled                    | Boolean          | void                      | Returns the enabled state of timer*X*                      | 
+| getTimer*X*Departure                  | void             | LocalTime                 | Returns the departure time of timer*X*                     | 
+| setTimer*X*Departure                  | LocalTime        | void                      | Sets the timer*X* departure time                           | 
+| getTimer*X*Days                       | void             | Set<DayOfWeek>            | Returns the days of week timer*X* is enabled for           | 
+| setTimer*X*Days                       | Set<DayOfWeek>   | void                      | sets the days of week timer*X* is enabled for              | 
+| getOverrideTimerEnabled               | void             | Boolean                   | Returns the enabled state of override timer                | 
+| setOverrideTimerEnabled               | Boolean          | void                      | Sets the enabled state of override timer                   | 
+| getOverrideTimerDeparture             | void             | LocalTime                 | Returns the departure time of override timer               | 
+| setOverrideTimerDeparture             | LocalTime        | void                      | Sets the override timer departure time                     | 
+| getOverrideTimerDays                  | void             | Set<DayOfWeek>            | Returns the days of week the overrideTimer is enabled for  | 
+| setOverrideTimerDays                  | Set<DayOfWeek>   | void                      | Sets the days of week the overrideTimer is enabled for     | 
+| cancelEditChargeProfile               | void             | void                      | Cancel current edit of charging profile                    | 
+| sendChargeProfile                     | void             | void                      | Sends the charging profile to the vehicle                  | 
+
+Values for valid charging mode get/set
+
+* *IMMEDIATE_CHARGING*
+* *DELAYED_CHARGING*
+
+
+## Further Descriptions
+
+### Dynamic Data
+
+<img align="right" src="./doc/ServiceOptions.png" width="400" height="350"/>
+
+There are 3 occurrences of dynamic data delivered
+
+* Upcoming Services delivered in group [Services](#services)
+* Check Control Messages delivered in group [Check Control](#check-control)
+* Last Destinations delivered in group [Destinations](#destinations)
+
+The channel id _name_ shows the first element as default. 
+All other possibilities are attached as options. 
+The picture on the right shows the _Service Name_ item and all four possible options. 
+Select the desired service and the corresponding _Service Date & Milage_ will be shown.  
+
+### TroubleShooting
+
+BMW has a high range of vehicles supported by ConnectedDrive.
+In case of any issues with this binding help to resolve it! 
+Please perform the following steps:
+
+* Can you [log into ConnectedDrive](https://www.bmw-connecteddrive.com/country-region-select/country-region-selection.html) with your credentials? Please note this isn't the BMW Customer portal - it's the ConnectedDrive portal
+* Is the vehicle listed in your account? There's a one-to-one relation from user to vehicle
+
+If the access to the portal is working and the vehicle is listed some debug data is needed in order to identify the issue. 
+
+#### Generate Debug Fingerprint
+
+If you checked the above pre-conditions you need to get the debug fingerprint from the logs.
+First [enable debug logging](https://www.openhab.org/docs/administration/logging.html#defining-what-to-log) for the binding.
+
+```
+log:set DEBUG org.openhab.binding.bmwconnecteddrive
+```
+
+The debug fingerprint is generated immediately after the vehicle thing is initialized the first time, e.g. after openHAB startup. 
+To force a new fingerprint disable the thing shortly and enable it again. 
+Personal data is eliminated from the log entries so it should be possible to share them in public.
+Data like
+
+* Dealer Properties
+* Vehicle Identification Number (VIN)
+* Location latitude / longitude 
+
+are anonymized.
+You'll find the fingerprint in the logs with the command
+
+```
+grep "Troubleshoot Fingerprint Data" openhab.log
+```
+
+After the corresponding fingerprint is generated please [follow the instructions to raise an issue](https://community.openhab.org/t/how-to-file-an-issue/68464) and attach the fingerprint data!
+Your feedback is highly appreciated!
+
+
+### Range vs Range Radius
+
+<img align="right" src="./doc/range-radius.png" width="400" height="350"/>
+
+You will observe differences in the vehicle range and range radius values. 
+While range is indicating the possible distance to be driven on roads the range radius indicates the reachable range on the map.
+
+The right picture shows the distance between Kassel and Frankfurt in Germany. 
+While the air-line distance is ~145 kilometer the route distance is ~192 kilometer.
+So range value is the normal remaining range while the range radius values can be used e.g. on [Mapview](https://www.openhab.org/docs/configuration/sitemaps.html#element-type-mapview) to indicate the reachable range on map.
+Please note this is just an indicator of the effective range.
+Especially for electric vehicles it depends on many factors like driving style and usage of electric consumers. 
+
+## Full Example
+
+The example is based on a BMW i3 with range extender (REX). 
+Exchange the three configuration parameters in the Things section
+
+* YOUR_USERNAME - with your ConnectedDrive login username
+* YOUR_PASSWORD - with your ConnectedDrive password credentials
+* VEHICLE_VIN - the vehicle identification number
+
+In addition search for all occurrences of *i3* and replace it with your Vehicle Identification like *x3* or *535d* and you're ready to go!
+
+### Things File
+
+```
+Bridge bmwconnecteddrive:account:user   "BMW ConnectedDrive Account" [userName="YOUR_USERNAME",password="YOUR_PASSWORD",region="ROW"] {
+         Thing bev_rex i3       "BMW i3 94h REX"                [ vin="VEHICLE_VIN",units="AUTODETECT",imageSize=600,imageViewport="FRONT",refreshInterval=5]
+}
+```
+
+### Items File
+
+```
+Number:Length           i3Mileage                 "Odometer [%d %unit%]"                        <line>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:range#mileage" }                                                                           
+Number:Length           i3Range                   "Range [%d %unit%]"                           <motion>        (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:range#hybrid"}
+Number:Length           i3RangeElectric           "Electric Range [%d %unit%]"                  <motion>        (i3,long)   {channel="bmwconnecteddrive:bev_rex:user:i3:range#electric"}   
+Number:Length           i3RangeFuel               "Fuel Range [%d %unit%]"                      <motion>        (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:range#fuel"}
+Number:Dimensionless    i3BatterySoc              "Battery Charge [%.1f %%]"                    <battery>       (i3,long)   {channel="bmwconnecteddrive:bev_rex:user:i3:range#soc"}
+Number:Volume           i3Fuel                    "Fuel [%.1f %unit%]"                          <oil>           (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:range#remaining-fuel"}
+Number:Length           i3RadiusElectric          "Electric Radius [%d %unit%]"                 <zoom>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:range#radius-electric" }
+Number:Length           i3RadiusHybrid            "Hybrid Radius [%d %unit%]"                   <zoom>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:range#radius-hybrid" }
+
+String                  i3DoorStatus              "Door Status [%s]"                            <lock>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:status#doors" }
+String                  i3WindowStatus            "Window Status [%s]"                          <lock>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:status#windows" }
+String                  i3LockStatus              "Lock Status [%s]"                            <lock>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:status#lock" }
+DateTime                i3NextServiceDate         "Next Service Date [%1$tb %1$tY]"             <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:status#service-date" }
+String                  i3NextServiceMileage      "Next Service Mileage [%d %unit%]"            <line>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:status#service-mileage" }
+String                  i3CheckControl            "Check Control [%s]"                          <error>         (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:status#check-control" }
+String                  i3ChargingStatus          "Charging [%s]"                               <energy>        (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:status#charge" } 
+DateTime                i3LastUpdate              "Update [%1$tA, %1$td.%1$tm. %1$tH:%1$tM]"    <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:status#last-update"}
+
+DateTime                i3TripDateTime            "Trip Date [%1$tA, %1$td.%1$tm. %1$tH:%1$tM]" <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:last-trip#date"}
+Number:Time             i3TripDuration            "Trip Duration [%d %unit%]"                   <time>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:last-trip#duration"}
+Number:Length           i3TripDistance            "Distance [%d %unit%]"                        <line>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:last-trip#distance" }                                                                           
+Number:Length           i3TripDistanceSinceCharge "Distance since last Charge [%d %unit%]"      <line>          (i3,long)   {channel="bmwconnecteddrive:bev_rex:user:i3:last-trip#distance-since-charging" }                                                                           
+Number:Energy           i3AvgTripConsumption      "Average Consumption [%.1f %unit%]"           <energy>        (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:last-trip#avg-consumption" }                                                                           
+Number:Volume           i3AvgTripCombined         "Average Combined Consumption [%.1f %unit%]"  <oil>           (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:last-trip#avg-combined-consumption" }                                                                           
+Number:Energy           i3AvgTripRecuperation     "Average Recuperation [%.1f %unit%]"          <energy>        (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:last-trip#avg-recuperation" }                                                                           
+
+Number:Length           i3TotalElectric           "Electric Distance Driven [%d %unit%]"        <line>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:lifetime#total-driven-distance" }                                                                           
+Number:Length           i3LongestEVTrip           "Longest Electric Trip [%d %unit%]"           <line>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:lifetime#single-longest-distance" }                                                                           
+Number:Energy           i3AvgConsumption          "Average Consumption [%.1f %unit%]"           <energy>        (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:lifetime#avg-consumption" }                                                                           
+Number:Volume           i3AvgCombined             "Average Combined Consumption [%.1f %unit%]"  <oil>           (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:lifetime#avg-combined-consumption" }                                                                           
+Number:Energy           i3AvgRecuperation         "Average Recuperation [%.1f %unit%]"          <energy>        (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:lifetime#avg-recuperation" }  
+
+Location                i3Location                "Location  [%s]"                              <zoom>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:location#gps" }                                                                           
+Number:Angle            i3Heading                 "Heading [%.1f %unit%]"                       <zoom>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:location#heading" }  
+
+String                  i3RemoteCommand           "Command [%s]"                                <switch>        (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:remote#command" } 
+String                  i3RemoteState             "Remote Execution State [%s]"                 <status>        (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:remote#state" } 
+
+String                  i3DriverDoor              "Driver Door [%s]"                            <lock>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:doors#driver-front" }
+String                  i3DriverDoorRear          "Driver Door Rear [%s]"                       <lock>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:doors#driver-rear" }
+String                  i3PassengerDoor           "Passenger Door [%s]"                         <lock>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:doors#passenger-front" }
+String                  i3PassengerDoorRear       "Passenger Door Rear [%s]"                    <lock>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:doors#passenger-rear" }
+String                  i3Hood                    "Hood [%s]"                                   <lock>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:doors#hood" }
+String                  i3Trunk                   "Trunk [%s]"                                  <lock>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:doors#trunk" }
+String                  i3DriverWindow            "Driver Window [%s]"                          <lock>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:doors#win-driver-front" }
+String                  i3DriverWindowRear        "Driver Window Rear [%s]"                     <lock>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:doors#win-driver-rear" }
+String                  i3PassengerWindow         "Passenger Window [%s]"                       <lock>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:doors#win-passenger-front" }
+String                  i3PassengerWindowRear     "Passenger Window Rear [%s]"                  <lock>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:doors#win-passenger-rear" }
+String                  i3RearWindow              "Rear Window [%s]"                            <lock>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:doors#win-rear" }
+String                  i3Sunroof                 "Sunroof [%s]"                                <lock>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:doors#sunroof" }
+
+String                  i3ServiceName             "Service Name [%s]"                           <text>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:service#name" }
+String                  i3ServiceDetails          "Service Details [%s]"                        <text>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:service#details" }
+Number:Length           i3ServiceMileage          "Service Mileage [%d %unit%]"                 <line>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:service#mileage" }
+DateTime                i3ServiceDate             "Service Date [%1$tb %1$tY]"                  <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:service#date" }
+
+String                  i3CCName                  "CheckControl Name [%s]"                      <text>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:check#name" }
+String                  i3CCDetails               "CheckControl Details [%s]"                   <text>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:check#details" }
+Number:Length           i3CCMileage               "CheckControl Mileage [%d %unit%]"            <line>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:check#mileage" }
+
+String                  i3DestName                "Destination [%s]"                            <house>         (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:destination#name" } 
+Location                i3DestLocation            "GPS [%s]"                                    <zoom>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:destination#gps" }                                                                           
+Switch                  i3ChargeProfileClimate    "Charge Profile Climatization"                <temperature>   (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#profile-climate" }  
+String                  i3ChargeProfileMode       "Charge Profile Mode [%s]"                    <energy>        (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#profile-mode" } 
+DateTime                i3ChargeWindowStart       "Charge Window Start [%1$tH:%1$tM]"           <time>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#window-start" } 
+Number                  i3ChargeWindowStartHour   "Charge Window Start Hour [%d]"               <time>          (i3)
+Number                  i3ChargeWindowStartMinute "Charge Window Start Minute [%d]"             <time>          (i3)
+DateTime                i3ChargeWindowEnd         "Charge Window End [%1$tH:%1$tM]"             <time>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#window-end" } 
+Number                  i3ChargeWindowEndHour     "Charge Window End Hour [%d]"                 <time>          (i3)
+Number                  i3ChargeWindowEndMinute   "Charge Window End Minute [%d]"               <time>          (i3)
+DateTime                i3Timer1Departure         "Timer 1 Departure [%1$tH:%1$tM]"             <time>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-departure" } 
+Number                  i3Timer1DepartureHour     "Timer 1 Departure Hour [%d]"                 <time>          (i3)
+Number                  i3Timer1DepartureMinute   "Timer 1 Departure Minute [%d]"               <time>          (i3)
+String                  i3Timer1Days              "Timer 1 Days [%s]"                           <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-days" } 
+Switch                  i3Timer1DayMon            "Timer 1 Monday"                              <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-day-mon" } 
+Switch                  i3Timer1DayTue            "Timer 1 Tuesday"                             <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-day-tue" } 
+Switch                  i3Timer1DayWed            "Timer 1 Wednesday"                           <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-day-wed" } 
+Switch                  i3Timer1DayThu            "Timer 1 Thursday"                            <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-day-thu" } 
+Switch                  i3Timer1DayFri            "Timer 1 Friday"                              <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-day-fri" } 
+Switch                  i3Timer1DaySat            "Timer 1 Saturday"                            <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-day-sat" } 
+Switch                  i3Timer1DaySun            "Timer 1 Sunday"                              <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-day-sun" } 
+Switch                  i3Timer1Enabled           "Timer 1 Enabled"                             <switch>        (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer1-enabled" }  
+DateTime                i3Timer2Departure         "Timer 2 Departure [%1$tH:%1$tM]"             <time>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-departure" } 
+Number                  i3Timer2DepartureHour     "Timer 2 Departure Hour [%d]"                 <time>          (i3)
+Number                  i3Timer2DepartureMinute   "Timer 2 Departure Minute [%d]"               <time>          (i3)
+String                  i3Timer2Days              "Timer 2 Days [%s]"                           <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-days" } 
+Switch                  i3Timer2DayMon            "Timer 2 Monday"                              <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-day-mon" } 
+Switch                  i3Timer2DayTue            "Timer 2 Tuesday"                             <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-day-tue" } 
+Switch                  i3Timer2DayWed            "Timer 2 Wednesday"                           <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-day-wed" } 
+Switch                  i3Timer2DayThu            "Timer 2 Thursday"                            <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-day-thu" } 
+Switch                  i3Timer2DayFri            "Timer 2 Friday"                              <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-day-fri" } 
+Switch                  i3Timer2DaySat            "Timer 2 Saturday"                            <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-day-sat" } 
+Switch                  i3Timer2DaySun            "Timer 2 Sunday"                              <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-day-sun" } 
+Switch                  i3Timer2Enabled           "Timer 2 Enabled"                             <switch>        (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer2-enabled" }  
+DateTime                i3Timer3Departure         "Timer 3 Departure [%1$tH:%1$tM]"             <time>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-departure" } 
+Number                  i3Timer3DepartureHour     "Timer 3 Departure Hour [%d]"                 <time>          (i3)
+Number                  i3Timer3DepartureMinute   "Timer 3 Departure Minute [%d]"               <time>          (i3)
+String                  i3Timer3Days              "Timer 3 Days [%s]"                           <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-days" } 
+Switch                  i3Timer3DayMon            "Timer 3 Monday"                              <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-day-mon" } 
+Switch                  i3Timer3DayTue            "Timer 3 Tuesday"                             <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-day-tue" } 
+Switch                  i3Timer3DayWed            "Timer 3 Wednesday"                           <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-day-wed" } 
+Switch                  i3Timer3DayThu            "Timer 3 Thursday"                            <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-day-thu" } 
+Switch                  i3Timer3DayFri            "Timer 3 Friday"                              <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-day-fri" } 
+Switch                  i3Timer3DaySat            "Timer 3 Saturday"                            <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-day-sat" } 
+Switch                  i3Timer3DaySun            "Timer 3 Sunday"                              <calendar>      (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-day-sun" } 
+Switch                  i3Timer3Enabled           "Timer 3 Enabled"                             <switch>        (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#timer3-enabled" }
+Switch                  i3OverrideEnabled         "Override Timer Enabled"                      <switch>        (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#override-enabled"}
+DateTime                i3OverrideDeparture       "Override Timer Departure [%1$tH:%1$tM]"      <time>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:charge#override-departure" } 
+Number                  i3OverrideDepartureHour   "Override Timer Departure Hour [%d]"          <time>          (i3)
+Number                  i3OverrideDepartureMinute "Override Timer Departure Minute [%d]"        <time>          (i3)
+
+Image                   i3Image                   "Image"                                                       (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:image#png" }  
+String                  i3ImageViewport           "Image Viewport [%s]"                         <zoom>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:image#view" }  
+Number                  i3ImageSize               "Image Size [%d]"                             <zoom>          (i3)        {channel="bmwconnecteddrive:bev_rex:user:i3:image#size" }  
+```
+
+### Sitemap File
+
+```
+sitemap BMW label="BMW" {
+  Frame label="BMW i3" {
+    Image  item=i3Image  
+                       
+  } 
+  Frame label="Range" {
+    Text    item=i3Mileage           
+    Text    item=i3Range             
+    Text    item=i3RangeElectric     
+    Text    item=i3RangeFuel         
+    Text    item=i3BatterySoc        
+    Text    item=i3Fuel              
+    Text    item=i3RadiusElectric       
+    Text    item=i3RadiusHybrid         
+  }
+  Frame label="Status" {
+    Text    item=i3DoorStatus           
+    Text    item=i3WindowStatus         
+    Text    item=i3LockStatus           
+    Text    item=i3NextServiceDate              
+    Text    item=i3NextServiceMileage       
+    Text    item=i3CheckControl         
+    Text    item=i3ChargingStatus           
+    Text    item=i3LastUpdate               
+  }
+  Frame label="Remote Services" {
+    Selection item=i3RemoteCommand              
+    Text      item=i3RemoteState              
+  }
+  Frame label="Last Trip" {
+    Text    item=i3TripDateTime            
+    Text    item=i3TripDuration            
+    Text    item=i3TripDistance            
+    Text    item=i3TripDistanceSinceCharge 
+    Text    item=i3AvgTripConsumption      
+    Text    item=i3AvgTripRecuperation     
+    Text    item=i3AvgTripCombined     
+  }
+  Frame label="Lifetime" {
+    Text    item=i3TotalElectric  
+    Text    item=i3LongestEVTrip      
+    Text    item=i3AvgConsumption     
+    Text    item=i3AvgRecuperation          
+    Text    item=i3AvgCombined          
+  }
+  Frame label="Services" {
+    Text    item=i3ServiceName          
+    Text    item=i3ServiceMileage          
+    Text    item=i3ServiceDate          
+  }
+  Frame label="CheckControl" {
+    Text    item=i3CCName          
+    Text    item=i3CCMileage          
+  }
+  Frame label="Door Details" {
+    Text    item=i3DriverDoor visibility=[i3DriverDoor!="INVALID"]
+    Text    item=i3DriverDoorRear visibility=[i3DriverDoorRear!="INVALID"]  
+    Text    item=i3PassengerDoor visibility=[i3PassengerDoor!="INVALID"]
+    Text    item=i3PassengerDoorRear visibility=[i3PassengerDoorRear!="INVALID"]
+    Text    item=i3Hood visibility=[i3Hood!="INVALID"]
+    Text    item=i3Trunk visibility=[i3Trunk!="INVALID"]
+    Text    item=i3DriverWindow visibility=[i3DriverWindow!="INVALID"]
+    Text    item=i3DriverWindowRear visibility=[i3DriverWindowRear!="INVALID"]
+    Text    item=i3PassengerWindow visibility=[i3PassengerWindow!="INVALID"]
+    Text    item=i3PassengerWindowRear visibility=[i3PassengerWindowRear!="INVALID"]
+    Text    item=i3RearWindow visibility=[i3RearWindow!="INVALID"]
+    Text    item=i3Sunroof visibility=[i3Sunroof!="INVALID"]
+  }
+  Frame label="Location" {
+    Text    item=i3Location          
+    Text    item=i3Heading             
+  }
+  Frame label="Charge Profile" {    
+    Switch    item=i3ChargeProfileClimate     
+    Selection item=i3ChargeProfileMode        
+    Text      item=i3ChargeWindowStart        
+    Setpoint  item=i3ChargeWindowStartHour maxValue=23 step=1 icon="time"
+    Setpoint  item=i3ChargeWindowStartMinute maxValue=55 step=5 icon="time"
+    Text      item=i3ChargeWindowEnd          
+    Setpoint  item=i3ChargeWindowEndHour maxValue=23 step=1 icon="time"
+    Setpoint  item=i3ChargeWindowEndMinute maxValue=55 step=5 icon="time"
+    Text      item=i3Timer1Departure          
+    Setpoint  item=i3Timer1DepartureHour maxValue=23 step=1 icon="time"
+    Setpoint  item=i3Timer1DepartureMinute maxValue=55 step=5 icon="time"
+    Text      item=i3Timer1Days               
+    Switch    item=i3Timer1DayMon            
+    Switch    item=i3Timer1DayTue            
+    Switch    item=i3Timer1DayWed            
+    Switch    item=i3Timer1DayThu            
+    Switch    item=i3Timer1DayFri            
+    Switch    item=i3Timer1DaySat            
+    Switch    item=i3Timer1DaySun            
+    Switch    item=i3Timer1Enabled            
+    Text      item=i3Timer2Departure          
+    Setpoint  item=i3Timer2DepartureHour maxValue=23 step=1 icon="time"
+    Setpoint  item=i3Timer2DepartureMinute maxValue=55 step=5 icon="time"
+    Text      item=i3Timer2Days               
+    Switch    item=i3Timer2DayMon            
+    Switch    item=i3Timer2DayTue            
+    Switch    item=i3Timer2DayWed            
+    Switch    item=i3Timer2DayThu            
+    Switch    item=i3Timer2DayFri            
+    Switch    item=i3Timer2DaySat            
+    Switch    item=i3Timer2DaySun            
+    Switch    item=i3Timer2Enabled            
+    Text      item=i3Timer3Departure          
+    Setpoint  item=i3Timer3DepartureHour maxValue=23 step=1 icon="time"
+    Setpoint  item=i3Timer3DepartureMinute maxValue=55 step=5 icon="time"
+    Text      item=i3Timer3Days               
+    Switch    item=i3Timer3DayMon            
+    Switch    item=i3Timer3DayTue            
+    Switch    item=i3Timer3DayWed            
+    Switch    item=i3Timer3DayThu            
+    Switch    item=i3Timer3DayFri            
+    Switch    item=i3Timer3DaySat            
+    Switch    item=i3Timer3DaySun            
+    Switch    item=i3Timer3Enabled            
+    Switch    item=i3OverrideEnabled            
+    Text      item=i3OverrideDeparture          
+    Setpoint  item=i3OverrideDepartureHour maxValue=23 step=1 icon="time"
+    Setpoint  item=i3OverrideDepartureMinute maxValue=55 step=5 icon="time"
+  } 
+  Frame label="Last Destinations" {    
+    Text  item=i3DestName                 
+    Text  item=i3DestLocation                                                                                   
+  }  
+  Frame label="Image Properties" {
+    Text    item=i3ImageViewport
+    Text    item=i3ImageSize 
+  } 
+}
+```
+
+### Rules File
+
+```
+rule "i3ChargeWindowStartSetpoint"
+when
+    Item i3ChargeWindowStartMinute changed or
+    Item i3ChargeWindowStartHour changed
+then
+    val hour = (i3ChargeWindowStartHour.state as Number).intValue
+    val minute = (i3ChargeWindowStartMinute.state as Number).intValue
+    val time = (i3ChargeWindowStart.state as DateTimeType).zonedDateTime
+    i3ChargeWindowStart.sendCommand(new DateTimeType(time.withHour(hour).withMinute(minute)))
+end
+
+rule "i3ChargeWindowStart"
+when
+    Item i3ChargeWindowStart changed
+then
+    val time = (i3ChargeWindowStart.state as DateTimeType).zonedDateTime
+    i3ChargeWindowStartMinute.sendCommand(time.minute)
+    i3ChargeWindowStartHour.sendCommand(time.hour)
+end
+
+rule "i3ChargeWindowEndSetpoint"
+when
+    Item i3ChargeWindowEndMinute changed or
+    Item i3ChargeWindowEndHour changed
+then
+    val hour = (i3ChargeWindowEndHour.state as Number).intValue
+    val minute = (i3ChargeWindowEndMinute.state as Number).intValue
+    val time = (i3ChargeWindowEnd.state as DateTimeType).zonedDateTime
+    i3ChargeWindowEnd.sendCommand(new DateTimeType(time.withHour(hour).withMinute(minute)))
+end
+
+rule "i3ChargeWindowEnd"
+when
+    Item i3ChargeWindowEnd changed
+then
+    val time = (i3ChargeWindowEnd.state as DateTimeType).zonedDateTime
+    i3ChargeWindowEndMinute.sendCommand(time.minute)
+    i3ChargeWindowEndHour.sendCommand(time.hour)
+end
+
+rule "i3Timer1DepartureSetpoint"
+when
+    Item i3Timer1DepartureMinute changed or
+    Item i3Timer1DepartureHour changed
+then
+    val hour = (i3Timer1DepartureHour.state as Number).intValue
+    val minute = (i3Timer1DepartureMinute.state as Number).intValue
+    val time = (i3Timer1Departure.state as DateTimeType).zonedDateTime
+    i3Timer1Departure.sendCommand(new DateTimeType(time.withHour(hour).withMinute(minute)))
+end
+
+rule "i3Timer1Departure"
+when
+    Item i3Timer1Departure changed
+then
+    val time = (i3Timer1Departure.state as DateTimeType).zonedDateTime
+    i3Timer1DepartureMinute.sendCommand(time.minute)
+    i3Timer1DepartureHour.sendCommand(time.hour)
+end
+
+rule "i3Timer2DepartureSetpoint"
+when
+    Item i3Timer2DepartureMinute changed or
+    Item i3Timer2DepartureHour changed
+then
+    val hour = (i3Timer2DepartureHour.state as Number).intValue
+    val minute = (i3Timer2DepartureMinute.state as Number).intValue
+    val time = (i3Timer2Departure.state as DateTimeType).zonedDateTime
+    i3Timer2Departure.sendCommand(new DateTimeType(time.withHour(hour).withMinute(minute)))
+end
+
+rule "i3Timer2Departure"
+when
+    Item i3Timer2Departure changed
+then
+    val time = (i3Timer2Departure.state as DateTimeType).zonedDateTime
+    i3Timer2DepartureMinute.sendCommand(time.minute)
+    i3Timer2DepartureHour.sendCommand(time.hour)
+end
+
+rule "i3Timer3DepartureSetpoint"
+when
+    Item i3Timer3DepartureMinute changed or
+    Item i3Timer3DepartureHour changed
+then
+    val hour = (i3Timer3DepartureHour.state as Number).intValue
+    val minute = (i3Timer3DepartureMinute.state as Number).intValue
+    val time = (i3Timer3Departure.state as DateTimeType).zonedDateTime
+    i3Timer3Departure.sendCommand(new DateTimeType(time.withHour(hour).withMinute(minute)))
+end
+
+rule "i3Timer3Departure"
+when
+    Item i3Timer3Departure changed
+then
+    val time = (i3Timer3Departure.state as DateTimeType).zonedDateTime
+    i3Timer3DepartureMinute.sendCommand(time.minute)
+    i3Timer3DepartureHour.sendCommand(time.hour)
+end
+
+rule "i3OverrideDepartureSetpoint"
+when
+    Item i3OverrideDepartureMinute changed or
+    Item i3OverrideDepartureHour changed
+then
+    val hour = (i3OverrideDepartureHour.state as Number).intValue
+    val minute = (i3OverrideDepartureMinute.state as Number).intValue
+    val time = (i3OverrideDeparture.state as DateTimeType).zonedDateTime
+    i3OverrideDeparture.sendCommand(new DateTimeType(time.withHour(hour).withMinute(minute)))
+end
+
+rule "i3OverrideDeparture"
+when
+    Item i3OverrideDeparture changed
+then
+    val time = (i3OverrideDeparture.state as DateTimeType).zonedDateTime
+    i3OverrideDepartureMinute.sendCommand(time.minute)
+    i3OverrideDepartureHour.sendCommand(time.hour)
+end
+```
+
+### Action example
+
+```
+  val profile = getActions("bmwconnecteddrive", "bmwconnecteddrive:bev_rex:user:i3")
+  val now = ZonedDateTime.now.toLocalTime
+  profile.setChargingMode("DELAYED_CHARGING")
+  profile.setTimer1Departure(now.minusHours(2))
+  profile.setTimer1Days(java.util.Set())
+  profile.setTimer1Enabled(true)
+  profile.setTimer2Enabled(false)
+  profile.setTimer3Enabled(false)
+  profile.setPreferredWindowStart(now.minusHours(6))
+  profile.setPreferredWindowEnd(now.minusHours(2))
+  profile.sendChargeProfile()
+```
+
+## Credits
+
+This work is based on the project of [Bimmer Connected](https://github.com/bimmerconnected/bimmer_connected). 
+Also a [manual installation based on python](https://community.openhab.org/t/script-to-access-the-bmw-connecteddrive-portal-via-oh/37345) was already available for openHAB.
+This binding is basically a port to openHAB based on these concept works!  
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/doc/AwayImage.png b/bundles/org.openhab.binding.bmwconnecteddrive/doc/AwayImage.png
new file mode 100644 (file)
index 0000000..24dd33c
Binary files /dev/null and b/bundles/org.openhab.binding.bmwconnecteddrive/doc/AwayImage.png differ
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/doc/CarStatusImages.png b/bundles/org.openhab.binding.bmwconnecteddrive/doc/CarStatusImages.png
new file mode 100644 (file)
index 0000000..b198ede
Binary files /dev/null and b/bundles/org.openhab.binding.bmwconnecteddrive/doc/CarStatusImages.png differ
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/doc/ChargingImage.png b/bundles/org.openhab.binding.bmwconnecteddrive/doc/ChargingImage.png
new file mode 100644 (file)
index 0000000..09a132f
Binary files /dev/null and b/bundles/org.openhab.binding.bmwconnecteddrive/doc/ChargingImage.png differ
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/doc/CheckControlImage.png b/bundles/org.openhab.binding.bmwconnecteddrive/doc/CheckControlImage.png
new file mode 100644 (file)
index 0000000..4f8aa93
Binary files /dev/null and b/bundles/org.openhab.binding.bmwconnecteddrive/doc/CheckControlImage.png differ
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/doc/ServiceOptions.png b/bundles/org.openhab.binding.bmwconnecteddrive/doc/ServiceOptions.png
new file mode 100644 (file)
index 0000000..5458f86
Binary files /dev/null and b/bundles/org.openhab.binding.bmwconnecteddrive/doc/ServiceOptions.png differ
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/doc/UnlockedImage.png b/bundles/org.openhab.binding.bmwconnecteddrive/doc/UnlockedImage.png
new file mode 100644 (file)
index 0000000..7518330
Binary files /dev/null and b/bundles/org.openhab.binding.bmwconnecteddrive/doc/UnlockedImage.png differ
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/doc/panel.png b/bundles/org.openhab.binding.bmwconnecteddrive/doc/panel.png
new file mode 100644 (file)
index 0000000..efc3a61
Binary files /dev/null and b/bundles/org.openhab.binding.bmwconnecteddrive/doc/panel.png differ
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/doc/properties.png b/bundles/org.openhab.binding.bmwconnecteddrive/doc/properties.png
new file mode 100644 (file)
index 0000000..ce2a263
Binary files /dev/null and b/bundles/org.openhab.binding.bmwconnecteddrive/doc/properties.png differ
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/doc/range-radius.png b/bundles/org.openhab.binding.bmwconnecteddrive/doc/range-radius.png
new file mode 100644 (file)
index 0000000..21fc8fb
Binary files /dev/null and b/bundles/org.openhab.binding.bmwconnecteddrive/doc/range-radius.png differ
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/pom.xml b/bundles/org.openhab.binding.bmwconnecteddrive/pom.xml
new file mode 100644 (file)
index 0000000..36567a6
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.bmwconnecteddrive</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: BMWConnectedDrive Binding</name>
+
+</project>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/feature/feature.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..bf96a99
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.bmwconnecteddrive-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${project.version}/xml/features</repository>
+
+       <feature name="openhab-binding-bmwconnecteddrive" description="BMWConnectedDrive Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bmwconnecteddrive/${project.version}</bundle>
+       </feature>
+</features>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveConfiguration.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveConfiguration.java
new file mode 100644 (file)
index 0000000..c082bff
--- /dev/null
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+/**
+ * The {@link ConnectedDriveConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class ConnectedDriveConfiguration {
+
+    /**
+     * Depending on the location the correct server needs to be called
+     */
+    public String region = Constants.EMPTY;
+
+    /**
+     * BMW Connected Drive Username
+     */
+    public String userName = Constants.EMPTY;
+
+    /**
+     * BMW Connected Drive Password
+     */
+    public String password = Constants.EMPTY;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveConstants.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveConstants.java
new file mode 100644 (file)
index 0000000..31e22c4
--- /dev/null
@@ -0,0 +1,200 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link ConnectedDriveConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - edit & send of charge profile
+ */
+@NonNullByDefault
+public class ConnectedDriveConstants {
+
+    private static final String BINDING_ID = "bmwconnecteddrive";
+
+    // Units
+    public static final String UNITS_AUTODETECT = "AUTODETECT";
+    public static final String UNITS_IMPERIAL = "IMPERIAL";
+    public static final String UNITS_METRIC = "METRIC";
+
+    public static final String VIN = "vin";
+
+    public static final int DEFAULT_IMAGE_SIZE_PX = 1024;
+    public static final int DEFAULT_REFRESH_INTERVAL_MINUTES = 5;
+    public static final String DEFAULT_IMAGE_VIEWPORT = "FRONT";
+
+    // See constants from bimmer-connected
+    // https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/vehicle.py
+    public enum VehicleType {
+        CONVENTIONAL("conv"),
+        PLUGIN_HYBRID("phev"),
+        ELECTRIC_REX("bev_rex"),
+        ELECTRIC("bev");
+
+        private final String type;
+
+        VehicleType(String s) {
+            type = s;
+        }
+
+        @Override
+        public String toString() {
+            return type;
+        }
+    }
+
+    public enum ChargingMode {
+        IMMEDIATE_CHARGING,
+        DELAYED_CHARGING
+    }
+
+    public enum ChargingPreference {
+        NO_PRESELECTION,
+        CHARGING_WINDOW
+    }
+
+    public static final Set<String> FUEL_VEHICLES = Set.of(VehicleType.CONVENTIONAL.toString(),
+            VehicleType.PLUGIN_HYBRID.toString(), VehicleType.ELECTRIC_REX.toString());
+    public static final Set<String> ELECTRIC_VEHICLES = Set.of(VehicleType.ELECTRIC.toString(),
+            VehicleType.PLUGIN_HYBRID.toString(), VehicleType.ELECTRIC_REX.toString());
+
+    // Countries with Mileage display
+    public static final Set<String> IMPERIAL_COUNTRIES = Set.of("US", "GB");
+
+    // List of all Thing Type UIDs
+    public static final ThingTypeUID THING_TYPE_CONNECTED_DRIVE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account");
+    public static final ThingTypeUID THING_TYPE_CONV = new ThingTypeUID(BINDING_ID,
+            VehicleType.CONVENTIONAL.toString());
+    public static final ThingTypeUID THING_TYPE_PHEV = new ThingTypeUID(BINDING_ID,
+            VehicleType.PLUGIN_HYBRID.toString());
+    public static final ThingTypeUID THING_TYPE_BEV_REX = new ThingTypeUID(BINDING_ID,
+            VehicleType.ELECTRIC_REX.toString());
+    public static final ThingTypeUID THING_TYPE_BEV = new ThingTypeUID(BINDING_ID, VehicleType.ELECTRIC.toString());
+    public static final Set<ThingTypeUID> SUPPORTED_THING_SET = Set.of(THING_TYPE_CONNECTED_DRIVE_ACCOUNT,
+            THING_TYPE_CONV, THING_TYPE_PHEV, THING_TYPE_BEV_REX, THING_TYPE_BEV);
+
+    // Thing Group definitions
+    public static final String CHANNEL_GROUP_STATUS = "status";
+    public static final String CHANNEL_GROUP_SERVICE = "service";
+    public static final String CHANNEL_GROUP_CHECK_CONTROL = "check";
+    public static final String CHANNEL_GROUP_DOORS = "doors";
+    public static final String CHANNEL_GROUP_RANGE = "range";
+    public static final String CHANNEL_GROUP_LOCATION = "location";
+    public static final String CHANNEL_GROUP_LAST_TRIP = "last-trip";
+    public static final String CHANNEL_GROUP_LIFETIME = "lifetime";
+    public static final String CHANNEL_GROUP_REMOTE = "remote";
+    public static final String CHANNEL_GROUP_CHARGE = "charge";
+    public static final String CHANNEL_GROUP_VEHICLE_IMAGE = "image";
+    public static final String CHANNEL_GROUP_DESTINATION = "destination";
+
+    // Generic Constants for several groups
+    public static final String NAME = "name";
+    public static final String DETAILS = "details";
+    public static final String DATE = "date";
+    public static final String MILEAGE = "mileage";
+    public static final String GPS = "gps";
+    public static final String HEADING = "heading";
+
+    // Status
+    public static final String DOORS = "doors";
+    public static final String WINDOWS = "windows";
+    public static final String LOCK = "lock";
+    public static final String SERVICE_DATE = "service-date";
+    public static final String SERVICE_MILEAGE = "service-mileage";
+    public static final String CHECK_CONTROL = "check-control";
+    public static final String CHARGE_STATUS = "charge";
+    public static final String CHARGE_END_REASON = "reason";
+    public static final String CHARGE_REMAINING = "remaining";
+    public static final String LAST_UPDATE = "last-update";
+
+    // Door Details
+    public static final String DOOR_DRIVER_FRONT = "driver-front";
+    public static final String DOOR_DRIVER_REAR = "driver-rear";
+    public static final String DOOR_PASSENGER_FRONT = "passenger-front";
+    public static final String DOOR_PASSENGER_REAR = "passenger-rear";
+    public static final String HOOD = "hood";
+    public static final String TRUNK = "trunk";
+    public static final String WINDOW_DOOR_DRIVER_FRONT = "win-driver-front";
+    public static final String WINDOW_DOOR_DRIVER_REAR = "win-driver-rear";
+    public static final String WINDOW_DOOR_PASSENGER_FRONT = "win-passenger-front";
+    public static final String WINDOW_DOOR_PASSENGER_REAR = "win-passenger-rear";
+    public static final String WINDOW_REAR = "win-rear";
+    public static final String SUNROOF = "sunroof";
+
+    // Charge Profile
+    public static final String CHARGE_PROFILE_CLIMATE = "profile-climate";
+    public static final String CHARGE_PROFILE_MODE = "profile-mode";
+    public static final String CHARGE_PROFILE_PREFERENCE = "profile-prefs";
+    public static final String CHARGE_WINDOW_START = "window-start";
+    public static final String CHARGE_WINDOW_END = "window-end";
+    public static final String CHARGE_TIMER1 = "timer1";
+    public static final String CHARGE_TIMER2 = "timer2";
+    public static final String CHARGE_TIMER3 = "timer3";
+    public static final String CHARGE_OVERRIDE = "override";
+    public static final String CHARGE_DEPARTURE = "-departure";
+    public static final String CHARGE_ENABLED = "-enabled";
+    public static final String CHARGE_DAYS = "-days";
+    public static final String CHARGE_DAY_MON = "-day-mon";
+    public static final String CHARGE_DAY_TUE = "-day-tue";
+    public static final String CHARGE_DAY_WED = "-day-wed";
+    public static final String CHARGE_DAY_THU = "-day-thu";
+    public static final String CHARGE_DAY_FRI = "-day-fri";
+    public static final String CHARGE_DAY_SAT = "-day-sat";
+    public static final String CHARGE_DAY_SUN = "-day-sun";
+
+    // Range
+    public static final String RANGE_HYBRID = "hybrid";
+    public static final String RANGE_ELECTRIC = "electric";
+    public static final String SOC = "soc";
+    public static final String RANGE_FUEL = "fuel";
+    public static final String REMAINING_FUEL = "remaining-fuel";
+    public static final String RANGE_RADIUS_ELECTRIC = "radius-electric";
+    public static final String RANGE_RADIUS_FUEL = "radius-fuel";
+    public static final String RANGE_RADIUS_HYBRID = "radius-hybrid";
+
+    // Last Trip
+    public static final String DURATION = "duration";
+    public static final String DISTANCE = "distance";
+    public static final String DISTANCE_SINCE_CHARGING = "distance-since-charging";
+    public static final String AVG_CONSUMPTION = "avg-consumption";
+    public static final String AVG_COMBINED_CONSUMPTION = "avg-combined-consumption";
+    public static final String AVG_RECUPERATION = "avg-recuperation";
+
+    // Lifetime + Average Consumptions
+    public static final String TOTAL_DRIVEN_DISTANCE = "total-driven-distance";
+    public static final String SINGLE_LONGEST_DISTANCE = "single-longest-distance";
+
+    // Image
+    public static final String IMAGE_FORMAT = "png";
+    public static final String IMAGE_VIEWPORT = "view";
+    public static final String IMAGE_SIZE = "size";
+
+    // Remote Services
+    public static final String REMOTE_SERVICE_LIGHT_FLASH = "light";
+    public static final String REMOTE_SERVICE_VEHICLE_FINDER = "finder";
+    public static final String REMOTE_SERVICE_DOOR_LOCK = "lock";
+    public static final String REMOTE_SERVICE_DOOR_UNLOCK = "unlock";
+    public static final String REMOTE_SERVICE_HORN = "horn";
+    public static final String REMOTE_SERVICE_AIR_CONDITIONING = "climate";
+    public static final String REMOTE_SERVICE_CHARGE_NOW = "charge-now";
+    public static final String REMOTE_SERVICE_CHARGING_CONTROL = "charge-control";
+    public static final String REMOTE_SERVICE_COMMAND = "command";
+    public static final String REMOTE_STATE = "state";
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveHandlerFactory.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveHandlerFactory.java
new file mode 100644 (file)
index 0000000..11f2b01
--- /dev/null
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.BMWConnectedDriveOptionProvider;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.ConnectedDriveBridgeHandler;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.VehicleHandler;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.core.i18n.LocaleProvider;
+import org.openhab.core.i18n.TimeZoneProvider;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link ConnectedDriveHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.bmwconnecteddrive", service = ThingHandlerFactory.class)
+public class ConnectedDriveHandlerFactory extends BaseThingHandlerFactory {
+
+    private final HttpClientFactory httpClientFactory;
+    private final BMWConnectedDriveOptionProvider optionProvider;
+    private boolean imperial = false;
+
+    @Activate
+    public ConnectedDriveHandlerFactory(final @Reference HttpClientFactory hcf,
+            final @Reference BMWConnectedDriveOptionProvider op, final @Reference LocaleProvider lp,
+            final @Reference TimeZoneProvider timeZoneProvider) {
+        httpClientFactory = hcf;
+        optionProvider = op;
+        imperial = IMPERIAL_COUNTRIES.contains(lp.getLocale().getCountry());
+        Converter.setTimeZoneProvider(timeZoneProvider);
+    }
+
+    @Override
+    public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return SUPPORTED_THING_SET.contains(thingTypeUID);
+    }
+
+    @Override
+    protected @Nullable ThingHandler createHandler(Thing thing) {
+        ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+        if (THING_TYPE_CONNECTED_DRIVE_ACCOUNT.equals(thingTypeUID)) {
+            return new ConnectedDriveBridgeHandler((Bridge) thing, httpClientFactory);
+        } else if (SUPPORTED_THING_SET.contains(thingTypeUID)) {
+            VehicleHandler vh = new VehicleHandler(thing, optionProvider, thingTypeUID.getId(), imperial);
+            return vh;
+        }
+        return null;
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/VehicleConfiguration.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/VehicleConfiguration.java
new file mode 100644 (file)
index 0000000..d0af7f7
--- /dev/null
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+/**
+ * The {@link VehicleConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class VehicleConfiguration {
+    /**
+     * Vehicle Identification Number (VIN)
+     */
+    public String vin = Constants.EMPTY;
+
+    /**
+     * Data refresh rate in minutes
+     */
+    public int refreshInterval = ConnectedDriveConstants.DEFAULT_REFRESH_INTERVAL_MINUTES;
+
+    /**
+     * Either Auto Detect Miles units (UK & US) or select Format directly
+     * <option value="AUTODETECT">Auto Detect</option>
+     * <option value="METRIC">Metric</option>
+     * <option value="IMPERIAL">Imperial</option>
+     */
+    public String units = ConnectedDriveConstants.UNITS_AUTODETECT;
+
+    /**
+     * image size - width & length (square)
+     */
+    public int imageSize = ConnectedDriveConstants.DEFAULT_IMAGE_SIZE_PX;
+
+    /**
+     * image viewport defined as options in thing xml
+     * <option value="FRONT">Front</option>
+     * <option value="REAR">Rear</option>
+     * <option value="SIDE">Slide</option>
+     * <option value="DASHBOARD">Dashboard</option>
+     * <option value="DRIVERDOOR">Driver Door</option>
+     */
+    public String imageViewport = ConnectedDriveConstants.DEFAULT_IMAGE_VIEWPORT;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/action/BMWConnectedDriveActions.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/action/BMWConnectedDriveActions.java
new file mode 100644 (file)
index 0000000..10884dd
--- /dev/null
@@ -0,0 +1,406 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.action;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper.ProfileKey.*;
+
+import java.time.DayOfWeek;
+import java.time.LocalTime;
+import java.util.Optional;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.VehicleHandler;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper.ProfileKey;
+import org.openhab.core.automation.annotation.ActionInput;
+import org.openhab.core.automation.annotation.ActionOutput;
+import org.openhab.core.automation.annotation.RuleAction;
+import org.openhab.core.thing.binding.ThingActions;
+import org.openhab.core.thing.binding.ThingActionsScope;
+import org.openhab.core.thing.binding.ThingHandler;
+
+/**
+ * The {@link BMWConnectedDriveActions} provides actions for VehicleHandler
+ *
+ * @author Norbert Truchsess - Initial contribution
+ */
+@ThingActionsScope(name = "bmwconnecteddrive")
+@NonNullByDefault
+public class BMWConnectedDriveActions implements ThingActions {
+
+    private Optional<VehicleHandler> handler = Optional.empty();
+
+    private Optional<ChargeProfileWrapper> profile = Optional.empty();
+
+    @RuleAction(label = "getTimer1Departure", description = "returns the departure time of timer1")
+    public @ActionOutput(name = "time", type = "java.util.Optional<java.time.LocalTime>") Optional<LocalTime> getTimer1Departure() {
+        return getTime(TIMER1);
+    }
+
+    @RuleAction(label = "setTimer1Departure", description = "sets the timer1 departure time")
+    public void setTimer1Departure(@ActionInput(name = "time", type = "java.time.LocalTime") @Nullable LocalTime time) {
+        setTime(TIMER1, time);
+    }
+
+    @RuleAction(label = "getTimer1Enabled", description = "returns the enabled state of timer1")
+    public @ActionOutput(name = "enabled", type = "java.util.Optional<java.lang.Boolean>") Optional<Boolean> getTimer1Enabled() {
+        return getEnabled(TIMER1);
+    }
+
+    @RuleAction(label = "setTimer1Enabled", description = "sets the enabled state of timer1")
+    public void setTimer1Enabled(@ActionInput(name = "enabled", type = "java.lang.Boolean") @Nullable Boolean enabled) {
+        setEnabled(TIMER1, enabled);
+    }
+
+    @RuleAction(label = "getTimer2Departure", description = "returns the departure time of timer2")
+    public @ActionOutput(name = "time", type = "java.util.Optional<java.time.LocalTime>") Optional<LocalTime> getTimer2Departure() {
+        return getTime(TIMER2);
+    }
+
+    @RuleAction(label = "setTimer2Departure", description = "sets the timer2 departure time")
+    public void setTimer2Departure(@ActionInput(name = "time", type = "java.time.LocalTime") @Nullable LocalTime time) {
+        setTime(TIMER2, time);
+    }
+
+    @RuleAction(label = "getTimer2Enabled", description = "returns the enabled state of timer2")
+    public @ActionOutput(name = "enabled", type = "java.util.Optional<java.lang.Boolean>") Optional<Boolean> getTimer2Enabled() {
+        return getEnabled(TIMER2);
+    }
+
+    @RuleAction(label = "setTimer2Enabled", description = "sets the enabled state of timer2")
+    public void setTimer2Enabled(@ActionInput(name = "enabled", type = "java.lang.Boolean") @Nullable Boolean enabled) {
+        setEnabled(TIMER2, enabled);
+    }
+
+    @RuleAction(label = "getTimer3Departure", description = "returns the departure time of timer3")
+    public @ActionOutput(name = "time", type = "java.util.Optional<java.time.LocalTime>") Optional<LocalTime> getTimer3Departure() {
+        return getTime(TIMER3);
+    }
+
+    @RuleAction(label = "setTimer3Departure", description = "sets the timer3 departure time")
+    public void setTimer3Departure(@ActionInput(name = "time", type = "java.time.LocalTime") @Nullable LocalTime time) {
+        setTime(TIMER3, time);
+    }
+
+    @RuleAction(label = "getTimer3Enabled", description = "returns the enabled state of timer3")
+    public @ActionOutput(name = "enabled", type = "java.util.Optional<java.lang.Boolean>") Optional<Boolean> getTimer3Enabled() {
+        return getEnabled(TIMER3);
+    }
+
+    @RuleAction(label = "setTimer3Enabled", description = "sets the enabled state of timer3")
+    public void setTimer3Enabled(@ActionInput(name = "enabled", type = "java.lang.Boolean") @Nullable Boolean enabled) {
+        setEnabled(TIMER3, enabled);
+    }
+
+    @RuleAction(label = "getOverrideTimerDeparture", description = "returns the departure time of overrideTimer")
+    public @ActionOutput(name = "time", type = "java.util.Optional<java.time.LocalTime>") Optional<LocalTime> getOverrideTimerDeparture() {
+        return getTime(OVERRIDE);
+    }
+
+    @RuleAction(label = "setOverrideTimerDeparture", description = "sets the overrideTimer departure time")
+    public void setOverrideTimerDeparture(
+            @ActionInput(name = "time", type = "java.time.LocalTime") @Nullable LocalTime time) {
+        setTime(OVERRIDE, time);
+    }
+
+    @RuleAction(label = "getOverrideTimerEnabled", description = "returns the enabled state of overrideTimer")
+    public @ActionOutput(name = "enabled", type = "java.util.Optional<java.lang.Boolean>") Optional<Boolean> getOverrideTimerEnabled() {
+        return getEnabled(OVERRIDE);
+    }
+
+    @RuleAction(label = "setOverrideTimerEnabled", description = "sets the enabled state of overrideTimer")
+    public void setOverrideTimerEnabled(
+            @ActionInput(name = "enabled", type = "java.lang.Boolean") @Nullable Boolean enabled) {
+        setEnabled(OVERRIDE, enabled);
+    }
+
+    @RuleAction(label = "getPreferredWindowStart", description = "returns the preferred charging-window start time")
+    public @ActionOutput(name = "time", type = "java.util.Optional<java.time.LocalTime>") Optional<LocalTime> getPreferredWindowStart() {
+        return getTime(WINDOWSTART);
+    }
+
+    @RuleAction(label = "setPreferredWindowStart", description = "sets the preferred charging-window start time")
+    public void setPreferredWindowStart(
+            @ActionInput(name = "time", type = "java.time.LocalTime") @Nullable LocalTime time) {
+        setTime(WINDOWSTART, time);
+    }
+
+    @RuleAction(label = "getPreferredWindowEnd", description = "returns the preferred charging-window end time")
+    public @ActionOutput(name = "time", type = "java.util.Optional<java.time.LocalTime>") Optional<LocalTime> getPreferredWindowEnd() {
+        return getTime(WINDOWEND);
+    }
+
+    @RuleAction(label = "setPreferredWindowEnd", description = "sets the preferred charging-window end time")
+    public void setPreferredWindowEnd(
+            @ActionInput(name = "time", type = "java.time.LocalTime") @Nullable LocalTime time) {
+        setTime(WINDOWEND, time);
+    }
+
+    @RuleAction(label = "getClimatizationEnabled", description = "returns the enabled state of climatization")
+    public @ActionOutput(name = "enabled", type = "java.util.Optional<java.lang.Boolean>") Optional<Boolean> getClimatizationEnabled() {
+        return getEnabled(CLIMATE);
+    }
+
+    @RuleAction(label = "setClimatizationEnabled", description = "sets the enabled state of climatization")
+    public void setClimatizationEnabled(
+            @ActionInput(name = "enabled", type = "java.lang.Boolean") @Nullable Boolean enabled) {
+        setEnabled(CLIMATE, enabled);
+    }
+
+    @RuleAction(label = "getChargingMode", description = "gets the charging-mode")
+    public @ActionOutput(name = "mode", type = "java.util.Optional<java.lang.String>") Optional<String> getChargingMode() {
+        return getProfile().map(profile -> profile.getMode());
+    }
+
+    @RuleAction(label = "setChargingMode", description = "sets the charging-mode")
+    public void setChargingMode(@ActionInput(name = "mode", type = "java.lang.String") @Nullable String mode) {
+        getProfile().ifPresent(profile -> profile.setMode(mode));
+    }
+
+    @RuleAction(label = "getTimer1Days", description = "returns the days of week timer1 is enabled for")
+    public @ActionOutput(name = "days", type = "java.util.Optional<java.util.Set<java.time.DayOfWeek>>") Optional<Set<DayOfWeek>> getTimer1Days() {
+        return getDays(TIMER1);
+    }
+
+    @RuleAction(label = "setTimer1Days", description = "sets the days of week timer1 is enabled for")
+    public void setTimer1Days(
+            @ActionInput(name = "days", type = "java.util.Set<java.time.DayOfWeek>") @Nullable Set<DayOfWeek> days) {
+        setDays(TIMER1, days);
+    }
+
+    @RuleAction(label = "getTimer2Days", description = "returns the days of week timer2 is enabled for")
+    public @ActionOutput(name = "days", type = "java.util.Optional<java.util.Set<java.time.DayOfWeek>>") Optional<Set<DayOfWeek>> getTimer2Days() {
+        return getDays(TIMER2);
+    }
+
+    @RuleAction(label = "setTimer2Days", description = "sets the days of week timer2 is enabled for")
+    public void setTimer2Days(
+            @ActionInput(name = "days", type = "java.util.Set<java.time.DayOfWeek>") @Nullable Set<DayOfWeek> days) {
+        setDays(TIMER2, days);
+    }
+
+    @RuleAction(label = "getTimer3Days", description = "returns the days of week timer3 is enabled for")
+    public @ActionOutput(name = "days", type = "java.util.Optional<java.util.Set<java.time.DayOfWeek>>") Optional<Set<DayOfWeek>> getTimer3Days() {
+        return getDays(TIMER3);
+    }
+
+    @RuleAction(label = "setTimer3Days", description = "sets the days of week timer3 is enabled for")
+    public void setTimer3Days(
+            @ActionInput(name = "days", type = "java.util.Set<java.time.DayOfWeek>") @Nullable Set<DayOfWeek> days) {
+        setDays(TIMER3, days);
+    }
+
+    @RuleAction(label = "getOverrideTimerDays", description = "returns the days of week the overrideTimer is enabled for")
+    public @ActionOutput(name = "days", type = "java.util.Optional<java.util.Set<java.time.DayOfWeek>>") Optional<Set<DayOfWeek>> getOverrideTimerDays() {
+        return getDays(OVERRIDE);
+    }
+
+    @RuleAction(label = "setOverrideTimerDays", description = "sets the days of week the overrideTimer is enabled for")
+    public void setOverrideTimerDays(
+            @ActionInput(name = "days", type = "java.util.Set<java.time.DayOfWeek>") @Nullable Set<DayOfWeek> days) {
+        setDays(OVERRIDE, days);
+    }
+
+    @RuleAction(label = "sendChargeProfile", description = "sends the charging profile to the vehicle")
+    public void sendChargeProfile() {
+        handler.ifPresent(handle -> handle.sendChargeProfile(getProfile()));
+    }
+
+    @RuleAction(label = "cancel", description = "cancel current edit of charging profile")
+    public void cancelEditChargeProfile() {
+        profile = Optional.empty();
+    }
+
+    public static Optional<LocalTime> getTimer1Departure(ThingActions actions) {
+        return ((BMWConnectedDriveActions) actions).getTimer1Departure();
+    }
+
+    public static void setTimer1Departure(ThingActions actions, @Nullable LocalTime time) {
+        ((BMWConnectedDriveActions) actions).setTimer1Departure(time);
+    }
+
+    public static Optional<Boolean> getTimer1Enabled(ThingActions actions) {
+        return ((BMWConnectedDriveActions) actions).getTimer1Enabled();
+    }
+
+    public static void setTimer1Enabled(ThingActions actions, @Nullable Boolean enabled) {
+        ((BMWConnectedDriveActions) actions).setTimer1Enabled(enabled);
+    }
+
+    public static Optional<LocalTime> getTimer2Departure(ThingActions actions) {
+        return ((BMWConnectedDriveActions) actions).getTimer2Departure();
+    }
+
+    public static void setTimer2Departure(ThingActions actions, @Nullable LocalTime time) {
+        ((BMWConnectedDriveActions) actions).setTimer2Departure(time);
+    }
+
+    public static Optional<Boolean> getTimer2Enabled(ThingActions actions) {
+        return ((BMWConnectedDriveActions) actions).getTimer2Enabled();
+    }
+
+    public static void setTimer2Enabled(ThingActions actions, @Nullable Boolean enabled) {
+        ((BMWConnectedDriveActions) actions).setTimer2Enabled(enabled);
+    }
+
+    public static Optional<LocalTime> getTimer3Departure(ThingActions actions) {
+        return ((BMWConnectedDriveActions) actions).getTimer3Departure();
+    }
+
+    public static void setTimer3Departure(ThingActions actions, @Nullable LocalTime time) {
+        ((BMWConnectedDriveActions) actions).setTimer3Departure(time);
+    }
+
+    public static Optional<Boolean> getTimer3Enabled(ThingActions actions) {
+        return ((BMWConnectedDriveActions) actions).getTimer3Enabled();
+    }
+
+    public static void setTimer3Enabled(ThingActions actions, @Nullable Boolean enabled) {
+        ((BMWConnectedDriveActions) actions).setTimer3Enabled(enabled);
+    }
+
+    public static Optional<LocalTime> getOverrideTimerDeparture(ThingActions actions) {
+        return ((BMWConnectedDriveActions) actions).getOverrideTimerDeparture();
+    }
+
+    public static void setOverrideTimerDeparture(ThingActions actions, @Nullable LocalTime time) {
+        ((BMWConnectedDriveActions) actions).setOverrideTimerDeparture(time);
+    }
+
+    public static Optional<Boolean> getOverrideTimerEnabled(ThingActions actions) {
+        return ((BMWConnectedDriveActions) actions).getOverrideTimerEnabled();
+    }
+
+    public static void setOverrideTimerEnabled(ThingActions actions, @Nullable Boolean enabled) {
+        ((BMWConnectedDriveActions) actions).setOverrideTimerEnabled(enabled);
+    }
+
+    public static Optional<LocalTime> getPreferredWindowStart(ThingActions actions) {
+        return ((BMWConnectedDriveActions) actions).getPreferredWindowStart();
+    }
+
+    public static void setPreferredWindowStart(ThingActions actions, @Nullable LocalTime time) {
+        ((BMWConnectedDriveActions) actions).setPreferredWindowStart(time);
+    }
+
+    public static Optional<LocalTime> getPreferredWindowEnd(ThingActions actions) {
+        return ((BMWConnectedDriveActions) actions).getPreferredWindowEnd();
+    }
+
+    public static void setPreferredWindowEnd(ThingActions actions, @Nullable LocalTime time) {
+        ((BMWConnectedDriveActions) actions).setPreferredWindowEnd(time);
+    }
+
+    public static Optional<Boolean> getClimatizationEnabled(ThingActions actions) {
+        return ((BMWConnectedDriveActions) actions).getClimatizationEnabled();
+    }
+
+    public static void setClimatizationEnabled(ThingActions actions, @Nullable Boolean enabled) {
+        ((BMWConnectedDriveActions) actions).setClimatizationEnabled(enabled);
+    }
+
+    public static Optional<String> getChargingMode(ThingActions actions) {
+        return ((BMWConnectedDriveActions) actions).getChargingMode();
+    }
+
+    public static void setChargingMode(ThingActions actions, @Nullable String mode) {
+        ((BMWConnectedDriveActions) actions).setChargingMode(mode);
+    }
+
+    public static Optional<Set<DayOfWeek>> getTimer1Days(ThingActions actions) {
+        return ((BMWConnectedDriveActions) actions).getTimer1Days();
+    }
+
+    public static void setTimer1Days(ThingActions actions, @Nullable Set<DayOfWeek> days) {
+        ((BMWConnectedDriveActions) actions).setTimer1Days(days);
+    }
+
+    public static Optional<Set<DayOfWeek>> getTimer2Days(ThingActions actions) {
+        return ((BMWConnectedDriveActions) actions).getTimer2Days();
+    }
+
+    public static void setTimer2Days(ThingActions actions, @Nullable Set<DayOfWeek> days) {
+        ((BMWConnectedDriveActions) actions).setTimer2Days(days);
+    }
+
+    public static Optional<Set<DayOfWeek>> getTimer3Days(ThingActions actions) {
+        return ((BMWConnectedDriveActions) actions).getTimer3Days();
+    }
+
+    public static void setTimer3Days(ThingActions actions, @Nullable Set<DayOfWeek> days) {
+        ((BMWConnectedDriveActions) actions).setTimer3Days(days);
+    }
+
+    public static Optional<Set<DayOfWeek>> getOverrideTimerDays(ThingActions actions) {
+        return ((BMWConnectedDriveActions) actions).getOverrideTimerDays();
+    }
+
+    public static void setOverrideTimerDays(ThingActions actions, @Nullable Set<DayOfWeek> days) {
+        ((BMWConnectedDriveActions) actions).setOverrideTimerDays(days);
+    }
+
+    public static void sendChargeProfile(ThingActions actions) {
+        ((BMWConnectedDriveActions) actions).sendChargeProfile();
+    }
+
+    public static void cancelEditChargeProfile(ThingActions actions) {
+        ((BMWConnectedDriveActions) actions).cancelEditChargeProfile();
+    }
+
+    @Override
+    public void setThingHandler(@Nullable ThingHandler handler) {
+        if (handler instanceof VehicleHandler) {
+            this.handler = Optional.of((VehicleHandler) handler);
+        }
+    }
+
+    @Override
+    public @Nullable ThingHandler getThingHandler() {
+        return handler.get();
+    }
+
+    private Optional<ChargeProfileWrapper> getProfile() {
+        if (profile.isEmpty()) {
+            profile = handler.flatMap(handle -> handle.getChargeProfileWrapper());
+        }
+        return profile;
+    }
+
+    private Optional<LocalTime> getTime(ProfileKey key) {
+        return getProfile().map(profile -> profile.getTime(key));
+    }
+
+    private void setTime(ProfileKey key, @Nullable LocalTime time) {
+        getProfile().ifPresent(profile -> profile.setTime(key, time));
+    }
+
+    private Optional<Boolean> getEnabled(ProfileKey key) {
+        return getProfile().map(profile -> profile.isEnabled(key));
+    }
+
+    private void setEnabled(ProfileKey key, @Nullable Boolean enabled) {
+        getProfile().ifPresent(profile -> profile.setEnabled(key, enabled));
+    }
+
+    private Optional<Set<DayOfWeek>> getDays(ProfileKey key) {
+        return getProfile().map(profile -> profile.getDays(key));
+    }
+
+    private void setDays(ProfileKey key, @Nullable Set<DayOfWeek> days) {
+        getProfile().ifPresent(profile -> {
+            profile.setDays(key, days);
+        });
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/discovery/VehicleDiscovery.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/discovery/VehicleDiscovery.java
new file mode 100644 (file)
index 0000000..55c66b5
--- /dev/null
@@ -0,0 +1,181 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.discovery;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.SUPPORTED_THING_SET;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.discovery.VehiclesContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.ConnectedDriveBridgeHandler;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link VehicleDiscovery} requests data from ConnectedDrive and is identifying the Vehicles after response
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class VehicleDiscovery extends AbstractDiscoveryService implements DiscoveryService, ThingHandlerService {
+
+    private final Logger logger = LoggerFactory.getLogger(VehicleDiscovery.class);
+    private static final int DISCOVERY_TIMEOUT = 10;
+    private Optional<ConnectedDriveBridgeHandler> bridgeHandler = Optional.empty();
+
+    public VehicleDiscovery() {
+        super(SUPPORTED_THING_SET, DISCOVERY_TIMEOUT, false);
+    }
+
+    public void onResponse(VehiclesContainer container) {
+        bridgeHandler.ifPresent(bridge -> {
+            final ThingUID bridgeUID = bridge.getThing().getUID();
+            container.vehicles.forEach(vehicle -> {
+                // the DriveTrain field in the delivered json is defining the Vehicle Type
+                String vehicleType = vehicle.driveTrain.toLowerCase();
+                SUPPORTED_THING_SET.forEach(entry -> {
+                    if (entry.getId().equals(vehicleType)) {
+                        ThingUID uid = new ThingUID(entry, vehicle.vin, bridgeUID.getId());
+                        Map<String, String> properties = new HashMap<>();
+                        // Dealer
+                        if (vehicle.dealer != null) {
+                            properties.put("dealer", vehicle.dealer.name);
+                            properties.put("dealerAddress", vehicle.dealer.street + " " + vehicle.dealer.country + " "
+                                    + vehicle.dealer.postalCode + " " + vehicle.dealer.city);
+                            properties.put("dealerPhone", vehicle.dealer.phone);
+                        }
+
+                        // Services & Support
+                        properties.put("servicesActivated", getObject(vehicle, Constants.ACTIVATED));
+                        String servicesSupported = getObject(vehicle, Constants.SUPPORTED);
+                        String servicesNotSupported = getObject(vehicle, Constants.NOT_SUPPORTED);
+                        if (vehicle.statisticsAvailable) {
+                            servicesSupported += Constants.STATISTICS;
+                        } else {
+                            servicesNotSupported += Constants.STATISTICS;
+                        }
+                        properties.put(Constants.SERVICES_SUPPORTED, servicesSupported);
+                        properties.put("servicesNotSupported", servicesNotSupported);
+                        properties.put("supportBreakdownNumber", vehicle.breakdownNumber);
+
+                        // Vehicle Properties
+                        if (vehicle.supportedChargingModes != null) {
+                            properties.put("vehicleChargeModes",
+                                    String.join(Constants.SPACE, vehicle.supportedChargingModes));
+                        }
+                        if (vehicle.hasAlarmSystem) {
+                            properties.put("vehicleAlarmSystem", "Available");
+                        } else {
+                            properties.put("vehicleAlarmSystem", "Not Available");
+                        }
+                        properties.put("vehicleBrand", vehicle.brand);
+                        properties.put("vehicleBodytype", vehicle.bodytype);
+                        properties.put("vehicleColor", vehicle.color);
+                        properties.put("vehicleConstructionYear", Short.toString(vehicle.yearOfConstruction));
+                        properties.put("vehicleDriveTrain", vehicle.driveTrain);
+                        properties.put("vehicleModel", vehicle.model);
+                        if (vehicle.chargingControl != null) {
+                            properties.put("vehicleChargeControl", Converter.toTitleCase(vehicle.model));
+                        }
+
+                        // Update Properties for already created Things
+                        bridge.getThing().getThings().forEach(vehicleThing -> {
+                            Configuration c = vehicleThing.getConfiguration();
+                            if (c.containsKey(ConnectedDriveConstants.VIN)) {
+                                String thingVIN = c.get(ConnectedDriveConstants.VIN).toString();
+                                if (vehicle.vin.equals(thingVIN)) {
+                                    vehicleThing.setProperties(properties);
+                                }
+                            }
+                        });
+
+                        // Properties needed for functional THing
+                        properties.put(ConnectedDriveConstants.VIN, vehicle.vin);
+                        properties.put("refreshInterval",
+                                Integer.toString(ConnectedDriveConstants.DEFAULT_REFRESH_INTERVAL_MINUTES));
+                        properties.put("units", ConnectedDriveConstants.UNITS_AUTODETECT);
+                        properties.put("imageSize", Integer.toString(ConnectedDriveConstants.DEFAULT_IMAGE_SIZE_PX));
+                        properties.put("imageViewport", ConnectedDriveConstants.DEFAULT_IMAGE_VIEWPORT);
+
+                        String vehicleLabel = vehicle.brand + " " + vehicle.model;
+                        Map<String, Object> convertedProperties = new HashMap<String, Object>(properties);
+                        thingDiscovered(DiscoveryResultBuilder.create(uid).withBridge(bridgeUID)
+                                .withRepresentationProperty(ConnectedDriveConstants.VIN).withLabel(vehicleLabel)
+                                .withProperties(convertedProperties).build());
+                    }
+                });
+            });
+        });
+    };
+
+    /**
+     * Get all field names from a DTO with a specific value
+     * Used to get e.g. all services which are "ACTIVATED"
+     *
+     * @param DTO Object
+     * @param compare String which needs to map with the value
+     * @return String with all field names matching this value separated with Spaces
+     */
+    public String getObject(Object dto, String compare) {
+        StringBuilder buf = new StringBuilder();
+        for (Field field : dto.getClass().getDeclaredFields()) {
+            try {
+                Object value = field.get(dto);
+                if (compare.equals(value)) {
+                    buf.append(Converter.capitalizeFirst(field.getName()) + Constants.SPACE);
+                }
+            } catch (IllegalArgumentException | IllegalAccessException e) {
+                logger.debug("Field {} not found {}", compare, e.getMessage());
+            }
+        }
+        return buf.toString();
+    }
+
+    @Override
+    public void setThingHandler(ThingHandler handler) {
+        if (handler instanceof ConnectedDriveBridgeHandler) {
+            bridgeHandler = Optional.of((ConnectedDriveBridgeHandler) handler);
+            bridgeHandler.get().setDiscoveryService(this);
+        }
+    }
+
+    @Override
+    public @Nullable ThingHandler getThingHandler() {
+        return bridgeHandler.orElse(null);
+    }
+
+    @Override
+    protected void startScan() {
+        bridgeHandler.ifPresent(ConnectedDriveBridgeHandler::requestVehicles);
+    }
+
+    @Override
+    public void deactivate() {
+        super.deactivate();
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/Destination.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/Destination.java
new file mode 100644 (file)
index 0000000..e9d653a
--- /dev/null
@@ -0,0 +1,60 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.utils.Constants.*;
+
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+
+/**
+ * The {@link Destination} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Destination {
+    public float lat;
+    public float lon;
+    public String country;
+    public String city;
+    public String street;
+    public String streetNumber;
+    public String type;
+    public String createdAt;
+
+    public String getAddress() {
+        StringBuilder buf = new StringBuilder();
+        if (street != null) {
+            buf.append(street);
+            if (streetNumber != null) {
+                buf.append(SPACE).append(streetNumber);
+            }
+        }
+        if (city != null) {
+            if (buf.length() > 0) {
+                buf.append(COMMA).append(SPACE).append(city);
+            } else {
+                buf.append(city);
+            }
+        }
+        if (buf.length() == 0) {
+            return UNDEF;
+        } else {
+            return Converter.toTitleCase(buf.toString());
+        }
+    }
+
+    public String getCoordinates() {
+        return lat + Constants.COMMA + lon;
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/DestinationContainer.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/DestinationContainer.java
new file mode 100644 (file)
index 0000000..882d7c0
--- /dev/null
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto;
+
+import java.util.List;
+
+/**
+ * The {@link DestinationContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class DestinationContainer {
+    public List<Destination> destinations;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/NetworkError.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/NetworkError.java
new file mode 100644 (file)
index 0000000..d751a73
--- /dev/null
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto;
+
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+
+/**
+ * The {@link NetworkError} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class NetworkError {
+    public String url;
+    public int status;
+    public String reason;
+    public String params;
+
+    @Override
+    public String toString() {
+        return new StringBuilder(url).append(Constants.HYPHEN).append(status).append(Constants.HYPHEN).append(reason)
+                .append(params).toString();
+    }
+
+    public String toJson() {
+        return Converter.getGson().toJson(this);
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/auth/AuthResponse.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/auth/AuthResponse.java
new file mode 100644 (file)
index 0000000..7363d49
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.auth;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link Timer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class AuthResponse {
+    @SerializedName("access_token")
+    public String accessToken;
+    @SerializedName("token_type")
+    public String tokenType;
+    @SerializedName("expires_in")
+    public int expiresIn;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/charge/ChargeProfile.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/charge/ChargeProfile.java
new file mode 100644 (file)
index 0000000..40da7f8
--- /dev/null
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.charge;
+
+/**
+ * The {@link ChargeProfile} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - edit & send of charge profile
+ */
+public class ChargeProfile {
+    public WeeklyPlanner weeklyPlanner;
+    public WeeklyPlanner twoTimesTimer;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/charge/ChargingWindow.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/charge/ChargingWindow.java
new file mode 100644 (file)
index 0000000..b303ddd
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.charge;
+
+/**
+ * The {@link ChargingWindow} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class ChargingWindow {
+    public String startTime;// ":"11:00",
+    public String endTime;// ":"17:00"}}
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/charge/Timer.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/charge/Timer.java
new file mode 100644 (file)
index 0000000..65a3f23
--- /dev/null
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.charge;
+
+import java.util.List;
+
+/**
+ * The {@link Timer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - edit & send of charge profile
+ */
+public class Timer {
+    public String departureTime;// ": "05:00",
+    public Boolean timerEnabled;// ": false,
+    public List<String> weekdays;
+    /**
+     * "MONDAY",
+     * "TUESDAY",
+     * "WEDNESDAY",
+     * "THURSDAY",
+     * "FRIDAY"
+     * ] '
+     */
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/charge/WeeklyPlanner.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/charge/WeeklyPlanner.java
new file mode 100644 (file)
index 0000000..c169e1a
--- /dev/null
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.charge;
+
+/**
+ * The {@link WeeklyPlanner} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - edit & send of charge profile
+ */
+public class WeeklyPlanner {
+    public Boolean climatizationEnabled; // ": true,
+    public String chargingMode;// ": "IMMEDIATE_CHARGING",
+    public String chargingPreferences; // ": "CHARGING_WINDOW",
+    public Timer timer1; // : {
+    public Timer timer2;// ": {
+    public Timer timer3;// ":{"departureTime":"00:00","timerEnabled":false,"weekdays":[]},"
+    public Timer overrideTimer;// ":{"departureTime":"12:00","timerEnabled":false,"weekdays":["SATURDAY"]},"
+    public ChargingWindow preferredChargingWindow;// ":{"startTime":"11:00","endTime":"17:00"}}
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/CBSMessageCompat.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/CBSMessageCompat.java
new file mode 100644 (file)
index 0000000..39677a3
--- /dev/null
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.compat;
+
+/**
+ * The {@link CBSMessageCompat} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CBSMessageCompat {
+    public String description; // "Nächster Wechsel spätestens zum angegebenen Termin.",
+    public String text; // "Bremsflüssigkeit",
+    public int id; // 3,
+    public String status; // "OK",
+    public String messageType; // "CBS",
+    public String date; // "2021-11"
+    public int unitOfLengthRemaining; // ": "2000"
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/CCMMessageCompat.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/CCMMessageCompat.java
new file mode 100644 (file)
index 0000000..5e0aafc
--- /dev/null
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.compat;
+
+/**
+ * The {@link CCMMessageCompat} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CCMMessageCompat {
+    public String text;// "Laden nicht möglich"
+    public int id;// 804,
+    public String status;// "NULL",
+    public String messageType;// "CCM",
+    public int unitOfLengthRemaining = -1; // "18312"
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/VehicleAttributes.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/VehicleAttributes.java
new file mode 100644 (file)
index 0000000..72cdde1
--- /dev/null
@@ -0,0 +1,142 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.compat;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link VehicleAttributes} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class VehicleAttributes {
+    // Windows & Doors
+    @SerializedName("door_driver_front")
+    public String doorDriverFront;// "CLOSED",
+    @SerializedName("door_driver_rear")
+    public String doorDriverRear;// "CLOSED",
+    @SerializedName("door_passenger_front")
+    public String doorPassengerFront;// "CLOSED",
+    @SerializedName("door_passenger_rear")
+    public String doorPassengerRear;// "CLOSED",
+    @SerializedName("hood_state")
+    public String hoodState;// "CLOSED",
+    @SerializedName("trunk_state")
+    public String trunkState;// "CLOSED",
+    @SerializedName("window_driver_front")
+    public String winDriverFront;// "CLOSED",
+    @SerializedName("window_driver_rear")
+    public String winDriverRear;// "CLOSED",
+    @SerializedName("window_passenger_front")
+    public String winPassengerFront;// "CLOSED",
+    @SerializedName("window_passenger_rear")
+    public String winPassengerRear;// "CLOSED",
+    @SerializedName("sunroof_state")
+    public String sunroofState;// "CLOSED",
+    @SerializedName("door_lock_state")
+    public String doorLockState;// "SECURED",
+    public String shdStatusUnified;// "CLOSED",
+
+    // Charge Status
+    public String chargingHVStatus;// "INVALID",
+    public String lastChargingEndReason;// "CHARGING_GOAL_REACHED",
+    public String connectorStatus;// "DISCONNECTED",
+    public String chargingLogicCurrentlyActive;// "NOT_CHARGING",
+    public String chargeNowAllowed;// "NOT_ALLOWED",
+    @SerializedName("charging_status")
+    public String chargingStatus;// "NOCHARGING",
+    public String lastChargingEndResult;// "SUCCESS",
+    public String chargingSystemStatus;// "NOCHARGING",
+    public String lastUpdateReason;// "VEHCSHUTDOWN_SECURED"
+
+    // Range
+    public int mileage;// "17236",
+    public double beMaxRangeElectric;// "209.0",
+    public double beMaxRangeElectricKm;// "209.0",
+    public double beRemainingRangeElectric;// "179.0",
+    public double beRemainingRangeElectricKm;// "179.0",
+    public double beMaxRangeElectricMile;// "129.0",
+    public double beRemainingRangeElectricMile;// "111.0",
+    public double beRemainingRangeFuelKm;// "67.0",
+    public double beRemainingRangeFuelMile;// "41.0",
+    public double beRemainingRangeFuel;// "67.0",
+    @SerializedName("kombi_current_remaining_range_fuel")
+    public double kombiRemainingRangeFuel;// "67.0",
+
+    public double chargingLevelHv;// "89.0",
+    @SerializedName("soc_hv_percent")
+    public double socHvPercent;// "82.6",
+    @SerializedName("remaining_fuel")
+    public double remainingFuel;// "4",
+    public double fuelPercent;// "47",
+
+    // Last Status update
+    public String updateTime;// "22.08.2020 12:55:46 UTC",
+    @SerializedName("updateTime_converted")
+    public String updateTimeConverted;// "22.08.2020 13:55",
+    @SerializedName("updateTime_converted_date")
+    public String updateTimeConvertedDate;// "22.08.2020",
+    @SerializedName("updateTime_converted_time")
+    public String updateTimeConvertedTime;// "13:55",
+    @SerializedName("updateTime_converted_timestamp")
+    public String updateTimeConvertedTimestamp;// "1598104546000",
+
+    // Last Trip Update
+    @SerializedName("Segment_LastTrip_time_segment_end")
+    public String lastTripEnd;// "22.08.2020 14:52:00 UTC",
+    @SerializedName("Segment_LastTrip_time_segment_end_formatted")
+    public String lastTripEndFormatted;// "22.08.2020 14:52",
+    @SerializedName("Segment_LastTrip_time_segment_end_formatted_date")
+    public String lastTripEndFormattedDate;// "22.08.2020",
+    @SerializedName("Segment_LastTrip_time_segment_end_formatted_time")
+    public String lastTripEndFormattedTime;// "14:52",
+
+    // Location
+    @SerializedName("gps_lat")
+    public float gpsLat;// "43.21",
+    @SerializedName("gps_lng")
+    public float gpsLon;// "8.765",
+    public int heading;// "41",
+
+    public String unitOfLength;// "km",
+    public String unitOfEnergy;// "kWh",
+    @SerializedName("vehicle_tracking")
+    public String vehicleTracking;// "1",
+    @SerializedName("head_unit_pu_software")
+    public String headunitSoftware;// "07/16",
+    @SerializedName("check_control_messages")
+    public String checkControlMessages;// "",
+    @SerializedName("sunroof_position")
+    public String sunroofPosition;// "0",
+    @SerializedName("single_immediate_charging")
+    public String singleImmediateCharging;// "isUnused",
+    public String unitOfCombustionConsumption;// "l/100km",
+    @SerializedName("Segment_LastTrip_ratio_electric_driven_distance")
+    public String lastTripElectricRation;// "100",
+    @SerializedName("condition_based_services")
+    public String conditionBasedServices;// "00003,OK,2021-11,;00017,OK,2021-11,;00001,OK,2021-11,;00032,OK,2021-11,",
+    @SerializedName("charging_inductive_positioning")
+    public String chargingInductivePositioning;// "not_positioned",
+    @SerializedName("lsc_trigger")
+    public String lscTrigger;// "VEHCSHUTDOWN_SECURED",
+    @SerializedName("lights_parking")
+    public String lightsParking;// "OFF",
+    public String prognosisWhileChargingStatus;// "NOT_NEEDED",
+    @SerializedName("head_unit")
+    public String headunit;// "EntryNav",
+    @SerializedName("battery_size_max")
+    public String batterySizeMax;// "33200",
+    @SerializedName("charging_connection_type")
+    public String chargingConnectionType;// "CONDUCTIVE",
+    public String unitOfElectricConsumption;// "kWh/100km",
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/VehicleAttributesContainer.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/VehicleAttributesContainer.java
new file mode 100644 (file)
index 0000000..3e8087f
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.compat;
+
+/**
+ * The {@link VehicleAttributesContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class VehicleAttributesContainer {
+    public VehicleAttributes attributesMap;
+    public VehicleMessages vehicleMessages;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/VehicleMessages.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/compat/VehicleMessages.java
new file mode 100644 (file)
index 0000000..7b76135
--- /dev/null
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.compat;
+
+import java.util.List;
+
+/**
+ * The {@link VehicleMessages} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @param <CBSMessage>
+ */
+public class VehicleMessages {
+    public List<CCMMessageCompat> ccmMessages;
+    public List<CBSMessageCompat> cbsMessages;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/discovery/Dealer.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/discovery/Dealer.java
new file mode 100644 (file)
index 0000000..1b1392b
--- /dev/null
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.discovery;
+
+/**
+ * The {@link Dealer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Dealer {
+    public String name;
+    public String street;
+    public String postalCode;
+    public String city;
+    public String country;
+    public String phone;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/discovery/Vehicle.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/discovery/Vehicle.java
new file mode 100644 (file)
index 0000000..f963b7d
--- /dev/null
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.discovery;
+
+import java.util.List;
+
+/**
+ * The {@link Vehicle} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Vehicle {
+    public String vin;
+    public String model;
+    public String driveTrain;
+    public String brand;
+    public short yearOfConstruction;
+    public String bodytype;
+    public String color;
+    public boolean statisticsCommunityEnabled;
+    public boolean statisticsAvailable;
+    public boolean hasAlarmSystem;
+    public Dealer dealer;
+    public String breakdownNumber;
+    public List<String> supportedChargingModes;
+    public String chargingControl;// ": "WEEKLY_PLANNER",
+
+    // Remote Services
+    public String vehicleFinder; // ACTIVATED
+    public String hornBlow; // ACTIVATED
+    public String lightFlash; // ACTIVATED
+    public String doorLock; // ACTIVATED
+    public String doorUnlock; // ACTIVATED
+    public String climateNow; // ACTIVATED
+    public String sendPoi; // ACTIVATED
+
+    public String remote360; // SUPPORTED
+    public String climateControl; // SUPPORTED
+    public String chargeNow; // SUPPORTED
+    public String lastDestinations; // SUPPORTED
+    public String carCloud; // SUPPORTED
+    public String remoteSoftwareUpgrade; // SUPPORTED
+
+    public String climateNowRES;// ": "NOT_SUPPORTED",
+    public String climateControlRES;// ": "NOT_SUPPORTED",
+    public String smartSolution;// ": "NOT_SUPPORTED",
+    public String ipa;// ": "NOT_SUPPORTED",
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/discovery/VehiclesContainer.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/discovery/VehiclesContainer.java
new file mode 100644 (file)
index 0000000..9c7a1a7
--- /dev/null
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.discovery;
+
+import java.util.List;
+
+/**
+ * The {@link VehiclesContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class VehiclesContainer {
+    public List<Vehicle> vehicles;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/remote/ExecutionStatus.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/remote/ExecutionStatus.java
new file mode 100644 (file)
index 0000000..4f80ac7
--- /dev/null
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.remote;
+
+/**
+ * The {@link ExecutionStatus} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class ExecutionStatus {
+    public String serviceType;// ": "DOOR_UNLOCK",
+    public String status;// ": "EXECUTED",
+    public String eventId;// ": "5639303536333926DA7B9400@bmw.de",
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/remote/ExecutionStatusContainer.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/remote/ExecutionStatusContainer.java
new file mode 100644 (file)
index 0000000..eca56f1
--- /dev/null
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.remote;
+
+/**
+ * The {@link ExecutionStatusContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class ExecutionStatusContainer {
+    public ExecutionStatus executionStatus;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/AllTrips.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/AllTrips.java
new file mode 100644 (file)
index 0000000..c2417e5
--- /dev/null
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.statistics;
+
+/**
+ * The {@link AllTrips} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class AllTrips {
+    public CommunityPowerEntry avgElectricConsumption;
+    public CommunityPowerEntry avgRecuperation;
+    public CommunityChargeCycleEntry chargecycleRange;
+    public CommunityEletricDistanceEntry totalElectricDistance;
+    public CommunityPowerEntry avgCombinedConsumption;
+    public float savedCO2;// ":461.083,"
+    public float savedCO2greenEnergy;// ":2712.255,"
+    public float totalSavedFuel;// ":0,"
+    public String resetDate;// ":"2020-08-24T14:40:40+0000","
+    public int batterySizeMax;// ":33200
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/AllTripsContainer.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/AllTripsContainer.java
new file mode 100644 (file)
index 0000000..607b79b
--- /dev/null
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.statistics;
+
+/**
+ * The {@link AllTripsContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class AllTripsContainer {
+    public AllTrips allTrips;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/CommunityChargeCycleEntry.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/CommunityChargeCycleEntry.java
new file mode 100644 (file)
index 0000000..a2e4b6a
--- /dev/null
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.statistics;
+
+/**
+ * The {@link CommunityChargeCycleEntry} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CommunityChargeCycleEntry {
+    public float communityAverage;// ": 194.21,
+    public float communityHigh;// ": 270,
+    public float userAverage;// ": 57.3,
+    public float userHigh;// ": 185.48,
+    public float userCurrentChargeCycle;// ": 68
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/CommunityEletricDistanceEntry.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/CommunityEletricDistanceEntry.java
new file mode 100644 (file)
index 0000000..6c525fd
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.statistics;
+
+/**
+ * The {@link CommunityEletricDistanceEntry} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CommunityEletricDistanceEntry {
+    public float communityLow;// ": 19,
+    public float communityAverage;// ": 40850.56,
+    public float communityHigh;// ": 193006,
+    public float userTotal;// ": 16629.4
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/CommunityPowerEntry.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/CommunityPowerEntry.java
new file mode 100644 (file)
index 0000000..2e4f57e
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.statistics;
+
+/**
+ * The {@link CommunityPowerEntry} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CommunityPowerEntry {
+    public float communityLow;// ": 11.05,
+    public float communityAverage;// ": 16.28,
+    public float communityHigh;// ": 21.99,
+    public float userAverage;// ": 16.46
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/LastTrip.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/LastTrip.java
new file mode 100644 (file)
index 0000000..69f7106
--- /dev/null
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.statistics;
+
+/**
+ * The {@link LastTrip} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class LastTrip {
+    public float efficiencyValue;// ": 0.98,
+    public float totalDistance;// ": 2,
+    public float electricDistance;// ": 2,
+    public float avgElectricConsumption;// ": 7,
+    public float avgRecuperation;// ": 6,
+    public float drivingModeValue;// ": 0.87,
+    public float accelerationValue;// ": 0.99,
+    public float anticipationValue;// ": 0.99,
+    public float totalConsumptionValue;// ": 1.25,
+    public float auxiliaryConsumptionValue;// ": 0.78,
+    public float avgCombinedConsumption;// ": 0,
+    public float electricDistanceRatio;// ": 100,
+    public float savedFuel;// ": 0,
+    public String date;// ": "2020-08-24T17:55:00+0000",
+    public float duration;// ": 5
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/LastTripContainer.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/statistics/LastTripContainer.java
new file mode 100644 (file)
index 0000000..86e46c9
--- /dev/null
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.statistics;
+
+/**
+ * The {@link LastTripContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class LastTripContainer {
+    public LastTrip lastTrip;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/CBSMessage.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/CBSMessage.java
new file mode 100644 (file)
index 0000000..14e127a
--- /dev/null
@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.status;
+
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+
+/**
+ * The {@link CBSMessage} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CBSMessage {
+    public String cbsType;// ": "BRAKE_FLUID",
+    public String cbsState;// ": "OK",
+    public String cbsDueDate;// ": "2021-11",
+    public String cbsDescription;// ": "Next change due at the latest by the stated date."
+    public int cbsRemainingMileage = -1; // 46000
+
+    public String cbsTypeConverted = null;
+    public String cbsDescriptionConverted = null;
+
+    public String getDueDate() {
+        if (cbsDueDate == null) {
+            return Constants.NULL_DATE;
+        } else {
+            return cbsDueDate + Constants.UTC_APPENDIX;
+        }
+    }
+
+    public String getType() {
+        if (cbsTypeConverted == null) {
+            if (cbsType == null) {
+                cbsTypeConverted = Constants.INVALID;
+            } else {
+                cbsTypeConverted = Converter.toTitleCase(cbsType);
+            }
+        }
+        return cbsTypeConverted;
+    }
+
+    public String getDescription() {
+        if (cbsDescriptionConverted == null) {
+            if (cbsDescription == null) {
+                cbsDescriptionConverted = Constants.INVALID;
+            } else {
+                cbsDescriptionConverted = cbsDescription;
+            }
+        }
+        return cbsDescriptionConverted;
+    }
+
+    @Override
+    public String toString() {
+        return new StringBuilder(cbsDueDate).append(Constants.HYPHEN).append(cbsRemainingMileage)
+                .append(Constants.HYPHEN).append(cbsType).toString();
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/CCMMessage.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/CCMMessage.java
new file mode 100644 (file)
index 0000000..4d9a43d
--- /dev/null
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.status;
+
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+/**
+ * The {@link CCMMessage} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class CCMMessage {
+    // if necessary. Perform reset after adjustment. See Owner's Handbook for further
+    // information.",
+    public String ccmDescriptionShort = Constants.INVALID;// ": "Tyre pressure notification",
+    public String ccmDescriptionLong = Constants.INVALID;// ": "You can continue driving. Check tyre pressure when tyres
+    // are cold and adjust
+    public int ccmId = -1;// ": 955,
+    public int ccmMileage = -1;// ": 41544
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/Doors.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/Doors.java
new file mode 100644 (file)
index 0000000..afab4e2
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.status;
+
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+/**
+ * The {@link Doors} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Doors {
+    public String doorDriverFront = Constants.UNDEF;// ": "CLOSED",
+    public String doorDriverRear = Constants.UNDEF;// ": "CLOSED",
+    public String doorPassengerFront = Constants.UNDEF;// ": "CLOSED",
+    public String doorPassengerRear = Constants.UNDEF;// ": "CLOSED",
+    public String trunk = Constants.UNDEF;// ": "CLOSED",
+    public String hood = Constants.UNDEF;// ": "CLOSED",
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/Position.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/Position.java
new file mode 100644 (file)
index 0000000..ee46dd3
--- /dev/null
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.status;
+
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+/**
+ * The {@link Position} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Position {
+    public float lat;// ": 46.55605,
+    public float lon;// ": 10.495669,
+    public int heading;// ": 219,
+    public String status;// ": "OK"
+
+    public String getCoordinates() {
+        return new StringBuilder(Float.toString(lat)).append(Constants.COMMA).append(Float.toString(lon)).toString();
+    }
+
+    @Override
+    public String toString() {
+        return getCoordinates();
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/VehicleStatus.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/VehicleStatus.java
new file mode 100644 (file)
index 0000000..57119de
--- /dev/null
@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.status;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link VehicleStatus} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class VehicleStatus {
+    public int mileage = Constants.INT_UNDEF;// ": 17273,
+    public double remainingFuel = Constants.INT_UNDEF;// ": 4,
+    public double remainingRangeElectric = Constants.INT_UNDEF;// ": 148,
+    public double remainingRangeElectricMls;// ": 91,
+    public double remainingRangeFuel = Constants.INT_UNDEF;// ": 70,"
+    public double remainingRangeFuelMls;// ":43,"
+    public double maxRangeElectric = Constants.INT_UNDEF;// ":216,"
+    public double maxRangeElectricMls;// ":134,"
+    public double maxFuel;// ":8.5,
+    public double chargingLevelHv;// ":71,
+    public String vin;// : "ANONYMOUS",
+    public String updateReason;// ": "VEHICLE_SHUTDOWN_SECURED",
+    public String updateTime;// ": "2020-08-24 T15:55:32+0000",
+    public String doorDriverFront = Constants.UNDEF;// ": "CLOSED",
+    public String doorDriverRear = Constants.UNDEF;// ": "CLOSED",
+    public String doorPassengerFront = Constants.UNDEF;// ": "CLOSED",
+    public String doorPassengerRear = Constants.UNDEF;// ": "CLOSED",
+    public String windowDriverFront = Constants.UNDEF;// ": "CLOSED",
+    public String windowDriverRear = Constants.UNDEF;// ": "CLOSED",
+    public String windowPassengerFront = Constants.UNDEF;// ": "CLOSED",
+    public String windowPassengerRear = Constants.UNDEF;// ": "CLOSED",
+    public String sunroof = Constants.UNDEF;// ": "CLOSED",
+    public String trunk = Constants.UNDEF;// ": "CLOSED",
+    public String rearWindow = Constants.UNDEF;// ": "INVALID",
+    public String hood = Constants.UNDEF;// ": "CLOSED",
+    public String doorLockState;// ": "SECURED",
+    public String parkingLight;// ": "OFF",
+    public String positionLight;// ": "ON",
+    public String connectionStatus;// ": "DISCONNECTED",
+    public String chargingStatus;// ": "INVALID","
+    public String lastChargingEndReason;// ": "CHARGING_GOAL_REACHED",
+    public String lastChargingEndResult;// ": "SUCCESS","
+    public Double chargingTimeRemaining;// ": "45",
+    public Position position;
+    public String internalDataTimeUTC;// ": "2020-08-24 T15:55:32",
+    public boolean singleImmediateCharging;// ":false,
+    public String chargingConnectionType;// ": "CONDUCTIVE",
+    public String chargingInductivePositioning;// ": "NOT_POSITIONED",
+    public String vehicleCountry;// ": "DE","+"
+    @SerializedName("DCS_CCH_Activation")
+    public String dcsCchActivation;// ": "NA",
+    @SerializedName("DCS_CCH_Ongoing")
+    public boolean dcsCchOngoing;// ":false
+    public List<CCMMessage> checkControlMessages = new ArrayList<CCMMessage>();// ":[],
+    public List<CBSMessage> cbsData = new ArrayList<CBSMessage>();
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/VehicleStatusContainer.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/VehicleStatusContainer.java
new file mode 100644 (file)
index 0000000..673930b
--- /dev/null
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.status;
+
+/**
+ * The {@link VehicleStatusContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class VehicleStatusContainer {
+    public VehicleStatus vehicleStatus;
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/Windows.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/status/Windows.java
new file mode 100644 (file)
index 0000000..a530f31
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto.status;
+
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+/**
+ * The {@link Windows} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class Windows {
+    public String windowDriverFront = Constants.UNDEF;// ": "CLOSED",
+    public String windowDriverRear = Constants.UNDEF;// ": "CLOSED",
+    public String windowPassengerFront = Constants.UNDEF;// ": "CLOSED",
+    public String windowPassengerRear = Constants.UNDEF;// ": "CLOSED",
+    public String sunroof = Constants.UNDEF;// ": "CLOSED",
+    public String rearWindow = Constants.UNDEF;// ": "INVALID",
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/BMWConnectedDriveOptionProvider.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/BMWConnectedDriveOptionProvider.java
new file mode 100644 (file)
index 0000000..82690ea
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
+import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
+import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * Dynamic provider of state options while leaving other state description fields as original.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@Component(service = { DynamicStateDescriptionProvider.class, BMWConnectedDriveOptionProvider.class })
+@NonNullByDefault
+public class BMWConnectedDriveOptionProvider extends BaseDynamicStateDescriptionProvider {
+
+    @Reference
+    protected void setChannelTypeI18nLocalizationService(
+            final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
+        this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
+    }
+
+    protected void unsetChannelTypeI18nLocalizationService(
+            final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
+        this.channelTypeI18nLocalizationService = null;
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ByteResponseCallback.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ByteResponseCallback.java
new file mode 100644 (file)
index 0000000..7aafc76
--- /dev/null
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link ByteResponseCallback} Interface for all raw byte results from ASYNC REST API
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public interface ByteResponseCallback extends ResponseCallback {
+
+    public void onResponse(byte[] result);
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveBridgeHandler.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveBridgeHandler.java
new file mode 100644 (file)
index 0000000..5c7ac48
--- /dev/null
@@ -0,0 +1,213 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.handler;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.utils.Constants.ANONYMOUS;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Optional;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConfiguration;
+import org.openhab.binding.bmwconnecteddrive.internal.discovery.VehicleDiscovery;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.discovery.Dealer;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.discovery.VehiclesContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.BimmerConstants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonParseException;
+
+/**
+ * The {@link ConnectedDriveBridgeHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class ConnectedDriveBridgeHandler extends BaseBridgeHandler implements StringResponseCallback {
+    private final Logger logger = LoggerFactory.getLogger(ConnectedDriveBridgeHandler.class);
+    private HttpClientFactory httpClientFactory;
+    private Optional<VehicleDiscovery> discoveryService = Optional.empty();
+    private Optional<ConnectedDriveProxy> proxy = Optional.empty();
+    private Optional<ScheduledFuture<?>> initializerJob = Optional.empty();
+    private Optional<String> troubleshootFingerprint = Optional.empty();
+
+    public ConnectedDriveBridgeHandler(Bridge bridge, HttpClientFactory hcf) {
+        super(bridge);
+        httpClientFactory = hcf;
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        // no commands available
+    }
+
+    @Override
+    public void initialize() {
+        troubleshootFingerprint = Optional.empty();
+        updateStatus(ThingStatus.UNKNOWN);
+        ConnectedDriveConfiguration config = getConfigAs(ConnectedDriveConfiguration.class);
+        if (!checkConfiguration(config)) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
+        } else {
+            proxy = Optional.of(new ConnectedDriveProxy(httpClientFactory, config));
+            // give the system some time to create all predefined Vehicles
+            // check with API call if bridge is online
+            initializerJob = Optional.of(scheduler.schedule(this::requestVehicles, 5, TimeUnit.SECONDS));
+        }
+    }
+
+    public static boolean checkConfiguration(ConnectedDriveConfiguration config) {
+        if (Constants.EMPTY.equals(config.userName) || Constants.EMPTY.equals(config.password)) {
+            return false;
+        } else if (BimmerConstants.AUTH_SERVER_MAP.containsKey(config.region)) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public void dispose() {
+        initializerJob.ifPresent(job -> job.cancel(true));
+    }
+
+    public void requestVehicles() {
+        proxy.ifPresent(prox -> prox.requestVehicles(this));
+    }
+
+    public String getDiscoveryFingerprint() {
+        return troubleshootFingerprint.map(fingerprint -> {
+            VehiclesContainer container = null;
+            try {
+                container = Converter.getGson().fromJson(fingerprint, VehiclesContainer.class);
+                if (container != null) {
+                    if (container.vehicles != null) {
+                        if (container.vehicles.isEmpty()) {
+                            return Constants.EMPTY_JSON;
+                        } else {
+                            container.vehicles.forEach(entry -> {
+                                entry.vin = ANONYMOUS;
+                                entry.breakdownNumber = ANONYMOUS;
+                                if (entry.dealer != null) {
+                                    Dealer d = entry.dealer;
+                                    d.city = ANONYMOUS;
+                                    d.country = ANONYMOUS;
+                                    d.name = ANONYMOUS;
+                                    d.phone = ANONYMOUS;
+                                    d.postalCode = ANONYMOUS;
+                                    d.street = ANONYMOUS;
+                                }
+                            });
+                            return Converter.getGson().toJson(container);
+                        }
+                    }
+                }
+            } catch (JsonParseException jpe) {
+                logger.debug("Cannot parse fingerprint {}", jpe.getMessage());
+            }
+            // Not a VehiclesContainer or Vehicles is empty so deliver fingerprint as it is
+            return fingerprint;
+        }).orElse(Constants.INVALID);
+    }
+
+    private void logFingerPrint() {
+        logger.debug("###### Discovery Troubleshoot Fingerprint Data - BEGIN ######");
+        logger.debug("### Discovery Result ###");
+        logger.debug("{}", getDiscoveryFingerprint());
+        logger.debug("###### Discovery Troubleshoot Fingerprint Data - END ######");
+    }
+
+    /**
+     * There's only the Vehicles response available
+     */
+    @Override
+    public void onResponse(@Nullable String response) {
+        boolean firstResponse = troubleshootFingerprint.isEmpty();
+        if (response != null) {
+            updateStatus(ThingStatus.ONLINE);
+            troubleshootFingerprint = discoveryService.map(discovery -> {
+                try {
+                    VehiclesContainer container = Converter.getGson().fromJson(response, VehiclesContainer.class);
+                    if (container != null) {
+                        if (container.vehicles != null) {
+                            discovery.onResponse(container);
+                            container.vehicles.forEach(entry -> {
+                                entry.vin = ANONYMOUS;
+                                entry.breakdownNumber = ANONYMOUS;
+                                if (entry.dealer != null) {
+                                    Dealer d = entry.dealer;
+                                    d.city = ANONYMOUS;
+                                    d.country = ANONYMOUS;
+                                    d.name = ANONYMOUS;
+                                    d.phone = ANONYMOUS;
+                                    d.postalCode = ANONYMOUS;
+                                    d.street = ANONYMOUS;
+                                }
+                            });
+                        }
+                        return Converter.getGson().toJson(container);
+                    }
+                } catch (JsonParseException jpe) {
+                    logger.debug("Fingerprint parse exception {}", jpe.getMessage());
+                }
+                // Unparseable or not a VehiclesContainer:
+                return response;
+            });
+        } else {
+            troubleshootFingerprint = Optional.of(Constants.EMPTY_JSON);
+        }
+        if (firstResponse) {
+            logFingerPrint();
+        }
+    }
+
+    @Override
+    public void onError(NetworkError error) {
+        boolean firstResponse = troubleshootFingerprint.isEmpty();
+        troubleshootFingerprint = Optional.of(error.toJson());
+        if (firstResponse) {
+            logFingerPrint();
+        }
+        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error.reason);
+    }
+
+    @Override
+    public Collection<Class<? extends ThingHandlerService>> getServices() {
+        return Collections.singleton(VehicleDiscovery.class);
+    }
+
+    public Optional<ConnectedDriveProxy> getProxy() {
+        return proxy;
+    }
+
+    public void setDiscoveryService(VehicleDiscovery discoveryService) {
+        this.discoveryService = Optional.of(discoveryService);
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java
new file mode 100644 (file)
index 0000000..af2164b
--- /dev/null
@@ -0,0 +1,324 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.handler;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.utils.HTTPConstants.*;
+
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.client.util.BufferingResponseListener;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.util.MultiMap;
+import org.eclipse.jetty.util.UrlEncoded;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConfiguration;
+import org.openhab.binding.bmwconnecteddrive.internal.VehicleConfiguration;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.auth.AuthResponse;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.simulation.Injector;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.BimmerConstants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ImageProperties;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link ConnectedDriveProxy} This class holds the important constants for the BMW Connected Drive Authorization.
+ * They
+ * are taken from the Bimmercode from github {@link https://github.com/bimmerconnected/bimmer_connected}
+ * File defining these constants
+ * {@link https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/account.py}
+ * https://customer.bmwgroup.com/one/app/oauth.js
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - edit & send of charge profile
+ */
+@NonNullByDefault
+public class ConnectedDriveProxy {
+    private final Logger logger = LoggerFactory.getLogger(ConnectedDriveProxy.class);
+    private final Token token = new Token();
+    private final HttpClient httpClient;
+    private final HttpClient authHttpClient;
+    private final String legacyAuthUri;
+    private final ConnectedDriveConfiguration configuration;
+
+    /**
+     * URLs taken from https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/const.py
+     */
+    final String baseUrl;
+    final String vehicleUrl;
+    final String legacyUrl;
+    final String vehicleStatusAPI = "/status";
+    final String lastTripAPI = "/statistics/lastTrip";
+    final String allTripsAPI = "/statistics/allTrips";
+    final String chargeAPI = "/chargingprofile";
+    final String destinationAPI = "/destinations";
+    final String imageAPI = "/image";
+    final String rangeMapAPI = "/rangemap";
+    final String serviceExecutionAPI = "/executeService";
+    final String serviceExecutionStateAPI = "/serviceExecutionStatus";
+
+    public ConnectedDriveProxy(HttpClientFactory httpClientFactory, ConnectedDriveConfiguration config) {
+        httpClient = httpClientFactory.getCommonHttpClient();
+        authHttpClient = httpClientFactory.createHttpClient(AUTH_HTTP_CLIENT_NAME);
+        authHttpClient.setFollowRedirects(false);
+        configuration = config;
+
+        final StringBuilder legacyAuth = new StringBuilder();
+        legacyAuth.append("https://");
+        legacyAuth.append(BimmerConstants.AUTH_SERVER_MAP.get(configuration.region));
+        legacyAuth.append(BimmerConstants.OAUTH_ENDPOINT);
+        legacyAuthUri = legacyAuth.toString();
+        vehicleUrl = "https://" + getRegionServer() + "/webapi/v1/user/vehicles";
+        baseUrl = vehicleUrl + "/";
+        legacyUrl = "https://" + getRegionServer() + "/api/vehicle/dynamic/v1/";
+    }
+
+    private synchronized void call(final String url, final boolean post, final @Nullable MultiMap<String> params,
+            final ResponseCallback callback) {
+        // only executed in "simulation mode"
+        // SimulationTest.testSimulationOff() assures Injector is off when releasing
+        if (Injector.isActive()) {
+            if (url.equals(baseUrl)) {
+                ((StringResponseCallback) callback).onResponse(Injector.getDiscovery());
+            } else if (url.endsWith(vehicleStatusAPI)) {
+                ((StringResponseCallback) callback).onResponse(Injector.getStatus());
+            } else {
+                logger.debug("Simulation of {} not supported", url);
+            }
+            return;
+        }
+        final Request req;
+        final String encoded = params == null || params.isEmpty() ? null
+                : UrlEncoded.encode(params, StandardCharsets.UTF_8, false);
+        final String completeUrl;
+
+        if (post) {
+            completeUrl = url;
+            req = httpClient.POST(url);
+            if (encoded != null) {
+                req.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, encoded, StandardCharsets.UTF_8));
+            }
+        } else {
+            completeUrl = encoded == null ? url : url + Constants.QUESTION + encoded;
+            req = httpClient.newRequest(completeUrl);
+        }
+        req.header(HttpHeader.AUTHORIZATION, getToken().getBearerToken());
+        req.header(HttpHeader.REFERER, BimmerConstants.REFERER_URL);
+
+        req.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(new BufferingResponseListener() {
+            @NonNullByDefault({})
+            @Override
+            public void onComplete(Result result) {
+                if (result.getResponse().getStatus() != 200) {
+                    NetworkError error = new NetworkError();
+                    error.url = completeUrl;
+                    error.status = result.getResponse().getStatus();
+                    if (result.getResponse().getReason() != null) {
+                        error.reason = result.getResponse().getReason();
+                    } else {
+                        error.reason = result.getFailure().getMessage();
+                    }
+                    error.params = result.getRequest().getParams().toString();
+                    logger.debug("HTTP Error {}", error.toString());
+                    callback.onError(error);
+                } else {
+                    if (callback instanceof StringResponseCallback) {
+                        ((StringResponseCallback) callback).onResponse(getContentAsString());
+                    } else if (callback instanceof ByteResponseCallback) {
+                        ((ByteResponseCallback) callback).onResponse(getContent());
+                    } else {
+                        logger.error("unexpected reponse type {}", callback.getClass().getName());
+                    }
+                }
+            }
+        });
+    }
+
+    public void get(String url, @Nullable MultiMap<String> params, ResponseCallback callback) {
+        call(url, false, params, callback);
+    }
+
+    public void post(String url, @Nullable MultiMap<String> params, ResponseCallback callback) {
+        call(url, true, params, callback);
+    }
+
+    public void requestVehicles(StringResponseCallback callback) {
+        get(vehicleUrl, null, callback);
+    }
+
+    public void requestVehcileStatus(VehicleConfiguration config, StringResponseCallback callback) {
+        get(baseUrl + config.vin + vehicleStatusAPI, null, callback);
+    }
+
+    public void requestLegacyVehcileStatus(VehicleConfiguration config, StringResponseCallback callback) {
+        // see https://github.com/jupe76/bmwcdapi/search?q=dynamic%2Fv1
+        get(legacyUrl + config.vin + "?offset=-60", null, callback);
+    }
+
+    public void requestLastTrip(VehicleConfiguration config, StringResponseCallback callback) {
+        get(baseUrl + config.vin + lastTripAPI, null, callback);
+    }
+
+    public void requestAllTrips(VehicleConfiguration config, StringResponseCallback callback) {
+        get(baseUrl + config.vin + allTripsAPI, null, callback);
+    }
+
+    public void requestChargingProfile(VehicleConfiguration config, StringResponseCallback callback) {
+        get(baseUrl + config.vin + chargeAPI, null, callback);
+    }
+
+    public void requestDestinations(VehicleConfiguration config, StringResponseCallback callback) {
+        get(baseUrl + config.vin + destinationAPI, null, callback);
+    }
+
+    public void requestRangeMap(VehicleConfiguration config, @Nullable MultiMap<String> params,
+            StringResponseCallback callback) {
+        get(baseUrl + config.vin + rangeMapAPI, params, callback);
+    }
+
+    public void requestImage(VehicleConfiguration config, ImageProperties props, ByteResponseCallback callback) {
+        final String localImageUrl = baseUrl + config.vin + imageAPI;
+        final MultiMap<String> dataMap = new MultiMap<String>();
+        dataMap.add("width", Integer.toString(props.size));
+        dataMap.add("height", Integer.toString(props.size));
+        dataMap.add("view", props.viewport);
+        get(localImageUrl, dataMap, callback);
+    }
+
+    private String getRegionServer() {
+        final String retVal = BimmerConstants.SERVER_MAP.get(configuration.region);
+        return retVal == null ? Constants.INVALID : retVal;
+    }
+
+    private String getAuthorizationValue() {
+        final String retVal = BimmerConstants.AUTHORIZATION_VALUE_MAP.get(configuration.region);
+        return retVal == null ? Constants.INVALID : retVal;
+    }
+
+    RemoteServiceHandler getRemoteServiceHandler(VehicleHandler vehicleHandler) {
+        return new RemoteServiceHandler(vehicleHandler, this);
+    }
+
+    // Token handling
+
+    /**
+     * Gets new token if old one is expired or invalid. In case of error the token remains.
+     * So if token refresh fails the corresponding requests will also fail and update the
+     * Thing status accordingly.
+     *
+     * @return token
+     */
+    public Token getToken() {
+        if (token.isExpired() || !token.isValid()) {
+            updateToken();
+        }
+        return token;
+    }
+
+    /**
+     * Authorize at BMW Connected Drive Portal and get Token
+     *
+     * @return
+     */
+    private synchronized void updateToken() {
+        if (!authHttpClient.isStarted()) {
+            try {
+                authHttpClient.start();
+            } catch (Exception e) {
+                logger.warn("Auth Http Client cannot be started {}", e.getMessage());
+                return;
+            }
+        }
+
+        final Request req = authHttpClient.POST(legacyAuthUri);
+        req.header(HttpHeader.CONNECTION, KEEP_ALIVE);
+        req.header(HttpHeader.HOST, getRegionServer());
+        req.header(HttpHeader.AUTHORIZATION, getAuthorizationValue());
+        req.header(CREDENTIALS, BimmerConstants.CREDENTIAL_VALUES);
+        req.header(HttpHeader.REFERER, BimmerConstants.REFERER_URL);
+
+        final MultiMap<String> dataMap = new MultiMap<String>();
+        dataMap.add("grant_type", "password");
+        dataMap.add(SCOPE, BimmerConstants.SCOPE_VALUES);
+        dataMap.add(USERNAME, configuration.userName);
+        dataMap.add(PASSWORD, configuration.password);
+        req.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
+                UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
+        try {
+            ContentResponse contentResponse = req.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send();
+            // Status needs to be 302 - Response is stored in Header
+            if (contentResponse.getStatus() == 302) {
+                final HttpFields fields = contentResponse.getHeaders();
+                final HttpField field = fields.getField(HttpHeader.LOCATION);
+                tokenFromUrl(field.getValue());
+            } else if (contentResponse.getStatus() == 200) {
+                final String stringContent = contentResponse.getContentAsString();
+                if (stringContent != null && !stringContent.isEmpty()) {
+                    try {
+                        final AuthResponse authResponse = Converter.getGson().fromJson(stringContent,
+                                AuthResponse.class);
+                        if (authResponse != null) {
+                            token.setToken(authResponse.accessToken);
+                            token.setType(authResponse.tokenType);
+                            token.setExpiration(authResponse.expiresIn);
+                        } else {
+                            logger.debug("not an Authorization response: {}", stringContent);
+                        }
+                    } catch (JsonSyntaxException jse) {
+                        logger.debug("Authorization response unparsable: {}", stringContent);
+                    }
+                } else {
+                    logger.debug("Authorization response has no content");
+                }
+            } else {
+                logger.debug("Authorization status {} reason {}", contentResponse.getStatus(),
+                        contentResponse.getReason());
+            }
+        } catch (InterruptedException | ExecutionException | TimeoutException e) {
+            logger.debug("Authorization exception: {}", e.getMessage());
+        }
+    }
+
+    void tokenFromUrl(String encodedUrl) {
+        final MultiMap<String> tokenMap = new MultiMap<String>();
+        UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII);
+        tokenMap.forEach((key, value) -> {
+            if (value.size() > 0) {
+                String val = value.get(0);
+                if (key.endsWith(ACCESS_TOKEN)) {
+                    token.setToken(val.toString());
+                } else if (key.equals(EXPIRES_IN)) {
+                    token.setExpiration(Integer.parseInt(val.toString()));
+                } else if (key.equals(TOKEN_TYPE)) {
+                    token.setType(val.toString());
+                }
+            }
+        });
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/RemoteServiceHandler.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/RemoteServiceHandler.java
new file mode 100644 (file)
index 0000000..fc1999d
--- /dev/null
@@ -0,0 +1,199 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.handler;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
+
+import java.util.Optional;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.util.MultiMap;
+import org.openhab.binding.bmwconnecteddrive.internal.VehicleConfiguration;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.remote.ExecutionStatusContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.HTTPConstants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link RemoteServiceHandler} handles executions of remote services towards your Vehicle
+ *
+ * @see https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/remote_services.py
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - edit & send of charge profile
+ */
+@NonNullByDefault
+public class RemoteServiceHandler implements StringResponseCallback {
+    private final Logger logger = LoggerFactory.getLogger(RemoteServiceHandler.class);
+
+    private static final String SERVICE_TYPE = "serviceType";
+    private static final String DATA = "data";
+    // after 6 retries the state update will give up
+    private static final int GIVEUP_COUNTER = 6;
+    private static final int STATE_UPDATE_SEC = HTTPConstants.HTTP_TIMEOUT_SEC + 1; // regular timeout + 1sec
+
+    private final ConnectedDriveProxy proxy;
+    private final VehicleHandler handler;
+    private final String serviceExecutionAPI;
+    private final String serviceExecutionStateAPI;
+
+    private int counter = 0;
+    private Optional<ScheduledFuture<?>> stateJob = Optional.empty();
+    private Optional<String> serviceExecuting = Optional.empty();
+
+    public enum ExecutionState {
+        READY,
+        INITIATED,
+        PENDING,
+        DELIVERED,
+        EXECUTED,
+        ERROR,
+    }
+
+    public enum RemoteService {
+        LIGHT_FLASH(REMOTE_SERVICE_LIGHT_FLASH, "Flash Lights"),
+        VEHICLE_FINDER(REMOTE_SERVICE_VEHICLE_FINDER, "Vehicle Finder"),
+        DOOR_LOCK(REMOTE_SERVICE_DOOR_LOCK, "Door Lock"),
+        DOOR_UNLOCK(REMOTE_SERVICE_DOOR_UNLOCK, "Door Unlock"),
+        HORN_BLOW(REMOTE_SERVICE_HORN, "Horn Blow"),
+        CLIMATE_NOW(REMOTE_SERVICE_AIR_CONDITIONING, "Climate Control"),
+        CHARGE_NOW(REMOTE_SERVICE_CHARGE_NOW, "Start Charging"),
+        CHARGING_CONTROL(REMOTE_SERVICE_CHARGING_CONTROL, "Send Charging Profile");
+
+        private final String command;
+        private final String label;
+
+        RemoteService(final String command, final String label) {
+            this.command = command;
+            this.label = label;
+        }
+
+        public String getCommand() {
+            return command;
+        }
+
+        public String getLabel() {
+            return label;
+        }
+    }
+
+    public RemoteServiceHandler(VehicleHandler vehicleHandler, ConnectedDriveProxy connectedDriveProxy) {
+        handler = vehicleHandler;
+        proxy = connectedDriveProxy;
+        final VehicleConfiguration config = handler.getConfiguration().get();
+        serviceExecutionAPI = proxy.baseUrl + config.vin + proxy.serviceExecutionAPI;
+        serviceExecutionStateAPI = proxy.baseUrl + config.vin + proxy.serviceExecutionStateAPI;
+    }
+
+    boolean execute(RemoteService service, String... data) {
+        synchronized (this) {
+            if (serviceExecuting.isPresent()) {
+                // only one service executing
+                return false;
+            }
+            serviceExecuting = Optional.of(service.name());
+        }
+        final MultiMap<String> dataMap = new MultiMap<String>();
+        dataMap.add(SERVICE_TYPE, service.name());
+        if (data.length > 0) {
+            dataMap.add(DATA, data[0]);
+        }
+        proxy.post(serviceExecutionAPI, dataMap, this);
+        return true;
+    }
+
+    public void getState() {
+        synchronized (this) {
+            serviceExecuting.ifPresentOrElse(service -> {
+                if (counter >= GIVEUP_COUNTER) {
+                    logger.warn("Giving up updating state for {} after {} times", service, GIVEUP_COUNTER);
+                    reset();
+                    // immediately refresh data
+                    handler.getData();
+                }
+                counter++;
+                final MultiMap<String> dataMap = new MultiMap<String>();
+                dataMap.add(SERVICE_TYPE, service);
+                proxy.get(serviceExecutionStateAPI, dataMap, this);
+            }, () -> {
+                logger.warn("No Service executed to get state");
+            });
+            stateJob = Optional.empty();
+        }
+    }
+
+    @Override
+    public void onResponse(@Nullable String result) {
+        if (result != null) {
+            try {
+                ExecutionStatusContainer esc = Converter.getGson().fromJson(result, ExecutionStatusContainer.class);
+                if (esc != null && esc.executionStatus != null) {
+                    String status = esc.executionStatus.status;
+                    synchronized (this) {
+                        handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), status);
+                        if (ExecutionState.EXECUTED.name().equals(status)) {
+                            // refresh loop ends - update of status handled in the normal refreshInterval. Earlier
+                            // update doesn't show better results!
+                            reset();
+                            return;
+                        }
+                    }
+                }
+            } catch (JsonSyntaxException jse) {
+                logger.debug("RemoteService response is unparseable: {} {}", result, jse.getMessage());
+            }
+        }
+        // schedule even if no result is present until retries exceeded
+        synchronized (this) {
+            stateJob.ifPresent(job -> {
+                if (!job.isDone()) {
+                    job.cancel(true);
+                }
+            });
+            stateJob = Optional.of(handler.getScheduler().schedule(this::getState, STATE_UPDATE_SEC, TimeUnit.SECONDS));
+        }
+    }
+
+    @Override
+    public void onError(NetworkError error) {
+        synchronized (this) {
+            handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null),
+                    ExecutionState.ERROR.name() + Constants.SPACE + Integer.toString(error.status));
+            reset();
+        }
+    }
+
+    private void reset() {
+        serviceExecuting = Optional.empty();
+        counter = 0;
+    }
+
+    public void cancel() {
+        synchronized (this) {
+            stateJob.ifPresent(action -> {
+                if (!action.isDone()) {
+                    action.cancel(true);
+                }
+                stateJob = Optional.empty();
+            });
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ResponseCallback.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ResponseCallback.java
new file mode 100644 (file)
index 0000000..5785cb2
--- /dev/null
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
+
+/**
+ * The {@link ResponseCallback} Marker Interface for all ASYNC REST API callbacks
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public interface ResponseCallback {
+    public void onError(NetworkError error);
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/StringResponseCallback.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/StringResponseCallback.java
new file mode 100644 (file)
index 0000000..45b5091
--- /dev/null
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link StringResponseCallback} Interface for all String results from ASYNC REST API
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public interface StringResponseCallback extends ResponseCallback {
+
+    public void onResponse(@Nullable String result);
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/Token.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/Token.java
new file mode 100644 (file)
index 0000000..22e4217
--- /dev/null
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+/**
+ * The {@link Token} BMW ConnectedDrive Token storage
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class Token {
+    private String token = Constants.EMPTY;
+    private String tokenType = Constants.EMPTY;
+    private long expiration = 0;
+
+    public String getBearerToken() {
+        return new StringBuilder(tokenType).append(Constants.SPACE).append(token).toString();
+    }
+
+    public void setToken(String token) {
+        this.token = token;
+    }
+
+    public void setExpiration(int expiration) {
+        this.expiration = System.currentTimeMillis() / 1000 + expiration;
+    }
+
+    /**
+     * @return true if Token expires in less than 1 second
+     */
+    public boolean isExpired() {
+        return (expiration - System.currentTimeMillis() / 1000) < 1;
+    }
+
+    public void setType(String type) {
+        tokenType = type;
+    }
+
+    public boolean isValid() {
+        return (!token.equals(Constants.EMPTY) && !tokenType.equals(Constants.EMPTY) && expiration > 0);
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleChannelHandler.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleChannelHandler.java
new file mode 100644 (file)
index 0000000..6ff369b
--- /dev/null
@@ -0,0 +1,515 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.handler;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
+
+import java.time.DayOfWeek;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+import javax.measure.quantity.Length;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.VehicleType;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.Destination;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.AllTrips;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.LastTrip;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.CBSMessage;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.CCMMessage;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.Doors;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.Position;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatus;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.Windows;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileUtils;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileUtils.TimedChannel;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper.ProfileKey;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.RemoteServiceUtils;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.VehicleStatusUtils;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PointType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.ImperialUnits;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.State;
+import org.openhab.core.types.StateOption;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link VehicleChannelHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - edit & send of charge profile
+ */
+@NonNullByDefault
+public abstract class VehicleChannelHandler extends BaseThingHandler {
+    protected final Logger logger = LoggerFactory.getLogger(VehicleChannelHandler.class);
+    protected boolean imperial = false;
+    protected boolean hasFuel = false;
+    protected boolean isElectric = false;
+    protected boolean isHybrid = false;
+
+    // List Interfaces
+    protected List<CBSMessage> serviceList = new ArrayList<CBSMessage>();
+    protected String selectedService = Constants.UNDEF;
+    protected List<CCMMessage> checkControlList = new ArrayList<CCMMessage>();
+    protected String selectedCC = Constants.UNDEF;
+    protected List<Destination> destinationList = new ArrayList<Destination>();
+    protected String selectedDestination = Constants.UNDEF;
+
+    protected BMWConnectedDriveOptionProvider optionProvider;
+
+    // Data Caches
+    protected Optional<String> vehicleStatusCache = Optional.empty();
+    protected Optional<String> lastTripCache = Optional.empty();
+    protected Optional<String> allTripsCache = Optional.empty();
+    protected Optional<String> chargeProfileCache = Optional.empty();
+    protected Optional<String> rangeMapCache = Optional.empty();
+    protected Optional<String> destinationCache = Optional.empty();
+    protected Optional<byte[]> imageCache = Optional.empty();
+
+    public VehicleChannelHandler(Thing thing, BMWConnectedDriveOptionProvider op, String type, boolean imperial) {
+        super(thing);
+        optionProvider = op;
+
+        this.imperial = imperial;
+        hasFuel = type.equals(VehicleType.CONVENTIONAL.toString()) || type.equals(VehicleType.PLUGIN_HYBRID.toString())
+                || type.equals(VehicleType.ELECTRIC_REX.toString());
+        isElectric = type.equals(VehicleType.PLUGIN_HYBRID.toString())
+                || type.equals(VehicleType.ELECTRIC_REX.toString()) || type.equals(VehicleType.ELECTRIC.toString());
+        isHybrid = hasFuel && isElectric;
+
+        setOptions(CHANNEL_GROUP_REMOTE, REMOTE_SERVICE_COMMAND, RemoteServiceUtils.getOptions(isElectric));
+    }
+
+    private void setOptions(final String group, final String id, List<StateOption> options) {
+        optionProvider.setStateOptions(new ChannelUID(thing.getUID(), group, id), options);
+    }
+
+    protected void updateChannel(final String group, final String id, final State state) {
+        updateState(new ChannelUID(thing.getUID(), group, id), state);
+    }
+
+    protected void updateCheckControls(List<CCMMessage> ccl) {
+        if (ccl.size() == 0) {
+            // No Check Control available - show not active
+            CCMMessage ccm = new CCMMessage();
+            ccm.ccmDescriptionLong = Constants.NO_ENTRIES;
+            ccm.ccmDescriptionShort = Constants.NO_ENTRIES;
+            ccm.ccmId = -1;
+            ccm.ccmMileage = -1;
+            ccl.add(ccm);
+        }
+
+        // add all elements to options
+        checkControlList = ccl;
+        List<StateOption> ccmDescriptionOptions = new ArrayList<>();
+        List<StateOption> ccmDetailsOptions = new ArrayList<>();
+        List<StateOption> ccmMileageOptions = new ArrayList<>();
+        boolean isSelectedElementIn = false;
+        int index = 0;
+        for (CCMMessage ccEntry : checkControlList) {
+            ccmDescriptionOptions.add(new StateOption(Integer.toString(index), ccEntry.ccmDescriptionShort));
+            ccmDetailsOptions.add(new StateOption(Integer.toString(index), ccEntry.ccmDescriptionLong));
+            ccmMileageOptions.add(new StateOption(Integer.toString(index), Integer.toString(ccEntry.ccmMileage)));
+            if (selectedCC.equals(ccEntry.ccmDescriptionShort)) {
+                isSelectedElementIn = true;
+            }
+            index++;
+        }
+        setOptions(CHANNEL_GROUP_CHECK_CONTROL, NAME, ccmDescriptionOptions);
+        setOptions(CHANNEL_GROUP_CHECK_CONTROL, DETAILS, ccmDetailsOptions);
+        setOptions(CHANNEL_GROUP_CHECK_CONTROL, MILEAGE, ccmMileageOptions);
+
+        // if current selected item isn't anymore in the list select first entry
+        if (!isSelectedElementIn) {
+            selectCheckControl(0);
+        }
+    }
+
+    protected void selectCheckControl(int index) {
+        if (index >= 0 && index < checkControlList.size()) {
+            CCMMessage ccEntry = checkControlList.get(index);
+            selectedCC = ccEntry.ccmDescriptionShort;
+            updateChannel(CHANNEL_GROUP_CHECK_CONTROL, NAME, StringType.valueOf(ccEntry.ccmDescriptionShort));
+            updateChannel(CHANNEL_GROUP_CHECK_CONTROL, DETAILS, StringType.valueOf(ccEntry.ccmDescriptionLong));
+            updateChannel(CHANNEL_GROUP_CHECK_CONTROL, MILEAGE, QuantityType.valueOf(
+                    Converter.round(ccEntry.ccmMileage), imperial ? ImperialUnits.MILE : Constants.KILOMETRE_UNIT));
+        }
+    }
+
+    protected void updateServices(List<CBSMessage> sl) {
+        // if list is empty add "undefined" element
+        if (sl.size() == 0) {
+            CBSMessage cbsm = new CBSMessage();
+            cbsm.cbsType = Constants.NO_ENTRIES;
+            cbsm.cbsDescription = Constants.NO_ENTRIES;
+            sl.add(cbsm);
+        }
+
+        // add all elements to options
+        serviceList = sl;
+        List<StateOption> serviceNameOptions = new ArrayList<>();
+        List<StateOption> serviceDetailsOptions = new ArrayList<>();
+        List<StateOption> serviceDateOptions = new ArrayList<>();
+        List<StateOption> serviceMileageOptions = new ArrayList<>();
+        boolean isSelectedElementIn = false;
+        int index = 0;
+        for (CBSMessage serviceEntry : serviceList) {
+            // create StateOption with "value = list index" and "label = human readable string"
+            serviceNameOptions.add(new StateOption(Integer.toString(index), serviceEntry.getType()));
+            serviceDetailsOptions.add(new StateOption(Integer.toString(index), serviceEntry.getDescription()));
+            serviceDateOptions.add(new StateOption(Integer.toString(index), serviceEntry.getDueDate()));
+            serviceMileageOptions
+                    .add(new StateOption(Integer.toString(index), Integer.toString(serviceEntry.cbsRemainingMileage)));
+            if (selectedService.equals(serviceEntry.getType())) {
+                isSelectedElementIn = true;
+            }
+            index++;
+        }
+        setOptions(CHANNEL_GROUP_SERVICE, NAME, serviceNameOptions);
+        setOptions(CHANNEL_GROUP_SERVICE, DETAILS, serviceDetailsOptions);
+        setOptions(CHANNEL_GROUP_SERVICE, DATE, serviceDateOptions);
+        setOptions(CHANNEL_GROUP_SERVICE, MILEAGE, serviceMileageOptions);
+
+        // if current selected item isn't anymore in the list select first entry
+        if (!isSelectedElementIn) {
+            selectService(0);
+        }
+    }
+
+    protected void selectService(int index) {
+        if (index >= 0 && index < serviceList.size()) {
+            CBSMessage serviceEntry = serviceList.get(index);
+            selectedService = serviceEntry.cbsType;
+            updateChannel(CHANNEL_GROUP_SERVICE, NAME,
+                    StringType.valueOf(Converter.toTitleCase(serviceEntry.getType())));
+            updateChannel(CHANNEL_GROUP_SERVICE, DETAILS,
+                    StringType.valueOf(Converter.toTitleCase(serviceEntry.getDescription())));
+            updateChannel(CHANNEL_GROUP_SERVICE, DATE,
+                    DateTimeType.valueOf(Converter.getLocalDateTime(serviceEntry.getDueDate())));
+            updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE,
+                    QuantityType.valueOf(Converter.round(serviceEntry.cbsRemainingMileage),
+                            imperial ? ImperialUnits.MILE : Constants.KILOMETRE_UNIT));
+        }
+    }
+
+    protected void updateDestinations(List<Destination> dl) {
+        // if list is empty add "undefined" element
+        if (dl.size() == 0) {
+            Destination dest = new Destination();
+            dest.city = Constants.NO_ENTRIES;
+            dest.lat = -1;
+            dest.lon = -1;
+            dl.add(dest);
+        }
+
+        // add all elements to options
+        destinationList = dl;
+        List<StateOption> destinationNameOptions = new ArrayList<>();
+        List<StateOption> destinationGPSOptions = new ArrayList<>();
+        boolean isSelectedElementIn = false;
+        int index = 0;
+        for (Destination destination : destinationList) {
+            destinationNameOptions.add(new StateOption(Integer.toString(index), destination.getAddress()));
+            destinationGPSOptions.add(new StateOption(Integer.toString(index), destination.getCoordinates()));
+            if (selectedDestination.equals(destination.getAddress())) {
+                isSelectedElementIn = true;
+            }
+            index++;
+        }
+        setOptions(CHANNEL_GROUP_DESTINATION, NAME, destinationNameOptions);
+        setOptions(CHANNEL_GROUP_DESTINATION, GPS, destinationGPSOptions);
+
+        // if current selected item isn't anymore in the list select first entry
+        if (!isSelectedElementIn) {
+            selectDestination(0);
+        }
+    }
+
+    protected void selectDestination(int index) {
+        if (index >= 0 && index < destinationList.size()) {
+            Destination destinationEntry = destinationList.get(index);
+            // update selected Item
+            selectedDestination = destinationEntry.getAddress();
+            // update coordinates according to new set location
+            updateChannel(CHANNEL_GROUP_DESTINATION, NAME, StringType.valueOf(destinationEntry.getAddress()));
+            updateChannel(CHANNEL_GROUP_DESTINATION, GPS, PointType.valueOf(destinationEntry.getCoordinates()));
+        }
+    }
+
+    protected void updateAllTrips(AllTrips allTrips) {
+        QuantityType<Length> qtTotalElectric = QuantityType
+                .valueOf(Converter.round(allTrips.totalElectricDistance.userTotal), Constants.KILOMETRE_UNIT);
+        QuantityType<Length> qtLongestElectricRange = QuantityType
+                .valueOf(Converter.round(allTrips.chargecycleRange.userHigh), Constants.KILOMETRE_UNIT);
+        QuantityType<Length> qtDistanceSinceCharge = QuantityType
+                .valueOf(Converter.round(allTrips.chargecycleRange.userCurrentChargeCycle), Constants.KILOMETRE_UNIT);
+
+        updateChannel(CHANNEL_GROUP_LIFETIME, TOTAL_DRIVEN_DISTANCE,
+                imperial ? Converter.getMiles(qtTotalElectric) : qtTotalElectric);
+        updateChannel(CHANNEL_GROUP_LIFETIME, SINGLE_LONGEST_DISTANCE,
+                imperial ? Converter.getMiles(qtLongestElectricRange) : qtLongestElectricRange);
+        updateChannel(CHANNEL_GROUP_LAST_TRIP, DISTANCE_SINCE_CHARGING,
+                imperial ? Converter.getMiles(qtDistanceSinceCharge) : qtDistanceSinceCharge);
+
+        // Conversion from kwh/100km to kwh/10mi has to be done manually
+        double avgConsumotion = imperial ? allTrips.avgElectricConsumption.userAverage * Converter.MILES_TO_KM_RATIO
+                : allTrips.avgElectricConsumption.userAverage;
+        double avgCombinedConsumption = imperial
+                ? allTrips.avgCombinedConsumption.userAverage * Converter.MILES_TO_KM_RATIO
+                : allTrips.avgCombinedConsumption.userAverage;
+        double avgRecuperation = imperial ? allTrips.avgRecuperation.userAverage * Converter.MILES_TO_KM_RATIO
+                : allTrips.avgRecuperation.userAverage;
+
+        updateChannel(CHANNEL_GROUP_LIFETIME, AVG_CONSUMPTION,
+                QuantityType.valueOf(Converter.round(avgConsumotion), Units.KILOWATT_HOUR));
+        updateChannel(CHANNEL_GROUP_LIFETIME, AVG_COMBINED_CONSUMPTION,
+                QuantityType.valueOf(Converter.round(avgCombinedConsumption), Units.LITRE));
+        updateChannel(CHANNEL_GROUP_LIFETIME, AVG_RECUPERATION,
+                QuantityType.valueOf(Converter.round(avgRecuperation), Units.KILOWATT_HOUR));
+    }
+
+    protected void updateLastTrip(LastTrip trip) {
+        // Whyever the Last Trip DateTime is delivered without offest - so LocalTime
+        updateChannel(CHANNEL_GROUP_LAST_TRIP, DATE,
+                DateTimeType.valueOf(Converter.getLocalDateTimeWithoutOffest(trip.date)));
+        updateChannel(CHANNEL_GROUP_LAST_TRIP, DURATION, QuantityType.valueOf(trip.duration, Units.MINUTE));
+
+        QuantityType<Length> qtTotalDistance = QuantityType.valueOf(Converter.round(trip.totalDistance),
+                Constants.KILOMETRE_UNIT);
+        updateChannel(CHANNEL_GROUP_LAST_TRIP, DISTANCE,
+                imperial ? Converter.getMiles(qtTotalDistance) : qtTotalDistance);
+
+        // Conversion from kwh/100km to kwh/10mi has to be done manually
+        double avgConsumtption = imperial ? trip.avgElectricConsumption * Converter.MILES_TO_KM_RATIO
+                : trip.avgElectricConsumption;
+        double avgCombinedConsumption = imperial ? trip.avgCombinedConsumption * Converter.MILES_TO_KM_RATIO
+                : trip.avgCombinedConsumption;
+        double avgRecuperation = imperial ? trip.avgRecuperation * Converter.MILES_TO_KM_RATIO : trip.avgRecuperation;
+
+        updateChannel(CHANNEL_GROUP_LAST_TRIP, AVG_CONSUMPTION,
+                QuantityType.valueOf(Converter.round(avgConsumtption), Units.KILOWATT_HOUR));
+        updateChannel(CHANNEL_GROUP_LAST_TRIP, AVG_COMBINED_CONSUMPTION,
+                QuantityType.valueOf(Converter.round(avgCombinedConsumption), Units.LITRE));
+        updateChannel(CHANNEL_GROUP_LAST_TRIP, AVG_RECUPERATION,
+                QuantityType.valueOf(Converter.round(avgRecuperation), Units.KILOWATT_HOUR));
+    }
+
+    protected void updateChargeProfileFromContent(String content) {
+        ChargeProfileWrapper.fromJson(content).ifPresent(this::updateChargeProfile);
+    }
+
+    protected void updateChargeProfile(ChargeProfileWrapper wrapper) {
+        updateChannel(CHANNEL_GROUP_CHARGE, CHARGE_PROFILE_PREFERENCE,
+                StringType.valueOf(Converter.toTitleCase(wrapper.getPreference())));
+        updateChannel(CHANNEL_GROUP_CHARGE, CHARGE_PROFILE_MODE,
+                StringType.valueOf(Converter.toTitleCase(wrapper.getMode())));
+        final Boolean climate = wrapper.isEnabled(ProfileKey.CLIMATE);
+        updateChannel(CHANNEL_GROUP_CHARGE, CHARGE_PROFILE_CLIMATE,
+                climate == null ? UnDefType.UNDEF : OnOffType.from(climate));
+        updateTimedState(wrapper, ProfileKey.WINDOWSTART);
+        updateTimedState(wrapper, ProfileKey.WINDOWEND);
+        updateTimedState(wrapper, ProfileKey.TIMER1);
+        updateTimedState(wrapper, ProfileKey.TIMER2);
+        updateTimedState(wrapper, ProfileKey.TIMER3);
+        updateTimedState(wrapper, ProfileKey.OVERRIDE);
+    }
+
+    protected void updateTimedState(ChargeProfileWrapper profile, ProfileKey key) {
+        final TimedChannel timed = ChargeProfileUtils.getTimedChannel(key);
+        if (timed != null) {
+            final LocalTime time = profile.getTime(key);
+            updateChannel(CHANNEL_GROUP_CHARGE, timed.time, time == null ? UnDefType.UNDEF
+                    : new DateTimeType(ZonedDateTime.of(Constants.EPOCH_DAY, time, ZoneId.systemDefault())));
+            if (timed.timer != null) {
+                final Boolean enabled = profile.isEnabled(key);
+                updateChannel(CHANNEL_GROUP_CHARGE, timed.timer + CHARGE_ENABLED,
+                        enabled == null ? UnDefType.UNDEF : OnOffType.from(enabled));
+                if (timed.hasDays) {
+                    final Set<DayOfWeek> days = profile.getDays(key);
+                    updateChannel(CHANNEL_GROUP_CHARGE, timed.timer + CHARGE_DAYS,
+                            days == null ? UnDefType.UNDEF : StringType.valueOf(ChargeProfileUtils.formatDays(days)));
+                    EnumSet.allOf(DayOfWeek.class).forEach(day -> {
+                        updateChannel(CHANNEL_GROUP_CHARGE, timed.timer + ChargeProfileUtils.getDaysChannel(day),
+                                days == null ? UnDefType.UNDEF : OnOffType.from(days.contains(day)));
+                    });
+                }
+            }
+        }
+    }
+
+    protected void updateDoors(Doors doorState) {
+        updateChannel(CHANNEL_GROUP_DOORS, DOOR_DRIVER_FRONT,
+                StringType.valueOf(Converter.toTitleCase(doorState.doorDriverFront)));
+        updateChannel(CHANNEL_GROUP_DOORS, DOOR_DRIVER_REAR,
+                StringType.valueOf(Converter.toTitleCase(doorState.doorDriverRear)));
+        updateChannel(CHANNEL_GROUP_DOORS, DOOR_PASSENGER_FRONT,
+                StringType.valueOf(Converter.toTitleCase(doorState.doorPassengerFront)));
+        updateChannel(CHANNEL_GROUP_DOORS, DOOR_PASSENGER_REAR,
+                StringType.valueOf(Converter.toTitleCase(doorState.doorPassengerRear)));
+        updateChannel(CHANNEL_GROUP_DOORS, TRUNK, StringType.valueOf(Converter.toTitleCase(doorState.trunk)));
+        updateChannel(CHANNEL_GROUP_DOORS, HOOD, StringType.valueOf(Converter.toTitleCase(doorState.hood)));
+    }
+
+    protected void updateWindows(Windows windowState) {
+        updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_DRIVER_FRONT,
+                StringType.valueOf(Converter.toTitleCase(windowState.windowDriverFront)));
+        updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_DRIVER_REAR,
+                StringType.valueOf(Converter.toTitleCase(windowState.windowDriverRear)));
+        updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_PASSENGER_FRONT,
+                StringType.valueOf(Converter.toTitleCase(windowState.windowPassengerFront)));
+        updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_PASSENGER_REAR,
+                StringType.valueOf(Converter.toTitleCase(windowState.windowPassengerRear)));
+        updateChannel(CHANNEL_GROUP_DOORS, WINDOW_REAR,
+                StringType.valueOf(Converter.toTitleCase(windowState.rearWindow)));
+        updateChannel(CHANNEL_GROUP_DOORS, SUNROOF, StringType.valueOf(Converter.toTitleCase(windowState.sunroof)));
+    }
+
+    protected void updatePosition(Position pos) {
+        updateChannel(CHANNEL_GROUP_LOCATION, GPS, PointType.valueOf(pos.getCoordinates()));
+        updateChannel(CHANNEL_GROUP_LOCATION, HEADING, QuantityType.valueOf(pos.heading, Units.DEGREE_ANGLE));
+    }
+
+    protected void updateVehicleStatus(VehicleStatus vStatus) {
+        // Vehicle Status
+        updateChannel(CHANNEL_GROUP_STATUS, LOCK, StringType.valueOf(Converter.toTitleCase(vStatus.doorLockState)));
+
+        // Service Updates
+        updateChannel(CHANNEL_GROUP_STATUS, SERVICE_DATE,
+                DateTimeType.valueOf(Converter.getLocalDateTime(VehicleStatusUtils.getNextServiceDate(vStatus))));
+
+        updateChannel(CHANNEL_GROUP_STATUS, SERVICE_MILEAGE,
+                QuantityType.valueOf(Converter.round(VehicleStatusUtils.getNextServiceMileage(vStatus)),
+                        imperial ? ImperialUnits.MILE : Constants.KILOMETRE_UNIT));
+        // CheckControl Active?
+        updateChannel(CHANNEL_GROUP_STATUS, CHECK_CONTROL,
+                StringType.valueOf(Converter.toTitleCase(VehicleStatusUtils.checkControlActive(vStatus))));
+        // last update Time
+        updateChannel(CHANNEL_GROUP_STATUS, LAST_UPDATE,
+                DateTimeType.valueOf(Converter.getLocalDateTime(VehicleStatusUtils.getUpdateTime(vStatus))));
+
+        Doors doorState = null;
+        try {
+            doorState = Converter.getGson().fromJson(Converter.getGson().toJson(vStatus), Doors.class);
+        } catch (JsonSyntaxException jse) {
+            logger.debug("Doors parse exception {}", jse.getMessage());
+        }
+        if (doorState != null) {
+            updateChannel(CHANNEL_GROUP_STATUS, DOORS, StringType.valueOf(VehicleStatusUtils.checkClosed(doorState)));
+            updateDoors(doorState);
+        }
+        Windows windowState = null;
+        try {
+            windowState = Converter.getGson().fromJson(Converter.getGson().toJson(vStatus), Windows.class);
+        } catch (JsonSyntaxException jse) {
+            logger.debug("Windows parse exception {}", jse.getMessage());
+        }
+        if (windowState != null) {
+            updateChannel(CHANNEL_GROUP_STATUS, WINDOWS,
+                    StringType.valueOf(VehicleStatusUtils.checkClosed(windowState)));
+            updateWindows(windowState);
+        }
+
+        // Range values
+        // based on unit of length decide if range shall be reported in km or miles
+        float totalRange = 0;
+        if (isElectric) {
+            totalRange += vStatus.remainingRangeElectric;
+            QuantityType<Length> qtElectricRange = QuantityType.valueOf(vStatus.remainingRangeElectric,
+                    Constants.KILOMETRE_UNIT);
+            QuantityType<Length> qtElectricRadius = QuantityType
+                    .valueOf(Converter.guessRangeRadius(vStatus.remainingRangeElectric), Constants.KILOMETRE_UNIT);
+
+            updateChannel(CHANNEL_GROUP_RANGE, RANGE_ELECTRIC,
+                    imperial ? Converter.getMiles(qtElectricRange) : qtElectricRange);
+            updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_ELECTRIC,
+                    imperial ? Converter.getMiles(qtElectricRadius) : qtElectricRadius);
+        }
+        if (hasFuel) {
+            totalRange += vStatus.remainingRangeFuel;
+            QuantityType<Length> qtFuelRange = QuantityType.valueOf(vStatus.remainingRangeFuel,
+                    Constants.KILOMETRE_UNIT);
+            QuantityType<Length> qtFuelRadius = QuantityType
+                    .valueOf(Converter.guessRangeRadius(vStatus.remainingRangeFuel), Constants.KILOMETRE_UNIT);
+
+            updateChannel(CHANNEL_GROUP_RANGE, RANGE_FUEL, imperial ? Converter.getMiles(qtFuelRange) : qtFuelRange);
+            updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_FUEL,
+                    imperial ? Converter.getMiles(qtFuelRadius) : qtFuelRadius);
+        }
+        if (isHybrid) {
+            QuantityType<Length> qtHybridRange = QuantityType.valueOf(totalRange, Constants.KILOMETRE_UNIT);
+            QuantityType<Length> qtHybridRadius = QuantityType.valueOf(Converter.guessRangeRadius(totalRange),
+                    Constants.KILOMETRE_UNIT);
+            updateChannel(CHANNEL_GROUP_RANGE, RANGE_HYBRID,
+                    imperial ? Converter.getMiles(qtHybridRange) : qtHybridRange);
+            updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_HYBRID,
+                    imperial ? Converter.getMiles(qtHybridRadius) : qtHybridRadius);
+        }
+
+        updateChannel(CHANNEL_GROUP_RANGE, MILEAGE,
+                QuantityType.valueOf(vStatus.mileage, imperial ? ImperialUnits.MILE : Constants.KILOMETRE_UNIT));
+        if (isElectric) {
+            updateChannel(CHANNEL_GROUP_RANGE, SOC, QuantityType.valueOf(vStatus.chargingLevelHv, Units.PERCENT));
+        }
+        if (hasFuel) {
+            updateChannel(CHANNEL_GROUP_RANGE, REMAINING_FUEL,
+                    QuantityType.valueOf(vStatus.remainingFuel, Units.LITRE));
+        }
+
+        // Charge Values
+        if (isElectric) {
+            if (vStatus.chargingStatus != null) {
+                if (Constants.INVALID.equals(vStatus.chargingStatus)) {
+                    updateChannel(CHANNEL_GROUP_STATUS, CHARGE_STATUS,
+                            StringType.valueOf(Converter.toTitleCase(vStatus.lastChargingEndReason)));
+                } else {
+                    // State INVALID is somehow misleading. Instead show the Last Charging End Reason
+                    updateChannel(CHANNEL_GROUP_STATUS, CHARGE_STATUS,
+                            StringType.valueOf(Converter.toTitleCase(vStatus.chargingStatus)));
+                }
+            } else {
+                updateChannel(CHANNEL_GROUP_STATUS, CHARGE_STATUS, UnDefType.NULL);
+            }
+            if (vStatus.chargingTimeRemaining != null) {
+                try {
+                    updateChannel(CHANNEL_GROUP_STATUS, CHARGE_REMAINING,
+                            QuantityType.valueOf(vStatus.chargingTimeRemaining, Units.MINUTE));
+                } catch (NumberFormatException nfe) {
+                    updateChannel(CHANNEL_GROUP_STATUS, CHARGE_REMAINING, UnDefType.UNDEF);
+                }
+            } else {
+                updateChannel(CHANNEL_GROUP_STATUS, CHARGE_REMAINING, UnDefType.NULL);
+            }
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java
new file mode 100644 (file)
index 0000000..035f0c1
--- /dev/null
@@ -0,0 +1,793 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.handler;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.VehicleConfiguration;
+import org.openhab.binding.bmwconnecteddrive.internal.action.BMWConnectedDriveActions;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.DestinationContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.compat.VehicleAttributesContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.AllTrips;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.AllTripsContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.LastTrip;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.LastTripContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatus;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatusContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.RemoteServiceHandler.ExecutionState;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.RemoteServiceHandler.RemoteService;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileUtils;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileUtils.ChargeKeyDay;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper.ProfileKey;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ImageProperties;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.RemoteServiceUtils;
+import org.openhab.core.io.net.http.HttpUtil;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.RawType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BridgeHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link VehicleHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - edit & send charge profile
+ */
+@NonNullByDefault
+public class VehicleHandler extends VehicleChannelHandler {
+    private int legacyMode = Constants.INT_UNDEF; // switch to legacy API in case of 404 Errors
+
+    private Optional<ConnectedDriveProxy> proxy = Optional.empty();
+    private Optional<RemoteServiceHandler> remote = Optional.empty();
+    private Optional<VehicleConfiguration> configuration = Optional.empty();
+    private Optional<ConnectedDriveBridgeHandler> bridgeHandler = Optional.empty();
+    private Optional<ScheduledFuture<?>> refreshJob = Optional.empty();
+    private Optional<ScheduledFuture<?>> editTimeout = Optional.empty();
+    private Optional<List<ResponseCallback>> callbackCounter = Optional.empty();
+
+    private ImageProperties imageProperties = new ImageProperties();
+    VehicleStatusCallback vehicleStatusCallback = new VehicleStatusCallback();
+    StringResponseCallback oldVehicleStatusCallback = new LegacyVehicleStatusCallback();
+    StringResponseCallback lastTripCallback = new LastTripCallback();
+    StringResponseCallback allTripsCallback = new AllTripsCallback();
+    StringResponseCallback chargeProfileCallback = new ChargeProfilesCallback();
+    StringResponseCallback rangeMapCallback = new RangeMapCallback();
+    DestinationsCallback destinationCallback = new DestinationsCallback();
+    ByteResponseCallback imageCallback = new ImageCallback();
+
+    private Optional<ChargeProfileWrapper> chargeProfileEdit = Optional.empty();
+    private Optional<String> chargeProfileSent = Optional.empty();
+
+    public VehicleHandler(Thing thing, BMWConnectedDriveOptionProvider op, String type, boolean imperial) {
+        super(thing, op, type, imperial);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        String group = channelUID.getGroupId();
+
+        // Refresh of Channels with cached values
+        if (command instanceof RefreshType) {
+            if (CHANNEL_GROUP_LAST_TRIP.equals(group)) {
+                lastTripCache.ifPresent(lastTrip -> lastTripCallback.onResponse(lastTrip));
+            } else if (CHANNEL_GROUP_LIFETIME.equals(group)) {
+                allTripsCache.ifPresent(allTrips -> allTripsCallback.onResponse(allTrips));
+            } else if (CHANNEL_GROUP_DESTINATION.equals(group)) {
+                destinationCache.ifPresent(destination -> destinationCallback.onResponse(destination));
+            } else if (CHANNEL_GROUP_STATUS.equals(group)) {
+                vehicleStatusCache.ifPresent(vehicleStatus -> vehicleStatusCallback.onResponse(vehicleStatus));
+            } else if (CHANNEL_GROUP_CHARGE.equals(group)) {
+                chargeProfileEdit.ifPresentOrElse(this::updateChargeProfile,
+                        () -> chargeProfileCache.ifPresent(this::updateChargeProfileFromContent));
+            } else if (CHANNEL_GROUP_VEHICLE_IMAGE.equals(group)) {
+                imageCache.ifPresent(image -> imageCallback.onResponse(image));
+            }
+            // Check for Channel Group and corresponding Actions
+        } else if (CHANNEL_GROUP_REMOTE.equals(group)) {
+            // Executing Remote Services
+            if (command instanceof StringType) {
+                String serviceCommand = ((StringType) command).toFullString();
+                remote.ifPresent(remot -> {
+                    switch (serviceCommand) {
+                        case REMOTE_SERVICE_LIGHT_FLASH:
+                        case REMOTE_SERVICE_AIR_CONDITIONING:
+                        case REMOTE_SERVICE_DOOR_LOCK:
+                        case REMOTE_SERVICE_DOOR_UNLOCK:
+                        case REMOTE_SERVICE_HORN:
+                        case REMOTE_SERVICE_VEHICLE_FINDER:
+                        case REMOTE_SERVICE_CHARGE_NOW:
+                            RemoteServiceUtils.getRemoteService(serviceCommand)
+                                    .ifPresentOrElse(service -> remot.execute(service), () -> {
+                                        logger.debug("Remote service execution {} unknown", serviceCommand);
+                                    });
+                            break;
+                        case REMOTE_SERVICE_CHARGING_CONTROL:
+                            sendChargeProfile(chargeProfileEdit);
+                            break;
+                        default:
+                            logger.debug("Remote service execution {} unknown", serviceCommand);
+                            break;
+                    }
+                });
+            }
+        } else if (CHANNEL_GROUP_VEHICLE_IMAGE.equals(group)) {
+            // Image Change
+            configuration.ifPresent(config -> {
+                if (command instanceof StringType) {
+                    if (channelUID.getIdWithoutGroup().equals(IMAGE_VIEWPORT)) {
+                        String newViewport = command.toString();
+                        synchronized (imageProperties) {
+                            if (!imageProperties.viewport.equals(newViewport)) {
+                                imageProperties = new ImageProperties(newViewport, imageProperties.size);
+                                imageCache = Optional.empty();
+                                proxy.ifPresent(prox -> prox.requestImage(config, imageProperties, imageCallback));
+                            }
+                        }
+                        updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, StringType.valueOf(newViewport));
+                    }
+                }
+                if (command instanceof DecimalType) {
+                    if (command instanceof DecimalType) {
+                        int newImageSize = ((DecimalType) command).intValue();
+                        if (channelUID.getIdWithoutGroup().equals(IMAGE_SIZE)) {
+                            synchronized (imageProperties) {
+                                if (imageProperties.size != newImageSize) {
+                                    imageProperties = new ImageProperties(imageProperties.viewport, newImageSize);
+                                    imageCache = Optional.empty();
+                                    proxy.ifPresent(prox -> prox.requestImage(config, imageProperties, imageCallback));
+                                }
+                            }
+                        }
+                        updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_SIZE, new DecimalType(newImageSize));
+                    }
+                }
+            });
+        } else if (CHANNEL_GROUP_DESTINATION.equals(group)) {
+            if (command instanceof StringType) {
+                int index = Converter.getIndex(command.toFullString());
+                if (index != -1) {
+                    selectDestination(index);
+                } else {
+                    logger.debug("Cannot select Destination index {}", command.toFullString());
+                }
+            }
+        } else if (CHANNEL_GROUP_SERVICE.equals(group)) {
+            if (command instanceof StringType) {
+                int index = Converter.getIndex(command.toFullString());
+                if (index != -1) {
+                    selectService(index);
+                } else {
+                    logger.debug("Cannot select Service index {}", command.toFullString());
+                }
+            }
+        } else if (CHANNEL_GROUP_CHECK_CONTROL.equals(group)) {
+            if (command instanceof StringType) {
+                int index = Converter.getIndex(command.toFullString());
+                if (index != -1) {
+                    selectCheckControl(index);
+                } else {
+                    logger.debug("Cannot select CheckControl index {}", command.toFullString());
+                }
+            }
+        } else if (CHANNEL_GROUP_CHARGE.equals(group)) {
+            handleChargeProfileCommand(channelUID, command);
+        }
+    }
+
+    @Override
+    public void initialize() {
+        callbackCounter = Optional.of(new ArrayList<ResponseCallback>());
+        updateStatus(ThingStatus.UNKNOWN);
+        final VehicleConfiguration config = getConfigAs(VehicleConfiguration.class);
+        configuration = Optional.of(config);
+        Bridge bridge = getBridge();
+        if (bridge != null) {
+            BridgeHandler handler = bridge.getHandler();
+            if (handler != null) {
+                bridgeHandler = Optional.of(((ConnectedDriveBridgeHandler) handler));
+                proxy = ((ConnectedDriveBridgeHandler) handler).getProxy();
+                remote = proxy.map(prox -> prox.getRemoteServiceHandler(this));
+            } else {
+                logger.debug("Bridge Handler null");
+            }
+        } else {
+            logger.debug("Bridge null");
+        }
+
+        // get Image after init with config values
+        synchronized (imageProperties) {
+            imageProperties = new ImageProperties(config.imageViewport, config.imageSize);
+        }
+        updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, StringType.valueOf((config.imageViewport)));
+        updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_SIZE, new DecimalType((config.imageSize)));
+
+        // check imperial setting is different to AutoDetect
+        if (!UNITS_AUTODETECT.equals(config.units)) {
+            imperial = UNITS_IMPERIAL.equals(config.units);
+        }
+
+        // start update schedule
+        startSchedule(config.refreshInterval);
+    }
+
+    private void startSchedule(int interval) {
+        refreshJob.ifPresentOrElse(job -> {
+            if (job.isCancelled()) {
+                refreshJob = Optional
+                        .of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES));
+            } // else - scheduler is already running!
+        }, () -> {
+            refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES));
+        });
+    }
+
+    @Override
+    public void dispose() {
+        refreshJob.ifPresent(job -> job.cancel(true));
+        editTimeout.ifPresent(job -> job.cancel(true));
+        remote.ifPresent(RemoteServiceHandler::cancel);
+    }
+
+    public void getData() {
+        proxy.ifPresentOrElse(prox -> {
+            configuration.ifPresentOrElse(config -> {
+                if (legacyMode == 1) {
+                    prox.requestLegacyVehcileStatus(config, oldVehicleStatusCallback);
+                } else {
+                    prox.requestVehcileStatus(config, vehicleStatusCallback);
+                }
+                addCallback(vehicleStatusCallback);
+                if (isSupported(Constants.STATISTICS)) {
+                    prox.requestLastTrip(config, lastTripCallback);
+                    prox.requestAllTrips(config, allTripsCallback);
+                    addCallback(lastTripCallback);
+                    addCallback(allTripsCallback);
+                }
+                if (isSupported(Constants.LAST_DESTINATIONS)) {
+                    prox.requestDestinations(config, destinationCallback);
+                    addCallback(destinationCallback);
+                }
+                if (isElectric) {
+                    prox.requestChargingProfile(config, chargeProfileCallback);
+                    addCallback(chargeProfileCallback);
+                }
+                synchronized (imageProperties) {
+                    if (!imageCache.isPresent() && !imageProperties.failLimitReached()) {
+                        prox.requestImage(config, imageProperties, imageCallback);
+                        addCallback(imageCallback);
+                    }
+                }
+            }, () -> {
+                logger.warn("ConnectedDrive Configuration isn't present");
+            });
+        }, () -> {
+            logger.warn("ConnectedDrive Proxy isn't present");
+        });
+    }
+
+    private synchronized void addCallback(ResponseCallback rc) {
+        callbackCounter.ifPresent(counter -> counter.add(rc));
+    }
+
+    private synchronized void removeCallback(ResponseCallback rc) {
+        callbackCounter.ifPresent(counter -> {
+            counter.remove(rc);
+            // all necessary callbacks received => print and set to empty
+            if (counter.isEmpty()) {
+                logFingerPrint();
+                callbackCounter = Optional.empty();
+            }
+        });
+    }
+
+    private void logFingerPrint() {
+        final String vin = configuration.map(config -> config.vin).orElse("");
+        logger.debug("###### Vehicle Troubleshoot Fingerprint Data - BEGIN ######");
+        logger.debug("### Discovery Result ###");
+        bridgeHandler.ifPresent(handler -> {
+            logger.debug("{}", handler.getDiscoveryFingerprint());
+        });
+        vehicleStatusCache.ifPresentOrElse(vehicleStatus -> {
+            logger.debug("### Vehicle Status ###");
+
+            // Anonymous data for VIN and Position
+            try {
+                VehicleStatusContainer container = Converter.getGson().fromJson(vehicleStatus,
+                        VehicleStatusContainer.class);
+                if (container != null) {
+                    VehicleStatus status = container.vehicleStatus;
+                    if (status != null) {
+                        status.vin = Constants.ANONYMOUS;
+                        if (status.position != null) {
+                            status.position.lat = -1;
+                            status.position.lon = -1;
+                            status.position.heading = -1;
+                        }
+                    }
+                }
+                logger.debug("{}", Converter.getGson().toJson(container));
+            } catch (JsonSyntaxException jse) {
+                logger.debug("{}", jse.getMessage());
+            }
+        }, () -> {
+            logger.debug("### Vehicle Status Empty ###");
+        });
+        lastTripCache.ifPresentOrElse(lastTrip -> {
+            logger.debug("### Last Trip ###");
+            logger.debug("{}", lastTrip.replaceAll(vin, Constants.ANONYMOUS));
+        }, () -> {
+            logger.debug("### Last Trip Empty ###");
+        });
+        allTripsCache.ifPresentOrElse(allTrips -> {
+            logger.debug("### All Trips ###");
+            logger.debug("{}", allTrips.replaceAll(vin, Constants.ANONYMOUS));
+        }, () -> {
+            logger.debug("### All Trips Empty ###");
+        });
+        if (isElectric) {
+            chargeProfileCache.ifPresentOrElse(chargeProfile -> {
+                logger.debug("### Charge Profile ###");
+                logger.debug("{}", chargeProfile.replaceAll(vin, Constants.ANONYMOUS));
+            }, () -> {
+                logger.debug("### Charge Profile Empty ###");
+            });
+        }
+        destinationCache.ifPresentOrElse(destination -> {
+            logger.debug("### Charge Profile ###");
+            try {
+                DestinationContainer container = Converter.getGson().fromJson(destination, DestinationContainer.class);
+                if (container != null) {
+                    if (container.destinations != null) {
+                        container.destinations.forEach(entry -> {
+                            entry.lat = 0;
+                            entry.lon = 0;
+                            entry.city = Constants.ANONYMOUS;
+                            entry.street = Constants.ANONYMOUS;
+                            entry.streetNumber = Constants.ANONYMOUS;
+                            entry.country = Constants.ANONYMOUS;
+                        });
+                        logger.debug("{}", Converter.getGson().toJson(container));
+                    }
+                } else {
+                    logger.debug("### Destinations Empty ###");
+                }
+            } catch (JsonSyntaxException jse) {
+                logger.debug("{}", jse.getMessage());
+            }
+        }, () -> {
+            logger.debug("### Charge Profile Empty ###");
+        });
+        rangeMapCache.ifPresentOrElse(rangeMap -> {
+            logger.debug("### Range Map ###");
+            logger.debug("{}", rangeMap.replaceAll(vin, Constants.ANONYMOUS));
+        }, () -> {
+            logger.debug("### Range Map Empty ###");
+        });
+        logger.debug("###### Vehicle Troubleshoot Fingerprint Data - END ######");
+    }
+
+    /**
+     * Don't stress ConnectedDrive with unnecessary requests. One call at the beginning is done to check the response.
+     * After cache has e.g. a proper error response it will be shown in the fingerprint
+     *
+     * @return
+     */
+    private boolean isSupported(String service) {
+        final String services = thing.getProperties().get(Constants.SERVICES_SUPPORTED);
+        if (services != null) {
+            if (services.contains(service)) {
+                return true;
+            }
+        }
+        // if cache is empty give it a try one time to collected Troubleshoot data
+        return lastTripCache.isEmpty() || allTripsCache.isEmpty() || destinationCache.isEmpty();
+    }
+
+    public void updateRemoteExecutionStatus(@Nullable String service, @Nullable String status) {
+        if (RemoteService.CHARGING_CONTROL.toString().equals(service)
+                && ExecutionState.EXECUTED.name().equals(status)) {
+            saveChargeProfileSent();
+        }
+        updateChannel(CHANNEL_GROUP_REMOTE, REMOTE_STATE, StringType
+                .valueOf(Converter.toTitleCase((service == null ? "-" : service) + Constants.SPACE + status)));
+    }
+
+    public Optional<VehicleConfiguration> getConfiguration() {
+        return configuration;
+    }
+
+    public ScheduledExecutorService getScheduler() {
+        return scheduler;
+    }
+
+    /**
+     * Callbacks for ConnectedDrive Portal
+     *
+     * @author Bernd Weymann
+     *
+     */
+    public class ChargeProfilesCallback implements StringResponseCallback {
+        @Override
+        public void onResponse(@Nullable String content) {
+            if (content != null) {
+                chargeProfileCache = Optional.of(content);
+                if (chargeProfileEdit.isEmpty()) {
+                    updateChargeProfileFromContent(content);
+                }
+            }
+            removeCallback(this);
+        }
+
+        /**
+         * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
+         */
+        @Override
+        public void onError(NetworkError error) {
+            logger.debug("{}", error.toString());
+            chargeProfileCache = Optional.of(Converter.getGson().toJson(error));
+            removeCallback(this);
+        }
+    }
+
+    public class RangeMapCallback implements StringResponseCallback {
+        @Override
+        public void onResponse(@Nullable String content) {
+            rangeMapCache = Optional.ofNullable(content);
+            removeCallback(this);
+        }
+
+        /**
+         * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
+         */
+        @Override
+        public void onError(NetworkError error) {
+            logger.debug("{}", error.toString());
+            rangeMapCache = Optional.of(Converter.getGson().toJson(error));
+            removeCallback(this);
+        }
+    }
+
+    public class DestinationsCallback implements StringResponseCallback {
+
+        @Override
+        public void onResponse(@Nullable String content) {
+            destinationCache = Optional.ofNullable(content);
+            if (content != null) {
+                try {
+                    DestinationContainer dc = Converter.getGson().fromJson(content, DestinationContainer.class);
+                    if (dc != null && dc.destinations != null) {
+                        updateDestinations(dc.destinations);
+                    }
+                } catch (JsonSyntaxException jse) {
+                    logger.debug("{}", jse.getMessage());
+                }
+            }
+            removeCallback(this);
+        }
+
+        /**
+         * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
+         */
+        @Override
+        public void onError(NetworkError error) {
+            logger.debug("{}", error.toString());
+            destinationCache = Optional.of(Converter.getGson().toJson(error));
+            removeCallback(this);
+        }
+    }
+
+    public class ImageCallback implements ByteResponseCallback {
+        @Override
+        public void onResponse(byte[] content) {
+            if (content.length > 0) {
+                imageCache = Optional.of(content);
+                String contentType = HttpUtil.guessContentTypeFromData(content);
+                updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_FORMAT, new RawType(content, contentType));
+            } else {
+                synchronized (imageProperties) {
+                    imageProperties.failed();
+                }
+            }
+            removeCallback(this);
+        }
+
+        /**
+         * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
+         */
+        @Override
+        public void onError(NetworkError error) {
+            logger.debug("{}", error.toString());
+            synchronized (imageProperties) {
+                imageProperties.failed();
+            }
+            removeCallback(this);
+        }
+    }
+
+    public class AllTripsCallback implements StringResponseCallback {
+        @Override
+        public void onResponse(@Nullable String content) {
+            if (content != null) {
+                allTripsCache = Optional.of(content);
+                try {
+                    AllTripsContainer atc = Converter.getGson().fromJson(content, AllTripsContainer.class);
+                    if (atc != null) {
+                        AllTrips at = atc.allTrips;
+                        if (at != null) {
+                            updateAllTrips(at);
+                        }
+                    }
+                } catch (JsonSyntaxException jse) {
+                    logger.debug("{}", jse.getMessage());
+                }
+            }
+            removeCallback(this);
+        }
+
+        /**
+         * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
+         */
+        @Override
+        public void onError(NetworkError error) {
+            logger.debug("{}", error.toString());
+            allTripsCache = Optional.of(Converter.getGson().toJson(error));
+            removeCallback(this);
+        }
+    }
+
+    public class LastTripCallback implements StringResponseCallback {
+        @Override
+        public void onResponse(@Nullable String content) {
+            if (content != null) {
+                lastTripCache = Optional.of(content);
+                try {
+                    LastTripContainer lt = Converter.getGson().fromJson(content, LastTripContainer.class);
+                    if (lt != null) {
+                        LastTrip trip = lt.lastTrip;
+                        if (trip != null) {
+                            updateLastTrip(trip);
+                        }
+                    }
+                } catch (JsonSyntaxException jse) {
+                    logger.debug("{}", jse.getMessage());
+                }
+            }
+            removeCallback(this);
+        }
+
+        /**
+         * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
+         */
+        @Override
+        public void onError(NetworkError error) {
+            logger.debug("{}", error.toString());
+            lastTripCache = Optional.of(Converter.getGson().toJson(error));
+            removeCallback(this);
+        }
+    }
+
+    /**
+     * The VehicleStatus is supported by all Vehicle Types so it's used to reflect the Thing Status
+     */
+    public class VehicleStatusCallback implements StringResponseCallback {
+        @Override
+        public void onResponse(@Nullable String content) {
+            if (content != null) {
+                // switch to non legacy mode
+                legacyMode = 0;
+                updateStatus(ThingStatus.ONLINE);
+                vehicleStatusCache = Optional.of(content);
+                try {
+                    VehicleStatusContainer status = Converter.getGson().fromJson(content, VehicleStatusContainer.class);
+                    if (status != null) {
+                        VehicleStatus vStatus = status.vehicleStatus;
+                        if (vStatus == null) {
+                            return;
+                        }
+                        updateVehicleStatus(vStatus);
+                        updateCheckControls(vStatus.checkControlMessages);
+                        updateServices(vStatus.cbsData);
+                        updatePosition(vStatus.position);
+                    }
+                } catch (JsonSyntaxException jse) {
+                    logger.debug("{}", jse.getMessage());
+                }
+            }
+            removeCallback(this);
+        }
+
+        @Override
+        public void onError(NetworkError error) {
+            logger.debug("{}", error.toString());
+            // only if legacyMode isn't set yet try legacy API
+            if (error.status != 200 && legacyMode == Constants.INT_UNDEF) {
+                logger.debug("VehicleStatus not found - try legacy API");
+                proxy.get().requestLegacyVehcileStatus(configuration.get(), oldVehicleStatusCallback);
+            }
+            vehicleStatusCache = Optional.of(Converter.getGson().toJson(error));
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error.reason);
+            removeCallback(this);
+        }
+    }
+
+    /**
+     * Fallback API if origin isn't supported.
+     * This comes from the Community Discussion where a Vehicle from 2015 answered with "404"
+     * https://community.openhab.org/t/bmw-connecteddrive-binding/105124
+     *
+     * Selection of API was discussed here
+     * https://community.openhab.org/t/bmw-connecteddrive-bmw-i3/103876
+     *
+     * I figured out that only one API was working for this Vehicle. So this backward compatible Callback is introduced.
+     * The delivered data is converted into the origin dto object so no changes in previous functional code needed
+     */
+    public class LegacyVehicleStatusCallback implements StringResponseCallback {
+        @Override
+        public void onResponse(@Nullable String content) {
+            if (content != null) {
+                try {
+                    VehicleAttributesContainer vac = Converter.getGson().fromJson(content,
+                            VehicleAttributesContainer.class);
+                    vehicleStatusCallback.onResponse(Converter.transformLegacyStatus(vac));
+                    legacyMode = 1;
+                    logger.debug("VehicleStatus switched to legacy mode");
+                } catch (JsonSyntaxException jse) {
+                    logger.debug("{}", jse.getMessage());
+                }
+            }
+        }
+
+        @Override
+        public void onError(NetworkError error) {
+            logger.debug("{}", error.toString());
+            vehicleStatusCallback.onError(error);
+        }
+    }
+
+    private void handleChargeProfileCommand(ChannelUID channelUID, Command command) {
+        if (chargeProfileEdit.isEmpty()) {
+            chargeProfileEdit = getChargeProfileWrapper();
+        }
+
+        chargeProfileEdit.ifPresent(profile -> {
+
+            boolean processed = false;
+
+            final String id = channelUID.getIdWithoutGroup();
+
+            if (command instanceof StringType) {
+                final String stringCommand = ((StringType) command).toFullString();
+                switch (id) {
+                    case CHARGE_PROFILE_PREFERENCE:
+                        profile.setPreference(stringCommand);
+                        updateChannel(CHANNEL_GROUP_CHARGE, CHARGE_PROFILE_PREFERENCE,
+                                StringType.valueOf(Converter.toTitleCase(profile.getPreference())));
+                        processed = true;
+                        break;
+                    case CHARGE_PROFILE_MODE:
+                        profile.setMode(stringCommand);
+                        updateChannel(CHANNEL_GROUP_CHARGE, CHARGE_PROFILE_MODE,
+                                StringType.valueOf(Converter.toTitleCase(profile.getMode())));
+                        processed = true;
+                        break;
+                    default:
+                        break;
+                }
+            } else if (command instanceof OnOffType) {
+                final ProfileKey enableKey = ChargeProfileUtils.getEnableKey(id);
+                if (enableKey != null) {
+                    profile.setEnabled(enableKey, OnOffType.ON.equals(command));
+                    updateTimedState(profile, enableKey);
+                    processed = true;
+                } else {
+                    final ChargeKeyDay chargeKeyDay = ChargeProfileUtils.getKeyDay(id);
+                    if (chargeKeyDay != null) {
+                        profile.setDayEnabled(chargeKeyDay.key, chargeKeyDay.day, OnOffType.ON.equals(command));
+                        updateTimedState(profile, chargeKeyDay.key);
+                        processed = true;
+                    }
+                }
+            } else if (command instanceof DateTimeType) {
+                DateTimeType dtt = (DateTimeType) command;
+                logger.debug("Accept {} for ID {}", dtt.toFullString(), id);
+                final ProfileKey key = ChargeProfileUtils.getTimeKey(id);
+                if (key != null) {
+                    profile.setTime(key, dtt.getZonedDateTime().toLocalTime());
+                    updateTimedState(profile, key);
+                    processed = true;
+                }
+            }
+
+            if (processed) {
+                // cancel current timer and add another 5 mins - valid for each edit
+                editTimeout.ifPresent(timeout -> timeout.cancel(true));
+                // start edit timer with 5 min timeout
+                editTimeout = Optional.of(scheduler.schedule(() -> {
+                    editTimeout = Optional.empty();
+                    chargeProfileEdit = Optional.empty();
+                    chargeProfileCache.ifPresent(this::updateChargeProfileFromContent);
+                }, 5, TimeUnit.MINUTES));
+            } else {
+                logger.debug("unexpected command {} not processed", command.toFullString());
+            }
+        });
+    }
+
+    private void saveChargeProfileSent() {
+        editTimeout.ifPresent(timeout -> {
+            timeout.cancel(true);
+            editTimeout = Optional.empty();
+        });
+        chargeProfileSent.ifPresent(sent -> {
+            chargeProfileCache = Optional.of(sent);
+            chargeProfileSent = Optional.empty();
+            chargeProfileEdit = Optional.empty();
+            chargeProfileCache.ifPresent(this::updateChargeProfileFromContent);
+        });
+    }
+
+    @Override
+    public Collection<Class<? extends ThingHandlerService>> getServices() {
+        return Set.of(BMWConnectedDriveActions.class);
+    }
+
+    public Optional<ChargeProfileWrapper> getChargeProfileWrapper() {
+        return chargeProfileCache.flatMap(cache -> {
+            return ChargeProfileWrapper.fromJson(cache).map(wrapper -> {
+                return wrapper;
+            }).or(() -> {
+                logger.debug("cannot parse charging profile: {}", cache);
+                return Optional.empty();
+            });
+        }).or(() -> {
+            logger.debug("No ChargeProfile recieved so far - cannot start editing");
+            return Optional.empty();
+        });
+    }
+
+    public void sendChargeProfile(Optional<ChargeProfileWrapper> profile) {
+        profile.map(profil -> profil.getJson()).ifPresent(json -> {
+            logger.debug("sending charging profile: {}", json);
+            chargeProfileSent = Optional.of(json);
+            remote.ifPresent(rem -> rem.execute(RemoteService.CHARGING_CONTROL, json));
+        });
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/simulation/Injector.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/simulation/Injector.java
new file mode 100644 (file)
index 0000000..fdc7208
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.handler.simulation;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link Injector} Simulates feedback of the ConnectedDrive Portal
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class Injector {
+    private static boolean active = false;
+
+    // copy discovery json here
+    private static String discovery = "";
+
+    // copy vehicle status json here
+    private static String status = "";
+
+    public static boolean isActive() {
+        return active;
+    }
+
+    public static String getDiscovery() {
+        return discovery;
+    }
+
+    public static String getStatus() {
+        return status;
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/BimmerConstants.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/BimmerConstants.java
new file mode 100644 (file)
index 0000000..5c0ed64
--- /dev/null
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.utils;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link BimmerConstants} This class holds the important constants for the BMW Connected Drive Authorization. They
+ * are taken from the Bimmercode from github {@link https://github.com/bimmerconnected/bimmer_connected}
+ * File defining these constants
+ * {@link https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/account.py}
+ * https://customer.bmwgroup.com/one/app/oauth.js
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class BimmerConstants {
+
+    // https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/country_selector.py
+    public static final String REGION_NORTH_AMERICA = "NORTH_AMERICA";
+    public static final String REGION_CHINA = "CHINA";
+    public static final String REGION_ROW = "ROW";
+
+    // https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/country_selector.py
+    public static final String AUTH_SERVER_NORTH_AMERICA = "b2vapi.bmwgroup.us/gcdm";
+    public static final String AUTH_SERVER_CHINA = "b2vapi.bmwgroup.cn/gcdm";
+    public static final String AUTH_SERVER_ROW = "b2vapi.bmwgroup.com/gcdm";
+    public static final Map<String, String> AUTH_SERVER_MAP = Map.of(REGION_NORTH_AMERICA, AUTH_SERVER_NORTH_AMERICA,
+            REGION_CHINA, AUTH_SERVER_CHINA, REGION_ROW, AUTH_SERVER_ROW);
+
+    public static final String OAUTH_ENDPOINT = "/oauth/token";
+
+    public static final String SERVER_NORTH_AMERICA = "b2vapi.bmwgroup.us";
+    public static final String SERVER_CHINA = "b2vapi.bmwgroup.cn:8592";
+    public static final String SERVER_ROW = "b2vapi.bmwgroup.com";
+    public static final Map<String, String> SERVER_MAP = Map.of(REGION_NORTH_AMERICA, SERVER_NORTH_AMERICA,
+            REGION_CHINA, SERVER_CHINA, REGION_ROW, SERVER_ROW);
+
+    // see https://github.com/bimmerconnected/bimmer_connected/pull/252/files
+    public static final Map<String, String> AUTHORIZATION_VALUE_MAP = Map.of(REGION_NORTH_AMERICA,
+            "Basic ZDc2NmI1MzctYTY1NC00Y2JkLWEzZGMtMGNhNTY3MmQ3ZjhkOjE1ZjY5N2Y2LWE1ZDUtNGNhZC05OWQ5LTNhMTViYzdmMzk3Mw==",
+            REGION_CHINA,
+            "Basic blF2NkNxdHhKdVhXUDc0eGYzQ0p3VUVQOjF6REh4NnVuNGNEanliTEVOTjNreWZ1bVgya0VZaWdXUGNRcGR2RFJwSUJrN3JPSg==",
+            REGION_ROW,
+            "Basic ZDc2NmI1MzctYTY1NC00Y2JkLWEzZGMtMGNhNTY3MmQ3ZjhkOjE1ZjY5N2Y2LWE1ZDUtNGNhZC05OWQ5LTNhMTViYzdmMzk3Mw==");
+
+    public static final String CREDENTIAL_VALUES = "nQv6CqtxJuXWP74xf3CJwUEP:1zDHx6un4cDjybLENN3kyfumX2kEYigWPcQpdvDRpIBk7rOJ";
+    public static final String REDIRECT_URI_VALUE = "https://www.bmw-connecteddrive.com/app/static/external-dispatch.html";
+    public static final String SCOPE_VALUES = "authenticate_user vehicle_data remote_services";
+
+    public static final String LEGACY_CREDENTIAL_VALUES = "nQv6CqtxJuXWP74xf3CJwUEP:1zDHx6un4cDjybLENN3kyfumX2kEYigWPcQpdvDRpIBk7rOJ";
+    public static final String REFERER_URL = "https://www.bmw-connecteddrive.de/app/index.html";
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/ChargeProfileUtils.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/ChargeProfileUtils.java
new file mode 100644 (file)
index 0000000..67da7ae
--- /dev/null
@@ -0,0 +1,136 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.utils;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
+
+import java.time.DayOfWeek;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper.ProfileKey;
+
+/**
+ * The {@link ChargeProfileUtils} utility functions for charging profiles
+ *
+ * @author Norbert Truchsess - initial contribution
+ */
+@NonNullByDefault
+public class ChargeProfileUtils {
+
+    // Charging
+    public static class TimedChannel {
+        public final String time;
+        public final @Nullable String timer;
+        public final boolean hasDays;
+
+        TimedChannel(final String time, @Nullable final String timer, final boolean hasDays) {
+            this.time = time;
+            this.timer = timer;
+            this.hasDays = hasDays;
+        }
+    }
+
+    @SuppressWarnings("serial")
+    private static final Map<ProfileKey, TimedChannel> TIMED_CHANNELS = new HashMap<>() {
+        {
+            put(ProfileKey.WINDOWSTART, new TimedChannel(CHARGE_WINDOW_START, null, false));
+            put(ProfileKey.WINDOWEND, new TimedChannel(CHARGE_WINDOW_END, null, false));
+            put(ProfileKey.TIMER1, new TimedChannel(CHARGE_TIMER1 + CHARGE_DEPARTURE, CHARGE_TIMER1, true));
+            put(ProfileKey.TIMER2, new TimedChannel(CHARGE_TIMER2 + CHARGE_DEPARTURE, CHARGE_TIMER2, true));
+            put(ProfileKey.TIMER3, new TimedChannel(CHARGE_TIMER3 + CHARGE_DEPARTURE, CHARGE_TIMER3, true));
+            put(ProfileKey.OVERRIDE, new TimedChannel(CHARGE_OVERRIDE + CHARGE_DEPARTURE, CHARGE_OVERRIDE, false));
+        }
+    };
+
+    @SuppressWarnings("serial")
+    private static final Map<DayOfWeek, String> DAY_CHANNELS = new HashMap<>() {
+        {
+            put(DayOfWeek.MONDAY, CHARGE_DAY_MON);
+            put(DayOfWeek.TUESDAY, CHARGE_DAY_TUE);
+            put(DayOfWeek.WEDNESDAY, CHARGE_DAY_WED);
+            put(DayOfWeek.THURSDAY, CHARGE_DAY_THU);
+            put(DayOfWeek.FRIDAY, CHARGE_DAY_FRI);
+            put(DayOfWeek.SATURDAY, CHARGE_DAY_SAT);
+            put(DayOfWeek.SUNDAY, CHARGE_DAY_SUN);
+        }
+    };
+
+    public static class ChargeKeyDay {
+        public final ProfileKey key;
+        public final DayOfWeek day;
+
+        ChargeKeyDay(final ProfileKey key, final DayOfWeek day) {
+            this.key = key;
+            this.day = day;
+        }
+    }
+
+    @SuppressWarnings("serial")
+    private static final Map<String, ProfileKey> CHARGE_ENABLED_CHANNEL_KEYS = new HashMap<>() {
+        {
+            TIMED_CHANNELS.forEach((key, channel) -> {
+                put(channel.timer + CHARGE_ENABLED, key);
+            });
+            put(CHARGE_PROFILE_CLIMATE, ProfileKey.CLIMATE);
+        }
+    };
+
+    @SuppressWarnings("serial")
+    private static final Map<String, ProfileKey> CHARGE_TIME_CHANNEL_KEYS = new HashMap<>() {
+        {
+            TIMED_CHANNELS.forEach((key, channel) -> {
+                put(channel.time, key);
+            });
+        }
+    };
+
+    @SuppressWarnings("serial")
+    private static final Map<String, ChargeKeyDay> CHARGE_DAYS_CHANNEL_KEYS = new HashMap<>() {
+        {
+            DAY_CHANNELS.forEach((dayOfWeek, dayChannel) -> {
+                put(CHARGE_TIMER1 + dayChannel, new ChargeKeyDay(ProfileKey.TIMER1, dayOfWeek));
+                put(CHARGE_TIMER2 + dayChannel, new ChargeKeyDay(ProfileKey.TIMER2, dayOfWeek));
+                put(CHARGE_TIMER3 + dayChannel, new ChargeKeyDay(ProfileKey.TIMER3, dayOfWeek));
+            });
+        }
+    };
+
+    public static @Nullable TimedChannel getTimedChannel(ProfileKey key) {
+        return TIMED_CHANNELS.get(key);
+    }
+
+    public static @Nullable String getDaysChannel(DayOfWeek day) {
+        return DAY_CHANNELS.get(day);
+    }
+
+    public static @Nullable ProfileKey getEnableKey(final String id) {
+        return CHARGE_ENABLED_CHANNEL_KEYS.get(id);
+    }
+
+    public static @Nullable ChargeKeyDay getKeyDay(final String id) {
+        return CHARGE_DAYS_CHANNEL_KEYS.get(id);
+    }
+
+    public static @Nullable ProfileKey getTimeKey(final String id) {
+        return CHARGE_TIME_CHANNEL_KEYS.get(id);
+    }
+
+    public static String formatDays(final Set<DayOfWeek> weekdays) {
+        return weekdays.stream().map(day -> Constants.DAYS.get(day)).collect(Collectors.joining(Constants.COMMA));
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/ChargeProfileWrapper.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/ChargeProfileWrapper.java
new file mode 100644 (file)
index 0000000..83a7276
--- /dev/null
@@ -0,0 +1,299 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.utils;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper.ProfileKey.*;
+import static org.openhab.binding.bmwconnecteddrive.internal.utils.Constants.*;
+
+import java.time.DayOfWeek;
+import java.time.LocalTime;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.ChargingMode;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.ChargingPreference;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.charge.ChargeProfile;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.charge.ChargingWindow;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.charge.Timer;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.charge.WeeklyPlanner;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link ChargeProfileWrapper} Wrapper for ChargeProfiles
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - add ChargeProfileActions
+ */
+@NonNullByDefault
+public class ChargeProfileWrapper {
+    private static final Logger LOGGER = LoggerFactory.getLogger(ChargeProfileWrapper.class);
+
+    public enum ProfileType {
+        WEEKLY,
+        TWO_TIMES,
+        EMPTY
+    }
+
+    public enum ProfileKey {
+        CLIMATE,
+        TIMER1,
+        TIMER2,
+        TIMER3,
+        TIMER4,
+        OVERRIDE,
+        WINDOWSTART,
+        WINDOWEND
+    }
+
+    protected final ProfileType type;
+
+    private Optional<ChargingMode> mode = Optional.empty();
+    private Optional<ChargingPreference> preference = Optional.empty();
+
+    private final Map<ProfileKey, Boolean> enabled = new HashMap<>();
+    private final Map<ProfileKey, LocalTime> times = new HashMap<>();
+    private final Map<ProfileKey, Set<DayOfWeek>> daysOfWeek = new HashMap<>();
+
+    public static Optional<ChargeProfileWrapper> fromJson(final String content) {
+        try {
+            final ChargeProfile cp = Converter.getGson().fromJson(content, ChargeProfile.class);
+            if (cp != null) {
+                return Optional.of(new ChargeProfileWrapper(cp));
+            }
+        } catch (JsonSyntaxException jse) {
+            LOGGER.debug("ChargeProfile unparsable: {}", content);
+        }
+        return Optional.empty();
+    }
+
+    private ChargeProfileWrapper(final ChargeProfile profile) {
+        final WeeklyPlanner planner;
+
+        if (profile.weeklyPlanner != null) {
+            type = ProfileType.WEEKLY;
+            planner = profile.weeklyPlanner;
+        } else if (profile.twoTimesTimer != null) {
+            type = ProfileType.TWO_TIMES;
+            planner = profile.twoTimesTimer;
+            // timer days not supported
+        } else {
+            type = ProfileType.EMPTY;
+            return;
+        }
+
+        setPreference(planner.chargingPreferences);
+        setMode(planner.chargingMode);
+
+        setEnabled(CLIMATE, planner.climatizationEnabled);
+
+        addTimer(TIMER1, planner.timer1);
+        addTimer(TIMER2, planner.timer2);
+
+        if (planner.preferredChargingWindow != null) {
+            addTime(WINDOWSTART, planner.preferredChargingWindow.startTime);
+            addTime(WINDOWEND, planner.preferredChargingWindow.endTime);
+        } else {
+            preference.ifPresent(pref -> {
+                if (ChargingPreference.CHARGING_WINDOW.equals(pref)) {
+                    addTime(WINDOWSTART, null);
+                    addTime(WINDOWEND, null);
+                }
+            });
+        }
+
+        if (isWeekly()) {
+            addTimer(TIMER3, planner.timer3);
+            addTimer(OVERRIDE, planner.overrideTimer);
+        }
+    }
+
+    public @Nullable Boolean isEnabled(final ProfileKey key) {
+        return enabled.get(key);
+    }
+
+    public void setEnabled(final ProfileKey key, @Nullable final Boolean enabled) {
+        if (enabled == null) {
+            this.enabled.remove(key);
+        } else {
+            this.enabled.put(key, enabled);
+        }
+    }
+
+    public @Nullable String getMode() {
+        return mode.map(m -> m.name()).orElse(null);
+    }
+
+    public void setMode(final @Nullable String mode) {
+        if (mode != null) {
+            try {
+                this.mode = Optional.of(ChargingMode.valueOf(mode));
+                return;
+            } catch (IllegalArgumentException iae) {
+                LOGGER.warn("unexpected value for chargingMode: {}", mode);
+            }
+        }
+        this.mode = Optional.empty();
+    }
+
+    public @Nullable String getPreference() {
+        return preference.map(pref -> pref.name()).orElse(null);
+    }
+
+    public void setPreference(final @Nullable String preference) {
+        if (preference != null) {
+            try {
+                this.preference = Optional.of(ChargingPreference.valueOf(preference));
+                return;
+            } catch (IllegalArgumentException iae) {
+                LOGGER.warn("unexpected value for chargingPreference: {}", preference);
+            }
+        }
+        this.preference = Optional.empty();
+    }
+
+    public @Nullable Set<DayOfWeek> getDays(final ProfileKey key) {
+        return daysOfWeek.get(key);
+    }
+
+    public void setDays(final ProfileKey key, final @Nullable Set<DayOfWeek> days) {
+        if (days == null) {
+            daysOfWeek.remove(key);
+        } else {
+            daysOfWeek.put(key, days);
+        }
+    }
+
+    public void setDayEnabled(final ProfileKey key, final DayOfWeek day, final boolean enabled) {
+        final Set<DayOfWeek> days = daysOfWeek.get(key);
+        if (days == null) {
+            daysOfWeek.put(key, enabled ? EnumSet.of(day) : EnumSet.noneOf(DayOfWeek.class));
+        } else {
+            if (enabled) {
+                days.add(day);
+            } else {
+                days.remove(day);
+            }
+        }
+    }
+
+    public @Nullable LocalTime getTime(final ProfileKey key) {
+        return times.get(key);
+    }
+
+    public void setTime(final ProfileKey key, @Nullable LocalTime time) {
+        if (time == null) {
+            times.remove(key);
+        } else {
+            times.put(key, time);
+        }
+    }
+
+    public String getJson() {
+        final ChargeProfile profile = new ChargeProfile();
+        final WeeklyPlanner planner = new WeeklyPlanner();
+
+        preference.ifPresent(pref -> planner.chargingPreferences = pref.name());
+        planner.climatizationEnabled = isEnabled(CLIMATE);
+        preference.ifPresent(pref -> {
+            if (ChargingPreference.CHARGING_WINDOW.equals(pref)) {
+                planner.chargingMode = getMode();
+                final LocalTime start = getTime(WINDOWSTART);
+                final LocalTime end = getTime(WINDOWEND);
+                if (start != null || end != null) {
+                    planner.preferredChargingWindow = new ChargingWindow();
+                    planner.preferredChargingWindow.startTime = start == null ? null : start.format(TIME_FORMATER);
+                    planner.preferredChargingWindow.endTime = end == null ? null : end.format(TIME_FORMATER);
+                }
+            }
+        });
+        planner.timer1 = getTimer(TIMER1);
+        planner.timer2 = getTimer(TIMER2);
+        if (isWeekly()) {
+            planner.timer3 = getTimer(TIMER3);
+            planner.overrideTimer = getTimer(OVERRIDE);
+            profile.weeklyPlanner = planner;
+        } else if (isTwoTimes()) {
+            profile.twoTimesTimer = planner;
+        }
+        return Converter.getGson().toJson(profile);
+    }
+
+    private void addTime(final ProfileKey key, @Nullable final String time) {
+        try {
+            times.put(key, time == null ? NULL_LOCAL_TIME : LocalTime.parse(time, TIME_FORMATER));
+        } catch (DateTimeParseException dtpe) {
+            LOGGER.warn("unexpected value for {} time: {}", key.name(), time);
+        }
+    }
+
+    private void addTimer(final ProfileKey key, @Nullable final Timer timer) {
+        if (timer == null) {
+            enabled.put(key, false);
+            addTime(key, null);
+            if (isWeekly()) {
+                daysOfWeek.put(key, EnumSet.noneOf(DayOfWeek.class));
+            }
+        } else {
+            enabled.put(key, timer.timerEnabled);
+            addTime(key, timer.departureTime);
+            if (isWeekly()) {
+                final EnumSet<DayOfWeek> daySet = EnumSet.noneOf(DayOfWeek.class);
+                if (timer.weekdays != null) {
+                    for (String day : timer.weekdays) {
+                        try {
+                            daySet.add(DayOfWeek.valueOf(day));
+                        } catch (IllegalArgumentException iae) {
+                            LOGGER.warn("unexpected value for {} day: {}", key.name(), day);
+                        }
+                    }
+                }
+                daysOfWeek.put(key, daySet);
+            }
+        }
+    }
+
+    private @Nullable Timer getTimer(final ProfileKey key) {
+        final Timer timer = new Timer();
+        timer.timerEnabled = enabled.get(key);
+        final LocalTime time = times.get(key);
+        timer.departureTime = time == null ? null : time.format(TIME_FORMATER);
+        if (isWeekly()) {
+            final Set<DayOfWeek> days = daysOfWeek.get(key);
+            if (days != null) {
+                timer.weekdays = new ArrayList<>();
+                for (DayOfWeek day : days) {
+                    timer.weekdays.add(day.name());
+                }
+            }
+        }
+        return timer.timerEnabled == null && timer.departureTime == null && timer.weekdays == null ? null : timer;
+    }
+
+    private boolean isWeekly() {
+        return ProfileType.WEEKLY.equals(type);
+    }
+
+    private boolean isTwoTimes() {
+        return ProfileType.TWO_TIMES.equals(type);
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/Constants.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/Constants.java
new file mode 100644 (file)
index 0000000..e5be2eb
--- /dev/null
@@ -0,0 +1,96 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.utils;
+
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.measure.Unit;
+import javax.measure.quantity.Length;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.library.unit.MetricPrefix;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link Constants} General Constant Definitions
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Norbert Truchsess - contributor
+ */
+@NonNullByDefault
+public class Constants {
+    // For Vehicle Status
+    public static final String OK = "Ok";
+    public static final String ACTIVE = "Active";
+    public static final String NOT_ACTIVE = "Not Active";
+    public static final String NO_ENTRIES = "No Entries";
+    public static final String OPEN = "Open";
+    public static final String INVALID = "Invalid";
+    public static final String CLOSED = "Closed";
+    public static final String INTERMEDIATE = "Intermediate";
+    public static final String UNDEF = UnDefType.UNDEF.toFullString();
+    public static final String UTC_APPENDIX = "-01T12:00:00";
+    public static final String NULL_DATE = "1900-01-01T00:00:00";
+    public static final String NULL_TIME = "00:00";
+    public static final int INT_UNDEF = -1;
+    public static final Unit<Length> KILOMETRE_UNIT = MetricPrefix.KILO(SIUnits.METRE);
+
+    // Services to query
+    public static final String SERVICES_SUPPORTED = "servicesSupported";
+    public static final String STATISTICS = "Statistics";
+    public static final String LAST_DESTINATIONS = "LastDestinations";
+
+    // Services in Discovery
+    public static final String ACTIVATED = "ACTIVATED";
+    public static final String SUPPORTED = "SUPPORTED";
+    public static final String NOT_SUPPORTED = "NOT_SUPPORTED";
+
+    // General Constants for String concatenation
+    public static final String NULL = "null";
+    public static final String SPACE = " ";
+    public static final String UNDERLINE = "_";
+    public static final String HYPHEN = " - ";
+    public static final String PLUS = "+";
+    public static final String EMPTY = "";
+    public static final String COMMA = ",";
+    public static final String QUESTION = "?";
+    public static final String COLON = ":";
+
+    public static final String ANONYMOUS = "Anonymous";
+    public static final int MILES_TO_FEET_FACTOR = 5280;
+    public static final String EMPTY_JSON = "{}";
+
+    // Time Constants for DateTime channels
+    public static final LocalDate EPOCH_DAY = LocalDate.ofEpochDay(0);
+    public static final DateTimeFormatter TIME_FORMATER = DateTimeFormatter.ofPattern("HH:mm");
+    public static final LocalTime NULL_LOCAL_TIME = LocalTime.parse(NULL_TIME, TIME_FORMATER);
+
+    @SuppressWarnings("serial")
+    public static final Map<DayOfWeek, String> DAYS = new HashMap<>() {
+        {
+            put(DayOfWeek.MONDAY, "Mon");
+            put(DayOfWeek.TUESDAY, "Tue");
+            put(DayOfWeek.WEDNESDAY, "Wed");
+            put(DayOfWeek.THURSDAY, "Thu");
+            put(DayOfWeek.FRIDAY, "Fri");
+            put(DayOfWeek.SATURDAY, "Sat");
+            put(DayOfWeek.SUNDAY, "Sun");
+        }
+    };
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/Converter.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/Converter.java
new file mode 100644 (file)
index 0000000..bc6e3fc
--- /dev/null
@@ -0,0 +1,298 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.utils;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import javax.measure.quantity.Length;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.compat.VehicleAttributes;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.compat.VehicleAttributesContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.compat.VehicleMessages;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.CBSMessage;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.CCMMessage;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.Position;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatus;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatusContainer;
+import org.openhab.core.i18n.TimeZoneProvider;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.ImperialUnits;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link Converter} Conversion Helpers
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class Converter {
+    public static final Logger LOGGER = LoggerFactory.getLogger(Converter.class);
+
+    public static final DateTimeFormatter SERVICE_DATE_INPUT_PATTERN = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+    public static final DateTimeFormatter SERVICE_DATE_OUTPUT_PATTERN = DateTimeFormatter.ofPattern("MMM yyyy");
+
+    public static final String LOCAL_DATE_INPUT_PATTERN_STRING = "dd.MM.yyyy HH:mm";
+    public static final DateTimeFormatter LOCAL_DATE_INPUT_PATTERN = DateTimeFormatter
+            .ofPattern(LOCAL_DATE_INPUT_PATTERN_STRING);
+
+    public static final String DATE_INPUT_PATTERN_STRING = "yyyy-MM-dd'T'HH:mm:ss";
+    public static final DateTimeFormatter DATE_INPUT_PATTERN = DateTimeFormatter.ofPattern(DATE_INPUT_PATTERN_STRING);
+
+    public static final String DATE_INPUT_ZONE_PATTERN_STRING = "yyyy-MM-dd'T'HH:mm:ssZ";
+    public static final DateTimeFormatter DATE_INPUT_ZONE_PATTERN = DateTimeFormatter
+            .ofPattern(DATE_INPUT_ZONE_PATTERN_STRING);
+
+    public static final DateTimeFormatter DATE_OUTPUT_PATTERN = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
+
+    private static final Gson GSON = new Gson();
+    private static final double SCALE = 10;
+    public static final double MILES_TO_KM_RATIO = 1.60934;
+    private static final String SPLIT_HYPHEN = "-";
+    private static final String SPLIT_BRACKET = "\\(";
+
+    public static Optional<TimeZoneProvider> timeZoneProvider = Optional.empty();
+
+    public static double round(double value) {
+        return Math.round(value * SCALE) / SCALE;
+    }
+
+    public static String getLocalDateTimeWithoutOffest(@Nullable String input) {
+        if (input == null) {
+            return Constants.NULL_DATE;
+        }
+        LocalDateTime ldt;
+        if (input.contains(Constants.PLUS)) {
+            ldt = LocalDateTime.parse(input, Converter.DATE_INPUT_ZONE_PATTERN);
+        } else {
+            ldt = LocalDateTime.parse(input, Converter.DATE_INPUT_PATTERN);
+        }
+        return ldt.format(Converter.DATE_INPUT_PATTERN);
+    }
+
+    public static String getLocalDateTime(@Nullable String input) {
+        if (input == null) {
+            return Constants.NULL_DATE;
+        }
+
+        LocalDateTime ldt;
+        if (input.contains(Constants.PLUS)) {
+            ldt = LocalDateTime.parse(input, Converter.DATE_INPUT_ZONE_PATTERN);
+        } else {
+            try {
+                ldt = LocalDateTime.parse(input, Converter.DATE_INPUT_PATTERN);
+            } catch (DateTimeParseException dtpe) {
+                ldt = LocalDateTime.parse(input, Converter.LOCAL_DATE_INPUT_PATTERN);
+            }
+        }
+        ZonedDateTime zdtUTC = ldt.atZone(ZoneId.of("UTC"));
+        ZonedDateTime zdtLZ;
+        zdtLZ = zdtUTC.withZoneSameInstant(ZoneId.systemDefault());
+        if (timeZoneProvider.isPresent()) {
+            zdtLZ = zdtUTC.withZoneSameInstant(timeZoneProvider.get().getTimeZone());
+        } else {
+            zdtLZ = zdtUTC.withZoneSameInstant(ZoneId.systemDefault());
+        }
+        return zdtLZ.format(Converter.DATE_INPUT_PATTERN);
+    }
+
+    public static void setTimeZoneProvider(TimeZoneProvider tzp) {
+        timeZoneProvider = Optional.of(tzp);
+    }
+
+    public static String toTitleCase(@Nullable String input) {
+        if (input == null) {
+            return toTitleCase(Constants.UNDEF);
+        } else {
+            String lower = input.replaceAll(Constants.UNDERLINE, Constants.SPACE).toLowerCase();
+            String converted = toTitleCase(lower, Constants.SPACE);
+            converted = toTitleCase(converted, SPLIT_HYPHEN);
+            converted = toTitleCase(converted, SPLIT_BRACKET);
+            return converted;
+        }
+    }
+
+    private static String toTitleCase(String input, String splitter) {
+        String[] arr = input.split(splitter);
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < arr.length; i++) {
+            if (i > 0) {
+                sb.append(splitter.replaceAll("\\\\", Constants.EMPTY));
+            }
+            sb.append(Character.toUpperCase(arr[i].charAt(0))).append(arr[i].substring(1));
+        }
+        return sb.toString().trim();
+    }
+
+    public static String capitalizeFirst(String str) {
+        return str.substring(0, 1).toUpperCase() + str.substring(1);
+    }
+
+    public static Gson getGson() {
+        return GSON;
+    }
+
+    /**
+     * Measure distance between 2 coordinates
+     *
+     * @param sourceLatitude
+     * @param sourceLongitude
+     * @param destinationLatitude
+     * @param destinationLongitude
+     * @return distance
+     */
+    public static double measureDistance(double sourceLatitude, double sourceLongitude, double destinationLatitude,
+            double destinationLongitude) {
+        double earthRadius = 6378.137; // Radius of earth in KM
+        double dLat = destinationLatitude * Math.PI / 180 - sourceLatitude * Math.PI / 180;
+        double dLon = destinationLongitude * Math.PI / 180 - sourceLongitude * Math.PI / 180;
+        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(sourceLatitude * Math.PI / 180)
+                * Math.cos(destinationLatitude * Math.PI / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
+        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+        return earthRadius * c;
+    }
+
+    /**
+     * Easy function but there's some measures behind:
+     * Guessing the range of the Vehicle on Map. If you can drive x kilometers with your Vehicle it's not feasible to
+     * project this x km Radius on Map. The roads to be taken are causing some overhead because they are not a straight
+     * line from Location A to B.
+     * I've taken some measurements to calculate the overhead factor based on Google Maps
+     * Berlin - Dresden: Road Distance: 193 air-line Distance 167 = Factor 87%
+     * Kassel - Frankfurt: Road Distance: 199 air-line Distance 143 = Factor 72%
+     * After measuring more distances you'll find out that the outcome is between 70% and 90%. So
+     *
+     * This depends also on the roads of a concrete route but this is only a guess without any Route Navigation behind
+     *
+     * In future it's foreseen to replace this with BMW RangeMap Service which isn't running at the moment.
+     *
+     * @param range
+     * @return mapping from air-line distance to "real road" distance
+     */
+    public static double guessRangeRadius(double range) {
+        return range * 0.8;
+    }
+
+    public static State getMiles(QuantityType<Length> qtLength) {
+        if (qtLength.intValue() == -1) {
+            return UnDefType.UNDEF;
+        }
+        QuantityType<Length> qt = qtLength.toUnit(ImperialUnits.MILE);
+        if (qt != null) {
+            return qt;
+        } else {
+            LOGGER.debug("Cannot convert {} to miles", qt);
+            return UnDefType.UNDEF;
+        }
+    }
+
+    public static int getIndex(String fullString) {
+        int index = -1;
+        try {
+            index = Integer.parseInt(fullString);
+        } catch (NumberFormatException nfe) {
+        }
+        return index;
+    }
+
+    public static String transformLegacyStatus(@Nullable VehicleAttributesContainer vac) {
+        if (vac != null) {
+            if (vac.attributesMap != null && vac.vehicleMessages != null) {
+                VehicleAttributes attributesMap = vac.attributesMap;
+                VehicleMessages vehicleMessages = vac.vehicleMessages;
+                // create target objects
+                VehicleStatusContainer vsc = new VehicleStatusContainer();
+                VehicleStatus vs = new VehicleStatus();
+                vsc.vehicleStatus = vs;
+
+                vs.mileage = attributesMap.mileage;
+                vs.doorLockState = attributesMap.doorLockState;
+
+                vs.doorDriverFront = attributesMap.doorDriverFront;
+                vs.doorDriverRear = attributesMap.doorDriverRear;
+                vs.doorPassengerFront = attributesMap.doorPassengerFront;
+                vs.doorPassengerRear = attributesMap.doorPassengerRear;
+                vs.hood = attributesMap.hoodState;
+                vs.trunk = attributesMap.trunkState;
+
+                vs.windowDriverFront = attributesMap.winDriverFront;
+                vs.windowDriverRear = attributesMap.winDriverRear;
+                vs.windowPassengerFront = attributesMap.winPassengerFront;
+                vs.windowPassengerRear = attributesMap.winPassengerRear;
+                vs.sunroof = attributesMap.sunroofState;
+
+                vs.remainingFuel = attributesMap.remainingFuel;
+                vs.remainingRangeElectric = attributesMap.beRemainingRangeElectricKm;
+                vs.remainingRangeElectricMls = attributesMap.beRemainingRangeElectricMile;
+                vs.remainingRangeFuel = attributesMap.beRemainingRangeFuelKm;
+                vs.remainingRangeFuelMls = attributesMap.beRemainingRangeFuelMile;
+                vs.remainingFuel = attributesMap.remainingFuel;
+                vs.chargingLevelHv = attributesMap.chargingLevelHv;
+                vs.chargingStatus = attributesMap.chargingHVStatus;
+                vs.lastChargingEndReason = attributesMap.lastChargingEndReason;
+
+                vs.updateTime = attributesMap.updateTimeConverted;
+
+                Position p = new Position();
+                p.lat = attributesMap.gpsLat;
+                p.lon = attributesMap.gpsLon;
+                p.heading = attributesMap.heading;
+                vs.position = p;
+
+                final List<CCMMessage> ccml = new ArrayList<CCMMessage>();
+                if (vehicleMessages != null) {
+                    if (vehicleMessages.ccmMessages != null) {
+                        vehicleMessages.ccmMessages.forEach(entry -> {
+                            CCMMessage ccmM = new CCMMessage();
+                            ccmM.ccmDescriptionShort = entry.text;
+                            ccmM.ccmDescriptionLong = Constants.HYPHEN;
+                            ccmM.ccmMileage = entry.unitOfLengthRemaining;
+                            ccml.add(ccmM);
+                        });
+                    }
+                }
+                vs.checkControlMessages = ccml;
+
+                final List<CBSMessage> cbsl = new ArrayList<CBSMessage>();
+                if (vehicleMessages != null) {
+                    if (vehicleMessages.cbsMessages != null) {
+                        vehicleMessages.cbsMessages.forEach(entry -> {
+                            CBSMessage cbsm = new CBSMessage();
+                            cbsm.cbsType = entry.text;
+                            cbsm.cbsDescription = entry.description;
+                            cbsm.cbsDueDate = entry.date;
+                            cbsm.cbsRemainingMileage = entry.unitOfLengthRemaining;
+                            cbsl.add(cbsm);
+                        });
+                    }
+                }
+                vs.cbsData = cbsl;
+                return Converter.getGson().toJson(vsc);
+            }
+        }
+        return Constants.EMPTY_JSON;
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/HTTPConstants.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/HTTPConstants.java
new file mode 100644 (file)
index 0000000..62f8786
--- /dev/null
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.utils;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link HTTPConstants} class contains fields mapping thing configuration parameters.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class HTTPConstants {
+    public static final int HTTP_TIMEOUT_SEC = 10;
+
+    public static final String AUTH_HTTP_CLIENT_NAME = "AuthHttpClient";
+    public static final String CONTENT_TYPE_URL_ENCODED = "application/x-www-form-urlencoded";
+    public static final String CONTENT_TYPE_JSON = "application/json";
+    public static final String KEEP_ALIVE = "Keep-Alive";
+    public static final String CLIENT_ID = "client_id";
+    public static final String RESPONSE_TYPE = "response_type";
+    public static final String TOKEN = "token";
+    public static final String REDIRECT_URI = "redirect_uri";
+    public static final String SCOPE = "scope";
+    public static final String CREDENTIALS = "Credentials";
+    public static final String USERNAME = "username";
+    public static final String PASSWORD = "password";
+    public static final String CONTENT_LENGTH = "Content-Length";
+
+    public static final String ACCESS_TOKEN = "access_token";
+    public static final String TOKEN_TYPE = "token_type";
+    public static final String EXPIRES_IN = "expires_in";
+    public static final String CHUNKED = "chunked";
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/ImageProperties.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/ImageProperties.java
new file mode 100644 (file)
index 0000000..6b943a2
--- /dev/null
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.utils;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link ImageProperties} Properties of current Vehicle Image
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class ImageProperties {
+    public static final int RETRY_COUNTER = 5;
+    public int failCounter = 0;
+    public String viewport = Constants.EMPTY;
+    public int size = -1;
+
+    public ImageProperties(String viewport, int size) {
+        this.viewport = viewport;
+        this.size = size;
+    }
+
+    public ImageProperties() {
+    }
+
+    public void failed() {
+        failCounter++;
+    }
+
+    public boolean failLimitReached() {
+        return failCounter > RETRY_COUNTER;
+    }
+
+    @Override
+    public String toString() {
+        return viewport + size;
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/RemoteServiceUtils.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/RemoteServiceUtils.java
new file mode 100644 (file)
index 0000000..a81f9c1
--- /dev/null
@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.utils;
+
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.RemoteServiceHandler.RemoteService;
+import org.openhab.core.types.StateOption;
+
+/**
+ * Helper class for Remote Service Commands
+ *
+ * @author Norbert Truchsess - Initial contribution
+ */
+@NonNullByDefault
+public class RemoteServiceUtils {
+
+    private static final Map<String, RemoteService> COMMAND_SERVICES = Stream.of(RemoteService.values())
+            .collect(Collectors.toUnmodifiableMap(RemoteService::getCommand, service -> service));
+
+    private static final Set<RemoteService> ELECTRIC_SERVICES = EnumSet.of(RemoteService.CHARGE_NOW,
+            RemoteService.CHARGING_CONTROL);
+
+    public static Optional<RemoteService> getRemoteService(final String command) {
+        return Optional.ofNullable(COMMAND_SERVICES.get(command));
+    }
+
+    public static List<StateOption> getOptions(final boolean isElectric) {
+        return Stream.of(RemoteService.values())
+                .filter(service -> isElectric ? true : !ELECTRIC_SERVICES.contains(service))
+                .map(service -> new StateOption(service.getCommand(), service.getLabel()))
+                .collect(Collectors.toUnmodifiableList());
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/VehicleStatusUtils.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/VehicleStatusUtils.java
new file mode 100644 (file)
index 0000000..1a11ee6
--- /dev/null
@@ -0,0 +1,141 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.utils;
+
+import static org.openhab.binding.bmwconnecteddrive.internal.utils.Constants.*;
+
+import java.lang.reflect.Field;
+import java.time.LocalDateTime;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.CBSMessage;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatus;
+
+/**
+ * The {@link VehicleStatusUtils} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class VehicleStatusUtils {
+
+    public static String getNextServiceDate(VehicleStatus vStatus) {
+        if (vStatus.cbsData == null) {
+            return Constants.NULL_DATE;
+        }
+        if (vStatus.cbsData.isEmpty()) {
+            return Constants.NULL_DATE;
+        } else {
+            LocalDateTime farFuture = LocalDateTime.now().plusYears(100);
+            LocalDateTime serviceDate = farFuture;
+            for (int i = 0; i < vStatus.cbsData.size(); i++) {
+                CBSMessage entry = vStatus.cbsData.get(i);
+                if (entry.cbsDueDate != null) {
+                    LocalDateTime d = LocalDateTime.parse(entry.cbsDueDate + Constants.UTC_APPENDIX);
+                    if (d.isBefore(serviceDate)) {
+                        serviceDate = d;
+                    }
+                }
+            }
+            if (serviceDate.equals(farFuture)) {
+                return Constants.NULL_DATE;
+            } else {
+                return serviceDate.format(Converter.DATE_INPUT_PATTERN);
+            }
+        }
+    }
+
+    public static int getNextServiceMileage(VehicleStatus vStatus) {
+        if (vStatus.cbsData == null) {
+            return -1;
+        }
+        if (vStatus.cbsData.isEmpty()) {
+            return -1;
+        } else {
+            int serviceMileage = Integer.MAX_VALUE;
+            for (int i = 0; i < vStatus.cbsData.size(); i++) {
+                CBSMessage entry = vStatus.cbsData.get(i);
+                if (entry.cbsRemainingMileage != -1) {
+                    if (entry.cbsRemainingMileage < serviceMileage) {
+                        serviceMileage = entry.cbsRemainingMileage;
+                    }
+                }
+            }
+            if (serviceMileage != Integer.MAX_VALUE) {
+                return serviceMileage;
+            } else {
+                return -1;
+            }
+        }
+    }
+
+    public static String checkControlActive(VehicleStatus vStatus) {
+        if (vStatus.checkControlMessages == null) {
+            return UNDEF;
+        }
+        if (vStatus.checkControlMessages.isEmpty()) {
+            return NOT_ACTIVE;
+        } else {
+            return ACTIVE;
+        }
+    }
+
+    public static String getUpdateTime(VehicleStatus vStatus) {
+        if (vStatus.internalDataTimeUTC != null) {
+            return vStatus.internalDataTimeUTC;
+        } else if (vStatus.updateTime != null) {
+            return vStatus.updateTime;
+        } else {
+            return Constants.NULL_DATE;
+        }
+    }
+
+    /**
+     * Check for certain Windows or Doors DTO object the "Closed" Status
+     * INVALID values will be ignored
+     *
+     * @param dto
+     * @return Closed if all "Closed", "Open" otherwise
+     */
+    public static String checkClosed(Object dto) {
+        String overallState = Constants.UNDEF;
+        for (Field field : dto.getClass().getDeclaredFields()) {
+            try {
+                Object d = field.get(dto);
+                if (d != null) {
+                    String state = d.toString();
+                    // skip invalid entries - they don't apply to this Vehicle
+                    if (!state.equalsIgnoreCase(INVALID)) {
+                        if (state.equalsIgnoreCase(OPEN)) {
+                            overallState = OPEN;
+                            // stop searching for more open items - overall Doors / Windows are OPEN
+                            break;
+                        } else if (state.equalsIgnoreCase(INTERMEDIATE)) {
+                            if (!overallState.equalsIgnoreCase(OPEN)) {
+                                overallState = INTERMEDIATE;
+                                // continue searching - maybe another Door / Window is OPEN
+                            }
+                        } else if (state.equalsIgnoreCase(CLOSED)) {
+                            // at least one valid object needs to be found in order to reply "CLOSED"
+                            if (overallState.equalsIgnoreCase(UNDEF)) {
+                                overallState = CLOSED;
+                            }
+                        }
+                    }
+                }
+            } catch (IllegalArgumentException | IllegalAccessException e) {
+            }
+        }
+        return Converter.toTitleCase(overallState);
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644 (file)
index 0000000..7e2b71d
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="bmwconnecteddrive" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+       <name>BMW ConnectedDrive</name>
+       <description>Provides access to your Vehicle Data via BMW Connected Drive Portal</description>
+
+</binding:binding>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/config/bridge-config.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/config/bridge-config.xml
new file mode 100644 (file)
index 0000000..66abc97
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+       <config-description uri="thing-type:bmwconnecteddrive:bridge">
+               <parameter name="userName" type="text" required="true">
+                       <label>Username</label>
+                       <description>BMW Connected Drive Username</description>
+               </parameter>
+               <parameter name="password" type="text" required="true">
+                       <label>Password</label>
+                       <description>BMW Connected Drive Password</description>
+                       <context>password</context>
+               </parameter>
+               <parameter name="region" type="text" required="true">
+                       <label>Region</label>
+                       <description>Select Region in order to connect to the appropriate BMW Server</description>
+                       <options>
+                               <option value="NORTH_AMERICA">North America</option>
+                               <option value="CHINA">China</option>
+                               <option value="ROW">Rest of the World</option>
+                       </options>
+                       <default>ROW</default>
+               </parameter>
+       </config-description>
+</config-description:config-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/config/thing-config.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/config/thing-config.xml
new file mode 100644 (file)
index 0000000..051be06
--- /dev/null
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+       <config-description uri="thing-type:bmwconnecteddrive:vehicle">
+               <parameter name="vin" type="text" required="true">
+                       <label>Vehicle Identification Number (VIN)</label>
+                       <description>Unique VIN given by BMW</description>
+               </parameter>
+               <parameter name="refreshInterval" type="integer" min="1" unit="min" required="true">
+                       <label>Refresh Interval</label>
+                       <description>Data Refresh Rate for your Vehicle data</description>
+                       <default>5</default>
+               </parameter>
+               <parameter name="units" type="text">
+                       <label>Unit Selection</label>
+                       <description>Units are selected via auto-detection but you can overrule</description>
+                       <options>
+                               <option value="AUTODETECT">Auto Detect</option>
+                               <option value="METRIC">Metric</option>
+                               <option value="IMPERIAL">Imperial</option>
+                       </options>
+                       <default>AUTODETECT</default>
+               </parameter>
+               <parameter name="imageSize" type="integer">
+                       <label>Image Picture Size</label>
+                       <description>Vehicle Image size for width and length</description>
+                       <default>1024</default>
+               </parameter>
+               <parameter name="imageViewport" type="text">
+                       <label>Image Viewport</label>
+                       <description>Viewport for Vehicle Image</description>
+                       <options>
+                               <option value="FRONT">Front View</option>
+                               <option value="REAR">Rear View</option>
+                               <option value="SIDE">Side View</option>
+                               <option value="DASHBOARD">Dashboard View</option>
+                               <option value="DRIVERDOOR">Driver Door View</option>
+                       </options>
+                       <default>FRONT</default>
+               </parameter>
+       </config-description>
+</config-description:config-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/i18n/bmwconnecteddrive_de.properties b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/i18n/bmwconnecteddrive_de.properties
new file mode 100644 (file)
index 0000000..ab6509c
--- /dev/null
@@ -0,0 +1,266 @@
+# Binding
+binding.bmwconnecteddrive.name = BMW ConnectedDrive
+binding.bmwconnecteddrive.description = Zeigt die Fahrzeugdaten Ã¼ber das BMW ConnectedDrive Portal
+
+# bridge types
+thing-type.bmwconnecteddrive.account.label = BMW ConnectedDrive Benutzerkonto
+thing-type.bmwconnecteddrive.account.description = Zugriff auf das BMW ConnectedDrive Portal für einen Benutzer
+thing-type.config.bmwconnecteddrive.account.userName = Benutzername für das ConnectedDrive Portal
+thing-type.config.bmwconnecteddrive.account.password = Passwort für das ConnectedDrive Portal
+thing-type.config.bmwconnecteddrive.account.region = Auswahl Ihrer Region zur Verbindung mit dem korrekten BMW Server 
+thing-type.config.bmwconnecteddrive.account.region.option.NORTH_AMERICA = Nordamerika 
+thing-type.config.bmwconnecteddrive.account.region.option.CHINA = China 
+thing-type.config.bmwconnecteddrive.account.region.option.ROW = Rest der Welt 
+
+# thing types
+thing-type.bmwconnecteddrive.bev_rex.label = Elektrofahrzeug mit REX
+thing-type.bmwconnecteddrive.bev_rex.description = Elektrofahrzeug mit Range Extender (bev_rex)
+thing-type.config.bmwconnecteddrive.bev_rex.vin.label = Fahrzeug Identifikationsnummer (VIN)
+thing-type.config.bmwconnecteddrive.bev_rex.vin.description = VIN des Fahrzeugs
+thing-type.config.bmwconnecteddrive.bev_rex.refreshInterval.label = Datenaktualisierung in Minuten
+thing-type.config.bmwconnecteddrive.bev_rex.refreshInterval.description = Rate der Datenaktualisierung Ihres Fahrzeugs
+thing-type.config.bmwconnecteddrive.bev_rex.units.label = Einheiten
+thing-type.config.bmwconnecteddrive.bev_rex.units.description = Automatische oder direkte Auswahl der Einheiten 
+thing-type.config.bmwconnecteddrive.bev_rex.units.option.AUTODETECT = Automatische Auswahl
+thing-type.config.bmwconnecteddrive.bev_rex.units.option.IMPERIAL = Angloamerikanisches System
+thing-type.config.bmwconnecteddrive.bev_rex.units.option.METRIC = Metrisches System
+thing-type.config.bmwconnecteddrive.bev_rex.imageSize.label = Bildgröße
+thing-type.config.bmwconnecteddrive.bev_rex.imageSize.description = Bildgröße des Fahrzeugs für Länge und Breite
+thing-type.config.bmwconnecteddrive.bev_rex.imageViewport.label = Bild Ansicht
+thing-type.config.bmwconnecteddrive.bev_rex.imageViewport.description = Ansicht des Fahrzeugs
+thing-type.config.bmwconnecteddrive.bev_rex.imageViewport.option.FRONT = Vorderansicht
+thing-type.config.bmwconnecteddrive.bev_rex.imageViewport.option.REAR = Rückansicht
+thing-type.config.bmwconnecteddrive.bev_rex.imageViewport.option.SIDE = Seitenansicht
+thing-type.config.bmwconnecteddrive.bev_rex.imageViewport.option.DASHBOARD = Innenansicht Armaturen
+thing-type.config.bmwconnecteddrive.bev_rex.imageViewport.option.DRIVERDOOR = Seitenansicht Fahrertür
+
+thing-type.bmwconnecteddrive.bev.label = Elektrofahrzeug
+thing-type.bmwconnecteddrive.bev.description = Batterieelektrisches Fahrzeug (bev)
+thing-type.config.bmwconnecteddrive.bev.vin.label = Fahrzeug Identifikationsnummer (VIN)
+thing-type.config.bmwconnecteddrive.bev.vin.description = VIN des Fahrzeugs
+thing-type.config.bmwconnecteddrive.bev.refreshInterval.label = Datenaktualisierung in Minuten
+thing-type.config.bmwconnecteddrive.bev.refreshInterval.description = Rate der Datenaktualisierung Ihres Fahrzeugs
+thing-type.config.bmwconnecteddrive.bev.units.label = Einheiten
+thing-type.config.bmwconnecteddrive.bev.units.description = Automatische oder direkte Auswahl der Einheiten 
+thing-type.config.bmwconnecteddrive.bev.units.option.AUTODETECT = Automatische Auswahl
+thing-type.config.bmwconnecteddrive.bev.units.option.IMPERIAL = Angloamerikanisches System
+thing-type.config.bmwconnecteddrive.bev.units.option.METRIC = Metrisches System
+thing-type.config.bmwconnecteddrive.bev.imageSize.label = Bildgröße
+thing-type.config.bmwconnecteddrive.bev.imageSize.description = Bildgröße des Fahrzeugs für Länge und Breite
+thing-type.config.bmwconnecteddrive.bev.imageViewport.label = Bild Ansicht
+thing-type.config.bmwconnecteddrive.bev.imageViewport.description = Ansicht des Fahrzeugs
+thing-type.config.bmwconnecteddrive.bev.imageViewport.option.FRONT = Vorderansicht
+thing-type.config.bmwconnecteddrive.bev.imageViewport.option.REAR = Rückansicht
+thing-type.config.bmwconnecteddrive.bev.imageViewport.option.SIDE = Seitenansicht
+thing-type.config.bmwconnecteddrive.bev.imageViewport.option.DASHBOARD = Innenansicht Armaturen
+thing-type.config.bmwconnecteddrive.bev.imageViewport.option.DRIVERDOOR = Seitenansicht Fahrertür
+
+thing-type.bmwconnecteddrive.phev.label = Plug-in-Hybrid Elektrofahrzeug
+thing-type.bmwconnecteddrive.phev.description = Konventionelles Fahrzeug mit Elektromotor (phev)
+thing-type.config.bmwconnecteddrive.phev.vin.label = Fahrzeug Identifikationsnummer (VIN)
+thing-type.config.bmwconnecteddrive.phev.vin.description = VIN des Fahrzeugs
+thing-type.config.bmwconnecteddrive.phev.refreshInterval.label = Datenaktualisierung in Minuten
+thing-type.config.bmwconnecteddrive.phev.refreshInterval.description = Rate der Datenaktualisierung Ihres Fahrzeugs
+thing-type.config.bmwconnecteddrive.phev.units.label = Einheiten
+thing-type.config.bmwconnecteddrive.phev.units.description = Automatische oder direkte Auswahl der Einheiten 
+thing-type.config.bmwconnecteddrive.phev.units.option.AUTODETECT = Automatische Auswahl
+thing-type.config.bmwconnecteddrive.phev.units.option.IMPERIAL = Angloamerikanisches System
+thing-type.config.bmwconnecteddrive.phev.units.option.METRIC = Metrisches System
+thing-type.config.bmwconnecteddrive.phev.imageSize.label = Bildgröße
+thing-type.config.bmwconnecteddrive.phev.imageSize.description = Bildgröße des Fahrzeugs für Länge und Breite
+thing-type.config.bmwconnecteddrive.phev.imageViewport.label = Bild Ansicht
+thing-type.config.bmwconnecteddrive.phev.imageViewport.description = Ansicht des Fahrzeugs
+thing-type.config.bmwconnecteddrive.phev.imageViewport.option.FRONT = Vorderansicht
+thing-type.config.bmwconnecteddrive.phev.imageViewport.option.REAR = Rückansicht
+thing-type.config.bmwconnecteddrive.phev.imageViewport.option.SIDE = Seitenansicht
+thing-type.config.bmwconnecteddrive.phev.imageViewport.option.DASHBOARD = Innenansicht Armaturen
+thing-type.config.bmwconnecteddrive.phev.imageViewport.option.DRIVERDOOR = Seitenansicht Fahrertür
+
+thing-type.bmwconnecteddrive.conv.label = Konventionelles Fahrzeug
+thing-type.bmwconnecteddrive.conv.description = Konventionelles Benzin/Diesel Fahrzeug (conv)
+thing-type.config.bmwconnecteddrive.conv.vin.label = Fahrzeug Identifikationsnummer (VIN)
+thing-type.config.bmwconnecteddrive.conv.vin.description = VIN des Fahrzeugs
+thing-type.config.bmwconnecteddrive.conv.refreshInterval.label = Datenaktualisierung in Minuten
+thing-type.config.bmwconnecteddrive.conv.refreshInterval.description = Rate der Datenaktualisierung Ihres Fahrzeugs
+thing-type.config.bmwconnecteddrive.conv.units.label = Einheiten
+thing-type.config.bmwconnecteddrive.conv.units.description = Automatische oder direkte Auswahl der Einheiten 
+thing-type.config.bmwconnecteddrive.conv.units.option.AUTODETECT = Automatische Auswahl
+thing-type.config.bmwconnecteddrive.conv.units.option.IMPERIAL = Angloamerikanisches System
+thing-type.config.bmwconnecteddrive.conv.units.option.METRIC = Metrisches System
+thing-type.config.bmwconnecteddrive.conv.imageSize.label = Bildgröße
+thing-type.config.bmwconnecteddrive.conv.imageSize.description = Bildgröße des Fahrzeugs für Länge und Breite
+thing-type.config.bmwconnecteddrive.conv.imageViewport.label = Bild Ansicht
+thing-type.config.bmwconnecteddrive.conv.imageViewport.description = Ansicht des Fahrzeugs
+thing-type.config.bmwconnecteddrive.conv.imageViewport.option.FRONT = Vorderansicht
+thing-type.config.bmwconnecteddrive.conv.imageViewport.option.REAR = Rückansicht
+thing-type.config.bmwconnecteddrive.conv.imageViewport.option.SIDE = Seitenansicht
+thing-type.config.bmwconnecteddrive.conv.imageViewport.option.DASHBOARD = Innenansicht Armaturen
+thing-type.config.bmwconnecteddrive.conv.imageViewport.option.DRIVERDOOR = Seitenansicht Fahrertür
+
+# Channel Groups
+channel-group-type.bmwconnecteddrive.charge-values.label = Elektrisches Laden
+channel-group-type.bmwconnecteddrive.charge-values.description = Ladezustand und Ladeprofile des Fahrzeugs
+channel-group-type.bmwconnecteddrive.ev-lifetime-values.label = Gesamtlaufzeit Statistik
+channel-group-type.bmwconnecteddrive.ev-lifetime-values.description = Verbrauchswerte und zurückgelegte Strecken Ã¼ber die Fahrzeug-Gesamtlaufzeit
+channel-group-type.bmwconnecteddrive.hybrid-lifetime-values.label = Gesamtlaufzeit Statistik
+channel-group-type.bmwconnecteddrive.hybrid-lifetime-values.description = Verbrauchswerte und zurückgelegte Strecken Ã¼ber die Fahrzeug-Gesamtlaufzeit
+channel-group-type.bmwconnecteddrive.ev-last-trip-values.label = Statistik der letzten Fahrt
+channel-group-type.bmwconnecteddrive.ev-last-trip-values.description = Verbrauchswerte und zurück gelegte Strecke der letzten Fahrt
+channel-group-type.bmwconnecteddrive.hybrid-last-trip-values.label = Statistik der letzten Fahrt
+channel-group-type.bmwconnecteddrive.hybrid-last-trip-values.description = Verbrauchswerte und zurück gelegte Strecke der letzten Fahrt
+channel-group-type.bmwconnecteddrive.ev-range-values.label = Elektrische Reichweite
+channel-group-type.bmwconnecteddrive.ev-range-values.description = Tachostand, Reichweiten und Ladestand des Fahrzeugs
+channel-group-type.bmwconnecteddrive.check-control-values.label = Warnungen
+channel-group-type.bmwconnecteddrive.check-control-values.description = Aktuelle Warungen des Fahrzeugs
+channel-group-type.bmwconnecteddrive.service-values.label = Wartung
+channel-group-type.bmwconnecteddrive.service-values.description = Zukünftige Wartungstermine des Fahrzeugs
+channel-group-type.bmwconnecteddrive.conv-range-values.label = Reichweite
+channel-group-type.bmwconnecteddrive.conv-range-values.description = Tachostand, Reichweite und Tankfüllung des Fahrzeugs
+channel-group-type.bmwconnecteddrive.hybrid-range-values.label = Hybride Reichweite
+channel-group-type.bmwconnecteddrive.hybrid-range-values.description = Tachostand, Reichweite, Ladezustand und Tankfüllung des Fahrzeugs
+channel-group-type.bmwconnecteddrive.image-values.label = Fahrzeug Bild
+channel-group-type.bmwconnecteddrive.image-values.description = Bild des Fahrzeug basierend auf der Ansicht in der Konfiguration
+channel-group-type.bmwconnecteddrive.remote-services.label = Fahrzeug Fernsteuerung
+channel-group-type.bmwconnecteddrive.remote-services.description = Fernsteuerung des Fahrzeugs Ã¼ber den BMW Server wie Türen schließen / Ã¶ffnen, Klimasteuerung und mehr
+channel-group-type.bmwconnecteddrive.vehicle-status.label = Fahrzeug Zustand
+channel-group-type.bmwconnecteddrive.vehicle-status.description = Zustand des Fahrzeugs Ã¼ber Türen, Fenster, abgeschlossen, anstehende Wartung und aktive Warnungen
+channel-group-type.bmwconnecteddrive.ev-vehicle-status.label = Fahrzeug Zustand
+channel-group-type.bmwconnecteddrive.ev-vehicle-status.description = Zustand des Fahrzeugs Ã¼ber Türen, Fenster, abgeschlossen, anstehende Wartung und aktive Warnungen
+channel-group-type.bmwconnecteddrive.location-values.label = Fahrzeug Standort
+channel-group-type.bmwconnecteddrive.location-values.description = Koordinaten und Ausrichtung des Fahrzeugs
+channel-group-type.bmwconnecteddrive.destination-values.label = Ziele
+channel-group-type.bmwconnecteddrive.destination-values.description = Zeigt die gespeicherten Ziele des Fahrzeugs
+channel-group-type.bmwconnecteddrive.troubleshoot-control.label = Fehlerbehebung
+channel-group-type.bmwconnecteddrive.troubleshoot-control.description = Generiert Daten zur Fehlerbehebung eines Problems
+channel-group-type.bmwconnecteddrive.door-values.label = Details aller Türen
+channel-group-type.bmwconnecteddrive.door-values.description = Zeigt die Details der Türen und Fenster des Fahrzeugs
+
+# Channel Types
+channel-type.bmwconnecteddrive.doors-channel.label = Gesamtzustand der Türen
+channel-type.bmwconnecteddrive.windows-channel.label = Gesamtzustand der Fenster
+channel-type.bmwconnecteddrive.lock-channel.label = Fahrzeug Abgeschlossen
+channel-type.bmwconnecteddrive.next-service-date-channel.label = Nächster Service Termin
+channel-type.bmwconnecteddrive.next-service-mileage-channel.label = Nächster Service in Kilometern
+channel-type.bmwconnecteddrive.check-control-channel.label = Warnung Aktiv
+channel-type.bmwconnecteddrive.charging-status-channel.label = Ladezustand
+channel-type.bmwconnecteddrive.charging-remaining-channel.label = Verbleibende Ladezeit
+channel-type.bmwconnecteddrive.last-update-channel.label = Letzte Aktualisierung
+
+channel-type.bmwconnecteddrive.driver-front-channel.label = Fahrertür 
+channel-type.bmwconnecteddrive.driver-rear-channel.label = Fahrertür Hinten
+channel-type.bmwconnecteddrive.passenger-front-channel.label = Beifahrertür
+channel-type.bmwconnecteddrive.passenger-rear-channel.label = Beifahrertür Hinten
+channel-type.bmwconnecteddrive.hood-channel.label = Frontklappe
+channel-type.bmwconnecteddrive.trunk-channel.label = Heckklappe
+channel-type.bmwconnecteddrive.window-driver-front-channel.label = Fahrertür Fenster
+channel-type.bmwconnecteddrive.window-driver-rear-channel.label = Fahrertür Hinten Fenster
+channel-type.bmwconnecteddrive.window-passenger-front-channel.label = Beifahrertür Fenster
+channel-type.bmwconnecteddrive.window-passenger-rear-channel.label = Beifahrertür Hinten Fenster
+channel-type.bmwconnecteddrive.window-rear-channel.label = Heckfenster
+channel-type.bmwconnecteddrive.sunroof-channel.label = Schiebedach
+
+channel-type.bmwconnecteddrive.mileage-channel.label = Tachostand
+channel-type.bmwconnecteddrive.range-hybrid-channel.label = Hybride Reichweite
+channel-type.bmwconnecteddrive.range-electric-channel.label = Elektrische Reichweite 
+channel-type.bmwconnecteddrive.soc-channel.label = Batterie Ladestand
+channel-type.bmwconnecteddrive.range-fuel-channel.label = Verbrenner Reichweite
+channel-type.bmwconnecteddrive.remaining-fuel-channel.label = Tankstand
+channel-type.bmwconnecteddrive.range-radius-electric-channel.label = Elektrischer Reichweiten-Radius
+channel-type.bmwconnecteddrive.range-radius-fuel-channel.label =  Verbrenner Reichweiten-Radius
+channel-type.bmwconnecteddrive.range-radius-hybrid-channel.label = Hybrider Reichweiten-Radius
+
+channel-type.bmwconnecteddrive.service-name-channel.label = Service
+channel-type.bmwconnecteddrive.service-details-channel.label = Service Details
+channel-type.bmwconnecteddrive.service-date-channel.label = Service Termin
+channel-type.bmwconnecteddrive.service-mileage-channel.label = Service in Kilometern
+
+channel-type.bmwconnecteddrive.checkcontrol-name-channel.label = Warnung
+channel-type.bmwconnecteddrive.checkcontrol-details-channel.label = Warnung Details
+channel-type.bmwconnecteddrive.checkcontrol-mileage-channel.label = Warnung bei Kilometer
+
+channel-type.bmwconnecteddrive.profile-climate-channel.label = Klimatisierung bei Abfahrt
+channel-type.bmwconnecteddrive.profile-mode-channel.label = Ladeprofil
+channel-type.bmwconnecteddrive.profile-mode-channel.option.IMMEDIATE_CHARGING = Sofortiges Laden
+channel-type.bmwconnecteddrive.profile-mode-channel.option.DELAYED_CHARGING = Laden im Zeitfenster
+channel-type.bmwconnecteddrive.profile-prefs-channel.label = Ladeprofil Präferenz
+channel-type.bmwconnecteddrive.profile-prefs-channel.option.NO_PRESELECTION = Keine Präferenz
+channel-type.bmwconnecteddrive.profile-prefs-channel.option.Charging Window = Zeitfenster
+channel-type.bmwconnecteddrive.window-start-channel.label = Ladefenster Startzeit
+channel-type.bmwconnecteddrive.window-start-hour-channel.label = Ladefenster Startzeit Stunde
+channel-type.bmwconnecteddrive.window-start-minute-channel.label = Ladefenster Startzeit Minute
+channel-type.bmwconnecteddrive.window-end-channel.label = Ladefenster Endzeit
+channel-type.bmwconnecteddrive.window-end-hour-channel.label = Ladefenster Endzeit Stunde
+channel-type.bmwconnecteddrive.window-end-minute-channel.label = Ladefenster Endzeit Minute
+channel-type.bmwconnecteddrive.timer1-enabled-channel.label = Zeitprofil 1 - Aktiviert
+channel-type.bmwconnecteddrive.timer1-departure-channel.label = Zeitprofil 1 - Abfahrtszeit
+channel-type.bmwconnecteddrive.timer1-departure-hour-channel.label = Zeitprofil 1 - Abfahrtszeit Stunde
+channel-type.bmwconnecteddrive.timer1-departure-minute-channel.label = Zeitprofil 1 - Abfahrtszeit Minute
+channel-type.bmwconnecteddrive.timer1-days-channel.label = Zeitprofil 1 - Tage
+channel-type.bmwconnecteddrive.timer1-day-mon-channel.label = Zeitprofil 1 - Montag
+channel-type.bmwconnecteddrive.timer1-day-tue-channel.label = Zeitprofil 1 - Dienstag
+channel-type.bmwconnecteddrive.timer1-day-wed-channel.label = Zeitprofil 1 - Mittwoch
+channel-type.bmwconnecteddrive.timer1-day-thu-channel.label = Zeitprofil 1 - Donnerstag
+channel-type.bmwconnecteddrive.timer1-day-fri-channel.label = Zeitprofil 1 - Freitag
+channel-type.bmwconnecteddrive.timer1-day-sat-channel.label = Zeitprofil 1 - Samstag
+channel-type.bmwconnecteddrive.timer1-day-sun-channel.label = Zeitprofil 1 - Sonntag
+channel-type.bmwconnecteddrive.timer2-enabled-channel.label = Zeitprofil 2 - Aktiviert
+channel-type.bmwconnecteddrive.timer2-departure-channel.label = Zeitprofil 2 - Abfahrtszeit
+channel-type.bmwconnecteddrive.timer2-departure-hour-channel.label = Zeitprofil 2 - Abfahrtszeit Stunde
+channel-type.bmwconnecteddrive.timer2-departure-minute-channel.label = Zeitprofil 2 - Abfahrtszeit Minute
+channel-type.bmwconnecteddrive.timer2-days-channel.label = Zeitprofil 2 - Tage
+channel-type.bmwconnecteddrive.timer2-day-mon-channel.label = Zeitprofil 2 - Montag
+channel-type.bmwconnecteddrive.timer2-day-tue-channel.label = Zeitprofil 2 - Dienstag
+channel-type.bmwconnecteddrive.timer2-day-wed-channel.label = Zeitprofil 2 - Mittwoch
+channel-type.bmwconnecteddrive.timer2-day-thu-channel.label = Zeitprofil 2 - Donnerstag
+channel-type.bmwconnecteddrive.timer2-day-fri-channel.label = Zeitprofil 2 - Freitag
+channel-type.bmwconnecteddrive.timer2-day-sat-channel.label = Zeitprofil 2 - Samstag
+channel-type.bmwconnecteddrive.timer2-day-sun-channel.label = Zeitprofil 2 - Sonnatg
+channel-type.bmwconnecteddrive.timer3-enabled-channel.label = Zeitprofil 3 - Aktiviert
+channel-type.bmwconnecteddrive.timer3-departure-channel.label = Zeitprofil 3 - Abfahrtszeit
+channel-type.bmwconnecteddrive.timer3-departure-hour-channel.label = Zeitprofil 3 - Abfahrtszeit Stunde
+channel-type.bmwconnecteddrive.timer3-departure-minute-channel.label = Zeitprofil 3 - Abfahrtszeit Minute
+channel-type.bmwconnecteddrive.timer3-days-channel.label = Zeitprofil 3 - Tage
+channel-type.bmwconnecteddrive.timer3-day-mon-channel.label = Zeitprofil 3 - Montag
+channel-type.bmwconnecteddrive.timer3-day-tue-channel.label = Zeitprofil 3 - Dienstag
+channel-type.bmwconnecteddrive.timer3-day-wed-channel.label = Zeitprofil 3 - Mittwoch
+channel-type.bmwconnecteddrive.timer3-day-thu-channel.label = Zeitprofil 3 - Donnerstag
+channel-type.bmwconnecteddrive.timer3-day-fri-channel.label = Zeitprofil 3 - Freitag
+channel-type.bmwconnecteddrive.timer3-day-sat-channel.label = Zeitprofil 3 - Samstag
+channel-type.bmwconnecteddrive.timer3-day-sun-channel.label = Zeitprofil 3 - Sonntag
+channel-type.bmwconnecteddrive.override-departure-channel.label = Einmaliges Zeitprofil - Abfahrtszeit
+channel-type.bmwconnecteddrive.override-departure-hour-channel.label = Einmaliges Zeitprofil - Abfahrtszeit Stunde 
+channel-type.bmwconnecteddrive.override-departure-minute-channel.label = Einmaliges Zeitprofil - Abfahrtszeit Minute
+channel-type.bmwconnecteddrive.override-enabled-channel.label = Einmaliges Zeitprofil - Aktiviert
+
+channel-type.bmwconnecteddrive.destination-name-channel.label = Zieladresse
+channel-type.bmwconnecteddrive.destination-gps-channel.label = Zielkoordinaten
+
+channel-type.bmwconnecteddrive.gps-channel.label = Koordinaten
+channel-type.bmwconnecteddrive.heading-channel.label = Ausrichtung
+
+channel-type.bmwconnecteddrive.trip-date-time-channel.label = Datum
+channel-type.bmwconnecteddrive.trip-duration-channel.label = Dauer
+channel-type.bmwconnecteddrive.distance-channel.label = Distanz 
+channel-type.bmwconnecteddrive.distance-since-charging-channel.label = Strecke seit Ladung
+channel-type.bmwconnecteddrive.average-consumption-channel.label = Elektrischer Verbrauch
+channel-type.bmwconnecteddrive.average-consumption-channel.description = Elektrischer Durchnittsverbaruch Ã¼ber 100 km/mi
+channel-type.bmwconnecteddrive.average-combined-consumption-channel.label = Kombinierter Verbrauch 
+channel-type.bmwconnecteddrive.average-combined-consumption-channel.description = Kombinierter Durchnittsverbaruch in Liter Ã¼ber 100 km/mi
+channel-type.bmwconnecteddrive.average-recuperation-channel.label = Rekuperation Durchschnitt 
+channel-type.bmwconnecteddrive.average-recuperation-channel.description = Durchschnittliche Rekuperation Ã¼ber 100 km/mi 
+channel-type.bmwconnecteddrive.total-driven-distance-channel.label = Elektrisch gefahrene Distanz
+channel-type.bmwconnecteddrive.single-longest-distance-channel.label = Längste Fahrt mit einer Ladung
+
+channel-type.bmwconnecteddrive.remote-command-channel.label = Kommando Auswahl
+channel-type.bmwconnecteddrive.remote-command-channel.option.light = Lichthupe Ausführen
+channel-type.bmwconnecteddrive.remote-command-channel.option.finder = Fahrzeug Lokalisieren
+channel-type.bmwconnecteddrive.remote-command-channel.option.lock = Fahrzeug Abschließen
+channel-type.bmwconnecteddrive.remote-command-channel.option.unlock = Fahrzug Aufschließen
+channel-type.bmwconnecteddrive.remote-command-channel.option.horn = Hupe Aktivieren
+channel-type.bmwconnecteddrive.remote-command-channel.option.climate = Klimatisierung Ausführen
+channel-type.bmwconnecteddrive.remote-state-channel.label = Ausführungszustand
+
+
+channel-type.bmwconnecteddrive.png-channel.label = Fahrzeug Bild 
+channel-type.bmwconnecteddrive.image-view-channel.label = Fahrzeug Ansicht 
+channel-type.bmwconnecteddrive.image-size-channel.label = Fahrzeug Bildgröße 
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/bridge-connected-drive.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/bridge-connected-drive.xml
new file mode 100644 (file)
index 0000000..0d67101
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <bridge-type id="account">
+               <label>BMW ConnectedDrive Account</label>
+               <description>Access to BMW ConnectedDrive Portal for a specific user</description>
+               <config-description-ref uri="thing-type:bmwconnecteddrive:bridge"/>
+       </bridge-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/charge-channel-groups.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/charge-channel-groups.xml
new file mode 100644 (file)
index 0000000..cb3537a
--- /dev/null
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="charge-values">
+               <label>Electric Charging</label>
+               <description>Charge Profiles of Vehicle</description>
+               <channels>
+                       <channel id="profile-climate" typeId="profile-climate-channel"/>
+                       <channel id="profile-mode" typeId="profile-mode-channel"/>
+                       <channel id="profile-prefs" typeId="profile-prefs-channel"/>
+                       <channel id="window-start" typeId="window-start-channel"/>
+                       <channel id="window-end" typeId="window-end-channel"/>
+                       <channel id="timer1-departure" typeId="timer1-departure-channel"/>
+                       <channel id="timer1-days" typeId="timer1-days-channel"/>
+                       <channel id="timer1-day-mon" typeId="timer1-day-mon-channel"/>
+                       <channel id="timer1-day-tue" typeId="timer1-day-tue-channel"/>
+                       <channel id="timer1-day-wed" typeId="timer1-day-wed-channel"/>
+                       <channel id="timer1-day-thu" typeId="timer1-day-thu-channel"/>
+                       <channel id="timer1-day-fri" typeId="timer1-day-fri-channel"/>
+                       <channel id="timer1-day-sat" typeId="timer1-day-sat-channel"/>
+                       <channel id="timer1-day-sun" typeId="timer1-day-sun-channel"/>
+                       <channel id="timer1-enabled" typeId="timer1-enabled-channel"/>
+                       <channel id="timer2-departure" typeId="timer2-departure-channel"/>
+                       <channel id="timer2-days" typeId="timer2-days-channel"/>
+                       <channel id="timer2-day-mon" typeId="timer2-day-mon-channel"/>
+                       <channel id="timer2-day-tue" typeId="timer2-day-tue-channel"/>
+                       <channel id="timer2-day-wed" typeId="timer2-day-wed-channel"/>
+                       <channel id="timer2-day-thu" typeId="timer2-day-thu-channel"/>
+                       <channel id="timer2-day-fri" typeId="timer2-day-fri-channel"/>
+                       <channel id="timer2-day-sat" typeId="timer2-day-sat-channel"/>
+                       <channel id="timer2-day-sun" typeId="timer2-day-sun-channel"/>
+                       <channel id="timer2-enabled" typeId="timer2-enabled-channel"/>
+                       <channel id="timer3-departure" typeId="timer3-departure-channel"/>
+                       <channel id="timer3-days" typeId="timer3-days-channel"/>
+                       <channel id="timer3-day-mon" typeId="timer3-day-mon-channel"/>
+                       <channel id="timer3-day-tue" typeId="timer3-day-tue-channel"/>
+                       <channel id="timer3-day-wed" typeId="timer3-day-wed-channel"/>
+                       <channel id="timer3-day-thu" typeId="timer3-day-thu-channel"/>
+                       <channel id="timer3-day-fri" typeId="timer3-day-fri-channel"/>
+                       <channel id="timer3-day-sat" typeId="timer3-day-sat-channel"/>
+                       <channel id="timer3-day-sun" typeId="timer3-day-sun-channel"/>
+                       <channel id="timer3-enabled" typeId="timer3-enabled-channel"/>
+                       <channel id="override-departure" typeId="override-departure-channel"/>
+                       <channel id="override-enabled" typeId="override-enabled-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/charge-channel-types.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/charge-channel-types.xml
new file mode 100644 (file)
index 0000000..47e9483
--- /dev/null
@@ -0,0 +1,208 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-type id="profile-climate-channel">
+               <item-type>Switch</item-type>
+               <label>A/C at Departure Time</label>
+       </channel-type>
+       <channel-type id="profile-mode-channel">
+               <item-type>String</item-type>
+               <label>Charge Mode</label>
+               <description>Mode for selecting immediate or delyed charging</description>
+               <command>
+                       <options>
+                               <option value="IMMEDIATE_CHARGING">Immediate Charging</option>
+                               <option value="DELAYED_CHARGING">Prefer Charging in Charging Window</option>
+                       </options>
+               </command>
+       </channel-type>
+       <channel-type id="profile-prefs-channel">
+               <item-type>String</item-type>
+               <label>Charge Preferences</label>
+               <description>Preferences for delayed charging</description>
+               <command>
+                       <options>
+                               <option value="NO_PRESELECTION">No Preference</option>
+                               <option value="CHARGING_WINDOW">Charging Window</option>
+                       </options>
+               </command>
+       </channel-type>
+       <channel-type id="window-start-channel">
+               <item-type>DateTime</item-type>
+               <label>Window Start Time</label>
+               <description>Start time of charging window</description>
+               <state pattern="%1$tH:%1$tM" readOnly="false"/>
+       </channel-type>
+       <channel-type id="window-end-channel">
+               <item-type>DateTime</item-type>
+               <label>Window End Time</label>
+               <description>End time of charging window</description>
+               <state pattern="%1$tH:%1$tM" readOnly="false"/>
+       </channel-type>
+       <channel-type id="timer1-departure-channel">
+               <item-type>DateTime</item-type>
+               <label>T1 Departure Time</label>
+               <description>Departure time for regular schedule timer 1</description>
+               <state pattern="%1$tH:%1$tM" readOnly="false"/>
+       </channel-type>
+       <channel-type id="timer1-days-channel">
+               <item-type>String</item-type>
+               <label>T1 Days</label>
+               <description>Days scheduled for timer 1</description>
+       </channel-type>
+       <channel-type id="timer1-day-mon-channel">
+               <item-type>Switch</item-type>
+               <label>T1 Monday</label>
+               <description>Monday scheduled for timer 1</description>
+       </channel-type>
+       <channel-type id="timer1-day-tue-channel">
+               <item-type>Switch</item-type>
+               <label>T1 Tuesday</label>
+               <description>Tuesday scheduled for timer 1</description>
+       </channel-type>
+       <channel-type id="timer1-day-wed-channel">
+               <item-type>Switch</item-type>
+               <label>T1 Wednesday</label>
+               <description>Wednesday scheduled for timer 1</description>
+       </channel-type>
+       <channel-type id="timer1-day-thu-channel">
+               <item-type>Switch</item-type>
+               <label>T1 Thursday</label>
+               <description>Thursday scheduled for timer 1</description>
+       </channel-type>
+       <channel-type id="timer1-day-fri-channel">
+               <item-type>Switch</item-type>
+               <label>T1 Friday</label>
+               <description>Friday scheduled for timer 1</description>
+       </channel-type>
+       <channel-type id="timer1-day-sat-channel">
+               <item-type>Switch</item-type>
+               <label>T1 Saturday</label>
+               <description>Saturday scheduled for timer 1</description>
+       </channel-type>
+       <channel-type id="timer1-day-sun-channel">
+               <item-type>Switch</item-type>
+               <label>T1 Sunday</label>
+               <description>Sunday scheduled for timer 1</description>
+       </channel-type>
+       <channel-type id="timer1-enabled-channel">
+               <item-type>Switch</item-type>
+               <label>T1 Enabled</label>
+               <description>Timer 1 enabled</description>
+       </channel-type>
+       <channel-type id="timer2-departure-channel">
+               <item-type>DateTime</item-type>
+               <label>T2 Departure Time</label>
+               <description>Departure time for regular schedule timer 2</description>
+               <state pattern="%1$tH:%1$tM" readOnly="false"/>
+       </channel-type>
+       <channel-type id="timer2-days-channel">
+               <item-type>String</item-type>
+               <label>T2 Days</label>
+               <description>Days scheduled for timer 2</description>
+       </channel-type>
+       <channel-type id="timer2-day-mon-channel">
+               <item-type>Switch</item-type>
+               <label>T2 Monday</label>
+               <description>Monday scheduled for timer 2</description>
+       </channel-type>
+       <channel-type id="timer2-day-tue-channel">
+               <item-type>Switch</item-type>
+               <label>T2 Tuesday</label>
+               <description>Tuesday scheduled for timer 2</description>
+       </channel-type>
+       <channel-type id="timer2-day-wed-channel">
+               <item-type>Switch</item-type>
+               <label>T2 Wednesday</label>
+               <description>Wednesday scheduled for timer 2</description>
+       </channel-type>
+       <channel-type id="timer2-day-thu-channel">
+               <item-type>Switch</item-type>
+               <label>T2 Thursday</label>
+               <description>Thursday scheduled for timer 2</description>
+       </channel-type>
+       <channel-type id="timer2-day-fri-channel">
+               <item-type>Switch</item-type>
+               <label>T2 Friday</label>
+               <description>Friday scheduled for timer 2</description>
+       </channel-type>
+       <channel-type id="timer2-day-sat-channel">
+               <item-type>Switch</item-type>
+               <label>T2 Saturday</label>
+               <description>Saturday scheduled for timer 2</description>
+       </channel-type>
+       <channel-type id="timer2-day-sun-channel">
+               <item-type>Switch</item-type>
+               <label>T2 Sunday</label>
+               <description>Sunday scheduled for timer 2</description>
+       </channel-type>
+       <channel-type id="timer2-enabled-channel">
+               <item-type>Switch</item-type>
+               <label>T2 Enabled</label>
+               <description>Timer 2 enabled</description>
+       </channel-type>
+       <channel-type id="timer3-departure-channel">
+               <item-type>DateTime</item-type>
+               <label>T3 Departure Time</label>
+               <description>Departure time for regular schedule timer 3</description>
+               <state pattern="%1$tH:%1$tM" readOnly="false"/>
+       </channel-type>
+       <channel-type id="timer3-days-channel">
+               <item-type>String</item-type>
+               <label>T3 Days</label>
+               <description>Days scheduled for timer 3</description>
+       </channel-type>
+       <channel-type id="timer3-day-mon-channel">
+               <item-type>Switch</item-type>
+               <label>T3 Monday</label>
+               <description>Monday scheduled for timer 3</description>
+       </channel-type>
+       <channel-type id="timer3-day-tue-channel">
+               <item-type>Switch</item-type>
+               <label>T3 Tuesday</label>
+               <description>Tuesday scheduled for timer 3</description>
+       </channel-type>
+       <channel-type id="timer3-day-wed-channel">
+               <item-type>Switch</item-type>
+               <label>T3 Wednesday</label>
+               <description>Wednesday scheduled for timer 3</description>
+       </channel-type>
+       <channel-type id="timer3-day-thu-channel">
+               <item-type>Switch</item-type>
+               <label>T3 Thursday</label>
+               <description>Thursday scheduled for timer 3</description>
+       </channel-type>
+       <channel-type id="timer3-day-fri-channel">
+               <item-type>Switch</item-type>
+               <label>T3 Friday</label>
+               <description>Friday scheduled for timer 3</description>
+       </channel-type>
+       <channel-type id="timer3-day-sat-channel">
+               <item-type>Switch</item-type>
+               <label>T3 Saturday</label>
+               <description>Saturday scheduled for timer 3</description>
+       </channel-type>
+       <channel-type id="timer3-day-sun-channel">
+               <item-type>Switch</item-type>
+               <label>T3 Sunday</label>
+               <description>Sunday scheduled for timer 3</description>
+       </channel-type>
+       <channel-type id="timer3-enabled-channel">
+               <item-type>Switch</item-type>
+               <label>T3 Enabled</label>
+               <description>Timer 3 enabled</description>
+       </channel-type>
+       <channel-type id="override-departure-channel">
+               <item-type>DateTime</item-type>
+               <label>OT Departure Time</label>
+               <description>Departure time for override timer</description>
+               <state pattern="%1$tH:%1$tM" readOnly="false"/>
+       </channel-type>
+       <channel-type id="override-enabled-channel">
+               <item-type>Switch</item-type>
+               <label>OT Enabled</label>
+               <description>Override timer enabled</description>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/check-control-channel-types.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/check-control-channel-types.xml
new file mode 100644 (file)
index 0000000..d1e9597
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-type id="checkcontrol-name-channel">
+               <item-type>String</item-type>
+               <label>CheckControl Description</label>
+       </channel-type>
+       <channel-type id="checkcontrol-details-channel">
+               <item-type>String</item-type>
+               <label>CheckControl Details</label>
+       </channel-type>
+       <channel-type id="checkcontrol-mileage-channel">
+               <item-type>Number:Length</item-type>
+               <label>Mileage Occurrence</label>
+               <state pattern="%d %unit%"/>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/check-control-group.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/check-control-group.xml
new file mode 100644 (file)
index 0000000..443c1a1
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="check-control-values">
+               <label>Check Control Messages</label>
+               <description>Show the current active CheckControl Messages</description>
+               <channels>
+                       <channel id="name" typeId="checkcontrol-name-channel"/>
+                       <channel id="details" typeId="checkcontrol-details-channel"/>
+                       <channel id="mileage" typeId="checkcontrol-mileage-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/conv-range-channel-group.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/conv-range-channel-group.xml
new file mode 100644 (file)
index 0000000..df8b118
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="conv-range-values">
+               <label>Range Data</label>
+               <description>Provides Mileage, remaining range and fuel level values</description>
+               <channels>
+                       <channel id="mileage" typeId="mileage-channel"/>
+                       <channel id="fuel" typeId="range-fuel-channel"/>
+                       <channel id="remaining-fuel" typeId="remaining-fuel-channel"/>
+                       <channel id="radius-fuel" typeId="range-radius-fuel-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/destination-channel-types.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/destination-channel-types.xml
new file mode 100644 (file)
index 0000000..4e87558
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-type id="destination-name-channel">
+               <item-type>String</item-type>
+               <label>Name</label>
+       </channel-type>
+       <channel-type id="destination-gps-channel">
+               <item-type>Location</item-type>
+               <label>GPS Coordinates</label>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/destination-group.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/destination-group.xml
new file mode 100644 (file)
index 0000000..0e4dc95
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="destination-values">
+               <label>Destination List</label>
+               <description>Shows Your Destinations in a List</description>
+               <channels>
+                       <channel id="name" typeId="destination-name-channel"/>
+                       <channel id="gps" typeId="destination-gps-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/door-status-channel-types.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/door-status-channel-types.xml
new file mode 100644 (file)
index 0000000..89aa6df
--- /dev/null
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-type id="driver-front-channel">
+               <item-type>String</item-type>
+               <label>Driver Door</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="driver-rear-channel">
+               <item-type>String</item-type>
+               <label>Driver Door Rear</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="passenger-front-channel">
+               <item-type>String</item-type>
+               <label>Passenger Door</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="passenger-rear-channel">
+               <item-type>String</item-type>
+               <label>Passenger Door Rear</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="hood-channel">
+               <item-type>String</item-type>
+               <label>Hood</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="trunk-channel">
+               <item-type>String</item-type>
+               <label>Trunk</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="window-driver-front-channel">
+               <item-type>String</item-type>
+               <label>Driver Window</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="window-driver-rear-channel">
+               <item-type>String</item-type>
+               <label>Driver Rear Window</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="window-passenger-front-channel">
+               <item-type>String</item-type>
+               <label>Passenger Window</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="window-passenger-rear-channel">
+               <item-type>String</item-type>
+               <label>Passenger Rear Window</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="window-rear-channel">
+               <item-type>String</item-type>
+               <label>Rear Window</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="sunroof-channel">
+               <item-type>String</item-type>
+               <label>Sunroof</label>
+               <state readOnly="true"/>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/doors-status-group.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/doors-status-group.xml
new file mode 100644 (file)
index 0000000..4a05e8c
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="door-values">
+               <label>Detailed Door Status</label>
+               <description>Detailed Status of all Doors and Windows</description>
+               <channels>
+                       <channel id="driver-front" typeId="driver-front-channel"/>
+                       <channel id="driver-rear" typeId="driver-rear-channel"/>
+                       <channel id="passenger-front" typeId="passenger-front-channel"/>
+                       <channel id="passenger-rear" typeId="passenger-rear-channel"/>
+                       <channel id="hood" typeId="hood-channel"/>
+                       <channel id="trunk" typeId="trunk-channel"/>
+                       <channel id="win-driver-front" typeId="window-driver-front-channel"/>
+                       <channel id="win-driver-rear" typeId="window-driver-rear-channel"/>
+                       <channel id="win-passenger-front" typeId="window-passenger-front-channel"/>
+                       <channel id="win-passenger-rear" typeId="window-passenger-rear-channel"/>
+                       <channel id="win-rear" typeId="window-rear-channel"/>
+                       <channel id="sunroof" typeId="sunroof-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-last-trip-group.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-last-trip-group.xml
new file mode 100644 (file)
index 0000000..5e0d008
--- /dev/null
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="ev-last-trip-values">
+               <label>Last Trip Statistics</label>
+               <description>EV Consumption Values and Distances for the Last Trip</description>
+               <channels>
+                       <channel id="date" typeId="trip-date-time-channel"/>
+                       <channel id="duration" typeId="trip-duration-channel"/>
+                       <channel id="distance" typeId="distance-channel"/>
+                       <channel id="distance-since-charging" typeId="distance-since-charging-channel"/>
+                       <channel id="avg-consumption" typeId="average-consumption-channel"/>
+                       <channel id="avg-recuperation" typeId="average-recuperation-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-lifetime-group.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-lifetime-group.xml
new file mode 100644 (file)
index 0000000..fcf1a65
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="ev-lifetime-values">
+               <label>Lifetime Statistics</label>
+               <description>Consumption Values and Distances over Lifetime</description>
+               <channels>
+                       <channel id="avg-consumption" typeId="average-consumption-channel"/>
+                       <channel id="avg-recuperation" typeId="average-recuperation-channel"/>
+                       <channel id="total-driven-distance" typeId="total-driven-distance-channel"/>
+                       <channel id="single-longest-distance" typeId="single-longest-distance-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-range-channel-group.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-range-channel-group.xml
new file mode 100644 (file)
index 0000000..c50b506
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="ev-range-values">
+               <label>Electric Range Data</label>
+               <description>Provides Mileage, remaining range and charge level values</description>
+               <channels>
+                       <channel id="mileage" typeId="mileage-channel"/>
+                       <channel id="electric" typeId="range-electric-channel"/>
+                       <channel id="soc" typeId="soc-channel"/>
+                       <channel id="radius-electric" typeId="range-radius-electric-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-vehicle-status-group.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-vehicle-status-group.xml
new file mode 100644 (file)
index 0000000..74fbaa8
--- /dev/null
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="ev-vehicle-status">
+               <label>Vehicle Status</label>
+               <description>Provides Status of Doors, Windows, Lock State, Service and Check Control Messages</description>
+               <channels>
+                       <channel id="doors" typeId="doors-channel"/>
+                       <channel id="windows" typeId="windows-channel"/>
+                       <channel id="lock" typeId="lock-channel"/>
+                       <channel id="service-date" typeId="next-service-date-channel"/>
+                       <channel id="service-mileage" typeId="next-service-mileage-channel"/>
+                       <channel id="check-control" typeId="check-control-channel"/>
+                       <channel id="charge" typeId="charging-status-channel"/>
+                       <channel id="remaining" typeId="charging-remaining-channel"/>
+                       <channel id="last-update" typeId="last-update-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/hybrid-last-trip-group.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/hybrid-last-trip-group.xml
new file mode 100644 (file)
index 0000000..ceac5b1
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="hybrid-last-trip-values">
+               <label>Last Trip Statistics</label>
+               <description>Hybrid Consumption Values and Distances for the Last Trip</description>
+               <channels>
+                       <channel id="date" typeId="trip-date-time-channel"/>
+                       <channel id="duration" typeId="trip-duration-channel"/>
+                       <channel id="distance" typeId="distance-channel"/>
+                       <channel id="distance-since-charging" typeId="distance-since-charging-channel"/>
+                       <channel id="avg-consumption" typeId="average-consumption-channel"/>
+                       <channel id="avg-combined-consumption" typeId="average-combined-consumption-channel"/>
+                       <channel id="avg-recuperation" typeId="average-recuperation-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/hybrid-lifetime-group.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/hybrid-lifetime-group.xml
new file mode 100644 (file)
index 0000000..1def2fc
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="hybrid-lifetime-values">
+               <label>Lifetime Statistics</label>
+               <description>Consumption Values and Distances over Lifetime</description>
+               <channels>
+                       <channel id="avg-consumption" typeId="average-consumption-channel"/>
+                       <channel id="avg-combined-consumption" typeId="average-combined-consumption-channel"/>
+                       <channel id="avg-recuperation" typeId="average-recuperation-channel"/>
+                       <channel id="total-driven-distance" typeId="total-driven-distance-channel"/>
+                       <channel id="single-longest-distance" typeId="single-longest-distance-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/hybrid-range-channel-group.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/hybrid-range-channel-group.xml
new file mode 100644 (file)
index 0000000..a260df0
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="hybrid-range-values">
+               <label>Hybrid Range Data</label>
+               <description>Provides Mileage, remaining range and fuel and charge level values</description>
+               <channels>
+                       <channel id="mileage" typeId="mileage-channel"/>
+                       <channel id="hybrid" typeId="range-hybrid-channel"/>
+                       <channel id="electric" typeId="range-electric-channel"/>
+                       <channel id="soc" typeId="soc-channel"/>
+                       <channel id="fuel" typeId="range-fuel-channel"/>
+                       <channel id="remaining-fuel" typeId="remaining-fuel-channel"/>
+                       <channel id="radius-electric" typeId="range-radius-electric-channel"/>
+                       <channel id="radius-hybrid" typeId="range-radius-hybrid-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/image-channel-group.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/image-channel-group.xml
new file mode 100644 (file)
index 0000000..88dc047
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="image-values">
+               <label>Vehicle Image</label>
+               <description>Provides an Image of your Vehicle</description>
+               <channels>
+                       <channel id="png" typeId="png-channel"/>
+                       <channel id="size" typeId="image-size-channel"/>
+                       <channel id="view" typeId="image-view-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/image-channel-types.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/image-channel-types.xml
new file mode 100644 (file)
index 0000000..78842c1
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-type id="png-channel">
+               <item-type>Image</item-type>
+               <label>Rendered Vehicle Image</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="image-view-channel">
+               <item-type>String</item-type>
+               <label>Image Viewport</label>
+               <command>
+                       <options>
+                               <option value="FRONT">Front View</option>
+                               <option value="REAR">Rear View</option>
+                               <option value="SIDE">Side View</option>
+                               <option value="DASHBOARD">Dashboard View</option>
+                               <option value="DRIVERDOOR">Driver Door View</option>
+                       </options>
+               </command>
+       </channel-type>
+       <channel-type id="image-size-channel">
+               <item-type>Number</item-type>
+               <label>Image Size</label>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/last-trip-channel-types.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/last-trip-channel-types.xml
new file mode 100644 (file)
index 0000000..332e19a
--- /dev/null
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-type id="trip-date-time-channel">
+               <item-type>DateTime</item-type>
+               <label>Date and Time</label>
+               <state pattern="%1$tA, %1$td.%1$tm. %1$tH:%1$tM" readOnly="true"/>
+       </channel-type>
+       <channel-type id="trip-duration-channel">
+               <item-type>Number:Time</item-type>
+               <label>Last Trip Duration</label>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="distance-channel">
+               <item-type>Number:Length</item-type>
+               <label>Last Trip Distance</label>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="distance-since-charging-channel">
+               <item-type>Number:Length</item-type>
+               <label>Distance since Charge</label>
+               <description>Total distance driven since last charging</description>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="average-consumption-channel">
+               <item-type>Number:Energy</item-type>
+               <label>Avg. Power Consumption</label>
+               <description>Average electric power consumption per 100 km/mi</description>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="average-combined-consumption-channel">
+               <item-type>Number:Volume</item-type>
+               <label>Avg. Combined Consumption</label>
+               <description>Average combined consumption in liter per 100 km/mi</description>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="average-recuperation-channel">
+               <item-type>Number:Energy</item-type>
+               <label>Avg. Recuperation</label>
+               <description>Average electric recuperation per 100 km/mi</description>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/lifetime-channel-types.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/lifetime-channel-types.xml
new file mode 100644 (file)
index 0000000..e4b1567
--- /dev/null
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-type id="total-driven-distance-channel">
+               <item-type>Number:Length</item-type>
+               <label>Total Electric Distance</label>
+               <state pattern="%.0f %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="single-longest-distance-channel">
+               <item-type>Number:Length</item-type>
+               <label>Longest 1-Charge Distance</label>
+               <state pattern="%.0f %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="average-consumption-channel">
+               <item-type>Number:Energy</item-type>
+               <label>Avg. Power Consumption</label>
+               <description>Average Combined Consumption electric power consumption per 100 km/mi</description>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="average-recuperation-channel">
+               <item-type>Number:Energy</item-type>
+               <label>Avg. Combined Consumption Recuperation</label>
+               <description>Average electric recuperation per 100 km/mi</description>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="average-combined-consumption-channel">
+               <item-type>Number:Volume</item-type>
+               <label>Avg. Combined Consumption</label>
+               <description>Average combined consumption in liter per 100 km/mi</description>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/location-channel-group.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/location-channel-group.xml
new file mode 100644 (file)
index 0000000..8c03858
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="location-values">
+               <label>Vehicle Location</label>
+               <description>Coordinates and Heading of the Vehcile</description>
+               <channels>
+                       <channel id="gps" typeId="gps-channel"/>
+                       <channel id="heading" typeId="heading-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/location-channel-types.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/location-channel-types.xml
new file mode 100644 (file)
index 0000000..e275a1b
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-type id="gps-channel">
+               <item-type>Location</item-type>
+               <label>GPS Coordinates</label>
+       </channel-type>
+       <channel-type id="heading-channel">
+               <item-type>Number:Angle</item-type>
+               <label>Heading Angle</label>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/range-channel-types.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/range-channel-types.xml
new file mode 100644 (file)
index 0000000..b289d9b
--- /dev/null
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-type id="mileage-channel">
+               <item-type>Number:Length</item-type>
+               <label>Total Distance Driven</label>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="range-electric-channel">
+               <item-type>Number:Length</item-type>
+               <label>Electric Range</label>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="range-fuel-channel">
+               <item-type>Number:Length</item-type>
+               <label>Fuel Range</label>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="range-hybrid-channel">
+               <item-type>Number:Length</item-type>
+               <label>Hybrid Range</label>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="soc-channel">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Battery Charge Level</label>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="remaining-fuel-channel">
+               <item-type>Number:Volume</item-type>
+               <label>Remaining Fuel</label>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="range-radius-electric-channel">
+               <item-type>Number:Length</item-type>
+               <label>Electric Range Radius</label>
+               <state pattern="%.0f %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="range-radius-fuel-channel">
+               <item-type>Number:Length</item-type>
+               <label>Fuel Range Radius</label>
+               <state pattern="%.0f %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="range-radius-hybrid-channel">
+               <item-type>Number:Length</item-type>
+               <label>Hybrid Range Radius</label>
+               <state pattern="%.0f %unit%" readOnly="true"/>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/remote-services-channel-group.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/remote-services-channel-group.xml
new file mode 100644 (file)
index 0000000..b96bce3
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="remote-services">
+               <label>Remote Services</label>
+               <description>Services can be executed via BMW Server like Door lock/unlock, Air Conditioning and more</description>
+               <channels>
+                       <channel id="command" typeId="remote-command-channel"/>
+                       <channel id="state" typeId="remote-state-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/remote-services-channel-types.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/remote-services-channel-types.xml
new file mode 100644 (file)
index 0000000..aa9b2df
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-type id="remote-command-channel">
+               <item-type>String</item-type>
+               <label>Remote Command</label>
+       </channel-type>
+       <channel-type id="remote-state-channel">
+               <item-type>String</item-type>
+               <label>Service Execution State</label>
+               <state readOnly="true"/>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/service-channel-types.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/service-channel-types.xml
new file mode 100644 (file)
index 0000000..3f558b1
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-type id="service-name-channel">
+               <item-type>String</item-type>
+               <label>Service Name</label>
+       </channel-type>
+       <channel-type id="service-details-channel">
+               <item-type>String</item-type>
+               <label>Service Details</label>
+       </channel-type>
+       <channel-type id="service-date-channel">
+               <item-type>DateTime</item-type>
+               <label>Service Date</label>
+               <state pattern="%1$tb %1$tY"/>
+       </channel-type>
+       <channel-type id="service-mileage-channel">
+               <item-type>Number:Length</item-type>
+               <label>Mileage till Service</label>
+               <state pattern="%d %unit%"/>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/service-group.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/service-group.xml
new file mode 100644 (file)
index 0000000..668fd63
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="service-values">
+               <label>Vehicle Services</label>
+               <description>All future Service schedules</description>
+               <channels>
+                       <channel id="name" typeId="service-name-channel"/>
+                       <channel id="details" typeId="service-details-channel"/>
+                       <channel id="date" typeId="service-date-channel"/>
+                       <channel id="mileage" typeId="service-mileage-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/thing-bev.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/thing-bev.xml
new file mode 100644 (file)
index 0000000..c9b6f83
--- /dev/null
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="bev">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="account"/>
+               </supported-bridge-type-refs>
+
+               <label>Electric Vehicle</label>
+               <description>Battery Electric Vehicle (BEV)</description>
+
+               <channel-groups>
+                       <channel-group id="status" typeId="ev-vehicle-status"/>
+                       <channel-group id="doors" typeId="door-values"/>
+                       <channel-group id="range" typeId="ev-range-values"/>
+                       <channel-group id="check" typeId="check-control-values"/>
+                       <channel-group id="service" typeId="service-values"/>
+                       <channel-group id="location" typeId="location-values"/>
+                       <channel-group id="remote" typeId="remote-services"/>
+                       <channel-group id="charge" typeId="charge-values"/>
+                       <channel-group id="last-trip" typeId="ev-last-trip-values"/>
+                       <channel-group id="lifetime" typeId="ev-lifetime-values"/>
+                       <channel-group id="destination" typeId="destination-values"/>
+                       <channel-group id="image" typeId="image-values"/>
+               </channel-groups>
+
+               <representation-property>vin</representation-property>
+
+               <config-description-ref uri="thing-type:bmwconnecteddrive:vehicle"/>
+       </thing-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/thing-bev_rex.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/thing-bev_rex.xml
new file mode 100644 (file)
index 0000000..a685d37
--- /dev/null
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="bev_rex">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="account"/>
+               </supported-bridge-type-refs>
+
+               <label>Electric Vehicle with REX</label>
+               <description>Battery Electric Vehicle with Range Extender (BEV_REX)</description>
+
+               <channel-groups>
+                       <channel-group id="status" typeId="ev-vehicle-status"/>
+                       <channel-group id="doors" typeId="door-values"/>
+                       <channel-group id="range" typeId="hybrid-range-values"/>
+                       <channel-group id="check" typeId="check-control-values"/>
+                       <channel-group id="service" typeId="service-values"/>
+                       <channel-group id="location" typeId="location-values"/>
+                       <channel-group id="remote" typeId="remote-services"/>
+                       <channel-group id="charge" typeId="charge-values"/>
+                       <channel-group id="last-trip" typeId="hybrid-last-trip-values"/>
+                       <channel-group id="lifetime" typeId="hybrid-lifetime-values"/>
+                       <channel-group id="destination" typeId="destination-values"/>
+                       <channel-group id="image" typeId="image-values"/>
+               </channel-groups>
+
+               <representation-property>vin</representation-property>
+
+               <config-description-ref uri="thing-type:bmwconnecteddrive:vehicle"/>
+       </thing-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/thing-conv.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/thing-conv.xml
new file mode 100644 (file)
index 0000000..a0056b2
--- /dev/null
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="conv">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="account"/>
+               </supported-bridge-type-refs>
+
+               <label>Conventional Vehicle</label>
+               <description>Conventional Fuel Vehicle (CONV)</description>
+
+               <channel-groups>
+                       <channel-group id="status" typeId="vehicle-status"/>
+                       <channel-group id="doors" typeId="door-values"/>
+                       <channel-group id="range" typeId="conv-range-values"/>
+                       <channel-group id="check" typeId="check-control-values"/>
+                       <channel-group id="service" typeId="service-values"/>
+                       <channel-group id="location" typeId="location-values"/>
+                       <channel-group id="remote" typeId="remote-services"/>
+                       <channel-group id="destination" typeId="destination-values"/>
+                       <channel-group id="image" typeId="image-values"/>
+               </channel-groups>
+
+               <representation-property>vin</representation-property>
+
+               <config-description-ref uri="thing-type:bmwconnecteddrive:vehicle"/>
+       </thing-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/thing-phev.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/thing-phev.xml
new file mode 100644 (file)
index 0000000..33ceb7f
--- /dev/null
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="phev">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="account"/>
+               </supported-bridge-type-refs>
+
+               <label>Plug-In-Hybrid Electric Vehicle</label>
+               <description>Conventional Fuel Vehicle with supporting Electric Engine (PHEV)</description>
+
+               <channel-groups>
+                       <channel-group id="status" typeId="ev-vehicle-status"/>
+                       <channel-group id="doors" typeId="door-values"/>
+                       <channel-group id="range" typeId="hybrid-range-values"/>
+                       <channel-group id="check" typeId="check-control-values"/>
+                       <channel-group id="service" typeId="service-values"/>
+                       <channel-group id="location" typeId="location-values"/>
+                       <channel-group id="remote" typeId="remote-services"/>
+                       <channel-group id="charge" typeId="charge-values"/>
+                       <channel-group id="last-trip" typeId="hybrid-last-trip-values"/>
+                       <channel-group id="lifetime" typeId="hybrid-lifetime-values"/>
+                       <channel-group id="destination" typeId="destination-values"/>
+                       <channel-group id="image" typeId="image-values"/>
+               </channel-groups>
+
+               <representation-property>vin</representation-property>
+
+               <config-description-ref uri="thing-type:bmwconnecteddrive:vehicle"/>
+       </thing-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml
new file mode 100644 (file)
index 0000000..1fa005a
--- /dev/null
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-type id="doors-channel">
+               <item-type>String</item-type>
+               <label>Overall Door Status</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="windows-channel">
+               <item-type>String</item-type>
+               <label>Overall Window Status</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="lock-channel">
+               <item-type>String</item-type>
+               <label>Doors Locked</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="next-service-date-channel">
+               <item-type>DateTime</item-type>
+               <label>Next Service Date</label>
+               <state pattern="%1$tb %1$tY" readOnly="true"/>
+       </channel-type>
+       <channel-type id="next-service-mileage-channel">
+               <item-type>Number:Length</item-type>
+               <label>Mileage Till Next Service</label>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="check-control-channel">
+               <item-type>String</item-type>
+               <label>Check Control</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="charging-status-channel">
+               <item-type>String</item-type>
+               <label>Charging Status</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="charging-remaining-channel">
+               <item-type>Number:Time</item-type>
+               <label>Remaining Charging Time</label>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="last-update-channel">
+               <item-type>DateTime</item-type>
+               <label>Last Status Timestamp</label>
+               <state pattern="%1$tA, %1$td.%1$tm. %1$tH:%1$tM" readOnly="true"/>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/vehicle-status-group.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/vehicle-status-group.xml
new file mode 100644 (file)
index 0000000..f8e99a6
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="bmwconnecteddrive"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <channel-group-type id="vehicle-status">
+               <label>Vehicle Status</label>
+               <description>Provides Status of Doors, Windows, Lock State, Service and Check Control Messages</description>
+               <channels>
+                       <channel id="doors" typeId="doors-channel"/>
+                       <channel id="windows" typeId="windows-channel"/>
+                       <channel id="lock" typeId="lock-channel"/>
+                       <channel id="service-date" typeId="next-service-date-channel"/>
+                       <channel id="service-mileage" typeId="next-service-mileage-channel"/>
+                       <channel id="check-control" typeId="check-control-channel"/>
+                       <channel id="last-update" typeId="last-update-channel"/>
+               </channels>
+       </channel-group-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/discovery/DiscoveryTest.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/discovery/DiscoveryTest.java
new file mode 100644 (file)
index 0000000..612b990
--- /dev/null
@@ -0,0 +1,99 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.discovery;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.discovery.VehiclesContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.ConnectedDriveBridgeHandler;
+import org.openhab.binding.bmwconnecteddrive.internal.util.FileReader;
+import org.openhab.core.config.discovery.DiscoveryListener;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ThingUID;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link DiscoveryTest} Test Discovery Results
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class DiscoveryTest {
+    private final Logger logger = LoggerFactory.getLogger(DiscoveryTest.class);
+    private static final Gson GSON = new Gson();
+    private static final int DISCOVERY_VEHICLES = 9;
+
+    @Test
+    public void testDiscovery() {
+        String content = FileReader.readFileInString("src/test/resources/webapi/connected-drive-account-info.json");
+        ConnectedDriveBridgeHandler bh = mock(ConnectedDriveBridgeHandler.class);
+        Bridge b = mock(Bridge.class);
+        when(bh.getThing()).thenReturn(b);
+        when(b.getUID()).thenReturn(new ThingUID("bmwconnecteddrive", "account", "abc"));
+        VehicleDiscovery discovery = new VehicleDiscovery();
+        discovery.setThingHandler(bh);
+        DiscoveryListener listener = mock(DiscoveryListener.class);
+        discovery.addDiscoveryListener(listener);
+        VehiclesContainer container = GSON.fromJson(content, VehiclesContainer.class);
+        ArgumentCaptor<DiscoveryResult> discoveries = ArgumentCaptor.forClass(DiscoveryResult.class);
+        ArgumentCaptor<DiscoveryService> services = ArgumentCaptor.forClass(DiscoveryService.class);
+        if (container != null) {
+            discovery.onResponse(container);
+            verify(listener, times(1)).thingDiscovered(services.capture(), discoveries.capture());
+            List<DiscoveryResult> results = discoveries.getAllValues();
+            assertEquals(1, results.size(), "Found Vehicles");
+            DiscoveryResult result = results.get(0);
+            assertEquals("bmwconnecteddrive:bev_rex:abc:MY_REAL_VIN", result.getThingUID().getAsString(), "Thing UID");
+        } else {
+            assertTrue(false);
+        }
+    }
+
+    @Test
+    public void testBimmerDiscovery() {
+        String content = FileReader.readFileInString("src/test/resources/responses/vehicles.json");
+        ConnectedDriveBridgeHandler bh = mock(ConnectedDriveBridgeHandler.class);
+        Bridge b = mock(Bridge.class);
+        when(bh.getThing()).thenReturn(b);
+        when(b.getUID()).thenReturn(new ThingUID("bmwconnecteddrive", "account", "abc"));
+        VehicleDiscovery discovery = new VehicleDiscovery();
+        discovery.setThingHandler(bh);
+        DiscoveryListener listener = mock(DiscoveryListener.class);
+        discovery.addDiscoveryListener(listener);
+        VehiclesContainer container = GSON.fromJson(content, VehiclesContainer.class);
+        ArgumentCaptor<DiscoveryResult> discoveries = ArgumentCaptor.forClass(DiscoveryResult.class);
+        ArgumentCaptor<DiscoveryService> services = ArgumentCaptor.forClass(DiscoveryService.class);
+
+        if (container != null) {
+            discovery.onResponse(container);
+            verify(listener, times(DISCOVERY_VEHICLES)).thingDiscovered(services.capture(), discoveries.capture());
+            List<DiscoveryResult> results = discoveries.getAllValues();
+            results.forEach(entry -> {
+                logger.info("{}", entry.toString());
+            });
+        } else {
+            assertTrue(false);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/ChargeProfileTest.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/ChargeProfileTest.java
new file mode 100644 (file)
index 0000000..7fdca00
--- /dev/null
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.charge.ChargeProfile;
+import org.openhab.binding.bmwconnecteddrive.internal.util.FileReader;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link ChargeProfileTest} Test json responses from ConnectedDrive Portal
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class ChargeProfileTest {
+    private static final Gson GSON = new Gson();
+
+    @Test
+    public void testChargeProfile() {
+        String resource1 = FileReader.readFileInString("src/test/resources/webapi/charging-profile.json");
+        ChargeProfile cp = GSON.fromJson(resource1, ChargeProfile.class);
+        assertNotNull(cp.weeklyPlanner);
+        assertNotNull(cp.weeklyPlanner.timer1);
+        assertFalse(cp.weeklyPlanner.timer1.timerEnabled);
+        assertNotNull(cp.weeklyPlanner.timer1.weekdays);
+        assertEquals(5, cp.weeklyPlanner.timer1.weekdays.size(), "Days");
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/ConnectedDriveTest.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/ConnectedDriveTest.java
new file mode 100644 (file)
index 0000000..03dd102
--- /dev/null
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.discovery.Vehicle;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.discovery.VehiclesContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.util.FileReader;
+import org.openhab.core.thing.ThingTypeUID;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link ConnectedDriveTest} Test json responses from ConnectedDrive Portal
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class ConnectedDriveTest {
+    private static final Gson GSON = new Gson();
+
+    @Test
+    public void testUserInfo() {
+        String resource1 = FileReader.readFileInString("src/test/resources/webapi/connected-drive-account-info.json");
+        VehiclesContainer container = GSON.fromJson(resource1, VehiclesContainer.class);
+        List<Vehicle> vehicles = container.vehicles;
+        assertEquals(1, vehicles.size(), "Number of Vehicles");
+        Vehicle v = vehicles.get(0);
+        assertEquals("MY_REAL_VIN", v.vin, "VIN");
+        assertEquals("i3 94 (+ REX)", v.model, "Model");
+        assertEquals("BEV_REX", v.driveTrain, "DriveTrain");
+        assertEquals("BMW_I", v.brand, "Brand");
+        assertEquals(2017, v.yearOfConstruction, "Year of Construction");
+    }
+
+    @Test
+    public void testChannelUID() {
+        ThingTypeUID thingTypePHEV = new ThingTypeUID("bmwconnecteddrive", "plugin-hybrid-vehicle");
+        assertEquals("plugin-hybrid-vehicle", thingTypePHEV.getId(), "Vehicle Type");
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/DestinationTest.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/DestinationTest.java
new file mode 100644 (file)
index 0000000..0f9c0f8
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.bmwconnecteddrive.internal.util.FileReader;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link DestinationTest} Test json responses from ConnectedDrive Portal
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class DestinationTest {
+    private final Logger logger = LoggerFactory.getLogger(DestinationTest.class);
+    private static final Gson GSON = new Gson();
+
+    @Test
+    public void testDestinations() {
+        String resource1 = FileReader.readFileInString("src/test/resources/webapi/destinations.json");
+        DestinationContainer container = GSON.fromJson(resource1, DestinationContainer.class);
+        List<Destination> destinations = container.destinations;
+        assertEquals(9, destinations.size(), "Number of Vehicles");
+        destinations.forEach(entry -> {
+            logger.info(entry.getAddress());
+            assertFalse(entry.getAddress().contains(Constants.NULL), "No Null contained");
+        });
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/LastTripTest.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/LastTripTest.java
new file mode 100644 (file)
index 0000000..0ac8bcf
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.LastTrip;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.LastTripContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.util.FileReader;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link LastTripTest} Test json responses from ConnectedDrive Portal
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class LastTripTest {
+    private static final Gson GSON = new Gson();
+
+    @Test
+    public void testUserInfo() {
+        String content = FileReader.readFileInString("src/test/resources/webapi/last-trip.json");
+        LastTripContainer lt = GSON.fromJson(content, LastTripContainer.class);
+        LastTrip trip = lt.lastTrip;
+        assertNotNull(trip);
+        assertEquals(2.0, trip.totalDistance, 0.01, "Distance Driven");
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/LifetimeWrapper.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/LifetimeWrapper.java
new file mode 100644 (file)
index 0000000..88861ba
--- /dev/null
@@ -0,0 +1,186 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.measure.Unit;
+import javax.measure.quantity.Length;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.VehicleType;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.AllTrips;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.AllTripsContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.ImperialUnits;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.types.State;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link LifetimeWrapper} Test json responses from ConnectedDrive Portal
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class LifetimeWrapper {
+    private static final Gson GSON = new Gson();
+    private static final Unit<Length> MILES = ImperialUnits.MILE;
+
+    private AllTrips allTrips;
+    private boolean imperial;
+    private boolean isElectric;
+    private boolean hasFuel;
+    private boolean isHybrid;
+
+    private Map<String, State> specialHandlingMap = new HashMap<String, State>();
+
+    public LifetimeWrapper(String type, boolean imperial, String statusJson) {
+        this.imperial = imperial;
+        hasFuel = type.equals(VehicleType.CONVENTIONAL.toString()) || type.equals(VehicleType.PLUGIN_HYBRID.toString())
+                || type.equals(VehicleType.ELECTRIC_REX.toString());
+        isElectric = type.equals(VehicleType.PLUGIN_HYBRID.toString())
+                || type.equals(VehicleType.ELECTRIC_REX.toString()) || type.equals(VehicleType.ELECTRIC.toString());
+        isHybrid = hasFuel && isElectric;
+        AllTripsContainer container = GSON.fromJson(statusJson, AllTripsContainer.class);
+        assertNotNull(container);
+        assertNotNull(container.allTrips);
+        allTrips = container.allTrips;
+    }
+
+    /**
+     * Test results auctomatically against json values
+     *
+     * @param channels
+     * @param states
+     * @return
+     */
+    public boolean checkResults(@Nullable List<ChannelUID> channels, @Nullable List<State> states) {
+        assertNotNull(channels);
+        assertNotNull(states);
+        assertTrue(channels.size() == states.size(), "Same list sizes ");
+        for (int i = 0; i < channels.size(); i++) {
+            checkResult(channels.get(i), states.get(i));
+        }
+        return true;
+    }
+
+    /**
+     * Add a specific check for a value e.g. hard coded "Upcoming Service" in order to check the right ordering
+     *
+     * @param specialHand
+     * @return
+     */
+    public LifetimeWrapper append(Map<String, State> compareMap) {
+        specialHandlingMap.putAll(compareMap);
+        return this;
+    }
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    private void checkResult(ChannelUID channelUID, State state) {
+        String cUid = channelUID.getIdWithoutGroup();
+        QuantityType<Length> qt;
+        switch (cUid) {
+            case DISTANCE_SINCE_CHARGING:
+                assertTrue(state instanceof QuantityType);
+                qt = ((QuantityType) state);
+                if (imperial) {
+                    assertEquals(MILES, qt.getUnit(), "Miles");
+                    assertEquals(allTrips.chargecycleRange.userCurrentChargeCycle / Converter.MILES_TO_KM_RATIO,
+                            qt.floatValue(), 0.1, "Distance since charging");
+                } else {
+                    assertEquals(Constants.KILOMETRE_UNIT, qt.getUnit(), "KM");
+                    assertEquals(allTrips.chargecycleRange.userCurrentChargeCycle, qt.floatValue(), 0.1,
+                            "Distance since charging");
+                }
+                break;
+            case SINGLE_LONGEST_DISTANCE:
+                assertTrue(state instanceof QuantityType);
+                qt = ((QuantityType) state);
+                if (imperial) {
+                    assertEquals(MILES, qt.getUnit(), "Miles");
+                    assertEquals(allTrips.chargecycleRange.userHigh / Converter.MILES_TO_KM_RATIO, qt.floatValue(), 0.1,
+                            "Longest Distance");
+                } else {
+                    assertEquals(Constants.KILOMETRE_UNIT, qt.getUnit(), "KM");
+                    assertEquals(allTrips.chargecycleRange.userHigh, qt.floatValue(), 0.1, "Longest Distance");
+                }
+                break;
+            case TOTAL_DRIVEN_DISTANCE:
+                assertTrue(state instanceof QuantityType);
+                qt = ((QuantityType) state);
+                if (imperial) {
+                    assertEquals(MILES, qt.getUnit(), "Miles");
+                    assertEquals(allTrips.totalElectricDistance.userTotal / Converter.MILES_TO_KM_RATIO,
+                            qt.floatValue(), 0.1, "Total Electric Distance");
+                } else {
+                    assertEquals(Constants.KILOMETRE_UNIT, qt.getUnit(), "KM");
+                    assertEquals(allTrips.totalElectricDistance.userTotal, qt.floatValue(), 0.1,
+                            "Total Electric Distance");
+                }
+                break;
+            case AVG_CONSUMPTION:
+                assertTrue(isElectric, "Is Electric");
+                assertTrue(state instanceof QuantityType);
+                qt = ((QuantityType) state);
+                assertEquals(Units.KILOWATT_HOUR, qt.getUnit(), "kw/h");
+                if (imperial) {
+                    assertEquals(allTrips.avgElectricConsumption.userAverage * Converter.MILES_TO_KM_RATIO,
+                            qt.floatValue(), 0.1, "Avg Consumption");
+                } else {
+                    assertEquals(allTrips.avgElectricConsumption.userAverage, qt.floatValue(), 0.1, "Avg Consumption");
+                }
+                break;
+            case AVG_RECUPERATION:
+                assertTrue(isElectric, "Is Electric");
+                assertTrue(state instanceof QuantityType);
+                qt = ((QuantityType) state);
+                assertEquals(Units.KILOWATT_HOUR, qt.getUnit(), "kw/h");
+                if (imperial) {
+                    assertEquals(allTrips.avgRecuperation.userAverage * Converter.MILES_TO_KM_RATIO, qt.floatValue(),
+                            0.1, "Avg Recuperation");
+                } else {
+                    assertEquals(allTrips.avgRecuperation.userAverage, qt.floatValue(), 0.1, "Avg Recuperation");
+                }
+                break;
+            case AVG_COMBINED_CONSUMPTION:
+                assertTrue(isHybrid, "Is Hybrid");
+                assertTrue(state instanceof QuantityType);
+                qt = ((QuantityType) state);
+                assertEquals(Units.LITRE, qt.getUnit(), "Liter");
+                if (imperial) {
+                    assertEquals(allTrips.avgCombinedConsumption.userAverage * Converter.MILES_TO_KM_RATIO,
+                            qt.floatValue(), 0.1, "Avg Combined Consumption");
+                } else {
+                    assertEquals(allTrips.avgCombinedConsumption.userAverage, qt.floatValue(), 0.1,
+                            "Avg Combined Consumption");
+                }
+                break;
+            default:
+                // fail in case of unknown update
+                assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+                break;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/RemoteStatusTest.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/RemoteStatusTest.java
new file mode 100644 (file)
index 0000000..3127b04
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.remote.ExecutionStatus;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.remote.ExecutionStatusContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.RemoteServiceHandler.ExecutionState;
+import org.openhab.binding.bmwconnecteddrive.internal.util.FileReader;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link RemoteStatusTest} Test json responses from ConnectedDrive Portal
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class RemoteStatusTest {
+    private static final Gson GSON = new Gson();
+
+    @Test
+    public void testStatus() {
+        String resource1 = FileReader.readFileInString("src/test/resources/webapi/remote-services/pending.json");
+        ExecutionStatusContainer esc = GSON.fromJson(resource1, ExecutionStatusContainer.class);
+        ExecutionStatus execStatus = esc.executionStatus;
+        assertEquals(ExecutionState.PENDING.name(), execStatus.status, "Status");
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/StatusWrapper.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/StatusWrapper.java
new file mode 100644 (file)
index 0000000..acd54c5
--- /dev/null
@@ -0,0 +1,517 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.measure.Unit;
+import javax.measure.quantity.Length;
+import javax.measure.quantity.Time;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.VehicleType;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.Doors;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatus;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatusContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.Windows;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.VehicleStatusUtils;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.PointType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.ImperialUnits;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link StatusWrapper} Test json responses from ConnectedDrive Portal
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class StatusWrapper {
+    private static final Gson GSON = new Gson();
+    private static final Unit<Length> KILOMETRE = Constants.KILOMETRE_UNIT;
+    private static final double ALLOWED_MILE_CONVERSION_DEVIATION = 1.5;
+    private static final double ALLOWED_KM_ROUND_DEVIATION = 0.1;
+
+    private VehicleStatus vStatus;
+    private boolean imperial;
+    private boolean isElectric;
+    private boolean hasFuel;
+    private boolean isHybrid;
+
+    private Map<String, State> specialHandlingMap = new HashMap<String, State>();
+
+    public StatusWrapper(String type, boolean imperial, String statusJson) {
+        this.imperial = imperial;
+        hasFuel = type.equals(VehicleType.CONVENTIONAL.toString()) || type.equals(VehicleType.PLUGIN_HYBRID.toString())
+                || type.equals(VehicleType.ELECTRIC_REX.toString());
+        isElectric = type.equals(VehicleType.PLUGIN_HYBRID.toString())
+                || type.equals(VehicleType.ELECTRIC_REX.toString()) || type.equals(VehicleType.ELECTRIC.toString());
+        isHybrid = hasFuel && isElectric;
+        VehicleStatusContainer container = GSON.fromJson(statusJson, VehicleStatusContainer.class);
+        assertNotNull(container);
+        assertNotNull(container.vehicleStatus);
+        vStatus = container.vehicleStatus;
+    }
+
+    /**
+     * Test results auctomatically against json values
+     *
+     * @param channels
+     * @param states
+     * @return
+     */
+    public boolean checkResults(@Nullable List<ChannelUID> channels, @Nullable List<State> states) {
+        assertNotNull(channels);
+        assertNotNull(states);
+        assertTrue(channels.size() == states.size(), "Same list sizes");
+        for (int i = 0; i < channels.size(); i++) {
+            checkResult(channels.get(i), states.get(i));
+        }
+        return true;
+    }
+
+    /**
+     * Add a specific check for a value e.g. hard coded "Upcoming Service" in order to check the right ordering
+     *
+     * @param specialHand
+     * @return
+     */
+    public StatusWrapper append(Map<String, State> compareMap) {
+        specialHandlingMap.putAll(compareMap);
+        return this;
+    }
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    private void checkResult(ChannelUID channelUID, State state) {
+        String cUid = channelUID.getIdWithoutGroup();
+        String gUid = channelUID.getGroupId();
+        QuantityType<Length> qt;
+        QuantityType<Time> qtt;
+        StringType st;
+        StringType wanted;
+        DateTimeType dtt;
+        PointType pt;
+        switch (cUid) {
+            case MILEAGE:
+                assertTrue(state instanceof QuantityType);
+                qt = ((QuantityType) state);
+                if (imperial) {
+                    assertEquals(ImperialUnits.MILE, qt.getUnit(), "Miles");
+                } else {
+                    assertEquals(KILOMETRE, qt.getUnit(), "KM");
+                }
+                switch (gUid) {
+                    case CHANNEL_GROUP_RANGE:
+                        assertEquals(qt.intValue(), vStatus.mileage, "Mileage");
+                        break;
+                    case CHANNEL_GROUP_SERVICE:
+                        if (vStatus.cbsData.isEmpty()) {
+                            assertEquals(qt.intValue(), -1, "Service Mileage");
+                        } else {
+                            assertEquals(qt.intValue(), vStatus.cbsData.get(0).cbsRemainingMileage, "Service Mileage");
+                        }
+                        break;
+                    case CHANNEL_GROUP_CHECK_CONTROL:
+                        if (vStatus.checkControlMessages.isEmpty()) {
+                            assertEquals(qt.intValue(), -1, "CheckControl Mileage");
+                        } else {
+                            assertEquals(qt.intValue(), vStatus.checkControlMessages.get(0).ccmMileage,
+                                    "CheckControl Mileage");
+                        }
+                        break;
+                    default:
+                        assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+                        break;
+                }
+                break;
+            case RANGE_ELECTRIC:
+                assertTrue(isElectric, "Is Eelctric");
+                assertTrue(state instanceof QuantityType);
+                qt = ((QuantityType) state);
+                if (imperial) {
+                    assertEquals(ImperialUnits.MILE, qt.getUnit(), "Miles");
+                    assertEquals(Converter.round(qt.floatValue()), Converter.round(vStatus.remainingRangeElectricMls),
+                            ALLOWED_MILE_CONVERSION_DEVIATION, "Mileage");
+                } else {
+                    assertEquals(KILOMETRE, qt.getUnit(), "KM");
+                    assertEquals(Converter.round(qt.floatValue()), Converter.round(vStatus.remainingRangeElectric),
+                            ALLOWED_KM_ROUND_DEVIATION, "Mileage");
+                }
+                break;
+            case RANGE_FUEL:
+                assertTrue(hasFuel, "Has Fuel");
+                if (!(state instanceof UnDefType)) {
+                    assertTrue(state instanceof QuantityType);
+                    qt = ((QuantityType) state);
+                    if (imperial) {
+                        assertEquals(ImperialUnits.MILE, qt.getUnit(), "Miles");
+                        assertEquals(Converter.round(qt.floatValue()), Converter.round(vStatus.remainingRangeFuelMls),
+                                ALLOWED_MILE_CONVERSION_DEVIATION, "Mileage");
+                    } else {
+                        assertEquals(KILOMETRE, qt.getUnit(), "KM");
+                        assertEquals(Converter.round(qt.floatValue()), Converter.round(vStatus.remainingRangeFuel),
+                                ALLOWED_KM_ROUND_DEVIATION, "Mileage");
+                    }
+                }
+                break;
+            case RANGE_HYBRID:
+                assertTrue(isHybrid, "Is Hybrid");
+                assertTrue(state instanceof QuantityType);
+                qt = ((QuantityType) state);
+                if (imperial) {
+                    assertEquals(ImperialUnits.MILE, qt.getUnit(), "Miles");
+                    assertEquals(Converter.round(qt.floatValue()),
+                            Converter.round(vStatus.remainingRangeElectricMls + vStatus.remainingRangeFuelMls),
+                            ALLOWED_MILE_CONVERSION_DEVIATION, "Mileage");
+                } else {
+                    assertEquals(KILOMETRE, qt.getUnit(), "KM");
+                    assertEquals(Converter.round(qt.floatValue()),
+                            Converter.round(vStatus.remainingRangeElectric + vStatus.remainingRangeFuel),
+                            ALLOWED_KM_ROUND_DEVIATION, "Mileage");
+                }
+                break;
+            case REMAINING_FUEL:
+                assertTrue(hasFuel, "Has Fuel");
+                assertTrue(state instanceof QuantityType);
+                qt = ((QuantityType) state);
+                assertEquals(Units.LITRE, qt.getUnit(), "Liter Unit");
+                assertEquals(Converter.round(vStatus.remainingFuel), Converter.round(qt.floatValue()), 0.01,
+                        "Fuel Level");
+                break;
+            case SOC:
+                assertTrue(isElectric, "Is Eelctric");
+                assertTrue(state instanceof QuantityType);
+                qt = ((QuantityType) state);
+                assertEquals(Units.PERCENT, qt.getUnit(), "Percent");
+                assertEquals(Converter.round(vStatus.chargingLevelHv), Converter.round(qt.floatValue()), 0.01,
+                        "Charge Level");
+                break;
+            case LOCK:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                assertEquals(Converter.toTitleCase(vStatus.doorLockState), st.toString(), "Vehicle locked");
+                break;
+            case DOORS:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                Doors doorState = GSON.fromJson(GSON.toJson(vStatus), Doors.class);
+                if (doorState != null) {
+                    assertEquals(VehicleStatusUtils.checkClosed(doorState), st.toString(), "Doors Closed");
+                } else {
+                    assertTrue(false);
+                }
+
+                break;
+            case WINDOWS:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                Windows windowState = GSON.fromJson(GSON.toJson(vStatus), Windows.class);
+                if (windowState != null) {
+                    if (specialHandlingMap.containsKey(WINDOWS)) {
+                        assertEquals(specialHandlingMap.get(WINDOWS).toString(), st.toString(), "Windows");
+                    } else {
+                        assertEquals(VehicleStatusUtils.checkClosed(windowState), st.toString(), "Windows");
+                    }
+                } else {
+                    assertTrue(false);
+                }
+
+                break;
+            case CHECK_CONTROL:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                if (specialHandlingMap.containsKey(CHECK_CONTROL)) {
+                    assertEquals(specialHandlingMap.get(CHECK_CONTROL).toString(), st.toString(), "Check Control");
+                } else {
+                    assertEquals(Converter.toTitleCase(VehicleStatusUtils.checkControlActive(vStatus)), st.toString(),
+                            "Check Control");
+                }
+                break;
+            case CHARGE_STATUS:
+                assertTrue(isElectric, "Is Electric");
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                if (vStatus.chargingStatus.contentEquals(Constants.INVALID)) {
+                    assertEquals(Converter.toTitleCase(vStatus.lastChargingEndReason), st.toString(), "Charge Status");
+                } else {
+                    assertEquals(Converter.toTitleCase(vStatus.chargingStatus), st.toString(), "Charge Status");
+                }
+                break;
+            case CHARGE_REMAINING:
+                assertTrue(isElectric, "Is Electric");
+                if (vStatus.chargingTimeRemaining == null) {
+                    assertTrue(state instanceof UnDefType, "expected UndefType");
+                } else {
+                    assertTrue(state instanceof QuantityType);
+                    qtt = ((QuantityType) state);
+                    assertEquals(qtt.doubleValue(), vStatus.chargingTimeRemaining);
+                    assertEquals(Units.MINUTE, qtt.getUnit(), "Minutes");
+                }
+                break;
+            case LAST_UPDATE:
+                assertTrue(state instanceof DateTimeType);
+                dtt = (DateTimeType) state;
+                DateTimeType expected = DateTimeType
+                        .valueOf(Converter.getLocalDateTime(VehicleStatusUtils.getUpdateTime(vStatus)));
+                assertEquals(expected.toString(), dtt.toString(), "Last Update");
+                break;
+            case GPS:
+                assertTrue(state instanceof PointType);
+                pt = (PointType) state;
+                assertNotNull(vStatus.position);
+                assertEquals(vStatus.position.getCoordinates(), pt.toString(), "Coordinates");
+                break;
+            case HEADING:
+                assertTrue(state instanceof QuantityType);
+                qt = ((QuantityType) state);
+                assertEquals(Units.DEGREE_ANGLE, qt.getUnit(), "Angle Unit");
+                assertNotNull(vStatus.position);
+                assertEquals(vStatus.position.heading, qt.intValue(), 0.01, "Heading");
+                break;
+            case RANGE_RADIUS_ELECTRIC:
+                assertTrue(state instanceof QuantityType);
+                assertTrue(isElectric);
+                qt = (QuantityType) state;
+                if (imperial) {
+                    assertEquals(Converter.guessRangeRadius(vStatus.remainingRangeElectricMls), qt.floatValue(), 1,
+                            "Range Radius Electric mi");
+                } else {
+                    assertEquals(Converter.guessRangeRadius(vStatus.remainingRangeElectric), qt.floatValue(), 0.1,
+                            "Range Radius Electric km");
+                }
+                break;
+            case RANGE_RADIUS_FUEL:
+                assertTrue(state instanceof QuantityType);
+                assertTrue(hasFuel);
+                qt = (QuantityType) state;
+                if (imperial) {
+                    assertEquals(Converter.guessRangeRadius(vStatus.remainingRangeFuelMls), qt.floatValue(), 1,
+                            "Range Radius Fuel mi");
+                } else {
+                    assertEquals(Converter.guessRangeRadius(vStatus.remainingRangeFuel), qt.floatValue(), 0.1,
+                            "Range Radius Fuel km");
+                }
+                break;
+            case RANGE_RADIUS_HYBRID:
+                assertTrue(state instanceof QuantityType);
+                assertTrue(isHybrid);
+                qt = (QuantityType) state;
+                if (imperial) {
+                    assertEquals(
+                            Converter.guessRangeRadius(
+                                    vStatus.remainingRangeElectricMls + vStatus.remainingRangeFuelMls),
+                            qt.floatValue(), ALLOWED_MILE_CONVERSION_DEVIATION, "Range Radius Hybrid mi");
+                } else {
+                    assertEquals(
+                            Converter.guessRangeRadius(vStatus.remainingRangeElectric + vStatus.remainingRangeFuel),
+                            qt.floatValue(), ALLOWED_KM_ROUND_DEVIATION, "Range Radius Hybrid km");
+                }
+                break;
+            case DOOR_DRIVER_FRONT:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                wanted = StringType.valueOf(Converter.toTitleCase(vStatus.doorDriverFront));
+                assertEquals(wanted.toString(), st.toString(), "Door");
+                break;
+            case DOOR_DRIVER_REAR:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                wanted = StringType.valueOf(Converter.toTitleCase(vStatus.doorDriverRear));
+                assertEquals(wanted.toString(), st.toString(), "Door");
+                break;
+            case DOOR_PASSENGER_FRONT:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                wanted = StringType.valueOf(Converter.toTitleCase(vStatus.doorPassengerFront));
+                assertEquals(wanted.toString(), st.toString(), "Door");
+                break;
+            case DOOR_PASSENGER_REAR:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                wanted = StringType.valueOf(Converter.toTitleCase(vStatus.doorPassengerRear));
+                assertEquals(wanted.toString(), st.toString(), "Door");
+                break;
+            case TRUNK:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                wanted = StringType.valueOf(Converter.toTitleCase(vStatus.trunk));
+                assertEquals(wanted.toString(), st.toString(), "Door");
+                break;
+            case HOOD:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                wanted = StringType.valueOf(Converter.toTitleCase(vStatus.hood));
+                assertEquals(wanted.toString(), st.toString(), "Door");
+                break;
+            case WINDOW_DOOR_DRIVER_FRONT:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                wanted = StringType.valueOf(Converter.toTitleCase(vStatus.windowDriverFront));
+                assertEquals(wanted.toString(), st.toString(), "Window");
+                break;
+            case WINDOW_DOOR_DRIVER_REAR:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                wanted = StringType.valueOf(Converter.toTitleCase(vStatus.windowDriverRear));
+                assertEquals(wanted.toString(), st.toString(), "Window");
+                break;
+            case WINDOW_DOOR_PASSENGER_FRONT:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                wanted = StringType.valueOf(Converter.toTitleCase(vStatus.windowPassengerFront));
+                assertEquals(wanted.toString(), st.toString(), "Window");
+                break;
+            case WINDOW_DOOR_PASSENGER_REAR:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                wanted = StringType.valueOf(Converter.toTitleCase(vStatus.windowPassengerRear));
+                assertEquals(wanted.toString(), st.toString(), "Window");
+                break;
+            case WINDOW_REAR:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                wanted = StringType.valueOf(Converter.toTitleCase(vStatus.rearWindow));
+                assertEquals(wanted.toString(), st.toString(), "Window");
+                break;
+            case SUNROOF:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                wanted = StringType.valueOf(Converter.toTitleCase(vStatus.sunroof));
+                assertEquals(wanted.toString(), st.toString(), "Window");
+                break;
+            case SERVICE_DATE:
+                assertTrue(state instanceof DateTimeType);
+                dtt = (DateTimeType) state;
+                if (gUid.contentEquals(CHANNEL_GROUP_STATUS)) {
+                    if (specialHandlingMap.containsKey(SERVICE_DATE)) {
+                        assertEquals(specialHandlingMap.get(SERVICE_DATE).toString(), dtt.toString(), "Next Service");
+                    } else {
+                        String dueDateString = VehicleStatusUtils.getNextServiceDate(vStatus);
+                        DateTimeType expectedDTT = DateTimeType.valueOf(Converter.getLocalDateTime(dueDateString));
+                        assertEquals(expectedDTT.toString(), dtt.toString(), "Next Service");
+                    }
+                } else if (gUid.equals(CHANNEL_GROUP_SERVICE)) {
+                    String dueDateString = vStatus.cbsData.get(0).getDueDate();
+                    DateTimeType expectedDTT = DateTimeType.valueOf(Converter.getLocalDateTime(dueDateString));
+                    assertEquals(expectedDTT.toString(), dtt.toString(), "First Service Date");
+                }
+                break;
+            case SERVICE_MILEAGE:
+                assertTrue(state instanceof QuantityType);
+                qt = ((QuantityType) state);
+                if (gUid.contentEquals(CHANNEL_GROUP_STATUS)) {
+                    if (imperial) {
+                        assertEquals(ImperialUnits.MILE, qt.getUnit(), "Next Service Miles");
+                        assertEquals(VehicleStatusUtils.getNextServiceMileage(vStatus), qt.intValue(), "Mileage");
+                    } else {
+                        assertEquals(KILOMETRE, qt.getUnit(), "Next Service KM");
+                        assertEquals(VehicleStatusUtils.getNextServiceMileage(vStatus), qt.intValue(), "Mileage");
+                    }
+                } else if (gUid.equals(CHANNEL_GROUP_SERVICE)) {
+                    if (imperial) {
+                        assertEquals(ImperialUnits.MILE, qt.getUnit(), "First Service Miles");
+                        assertEquals(vStatus.cbsData.get(0).cbsRemainingMileage, qt.intValue(),
+                                "First Service Mileage");
+                    } else {
+                        assertEquals(KILOMETRE, qt.getUnit(), "First Service KM");
+                        assertEquals(vStatus.cbsData.get(0).cbsRemainingMileage, qt.intValue(),
+                                "First Service Mileage");
+                    }
+                }
+                break;
+            case NAME:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                switch (gUid) {
+                    case CHANNEL_GROUP_SERVICE:
+                        wanted = StringType.valueOf(Converter.toTitleCase(Constants.NO_ENTRIES));
+                        if (!vStatus.cbsData.isEmpty()) {
+                            wanted = StringType.valueOf(Converter.toTitleCase(vStatus.cbsData.get(0).getType()));
+                        }
+                        assertEquals(wanted.toString(), st.toString(), "Service Name");
+                        break;
+                    case CHANNEL_GROUP_CHECK_CONTROL:
+                        wanted = StringType.valueOf(Constants.NO_ENTRIES);
+                        if (!vStatus.checkControlMessages.isEmpty()) {
+                            wanted = StringType.valueOf(vStatus.checkControlMessages.get(0).ccmDescriptionShort);
+                        }
+                        assertEquals(wanted.toString(), st.toString(), "CheckControl Name");
+                        break;
+                    default:
+                        assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+                        break;
+                }
+                break;
+            case DETAILS:
+                assertTrue(state instanceof StringType);
+                st = (StringType) state;
+                switch (gUid) {
+                    case CHANNEL_GROUP_SERVICE:
+                        wanted = StringType.valueOf(Converter.toTitleCase(Constants.NO_ENTRIES));
+                        if (!vStatus.cbsData.isEmpty()) {
+                            wanted = StringType.valueOf(Converter.toTitleCase(vStatus.cbsData.get(0).getDescription()));
+                        }
+                        assertEquals(wanted.toString(), st.toString(), "Service Details");
+                        break;
+                    case CHANNEL_GROUP_CHECK_CONTROL:
+                        wanted = StringType.valueOf(Constants.NO_ENTRIES);
+                        if (!vStatus.checkControlMessages.isEmpty()) {
+                            wanted = StringType.valueOf(vStatus.checkControlMessages.get(0).ccmDescriptionLong);
+                        }
+                        assertEquals(wanted.toString(), st.toString(), "CheckControl Details");
+                        break;
+                    default:
+                        assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+                        break;
+                }
+                break;
+            case DATE:
+                assertTrue(state instanceof DateTimeType);
+                dtt = (DateTimeType) state;
+                switch (gUid) {
+                    case CHANNEL_GROUP_SERVICE:
+                        String dueDateString = Constants.NULL_DATE;
+                        if (!vStatus.cbsData.isEmpty()) {
+                            dueDateString = vStatus.cbsData.get(0).getDueDate();
+                        }
+                        DateTimeType expectedDTT = DateTimeType.valueOf(Converter.getLocalDateTime(dueDateString));
+                        assertEquals(expectedDTT.toString(), dtt.toString(), "ServiceSate");
+                        break;
+                    default:
+                        assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+                        break;
+                }
+                break;
+            default:
+                // fail in case of unknown update
+                assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+                break;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/TripWrapper.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/TripWrapper.java
new file mode 100644 (file)
index 0000000..9a386ae
--- /dev/null
@@ -0,0 +1,172 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.measure.Unit;
+import javax.measure.quantity.Length;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.VehicleType;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.LastTrip;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.LastTripContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.ImperialUnits;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.types.State;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link TripWrapper} Test json responses from ConnectedDrive Portal
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class TripWrapper {
+    private static final Gson GSON = new Gson();
+    private static final Unit<Length> MILES = ImperialUnits.MILE;
+
+    private LastTrip lastTrip;
+    private boolean imperial;
+    private boolean isElectric;
+    private boolean hasFuel;
+    private boolean isHybrid;
+
+    private Map<String, State> specialHandlingMap = new HashMap<String, State>();
+
+    public TripWrapper(String type, boolean imperial, String statusJson) {
+        this.imperial = imperial;
+        hasFuel = type.equals(VehicleType.CONVENTIONAL.toString()) || type.equals(VehicleType.PLUGIN_HYBRID.toString())
+                || type.equals(VehicleType.ELECTRIC_REX.toString());
+        isElectric = type.equals(VehicleType.PLUGIN_HYBRID.toString())
+                || type.equals(VehicleType.ELECTRIC_REX.toString()) || type.equals(VehicleType.ELECTRIC.toString());
+        isHybrid = hasFuel && isElectric;
+        LastTripContainer container = GSON.fromJson(statusJson, LastTripContainer.class);
+        assertNotNull(container);
+        assertNotNull(container.lastTrip);
+        lastTrip = container.lastTrip;
+    }
+
+    /**
+     * Test results auctomatically against json values
+     *
+     * @param channels
+     * @param states
+     * @return
+     */
+    public boolean checkResults(@Nullable List<ChannelUID> channels, @Nullable List<State> states) {
+        assertNotNull(channels);
+        assertNotNull(states);
+        assertTrue(channels.size() == states.size(), "Same list sizes");
+        for (int i = 0; i < channels.size(); i++) {
+            checkResult(channels.get(i), states.get(i));
+        }
+        return true;
+    }
+
+    /**
+     * Add a specific check for a value e.g. hard coded "Upcoming Service" in order to check the right ordering
+     *
+     * @param specialHand
+     * @return
+     */
+    public TripWrapper append(Map<String, State> compareMap) {
+        specialHandlingMap.putAll(compareMap);
+        return this;
+    }
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    private void checkResult(ChannelUID channelUID, State state) {
+        String cUid = channelUID.getIdWithoutGroup();
+        QuantityType<Length> qt;
+        DateTimeType dtt;
+        switch (cUid) {
+            case DATE:
+                assertTrue(state instanceof DateTimeType);
+                dtt = ((DateTimeType) state);
+                DateTimeType expected = DateTimeType.valueOf(Converter.getLocalDateTimeWithoutOffest(lastTrip.date));
+                assertEquals(expected.toString(), dtt.toString(), "Trip Date");
+                break;
+            case DURATION:
+                assertTrue(state instanceof QuantityType);
+                qt = ((QuantityType) state);
+                assertEquals(Units.MINUTE, qt.getUnit(), "Minute");
+                assertEquals(lastTrip.duration, qt.floatValue(), 0.1, "Duration");
+                break;
+            case DISTANCE:
+                assertTrue(state instanceof QuantityType);
+                qt = ((QuantityType) state);
+                if (imperial) {
+                    assertEquals(MILES, qt.getUnit(), "Miles");
+                    assertEquals(lastTrip.totalDistance / Converter.MILES_TO_KM_RATIO, qt.floatValue(), 0.1,
+                            "Distance");
+
+                } else {
+                    assertEquals(Constants.KILOMETRE_UNIT, qt.getUnit(), "KM");
+                    assertEquals(lastTrip.totalDistance, qt.floatValue(), 0.1, "Distance");
+                }
+                break;
+            case AVG_CONSUMPTION:
+                assertTrue(state instanceof QuantityType);
+                qt = ((QuantityType) state);
+                assertEquals(Units.KILOWATT_HOUR, qt.getUnit(), "kw/h");
+                if (imperial) {
+                    assertEquals(lastTrip.avgElectricConsumption * Converter.MILES_TO_KM_RATIO, qt.floatValue(), 0.1,
+                            "Avg Consumption");
+                } else {
+                    assertEquals(lastTrip.avgElectricConsumption, qt.floatValue(), 0.1, "Avg Consumption");
+                }
+                break;
+            case AVG_COMBINED_CONSUMPTION:
+                assertTrue(isHybrid, "Is Hybrid");
+                assertTrue(state instanceof QuantityType);
+                qt = ((QuantityType) state);
+                assertEquals(Units.LITRE, qt.getUnit(), "Liter");
+                if (imperial) {
+                    assertEquals(Converter.round(lastTrip.avgCombinedConsumption * Converter.MILES_TO_KM_RATIO),
+                            qt.floatValue(), 0.01, "Percent");
+                } else {
+                    assertEquals(Converter.round(lastTrip.avgCombinedConsumption), qt.floatValue(), 0.01, "Percent");
+                }
+                break;
+            case AVG_RECUPERATION:
+                assertTrue(state instanceof QuantityType);
+                qt = ((QuantityType) state);
+                assertEquals(Units.KILOWATT_HOUR, qt.getUnit(), "kw/h");
+                if (imperial) {
+                    assertEquals(lastTrip.avgRecuperation * Converter.MILES_TO_KM_RATIO, qt.floatValue(), 0.1,
+                            "Avg Recuperation");
+                } else {
+                    assertEquals(lastTrip.avgRecuperation, qt.floatValue(), 0.1, "Avg Recuperation");
+                }
+                break;
+            default:
+                // fail in case of unknown update
+                assertFalse(true, "Channel " + channelUID + " " + state + " not found");
+                break;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/VehicleStatusTest.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/VehicleStatusTest.java
new file mode 100644 (file)
index 0000000..b801415
--- /dev/null
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.dto;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.compat.VehicleAttributesContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.CBSMessage;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.Position;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatus;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatusContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.util.FileReader;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link VehicleStatusTest} Test json responses from ConnectedDrive Portal
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class VehicleStatusTest {
+    private static final Gson GSON = new Gson();
+
+    @Test
+    public void testBevRexValues() {
+        String resource1 = FileReader.readFileInString("src/test/resources/webapi/vehicle-status.json");
+        VehicleStatusContainer status = GSON.fromJson(resource1, VehicleStatusContainer.class);
+        VehicleStatus vStatus = status.vehicleStatus;
+        assertEquals(17273.0, vStatus.mileage, 0.1, "Mileage");
+        Position p = vStatus.position;
+        assertEquals(219, p.heading, "Heading");
+
+        assertEquals("NA", vStatus.dcsCchActivation, "DCS Activation");
+        assertEquals(false, vStatus.dcsCchOngoing, "DCS Ongoing");
+    }
+
+    @Test
+    public void testServices() {
+        String resource1 = FileReader.readFileInString("src/test/resources/webapi/vehicle-status.json");
+        VehicleStatusContainer status = GSON.fromJson(resource1, VehicleStatusContainer.class);
+        VehicleStatus vStatus = status.vehicleStatus;
+        List<CBSMessage> services = vStatus.cbsData;
+        CBSMessage message = services.get(0);
+        assertEquals(15345, message.cbsRemainingMileage, "Service Mileage");
+        message = services.get(1);
+        assertEquals(-1, message.cbsRemainingMileage, "Service Mileage ");
+    }
+
+    @Test
+    public void testCompatibility() {
+        String resource = FileReader.readFileInString("src/test/resources/api/vehicle/vehicle-ccm.json");
+        VehicleAttributesContainer vac = GSON.fromJson(resource, VehicleAttributesContainer.class);
+        assertEquals("Laden nicht möglich", vac.vehicleMessages.ccmMessages.get(0).text, "CCM");
+        // Time Test to be removed - different Machines = different Time Zones
+        // VehicleStatusContainer vsc = GSON.fromJson(vac.transform(), VehicleStatusContainer.class);
+        // assertEquals("27.09.2020 13:18", vsc.vehicleStatus.getUpdateTime(), "UTC DateTime");
+        // String ldt = Converter.getLocalDateTime(vsc.vehicleStatus.getUpdateTime());
+        // assertEquals("2020-09-27T15:18:00", ldt.toString(), "Local DateTime");
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AllTripTests.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AllTripTests.java
new file mode 100644 (file)
index 0000000..98289e0
--- /dev/null
@@ -0,0 +1,136 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.VehicleType;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.LifetimeWrapper;
+import org.openhab.binding.bmwconnecteddrive.internal.util.FileReader;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandlerCallback;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link AllTripTests} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class AllTripTests {
+    private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
+
+    private static final int HYBRID_CALL_TIMES = 6;
+
+    @Nullable
+    ArgumentCaptor<ChannelUID> channelCaptor;
+    @Nullable
+    ArgumentCaptor<State> stateCaptor;
+    @Nullable
+    ThingHandlerCallback tc;
+    @Nullable
+    VehicleHandler cch;
+    @Nullable
+    List<ChannelUID> allChannels;
+    @Nullable
+    List<State> allStates;
+    String driveTrain = Constants.EMPTY;
+    boolean imperial;
+
+    /**
+     * Prepare environment for Vehicle Status Updates
+     */
+    public void setup(String type, boolean imperial) {
+        driveTrain = type;
+        this.imperial = imperial;
+        Thing thing = mock(Thing.class);
+        when(thing.getUID()).thenReturn(new ThingUID("testbinding", "test"));
+        BMWConnectedDriveOptionProvider op = mock(BMWConnectedDriveOptionProvider.class);
+        cch = new VehicleHandler(thing, op, type, imperial);
+        tc = mock(ThingHandlerCallback.class);
+        cch.setCallback(tc);
+        channelCaptor = ArgumentCaptor.forClass(ChannelUID.class);
+        stateCaptor = ArgumentCaptor.forClass(State.class);
+    }
+
+    private boolean testTrip(String statusContent, int callbacksExpected, Optional<Map<String, State>> concreteChecks) {
+        assertNotNull(statusContent);
+        cch.allTripsCallback.onResponse(statusContent);
+        verify(tc, times(callbacksExpected)).stateUpdated(channelCaptor.capture(), stateCaptor.capture());
+        allChannels = channelCaptor.getAllValues();
+        allStates = stateCaptor.getAllValues();
+
+        assertNotNull(driveTrain);
+        LifetimeWrapper checker = new LifetimeWrapper(driveTrain, imperial, statusContent);
+        trace();
+        if (concreteChecks.isPresent()) {
+            return checker.append(concreteChecks.get()).checkResults(allChannels, allStates);
+        } else {
+            return checker.checkResults(allChannels, allStates);
+        }
+    }
+
+    private void trace() {
+        for (int i = 0; i < allChannels.size(); i++) {
+            logger.info("Channel {} {}", allChannels.get(i), allStates.get(i));
+        }
+    }
+
+    @Test
+    public void testi3Rex() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.ELECTRIC_REX.toString(), false);
+        String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/all-trips.json");
+        assertTrue(testTrip(content, HYBRID_CALL_TIMES, Optional.empty()));
+    }
+
+    @Test
+    public void test530E() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.PLUGIN_HYBRID.toString(), false);
+        String content = FileReader.readFileInString("src/test/resources/responses/530E/all-trips.json");
+        assertTrue(testTrip(content, HYBRID_CALL_TIMES, Optional.empty()));
+    }
+
+    @Test
+    public void testi3RexImperial() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.ELECTRIC_REX.toString(), true);
+        String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/all-trips.json");
+        assertTrue(testTrip(content, HYBRID_CALL_TIMES, Optional.empty()));
+    }
+
+    @Test
+    public void test530EImperial() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.PLUGIN_HYBRID.toString(), true);
+        String content = FileReader.readFileInString("src/test/resources/responses/530E/all-trips.json");
+        assertTrue(testTrip(content, HYBRID_CALL_TIMES, Optional.empty()));
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthTest.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthTest.java
new file mode 100644 (file)
index 0000000..3b34f58
--- /dev/null
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.*;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConfiguration;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.BimmerConstants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.HTTPConstants;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link AuthTest} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class AuthTest {
+    private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
+
+    @Test
+    public void testAuthServerMap() {
+        Map<String, String> authServers = BimmerConstants.AUTH_SERVER_MAP;
+        assertEquals(3, authServers.size(), "Number of Servers");
+        Map<String, String> api = BimmerConstants.SERVER_MAP;
+        assertEquals(3, api.size(), "Number of Servers");
+    }
+
+    @Test
+    public void testTokenDecoding() {
+        String headerValue = "https://www.bmw-connecteddrive.com/app/static/external-dispatch.html#access_token=SfXKgkEXeeFJkVqdD4XMmfUU224MRuyh&token_type=Bearer&expires_in=7199";
+        HttpClientFactory hcf = mock(HttpClientFactory.class);
+        when(hcf.getCommonHttpClient()).thenReturn(mock(HttpClient.class));
+        when(hcf.createHttpClient(HTTPConstants.AUTH_HTTP_CLIENT_NAME)).thenReturn(mock(HttpClient.class));
+        ConnectedDriveConfiguration config = new ConnectedDriveConfiguration();
+        config.region = BimmerConstants.REGION_ROW;
+        ConnectedDriveProxy dcp = new ConnectedDriveProxy(hcf, config);
+        dcp.tokenFromUrl(headerValue);
+        Token t = dcp.getToken();
+        assertEquals("Bearer SfXKgkEXeeFJkVqdD4XMmfUU224MRuyh", t.getBearerToken(), "Token");
+    }
+
+    public void testRealTokenUpdate() {
+        ConnectedDriveConfiguration config = new ConnectedDriveConfiguration();
+        config.region = BimmerConstants.REGION_ROW;
+        config.userName = "bla";
+        config.password = "blub";
+        HttpClientFactory hcf = mock(HttpClientFactory.class);
+        when(hcf.getCommonHttpClient()).thenReturn(mock(HttpClient.class));
+        when(hcf.createHttpClient(HTTPConstants.AUTH_HTTP_CLIENT_NAME)).thenReturn(mock(HttpClient.class));
+        ConnectedDriveProxy dcp = new ConnectedDriveProxy(hcf, config);
+        Token t = dcp.getToken();
+        logger.info("Token {}", t.getBearerToken());
+        logger.info("Expires {}", t.isExpired());
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ChargeProfileTest.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ChargeProfileTest.java
new file mode 100644 (file)
index 0000000..0f181d5
--- /dev/null
@@ -0,0 +1,120 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.*;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.VehicleType;
+import org.openhab.binding.bmwconnecteddrive.internal.util.FileReader;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandlerCallback;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link ChargeProfileTest} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class ChargeProfileTest {
+    private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
+
+    private static final int PROFILE_CALLBACK_NUMBER = 37;
+
+    @Nullable
+    ArgumentCaptor<ChannelUID> channelCaptor;
+    @Nullable
+    ArgumentCaptor<State> stateCaptor;
+    @Nullable
+    ThingHandlerCallback tc;
+    @Nullable
+    VehicleHandler cch;
+    @Nullable
+    List<ChannelUID> allChannels;
+    @Nullable
+    List<State> allStates;
+    String driveTrain = Constants.EMPTY;
+    boolean imperial;
+
+    /**
+     * Prepare environment for Vehicle Status Updates
+     */
+    public void setup(String type, boolean imperial) {
+        driveTrain = type;
+        this.imperial = imperial;
+        Thing thing = mock(Thing.class);
+        when(thing.getUID()).thenReturn(new ThingUID("testbinding", "test"));
+        BMWConnectedDriveOptionProvider op = mock(BMWConnectedDriveOptionProvider.class);
+        cch = new VehicleHandler(thing, op, type, imperial);
+        tc = mock(ThingHandlerCallback.class);
+        cch.setCallback(tc);
+        channelCaptor = ArgumentCaptor.forClass(ChannelUID.class);
+        stateCaptor = ArgumentCaptor.forClass(State.class);
+    }
+
+    private boolean testProfile(String statusContent, int callbacksExpected) {
+        assertNotNull(statusContent);
+
+        cch.chargeProfileCallback.onResponse(statusContent);
+        verify(tc, times(callbacksExpected)).stateUpdated(channelCaptor.capture(), stateCaptor.capture());
+        allChannels = channelCaptor.getAllValues();
+        allStates = stateCaptor.getAllValues();
+
+        assertNotNull(driveTrain);
+        trace();
+        return true;
+    }
+
+    private void trace() {
+        for (int i = 0; i < allChannels.size(); i++) {
+            logger.info("Channel {} {}", allChannels.get(i), allStates.get(i));
+        }
+    }
+
+    /**
+     * Channel testbinding::test:charge#profile-climate ON
+     * Channel testbinding::test:charge#profile-mode IMMEDIATE_CHARGING
+     * Channel testbinding::test:charge#window-start 11:00
+     * Channel testbinding::test:charge#window-end 17:00
+     * Channel testbinding::test:charge#timer1-departure 05:00
+     * Channel testbinding::test:charge#timer1-enabled OFF
+     * Channel testbinding::test:charge#timer1-days MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY
+     * Channel testbinding::test:charge#timer2-departure 12:00
+     * Channel testbinding::test:charge#timer2-enabled ON
+     * Channel testbinding::test:charge#timer2-days SATURDAY
+     * Channel testbinding::test:charge#timer3-departure 00:00
+     * Channel testbinding::test:charge#timer3-enabled OFF
+     * Channel testbinding::test:charge#timer3-days
+     */
+    @Test
+    public void testChargingProfile() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.ELECTRIC_REX.toString(), false);
+        String content = FileReader.readFileInString("src/test/resources/webapi/charging-profile.json");
+        testProfile(content, PROFILE_CALLBACK_NUMBER);
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConfigurationTest.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConfigurationTest.java
new file mode 100644 (file)
index 0000000..4f5ab1c
--- /dev/null
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConfiguration;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.BimmerConstants;
+
+/**
+ * The {@link ConfigurationTest} test different configurations
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class ConfigurationTest {
+
+    @Test
+    public void testAuthServerMap() {
+        ConnectedDriveConfiguration cdc = new ConnectedDriveConfiguration();
+        assertFalse(ConnectedDriveBridgeHandler.checkConfiguration(cdc));
+        cdc.userName = "a";
+        assertFalse(ConnectedDriveBridgeHandler.checkConfiguration(cdc));
+        cdc.password = "b";
+        assertFalse(ConnectedDriveBridgeHandler.checkConfiguration(cdc));
+        cdc.region = "c";
+        assertFalse(ConnectedDriveBridgeHandler.checkConfiguration(cdc));
+        cdc.region = BimmerConstants.REGION_NORTH_AMERICA;
+        assertTrue(ConnectedDriveBridgeHandler.checkConfiguration(cdc));
+        cdc.region = BimmerConstants.REGION_ROW;
+        assertTrue(ConnectedDriveBridgeHandler.checkConfiguration(cdc));
+        cdc.region = BimmerConstants.REGION_CHINA;
+        assertTrue(ConnectedDriveBridgeHandler.checkConfiguration(cdc));
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ErrorResponseTest.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ErrorResponseTest.java
new file mode 100644 (file)
index 0000000..0fecdcd
--- /dev/null
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.handler;
+
+import static org.mockito.Mockito.*;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandlerCallback;
+import org.openhab.core.types.State;
+
+/**
+ * The {@link ErrorResponseTest} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class ErrorResponseTest {
+    @Nullable
+    ArgumentCaptor<ChannelUID> channelCaptor;
+    @Nullable
+    ArgumentCaptor<State> stateCaptor;
+    @Nullable
+    ThingHandlerCallback tc;
+    @Nullable
+    VehicleHandler cch;
+    @Nullable
+    List<ChannelUID> allChannels;
+    @Nullable
+    List<State> allStates;
+    @Nullable
+    String driveTrain;
+    boolean imperial;
+
+    /**
+     * Prepare environment for Vehicle Status Updates
+     */
+    public void setup(String type, boolean imperial) {
+        driveTrain = type;
+        this.imperial = imperial;
+        Thing thing = mock(Thing.class);
+        when(thing.getUID()).thenReturn(new ThingUID("testbinding", "test"));
+        BMWConnectedDriveOptionProvider op = mock(BMWConnectedDriveOptionProvider.class);
+        cch = new VehicleHandler(thing, op, type, imperial);
+        tc = mock(ThingHandlerCallback.class);
+        cch.setCallback(tc);
+        channelCaptor = ArgumentCaptor.forClass(ChannelUID.class);
+        stateCaptor = ArgumentCaptor.forClass(State.class);
+    }
+
+    @Test
+    public void testErrorResponseCallbacks() {
+        String error = "{\"error\":true,\"reason\":\"offline\"}";
+        setup("BEV", false);
+        cch.vehicleStatusCallback.onResponse(error);
+        cch.allTripsCallback.onResponse(error);
+        cch.lastTripCallback.onResponse(error);
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/FingerprintTest.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/FingerprintTest.java
new file mode 100644 (file)
index 0000000..bb8f873
--- /dev/null
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
+import org.openhab.binding.bmwconnecteddrive.internal.util.FileReader;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ThingUID;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link FingerprintTest} Test Discovery Results
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class FingerprintTest {
+    private final Logger logger = LoggerFactory.getLogger(FingerprintTest.class);
+
+    public void testDiscoveryFingerprint() {
+        Bridge b = mock(Bridge.class);
+        when(b.getUID()).thenReturn(new ThingUID("bmwconnecteddrive", "account", "user"));
+        HttpClientFactory hcf = mock(HttpClientFactory.class);
+        ConnectedDriveBridgeHandler bh = new ConnectedDriveBridgeHandler(b, hcf);
+        // when(bh.getThing()).thenReturn(b);
+
+        bh.onResponse(Constants.EMPTY_JSON);
+        assertEquals(Constants.EMPTY_JSON, bh.getDiscoveryFingerprint(), "Empty Response");
+
+        bh.onResponse(null);
+        assertEquals(Constants.EMPTY_JSON, bh.getDiscoveryFingerprint(), "Empty Response");
+
+        String content = FileReader.readFileInString("src/test/resources/webapi/connected-drive-account-info.json");
+        bh.onResponse(content);
+        String fingerprint = bh.getDiscoveryFingerprint();
+        logger.info("{}", fingerprint);
+        assertFalse(fingerprint.contains("My Real"), "Anonymous Fingerprint");
+        assertFalse(fingerprint.contains("MY_REAL_VIN"), "Anonymous Fingerprint");
+
+        NetworkError err = new NetworkError();
+        err.url = "Some URL";
+        err.status = 500;
+        err.reason = "Internal Server Error";
+        bh.onError(err);
+        assertEquals(err.toJson(), bh.getDiscoveryFingerprint(), "Empty Response");
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/LastTripTests.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/LastTripTests.java
new file mode 100644 (file)
index 0000000..d9fee97
--- /dev/null
@@ -0,0 +1,136 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.VehicleType;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.TripWrapper;
+import org.openhab.binding.bmwconnecteddrive.internal.util.FileReader;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandlerCallback;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link LastTripTests} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class LastTripTests {
+    private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
+
+    private static final int HYBRID_CALL_TIMES = 6;
+
+    @Nullable
+    ArgumentCaptor<ChannelUID> channelCaptor;
+    @Nullable
+    ArgumentCaptor<State> stateCaptor;
+    @Nullable
+    ThingHandlerCallback tc;
+    @Nullable
+    VehicleHandler cch;
+    @Nullable
+    List<ChannelUID> allChannels;
+    @Nullable
+    List<State> allStates;
+    String driveTrain = Constants.EMPTY;
+    boolean imperial;
+
+    /**
+     * Prepare environment for Vehicle Status Updates
+     */
+    public void setup(String type, boolean imperial) {
+        driveTrain = type;
+        this.imperial = imperial;
+        Thing thing = mock(Thing.class);
+        when(thing.getUID()).thenReturn(new ThingUID("testbinding", "test"));
+        BMWConnectedDriveOptionProvider op = mock(BMWConnectedDriveOptionProvider.class);
+        cch = new VehicleHandler(thing, op, type, imperial);
+        tc = mock(ThingHandlerCallback.class);
+        cch.setCallback(tc);
+        channelCaptor = ArgumentCaptor.forClass(ChannelUID.class);
+        stateCaptor = ArgumentCaptor.forClass(State.class);
+    }
+
+    private boolean testTrip(String statusContent, int callbacksExpected, Optional<Map<String, State>> concreteChecks) {
+        assertNotNull(statusContent);
+        cch.lastTripCallback.onResponse(statusContent);
+        verify(tc, times(callbacksExpected)).stateUpdated(channelCaptor.capture(), stateCaptor.capture());
+        allChannels = channelCaptor.getAllValues();
+        allStates = stateCaptor.getAllValues();
+
+        assertNotNull(driveTrain);
+        TripWrapper checker = new TripWrapper(driveTrain, imperial, statusContent);
+        trace();
+        if (concreteChecks.isPresent()) {
+            return checker.append(concreteChecks.get()).checkResults(allChannels, allStates);
+        } else {
+            return checker.checkResults(allChannels, allStates);
+        }
+    }
+
+    private void trace() {
+        for (int i = 0; i < allChannels.size(); i++) {
+            logger.info("Channel {} {}", allChannels.get(i), allStates.get(i));
+        }
+    }
+
+    @Test
+    public void testi3Rex() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.ELECTRIC_REX.toString(), false);
+        String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/last-trip.json");
+        assertTrue(testTrip(content, HYBRID_CALL_TIMES, Optional.empty()));
+    }
+
+    @Test
+    public void test530E() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.PLUGIN_HYBRID.toString(), false);
+        String content = FileReader.readFileInString("src/test/resources/responses/530E/last-trip.json");
+        assertTrue(testTrip(content, HYBRID_CALL_TIMES, Optional.empty()));
+    }
+
+    @Test
+    public void testi3RexImperial() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.ELECTRIC_REX.toString(), true);
+        String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/last-trip.json");
+        assertTrue(testTrip(content, HYBRID_CALL_TIMES, Optional.empty()));
+    }
+
+    @Test
+    public void test530EImperial() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.PLUGIN_HYBRID.toString(), true);
+        String content = FileReader.readFileInString("src/test/resources/responses/530E/last-trip.json");
+        assertTrue(testTrip(content, HYBRID_CALL_TIMES, Optional.empty()));
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/SimulationTest.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/SimulationTest.java
new file mode 100644 (file)
index 0000000..897a524
--- /dev/null
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.bmwconnecteddrive.internal.handler.simulation.Injector;
+
+/**
+ * The {@link SimulationTest} Assures simulation is off
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class SimulationTest {
+
+    @Test
+    public void testSimulationOff() {
+        assertFalse(Injector.isActive(), "Simulation off");
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleTests.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleTests.java
new file mode 100644 (file)
index 0000000..91221b9
--- /dev/null
@@ -0,0 +1,418 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.VehicleType;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.StatusWrapper;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.compat.VehicleAttributesContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.util.FileReader;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandlerCallback;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link VehicleTests} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class VehicleTests {
+    private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
+
+    private static final int STATUS_ELECTRIC = 9;
+    private static final int STATUS_CONV = 7;
+    private static final int RANGE_HYBRID = 9;
+    private static final int RANGE_CONV = 4;
+    private static final int RANGE_ELECTRIC = 4;
+    private static final int DOORS = 12;
+    private static final int CHECK_EMPTY = 3;
+    private static final int CHECK_AVAILABLE = 3;
+    private static final int SERVICE_AVAILABLE = 4;
+    private static final int SERVICE_EMPTY = 4;
+    private static final int POSITION = 2;
+
+    @Nullable
+    ArgumentCaptor<ChannelUID> channelCaptor;
+    @Nullable
+    ArgumentCaptor<State> stateCaptor;
+    @Nullable
+    ThingHandlerCallback tc;
+    @Nullable
+    VehicleHandler cch;
+    @Nullable
+    List<ChannelUID> allChannels;
+    @Nullable
+    List<State> allStates;
+    String driveTrain = Constants.EMPTY;
+    boolean imperial;
+
+    /**
+     * Prepare environment for Vehicle Status Updates
+     */
+    public void setup(String type, boolean imperial) {
+        driveTrain = type;
+        this.imperial = imperial;
+        Thing thing = mock(Thing.class);
+        when(thing.getUID()).thenReturn(new ThingUID("testbinding", "test"));
+        BMWConnectedDriveOptionProvider op = mock(BMWConnectedDriveOptionProvider.class);
+        cch = new VehicleHandler(thing, op, type, imperial);
+        tc = mock(ThingHandlerCallback.class);
+        cch.setCallback(tc);
+        channelCaptor = ArgumentCaptor.forClass(ChannelUID.class);
+        stateCaptor = ArgumentCaptor.forClass(State.class);
+    }
+
+    private boolean testVehicle(String statusContent, int callbacksExpected,
+            Optional<Map<String, State>> concreteChecks) {
+        assertNotNull(statusContent);
+        cch.vehicleStatusCallback.onResponse(statusContent);
+        verify(tc, times(callbacksExpected)).stateUpdated(channelCaptor.capture(), stateCaptor.capture());
+        allChannels = channelCaptor.getAllValues();
+        allStates = stateCaptor.getAllValues();
+
+        assertNotNull(driveTrain);
+        StatusWrapper checker = new StatusWrapper(driveTrain, imperial, statusContent);
+        trace();
+        if (concreteChecks.isPresent()) {
+            return checker.append(concreteChecks.get()).checkResults(allChannels, allStates);
+        } else {
+            return checker.checkResults(allChannels, allStates);
+        }
+    }
+
+    private void trace() {
+        for (int i = 0; i < allChannels.size(); i++) {
+            logger.info("Channel {} {}", allChannels.get(i), allStates.get(i));
+        }
+    }
+
+    /**
+     * Test various Vehicles from users which delivered their fingerprint.
+     * The tests are checking the chain from "JSON to Channel update".
+     * Checks are done in an automated way cross checking the data from JSON and data delivered via Channel.
+     * Also important the updates are counted in order to check if code changes are affecting Channel Updates.
+     *
+     * With the given output the updated Channels are visible.
+     * Example:
+     *
+     * testi3Rex
+     * [main] INFO org.eclipse.jetty.util.log - Logging initialized @1731ms
+     * Channel testbinding::test:status#lock Secured
+     * Channel testbinding::test:status#service-date 2021-11-01T13:00:00.000+0100
+     * Channel testbinding::test:status#service-mileage -1.0 km
+     * Channel testbinding::test:status#check-control Not Active
+     * Channel testbinding::test:status#last-update 2020-08-24T17:55:32.000+0200
+     * Channel testbinding::test:status#doors CLOSED
+     * Channel testbinding::test:status#windows CLOSED
+     * Channel testbinding::test:doors#driver-front CLOSED
+     * Channel testbinding::test:doors#driver-rear CLOSED
+     * Channel testbinding::test:doors#passenger-front CLOSED
+     * Channel testbinding::test:doors#passenger-rear CLOSED
+     * Channel testbinding::test:doors#trunk CLOSED
+     * Channel testbinding::test:doors#hood CLOSED
+     * Channel testbinding::test:doors#window-driver-front CLOSED
+     * Channel testbinding::test:doors#window-driver-rear CLOSED
+     * Channel testbinding::test:doors#window-passenger-front CLOSED
+     * Channel testbinding::test:doors#window-passenger-rear CLOSED
+     * Channel testbinding::test:doors#window-rear INVALID
+     * Channel testbinding::test:doors#sunroof CLOSED
+     * Channel testbinding::test:range#mileage 17273.0 km
+     * Channel testbinding::test:range#electric 148.0 km
+     * Channel testbinding::test:range#radius-electric 118.4 km
+     * Channel testbinding::test:range#fuel 70.0 km
+     * Channel testbinding::test:range#radius-fuel 56.0 km
+     * Channel testbinding::test:range#hybrid 218.0 km
+     * Channel testbinding::test:range#radius-hybrid 174.4 km
+     * Channel testbinding::test:range#soc 71.0 %
+     * Channel testbinding::test:range#remaining-fuel 4.0 l
+     * Channel testbinding::test:status#charge Charging Goal Reached
+     * Channel testbinding::test:check#size 0
+     * Channel testbinding::test:check#name INVALID
+     * Channel testbinding::test:check#mileage -1.0 km
+     * Channel testbinding::test:check#index -1
+     * Channel testbinding::test:service#size 4
+     * Channel testbinding::test:service#name Brake Fluid
+     * Channel testbinding::test:service#date 2021-11-01T13:00:00.000+0100
+     * Channel testbinding::test:service#mileage 15345.0 km
+     * Channel testbinding::test:service#index 0
+     * Channel testbinding::test:location#latitude 50.55604934692383
+     * Channel testbinding::test:location#longitude 8.4956693649292
+     * Channel testbinding::test:location#heading 219.0 Â°
+     *
+     */
+
+    @Test
+    public void testi3Rex() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.ELECTRIC_REX.toString(), false);
+        String content = FileReader.readFileInString("src/test/resources/webapi/vehicle-status.json");
+        assertTrue(testVehicle(content,
+                STATUS_ELECTRIC + RANGE_HYBRID + DOORS + CHECK_EMPTY + SERVICE_AVAILABLE + POSITION, Optional.empty()));
+    }
+
+    @Test
+    public void testi3RexMiles() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.ELECTRIC_REX.toString(), true);
+        String content = FileReader.readFileInString("src/test/resources/webapi/vehicle-status.json");
+        // assertTrue(testVehicle(content, HYBRID_CALL_TIMES + LIST_UPDATES, Optional.empty()));
+        assertTrue(testVehicle(content,
+                STATUS_ELECTRIC + RANGE_HYBRID + DOORS + CHECK_EMPTY + SERVICE_AVAILABLE + POSITION, Optional.empty()));
+    }
+
+    @Test
+    public void testF15() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.CONVENTIONAL.toString(), false);
+        String content = FileReader.readFileInString("src/test/resources/responses/F15/status.json");
+        // Check earliest Service by hard
+        Map<String, State> m = new HashMap<String, State>();
+        // Don>'t test on concrete timestamp - it's is different on each machine
+        // Check for cbsType which is "Oil" instead
+        // m.put(ConnectedDriveConstants.SERVICE_DATE, DateTimeType.valueOf("2018-06-01T14:00:00.000+0200"));
+        m.put(ConnectedDriveConstants.NAME, StringType.valueOf("Oil"));
+        assertTrue(testVehicle(content, STATUS_CONV + DOORS + RANGE_CONV + POSITION + SERVICE_AVAILABLE + CHECK_EMPTY,
+                Optional.of(m)));
+    }
+
+    @Test
+    public void testF15Miles() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.CONVENTIONAL.toString(), true);
+        String content = FileReader.readFileInString("src/test/resources/responses/F15/status.json");
+        // Check earliest Service by hard
+        Map<String, State> m = new HashMap<String, State>();
+        // Don>'t test on concrete timestamp - it's idfferent on each machine
+        // Check for cbsType which is "Oil" instead
+        // m.put(ConnectedDriveConstants.SERVICE_DATE, DateTimeType.valueOf("2018-06-01T14:00:00.000+0200"));
+        m.put(ConnectedDriveConstants.NAME, StringType.valueOf("Oil"));
+        assertTrue(testVehicle(content, STATUS_CONV + DOORS + RANGE_CONV + POSITION + SERVICE_AVAILABLE + CHECK_EMPTY,
+                Optional.of(m)));
+    }
+
+    @Test
+    public void testF31() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.CONVENTIONAL.toString(), false);
+        String content = FileReader.readFileInString("src/test/resources/responses/F31/status.json");
+        assertTrue(testVehicle(content, STATUS_CONV + DOORS + RANGE_CONV + POSITION + SERVICE_AVAILABLE + CHECK_EMPTY,
+                Optional.empty()));
+    }
+
+    @Test
+    public void testF31Miles() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.CONVENTIONAL.toString(), true);
+        String content = FileReader.readFileInString("src/test/resources/responses/F31/status.json");
+        assertTrue(testVehicle(content, STATUS_CONV + DOORS + RANGE_CONV + POSITION + SERVICE_AVAILABLE + CHECK_EMPTY,
+                Optional.empty()));
+    }
+
+    @Test
+    public void testF35() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.CONVENTIONAL.toString(), false);
+        String content = FileReader.readFileInString("src/test/resources/responses/F35/status.json");
+        assertTrue(testVehicle(content, STATUS_CONV + DOORS + RANGE_CONV + POSITION + SERVICE_EMPTY + CHECK_EMPTY,
+                Optional.empty()));
+    }
+
+    @Test
+    public void testF35Miles() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.CONVENTIONAL.toString(), true);
+        String content = FileReader.readFileInString("src/test/resources/responses/F35/status.json");
+        assertTrue(testVehicle(content, STATUS_CONV + DOORS + RANGE_CONV + POSITION + SERVICE_EMPTY + CHECK_EMPTY,
+                Optional.empty()));
+    }
+
+    @Test
+    public void testF45() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.CONVENTIONAL.toString(), false);
+        String content = FileReader.readFileInString("src/test/resources/responses/F45/status.json");
+        // assertTrue(testVehicle(content, 27, Optional.empty()));
+        assertTrue(testVehicle(content, STATUS_CONV + DOORS + RANGE_CONV + SERVICE_EMPTY + CHECK_EMPTY + POSITION,
+                Optional.empty()));
+    }
+
+    @Test
+    public void testF45Miles() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.CONVENTIONAL.toString(), true);
+        String content = FileReader.readFileInString("src/test/resources/responses/F45/status.json");
+        assertTrue(testVehicle(content, STATUS_CONV + DOORS + RANGE_CONV + SERVICE_EMPTY + CHECK_EMPTY + POSITION,
+                Optional.empty()));
+    }
+
+    @Test
+    public void testF48() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.CONVENTIONAL.toString(), false);
+        String content = FileReader.readFileInString("src/test/resources/responses/F48/status.json");
+        assertTrue(testVehicle(content,
+                STATUS_CONV + DOORS + RANGE_CONV + SERVICE_AVAILABLE + CHECK_AVAILABLE + POSITION, Optional.empty()));
+    }
+
+    @Test
+    public void testF48Miles() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.CONVENTIONAL.toString(), true);
+        String content = FileReader.readFileInString("src/test/resources/responses/F48/status.json");
+        assertTrue(testVehicle(content,
+                STATUS_CONV + DOORS + RANGE_CONV + SERVICE_AVAILABLE + CHECK_AVAILABLE + POSITION, Optional.empty()));
+    }
+
+    @Test
+    public void testG31NBTEvo() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.CONVENTIONAL.toString(), false);
+        String content = FileReader.readFileInString("src/test/resources/responses/G31_NBTevo/status.json");
+        // assertTrue(testVehicle(content, 27, Optional.empty()));
+        assertTrue(testVehicle(content, STATUS_CONV + DOORS + RANGE_CONV + SERVICE_AVAILABLE + CHECK_EMPTY + POSITION,
+                Optional.empty()));
+    }
+
+    @Test
+    public void testG31NBTEvoMiles() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.CONVENTIONAL.toString(), true);
+        String content = FileReader.readFileInString("src/test/resources/responses/G31_NBTevo/status.json");
+        assertTrue(testVehicle(content, STATUS_CONV + DOORS + RANGE_CONV + SERVICE_AVAILABLE + CHECK_EMPTY + POSITION,
+                Optional.empty()));
+    }
+
+    @Test
+    public void testI01NoRex() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.ELECTRIC.toString(), false);
+        String content = FileReader.readFileInString("src/test/resources/responses/I01_NOREX/status.json");
+        assertTrue(testVehicle(content,
+                STATUS_ELECTRIC + DOORS + RANGE_ELECTRIC + SERVICE_AVAILABLE + CHECK_EMPTY + POSITION,
+                Optional.empty()));
+    }
+
+    @Test
+    public void testI01NoRexMiles() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.ELECTRIC.toString(), true);
+        String content = FileReader.readFileInString("src/test/resources/responses/I01_NOREX/status.json");
+        assertTrue(testVehicle(content,
+                STATUS_ELECTRIC + DOORS + RANGE_ELECTRIC + SERVICE_AVAILABLE + CHECK_EMPTY + POSITION,
+                Optional.empty()));
+    }
+
+    @Test
+    public void testI01Rex() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.ELECTRIC_REX.toString(), false);
+        String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/status.json");
+        assertTrue(testVehicle(content,
+                STATUS_ELECTRIC + DOORS + RANGE_HYBRID + SERVICE_AVAILABLE + CHECK_EMPTY + POSITION, Optional.empty()));
+    }
+
+    @Test
+    public void testI01RexMiles() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.ELECTRIC_REX.toString(), true);
+        String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/status.json");
+        assertTrue(testVehicle(content,
+                STATUS_ELECTRIC + DOORS + RANGE_HYBRID + SERVICE_AVAILABLE + CHECK_EMPTY + POSITION, Optional.empty()));
+    }
+
+    @Test
+    public void test318iF31() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.CONVENTIONAL.toString(), false);
+        String content = FileReader.readFileInString("src/test/resources/responses/F31/status-318i.json");
+        Map<String, State> m = new HashMap<String, State>();
+        m.put(ConnectedDriveConstants.WINDOWS, StringType.valueOf(Constants.INTERMEDIATE));
+        assertTrue(testVehicle(content, STATUS_CONV + DOORS + RANGE_CONV + SERVICE_AVAILABLE + CHECK_EMPTY + POSITION,
+                Optional.empty()));
+    }
+
+    @Test
+    public void test318iF31Miles() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.CONVENTIONAL.toString(), true);
+        String content = FileReader.readFileInString("src/test/resources/responses/F31/status-318i.json");
+        Map<String, State> m = new HashMap<String, State>();
+        m.put(ConnectedDriveConstants.WINDOWS, StringType.valueOf(Constants.INTERMEDIATE));
+        assertTrue(testVehicle(content, STATUS_CONV + DOORS + RANGE_CONV + SERVICE_AVAILABLE + CHECK_EMPTY + POSITION,
+                Optional.empty()));
+    }
+
+    @Test
+    public void testI01RexCompat() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.ELECTRIC_REX.toString(), false);
+        String content = FileReader.readFileInString("src/test/resources/api/vehicle/vehicle-ccm.json");
+        VehicleAttributesContainer vac = Converter.getGson().fromJson(content, VehicleAttributesContainer.class);
+        assertTrue(testVehicle(Converter.transformLegacyStatus(vac),
+                STATUS_ELECTRIC + DOORS + RANGE_HYBRID + SERVICE_AVAILABLE + CHECK_AVAILABLE + POSITION,
+                Optional.empty()));
+    }
+
+    @Test
+    public void testI01RexMilesCompat() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.ELECTRIC_REX.toString(), true);
+        String content = FileReader.readFileInString("src/test/resources/api/vehicle/vehicle-ccm.json");
+        VehicleAttributesContainer vac = Converter.getGson().fromJson(content, VehicleAttributesContainer.class);
+        assertTrue(testVehicle(Converter.transformLegacyStatus(vac),
+                STATUS_ELECTRIC + DOORS + RANGE_HYBRID + SERVICE_AVAILABLE + CHECK_AVAILABLE + POSITION,
+                Optional.empty()));
+    }
+
+    @Test
+    public void testF11Compat() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.CONVENTIONAL.toString(), false);
+        String content = FileReader.readFileInString("src/test/resources/responses/F11/vehicle-status.json");
+        VehicleAttributesContainer vac = Converter.getGson().fromJson(content, VehicleAttributesContainer.class);
+        assertTrue(testVehicle(Converter.transformLegacyStatus(vac),
+                STATUS_CONV + DOORS + RANGE_CONV + SERVICE_AVAILABLE + CHECK_EMPTY + POSITION, Optional.empty()));
+    }
+
+    @Test
+    public void testF11MilesCompat() {
+        logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
+        setup(VehicleType.CONVENTIONAL.toString(), true);
+        String content = FileReader.readFileInString("src/test/resources/responses/F11/vehicle-status.json");
+        VehicleAttributesContainer vac = Converter.getGson().fromJson(content, VehicleAttributesContainer.class);
+        assertTrue(testVehicle(Converter.transformLegacyStatus(vac),
+                STATUS_CONV + DOORS + RANGE_CONV + SERVICE_AVAILABLE + CHECK_EMPTY + POSITION, Optional.empty()));
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/util/FileReader.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/util/FileReader.java
new file mode 100644 (file)
index 0000000..2758042
--- /dev/null
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.util;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
+
+/**
+ * The {@link FileReader} Helper Util to read test resource files
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class FileReader {
+
+    public static String readFileInString(String filename) {
+        try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(filename), "UTF-8"));) {
+            StringBuilder buf = new StringBuilder();
+            String sCurrentLine;
+
+            while ((sCurrentLine = br.readLine()) != null) {
+                buf.append(sCurrentLine);
+            }
+            return buf.toString();
+        } catch (IOException e) {
+            // fail if file cannot be read
+            assertTrue(false);
+        }
+        return Constants.UNDEF;
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/util/LocaleTest.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/util/LocaleTest.java
new file mode 100644 (file)
index 0000000..a4256c5
--- /dev/null
@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bmwconnecteddrive.internal.util;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.Locale;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatus;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatusContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
+import org.openhab.core.library.types.DateTimeType;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link LocaleTest} is testing locale settings
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("null")
+public class LocaleTest {
+    private static final Gson GSON = new Gson();
+
+    @Test
+    public void languageTest() {
+        assertTrue(ConnectedDriveConstants.IMPERIAL_COUNTRIES.contains(Locale.UK.getCountry()), "United Kingdom");
+        assertTrue(ConnectedDriveConstants.IMPERIAL_COUNTRIES.contains(Locale.US.getCountry()), "United States");
+        assertFalse(ConnectedDriveConstants.IMPERIAL_COUNTRIES.contains(Locale.FRANCE.getCountry()), "France");
+        assertFalse(ConnectedDriveConstants.IMPERIAL_COUNTRIES.contains(Locale.GERMAN.getCountry()), "Germany");
+    }
+
+    public void testTimeUTCToLocaleTime() {
+        String resource1 = FileReader.readFileInString("src/test/resources/webapi/vehicle-status.json");
+        VehicleStatusContainer status = GSON.fromJson(resource1, VehicleStatusContainer.class);
+        VehicleStatus vStatus = status.vehicleStatus;
+
+        String inputTime = vStatus.internalDataTimeUTC;
+        String localeTime = Converter.getLocalDateTime(inputTime);
+        String dateTimeType = DateTimeType.valueOf(localeTime).toString();
+        assertEquals("2020-08-24T15:55:32", inputTime, "Input DateTime");
+        assertEquals("2020-08-24T17:55:32", localeTime, "Output DateTime");
+        assertEquals("2020-08-24T17:55:32.000+0200", dateTimeType, "DateTimeType Value");
+
+        inputTime = vStatus.updateTime;
+        localeTime = Converter.getLocalDateTime(inputTime);
+        dateTimeType = DateTimeType.valueOf(localeTime).toString();
+        assertEquals("2020-08-24T15:55:32+0000", inputTime, "Input DateTime");
+        assertEquals("2020-08-24T17:55:32", localeTime, "Output DateTime");
+        assertEquals("2020-08-24T17:55:32.000+0200", dateTimeType, "DateTimeType Value");
+
+        inputTime = vStatus.updateTime;
+        localeTime = Converter.getLocalDateTimeWithoutOffest(inputTime);
+        dateTimeType = DateTimeType.valueOf(localeTime).toString();
+        assertEquals("2020-08-24T15:55:32+0000", inputTime, "Input DateTime");
+        assertEquals("2020-08-24T15:55:32", localeTime, "Output DateTime");
+        assertEquals("2020-08-24T15:55:32.000+0200", dateTimeType, "DateTimeType Value");
+    }
+
+    @Test
+    public void testDistance() {
+        double lat = 45.678;
+        double lon = 8.765;
+        double distance = 0.005;
+        double dist = Converter.measureDistance(lat, lon, lat + distance, lon + distance);
+        assertTrue(dist < 1, "Distance below 1 km");
+    }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/api/vehicle/efficiency.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/api/vehicle/efficiency.json
new file mode 100644 (file)
index 0000000..cf3436c
--- /dev/null
@@ -0,0 +1,85 @@
+{
+       "communitySwitch": false,
+       "modelType": "I3",
+       "scoreList": [
+               {
+                       "attrName": "AVERAGE_ELECTRIC_CONSUMPTION",
+                       "attrUnit": "KWH_PER_100KM",
+                       "minValue": 0.0,
+                       "maxValue": 40.0,
+                       "lifeTime": 16.5
+               },
+               {
+                       "attrName": "AVERAGE_RECUPERATED_ENERGY_PER_100_KM",
+                       "attrUnit": "KWH_PER_100KM",
+                       "minValue": 0.0,
+                       "maxValue": 20.0,
+                       "lifeTime": 4.5
+               },
+               {
+                       "attrName": "CUMULATED_ELECTRIC_DRIVEN_DISTANCE",
+                       "attrUnit": "KM",
+                       "minValue": 0.0,
+                       "maxValue": 16593.4,
+                       "lifeTime": 16592.4
+               },
+               {
+                       "attrName": "LONGEST_DISTANCE_WITHOUT_CHARGING",
+                       "attrUnit": "KM",
+                       "minValue": 0.0,
+                       "maxValue": 270.0,
+                       "lifeTime": 185.5
+               }
+       ],
+       "lastTripList": [
+               {
+                       "name": "LASTTRIP_DELTA_KM",
+                       "unit": "KM",
+                       "lastTrip": "2.0"
+               },
+               {
+                       "name": "ACTUAL_DISTANCE_WITHOUT_CHARGING",
+                       "unit": "KM",
+                       "lastTrip": "31.0"
+               },
+               {
+                       "name": "AVERAGE_ELECTRIC_CONSUMPTION",
+                       "unit": "KWH_PER_100KM",
+                       "lastTrip": "14.5"
+               },
+               {
+                       "name": "AVERAGE_RECUPERATED_ENERGY_PER_100_KM",
+                       "unit": "KWH_PER_100KM",
+                       "lastTrip": "8.0"
+               },
+               {
+                       "name": "CUMULATED_ELECTRIC_DRIVEN_DISTANCE",
+                       "unit": "KM",
+                       "lastTrip": "16592.4"
+               }
+       ],
+       "lifeTimeList": [],
+       "efficiencyQuotient": 44,
+       "characteristicList": [
+               {
+                       "characteristic": "TOTAL_CONSUMPTION",
+                       "quantity": 2
+               },
+               {
+                       "characteristic": "AUXILIARY_CONSUMPTION",
+                       "quantity": 2
+               },
+               {
+                       "characteristic": "DRIVING_MODE",
+                       "quantity": 0
+               },
+               {
+                       "characteristic": "ACCELERATION",
+                       "quantity": 3
+               },
+               {
+                       "characteristic": "ANTICIPATION",
+                       "quantity": 3
+               }
+       ]
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/api/vehicle/navigation.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/api/vehicle/navigation.json
new file mode 100644 (file)
index 0000000..995c6a5
--- /dev/null
@@ -0,0 +1,12 @@
+{
+       "latitude": 56.789,
+       "longitude": 8.765,
+       "isoCountryCode": "DEU",
+       "auxPowerRegular": 1.4,
+       "auxPowerEcoPro": 1.2,
+       "auxPowerEcoProPlus": 0.4,
+       "soc": 25.952999114990234,
+       "pendingUpdate": false,
+       "vehicleTracking": true,
+       "socmax": 29.84
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/api/vehicle/test.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/api/vehicle/test.json
new file mode 100644 (file)
index 0000000..d04e754
--- /dev/null
@@ -0,0 +1,107 @@
+{
+       "attributesMap": {
+               "unitOfLength": "km",
+               "sunroof_state": "CLOSED",
+               "chargingLogicCurrentlyActive": "NOT_CHARGING",
+               "vehicle_tracking": "1",
+               "chargeNowAllowed": "NOT_ALLOWED",
+               "updateTime_converted": "16.08.2020 15:34",
+               "door_driver_rear": "CLOSED",
+               "head_unit_pu_software": "07/16",
+               "beMaxRangeElectricKm": "203.0",
+               "door_passenger_rear": "CLOSED",
+               "beRemainingRangeFuelKm": "62.0",
+               "Segment_LastTrip_time_segment_end_formatted_date": "15.08.2020",
+               "door_driver_front": "CLOSED",
+               "shdStatusUnified": "CLOSED",
+               "hood_state": "CLOSED",
+               "charging_status": "CHARGINGACTIVE",
+               "kombi_current_remaining_range_fuel": "62.0",
+               "beMaxRangeElectric": "203.0",
+               "window_driver_rear": "CLOSED",
+               "beRemainingRangeElectricKm": "203.0",
+               "mileage": "17044",
+               "Segment_LastTrip_time_segment_end_formatted_time": "19:11",
+               "beMaxRangeElectricMile": "126.0",
+               "Segment_LastTrip_time_segment_end_formatted": "15.08.2020 19:11",
+               "lastChargingEndResult": "UNKNOWN",
+               "unitOfEnergy": "kWh",
+               "beRemainingRangeElectric": "203.0",
+               "sunroof_position": "0",
+               "soc_hv_percent": "65.4",
+               "single_immediate_charging": "isUnused",
+               "updateTime_converted_time": "15:34",
+               "chargingHVStatus": "FINISHED_FULLY_CHARGED",
+               "connectorStatus": "CONNECTED",
+               "chargingLevelHv": "100.0",
+               "chargingSystemStatus": "CHARGINGACTIVE",
+               "fuelPercent": "47",
+               "unitOfCombustionConsumption": "l/100km",
+               "gps_lat": "xxx",
+               "window_driver_front": "CLOSED",
+               "Segment_LastTrip_ratio_electric_driven_distance": "100",
+               "gps_lng": "xxx",
+               "condition_based_services": "00003,OK,2021-11,;00017,OK,2021-11,;00001,OK,2021-11,;00032,OK,2021-11,",
+               "window_passenger_front": "CLOSED",
+               "window_passenger_rear": "CLOSED",
+               "lastChargingEndReason": "UNKNOWN",
+               "updateTime_converted_date": "16.08.2020",
+               "beRemainingRangeFuelMile": "38.0",
+               "beRemainingRangeFuel": "62.0",
+               "door_passenger_front": "CLOSED",
+               "updateTime_converted_timestamp": "1597592093000",
+               "remaining_fuel": "4",
+               "charging_inductive_positioning": "not_positioned",
+               "heading": "221",
+               "lsc_trigger": "DOORSTATECHANGED",
+               "lights_parking": "OFF",
+               "door_lock_state": "UNLOCKED",
+               "updateTime": "16.08.2020 14:34:53 UTC",
+               "prognosisWhileChargingStatus": "IS_PERFORMED",
+               "head_unit": "EntryNav",
+               "trunk_state": "CLOSED",
+               "battery_size_max": "33200",
+               "charging_connection_type": "CONDUCTIVE",
+               "beRemainingRangeElectricMile": "126.0",
+               "unitOfElectricConsumption": "kWh/100km",
+               "Segment_LastTrip_time_segment_end": "15.08.2020 19:11:00 UTC",
+               "lastUpdateReason": "DOORSTATECHANGED"
+       },
+       "vehicleMessages": {
+               "ccmMessages": [],
+               "cbsMessages": [
+                       {
+                               "description": "Nächster Wechsel spätestens zum angegebenen Termin.",
+                               "text": "Bremsflüssigkeit",
+                               "id": 3,
+                               "status": "OK",
+                               "messageType": "CBS",
+                               "date": "2021-11"
+                       },
+                       {
+                               "description": "Nächste Sichtprüfung nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+                               "text": "Fahrzeug-Check",
+                               "id": 17,
+                               "status": "OK",
+                               "messageType": "CBS",
+                               "date": "2021-11"
+                       },
+                       {
+                               "description": "Nächster Wechsel nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+                               "text": "Motoröl",
+                               "id": 1,
+                               "status": "OK",
+                               "messageType": "CBS",
+                               "date": "2021-11"
+                       },
+                       {
+                               "description": "Nächste gesetzliche Fahrzeuguntersuchung zum angegebenen Termin.",
+                               "text": "§ Fahrzeuguntersuchung",
+                               "id": 32,
+                               "status": "OK",
+                               "messageType": "CBS",
+                               "date": "2021-11"
+                       }
+               ]
+       }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/api/vehicle/vehicle-ccm.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/api/vehicle/vehicle-ccm.json
new file mode 100644 (file)
index 0000000..42e438f
--- /dev/null
@@ -0,0 +1,117 @@
+{
+       "attributesMap": {
+               "unitOfLength": "km",
+               "sunroof_state": "CLOSED",
+               "chargingLogicCurrentlyActive": "NOT_CHARGING",
+               "vehicle_tracking": "1",
+               "chargeNowAllowed": "ALLOWED",
+               "updateTime_converted": "27.09.2020 13:18",
+               "door_driver_rear": "CLOSED",
+               "head_unit_pu_software": "07/16",
+               "beMaxRangeElectricKm": "203.0",
+               "door_passenger_rear": "CLOSED",
+               "beRemainingRangeFuelKm": "65.0",
+               "Segment_LastTrip_time_segment_end_formatted_date": "27.09.2020",
+               "door_driver_front": "CLOSED",
+               "shdStatusUnified": "CLOSED",
+               "hood_state": "CLOSED",
+               "charging_status": "CHARGINGERROR",
+               "kombi_current_remaining_range_fuel": "65.0",
+               "beMaxRangeElectric": "203.0",
+               "window_driver_rear": "CLOSED",
+               "beRemainingRangeElectricKm": "100.0",
+               "mileage": "18313",
+               "Segment_LastTrip_time_segment_end_formatted_time": "13:24",
+               "beMaxRangeElectricMile": "126.0",
+               "Segment_LastTrip_time_segment_end_formatted": "27.09.2020 13:24",
+               "lastChargingEndResult": "FAILED",
+               "check_control_messages": "00804,18312",
+               "unitOfEnergy": "kWh",
+               "beRemainingRangeElectric": "100.0",
+               "sunroof_position": "0",
+               "soc_hv_percent": "51.6",
+               "single_immediate_charging": "isUnused",
+               "updateTime_converted_time": "13:18",
+               "chargingHVStatus": "ERROR",
+               "connectorStatus": "CONNECTED",
+               "chargingLevelHv": "51.0",
+               "chargingSystemStatus": "CHARGINGERROR",
+               "fuelPercent": "47",
+               "unitOfCombustionConsumption": "l/100km",
+               "gps_lat": "56.789",
+               "window_driver_front": "CLOSED",
+               "Segment_LastTrip_ratio_electric_driven_distance": "100",
+               "gps_lng": "8.765",
+               "condition_based_services": "00003,OK,2021-11,;00017,OK,2021-11,;00001,OK,2021-11,;00032,OK,2021-11,",
+               "window_passenger_front": "CLOSED",
+               "targetSoc": "100.0",
+               "window_passenger_rear": "CLOSED",
+               "lastChargingEndReason": "POWERGRID_FAILED",
+               "updateTime_converted_date": "27.09.2020",
+               "beRemainingRangeFuelMile": "40.0",
+               "beRemainingRangeFuel": "65.0",
+               "door_passenger_front": "CLOSED",
+               "updateTime_converted_timestamp": "1601212738000",
+               "remaining_fuel": "4",
+               "charging_inductive_positioning": "not_positioned",
+               "heading": "39",
+               "lsc_trigger": "CHARGINGINTERRUPTED",
+               "lights_parking": "OFF",
+               "door_lock_state": "SECURED",
+               "updateTime": "27.09.2020 13:18:58 UTC",
+               "prognosisWhileChargingStatus": "NOT_NEEDED",
+               "head_unit": "EntryNav",
+               "trunk_state": "CLOSED",
+               "battery_size_max": "33200",
+               "charging_connection_type": "CONDUCTIVE",
+               "beRemainingRangeElectricMile": "62.0",
+               "unitOfElectricConsumption": "kWh/100km",
+               "Segment_LastTrip_time_segment_end": "27.09.2020 13:24:00 UTC",
+               "lastUpdateReason": "CHARGINGINTERRUPTED"
+       },
+       "vehicleMessages": {
+               "ccmMessages": [
+                       {
+                               "text": "Laden nicht möglich",
+                               "id": 804,
+                               "status": "NULL",
+                               "messageType": "CCM",
+                               "unitOfLengthRemaining": "18312"
+                       }
+               ],
+               "cbsMessages": [
+                       {
+                               "description": "Nächster Wechsel spätestens zum angegebenen Termin.",
+                               "text": "Bremsflüssigkeit",
+                               "id": 3,
+                               "status": "OK",
+                               "messageType": "CBS",
+                               "date": "2021-11"
+                       },
+                       {
+                               "description": "Nächste Sichtprüfung nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+                               "text": "Fahrzeug-Check",
+                               "id": 17,
+                               "status": "OK",
+                               "messageType": "CBS",
+                               "date": "2021-11"
+                       },
+                       {
+                               "description": "Nächster Wechsel nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+                               "text": "Motoröl",
+                               "id": 1,
+                               "status": "OK",
+                               "messageType": "CBS",
+                               "date": "2021-11"
+                       },
+                       {
+                               "description": "Nächste gesetzliche Fahrzeuguntersuchung zum angegebenen Termin.",
+                               "text": "§ Fahrzeuguntersuchung",
+                               "id": 32,
+                               "status": "OK",
+                               "messageType": "CBS",
+                               "date": "2021-11"
+                       }
+               ]
+       }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/api/vehicle/vehicle.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/api/vehicle/vehicle.json
new file mode 100644 (file)
index 0000000..129b58d
--- /dev/null
@@ -0,0 +1,108 @@
+{
+       "attributesMap": {
+               "unitOfLength": "km",
+               "sunroof_state": "CLOSED",
+               "chargingLogicCurrentlyActive": "NOT_CHARGING",
+               "vehicle_tracking": "1",
+               "chargeNowAllowed": "NOT_ALLOWED",
+               "updateTime_converted": "22.08.2020 13:55",
+               "door_driver_rear": "CLOSED",
+               "head_unit_pu_software": "07/16",
+               "beMaxRangeElectricKm": "209.0",
+               "door_passenger_rear": "CLOSED",
+               "beRemainingRangeFuelKm": "67.0",
+               "Segment_LastTrip_time_segment_end_formatted_date": "22.08.2020",
+               "door_driver_front": "CLOSED",
+               "shdStatusUnified": "CLOSED",
+               "hood_state": "CLOSED",
+               "charging_status": "NOCHARGING",
+               "kombi_current_remaining_range_fuel": "67.0",
+               "beMaxRangeElectric": "209.0",
+               "window_driver_rear": "CLOSED",
+               "beRemainingRangeElectricKm": "179.0",
+               "mileage": "17236",
+               "Segment_LastTrip_time_segment_end_formatted_time": "14:52",
+               "beMaxRangeElectricMile": "129.0",
+               "Segment_LastTrip_time_segment_end_formatted": "22.08.2020 14:52",
+               "lastChargingEndResult": "SUCCESS",
+               "check_control_messages": "",
+               "unitOfEnergy": "kWh",
+               "beRemainingRangeElectric": "179.0",
+               "sunroof_position": "0",
+               "soc_hv_percent": "82.6",
+               "single_immediate_charging": "isUnused",
+               "updateTime_converted_time": "13:55",
+               "chargingHVStatus": "INVALID",
+               "connectorStatus": "DISCONNECTED",
+               "chargingLevelHv": "89.0",
+               "chargingSystemStatus": "NOCHARGING",
+               "fuelPercent": "47",
+               "unitOfCombustionConsumption": "l/100km",
+               "gps_lat": "56.789",
+               "window_driver_front": "CLOSED",
+               "Segment_LastTrip_ratio_electric_driven_distance": "100",
+               "gps_lng": "8.765",
+               "condition_based_services": "00003,OK,2021-11,;00017,OK,2021-11,;00001,OK,2021-11,;00032,OK,2021-11,",
+               "window_passenger_front": "CLOSED",
+               "window_passenger_rear": "CLOSED",
+               "lastChargingEndReason": "CHARGING_GOAL_REACHED",
+               "updateTime_converted_date": "22.08.2020",
+               "beRemainingRangeFuelMile": "41.0",
+               "beRemainingRangeFuel": "67.0",
+               "door_passenger_front": "CLOSED",
+               "updateTime_converted_timestamp": "1598104546000",
+               "remaining_fuel": "4",
+               "charging_inductive_positioning": "not_positioned",
+               "heading": "41",
+               "lsc_trigger": "VEHCSHUTDOWN_SECURED",
+               "lights_parking": "OFF",
+               "door_lock_state": "SECURED",
+               "updateTime": "22.08.2020 12:55:46 UTC",
+               "prognosisWhileChargingStatus": "NOT_NEEDED",
+               "head_unit": "EntryNav",
+               "trunk_state": "CLOSED",
+               "battery_size_max": "33200",
+               "charging_connection_type": "CONDUCTIVE",
+               "beRemainingRangeElectricMile": "111.0",
+               "unitOfElectricConsumption": "kWh/100km",
+               "Segment_LastTrip_time_segment_end": "22.08.2020 14:52:00 UTC",
+               "lastUpdateReason": "VEHCSHUTDOWN_SECURED"
+       },
+       "vehicleMessages": {
+               "ccmMessages": [],
+               "cbsMessages": [
+                       {
+                               "description": "Nächster Wechsel spätestens zum angegebenen Termin.",
+                               "text": "Bremsflüssigkeit",
+                               "id": 3,
+                               "status": "OK",
+                               "messageType": "CBS",
+                               "date": "2021-11"
+                       },
+                       {
+                               "description": "Nächste Sichtprüfung nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+                               "text": "Fahrzeug-Check",
+                               "id": 17,
+                               "status": "OK",
+                               "messageType": "CBS",
+                               "date": "2021-11"
+                       },
+                       {
+                               "description": "Nächster Wechsel nach der angegebenen Fahrstrecke oder zum angegebenen Termin.",
+                               "text": "Motoröl",
+                               "id": 1,
+                               "status": "OK",
+                               "messageType": "CBS",
+                               "date": "2021-11"
+                       },
+                       {
+                               "description": "Nächste gesetzliche Fahrzeuguntersuchung zum angegebenen Termin.",
+                               "text": "§ Fahrzeuguntersuchung",
+                               "id": 32,
+                               "status": "OK",
+                               "messageType": "CBS",
+                               "date": "2021-11"
+                       }
+               ]
+       }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/api/vehicle/webapi-status.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/api/vehicle/webapi-status.json
new file mode 100644 (file)
index 0000000..bfb14fe
--- /dev/null
@@ -0,0 +1,76 @@
+{
+       "vehicleStatus": {
+               "vin": "ANONYMOUS",
+               "mileage": 17131,
+               "updateReason": "VEHICLE_SECURED",
+               "updateTime": "2020-08-18T17:54:12+0000",
+               "doorDriverFront": "CLOSED",
+               "doorDriverRear": "CLOSED",
+               "doorPassengerFront": "CLOSED",
+               "doorPassengerRear": "CLOSED",
+               "windowDriverFront": "CLOSED",
+               "windowDriverRear": "CLOSED",
+               "windowPassengerFront": "CLOSED",
+               "windowPassengerRear": "CLOSED",
+               "sunroof": "CLOSED",
+               "trunk": "CLOSED",
+               "rearWindow": "INVALID",
+               "hood": "CLOSED",
+               "doorLockState": "SECURED",
+               "parkingLight": "OFF",
+               "positionLight": "ON",
+               "remainingFuel": 4,
+               "remainingRangeElectric": 184,
+               "remainingRangeElectricMls": 114,
+               "remainingRangeFuel": 72,
+               "remainingRangeFuelMls": 44,
+               "maxRangeElectric": 224,
+               "maxRangeElectricMls": 139,
+               "maxFuel": 8.5,
+               "connectionStatus": "DISCONNECTED",
+               "chargingStatus": "INVALID",
+               "chargingLevelHv": 84,
+               "lastChargingEndReason": "END_REQUESTED_BY_DRIVER",
+               "lastChargingEndResult": "SUCCESS",
+               "position": {
+                       "lat": 56.789,
+                       "lon": 8.765,
+                       "heading": 41,
+                       "status": "OK"
+               },
+               "internalDataTimeUTC": "2020-08-18T17:54:12",
+               "singleImmediateCharging": false,
+               "chargingConnectionType": "CONDUCTIVE",
+               "chargingInductivePositioning": "NOT_POSITIONED",
+               "vehicleCountry": "DE",
+               "checkControlMessages": [],
+               "cbsData": [
+                       {
+                               "cbsType": "BRAKE_FLUID",
+                               "cbsState": "OK",
+                               "cbsDueDate": "2021-11",
+                               "cbsDescription": "Next change due at the latest by the stated date."
+                       },
+                       {
+                               "cbsType": "VEHICLE_CHECK",
+                               "cbsState": "OK",
+                               "cbsDueDate": "2021-11",
+                               "cbsDescription": "Next visual inspection due when the stated distance has been covered or by the stated date."
+                       },
+                       {
+                               "cbsType": "OIL",
+                               "cbsState": "OK",
+                               "cbsDueDate": "2021-11",
+                               "cbsDescription": "Next change due when the stated distance has been covered or by the specified date."
+                       },
+                       {
+                               "cbsType": "VEHICLE_TUV",
+                               "cbsState": "OK",
+                               "cbsDueDate": "2021-11",
+                               "cbsDescription": "Next statutory vehicle inspection due by the stated date."
+                       }
+               ],
+               "DCS_CCH_Activation": "NA",
+               "DCS_CCH_Ongoing": false
+       }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/530E/all-trips.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/530E/all-trips.json
new file mode 100644 (file)
index 0000000..6497619
--- /dev/null
@@ -0,0 +1,40 @@
+{
+       "allTrips": {
+               "avgElectricConsumption": {
+                       "communityLow": -4.74,
+                       "communityAverage": 5.8,
+                       "communityHigh": 21.04,
+                       "userAverage": 6.73
+               },
+               "avgRecuperation": {
+                       "communityLow": 1.27,
+                       "communityAverage": 5.29,
+                       "communityHigh": 19.77,
+                       "userAverage": 5.1
+               },
+               "chargecycleRange": {
+                       "communityAverage": 11.04,
+                       "communityHigh": 169.59,
+                       "userAverage": 23.74,
+                       "userHigh": 3.62,
+                       "userCurrentChargeCycle": 28.06
+               },
+               "totalElectricDistance": {
+                       "communityLow": 20.33,
+                       "communityAverage": 16575.07,
+                       "communityHigh": 63175.2,
+                       "userTotal": 20102.5
+               },
+               "avgCombinedConsumption": {
+                       "communityLow": 2.45,
+                       "communityAverage": 6.08,
+                       "communityHigh": 10.86,
+                       "userAverage": 4.93
+               },
+               "savedCO2": 557.382,
+               "savedCO2greenEnergy": 3278.717,
+               "totalSavedFuel": 1156.8,
+               "resetDate": "2020-09-19T12:54:20+0000",
+               "batterySizeMax": 9140
+       }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/530E/charge-profile.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/530E/charge-profile.json
new file mode 100644 (file)
index 0000000..22f4fb1
--- /dev/null
@@ -0,0 +1,42 @@
+{
+       "weeklyPlanner": {
+               "climatizationEnabled": true,
+               "chargingMode": "IMMEDIATE_CHARGING",
+               "chargingPreferences": "CHARGING_WINDOW",
+               "timer1": {
+                       "departureTime": "07:10",
+                       "timerEnabled": true,
+                       "weekdays": [
+                               "MONDAY",
+                               "TUESDAY",
+                               "WEDNESDAY",
+                               "THURSDAY",
+                               "FRIDAY"
+                       ]
+               },
+               "timer2": {
+                       "departureTime": "09:00",
+                       "timerEnabled": true,
+                       "weekdays": [
+                               "SATURDAY",
+                               "SUNDAY"
+                       ]
+               },
+               "timer3": {
+                       "departureTime": "00:00",
+                       "timerEnabled": false,
+                       "weekdays": []
+               },
+               "overrideTimer": {
+                       "departureTime": "09:00",
+                       "timerEnabled": false,
+                       "weekdays": [
+                               "SUNDAY"
+                       ]
+               },
+               "preferredChargingWindow": {
+                       "startTime": "00:00",
+                       "endTime": "00:00"
+               }
+       }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/530E/discovery.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/530E/discovery.json
new file mode 100644 (file)
index 0000000..10289f0
--- /dev/null
@@ -0,0 +1,46 @@
+{
+       "vehicles": [
+               {
+                       "vin": "ANONYMOUS",
+                       "model": "530e iPerformance",
+                       "driveTrain": "PHEV",
+                       "brand": "BMW",
+                       "yearOfConstruction": 2018,
+                       "bodytype": "G30",
+                       "color": "SCHWARZ  II",
+                       "statisticsCommunityEnabled": false,
+                       "statisticsAvailable": true,
+                       "hasAlarmSystem": true,
+                       "dealer": {
+                               "name": "ANONYMOUS",
+                               "street": "ANONYMOUS",
+                               "postalCode": "ANONYMOUS",
+                               "city": "ANONYMOUS",
+                               "country": "ANONYMOUS",
+                               "phone": "ANONYMOUS"
+                       },
+                       "breakdownNumber": "ANONYMOUS",
+                       "supportedChargingModes": [
+                               "AC_LOW"
+                       ],
+                       "chargingControl": "WEEKLY_PLANNER",
+                       "vehicleFinder": "ACTIVATED",
+                       "hornBlow": "ACTIVATED",
+                       "lightFlash": "ACTIVATED",
+                       "doorLock": "ACTIVATED",
+                       "doorUnlock": "ACTIVATED",
+                       "climateNow": "ACTIVATED",
+                       "sendPoi": "ACTIVATED",
+                       "remote360": "NOT_SUPPORTED",
+                       "climateControl": "NOT_SUPPORTED",
+                       "chargeNow": "NOT_SUPPORTED",
+                       "lastDestinations": "NOT_SUPPORTED",
+                       "carCloud": "ACTIVATED",
+                       "remoteSoftwareUpgrade": "NOT_SUPPORTED",
+                       "climateNowRES": "NOT_SUPPORTED",
+                       "climateControlRES": "NOT_SUPPORTED",
+                       "smartSolution": "NOT_SUPPORTED",
+                       "ipa": "NOT_SUPPORTED"
+               }
+       ]
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/530E/last-trip.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/530E/last-trip.json
new file mode 100644 (file)
index 0000000..aa592b8
--- /dev/null
@@ -0,0 +1,18 @@
+{
+       "lastTrip": {
+               "efficiencyValue": 1,
+               "totalDistance": 2,
+               "electricDistance": 2,
+               "avgElectricConsumption": 9,
+               "avgRecuperation": 9,
+               "drivingModeValue": 0.5,
+               "totalConsumptionValue": 1.25,
+               "avgCombinedConsumption": 0,
+        "electricDistanceRatio": 100,
+        "savedFuel": 1.53,
+        "date": "2020-09-19T16:03:00+0000",
+        "duration": 3,
+               "chargingBehaviorValue": 1.25,
+               "electricDistanceShareValue": 1.25
+       }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/530E/status.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/530E/status.json
new file mode 100644 (file)
index 0000000..2abfe73
--- /dev/null
@@ -0,0 +1,72 @@
+{
+       "vehicleStatus": {
+               "mileage": 47035,
+               "remainingFuel": 18.0,
+               "remainingRangeElectric": 3.0,
+               "remainingRangeElectricMls": 1.0,
+               "remainingRangeFuel": 249.0,
+               "remainingRangeFuelMls": 154.0,
+               "maxRangeElectric": 32.0,
+               "maxRangeElectricMls": 19.0,
+               "maxFuel": 0.0,
+               "chargingLevelHv": 17.0,
+               "vin": "ANONYMOUS",
+               "updateReason": "VEHICLE_SHUTDOWN_SECURED",
+               "updateTime": "2020-09-19T14:04:25+0000",
+               "doorDriverFront": "CLOSED",
+               "doorDriverRear": "CLOSED",
+               "doorPassengerFront": "CLOSED",
+               "doorPassengerRear": "CLOSED",
+               "windowDriverFront": "CLOSED",
+               "windowDriverRear": "CLOSED",
+               "windowPassengerFront": "CLOSED",
+               "windowPassengerRear": "CLOSED",
+               "trunk": "CLOSED",
+               "rearWindow": "INVALID",
+               "hood": "CLOSED",
+               "doorLockState": "SECURED",
+               "parkingLight": "OFF",
+               "positionLight": "OFF",
+               "connectionStatus": "DISCONNECTED",
+               "chargingStatus": "INVALID",
+               "lastChargingEndReason": "CHARGING_GOAL_REACHED",
+               "lastChargingEndResult": "SUCCESS",
+               "position": {
+                       "lat": -1.0,
+                       "lon": -1.0,
+                       "heading": -1,
+                       "status": "OK"
+               },
+               "internalDataTimeUTC": "2020-09-19T14:04:25",
+               "singleImmediateCharging": false,
+               "chargingConnectionType": "CONDUCTIVE",
+               "chargingInductivePositioning": "NOT_POSITIONED",
+               "vehicleCountry": "SE",
+               "DCS_CCH_Activation": "NA",
+               "DCS_CCH_Ongoing": false,
+               "checkControlMessages": [],
+               "cbsData": [
+                       {
+                               "cbsType": "OIL",
+                               "cbsState": "OK",
+                               "cbsDueDate": "2022-01",
+                               "cbsDescription": "Next service due when the stated distance has been covered or by the specified date.",
+                               "cbsRemainingMileage": 19000
+                       },
+                       {
+                               "cbsType": "VEHICLE_CHECK",
+                               "cbsState": "OK",
+                               "cbsDueDate": "2022-01",
+                               "cbsDescription": "Next visual inspection due when the stated distance has been covered or by the stated date.",
+                               "cbsRemainingMileage": 19000
+                       },
+                       {
+                               "cbsType": "BRAKE_FLUID",
+                               "cbsState": "OK",
+                               "cbsDueDate": "2021-11",
+                               "cbsDescription": "Next change due at the latest by the stated date.",
+                               "cbsRemainingMileage": 0
+                       }
+               ]
+       }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/F11/vehicle-status.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/F11/vehicle-status.json
new file mode 100644 (file)
index 0000000..c3b787d
--- /dev/null
@@ -0,0 +1,46 @@
+{
+       "attributesMap": {
+               "remaining_fuel": "34",
+               "unitOfCombustionConsumption": "l/100km",
+               "unitOfLength": "km",
+               "vehicle_tracking": "0",
+               "unitOfEnergy": "kWh",
+               "head_unit_pu_software": "07/14",
+               "condition_based_services": "00001,PENDING,2021-08,2000;00100,OK,2023-08,23000;00003,OK,2021-08,",
+               "head_unit": "NBT",
+               "unitOfElectricConsumption": "kWh/100km",
+               "lastUpdateReason": "Error",
+               "mileage": "113930"
+       },
+       "vehicleMessages": {
+               "ccmMessages": [],
+               "cbsMessages": [
+                       {
+                               "description": "Onderhoud binnenkort nodig. Maak a.u.b. een afspraak met uw Servicepartner.",
+                               "text": "Motorolie",
+                               "id": 1,
+                               "status": "PENDING",
+                               "messageType": "CBS",
+                               "date": "2021-08",
+                               "unitOfLengthRemaining": "2000"
+                       },
+                       {
+                               "description": "Volgende optische controle uiterlijk na afloop van de aangegeven afstand of uiterlijk op het aangegeven tijdstip",
+                               "text": "Voertuig check",
+                               "id": 100,
+                               "status": "OK",
+                               "messageType": "CBS",
+                               "date": "2023-08",
+                               "unitOfLengthRemaining": "23000"
+                       },
+                       {
+                               "description": "Volgende vervanging uiterlijk op het aangegeven tijdstip.",
+                               "text": "Remvloeistof",
+                               "id": 3,
+                               "status": "OK",
+                               "messageType": "CBS",
+                               "date": "2021-08"
+                       }
+               ]
+       }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/F15/status.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/F15/status.json
new file mode 100644 (file)
index 0000000..801f415
--- /dev/null
@@ -0,0 +1,66 @@
+{
+  "vehicleStatus" : {
+    "doorPassengerFront" : "CLOSED",
+    "cbsData" : [ {
+      "cbsState" : "OK",
+      "cbsRemainingMileage" : 47000,
+      "cbsDescription" : "Next change due at the latest when the stated distance has been covered.",
+      "cbsType" : "BRAKE_PADS_FRONT"
+    }, {
+      "cbsState" : "OK",
+      "cbsRemainingMileage" : 46000,
+      "cbsDescription" : "Next change due at the latest when the stated distance has been covered.",
+      "cbsType" : "BRAKE_PADS_REAR"
+    }, {
+      "cbsDueDate" : "2018-06",
+      "cbsState" : "OK",
+      "cbsRemainingMileage" : 12000,
+      "cbsDescription" : "Next service due when the stated distance has been covered or by the specified date.",
+      "cbsType" : "OIL"
+    }, {
+      "cbsDueDate" : "2021-06",
+      "cbsState" : "OK",
+      "cbsRemainingMileage" : 50000,
+      "cbsDescription" : "Next visual inspection due when the stated distance has been covered or by the stated date.",
+      "cbsType" : "VEHICLE_CHECK"
+    }, {
+      "cbsDueDate" : "2020-04",
+      "cbsState" : "OK",
+      "cbsDescription" : "Next change due at the latest by the stated date.",
+      "cbsType" : "BRAKE_FLUID"
+    } ],
+    "windowDriverFront" : "CLOSED",
+    "DCS_CCH_Ongoing" : false,
+    "updateReason" : "VEHICLE_SHUTDOWN",
+    "rearWindow" : "INVALID",
+    "remainingRangeFuelMls" : 154,
+    "DCS_CCH_Activation" : "NA",
+    "singleImmediateCharging" : false,
+    "positionLight" : "OFF",
+    "vin" : "F15_VIN",
+    "doorDriverFront" : "CLOSED",
+    "mileage" : 1629,
+    "checkControlMessages" : [ ],
+    "parkingLight" : "OFF",
+    "windowDriverRear" : "CLOSED",
+    "steering" : "LH",
+    "updateTime" : "2018-03-08T22:21:39-0500",
+    "remainingFuel" : 30,
+    "windowPassengerRear" : "CLOSED",
+    "trunk" : "CLOSED",
+    "hood" : "CLOSED",
+    "internalDataTimeUTC" : "2018-03-09T03:21:39",
+    "windowPassengerFront" : "CLOSED",
+    "doorLockState" : "UNLOCKED",
+    "doorPassengerRear" : "CLOSED",
+    "remainingRangeFuel" : 249,
+    "doorDriverRear" : "CLOSED",
+    "sunroof" : "CLOSED",
+    "position" : {
+      "heading" : 11,
+      "lon" : -23.123,
+      "lat" : 23.123,
+      "status" : "OK"
+    }
+  }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/F31/status-318i.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/F31/status-318i.json
new file mode 100644 (file)
index 0000000..211b8f9
--- /dev/null
@@ -0,0 +1,67 @@
+{
+    "vehicleStatus": {
+        "mileage": 26112,
+        "remainingFuel": 55.0,
+        "remainingRangeElectric": 0.0,
+        "remainingRangeElectricMls": 0.0,
+        "remainingRangeFuel": 879.0,
+        "remainingRangeFuelMls": 546.0,
+        "maxRangeElectric": 0.0,
+        "maxRangeElectricMls": 0.0,
+        "maxFuel": 0.0,
+        "chargingLevelHv": 0.0,
+        "vin": "ANONYMOUS",
+        "updateReason": "DOOR_STATE_CHANGED",
+        "updateTime": "2020-09-28T13:17:20+0000",
+        "doorDriverFront": "CLOSED",
+        "doorDriverRear": "CLOSED",
+        "doorPassengerFront": "CLOSED",
+        "doorPassengerRear": "CLOSED",
+        "windowDriverFront": "CLOSED",
+        "windowDriverRear": "CLOSED",
+        "windowPassengerFront": "CLOSED",
+        "windowPassengerRear": "CLOSED",
+        "sunroof": "UNKOWN",
+        "trunk": "CLOSED",
+        "rearWindow": "CLOSED",
+        "hood": "CLOSED",
+        "doorLockState": "SECURED",
+        "parkingLight": "OFF",
+        "positionLight": "OFF",
+        "position": {
+            "lat": -1.0,
+            "lon": -1.0,
+            "heading": -1,
+            "status": "OK"
+        },
+        "internalDataTimeUTC": "2020-09-28T13:16:36",
+        "singleImmediateCharging": false,
+        "vehicleCountry": "NL",
+        "DCS_CCH_Activation": "NA",
+        "DCS_CCH_Ongoing": false,
+        "checkControlMessages": [],
+        "cbsData": [
+            {
+                "cbsType": "OIL",
+                "cbsState": "OK",
+                "cbsDueDate": "2022-07",
+                "cbsDescription": "Next service due when the stated distance has been covered or by the specified date.",
+                "cbsRemainingMileage": 28000
+            },
+            {
+                "cbsType": "VEHICLE_CHECK",
+                "cbsState": "OK",
+                "cbsDueDate": "2022-07",
+                "cbsDescription": "Next visual inspection due when the stated distance has been covered or by the stated date.",
+                "cbsRemainingMileage": 28000
+            },
+            {
+                "cbsType": "BRAKE_FLUID",
+                "cbsState": "OK",
+                "cbsDueDate": "2022-01",
+                "cbsDescription": "Next change due at the latest by the stated date.",
+                "cbsRemainingMileage": -1
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/F31/status.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/F31/status.json
new file mode 100644 (file)
index 0000000..1f6b6d5
--- /dev/null
@@ -0,0 +1,14 @@
+{
+  "DCS_CCH_Activation": "NA",  
+  "DCS_CCH_Ongoing": false,
+  "vehicleStatus": {
+      "position": {
+          "lat": 12.3456,
+          "lon": 34.5678,
+          "status": "OK"
+      },
+      "steering": "RH",
+      "updateTime": "2018-07-25T16:02:04+0200",
+      "vin": "F31_VIN"
+  }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/F35/status.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/F35/status.json
new file mode 100644 (file)
index 0000000..2dd6a25
--- /dev/null
@@ -0,0 +1,12 @@
+{
+  "vehicleStatus": {
+    "DCS_CCH_Activation": "NA",
+    "DCS_CCH_Ongoing": false,
+    "position": {
+      "status": "DRIVER_DISABLED"
+    },
+    "updateTime": "2018-07-16T21:47:46+0000",
+    "vehicleCountry": "CN",
+    "vin": "some_vin"
+  }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/F45/status.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/F45/status.json
new file mode 100644 (file)
index 0000000..3328f50
--- /dev/null
@@ -0,0 +1,12 @@
+{
+  "vehicleStatus": {
+    "position": {
+      "lat": 12.3456,
+      "lon": 34.5678,
+      "status": "OK"
+    },
+    "steering": "LH",
+    "updateTime": "2018-04-06T13:56:47+0200",
+    "vin": "F45_vin"
+  }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/F48/status.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/F48/status.json
new file mode 100644 (file)
index 0000000..df3772a
--- /dev/null
@@ -0,0 +1,62 @@
+{
+  "vehicleStatus" : {
+    "doorPassengerFront" : "CLOSED",
+    "cbsData" : [ {
+      "cbsDueDate" : "2019-07",
+      "cbsState" : "OK",
+      "cbsRemainingMileage" : 9000,
+      "cbsDescription" : "Next service due when the stated distance has been covered or by the specified date.",
+      "cbsType" : "OIL"
+    }, {
+      "cbsDueDate" : "2021-07",
+      "cbsState" : "OK",
+      "cbsRemainingMileage" : 39000,
+      "cbsDescription" : "Next visual inspection due when the stated distance has been covered or by the stated date.",
+      "cbsType" : "VEHICLE_CHECK"
+    }, {
+      "cbsDueDate" : "2020-07",
+      "cbsState" : "OK",
+      "cbsDescription" : "Next change due at the latest by the stated date.",
+      "cbsType" : "BRAKE_FLUID"
+    } ],
+    "windowDriverFront" : "CLOSED",
+    "DCS_CCH_Ongoing" : false,
+    "updateReason" : "DOOR_STATE_CHANGED",
+    "rearWindow" : "INVALID",
+    "remainingRangeFuelMls" : 366,
+    "DCS_CCH_Activation" : "NA",
+    "singleImmediateCharging" : false,
+    "positionLight" : "OFF",
+    "vin" : "F48_VIN",
+    "doorDriverFront" : "CLOSED",
+    "mileage" : 21529,
+    "checkControlMessages": [
+      {
+        "ccmDescriptionLong": "You can continue driving. Check tyre pressure when tyres are cold and adjust if necessary. Perform reset after adjustment. See Owner's Handbook for further information.",
+        "ccmDescriptionShort": "Tyre pressure notification",
+        "ccmId": 955,
+        "ccmMileage": 41544
+      }
+    ],
+    "parkingLight" : "OFF",
+    "windowDriverRear" : "CLOSED",
+    "steering" : "LH",
+    "updateTime" : "2018-03-10T19:35:30+0100",
+    "remainingFuel" : 39,
+    "windowPassengerRear" : "CLOSED",
+    "trunk" : "CLOSED",
+    "hood" : "CLOSED",
+    "internalDataTimeUTC" : "2018-03-10T18:35:30",
+    "windowPassengerFront" : "CLOSED",
+    "doorLockState" : "SECURED",
+    "doorPassengerRear" : "CLOSED",
+    "remainingRangeFuel" : 590,
+    "doorDriverRear" : "CLOSED",
+    "position" : {
+      "heading" : 141,
+      "lon" : 10.1010101,
+      "lat" : 50.505050,
+      "status" : "OK"
+    }
+  }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/G31_NBTevo/auth_response.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/G31_NBTevo/auth_response.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.bmwconnecteddrive/src/test/resources/responses/G31_NBTevo/flash_delivered.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/G31_NBTevo/flash_delivered.json
new file mode 100644 (file)
index 0000000..f341d84
--- /dev/null
@@ -0,0 +1,10 @@
+{
+  "executionStatus" : {
+    "serviceType" : "LIGHT_FLASH",
+    "eventId" : "424C39333232312237B3E900@bmw.de",
+    "extendedStatus" : {
+      "result" : "STATUS_UNKNOWN"
+    },
+    "status" : "DELIVERED"
+  }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/G31_NBTevo/flash_executed.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/G31_NBTevo/flash_executed.json
new file mode 100644 (file)
index 0000000..88d379a
--- /dev/null
@@ -0,0 +1,11 @@
+{
+  "executionStatus" : {
+    "serviceType" : "LIGHT_FLASH",
+    "eventId" : "424C39333232312237B3E900@bmw.de",
+    "extendedStatus" : {
+      "result" : "STATUS_CHANGED",
+      "ignitionOnStatus" : "false"
+    },
+    "status" : "EXECUTED"
+  }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/G31_NBTevo/flash_initiated.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/G31_NBTevo/flash_initiated.json
new file mode 100644 (file)
index 0000000..392713e
--- /dev/null
@@ -0,0 +1,7 @@
+{
+  "executionStatus" : {
+    "serviceType" : "LIGHT_FLASH",
+    "eventId" : "424C39333232312237B3E900@bmw.de",
+    "status" : "INITIATED"
+  }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/G31_NBTevo/flash_pending.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/G31_NBTevo/flash_pending.json
new file mode 100644 (file)
index 0000000..c338477
--- /dev/null
@@ -0,0 +1,10 @@
+{
+  "executionStatus" : {
+    "serviceType" : "LIGHT_FLASH",
+    "eventId" : "424C39333232312237B3E900@bmw.de",
+    "extendedStatus" : {
+      "result" : "STATUS_UNKNOWN"
+    },
+    "status" : "PENDING"
+  }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/G31_NBTevo/status.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/G31_NBTevo/status.json
new file mode 100644 (file)
index 0000000..fc284ff
--- /dev/null
@@ -0,0 +1,55 @@
+{
+  "vehicleStatus" : {
+    "doorPassengerFront" : "CLOSED",
+    "cbsData" : [ {
+      "cbsDueDate" : "2020-01",
+      "cbsState" : "OK",
+      "cbsRemainingMileage" : 25000,
+      "cbsDescription" : "Next service due when the stated distance has been covered or by the specified date.",
+      "cbsType" : "OIL"
+    }, {
+      "cbsDueDate" : "2022-01",
+      "cbsState" : "OK",
+      "cbsRemainingMileage" : 60000,
+      "cbsDescription" : "Next visual inspection due when the stated distance has been covered or by the stated date.",
+      "cbsType" : "VEHICLE_CHECK"
+    }, {
+      "cbsDueDate" : "2021-01",
+      "cbsState" : "OK",
+      "cbsDescription" : "Next change due at the latest by the stated date.",
+      "cbsType" : "BRAKE_FLUID"
+    } ],
+    "windowDriverFront" : "CLOSED",
+    "DCS_CCH_Ongoing" : false,
+    "updateReason" : "VEHICLE_SHUTDOWN",
+    "rearWindow" : "CLOSED",
+    "remainingRangeFuelMls" : 199,
+    "DCS_CCH_Activation" : "NA",
+    "singleImmediateCharging" : false,
+    "positionLight" : "OFF",
+    "vin" : "G31_NBTevo_VIN",
+    "doorDriverFront" : "CLOSED",
+    "mileage" : 4126,
+    "checkControlMessages" : [ ],
+    "parkingLight" : "OFF",
+    "windowDriverRear" : "CLOSED",
+    "steering" : "LH",
+    "updateTime" : "2018-03-10T11:39:41+0100",
+    "remainingFuel" : 33,
+    "windowPassengerRear" : "CLOSED",
+    "trunk" : "CLOSED",
+    "hood" : "CLOSED",
+    "internalDataTimeUTC" : "2018-03-10T10:39:41",
+    "windowPassengerFront" : "CLOSED",
+    "doorLockState" : "SECURED",
+    "doorPassengerRear" : "CLOSED",
+    "remainingRangeFuel" : 321,
+    "doorDriverRear" : "CLOSED",
+    "position" : {
+      "heading" : 174,
+      "lon" : 10.1010,
+      "lat" : 50.5050,
+      "status" : "OK"
+    }
+  }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/G31_NBTevo/status_position_disabled.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/G31_NBTevo/status_position_disabled.json
new file mode 100644 (file)
index 0000000..94b2570
--- /dev/null
@@ -0,0 +1,52 @@
+{
+  "vehicleStatus" : {
+    "doorPassengerFront" : "CLOSED",
+    "cbsData" : [ {
+      "cbsDueDate" : "2020-01",
+      "cbsState" : "OK",
+      "cbsRemainingMileage" : 25000,
+      "cbsDescription" : "Next service due when the stated distance has been covered or by the specified date.",
+      "cbsType" : "OIL"
+    }, {
+      "cbsDueDate" : "2022-01",
+      "cbsState" : "OK",
+      "cbsRemainingMileage" : 60000,
+      "cbsDescription" : "Next visual inspection due when the stated distance has been covered or by the stated date.",
+      "cbsType" : "VEHICLE_CHECK"
+    }, {
+      "cbsDueDate" : "2021-01",
+      "cbsState" : "OK",
+      "cbsDescription" : "Next change due at the latest by the stated date.",
+      "cbsType" : "BRAKE_FLUID"
+    } ],
+    "windowDriverFront" : "CLOSED",
+    "DCS_CCH_Ongoing" : false,
+    "updateReason" : "VEHICLE_SHUTDOWN_SECURED",
+    "rearWindow" : "CLOSED",
+    "remainingRangeFuelMls" : 187,
+    "DCS_CCH_Activation" : "NA",
+    "singleImmediateCharging" : false,
+    "positionLight" : "OFF",
+    "vin" : "G31_VIN",
+    "doorDriverFront" : "CLOSED",
+    "mileage" : 4134,
+    "checkControlMessages" : [ ],
+    "parkingLight" : "OFF",
+    "windowDriverRear" : "CLOSED",
+    "steering" : "LH",
+    "updateTime" : "2018-03-12T08:56:16+0100",
+    "remainingFuel" : 32,
+    "windowPassengerRear" : "CLOSED",
+    "trunk" : "CLOSED",
+    "hood" : "CLOSED",
+    "internalDataTimeUTC" : "2018-03-12T07:56:16",
+    "windowPassengerFront" : "CLOSED",
+    "doorLockState" : "SECURED",
+    "doorPassengerRear" : "CLOSED",
+    "remainingRangeFuel" : 302,
+    "doorDriverRear" : "CLOSED",
+    "position" : {
+      "status" : "DRIVER_DISABLED"
+    }
+  }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/I01_NOREX/status.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/I01_NOREX/status.json
new file mode 100644 (file)
index 0000000..55b1760
--- /dev/null
@@ -0,0 +1,61 @@
+{
+  "vehicleStatus": {
+    "doorPassengerFront": "CLOSED",
+    "cbsData": [
+      {
+        "cbsDueDate": "2018-11",
+        "cbsState": "OK",
+        "cbsDescription": "Next change due at the latest by the stated date.",
+        "cbsType": "BRAKE_FLUID"
+      },
+      {
+        "cbsDueDate": "2018-11",
+        "cbsState": "OK",
+        "cbsDescription": "Next visual inspection due when the stated distance has been covered or by the stated date.",
+        "cbsType": "VEHICLE_CHECK"
+      }
+    ],
+    "windowDriverFront": "CLOSED",
+    "DCS_CCH_Ongoing": false,
+    "updateReason": "VEHICLE_SHUTDOWN_SECURED",
+    "rearWindow": "INVALID",
+    "DCS_CCH_Activation": "NA",
+    "chargingInductivePositioning": "NOT_POSITIONED",
+    "remainingRangeElectric": 53,
+    "singleImmediateCharging": false,
+    "maxRangeElectric": 103,
+    "chargingConnectionType": "CONDUCTIVE",
+    "positionLight": "OFF",
+    "lastChargingEndReason": "END_REQUESTED_BY_DRIVER",
+    "chargingStatus": "INVALID",
+    "vin": "I01_NOREX_VIN",
+    "doorDriverFront": "CLOSED",
+    "mileage": 16059,
+    "checkControlMessages": [],
+    "parkingLight": "OFF",
+    "windowDriverRear": "CLOSED",
+    "remainingRangeElectricMls": 32,
+    "lastChargingEndResult": "SUCCESS",
+    "steering": "RH",
+    "updateTime": "2018-03-15T15:44:30+0100",
+    "remainingFuel": 0,
+    "windowPassengerRear": "CLOSED",
+    "trunk": "CLOSED",
+    "hood": "CLOSED",
+    "internalDataTimeUTC": "2018-03-15T14:44:30",
+    "maxRangeElectricMls": 64,
+    "windowPassengerFront": "CLOSED",
+    "doorLockState": "SECURED",
+    "doorPassengerRear": "CLOSED",
+    "doorDriverRear": "CLOSED",
+    "sunroof": "CLOSED",
+    "connectionStatus": "DISCONNECTED",
+    "chargingLevelHv": 58,
+    "position": {
+      "heading": 123,
+      "lon": 12.3456,
+      "lat": -65.4321,
+      "status": "OK"
+    }
+  }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/I01_REX/all-trips.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/I01_REX/all-trips.json
new file mode 100644 (file)
index 0000000..3fe434e
--- /dev/null
@@ -0,0 +1,40 @@
+{
+       "allTrips": {
+               "avgElectricConsumption": {
+                       "communityLow": 11.05,
+                       "communityAverage": 16.28,
+                       "communityHigh": 21.99,
+                       "userAverage": 16.46
+               },
+               "avgRecuperation": {
+                       "communityLow": 0.47,
+                       "communityAverage": 3.37,
+                       "communityHigh": 11.51,
+                       "userAverage": 4.53
+               },
+               "chargecycleRange": {
+                       "communityAverage": 194.21,
+                       "communityHigh": 270,
+                       "userAverage": 57.3,
+                       "userHigh": 185.48,
+                       "userCurrentChargeCycle": 68
+               },
+               "totalElectricDistance": {
+                       "communityLow": 19,
+                       "communityAverage": 40850.56,
+                       "communityHigh": 193006,
+                       "userTotal": 16629.4
+               },
+               "avgCombinedConsumption": {
+                       "communityLow": 0,
+                       "communityAverage": 0.92,
+                       "communityHigh": 4.44,
+                       "userAverage": 0.64
+               },
+               "savedCO2": 461.083,
+               "savedCO2greenEnergy": 2712.255,
+               "totalSavedFuel": 0,
+               "resetDate": "2020-08-24T14:40:40+0000",
+               "batterySizeMax": 33200
+       }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/I01_REX/charge-profile.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/I01_REX/charge-profile.json
new file mode 100644 (file)
index 0000000..840559b
--- /dev/null
@@ -0,0 +1,41 @@
+{
+       "weeklyPlanner": {
+               "climatizationEnabled": true,
+               "chargingMode": "IMMEDIATE_CHARGING",
+               "chargingPreferences": "CHARGING_WINDOW",
+               "timer1": {
+                       "departureTime": "05:00",
+                       "timerEnabled": false,
+                       "weekdays": [
+                               "MONDAY",
+                               "TUESDAY",
+                               "WEDNESDAY",
+                               "THURSDAY",
+                               "FRIDAY"
+                       ]
+               },
+               "timer2": {
+                       "departureTime": "12:00",
+                       "timerEnabled": true,
+                       "weekdays": [
+                               "SATURDAY"
+                       ]
+               },
+               "timer3": {
+                       "departureTime": "00:00",
+                       "timerEnabled": false,
+                       "weekdays": []
+               },
+               "overrideTimer": {
+                       "departureTime": "12:00",
+                       "timerEnabled": false,
+                       "weekdays": [
+                               "SATURDAY"
+                       ]
+               },
+               "preferredChargingWindow": {
+                       "startTime": "11:00",
+                       "endTime": "17:00"
+               }
+       }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/I01_REX/discovery.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/I01_REX/discovery.json
new file mode 100644 (file)
index 0000000..73b5946
--- /dev/null
@@ -0,0 +1,63 @@
+{
+       "vehicles": [
+               {
+                       "vin": "MY_REAL_VIN",
+                       "model": "i3 94 (+ REX)",
+                       "bodytype": "I01",
+                       "driveTrain": "BEV_REX",
+                       "color": "CAPPARISWEISS MIT AKZENT BMW I BLAU",
+                       "colorCode": "B85",
+                       "brand": "BMW_I",
+                       "yearOfConstruction": 2017,
+                       "statisticsCommunityEnabled": false,
+                       "statisticsAvailable": true,
+                       "hub": "HUB_ECE",
+                       "hasAlarmSystem": true,
+                       "dealer": {
+                               "name": "My Real Dealer Name",
+                               "street": "My Real Dealer Address",
+                               "postalCode": "4711",
+                               "city": "My Real Dealer City",
+                               "country": "DE",
+                               "phone": "My Real Dealer Phone"
+                       },
+                       "breakdownNumber": "Real Phone Number",
+                       "countryCode": "V6",
+                       "egoVehiclePath": "",
+                       "chargingUpdateMode": "NORMAL_PROGNOSE_BASED",
+                       "steering": "LH",
+                       "vehicleFinderRestriction": "NONE",
+                       "hmiVersion": "ID4",
+                       "a4a": "USB_ONLY",
+                       "vehicleFinder": "ACTIVATED",
+                       "remote360": "NOT_SUPPORTED",
+                       "hornBlow": "ACTIVATED",
+                       "lightFlash": "ACTIVATED",
+                       "doorLock": "ACTIVATED",
+                       "doorUnlock": "ACTIVATED",
+                       "climateControl": "NOT_SUPPORTED",
+                       "climateNow": "ACTIVATED",
+                       "climateNowRES": "NOT_SUPPORTED",
+                       "climateControlRES": "NOT_SUPPORTED",
+                       "chargingControl": "WEEKLY_PLANNER",
+                       "chargeNow": "NOT_SUPPORTED",
+                       "sendPoi": "ACTIVATED",
+                       "rangeMap": "RANGE_CIRCLE",
+                       "lastDestinations": "SUPPORTED",
+                       "intermodalRouting": "NOT_AVAILABLE",
+                       "climateFunction": "AIRCONDITIONING",
+                       "onlineSearchMode": "MAP",
+                       "onlineSearchProvider": "GOOGLE",
+                       "smartSolution": "NOT_SUPPORTED",
+                       "carCloud": "NOT_SUPPORTED",
+                       "supportedChargingModes": [
+                               "AC_LOW",
+                               "DC"
+                       ],
+                       "lscType": "I_LSC_IMM",
+                       "ipa": "NOT_SUPPORTED",
+                       "puStep": "1119",
+                       "remoteSoftwareUpgrade": "NOT_SUPPORTED"
+               }
+       ]
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/I01_REX/last-trip.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/I01_REX/last-trip.json
new file mode 100644 (file)
index 0000000..c129bf3
--- /dev/null
@@ -0,0 +1,19 @@
+{
+       "lastTrip": {
+               "efficiencyValue": 0.98,
+               "totalDistance": 2,
+               "electricDistance": 2,
+               "avgElectricConsumption": 7,
+               "avgRecuperation": 6,
+               "drivingModeValue": 0.87,
+               "totalConsumptionValue": 1.25,
+               "avgCombinedConsumption": 0,
+               "electricDistanceRatio": 100,
+               "savedFuel": 0,
+               "date": "2020-08-24T17:55:00+0000",
+               "duration": 5,
+        "auxiliaryConsumptionValue": 0.78,
+        "anticipationValue": 0.99,
+        "accelerationValue": 0.99
+       }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/I01_REX/status.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/I01_REX/status.json
new file mode 100644 (file)
index 0000000..5b2415b
--- /dev/null
@@ -0,0 +1,61 @@
+{
+  "vehicleStatus" : {
+    "doorPassengerFront" : "CLOSED",
+    "cbsData" : [ {
+      "cbsDueDate" : "2018-05",
+      "cbsState" : "OK",
+      "cbsDescription" : "Next change due at the latest by the stated date.",
+      "cbsType" : "BRAKE_FLUID"
+    }, {
+      "cbsDueDate" : "2018-05",
+      "cbsState" : "OK",
+      "cbsDescription" : "Next visual inspection due when the stated distance has been covered or by the stated date.",
+      "cbsType" : "VEHICLE_CHECK"
+    } ],
+    "windowDriverFront" : "CLOSED",
+    "DCS_CCH_Ongoing" : false,
+    "updateReason" : "VEHICLE_SHUTDOWN_SECURED",
+    "rearWindow" : "INVALID",
+    "remainingRangeFuelMls" : 65,
+    "DCS_CCH_Activation" : "NA",
+    "chargingInductivePositioning" : "NOT_POSITIONED",
+    "remainingRangeElectric" : 48,
+    "singleImmediateCharging" : false,
+    "maxRangeElectric" : 94,
+    "chargingConnectionType" : "CONDUCTIVE",
+    "positionLight" : "OFF",
+    "lastChargingEndReason" : "UNKNOWN",
+    "chargingStatus" : "CHARGING",
+    "vin" : "I01_VIN",
+    "doorDriverFront" : "CLOSED",
+    "mileage" : 38807,
+    "chargingTimeRemaining" : 332,
+    "checkControlMessages" : [ ],
+    "parkingLight" : "OFF",
+    "windowDriverRear" : "CLOSED",
+    "remainingRangeElectricMls" : 30,
+    "lastChargingEndResult" : "UNKNOWN",
+    "steering" : "LH",
+    "updateTime" : "2018-03-12T08:38:57+0100",
+    "remainingFuel" : 8,
+    "windowPassengerRear" : "CLOSED",
+    "trunk" : "CLOSED",
+    "hood" : "CLOSED",
+    "internalDataTimeUTC" : "2018-03-12T06:21:01",
+    "maxRangeElectricMls" : 58,
+    "maxFuel" : 8,
+    "windowPassengerFront" : "CLOSED",
+    "doorLockState" : "SECURED",
+    "doorPassengerRear" : "CLOSED",
+    "remainingRangeFuel" : 106,
+    "doorDriverRear" : "CLOSED",
+    "connectionStatus" : "CONNECTED",
+    "chargingLevelHv" : 54,
+    "position" : {
+      "heading" : 356,
+      "lon" : 15.00000,
+      "lat" : 58.000000,
+      "status" : "OK"
+    }
+  }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/vehicles.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/vehicles.json
new file mode 100644 (file)
index 0000000..3599d59
--- /dev/null
@@ -0,0 +1,450 @@
+{
+       "vehicles": [
+               {
+                       "vehicleFinderRestriction": "NONE",
+                       "doorUnlock": "ACTIVATED",
+                       "color": "SOPHISTOGRAU BRILLANTEFFEKT METALLI",
+                       "onlineSearchMode": "MAP",
+                       "breakdownNumber": "+4912345678",
+                       "smartSolution": "NOT_SUPPORTED",
+                       "yearOfConstruction": 2018,
+                       "driveTrain": "CONV",
+                       "rangeMap": "NOT_SUPPORTED",
+                       "vehicleFinder": "ACTIVATED",
+                       "a4a": "BLUETOOTH",
+                       "hornBlow": "ACTIVATED",
+                       "chargeNow": "NOT_SUPPORTED",
+                       "countryCode": "V6",
+                       "bodytype": "G31",
+                       "lightFlash": "ACTIVATED",
+                       "vin": "G31_NBTevo_VIN",
+                       "model": "530i xDrive",
+                       "ipa": "NOT_SUPPORTED",
+                       "brand": "BMW",
+                       "climateControl": "DEPARTURE_TIMER",
+                       "sendPoi": "ACTIVATED",
+                       "lastDestinations": "NOT_SUPPORTED",
+                       "doorLock": "ACTIVATED",
+                       "chargingControl": "NOT_SUPPORTED",
+                       "onlineSearchProvider": "GOOGLE",
+                       "steering": "LH",
+                       "intermodalRouting": "NOT_AVAILABLE",
+                       "lscType": "LSC_BASIS",
+                       "hasAlarmSystem": true,
+                       "statisticsCommunityEnabled": false,
+                       "climateNow": "ACTIVATED",
+                       "fuelType": "PETROL",
+                       "hub": "HUB_ECE",
+                       "remote360": "NOT_SUPPORTED",
+                       "hmiVersion": "ID5",
+                       "climateFunction": "PARK_HEATING",
+                       "colorCode": "A90",
+                       "carCloud": "ACTIVATED",
+                       "statisticsAvailable": false
+               },
+               {
+                       "vehicleFinderRestriction": "NONE",
+                       "doorUnlock": "ACTIVATED",
+                       "color": "ALPINWEISS  III",
+                       "onlineSearchMode": "MAP",
+                       "breakdownNumber": "+4989358111111",
+                       "smartSolution": "NOT_SUPPORTED",
+                       "yearOfConstruction": 2017,
+                       "driveTrain": "CONV",
+                       "rangeMap": "NOT_SUPPORTED",
+                       "vehicleFinder": "ACTIVATED",
+                       "a4a": "BLUETOOTH",
+                       "hornBlow": "ACTIVATED",
+                       "chargeNow": "NOT_SUPPORTED",
+                       "countryCode": "V1-NL",
+                       "bodytype": "F48",
+                       "lightFlash": "ACTIVATED",
+                       "vin": "F48_VIN",
+                       "model": "X1 sDrive18i",
+                       "ipa": "NOT_SUPPORTED",
+                       "brand": "BMW",
+                       "climateControl": "START_TIMER",
+                       "sendPoi": "ACTIVATED",
+                       "lastDestinations": "NOT_SUPPORTED",
+                       "doorLock": "ACTIVATED",
+                       "chargingControl": "NOT_SUPPORTED",
+                       "onlineSearchProvider": "GOOGLE",
+                       "steering": "LH",
+                       "intermodalRouting": "NOT_AVAILABLE",
+                       "lscType": "LSC_BASIS",
+                       "hasAlarmSystem": true,
+                       "statisticsCommunityEnabled": false,
+                       "climateNow": "ACTIVATED",
+                       "fuelType": "PETROL",
+                       "hub": "HUB_ECE",
+                       "remote360": "NOT_SUPPORTED",
+                       "hmiVersion": "ID5",
+                       "dealer": {
+                               "country": "NL",
+                               "city": "city",
+                               "phone": "phone",
+                               "street": "street",
+                               "postalCode": "1234 AB",
+                               "name": "BMW dealer"
+                       },
+                       "climateFunction": "VENTILATION",
+                       "colorCode": "300",
+                       "carCloud": "ACTIVATED",
+                       "statisticsAvailable": false
+               },
+               {
+                       "vehicleFinderRestriction": "NONE",
+                       "doorUnlock": "ACTIVATED",
+                       "color": "ARRAVANIGRAU/BMW i BLAU",
+                       "onlineSearchMode": "MAP",
+                       "breakdownNumber": "+4989358957103",
+                       "smartSolution": "NOT_SUPPORTED",
+                       "yearOfConstruction": 2014,
+                       "driveTrain": "BEV_REX",
+                       "rangeMap": "RANGE_CIRCLE",
+                       "vehicleFinder": "ACTIVATED",
+                       "a4a": "USB_ONLY",
+                       "hornBlow": "ACTIVATED",
+                       "chargeNow": "NOT_SUPPORTED",
+                       "licensePlate": "ABC000",
+                       "countryCode": "V1-R1-SE",
+                       "bodytype": "I01",
+                       "lightFlash": "ACTIVATED",
+                       "vin": "I01_VIN",
+                       "model": "i3 (+ REX)",
+                       "ipa": "NOT_SUPPORTED",
+                       "brand": "BMW_I",
+                       "climateControl": "NOT_SUPPORTED",
+                       "sendPoi": "ACTIVATED",
+                       "lastDestinations": "SUPPORTED",
+                       "doorLock": "ACTIVATED",
+                       "chargingControl": "WEEKLY_PLANNER",
+                       "onlineSearchProvider": "GOOGLE",
+                       "steering": "LH",
+                       "intermodalRouting": "NOT_AVAILABLE",
+                       "supportedChargingModes": [
+                               "AC_LOW",
+                               "AC_HIGH",
+                               "DC"
+                       ],
+                       "lscType": "I_LSC_IMM",
+                       "hasAlarmSystem": false,
+                       "statisticsCommunityEnabled": false,
+                       "climateNow": "ACTIVATED",
+                       "hub": "HUB_ECE",
+                       "remote360": "NOT_SUPPORTED",
+                       "hmiVersion": "ID4",
+                       "dealer": {
+                               "country": "SE",
+                               "city": "Solna",
+                               "phone": "+46 8 7353900",
+                               "street": "G�rdsv�gen 9-11",
+                               "postalCode": "169 70",
+                               "name": "Bavaria Sverige Bil AB/ Solna"
+                       },
+                       "climateFunction": "AIRCONDITIONING",
+                       "colorCode": "B74",
+                       "carCloud": "NOT_SUPPORTED",
+                       "statisticsAvailable": true
+               },
+               {
+                       "vehicleFinderRestriction": "NONE",
+                       "doorUnlock": "NOT_SUPPORTED",
+                       "color": "BLACK SAPPHIRE METALLIC",
+                       "onlineSearchMode": "MAP",
+                       "breakdownNumber": "+4989358957103",
+                       "smartSolution": "NOT_SUPPORTED",
+                       "yearOfConstruction": 2017,
+                       "driveTrain": "CONV",
+                       "rangeMap": "NOT_SUPPORTED",
+                       "vehicleFinder": "ACTIVATED",
+                       "a4a": "BLUETOOTH",
+                       "hornBlow": "ACTIVATED",
+                       "chargeNow": "NOT_SUPPORTED",
+                       "countryCode": "V2-CA",
+                       "bodytype": "F15",
+                       "lightFlash": "ACTIVATED",
+                       "vin": "F15_VIN",
+                       "model": "X5 xDrive35i",
+                       "ipa": "NOT_SUPPORTED",
+                       "brand": "BMW",
+                       "climateControl": "DEPARTURE_TIMER",
+                       "sendPoi": "ACTIVATED",
+                       "lastDestinations": "NOT_SUPPORTED",
+                       "doorLock": "ACTIVATED",
+                       "chargingControl": "NOT_SUPPORTED",
+                       "steering": "LH",
+                       "intermodalRouting": "NOT_AVAILABLE",
+                       "lscType": "LSC_BASIS",
+                       "hasAlarmSystem": true,
+                       "statisticsCommunityEnabled": false,
+                       "climateNow": "ACTIVATED",
+                       "fuelType": "PETROL",
+                       "hub": "HUB_US",
+                       "remote360": "NOT_SUPPORTED",
+                       "hmiVersion": "ID5",
+                       "dealer": {
+                               "country": "CA",
+                               "city": "Ottawa",
+                               "phone": "1-866-599-4999",
+                               "street": "85 Wellington St",
+                               "postalCode": "K1A 1A1",
+                               "name": "Parliament of Canada"
+                       },
+                       "climateFunction": "VENTILATION",
+                       "colorCode": "475",
+                       "carCloud": "NOT_SUPPORTED",
+                       "statisticsAvailable": false
+               },
+               {
+                       "vehicleFinderRestriction": "NONE",
+                       "doorUnlock": "ACTIVATED",
+                       "color": "SOLARORANGE MET. M. AKZENT FROZEN G",
+                       "onlineSearchMode": "MAP",
+                       "breakdownNumber": "+4989358957103",
+                       "smartSolution": "NOT_SUPPORTED",
+                       "yearOfConstruction": 2014,
+                       "driveTrain": "BEV",
+                       "rangeMap": "RANGE_CIRCLE",
+                       "vehicleFinder": "ACTIVATED",
+                       "a4a": "USB_ONLY",
+                       "hornBlow": "ACTIVATED",
+                       "chargeNow": "NOT_SUPPORTED",
+                       "licensePlate": "HIDDEN",
+                       "countryCode": "B3-ZA",
+                       "bodytype": "I01",
+                       "lightFlash": "ACTIVATED",
+                       "vin": "I01_NOREX_VIN",
+                       "model": "i3",
+                       "ipa": "NOT_SUPPORTED",
+                       "brand": "BMW_I",
+                       "climateControl": "NOT_SUPPORTED",
+                       "sendPoi": "ACTIVATED",
+                       "lastDestinations": "SUPPORTED",
+                       "doorLock": "ACTIVATED",
+                       "chargingControl": "WEEKLY_PLANNER",
+                       "onlineSearchProvider": "GOOGLE",
+                       "steering": "RH",
+                       "intermodalRouting": "NOT_AVAILABLE",
+                       "supportedChargingModes": [
+                               "AC_LOW",
+                               "AC_HIGH"
+                       ],
+                       "lscType": "I_LSC_IMM",
+                       "hasAlarmSystem": true,
+                       "statisticsCommunityEnabled": true,
+                       "climateNow": "ACTIVATED",
+                       "hub": "HUB_ECE",
+                       "remote360": "NOT_SUPPORTED",
+                       "hmiVersion": "ID4",
+                       "dealer": {
+                               "country": "ZA",
+                               "city": "Midrand",
+                               "phone": "+27 12 522 3000",
+                               "street": "1 Bavaria Avenue",
+                               "postalCode": "1685",
+                               "name": "BMW (South Africa) (Pty) Ltd. ZA"
+                       },
+                       "climateFunction": "AIRCONDITIONING",
+                       "colorCode": "B78",
+                       "carCloud": "NOT_SUPPORTED",
+                       "statisticsAvailable": true
+               },
+               {
+                       "a4a": "USB_ONLY",
+                       "bodytype": "F45",
+                       "brand": "BMW",
+                       "breakdownNumber": "+4989358957103",
+                       "carCloud": "NOT_SUPPORTED",
+                       "chargeNow": "NOT_SUPPORTED",
+                       "chargingControl": "NOT_SUPPORTED",
+                       "climateControl": "START_TIMER",
+                       "climateFunction": "VENTILATION",
+                       "climateNow": "ACTIVATED",
+                       "color": "MEDITERRANBLAU METALLIC",
+                       "colorCode": "C10",
+                       "countryCode": "V1-ES",
+                       "dealer": {
+                               "city": "Madrid",
+                               "country": "ES",
+                               "name": "BMW Iberica S.A.",
+                               "phone": "+34 913350505",
+                               "postalCode": "28050",
+                               "street": "Avenida de Burgos ,118"
+                       },
+                       "doorLock": "ACTIVATED",
+                       "doorUnlock": "ACTIVATED",
+                       "driveTrain": "CONV",
+                       "fuelType": "PETROL",
+                       "hasAlarmSystem": false,
+                       "hmiVersion": "ID4",
+                       "hornBlow": "ACTIVATED",
+                       "hub": "HUB_ECE",
+                       "intermodalRouting": "NOT_AVAILABLE",
+                       "ipa": "NOT_SUPPORTED",
+                       "lastDestinations": "NOT_SUPPORTED",
+                       "licensePlate": "some_license_plate",
+                       "lightFlash": "ACTIVATED",
+                       "lscType": "NOT_SUPPORTED",
+                       "model": "225i",
+                       "onlineSearchMode": "MAP",
+                       "onlineSearchProvider": "GOOGLE",
+                       "rangeMap": "NOT_SUPPORTED",
+                       "remote360": "NOT_SUPPORTED",
+                       "sendPoi": "ACTIVATED",
+                       "smartSolution": "NOT_SUPPORTED",
+                       "statisticsAvailable": false,
+                       "statisticsCommunityEnabled": false,
+                       "steering": "LH",
+                       "vehicleFinder": "ACTIVATED",
+                       "vehicleFinderRestriction": "NONE",
+                       "vin": "F45_VIN",
+                       "yearOfConstruction": 2016
+               },
+               {
+                       "a4a": "USB_ONLY",
+                       "bodytype": "F31",
+                       "brand": "BMW",
+                       "breakdownNumber": "+4989358957103",
+                       "carCloud": "NOT_SUPPORTED",
+                       "chargeNow": "NOT_SUPPORTED",
+                       "chargingControl": "NOT_SUPPORTED",
+                       "climateControl": "START_TIMER",
+                       "climateControlRES": "NOT_SUPPORTED",
+                       "climateFunction": "VENTILATION",
+                       "climateNow": "ACTIVATED",
+                       "climateNowRES": "NOT_SUPPORTED",
+                       "color": "PLATINSILBER METALLIC",
+                       "colorCode": "C08",
+                       "countryCode": "V1-UK",
+                       "dealer": {
+                               "city": "Farnborough",
+                               "country": "GB",
+                               "name": "BMW (UK) Ltd. ICS - DIRECT SUPPLY",
+                               "phone": "+44 1252 920000",
+                               "postalCode": "GU14 0FB",
+                               "street": "Summit ONE"
+                       },
+                       "doorLock": "ACTIVATED",
+                       "doorUnlock": "ACTIVATED",
+                       "driveTrain": "CONV",
+                       "fuelType": "DIESEL",
+                       "hasAlarmSystem": true,
+                       "hmiVersion": "ID4",
+                       "hornBlow": "NOT_SUPPORTED",
+                       "hub": "HUB_ECE",
+                       "intermodalRouting": "NOT_AVAILABLE",
+                       "ipa": "NOT_SUPPORTED",
+                       "lastDestinations": "NOT_SUPPORTED",
+                       "licensePlate": "some_license_plate",
+                       "lightFlash": "ACTIVATED",
+                       "lscType": "NOT_SUPPORTED",
+                       "model": "320d",
+                       "onlineSearchMode": "MAP",
+                       "onlineSearchProvider": "GOOGLE",
+                       "rangeMap": "NOT_SUPPORTED",
+                       "remote360": "NOT_SUPPORTED",
+                       "remoteSoftwareUpgrade": "NOT_SUPPORTED",
+                       "sendPoi": "ACTIVATED",
+                       "smartSolution": "NOT_SUPPORTED",
+                       "statisticsAvailable": false,
+                       "statisticsCommunityEnabled": false,
+                       "steering": "RH",
+                       "vehicleFinder": "ACTIVATED",
+                       "vehicleFinderRestriction": "NONE",
+                       "vin": "F31_VIN",
+                       "yearOfConstruction": 2015
+               },
+               {
+                       "a4a": "NOT_SUPPORTED",
+                       "bodytype": "F35",
+                       "brand": "BMW",
+                       "breakdownNumber": "+4989358957103",
+                       "carCloud": "NOT_SUPPORTED",
+                       "chargeNow": "NOT_SUPPORTED",
+                       "chargingControl": "NOT_SUPPORTED",
+                       "climateControl": "NOT_SUPPORTED",
+                       "climateControlRES": "NOT_SUPPORTED",
+                       "climateFunction": "VENTILATION",
+                       "climateNow": "NOT_SUPPORTED",
+                       "climateNowRES": "NOT_SUPPORTED",
+                       "color": "MINERALWEISS METALLIC",
+                       "colorCode": "A96",
+                       "countryCode": "V5-CN",
+                       "dealer": {
+                               "city": "Beijing",
+                               "country": "CN",
+                               "name": "Beijing Baozen Baiwang Automotive Sales Co., Ltd.",
+                               "phone": "+86 10 62826789",
+                               "postalCode": "100094",
+                               "street": "F2 Baiwang Green Valley"
+                       },
+                       "doorLock": "NOT_SUPPORTED",
+                       "doorUnlock": "NOT_SUPPORTED",
+                       "driveTrain": "CONV",
+                       "fuelType": "PETROL",
+                       "hasAlarmSystem": false,
+                       "hmiVersion": "ID5",
+                       "hornBlow": "NOT_SUPPORTED",
+                       "hub": "HUB_CN",
+                       "intermodalRouting": "NOT_AVAILABLE",
+                       "ipa": "NOT_SUPPORTED",
+                       "lastDestinations": "NOT_SUPPORTED",
+                       "licensePlate": "some_license_plate",
+                       "lightFlash": "NOT_SUPPORTED",
+                       "lscType": "NOT_SUPPORTED",
+                       "model": "328Li",
+                       "onlineSearchMode": "MAP",
+                       "rangeMap": "NOT_SUPPORTED",
+                       "remote360": "NOT_SUPPORTED",
+                       "remoteSoftwareUpgrade": "NOT_SUPPORTED",
+                       "sendPoi": "NOT_SUPPORTED",
+                       "smartSolution": "NOT_SUPPORTED",
+                       "statisticsAvailable": false,
+                       "statisticsCommunityEnabled": false,
+                       "steering": "LH",
+                       "vehicleFinder": "NOT_SUPPORTED",
+                       "vin": "F31_VIN",
+                       "yearOfConstruction": 2015
+               },
+               {
+                       "vin": "ANONYMOUS",
+                       "model": "318i",
+                       "driveTrain": "CONV",
+                       "brand": "BMW",
+                       "yearOfConstruction": 2019,
+                       "bodytype": "F31",
+                       "color": "MINERALGRAU METALLIC",
+                       "statisticsCommunityEnabled": false,
+                       "statisticsAvailable": false,
+                       "hasAlarmSystem": true,
+                       "dealer": {
+                               "name": "ANONYMOUS",
+                               "street": "ANONYMOUS",
+                               "postalCode": "ANONYMOUS",
+                               "city": "ANONYMOUS",
+                               "country": "ANONYMOUS",
+                               "phone": "ANONYMOUS"
+                       },
+                       "breakdownNumber": "ANONYMOUS",
+                       "chargingControl": "NOT_SUPPORTED",
+                       "vehicleFinder": "ACTIVATED",
+                       "hornBlow": "ACTIVATED",
+                       "lightFlash": "ACTIVATED",
+                       "doorLock": "ACTIVATED",
+                       "doorUnlock": "ACTIVATED",
+                       "climateNow": "ACTIVATED",
+                       "sendPoi": "ACTIVATED",
+                       "remote360": "NOT_SUPPORTED",
+                       "climateControl": "START_TIMER",
+                       "chargeNow": "NOT_SUPPORTED",
+                       "lastDestinations": "NOT_SUPPORTED",
+                       "carCloud": "ACTIVATED",
+                       "remoteSoftwareUpgrade": "NOT_SUPPORTED",
+                       "climateNowRES": "NOT_SUPPORTED",
+                       "climateControlRES": "NOT_SUPPORTED",
+                       "smartSolution": "NOT_SUPPORTED",
+                       "ipa": "NOT_SUPPORTED"
+               }
+       ]
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/all-trips.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/all-trips.json
new file mode 100644 (file)
index 0000000..3fe434e
--- /dev/null
@@ -0,0 +1,40 @@
+{
+       "allTrips": {
+               "avgElectricConsumption": {
+                       "communityLow": 11.05,
+                       "communityAverage": 16.28,
+                       "communityHigh": 21.99,
+                       "userAverage": 16.46
+               },
+               "avgRecuperation": {
+                       "communityLow": 0.47,
+                       "communityAverage": 3.37,
+                       "communityHigh": 11.51,
+                       "userAverage": 4.53
+               },
+               "chargecycleRange": {
+                       "communityAverage": 194.21,
+                       "communityHigh": 270,
+                       "userAverage": 57.3,
+                       "userHigh": 185.48,
+                       "userCurrentChargeCycle": 68
+               },
+               "totalElectricDistance": {
+                       "communityLow": 19,
+                       "communityAverage": 40850.56,
+                       "communityHigh": 193006,
+                       "userTotal": 16629.4
+               },
+               "avgCombinedConsumption": {
+                       "communityLow": 0,
+                       "communityAverage": 0.92,
+                       "communityHigh": 4.44,
+                       "userAverage": 0.64
+               },
+               "savedCO2": 461.083,
+               "savedCO2greenEnergy": 2712.255,
+               "totalSavedFuel": 0,
+               "resetDate": "2020-08-24T14:40:40+0000",
+               "batterySizeMax": 33200
+       }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/charging-profile.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/charging-profile.json
new file mode 100644 (file)
index 0000000..840559b
--- /dev/null
@@ -0,0 +1,41 @@
+{
+       "weeklyPlanner": {
+               "climatizationEnabled": true,
+               "chargingMode": "IMMEDIATE_CHARGING",
+               "chargingPreferences": "CHARGING_WINDOW",
+               "timer1": {
+                       "departureTime": "05:00",
+                       "timerEnabled": false,
+                       "weekdays": [
+                               "MONDAY",
+                               "TUESDAY",
+                               "WEDNESDAY",
+                               "THURSDAY",
+                               "FRIDAY"
+                       ]
+               },
+               "timer2": {
+                       "departureTime": "12:00",
+                       "timerEnabled": true,
+                       "weekdays": [
+                               "SATURDAY"
+                       ]
+               },
+               "timer3": {
+                       "departureTime": "00:00",
+                       "timerEnabled": false,
+                       "weekdays": []
+               },
+               "overrideTimer": {
+                       "departureTime": "12:00",
+                       "timerEnabled": false,
+                       "weekdays": [
+                               "SATURDAY"
+                       ]
+               },
+               "preferredChargingWindow": {
+                       "startTime": "11:00",
+                       "endTime": "17:00"
+               }
+       }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/connected-drive-account-info.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/connected-drive-account-info.json
new file mode 100644 (file)
index 0000000..73b5946
--- /dev/null
@@ -0,0 +1,63 @@
+{
+       "vehicles": [
+               {
+                       "vin": "MY_REAL_VIN",
+                       "model": "i3 94 (+ REX)",
+                       "bodytype": "I01",
+                       "driveTrain": "BEV_REX",
+                       "color": "CAPPARISWEISS MIT AKZENT BMW I BLAU",
+                       "colorCode": "B85",
+                       "brand": "BMW_I",
+                       "yearOfConstruction": 2017,
+                       "statisticsCommunityEnabled": false,
+                       "statisticsAvailable": true,
+                       "hub": "HUB_ECE",
+                       "hasAlarmSystem": true,
+                       "dealer": {
+                               "name": "My Real Dealer Name",
+                               "street": "My Real Dealer Address",
+                               "postalCode": "4711",
+                               "city": "My Real Dealer City",
+                               "country": "DE",
+                               "phone": "My Real Dealer Phone"
+                       },
+                       "breakdownNumber": "Real Phone Number",
+                       "countryCode": "V6",
+                       "egoVehiclePath": "",
+                       "chargingUpdateMode": "NORMAL_PROGNOSE_BASED",
+                       "steering": "LH",
+                       "vehicleFinderRestriction": "NONE",
+                       "hmiVersion": "ID4",
+                       "a4a": "USB_ONLY",
+                       "vehicleFinder": "ACTIVATED",
+                       "remote360": "NOT_SUPPORTED",
+                       "hornBlow": "ACTIVATED",
+                       "lightFlash": "ACTIVATED",
+                       "doorLock": "ACTIVATED",
+                       "doorUnlock": "ACTIVATED",
+                       "climateControl": "NOT_SUPPORTED",
+                       "climateNow": "ACTIVATED",
+                       "climateNowRES": "NOT_SUPPORTED",
+                       "climateControlRES": "NOT_SUPPORTED",
+                       "chargingControl": "WEEKLY_PLANNER",
+                       "chargeNow": "NOT_SUPPORTED",
+                       "sendPoi": "ACTIVATED",
+                       "rangeMap": "RANGE_CIRCLE",
+                       "lastDestinations": "SUPPORTED",
+                       "intermodalRouting": "NOT_AVAILABLE",
+                       "climateFunction": "AIRCONDITIONING",
+                       "onlineSearchMode": "MAP",
+                       "onlineSearchProvider": "GOOGLE",
+                       "smartSolution": "NOT_SUPPORTED",
+                       "carCloud": "NOT_SUPPORTED",
+                       "supportedChargingModes": [
+                               "AC_LOW",
+                               "DC"
+                       ],
+                       "lscType": "I_LSC_IMM",
+                       "ipa": "NOT_SUPPORTED",
+                       "puStep": "1119",
+                       "remoteSoftwareUpgrade": "NOT_SUPPORTED"
+               }
+       ]
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/destinations.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/destinations.json
new file mode 100644 (file)
index 0000000..2c63438
--- /dev/null
@@ -0,0 +1,94 @@
+{
+       "destinations": [
+               {
+                       "lat": 47.11,
+                       "lon": 47.11,
+                       "country": "DEUTSCHLAND",
+                       "city": "My Real City",
+                       "street": "My Real Street",
+                       "streetNumber": "My Real Number",
+                       "type": "DESTINATION",
+                       "createdAt": "2020-08-16T12:52:58+0000"
+               },
+               {
+            "lat": 47.11,
+            "lon": 47.11,
+            "country": "DEUTSCHLAND",
+            "city": "My Real City",
+            "street": "My Real Street",
+            "streetNumber": "My Real Number",
+                       "type": "DESTINATION",
+                       "createdAt": "2020-08-12T17:03:35+0000"
+               },
+               {
+            "lat": 47.11,
+            "lon": 47.11,
+            "country": "DEUTSCHLAND",
+            "city": "My Real City",
+            "street": "My Real Street",
+            "streetNumber": "My Real Number",
+                       "type": "DESTINATION",
+                       "createdAt": "2020-08-03T08:15:20+0000"
+               },
+               {
+            "lat": 47.11,
+            "lon": 47.11,
+            "country": "DEUTSCHLAND",
+            "city": "My Real City",
+            "street": "My Real Street",
+            "streetNumber": "My Real Number",
+                       "type": "DESTINATION",
+                       "createdAt": "2020-07-31T13:09:15+0000"
+               },
+               {
+            "lat": 47.11,
+            "lon": 47.11,
+            "country": "DEUTSCHLAND",
+            "city": "My Real City",
+            "street": "My Real Street",
+            "streetNumber": "My Real Number",
+                       "type": "DESTINATION",
+                       "createdAt": "2020-07-25T11:20:18+0000"
+               },
+               {
+            "lat": 47.11,
+            "lon": 47.11,
+            "country": "DEUTSCHLAND",
+            "city": "My Real City",
+            "street": "My Real Street",
+            "streetNumber": "My Real Number",
+                       "type": "DESTINATION",
+                       "createdAt": "2020-07-18T11:22:37+0000"
+               },
+               {
+            "lat": 47.11,
+            "lon": 47.11,
+            "country": "DEUTSCHLAND",
+            "city": "My Real City",
+            "street": "My Real Street",
+            "streetNumber": "My Real Number",
+                       "type": "DESTINATION",
+                       "createdAt": "2020-02-08T11:06:52+0000"
+               },
+               {
+            "lat": 47.11,
+            "lon": 47.11,
+            "country": "DEUTSCHLAND",
+            "city": "My Real City",
+            "street": "My Real Street",
+            "streetNumber": "My Real Number",
+                       "type": "DESTINATION",
+                       "createdAt": "2020-02-02T14:07:54+0000"
+               },
+               {
+            "lat": 47.11,
+            "lon": 47.11,
+            "country": "DEUTSCHLAND",
+            "city": "My Real City",
+            "street": "My Real Street",
+            "streetNumber": "My Real Number",
+                       "type": "DESTINATION",
+                       "createdAt": "2020-02-02T13:24:36+0000"
+               }
+       ]
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/last-trip.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/last-trip.json
new file mode 100644 (file)
index 0000000..c129bf3
--- /dev/null
@@ -0,0 +1,19 @@
+{
+       "lastTrip": {
+               "efficiencyValue": 0.98,
+               "totalDistance": 2,
+               "electricDistance": 2,
+               "avgElectricConsumption": 7,
+               "avgRecuperation": 6,
+               "drivingModeValue": 0.87,
+               "totalConsumptionValue": 1.25,
+               "avgCombinedConsumption": 0,
+               "electricDistanceRatio": 100,
+               "savedFuel": 0,
+               "date": "2020-08-24T17:55:00+0000",
+               "duration": 5,
+        "auxiliaryConsumptionValue": 0.78,
+        "anticipationValue": 0.99,
+        "accelerationValue": 0.99
+       }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/remote-services/delivered.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/remote-services/delivered.json
new file mode 100644 (file)
index 0000000..cfc5300
--- /dev/null
@@ -0,0 +1,10 @@
+{
+       "executionStatus": {
+               "serviceType": "DOOR_UNLOCK",
+               "status": "DELIVERED",
+               "eventId": "5639303536333926DA7B9400@bmw.de",
+               "extendedStatus": {
+                       "result": "STATUS_UNKNOWN"
+               }
+       }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/remote-services/executed.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/remote-services/executed.json
new file mode 100644 (file)
index 0000000..0cd2101
--- /dev/null
@@ -0,0 +1,12 @@
+{
+       "executionStatus": {
+               "serviceType": "DOOR_UNLOCK",
+               "status": "EXECUTED",
+               "eventId": "5639303536333926DA7B9400@bmw.de",
+               "extendedStatus": {
+                       "newDoorStatus": "INVALID",
+                       "oldDoorStatus": "INVALID",
+                       "result": "STATUS_NOT_CHANGED"
+               }
+       }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/remote-services/pending.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/remote-services/pending.json
new file mode 100644 (file)
index 0000000..1a46f71
--- /dev/null
@@ -0,0 +1,10 @@
+{
+       "executionStatus": {
+               "serviceType": "DOOR_UNLOCK",
+               "status": "PENDING",
+               "eventId": "5639303536333926DA7B9400@bmw.de",
+               "extendedStatus": {
+                       "result": "STATUS_UNKNOWN"
+               }
+       }
+}
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/vehicle-status-ccm-tyre.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/vehicle-status-ccm-tyre.json
new file mode 100644 (file)
index 0000000..b8aed73
--- /dev/null
@@ -0,0 +1,87 @@
+{
+       "vehicleStatus": {
+               "mileage": 18390,
+               "remainingFuel": 4.0,
+               "remainingRangeElectric": 67.0,
+               "remainingRangeElectricMls": 41.0,
+               "remainingRangeFuel": 59.0,
+               "remainingRangeFuelMls": 36.0,
+               "maxRangeElectric": 211.0,
+               "maxRangeElectricMls": 131.0,
+               "maxFuel": 8.5,
+               "chargingLevelHv": 35.0,
+               "vin": "ANONYMOUS",
+               "updateReason": "VEHICLE_SHUTDOWN_SECURED",
+               "updateTime": "2020-09-29T12:52:12+0000",
+               "doorDriverFront": "CLOSED",
+               "doorDriverRear": "CLOSED",
+               "doorPassengerFront": "CLOSED",
+               "doorPassengerRear": "CLOSED",
+               "windowDriverFront": "CLOSED",
+               "windowDriverRear": "CLOSED",
+               "windowPassengerFront": "CLOSED",
+               "windowPassengerRear": "CLOSED",
+               "sunroof": "CLOSED",
+               "trunk": "CLOSED",
+               "rearWindow": "INVALID",
+               "hood": "CLOSED",
+               "doorLockState": "SECURED",
+               "parkingLight": "OFF",
+               "positionLight": "ON",
+               "connectionStatus": "DISCONNECTED",
+               "chargingStatus": "INVALID",
+               "lastChargingEndReason": "END_REQUESTED_BY_DRIVER",
+               "lastChargingEndResult": "SUCCESS",
+               "position": {
+                       "lat": -1.0,
+                       "lon": -1.0,
+                       "heading": -1,
+                       "status": "OK"
+               },
+               "internalDataTimeUTC": "2020-09-29T12:52:12",
+               "singleImmediateCharging": false,
+               "chargingConnectionType": "CONDUCTIVE",
+               "chargingInductivePositioning": "NOT_POSITIONED",
+               "vehicleCountry": "DE",
+               "DCS_CCH_Activation": "NA",
+               "DCS_CCH_Ongoing": false,
+               "checkControlMessages": [
+                       {
+                               "ccmDescriptionLong": "Check tyre pressure and correct if necessary. Afterwards, perform Tyre Pressure Monitor (RDC) reset. See Owner\u0027s Handbook for more information.",
+                               "ccmDescriptionShort": "Check tyre pressure. Initialise RDC",
+                               "ccmId": 142,
+                               "ccmMileage": 18384
+                       }
+               ],
+               "cbsData": [
+                       {
+                               "cbsType": "BRAKE_FLUID",
+                               "cbsState": "OK",
+                               "cbsDueDate": "2021-11",
+                               "cbsDescription": "Next change due at the latest by the stated date.",
+                               "cbsRemainingMileage": -1
+                       },
+                       {
+                               "cbsType": "VEHICLE_CHECK",
+                               "cbsState": "OK",
+                               "cbsDueDate": "2021-11",
+                               "cbsDescription": "Next visual inspection due when the stated distance has been covered or by the stated date.",
+                               "cbsRemainingMileage": -1
+                       },
+                       {
+                               "cbsType": "OIL",
+                               "cbsState": "OK",
+                               "cbsDueDate": "2021-11",
+                               "cbsDescription": "Next change due when the stated distance has been covered or by the specified date.",
+                               "cbsRemainingMileage": -1
+                       },
+                       {
+                               "cbsType": "VEHICLE_TUV",
+                               "cbsState": "OK",
+                               "cbsDueDate": "2021-11",
+                               "cbsDescription": "Next statutory vehicle inspection due by the stated date.",
+                               "cbsRemainingMileage": -1
+                       }
+               ]
+       }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/vehicle-status-charging.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/vehicle-status-charging.json
new file mode 100644 (file)
index 0000000..1dd32fa
--- /dev/null
@@ -0,0 +1,77 @@
+{
+       "vehicleStatus": {
+               "vin": "ANONYMOUS",
+               "mileage": 17307,
+               "updateReason": "CHARGING_STARTED",
+               "updateTime": "2020-08-25T15:37:16+0000",
+               "doorDriverFront": "CLOSED",
+               "doorDriverRear": "CLOSED",
+               "doorPassengerFront": "CLOSED",
+               "doorPassengerRear": "CLOSED",
+               "windowDriverFront": "CLOSED",
+               "windowDriverRear": "CLOSED",
+               "windowPassengerFront": "CLOSED",
+               "windowPassengerRear": "CLOSED",
+               "sunroof": "CLOSED",
+               "trunk": "CLOSED",
+               "rearWindow": "INVALID",
+               "hood": "CLOSED",
+               "doorLockState": "SECURED",
+               "parkingLight": "OFF",
+               "positionLight": "ON",
+               "remainingFuel": 4,
+               "remainingRangeElectric": 182,
+               "remainingRangeElectricMls": 113,
+               "remainingRangeFuel": 69,
+               "remainingRangeFuelMls": 42,
+               "maxRangeElectric": 213,
+               "maxRangeElectricMls": 132,
+               "maxFuel": 8.5,
+               "connectionStatus": "CONNECTED",
+               "chargingStatus": "CHARGING",
+               "chargingTimeRemaining": 76,
+               "chargingLevelHv": 86,
+               "lastChargingEndReason": "UNKNOWN",
+               "lastChargingEndResult": "UNKNOWN",
+               "position": {
+                       "lat": 56.789,
+                       "lon": 8.765,
+                       "heading": 41,
+                       "status": "OK"
+               },
+               "internalDataTimeUTC": "2020-08-25T12:57:59",
+               "singleImmediateCharging": false,
+               "chargingConnectionType": "CONDUCTIVE",
+               "chargingInductivePositioning": "NOT_POSITIONED",
+               "vehicleCountry": "DE",
+               "checkControlMessages": [],
+               "cbsData": [
+                       {
+                               "cbsType": "BRAKE_FLUID",
+                               "cbsState": "OK",
+                               "cbsDueDate": "2021-11",
+                               "cbsDescription": "Next change due at the latest by the stated date."
+                       },
+                       {
+                               "cbsType": "VEHICLE_CHECK",
+                               "cbsState": "OK",
+                               "cbsDueDate": "2021-11",
+                               "cbsDescription": "Next visual inspection due when the stated distance has been covered or by the stated date."
+                       },
+                       {
+                               "cbsType": "OIL",
+                               "cbsState": "OK",
+                               "cbsDueDate": "2021-11",
+                               "cbsDescription": "Next change due when the stated distance has been covered or by the specified date."
+                       },
+                       {
+                               "cbsType": "VEHICLE_TUV",
+                               "cbsState": "OK",
+                               "cbsDueDate": "2021-11",
+                               "cbsDescription": "Next statutory vehicle inspection due by the stated date."
+                       }
+               ],
+               "DCS_CCH_Activation": "NA",
+               "DCS_CCH_Ongoing": false
+       }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/vehicle-status.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/webapi/vehicle-status.json
new file mode 100644 (file)
index 0000000..ec4235a
--- /dev/null
@@ -0,0 +1,77 @@
+{
+       "vehicleStatus": {
+               "vin": "ANONYMOUS",
+               "mileage": 17273,
+               "updateReason": "VEHICLE_SHUTDOWN_SECURED",
+               "updateTime": "2020-08-24T15:55:32+0000",
+               "doorDriverFront": "CLOSED",
+               "doorDriverRear": "CLOSED",
+               "doorPassengerFront": "CLOSED",
+               "doorPassengerRear": "CLOSED",
+               "windowDriverFront": "CLOSED",
+               "windowDriverRear": "CLOSED",
+               "windowPassengerFront": "CLOSED",
+               "windowPassengerRear": "CLOSED",
+               "sunroof": "CLOSED",
+               "trunk": "CLOSED",
+               "rearWindow": "INVALID",
+               "hood": "CLOSED",
+               "doorLockState": "SECURED",
+               "parkingLight": "OFF",
+               "positionLight": "ON",
+               "remainingFuel": 4,
+               "remainingRangeElectric": 148,
+               "remainingRangeElectricMls": 91,
+               "remainingRangeFuel": 70,
+               "remainingRangeFuelMls": 43,
+               "maxRangeElectric": 216,
+               "maxRangeElectricMls": 134,
+               "maxFuel": 8.5,
+               "connectionStatus": "DISCONNECTED",
+               "chargingStatus": "INVALID",
+               "chargingLevelHv": 71,
+               "lastChargingEndReason": "CHARGING_GOAL_REACHED",
+               "lastChargingEndResult": "SUCCESS",
+               "position": {
+                       "lat": 56.789,
+                       "lon": 8.765,
+                       "heading": 219,
+                       "status": "OK"
+               },
+               "internalDataTimeUTC": "2020-08-24T15:55:32",
+               "singleImmediateCharging": false,
+               "chargingConnectionType": "CONDUCTIVE",
+               "chargingInductivePositioning": "NOT_POSITIONED",
+               "vehicleCountry": "DE",
+               "checkControlMessages": [],
+               "cbsData": [
+                       {
+                               "cbsType": "BRAKE_FLUID",
+                               "cbsState": "OK",
+                               "cbsDueDate": "2021-11",
+                               "cbsDescription": "Next change due at the latest by the stated date.",
+                               "cbsRemainingMileage": 15345
+                       },
+                       {
+                               "cbsType": "VEHICLE_CHECK",
+                               "cbsState": "OK",
+                               "cbsDueDate": "2021-11",
+                               "cbsDescription": "Next visual inspection due when the stated distance has been covered or by the stated date."
+                       },
+                       {
+                               "cbsType": "OIL",
+                               "cbsState": "OK",
+                               "cbsDueDate": "2021-11",
+                               "cbsDescription": "Next change due when the stated distance has been covered or by the specified date."
+                       },
+                       {
+                               "cbsType": "VEHICLE_TUV",
+                               "cbsState": "OK",
+                               "cbsDueDate": "2021-11",
+                               "cbsDescription": "Next statutory vehicle inspection due by the stated date."
+                       }
+               ],
+               "DCS_CCH_Activation": "NA",
+               "DCS_CCH_Ongoing": false
+       }
+}
\ No newline at end of file
index 091bbaf5facbddb9dcb9c1cd197aa2514e750221..f1bf230403ac0f96c8281eb6a5563b4c825f32a1 100644 (file)
@@ -69,6 +69,7 @@
     <module>org.openhab.binding.bluetooth.govee</module>
     <module>org.openhab.binding.bluetooth.roaming</module>
     <module>org.openhab.binding.bluetooth.ruuvitag</module>
+    <module>org.openhab.binding.bmwconnecteddrive</module>
     <module>org.openhab.binding.boschindego</module>
     <module>org.openhab.binding.boschshc</module>
     <module>org.openhab.binding.bosesoundtouch</module>